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