Skip to content

Hardware porting

A port is a set of C files that implement the functions declared in hal/hal.h for one specific platform. The build system selects the correct port directory based on TARGET; there is exactly one port per build and no runtime fallback. Adding a target never requires touching shared code.

A port is conformant if and only if it:

  1. implements all the functions in the HAL API — none may be a silent no-op without explicit documentation;
  2. introduces no unapproved dependencies (no vendor SDK, except where architecturally unavoidable, e.g. ESP-IDF for the esp32 target);
  3. uses no dynamic allocation (malloc/calloc/realloc are forbidden);
  4. modifies no files in hal/ outside its own hal/<target>/ directory;
  5. exposes no port header to application code — internal headers (e.g. sim_bus.h) are private to the port directory.

The POSIX port is always complete, always tested in CI, and never gated on the physical hardware you happen to have. It is the reference against which every other port is measured.

The STM32L4 port (and future MCU ports) follows a strict rule for each new peripheral, so CI never breaks for lack of an MCU implementation:

  1. Compiling stub in the same PR. When a peripheral is added to the HAL and implemented on POSIX, the STM32L4 port gets a stub that compiles cleanly but does nothing (or returns a neutral value). CI stays green.
  2. Real implementation as a deferred issue. The actual STM32 driver (registers, RCC clocks, polling) is tracked as a feature + hal + deferred issue, planned for when physical hardware is available.
  3. Hardware tests as a deferred issue. Tests on a physical Nucleo are tracked separately as a test + hal + deferred issue. They are never a prerequisite for merging.

Target board: STM32L452RE / Nucleo-L452RE, direct register access, no STM32Cube.

PeripheralStatus
GPIO✅ implemented (MODER, BSRR, IDR, ODR)
Timer✅ implemented (SysTick, sub-ms timestamp)
UART✅ implemented (USART2, PA2/PA3, polling)
ADC⚠️ stub — pending hardware
SPI⚠️ stub — pending hardware
I2C⚠️ stub — pending hardware

Each toolchain lives in a single make/<target>.mk file that defines the compiler, flags, and a handful of variables consumed by the root Makefile and the example Makefiles:

VariablePurpose
CC, AR, OBJCOPYtoolchain
PORTport directory name (posix, stm32l4, …)
CFLAGS / LDFLAGScompile / link flags (linker script, -nostdlib, …)
PORT_EXTRA_SRCSextra sources beyond hal_<port>.c (e.g. sim_bus.c, startup.c)
LINK_LIBSlibraries for linking executables (e.g. -lpthread, -lgcc)
POST_BUILDoptional post-link command (e.g. objcopy -O binary to produce a .bin)

Adding a port — say AVR — means adding make/avr.mk and a hal/avr/ directory. Nothing else changes. There are no #ifdef TARGET blocks anywhere in the source.

Supported targets, by priority: posix (first class), stm32l4, esp32 (wraps ESP-IDF), avr (avr-libc, mind the 8-bit constraints).