pacman: sample efficiency, global refresh/alignment
the pac utility queries accumulators across multiple devices in order to
establish power measurements for a device. This change modifies the
refresh behavior for the measurements to send the "refresh_g" command,
aligning measurements from all pacs. When binning time-domain data from
the test frameworks, this enables the time-domain data to be time
aligned. Additionally, this initializes the I2C "bus" objects outside of
the critical loop, and uses the auto-incrementing pointer to read all of
the accumulator data in one set (allowing for finer-grained ~100ms)
measurements.
BUG=b:235834092
TEST=running pacman.py, running pac_telemetry_logger
Change-Id: I2b09ebd20875334b4339a6bc1b8e642cc9614ec4
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/3706067
Tested-by: Parker Holloway <jparkerh@google.com>
Reviewed-by: Raul Rangel <rrangel@chromium.org>
Commit-Queue: Parker Holloway <jparkerh@google.com>
Reviewed-by: Robert Zieba <robertzieba@google.com>
diff --git a/contrib/power_measurement/pacman/pac_utils.py b/contrib/power_measurement/pacman/pac_utils.py
index ee075e5..1cc3514 100644
--- a/contrib/power_measurement/pacman/pac_utils.py
+++ b/contrib/power_measurement/pacman/pac_utils.py
@@ -12,6 +12,49 @@
from pyftdi.i2c import I2cController
from pyftdi.i2c import I2cNackError
+REFRESH_WAIT_TIME = 0.001
+
+
+class PacBus:
+ def __init__(self, i2c):
+ """
+ PacBus class holds initialized i2c port/pac objects, and can be
+ used to refresh pac devices globally
+
+ @param i2c: the i2c bus to use for the pac bus
+ """
+ self._i2c = i2c
+ self._pac_devices = {}
+
+ def add_device(self, addr):
+ """read_from will initialize a new pac on the i2c bus
+
+ @param addr: address of the new device to add
+
+ @returns: the newly initialized port (or existing open port)
+ """
+ if addr not in self._pac_devices:
+ self._pac_devices[addr] = self._i2c.get_port(addr)
+ return self._pac_devices[addr]
+
+ def get_device(self, addr):
+ """read_from will initialize a new pac on the i2c bus
+
+ @param addr: address of the new device to add
+ """
+ if addr not in self._pac_devices:
+ raise KeyError("Pac device not initialized at this address")
+ return self._pac_devices[addr]
+
+ def refresh_all(self):
+ """
+ refresh_all writes to the "general call" for the pac
+ devices, refreshing all accumulators synchronously
+ """
+ device = self._i2c.get_port(0x0)
+ device.write_to(pac19xx.REFRESH_G, 0)
+ time.sleep(REFRESH_WAIT_TIME)
+
def read_pac(device, reg, num_bytes):
"""Reads num_bytes from PAC I2C register using pyftdi driver.
@@ -115,14 +158,17 @@
return bool(gpio & (1 << 7))
-def reset_accumulator(device):
+def reset_accumulator(device, refresh_all=False):
"""Command to reset PAC accumulators.
Args:
device: i2c port object.
"""
+ if refresh_all:
+ device.refresh_all()
+ return
device.write_to(pac19xx.REFRESH, 0)
- time.sleep(0.001)
+ time.sleep(REFRESH_WAIT_TIME)
def print_registers(device, pac_address):
@@ -201,16 +247,63 @@
'2': pac19xx.VACC3,
'3': pac19xx.VACC4
}
- reg = device.read_from(lut[str(ch_num)], 7)
+ reg_bytes = device.read_from(lut[str(ch_num)], 7)
count = device.read_from(pac19xx.ACC_COUNT, 4)
count = int.from_bytes(count, byteorder='big', signed=False)
if polarity == 'unipolar':
- reg = int.from_bytes(reg, byteorder='big', signed=False)
+ reg = int.from_bytes(reg_bytes, byteorder='big', signed=False)
else:
- reg = int.from_bytes(reg, byteorder='big', signed=True)
+ reg = int.from_bytes(reg_bytes, byteorder='big', signed=True)
return (reg, count)
+def unpack_bytes_at(bytes_array, offset, length):
+ """
+ unpack_bytes_at extracts a subset of bytes from the bytes object and
+ returns them.
+
+ @param bytes: the input bytes_like object
+ @param offset: the number of bytes to offset into the array
+ @param length: the number of bytes the output
+ """
+ return bytes_array[offset:offset + length]
+
+
+def dump_all_accumulators(device, polarity):
+ """Command to acquire all voltage accumulator and counts for a PAC.
+
+ Args:
+ device: i2c port object
+ polarity: str, unipolar/bipolar
+
+ Returns:
+ reg: (long) voltage accumulator register.
+ count: (int) number of accumulations.
+ """
+ ACCUMULATOR_WIDTH = 7
+ ACCUMULATOR_COUNT_WIDTH = 4
+
+ all_bytes = device.read_from(pac19xx.ACC_COUNT, (ACCUMULATOR_WIDTH * 4) +
+ ACCUMULATOR_COUNT_WIDTH)
+ count_bytes = unpack_bytes_at(all_bytes, 0, ACCUMULATOR_COUNT_WIDTH)
+ accum_bytes = unpack_bytes_at(all_bytes, ACCUMULATOR_COUNT_WIDTH,
+ ACCUMULATOR_WIDTH * 4)
+ accums = []
+ for accum_idx in range(4):
+ reg = 0
+ sel_bytes = unpack_bytes_at(accum_bytes, accum_idx * ACCUMULATOR_WIDTH,
+ ACCUMULATOR_WIDTH)
+ if polarity == 'unipolar':
+ reg = int.from_bytes(sel_bytes, byteorder='big', signed=False)
+ else:
+ reg = int.from_bytes(sel_bytes, byteorder='big', signed=True)
+ accums.append(reg)
+ count = int.from_bytes(all_bytes[:ACCUMULATOR_COUNT_WIDTH],
+ byteorder='big',
+ signed=False)
+ return (accums, count)
+
+
def pac_info(ftdi_url):
"""Returns PAC debugging info
@@ -247,6 +340,7 @@
device.write_to(pac19xx.NEG_PWR_FSR, reg)
print(f'Set \t{hex(device.address)} polarity to: {polarity}')
+
terminate_signal = False
@@ -270,7 +364,9 @@
voltage=True,
current=True,
power=True,
- polarity='bipolar'):
+ polarity='bipolar',
+ sample_time=5,
+ refresh_all=True):
"""High level function to reset, log, then dump PAC power accumulations.
Args:
@@ -283,6 +379,8 @@
current: (boolean) log current.
power: (boolean) log power.
polarity: (string) ['unipolar', 'bipolar']
+ sample_time: (float) time in seconds between log samples
+ refresh_all: (boolean) refresh all pac devices synchronously
Returns:
time_log: (Pandas DataFrame) Time series log.
@@ -292,6 +390,7 @@
# Init the bus.
i2c = I2cController()
i2c.configure(ftdi_url)
+ accumulators = {}
# Filter the config to rows we care about.
allow_rails = set()
@@ -302,13 +401,13 @@
# Clear all accumulators
skip_pacs = set()
+ pacbus = PacBus(i2c)
for addr in config.pacs_by_addr:
try:
- device = i2c.get_port(addr)
+ device = pacbus.add_device(addr)
set_polarity(device, polarity)
disable_slow(device)
reset_accumulator(device)
- time.sleep(0.001)
except I2cNackError:
# This happens on the DB PAC in Z states.
print(f'Unable to reset PAC {addr:#x}. Ignoring value')
@@ -322,68 +421,78 @@
start_time = time.time()
timeout = start_time + float(record_length)
- prev_accum_by_rail = {}
- prev_count_by_rail = {}
+ refresh_time = 0
+ last_target_sample = time.time() - sample_time
while True:
if time.time() > timeout:
break
+ sleep = last_target_sample + sample_time - time.time()
+ if sleep > 0:
+ time.sleep(sleep)
+ last_target_sample = last_target_sample + sample_time
if terminate_signal:
break
print('Logging: %.2f / %.2f s...' %
(time.time() - start_time, float(record_length)),
end='\r')
- # Group measurements by pac for speed.
+ if refresh_all:
+ pacbus.refresh_all()
+ refresh_time = time.time()
for addr, group in config.pacs_by_addr.items():
if addr in skip_pacs:
continue
- # Parse from the dataframe.
- device = i2c.get_port(addr)
- # Setup any configuration changes here prior to refresh.
- device.write_to(pac19xx.REFRESH_V, 0)
- # Wait 1ms after REFRESH for registers to stablize.
- time.sleep(.001)
+ device = pacbus.get_device(addr)
+ if not refresh_all:
+ device.write_to(pac19xx.REFRESH_V, 0)
+ refresh_time = time.time()
+ time.sleep(REFRESH_WAIT_TIME)
+
+ (accum, count) = dump_all_accumulators(device, polarity)
# Log every rail on this pac we need to.
for pac in group:
if len(allow_rails) > 0 and pac.name not in allow_rails:
continue
-
try:
- ch_num = pac.channel
+ ch_num = int(pac.channel)
sense_r = float(pac.rsense)
- prev_accum = prev_accum_by_rail.get(pac.name, 0)
- prev_count = prev_count_by_rail.get(pac.name, 0)
-
- prev_accum = prev_accum_by_rail.get(pac.name, 0)
- prev_count = prev_count_by_rail.get(pac.name, 0)
-
# Grab a log timestamp
tmp = {}
- tmp['systime'] = time.time()
- tmp['relativeTime'] = tmp['systime'] - start_time
+ tmp['systime'] = refresh_time
+ tmp['relativeTime'] = refresh_time - start_time
tmp['rail'] = pac.name
- (accum, count) = dump_accumulator(device, ch_num, polarity)
-
- tmp['rawAccumReg'] = accum
- tmp['rawCount'] = count
-
- prev_accum_by_rail[pac.name] = accum
- prev_count_by_rail[pac.name] = count
-
- accum = accum - prev_accum
- count = count - prev_count
-
depth = {'unipolar': 2**30, 'bipolar': 2**29}
- # Equation 3-8 Energy Calculation.
- tmp['accumReg'] = accum
+ # Equation 5-8 Energy Calculation.
+ tmp['accumReg'] = accum[ch_num]
tmp['count'] = count
pwrFSR = 3.2 / sense_r
- tmp['power'] = accum / (depth[polarity] * count) * pwrFSR
+ tmp['power'] = (accum[ch_num] / (depth[polarity] * count) *
+ pwrFSR)
+ if pac.name not in accumulators:
+ accumulators[pac.name] = {
+ 'Rail': pac.name,
+ 'rSense': pac.rsense,
+ 'count': 0,
+ 'accumReg': 0,
+ 'Average Power (w)': 0
+ }
+
+ accumulators[
+ pac.name]['tAccum'] = refresh_time - start_time
+ accumulators[pac.name]['count'] += count
+
+ accumulators[pac.name]['accumReg'] += accum[ch_num]
+ new_weighting = count / accumulators[pac.name]['count']
+ old_weighting = 1 - new_weighting
+ accumulators[
+ pac.name]['Average Power (w)'] *= old_weighting
+ accumulators[pac.name]['Average Power (w)'] += (
+ tmp['power']) * new_weighting
log.append(tmp)
except I2cNackError:
print('NACK detected, continuing measurements')
@@ -395,33 +504,7 @@
pandas.options.display.float_format = '{:,.3f}'.format
stats = time_log.groupby('rail').power.describe()
- accumulators = []
- # Dump the accumulator.
- for pac in config.pacs:
- if pac.addr in skip_pacs:
- continue
-
- if len(allow_rails) > 0 and pac.name not in allow_rails:
- continue
-
- accumulator = {}
- device = i2c.get_port(pac.addr)
- time.sleep(.001)
-
- accumulator['Rail'] = pac.name
- (accum, count) = dump_accumulator(device, pac.channel, polarity)
- accumulator['tAccum'] = time.time() - start_time
- accumulator['count'] = count
- depth = {'unipolar': 2**30, 'bipolar': 2**29}
- # Equation 3-8 Energy Calculation.
- accumulator['accumReg'] = accum
- accumulator['rSense'] = pac.rsense
- pwrFSR = 3.2 / pac.rsense
- accumulator['Average Power (w)'] = accum / (depth[polarity] *
- count) * pwrFSR
- accumulators.append(accumulator)
-
- accumulatorLog = pandas.DataFrame(accumulators)
+ accumulatorLog = pandas.DataFrame(accumulators.values())
print('Accumulator Power Measurements by Rail (W)')
print(accumulatorLog.sort_values(by='Average Power (w)', ascending=False))
@@ -499,7 +582,8 @@
# Read power.
tmp['power'] = read_power(device, pac.rsense, pac.channel, polarity)
# Read current.
- tmp['current'] = read_current(device, pac.rsense, pac.channel, polarity)
+ tmp['current'] = read_current(device, pac.rsense, pac.channel,
+ polarity)
log.append(tmp)
log = pandas.DataFrame(log)
diff --git a/contrib/power_measurement/pacman/pacman.py b/contrib/power_measurement/pacman/pacman.py
index 932822e..28c4817 100755
--- a/contrib/power_measurement/pacman/pacman.py
+++ b/contrib/power_measurement/pacman/pacman.py
@@ -37,6 +37,10 @@
'--time',
default=9999999,
help='Time to capture in seconds')
+ argparser.add_argument('--sample_time',
+ default=1,
+ type=float,
+ help='Sample time in seconds')
argparser.add_argument(
'-c',
'--config',
@@ -96,7 +100,8 @@
# Record the sample and log to file.
(log, accumulator_log) = pac_utils.record(config,
record_length=args.time,
- polarity=args.polarity)
+ polarity=args.polarity,
+ sample_time=args.sample_time)
log.to_csv(time_log_path)
accumulator_log.to_csv(accumulator_log_path)
@@ -149,8 +154,7 @@
accumulator_log['Average Power (w)'] = avg_power
# Voltage column used for color coding
accumulator_log['voltage (mv)'] = accumulator_log.Rail.apply(
- lambda x: x.split('_')[0].strip('PP')
- )
+ lambda x: x.split('_')[0].strip('PP'))
star_plot = plotly.express.sunburst(accumulator_log,
names='Rail',
parents='Parent',
@@ -159,7 +163,8 @@
color='voltage (mv)')
# Calculate what the sum of the child rails of root is
root = 'PPVAR_SYS'
- root_pwr = accumulator_log[accumulator_log.Rail == root]['Average Power (w)']
+ root_pwr = accumulator_log[accumulator_log.Rail ==
+ root]['Average Power (w)']
# This should be a single element
root_pwr = root_pwr.iloc[0]
# tier 1 rails, children of root
@@ -170,8 +175,7 @@
t1_summary_text = (
f"{'T1 Rail Total:':<20}{t1_pwr:>20.3f}" + '\n' +
f"{'T1 Root %s' % root:<20}{root_pwr:>20.3f}" + '\n' +
- f"{'Root - T1 Total:':<20}{(root_pwr - t1_pwr):>20.3f}" + '\n'
- )
+ f"{'Root - T1 Total:':<20}{(root_pwr - t1_pwr):>20.3f}" + '\n')
print('Tier1 Summary')
print(t1_summary_text)
print(t1_summary)
@@ -179,14 +183,12 @@
# HTML summary table
t1_summary_table = plotly.graph_objects.Figure(data=[
plotly.graph_objects.Table(
- header=dict(values=summary_columns,
- align='left'),
+ header=dict(values=summary_columns, align='left'),
cells=dict(values=[
- t1_summary.Rail,
- t1_summary['voltage (mv)'],
+ t1_summary.Rail, t1_summary['voltage (mv)'],
t1_summary['Average Power (w)'].round(3)
],
- align='left'))
+ align='left'))
])
t1_summary_table.update_layout(
title=f"{'T1 Rail Total: %.3f Watts' % t1_pwr:6>}")
@@ -197,10 +199,11 @@
with open(report_log_path, 'w') as f:
f.write(summary_table.to_html(full_html=False, include_plotlyjs='cdn'))
if not skip_sunplot:
- f.write(t1_summary_table.to_html(full_html=False,
- include_plotlyjs='cdn',
- default_width='100%',
- default_height='50%'))
+ f.write(
+ t1_summary_table.to_html(full_html=False,
+ include_plotlyjs='cdn',
+ default_width='100%',
+ default_height='50%'))
f.write(star_plot.to_html(full_html=False, include_plotlyjs='cdn'))
f.write(box_plot.to_html(full_html=False, include_plotlyjs='cdn'))
f.write(time_plot.to_html(full_html=False, include_plotlyjs='cdn'))