The board is data.
The compiler is the validator.

Describe your hardware as constexpr tables. Firn's consteval engine joins every cross-reference, rejects every inconsistency with an error that names the offender, and C++26 reflection generates your typed, zero-overhead device API from the same tables. No code generator. No DSL. Nothing to drift.

C++26 reflection (P2996) — GCC 16 trunk today

AYou write data
inline constexpr Pin kPins[] = {
  { "LedRed",  "P0", 13,
    Direction::OUT, Signal::DIGITAL,
    true, Pull::NONE },
  { "Button3", "P0", 11,
    Direction::IN, Signal::DIGITAL,
    false, Pull::UP,
    0, 1, 0.0, 1.0, "ButtonTopic" },
};

inline constexpr Sensor kSensors[] = {
  { "AmbientLight", "I2C_7", 0x55 },
};  // bus is a keyref
BReflection generates the API — typed, zero overhead
dev.LedRed.write(true);
// active-low handled; one store

double lux = dev.Light.getPhysical();

dev.AmbientLight.lux();
// bus resolved at compile time

dev.Button3.write(true);
// ERROR: write() requires dir != IN
CAnd a wrong board? It doesn't compile
tests/fail/unknown_bus.cpp:8:23:
error: static assertion failed:
  sensor 'AmbientLight' references
  unknown bus 'I2C_9'

  8 | static_assert(kDiag.ok(), kDiag);
    |               ~~~~~~~~^~

Panel C is verbatim g++-16 output. That's a real compiler message. That's the whole idea. A wrong board doesn't boot wrong — it doesn't compile.

$ g++ -std=c++26 board.cpp → board valid — firn::dev generated, zero overhead

Why it holds

Three claims, each with a checked artifact behind it

Validated by the compiler

Cross-references between tables — a sensor naming its bus, an encoder naming its chain bits, an LCD role naming its pin — are consteval joins. Uniqueness, pin conflicts, capacities, orphaned encoder phases: all checked before anything runs. A violation is a compile error that names the offender.

21 compile-fail regression tests, every one a named-offender diagnostic

Generated by reflection

C++26 define_aggregate walks the tables and emits a struct whose member names come from the data: dev.LedRed, dev.AmbientLight. Add a row and a typed handle appears — no hand-written handle list can drift.

live run: the whole device surface from one board.hpp

Zero overhead, proven

The reflection-generated C→C++ ISR bridge thunk compiles to the same instructions as a hand-written thunk and as a direct call. The test suite diffs the disassembly instruction-for-instruction on every run.

asm diff in CI: reflection == hand-written == direct

Try it

Break the board. The compiler catches it.

Click a highlighted value to sabotage the topology — and watch the build answer. Every diagnostic below is the verbatim message from Firn's compile-fail regression suite; fix the board and the generated firn::dev surface comes back.

board.hppclick a highlighted value
inline constexpr Pin kPins[] = {
    { "LedRed",   "P0", 13,
      Direction::OUT, /*active-low*/ },
    { "LedGreen", , 0,
      Direction::OUT, /*active-low*/ },
    { "Button3",  "P0", ,
      Direction::IN, Pull::UP },
};

inline constexpr Bus kBuses[] = {
    { "I2C_7", 7, 400 },
};

inline constexpr Sensor kSensors[] = {
    { "AmbientLight", , 0x55 },
};
$the compiler answers

The squiggle marks what you broke; the message names it. Wrong usage fails the same way — writing to an input pin or reading analog from a digital pin is a compile error too. .

Zero overhead, verbatim

The assembly is the argument

A vendor C driver's interrupt calls a typed C++ method through firn::bridge::Thunk — one cast, zero cost. From the test suite's disassembly diff (all -O2):

── reflection thunk  Gpio<0x500>::set ──
      0:  endbr64
      4:  movl   $0x501,0x0(%rip)
      e:  ret
── hand-written thunk ──
      0:  endbr64
      4:  movl   $0x501,0x0(%rip)
      e:  ret
── direct dev.set() ──
     10:  endbr64
     14:  movl   $0x501,0x0(%rip)
     1e:  ret

=> reflection thunk == hand-written == direct; bridge adds 0 instructions.

Everything consteval folds to constants; none of it reaches flash. Proven on a Cortex-M0+ with a GCC-16 arm-none-eabi cross — see the case study.

Lineage

Twenty-five years in the making. Compiled at last.

One idea — describe the hardware as data, let a machine turn it into typed, validated handles — carried by one person since 2001.

2003 — ACT I

Reactor (GekkoWare)

hwConf.xml + XSD keyref integrity on PREEMPT_RT Linux. The rigor — but it needed a PC. Still in production in 2026.

2017 — ACT II

SnowFlake

peripherals.json + Python codegen on Cortex-M and µOS++. Reached the microcontroller — but traded the rigor away.

2026 — ACT III

Firn

constexpr tables, consteval joins, reflection-generated handles. The rigor, the binding, and the footprint — at once.

Read the story →