Back to resources
HIL
CI
Testing

Orchestrating HIL Tests in CI

From a first UART-driven test on the bench to self-hosted CI runners flashing real hardware on every push.

Edward Viaene · April 21, 2026 · 3 min read

Once the rig has power control, console access, and a way to provision the DUT, the next step is writing tests against it.

A First Test

The easiest place to start is to trigger a real event through a real interface and watch what the firmware does.

UART is the lowest-friction option:

import time
import serial

def run_start_test(port="/dev/ttyUSB0", baudrate=115200, timeout=5):
    with serial.Serial(port, baudrate=baudrate, timeout=0.1) as ser:
        ser.reset_input_buffer()
        ser.write(b"START\n")
        ser.flush()

        deadline = time.time() + timeout
        while time.time() < deadline:
            line = ser.readline().decode("utf-8", errors="replace").strip()
            if not line:
                continue
            if "OK" in line:
                return True
            if "ERROR" in line:
                return False

    raise TimeoutError("DUT did not respond within timeout")

As the rig matures, tests can drive real-world sequences: a button held for 500 ms, a sensor crossing a threshold, a bus going silent for three seconds, or a device rebooting mid-transaction. These are hard to reproduce reliably by hand, and nearly impossible to cover with CPU-only simulation.

Wiring Tests into CI

Once the tests pass locally, the next step is to run them on every change. The simplest setup is a self-hosted CI runner on the management node. GitHub Actions, GitLab Runner, and most other CI platforms support that model.

A push triggers a workflow that builds the firmware, flashes or netboots the DUT, runs the test suite, captures logs, and reports the results back to the PR.

What You Outgrow

A breadboard prototype with a Pi, a relay board, a USB UART hub, and a small MCU as a signal router gets you a long way, and you can put it together in a few days. It does have limits you'll hit sooner or later:

  • No electrical protection if you're relying on relays
  • No power measurement
  • Custom wiring per target device
  • No protection against bus contention
  • Limited scaling once you have many boards and many users
  • A growing share of your time spent testing the test setup itself

The next iteration is usually a proper PCB power board paired with a dedicated signal router. The breadboard prototype is what de-risks that PCB and tells you exactly what's going to break, before the hardware becomes expensive to change.

Reference Hardware

A representative parts list for the prototype:

ComponentExampleNotes
Management nodeRaspberry Pi 4Any Linux SBC can work
Relay4 or 8 port relay boardCheap hard on/off control
USB UART hubCH9344 or CH348Stable names need udev rules
Signal router MCUPico, Pico 2, STM32, ESP32Choose what your team can maintain
Debug probePico CMSIS-DAPGood enough for many MCU targets
Logic analyzerGeneric USB analyzerUseful from day one
Pull-up resistors4.7k ohmCommon I2C starting point
Managed switchVLAN-capableNeeded for isolated netboot