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