Stack conventions
Embedded firmware (C/C++, PlatformIO)
Firmware for microcontrollers and edge devices (ESP32, STM32, nRF, RP2040 and more) in C and C++ with PlatformIO: hardware-independent logic unit-tested on the host, static analysis, and the memory, timing and safety rules of code that runs on devices.
Firmware for microcontrollers and edge devices (ESP32, STM32, nRF, RP2040 and more) in C and C++ with PlatformIO: hardware-independent logic unit-tested on the host, static analysis, and the memory, timing and safety rules of code that runs on devices.
Used for: embedded.
Principles
- Keep hardware-independent logic (protocols, parsers, control, state machines) in libraries with no board headers, so it is unit-tested on the host (the native environment).
- Put every hardware access behind a small hardware abstraction layer (HAL) interface, so tests and simulators supply fakes.
- Allocate statically - no heap allocation after start-up in production firmware; size every buffer and queue at compile time.
- Bound everything - every loop, wait and retry has a limit and a timeout; nothing blocks forever.
- Design for failure - a hardware watchdog is enabled and fed only by a healthy main loop, and every fault path ends in a defined safe state.
- Keep interrupt service routines minimal - capture data, set a flag or push to a lock-free queue, and do the work in the main loop or a task.
- Treat integer widths, overflow and endianness explicitly - fixed-width types (uint32_t), checked arithmetic and explicit byte order on the wire.
- Pin the platform, framework and library versions in platformio.ini so every build is reproducible.
- Ship firmware updates only signed and verified by the bootloader, with a rollback slot.
Conventions
Accessibility
Concurrency
- Share data between an ISR and the main loop only through volatile flags, atomics or a lock-free queue; keep critical sections (interrupts disabled) minimal.
- With an RTOS, communicate through queues and notifications, use mutexes with priority inheritance, and never block in an ISR.
- Size every task's stack and check the high-water mark in testing.
Dependencies
- Declare libraries in platformio.ini lib_deps with exact or caret versions; never copy vendor code into the tree without its version and licence.
- Track the SDK and framework versions (ESP-IDF, STM32Cube, Zephyr) and their security advisories; upgrade deliberately.
Documentation
- Document every module's public functions (ownership, units, timing, ISR safety) in its header.
- Keep the README current with supported boards, how to build (pio run -e
), flash, test and read logs, and the pin map.
Errors
- Check every driver and peripheral return code; on a fault, log it, count it and move to a defined safe state.
- Use assertions for programming errors and route them to a fault handler that records the location and resets safely.
- Never ignore a watchdog reset or brown-out; read and record the reset reason at boot.
Logging
- Log through one logging module with levels that compiles out below the build's level; keep messages short and rate-limited.
- Never log keys, credentials or personal data; keep a fault log (reset reason, last error) in RAM or flash for field diagnosis.
Naming
- Prefix public symbols with the module name (motor_set_speed, uart_rx_push); make file-local functions and data static.
- Use fixed-width integer types (uint8_t, int32_t) for anything stored, transmitted or bit-manipulated; name units in identifiers (timeout_ms, voltage_mv).
- Macros and enumerators in ALL_CAPS; keep macros to constants, register definitions and conditional compilation.
Performance
- Know the budgets - RAM, flash, stack per task and loop period - and check them after every build (pio run -t size).
- Avoid floating point in interrupts and on parts without an FPU; avoid printf-family calls in time-critical paths.
- Measure timing with a logic analyser or cycle counter before optimising; keep ISRs to a few microseconds.
Security
- Enable secure boot and flash encryption where the part supports them; accept only signed firmware updates with anti-rollback.
- Keep secrets (Wi-Fi passwords, API keys, certificates) out of source and binaries; provision them per device at manufacture or first boot into protected storage, and never log them.
- No universal default passwords; every device has unique credentials (ETSI EN 303 645).
- Validate every frame received over a bus or radio (length, CRC, ranges) before use; never copy beyond a buffer's size.
- Use TLS with certificate validation for network connections; never disable verification in production builds.
- Never pass received data to a command interpreter or format string (command injection, format-string attacks).
Structure
- A PlatformIO project - platformio.ini at the root, src/ for the firmware entry point, lib/
/ for hardware-independent libraries, include/ for shared headers, test/ for unit tests. - Keep a native environment ([env:native]) that builds and tests the libraries on the host, next to one environment per board.
- Keep board-specific code in one place (src/ or lib/board_
/) behind the HAL; libraries never include board or framework headers. - Pin platform, framework and library versions (platform = [email protected], lib_deps = owner/name@^x.y.z) in platformio.ini.
Testing
- Unit-test the libraries on the host with PlatformIO's test runner (Unity) in the native environment - pio test -e native - in every pull request.
- Test hardware-dependent code against fakes of the HAL; integration tests run on the target (pio test -e
) on a hardware-in-the-loop rig where one exists, covering peripherals, timing and the bus protocols end to end. - Run the native tests under AddressSanitizer and UndefinedBehaviorSanitizer (build_flags in the native environment).
- Name tests after the behaviour (
test_parser_rejects_short_frame), one behaviour per test. - Done means tested: new or changed behaviour ships with tests in the same change, and a fixed bug gets a regression test that fails without the fix.
Review checklist
- Is hardware-independent logic in libraries with no board headers, and unit-tested in the native environment?
- Is every hardware access behind the HAL, with a fake for tests?
- Is memory allocated statically, with buffer and queue sizes checked?
- Does every loop, wait and retry have a bound and a timeout?
- Are ISRs minimal and data shared with the main loop safely (volatile, atomics, lock-free queue)?
- Is the watchdog fed only by a healthy main loop, and does every fault end in a safe state?
- Are received frames validated (length, CRC, ranges) before use?
- Are secrets kept out of source and logs, and updates signed?
- Are platform, framework and library versions pinned in platformio.ini?
- Do RAM, flash and stack stay within budget after the change?
- Do pio test -e native and cppcheck pass?
- Are headers documented with units, timing and ISR safety?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
clang-format |
format | clang-format | no |
pio-check |
static | pio check (cppcheck) | no |
cppcheck |
static | cppcheck | no |
native-sanitizer-tests |
test | pio test (native, ASan/UBSan) | no |
osv-scanner |
security | osv-scanner | no |
gitleaks |
security | gitleaks | no |