M4: the board reads the disk itself

/ UniDisk 3.5 controller / updated 2026-10-10 unidisk rp2350 gcr firmware m4

The Pico decodes the disk now. In M3 it shipped raw flux timings over USB and a Python script on the Mac turned them into sectors. In M4 core 1 does the GCR decode on the RP2350 and hands back finished 512-byte blocks. The whole REF800K test disk came back byte-identical in 26.1 s. Same SHA-256 as the image I wrote it from.

readdisk: 26102 ms, decode 1179 ns/interval (mean), ring backlog max 37 of 8192,
          0 failed track sides, 0 with soft errors
compare vs REF800K.po: 0 blocks differ []
M3 (decode on the Mac)M4 (decode on the RP2350)
Whole disk2 min 12 s26.1 s
Blocks OK1600/16001600/1600
Image SHA-256matchesmatches (bfed064f...657742ab)

This is Drive A only. Drive B sat this one out.

How it’s split across the two cores

Core 1 owns the mechanism. A PIO program times every flux edge in 40 ns counts and DMA drops them into a 16 KB ring that never stops. Core 1 pulls intervals out of the ring and decodes them as they arrive, so a track side never has to be buffered as raw flux. Core 0 runs the USB console and talks to core 1 through two small lock-free queues.

The retry rules come from the original drive ROM, read as a reference: 12 revolutions per sector with a recalibrate after 6, a budget of 4 wrong-track headers, and a 5 s cap. The status codes are the ROM’s too, so a missing disk comes back as AF and an unreadable sector as A7. That matters later when the Apple asks for blocks and expects those exact codes.

CommandWhat comes back
readblk <n>BLK n ss ff plus 512 bytes as hex
readdiskevery track side as binary with a CRC, then DISK END with timing stats

Single blocks first

Blocks 0, 1, 23, 400, 900, 1300 and 1599 cover all five speed zones and both sides. Every one came back status 00, no soft errors, and matched the reference image.

The error paths behaved too. readblk 1600 is ERR bad command. Mechanism commands get ERR server on while core 1 owns the drive. With the disk ejected, readblk 0 returns BLK 0 af 00 and the mechanism doesn’t move.

Decode headroom

The mean decode cost is 1179 ns per flux interval. At the fastest flux rate an interval shows up every 2 us, so core 1 would be about 60% busy in the worst case. The DMA ring never got more than 37 intervals behind out of 8192. That’s fine for reads. M5 adds GCR encoding for writes, so I’m keeping an eye on it.

OpenOCD was leaving core 1 halted

After every flash, server on said ERR server did not start. Core 1 was parked in the boot ROM at PC 0xda. A cold power cycle fixed it, and resetting core 1 in firmware before launching it didn’t.

If core 1 on an RP2350 won’t start after flashing with OpenOCD, add -c "set USE_CORE 0" before -f target/rp2350.cfg. By default OpenOCD attaches to both cores and leaves core 1 halted under debug. With that line, server on works straight after a reset.

The disk was rubbing on the desk

Halfway through, every block came back A7 and the decoder saw no sector headers at all. The plain flux capture from M3 also got 0 intervals. I flashed the old M3 firmware to A/B it and that got nothing either, so the new code was off the hook. Voltages at the mechanism connector were normal this time.

The spinning disk was touching the desk. Once it was free, flux came back and every M4 step passed.

My guess is the mechanism mutes RD when the spindle speed is out of tolerance. That would also explain M3, when the +5V sag killed reads while status, tach and stepping kept working. It’s not confirmed. Next time RD goes quiet I’ll check tach and READY before anything else.

What the reviews caught

Each piece got a review before it went near the bench, plus one over the whole branch. The ones worth remembering:

Still open