micro:bit v2.2 bench

nRF52833 target only. Loader and serial, no DAPLink

docs, support matrix, 239 tests green

Docs

One bench, two layers: the chip core runs your firmware, the virtual board answers it. Below: how to run it, how to drive the board, what it supports, how it is built, and how to check a run.

Run firmware

  1. Load a .hex or .bin built for flash 0x0 with ARM GCC and no SoftDevice — or pick a preset from the bench. The loader parses Intel HEX (type-02 and type-04 addressing, up to 512 KB) and collects 0x1000xxxx records as UICR bytes for later; a .bin loads raw at zero. Staging parses and holds the image; nothing executes yet.
  2. Press Run. Staging boots through the same bootImage path as a dropped file: re-register taps, build the model, create the CPU from the vector-table SP/PC, load firmware at zero, enable IRQ delivery, apply UICR words. The status line names the vectors it booted.
  3. For stock MicroPython, press Boot MicroPython app: the page re-inits pristine (frames between load and click leave dirty timers and NVIC state), re-applies UICR, hand-installs the MBR parameter page (*(0x20000000)=0x1000, *(0x20000004)=0x1C000) the way a real MBR-to-SoftDevice boot would, sanity-checks the vectors at 0x1C000, and resets the CPU there. Banner follows on the console.

If Run reports nothing staged, Load a file or pick a preset first. If MicroPython reports no vector table, the image at 0x1C000 is blank or erased (0xFFFFFFFF). If it reports no valid app, the SP/PC fail the gate (SP inside 0x20000000–0x20020000, PC inside 0x1C000–0x80000) — the bench refuses to jump into the weeds and says so.

Drive the board

Every control on the bench answers through the same peripheral path real firmware uses — there are no shortcuts.

Board support matrix

Status key: F runs timed with interrupts and a firmware proof, H answers the handshake minimum with no timed behavior or no consumer, – missing or deliberately out of scope. Source: the repo status audit against monox/nrf52833.svd, suite 274 green.

micro:bit v2 board hardware → what the bench does
Board featureStatusWhat worksWhat is leftRemark
5×5 LED matrix (TIMER4 + GPIOTE/PPI)FDIR+OUT gated render from P0/P1 OUT with 3-frame strobe persistence; matrix_nrf strobes the "A" glyph (correct masks 0xD1688800); JS Matrix-demo button drives the same patterns without firmwareContent never drawn pre-scrollInit stalls before first show; a toggling latch on an input pin stays dark, as on silicon
Buttons A/B (P0.14/P0.23, active-low)FPress and release drive inputs; level-poll proof prints BTN:1/0Nothinginput_state survives re-init, so a held button reads pressed across reset
LSM303AGR accel 0x19 + mag 0x1E (TWIM1)FWHO_AM_I 0x33/0x40, STATUS data-ready, CTRL echo, live tiltNothingDRDY pulses 60 ms low / 140 ms high on P0.25; a stuck low trips the shared KL27 threshold
KL27 interface, UIPM 0x70FValid protocol frames: BOARD_REV 0x9904, I2C v2, DAPLink, power/HSB state, LED state, user-event queue; e8777 NOP-wake; unknown → ERR/UNKNOWNDAPLink USB itself never emulated (this page is the loader)parts/kl27.js Kl27Uipm via TWIM1. Mock: protocol frames verified in smoke
KL27 USB flash 0x39 (wire 0x72<<1)Ftransact() contract: filename/filesize/visibility/geometry + READ/WRITE/ERASE on a 0x1F000 image (4096×31); always-valid answers, no retry burnNo real flash-chip wearparts/kl27.js Kl27Flash via TWIM1. Mock: config + roundtrip + erase verified in smoke
Edge I2C + SSD1306 OLED add-on (TWIM0, 0x3C)FInit, fill, framebuffer and DMA paths render to the scopeNothing128×64 GDDRAM model
Edge SPI (P13–P16, SPIM2/3)FDMA bytes route to the SPI tap; ST7789 240×240 scope renders CASET/RASET/RAMWR + MISO ID (P114)No physical display wiredLive panel canvas; headless check in parts/spidisplay_check.mjs
USB serial (UARTE0)FConsole drain plus paced RX drip with DMA mirror; TX snapshot proven 0/60 guarded (P112)MPY serial stalls at TX-only init (RX never initialized, baud 0 — P113); not a UARTE model gapBulk-completing RX early stalls input, so bytes drip one at a time
Speaker P0.00, mic P0.05/RUN_MIC P0.20, logo P1.04FSpeaker P0.00 edge + PWM1 SEQSTART observer (host mute); mic fills PDM when RUN_MIC powers it (else silence); logo button drives P1.04; I2S TX streams to WebAudio 16 kHz monoNo acoustic modelingparts/kl27.js SpeakerPart/MicPart/LogoTouchPart. Mock: edges/mute/silence/tone/press verified in smoke
NFC antennaFUICR.NFCPINS gate (P114): antenna pins reserve P0.09/P0.10 + gate field sense; GPIO mode routes pins normallyNo RF physicsPin-route truth, not air emulation
Power, deep sleep (WFE/WFI)FSleep-aware pump: tick_n plus wake on pending IRQNothingWithout it the core naps forever at first idle
QSPI external flashFRegistered image, program, erase, take/complete APINo board chip wiredModel proven, no consumer
USB deviceFEPIN/EPOUT pump, USBRESET inject, SETUP injectNothingair preset pre-signals USBRESET on Run
Radio 2.4 GHzHTwo-instance loopback bridge, RSSI and corrupt injectsNo real airBare-metal CODAL datagram surface; no WebBluetooth
BLE stack (SoftDevice SVC face + Bumble air bridge)FAll 67 S132 BLE SVC numbers claimed (common 0x60–0x69, GAP 0x70–0x8E, GATTC 0x90–0x99, GATTS 0xA0–0xAC, L2CAP 0xB0–0xB2); GAP connect/disconnect/scan/RSSI, full GATT client (incl. SIGNED/PREP/EXEC writes with op echo) + local GATTS table with CCCD-gated notify/indicate, full SMP reply legs with S132 status codes, bond store (hit/miss/delete + bridge keys leg), TX-flow refill + TX_COMPLETE, dial-in CONNECTED (PERIPH role), param-update event, driver-posted request/report/timeout legs (SEC_REQUEST, CONN_PARAM_UPDATE_REQUEST, SCAN_REQ_REPORT, GAP/GATTC/GATTS TIMEOUTs, USER_MEM pair, RW_AUTHORIZE_REQUEST, SYS_ATTR_MISSING, SC_CONFIRM), TX-power store + adv-state, two-link two-peer air (battery 87 vs 64), serialized ATT queue, live reads on the BLE panel; SoC flash events via SVC 16/82 (P114)No SMP crypto math (MAC check is firmware-side); adv-state arms no air; no MTU/DLE/PHY; no BLE-enabled stock image; virtual air with link-budget RSSI, not RF — see "BLE: is it fully done?" belowSelf-test button runs the full SVC flow on loopback; depth probe covers it headless; E2E proves 42 checks over air; enable BLE air for the live Bumble peers

Chip peripheral matrix

Every nRF52833 peripheral in the SVD, same status key. Unlisted offsets read as zero and writes are ignored, because real HALs probe reserved registers.

nRF52833 peripherals → model coverage
PeripheralStatusWhat worksWhat is leftRemark
CLOCK + POWER (0x40000000)FHF/LF STARTED, USB detect and ready, RESETREAS, GPREGRET, RAMSTATUSNothingAIRCR SYSRESETREQ reboots and self-clears
RADIO (0x40001000)FTX take, RX completion and inject, CRC error, RSSI, SHORTS, 802.15.4 helpers; link-budget air level (P119: TXPOWER-code table + path-loss injects, RX stamps the packet's own level); real CRC engine + whitening + interference floor (P124)Real airLoopback proof green
UARTE0 + UARTE1FTX DMA, RXDMA ring, OVERRUN, ERROR, TXSTOPPED; UARTE1 TX+RX DMA firmware proof (uarte1_nrf, P110)NothingSTARTTX snapshots bytes synchronously so late takes cannot smear them
TWIM/TWI/SPIM/SPIS/TWIS/SPI families (incl. SPIM2/3)FSHORTS, NACK without slave (SPI never NACKs), slave engines, MISO routing, shifted-address match, SPIM2/3 DMA firmware proof (spim23_nrf, P110)Nothingnrfx shifted addresses normalize to 7-bit. Mock: TWIS write-to-RAM plus read-back, SPIS exchange, edge-display frames
TIMER0–4FPrescaler, bitmode, SHORTS-CLEAR, CAPTURE snapshots, interruptsTIMER4 never starts in MakeCodeDisplay never constructed — not a timer gap
RTC0–2, TEMP, RNG, EGU, PWMFFull instances, driver-settable values where applicable, second depth proofs (COMPARE/OVRFLW, STOP/INTEN, DATARDY/INTEN, SHORTS/re-arm, channels/mask — P110)NothingDepth closed P110. Mock: thermometer DATARDY plus quarter-degree read
SAADC, PDM, QSPI, NVMC, PPI, I2S, USBD, GPIOTE, NFCTFTake/complete pumps, state machines, group dispatch, field detect; crypto+QSPI live in pumpDma (P109)MWU/NFCT beyond proof-level drivingEvery take/complete pair is exported. Mock: limit edges, stream silence plus capture, flash program plus erase, tag tap plus frames, SETUP packet
ECB, AAR, CCMFTake/complete with FIPS-197 and roundtrip proofs; live pumpDma (shared crypto.js, P109)AAR has no wasm exportCrypto runs driver-side by design. Mock: FIPS-197 block, resolve, CTR plus MIC
WDT, QDEC, COMP, MWU, FICR/UICR, GPIO, NVIC/SCB/SysTick/MPU/FPUFExpiry and reboot, quadrature, thresholds, region watch, UICR store, CNF→DIR, lazy FP stacking, nRF engine UNUSED stub (fpu_engine_nrf.rs)NothingDocumented, matches the audit. Mock: knob steps, comparator band edges
ACL (no SPU on 833)F8 regions ADDR/SIZE/PERM sticky; erase refused into write-protected pages; read-block enforced in the memory layer (MWU-patterned armed flag, bus fault + 0 on blocked reads)NothingSVD-verified: 70 peripherals, none named SPU. Test: acl_regions_sticky_and_block_erase

BLE: is it fully done?

No — and here is the honest boundary, file by file. The firmware-visible contract is complete: every one of the 67 S132 BLE SVC numbers (common 0x60–0x69, GAP 0x70–0x8E, GATTC 0x90–0x99, GATTS 0xA0–0xAC, L2CAP 0xB0–0xB2) is claimed by nrf52833-periph-wasm/src/sd_ble.rs and answered per the header register contracts — no call falls through to a fault, and cargo test (274 green, 14 sd_ble tests incl. byte-offset asserts) plus the headless mock (real SVC bytes, 18/18) plus the live E2E (42/42 over-air checks on two links) prove the whole flow: enable, table build, connect, discovery ×6, read, write (REQ/CMD/SIGNED/PREP/EXEC), L2CAP, pairing + peer-pairing with passkey, indicate + service-changed, ADV/SCAN roles, scan, RSSI, disconnect. The air side is real too: tools/ble_air_bridge.py resolves every job over a Bumble LocalLink against two advertising peers (battery 87 PeerBatt + heart-rate twin 64 PeerHR, distinct addresses), with per-peer routing, serialized ATT bursts, op-echoed WRITE_RSP, and HCI-RSSI-first sampling. A compiled BLE-enabled image proof also exists: blinky/ble_fw/ble_pairing_fw.c drives the CODAL-BLE-shaped JustWorks flow (ENABLE→GATTS table→CONNECT→GATT→AUTHENTICATE→CONN_SEC_GET→DISCONNECT) end to end natively (cargo test nrf_ble_pairing_fw_markers, 17 BLEP:* markers, 2nd-run clean) plus blinky/ble_fw/ble_roles_fw.c for the ADV/SCAN/whitelist/role legs (21 BLER:* markers, 2nd-run clean, bit-identical rebuild).

What is genuinely finished:

What is deliberately not done (each is a named, bounded gap — none is a hidden fault):

If you need one sentence: the BLE contract firmware calls is fully answered and proven over virtual air; the radio physics and the crypto math are not.

How the bench is built

The page runs a loop you can read in one sitting. Each animation frame steps the core five times — cpu.step(20000), tick_peripherals(), then the DMA pump — because duties once per frame starve polled firmware: a one-byte transfer staged at slice zero would wait a hundred thousand instructions for completion. When firmware naps in WFE/WFI, the loop advances virtual time and wakes on a pending interrupt; without that the core sleeps forever at its first idle. Faults break the sub-loop and name the program counter on the status line.

Depth probes: every handshake earns a consumer

Handshake status used to mean the registers answer with no timed behavior or no consumer. That second half is now closed: demo/parts/mocks.js holds a live consumer for each one, in the same register-then-poll shape as the board parts. Press Run depth probes on the bench, or run node demo/parts/handshake.mjs headless against the built package — eighteen checks, one per surface:

The KL27 stubs stay handshake by decision: the interface chip is deliberately not emulated, so an empty version frame and a request echo are the whole consumer. The NFC pin-route is modeled (UICR.NFCPINS reserves P0.09/P0.10 + gates field sense); RF physics is not.

Checks before you trust a run