|
| 1 | +# STM32 Embedded Project Template |
| 2 | +Template repo for STM32(H7) projects on the team. Based on [Processor 2025 Firmware](https://github.com/waterloo-rocketry/cansw_processor_canards). It includes a unit test framework and pipeline. |
| 3 | + |
| 4 | +## READ FIRST: How to customize this template |
| 5 | +1. Inside `.devcontainer/Dockerfile`, edit the following: |
| 6 | + |
| 7 | +- `ENV BOARD_NAME="<board_name>"` e.g. "proc |
| 8 | +- `ENV STM_DEVICE="<chip_family>"` e.g. STM32H750xx |
| 9 | +- `ENV STM_DEVICE_EXACT="<chip_model>"` e.g. STM32H750ZBTX |
| 10 | + |
| 11 | +2. Open STM32CubeMX and generate the .ioc file for your specific chip **in a temporary location**. Make sure you set the following: |
| 12 | +- Under `Project Manager`->`Project` |
| 13 | + - Project Name - whatever you want, be consistent |
| 14 | + - Application Structure -> Advanced, leave "Do not generate the main" **unchecked** |
| 15 | + - Toolchain -> STM32CubeIDE, **check** "Generate Under Root" |
| 16 | + |
| 17 | +- Under `Project Manager`->`Code Generator` |
| 18 | + - Select "Copy only the necessary files" |
| 19 | + - Check all 4 boxes under "Generated files" |
| 20 | + - Leave everything else unchanged |
| 21 | + |
| 22 | +- Under `Pinout & Configuration`, two peripherals need to be enabled by default. These are FDCANx, which interfaces with canlib, and UART4, which interfaces with our custom `printf` library. If your board does not use one or both of these libraries, you will need to update CMake to not look for them. |
| 23 | + - The FDCAN handle is passed to canlib at init, so you can use any one (the HAL sources will generate reagrdless) |
| 24 | + - The UART4 handle is hard coded into `/workspaces/stm32_template/src/third_party/printf/printf.c`, line 269, and simply needs to be changed to match the UART you want to use. It is hardcoded so that you can printf from anywhere without needing to confirm your printf was initialized every time. |
| 25 | + |
| 26 | +3. Replace `PLACEHOLDER.ioc` in the root directory with your ioc file |
| 27 | +4. Open the ioc in CubeMX from the new location, and generate code |
| 28 | + |
| 29 | +## Project Structure |
| 30 | +- `src/drivers/`: custom peripheral driver modules |
| 31 | +- `src/application/`: high-level application logic modules |
| 32 | +- `src/third_party/`: third-party libraries |
| 33 | +- `src/common/`: shared resources specific to this project |
| 34 | +- `tests/`: everything for [testing](#Unit-Testing) |
| 35 | +- Everything else is autogenerated by STM32CubeIDE with few modifications |
| 36 | + |
| 37 | +## Developer Setup |
| 38 | +This project is not dependent on STM32CubeIDE. |
| 39 | +Code editing, unit testing, and building should be done in the devcontainer. |
| 40 | +Only running/debugging on target should be done in STM32CubeIDE. |
| 41 | + |
| 42 | +#### 1. Clone repo |
| 43 | +- Clone repo and initialize submodules: ``` |
| 44 | + git clone --recurse-submodules https://github.com/waterloo-rocketry/cansw_processor_canards ``` |
| 45 | + - (Note: if you choose to clone with ssh instead, you have to manually setup ssh forwarding in the devcontainer.) |
| 46 | + |
| 47 | +#### 2. Open project in devcontainer |
| 48 | +The devcontainer has everything setup for editing, unit testing, and building. Most dev work should occur in it. |
| 49 | +- Open the project using vscode devcontainers. |
| 50 | + - [Install devcontainers](https://code.visualstudio.com/docs/devcontainers/tutorial) |
| 51 | + - In a new vscode window, use `Ctrl+Shift+P`, search `Dev Containers: Open Folder In Container...`, then select this project folder |
| 52 | + - The first time opening the project will take several minutes to build the devcontainer. Subsequent times will be instant. |
| 53 | + |
| 54 | +#### 3. Build and test project in devcontainer |
| 55 | +*Recommended: use vscode cmake plugin:* |
| 56 | +- Open the CMake plugin tab from the sidebar |
| 57 | +- Under `Configure`, select which build type you want |
| 58 | +- Hover over `Build`, click the build icon to build the configuration |
| 59 | + - The build preset should automatically be selected (eg, `Build Firmware (Debug) preset`) |
| 60 | + |
| 61 | +#### 4. Run/debug in STM32CubeIDE |
| 62 | +STM32CubeIDE is required for flashing/debugging on hardware. NOTE: STM32CubeIDE is not able to *build* this project. STM32CubeIDE is only used to *flash* the build from step 3. |
| 63 | +- Import the project into STM32CubeIDE (version 1.16.1 recommended): `File -> Import... -> Existing Projects into Workspace` |
| 64 | +- Build the project firmware binary using vscode (step 3) |
| 65 | +- Use an ST-Link programmer to connect to processor board. |
| 66 | +- Use STM32CubeIDE launch/debug as usual |
| 67 | + - NOTE: since the project can't be built in STM32CubeIDE, auto-building before launch is turned off. **Remember to always build the project after making edits.** |
| 68 | + |
| 69 | +## Unit Testing |
| 70 | +We use GoogleTest and Fake Function Framework (fff) for unit testing. All testing-related files are in `tests/`. |
| 71 | +- Tests are built from `tests/CMakeLists.txt` which is separate from the project's main build config. Building and running tests is done via cmake. |
| 72 | +- Test source code should be written in `tests/unit/`. |
| 73 | +- Mocks should be made with fff in `tests/mocks/`. |
| 74 | + |
| 75 | +### Add a test |
| 76 | +- Add a new test group file in `tests/unit/`. See `test_dummy.cpp` for example of test structure. |
| 77 | +- Add the test group to the cmake build system by editing `tests/CMakeLists.txt`: |
| 78 | + - At the bottom of the file, add the new test group using the `add_test_group()` helper. |
| 79 | + (Read the comments + existing examples explaining how it works) |
| 80 | + |
| 81 | +### Add a mock |
| 82 | +We do not include the STM32 HAL library nor FreeRTOS when compiling the project for unit tests. |
| 83 | +So if a source file uses a HAL or FreeRTOS file, those files and their functions must be mocked using fff. |
| 84 | +This works similarly for mocking other proc modules that a test interacts with but doesn't test. |
| 85 | + |
| 86 | +Example 1: |
| 87 | +- `src/drivers/gpio/gpio.c` uses FreeRTOS semaphores via `#include "semphr.h`. |
| 88 | + - In the actual firmware, the real `semphr.h` is included when compiling. But for unit tests, the real `semphr.h` is not included when compiling. So, the unit tests fail to compile (it can't find a `semphr.h` file). |
| 89 | + - To correct this we add a "fake" `semphr.h` in `tests/mocks/semphr.h`. All files in this folder are included when compiling unit test, so the tests now compile. |
| 90 | + - The gpio code uses functions from the real `semphr.h` like `xSemaphoreTake()`. These don't exist in our fake `semphr.h` yet. |
| 91 | + - To correct this we need to create a mock `xSemaphoreTake()` function using fff. |
| 92 | + [fff's Readme](https://github.com/meekrosoft/fff?tab=readme-ov-file#hello-fake-world) describes how to create fake functions. Here's the mock for `xSemaphoreTake()`: |
| 93 | + ``` |
| 94 | + // The func to mock: BaseType_t xSemaphoreTake(SemaphoreHandle_t arg0, TickType_t arg1) |
| 95 | + |
| 96 | + DECLARE_FAKE_VALUE_FUNC(BaseType_t, xSemaphoreTake, SemaphoreHandle_t, TickType_t); |
| 97 | + ``` |
| 98 | + First we put the *declaration* (`DECLARE_FAKE...`) in `mocks/semphr.h`. Then, put the actual *definition* (`DEFINE_FAKE...`) in the corresponding `mocks/semphr.c`. |
| 99 | +- Now in the gpio tests, we can access the mocked semaphore functions via fff to test that the gpio code uses semaphores correctly. |
| 100 | +
|
| 101 | +Example 2: |
| 102 | +- `src/application/estimator/estimator.c` takes input from flightphase via `#include "application/flight_phase/flight_phase.h`. |
| 103 | + - To test estimator, we will test various inputs from flightphase. But we don't want to be testing flightphase at the same time. |
| 104 | + So, mock flightphase. |
| 105 | + - The real `flight_phase.h` header is built in unit tests, but the source file `flight_phase.c` is not. |
| 106 | + - Since a function *declaration* for `get_flight_phase()` exists in the header, we can use fff's `DEFINE_FAKE...` to create a |
| 107 | + fake *definition* of that `get_flight_phase()`. That definition should be in the test .cpp file, or in the mocks folder if it's applicable for wider use. |
| 108 | + - (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...`) |
| 109 | +
|
| 110 | +### Run/debug tests |
| 111 | +- Build in vscode using cmake (see step 3 above) |
| 112 | + - The default `Build Unit Tests With Coverage preset` also automatically runs all tests and generates coverage report. |
| 113 | + - View the coverage report html pages in `build/test/coverage_report` in a local browser |
| 114 | +- Use the vscode cmake `Launch` and `Debug` tabs to run/debug individual test groups (cmake shows the available test groups) |
| 115 | +
|
| 116 | +
|
| 117 | +## Debugging on hardware |
| 118 | +- Use STM32CubeIDE debugging as directed above |
| 119 | +- The ST-link programmer has a serial output so you can listen to uart4 from a laptop COM port. The printf library (NOT THE STDLIB PRINTF) is configured to print strings to that COM port. Use `printf_("string to print..")` - note the `_` character. |
| 120 | + - This should rarely be used. Please instead learn how to use the debugger (breakpoints, dynamic print breakpoints, step, etc) for efficient and pleasant debugging. |
| 121 | +
|
| 122 | +## Code Standards |
| 123 | +This project follows the [team-wide embedded coding standard](https://docs.waterloorocketry.com/general/standards/embedded-coding-standard.html). |
| 124 | +- The devcontainer sets up vscode format-on-save to automatically use the team's clang-format. |
| 125 | + - In case you want to format manually, the script can be run from the project root directory: |
| 126 | + ```bash |
| 127 | + ./scripts/format.sh |
| 128 | + ``` |
| 129 | + |
| 130 | +- Rocketlib is included at `src/third_party/rocketlib`. |
| 131 | +- Developers should be aware of the BARR Standard and make an effort to follow it. |
| 132 | + |
| 133 | +## Adding Log Messages |
| 134 | +When adding a new type of data log message, all of the following should be updated: |
| 135 | +- src/application/logger/log.h |
| 136 | + - Add a new enum value to `log_data_type_t`: |
| 137 | + ```diff |
| 138 | + typedef enum { |
| 139 | + + LOG_TYPE_XXX = M(unique_small_integer), |
| 140 | + } log_data_type_t; |
| 141 | + ``` |
| 142 | + - Add a struct definition of your message's data fields to `log_data_container_t`: |
| 143 | + ```diff |
| 144 | + typedef union __attribute__((packed)) { |
| 145 | + + struct __attribute__((packed)) { |
| 146 | + + uint32_t l; |
| 147 | + + float f; |
| 148 | + + // ... |
| 149 | + + } typename; |
| 150 | + } log_data_container_t; |
| 151 | + ``` |
| 152 | +- scripts/logparse.py |
| 153 | + - Add a new format spec to the `FORMATS` dict: |
| 154 | + ```diff |
| 155 | + FORMATS = { |
| 156 | + + M(unique_small_integer): Spec(name, format, [field, ...]), |
| 157 | + } |
| 158 | + ``` |
| 159 | + |
| 160 | +## Notes |
| 161 | +- Auto-gen stm32 files used STM32CubeMX Version: 6.12.1-RC4, |
| 162 | + STM32CubeIDE version 1.16.1 |
| 163 | + |
0 commit comments