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 7Background 8 - INA219 is one of two modules on TTCI board 9 - This library provides methods to interact with INA219 programmatically 10 11Dependency 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 15Notes: 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 22import logging, re 23from autotest_lib.client.common_lib import i2c_slave 24 25 26# INA219 registers 27INA_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 37HEX_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 41INA_CONF_INIT_VAL = 0x9f31 42INA_CALIB_INIT_VAL = 0xc90e 43 44# Default values used to calculate/interpret voltage and current measurements. 45DEFAULT_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 53class InaError(Exception): 54 """Base class for all errors in this module.""" 55 56 57class 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