Skip to content

C++ Embedded Coding Guidelines

Applies on top of the general C++ coding guidelines.

General Embedded

  • No exceptions
  • No RTTI
  • Prefer static allocation
  • Avoid dynamic memory allocation; prefer fixed-size buffers to prevent heap fragmentation
  • No busy waiting, prefer interrupts and state machines
  • Always fully qualify std:: — never a using std::X; declaration, even function-scoped (stricter than the general guideline's allowance): keeps every standard-library use visibly distinguishable from project code, matching MISRA C++/AUTOSAR C++14 convention for safety-relevant firmware

Arduino (8 bit AVR)

  • Dialect: C++17, enforced via platform.local.txt:
compiler.cpp.flags=-c -g -Os -w -std=gnu++17 -ffunction-sections -fdata-sections -fno-exceptions -ffreestanding -fno-threadsafe-statics
  • <project>.ino as entry point; all logic in .cpp/.hpp files.
  • No heavy STL (std::vector, std::string); use C-strings, fixed arrays, or Arduino String.
  • F() macro for string literals in Serial.print() to keep them in flash.
  • No delay(); use millis()-based state machines.
  • constexpr for pin definitions (not #define).
  • Hardware objects declared global; .begin() called in setup().
  • Check all .begin()/.connect() return values — hardware may not be ready.

STM32 (Cortex-M via CubeMX)

  • Dialect: C++23, -fno-exceptions -fno-rtti -fno-threadsafe-statics (matches the firmware toolchain file and the host build's compile options — code behaves identically in both).
  • Everything CubeMX generates lives under vendor/cubemx/ and is never hand-edited — see that directory's README.md.
  • Hardware access goes exclusively through the Hal interface (src/infra/hal.hpp); App itself stays free of any STM32 HAL header so it compiles for the host (tests) and the target (firmware) unchanged.
  • The App's main-loop entry point is called forever from the target's main loop and a fixed number of times from host tests — no shutdown/exit path. Embedded firmware never returns from main(); if there is nothing to do, enter a low-power mode (e.g. __WFI()) there instead of trying to leave the loop.
  • Calling hand-written C++ from CubeMX's generated main.c goes exclusively through the extern "C" bridge in src/app_c_api.h, wired into CubeMX's USER CODE BEGIN/END markers — never edit main.c outside those markers, or the next CubeMX regeneration silently discards it.
  • HAL callbacks (HAL_*Callback) live in src/app_c_api.cpp with extern "C" linkage, never in main.c; main.c only includes app_c_api.h and calls app_init() and app_poll().
  • Catch2 (not Unity) for tests, host-side only; on-target tests are deferred until the hardware interaction is worth automating.
  • No Conan for the firmware build: the vendor toolchain and HAL come from CubeMX, not from a package manager.

Acceptance Tests

Bare-metal firmware has no operating system and no network-reachable running instance, so pytest-bdd cannot drive it like a service. A thin subprocess-drivable harness in test/bdd/harness/ wires App to the same mocked HAL the Catch2 tests use, not to the app_c_api.h/Target_Hal bridge, which grows real hardware calls and stops compiling on the host; the ARM cross-build exercises that bridge. This is a host-side acceptance tier only and does not replace on-target or hardware-in-the-loop tests.

Directives

  • No exceptions, no RTTI, no dynamic allocation on the target; prefer static allocation and fixed-size buffers
  • No busy waiting; use interrupts and state machines instead of delay()-style loops
  • Always fully qualify std::; never a using std::X; declaration, even scoped — stricter than the general guideline
  • Arduino (AVR): C++17 via platform.local.txt; .ino is the entry point only, all logic lives in .cpp/.hpp
  • Arduino: no heavy STL (std::vector, std::string) — use C-strings, fixed arrays, or Arduino String; wrap string literals in F() to keep them in flash
  • Arduino: no delay() — use millis()-based state machines; constexpr for pin definitions, not #define
  • Arduino: hardware objects declared global, .begin() called in setup(); always check .begin()/.connect() return values
  • CubeMX-generated code lives only under vendor/cubemx/; never hand-edit it
  • Hardware access only through the Hal interface; keep App free of any vendor HAL header
  • No shutdown/exit path; a target with nothing to do enters a low-power mode inside its main-loop entry point
  • Call hand-written C++ from main.c only through the extern "C" bridge, wired into CubeMX's USER CODE BEGIN/END markers — never edit main.c outside those markers
  • HAL callbacks live in src/app_c_api.cpp with extern "C" linkage, never in main.c
  • Catch2 for host-side tests; no on-target test framework until a concrete need arises
  • Firmware BDD drives a host harness through a subprocess, wired to the same mocked HAL as the Catch2 tests, never to the target-only bridge