Project Structure
A project is a compilable program that will run on a single device. It should have one main()
function. Each ECU in our vehicle has its own project.
This article will explain our project structure through a tutorial in which you will create a project from scratch.
Warning
Knowledge of our firmware architecture is assumed. Read the Architecture article before continuing.
Note
Unless otherwise mentioned, all file paths are relative to racecar/firmware
.
The simplest project is Blink (see projects/Demo/Blink
). This project toggles a digital output indefinitely, toggling the state every 1 second.
In this tutorial, we will recreate the Blink project from scratch for multiple platforms. You will learn how projects are structured, how to configure an CubeMX for the stm32f767 platform, and how to build and run a project.
Prepare the project folder
All projects are stored under the projects/
directory, with each project organized in its own folder. Projects can also be grouped into subfolders for better organization. For example, the FrontController
project is stored at projects/FrontController/
, while demo projects can be grouped under projects/Demo/
, such as the Blink
project stored at projects/Demo/Blink/
.
-
Create a new folder to hold the project at
projects/MyBlink
. -
Add a
README.md
file explaining the purpose of this project.projects/MyBlink/README.md# MyBlink Recreate the `Demo/Blink` project by following the tutorial at <https://macformula.github.io/racecar/firmware/project-structure>.
-
Add a
platforms/
folder to the project. We will populate the platform implementations later.
You should now have the following directory structure.
projects/
└── MyBlink/
├── platforms/
└── README.md
Bindings contract
A key feature of our firmware architecture is the complete abstraction of the app-level code from any specific platform implementation (See the Architecture article). The two layers are interfaced by the "bindings" contract which we will write first.
Create a new file bindings.h
in the project directory. This is a header file since it only declares the interface objects and methods rather than using or implementing them.
#pragma once
namespace bindings {
} // namespace bindings
Info
#pragma once
Ensures the header file is only ever included once.
We use the bindings
namespace to indicate that the contained functions and objects form the interface between the app and platform layers.
We now add 3 elements to our contract.
-
A digital output which we will call
indicator
. Include theshared/periph/gpio.h
to gain access to the app-levelDigitalOutput
class.projects/MyBlink/bindings.h#pragma once #include "shared/periph/gpio.h" namespace bindings { extern shared::periph::DigitalOutput& indicator; } // namespace bindings
Notice the ampersand
&
in the type specifier. This makesindicator
a reference to aDigitalOutput
object. This is necessary sinceDigitalOutput
is a virtual class so its size-in-memory is not defined. -
A function to wait between toggling. Time delay mechanisms are platform specific and thus must be included in the bindings contract. We will declare a
DelayMS
function which receives anunsigned int
representing the number of milliseconds to delay for.projects/MyBlink/bindings.h#pragma once #include "shared/periph/gpio.h" namespace bindings { extern shared::periph::DigitalOutput& indicator; extern void DelayMS(unsigned int ms); } // namespace bindings
-
An initialization function. Some platforms must execute initialization code before they are ready to run. For example, stm32f767 has several
HAL_*_Init
functions which configure the peripherals. We include anInitialize
function in bindings so that each platform can define its own behaviour.projects/MyBlink/bindings.h#pragma once #include "shared/periph/gpio.h" namespace bindings { extern shared::periph::DigitalOutput& indicator; extern void DelayMS(unsigned int ms); extern void Initialize(); } // namespace bindings
This completes the bindings contract. We can now write the application code, knowing that the platforms will all satisfy this contract.
Application code
Our main
method is very simple in this project:
- Initialize the platform.
- Turn
indicator
on. - Wait 1 second.
- Turn
indicator
off. - Wait 1 second.
- Return to step 2.
Create a main.cc
file in the project folder.
#include "bindings.h"
int main() {
bindings::Initialize();
while (true) {
bindings::indicator.SetHigh();
bindings::DelayMS(1000);
bindings::indicator.SetLow();
bindings::DelayMS(1000);
}
return 0;
}
In this simple project, all functions and peripherals are inside the bindings
namespace. A more complex project could have app-level functions defined in main.cc
.
The SetHigh
and SetLow
methods of indicator
are declared in the virtual class under shared/periph/gpio.h
.
CMake Sources
Our build system needs help determining which source files to compile. This is configured using a CMakeLists
file that adds our main.cc
file as a source to the global main
target.
target_sources(main PUBLIC main.cc)
This concludes the app-level code. Your project directory should look like
projects/
└── MyBlink/
├── platforms/
├── bindings.h
├── CMakeLists.txt
├── main.cc
└── README.md
Platform code
We will create a platform implementation for the command line interface and the stm32f767. Both of these platforms have an MCAL library under racecar/firmware/mcal/
.
Command Line Interface
Create a cli
subfolder of MyBlink/platforms
. We will satisfy the bindings contract in a bindings.cc
file.
-
Configure the
indicator
output. The concrete peripheral implementation is provided bymcal/cli/periph/gpio.h
. Include this header and create a peripheral.projects/MyBlink/platforms/cli/bindings.cc#include "mcal/cli/periph/gpio.h" namespace mcal { cli::periph::DigitalOutput indicator{"Indicator"}; } // namespace mcal
Now "bind" it to the app-level handle in the
bindings
namespace.projects/MyBlink/platforms/cli/bindings.cc#include "../../bindings.h" #include "mcal/cli/periph/gpio.h" #include "shared/periph/gpio.h" namespace mcal { cli::periph::DigitalOutput indicator{"Indicator"}; } // namespace mcal namespace bindings { shared::periph::DigitalOutput& indicator = mcal::indicator; } // namespace bindings
Again, note the ampersand
&
on the type specifier. -
Implement
DelayMS
. Our CLI will use the POSIXusleep
function fromunistd.h
.The
usleep
delay is measured in microseconds. Multiply our milliseconds parameter by 1000 to convert it to microseconds.projects/MyBlink/platforms/cli/bindings.cc#include <unistd.h> #include "../../bindings.h" #include "mcal/cli/periph/gpio.h" #include "shared/periph/gpio.h" namespace mcal { cli::periph::DigitalOutput indicator{"Indicator"}; } // namespace mcal namespace bindings { shared::periph::DigitalOutput& indicator = mcal::indicator; void DelayMS(unsigned int ms) { usleep(ms * 1000); } } // namespace bindings
-
Implement the
Initialize
method. Our CLI does not need any initialization, but we can print a message to demonstrate that it is being executed.projects/MyBlink/platforms/cli/bindings.cc#include <unistd.h> #include <iostream> #include "../../bindings.h" #include "mcal/cli/periph/gpio.h" #include "shared/periph/gpio.h" namespace mcal { cli::periph::DigitalOutput indicator{"Indicator"}; } // namespace mcal namespace bindings { shared::periph::DigitalOutput& indicator = mcal::indicator; void DelayMS(unsigned int ms) { usleep(ms * 1000); } void Initialize() { std::cout << "Initializing the CLI..." << std::endl; } } // namespace bindings
This completes the bindings implementation for the CLI.
CMake Configuration
We must add 2 files to help the build system find the correct sources.
target_sources(bindings PRIVATE bindings.cc)
target_link_libraries(bindings PUBLIC mcal-cli)
The first line adds the bindings.cc
file to the globally defined bindings
target. The second links the required MCAL library to the target.
The second file tells CMake which MCAL library to include.
set(MCAL cli)
Historical Note
In 2023, the mcal was selected based on the platform folder name rather than by the mcal_conf.cmake
file. However, this prevented us from defining two platform implementations using the same mcal, as the two folder names had to be unique but then couldn't reference the same mcal. mcal_conf.cmake
was added to allow for this configuration.
This conf file duplicates logic in the second line of
CMakeLists.txt
. Make a PR if you have a better solution.
Your projects/MyBlink/platforms
directory should look like
projects/MyBlink/platforms/
└── cli/
├── bindings.cc
├── CMakeLists.txt
└── mcal_conf.cmake
Test the CLI
Tip
Read the Compiling a Project article for a full explanation of the compilation procedure.
To compile the MyBlink project with the CLI platform, open a terminal and navigate to the racecar/firmware
directory. Run this command
make PROJECT=MyBlink PLATFORM=cli
This will compile the project to an executable which is stored in the firmware/build/MyBlink/cli
directory.
Run the executable and see the indicator toggling every 1 second.
$ ./build/MyBlink/cli/main.exe
Initializing the CLI...
Setting DigitalOutput Channel Indicator to true
Setting DigitalOutput Channel Indicator to false
Setting DigitalOutput Channel Indicator to true
Setting DigitalOutput Channel Indicator to false
# repeats forever