Open-source flight controller firmware for UBC Rocket's experimental VTOL platform
Ulysses powers UBC Rocket's Coaxial Counter-Rotating Vertical Take-off and Landing Drone — a testbed designed for developing and validating advanced rocket flight control algorithms in real-world conditions.
Built on the STM32H563 (Cortex-M33, 250 MHz) running FreeRTOS, the firmware fuses IMU, barometer, and GPS data through an Extended Kalman Filter, drives a PID-based flight controller at 800 Hz, and communicates over a protobuf-based radio link.
- Project Roadmap
- RTOS Architecture
- Prerequisites
- Building
- Flashing and Debugging
- Project Structure
- Contributing
| Phase | Deliverables | Planned Completion |
|---|---|---|
| Phase 1 | Prototype capable of moving up, hovering, and landing | Jan 2026 |
| Phase 2 | Prototype capable of moving along a horizontal plane | May 2026 |
| Phase 3 | Swapping from PID to MPC control; FPGA integration; general polishing | Late 2026 |
The firmware runs four FreeRTOS tasks with the following priority hierarchy:
Priority High ──────────── Controls (800 Hz)
Debug Logging (optional)
Priority AboveNormal ───── State Estimation (event-driven)
Priority Normal ────────── Mission Manager (10 Hz telemetry)
| Priority | High |
| Rate | 800 Hz (TIM4 interrupt-driven) |
| Stack | 2,048 bytes |
Runs the flight controller loop. Reads the latest fused state from the state exchange, computes attitude corrections (PD controller) and thrust commands (PID controller), and outputs PWM signals to servos (gimbal) and ESCs (motors). Includes a staleness check — outputs zero control if state data is too old.
| Priority | AboveNormal |
| Rate | Event-driven (ISR notifications) |
| Stack | 2,048 bytes |
Fuses sensor data through a dual Extended Kalman Filter:
- Orientation EKF — quaternion state, driven by accelerometer and gyroscope
- Body EKF — position and velocity, driven by barometer and GPS
On startup, performs a calibration phase (20 samples) to compute gyro/accel biases. Publishes fused state (position, velocity, attitude, angular rate) to the shared state exchange. Logs all sensor samples and fused state to the SD card.
Responds to ISR flags: BMI088_ACCEL_SAMPLE_FLAG, BMI088_GYRO_SAMPLE_FLAG, MS5611_BARO_SAMPLE_FLAG, MS5607_BARO2_SAMPLE_FLAG, GNSS_GPS_FIX_READY_FLAG.
| Priority | Normal |
| Rate | 100 ms timeout / radio event |
| Stack | 2,048 bytes |
Manages the flight state machine and radio communications:
- Flight states:
IDLE→RISE→HOVER→LOWER(orE_STOPat any time) - Radio RX: Decodes protobuf
FlightCommandmessages (ARM, LAUNCH, ABORT, LAND, PID updates) - Radio TX: Sends telemetry at 10 Hz and system status at 1 Hz
- SD Logging: Logs flight state transitions and periodically flushes the log buffer
| Priority | High |
| Stack | 512 bytes |
| Enabled by | ULYSSES_ENABLE_DEBUG_LOGGING (ON by default) |
Drains the debug log buffer and outputs formatted messages over USART1. Disable via the CMake option for production builds.
Sensor data acquisition is handled asynchronously via DMA — no dedicated polling task is needed. SPI transfers for the BMI088 IMU, MS5611/MS5607 barometers, and GNSS radio run in the background and fire interrupts when data is ready, which notify the State Estimation task.
Windows is not supported. The STM32CubeMX code generation uses forward slashes in paths, which breaks native Windows builds. If you're on Windows, use WSL2 with the Linux instructions below.
| Tool | Version | Purpose |
|---|---|---|
| ARM GNU Toolchain | Latest (arm-none-eabi-gcc) |
Cross-compiler for Cortex-M33 |
| CMake | >= 3.22 | Build system generator |
| Ninja | Latest | Build system |
| STM32CubeIDE | Latest | Provides ST's OpenOCD (required for STM32H563 support) |
| Git | Latest | Version control + submodules |
| Tool | Purpose |
|---|---|
| STM32CubeMX | GUI pin/peripheral configuration (edits ulysses.ioc) |
| VS Code | Recommended editor with Cortex-Debug integration |
brew install --cask gcc-arm-embedded # ARM toolchain
brew install cmake ninja gitSTM32CubeIDE must be downloaded manually from st.com (requires a free myST account).
sudo apt update
sudo apt install gcc-arm-none-eabi cmake ninja-build gitDownload STM32CubeIDE from st.com.
arm-none-eabi-gcc --version
cmake --version
ninja --versiongit clone https://github.com/UBC-Rocket/ulysses.git
cd ulysses
git submodule update --init --recursiveUsing CMake presets (recommended):
# Debug build
cmake --preset debug
cmake --build --preset debug
# Release build
cmake --preset release
cmake --build --preset releaseOr manually:
cmake -B build/debug \
-DCMAKE_TOOLCHAIN_FILE=cmake/gcc-arm-none-eabi.cmake \
-DCMAKE_BUILD_TYPE=Debug \
-G Ninja
cmake --build build/debugThe output binary is build/debug/ulysses.elf.
| Option | Default | Description |
|---|---|---|
ULYSSES_ENABLE_DEBUG_LOGGING |
ON |
Enable serial debug output over USART1 |
ULYSSES_USE_USB |
OFF |
Enable USB support |
Pass options during configuration:
cmake --preset debug -DULYSSES_ENABLE_DEBUG_LOGGING=OFF- Install the recommended extensions (open Command Palette →
Extensions: Show Recommended Extensions) - VS Code will auto-detect
CMakePresets.json— selectdebugorreleasewhen prompted - Click Build in the status bar or press
F7
Flashing and debugging require an ST-Link debugger connected to the board and ST's OpenOCD (bundled with STM32CubeIDE). Upstream OpenOCD does not yet support the STM32H563.
You need to set STM32_OPENOCD_SCRIPTS_PATH to point to the ST OpenOCD scripts directory inside your STM32CubeIDE installation.
macOS:
# Add to your ~/.zshrc, ~/.bashrc, or ~/.config/fish/config.fish
export STM32_OPENOCD_SCRIPTS_PATH="/Applications/STM32CubeIDE.app/Contents/Eclipse/plugins/com.st.stm32cube.ide.mcu.debug.openocd_2.3.200.202510310951/resources/openocd/st_scripts"The exact plugin version number (e.g.
2.3.200.202510310951) will vary based on your STM32CubeIDE version. To find it:ls /Applications/STM32CubeIDE.app/Contents/Eclipse/plugins/ | grep openocd
Linux / WSL2:
# Add to your ~/.bashrc or ~/.zshrc
export STM32_OPENOCD_SCRIPTS_PATH="/opt/st/stm32cubeide_1.19.0/plugins/com.st.stm32cube.ide.mcu.debug.openocd_2.3.100.202501240831/resources/openocd/st_scripts"Find the exact path with:
find /opt/st -path "*/openocd/st_scripts" -type d
Important: Restart your shell and VS Code after setting the variable.
Create or edit .vscode/settings.json:
{
"cortex-debug.openocdPath": "<OPENOCD_BINARY_PATH>"
}Where <OPENOCD_BINARY_PATH> is:
| Platform | Typical Path |
|---|---|
| macOS | /Applications/STM32CubeIDE.app/Contents/Eclipse/plugins/com.st.stm32cube.ide.mcu.externaltools.openocd.macos64_2.4.200.202505051030/tools/bin/openocd |
| Linux | /opt/st/stm32cubeide_1.19.0/plugins/com.st.stm32cube.ide.mcu.externaltools.openocd.linux64_2.4.200.202505051030/tools/bin/openocd |
Find the exact path with:
# macOS find /Applications/STM32CubeIDE.app -name "openocd" -type f # Linux find /opt/st -name "openocd" -type f
From VS Code (recommended):
- Connect your ST-Link to the board
- Open the Run and Debug panel (
Ctrl+Shift+D/Cmd+Shift+D) - Select Ulysses - Debug or Ulysses - Release
- Press
F5to flash and start debugging - The debugger will halt at
main()— pressF5again to continue
The launch configurations use:
- Interface:
interface/stlink-dap.cfg - Target:
target/stm32h5x.cfg - SVD:
platform/svd/STM32H563.svd(enables peripheral register inspection)
From the command line (flash only):
# Find the openocd binary (same path as OPENOCD_BINARY_PATH above)
OPENOCD=/path/to/openocd
$OPENOCD \
-s "$STM32_OPENOCD_SCRIPTS_PATH" \
-f interface/stlink-dap.cfg \
-f target/stm32h5x.cfg \
-c "program build/debug/ulysses.elf verify reset exit"ulysses/
├── Core/
│ ├── Inc/ # Headers
│ │ ├── controls/ # Flight controller, PID
│ │ ├── state_estimation/ # EKF, quaternions, matrices
│ │ ├── motor_drivers/ # PWM, ESC, servo drivers
│ │ ├── spi_drivers/ # BMI088, MS5611, MS5607, GNSS radio
│ │ ├── SD_logging/ # SD card logging
│ │ └── debug/ # Debug log facilities
│ └── Src/ # Source files (mirrors Inc/ layout)
│ └── tasks/ # FreeRTOS task implementations
├── Drivers/ # STM32 HAL & CMSIS (auto-generated)
├── Middlewares/ # FreeRTOS kernel
├── rocket-protocol-lib/ # Protobuf radio protocol (submodule)
│ ├── generated/ # Nanopb-generated C code
│ ├── nanopb/ # Nanopb runtime library
│ └── src/ # Codec, COBS framing, CRC16
├── cmake/
│ ├── gcc-arm-none-eabi.cmake # ARM Cortex-M33 toolchain file
│ ├── dependencies.cmake # External deps (printf library)
│ └── stm32cubemx/ # CubeMX-generated CMake sources
├── docs/ # Additional documentation
├── platform/svd/ # SVD file for register debugging
├── tools/ # SD card and serial utilities
├── CMakeLists.txt # Main build configuration
├── CMakePresets.json # Build presets (debug/release)
├── ulysses.ioc # STM32CubeMX project configuration
├── STM32H563xx_FLASH.ld # Linker script
└── startup_stm32h563xx.s # MCU startup assembly
See CONTRIBUTING.md and the Development Setup Guide for detailed toolchain installation instructions.