This repository contains the IMU firmware for Polaris.
This project's toolchain is based on PlatformIO, configured for the RP2040.
A dev shell is provided in flake.nix. If you use
direnv, it will load automatically when you enter the
directory. Otherwise:
nix developInstall Nix using the Determinate installer:
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
mkdir -p ~/.config/nix/
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.confThen install direnv through Nix (the apt package is too old to support use flake):
nix profile add nixpkgs#direnv
echo '# Direnv shell hook'
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
exec bash
direnv allowInstall Nix using the Determinate installer:
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
mkdir -p ~/.config/nix/
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.confThen install direnv through Nix:
nix profile add nixpkgs#direnv
echo '# Direnv shell hook'
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
exec zsh
direnv allowThe recommended path is WSL2 with Ubuntu 24.04. Install WSL2 from PowerShell:
wsl --install -d Ubuntu-24.04Once in WSL, follow the Linux instructions above.
For flashing, the RP2040 needs to be forwarded from Windows into WSL using usbipd-win:
usbipd list # find the RP2040 bus ID
usbipd bind --busid <id>
usbipd attach --wsl --busid <id>Skip this section if you've already configured git and your GitHub credentials.
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
git config --global core.autocrlf false # Very important you don't miss this on WSL!Linux:
sudo apt update && sudo apt install ghmacOS:
brew install ghThen authenticate:
gh auth loginmake test # host Unity tests
make build # build firmware
make upload # flash the RP2040
make format # clang-format all sources in-placeRun make with no arguments to list all targets.
A Python harness at sim/ runs the C++ EKF offline for visualization and
tuning. The same filter that ships on the RP2040 runs in the sim, so Q values
transfer faithfully. Synthetic trajectories (yaw, heel, pitch), injectable IMU
and GNSS noise, and three views are available.
make sim drops into an arrow-key picker — choose a view, then a scenario:
Timeseries — heading, roll, and pitch channels; truth, GNSS measurements, EKF estimate, and open-loop gyro integration overlaid; residual vs truth with ±1σ band on the lower axes:
Mounting — calibration geometry: a level hull with the IMU triad rotated by the mount offset and the GNSS baseline arrow:
Simulate — animated 3D boat over a 50 s run; truth (blue), EKF estimate (orange), IMU raw attitude (green), rendered as a GIF. Final frame:
A static pose filmstrip is also available via --view pose on the CLI:
make sim # interactive picker
make sim SCENARIO=wave_tack VIEW=timeseries # skip the picker
make sim SCENARIO=wave_tack VIEW=simulate
make sim-test # pytest suite
make sim-format # ruff format + checkDirect CLI with noise, EKF, and calibration flags:
cd sim
uv run python -m plrs_sim sim wave_tack --view simulate \
--duration 50 --seed 7 \
--gyro-bias 0.02 --gnss-std 2.0 --gnss-dropout 0.1 \
--q-heading 0.05 \
--mount-roll 8 --baseline-offset 20 \
--save /tmp/wave_tack.gifA 0 for any --gyro-*, --gnss-std, or --mti-attitude-std flag disables
that effect. Pass --no-show for headless runs. See
python -m plrs_sim sim --help for the full flag list.
Pre-push hooks are tracked in hooks/ and mirror the CI checks (clang-format
- native tests). If you use direnv, they are installed automatically when you enter the directory. Without direnv, install them once manually:
ln -sf ../../hooks/pre-push .git/hooks/pre-pushSee CONTRIBUTING.md.
The goal is heading accuracy of ≤2° on Polaris by fusing IMU and GNSS measurements through a Kalman filter (likely an Extended Kalman Filter given time constraints).
TinyEKF: Lightweight C/C++ Extended Kalman Filter.
See docs/tuning.md for theory, tradeoffs, and the record-and-replay workflow. Summary:
The filter has four parameters in TinyEkfFilter::Config.
q_heading_deg2 and q_bias_deg2_s2 are the load-bearing knobs. Derive
starting values from the MTi-3 datasheet rather than guessing:
q_heading— gyro noise density (deg/s/√Hz) squared, scaled by dtq_bias— in-run bias stability (deg/s, from the Allan deviation plot) squared
p0_heading_deg2 and p0_bias_deg2_s2 only affect convergence from startup;
set them large.
The efficient tuning workflow: capture a session with the serial logger (milestone 1), replay it through the Python sim (milestone 2) with different configs, then flash once.
To diagnose a live filter, log the innovation (gnss_heading - filter_heading).
Zero-mean innovations indicate a well-tuned filter; persistently large
innovations mean Q is too small.
MCU: Raspberry Pi RP2040
MEMS IMU (accelerometer, gyroscope, magnetometer): Xsens MTi-3-5A-T
GNSS kit (dual antenna): Septentrio mosaic-go H
| Milestone | Issues |
|---|---|
| Python Logger | Serial capture script, log format, replay utility |
| Python EKF Sim | pybind11 bindings, synthetic data generator, sim runner, plotter |
| HIL Testing | On-device test suite, PIO Remote agent, CI integration |



