Skip to content

Commit 55b5c96

Browse files
committed
docs: comprehensive LED documentation update
guides/led.md: rewrite with build-time knobs (MAX_LEDS, LED_KIND, LED_PIN, LED_ORDER), effects table with default mapping, customisation examples with --effect/--speed, updated wire-format reference, troubleshooting entries. build.md: add MAX_LEDS build knob, update boot-defaults sentence. hardware.md: add MAX_LEDS knob, update three-LED-knobs wording to four.
1 parent c5d04c9 commit 55b5c96

3 files changed

Lines changed: 123 additions & 109 deletions

File tree

docs/build.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ flowchart TD
4444
| `LED_PIN` | `16` | `0..=29` | The status-LED GPIO for the `ws2812` and `gpio` backends (RP2350A). Default GPIO16 is the Waveshare RP2350-One. Point it at a free GPIO on boards that use 16 for something else; the indicator simply drives whatever pin you pick. (Unused by `pimoroni`, which has fixed PWM pins, and by `none`.) |
4545
| `LED_KIND` | `ws2812` | `ws2812` / `gpio` / `pimoroni` / `none` | The LED driver backend, and the **boot default** — a non-`none` build compiles all three so the driver/pin/order are runtime-switchable via `rsk hw` / PicoForge (see below). `ws2812` = a single addressable RGB on `LED_PIN` (the Waveshare default). `gpio` = a plain on/off LED on `LED_PIN` — hue/brightness collapse to lit/unlit, but the blink *pattern* still distinguishes statuses. `pimoroni` = a 3-pin PWM RGB (Pimoroni Tiny 2350: R=GPIO18, G=GPIO19, B=GPIO20, common-anode). `none` = no indicator (the status engine still runs; nothing renders it, and the phy LED fields are ignored). |
4646
| `LED_ORDER` | `rgb` | `rgb` / `grb` | WS2812 wire byte order (`ws2812` backend only). The Waveshare RP2350-One is unusually `rgb` (the default); standard WS2812B parts (e.g. the TenStar RP2350-USB) are `grb`. If a `ws2812` board comes up with red and green swapped (blue fine), flip this to `grb`. |
47+
| `MAX_LEDS` | `8` | `1``64` | PIO state-machine and frame-buffer ceiling for addressable LEDs. The **actual** connected count is set at runtime via `rsk hw --led-num` and must be ≤ `MAX_LEDS`. Default 8 covers the common 1–8 range; boards with more (up to 64) override this.|
4748
| `FAKE_MKEK` / `FAKE_DEVK` | unset | 64 hex chars | **Test builds only.** Bakes a fake OTP master key / device key into the image instead of reading the OTP fuses, so the whole OTP migration path can be exercised with zero fuse writes. The build prints a loud warning and the key is greppable in the binary. Flashing a FAKE build onto a provisioned device migrates its data under the fake key — going back orphans that data (recovery = per-applet resets). Never flash one on a device you care about. |
4849

49-
The `LED_PIN` / `LED_KIND` / `LED_ORDER` values are **boot defaults** only. A
50+
The `LED_PIN` / `LED_KIND` / `LED_ORDER` / `MAX_LEDS` values are **boot defaults**
51+
only. A
5052
non-`none` build compiles all three backends, so the LED pin, driver, and wire
5153
order are runtime-configurable — no reflash — with `rsk hw` (or PicoForge),
5254
which write them to the device's `phy` record. The build knobs decide the

docs/guides/led.md

Lines changed: 113 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,66 @@
33
The status LED is the device's only display. On the reference board — the
44
**Waveshare RP2350-One** — it's a WS2812 addressable RGB on GPIO16.
55

6-
Three properties of the indicator are **runtime-configurable**, like the USB
7-
identity: they live in the device's `phy` record and change with `rsk hw` (or
8-
PicoForge) **without reflashing**, because a non-`none` build compiles all
9-
three backends. The `LED_KIND` / `LED_PIN` / `LED_ORDER` build knobs set the
10-
*boot defaults* used when the phy record doesn't override them
11-
([build.md](../build.md), [hardware.md](../hardware.md)):
12-
13-
- **backend**`LED_KIND` / `rsk hw --led-driver`: `ws2812` (addressable RGB,
14-
the default), `gpio` (a plain on/off LED — can't show the colours below, but
15-
the blink *pattern* still tells the four states apart), `pimoroni` (a 3-pin
16-
PWM RGB, Pimoroni Tiny 2350), or the build-only `none`.
17-
- **pin**`LED_PIN` / `rsk hw --led-pin`: the `ws2812`/`gpio` data GPIO
18-
(`0..=29`), for a board that wires its LED off GPIO16.
19-
- **wire order**`LED_ORDER` / `rsk hw --led-order`: a `ws2812` board whose
20-
**red and green come out swapped** (blue unaffected) has the other byte
21-
order — the Waveshare RP2350-One is `rgb` (the default), most other WS2812B
22-
parts are `grb`.
23-
24-
A `none` build is the exception: it renders nothing and ignores the phy LED
25-
fields (there is no backend compiled to switch to).
26-
27-
The LED runs on its own high-priority task, so it keeps animating even while
28-
the firmware blocks waiting for a touch or grinds through a long RSA keygen —
29-
a frozen LED means the firmware itself is wedged, not just busy.
30-
31-
## What the states mean
32-
33-
There are four states. Each has a **fixed blink timing** baked into the
34-
firmware (`firmware/src/led.rs`); only the **color and brightness** are
35-
configurable.
36-
37-
| State | Default color | Blink (on/off) | Means |
6+
## Build-time knobs
7+
8+
Three hardware properties of the indicator are **compile-time** knobs set by build
9+
flags; a fourth, `MAX_LEDS`, sets the upper bound for the PIO buffer. The actual
10+
number of connected LEDs is configured at **runtime** via `rsk hw --led-num` (or
11+
PicoForge) and must be ≤ `MAX_LEDS`.
12+
13+
| Knob | Default | When to change it |
14+
|---|---|---|
15+
| `LED_KIND` | `ws2812` | `ws2812` (addressable RGB, default), `gpio` (plain on/off), `pimoroni` (3-pin PWM RGB), or `none` (no indicator). See [build.md](build.md). |
16+
| `LED_PIN` | `16` | A board whose addressable LED is on a different GPIO (`0..=29`). |
17+
| `LED_ORDER` | `rgb` | A WS2812 board with swapped red/green — set `grb` (the WS2812B standard). The Waveshare RP2350-One is `rgb`; most other parts are `grb`. |
18+
| `MAX_LEDS` | `8` | A board with **more than 8** daisy-chained addressable LEDs (max `64`). The actual connected count is set at runtime with `rsk hw --led-num`. |
19+
20+
```sh
21+
# example: build for a 4-LED board with standard GRB order
22+
env MAX_LEDS=4 LED_ORDER=grb cargo build --release -p firmware
23+
# then set the runtime count (persists across reboots):
24+
rsk hw --led-num 4
25+
```
26+
27+
Once built, a non-`none` build compiles all backends, so the pin, driver, wire
28+
order, and LED count are **runtime-changeable** — no reflash — with `rsk hw`
29+
([build.md](../build.md)). The build knobs set the boot defaults.
30+
31+
What the LED shows — colour, brightness, and the **visual effect** — is
32+
runtime-configurable separately, covered next.
33+
34+
## Effects
35+
36+
Each of the four states can run one of several animated effects. The effect
37+
determines *how* the LED(s) display the state's colour and brightness. All
38+
effects work with any number of LEDs — `vapor` and `sparkle` shine on a single
39+
LED too; `bounce` and `flow` naturally reduce to a static colour or a single
40+
pixel when there is only one LED.
41+
42+
Effects only render on the **`ws2812`** backend (addressable RGB). The `gpio` and
43+
`pimoroni` backends always use the classic on/off blink, regardless of the
44+
effect setting — they lack per-LED control and pixel-level colour.
45+
46+
| Effect | ID | What you see | Suits |
3847
|---|---|---|---|
39-
| idle | green | 500 / 500 ms — slow, even | ready, nothing in flight |
40-
| processing | green | 50 / 50 ms — fast flicker | handling an APDU / crypto op |
41-
| **waiting for touch** | yellow | 1000 / 100 ms — long on, brief blink | press the button to confirm |
42-
| boot | red | 500 / 500 ms | the brief power-up state |
48+
| `legacy` | 0 | Classic on/off blink (TIMING table) | Original blink behaviour |
49+
| `vapor` | 1 | All LEDs breathe together — smooth triangle-wave brightness | Idle (default) |
50+
| `bounce` | 2 | A wide hump of light glides back and forth with half-step interpolation | Touch (default) |
51+
| `flow` | 3 | Yellow→red gradient flowing left to right with a trailing wake | Processing (default) |
52+
| `sparkle` | 4 | Each LED flashes an independent random colour | Boot (default) |
53+
54+
### Default mapping (multiple LEDs)
4355

44-
The **touch** state is the one to learn. WebAuthn dialogs, `ssh`, and `gpg`
45-
look hung at exactly the moment the device is waiting for your press — a
46-
near-solid yellow that ticks off once a second is the cue to tap the button.
56+
| State | Default effect | Default colour | Means |
57+
|---|---|---|---|
58+
| idle | `vapor` — gentle breathing | green | ready, nothing in flight |
59+
| processing | `flow` — warm-colour flow | yellow→red gradient | handling an APDU / crypto op |
60+
| **waiting for touch** | `bounce` — smooth bounce | yellow | press the button to confirm |
61+
| boot | `sparkle` — random sparkle | red | the brief power-up state |
4762

4863
A few honest details:
4964

50-
- **No dedicated error color.** The firmware does not light a distinct "error"
65+
- **No dedicated error colour.** The firmware does not light a distinct "error"
5166
state; a failed operation just drops back to idle. Read the host tool's exit
5267
code, not the LED, for success or failure.
5368
- **The touch state needs the touch build.** It is only ever shown on the
@@ -67,9 +82,10 @@ whatever the ROM does. That mode is for flashing firmware and OTP, covered in
6782

6883
## Customize
6984

70-
Color and per-channel brightness are configurable **per state**; the values
71-
persist in flash (`EF_LED_CONF`) and apply live — no reboot. The host command
72-
is `rsk led`:
85+
### Colour & brightness
86+
87+
Per-state colour and per-channel brightness are configurable; the values
88+
persist in flash (`EF_LED_CONF`) and apply live — no reboot:
7389

7490
```sh
7591
rsk led --get # print the current config
@@ -78,97 +94,86 @@ rsk led --status idle --brightness 64 # 0–255; 0 = that state goes da
7894
rsk led --status idle --color blue --brightness 64
7995
```
8096

81-
Selectors and values:
97+
### Effect & speed
8298

83-
| Flag | Values |
84-
|---|---|
85-
| `--status` | `idle`, `processing`, `touch`, `boot` (default `idle`) |
86-
| `--color` | `off`, `red`, `green`, `blue`, `yellow`, `magenta`, `cyan`, `white` |
87-
| `--brightness` | `0``255` per channel (`0` = off) |
88-
| `--steady` | solid color, no blinking — **global**, affects every state |
89-
| `--blink` | the opposite: restore blinking |
99+
Each state's effect and animation speed are configurable the same way:
100+
101+
```sh
102+
rsk led --status idle --effect vapor # change the effect
103+
rsk led --status touch --effect bounce --speed 15 # custom speed (ticks per step)
104+
rsk led --status processing --effect legacy # revert to classic on/off blink
105+
```
106+
107+
`--speed 0` (or omitting `--speed`) uses the effect's built-in default.
108+
109+
### Steady / blink
90110

91111
`--steady` and `--blink` are global, not per-state: the firmware keeps each
92112
state's timing internally, but a single flag decides whether *any* of them
93-
blink. So `--steady` makes the whole indicator a solid lamp whose color tracks
113+
blink. So `--steady` makes the whole indicator a solid lamp whose colour tracks
94114
the current state, and `--blink` brings the blink patterns back.
95115

96116
```sh
97117
rsk led --status idle --color cyan --steady # solid cyan at idle, no pulse
98118
rsk led --blink # back to the blink patterns
99119
```
100120

101-
`rsk-tui` has a "Cycle idle color" action that steps the idle state through
102-
the palette, plus "Read LED state" — for per-state color, brightness, or the
121+
`rsk-tui` has a "cycle idle color" action that steps the idle state through
122+
the palette, plus "Read LED state" — for per-state colour, brightness, or the
103123
steady toggle, use `rsk led`.
104124

125+
### Selectors and values
126+
127+
| Flag | Values |
128+
|---|---|
129+
| `--status` | `idle`, `processing`, `touch`, `boot` (default `idle`) |
130+
| `--color` | `off`, `red`, `green`, `blue`, `yellow`, `magenta`, `cyan`, `white` |
131+
| `--brightness` | `0``255` per channel (`0` = off) |
132+
| `--effect` | `legacy`, `vapor`, `bounce`, `flow`, `sparkle` |
133+
| `--speed` | `0``255` (`0` = effect's built-in default) |
134+
| `--steady` | solid colour, no blinking — **global**, affects every state |
135+
| `--blink` | the opposite: restore blinking |
136+
105137
## Hardware wiring (`rsk hw`)
106138

107-
The **look** (`rsk led`, above) is one layer; the **wiring** — which pin, which
108-
driver, which wire order — is the other. The wiring lives in the `phy` record
109-
(the same device-config blob PicoForge writes) and is applied at **boot**, so a
110-
change needs a reboot — `rsk hw` issues a warm one for you unless `--no-reboot`:
139+
See [hw.md](hw.md) for the full reference. The LED wiring — pin, driver, wire
140+
order — lives in the `phy` record, shared with PicoForge:
111141

112142
```sh
113-
rsk hw # show the current phy LED config
114143
rsk hw --led-pin 22 # move the WS2812/gpio data pin to GPIO22
115144
rsk hw --led-driver gpio # switch to a plain on/off LED
116145
rsk hw --led-order grb # fix a red/green swap on a GRB part
117-
rsk hw --led-pin 22 --led-order grb # e.g. the TenStar RP2350-USB
118-
rsk hw --led-driver ws2812 --no-reboot # stage a change; reboot later
119146
```
120147

121-
| Flag | Values |
122-
|---|---|
123-
| `--led-pin` | `0``29` (RP2350A GPIOs) — the `ws2812`/`gpio` data pin |
124-
| `--led-driver` | `gpio`, `pimoroni`, `ws2812` |
125-
| `--led-order` | `rgb`, `grb` (the `ws2812` backend only) |
126-
| `--get` | print the current phy LED config and exit |
127-
| `--no-reboot` | write but don't reboot (the change applies on the next boot) |
128-
129-
A field you never set stays at the firmware build default (`rsk hw` with no
130-
setters, or `--get`, prints which are overridden). `rsk hw` does a
131-
read-modify-write of *only* the LED fields, so a USB identity or other phy
132-
option set elsewhere (PicoForge) is preserved. A `none` build ignores these —
133-
there is no backend compiled to render the LED.
134-
135148
### Reset to defaults
136149

137-
There's no single "reset LED" verb; set the values back yourself. The factory
138-
defaults are the table above at brightness 16, blinking:
139-
140150
```sh
141-
rsk led --status idle --color green --brightness 16
142-
rsk led --status processing --color green --brightness 16
143-
rsk led --status touch --color yellow --brightness 16
144-
rsk led --status boot --color red --brightness 16
151+
rsk led --status idle --color green --brightness 16 --effect vapor
152+
rsk led --status processing --color green --brightness 16 --effect flow
153+
rsk led --status touch --color yellow --brightness 16 --effect bounce
154+
rsk led --status boot --color red --brightness 16 --effect sparkle
145155
rsk led --blink
146156
```
147157

148158
## Under the hood
149159

150160
`rsk led` talks to the firmware's vendor applet over CCID
151-
(`tools/rsk/led.py`, `firmware/src/vendor.rs`): **SET LED** (`INS 0x10`) packs
152-
brightness into `P1` and color + the steady bit + the target state into `P2`;
153-
**GET LED** (`INS 0x11`) returns the whole `[steady, (color, brightness) × 4]`
154-
block that `--get` prints. The firmware writes it to `EF_LED_CONF` and reloads
155-
it on every boot, so your colors survive a power cycle but not an OpenPGP/FIDO
156-
factory reset of other applets (those don't touch this file).
157-
158-
`rsk hw` instead writes the **wiring** to the `phy` record (`EF_PHY`) via the
159-
rescue applet (`tools/rsk/hw.py`, `crates/rsk-rescue/src/phy.rs`): **READ**
160-
(`INS 0x1E`, P1=01) and **WRITE** (`INS 0x1C`, P1=01) the same TLV blob
161-
PicoForge uses — `led_gpio` (tag `0x04`), `led_driver` (`0x0C`), plus the
162-
RS-Key vendor `led_order` tag (`0x0D`, which PicoForge skips as unknown). At
163-
boot `firmware/src/main.rs` reads those fields and selects the pin (a `match`
164-
over GPIO `0..=29`) and the driver; the wire order is a runtime red/green swap
165-
in the render task, so one binary serves both RGB- and GRB-wired parts.
166-
`EF_PHY` survives every applet factory reset.
167-
168-
One board quirk worth knowing: the Waveshare RP2350-One's WS2812 takes bytes in
169-
**RGB** wire order, not the WS2812B-standard GRB — the `rgb` default matches it,
170-
and a `grb` board just flips `LED_ORDER` (build) or `rsk hw --led-order grb`
171-
(runtime); the swap touches red/green only (blue is unaffected).
161+
(`tools/rsk/led.py`, `firmware/src/vendor.rs`):
162+
163+
- **SET LED** (`INS 0x10`) packs brightness into `P1` and colour + the steady
164+
bit + the target state into `P2`. When the caller sends 1–2 data bytes, they
165+
set the effect and speed for that state.
166+
- **GET LED** (`INS 0x11`) returns the whole config block:
167+
`[steady:1, (effect:1, color:1, brightness:1, speed:1) × 4]` (17 bytes).
168+
169+
The firmware writes the block to `EF_LED_CONF` and reloads it on every boot,
170+
so your settings survive a power cycle but not an OpenPGP/FIDO factory reset
171+
(those don't touch this file). The `led.rs` module keeps per-status atomics
172+
that the render task reads live — SET LED updates them immediately, then
173+
persists the full block to flash.
174+
175+
For the wiring half (`rsk hw`), see [hw.md](hw.md); it writes to `EF_PHY` via
176+
the rescue applet and applies at next boot.
172177

173178
## Troubleshooting
174179

@@ -180,6 +185,11 @@ and a `grb` board just flips `LED_ORDER` (build) or `rsk hw --led-order grb`
180185
- **Red and green look swapped.** Wrong wire order for your LED part — flip it
181186
with `rsk hw --led-order grb` (or build with `LED_ORDER=grb`); see the
182187
RGB-vs-GRB note above.
188+
- **Only the first LED lights up; the rest stay dark.** The board has multiple
189+
daisy-chained addressable LEDs, but the runtime LED count was never set.
190+
Run `rsk hw --led-num <your count>` to configure it (persists across reboots;
191+
the change applies after a warm reboot). If you need a higher buffer ceiling,
192+
rebuild with `MAX_LEDS=<n>`.
183193
- **`rsk led` can't reach the device.** It needs the CCID interface up
184194
(`pcscd` on Linux); if `gpg --card-status` / `rsk status` also fail, fix that
185195
first ([linux.md](../linux.md)).

docs/hardware.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ cover it:
2929
|---|---|---|
3030
| `FLASH_SIZE` | `4M` | A board with a different QSPI flash chip (e.g. `8M`). `build.rs` regenerates `memory.x` from it. Must be ≥ ~2 MB and ≤ 16 MB. |
3131
| `LED_PIN` | `16` | A board that uses GPIO16 for something else, or wires its addressable LED elsewhere (RP2350A: GPIO `0..=29`). |
32-
| `LED_KIND` | `ws2812` | A board with a different indicator: `gpio` (a plain on/off LED on `LED_PIN`), `pimoroni` (3-pin PWM RGB, Pimoroni Tiny 2350), or `none`. Default `ws2812` is the Waveshare addressable RGB. |
32+
| `LED_KIND` | `ws2812` | `ws2812` (addressable RGB, default), `gpio` (plain on/off), `pimoroni` (3-pin PWM RGB), or `none` (no indicator). See [build.md](build.md). |
3333
| `LED_ORDER` | `rgb` | A `ws2812` board whose red and green come out swapped (blue fine): set `grb` (the WS2812B standard). The Waveshare RP2350-One is `rgb`; most other parts are `grb`. |
34+
| `MAX_LEDS` | `8` | A board with **more than 8** daisy-chained addressable LEDs. The buffer ceiling; the actual connected count is set at runtime ([guides/led.md](guides/led.md)). |
3435

3536
```sh
3637
# example: an 8 MB board with a plain LED on GPIO25
@@ -40,10 +41,11 @@ env FLASH_SIZE=8M LED_KIND=gpio LED_PIN=25 cargo build --release -p firmware
4041
env FLASH_SIZE=16M LED_PIN=22 LED_ORDER=grb cargo build --release -p firmware
4142
```
4243

43-
The three LED knobs (`LED_PIN` / `LED_KIND` / `LED_ORDER`) set only the *boot
44-
defaults*: a non-`none` build compiles all three backends, so the pin, driver,
45-
and wire order are also changeable at **runtime** — no reflash — with `rsk hw`
46-
or PicoForge, which write them to the device's `phy` record
44+
The four LED knobs (`LED_PIN` / `LED_KIND` / `LED_ORDER` / `MAX_LEDS`) set only the
45+
*boot defaults*: a non-`none` build compiles all three backends, so the pin,
46+
driver, wire order, and buffer ceiling are also changeable at **runtime** — no
47+
reflash — with `rsk hw` or PicoForge, which write them to the device's `phy`
48+
record
4749
([guides/led.md](guides/led.md)). The build knobs still matter for picking a
4850
lean `none` build and for the out-of-the-box default.
4951

0 commit comments

Comments
 (0)