Rudder controller

From 'Engineers of Innovation Wiki'
Jump to navigation Jump to search

The rudder controller (project name staartstuk controller) is the CAN-bus node that operates everything inside the new main propulsion unit described on the Rudder page. It replaces the off-the-shelf prototyping platform listed in the requirements table there with a purpose-built 4-layer board.

One board handles four jobs: it positions the back hydrofoil actuator, drives and supervises the cooling-water pump, measures the water cooling circuit, and reports the steering angle. It has no local user interface — every command arrives over the CAN-bus and every measurement leaves over it.

At a glance

Rudder controller board summary
Property Value
Microcontroller STM32L471RGT6 (Cortex-M4F, LQFP64, 1 MB flash, 96 KB SRAM)
System clock 80 MHz, from a 16 MHz crystal through the PLL
Supply 24 V from the CAN-bus cable; on-board 3.3 V step-down module
Bus CAN 2.0A, 1 Mbit/s, 11-bit standard identifiers
PCB 4 copper layers, roughly 88 × 45 mm
Application type 0x01 — its address in the bootloader protocol
Firmware Rust, embassy async runtime, binary rudder-controller
Firmware source firmware/ in the eoi-can monorepo
Hardware source Altium project AKD-Rudder_Controller (Altium 365)
Bus-wide message list CAN_MESSAGES.md
Field update Over CAN, no debug probe needed — see #Firmware update over CAN

The same firmware repository also builds two other boards, the height-sensor controller and the e-paper dashboard. Those are different PCBs; they share the microcontroller, the clock setup, the CAN driver and the bootloader, but nothing else. This page covers the rudder controller only.

What the board controls

Hydrofoil actuator

A geared NEMA 11 stepper drives the hydrofoil trim pushrod through a TMC2209 driver. Position is open-loop step counting: there is no encoder, and the only absolute reference is a mechanical end stop found by StallGuard during homing. The commanded position is a dimensionless 1000–2000 setpoint (servo convention) spanning the full mechanical travel, which is roughly 12 degrees of foil movement.

  • Motor: StepperOnline 11HS12-0674D-PG14 — 0.67 A per phase, 200 full steps per revolution, 13.73:1 planetary gearbox
  • Driver: TMC2209 at 8 microsteps, configured over a single-wire UART at 115200 baud
  • Run current about 0.47 A rms, reduced to about 0.31 A rms while homing into the stop
  • Current sensing through 100 mΩ 1 W resistors, giving roughly 1.06 A rms full scale
  • Step rate ramps from 400 Hz to a 2 kHz cruise, about 33 degrees per second at the foil shaft
  • Full travel is 20000 microsteps

Behavior is covered under #Back-foil servo behavior.

Cooling-water pump

The board switches and current-limits the 24 V circulation pump (ZC-A210) through a TI DRV8256E H-bridge, and reports the driver's fault line.

  • The current limit is set by the microcontroller's DAC feeding the driver's VREF input, which is why it can be changed in firmware rather than by swapping resistors. The driver's relation is I_max = VREF / 0.66 V per amp; the firmware programs 1.0 A currently.
  • The driver's SLEEP pin is held awake and PHASE (direction) is held forward as static configuration. Only ENABLE is switched.
  • The pump only runs while the battery is actually discharging. The board watches BMS message 0x107 and enables the pump only while its discharge state reads On. If no BMS frame arrives for 2 seconds the pump is disabled. This is deliberate: there is no point circulating water when the drive is not drawing power, and a silent bus must not leave the pump running unattended.
  • The FAULT output is open-drain and active-low, pulled up inside the microcontroller. Its raw level is broadcast once per second on 0x212.

Cooling-circuit and motor instrumentation

Two DWS-MH-02 flowmeters were designed in, one on the cooling inlet and one on the outlet. Each provides a pulse output and a built-in NTC. The pulse output is counted in hardware by a timer in external-clock mode, so no pulse is lost to interrupt latency, and the count is converted to mL/min once per second.

Only the inlet flowmeter is read by the current firmware. The outlet flowmeter's NTC input shares a microcontroller pin with the motor NTC, and the motor temperature won that pin, so 0x216 (FlowSensorOut) is not transmitted. The code for it still exists and can be re-enabled if the pin conflict is resolved in a later board revision.

  • Flow scaling: 22.9 Hz corresponds to 1880 mL/min (datasheet)
  • Both NTC inputs sit in a divider with 47 kΩ to 3.3 V, a 1 kΩ series resistor and a 100 nF filter capacitor, sized for the flowmeter's 50 kΩ (25 °C, B = 3950 K) thermistor
  • An NTC reading at either rail (open or shorted) reports the sentinel value −32768 rather than an absurd temperature

Note: see #Known gaps the motor temperature on address 0x217 is deprecated. Currently on the boat, the motor temperature shown on the display comes from the separate motor-NTC node on 0x219 instead.

Steering angle

A potentiometer on the steering column is read by ADC1 and reported ten times per second as a normalized position: −1000 fully left, 0 centred, +1000 fully right. The potentiometer has no fixed raw-to-angle relation, so the three reference positions are calibrated on the vehicle and stored in the board's emulated EEPROM. See #Steering angle calibration.

The sensor connector's third pin is a presence detect: the sensor pulls it to ground, so with the internal pull-up a high reading means nothing is plugged in.

Both the 3.3 V feed and the ground return of this connector go through 47 Ω series resistors, so a shorted sensor cable cannot pull the board's rails down.

Board temperature

A Würth WSEN-TIDS sensor on I²C2 (address 0x3F, its address pin tied low) reports the PCB temperature once per second on 0x211 in hundredths of a degree. This is a board health measurement, not a process measurement — it is how you tell whether the sealed compartment is cooking.

Hardware

Power

24 V arrives on the CAN-bus connector and splits three ways:

  • Logic — through a resettable PTC fuse (30 V, 100 mA hold) into a Würth 173950336 fixed step-down module that produces the 3.3 V rail.
  • Pump — straight to the DRV8256E, with a 150 µF aluminium-polymer bulk capacitor local to the driver.
  • Stepper — straight to the TMC2209, with its own 150 µF bulk capacitor.

The two motor rails are unfused on the board, so the inrush requirement from the Rudder requirements table is met by the bulk capacitors rather than by current limiting. The USB port is separately powered from its own VBUS through a second PTC fuse and a 3.3 V LDO, so plugging in USB does not back-feed the 24 V side.

Key components

Principal components
Designator Part Function
U1 STM32L471RGT6TR Microcontroller
U2 DRV8256ERGER H-bridge for the cooling pump
U3 TCAN3403DRBRQ1 3.3 V CAN FD transceiver with standby control
U4 FT234XD-R USB-to-UART bridge on the USB-C port
U5 Würth 173950336 24 V to 3.3 V step-down module
U6 TMC2209-LA-T Stepper driver for the trim actuator
U7 AP2138N-3.3 3.3 V LDO for the USB side only
U8 Würth 2521020222501 WSEN-TIDS I²C board temperature sensor
X1 Würth 830069392 16 MHz crystal — system clock
X2 Würth 830062558 32.768 kHz crystal — not used by the firmware
SW1 Würth 428542320816 WS-ROSV sealed IP67 rotary switch, 4-bit — not read by the firmware
D2 Würth 156120M173000 RGB status LED
F1, F2 1210L010WR, 0ZCK0010FF2G Resettable PTC fuses — logic rail and USB VBUS

Connectors

All field connectors are Molex Micro-Lock Plus, 1.25 mm pitch, top entry SMD.

Connector overview
Designator Part Positions Purpose
J3, J4 505568-0581 5 CAN-bus in and out — daisy chain
J6 505568-1071 10 To the Rudder. Contains Stepper, pump and inlet flowmeter
J2 505568-0581 5 Outlet flowmeter
J5 505568-0471 4 Steering angle sensor
J1 Würth 490107670812 8 Debug — SKEDD solderless connector
USB1 USB4145-03-0070-C USB-C Serial console over USB

J3 and J4 — CAN-bus (5-pin)

The two connectors are wired in parallel so the board sits in the middle of a daisy chain. All five signals, including the safety line, pass straight through.

Pin Signal
1 Safety
2 24 V
3 GND
4 CAN L
5 CAN H

Note: this is the board's own pin order. It is not the same order as the 5-pin Binder connector on the CAN-bus cable — check the harness, do not assume the numbering carries across.

The safety line is a pass-through between J3 and J4 only. It is not connected to the microcontroller, so this board neither reads nor breaks the safety circuit.

J6 — stepper, pump and inlet flowmeter (10-pin)

Pin Signal Notes
1 3.3 V Flowmeter supply, through a 47 Ω series resistor
2 Inlet flowmeter NTC 47 kΩ divider to 3.3 V
3 Inlet flowmeter pulse Hardware pulse counter
4 GND
5 Stepper A1 Coil A
6 Stepper A2 Coil A
7 Stepper B1 Coil B
8 Stepper B2 Coil B
9 Pump +
10 Pump −

The motor's wire colours are listed in the connection table on the Rudder page (red B+, black A+, blue B−, green A−). Which end of each coil goes to A1 versus A2 is not fixed by the schematic — check it against the motor datasheet before first power-up, because reversing a coil reverses the direction of travel and the homing direction is fixed in firmware.

Both motor loads share this one connector, so check its per-contact current rating against the 1.0 A pump limit plus the stepper phase current before running both at full load.

J2 — outlet flowmeter (5-pin)

Pin Signal
1 3.3 V, through a 47 Ω series resistor
2 Outlet flowmeter pulse
3 Outlet flowmeter NTC
4 Not connected
5 GND

Not read by the current firmware.

J5 — steering angle sensor (4-pin)

Pin Signal
1 3.3 V, through a 47 Ω series resistor
2 Potentiometer wiper — ADC input
3 Presence detect — pull to ground to signal "sensor connected"
4 GND, through a 47 Ω series resistor

J1 — debug

Standard SKEDD SWD programming connector: the programmer's cable presses directly into the board, so no header has to be fitted or removed.

Pin Signal Notes
1 VTREF (3.3 V)
2 SWDIO
3 SWCLK
4 SWO
5 J-Link RX Footprint only, not connected
6 J-Link TX Footprint only, not connected
7 RESET
8 GND

Microcontroller pin map

Every assignment below was cross-checked against both the schematic and the firmware source.

STM32L471RGT6 pin assignment
Pin Peripheral Net Function
PA0 TIM2_CH1 COUNT_FLOW_IN Inlet flowmeter pulse input, hardware counter on rising edges
PA1 ADC2 NTC_FLOW_IN Inlet flowmeter NTC
PA2 TIM15_CH1 COUNT_FLOW_OUT Outlet flowmeter pulse input (unused by firmware)
PA3 ADC2 NTC_FLOW_OUT Used by firmware as the motor NTC (see #Known gaps)
PA4 DAC1_OUT1 SET_CURR Pump driver VREF — sets the current limit
PA5 GPIO out SLEEP Pump driver awake, held high
PA6 GPIO out ENABLE Pump on/off, gated by the BMS discharge state
PA7 GPIO out DIR Pump direction, held low (forward)
PA9 USART1_TX TX_UART1 USB serial console (unused by firmware)
PA10 USART1_RX RX_UART1 USB serial console (unused by firmware)
PA13 SWDIO SWDIO Debug
PA14 SWCLK SWCLK Debug
PB1 ADC1 POT_ANALOG_IN Steering angle potentiometer
PB2 GPIO in, pull-up POT_FEEDBACK Steering sensor presence detect — low means connected
PB3 STEP_INDEX TMC2209 INDEX (unused)
PB4 GPIO in STEP_DIAG TMC2209 DIAG — high on a StallGuard stall
PB5 GPIO out STEP_EN TMC2209 ENABLE, active low
PB7 GPIO out CAN1_STBY Transceiver standby, held low so the transceiver is active
PB8 CAN1_RX CAN1_RX CAN-bus
PB9 CAN1_TX CAN1_TX CAN-bus
PB10 I2C2_SCL I2C2_SCL Board temperature sensor, 10 kΩ pull-up
PB11 I2C2_SDA I2C2_SDA Board temperature sensor, 10 kΩ pull-up
PB12 GPIO in CAN_SETTING1 Rotary switch, weight 1 (unused by firmware)
PB13 GPIO in CAN_SETTING4 Rotary switch, weight 8 (unused by firmware)
PB14 GPIO in CAN_SETTING2 Rotary switch, weight 2 (unused by firmware)
PB15 GPIO in CAN_SETTING3 Rotary switch, weight 4 (unused by firmware)
PC1 GPIO out GREENn Green LED — heartbeat, toggles once per second
PC2 GPIO out REDn Red LED — the bootloader double-flashes it
PC3 GPIO out BLUEn Blue LED
PC5 GPIO in, pull-up nFAULT Pump driver fault, open drain, active low
PC10 GPIO out STEP_DIR TMC2209 DIR
PC11 GPIO out STEP TMC2209 STEP
PC12 UART5_TX TX_UART5 TMC2209 single-wire UART, through 1 kΩ
PD2 UART5_RX RX_UART5 TMC2209 single-wire UART

Watch the rotary switch bit order. The weights are not in pin order: PB12 is 1, PB14 is 2, PB15 is 4 and PB13 is 8. There are no external pull-ups on these lines — the schematic expects the microcontroller's internal pull-ups to be enabled, and the switch pulls each bit to ground.

Clocking

The board runs at 80 MHz derived from the 16 MHz crystal (×10 PLL, ÷2). The firmware starts the crystal itself with a bounded wait rather than letting the runtime spin on it forever, so a dead crystal produces a diagnosable degraded boot instead of a silent hang. If the crystal does not start, the firmware falls back to the internal 16 MHz oscillator, logs an error and keeps running — but CAN is unreliable in that state, because the internal oscillator's ±1 % accuracy is not good enough for 1 Mbit/s. A board that intermittently drops off the bus is worth checking here first.

The 32.768 kHz crystal X2 is in the design, but the firmware keeps the low-speed oscillator switched off: boards were found to hang waiting for it to start, and nothing in the firmware uses the RTC. The embassy 32.768 kHz time base is unrelated and is derived from a general-purpose timer.

The ADCs are clocked from the system clock.

CAN messages

The board joins the bus at 1 Mbit/s using 11-bit standard identifiers and an accept-all hardware filter, so it sees every frame on the bus and ignores what does not concern it. All multi-byte fields are little-endian.

Transmitted by the rudder controller

Frames the rudder controller sends
ID Message DLC Rate
0x020 ServoRudderStatus 6 every 100 ms
0x032 Bootloader response 2–8 on request only
0x211 TemperatureRudderController 2 every 1 s
0x212 CoolingPumpStatus 1 every 1 s
0x213 SteeringAngle 5 every 100 ms
0x215 FlowSensorIn 8 every 1 s
0x217 MotorTemperature 4 every 1 s
0x218 SteeringAngleCalibrationAck 6 once per accepted calibration command

0x216 (FlowSensorOut) is allocated to this board but not transmitted by the current firmware.

Received by the rudder controller

Frames the rudder controller acts on
ID Message DLC Effect
0x010 ServoRudderSetpoint 2 Commands the back-foil position and feeds the 2 s watchdog
0x021 ServoRudderCommand 1 0x00 = Initialize, starts homing
0x030 Bootloader discovery 1 Answers with state and version on 0x032
0x031 Bootloader command 1 Addressed to this board only
0x033 Bootloader write data 8 Firmware payload during an update
0x107 BMS TemperaturesAndStates 8 Byte 7, the discharge state, gates the cooling pump
0x214 SteeringAngleCalibration 1 Captures a calibration reference point

Every other frame on the bus is ignored.

Message layouts

0x010 — ServoRudderSetpoint (received)

Byte Field Type Values
0–1 Setpoint u16 LE 1000–2000. Values outside this range are rejected, and a rejected setpoint does not feed the communication watchdog.

0x020 — ServoRudderStatus (sent, 10 Hz)

Byte Field Type Values
0 State u8 enum 0 = Uninitialized, 1 = Operational, 2 = Homing, 3 = FailSafe, 4 = Fault
1–2 Current setpoint u16 LE 1000–2000
3–4 Actual position u16 LE 1000–2000, in setpoint units
5 Fault cause u8 enum 0 = None, 1 = StallDuringMove, 2 = HomingTimeout, 3 = DriverNoUartResponse, 4 = DriverError

0x021 — ServoRudderCommand (received)

Byte Field Type Values
0 Command u8 enum 0 = Initialize. Starts homing from any state, and is the only way out of FailSafe or Fault.

0x211 — TemperatureRudderController (sent, 1 Hz)

Byte Field Type Values
0–1 Board temperature i16 LE Hundredths of a degree Celsius. 2500 = 25.00 °C.

0x212 — CoolingPumpStatus (sent, 1 Hz)

Byte Field Type Values
0 Driver fault input level u8 Raw pin level: 0 = fault asserted, 1 = healthy

This reports the driver's fault line only. It does not say whether the pump is currently enabled — that follows from the BMS discharge state on 0x107.

0x213 — SteeringAngle (sent, 10 Hz)

Byte Field Type Values
0–1 Position i16 LE −1000 full left to +1000 full right, 0 = centre. Held at 0 whenever the calibration is not valid.
2–3 Averaged raw ADC code u16 LE 0–4095, 12-bit
4 Status bits u8 See below
Steering status bits — byte 4 of 0x213, byte 5 of 0x218
Bit Name Meaning
0 CalValid Calibration present and plausible; the reported position is meaningful
1 CalMissing Nothing stored yet, or every stored record is corrupt
2 CalInvalid Stored but incomplete or implausible
3 OutOfRange Raw reading is outside the calibrated travel; position is clamped
4 StorageError The last write to persistent storage failed; sticky until one succeeds
5 NotConnected Presence pin reads high, so no sensor is plugged in

Bits 0, 1 and 2 are mutually exclusive. Bit 5 is independent of the others: a stored calibration stays valid while the sensor is unplugged.

0x214 — SteeringAngleCalibration (received)

Byte Field Type Values
0 Command u8 enum 1 = CaptureLeft, 2 = CaptureCenter, 3 = CaptureRight, 4 = Clear

Remaining bytes are ignored. No calibration values travel over the bus — the board captures whatever the sensor reads at the moment the command arrives.

0x215 — FlowSensorIn (sent, 1 Hz)

Byte Field Type Values
0–1 Flow rate u16 LE mL/min
2–3 Water temperature i16 LE Hundredths of a degree Celsius. −32768 means the NTC reads open or shorted.
4–5 Raw pulses u16 LE Pulses counted in the last one-second window
6–7 Raw ADC code u16 LE 0–4095, 12-bit NTC reading

0x217 — MotorTemperature (sent, 1 Hz)

Byte Field Type Values
0–1 Motor temperature i16 LE Hundredths of a degree Celsius. −32768 means the NTC reads open or shorted. See #Known gaps.
2–3 Raw ADC code u16 LE 0–4095, 12-bit

0x218 — SteeringAngleCalibrationAck (sent)

Byte Field Type Values
0 Echoed command u8 As received on 0x214
1 Result u8 enum 0 = ok, 1 = storage error, 2 = stored but the set is not yet usable
2–3 Captured raw ADC code u16 LE 0 for Clear
4 Captured endpoints u8 bits 0x01 = left, 0x02 = centre, 0x04 = right
5 Status bits u8 Same encoding as byte 4 of 0x213

0x107 — BMS TemperaturesAndStates (received)

Only byte 7 is used by this board.

Byte Field Type Values
7 Discharge state u8 enum 0 = Init, 1 = Idle, 2 = PreChargeOn, 3 = On, 4 = PreChargeTimeout, 5 = Error

The cooling pump is enabled only while this byte reads 3.

Back-foil servo behaviour

State machine

Servo states
State Meaning Setpoints Motor
0 Uninitialized Boot state. No absolute position known. Ignored Driver disabled, shaft free
1 Operational Homed and following setpoints. Followed Energized, hold current at standstill
2 Homing Running the homing sequence. Ignored; the latest is picked up on entering Operational Energized, reduced current
3 FailSafe Setpoint watchdog expired; parked at setpoint 1000. Ignored, latched Energized, holding
4 Fault See the fault causes below. Ignored, latched Holding if the recovery re-home succeeded, disabled otherwise

Transitions:

  • Initialize on 0x021 from any state moves to Homing. This is the only way out of FailSafe and Fault — recovery is a deliberate operator action, never automatic.
  • Homing success moves to Operational and the watchdog starts immediately. If no setpoint arrives within 2 seconds the servo parks in FailSafe.
  • Homing failure moves to Fault with the driver disabled.
  • Watchdog: in Operational, every valid setpoint re-arms a 2-second timer. On expiry the servo moves to setpoint 1000 and latches FailSafe.
  • Stall while moving: if DIAG trips during a move the step count can no longer be trusted, so the servo latches Fault (StallDuringMove) and immediately re-runs homing to park, holding, at the failsafe position. It stays in Fault until Initialize.

Homing sequence

  1. Read the driver's IFCNT register over UART, three attempts. No response gives Fault (DriverNoUartResponse) with the driver left disabled.
  2. Write the five configuration registers, then read IFCNT again and require it to have advanced by exactly 5. A mismatch gives Fault (DriverError) — this catches a driver that answers but is not actually accepting writes.
  3. Enable the driver and step at constant speed toward the home stop until DIAG trips. The StallGuard load value is logged about every 100 ms for threshold tuning.
  4. If 1.2× the full travel is stepped without a stall, give up with Fault (HomingTimeout).
  5. Back off 200 microsteps from the stop. That position is defined as position 0, which is setpoint 1000. Switch to the normal run current and go Operational.

Fault causes

Fault cause, byte 5 of 0x020
Value Cause What to check
0 None
1 StallDuringMove Mechanism jammed, or the StallGuard threshold is too sensitive. The servo re-homed itself and holds at 1000. Clear the jam, then send Initialize.
2 HomingTimeout No stall found: motor unplugged, driver unpowered, threshold too insensitive, or the homing direction inverted.
3 DriverNoUartResponse TMC2209 not answering: no driver supply, UART wiring, or wrong slave address.
4 DriverError UART works but register writes did not stick.

Tuning

The tuning constants sit in one block at the top of firmware/app/src/servo_rudder.rs, with a bring-up checklist in firmware/docs/rudder-servo.md. The values that must be calibrated against the real mechanics before the servo is trusted are the full travel in microsteps, the StallGuard threshold, and the direction level that moves toward the home stop.

A quick bench sweep:

cansend can0 021#00      # Initialize (home)
cansend can0 010#E803    # setpoint 1000
cansend can0 010#D007    # setpoint 2000

Steering angle calibration

Procedure

Calibration is captured live — no values are sent over the bus. Move the steering to a reference position, then send the matching command on 0x214. The board averages readings over 200 ms and stores the result immediately, so a session can be interrupted and resumed.

  1. Steer fully left, send 0x01
  2. Steer to centre, send 0x02
  3. Steer fully right, send 0x03

Order does not matter. Hold the steering still for the 200 ms capture. The calibration becomes active on the next sample once all three points are captured and plausible. 0x04 clears it and returns the board to the uncalibrated safe state.

cansend can0 214#01      # capture full left
cansend can0 214#02      # capture centre
cansend can0 214#03      # capture full right
cansend can0 214#04      # clear

A calibration is rejected, and the reported position forced to 0, when any of the three points is missing, when a raw code exceeds 4095, when the center does not lie strictly between the two endpoints, or when either half of the travel is narrower than 100 ADC codes. Both wiring polarities are accepted — full left may read either above or below full right.

Where it is stored

The STM32L471 has no data EEPROM, so a dedicated 4 KB block at the very end of flash emulates one. It sits outside the application partition, so a firmware update over CAN leaves the calibration intact, and it is in the other flash bank from the executing code, so writing it never stalls the processor. The block is an append-only log of 256 records, each with a magic number, version and CRC. The newest record that validates wins on load; when the log fills, the block is erased and writing restarts.

Noise rejection

The potentiometer signal is electrically noisy — the stepper driver and the pump are both close by. Two layers of averaging handle it, because either alone leaves a gap:

  • The ADC averages 16 conversions per read in hardware. That suppresses white noise by 4×, but the burst spans only about 52 µs, so it does nothing for interference slower than roughly 20 kHz.
  • Twenty reads are spread evenly across each 100 ms reporting window and box-averaged. Averaging over exactly one window puts nulls at 10 Hz and every harmonic, which is what rejects the periodic interference from the stepper and pump.

Together that is 320 conversions per report for about 1 % ADC duty cycle and roughly 18× white-noise rejection, at the cost of 50 ms of group delay. Calibration captures average 40 reads over 200 ms, so an endpoint is never set by a single noisy moment.

Firmware update over CAN

The first 80 KB of flash holds a CAN bootloader, so the board can be updated in place without opening the compartment or attaching a debug probe. The bootloader validates the application on boot (magic number plus CRC32) and auto-boots it two seconds after the last command addressed to this board.

Flash layout

Region    Address Range               Size   Description
--------  --------------------------  -----  --------------------------
BOOT      0x08000000 - 0x08013FFF     80K    Bootloader
HEADER    0x08014000 - 0x080147FF     2K     Application metadata header
APP       0x08014800 - 0x080FEFFF     938K   Application firmware
CONFIG    0x080FF000 - 0x080FFFFF     4K     Emulated EEPROM (steering calibration)

Addressing

Because several boards share the bus, each board type owns its own block of three identifiers derived from its application type, and the bootloader's hardware filter rejects the other blocks outright. This is what stops an erase command aimed at one board from wiping the others.

Bootloader identifier allocation
Board App type Command Response Write data
Rudder controller 0x01 0x031 0x032 0x033
Height-sensor controller 0x02 0x034 0x035 0x036
Dashboard 0x03 0x037 0x038 0x039

0x030 is a discovery broadcast. The host sends a state query on it and every board answers on its own response identifier, which is how the bus is enumerated. Answering discovery does not extend a board's auto-boot window, so repeated scanning cannot pin boards in the bootloader.

A running application answers state and version queries too, and rejects everything destructive. That is how the host tool tells a bootloader from a running application on one identifier, and why updating a board reboots it first rather than erasing under a live application.

Using the flash tool

Set up the interface on the host once:

sudo ip link set can0 type can bitrate 1000000
sudo ip link set can0 up

Then:

eoi-flash-tool scan                                  # what is on the bus?
eoi-flash-tool flash rudder-controller               # target comes from the ELF
eoi-flash-tool --board rudder-controller version     # which build is on it?
eoi-flash-tool --board rudder-controller reboot      # reset just this board

flash needs no board argument: the target is read from the firmware image itself, so pointing the tool at a file can only ever address the board that file was built for. The other boards keep running untouched.

The bootloader itself can only be replaced over SWD, so it routinely sits several commits behind the application it boots. The version command reports which of the two answered.

Diagnostics

Reading the board without a debugger
Symptom Meaning
Green LED toggling once per second Application running normally
Red LED double-flashing Sitting in the bootloader, waiting or flashing
No LED activity No power, or a boot hang
On the bus but dropping frames Suspect the 16 MHz crystal; the firmware falls back to the internal oscillator, which is not accurate enough for 1 Mbit/s
0x020 state stuck at 0 The servo has never been homed since power-up. Send Initialize.
0x020 state 3 (FailSafe) No setpoint arrived for 2 seconds. Whatever commands the foil has stopped talking.
0x213 position stuck at 0 Steering calibration missing or invalid; read the status byte
0x213 status bit 5 set Steering sensor not plugged in
0x212 byte 0 reads 0 Pump driver is reporting a fault

A 4-second independent watchdog runs in hardware and is petted once per second by the heartbeat task, so wedged firmware resets itself rather than sitting silent on the bus. Detailed logging is available over SWD through defmt.

Known gaps

  • Outlet flowmeter is not read. 0x216 is allocated and J2 is fitted, but its NTC line shares a pin with the motor NTC. Resolving this needs a board change, not a firmware change.
  • Servo tuning is not finished. The full travel in microsteps, the StallGuard threshold and the homing direction are all still marked for calibration against the real mechanics in the firmware. Until they are measured, homing behaviour on the real mechanism is not guaranteed.
  • The rotary switch is not read. A sealed 4-bit hex switch is fitted and wired to PB12–PB15, but no firmware uses it. It was presumably intended for a node address or a variant selection.
  • The USB console is not used. The USB-C port and its FT234XD bridge are wired to USART1 (PA9/PA10), but the firmware logs over SWD instead. The port currently only supplies its own 3.3 V rail.
  • The bus-wide reference is out of date for this board. CAN_MESSAGES.md still describes 0x213 as an angle in degrees over 4 bytes and 0x214 as reserved. The firmware sends a normalized ±1000 position over 5 bytes with a status byte, and the calibration protocol is implemented. This page follows the firmware.
  • Motor specification mismatch. The Rudder page lists the trim actuator as an 11HS12-0674S-PG5 (5:1); the firmware is tuned for an 11HS12-0674D-PG14 (13.73:1). One of the two is wrong, and it changes the travel and speed figures.

See also

  • Rudder — the propulsion unit this board lives in
  • CAN-bus — bus wiring, cable pinout and the bus-wide identifier map
  • Battery — source of the discharge state that gates the cooling pump
  • Foils — what the back-foil trim actuator is for
  • Instrumentation Panel — where these measurements are displayed
  • Datalogger — where they are recorded
  • Altium PCB — house PCB design conventions