# Copyright (c) 2011 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """A Python library to interact with INA219 module for TPM testing. Background - INA219 is one of two modules on TTCI board - This library provides methods to interact with INA219 programmatically Dependency - This library depends on a new C shared library called "libsmogcheck.so". - In order to run test cases built using this API, one needs a TTCI board Notes: - An exception is raised if it doesn't make logical sense to continue program flow (e.g. I/O error prevents test case from executing) - An exception is caught and then converted to an error code if the caller expects to check for error code per API definition """ import logging, re from autotest_lib.client.common_lib import i2c_slave # INA219 registers INA_REG = { 'CONF': 0, # Configuration Register 'SHUNT_VOLT': 1, # Shunt Voltage 'BUS_VOLT': 2, # Bus Voltage 'POWER': 3, # Power 'CURRENT': 4, # Current 'CALIB': 5, # Calibration } # Regex pattern for measurement value HEX_STR_PATTERN = re.compile('^0x([0-9a-f]{2})([0-9a-f]{2})$') # Constants used to initialize INA219 registers # TODO(tgao): add docstring for these values after stevenh replies INA_CONF_INIT_VAL = 0x9f31 INA_CALIB_INIT_VAL = 0xc90e # Default values used to calculate/interpret voltage and current measurements. DEFAULT_MEAS_RANGE_VALUE = { 'current': {'max': 0.1, 'min': 0.0, 'denom': 10000.0, 'reg': INA_REG['CURRENT']}, 'voltage': {'max': 3.35, 'min': 3.25, 'denom': 2000.0, 'reg': INA_REG['BUS_VOLT']}, } class InaError(Exception): """Base class for all errors in this module.""" class InaController(i2c_slave.I2cSlave): """Object to control INA219 module on TTCI board.""" def __init__(self, slave_addr=None, range_dict=None): """Constructor. Mandatory params: slave_addr: slave address to set. Default: None. Optional param: range_dict: desired max/min thresholds for measurement values. Default: DEFAULT_MEAS_RANGE_VALUE. Args: slave_addr: an integer, address of main or backup power. range_dict: desired max/min thresholds for measurement values. Raises: InaError: if error initializing INA219 module or invalid range_dict. """ super(InaController, self).__init__() if slave_addr is None: raise InaError('Error slave_addr expected') try: if range_dict is None: range_dict = DEFAULT_MEAS_RANGE_VALUE else: self._validateRangeDict(DEFAULT_MEAS_RANGE_VALUE, range_dict) self.range_dict = range_dict self.setSlaveAddress(slave_addr) self.writeWord(INA_REG['CONF'], INA_CONF_INIT_VAL) self.writeWord(INA_REG['CALIB'], INA_CALIB_INIT_VAL) except InaError, e: raise InaError('Error initializing INA219: %s' % e) def _validateRangeDict(self, d_ref, d_in): """Validates keys and types of value in range_dict. Iterate over d_ref to make sure all keys exist in d_in and values are of the correct type. Args: d_ref: a dictionary, used as reference. d_in: a dictionary, to be validated against reference. Raises: InaError: if range_dict is invalid. """ for k, v in d_ref.iteritems(): if k not in d_in: raise InaError('Key %s not present in dict %r' % (k, d_in)) if type(v) != type(d_in[k]): raise InaError( 'Value type mismatch for key %s. Expected: %s; actual = %s' % (k, type(v), type(d_in[k]))) if type(v) is dict: self._validateRangeDict(v, d_in[k]) def readMeasure(self, measure): """Reads requested measurement. Args: measure: a string, 'current' or 'voltage'. Returns: a float, measurement in native units. Or None if error. Raises: InaError: if error reading requested measurement. """ try: hex_str = '0x%.4x' % self.readWord(self.range_dict[measure]['reg']) logging.debug('Word read = %r', hex_str) return self._checkMeasureRange(hex_str, measure) except InaError, e: logging.error('Error reading %s: %s', measure, e) def getPowerMetrics(self): """Get measurement metrics for Main Power. Returns: an integer, 0 for success and -1 for error. a float, voltage value in Volts. Or None if error. a float, current value in Amps. Or None if error. """ logging.info('Attempt to get power metrics') try: return (0, self.readMeasure('voltage'), self.readMeasure('current')) except InaError, e: logging.error('getPowerMetrics(): %s', e) return (-1, None, None) def _checkMeasureRange(self, hex_str, measure): """Checks if measurement value falls within a pre-specified range. Args: hex_str: a string (hex value). measure: a string, 'current' or 'voltage'. Returns: measure_float: a float, measurement value. Raises: InaError: if value doesn't fall in range. """ measure_float = self._convertHexToFloat( hex_str, self.range_dict[measure]['denom']) measure_msg = '%s value %.2f' % (measure, measure_float) range_msg = '[%(min).2f, %(max).2f]' % self.range_dict[measure] if (measure_float < self.range_dict[measure]['min'] or measure_float > self.range_dict[measure]['max']): raise InaError('%s is out of range %s' % measure_msg, range_msg) logging.info('%s is in range %s', measure_msg, range_msg) return measure_float def _convertHexToFloat(self, hex_str, denom): """Performs measurement calculation. The measurement reading from INA219 module is a 2-byte hex string. To convert this hex string to a float, we need to swap these two bytes and perform a division. An example: response = 0xca19 swap bytes to get '0x19ca' convert to decimal value = 6602 divide decimal by 2000.0 = 3.301 (volts) Args: hex_str: a string (raw hex value). denom: a float, denominator used for hex-to-float conversion. Returns: a float, measurement value. Raises: InaError: if error converting measurement to float. """ match = HEX_STR_PATTERN.match(hex_str) if not match: raise InaError('Error: hex string %s does not match ' 'expected pattern' % hex_str) decimal = int('0x%s%s' % (match.group(2), match.group(1)), 16) return decimal/denom