Repository navigation
feat(firmware): show amber on a watchdog trip or stall fault - #98
Merged
Merged
Conversation
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
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requires bread-slice-core 0.2.0 (
^0.2.0in platformio.ini and slice.yaml) and uses its newHoldstate: the status LED shows amber while a command-watchdog trip or a latched stall fault (#97) holds the motors, the same holdmotorControlLogic()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_holdruns the real 0.2.0StatusLedwith 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.