CM5 Bring-Up

How to get from two stock IO boards to a machine that boots fast enough to measure. Three things: where the Raspberry Pi firmware has moved underneath the design in cm45, the order to retire risks in, and the order to integrate subsystems in. The card layout those produce is os_card.

Status: plan. The firmware corrections are sourced and dated; everything else is a plan, not a measurement.

Back to cm45 · the card: os_card · the supervisor: demon


Two orderings, not one

The two lists below are different axes and they run in parallel. Confusing them is how the load-bearing questions end up answered last.

  • Risk retirement — short questions, mostly answerable on stock Pi OS, ordered by what could reshape the plan. cm45 §20 already ranks these and that ranking is right.
  • The integration ladder — bringing subsystems up one at a time, ordered by what each unblocks and gated by hardware arriving.

Firmware corrections

Checked against raspberrypi/documentation at 2026-08-26, the 2711/2712 EEPROM release notes at 2026-08-19, and the firmware boot/ tree at 2026-08-24. Numbers 1–3 change decisions; the rest are savings, simplifications or traps.

1. Two GPIO pins do not give four states — breaks cm45 §9

All GPIO conditionals in config.txt count as the same filter type, so they replace each other rather than combining: [gpio19=0] followed by [gpio20=1] leaves only the last one active. There is no AND, and the four-row mode table in §9 cannot be expressed.

Worse, the pins are not there either. The GPIO map in demon now allocates GP0–GP27 completely — I²C1, UART0 to Clicky, UART2 to Pulse, UART4 to Phreak, SPI0, SPI1/JTAG, UPDI, VBLANK, INTB and SDIO for A:. On the CM4, GP0/GP1 are free because quad-SPI is CM5-only; on the CM5 there is nothing spare at all.

So the firmware level gets one binary decision, and it should be the only one that genuinely needs pre-Linux effect: recovery. Everything else — dev mode, verbose boot, safe mode, skip splash — was already going to be a latched key modifier read by antboot or Clicky, and that is now not a preference but the only option.

2. PARTITION_WALK now defaults to 1

Since the August 2025 bootloader, if the requested partition fails to boot the firmware scans partitions 0–8 looking for another bootable one. On a card whose whole point is a deliberate A/B/recovery choice, that is a silent override — a failing slot A could land you in recovery, or in whatever else looks bootable, with no signal that selection was overruled.

Set PARTITION_WALK=0. Deterministic failure is what the recovery ladder in §12 is built on.

3. os_prefix fails silently, not loudly

The firmware tests the prefix for viability: if the expected kernel and .dtb are not found at the prefixed location, the prefix is discarded and set to empty. A corrupt or half-written slot directory therefore does not fail — it quietly boots whatever is at the partition root.

The fix is one line of policy: leave no kernel and no DTB at the root of the boot partition. Then a discarded prefix has nothing to fall back onto and turns into a clean, visible boot failure instead of a wrong-slot boot.

Second trap in the same mechanism: overlays only follow the prefix if ${os_prefix}${overlay_prefix}README exists — otherwise they are treated as shared. Overlays are kernel-coupled, so put a README in every slot's overlays/ or slot B will silently load slot A's.

4. disable_splash=1 is a no-op on the CM5 — corrects §8

It suppressed the rainbow screen drawn by start.elf, and there is no start.elf on a 2712 — the bootloader has the firmware embedded and loads the kernel directly. What actually paints a screen before your kernel there is the bootloader's HDMI diagnostics and network-install UI.

So §8's belt-and-braces list splits by module: disable_splash=1 stays correct under [cm4], and the CM5 equivalent moves into the EEPROM config as DISABLE_HDMI=1, HDMI_DELAY=0, NET_INSTALL_ENABLED=0. The FPGA gating the overlay layer still does the real work; this is the bench-visible half.

5. The boot-budget table is CM4-shaped — corrects §8

"Reading start4.elf (~3 MB) + kernel" describes the 2711 only. The CM5 reads no ELF at all, so its handoff has a different — and probably smaller — floor. The start4cd.elf suggestion in the same section is likewise CM4-only.

You have two boot budgets, not one, and the eZX's is the one with the fixed 3 MB read in it. Measure both before assuming ~1.5 s covers the pair.

6. Network install costs about a second

Keyboard detection for the network-install UI adds roughly 1 s, plus a 900 ms keyboard wait. NET_INSTALL_ENABLED defaults to 0 on Compute Modules, but shipped EEPROM configs do set NET_INSTALL_AT_POWER_ON=1 — clear it. Check what is actually on your modules with rpi-eeprom-config first.

The same check settles another §7 item: the note that the CM4 default is BOOT_ORDER=0xf25641 describes a shipped config, not the firmware default, which is 0xf41 when the field is empty.

7. Some of the boot tuning already landed upstream

initial_turbo changed from 0 to 60 in the November 2024 firmware specifically to cut boot time, and the governor moved from powersave to ondemand. The 2712 bootloader enables turbo clocks before loading the kernel as of April 2026, and a fast eMMC boot path for CM5 arrived in February 2026. Do not spend a day re-deriving these.

8. The bootloader can count failed boots now — CM5 only

New expression filters include [boot_count>N], [boot_partition=N] and [boot_arg1=…]. §9 rejected the kexec selector on the grounds that tryboot is one-shot and cannot count, and that "three failed boots, fall into recovery" needed a stage that could — [boot_count>3] expresses exactly that, in config.txt, for free. This partly answers §21 open question 13.

The catch: these filters are Pi 5 family only. So the Ant64 gets automatic recovery fallback at firmware level and the eZX on a CM4 does not — an asymmetry to decide deliberately rather than discover. Either accept it and give the CM4 a userspace equivalent, or implement the counting policy in antboot on both so the machines behave identically.

9. A minimal CM5 image no longer needs os_check=0

Since the March 2025 bootloader, os_check looks for the CM5 IO board DTBs rather than the Pi 5 board one. But CM5 Lite is board type 0x1a and takes bcm2712-rpi-cm5l-cm5io.dtb, not the cm5- file — and Lite is what both products ship. Put both names in every slot.

Related, for the day it comes up: a CM5 on the CM4 IO board needs device_tree=bcm2712-rpi-cm5-cm4io.dtb set explicitly — that DTB exists but is not auto-selected.

The one nobody has confirmed

Do [gpioN=…] filters work at all on the CM5? On a 2712 the GPIOs are behind RP1, reached over an internal PCIe link, and the documentation says nothing either way — every worked example is Pi 4-era. Recovery selection on the Ant64 depends on the answer.

It is a ten-minute test and it belongs early: put [gpio0=1] kernel=nonexistent.img in config.txt, strap the pin both ways, and see whether the machine refuses to boot. Do it on the CM5 before any of the boot-mode work is designed around it.


Risk retirement

cm45 §20's ordering, with what has changed since. *Resist writing antboot or touching the `sys_` port until 1–5 are done* — two of them could change what you are porting to*.

# Test Retires
1 Three concurrent displays on the CM5IO — DSI panel plus two HDMI on vc4-kms-v3d The v2 link allocation. Ant64-only — the eZX has no keyboard panel, so all DSI work is CM5IO-only and the CM4IO never needs a panel or its cable. No longer blocked: see below
2 ImGui on GLES at 720p, stock Pi OS, SDL2 KMSDRM The whole premise. glGetString(GL_RENDERER) must say V3D, not llvmpipe. Log frame time against the P4's 35.7 ms softraster spike
3 Tang Mega into the CM4IO PCIe slot — lspci, map a BAR, and does Gowin's IP expose a requester interface? §21 open question 1, the most architecturally load-bearing unknown. Decides push-model versus PIO fallback
4 Boot floor — BOOT_UART=1, then dmesg Whether boot tuning is worth doing. Needs doing twice (correction 5)
5 DSI1 driving the real production panel, standalone Highest-risk hardware item; cheap now, expensive late. A stand-in panel retires the software half early — see below
6 Transceiver maximum line rate, from the datasheet Whether the main port can claim 4K60
7 rpi-image-gen under WSL2 Whether the Windows build path works with the privileges it needs
8 PCIe round-trip and throughput under display-list-like traffic The bandwidth assumptions
9 TMDS ingest — CM HDMI into the FPGA's DVI Rx, one 720p frame into DDR3 The overlay path

Two notes on these:

  • Test 4 needs preparation on the CM5. Bootloader output goes to the module-side debug connector, not GPIO14/15; enable_uart=1 there gives the kernel console only, and the early VPU bootstage output cannot be redirected to RP1 GPIOs. Fit the JST or wire TP35/TP36 before the module goes near a case. The CM4 is the rig for the bootloader stage.
  • Take the GPIO-filter test above at the same sitting as test 4 — same rig, same afternoon.
  • Test 7 is worth doing early despite its position. §11 flags it as needing verification before the build process commits to it: rpi-image-gen expects a Debian host and invokes mmdebstrap in a private mount namespace needing CAP_SYS_ADMIN; only Bookworm/Trixie arm64 is formally supported, and a non-arm64 host runs under QEMU — slower and explicitly unsupported. If that path does not work the whole update story needs a different foundation.

Risks 1 and 5 are unblocked by a stand-in panel

Both were waiting on a DSI panel to exist on the bench. Two are now on the way — the official 5″ Touch Display 2 (720×1280) and a Waveshare 4.3″ (480×800) — and either is enough to start.

Neither exposes the 1.8 V problem, and that is not a property of the panels. The 1.8 V domain is a property of a bare panel module: the ST7102's FPC brings out VCC 2.8 V, IOVCC 1.8 V and every digital line — touch I²C, both resets, TE, PWM — in that domain. A finished display board carries its own regulators, and what reaches the connector is DSI D-PHY (self-referencing differential, never shifted) plus power and 3.3 V I²C on dedicated pins. Nothing to translate on either stand-in.

So they retire the precursor to risk 5, not risk 5:

  • Proved — on the bench, CM5 + Touch Display 2: the CM's DSI hardware works on CAM/DISP0 with ,dsi0 and no J6 jumpers; vc4-kms-v3d brings up a DSI panel; and three concurrent displays hold — two HDMI plus the DSI panel, with a mode of 1280×720 set. That last one is not a bench curiosity: it is the Ant64's production display topology (panel + two HDMI to FireStorm, one overlay and one data-in-video), so the shape of the machine is now known to be possible on the silicon rather than assumed.
  • Still unproved on the software side: three concurrent connectors lighting up under KMS is not three ImGui contexts sustaining frame time. Connectors are cheap; GL surfaces and their bandwidth are not. The second DRM connector carrying a second ImGui context remains rung 5's software half — now testable months early, on the real topology.
  • Not proved, hardware: the production panel's driver, the level translator, the backlight driver, and panel reset timing on a CM GPIO. Also touch — a panel that displays while touch does not is the expected intermediate state, so confirm it explicitly rather than inferring it from a working picture.

The bench just answered a question nobody had asked: DSI is a different DRM device

card1-HDMI-A-1, card1-HDMI-A-2, card2-DSI-1. The two HDMI outputs share a device; the panel does not. This is not a quirk of enumeration — it is architecture. Raspberry Pi's own DSI whitepaper: on Pi 5 both DSI ports "have moved from the SoC to the Raspberry Pi RP1 device", and "the DSI block is part of the Raspberry Pi RP1 device and is allocated its own PLL". HDMI stays on the BCM2712's VideoCore. Two blocks, two drivers, two DRM devices, two clock domains.

Measured on the bench, CM5:

Node Driver Role
card0 v3d Render only — the GPU. No connectors, no KMS
card1 vc4-drm Scanout for both HDMI outputs
card2 drm-rp1-dsi Scanout for the panel

So it is three devices, and rendering was already separate from scanout before the panel arrived — the standard split-render/display arrangement, where the GPU produces a buffer and a different device scans it out. That part is well-trodden and Mesa handles it; the new thing is that there are now two display devices and nothing spans them.

Three consequences, and they are the expensive kind:

  1. There is no atomic commit across the panel and the HDMI outputs. Separate DRM devices are separate commit domains. Nothing can present a frame to the panel and to FireStorm as one operation.
  2. They will drift. Independent PLLs mean independent vblank. Any design that assumes the panel and the overlay are frame-locked is wrong, and would have been discovered late.
  3. Buffers cross by dma-buf/PRIME, not by sharing a GBM device. Render on the v3d node, export, import into each scanout device — a different shape of code from "one device, N connectors".

The good news is where the split falls. The two surfaces that must stay coherent — the overlay HDMI and the data-in-video HDMI, both read by FireStorm — are on the same device and can be committed together. The one that is free to drift, the keyboard panel showing its own content, is the one that is isolated. Had it landed the other way round it would have been a serious problem; as it is, it is a constraint we can design to. The eZX, with no panel, is single-device throughout.

So rung 0's display layer is "N contexts across M devices", not "N contexts". That is a materially bigger abstraction than the page assumed, and precisely the thing this result arrived early enough to change.

Two smaller notes in the same finding:

  • CM4-era DSI knowledge does not transfer. Pi 1–4 drove DSI from the SoC; ours is an RP1 block. Any porting reference from CM4 DSI work describes different hardware.
  • The dedicated PLL is a real win for the production panel. Pi 1–4 derived DSI's clock by "an integer divider from a fixed phase-locked loop"; RP1's DSI has its own. Hitting the ST7102's exact pixel clock stops being a divisor-hunting exercise.
  • card0 being v3d is also the premise test's expected answer. dev_step2 checks that glGetString(GL_RENDERER) reports V3D rather than a software rasteriser; the device list now says where that comes from.

The one that decides an API choice: SDL is probably the wrong foundation for the display layer. SDL2's KMSDRM backend opens one DRM device — it was never built to span two. That is fine for the premise test, which asks a single question about frame time on one output, but it cannot be the architecture, because the Ant64 needs the panel and the HDMI pair driven together. Verify it rather than take it on trust (SDL_KMSDRM_DEVICE_INDEX will select which card SDL takes, which is itself the useful bench lever for testing the panel and HDMI separately) — but plan on AntOS's display layer sitting on libdrm/GBM/EGL directly, with SDL, if it appears at all, above that rather than beneath it.

This is exactly the retrofit rung 0 warns about, and it is the second time this one bench result has moved the same decision.

Confirm the drivers behind each card while the hardware is up:

for d in /sys/class/drm/card?; do
  printf '%s -> %s\n' "$(basename "$d")" "$(basename "$(readlink -f "$d/device/driver")")"
done
ls /dev/dri/          # note which renderD node exists, and on which device

Caveat on the display result: it was obtained under the full desktop OS, so a Wayland compositor was doing the multi-device work, not us. That does not weaken the finding — card0/card1/card2 and their drivers are kernel facts, and the three-connector topology is proved possible on the silicon either way. It does mean nothing has yet shown our code spanning two DRM devices; the compositor's success is a proof of possibility, not a proof of our design. Re-confirm the three displays on Lite once the OS is swapped.

Record this configuration now, while it works. Exact dtoverlay lines, the cmdline.txt line, the DRM connector names from /sys/class/drm/, and the EEPROM version from dev_step1 §7. A known-good three-display baseline is the thing you will want to diff against the first time something stops working, and it is trivially easy to lose.

What this unlocks, and it is the expensive-to-retrofit part: rung 0 says build the display layer for N contexts from the start. The hardware that can prove an N-context layer is now on the desk. Building it single-context and widening later is the refactor this page keeps warning about — so widen it now, while the counter-example is plugged in.

Bench notes, all of the evening-costing kind:

Official 5″ Display 2 Waveshare 4.3″
Resolution 720×1280, portrait-native 480×800 — the production resolution
Overlay (CMs are not auto-detected) dtoverlay=vc4-kms-dsi-ili9881-5inch dtoverlay=vc4-kms-dsi-7inch — the 7inch overlay, per Waveshare's wiki
DSI port ,dsi0 — DISP0 is our decided port, see demon ,dsi0 too, overriding Waveshare's DSI1 advice on purpose
Power 5 V from GPIO header pins 2 and 6, separate lead from the DSI cable alone, ~1.2 W
Cable 22-pin 0.5 mm ↔ 15-pin 1 mm (RPi "Standard–Mini" display cable) check what arrived — see below

J6 jumpers: not needed, because we chose DISP0. Raspberry Pi's instruction to "add two jumpers to J6" applies to the CM5IO's CAM/DISP 1 (and the CM4IO's DISP0) — on the CM5IO's CAM/DISP 0 the I²C is already dedicated, so there is nothing to link. That is the same property that decided the port in the first place; it just also removes a step. (The CM4IO note does not apply at all — that board never drives a panel, so all DSI work is CM5IO-only and the CM4IO needs neither panel nor cable.)

Compute Modules do not auto-detect the Touch Display 2 — Raspberry Pi say so explicitly — so the overlay line is mandatory, not a fallback. Consistent with display_auto_detect=0 in os_card.

dtoverlay lines accumulate. Each is applied in turn, so a panel overlay is added to a block, never a replacement for what is already there. Two traps that follow: parameters after a dtoverlay attach to that overlay, so keep each overlay's parameters on its own line; and a bare dtoverlay= terminates the parameter list.

Orientation — and the finding that matters more than the recipe

Both panels are portrait-native and the production panel is mounted portrait beside the keyboard, so rotation is a permanent property of the machine, not a bench convenience.

On Lite, rotation goes in /boot/firmware/cmdline.txt — one line, appended with a space, never a new line:

video=DSI-1:720x1280@60,rotate=90

Confirm the connector name rather than deriving it from the port. DRM names connectors <type>-<n> counting from 1 per type, independent of which physical MIPI socket is used — so a panel on DISP0 is still very likely DSI-1, and DSI-0 names nothing. A video= line naming a non-existent connector fails silently, which is indistinguishable from the rotation not working. Read the real name off the machine:

ls /sys/class/drm/          # card1-DSI-1, card1-HDMI-A-1, …

Touch has to be rotated separately, as parameters on the panel's dtoverlay line — swapxy, invx, invy — and Raspberry Pi note these "are only required when rotating the display and using touch inputs in a non-desktop environment", which is exactly our environment. Expect to find the right combination by trial; a display that rotates while touch does not is the normal intermediate state, not a fault.

The one to write down: rotate= does not rotate us. Raspberry Pi are explicit — "The rotate= setting only rotates the text-mode console. For applications that write directly to DRM, such as cvlc or the libcamera apps, use the rotation options that come with them." AntOS writes directly to DRM. So AntOS owns its own rotation, and the cmdline.txt line only ever makes the boot console legible.

That is the right architecture regardless: rotation becomes a property of the board descriptor alongside resolution, a matrix in the display layer costing nothing per frame, applied per DRM connector — which the multi-context design at rung 0 already has to support, since the panel and the HDMI overlay do not share an orientation. Build it there and the eZX inherits a rotation of zero for free.

Do not reach for display_rotate or lcd_rotate. They are start.elf-era firmware options, and the CM5 has no start.elf — same category as boot_delay in os_card's rejected-lines table.

"22-pin" is not one connector. The CM5IO takes 22-pin 0.5 mm — the Pi 5-generation "mini" FPC — and the display end is 15-pin 1 mm standard, so the part is the Raspberry Pi 5 FPC display cable, 22-pin 0.5 mm to 15-pin 1 mm. There is also a 22-pin 1 mm connector in the world, and vendor documentation that lumps "22-pin for Pi 5 / CM5 / CM4 / CM3+" together is glossing over the difference. Worth physically checking whatever cable arrives with the Waveshare against the CM5IO socket rather than at the bench with a panel in hand.

Related trap: the 22-to-15 camera and display cables are different parts with different wiring, and they look identical. Make sure any spare you reach for is the display one.

One known pattern worth expecting: the Waveshare panel has a recurring history of the display coming up while touch does not. Budget for that being the thing you debug rather than the display.

The panel-sourcing decision this opens

The Waveshare being 480×800 — the production resolution makes it more than a stand-in. Layout, font sizing and touch-target work done against it transfer 1:1, where the 5″ Display 2's 720×1280 does not. And if a Waveshare-compatible 4.3″ 480×800 board can be sourced in volume, it would delete a cluster of carrier work: the bidirectional translator on touch I²C, the 5 V→1.8 V shifting for both resets and PWM, the 1.8 V→3.3 V return for TE and TP_INT, the constant-current boost backlight driver, the 2.8 V and 1.8 V rails, and the panel RESET GPIO. It would also hand back MCP23017 PA4 and PA5, taking the expander's spares to five.

Three things to weigh against that, in order of how much they should matter:

  • The backlight requirement is the one at real risk, and the Waveshare confirms the risk is real. cm45 §8 requires the keyboard panel to stay dark until AntOS signals ready — the FPGA can gate the main overlay, but not this panel, so Clicky holds its backlight off from power-on, on PB2 as a PWM output whose power-on duty is zero. (This supersedes the earlier assignment to Sticky: the panel sits on Clicky's board, and one PWM line serves as both enable and brightness control.) Waveshare's own wiki is the warning — its 4.3″ exposes brightness only as /sys/class/backlight/*/brightness, a Linux device that arrives far too late, with hardware PWM available solely as a modification: unsolder a resistor and wire its pad to a host GPIO. Check this specifically on any candidate board — and if no PWM pad is reachable, the fallback is Clicky gating the board's power rather than its backlight, which is cruder and reintroduces panel init-sequencing concerns.
  • Supply risk. A bare panel can be second-sourced; a specific vendor's finished board makes a consumer accessory into a product-defining dependency. Worth knowing whether the underlying panel is available separately before committing.
  • Cost and mechanics at volume. A finished board is usually dearer per unit than panel-plus-your-own-regulators, and it is a PCB with its own thickness and mounting rather than a panel and an FPC — which the case design has to accommodate either way, possibly more easily.

The honest position: the stand-in is worth using regardless, and a bulk-sourced compatible board is worth pricing — but the backlight line decides whether it is viable at all, so establish that first.


The integration ladder

Rungs group by who is the master, which is the real dependency structure and worth holding onto when the order comes under pressure. Anything that needs the expander cannot precede rung 1; anything on Pulse's SPI bus cannot precede rung 3.

Pull one thing out of the ladder and do it now. Putting the FPGA last is right for integration, but the PCIe question buried inside it (risk 3 above) needs no AntOS at all — a stock Pi OS card, the Tang Mega in the CM4IO slot, lspci, and an afternoon with Gowin's IP documentation. Do it on a separate track from rung 0, not after rung 7, or you can build months of AntOS on an assumption that turns out false.

Rung 0 — AntOS: two machines, one binary, HDMI only

CM4 on the CM4IO and CM5 on the CM5IO, USB keyboard and mouse through evdev, one ImGui context at 1280×720 straight out of HDMI0. Underneath it: the DRM/KMS display layer, the console with its VT100 emulation, the pty that captures stdout/stderr into it, the Luau runtime, the shell, and DBFS on a USB stick as D:.

This rung is not comparable in size to the ones above it. "Then we add the IO expander" makes 0 and 1 sound like peers; rung 0 is the platform-layer rewrite — the whole ESP-IDF-shaped sys_* port — and everything after it is comparatively small. Worth stating so the schedule reflects it.

Two things to get right here because retrofitting them is unpleasant: the board descriptor architecture must exist from the start, driven by the ANT64_BOARD= override, or rung 1 becomes a refactor instead of an addition. And on the CM4, dtoverlay=dwc2,dr_mode=host — without it the IO board's hub reports "hub doesn't have any ports" and there is no USB keyboard at all.

Exit: an AntOS prompt on both machines running a Luau script, from the same binary — CM4 on the A72 build, CM5 on the A76.

Rung 1 — the I²C housekeeping bus: three parts, not one

The expander arrives with two companions on the same two wires, and bringing them up together costs barely more than one. On GP2/GP3, on breakouts, since neither IO board has any of them:

  • MCP23017 at 0x20 — reset/enable/strap lines and the INTB aggregation. Note the board ID is no longer here: it moved to Clicky, so the ANT64_BOARD= override stays load-bearing until rung 5.
  • MCP79410 at 0x6F/0x57 — this is the one the boot design needs. antboot's slot selection reads its battery-backed SRAM, so without it every boot-mode and rollback mechanism in os_card is untestable. It deserves to be in the ladder in its own right rather than arriving as a detail of the expander.
  • ATECC608C at 0x60 — machine identity, and the reason identity survives a compute-module swap.
  • Sticky at 0x2A, if you have one — its I²C read path needs no Pulse, so a joystick becomes a usable input here rather than at rung 4.

The optional debug OLED at 0x3C is free to add here; it is on the bus already. One caution for later: this bus is 3.3 V and the keyboard panel's touch controller is 1.8 V behind a translator — do not hang the panel on it at rung 5 without that part.

Exit: board ID read from silicon rather than the environment; antboot selecting a slot from RTC SRAM; a signed challenge back from the 608C.

Rung 2 — Phreak (ESP32-C5)

Data over SPI0 CS0 and UART4 on GP12/13, and the SPI handshake on GP18 direct — but the EN and BOOT lines live on the expander (PA6 = EN/reset, PA7 = BOOT, per DeMon), so Phreak is not reachable in its designed form until rung 1 exists.

The thing to prove here is not networking; the IO boards have Ethernet, so nothing on the bench is blocked on Phreak. It is the anti-brick path — that DeMon can drop the C5 into ROM download mode over I²C and reflash it. That mechanism is the whole reason the EN/BOOT lines are on the expander, and it is far easier to verify now than after a carrier exists.

Exit: AT-comms driver talking over the link, and a full C5 reflash driven by DeMon with no cable touched.

Rung 3 — Pulse (ESP32-P4): a gateway, not a leaf

SPI0 CS1 for DeMon → Pulse, UART on GP4/5 plus an interrupt on expander PB3 for Pulse → DeMon. Peers rather than a hierarchy, so both directions need exercising.

One divergence to build for from the start: the DeMon → Pulse link is QSPI on the CM5 and narrower on the CM4, because SPI0_SIO2/SIO3 land on GP0/GP1, which demon's map marks CM5-only. Same protocol, different width — so bulk transfers (firmware payloads, sample data) are faster on the Ant64, and any timing assumption calibrated on the CM5 will be wrong on the eZX.

What makes this rung bigger than it looks: Pulse is the SPI master for Sticky, Cranky and the round TFT, and the UPDI master that programs both AVRs. So update sticky and update crank are three-hop operations — AntOS orchestrates, Pulse parks its sequencer work and runs the UPDI session, the AVR is written and verified. Nothing on the Pulse side of the board is reachable until this link works.

What still cannot be tested: Pulse's real job also includes feeding the FPGA over MIPI in blocks and owning the AG6201 DDC and CEC. None of that exists until rung 7, so this rung retires the link risk, not the Pulse integration risk.

Exit: transactions initiated from both ends, and Pulse's own firmware updated over the link.

Rung 4 — behind Pulse: Sticky and Cranky

Grouping these with Clicky is the one thing in the obvious plan to change. Clicky talks to DeMon (UART0, I²C at 0x28, UPDI on GP18); Sticky and Cranky talk to Pulse, on its SPI bus with their UPDI lines on its GPIOs, because they sit physically on that side of the board and UPDI is timing-sensitive. They are not three peers of each other — one DeMon-attached controller and two Pulse-attached ones.

  • Sticky — four DB9 joystick, paddle, mouse and multitap ports. Both machines. It is only partly behind Pulse, and now spans three rungs: the I²C client to DeMon at 0x2A comes up at rung 1, the SPI client to Pulse here, and the direct UART to FireStorm not until rung 7. Only the SPI path and UPDI actually wait for this rung.
  • Cranky — eight illuminated jog dials, sixteen quadrature phases and eight buttons decoded on-chip, reporting detent deltas over SPI with a CIRQ interrupt so idle dials cost nothing; 24 LED channels through an external constant-current driver, and twelve analog channels on the audio expansion connector. Ant64 only.

One dependency easy to miss: the round TFT is Pulse's surface, but its reset and backlight lines sit on Cranky. The screen cannot be initialised until Cranky is up, even though Cranky never draws a pixel on it.

And one consequence for Pulse's firmware: Pulse has no board-ID pin of its own. It can only learn which machine it is in from DeMon over the peer link, which happens after DeMon has booted and read the expander — so Pulse must start with the conservative default (no Cranky, no round screen) and be told otherwise, rather than probing at startup for devices that may not exist.

Exit: a joystick read through Sticky on both machines; on the Ant64, a dial turned and its stem lit through Cranky. Both AVRs reflashed by update sticky / update crank without a cable.

Rung 5 — DeMon side: Clicky, and the panel the eZX doesn't have

Clicky is on both machines. It brings the real keyboard, the power button, NFC, and — the part that matters to correction 1 — the latched key modifiers. Since GPIO conditionals can express only one firmware-level decision, every other boot option has to arrive this way. Clicky is where the boot-mode design lands, not a late nicety.

Clicky's firmware path is behind Pulse, even though Clicky is not. Its UPDI line now runs across the board to Pulse, which is the single serialUPDI programmer for all three AVRs over a matrix-switched UART4. Runtime is unaffected — key events still come to DeMon over UART0, status and LEDs over I²C — but update clicky is a three-hop operation, DeMon parses → Pulse drives the wire.

Bench consequence for this rung: you can bring Clicky up without Pulse, but you cannot reflash it in-system without Pulse. Program it with a standalone UPDI dongle while rungs 3–4 are still ahead of you, and treat the in-system path as something rung 3 unlocks rather than something this rung needs.

Recovery consequence worth writing down: DeMon no longer has a UPDI pin of its own, so a machine whose Pulse firmware is broken cannot reflash its keyboard controller. The ladder still terminates — Pulse is recovered over USB-Serial-JTAG through DeMon's FS hub — but it is one rung longer than it was, and on the eZX it means the keyboard's firmware path runs through the music MCU.

The 4.3″ ST7102 panel on DSI is Ant64-only and is the highest-risk hardware item. It needs the 1.8 V translator on its touch I²C, a backlight driver whose EN pin is pulled down by default, and its RESET on a CM GPIO with a pull-down. It gives AntOS its second ImGui context — build the display layer for N contexts at rung 0 and this is configuration rather than surgery.

Its absence on the eZX is a real simplification: DSI1 is unused there, so that carrier needs no DSI routing, no level translator, no backlight driver and no panel reset GPIO.

Exit: typing on Clicky and a boot option selected by holding a key at power-on, on both machines. On the Ant64, AntOS drawing on the panel while HDMI shows the main screen.

Rung 6 — storage: drive A: and the USB hubs

A: is SDIO on GP22–27, and it is one of the few genuine [cm4]/[cm5] divergences left: dtoverlay=sdio on the CM4, dtoverlay=sdio-pi5 selecting rp1_mmc0 on the CM5. Same pins, different controller, different overlay.

This may be the one item that genuinely needs the carrier. Neither IO board wires an SD slot to GP22–27, so bench-testing A: means a breakout on flying leads — and SDIO is layout- and drive-strength-sensitive with an SDR25-ish ceiling that community reports describe as finicky at the best of times. Flying leads at 25 MHz is exactly where signal integrity bites. Attempt it early enough to prove the software path — both overlays load, both controllers enumerate, a card is detected on PB5 — but do not read a negative result as evidence about the design. Underclock first; a working slow link tells you the driver stack is right and the wiring is the problem.

The USB hubs are where §21 open question 16 finally gets answered: which sysfs paths map to which physical sockets, measured rather than derived.

demon now specifies the topology as a split by role rather than by speed: the HS OTG controller drives a storage hub (D:, D!:, B:, C:, E:) and the FS OTG controller drives a HID hub (keyboard, mouse, and the USB-Serial-JTAG links to Phreak and Pulse). That is a good split — it keeps bulk traffic off the same controller as the debug links, and it means a wedged HID device cannot stall storage.

This contradicts cm45 §3 and needs a decision. §3 has the Ant64 giving D: a whole USB 3.0 controller to itself, B:/C:/D!:/E: behind a second USB3 hub, and USB2 for slow devices — and "CM5 has USB3, so storage will be faster" was one of the stated reasons for putting the CM5 in the Ant64 at all. demon's HS/FS split is a USB 2.0 topology, which is what the CM4 can do and therefore what both products can share.

Both are defensible: one design for both machines, or the Ant64 spending its USB3. But they cannot both be the port map, and the answer changes what rung 6 measures. Worth resolving before the eZX carrier is laid out, since it also decides whether routing the USB3 pins there is worth the layout cost.

The dwc2 line is this conflict in miniature, which is why it belongs under [cm4] and nowhere else. On the CM4 that overlay is not a tweak — dwc2 is the SoC's USB 2.0 controller, and it is the machine's only USB controller, so the entire eZX topology hangs behind one HS root. The CM5 is a different architecture: its ports come from RP1's PCIe-attached xHCI, which Raspberry Pi note "does not support OTG" and which needs no overlay at all. So the split is not two configurations of one design; it is two designs. Any port map claiming to serve both has to be a USB 2.0 map that the Ant64 under-uses, or two maps.

Smaller note in the same section: the storage hub is described as carrying "up to four USB mass-storage devices" but then lists five — D:, D!:, B:, C:, E:.

Whichever way it lands, reproduce the cascade on the bench and not just the socket count: cascaded hubs give deeper paths — 1-1.2.3 rather than 1-1.3 — which is exactly the case a hand-derived port map gets wrong.

Exit: drive letters resolving through the descriptor's port map rather than by enumeration order, on both machines, with the bench topology as just another descriptor entry.

Rung 7 — FireStorm

Everything up to here has been AntOS on a Raspberry Pi. This rung is PCIe enumeration and the register shadow, then TMDS ingest of the CM's HDMI into DDR3, then overlay compositing, then the gating that keeps Linux invisible until AntOS signals ready.

Deferring it to last is right precisely because the PCIe question was pulled forward: by the time you arrive here the architecture is settled and this is integration work rather than discovery.

It is also where the direct input UART finally matters. sticky reaches the FPGA without a supervisor in the path — its own UART carrying normalised controller state straight to the fabric. clicky no longer joins it: its UART5 to FireStorm was removed to reclaim the pin, so the key/matrix stream now goes to DeMon, which forwards key events to FireStorm (the eZX soft core's ULA can no longer shadow that stream — its keys arrive via DeMon like everything else). personality makes the direct case explicit for the joystick: in personality mode the controller reaches the running softcore over Sticky's UART, no Pulse in the path.

That is a deliberate pattern worth naming, because it is easy to lose in a refactor: a running personality gets its joystick straight from Sticky's AVR — no DeMon or Pulse in the joystick loop, so it has the latency of a UART rather than of a supervisor's scheduler, and it keeps working while AntOS is busy, reconfiguring, or updating something. The keyboard is now the exception: Clicky's direct FPGA UART was removed to reclaim a pin, so key events reach a personality through DeMon (Clicky → DeMon → FireStorm) — event latency and a dependency on DeMon being up, traded for the pin. Sticky still reports to three places — SPI to Pulse and I²C to DeMon for menus, UI and the sequencer, and its direct UART to the fabric for the softcore; Clicky now reports only to DeMon (UART for keys, I²C for LEDs and the buzzer).

Exit: the AntOS overlay composited by the FPGA onto a personality's video, with the CM's own HDMI no longer reaching a monitor at all — and a joystick driving a softcore with both supervisors idle.


Boot: systemd is the wrong first lever, and READY=1 is the right one

Yes, decouple AntOS from the rest of userspace — but the mechanism is honest dependencies, not "start it early". systemd already starts everything in parallel subject to declared ordering. A unit does not become early by being asked to be; it becomes late by being ordered behind things it does not need. So the work is subtractive.

What AntOS actually waits for

For an ImGui-on-GLES app on KMSDRM the true prerequisites are the DRM node, the v3d/vc4 drivers bound, and input. That is all. It does not need network-online.target, multi-user.target, time sync, or a user session — and we already designed the network dependency away: the MCP79410 gives DeMon the wall clock without NTP, so nothing about AntOS starting has to wait for a link.

The mechanism that actually decouples is DefaultDependencies=no, which drops the implicit After=basic.target / After=sysinit.target that otherwise serialises a normal unit behind all of early boot. Then order explicitly on the DRM device rather than on a target, so AntOS starts the moment the GPU node appears.

DefaultDependencies=no is a foot-gun, so treat it as one. It also drops Conflicts=shutdown.target and Before=shutdown.target, which must be re-added by hand or the machine will not shut down cleanly. It also means /tmp, /var and udev may not be ready — acceptable only because our root is read-only squashfs with no data partition, so AntOS has very little to be early of.

Make "AntOS ready" a real signal, not a guess

cm45 §8's "until AntOS signals ready" appears in three places on this page — the panel backlight on Clicky PB2, the FPGA's gating of the Linux console at rung 7, and the status-pixel handover above. All three currently describe a moment nobody emits.

Give it one emitter. Run the AntOS unit as Type=notify and call sd_notify(READY=1) at the point the first frame is presentable. Everything that means "AntOS is up" then hangs off that single fact rather than a timer, a sleep, or a guess — the backlight comes on because AntOS said so, not 3 seconds after boot. The seven boot stages named in Two jobs, by phase map onto the same idea: each is a state change something can publish, not a duration to wait out.

The ordering of the work, which matters more than any of the above

  1. Do nothing until dev_step2 answers. If ImGui on GLES does not hold frame time, the boot budget is irrelevant.
  2. Then measure, on a recorded EEPROM version: systemd-analyze blame, critical-chain, plot. Optimising a boot you have not measured is guesswork, and the same discipline this page already demands of the EEPROM applies here.
  3. Then subtract. Mask what an appliance does not run — the *-wait-online units are the usual worst offender, along with apt-daily*, man-db, dphys-swapfile, ModemManager, triggerhappy, keyboard-setup, and getty@tty1, which will fight AntOS for the console. Treat that as a list of candidates to confirm against blame, not a recipe.
  4. Only then reshape units. DefaultDependencies=no is a last resort, after the cheap subtractions.

And keep the perspective: most of the boot time is not in systemd at all. It is the bootloader, initial_turbo, the kernel, and the initramfs — see os_card, where the config.txt sketch and the EEPROM block hold the seconds that actually move.


Keep direct-HDMI forever — it is not a scaffold

"HDMI directly out at 720p" reads like a temporary measure, but it should become a supported mode selected by the board descriptor, not a hack deleted at rung 7. 720p is the real overlay authoring resolution, so nothing built for it is throwaway — and a mode where AntOS drives a monitor directly is exactly what you want when the FPGA is absent, powered down, being reconfigured, or holding a bad bitstream.

It is also the configuration a developer without a carrier can work in, which matters for as long as carriers are scarce. The scaffolding built this month is the dev mode relied on for years.


The eZX has no display when the FPGA is down

The Ant64 has two always-on surfaces that survive any FPGA state — the keyboard panel and Pulse's round screen — and the docs lean on them: boot UI before FireStorm is loaded, recovery screens, progress during a reconfigure. The eZX has neither. What it does have, and what both machines have, is better than nothing and in one respect better than a screen:

  • Eight status NeoPixels, the head of Clicky's WS2812 chain, addressed as status[0..7], driven by intent sent over the housekeeping I²C to Clicky's TWI0 slave
  • The RGB power light, deliberately not on that chain so it works even when the chain is unpowered or mid-refresh
  • The buzzer, on the same I²C delegation — DeMon names a tone and duration, Clicky handles timing

All three are on Clicky, which runs at 5 V and comes up in milliseconds, so they are alive long before Linux — the same class of indicator as the power button, and available across the whole window where a display cannot help.

One robustness property falls out of the wiring for free: WS2812 is a daisy chain and the status LEDs read their bits first, with the keyboard's per-key pixels downstream. Status works even if the keyboard section of the chain is absent, unpopulated or broken — which is exactly the condition under which you want it.

The requirement this forces on Clicky's firmware

DeMon drives the status LEDs by sending intent to Clicky, so a DeMon-driven pattern needs DeMon already up and the link working — and the window that matters most is bootloader → kernel → antboot, when DeMon can say nothing at all. If Clicky only ever relays, the LEDs are dark precisely when they are the only thing that could speak.

So Clicky must own an autonomous pre-DeMon pattern and hand over when the first message arrives, exactly as the RGB power light already works ("the light is never dark while powered"). Clicky knows two facts without being told — power is applied, and DeMon has not spoken yet — and that is enough for a "waiting" state that resolves into a fault indication if DeMon never appears. That, rather than a timeout in the FPGA, is the real answer to §8's "a permanently black screen is indistinguishable from a dead machine".

Two jobs, by phase

demon already defines the running half: a fixed per-subsystem panel — Power, external SD, cartridge SD, main SD, network, Wi-Fi, Bluetooth, MIDI — with blue for detected, green for R/W, red for error, and a white self-test sweep at startup. That is the right shape, and LED 0's white → amber → green is already a boot indicator in miniature.

What is missing is the boot half, and the note that LED functions "can be repurposed if needed" is the licence to add it. Before AntOS is up there is nothing to report about network or MIDI, but there are about seven real stages to count: FPGA configured, kernel entry, antboot, AntOS init, storage mounted, FPGA link up, ready. So the eight pixels run as a progress indicator during boot and settle into demon's assignment table once AntOS is running.

The transition between the two is itself information. A progress bar tells you it stopped; the per-subsystem panel tells you what stopped — which is what the eZX has no other way to say.

Two descriptor notes on that table: LED 2 (cartridge SD) has no meaning on the eZX, which has no cartridge port, so it is free there; and LED 3 is "main (fast) SD", which is a drive-letter question that rung 6 has to settle first.

Two cautions. Encode meaning in position and blink pattern as well as hue — eight coloured LEDs as the sole diagnostic fails anyone who is colour-blind, and red/green is the worst possible pair to lean on. And treat the buzzer as the companion channel rather than an afterthought: boot-fault beep codes are the same mechanism, cost nothing extra, and are entirely in keeping with the machine.

The honest limit: this fixes "is it alive, and where did it stop". It cannot show a message or offer a choice. Anything needing the user to read something or pick an option still requires either the FPGA up or a PC — so the eZX's recovery UX is non-textual by construction, and the flows should be designed knowing that rather than discovering it.


Developing both machines at once

One image, one binary set, and Clicky's KPRODUCT strap telling AntOS at boot whether it is an eZX or an Ant64 — read over the I²C status interface, cached in RTC SRAM. The problem is purely that neither IO board has a Clicky on it, so without a way around it you can only develop one product at a time.

Give the board descriptor a four-level lookup: ANT64_BOARD=ezx|ant64 from the environment, then a kernel cmdline parameter, then the RTC SRAM cache, then a live read from Clicky. One CM5 on the desk is then an Ant64 in one terminal and an eZX in another, and the descriptor gets exercised as a descriptor from the first day rather than as a constant that happens to be right. Make it log loudly at startup and refuse to engage outside dev mode, so it can never quietly ship.

Second payoff: the eZX can carry either module, since the compute module is user-upgradeable — so two boards cover three shipping configurations. The CM4 is the eZX as sold; the CM5 is both the Ant64 and the upgraded eZX.

What the descriptor holds

§3 described it as a table carrying "the port map, socket count, panel presence, fan, LED counts". The differences are now concrete, and they are mostly absences — the easier kind of divergence, because the Ant64 build is the superset and the eZX is what you get by turning things off.

Field eZX Ant64 Gates
ImGui contexts 1 2 HDMI overlay only, versus overlay plus keyboard panel
Keyboard panel absent 4.3″ DSI DSI1 routing, 1.8 V translator, backlight driver, panel reset GPIO
Round screen absent 360×360 QSPI Pulse's second surface, and its reset/backlight lines on Cranky
Cranky absent present Jog dials, 24 LED channels, the twelve-channel control-surface expansion
Sticky · Clicky · Pulse · DeMon present present The shared spine — and Clicky's KPRODUCT strap is what selects this whole column
Status NeoPixels · power RGB · buzzer present present The non-textual diagnostic channels
Cartridge port (C:) absent present Drive-letter map, the service/debug access route, and status LED 2
USB topology HS storage hub + FS HID hub see the open question in rung 6 The port map, and whether USB3 exists at all
SPI width to Pulse / Phreak single/dual — GP0/GP1 are CM5-only quad (QSPI) Bulk transfer rate on the peer links
Fallback display none panel + round screen Whether the FPGA timeout is a safety valve or the only diagnostic

The last row is the one to design against rather than around. Every other difference is something the eZX does without; that one is something it cannot fall back on, and it is why the override matters beyond convenience — an eZX-shaped configuration is the harder case, and running it on the bench is how the recovery paths get exercised at all.

The variant rule, before anything else in antboot

Both CPU variants ship on both products and antboot picks by detected CPU. The rule that keeps it safe is asymmetric: prefer the exact match, fall back to A72 when the A76 build is missing, and never fall back to A76. An A76 binary on a CM4 does not run slowly — it dies on an illegal instruction. An unrecognised future part falls back to A72 too, which is what would let a pin-compatible CM6 boot on day one.

Keep the part-ID-to-variant map in manifest.inf rather than in antboot's code, so adding a generation is a build-and-manifest change with no code edit — the same reasoning that already puts a one-byte index in SRAM instead of a filename.


Documentation conflicts

Resolved — which bus carries LED intent. antos_led now states it: per-LED colour and patterns are control traffic on DeMon's housekeeping I²C, with Clicky as a slave on that bus (TWI0) clocking the WS2812 string itself. The DeMon↔Clicky UART (DeMon UART0 / Clicky UART2) carries key input and bulk config, not LED patterns — the same split the buzzer uses. This matters to the status-LED plan above: the LED path and the boot-mode key-modifier path are on different buses, so a fault on one does not silence the other.

Resolved — expander pins now agree. demon puts display RESET on PA4 and touch reset on PA5, and clicky references the same pair. The collision that made this worth tracking has since evaporated from both ends: moving the Phreak handshake to GP18 freed PB6, and moving board type to Clicky freed PB7, so neither contested pin exists any more.

Resolved — UPDI is Pulse's, for all three AVRs. Clicky's UPDI line runs across to Pulse alongside Sticky's and Cranky's, driven as serialUPDI over a hardware UART4 time-multiplexed by the P4's GPIO matrix onto each target's pin pair. DeMon has no UPDI pin at all; the one it had (GP18) now carries the Phreak SPI handshake directly, off the expander. The wiring matches the documented SerialUPDI topology — 470 Ω series into the UPDI pin, 4.7 kΩ from TX to that node, optional 1N5819 across it to permit a larger series resistor.

Three notes on the shared UART4, since it now serves four functions — CI1303, a MIDI Thru, and three UPDI targets:

  • Reconfigure framing, not just the pin matrix. UPDI is ~225 kbaud with even parity and two stop bits; MIDI DIN is 31.25 kbaud 8N1; the CI1303 is something else again. Every switch has to change baud and parity and stop bits. Routing the matrix and inheriting the previous framing is the failure that half-works.
  • Specify the parked state of the six UPDI pins. When UART4 is matrix-switched away from a target, that TX pin must not be left driving or floating low — a sustained low on UPDI is a break. Park them as inputs and the 4.7 kΩ holds each node idle-high, which is the behaviour you want; it matters most for Clicky, where a disturbance reaches the keyboard and power button.
  • The resistor network is per target, not shared. Three sets of 470 Ω + 4.7 kΩ (+ diode), one at each AVR — worth confirming against the BOM, since the pin count already assumes it.

Resolved in clicky.md

All five items raised here are now fixed in the page:

  • The DeMon-side UART pins read GP14 TX / GP15 RX, matching demon — the io53/io54 ESP32-P4 numbering is gone.
  • KUPDI is attributed to Pulse in the Pin Naming table as well as the pinout and the Programming section.
  • The SPI1 bus has a purpose — it is the NFC master to the PN532 on PB4–PB7, which also explains the SPI0-stays-internal split. Clicky now has no direct link to FireStorm: the UART5 key/matrix stream has been removed (PG4/PG5 freed) and the earlier Clicky↔FireStorm I²C dropped, so key events reach the fabric via DeMon. The remaining spare, PB3, is the designated pin reserve — where a GPIO comes from if ever needed elsewhere.
  • The 10 s force-off is documented as hardware, with a note on why a firmware implementation would fail in the one case it exists for, and a flag that the SparkFun Mk2 as documented does not provide it.
  • The PN532 interface is level-translated. See below — this was the one that could have damaged a part.

The PN532 fix, for the record

Moving NFC to SPI was right; running it as a 5 V link was not. The PN532 has two supplies and only one takes 5 V: VBAT is 2.7–5.4 V, but the host interface runs off PVDD, 1.6–3.6 V with a 4 V absolute maximum. Clicky's 5 V outputs on MOSI, SCK, SS and KNFC_RST would have sat a volt above that absolute max continuously. The return direction was marginal for the mirror reason — MISO and IRQ drive to PVDD, so at most 3.3–3.6 V against Clicky's ≈3.5 V V<sub>IH</sub> — and the datasheet forbids PVDD above VBAT, so there is no escape by raising it.

Six lines, two directions, two parts: a 74LVC125A at 3.3 V takes the four outbound lines (its inputs are 5 V-tolerant at that supply), and a 74HCT125 at 5 V takes MISO and IRQ inbound (HCT thresholds accept 3.3 V as a valid high). Fixed-direction buffers rather than an auto-direction TXB part on purpose — TXB drives weakly into capacitance and there is no bidirectional line here to justify the risk. NFC is slow enough that ordinary buffers are comfortably fast.

Worth confirming against the chosen module: most PN532 breakouts regulate their own 3.3 V from a 5 V input, which makes the interface 3.3 V regardless of what the "5 V" pin is fed.

Board type consolidated onto Clicky

There were two straps for one fact — Clicky's KPRODUCT on PF2 and PB7 on the MCP23017 — which a mis-stuffed board could set inconsistently, leaving Clicky scanning one product's matrix while AntOS loaded the other's descriptor. PB7 is now free and Clicky is the single source, with AntOS reading board type over the I²C status interface at 0x28.

Clicky has to have the strap regardless: it boots in milliseconds and must pick a matrix-scan path long before DeMon exists. And a single reader makes the strap electrically trivial — a shared net would have needed translation in both directions (3.3 V is below Clicky's ≈3.5 V V<sub>IH</sub>; 5 V exceeds the expander's absolute maximum), where one reader is a plain tie to 5 V or GND.

One thing to design for. The expander was a dumb part that always answered; Clicky is a microcontroller running firmware, and during update clicky or after a bad flash it answers nothing. A recovery boot that cannot tell an eZX from an Ant64 does not know its port map, panel presence or drive letters — which is precisely the wrong time not to know. So AntOS should cache the value in a byte of the MCP79410's battery-backed SRAM on first successful read and fall back to it when Clicky is silent. Board type is immutable for the life of the machine, so the cache can never go stale, and the RTC is a dumb part on the same bus that always responds.

Two bench consequences for rung 1: the descriptor's real source arrives with Clicky at rung 5 rather than with the expander here, so the ANT64_BOARD= override stays load-bearing one rung longer than it would have; and the RTC cache is another reason that rung wants the MCP79410 present, not just the expander.

One thing the move costs, worth noting rather than fixing. cm45 §3 previously had two expander pins — four board IDs, with 2 and 3 reserved. A single strap is one bit, so the room for a third product has gone. That is almost certainly the right trade today, and Clicky has no spare pin to widen it with; but if a third carrier ever appears, the cheap answer is probably not a second strap.

The better home would be the ATECC608C. It already holds a write-once, slot-locked machine ID provisioned at first test, and a product field there would be immutable, survive a Clicky reflash, cost no pin, and be readable whenever the housekeeping bus works — strictly better than the RTC cache, which is soft state AntOS writes and a dead CR2450 loses. Clicky's strap still has to exist, because Clicky needs the answer for its own matrix decision before anything else is up; the 608C would just be the authoritative fallback instead of the RTC. Since the provisioning flow is undesigned anyway, adding a product field to it costs nothing at the point it gets written.

Sticky now reaches DeMon directly — which moves it in the ladder

sticky gives Sticky three host links, not one: SPI client to Pulse with its own interrupt, I²C client to DeMon with an interrupt line, and a UART to FireStorm. demon has added it at 0x2A on the housekeeping bus with SDIRQ on expander PB4.

That changes rung 4. Sticky is no longer purely behind Pulse: its controller state is readable by DeMon over I²C as soon as rung 1 exists, with no Pulse in the path. Only the SPI link and the UPDI programming path need rung 3. So the useful split is to read Sticky at rung 1 alongside the RTC and the expander — which also gets a joystick working as an input device long before Clicky arrives, on a bench that otherwise has only a USB keyboard.

One knock-on: expander PB4 is no longer free. The spares are now PA0, PB6 and PB7 (PB7 freed by moving board type to Clicky, above).

Still open — the status LEDs have no autonomous mode

clicky now distinguishes the two refresh strategies clearly, and the keyboard pixels can animate autonomously — idle breathing, layer indication, an AntOS busy spinner. But status[0..7] is specified as DeMon-driven only: "DeMon sends 'set status LED N to colour C' intent over I²C… Event-driven, not animated."

That is the gap named earlier on this page. The RGB power light is covered ("comes up on the first instruction of Clicky's boot") and the keymap is covered (built-in EEPROM default, so the keyboard works before DeMon is ready) — the status pixels are the one indicator with no defined behaviour in the window before DeMon speaks, which is the window they are most needed in.


Still without a home

  • The ATECC608C provisioning flow — writing the structured machine ID into a lockable slot at first test. A factory process rather than a bring-up step, but it wants designing before the first units are built.
  • Sticky's product split. Sticky is on both machines and its UPDI path runs through Pulse, which the eZX also has — so this resolves. But the eZX's controller complement is not written down anywhere in the doc set; ezx names none of them.

Related

  • os_card — the partition layout, config.txt, EEPROM config and the update tiers
  • cm45 — the design notes these correct and extend
  • demon — GPIO map, I²C device map, expander pin assignments
  • firmware — the A/B update flow and the recovery ladder

Important: The Ant64 family of home computers are at early design/prototype stage, everything you see here is subject to change.