← Design notebook

BUILT BY AUTONOMOUS SYSTEMS · WORK IN PROGRESS

Turret firmware plan

The turret's controller is an Adafruit Feather M4 CAN Express (SAME51) on level 2 of the turret bay. This page covers what its firmware does and how it talks to the robot (docs/turret-plan.md task T4, docs/turret-prototype-plan.md task T7): the pin map, the states, pan homing on continuous rotation, the flywheels and the feed, the aiming rules the physics asks for, the CAN message set, and the bench harness. The hardware arm chain (docs/turret-wiring.md section 3) sits under all of it. The firmware can stop the turret, never start it on its own.

The reference implementation is pure Python in firmware/turret/, and the Feather port follows it line for line:

File What it is
config.py every number on this page: the build constants (KV, pole pairs, the eRPM limit, the feed's timings) and the runtime CONFIG the host may set within limits
turret_fw.py the firmware: states, safety gates, homing, aim, bursts, bench commands, telemetry; one tick() a millisecond against a HAL
homing.py pan homing and turn counting (unchanged since T4)
feed.py the dial: the bus port, the driver (step, settle, jam, reverse, retry, dither, drop-off and re-init) and the gate-paced burst
flywheel.py set speed, the 300 ms ramp, the bounded trim, READY, the eRPM guard (the pre-feed check)
sts.py, dshot.py, can_msgs.py the STS3215 protocol, bidirectional DShot with eRPM/EDT telemetry, the CAN set
sim_servo.py, sim_turret.py the simulated servo and turret the tests run against (not firmware)
test_homing.py, test_feed.py, test_firmware.py the proofs (section 8)
bench_host.py the host that runs the bench tests on the turret or on the simulation (section 7)
am32_settings.md the AM32 configurator settings for the two flywheel ESCs

0. What changed for v0 (2026-10-27)

v0 is the turret as drawn, with the slip ring, continuous pan and the pulse-train safety line, plus three swaps for availability (docs/turret-prototype-plan.md section 0). For the firmware:

1. Pin map (Feather M4 CAN Express)

Pin Use Notes
D0 / D1 (Serial1) the Feetech servo bus: pan (ID 1) in the base across ring circuit 9, tilt (ID 2) on the +y arm, feed (ID 3) through CT signal pin 3 half-duplex: TX through the 74AHCT125, RX through a Schottky with a 10 k pull-up (it echoes TX); 500 kbit/s (margin over the ring)
D4 SERVO_DIR the buffer's direction
D5, D6 ESC_L, ESC_R bidirectional DShot300 (timer channels), eRPM and EDT telemetry back on the same pin; held low (no frames) while ARM_IN is low, so nothing feeds an unpowered ESC
D9, D10 GATE_1, GATE_2 the chronograph comparators; interrupts (EXTINT 3, 4) with microsecond timestamps
D11 PAN_INDEX the hall switch (A1120, open drain, pull-up to 3.3 V); interrupt (EXTINT 5) on both edges
D12 ARM_IN the arm chain's output through 100 k (read only; interrupt EXTINT 6)
A1 TRIP_IN the trip latch (read only), polled: A1 shares EXTINT 5 with D11
A3 PULSE_MON the 4N35's output through 100 k: interrupt (EXTINT 9) on falling edges (the opto turning on: the fast edge); the train's period (0.85-1.15 ms), the release, the ring-drop counter. Not A4: A4 shares EXTINT 4 with D10
A5 LASER_EN through the 2N7000 (Quarton VLM-650-03 LPT, Class 2); not D13, which also lights the red LED and which the bootloader pulses on a reset (the laser would flash)
A2 HOPPER_BEAM v1 (not fitted in v0)
SDA NEOPIXEL v1 (not fitted in v0); SCL spare
A0 BUS_V the ring's +24 V through 91 k / 10 k (10.1 : 1)
CAN (on-board transceiver) the turret's CAN segment 1 Mbit/s classic; only the turret and the host are on it now

2. States

BOOT -> HOMING -> IDLE <-> ARMED -> SPIN_UP -> READY <-> FIRING
                    ^         |          |         |         |
                    +---- SAFE <---------+---------+---------+   (any condition lost: zero throttle, dial stopped)
                    +---- FAULT (a latched fault, or homing failed: aim only)

3. Pan homing and continuous pan

The pan servo's 12-bit angle covers one servo turn, 120 deg of pan through 63:21, so at power-up the pan is known only modulo 120 deg. Reference implementation: firmware/turret/homing.py (ported line for line).

  1. Tilt to the 0 deg stop first: then nothing can meet the lidar or the deck while the pan searches.
  2. Sweep in +pan at 60 deg/s (the direction the index was calibrated in) until the hall's window has been crossed: its two edges are timestamped by the interrupt, the servo position by its reads (100 Hz), and the window's centre comes from interpolating the reads at the edge times. The centre sits half the switch's hysteresis past the magnet in a +pan sweep; the assembly calibration measured that offset in the same direction, so it cancels.
  3. Zero the turn counter there: pan = (unwrapped servo counts - counts at the index) x 120 / 4096 deg. The unwrapping is unambiguous at 50 Hz and above (90 deg/s of pan is 270 deg/s at the servo: 5.4 deg a read at 50 Hz).
  4. The gear's 0.44 deg of backlash: the tracker follows the drive's direction and adds the backlash back after a -pan move. Aiming ends every move in +pan (section 5), so the turntable always rests on the same flank.
  5. Every later +pan pass over the index re-checks the count; more than 2 deg off asks the host for a re-home (fault bit REHOME, EVENT REHOME_ASKED).
  6. Drive mode (changed for v0): step mode (mode 3). Every move is one relative step computed from the unwrapped angle, compensated for the read's age. Moves longer than 30,000 counts (~880 deg of pan) are chained in segments, so continuous pan has no window. Position mode's multi-turn window (+-7 servo turns, +-840 deg of pan) could not hold the bench's 10 continuous turns (test 3.5) or an hour at 15 rpm (3.8, 4.9), and moving its frame is not a documented RAM operation. Step mode's reference (the present position or the last goal) is checked on the bench first (test 3.9 on the feed servo); the code supports both. A bus fault leaves the servo holding, never free-running.

Proof (task T4: 1,000 random starts). firmware/turret/test_homing.py runs the reference against a model of the hardware: random start and servo zero, 50-100 Hz reads with jitter and up to 3 % failures, +-1 count of noise, the gear's backlash, a 60 ms drive lag, hall windows of +-1.5..4 deg with 0.3-1 deg hysteresis, +-50 us interrupt jitter, and +-0.3 deg of calibration. It then runs 20 s of random moves up to +-720 deg at up to 90 deg/s with the index re-check. Results are in exports/turret/firmware/homing.json: 1,000 of 1,000 homed, at most 6.7 s, repeatability 0.07 deg, unchanged by v0. On the simulated turret with the full firmware (test 3.5), 50 power-ups homed in at most 6.3 s, with a 0.053 deg spread of the index centre.

4. Flywheels

5. Feeding and aiming

The dial (feed.py) is an STS3215 in step mode, with the pockets at absolute positions (the assembly zero + 512 k), so errors never accumulate.

The burst (feed.py Burst) is paced on the chronograph, because the feed rerun found balls hanging in the exit hole through their own step at -25 deg, when panning at 90-150 deg/s and under a 15 Hz shake, and a step on top of a hung ball releases two.

Aiming (unchanged rules, with the v0 details):

6. CAN message set

The turret has its own CAN segment (DT12 pins 5-6, ring circuits 5-6) to the host: the Jetson's turret port on the robot, the USB-CAN on the bench. Classic CAN at 1 Mbit/s, the 0x300 block, little-endian, at most 8 bytes a frame (can_msgs.py is the definition). The C610 is gone, so the turret and the host are the only nodes.

ID Direction Rate Payload
0x300 HOST_HEARTBEAT host -> turret 10 Hz counter u8, host state u8, flags u8 (bit 0: arm request); older than 200 ms = no arm
0x301 AIM host -> turret on change pan goal i32 (0.01 deg, continuous), tilt goal i16 (0.01 deg; above 0 is clamped), rate limit u8 (deg/s, <= 100)
0x302 FIRE host -> turret on request balls u8, rate u8 (0.1/s, <= 3/s), ball speed u16 (0.1 fps; 0 = the CONFIG default, 52), request id u8
0x303 LASER host -> turret on change on u8
0x304 CONFIG host -> turret at start kind u8, index u8, a i16, b i16. Kinds: 1 depression row (pan 0.1 deg, min tilt 0.01 deg), 2 keep-out (from, to: 0.1 deg), 3 tyre (mm: 34 only), 4 ring type (1 = ring, must match the build), 5 mount (0 bench, 1 robot, 2 box), 6 launch ratio (x 10^4), 7 throttle map row (throttle 0.1 %, rpm), 8 clear a table
0x305 COMMAND host -> turret rare code u8, a u16, b u16, c u8: 1 re-home, 2 stop feeding, 3 spin down, 4 clear faults (line released only); bench codes, accepted only with CONFIG mount = bench: 0x10 dial steps alone (n, rate), 0x11 wheels at an open-loop throttle (0.1 %, hold), 0x12 balls at an open-loop throttle (throttle, balls, rate), 0x13 stream POSE (Hz, s), 0x14 wheels at the set speed (fps, hold), 0x15 a burst that does not wait for a settled aim (balls, fps, rate)
0x310 TURRET_HEARTBEAT turret -> host 10 Hz state u8, fault bits u16, flags u8 (ARM, TRIP, homed, pulses good, armed, re-arm wait, feed servo online, wheels ready), seq u8
0x311 POSE turret -> host 50 Hz (up to 1 kHz streamed) pan i32 (0.01 deg), tilt i16 (0.01 deg), pan rate i8 (deg/s), tilt rate i8 (deg/s)
0x312 WHEELS turret -> host 20 Hz rpm L u16, rpm R u16, ESC temperatures u8 x 2 (EDT)
0x313 SHOT turret -> host per shot shot no u16, speed u16 (0.1 fps), wheel rpm u16, flags u8 (stray, over 66 fps, after a release, gate 1 only, throttle mode, double)
0x314 FEED turret -> host 5 Hz balls fired u16, empty steps u8, jams u8, flags u8 (hopper full [v1], feed fault, servo online, hopper empty), dial angle u8 (360/256 deg)
0x315 POWER turret -> host 5 Hz ring bus voltage u16 (mV), the servos' supply as each reads it u8 x 3 (0.1 V: 12 V PAN, 12 V MAIN (the tilt), 12 V FLY (the feed); 0 = off the bus)
0x316 HEALTH turret -> host 1 Hz CAN TEC u8, REC u8, bus-off count u8, ring-drop counter u16 (missing safety pulses), servo bus errors u16, index re-check worst u8 (0.01 deg)
0x317 EVENT turret -> host per event code u8, seq u8, a u16, b u16, c u16: homed (time, index centre in 1/16 count, swept), home failed, feed step (step, settle ms, load mean, clean flag), jam (step, ms since the command, cause and count in a row), feed fault, empty step, SAFE (cause bits, latency ms, value), servo lost or back, burst done (balls, steps, why), dither, stray ball, re-home asked, bench done, refused (why), lift

What changed in the set, and why it was forced:

This mirrors the simulator's turret (sim/turret.py TurretController): aim within the depression table, the laser before shots, fire only when settled, and report shots. The host-side bridge maps one onto the other.

7. Bench harness

On the stand (cad/turret_stand.py), 24 V comes through the XT60E-M inlet, the rocker switch and the DC-rated 5 A fuse to DT12 pins 1-4. The pendant powers the base's 555 through DT12 pins 7-8. The USB-CAN adapter (a candleLight / gs_usb one, as can0 under socketcan) sits on pins 5-6 to the laptop and terminates that end. The ring map and its mandatory soft start are in docs/turret-wiring.md section 1.2.

firmware/turret/bench_host.py runs every test of docs/turret-prototype-plan.md section 5 that needs the firmware, its CAN link or a reading to record: 3.1-3.9, 4.1-4.9, 5.1-5.5 and 6.1-6.8. Tests 1.x and 2.x need neither. Each test writes its readings and pass/fail to exports/turret/bench/<file>.json (--list shows the files).

8. Proofs in simulation

Test What it proves Result (2026-10-27)
test_homing.py homing and turn counting, 1,000 random starts unchanged: 1,000 / 1,000, max 6.7 s, repeatability 0.07 deg
test_feed.py the feed driver against a simulated STS3215 through the real packet code: 3/s with a jam injected and recovered; three jams fault; the load catches a jam before the timeout; the drop-off on release and the re-init; lost replies never double a step; the step time; the gate-paced burst with hung balls freed by the dither, the 5-ball hopper at -25 deg, the empty hopper all pass: jam caught 68 ms after contact (load), reversed, retried, recovered; clean steps 229 ms mean, 236 max; 6 of 6 hung balls freed by a dither, 0 by a step, 0 doubles; 335 ms step to step
test_firmware.py protocols; 8,855 rpm / 61,985 eRPM; Ø36 refused; the tilt never above level; no spin without ARM; the re-arm; zero throttle when the heartbeat goes; a 300 Hz train refused though the hardware stays armed; the ramp never steps; the 24 V guard; one bad frame no trip; a 75 fps pair latches SAFE; aim live and capped while disarmed; the feed servo's drop-off and re-init through the firmware; a new set speed ramps; no DShot frame while the ESCs are unpowered all 19 pass
bench_host.py --sim every bench test runs end to end 33 of 33 pass on the circuit as drawn now (docs/turret-wiring.md 3, D47)

9. Open items found while building v0 (for the hardware and the plan)