Replace the plain status LED with a WS2812B on PIO

The boards this targets carry an addressable LED on GPIO16 rather than a
plain LED, so the GPIO25 heartbeat had nothing to drive. Green means
running, blue flashes on each measurement, red means the sensor is
failing, with the most urgent condition winning. A healthy node reads as
dim green with a blue pip once a second, so a stalled or failing one is
obvious with no serial port attached.

Driven from PIO directly rather than via ws2812-pio, whose current
release pins rp2040-hal 0.11 against this project's 0.12. Those are
semver incompatible: cargo would build both HALs and the driver would not
accept our PIO, state machine or pin types. The alternative was pinning
the whole project back a HAL version, for four instructions of PIO.

Generating the waveform in the state machine matters beyond convenience.
The bus tasks preempt the LED task freely, and a CPU-timed WS2812 would
glitch the moment a frame arrived mid-update. The >50 us latch gap needs
no explicit delay either, since repaints are 25 ms apart.

Byte order is a named constant rather than two transposed shifts, because
it is a property of the part fitted and not of the protocol: many parts
sold as WS2812B, along with the WS2811 and SK6812 families, want red
first. These do. The unused ChannelOrder variant stays so the choice, and
the fix if a board differs, is legible.

The task derives everything from existing shared state — the reading's
own timestamp already says when the last measurement landed — so nothing
is published purely to drive an LED.

Colours confirmed on hardware after correcting the channel order; bus
suite still 15/15 and sensor counters clean, so PIO0 disturbs nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Walter
2026-07-28 21:26:14 +02:00
parent 46c26796cc
commit f2d06278af
6 changed files with 302 additions and 24 deletions
+33 -4
View File
@@ -39,7 +39,7 @@ performs no I/O and touches no peripherals; everything hardware-shaped lives in
| 2 | SIO output | transceiver `DE` **and** `/RE`, tied together |
| 14 | I2C1 SDA | SHT31 `SDA` |
| 15 | I2C1 SCL | SHT31 `SCL` |
| 25 | SIO output | status LED, active high |
| 16 | PIO0 | WS2812B status indicator, data in |
| — | SWD | `SWCLK`/`SWDIO` on the dedicated pads, for the probe |
Why this arrangement:
@@ -54,8 +54,10 @@ Why this arrangement:
- **I2C1 on GPIO 14/15, physically far from the RS485 block.** The sensor is the
noise-sensitive part and the differential driver is the noisy part. Using I2C1
also leaves I2C0 free.
- **GPIO 25 for the LED** matches the Raspberry Pi Pico, so this image blinks on
a bare Pico as well — handy for bring-up before the real board exists.
- **GPIO 16 for the WS2812B**, because that is where the addressable LED sits on
the boards this targets. Any GPIO would do, since PIO can drive the waveform
from any pin, but it is kept clear of the RS485 block so the LED's switching
current does not share a return path with the differential pair.
### Hardware notes the firmware cannot enforce
@@ -70,6 +72,33 @@ Why this arrangement:
reset command.
- `.boot2` is built for a **W25Q080-class** QSPI flash. Change
`BOOT2_FIRMWARE` in `firmware/src/main.rs` if the board carries something else.
- The **WS2812B** wants 100 nF of local decoupling and draws tens of mA at full
brightness. The firmware keeps it deliberately dim, which is both easier to
read on a bench and easier on that current.
## Status indicator
The WS2812B on GPIO16 reports health at a glance, with no serial port attached:
| Colour | Meaning | Condition |
|---|---|---|
| Green, dim | running | default |
| Blue | measurement taken | within 120 ms of a successful reading |
| Red | sensor error | any failed poll, or `SENSOR_OK` clear |
The most urgent condition wins: red over blue, blue over green. So a healthy node
reads as dim green with a distinct blue pip once a second, and a stalled or
failing one is obvious immediately.
Driven from PIO (`firmware/src/status_led.rs`) rather than via `ws2812-pio`, whose
current release pins `rp2040-hal` 0.11 against this project's 0.12 — see that
module's header. Generating the waveform in the state machine also means the bus
tasks can preempt the LED task freely without glitching it.
Note that **many parts sold as WS2812B expect red-first rather than green-first
byte order**, including the boards this targets. That is one constant,
`CHANNEL_ORDER`; if red and green come out swapped while blue is correct, it is
the only thing to change.
## Protocol
@@ -403,7 +432,7 @@ drives it from registers thereafter.
| 1 | `sensor_task` | async | Poll the SHT31 once a second |
| 1 | `usb_irq` | hardware | Service the USB CDC diagnostic port |
| 1 | `usb_report` | async | Emit a diagnostic line once a second |
| 1 | `heartbeat` | async | Blink the status LED |
| 1 | `status_led` | async | Repaint the WS2812B indicator |
The central decision is that **requests are answered entirely from a cached
reading**. An SHT31 high-repeatability conversion takes up to 15 ms — far longer