Architecture
Zenbedded is intentionally small, but the pieces are layered to make the firmware portable across boards while honouring our core promises: control flow is explicit, performance decisions are justified, and the developer experience stays welcoming.
Compile-Time Assembly
src/firmware/main.zig
│
├── imports console backend based on build_options.board
├── pulls in the Cortex-M startup module for reset handling
└── emits a freestanding binary tailored to the selected board
build.zigexposes a-Dboard=<name>switch. During compilation we resolve the target triple, CPU model, and linker script for that board and wire in build options explicitly.- The
build_optionsmodule injects the board name so thatsrc/board/console.zigcan select the appropriate backend atcomptime, keeping branching centralized.
Startup & Linker Scripts
Every board reuses the same resetHandler defined in src/hal/cortex_m/startup.zig. Per-board customization lives in the linker script under src/bsp/<board>/memory.ld. Those files:
- place the vector table at the correct flash offset,
- define
.data,.bss, and stack segments with explicit bounds, - expose
_stack_top,_data_start, etc., which the startup routine references.
Because the handler is written in Zig, we can reason about it with the same tools we use elsewhere, add assertions to guard invariants, and avoid inline assembly unless absolutely necessary.
Board Console Abstraction
The console interface is a thin layer that exposes init, write, and idle. Each backend configures clocks, GPIO, and UART registers using volatile pointers but hides those differences behind a uniform API. The parent module orchestrates control flow; the leaf modules remain pure computations or side-effect blocks. This structure is the seed for broader driver categories (timers, GPIO, storage) that will follow the same pattern.
Peripherals & Drivers
src/board/spi.zigdefines an explicit SPI contract: configuration validation, bounded transfers (MAX_TRANSFER_SIZE), and a vtable-based implementation so boards can provide their own transport.src/board/spi/test_double.zigsupplies a recording test double used by unit tests to assert exact byte sequences without hardware.src/board/pin.zigmodels digital output pins (set/clear/toggle) with a companion test double.src/drivers/st7789.zigbuilds on those contracts to implement a display driver. Command tables are compile-time constants, the state machine is explicit, and drawing routines honour the SPI transfer bounds.
Renode Integration
Instead of depending on the global Renode installation for platform descriptions, the repository vendors minimal .repl files under renode/platforms/. This gives us complete control over available peripherals and allows CI to run Renode with deterministic behaviour. The .mise scripts load these local descriptions, apply any stubs needed to silence warnings, and write UART output to build/renode/*.log, which our GitHub Actions workflow archives for inspection.
Documentation & Tooling
The mdBook you are reading mirrors the source tree and refers back to the specification in SPEC.md. It is part of the definition of done: update it whenever behaviour changes. We ship the Ayu theme by default to match the Zig aesthetic and keep the documentation pleasant to browse in both light and dark modes.