A processor does not draw a fixed power: consumption depends on frequency, on voltage, and on how much time it spends idle. In this lab we turn a Raspberry Pi board (5 or 3) into an instrument that measures itself - it shows the real-time power consumption on an LCD and lets us compare, with real numbers, what "fast" means and what "economical" means.
1Objectives of the lab
- Understanding the relationship P = C·V²·f and why voltage matters more than frequency
- Reading the processor state from
sysfs: current frequency, governor, thermal throttling - Measuring the power consumed through two independent methods: the internal power management circuit (Pi 5 only) and an external INA219 module (the only method on a Pi 3)
- Changing the DVFS governor and observing the effect on execution time and energy
- Building an LCD display that reports power, frequency and temperature in real time
- Calculating energy per task and the performance-per-watt ratio - the indicators that actually matter
2About the project
We build an energy test bench: a program that runs the same compute task several times, once for each processor frequency policy, measuring each time how long it took and how much energy it consumed. The results appear on the LCD in real time and are saved to a CSV file that you can open in Excel for your report.
The lab can also be done on a Raspberry Pi 3, with one important difference: the Pi 3 has no such circuit, so power can only be measured externally, with the INA219 module. The differences between the two boards are collected in the next section, and wherever the steps differ, the page says so explicitly.
3Materials needed
For both variants
- 1 32 GB microSD card with Raspberry Pi OS (64-bit, Bookworm)
- 1 16×2 LCD display with I²C adapter (PCF8574)
- 8 Female-to-female jumper wires
- 1 3.3 V ↔ 5 V I²C level shifter (recommended, for the LCD)
Raspberry Pi 5 variant
- 1 Raspberry Pi 5 (4 or 8 GB)
- 1 Official 27 W USB-C power supply
- 1 Official active cooler (recommended - otherwise thermal throttling occurs)
- 1 INA219 or INA226 module (optional, for the second part)
Raspberry Pi 3 variant
- 1 Raspberry Pi 3 Model B or B+
- 1 INA219 module (required - it is the only power measurement)
- 1 Adjustable 5 V / 3 A bench power supply with current limiting
- 2 Crocodile-clip or male-to-female wires for the supply
- 1 Heatsink on the processor (recommended)
Pi 5 or Pi 3: what changes
First find out which board you have in front of you. The following command tells you directly:
cat /proc/device-tree/model; echo
The answer looks like Raspberry Pi 5 Model B Rev 1.0 or Raspberry Pi 3 Model B Plus Rev 1.3.
Write it down in your report - sections 8, 9 and 12 depend on it.
| Raspberry Pi 5 | Raspberry Pi 3 (B / B+) | |
|---|---|---|
| Processor | BCM2712, 4 × Cortex-A76 | BCM2837, 4 × Cortex-A53 |
| Frequency steps | several, roughly 1500-2400 MHz | only two: 600 and 1200 MHz (1400 MHz on the B+) |
| Internal power measurement | yes, vcgencmd pmic_read_adc | does not exist - the command answers Command not registered |
| How we measure power | PMIC, with the INA219 as a witness | INA219 only, inserted into the board's supply |
| Power supply | USB-C, 5 V / 5 A | micro-USB, 5 V / 2.5 A (in this lab: a bench supply) |
| Sections that differ | - | 8 (read it, do not run it), 9 (wiring required), 11 and 12 |
4Where the consumption comes from
The two components of power
The power consumed by a digital circuit has two sources, and they behave completely differently:
| Component | Formula | When it occurs | How to reduce it |
|---|---|---|---|
| Dynamic - transistor switching | Pd = α·C·V²·f |
only while the circuit is working | lower V and f, or stop the clock (clock gating) |
| Static - leakage currents | Ps ≈ V·Ileak |
permanently, as long as the circuit is powered | cut the power (power gating) |
The key observation is the exponent: dynamic power depends on voltage squared, but only linearly on frequency. If we halve the frequency and can thereby also lower the voltage by 20%, the power drops to 0.5 · 0.8² = 32% of the initial value. That is why the two are always adjusted together - hence the name of the technique: DVFS, Dynamic Voltage and Frequency Scaling.
The trap: energy is not power
This is where most students trip up. Lower power does not automatically mean lower energy, because the task takes longer:
If we halve the frequency, the task takes twice as long. The dynamic energy per task stays nearly unchanged (P falls linearly with f, t rises linearly with 1/f - they cancel out). What really changes is the static energy, which increases because the circuit stays on twice as long, and the dynamic energy through the lower voltage.
The two opposing strategies
| Strategy | How it sounds | When it wins |
|---|---|---|
| Race to idle (run then sleep) | run at maximum frequency, finish as fast as possible, then enter a low-power state | when idle power and leakage are large - i.e. on modern application processors |
| Just in time (exactly on time) | choose the lowest frequency that still meets the deadline | when idling costs almost nothing and dynamic power dominates - simple microcontrollers |
There is no universal answer. Which one wins depends on the ratio between idle power and active power - and we can discover that for ourselves with the simulator below.
5DVFS explorer: where is the optimum
You have a task with a certain number of cycles and a deadline by which it must finish. Choose the frequency. The chart shows the total energy consumed by the deadline, for every possible frequency - the yellow dot is the optimum.
| System power | Optimal frequency | What it means |
|---|---|---|
| 0 W | ≈ 800 MHz | the processor is alone in the world; only voltage matters, so work slowly |
| 0.5 W | ≈ 1200 MHz | balance between the lower voltage and the longer time |
| 3 W | ≈ 2200 MHz | every second is expensive - finish and put the whole system to sleep |
6Setting up the system
Go through the steps in order. Each step can be checked immediately - do not move on until you see the expected result.
- Update the system and install the tools
We assume 64-bit Raspberry Pi OS (Bookworm or newer), with terminal access.
updatesudo apt update && sudo apt full-upgrade -y sudo apt install -y python3-pip python3-venv i2c-tools build-essential stress-ng
- Enable the I²C bus
Through the configuration menu:
Interface Options→I2C→Yes.configurationsudo raspi-config sudo reboot
After rebooting, check that the bus exists:
checkls -l /dev/i2c-1 i2cdetect -y 1
A grid of addresses should appear. Since nothing is connected yet, all the cells are
--. If you get "No such file or directory", I²C was not enabled - redo the step. - Create a Python virtual environment
On Bookworm, Python packages are no longer installed directly into the system. The
--system-site-packagesoption still lets us use libraries already installed via apt.working environmentpython3 -m venv --system-site-packages ~/si-lab source ~/si-lab/bin/activate pip install smbus2 RPLCD adafruit-circuitpython-ina219
Worth rememberingEvery new terminal session you must runsource ~/si-lab/bin/activateagain, otherwise Python cannot find the libraries. - Create the working folderfolder
mkdir -p ~/si-lab/lab01 && cd ~/si-lab/lab01
- Check which governors the processor hasavailable governors
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
You should see a list containing at least
ondemand,powersave,performanceandschedutil, and the current frequency expressed in kHz (for example1500000= 1.5 GHz).On a Pi 3 the current frequency can only be
600000or1200000(1400000on the B+) - the processor has only these two steps.
/sys is
It is not a folder of files on the card. It is a window into the operating system kernel's
structures: when you read scaling_cur_freq, the kernel queries the clock controller on
the spot and returns the answer as text. When you write to scaling_governor, the
kernel actually changes the policy. It is the same idea as memory-mapped registers on
microcontrollers, just moved one level up.7Reading the processor state
The first program reads everything the system can tell us about the processor's state. It does not need any wire connected.
#!/usr/bin/env python3
"""Reads the processor state (Raspberry Pi 5 or 3) from sysfs and vcgencmd."""
import pathlib
import subprocess
NUM_CORES = 4
CPUFREQ = "/sys/devices/system/cpu/cpu{}/cpufreq/{}"
def read_value(path):
"""Reads a sysfs file; returns None if it does not exist."""
try:
return pathlib.Path(path).read_text().strip()
except OSError:
return None
def frequency(core=0):
"""Current frequency of a core, in MHz."""
khz = read_value(CPUFREQ.format(core, "scaling_cur_freq"))
return int(khz) / 1000 if khz else None
def governor(core=0):
return read_value(CPUFREQ.format(core, "scaling_governor"))
def available_steps():
"""List of the frequency steps, in MHz."""
val = read_value(CPUFREQ.format(0, "scaling_available_frequencies"))
if not val:
# Some cores only expose the limits, not the full list.
low = read_value(CPUFREQ.format(0, "cpuinfo_min_freq"))
high = read_value(CPUFREQ.format(0, "cpuinfo_max_freq"))
return [int(low) / 1000, int(high) / 1000] if low and high else []
return [int(v) / 1000 for v in val.split()]
def temperature():
"""Processor temperature, in degrees Celsius."""
val = read_value("/sys/class/thermal/thermal_zone0/temp")
return int(val) / 1000 if val else None
# Each bit of vcgencmd get_throttled flags a power or temperature issue.
# Bits 0-3 = happening now, bits 16-19 = has happened since boot.
FLAGS = {
0: "undervoltage NOW",
1: "frequency throttled NOW",
2: "thermal throttling NOW",
3: "soft temperature limit NOW",
16: "undervoltage has occurred",
17: "frequency throttling has occurred",
18: "thermal throttling has occurred",
19: "soft temperature limit has been reached",
}
def throttle_flags():
"""Returns the list of problems reported by the firmware."""
try:
output = subprocess.run(["vcgencmd", "get_throttled"],
capture_output=True, text=True, check=True).stdout
except (OSError, subprocess.CalledProcessError):
return ["vcgencmd unavailable"]
value = int(output.strip().split("=")[1], 16)
return [text for bit, text in FLAGS.items() if value & (1 << bit)]
if __name__ == "__main__":
print("active governor :", governor())
print("steps (MHz) :", available_steps())
print("temperature : %.1f degC" % temperature())
print()
for n in range(NUM_CORES):
print(" core %d: %7.1f MHz" % (n, frequency(n)))
print()
issues = throttle_flags()
print("throttling :", ", ".join(issues) if issues else "none")Run it twice: once on an idle system, then again while loading the processor. The difference should be visible right away.
python3 cpu_state.py # idle system stress-ng --cpu 4 --timeout 30s & # load all 4 cores sleep 5 && python3 cpu_state.py # and now?
vcgencmd measure_clock arm vcgencmd measure_volts core
stress-ng. On some boards (especially the B+) the voltage
rises together with the frequency, on others it stays almost constant. Both results are informative: without a
voltage drop, DVFS gains much less than the formula in section 4 promises. Also on the Pi 3, "undervoltage" often
shows up with phone chargers: the board needs 5 V / 2.5 A.8Internal power measurement (Pi 5 only)
vc_gencmd_read_response returned -1 and error=1 error_msg="Command not registered":
the firmware does not know the command. It is not an installation mistake and no update will fix it. Read the
section anyway, to understand what you are missing, then continue with section 9, where the INA219 becomes the
main measurement.On a Pi 5, the same error means the firmware is too old: run
sudo apt full-upgrade and
sudo rpi-eeprom-update -a, then reboot the board.The Raspberry Pi 5 has a dedicated power management circuit (PMIC) that distributes voltages to the processor, memory and peripherals. This circuit has analog-to-digital converters on every rail and can report its own currents and voltages. The command is:
vcgencmd pmic_read_adc
The output looks like this (excerpt):
3V7_WL_SW_A current(0)=0.00224000A
3V3_SYS_A current(1)=0.31752000A
1V8_SYS_A current(2)=0.29892000A
VDD_CORE_A current(7)=2.61856000A
...
3V7_WL_SW_V volt(16)=3.69946289V
3V3_SYS_V volt(17)=3.30712891V
VDD_CORE_V volt(23)=0.82031250V
EXT5V_V volt(24)=5.09843750VEvery power rail appears twice: once with the _A suffix (the current) and once with
_V (the voltage). The power of a rail is their product, and the total power is the
sum over all rails. The following program does exactly that:
#!/usr/bin/env python3
"""Reads the power consumed directly from the Pi 5 board's power management circuit."""
import re
import subprocess
# Example line: VDD_CORE_A current(7)=2.61856000A
LINE = re.compile(r"^\s*(?P<rail>\S+?)_(?P<kind>[AV])\s+\w+\(\d+\)=(?P<val>[0-9.]+)[AV]\s*$")
def measure():
"""Returns (total_power_W, dict with the power on each rail)."""
try:
output = subprocess.run(["vcgencmd", "pmic_read_adc"],
capture_output=True, text=True, check=True).stdout
except (OSError, subprocess.CalledProcessError):
return None, {}
raw = {}
for line in output.splitlines():
m = LINE.match(line)
if m:
raw.setdefault(m.group("rail"), {})[m.group("kind")] = float(m.group("val"))
# Only rails that report both current and voltage can yield a power.
# EXT5V and BATT only report the voltage, so they are left out.
detail = {name: v["A"] * v["V"] for name, v in raw.items() if "A" in v and "V" in v}
if not detail:
return None, {} # the board has no measuring PMIC (e.g. a Pi 3)
return sum(detail.values()), detail
if __name__ == "__main__":
total, detail = measure()
if total is None:
raise SystemExit("PMIC unavailable - it only exists on a Raspberry Pi 5 (on a Pi 3 use power_ina219.py)")
for name, p in sorted(detail.items(), key=lambda x: -x[1]):
percent = 100 * p / total
bar = "#" * int(percent / 2)
print("%-12s %6.3f W %5.1f%% %s" % (name, p, percent, bar))
print("-" * 46)
print("%-12s %6.3f W" % ("TOTAL", total))VDD_CORE rail - the processor cores' supply - dominates under load and becomes
almost negligible while idle. Run the program once idle and once under stress-ng and
compare the breakdown. This is a measurement you cannot make on most development boards.9External measurement with INA219
The INA219 module measures the voltage drop across a 0.1 Ω resistor placed in series with the load and converts it into a current. On a Pi 5 we use it as a "witness" for the internal measurement, while on a Pi 3 it is the only power measurement. It communicates over I²C, so it shares the bus with the LCD.
One detail worth knowing: the adapter has pull-up resistors on SDA and SCL to its own supply, so at 5 V it pulls the Pi's lines slightly above 3.3 V. In practice it usually works, but the proper solution is an I²C level shifter between the Pi and the LCD, or removing the adapter's two pull-up resistors (the Pi has its own pull-ups to 3.3 V).
On a Raspberry Pi 3: the INA219 on the board's supply
The Pi 3 has no PMIC, so the INA219 must see all of the board's current. The simplest way, without cutting cables, is to power the board from a bench supply through the 5 V pin of the header, with the module's shunt inserted into the +5 V wire. The I²C connections (3V3, SDA, SCL, GND) remain those of Fig. 2.
2. Set the supply to 5.15-5.2 V and limit the current to 2.5 A before connecting it to the board. Do not exceed 5.25 V.
3. The 5 V pin bypasses the board's protection fuse: check the polarity twice before powering up.
4. The shunt and the wires drop the voltage by about 0.1 V at 1 A, which is why the supply is set slightly above 5 V. If
cpu_state.py reports "undervoltage", raise the supply voltage slightly, without
exceeding the limit above.Without a bench supply you can use a sacrificial micro-USB cable, with the red (+5 V) wire cut and routed through Vin+ → Vin−; the other wires stay uninterrupted. The bench-supply variant is, however, safer in the lab.
On either board, then check that the modules are seen on the bus:
i2cdetect -y 1
40 (INA219) and 27 or 3f (LCD) should appear. If nothing
appears, check the GND wire - it is the one most often forgotten.
#!/usr/bin/env python3
"""Reads voltage, current and power from an INA219 module."""
import time
import board
from adafruit_ina219 import ADCResolution, BusVoltageRange, INA219
i2c = board.I2C() # automatically uses the I2C-1 bus
sensor = INA219(i2c) # default address 0x40
# Configuration for quiet measurements: maximum resolution and averaging
# over 128 samples. Reduces noise, at the cost of a slower reading.
sensor.bus_adc_resolution = ADCResolution.ADCRES_12BIT_128S
sensor.shunt_adc_resolution = ADCResolution.ADCRES_12BIT_128S
sensor.bus_voltage_range = BusVoltageRange.RANGE_16V
def measure():
"""Returns (voltage_V, current_mA, power_W)."""
# The real voltage on the load = the bus voltage + the drop across the shunt.
voltage = sensor.bus_voltage + sensor.shunt_voltage
current = sensor.current # mA
return voltage, current, voltage * current / 1000.0
if __name__ == "__main__":
print("%8s %10s %10s" % ("V", "mA", "W"))
try:
while True:
u, i, p = measure()
print("%8.3f %10.2f %10.4f" % (u, i, p))
time.sleep(0.5)
except KeyboardInterrupt:
print("\nstopped")power_ina219.py shows the power of the whole board. Run it once
idle and once under stress-ng --cpu 4. As an order of magnitude, expect a few watts, a significant part
of which is already consumed at idle (memory, USB, network). You do not get the per-rail breakdown of the Pi 5, but
you get something the Pi 5's PMIC does not give you: the real consumption from the supply.One measurement module for both boards
So that the following programs run unchanged on both the Pi 5 and the Pi 3, we hide the measurement source
behind a small module that picks it by itself: the PMIC if it exists, the INA219 otherwise. Both variants return
the same result shape, (total_power_W, detail).
#!/usr/bin/env python3
"""Automatically picks where we read the power from:
Pi 5 -> the internal PMIC (power_pmic.py),
Pi 3 -> the INA219 module on the board's supply (power_ina219.py)."""
import power_pmic
SOURCE = None
def measure():
"""No source available: returns the same format, but empty."""
return None, {}
if power_pmic.measure()[0] is not None:
SOURCE = "PMIC"
measure = power_pmic.measure
else:
try:
import power_ina219 # initializes the sensor; fails if it is missing
power_ina219.measure() # a test reading
except Exception as e:
print("No PMIC, and the INA219 does not answer (%s)" % e)
else:
SOURCE = "INA219"
def measure():
"""Same shape as power_pmic.measure(): (total_W, detail)."""
_, _, p = power_ina219.measure()
return p, {"BOARD_5V": p}
if __name__ == "__main__":
total, _ = measure()
if total is None:
raise SystemExit("nothing to measure the power with - see sections 8 and 9")
print("source: %s power: %.3f W" % (SOURCE, total))python3 power.py # Pi 5: source PMIC Pi 3: source INA219
10The real-time display
The LCD has 32 characters. We have to choose what is worth showing - a useful exercise in itself, because in embedded systems limited resources are always the rule, not the exception.
#!/usr/bin/env python3
"""16x2 LCD display for the energy test bench."""
from RPLCD.i2c import CharLCD
# If i2cdetect shows 3f instead of 27, change the address below.
ADDRESS = 0x27
class Panel:
"""Wrapper around the LCD that does not complain if the screen is missing."""
def __init__(self, address=ADDRESS):
self.lcd = None
try:
self.lcd = CharLCD("PCF8574", address, cols=16, rows=2, auto_linebreaks=False)
self.lcd.clear()
# Custom degree symbol, in the screen's character memory.
self.lcd.create_char(0, [0b01100, 0b10010, 0b10010, 0b01100,
0b00000, 0b00000, 0b00000, 0b00000])
except Exception as e:
print("LCD unavailable (%s) - continuing in the console only" % e)
def write(self, top, bottom):
"""Writes two lines of at most 16 characters each."""
top, bottom = top[:16].ljust(16), bottom[:16].ljust(16)
if self.lcd is None:
print("\r[%s|%s]" % (top, bottom), end="", flush=True)
return
# We write at fixed positions, not with clear(): otherwise the screen flickers.
self.lcd.cursor_pos = (0, 0)
self.lcd.write_string(top)
self.lcd.cursor_pos = (1, 0)
self.lcd.write_string(bottom)
def close(self):
if self.lcd is not None:
self.lcd.clear()
self.lcd.close(clear=True)
if __name__ == "__main__":
import time
from cpu_state import frequency, temperature
from power import measure
panel = Panel()
try:
while True:
p, _ = measure()
panel.write("%4.0fMHz %4.1fW" % (frequency(), p or 0),
"temp %4.1f degC" % temperature())
time.sleep(0.5)
except KeyboardInterrupt:
panel.close()
print("\nstopped")clear() every frame
Fully clearing the screen takes a few milliseconds and leaves the display blank during that time,
which shows up as flicker. Overwriting at fixed positions, with text padded with spaces to 16
characters, gives a stable image. The same technique is used on graphical screens too, where it is
called double buffering.1.
i2cdetect -y 1 must show 27 or 3f. If it shows 3f, change
ADDRESS in display.py. If nothing shows up, check the SDA, SCL and GND wires.2. If "LCD unavailable" appears in the console, the program cannot find the display and only writes the values to the terminal.
3. Backlight on, but no characters: check that the LCD's VCC is at 5 V (pin 2), then slowly turn the blue potentiometer on the back of the adapter while the program is running.
4. A row of solid blocks: the display is powered but receives no data - the problem is on I²C (address or wires).
11The complete program
Now we put everything together. First we need a compute task that does exactly the same thing every time, regardless of the processor's frequency - otherwise the comparison makes no sense.
/* Test task: a fixed number of integer operations.
The result is printed at the end, so the compiler cannot eliminate it. */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
static uint64_t work(uint64_t n) {
uint64_t x = 88172645463325252ULL; /* xorshift64 generator */
for (uint64_t i = 0; i < n; i++) {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
}
return x;
}
int main(int argc, char **argv) {
uint64_t n = (argc > 1) ? strtoull(argv[1], NULL, 10) : 400000000ULL;
printf("%llu\n", (unsigned long long) work(n));
return 0;
}gcc -O2 -o bench bench.c time ./bench 400000000 # how long does it take? adjust the number for ~10 seconds
On a Pi 3, the same number of steps takes several times longer. Choose the value based on time,
again for roughly 10 seconds at the maximum frequency, and put it in CYCLES in bench.py.
-O2 and not -O3
With aggressive optimizations, the compiler may vectorize or even eliminate the loop, and the
"constant task" stops being constant. We come back to vectorization deliberately in Laboratory 5;
here we need a stable reference.And now the bench harness itself:
#!/usr/bin/env python3
"""Energy test bench: runs the same task under every governor
and compares the time, average power and energy consumed."""
import csv
import subprocess
import sys
import time
from display import Panel
from power import SOURCE, measure
from cpu_state import frequency, governor, throttle_flags, temperature
GOVERNORS = ["powersave", "ondemand", "schedutil", "performance"]
CYCLES = 400_000_000 # adjust to get roughly 10 seconds (less on a Pi 3)
PERIOD = 0.2 # power sampling interval, in seconds
NUM_CORES = 4
def set_governor(name):
"""Writes the same governor to every core. Requires root privileges."""
for n in range(NUM_CORES):
path = "/sys/devices/system/cpu/cpu%d/cpufreq/scaling_governor" % n
subprocess.run(["sudo", "tee", path], input=name.encode(),
stdout=subprocess.DEVNULL, check=True)
time.sleep(2) # let the governor settle
def measure_idle(seconds=5):
"""Average power with the system idle - the reference for the useful energy."""
samples = []
end = time.time() + seconds
while time.time() < end:
p, _ = measure()
if p:
samples.append(p)
time.sleep(PERIOD)
return sum(samples) / len(samples) if samples else 0.0
def run_benchmark(panel, governor_name, p_idle):
"""Runs the task and samples the power while it is working."""
set_governor(governor_name)
process = subprocess.Popen(["./bench", str(CYCLES)], stdout=subprocess.DEVNULL)
samples, frequencies = [], []
t0 = time.time()
while process.poll() is None:
p, _ = measure()
if p:
samples.append(p)
f = frequency()
if f:
frequencies.append(f)
panel.write("%-11s%4.1fW" % (governor_name[:11], p or 0),
"%4.0fMHz %4.1fC" % (f or 0, temperature()))
time.sleep(PERIOD)
duration = time.time() - t0
p_avg = sum(samples) / len(samples) if samples else 0.0
f_avg = sum(frequencies) / len(frequencies) if frequencies else 0.0
return {
"governor": governor_name,
"duration_s": round(duration, 3),
"avg_frequency_MHz": round(f_avg, 1),
"avg_power_W": round(p_avg, 3),
# Total energy consumed over the duration of the task.
"energy_J": round(p_avg * duration, 2),
# Energy above the idle consumption - the part actually "paid" for the computation.
"useful_energy_J": round((p_avg - p_idle) * duration, 2),
# How much computation we get for every joule spent.
"Mcycles_per_J": round(CYCLES / 1e6 / max(p_avg * duration, 1e-9), 2),
"temperature_C": round(temperature(), 1),
}
def main():
if SOURCE is None:
raise SystemExit("Nothing to measure the power with - see sections 8 and 9.")
print("Power measurement source: %s" % SOURCE)
# Keep only the governors the kernel actually has.
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors") as f:
available = f.read().split()
to_run = [g for g in GOVERNORS if g in available]
initial = governor()
panel = Panel()
issues = throttle_flags()
if issues:
print("WARNING - power supply or temperature:", ", ".join(issues))
print("The results will not be comparable. Continuing anyway in 5 seconds...")
time.sleep(5)
print("Measuring idle consumption...")
p_idle = measure_idle()
print("Idle: %.3f W\n" % p_idle)
results = []
try:
for g in to_run:
print("Running with governor %s..." % g)
r = run_benchmark(panel, g, p_idle)
results.append(r)
print(" %5.2f s %5.2f W %7.1f J %6.0f MHz"
% (r["duration_s"], r["avg_power_W"], r["energy_J"],
r["avg_frequency_MHz"]))
finally:
set_governor(initial) # leave the system as we found it
panel.close()
with open("results.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(results[0].keys()))
writer.writeheader()
writer.writerows(results)
print("\n%-12s %8s %8s %9s %10s" % ("governor", "duration", "power", "energy", "Mcycles/J"))
for r in results:
print("%-12s %7.2fs %7.2fW %8.1fJ %9.2f"
% (r["governor"], r["duration_s"], r["avg_power_W"],
r["energy_J"], r["Mcycles_per_J"]))
fastest = min(results, key=lambda r: r["duration_s"])
most_efficient = min(results, key=lambda r: r["energy_J"])
print("\nfastest : %s" % fastest["governor"])
print("most efficient : %s" % most_efficient["governor"])
if fastest["governor"] == most_efficient["governor"]:
print("-> Same winner: 'race to idle' works on this platform.")
else:
print("-> Different winners: there is a trade-off between speed and energy.")
if __name__ == "__main__":
sys.exit(main())cd ~/si-lab/lab01 source ~/si-lab/bin/activate python3 bench.py
12The experiment and its interpretation
On a Raspberry Pi 5, a typical results table looks something like this (your values will differ depending on cooling, power supply and firmware version):
| Governor | Duration | Average frequency | Average power | Energy | Mcycles/J |
|---|---|---|---|---|---|
powersave | 16.8 s | 1500 MHz | 4.1 W | 68.9 J | 5.81 |
ondemand | 10.6 s | 2380 MHz | 6.5 W | 68.9 J | 5.81 |
schedutil | 10.5 s | 2390 MHz | 6.6 W | 69.3 J | 5.77 |
performance | 10.5 s | 2400 MHz | 6.6 W | 69.3 J | 5.77 |
powersave runs at 600 MHz, while
ondemand, schedutil and performance all climb to the maximum frequency and
give almost identical results. The important difference from the Pi 5 is what you are measuring: the INA219 sees
the whole board, including the idle consumption of the memory, the USB port and the network. In the terms of
the explorer in section 5, the "system power" is high, and the theory predicts that powersave will
consume more total energy, even though its power is lower. Check whether the measurement confirms the
prediction, and compare the useful energy too, not just the total.What you must compare in the report
- Duration - how quickly the task finished
- Total energy - what it actually cost the battery
- Useful energy (above the idle consumption) - what was paid for the computation itself, without the surrounding infrastructure
- Mcycles/J - the efficiency, i.e. performance per watt
- Final temperature - to check whether thermal throttling occurred
How to check that the measurement is valid
- Run each configuration three times; if the results differ by more than 5%, something is varying uncontrollably
- Let the board cool down between runs (check with
vcgencmd measure_temp) - Close the graphical interface:
sudo systemctl isolate multi-user.target - Check
vcgencmd get_throttledafter every run - it must return0x0 - On a Pi 5: compare the power from the PMIC with the one from the INA219; the constant difference is the loss in the regulators
- On a Pi 3:
vcgencmd get_throttledmust stay0x0with the INA219 wired in too; if undervoltage appears, the shunt and the wires drop the voltage too much (see the rules in section 9)
13Assignments
- Run
cpu_state.pyidle and under load. Note the frequencies of the four cores in both situations and explain why they are not necessarily equal. - Run
power_pmic.pyidle and understress-ng --cpu 4. Build a table with the power of each supply rail in both cases and calculate what percentage of the total increase comes fromVDD_CORE.
On a Pi 3: runpower_ina219.pyin the same two situations. You do not have the per-rail breakdown, so calculate the increase in the whole board's power and note, withvcgencmd measure_volts core, how the core voltage changes. - Wire up the LCD and run
display.py. Modify the program so the second line alternates every two seconds between the temperature and the energy accumulated since startup. - Run the complete bench and fill in the table from the previous section with your own values.
- Based on the CSV file, plot two charts: energy versus governor and efficiency (Mcycles/J) versus governor. Comment on whether the two lead to the same conclusion.
- Manually fix the maximum frequency at 1500, 1800, 2100 and 2400 MHz and repeat the measurement:
Plot the energy-versus-frequency curve and compare its shape with the one from the DVFS explorer in section 5.fixing the frequency
echo 1800000 | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq
On a Pi 3 there are only two steps, so the curve has two points: fix600000and1200000(1400000on the B+) and compare the two energies. Discuss what a two-point curve can tell you and what it cannot.
14Deeper-dive challenge
scaling_max_freq by one step; if it stays below the budget
for more than three seconds, it raises it back by one step. Display the budget, the current power
and the frequency limit in effect on the LCD.
This is, in essence, exactly what server processors do under the name RAPL power capping. The question you must answer in the report: how fast must the loop react so the system does not oscillate? Try periods of 0.1 s, 1 s and 5 s and describe the behavior in each case.
On a Pi 3, with only two steps, the loop can only switch between 600 MHz and the maximum frequency. Choose a budget between the idle power and the full-load power and compare the oscillation with that on the Pi 5.
15Self-check questions
16Resources
- Chapter 4 - Energy consumption in embedded systems
- Chapter 5 - Techniques for optimizing energy consumption
- Chapter 6 - Software power management
- Official Raspberry Pi documentation - vcgencmd and board configuration
- Linux kernel documentation - the cpufreq subsystem and its governors
- Texas Instruments - INA219 product page and datasheet