Firmware for 2025-26 Canard Board, performing state estimation and control for the 2nd iteration of the canards system. Project documentation can be found in the team Google Drive. In 2024-25, this project was firmware for Processor Board in the first canards system
src/drivers/: custom peripheral driver modulessrc/application/: high-level application logic modulessrc/third_party/: third-party libraries, including CubeMX auto-gen filessrc/common/: shared resources specific to this projecttests/: everything for testing
- Clone repo and initialize submodules:
git clone --recurse-submodules https://github.com/waterloo-rocketry/cansw_processor_canards
All firmware dev is done in VSCode using PlatformIO (previously using devcontainer) for easy cross-platform usage.
- Install the PlatformIO extension in VSCode and open this folder
- PlatformIO should auto-detect this project by recognizing the
platformio.inifile - Use the PlatformIO tab to Build and Upload. Ex,
Project Tasks > General > Build - Use the "Run and Debug" tab to debug on the board once connected via ST-Link
- If needed, switch between debug/release environments:
Ctrl+Shift+P > PlatformIO: Pick Project Environment - Auto-format the project code:
Project Tasks > Custom > Format
The devcontainer has everything installed to run unit tests using CMake. One could also install all the dependencies locally if desired but the devcontainer is recommended to avoid "but it works on my machine!" issues.
Devcontainer setup:
- Install devcontainers
- In a new vscode window, use
Ctrl+Shift+P, searchDev Containers: Open Folder In Container..., then select this project folder- The first time opening the project will take several minutes to build the devcontainer. Subsequent times will be instant.
Run tests:
- Open the CMake plugin tab from the sidebar
- Under
Configure, select which build type you want - Hover over
Build, click the build icon to build the configuration- The build preset should automatically be selected (eg,
Build Firmware (Debug) preset)
- The build preset should automatically be selected (eg,
We use GoogleTest and Fake Function Framework (fff) for unit testing. All testing-related files are in tests/.
- Tests are built from
tests/CMakeLists.txtwhich is separate from the project's main build config. Building and running tests is done via cmake. - Test source code should be written in
tests/unit/. - Mocks should be made with fff in
tests/mocks/.
- Add a new test group file in
tests/unit/. Seetest_dummy.cppfor example of test structure. - Add the test group to the cmake build system by editing
tests/CMakeLists.txt:- At the bottom of the file, add the new test group using the
add_test_group()helper. (Read the comments + existing examples explaining how it works)
- At the bottom of the file, add the new test group using the
We do not include the STM32 HAL library nor FreeRTOS when compiling the project for unit tests. So if a source file uses a HAL or FreeRTOS file, those files and their functions must be mocked using fff. This also lets us mock any module that a test interacts with but doesn't test.
Example 1:
-
src/drivers/gpio/gpio.cuses FreeRTOS semaphores via#include "semphr.h.- In the actual firmware, the real
semphr.his included when compiling. But for unit tests, the realsemphr.his not included when compiling. So, the unit tests fail to compile (it can't find asemphr.hfile).- To correct this we add a "fake"
semphr.hintests/mocks/semphr.h. All files in this folder are included when compiling unit test, so the tests now compile.
- To correct this we add a "fake"
- The gpio code uses functions from the real
semphr.hlikexSemaphoreTake(). These don't exist in our fakesemphr.hyet.-
To correct this we need to create a mock
xSemaphoreTake()function using fff. fff's Readme describes how to create fake functions. Here's the mock forxSemaphoreTake():// The func to mock: BaseType_t xSemaphoreTake(SemaphoreHandle_t arg0, TickType_t arg1) DECLARE_FAKE_VALUE_FUNC(BaseType_t, xSemaphoreTake, SemaphoreHandle_t, TickType_t);First we put the declaration (
DECLARE_FAKE...) inmocks/semphr.h. Then, put the actual definition (DEFINE_FAKE...) in the correspondingmocks/semphr.c.
-
- In the actual firmware, the real
-
Now in the gpio tests, we can access the mocked semaphore functions via fff to test that the gpio code uses semaphores correctly.
Example 2:
src/application/estimator/estimator.ctakes input from flightphase via#include "application/flight_phase/flight_phase.h.- To test estimator, we will test various inputs from flightphase. But we don't want to be testing flightphase at the same time. So, mock flightphase.
- The real
flight_phase.hheader is built in unit tests, but the source fileflight_phase.cis not.- Since a function declaration for
get_flight_phase()exists in the header, we can use fff'sDEFINE_FAKE...to create a fake definition of thatget_flight_phase(). That definition should be in the test .cpp file, or in the mocks folder if it's applicable for wider use. - (Note: unlike example 1 where the header isn't included in unit tests, the flightphase header is included so it doesnt need a
DECLARE_FAKE...)
- Since a function declaration for
- Build in vscode using cmake (see step 3 above)
- The default
Build Unit Tests With Coverage presetalso automatically runs all tests and generates coverage report. - View the coverage report html pages in
build/test/coverage_reportin a local browser
- The default
- Use the vscode cmake
LaunchandDebugtabs to run/debug individual test groups (cmake shows the available test groups)
This project follows the team-wide embedded coding standard.
-
Use the
Formattarget via PlatformIO to format via clang-format (see step 2) -
Rocketlib is included at
src/third_party/rocketlib. -
Developers should be aware of the BARR Standard and make an effort to follow it.
When adding a new type of data log message, all of the following should be updated:
- src/application/logger/log.h
- Add a new enum value to
log_data_type_t:typedef enum { + LOG_TYPE_XXX = M(unique_small_integer), } log_data_type_t; - Add a struct definition of your message's data fields to
log_data_container_t:typedef union __attribute__((packed)) { + struct __attribute__((packed)) { + uint32_t l; + float f; + // ... + } typename; } log_data_container_t;
- Add a new enum value to
- scripts/logparse.py
- Add a new format spec to the
FORMATSdict:FORMATS = { + M(unique_small_integer): Spec(name, format, [field, ...]), }
- Add a new format spec to the