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

Concepts for Hardware Platforms, Not Vtables

Part 4 was about time discipline. Part 5 is about architecture discipline: separating core logic from hardware glue without pulling runtime polymorphism, hidden allocation, or preprocessor sprawl into your hot paths.

The idea is straightforward: define a small compile-time contract for “platform” (and optionally “comms”), then write your control logic against that contract. You get clean boundaries, host-testable code, and zero runtime dispatch.

The boundary: core logic vs platform glue

A deterministic firmware architecture usually has three layers:

  • Core logic: control policy, safety logic, state handling, scheduling decisions. No vendor headers.
  • Platform layer: GPIO/ADC/timers/UART/DMA/watchdog, board init, vendor SDK integration.
  • Host tests/tools: unit tests, simulators, log parsers, visualizers.

If vendor SDK headers leak into core code, the boundary is already broken. Fix that first. It only gets harder later.

Why vtables are the wrong default

Virtual dispatch is not inherently slow, but it is a poor default in firmware because it encourages runtime mechanisms in places you want compile-time certainty:

  • Increases architectural drift: “just one virtual interface” becomes “vtables everywhere.”
  • Makes it easier to accidentally depend on RTTI or other runtime machinery.
  • Pushes errors later: concepts fail at compile time, not in integration testing.

If your Part 3 rules include “no virtual dispatch in hot paths,” concepts are the direct replacement.

Concepts as a compile-time platform contract

A concept is a named set of requirements. Keep it narrow: only require what the core actually uses.

#include <concepts>
#include <chrono>

struct Inputs final {
    int adc0{0};
    bool estop{false};
};

struct Outputs final {
    bool pump{false};
    unsigned pwm{0U};
};

template <typename T>
concept HardwarePlatform =
    requires(T p, const Outputs& out) {
        { p.initialize() } noexcept -> std::same_as<bool>;
        { p.read_inputs() } noexcept -> std::same_as<Inputs>;
        { p.write_outputs(out) } noexcept -> std::same_as<void>;
        { p.now() } noexcept -> std::same_as<std::chrono::milliseconds>;
        { p.is_healthy() } noexcept -> std::same_as<bool>;
    };

template <HardwarePlatform P>
void tick(P& platform) noexcept {
    const Inputs in = platform.read_inputs();
    Outputs out{};

    // Core logic decides outputs based on inputs and time.
    // No vendor SDK calls here.

    platform.write_outputs(out);
}

Key points:

  • No vtable, no runtime dispatch, no allocation implied.
  • noexcept is enforced at compile time for the required operations.
  • A mock platform can satisfy the same concept for host testing.

Keeping the core clean without ifdef sprawl

The usual failure mode is conditional compilation everywhere. Instead:

  • Build different firmware targets that instantiate the core with different platform types.
  • Keep the core translation units identical across targets.
  • Put vendor SDK code in platform translation units only.

This approach scales better when you add a new board, and it makes CI much simpler.

Tooling support: enforce the boundary

Your pipeline should make boundary violations fail fast:

  • Target builds with exceptions and RTTI disabled.
  • Static analysis rules that flag vendor includes in core directories.
  • Checks that reject virtual methods or forbidden headers in hot-path modules.
  • Separate host and target builds so host-only dependencies never link into firmware.

Unsolicited advice: add a “core purity” gate early. Once platform dependencies creep into core, removing them becomes a rewrite.

Minimal checklist

  • Define a narrow HardwarePlatform concept for what core actually needs.
  • Require noexcept in the concept for hot-path calls.
  • Write core logic against the concept (templates), not against virtual interfaces.
  • Keep vendor SDK headers out of core code.
  • Enforce boundaries in CI (compile flags, include restrictions, static analysis).

Next is Part 6: memory discipline and binary hygiene. Concepts keep the architecture clean. Memory rules keep it deterministic under load.

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