Modern C++ Firmware: Proven Strategies for Tiny, Critical Systems (Part 7/10)

Test the Firmware Without the Board: Host First Strategy

Parts 3 through 6 established the foundation: deterministic rules, time discipline, clean platform boundaries, and a memory policy that survives CI. Part 7 is where you get the payoff.

A host-first strategy means the majority of your firmware logic is testable on a developer machine, quickly, repeatedly, and in CI, without physical hardware in the loop. Hardware still matters, but hardware becomes the final verification step, not the primary debugging environment.

The result is fewer late surprises, faster iteration, and less “it only happens on the bench” drama.

The Test boundary: what you test on host vs on hardware

A simple split works well:

  • Host tests: core logic, scheduling decisions, state behavior, safety logic, protocol parsing/encoding, error handling, overflow behavior, timing slip behavior.
  • Hardware tests: electrical characteristics, real ISR behavior, DMA interactions, pin mux mistakes, analog edge cases, clock tolerances.

If a behavior can be tested without the board, it should be. Save the board for the problems the board is uniquely qualified to reveal.

Host-first architecture recap

A host-first strategy is enabled by the previous parts:

  • Part 4 chrono-based time interfaces make scheduling testable.
  • Part 5 concepts keep hardware glue out of core logic.
  • Part 6 fixed-capacity memory keeps the core deterministic and portable.

The practical rule:

  • The core compiles and runs on x86_64 as a normal executable.
  • The target firmware links the same core library, plus platform code.

If you cannot build the core on a host, fix the architecture before writing more tests.

Mock platform: satisfy the same contract as real hardware

Your core should depend on a narrow platform contract (concept or interface boundary). The host test implementation of that contract is a mock platform.

Mock platforms should do three things well:

  • Feed deterministic inputs to the core.
  • Capture outputs produced by the core.
  • Provide controllable time.

The mock is not “fake hardware.” It is a deterministic test harness.

Scenario-driven tests: inputs over time, outputs and state as assertions

The fastest way to test embedded control logic is scenario-driven tests: provide a sequence of inputs over time, run ticks, then assert on state transitions, output sequences, counters, and fault behavior.

This pattern is more valuable than “unit test every function” because it tests the integration of logic as the firmware actually runs.

A good scenario test answers questions like:

  • Does the system reach the expected state given a plausible input sequence?
  • What outputs were produced and when?
  • What happens if an input goes out of range for three ticks?
  • What happens if the platform reports unhealthy?
  • Do overflow counters increment under stress, and is behavior still safe?

One tight test example: a scenario test loop

Below is a minimal sketch. It is intentionally generic. The point is the structure: time-controlled ticks, deterministic inputs, assertions on behavior.

#include <gtest/gtest.h>
#include <chrono>
using namespace std::chrono_literals;

TEST(Scenario, NominalStartSequence) {
    MockPlatform platform;
    Controller<MockPlatform> controller(platform);

    platform.set_scenario({
        Inputs{.adc0 = 100, .estop = false},
        Inputs{.adc0 = 120, .estop = false},
        Inputs{.adc0 = 140, .estop = false},
        // ...
    });

    for(int i = 0; i < 200; ++i) {
        controller.tick();
        platform.advance(10ms);  // 100 Hz loop
    }

    EXPECT_TRUE(controller.is_in_expected_state());
    EXPECT_EQ(platform.outputs().size(), 200U);
    EXPECT_EQ(platform.overflow_count(), 0U);
}

What matters:

  • You control time explicitly.
  • You can run 200 ticks in milliseconds on a host.
  • You can inspect outputs, counters, and state deterministically.

Unsolicited advice: treat “output history” as a first-class test artifact. It is often more revealing than a single final state.

Fault injection: the real power of host tests

Host tests become valuable when you lean into fault injection:

  • Force a sensor to saturate.
  • Toggle an interlock.
  • Drop an input update for N ticks.
  • Force platform health false for a window.
  • Inject invalid command frames.

On hardware, injecting these faults is slow and often unsafe. On host, it is trivial.

This is how you validate deterministic failure modes: safe outputs, controlled state transitions, counters incrementing, and no undefined behavior.

Sanitizers and coverage: why host builds outperform target debugging

Target debuggers are useful, but they are not a substitute for the tooling available on a host:

  • AddressSanitizer and UndefinedBehaviorSanitizer catch classes of bugs that are brutal on real hardware.
  • Coverage makes it obvious what logic is never exercised.
  • Static analysis is faster when you can build the core without vendor SDK friction.

This is the simplest way to remove “latent bugs” from firmware before you ever flash a board.

Unsolicited advice: if you only run sanitizers on “some builds,” you are treating memory safety as optional. Make them part of CI.

GitLab CI: make host tests the default gate

A host-first strategy is only real if CI enforces it.

Minimum CI expectations:

  • Host build and test jobs must run on every merge request.
  • Coverage thresholds should be enforced for core logic.
  • Sanitizer jobs should be required for merge (at least ASAN and UBSAN on host).
  • The target build still runs, but it should not be the first time logic is exercised.

This flips the traditional embedded workflow. Instead of “flash early and often,” the workflow becomes “kill bugs on host, then validate on hardware.”

What host-first does not replace

Host-first testing is not a claim that hardware does not matter. It is a claim about sequence and leverage.

Hardware is still needed for:

  • Electrical behavior and analog characteristics.
  • Real ISR timing and DMA interactions.
  • Pin mux and board support issues.
  • EMI, brownout behavior, clock tolerance edge cases.

Host-first reduces the number of times you need hardware to answer a question, and it raises the quality of the firmware that reaches hardware testing.

Minimal checklist

  • Core logic builds and runs on host without vendor headers.
  • A mock platform provides deterministic inputs, time control, and output capture.
  • Scenario tests exercise behavior over time, not just individual functions.
  • Host CI runs unit tests, sanitizers, and coverage on every merge request.
  • Hardware testing is reserved for hardware-specific truth, not basic logic validation.

Part 8 will cover the next layer: hardware-in-the-loop testing with Python and simple protocols. That is how you script the unavoidable hardware checks without turning your bench into a manual test ritual.

The Complete “Modern C++ Firmware” Series:


Need professional firmware development help? Engage with Polyrhythm!


Discover more from John Farrier

Subscribe to get the latest posts sent to your email.

Leave a Reply

Discover more from John Farrier

Subscribe now to keep reading and get access to the full archive.

Continue reading