UBC Sailbot

Polaris in the water

Polaris in the water

Overview

I joined UBC Sailbot in the fall of 2025 and work on the firmware for the microcontrollers on Polaris, our autonomous sailboat.

My main contributions were:

The IMU itself has its own page: Polaris' IMU. I also competed in the team's hackathon.

Motivation

Polaris is meant to sail for months without anyone on board, so the small controllers that read sensors and move the rudder and sail need to keep working without anyone around to reset them.

Architecture

Every device on the boat is run by a "COM module", which is an STM32U5 Nucleo board plugged into a custom breakout board made for that device. The Nucleo is the same on every module, so all of the controllers (wingsail, rudder, power distribution, and sensors) live in one repository and share code for things like CAN and sensor drivers. Each controller talks to the main computer over a shared CAN bus.

Implementation

Wingsail controller

The wind sensor, GPS, and AIS receiver all send plain-text serial messages in the same marine format (NMEA 0183), so I wrote one parser for all three and turned each message into a proper struct instead of raw numbers. The controller sends the latest data to the main computer over CAN twice a second.

I didn't have the real sensors at first, so I sent fake messages from an Analog Discovery 3 into the Nucleo to test the parser on my desk. I also wrote unit tests that run the parser on my laptop, so I could test changes without flashing a board each time.

Fake AIS messages sent from the Analog Discovery 3 (left) and what the Nucleo received (right)

NMEA messages printing

I set up a makefile and language server config so I could build, flash, and debug from my own editor and GDB instead of STM32CubeIDE.

Stepping through the wind sensor code in GDB

GDB debugging

Rudder controller and the IMU

The rudder used to get its heading from an electronic compass. I replaced it with a link to our new IMU, which sends the rudder its heading, heel, and turning rate. If the IMU stops sending, or isn't confident in its heading, the rudder keeps steering on the last good heading instead of a bad one.

Merging the branches

Each board had its own working branch, and they had been drifting apart for months, with four branches each carrying their own copy of the shared CAN code. I merged all four into main in one pull request, so the conflicts were sorted out once instead of by whoever merged last. For shared code I kept the version that was actually safe to share. For example, I kept the rudder's copy of the CAN code because it had the fix for the bench freeze described below.

In the same pull request I set up the team's first CI, which runs the unit tests and checks each board's peripheral config against its generated code, and I pinned the STM32 tool versions with Nix so CI and everyone's laptops match. I also wrote down a workflow where every test on the boat is flashed from a tagged release, so we always know exactly what firmware was running.

Checking the rudder refactor

While the rudder was being tuned on one branch, a teammate refactored the rudder control code on another. After the merge nobody could say for sure if the merged controller still behaved like the one that had been on the water, so I built both versions and fed them the same 200 randomized runs to compare their rudder outputs.

The boat only runs the "sail straight" part of the controller right now, and on that part the two matched except for two bugs, both from before the refactor:

  1. The integral part of the controller was being added twice each cycle on the tuning branch, so it ran 1.5x stronger than it should have. The gains from the dryland tuning sessions were tuned against this, so they need to be re-checked before the next time the boat is on the water.
  2. The controller's starting state was never zeroed, so it started with whatever was left in memory.

The rest of the controller (tacking, gybing, getting out of irons) is turned off on the boat, and the refactor changed it on purpose. I found a few more bugs in that part and added tests for the part the boat actually runs, since the existing tests only covered the part that was turned off. Those fixes are still in an open pull request.

Debugging

  1. The main computer was getting stale wind data
    • Solution: The wind sensor sends a wind message and a temperature message back to back, and the controller was sending wind data to the main computer after both. The temperature message doesn't update the wind, so every second send was old data. I changed it to only send after the wind message.
  2. The rudder controller would freeze on the bench and stop reading the IMU
    • Solution: When a CAN message failed to send, the code called the error handler, which stops the whole board. On the bench there was no CAN bus to acknowledge messages, so this happened all the time. I changed it to count the failures and keep going.
  3. The wingsail controller crashed when its serial receive buffer filled up
    • Solution: I changed the buffer to overwrite the oldest message instead of faulting.

Repositories