Skip to content

feat(firmware): show amber on a watchdog trip or stall fault - #98

Merged
CameronBrooks11 merged 12 commits into
mainfrom
feat/amber-hold
Nov 1, 2026
Merged

CameronBrooks11 merged 12 commits into
mainfrom
feat/amber-hold

Conversation

@CameronBrooks11

Copy link
Copy Markdown
Member

Requires bread-slice-core 0.2.0 (^0.2.0 in platformio.ini and slice.yaml) and uses its new Hold state: the status LED shows amber while a command-watchdog trip or a latched stall fault (#97) holds the motors, the same hold motorControlLogic() applies minus the e-stop. Red still wins over amber. The e-stop input keeps the internal pull-up. This is the same change as feastorg/Slice_RLHT#83.

test_status_led_hold runs the real 0.2.0 StatusLed with frame capture: running is green; a trip and a stall fault each show amber, including after an e-stop over them is released; an e-stop over either shows red; clearing returns to green. Gen1 drives no LED. Each check fails under a mutation aimed at it: the watchdog term dropped, the fault term dropped, amber winning over red, main's original line, and a hold that stays after clearing.

Host suite: 80 runs, 818 ok, 0 failed, 2 skipped (the speed-freeze test on the Nano envs, as on main). Flash +42 B on gen2 (+14 B of that from the core bump), +2 B on gen1.

The amber (red 50, green 16) has not been seen on a board yet; that is a step for the next bench session.

pio pkg install keeps a release archive's mtimes, so after a pin change
the new CRUMBS sources looked older than the objects built from the old
one, and make kept linking those. A stamp of the installed versions, from
each package's .piopm, is rewritten only when one changes, and the CRUMBS
objects and the test binaries depend on it.

Closes #88
$(wildcard) splits on spaces, and a PlatformIO package directory name
may hold one, which broke the stamp's cat. A shell loop reads each
.piopm whole and skips the pattern when nothing matches.
The host tests stub DCMotorServo and DCMotorTacho, so no test here can
reach the closed-loop behaviour behind #62, #89 and #90. A second set of
binaries, `make motor`, builds main.cpp with the real DCMotorServo,
DCMotorTacho and PID_v1 from libdeps: their directories come before
stubs/ on the include path. The firmware's own gains, PWM skip and
limit, accuracy, speed interval and lead cap then apply unchanged.

motor_sim.cpp models each motor between the LMD18200 and Encoder stubs:
first order, 24.3 counts/s per PWM above a deadband of 10, 30 ms, the
fit from #90. It steps both models after every loop() pass, records
motor 1's count and applied PWM, and gives a test RPM over a window and
sim_speed_run(), which commands a speed and holds the shaft over a span.

The motor binaries are built for the two Nano Every envs, which build
the speed loop. The default target runs them after the stub tests, so
CI's `make -C test/host` does too. DCMS= points a run at another
DCMotorServo checkout. The Arduino stub gains a small String, which the
library's debug helpers build.

Refs #94
Three motor tests on the real DCMotorServo 1.2.0, DCMotorTacho and
PID 1.2.1 with the firmware's configuration.

test_speed_entry_62 enters closed-loop speed at +60 RPM after open-loop
travel, through SET_SETPOINT and SET_MODE. After 600 ms at -255 it
enters with the shaft still coasting backwards; after 600 ms at +255 and
300 ms at 0 it enters with the shaft at rest. In the first second the
applied PWM stays between 0 and 54, and 0 and 38, below the 150 limit,
and the stopped shaft never moves back. It passes on 1.2.0, whose stop()
syncs the target to the encoder and whose setSpeedRPM() restarts the
speed sample. With either undone in a copy of the library, both cases
fail (PWM down to -150 or up to 150, the shaft back up to 122 counts).

test_speed_baseline_90 pins the #90 shortfall at today's gains: 45.1 RPM
over 4-5 s (75 % of 60), 59.3 RPM over 55-60 s. It fails with speed Ki
0.5 (60.0 at 4-5 s) and with Ki 0 (41.5 at 55-60 s).

test_stall_release_89 pins the #89 overshoot: 52.0 RPM before a 4 s hold
at 20 s, a 62.7 RPM peak 1 s mean after release. It fails with the lead
cap off (255.7 RPM), with speed Ki 0 (no overshoot), and with the speed
PID held whenever the lead cap clamps (46.0 RPM after release).

Closes #62
Refs #89, #90, #94
No test pinned motorControlLogic()'s held branch: dropping the e-stop
or the watchdog trip from `held` passed the whole suite, because the
hold also sets both brake flags and the handlers refuse a release, so
the mode branches brake too.

test_hold_brakes engages an e-stop, then a watchdog trip, each alone,
clears the brake flags and stores a PWM behind the hold's back, and
checks that both motors are braked on every one of 50 passes with no
PWM written. Dropping the e-stop or the trip from `held` now fails it,
and so does removing the branch's stop_control_loops() and brakes
together. Removing only its motor1_brake()/motor2_brake() still passes:
stop_control_loops() brakes both motors through DCMotorServo::stop().
DCMotorServo's stall detection is now on for both servos, at
DCMT_STALL_TIMEOUT_MS (1000) and DCMT_STALL_MIN_COUNTS (4), build flags
in config_hardware.h. It runs in closed loop only. Before, a stalled
motor, or one with a dead encoder, was driven at up to PWM 150 for as
long as closed loop ran.

A library stall latches DCMT_FAULT_M1_STALL or _M2_STALL in motorFaults
and holds as a watchdog trip does: both motors braked, PWM commands and
speed setpoints 0, brake flags set, position setpoints following the
encoders. SET_OPEN_LOOP, SET_SETPOINT, SET_MODE and any brake release
are refused BREAD_REFUSE_FAULT, after an e-stop's or a trip's reason.
GET_FAULTS (0x7A) replies [faults:u8]; CLEAR_FAULTS (0x7B, empty, else
MALFORMED) and serial FAULTCLEAR clear it and resume nothing. GET_CAPS
sets DCMT_CAP_FAULTS; GET_STATE stays 20 bytes; READ ends FAULT:0x<bits>.

The library's latch is cleared as the firmware's is set, in the main
loop: CLEAR_FAULTS runs in the I2C interrupt, and clearStall() reads the
encoder, whose read() re-enables interrupts.

The motor tests show a shaft held at 60 RPM latching 996 ms after the
hold and staying braked after release, a shaft held in position mode
latching, and none at rest in position mode for 10 s or at 10 RPM for
20 s. #89's 4 s hold now latches instead of lunging, so its test holds
for 900 ms, under the timeout, where the overshoot remains.

Closes #64
While a motor's position target sits DCMT_TACHO_MAX_LEAD counts or more
from its encoder, its speed PID now runs with Ki 0 and the runtime Kp and
Kd. PID_v1 doesn't rescale its integral when Ki changes, so the integral
is frozen rather than reset, and a held shaft no longer winds it up for a
lunge on release. The effective gains go through the existing
apply-if-changed cache, so SET_PID speed gains still take effect, clamped
or not, and the frozen gains are set once per change, not every pass.

Conditional integration belongs in DCMotorTacho (feastorg/dcmotorservo);
this is a stopgap until it lands there.

test_speed_freeze checks the gains handed to the tacho stub, including a
SET_PID sent while clamped. test_stall_release_89 becomes the #89
regression: after a 0.9 s hold the 1 s peak is within 5 RPM of the target.

Refs #89
With Ki 0.01 the speed loop stayed P-only for seconds: in the host model,
60 RPM sat at 75 % of the target 4-5 s after the command and closed the
rest with a time constant of about 20 s. With 0.2 a 200 ms mean is within
5 % of the target 1.29 s after the command and within 1 RPM over 9-10 s.

The value was chosen against a first-order model fitted to the single
bench motor, so it stays a default, not a property of the firmware:
DCMT_SPEED_PID_KI is now #ifndef-guarded for a build flag, and SET_PID /
PIDSPEED= still set the speed gains at runtime. The config header and
firmware/README.md now say that every motor-specific default comes from
that one motor and is a starting point to tune per motor.

test_speed_baseline_90 becomes the #90 regression: within 5 % by 2 s,
within 1 RPM over 9-10 s and 15-20 s, and under 10 % overshoot.

Refs #90
The position and speed PID gains, the servo PWM skip, PWM cap and
accuracy, the tacho interval and lead cap, and both motors' counts/rev
are now #ifndef-guarded in config_hardware.h, as the stall thresholds
already were. A build for another motor sets them with -D instead of
editing the header; a -D that redefined one used to be a redefinition
warning with the header's value winning.

The defaults are unchanged: all four envs build byte-identical
firmware.hex before and after.
The status LED stayed green while a command-watchdog trip or a latched
stall fault held the motors, so a held Slice looked like a running one.
updateStatusLed() now takes its state from bsc::StatusLed::stateFor(),
with the same hold term motorControlLogic() uses
(commandWatchdog.tripped() || motorFaults != 0): amber on a hold, red
while the e-stop is held, green once the hold is cleared.

I bumped bread-slice-core to ^0.2.0 in platformio.ini and slice.yaml for
the Hold state. The e-stop input keeps the internal pull-up.

test_status_led_hold covers a trip, a stall fault, an e-stop over each,
the release while each stays latched, and each clear. The amber is
worked from the WS2812B datasheet and has not been seen on a board.

Flash/RAM against afd0810: gen2 Nano 23318 -> 23360 B / 925 -> 926 B,
gen2 Nano Every 29893 -> 29935 B / 1362 -> 1363 B, gen1 Nano
23120 -> 23122 B / 925 -> 926 B, gen1 Nano Every 29693 -> 29695 B /
1360 -> 1361 B.
@CameronBrooks11
CameronBrooks11 merged commit 41c62c6 into main Nov 1, 2026
10 checks passed
@CameronBrooks11
CameronBrooks11 deleted the feat/amber-hold branch November 1, 2026 03:07
CameronBrooks11 added a commit that referenced this pull request Nov 1, 2026
Adds the 2026-11-01 bench record, on `main` at `6a8df02`. The board is
the gen2 Nano Every, with two E-S Motor 36PG-3429-19-EN gearmotors on M1
and M2.

Passed:

- D2, 60 RPM for 60 s (#90): within 5 % in 1.21 s (M1) and 1.01 s (M2)
after the brake release. Last-5-s mean 60.7 and 60.1 RPM.
- D2 tail, setpoint 0 for 2 s: `spd=0,0` on both. Verifies #99 /
feastorg/dcmotorservo#18.
- D4, holding a shaft at 60 RPM (#97): `M1_STALL` and `M2_STALL` latched
about 1 s after the shaft stopped. SET_* were refused with reason 8, and
`fclear` cleared without restarting anything. The M2 LED went amber,
then green after the clear (#98).
- D5, 10 RPM for 20 s: no latch on either motor.
- D6, speed mode entered after a full reverse: no negative PWM (#62).
- D7, the watchdog trip: LED amber, red with the e-stop, amber after the
release, green after the clear (#98).

Findings:

- With M1 running at 60 RPM, the board held SCL low. Both slices stopped
answering, so the brake command never arrived, and only the e-stop
jumper stopped M1. A reset of the DCMT cleared it (#101).
- In position mode, an 11-count step on M2 latched `M2_STALL`: PWM 16
did not turn the shaft. A 100-count step was fine (#102).
- After grips of 0.4 s or less, M2's peak 1 s mean speed reached 66.8
and 67.4 RPM (M1 64.4). #89 is improved but not fixed (figures on #89).

The record also notes counts per revolution: driven flag checks agree
with the firmware's 798, while turning the shaft by hand gave 846 to
861. The datasheet gives 912 (#103).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant