Skip to content

The clock driver becomes a driver, and two bugs it took hardware to find

Choose a tag to compare

@domschl domschl released this 24 Aug 11:00
· 306 commits to main since this release

A patch release with no new features. It is about the clock persona being right rather than bigger: three days of living with the clock produced three complaints, and chasing them ended in a structural fix and two bugs that only hardware could have found.

Brightness, rebuilt around how eyes actually work

In a dark room the bottom of the brightness scale was still glaring, the panel flickered when the room sat between two levels, and one scan line was momentarily brighter than the rest once a second.

The ramp was linear in duty cycle — and the eye is roughly logarithmic in luminance, so a "1/7th" setting was nowhere near 1/7th of the light. The seven levels are geometric now (8, 18, 40, 90, 200, 450 µs of OE per 1000 µs row), which puts level 1 at ~0.8 % duty instead of 14 %. 8 µs is a deliberate floor: the pulse is a busy-wait between two GPIO writes, and shorter than that stops being reproducible row to row.

Automatic brightness had never had levels at all — it was one vendor threshold and one fixed dim value. It now walks a six-boundary ladder with an EMA over the readings and a ±150-count deadband per boundary, so a room sitting on a threshold no longer oscillates and a genuinely dark room reaches the bottom of the scale.

The brighter scan line was the row that happened to be lit when the loop paused: OE stayed open across the gap. Every frame now ends with OE closed, so each row gets its own period and nothing extra.

The clock task becomes a real driver task

Phase 12 had served the entire appliance loop — menu state machine, I²C reads, DCF-77 feed, console polling — as one long chan_call inside the driver task. That made the clock the only RP2350 driver task still running in kernel mode, and it made phase 17's own clockisotest item impossible as written: there was no domain to put on trial.

Chess had answered the same question the other way round: chess_ui.c runs in the shell/Lisp task and calls thin U-mode drivers underneath, which is why nobody ever expected a chessisotest. The clock now matches. The appliance runs in the caller's task, and the clock task is a frame-buffer-and-row-scan server confined in U-mode under five PMP grants — exactly the RP2350 maximum, with TIMER0 granted read-only (a display driver has no business setting the system clock) and the stack sharing one region with driver state, laid out stack-low/state-high so an overflow leaves the region and faults rather than scribbling on the frame buffer.

What made it affordable was the unit of work: an op carrying one whole frame (eight rows, ~8 ms, ~125 calls/s) rather than one row. The ~1 kHz per-row cadence that phase 12 rightly refused to put on a channel never had to leave the driver — only the policy did.

Two bugs that needed real hardware

RP2350's ACCESSCTRL gates peripherals to Secure-privileged by default. It sits upstream of PMP, and a task's own memory domain cannot grant its way around it. The newly-confined driver faulted on its very first TIMER0 read — the one peripheral no previous U-mode driver had needed, because none of them kept its own clock.

The trap that made this expensive is worth passing on: when a driver task dies, its clients silently fall back to direct hardware access — so the panel kept working while USB died. The display is not evidence about the driver. /proc/ps is.

console_pump() latched Ctrl-C inside the loop that stops when its 128-byte ring is full. Stopping is correct for data (it is the back-pressure that keeps surplus input in the device instead of destroying it) and catastrophic for interrupts: once that ring filled, no Ctrl-C could ever be latched again for the rest of the boot. On an appliance nothing ever drains that ring, so a running program became impossible to interrupt — permanently. What filled it, fittingly, was our own tooling's 9P port-probe frames, whose comment described them as "harmless line noise". The latch now runs off a non-consuming peek that cannot be starved by unread input.

Also

clockisotest exists and passes. tools/sizereport-rp2350-clock.json gives the clock persona the static-RAM baseline it never had — and earned its keep the same day by catching this release's own +2066 bytes. Phase 17 is concluded.

One behaviour change worth knowing: clockstats now advances ~125 times a second while the clock runs, where it used to read calls=1 for an entire session. That is the frame op doing its job, not a regression.

Verified on a Waveshare Pico-Clock-Green board: clockisotest ISOLATED with the probe faulting as it must, Ctrl-C exiting the appliance with the pushback ring deliberately stuffed, a soak with USB and 9P responsive throughout, QEMU 261/261 on both targets, and all four board personas building clean.