Skip to content

ADR-001: CubeMX-Generated Code Lives Under vendor/cubemx/

Field Value
Date 2026-09-29
Status Accepted
Deciders TODO
Supersedes —
Superseded by —

Context and Problem Statement

STM32CubeMX generates and re-generates MCU init code, HAL drivers, and a linker script into fixed paths relative to its own configured project root, whenever the developer re-runs it from the CubeMX GUI. This generator (project templates' repo command) also scaffolds and later updates (repo update) the same project over time. If both tools write into overlapping paths, one can silently clobber the other's output, and renaming any path CubeMX owns breaks CubeMX's own re-generation the next time it runs.

This project type also builds two different things with two different compilers: host-side unit tests (Catch2, native compiler, driven by the repository-root CMakeLists.txt) and the actual firmware (ARM cross-compiler). Those need separate CMake project roots regardless of the CubeMX question — see the "Firmware Subtree" decision driver below.

Decision Drivers

  • CubeMX must be able to re-generate its output at any time without the generator interfering, and vice versa.
  • Vendor-provided or vendor-generated code should live under vendor/, matching this project type's stated preference for keeping hand-written and generated/foreign code separated.
  • A directory should have one owner and one reason to change. firmware/'s own config changes with the project's build setup; CubeMX's output changes only when CubeMX regenerates. Nesting one inside the other mixes those two reasons to change and makes firmware/'s purpose unclear from its contents alone.
  • A real-world CubeMX project (aka The Example) already demonstrates a self-contained CubeMX subtree (cmake/stm32cubemx/CMakeLists.txt) that isolates its generated sources behind INTERFACE/OBJECT CMake targets — this subtree can be relocated wholesale without internal changes.
  • The repository root CMakeLists.txt must stay the one Conan's host toolchain configures (so the existing make build/make test/make lint Makefile targets keep working unchanged for every C++ project type, this one included). The firmware/CubeMX side therefore cannot also live at the repository root — it needs its own subtree with its own CMakeLists.txt.

Considered Options

  1. Let CubeMX generate directly into the project root (Core/, Drivers/, etc. at the top level), as the example does today.
  2. Configure CubeMX's own project path to firmware/vendor/cubemx/, nested inside the firmware cross-build's own firmware/ subtree.
  3. Configure CubeMX's own project path to a repository-root vendor/cubemx/, a sibling of firmware/ rather than nested inside it; firmware/ then holds exclusively its own hand-maintained cross-build configuration (CMakeLists.txt, toolchain file, CMakePresets.json) — no foreign code. The generator scaffolds only a README placeholder under vendor/cubemx/ once and never regenerates or overwrites anything under it afterward.
  4. Have this generator (repo) itself invoke CubeMX or merge CubeMX's output into its structure programmatically.

Decision Outcome

Chosen option: 3. CubeMX is pointed at vendor/cubemx/ (repository root) as its own project root via a one-time manual GUI step (documented in the scaffolded vendor/cubemx/README.md). This generator treats that whole subtree as opaque: it scaffolds a placeholder once and registers no further action against any path inside it, so repo update never touches it. firmware/ (firmware/CMakeLists.txt) pulls in the shared, host-testable logic from ../src/ and links it against the CubeMX-generated HAL/init code in ../vendor/cubemx/; the repository-root CMakeLists.txt remains the host/test project untouched by any of this.

Because ../vendor/cubemx/cmake/stm32cubemx lies outside firmware/'s own source tree, firmware/CMakeLists.txt's add_subdirectory() call for it must pass an explicit binary_dir argument — CMake cannot auto-derive a build-tree location for an out-of-tree source directory.

Positive Consequences

  • Clean, permanent separation — CubeMX never needs a path renamed, no matter how often repo update runs.
  • Matches the project's vendor/ convention for foreign/generated code.
  • firmware/ has exactly one job — turn ../src/ plus ../vendor/cubemx/ into a target executable — and its own contents are entirely hand-maintained project files; nothing inside it is foreign or CubeMX-owned.
  • The example's existing cmake/stm32cubemx/CMakeLists.txt pattern relocates into vendor/cubemx/cmake/stm32cubemx/ without any internal changes — just an add_subdirectory(../vendor/cubemx/cmake/stm32cubemx ...) from firmware/CMakeLists.txt.
  • The repository-root CMakeLists.txt (host/test build) and firmware/CMakeLists.txt (cross build) stay two independent CMake project roots, each configured with its own toolchain — no conditional branching on CMAKE_CROSSCOMPILING inside a single shared CMakeLists.txt.

Negative Consequences / Risks

  • Requires a manual, one-time step: pointing CubeMX's GUI at vendor/cubemx/ when the project is first configured. If a developer skips this and lets CubeMX generate at the root instead, the two tools' file sets can collide.
  • add_subdirectory() with an out-of-tree source directory needs an explicit binary directory argument; a future contributor copying this pattern for another out-of-tree subdirectory must remember the same requirement.

Pros and Cons of the Options

Option 1 — CubeMX generates at the project root

  • Pro: Zero manual CubeMX configuration step.
  • Con: Board-init code sits directly in the tree this generator also manages, so repo update and CubeMX re-generation can conflict; contradicts the vendor/ convention.

Option 2 — firmware/vendor/cubemx/ boundary (nested)

  • Pro: Permanent, conflict-free separation from the generator; matches the vendor/ convention; add_subdirectory() targets stay inside firmware/'s own tree, no explicit binary_dir needed.
  • Con: Mixes firmware/'s own hand-maintained build configuration with CubeMX's foreign, regenerated output in one directory — obscures at a glance what firmware/ is actually for.

Option 3 — vendor/cubemx/ at the repository root (chosen)

  • Pro: Permanent, conflict-free separation from the generator; matches the vendor/ convention; firmware/ reads unambiguously as build configuration only, vendor/ as foreign code only.
  • Con: One manual GUI step required when the project is first set up; the out-of-tree add_subdirectory() call needs an explicit binary_dir.

Option 4 — Generator drives CubeMX programmatically

  • Pro: Fully automated, no manual step.
  • Con: CubeMX has no supported non-interactive/scriptable code-generation interface for this workflow; reverse-engineering .ioc regeneration would be large, fragile scope with no guaranteed stability across CubeMX versions. Rejected as infeasible for this generator.

Notes

vendor/cubemx/README.md documents the one-time CubeMX project-path configuration step for whoever sets up or re-configures the CubeMX side of the project. firmware/CMakePresets.json is what the STM32 VS Code plug-in consumes — point its CMake Tools extension at the firmware/ subdirectory (cmake.sourceDirectory), not at the repository root.