Skip to content

Fix #112 watchdog race in Out::update() causing spurious disarm#118

Merged
qqqlab merged 1 commit into
qqqlab:mainfrom
evanofficial:fix-watchdog-race
May 18, 2026
Merged

Fix #112 watchdog race in Out::update() causing spurious disarm#118
qqqlab merged 1 commit into
qqqlab:mainfrom
evanofficial:fix-watchdog-race

Conversation

@evanofficial

Copy link
Copy Markdown
Contributor

Fixes #112

Problem

Out::update() checks the motor watchdog with:

if(_mode != mode_enum::DISARMED && micros() - _watchdog_ts >= OUT_MOT_TIMEOUT) {
   _set_mode(mode_enum::DISARMED);
}

C++ does not specify evaluation order between the micros() call and the load of _watchdog_ts. If set_output() (or testmotor_set_output()) updates _watchdog_ts from another task/core between those two reads, micros() can be sampled with an older value while _watchdog_ts is reloaded with a newer one. The unsigned subtraction then underflows to a value far greater than OUT_MOT_TIMEOUT, the comparison trips, and the motors are spuriously disarmed mid-flight — exactly the OUT: ARMED stutter the reporter observed.

The reporter (@slavicd) hardware-verified on an Arduino Nano ESP32 that snapshotting both values into locals before the comparison makes the stutter unreproducible.

Fix

  • Snapshot _watchdog_ts into a local before reading micros(), locking in evaluation order.
  • Mark _watchdog_ts as volatile so the compiler cannot reload it between the two references or hoist the load across the micros() call.

Architectures

_watchdog_ts is uint32_t, so individual loads/stores are already atomic on every supported target (ESP32-S3/ESP32, RP2350/RP2040, STM32 — all 32-bit cores). No platform-specific atomics or critical sections are needed; the fix is portable C++ and compiles cleanly under both Arduino IDE and PlatformIO.

Audit

All readers and writers of _watchdog_ts were audited:

  • Reader: Out::update() (line 181) — fixed
  • Writers: Out::set_output(), Out::testmotor_enable(), Out::testmotor_set_output() — unchanged, none in ISR context

Test plan

  • Builds on ESP32, RP2, STM32 via the existing CI compile matrix.
  • Behavioral verification by the original reporter on Arduino Nano ESP32 (their patch is functionally equivalent to this one).

The check 'micros() - _watchdog_ts >= OUT_MOT_TIMEOUT' has no guaranteed
evaluation order between the micros() call and the _watchdog_ts load. If
set_output() runs between those two reads (e.g. from another task/core),
micros() can be sampled before _watchdog_ts is reloaded with a later
timestamp. The unsigned subtraction then underflows and the comparison
spuriously trips, disarming motors mid-flight (the OUT: ARMED stutter).

Snapshot _watchdog_ts into a local before reading micros(), and mark
_watchdog_ts volatile so the compiler cannot reload it between the two
references. Hardware-verified workaround by the reporter on Arduino Nano
ESP32.
@qqqlab

qqqlab commented May 18, 2026

Copy link
Copy Markdown
Owner

@evanofficial Many thanks for fixing this!

@qqqlab qqqlab merged commit ba5974c into qqqlab:main May 18, 2026
5 checks passed
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.

Bug related to watchdog and OUT_MOT_TIMEOUT (stuttering motors)

2 participants