Home | History | Annotate | Download | only in common_lib
      1 # Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """A Python library to interact with INA219 module for TPM testing.
      6 
      7 Background
      8  - INA219 is one of two modules on TTCI board
      9  - This library provides methods to interact with INA219 programmatically
     10 
     11 Dependency
     12  - This library depends on a new C shared library called "libsmogcheck.so".
     13  - In order to run test cases built using this API, one needs a TTCI board
     14 
     15 Notes:
     16  - An exception is raised if it doesn't make logical sense to continue program
     17    flow (e.g. I/O error prevents test case from executing)
     18  - An exception is caught and then converted to an error code if the caller
     19    expects to check for error code per API definition
     20 """
     21 
     22 import logging, re
     23 from autotest_lib.client.common_lib import i2c_slave
     24 
     25 
     26 # INA219 registers
     27 INA_REG = {
     28     'CONF': 0,        # Configuration Register
     29     'SHUNT_VOLT': 1,  # Shunt Voltage
     30     'BUS_VOLT': 2,    # Bus Voltage
     31     'POWER': 3,       # Power
     32     'CURRENT': 4,     # Current
     33     'CALIB': 5,       # Calibration
     34     }
     35 
     36 # Regex pattern for measurement value
     37 HEX_STR_PATTERN = re.compile('^0x([0-9a-f]{2})([0-9a-f]{2})$')
     38 
     39 # Constants used to initialize INA219 registers
     40 # TODO(tgao): add docstring for these values after stevenh replies
     41 INA_CONF_INIT_VAL = 0x9f31
     42 INA_CALIB_INIT_VAL = 0xc90e
     43 
     44 # Default values used to calculate/interpret voltage and current measurements.
     45 DEFAULT_MEAS_RANGE_VALUE = {
     46     'current': {'max': 0.1, 'min': 0.0, 'denom': 10000.0,
     47                 'reg': INA_REG['CURRENT']},
     48     'voltage': {'max': 3.35, 'min': 3.25, 'denom': 2000.0,
     49                 'reg': INA_REG['BUS_VOLT']},
     50     }
     51 
     52 
     53 class InaError(Exception):
     54     """Base class for all errors in this module."""
     55 
     56 
     57 class InaController(i2c_slave.I2cSlave):
     58     """Object to control INA219 module on TTCI board."""
     59 
     60     def __init__(self, slave_addr=None, range_dict=None):
     61         """Constructor.
     62 
     63         Mandatory params:
     64           slave_addr: slave address to set. Default: None.
     65 
     66         Optional param:
     67           range_dict: desired max/min thresholds for measurement values.
     68                       Default: DEFAULT_MEAS_RANGE_VALUE.
     69 
     70         Args:
     71           slave_addr: an integer, address of main or backup power.
     72           range_dict: desired max/min thresholds for measurement values.
     73 
     74         Raises:
     75           InaError: if error initializing INA219 module or invalid range_dict.
     76         """
     77         super(InaController, self).__init__()
     78         if slave_addr is None:
     79             raise InaError('Error slave_addr expected')
     80 
     81         try:
     82             if range_dict is None:
     83                 range_dict = DEFAULT_MEAS_RANGE_VALUE
     84             else:
     85                 self._validateRangeDict(DEFAULT_MEAS_RANGE_VALUE, range_dict)
     86             self.range_dict = range_dict
     87 
     88             self.setSlaveAddress(slave_addr)
     89             self.writeWord(INA_REG['CONF'], INA_CONF_INIT_VAL)
     90             self.writeWord(INA_REG['CALIB'], INA_CALIB_INIT_VAL)
     91         except InaError, e:
     92             raise InaError('Error initializing INA219: %s' % e)
     93 
     94     def _validateRangeDict(self, d_ref, d_in):
     95         """Validates keys and types of value in range_dict.
     96 
     97         Iterate over d_ref to make sure all keys exist in d_in and
     98         values are of the correct type.
     99 
    100         Args:
    101           d_ref: a dictionary, used as reference.
    102           d_in: a dictionary, to be validated against reference.
    103 
    104         Raises:
    105           InaError: if range_dict is invalid.
    106         """
    107         for k, v in d_ref.iteritems():
    108             if k not in d_in:
    109                 raise InaError('Key %s not present in dict %r' % (k, d_in))
    110             if type(v) != type(d_in[k]):
    111                 raise InaError(
    112                     'Value type mismatch for key %s. Expected: %s; actual = %s'
    113                     % (k, type(v), type(d_in[k])))
    114             if type(v) is dict:
    115                 self._validateRangeDict(v, d_in[k])
    116 
    117     def readMeasure(self, measure):
    118         """Reads requested measurement.
    119 
    120         Args:
    121           measure: a string, 'current' or 'voltage'.
    122 
    123         Returns:
    124           a float, measurement in native units. Or None if error.
    125 
    126         Raises:
    127           InaError: if error reading requested measurement.
    128         """
    129         try:
    130             hex_str = '0x%.4x' % self.readWord(self.range_dict[measure]['reg'])
    131             logging.debug('Word read = %r', hex_str)
    132             return self._checkMeasureRange(hex_str, measure)
    133         except InaError, e:
    134             logging.error('Error reading %s: %s', measure, e)
    135 
    136     def getPowerMetrics(self):
    137         """Get measurement metrics for Main Power.
    138 
    139         Returns:
    140           an integer, 0 for success and -1 for error.
    141           a float, voltage value in Volts. Or None if error.
    142           a float, current value in Amps. Or None if error.
    143         """
    144         logging.info('Attempt to get power metrics')
    145         try:
    146             return (0, self.readMeasure('voltage'),
    147                     self.readMeasure('current'))
    148         except InaError, e:
    149             logging.error('getPowerMetrics(): %s', e)
    150             return (-1, None, None)
    151 
    152     def _checkMeasureRange(self, hex_str, measure):
    153         """Checks if measurement value falls within a pre-specified range.
    154 
    155         Args:
    156           hex_str: a string (hex value).
    157           measure: a string, 'current' or 'voltage'.
    158 
    159         Returns:
    160           measure_float: a float, measurement value.
    161 
    162         Raises:
    163           InaError: if value doesn't fall in range.
    164         """
    165         measure_float = self._convertHexToFloat(
    166             hex_str, self.range_dict[measure]['denom'])
    167         measure_msg = '%s value %.2f' % (measure, measure_float)
    168         range_msg = '[%(min).2f, %(max).2f]' % self.range_dict[measure]
    169         if (measure_float < self.range_dict[measure]['min'] or
    170             measure_float > self.range_dict[measure]['max']):
    171             raise InaError('%s is out of range %s' % measure_msg, range_msg)
    172         logging.info('%s is in range %s', measure_msg, range_msg)
    173         return measure_float
    174 
    175     def _convertHexToFloat(self, hex_str, denom):
    176         """Performs measurement calculation.
    177 
    178         The measurement reading from INA219 module is a 2-byte hex string.
    179         To convert this hex string to a float, we need to swap these two bytes
    180         and perform a division. An example:
    181           response = 0xca19
    182           swap bytes to get '0x19ca'
    183           convert to decimal value = 6602
    184           divide decimal by 2000.0 = 3.301 (volts)
    185 
    186         Args:
    187           hex_str: a string (raw hex value).
    188           denom: a float, denominator used for hex-to-float conversion.
    189 
    190         Returns:
    191           a float, measurement value.
    192 
    193         Raises:
    194           InaError: if error converting measurement to float.
    195         """
    196         match = HEX_STR_PATTERN.match(hex_str)
    197         if not match:
    198             raise InaError('Error: hex string %s does not match '
    199                            'expected pattern' % hex_str)
    200 
    201         decimal = int('0x%s%s' % (match.group(2), match.group(1)), 16)
    202         return decimal/denom
    203