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