A clock that sets itself, and appliance mode
The Pico-Clock-Green persona becomes a finished appliance: it sets itself from longwave radio, is driven entirely from its own three buttons, and boots with no computer attached. Both RP2350 personas now start from a bare USB power adapter.
DCF-77 as a time source
A longwave receiver decodes the signal from Mainflingen and sets the clock. The frame decoder is target-independent and self-tests against synthetic frames on both QEMU targets, so the fiddly part was never debugged by flashing a board and waiting for radio.
The receiver is listened to continuously and the clock is written only on request — the line that keeps a radio from silently overriding a clock somebody set by hand. A sync asked for after good reception commits immediately off frames already verified, rather than waiting two more minutes. Nightly sync at 03:17 local, keyed on the calendar day so a clock being corrected cannot fire twice or skip. /proc/dcf77 and (dcf-status) report it, and both distinguish when the clock was last changed from what the radio most recently decoded.
The SIG signal monitor draws one column per second on the matrix. Its quality score grades rather than passes or fails: spacing and width are stable right up until reception collapses, so the score is built mostly from sub-debounce glitch count, which climbs smoothly as a ferrite rod turns off broadside. That is the difference between a light and a meter when you are aiming an antenna.
The whole UI moved onto the board
Three buttons, a proportional 7-row font in flash, weekday and indicator LEDs, and a menu written as a pure state machine — built on every target including QEMU, which is what lets 78 cases drive it from synthetic key presses with no hardware attached.
SIG · SYNC · LAST · AUTO · BRT · TSET · OFFS · 24H · BEEP · TEMP · DATE · EXIT
SET short is forward everywhere and ends the errand; SET long is back one level. The automatic time/temperature alternation is gone — UP and DOWN are idle-screen shortcuts instead, because a clock whose display changes on its own cannot be glanced at.
The kernel clock keeps UTC
Local time is computed from a POSIX TZ rule (CET-1CEST,M3.5.0,M10.5.0/3 by default), never stored. GPS and NTP speak UTC, DCF-77 states its own offset in every frame, and a stored local time has no correct value during the hour that repeats each October. 29 timezone cases run on QEMU, including both sides of both European switchovers and a southern-hemisphere zone.
Migration: the DS3231 now holds UTC. A chip written by 0.12.x reads an hour or two out until the next date, (set-date ...) or (dcf-sync N 1).
Appliance mode
Both RP2350 personas boot standalone. Two pieces of the system exist because of that, on a board whose only outputs are an LED matrix and a buzzer: CONFIG_CLOCK_BOOT_BEACON (one click per CLOCK_BOOT_MARK(n) plus a latching LED count, so a hang leaves its last mark lit rather than merely stopping), and tests/hw/flash.py (flashes over the 1200-baud DTR touch, no BOOTSEL press).
Three bugs this hardware exposed that were not clock bugs
- TIMER0's tick divisor was OR-ed into a register the bootrom does not hand over at zero — 28 cycles per tick instead of 12. Every clock in the system, including uptime, all bit-banged driver delays, the display refresh and chess's search budget, had been running at 42.9% of real time. Found because DCF-77's pulse spacing is a caesium standard, which makes any local time-base error immediately visible.
- The DS3231's I²C wire format
memcpy'd a native struct onto a hand-decoded byte protocol. The read path had been silently falling back to direct hardware access for months, and the year came back byte-swapped as 2055 while the month and day beside it were correct. - The PMP-granted
.ustacksNregions were never zeroed at boot, sousb_cdc.c's state came up as whatever SRAM held — its guard passed on garbage and it then indexed a ring with an unmasked garbage head. Neither persona would boot from a USB power adapter unless it had just been flashed, because a BOOTSEL session leaves that SRAM in a state that happens to fail the guard.
Also fixed: printk never parsed the - flag, so %-4s printed itself; uart_hw_putc() called task_block() on a full FIFO, which before sched_init() is a stop with nothing to wake it; and usb_cdc_init()'s enumeration handshake ran a fixed 500,000 iterations with no early exit.
None of the three was visible from a build or from QEMU. plan/phase17_clock_ui_and_dcf77.md §9 records how the last one was found — and that four theories were disproved by measurement first.
Testing
261/261 on both QEMU targets, all four presets build clean. Hardware-verified on the Pico-Clock-Green board; appliance boot verified repeatedly on both RP2350 personas, on computer USB and on a USB power adapter.