Get started
Requirements first, honestly
Firn makes an early bet on C++26 reflection — the same kind of bet that put C++17 on Cortex-M before the field thought it reasonable. Here is exactly what that costs you today.
Requirements — stated plainly
- g++-16 (trunk) with
-freflectionfor the reflective parts. P2996 is C++26; no release compiler ships it yet. Ubuntu:apt install g++-16where available. - The non-reflective parts — schema primitives, consteval joins,
validation, the generic drivers,
firn::Editable, pub/sub — compile from C++20/23. A consuming firmware can mix translation units: the case study compiles its RTOS TUs at gnu++17/23 and only the reflective TUs at c++26. - Header-only. Firmware-flag friendly: builds under
-Os -fno-exceptions -fno-rtti(compile-time errors do not usethrow). This is by nature, not by choice —constevalruns in the compiler, so "read-only headers" is the library boundary. - For ARM cross builds: no official GCC-16
arm-none-eabiexists yet; building one is documented (no sudo, local prefix) and proven on Cortex-M0+.
Consume via xpm (the xPack ecosystem, same as µOS++)
// package.json of your firmware project
{
"xpack": {
"dependencies": {
"@firnware/firn": "…", // registry or git URL once published
"@micro-os-plus/micro-os-plus-iii": "github:micro-os-plus/micro-os-plus-iii#v7.1.0"
}
}
}
xpm install
g++ -std=c++26 -freflection -I xpacks/@firnware/firn/include ...
#include <firn/topology.hpp>
Or plain: clone, add -I firn/include, done. There is nothing
to build.
The 60-second taste (works today, host-only)
git clone https://github.com/henris42/firn && cd firn
tests/check.sh # run tests + asm proof + compile-fail diagnostics
examples/build.sh # the demos: generated boards, dynamic device tree, bridge
examples/build/genboard
What genboard prints — a real run, showing the device surface
generated from board.hpp, the single authored file:
[hw] cfg pin 13 OUT
[hw] cfg pin 32 OUT
[hw] cfg pin 11 IN pull-up
[hw] cfg pin 4 IN
[hw] i2c7 init @ 400 kHz
== firn::dev generated from board.hpp (the single authored file) ==
dev.LedRed.pin = 13
dev.LedGreen.pin = 32
dev.Button3.pin = 11
dev.Light.pin = 4
dev.I2C_7.index = 7 (generated bus handle)
dev.AmbientLight = addr 0x55 on i2c7 (generated sensor handle)
dev.LedRed.write(true) -> hw line 0 (active-low)
dev.Light.getPhysical() = 500.0 lux (analog handle, also generated)
[event] Button3 edge via GENERATED handle: pin 11, level 1
No handle was written by hand -- add a row to board.hpp and a new
dev.* handle appears, typed, validated, and topic-wired. The table
is the only artifact.
What a board file looks like
The one file you author. Ports and buses are physical devices; pins and sensors are logical channels that reference a device by name — a keyref the compiler enforces.
#include <firn/topology.hpp>
namespace firn {
inline constexpr Port kPorts[] = {
{ "P0", 0 },
{ "P1", 32 },
};
inline constexpr Pin kPins[] = {
{ "LedRed", "P0", 13, Direction::OUT, Signal::DIGITAL, true, Pull::NONE },
{ "LedGreen", "P1", 0, Direction::OUT, Signal::DIGITAL, true, Pull::NONE },
{ "Button3", "P0", 11, Direction::IN, Signal::DIGITAL, false, Pull::UP,
0, 1, 0.0, 1.0, "ButtonTopic" }, // publishes edge events
{ "Light", "P0", 4, Direction::IN, Signal::ANALOG, false, Pull::NONE,
0, 65535, 0.0, 1000.0 }, // 0..65535 counts -> 0..1000 lux
};
inline constexpr Bus kBuses[] = {
{ "I2C_7", 7, 400 },
};
inline constexpr Sensor kSensors[] = {
{ "AmbientLight", "I2C_7", 0x55 }, // bus -> I2C_7
};
} // namespace firn
Binding your silicon
Firn's generic peripheral drivers — shift-register chains (74HC165/595),
quadrature decoding, mux'd pot scanning with IIR + deadband, HD44780 LCDs,
LED banks, SPI/I2C buses — are written once, board-independent. Config arrives
as compile-time specs joined from your topology; your silicon arrives as a
tiny static-function Io policy:
// The ENTIRE project-side cost of Firn's generic drivers:
struct Io {
static void gpio_write(firn::PinRef p, bool v) { hal::gpio_write(p, v); }
static std::uint8_t spi_xfer(std::uint32_t i, std::uint8_t b)
{ return hal::spi_xfer(i, b); }
// delay_us, adc_read, mux_select ...
};
using Chain = firn::ShiftReg<firn::SpiBus<kSpiSr, Io>, Io, kSrLoad, kSrRclk>;
using EncoderBank = firn::QuadDecoder<kEncSpecs>; // masks joined from the board
using Lcd = firn::Hd44780<Io>; // role pins joined from the board
Running on µOS++
The case-study firmware documents everything a CMSIS-startup + µOS++ build
needs, each one a hard-won fact: working .init_array linker
sections, the interrupt-stack watermark fill, memory-resource setup on cross
builds, thread objects in static storage, ≥1 KB thread stacks. A
"Running on µOS++" docs page will collect these as the material is adapted for
publication.
@firnware/firn (xpm / npm, or plain -I include).
Status: the framework is real and regression-tested; the
public repository and package registry entry are being prepared — until then,
the case study is the proof it runs.