This is the fast path from nothing to a passing hardware-in-the-loop test: install the library, wire one board to a BenchPod, and write a test that flashes firmware and checks it boots. If you've written a pytest test before, this will feel familiar — the hardware is just behind a fixture.
1. Install
pip install "embeddedci[pytest]" # Python 3.10+; the [pytest] extra installs pytest tooFlashing shells out to OpenOCD, which needs a build with the cmsis_dap_tcp backend — newer than the stock 0.12.0 packages. On macOS that's brew install --HEAD open-ocd; on Linux the xPack OpenOCD 0.12.0-7 snapshot has it. The pytest framework docs explain why, and the troubleshooting page covers the error you'll see if it's the wrong version.
2. Wire the target
The pod has no dedicated SWD/UART pins — it exposes 12 identical logic-analyzer channels (pins.pin_1 … pins.pin_12), and any DUT signal can be wired to any of them. So you map your bench's wiring once, at the top of the test, instead of relying on role-named pins:
- SWCLK / SWDIO — the two SWD lines for flashing.
- UART — the pod samples the DUT's TX on the rx channel and drives the DUT's RX on the tx channel.
- Target power — the eFuse rail that feeds the board (
--benchpod-efuse; 1 = internal 5V). - Logic level — the LA bank runs at 1.8 V or 3.3 V; you set the one your board uses once, in
conftest.py(next step).
Pull-ups are available on LA1-6 only (LA1/2 = 4.7k, LA3/4 = 2.2k, LA5/6 = 10k); LA7/8 carry 10k pull-downs and LA9-12 have nothing — so put an open-drain bus (like I2C) on LA1-6.
3. Write the test
First, a conftest.py next to the test declares the board's I/O voltage — once, for the whole suite. The pod refuses to flash or open the UART until its LA bank voltage is set, and the benchpod fixture selects this value every time it connects:
# conftest.py
import pytest
@pytest.fixture(scope="session")
def benchpod_la_voltage():
return 3.3 # the board's I/O voltage — change to 1.8 for a 1V8 boardThen the test itself:
import pytest
from types import SimpleNamespace
@pytest.fixture
def wiring(pins):
# This bench's wiring: DUT signal -> BenchPod LA channel. Edit for your board.
return SimpleNamespace(
swclk=pins.pin_11, swdio=pins.pin_12,
uart_rx=pins.pin_5, uart_tx=pins.pin_4, # pod samples DUT TX / drives DUT RX
efuse=pins.efuse,
)
@pytest.mark.hardware
def test_firmware_boots(benchpod, wiring, firmware):
# Flash the firmware over SWD and power the target from the eFuse.
assert benchpod.flash(
file=firmware, target="target/stm32f4x.cfg",
swclk=wiring.swclk, swdio=wiring.swdio,
target_power=wiring.efuse,
).ok
# Power-cycle and assert the boot banner shows up.
benchpod.power_off(wiring.efuse)
benchpod.power_on(wiring.efuse, delay=1.5)
with benchpod.open_uart(rx=wiring.uart_rx, tx=wiring.uart_tx) as uart:
assert uart.read_until(r"APP_OK", timeout=12), uart.textbenchpod, pins, and firmware are fixtures the library registers for you — a connected pod, the pod's LA channels + eFuse, and the firmware path. The wiring fixture is your own: it names which channel each signal is on.
4. Run it
pytest test_firmware.py \
--benchpod-connection=192.168.1.213 \
--benchpod-firmware=build/app.elfPoint --benchpod-connection at the pod's IP, a serial port like /dev/ttyACM0, or embeddedci:<device-name> for a pod in the cloud (install "embeddedci[cloud]" and set BENCHPOD_API_KEY to an API key, or run from GitHub Actions with OIDC). That's a passing HIL test — firmware flashed to real silicon and verified by its own boot output.
It stays green without hardware
Leave the connection off and the benchpod fixture skips instead of failing:
pytest test_firmware.py # no --benchpod-connection → test skips, suite stays greenSo the same test sits happily in a suite that runs on laptops and plain CI runners, and only does real work when a pod is actually wired up.
Where to go next
- Make the pod emulate a sensor and assert on the I2C bus.
- Run this exact test on real hardware from GitHub Actions.
- Drive the bench from an AI agent with the MCP server.