blob: 90c4e60da70dd409ea4ce8624261306cf31da26e [file] [log] [blame]
# Copyright (c) 2012 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.
"""Interface to power measurement capabilities of TPS65090 PMIC.
Device contains 14 seperate ADCs allowing 12 power measurements of:
1. Main AC input (via ADC VAC(0) & IAC(2))
2. Battery Charging (via ADCs VBAT(1) & IBAT(3))
3-5. Current measurement of 3 DC->DC converters. Voltage must be provided.
6-12. Current measurement of 7 power fets. Voltage must be provided.
Module provides interface to register and read these power sources.
For example:
i2c = TPS65090I2c(4)
pwr = TPS65090Power(i2c)
pwr_names = []
pwr.register('ac', new_name='ac_mw')
pwr_names.append('ac_mw')
pwr.register('bat', new_name='bat_mw')
pwr_names.append('bat_mw')
voltages = [5, 3.3, 1.35, 40, 5, 3.3, 3.3, 3.3, 3.3, 5]
for i,name in enumerate(AD_OUT_DEFAULT_NAMES[4:]):
new_name = new_name='%s_mw' % name,
pwr.register(name, new_name=name, voltage=voltages[i])
pwr_names.append(new_name)
for name in new_names:
print '%s = %2.f\n' % (name, pwr.read_mw(name))
TODO(tbroch) Work with vendor to resolve discrepancies in measurements
"""
import commands
import logging
import numpy
import sys
import time
# TODO(tbroch) maybe this should be optarg
MAX_STATS = 20
# Lower 2bits configureable via Texas Instruments
I2C_SLV_DEFAULT = 0x48
REG_IDX_AD_CTRL = 0x16
# OUT1 is lower byte, OUT2 is upper byte of 10-bit ADC
REG_IDX_AD_OUT1 = 0x17
REG_IDX_AD_OUT2 = 0x18
# AD_CTRL POR value
# TODO(tbroch): Verify why <7> (reserved) != '1' despite datasheet POR
AD_CTRL_POR = 0x20
# Start a conversion measurement. ~1mA of additional load to TPS65090 Vin
AD_CTRL_START = 0x40
# Latest conversion is completed
AD_CTRL_EOC = 0x20
# ADC conversion vref is enabled. ~.5mA of additional load to TPS65090 Vin
AD_CTRL_EN = 0x10
AD_OUT_RANGE = 2**10 - 1
AD_OUT_LSB = [17./AD_OUT_RANGE,
17./AD_OUT_RANGE,
3./AD_OUT_RANGE,
4./AD_OUT_RANGE,
5./AD_OUT_RANGE,
5./AD_OUT_RANGE,
5./AD_OUT_RANGE,
1.1/AD_OUT_RANGE,
0.22/AD_OUT_RANGE,
3.3/AD_OUT_RANGE,
1.1/AD_OUT_RANGE,
1.1/AD_OUT_RANGE,
1.1/AD_OUT_RANGE,
1.1/AD_OUT_RANGE]
AD_OUT_DEFAULT_NAMES = ['vac', 'vbat', 'ac', 'bat', 'dcdc1', 'dcdc2',
'dcdc3', 'fet1', 'fet2', 'fet3', 'fet4', 'fet5',
'fet6', 'fet7']
class TPS65090Error(Exception):
"""Exception class for TPS65090."""
class TPS65090I2c(object):
"""Class for TPS65090 I2c communication."""
# -f to force, -y to respond 'yes' to interactive queries
_GET_CMD = 'i2cget -f -y'
_SET_CMD = 'i2cset -f -y'
# TODO(tbroch) deprecate retry once there are no reports of errors on i2c
_RETRY_MAX = 1
def __init__(self, bus, slv=I2C_SLV_DEFAULT):
"""Constructor."""
self._bus = bus
self._slv = slv
def _do_cmd(self, cmd):
"""Run command and return results.
Args:
cmd: string, i2c command to run
Returns:
string, output from shell command
Raises:
TPS65090Error: Bad status returned from shell command after retries
exhausted
"""
retry = self._RETRY_MAX
status = 1
while status and retry:
(status, output) = commands.getstatusoutput(cmd)
logging.debug('cmd: %s :: status: %d :: output %s',
cmd, status, output)
retry -= 1
if status:
raise TPS65090Error('Non-zero status from i2c get returned '
'status:%d' % status)
return output
def get(self, index):
"""Get I2c value.
Args:
index: integer, register index to write byte to
"""
cmd = '%s %d 0x%x 0x%x b' % (self._GET_CMD, self._bus, self._slv, index)
output = self._do_cmd(cmd)
return int(output, 0)
def set(self, index, val):
"""Set I2c value.
Args:
index: integer, register index to write byte to
val: integer, byte value to writ to index
"""
cmd = '%s %d 0x%x 0x%x 0x%x' % \
(self._SET_CMD, self._bus, self._slv, index, val)
self._do_cmd(cmd)
class TPS65090ADC(object):
"""Class for TPS65090 ADC."""
# Number of times to retry read of AD_CTRL for EOC
_RETRY_MAX = 10
def __init__(self, i2c, sensor_id):
"""."""
self._i2c = i2c
self._sensor_id = sensor_id
def read_adc(self):
"""Read value for ADC.
Steps required:
1. Write ADC value into AD_CTRL[3:0]
2. Enable measurement
3. Poll conversion ready
4. Read AD_OUT1
5. Read AD_OUT2
6. merge AD_OUT2[1:0] << 8 | AD_OUT1
7. return 10bit * units_per_lsb
Raises:
TPS65090Error: if ADC conversion fails to complete
"""
wr_val = AD_CTRL_EN | self._sensor_id
self._i2c.set(REG_IDX_AD_CTRL, wr_val)
wr_val |= AD_CTRL_START
self._i2c.set(REG_IDX_AD_CTRL, wr_val)
time.sleep(.01)
rd_val = self._i2c.get(REG_IDX_AD_CTRL)
retry = self._RETRY_MAX
while retry and not (rd_val & AD_CTRL_EOC):
time.sleep(.01)
rd_val = self._i2c.get(REG_IDX_AD_CTRL)
retry -= 1
if not retry:
raise TPS65090Error('Failed to see ADC conversion complete')
rd_val1 = self._i2c.get(REG_IDX_AD_OUT1)
rd_val2 = self._i2c.get(REG_IDX_AD_OUT2)
adc_val = ((rd_val2 << 8) | rd_val1) * AD_OUT_LSB[self._sensor_id]
logging.debug('adc%02d = %f', self._sensor_id, adc_val)
return adc_val
class TPS65090Power(object):
"""Class for TPS65090 PMIC Power.
Attributes:
_i2c: object, TPS65090I2c instance to communicate to PMIC over I2c
_voltage: list of voltages. Either float or TPS65090ADC instances (for
AC & battery)
_current: list of currents. All are TPS65090ADC instances
"""
def __init__(self, i2c):
"""Constructor.
Args:
i2c: object, TPS65090I2c instance to communicate to PMIC over I2c
"""
self._i2c = i2c
self._voltage = {}
self._current = {}
val = self._i2c.get(REG_IDX_AD_CTRL)
if val != AD_CTRL_POR:
raise TPS65090Error('Unable to read POR value for AD_CTRL')
self._i2c.set(REG_IDX_AD_CTRL, AD_CTRL_EN)
def __del__(self):
self._i2c.set(REG_IDX_AD_CTRL, AD_CTRL_POR)
def register(self, default_current_name, new_name=None, voltage=None):
"""Register a power measurement.
The TPS65090 provides two built-in power measurements by way
of voltage and current measurements for the AC input and the
battery charge. The remaining 10 current sensors can be made
to measure power with voltage provided.
Args:
default_current_name: string, current sensor default name per datasheet.
new_name: string, name to call power measurement. Defaults to
default_current_name.
voltage: float, value for potential of rail
Raises:
TPS65090Error: if voltage isn't provided for DCDC or FET sensors
"""
sensor_id = AD_OUT_DEFAULT_NAMES.index(default_current_name)
if not new_name:
new_name = default_current_name
if voltage is None:
if sensor_id not in [2, 3]:
raise TPS65090Error('Must provide nominal voltage')
if sensor_id == 2:
self._voltage[new_name] = TPS65090ADC(self._i2c, 0)
else:
self._voltage[new_name] = TPS65090ADC(self._i2c, 1)
else:
self._voltage[new_name] = float(voltage)
self._current[new_name] = TPS65090ADC(self._i2c, sensor_id)
logging.info('registered %s ( %s ) @ sensor %d', new_name,
default_current_name, sensor_id)
def read_mw(self, name):
"""Read power in milliwatts.
Returns:
float, power in milliwatts
Raises:
TPS65090Error: if current ADC is unable to be found
"""
if name not in self._current:
raise TPS65090Error('Unable to measure current. Did you '
'register the power?')
if type(self._voltage[name]) is float:
voltage = self._voltage[name]
else:
voltage = self._voltage[name].read_adc()
current = self._current[name].read_adc()
return current * voltage * 1000
def main():
"""main loop to read power."""
logging.basicConfig(level=logging.INFO)
logging.info('start')
i2c = TPS65090I2c(4)
pwr = TPS65090Power(i2c)
pwr_names = []
pwr.register('ac', new_name='ac_mw')
pwr_names.append('ac_mw')
pwr.register('bat', new_name='bat_mw')
pwr_names.append('bat_mw')
voltages = [5, 3.3, 1.35, 12, 5, 3.3, 3.3, 3.3, 3.3, 5]
names = ['pp5000_aux', 'pp3300_aux', 'pp1350_ddr', 'bl', 'pp5000_video',
'pp3300_wwan', 'pp3300_sdcard', 'pp3300_cam', 'pp3300_lcd',
'pp5000_ts']
for i, name in enumerate(AD_OUT_DEFAULT_NAMES[4:]):
new_name = '%s_mw' % names[i]
pwr.register(name, new_name=new_name, voltage=voltages[i])
pwr_names.append(new_name)
rails = pwr_names
if len(sys.argv) > 1:
rails = sys.argv[1:]
stats = {}
for name in rails:
stats[name] = numpy.array([])
while True:
for name in rails:
mw_val = pwr.read_mw(name)
if len(stats[name]) == MAX_STATS:
stats[name] = numpy.delete(stats[name], 0)
stats[name] = numpy.append(stats[name], mw_val)
print '%s:%.f' % (name, stats[name].mean())
# delay in order to not raise power via the script
# TODO(tbroch) sleep should be configureable
time.sleep(2)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
sys.exit(0)