Tan Gao | 3768ffe | 2011-08-30 11:12:20 -0700 | [diff] [blame] | 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 |