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:
Generated
+1
@@ -1236,6 +1236,7 @@ dependencies = [
|
|||||||
"fugit",
|
"fugit",
|
||||||
"heapless",
|
"heapless",
|
||||||
"panic-probe",
|
"panic-probe",
|
||||||
|
"pio",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"rp2040-boot2",
|
"rp2040-boot2",
|
||||||
"rp2040-hal",
|
"rp2040-hal",
|
||||||
|
|||||||
@@ -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 |
|
| 2 | SIO output | transceiver `DE` **and** `/RE`, tied together |
|
||||||
| 14 | I2C1 SDA | SHT31 `SDA` |
|
| 14 | I2C1 SDA | SHT31 `SDA` |
|
||||||
| 15 | I2C1 SCL | SHT31 `SCL` |
|
| 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 |
|
| — | SWD | `SWCLK`/`SWDIO` on the dedicated pads, for the probe |
|
||||||
|
|
||||||
Why this arrangement:
|
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
|
- **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
|
noise-sensitive part and the differential driver is the noisy part. Using I2C1
|
||||||
also leaves I2C0 free.
|
also leaves I2C0 free.
|
||||||
- **GPIO 25 for the LED** matches the Raspberry Pi Pico, so this image blinks on
|
- **GPIO 16 for the WS2812B**, because that is where the addressable LED sits on
|
||||||
a bare Pico as well — handy for bring-up before the real board exists.
|
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
|
### Hardware notes the firmware cannot enforce
|
||||||
|
|
||||||
@@ -70,6 +72,33 @@ Why this arrangement:
|
|||||||
reset command.
|
reset command.
|
||||||
- `.boot2` is built for a **W25Q080-class** QSPI flash. Change
|
- `.boot2` is built for a **W25Q080-class** QSPI flash. Change
|
||||||
`BOOT2_FIRMWARE` in `firmware/src/main.rs` if the board carries something else.
|
`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
|
## Protocol
|
||||||
|
|
||||||
@@ -403,7 +432,7 @@ drives it from registers thereafter.
|
|||||||
| 1 | `sensor_task` | async | Poll the SHT31 once a second |
|
| 1 | `sensor_task` | async | Poll the SHT31 once a second |
|
||||||
| 1 | `usb_irq` | hardware | Service the USB CDC diagnostic port |
|
| 1 | `usb_irq` | hardware | Service the USB CDC diagnostic port |
|
||||||
| 1 | `usb_report` | async | Emit a diagnostic line once a second |
|
| 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
|
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
|
reading**. An SHT31 high-repeatability conversion takes up to 15 ms — far longer
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ portable-atomic = { version = "1", features = ["critical-section"] }
|
|||||||
embedded-hal = "1.0"
|
embedded-hal = "1.0"
|
||||||
fugit = "0.3"
|
fugit = "0.3"
|
||||||
|
|
||||||
|
# WS2812 status indicator. Version must track rp2040-hal's own `pio` dependency
|
||||||
|
# so the assembled program type matches what its PIO driver accepts. The
|
||||||
|
# assembler proc macro comes along with it, so pio-proc needs no direct entry.
|
||||||
|
pio = "0.3"
|
||||||
|
|
||||||
defmt = { workspace = true }
|
defmt = { workspace = true }
|
||||||
defmt-rtt = "1.0"
|
defmt-rtt = "1.0"
|
||||||
panic-probe = { version = "1.0", features = ["print-defmt"] }
|
panic-probe = { version = "1.0", features = ["print-defmt"] }
|
||||||
|
|||||||
+30
-9
@@ -11,7 +11,7 @@
|
|||||||
//! │ 2 │ SIO out │ transceiver DE and /RE, tied together │
|
//! │ 2 │ SIO out │ transceiver DE and /RE, tied together │
|
||||||
//! │ 14 │ I2C1 SDA │ SHT31 SDA │
|
//! │ 14 │ I2C1 SDA │ SHT31 SDA │
|
||||||
//! │ 15 │ I2C1 SCL │ SHT31 SCL │
|
//! │ 15 │ I2C1 SCL │ SHT31 SCL │
|
||||||
//! │ 25 │ SIO out │ status LED, active high │
|
//! │ 16 │ PIO0 │ WS2812B status indicator, data in │
|
||||||
//! │ SWD │ – │ SWCLK/SWDIO on the dedicated pads, for probe-rs │
|
//! │ SWD │ – │ SWCLK/SWDIO on the dedicated pads, for probe-rs │
|
||||||
//! └────────┴───────────┴─────────────────────────────────────────────────────┘
|
//! └────────┴───────────┴─────────────────────────────────────────────────────┘
|
||||||
//! ```
|
//! ```
|
||||||
@@ -33,11 +33,17 @@
|
|||||||
//! - **I2C1 on GPIO 14/15, physically distant from the RS485 block.** The
|
//! - **I2C1 on GPIO 14/15, physically distant from the RS485 block.** The
|
||||||
//! sensor is the noise-sensitive part; the differential driver is the noisy
|
//! sensor is the noise-sensitive part; the differential driver is the noisy
|
||||||
//! part. Using I2C1 leaves I2C0 free.
|
//! part. Using I2C1 leaves I2C0 free.
|
||||||
//! - **GPIO 25 for the LED** matches the Raspberry Pi Pico, so this firmware
|
//! - **GPIO 16 for the WS2812B**, because that is where the addressable LED sits
|
||||||
//! blinks on a bare Pico too, which is convenient for bring-up.
|
//! on the boards this is built for. Any GPIO would do — 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 not expressible in firmware
|
//! # Hardware notes not expressible in firmware
|
||||||
//!
|
//!
|
||||||
|
//! - The WS2812B needs its own decoupling (100 nF close to the package) and
|
||||||
|
//! draws tens of mA at full brightness. The firmware keeps it deliberately dim,
|
||||||
|
//! partly to be readable on a bench and partly to keep that current small.
|
||||||
|
//!
|
||||||
//! - Fit the 120 Ω termination only at the two physical ends of the segment, and
|
//! - Fit the 120 Ω termination only at the two physical ends of the segment, and
|
||||||
//! fail-safe bias resistors at one point on the segment.
|
//! fail-safe bias resistors at one point on the segment.
|
||||||
//! - The SHT31 ADDR pin selects the I2C address: low for `0x44`, high for
|
//! - The SHT31 ADDR pin selects the I2C address: low for `0x44`, high for
|
||||||
@@ -50,8 +56,9 @@
|
|||||||
|
|
||||||
use rp2040_hal::{
|
use rp2040_hal::{
|
||||||
gpio::{
|
gpio::{
|
||||||
bank0::{Gpio0, Gpio1, Gpio14, Gpio15, Gpio2, Gpio25},
|
bank0::{Gpio0, Gpio1, Gpio14, Gpio15, Gpio16, Gpio2},
|
||||||
FunctionI2c, FunctionSioOutput, FunctionUart, Pin, PullDown, PullNone, PullUp,
|
FunctionI2c, FunctionPio0, FunctionSioOutput, FunctionUart, Pin, PullDown, PullNone,
|
||||||
|
PullUp,
|
||||||
},
|
},
|
||||||
i2c::I2C,
|
i2c::I2C,
|
||||||
pac,
|
pac,
|
||||||
@@ -112,8 +119,16 @@ pub const STALE_AFTER_MS: u32 = 3_000;
|
|||||||
/// starts attempting soft resets.
|
/// starts attempting soft resets.
|
||||||
pub const SENSOR_FAULT_THRESHOLD: u32 = 5;
|
pub const SENSOR_FAULT_THRESHOLD: u32 = 5;
|
||||||
|
|
||||||
/// Status LED blink half-period.
|
// ------------------------------------------------------------- status LED ---
|
||||||
pub const HEARTBEAT_MS: u32 = 500;
|
|
||||||
|
/// How often the status indicator is repainted.
|
||||||
|
pub const LED_TICK_MS: u32 = 25;
|
||||||
|
|
||||||
|
/// How long the indicator stays blue after a measurement.
|
||||||
|
///
|
||||||
|
/// Short relative to [`SENSOR_PERIOD_MS`], so a healthy node reads as green with
|
||||||
|
/// a distinct blue pip once a second rather than a muddy blend of the two.
|
||||||
|
pub const LED_MEASURING_MS: u32 = 120;
|
||||||
|
|
||||||
// ------------------------------------------------------- USB diagnostics ---
|
// ------------------------------------------------------- USB diagnostics ---
|
||||||
|
|
||||||
@@ -160,5 +175,11 @@ pub type SclPin = Pin<Gpio15, FunctionI2c, PullUp>;
|
|||||||
/// The configured I2C controller the sensor driver talks through.
|
/// The configured I2C controller the sensor driver talks through.
|
||||||
pub type SensorI2c = I2C<pac::I2C1, (SdaPin, SclPin)>;
|
pub type SensorI2c = I2C<pac::I2C1, (SdaPin, SclPin)>;
|
||||||
|
|
||||||
/// Status LED.
|
/// GPIO number carrying the WS2812B data line.
|
||||||
pub type StatusLedPin = Pin<Gpio25, FunctionSioOutput, PullDown>;
|
///
|
||||||
|
/// Must match [`StatusLedPin`]: PIO addresses pins by number, so the type alias
|
||||||
|
/// alone is not enough to configure the state machine.
|
||||||
|
pub const STATUS_LED_GPIO: u8 = 16;
|
||||||
|
|
||||||
|
/// WS2812B data line, driven by PIO0 rather than as a plain output.
|
||||||
|
pub type StatusLedPin = Pin<Gpio16, FunctionPio0, PullDown>;
|
||||||
|
|||||||
+53
-11
@@ -11,7 +11,7 @@
|
|||||||
//! | 1 | `sensor_task` | async | Poll the SHT31 once a second |
|
//! | 1 | `sensor_task` | async | Poll the SHT31 once a second |
|
||||||
//! | 1 | `usb_irq` | hardware | Service the USB CDC diagnostic port |
|
//! | 1 | `usb_irq` | hardware | Service the USB CDC diagnostic port |
|
||||||
//! | 1 | `usb_report` | async | Emit a diagnostic line once a second |
|
//! | 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 USB port is a diagnostic aid, not part of the protocol: it lets the node
|
//! The USB port is a diagnostic aid, not part of the protocol: it lets the node
|
||||||
//! be verified over a plain USB cable with no SWD probe and no RS485 adapter. It
|
//! be verified over a plain USB cable with no SWD probe and no RS485 adapter. It
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
mod board;
|
mod board;
|
||||||
mod rs485;
|
mod rs485;
|
||||||
mod sensor;
|
mod sensor;
|
||||||
|
mod status_led;
|
||||||
|
|
||||||
use defmt_rtt as _;
|
use defmt_rtt as _;
|
||||||
use panic_probe as _;
|
use panic_probe as _;
|
||||||
@@ -58,7 +59,6 @@ mod app {
|
|||||||
|
|
||||||
use core::fmt::Write as _;
|
use core::fmt::Write as _;
|
||||||
|
|
||||||
use embedded_hal::digital::StatefulOutputPin;
|
|
||||||
use fugit::RateExtU32;
|
use fugit::RateExtU32;
|
||||||
use heapless::String;
|
use heapless::String;
|
||||||
use rp2040_hal as hal;
|
use rp2040_hal as hal;
|
||||||
@@ -79,6 +79,7 @@ mod app {
|
|||||||
use crate::board::{self, Instant};
|
use crate::board::{self, Instant};
|
||||||
use crate::rs485::{self, Rs485, RxBuffer, FIFO_DEPTH};
|
use crate::rs485::{self, Rs485, RxBuffer, FIFO_DEPTH};
|
||||||
use crate::sensor::Sht31;
|
use crate::sensor::Sht31;
|
||||||
|
use crate::status_led::{Rgb, StatusLed};
|
||||||
|
|
||||||
/// A cached sensor reading and when it was taken.
|
/// A cached sensor reading and when it was taken.
|
||||||
///
|
///
|
||||||
@@ -128,7 +129,7 @@ mod app {
|
|||||||
#[local]
|
#[local]
|
||||||
struct Local {
|
struct Local {
|
||||||
sensor: Sht31<board::SensorI2c>,
|
sensor: Sht31<board::SensorI2c>,
|
||||||
led: board::StatusLedPin,
|
led: StatusLed,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The USB bus allocator must outlive the device and class that borrow from
|
// The USB bus allocator must outlive the device and class that borrow from
|
||||||
@@ -188,7 +189,13 @@ mod app {
|
|||||||
);
|
);
|
||||||
let sensor = Sht31::new(i2c, board::SENSOR_I2C_ADDR);
|
let sensor = Sht31::new(i2c, board::SENSOR_I2C_ADDR);
|
||||||
|
|
||||||
let led = pins.gpio25.reconfigure();
|
// ---- WS2812B status indicator on PIO0 --------------------------------
|
||||||
|
let led = StatusLed::new(
|
||||||
|
cx.device.PIO0,
|
||||||
|
pins.gpio16.reconfigure(),
|
||||||
|
&mut cx.device.RESETS,
|
||||||
|
clocks.system_clock.freq(),
|
||||||
|
);
|
||||||
|
|
||||||
// ---- USB CDC diagnostics --------------------------------------------
|
// ---- USB CDC diagnostics --------------------------------------------
|
||||||
// `force_vbus_detect_bit` is true because the Pico ties VBUS to the chip;
|
// `force_vbus_detect_bit` is true because the Pico ties VBUS to the chip;
|
||||||
@@ -223,7 +230,7 @@ mod app {
|
|||||||
);
|
);
|
||||||
|
|
||||||
sensor_task::spawn().ok();
|
sensor_task::spawn().ok();
|
||||||
heartbeat::spawn().ok();
|
status_led::spawn().ok();
|
||||||
usb_report::spawn().ok();
|
usb_report::spawn().ok();
|
||||||
|
|
||||||
(
|
(
|
||||||
@@ -623,13 +630,48 @@ mod app {
|
|||||||
sensor.read_measurement()
|
sensor.read_measurement()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blink the status LED. Says the executor is alive; says nothing about the
|
/// Repaint the WS2812B status indicator.
|
||||||
/// bus or the sensor, both of which are reported over the protocol instead.
|
///
|
||||||
#[task(priority = 1, local = [led])]
|
/// Green for running, blue for a measurement, red for a sensor error, with
|
||||||
async fn heartbeat(cx: heartbeat::Context) {
|
/// the most urgent condition winning: red over blue, blue over green. Read at
|
||||||
|
/// a glance, a healthy node is dim green with a blue pip once a second, so a
|
||||||
|
/// stalled or failing node is obvious without a serial port.
|
||||||
|
///
|
||||||
|
/// Derived entirely from existing shared state — the reading's own timestamp
|
||||||
|
/// already says when the last measurement landed, so nothing extra is
|
||||||
|
/// published just to drive an LED.
|
||||||
|
#[task(priority = 1, shared = [reading, state], local = [led])]
|
||||||
|
async fn status_led(mut cx: status_led::Context) {
|
||||||
|
let tick = board::Duration::millis(u64::from(board::LED_TICK_MS));
|
||||||
|
let flash = board::Duration::millis(u64::from(board::LED_MEASURING_MS));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
cx.local.led.toggle().ok();
|
let now = Mono::now();
|
||||||
Mono::delay(board::Duration::millis(u64::from(board::HEARTBEAT_MS))).await;
|
|
||||||
|
let measured_recently = cx
|
||||||
|
.shared
|
||||||
|
.reading
|
||||||
|
.lock(|slot| slot.as_ref().map(|r| r.taken_at))
|
||||||
|
.and_then(|taken_at| now.checked_duration_since(taken_at))
|
||||||
|
.is_some_and(|age| age < flash);
|
||||||
|
|
||||||
|
let (sensor_ok, failures) = cx
|
||||||
|
.shared
|
||||||
|
.state
|
||||||
|
.lock(|s| (s.sensor_ok, s.consecutive_failures));
|
||||||
|
|
||||||
|
let colour = if failures > 0 || !sensor_ok {
|
||||||
|
// Any failed poll shows red, not just a latched hard fault: a
|
||||||
|
// sensor dropping the occasional reading is worth seeing.
|
||||||
|
Rgb::ERROR
|
||||||
|
} else if measured_recently {
|
||||||
|
Rgb::MEASURING
|
||||||
|
} else {
|
||||||
|
Rgb::RUNNING
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.local.led.set(colour);
|
||||||
|
Mono::delay(tick).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
//! WS2812B status indicator, driven from PIO.
|
||||||
|
//!
|
||||||
|
//! # Why this is hand-rolled
|
||||||
|
//!
|
||||||
|
//! `ws2812-pio` would be the obvious choice, but its latest release pins
|
||||||
|
//! `rp2040-hal` 0.11 while this firmware is on 0.12. Those are semver
|
||||||
|
//! incompatible, so its `PIO`, state-machine and `Pin` types will not unify with
|
||||||
|
//! ours — cargo would build both HALs and the driver simply would not accept our
|
||||||
|
//! peripherals. The wire protocol is four PIO instructions, so owning it is far
|
||||||
|
//! cheaper than pinning the whole project to an older HAL.
|
||||||
|
//!
|
||||||
|
//! # How it works
|
||||||
|
//!
|
||||||
|
//! A WS2812B bit is 1.25 µs long, and the value is encoded in the duty cycle: a
|
||||||
|
//! one is ~0.8 µs high then ~0.45 µs low, a zero ~0.4 µs high then ~0.85 µs low.
|
||||||
|
//! Latching happens after the line is held low for >50 µs.
|
||||||
|
//!
|
||||||
|
//! The program below spends a fixed **10 state-machine cycles per bit**, split
|
||||||
|
//! 2/5/3, and drives the line from the side-set pin. Clocking the machine at
|
||||||
|
//! 8 MHz therefore yields the 1.25 µs bit period, with the high phase lasting
|
||||||
|
//! either 2 or 7 cycles depending on the bit. Timing is generated entirely by
|
||||||
|
//! the state machine, so it is immune to whatever the CPU is doing — which
|
||||||
|
//! matters here, because the bus tasks preempt the LED task freely.
|
||||||
|
//!
|
||||||
|
//! The >50 µs latch gap needs no explicit delay: the caller updates at most
|
||||||
|
//! every few tens of milliseconds, which is three orders of magnitude longer.
|
||||||
|
|
||||||
|
use fugit::HertzU32;
|
||||||
|
use rp2040_hal::{
|
||||||
|
pac,
|
||||||
|
pio::{PIOBuilder, PIOExt, PinDir, Running, ShiftDirection, StateMachine, Tx, SM0},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::board::{StatusLedPin, STATUS_LED_GPIO};
|
||||||
|
|
||||||
|
/// State-machine cycles per WS2812 bit, as laid out by the program below.
|
||||||
|
const CYCLES_PER_BIT: u32 = 10;
|
||||||
|
|
||||||
|
/// WS2812B bit rate: 1.25 µs per bit.
|
||||||
|
const BIT_RATE_HZ: u32 = 800_000;
|
||||||
|
|
||||||
|
/// Order in which a part expects its three colour bytes on the wire.
|
||||||
|
///
|
||||||
|
/// Only one variant is selected in any given build, so the other is necessarily
|
||||||
|
/// unconstructed. It stays because naming both is what makes the choice — and
|
||||||
|
/// the fix, if a board turns out to differ — obvious.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum ChannelOrder {
|
||||||
|
/// Green, red, blue — the canonical WS2812B order.
|
||||||
|
Grb,
|
||||||
|
/// Red, green, blue — used by many parts sold as WS2812B, and by the WS2811
|
||||||
|
/// and SK6812 families.
|
||||||
|
Rgb,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Byte order of the part actually fitted to this board.
|
||||||
|
///
|
||||||
|
/// Determined by testing, not from the datasheet name: with [`ChannelOrder::Grb`]
|
||||||
|
/// this board's red and green come out swapped. Blue is unaffected either way,
|
||||||
|
/// which is the giveaway that only the first two bytes are transposed. If a
|
||||||
|
/// future board shows green where you asked for red, this is the line to change.
|
||||||
|
const CHANNEL_ORDER: ChannelOrder = ChannelOrder::Rgb;
|
||||||
|
|
||||||
|
/// A 24-bit colour.
|
||||||
|
///
|
||||||
|
/// Kept local rather than using `smart-leds`, which would add a dependency and
|
||||||
|
/// its own version churn for one three-field struct.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Rgb {
|
||||||
|
/// Red channel.
|
||||||
|
pub r: u8,
|
||||||
|
/// Green channel.
|
||||||
|
pub g: u8,
|
||||||
|
/// Blue channel.
|
||||||
|
pub b: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rgb {
|
||||||
|
/// Dark.
|
||||||
|
pub const OFF: Self = Self { r: 0, g: 0, b: 0 };
|
||||||
|
|
||||||
|
/// Running normally.
|
||||||
|
pub const RUNNING: Self = Self { r: 0, g: 12, b: 0 };
|
||||||
|
|
||||||
|
/// A measurement was just taken.
|
||||||
|
pub const MEASURING: Self = Self { r: 0, g: 0, b: 40 };
|
||||||
|
|
||||||
|
/// The sensor is failing.
|
||||||
|
pub const ERROR: Self = Self { r: 48, g: 0, b: 0 };
|
||||||
|
|
||||||
|
/// Pack into the order the fitted part expects, each byte most-significant
|
||||||
|
/// bit first and the whole thing left-aligned so a 24-bit autopull shifts
|
||||||
|
/// exactly one pixel out and stops.
|
||||||
|
const fn to_wire_word(self) -> u32 {
|
||||||
|
let (first, second) = match CHANNEL_ORDER {
|
||||||
|
ChannelOrder::Grb => (self.g, self.r),
|
||||||
|
ChannelOrder::Rgb => (self.r, self.g),
|
||||||
|
};
|
||||||
|
((first as u32) << 24) | ((second as u32) << 16) | ((self.b as u32) << 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single WS2812B on a PIO state machine.
|
||||||
|
pub struct StatusLed {
|
||||||
|
tx: Tx<(pac::PIO0, SM0)>,
|
||||||
|
/// Held so the state machine cannot be reclaimed or reconfigured elsewhere.
|
||||||
|
/// Dropping it happens to leave the machine running today, but that is an
|
||||||
|
/// absence of a `Drop` impl rather than a promise, so we keep ownership.
|
||||||
|
_sm: StateMachine<(pac::PIO0, SM0), Running>,
|
||||||
|
/// Last colour written, so a repeated set costs nothing.
|
||||||
|
current: Option<Rgb>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusLed {
|
||||||
|
/// Claim PIO0's first state machine and drive `pin` with it.
|
||||||
|
pub fn new(
|
||||||
|
pio0: pac::PIO0,
|
||||||
|
_pin: StatusLedPin,
|
||||||
|
resets: &mut pac::RESETS,
|
||||||
|
system_clock: HertzU32,
|
||||||
|
) -> Self {
|
||||||
|
// 2 cycles high for a zero, 7 for a one, out of 10 per bit. The delays
|
||||||
|
// are one less than the cycle count because the instruction itself takes
|
||||||
|
// one cycle.
|
||||||
|
let program = pio::pio_asm!(
|
||||||
|
".side_set 1",
|
||||||
|
".wrap_target",
|
||||||
|
"bitloop:",
|
||||||
|
" out x, 1 side 0 [2]", // low tail of the previous bit
|
||||||
|
" jmp !x do_zero side 1 [1]", // every bit opens with a high pulse
|
||||||
|
"do_one:",
|
||||||
|
" jmp bitloop side 1 [4]", // a one holds the line high
|
||||||
|
"do_zero:",
|
||||||
|
" nop side 0 [4]", // a zero drops it early
|
||||||
|
".wrap",
|
||||||
|
);
|
||||||
|
|
||||||
|
let (mut pio, sm0, _, _, _) = pio0.split(resets);
|
||||||
|
let installed = pio
|
||||||
|
.install(&program.program)
|
||||||
|
.expect("WS2812 program does not fit in PIO0");
|
||||||
|
|
||||||
|
// Fixed-point 16.8 divisor from the system clock down to 8 MHz. At the
|
||||||
|
// stock 125 MHz this is 15 + 160/256 = 15.625.
|
||||||
|
let divisor =
|
||||||
|
(u64::from(system_clock.to_Hz()) * 256) / u64::from(BIT_RATE_HZ * CYCLES_PER_BIT);
|
||||||
|
|
||||||
|
let (mut sm, _rx, tx) = PIOBuilder::from_installed_program(installed)
|
||||||
|
.side_set_pin_base(STATUS_LED_GPIO)
|
||||||
|
// Colours are shifted out most-significant bit first, and autopull
|
||||||
|
// at 24 bits means one FIFO word is exactly one pixel.
|
||||||
|
.out_shift_direction(ShiftDirection::Left)
|
||||||
|
.autopull(true)
|
||||||
|
.pull_threshold(24)
|
||||||
|
.clock_divisor_fixed_point((divisor / 256) as u16, (divisor % 256) as u8)
|
||||||
|
.build(sm0);
|
||||||
|
|
||||||
|
sm.set_pindirs([(STATUS_LED_GPIO, PinDir::Output)]);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
tx,
|
||||||
|
_sm: sm.start(),
|
||||||
|
current: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show `colour`.
|
||||||
|
///
|
||||||
|
/// Dropped rather than blocked if the FIFO is somehow full, since a missed
|
||||||
|
/// status update is not worth stalling a task over — the next tick repaints.
|
||||||
|
pub fn set(&mut self, colour: Rgb) {
|
||||||
|
if self.current == Some(colour) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.tx.write(colour.to_wire_word()) {
|
||||||
|
self.current = Some(colour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user