Releases: domschl/LugalOS
Release list
0.15.0 — A node that measures something, and says so
A Pico 2 W with a BME280 on four wires, told its broker once, joins its WiFi and publishes
temperature, pressure and humidity from a phone charger — with nothing typed. It announces itself
on arrival, and its will announces it gone when it dies, which is what lets a subscriber tell
"quiet because nothing changed" from "off".
The one piece of new plumbing was neither MQTT nor I2C
net/tcp.c handed out p9_link_t and nothing else, because when it was written the only thing
that spoke over a connection was 9P and its length prefix was the framing. A protocol with
framing of its own needed a second view of a connection rather than a second transport:
tcp_stream_*, on the same slot table, the same buffers and the same pump, costing no new memory.
Writing it taught the rest. The first version transmitted from the writer's task, which broke the
"everything on one call stack" premise net_task_body() had documented all along — a 64 KB echo
stalled about one run in four, and the capture showed 3840-byte segments on a 1500-byte MTU:
two frame builds interleaved in the one shared transmit buffer. The receive side had the same
shape, which a single hart merely hides, so a stream's receive buffer is now a lock-free SPSC ring.
Each measurement decides for itself when to speak
An exponential moving average kills the jitter; a rule per measurement decides the rest — publish
on a move of delta, never faster than a minimum, always within a maximum. The heartbeat is not
optional: a value that has not changed all afternoon must still arrive, or it cannot be told from a
dead node.
One board, one three-minute window, the same 5 s sampling, with a finger held on the sensor partway
through:
| topic | publishes | why |
|---|---|---|
temperature |
25 | touched: 26.3 → 30.7 °C and back |
humidity |
16 | followed the touch, but a 0.5 %RH delta |
pressure |
2 | unaffected — heartbeats only |
What silicon showed, again
The radio wedged seconds after every association and stayed wedged — and the cause was not the PIO
program. The CYW43 supervisor's RSSI poll reached the bus without the bus lock, and
pio_gspi_transfer() begins by disabling the state machine, so a preempted transfer had its SM
stopped underneath it. The capture said so literally: ctrl=0x00000000.
That bug was found twice independently, from opposite ends — once from the board's own boot log the
day it appeared, once from a packet capture five days later — and this release keeps the earlier,
better fix, which also drops the poll from every two seconds to every thirty. On top of it: a
warn-once guard so the next caller that forgets the lock is told rather than silently corrupting
the stream, and a recovery level that resets the chip and reloads its firmware, because levels 1
and 2 send ioctls and on a desynchronised bus an ioctl is what is broken. Fifteen minutes of ping
afterwards: 1797/1800, no stall.
It also produced the release's most misleading symptom. While the radio was wedged, I2C reads
started failing too — three sensor reads in eight — which is exactly the radio/I2C timing
contention the plan had listed in advance as a risk, and was nothing of the kind. The measurement
that separates them is a ping flood against a healthy radio: 10/10 reads, 0.8% loss. A
predicted-and-plausible wrong answer is the one that sticks, so it is written down next to the risk
it impersonated.
Also here
IDSTORE_FIELD_MQTT— the broker lives beside the address and WLAN credentials, so a stored
broker is the intent to publish, with no separate enable flag.I2C_OP_XFER— one generic transfer, so the next I2C part costs no kernel-surface change.tests/mqttbroker.py— a broker that can misbehave on purpose: refuse a CONNECT, split a
header across segments, or die and be replaced on the same port. A peer that can only behave
correctly cannot test a client.- 359 QEMU tests, on RV32 NOMMU, RV64 MMU and a two-hart RV64 SMP target.
Known limitations, stated plainly
- The password crosses the LAN in the clear. No TLS. This is phase 18's threat model unchanged:
auth proves who is talking, it does not hide what they say. pressureis station pressure, not reduced to sea level. At 520 m that is ~60 hPa from what
a weather service shows. Reducing it needs the sensor's installation altitude, which only the
installer knows — inventing one would be worse than publishing the honest raw quantity.- The 24-hour soak has not been run. The longest continuous evidence is fifteen minutes of ping
plus several minutes of publishing. - QoS 1 is unbuilt, on purpose: what it protects is the reconnect, and a superseded measurement
is not worth re-delivering. - ~1.4 KB of static RAM is paid by every persona whether or not it publishes; the build flags
that would have avoided that were judged disproportionate at 420 bytes and never added.
Both cores of an RP2350, running one scheduler
Both Hazard3 cores of an RP2350 now run one scheduler, and two real workloads use the second one. That is the headline, and phases 22 and 23 are what it took. But 0.14.0 is the first release since 0.13.1 in August, so it also carries the network stack, the identity store and the clock-precision work that landed in between — summarised under Also in 0.14.0 below.
Two cores, one scheduler
Phase 22 built the locking foundation first, deliberately, before a second hart existed to need it. Per-hart identity through tp and a hart_t record; a spinlock_t and a re-entrant yielding ylock_t built on RISC-V amoswap behind an arch seam; every hand-rolled lock in the tree converted and the rest audited; and the scheduler lock held across ctx_switch() and released on the incoming stack. The rule there was inverted in the plan as written and corrected against the tree — every shared structure a second hart could touch had to be protected before that hart was ever allowed to run kernel code, because waking it first converts a latent race into an active one on day one.
Phase 23 woke the core. Two harts pull from one ready queue; driver tasks are pinned to core 0 and what that pinning does not cover is written down rather than assumed; RP2350's core 1 is launched over the SIO FIFO and needs no separate preemption timer; and the isolation suite was re-run with both harts demonstrably mid-task at the instant of each fault, so PMP domains are enforced on a non-primary Hazard3 core and not merely believed to be.
smpstart join performs the launch. cat /proc/cpuinfo reports harts_online, and ps gains a Hart column showing what each task is pinned to.
What the second core is actually for
A second core that only makes ps longer is not worth the locking. Two workloads use it:
perft, RP2350 10450 -> 5322 ms 1.96x
chess, RP2350 9287 -> 5924 ms 1.60x
smptest locked=80000 (want 80000), harts=2, zero lost updates
(perft 4 2) splits move generation across both cores and is checked against the published node counts, not against itself — so a parallelisation bug surfaces as a wrong number rather than as a faster wrong answer. (chess 2) is a Lazy SMP search over a lock-free transposition table, 1.60x to a fixed depth. (chess) and (chess 1) are the single-core engine, byte-for-byte unchanged.
Three things emulation could not have shown
This project's own history says a second QEMU hart is not the same claim as a second real Hazard3 core. It was right again:
- A tight test-and-set spin starves the other core when both share one bus, and deadlocks the machine. Invisible under emulation, fatal on silicon.
- Hazard3's per-core interrupt force array (
meifa) is never cleared by this kernel — survivable for core 0, whichboot_header.Sbrings up from reset, and fatal for core 1, which arrives from the bootrom. - A transposition table entry torn between two cores yields a move that is legal in the current position but belongs to another one. Nothing in the engine rejects it.
Phase 23's own §1 premise — that RP2350 boots both of its Hazard3 cores — was falsified on hardware and corrected where it was made.
Opt-in, and what it costs when you don't opt in
CONFIG_ENABLE_SMP is set by exactly two presets, rv64-smp and rp2350-smp. Every other persona boots on a single hart exactly as it did before, with the second-core code compiled out entirely, so a regression in secondary bring-up cannot reach a board that never asked for it. On RP2350 the launch stays an explicit shell command rather than happening at boot, because a board that boots is a board that can be reflashed — which cost two BOOTSEL recoveries to learn.
One cost is not opt-in and is stated rather than hidden: core 1's own 16 KB stack costs 4 pages on every RP2350 persona, SMP-enabled or not, because a linker script cannot see the generated config header.
Also in 0.14.0
Everything below shipped between 0.13.1 and this tag and has had no release of its own.
An IP stack of our own, over two different wires
ARP, IPv4, ICMP, UDP and a server-side TCP, written here rather than bought in silicon — about 2,100 lines under net/, sized for an RP2350 and developed against a packet-level QEMU peer before either piece of hardware was in hand, which is why the same code came up on both wires without a per-part IP path. Two frame sources feed it through one netif_t seam: a wired ENC28J60 (SPI, MAC-only, no closed firmware anywhere) and the CYW43439 radio on a Pico 2 W, joining WPA2 and carrying 9P over the air.
The phase began by cancelling a W5500, and the reasoning is in the README because it decides the roadmap: the distinction that matters is not blob size, it is what is left to implement. A part whose closed firmware ends at the MAC layer leaves the network to us; a part whose closed firmware ends at TCP does not. The corollary is worth stating too — the CYW43439 is strictly more work than the W5500 was, not less.
Above the stack: 9P over TCP on port 564 with the same authentication and grants a serial link uses, host/fuse-p9 mounting a board's whole namespace onto a Linux host over either wire, and an SNTP client so a board can set its own clock from the segment. What the stack deliberately does not do — no IPv6, no DHCP, no TCP options past MSS — is listed in the README, because an unstated limit gets credited as a feature.
An identity that belongs to the silicon
A node's identity, its device key, its peer grants, its address and its WLAN credential now survive a firmware reflash: on RP2350 the device UID is read from OTP CHIPID, and the 4 KB record lives in its own reserved flash sector. /flash0 became its own independently flashable segment for the same reason, and the OS image halved as a side effect. Two boards were checked to report two different UIDs, and a provisioned identity was verified to survive a UF2 reflash.
Grants turn authentication into authorization: each entry names a peer, its key, the one subtree it may attach at, and whether it is read-only — where before, any peer that proved it held some configured key received the entire exported namespace, including a directory that runs Lisp programs by design. The rule that shapes the record is that a value used to prove who a node is must never also be the value used to decide who else may attach; an earlier milestone conflated the two, and the split (p9_auth_own_key() vs p9_auth_key_for()) exists specifically to close that gap.
WLAN credentials are stored as the derived PSK, never the passphrase. wifi join with no arguments and netcfg read from the record, so a board brings its own network up after a power cut with nothing typed.
One verify item is deliberately still open and not hand-waved: an interrupted flash write leaving the store readable as corrupt has not been attempted.
A clock that knows how wrong it is (phase 24, in progress)
Not finished, but well past the interesting part. The DCF-77 receiver's delay is now measured — CONFIG_DCF77_DELAY_US = 37886, against a GPS module's own PPS wired to the board, which removes the network from the measurement entirely — rather than fitted. The clock is disciplined between syncs instead of stepped once a night, the discipline is measured against the pulse and not only against the network, and the board can serve NTP to the segment and refuses to when it does not know the time. The GPS is a transfer standard: attached for the calibration, removed afterwards, and nothing in the shipped appliance depends on it.
Fixes worth naming
- Seven bugs in
ccanded, behind one failure that the suite could not see. The root of several wassizeof(buf)left behind on buffers that had moved to the heap —edwas silently destroying files on save, andccwas reading three bytes of any header. - The RTC:
OSFis sticky, so it means unverified, not unusable; a DS1307 is not a DS3231 past the clock registers; and the U-mode driver task never clearedOSF, so the lamp never went out. - 9P: FAT32's own
.and..were leaking into directory reads, andp9srvwas overflowing its stack in a waypscould not report. - A real double-dispatch race in
net/tcp.c, found while wiring up the ENC28J60.
Verified
QEMU 343/343 across rv32-nommu, rv64-mmu and the two-hart rv64-smp target. On real RP2350 silicon: the 24/24 core hardware suite on both personas, 15 more for the wired gateway, 6 over the radio, and 3 against a GPS-disciplined reference clock.
Downloads
Prebuilt UF2s for the rp2350-chess, rp2350-smp and rp2350-clock personas are attached, with SHA256SUMS and their own README.md. Flash two files: the persona image and lugalos-0.14.0-flashfs.uf2, which is now its own flash segment. The rp2350-smp image is the only attached one that can use the second core, and it does so only after smpstart join.
The clock driver becomes a driver, and two bugs it took hardware to find
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.
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.
Two-device chess polish, and a keypad new-game fix that could lose a game
A patch release finishing 0.12.0's two-device chess work, and fixing one path that could still lose
a game. Recommended for anyone running 0.12.0 on the chess persona.
Fixed: the keypad's own "new game" discarded the previous game
0.12.0 made new archive the outgoing game before starting a fresh one — but only the console
new. The keypad menu's own new-game item predated that work and reset the board directly, leaving
it as the one remaining route that could still throw away a game auto-save had been carefully
keeping. Both paths archive now.
The terminal stops talking in key codes
Entering a move on the keypad printed four tm_wait_key: raw key=N lines to the terminal — debug
scaffolding from when the key protocol was being worked out, which was harmless while the terminal
was a debugging channel and noise now that it is the session's other half.
Instead, a completed move announces itself in Standard Algebraic Notation (Board plays: Nf3),
which the terminal otherwise has no way to learn. Engine replies moved to SAN on both front ends
too, so a session speaks one notation and it is the one the PGN files use.
Keypad menu changes reach the terminal
The key presses stay silent — which keys a human is pushing is the board's business — but the state
they change does not. Setting the level from the keypad menu now prints the same line the console's
level command does, and likewise for the auto-reply toggle, save and load. Read-only menu items
(score, side to move, halfmove clock, move count) stay quiet: they are queries, and a query leaves
nothing stale.
With this, the keypad/TFT board and a terminal are fully bidirectional in both directions and for
both moves and settings — confirmed on real hardware.
Verified: 253/253 QEMU tests (RV32 NOMMU + RV64 Sv39), 24/24 hardware-in-the-loop tests against
real RP2350 silicon running these exact images.
Chess as a two-headed appliance: mirrored keypad/terminal UI and PGN save-games
The chess persona becomes a proper appliance: two input devices that behave like one machine, and
games that survive being switched off.
Mirrored two-device UI
The keypad/TFT board and a terminal were already one session for input (0.11.0), but rendering
still hung off whichever loop noticed a change — so typing a move redrew the ASCII board and left
the TFT on the previous position, while playing one on the keypad updated the TFT and left the
terminal showing raw key codes. Whichever device you were not looking at was silently stale.
Rendering now hangs off the position changing instead. A move from either device redraws the
terminal board, the TFT and the 7-segment slots alike, and the engine's replies appear on both, so
a session driven from both reads as one transcript.
PGN save-games (phase 14b)
Games are stored as real PGN with proper SAN — Nf3, exd5, O-O, Qxe7+, with correct
disambiguation — so any chess GUI or online import will open them. That notation is the substance
of the feature: the engine only had long algebraic (g1f3), which no GUI accepts.
- Auto-save to
/sd0/chess/current.pgnafter every completed move, from either input device,
and silent in both directions — a full or absent card must not stop the board being a chess
computer. - Auto-restore when a session starts, which matters most on the persona that boots straight
into chess with no shell to typeloadat. newarchives the outgoing game tochess/games/game-NNN.pgnfirst, so starting a new game
never discards the old one.- Named saves at a terminal:
save <name>,load <name>,games. The keypad keeps a single
current game — an eight-character seven-segment display is a poor file picker. - Games begun from a custom position carry
[SetUp]/[FEN]tags, so they reload as themselves.
A notation self-test ((chess-san-selftest)) round-trips every legal move across positions chosen
for the cases that break naive implementations, then saves and reloads whole games. It found a real
disambiguation bug on its first run.
Upgrade note: the old single-slot chess.save (FEN plus level, under the volume's system/
directory) is not read by this release. Nothing is deleted; the first move of your next game simply
writes a fresh current.pgn under /sd0/chess/.
Verified: 253/253 QEMU tests (RV32 NOMMU + RV64 Sv39), 24/24 hardware-in-the-loop tests against
real RP2350 silicon running these exact images.
Heap space optimization: RP2350 heap 212 KB -> 356 KB
Heap space optimization for RP2350.
On RP2350 .bss and the heap are the same budget — the page allocator starts where the image
ends — so a static buffer serving an idle subsystem is heap no other subsystem can have. The
chess persona had reached 100% of the heap at peak, which meant nothing further could be added
to the system at all.
Managed heap 212 KB → 356 KB (53 → 89 pages); static RAM 283 KB → 144 KB; chess peak 53/53 → 28/89.
Rare-but-large working memory (the compiler's pools, the chess engine's move-list pools and
position scratch, Lisp's file buffers, the U-mode probe stacks) is now taken from the heap on
demand and given straight back; the Lisp string pool is tiered by measured string length; the 9P
message size and chess search limits are board-scaled; and constant tables that were computed
into RAM at boot live in flash. Every change was measured on real silicon rather than estimated —
including confirming that reading the Zobrist tables from XIP flash costs nothing (188.6K vs
185.5K nps on perft).
Guarded going forward by a link-time heap floor (the build fails if the heap drops below
256 KB), a sizecheck target that fails on any static-RAM growth against a per-file baseline, and
/proc/meminfo / /proc/ps reporting the static breakdown and per-task stack high-water marks.
This release also carries the previously unreleased 0.10.0 work: Lisp tail-call optimization, a
mark-sweep garbage collector, a C-primitive standard library, and the host/p9lib + host/fuse-p9
9P host tooling (mount a running board as a filesystem). Six unrelated bugs were found and fixed
along the way, including a stale-board-config build hazard, a console input path that discarded
type-ahead during long commands, and a 9P iounit that promised more than the connection could
carry.
The chess board UI and the terminal are now one session with two live input devices: console
commands work while a game is being played on the keypad.
Verified: 249/249 QEMU tests (RV32 NOMMU + RV64 Sv39), 24/24 hardware-in-the-loop tests against
real RP2350 silicon running these exact images.
Micro-Kernel with hardware-isolated drivers on RP2350 and QEMU
Proof-of-concept applications:
- pico-clock-green (minimal clock / temperature display on waveshare pico-clock-green led matrix display)
- chess-computer (an old-style chess computer with 1.8" TFT board display and 4x4 keypad with 7-segment leds for move entry)