diff --git a/CHANGELOG.md b/CHANGELOG.md index 270182d..356ac8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ Format: [Keep a Changelog](https://keepachangelog.com/). Versions follow ## [Unreleased] +### Added +- `bsc::StatusLed`: a `Hold` state, amber (red 50, green 16), for a firmware + hold: a command-watchdog trip or a latched fault. `StatusLed::stateFor()` + picks the main loop's state, with red winning over amber. The amber value is + worked from the WS2812B datasheet and has not been seen on a board. +- `bsc::EStop`: an input mode, `EStop(pin, EStop::NoPullUp)`, that asserts on + LOW without the internal pull-up, for a Loaf that pulls an open line LOW. + The existing constructor, with the pull-up, is unchanged and stays the + default: today's Loafs need it. + ## [0.1.0] - 2026-10-23 Host-tested only: no Slice uses it yet, and it has not run on a board. diff --git a/README.md b/README.md index 02eebea..967f74c 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,13 @@ the calls in the order a Slice makes them. `EStop(pin, activeLow = true, releaseDebounceMs = 25)`. `begin()` sets the pin's mode: the internal pull-up when `activeLow`. +`EStop(pin, EStop::NoPullUp, releaseDebounceMs = 25)` asserts on LOW with no +internal pull-up, for a Loaf whose pull-down holds an open line LOW, as the +[e-stop polarity rule](https://github.com/feastorg/bread-infra/blob/main/spec/slice/firmware.md#e-stop-polarity) +asks: pull-ups on several Slices in parallel would hold an open line HIGH. +`EStop::PullUp` is the same as the default. Keep the default until the Loaf +has the pull-down; without it, a line with no pull-up floats. + `poll(millis())` once per main-loop pass returns `None`, `Pressed` or `Released`. One active sample is a press. A release needs the line inactive for the debounce time without a break, and always at least two samples. @@ -49,9 +56,18 @@ Arming, disarming and re-arming leave a trip set. `arm()`, `stamp()` and ### Status LED (`bsc::StatusLed`) One WS2812B on D5, on an ATmega328P or ATmega4809 at 16 MHz; anything else -fails to build. `begin()`, then `set(Boot)` (blue), `set(Running)` (green) or -`set(EStop)` (red). A frame goes out only when the state changes; interrupts -are off for about 32 us while it does. +fails to build. `begin()`, then `set(Boot)` (blue), `set(Running)` (green), +`set(EStop)` (red) or `set(Hold)` (amber). A frame goes out only when the state +changes; interrupts are off for about 32 us while it does. + +From the main loop, `set(StatusLed::stateFor(estop.asserted(), hold))` picks +the state, where `hold` is a command-watchdog trip or a latched fault: red +while the e-stop is held, amber while held without it, green otherwise. Red +wins over amber. + +Amber is red 50, green 16. The WS2812B datasheet's typical intensities put +its green at about twice its red, so about 3:1 in drive mixes near 590 nm. That +ratio is worked from the datasheet, not yet seen on a board. ### Strict parsing diff --git a/examples/slice_skeleton/slice_skeleton.ino b/examples/slice_skeleton/slice_skeleton.ino index 0b17665..8b44371 100644 --- a/examples/slice_skeleton/slice_skeleton.ino +++ b/examples/slice_skeleton/slice_skeleton.ino @@ -34,7 +34,8 @@ void loop() { wdt_reset(); onEStop(estop.poll(millis())); - led.set(estop.asserted() ? bsc::StatusLed::EStop : bsc::StatusLed::Running); + // A Slice with latched faults ORs them into the hold. + led.set(bsc::StatusLed::stateFor(estop.asserted(), watchdog.tripped())); if (watchdog.check(millis())) driveOutputsSafe(); diff --git a/src/bsc_estop.cpp b/src/bsc_estop.cpp index 758af93..e8c6de3 100644 --- a/src/bsc_estop.cpp +++ b/src/bsc_estop.cpp @@ -9,13 +9,18 @@ namespace bsc { EStop::EStop(uint8_t pin, bool activeLow, uint16_t releaseDebounceMs) - : pin_(pin), activeLow_(activeLow), debounceMs_(releaseDebounceMs) + : pin_(pin), activeLow_(activeLow), pullUp_(activeLow), debounceMs_(releaseDebounceMs) +{ +} + +EStop::EStop(uint8_t pin, Input input, uint16_t releaseDebounceMs) + : pin_(pin), activeLow_(true), pullUp_(input == PullUp), debounceMs_(releaseDebounceMs) { } void EStop::begin() { - pinMode(pin_, activeLow_ ? INPUT_PULLUP : INPUT); + pinMode(pin_, pullUp_ ? INPUT_PULLUP : INPUT); } EStop::Event EStop::poll(unsigned long nowMs) diff --git a/src/bsc_estop.h b/src/bsc_estop.h index de5414a..1c0c54f 100644 --- a/src/bsc_estop.h +++ b/src/bsc_estop.h @@ -26,10 +26,21 @@ class EStop Released, }; + // How an active-low line is read. Both assert on LOW. + enum Input : uint8_t + { + PullUp, // the internal pull-up, which today's Loafs need + NoPullUp, // no pull-up: the Loaf's pull-down holds an open line LOW, + // and pull-ups on several Slices would outvote it + }; + // activeLow: the input uses the internal pull-up and LOW is a stop, as on // the BREADS Slices today. explicit EStop(uint8_t pin, bool activeLow = true, uint16_t releaseDebounceMs = 25); + // Active low, with the input read as given. + EStop(uint8_t pin, Input input, uint16_t releaseDebounceMs = 25); + // Configures the pin. Call before the first poll(). void begin(); @@ -46,6 +57,7 @@ class EStop private: uint8_t pin_; bool activeLow_; + bool pullUp_; uint16_t debounceMs_; bool releasePending_ = false; uint32_t releaseStartMs_ = 0; diff --git a/src/bsc_status_led.h b/src/bsc_status_led.h index 9a3126a..e9b598e 100644 --- a/src/bsc_status_led.h +++ b/src/bsc_status_led.h @@ -52,8 +52,19 @@ class StatusLed Boot, // blue, through the boot wait Running, // green EStop, // red, while the e-stop is held + Hold, // amber, a firmware hold: a command-watchdog trip or a + // latched fault }; + // The main loop's state: red while the e-stop is held, amber while the + // Slice holds on its own, green otherwise. Red wins over amber. + static State stateFor(bool estopHeld, bool hold) + { + if (estopHeld) + return EStop; + return hold ? Hold : Running; + } + // Drives D5 low as an output. void begin() { @@ -80,6 +91,12 @@ class StatusLed case EStop: show(50, 0, 0); break; + case Hold: + // The WS2812B's green die is about twice as bright as its red + // (datasheet typicals), so about 3:1 red to green in drive mixes + // near 590 nm. Worked from the datasheet, not seen on a board. + show(50, 16, 0); + break; } } diff --git a/tests/fakes/Arduino.cpp b/tests/fakes/Arduino.cpp index 7fd6cbb..e45e071 100644 --- a/tests/fakes/Arduino.cpp +++ b/tests/fakes/Arduino.cpp @@ -2,5 +2,6 @@ int fake_level = HIGH; int fake_pin_mode = -1; +int fake_pullup_calls = 0; unsigned long fake_now = 0; unsigned long fake_step = 0; diff --git a/tests/fakes/Arduino.h b/tests/fakes/Arduino.h index 80c65ad..106bba2 100644 --- a/tests/fakes/Arduino.h +++ b/tests/fakes/Arduino.h @@ -9,9 +9,11 @@ #define INPUT 0 #define INPUT_PULLUP 2 -// The level digitalRead() returns for every pin, and the last pinMode(). +// The level digitalRead() returns for every pin, the last pinMode(), and how +// many pinMode() calls asked for the pull-up. extern int fake_level; extern int fake_pin_mode; +extern int fake_pullup_calls; // millis() returns fake_now, then adds fake_step, so a loop that polls the // clock advances it. @@ -25,6 +27,11 @@ inline unsigned long millis() return t; } inline int digitalRead(uint8_t) { return fake_level; } -inline void pinMode(uint8_t, uint8_t mode) { fake_pin_mode = mode; } +inline void pinMode(uint8_t, uint8_t mode) +{ + fake_pin_mode = mode; + if (mode == INPUT_PULLUP) + fake_pullup_calls++; +} #endif diff --git a/tests/test_estop.cpp b/tests/test_estop.cpp index 54273a9..b6d9abc 100644 --- a/tests/test_estop.cpp +++ b/tests/test_estop.cpp @@ -27,6 +27,28 @@ int main() CHECK(fake_pin_mode == INPUT); } + // The input mode: PullUp is the default's pull-up; NoPullUp never + // enables it, and still asserts on LOW. + { + EStop p(2, EStop::PullUp); + fake_pullup_calls = 0; + p.begin(); + CHECK(fake_pin_mode == INPUT_PULLUP && fake_pullup_calls == 1); + EStop n(2, EStop::NoPullUp, 5); + fake_pin_mode = -1; + fake_pullup_calls = 0; + n.begin(); + CHECK(fake_pin_mode == INPUT && fake_pullup_calls == 0); + fake_level = HIGH; + CHECK(n.poll(0) == EStop::None); + fake_level = LOW; + CHECK(n.poll(1) == EStop::Pressed); + fake_level = HIGH; + n.poll(2); + CHECK(n.poll(7) == EStop::Released); + fake_level = HIGH; + } + // One active sample asserts and counts. { EStop e(2); diff --git a/tests/test_status_led.cpp b/tests/test_status_led.cpp index a4fe1a3..b2a3f68 100644 --- a/tests/test_status_led.cpp +++ b/tests/test_status_led.cpp @@ -1,4 +1,5 @@ -// Status LED: each state's colour in GRB order, written only on a change. +// Status LED: each state's colour in GRB order, written only on a change, and +// the main loop's choice of state. #include #include "check.h" @@ -32,5 +33,17 @@ int main() led.set(StatusLed::Running); CHECK(bsc_host_led_writes == 4 && frame(25, 0, 0)); + // Amber: red 50, green 16, no blue. + led.set(StatusLed::Hold); + CHECK(bsc_host_led_writes == 5 && frame(16, 50, 0)); + led.set(StatusLed::Hold); + CHECK(bsc_host_led_writes == 5); + + // Red wins over amber; amber only while held without the e-stop. + CHECK(StatusLed::stateFor(false, false) == StatusLed::Running); + CHECK(StatusLed::stateFor(false, true) == StatusLed::Hold); + CHECK(StatusLed::stateFor(true, false) == StatusLed::EStop); + CHECK(StatusLed::stateFor(true, true) == StatusLed::EStop); + CHECK_DONE(); }