In this article, I will talk about the reusable project structure I use for bare metal firmware or for RTOS-based firmware that doesn’t already define its own structure (e.g. when I use Zephyr, I use a different project structure).
My project structure is currently as follows; however, it is continuously evolving and being improved over time.
my_project_skeleton/
│
├── my_project_app/
│ ├── boards/
│ │ ├── nucleo_stm32f401re/
│ │ ├── ...
│ │ └── ...
│ ├── doc/
│ │ ├── doxygen/
│ │ ├── ...
│ │ └── CMakeLists.txt
│ ├── app/
│ │ ├── scheduler/
│ │ ├── ...
│ │ └── main.c
│ ├── tests/
│ │ ├── mock/
│ │ ├── src/
│ │ └── CMakeLists.txt
│ └── CMakeLists.txt
│
├── my_project_boot/
│ ├── boards/
│ │ ├── nucleo_stm32f401re/
│ │ ├── ...
│ │ └── ...
│ ├── doc/
│ │ ├── doxygen/
│ │ ├── ...
│ │ └── CMakeLists.txt
│ ├── app/
│ │ ├── ...
│ │ └── main.c
│ ├── tests/
│ │ ├── mock/
│ │ ├── src/
│ │ └── CMakeLists.txt
│ └── CMakeLists.txt
│
├── cmake/
│ ├── analysis/
│ ├── ...
│ ├── toolchains/
│ │ ├── arm/
│ │ └── ...
│ └── unit_test/
│
├── lib/
│ ├── freertos/
│ ├── stm32/
│ ├── unity/
│ ├── ...
│ ├── CMakeLists.txt
│ └── README.md
│
├── template/
│ ├── template.c
│ ├── template.h
│ └── ...
│
├── .clang-format
├── .clang-tidy
├── .gitignore
├── .gitmodules
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
└── requirements.txtmy_project_skeleton, the main folder
My project skeleton is structured around three main components: the application firmware, the bootloader, and a set of reusable scripts and tools. In this article I will focus only on the part of the code development. The CI/CD pipelines and build/tasks automation I use to compile, test, and deploy the project are a topic for a separate post.
my_project_app folder
This is the main folder of the application layer of the firmware
Boards
In this folder there is the board-specific code. It could be a development kit or a custom board. The purpose of this folder is to separate the app and the board layer. When changing a board no change is needed at the app layer. In this folder there are also the linker file and the startup file.
Doc
In this folder there are the documentation files related to the application. The doxygen subdirectory contains the configuration and templates to generate documentation from source comments in both html and pdf format. In some cases, other types of documentations could be present (markdown files, sphinx, …).
App
In this folder is present the main app logic: state machines, schedulers, communication protocols, and the top-level main.c. The code in this directory is and must be totally independent from the HW used. The communication from this layer to the board one is ensured through interfaces.
Tests
In this folder there are the unit tests for the application layer.
my_project_boot folder
This folder is structured as the my_project_app folder and it contains all the code related to the bootloader, if needed for the project.
Cmake folder
This folder contains all CMake modules, helper scripts, and toolchain files. They could be managed through submodule or directly copied into the folder. In case the project needs a different build systems, another folder is used (e.g. Makefile, Meson, …).
Lib folder
Third-party libraries and vendor SDKs treated as read-only dependencies: the RTOS, the MCU HAL, the test framework, and any other external component the project relies on. Nothing in lib/ is modified directly. They are updates are managed through git submodules or CMake’s FetchContent. The README.md is used to keep the traceability of the modules used in this lib.
Template folder
This folder contains template files that I use as a reference for standard code patterns and comment structures (e.g., Doxygen), ensuring a consistent documentation and formatting style across all project files.
For example, when I create a new function, I copy the following snippet, forcing me to fill all the needed fields for its Doxygen documentation:
/**
* @brief Short description of the function.
*
* @details Detailed explanation of the function behavior,
* purpose, and important implementation notes.
* Add usage constraints or side effects if needed.
*
* @param val Description of the returned value
* @return Description of the returned value.
*
* @date dd.mm.yyyy
* @author Name Surname
*/
void mod_functionName(void)
{
// ...
}Remaining files
The remaining files in the main folder are the following:
- .clang-format: defines automatic code formatting rules (indentation, brace style, line length).
- .clang-tidy: configures static analysis checks and linting warnings on the C/C++ source.
- .gitignore: lists files and directories Git should not track (I have found a really nice collection of .gitignore template here. Usually I combine more of them, for example C and Python).
- .gitmodules: registers external Git repositories included as submodules.
- .pre-commit-config.yaml: defines automated checks that run before every commit (formatting, linting, trailing whitespace).
- LICENSE: states the legal terms under which the project can be used, modified, and distributed. I use only on the code that I share with others (here you can find nice license generator to use).
- README.md: information on what the project is, how to build it, how to get started
- requirements.txt: lists Python package dependencies needed to run the project’s scripts and tooling.
Resources
On the web there are a lot of suggestion for the perfect project structure. Here is a selection of the resources from which I have taken inspiration to create my own project structure, along with my working experience:
- Embedded Artistry: a reusable CMake project skeleton.
- Artful Bytes: an interesting video presenting his own project structure.