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 "embeddedci[pytest]"    # Python 3.10+; the [pytest] extra installs pytest too

Flashing 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_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).
  • 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 board

Then 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.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 (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 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