普通文本  |  203行  |  6.97 KB

# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""A Python library to interact with INA219 module for TPM testing.

Background
 - INA219 is one of two modules on TTCI board
 - This library provides methods to interact with INA219 programmatically

Dependency
 - This library depends on a new C shared library called "libsmogcheck.so".
 - In order to run test cases built using this API, one needs a TTCI board

Notes:
 - An exception is raised if it doesn't make logical sense to continue program
   flow (e.g. I/O error prevents test case from executing)
 - An exception is caught and then converted to an error code if the caller
   expects to check for error code per API definition
"""

import logging, re
from autotest_lib.client.common_lib import i2c_slave


# INA219 registers
INA_REG = {
    'CONF': 0,        # Configuration Register
    'SHUNT_VOLT': 1,  # Shunt Voltage
    'BUS_VOLT': 2,    # Bus Voltage
    'POWER': 3,       # Power
    'CURRENT': 4,     # Current
    'CALIB': 5,       # Calibration
    }

# Regex pattern for measurement value
HEX_STR_PATTERN = re.compile('^0x([0-9a-f]{2})([0-9a-f]{2})$')

# Constants used to initialize INA219 registers
# TODO(tgao): add docstring for these values after stevenh replies
INA_CONF_INIT_VAL = 0x9f31
INA_CALIB_INIT_VAL = 0xc90e

# Default values used to calculate/interpret voltage and current measurements.
DEFAULT_MEAS_RANGE_VALUE = {
    'current': {'max': 0.1, 'min': 0.0, 'denom': 10000.0,
                'reg': INA_REG['CURRENT']},
    'voltage': {'max': 3.35, 'min': 3.25, 'denom': 2000.0,
                'reg': INA_REG['BUS_VOLT']},
    }


class InaError(Exception):
    """Base class for all errors in this module."""


class InaController(i2c_slave.I2cSlave):
    """Object to control INA219 module on TTCI board."""

    def __init__(self, slave_addr=None, range_dict=None):
        """Constructor.

        Mandatory params:
          slave_addr: slave address to set. Default: None.

        Optional param:
          range_dict: desired max/min thresholds for measurement values.
                      Default: DEFAULT_MEAS_RANGE_VALUE.

        Args:
          slave_addr: an integer, address of main or backup power.
          range_dict: desired max/min thresholds for measurement values.

        Raises:
          InaError: if error initializing INA219 module or invalid range_dict.
        """
        super(InaController, self).__init__()
        if slave_addr is None:
            raise InaError('Error slave_addr expected')

        try:
            if range_dict is None:
                range_dict = DEFAULT_MEAS_RANGE_VALUE
            else:
                self._validateRangeDict(DEFAULT_MEAS_RANGE_VALUE, range_dict)
            self.range_dict = range_dict

            self.setSlaveAddress(slave_addr)
            self.writeWord(INA_REG['CONF'], INA_CONF_INIT_VAL)
            self.writeWord(INA_REG['CALIB'], INA_CALIB_INIT_VAL)
        except InaError, e:
            raise InaError('Error initializing INA219: %s' % e)

    def _validateRangeDict(self, d_ref, d_in):
        """Validates keys and types of value in range_dict.

        Iterate over d_ref to make sure all keys exist in d_in and
        values are of the correct type.

        Args:
          d_ref: a dictionary, used as reference.
          d_in: a dictionary, to be validated against reference.

        Raises:
          InaError: if range_dict is invalid.
        """
        for k, v in d_ref.iteritems():
            if k not in d_in:
                raise InaError('Key %s not present in dict %r' % (k, d_in))
            if type(v) != type(d_in[k]):
                raise InaError(
                    'Value type mismatch for key %s. Expected: %s; actual = %s'
                    % (k, type(v), type(d_in[k])))
            if type(v) is dict:
                self._validateRangeDict(v, d_in[k])

    def readMeasure(self, measure):
        """Reads requested measurement.

        Args:
          measure: a string, 'current' or 'voltage'.

        Returns:
          a float, measurement in native units. Or None if error.

        Raises:
          InaError: if error reading requested measurement.
        """
        try:
            hex_str = '0x%.4x' % self.readWord(self.range_dict[measure]['reg'])
            logging.debug('Word read = %r', hex_str)
            return self._checkMeasureRange(hex_str, measure)
        except InaError, e:
            logging.error('Error reading %s: %s', measure, e)

    def getPowerMetrics(self):
        """Get measurement metrics for Main Power.

        Returns:
          an integer, 0 for success and -1 for error.
          a float, voltage value in Volts. Or None if error.
          a float, current value in Amps. Or None if error.
        """
        logging.info('Attempt to get power metrics')
        try:
            return (0, self.readMeasure('voltage'),
                    self.readMeasure('current'))
        except InaError, e:
            logging.error('getPowerMetrics(): %s', e)
            return (-1, None, None)

    def _checkMeasureRange(self, hex_str, measure):
        """Checks if measurement value falls within a pre-specified range.

        Args:
          hex_str: a string (hex value).
          measure: a string, 'current' or 'voltage'.

        Returns:
          measure_float: a float, measurement value.

        Raises:
          InaError: if value doesn't fall in range.
        """
        measure_float = self._convertHexToFloat(
            hex_str, self.range_dict[measure]['denom'])
        measure_msg = '%s value %.2f' % (measure, measure_float)
        range_msg = '[%(min).2f, %(max).2f]' % self.range_dict[measure]
        if (measure_float < self.range_dict[measure]['min'] or
            measure_float > self.range_dict[measure]['max']):
            raise InaError('%s is out of range %s' % measure_msg, range_msg)
        logging.info('%s is in range %s', measure_msg, range_msg)
        return measure_float

    def _convertHexToFloat(self, hex_str, denom):
        """Performs measurement calculation.

        The measurement reading from INA219 module is a 2-byte hex string.
        To convert this hex string to a float, we need to swap these two bytes
        and perform a division. An example:
          response = 0xca19
          swap bytes to get '0x19ca'
          convert to decimal value = 6602
          divide decimal by 2000.0 = 3.301 (volts)

        Args:
          hex_str: a string (raw hex value).
          denom: a float, denominator used for hex-to-float conversion.

        Returns:
          a float, measurement value.

        Raises:
          InaError: if error converting measurement to float.
        """
        match = HEX_STR_PATTERN.match(hex_str)
        if not match:
            raise InaError('Error: hex string %s does not match '
                           'expected pattern' % hex_str)

        decimal = int('0x%s%s' % (match.group(2), match.group(1)), 16)
        return decimal/denom