Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion examples/slice_skeleton/slice_skeleton.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
9 changes: 7 additions & 2 deletions src/bsc_estop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions src/bsc_estop.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -46,6 +57,7 @@ class EStop
private:
uint8_t pin_;
bool activeLow_;
bool pullUp_;
uint16_t debounceMs_;
bool releasePending_ = false;
uint32_t releaseStartMs_ = 0;
Expand Down
17 changes: 17 additions & 0 deletions src/bsc_status_led.h
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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;
}
}

Expand Down
1 change: 1 addition & 0 deletions tests/fakes/Arduino.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
11 changes: 9 additions & 2 deletions tests/fakes/Arduino.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
22 changes: 22 additions & 0 deletions tests/test_estop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 14 additions & 1 deletion tests/test_status_led.cpp
Original file line number Diff line number Diff line change
@@ -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 <bsc_status_led.h>

#include "check.h"
Expand Down Expand Up @@ -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();
}
Loading