Back to resources
HIL
Testing

Your First HIL Test in 15 Minutes

From pip install to a green hardware-in-the-loop test: wire a board to a BenchPod, flash it, and assert on its boot output — the whole loop, start to finish.

Edward Viaene · June 7, 2026 · 3 min read

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 pytest embeddedci

Flashing shells out to OpenOCD, which needs to be a recent build (the stock 0.12.0 package is too old). The quickest route is xPack OpenOCD: npm install -g @xpack-dev-tools/openocd. 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_1pins.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).

Pull-ups are available on LA1-8 only (LA1/2 = 4.7k, LA3/4 = 2.2k, LA5-8 = 10k); LA9-12 have none — so put an open-drain bus (like I2C) on a pull-up-capable channel.

3. Write the test

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.text

benchpod, 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.elf

Point --benchpod-connection at the pod's IP, a serial port like /dev/ttyACM0, or embeddedci:<device-name> for a pod in the cloud. 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 green

So 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