
RP2040-Firmware, die ein Toshiba MK4001MTD 0.85"-SDIO-Mikrolaufwerk als USB-Massenspeichergerät überbrückt und dabei den vollständigen SDIO-ATA-Protokollstapel von Grund auf implementiert, mit PIO-beschleunigten Lese-/Schreibvorgängen und Wiederherstellung fehlerhafter Sektoren.
RP2040 Pico-Firmware, die ein Toshiba MK4001MTD 0,85-Zoll-SDIO-Mikrolaufwerk als USB-Massenspeichergerät anbindet.

Das MK4001MTD ist ein 4-GB-Mikrolaufwerk, das ursprünglich im Nokia N91-Musiktelefon und in einigen anderen Geräten wie MP3-Playern oder USB-Laufwerken verwendet wurde, als Flash-Speicher noch recht teuer war.
Vielleicht haben Sie Einführungen gesehen, die behaupten, dieses Laufwerk verwende das MMC-Protokoll, aber das ist tatsächlich falsch. Ich habe dies eine Weile untersucht: Ich habe versucht, einen 8-Bit-MMCplus-Kartenleser zu bauen und verschiedene SD/MMC-Leser getestet, jedoch ohne Erfolg. Als letztes Mittel kaufte ich ein Nokia N91, um Logikspuren aufzuzeichnen und zu bestätigen, welches Protokoll es tatsächlich verwendet.
Hier ist das Foto, als ich versuchte, es mit meiner 8-Bit-MMCPlus-Leserplatine zu verwenden, und es stellte sich heraus, dass es kein MMC ist :(

Also besorgte ich mir schließlich ein N91, um Spuren aufzuzeichnen:

Im Gegensatz zu standardmäßigen ATA/CF-Mikrolaufwerken verwendet es eine SDIO-Schnittstelle mit ATA-Befehlen, die durch CMD52/CMD53 getunnelt werden. Kein vorhandener Treiber unterstützt dieses Protokoll, daher implementiert diese Firmware den gesamten Stack von Grund auf neu.
Das hat mich überrascht, da es einen SDIO-zu-ATA-Standard namens CE-ATA gibt. Aber wenn man sich die Veröffentlichungszeitleiste genau ansieht, kam CE-ATA später als dieses Laufwerk. Daher verlässt sich dieses Laufwerk vollständig auf SDIO-Befehle, und CE-ATA ist nicht verfügbar. CE-ATA hat zwei neue Befehle CMD60/CMD61 und verwendet CMD12/39, aber wie man an den Spuren sehen kann, verwendet es keinen davon.
Der zweite Hardware-Punkt, den es zu erwähnen gilt, ist, dass eine weitere Fehlinformation, die kursiert – dass es sich um eine 8-Bit-MMCPlus-Karte handele – nicht nur falsch ist, sondern dass die Pinbelegung auch nicht dem MMC-Standard folgt. Sie finden das Nokia N91 Service Manual mit einiger Dokumentation zur Pinbelegung: Während die Pinnummerierung dem MMCPlus-Standard folgt, tut dies die Pinzuordnung nicht. Dies ist ein wichtiges Detail, wenn Sie es selbst verdrahten: Es verwendet den gleichen MMC-Anschluss, aber die Pinzuordnung ist anders, mehr im Hardware-Abschnitt.
Abschließend sei angemerkt, dass dies gemeinsam mit Claude/OpenClaw entwickelt wurde. Ich habe die Logikspuren manuell gesammelt und einen Closed-Loop-Testaufbau für OpenClaw eingerichtet, um iterativ zu entwickeln – die Spuren zu analysieren und Funktionen zu implementieren. Die Dokumentation wird hauptsächlich von Claude verfasst; ich werde meine Notizen ebenfalls inline hinzufügen. Ich habe die Dokumentation auch selbst gelesen und überprüft, und sie sollte zuverlässig und leicht verständlich sein.
Einblicke in die Analyse der N91-Spuren finden Sie unter /docs/N91_TRACE_ANALYSIS.md; ich habe dort auch das N91-Service-Manual zusammen mit den rohen Logikspuren abgelegt.
Weitere Informationen im Blogbeitrag hier: https://www.willwhang.dev/Reading-MK4001MTD/
In Aktion sehen Sie es hier: https://youtu.be/GC4xil3_Bbc
Voll funktionsfähiger USB-Massenspeicher mit PIO-beschleunigtem Lesen/Schreiben und Leerlauf-Leistungsverwaltung.
USB Host ←→ USB MSC (TinyUSB) ←→ ATA Layer ←→ SDIO Layer (PIO) ←→ MK4001MTD
Die Firmware besteht aus vier Schichten:
USB MSC (msc_device.c) — TinyUSB Massenspeicherklasse. Übersetzt SCSI READ(10)/WRITE(10) in ATA-Sektoroperationen. 32 KB EP-Puffer, Batching von bis zu 64 Sektoren pro USB-Übertragung. Festplatten-I/O wird in beide Richtungen mit USB überlappt, wie bei einer echten ATA-USB-Brücke mit Caching-Laufwerk: Ein sequentieller Leseprefetcher holt das nächste Stück, während das vorherige zum Host streamt, und Schreibvorgänge werden gestapelt und geleert, während USB das nächste Stück empfängt. Das Gerät bewirbt seinen Schreibcache (Caching-Modusseite, WCE=1 – Hosts melden "Write cache: enabled" und geben SYNCHRONIZE CACHE bei fsync/unmount/suspend aus, was die Firmware respektiert). Ein fehlgeschlagenes Hintergrund-Flush wird als MEDIUM ERROR beim nächsten WRITE oder SYNCHRONIZE CACHE gemeldet; Schreibvorgänge auf bekanntermaßen fehlerhafte Sektoren nehmen einen strengen synchronen Pfad.
ATA-over-SDIO (ata_sdio.c) — Implementiert ATA-Befehle (IDENTIFY, READ SECTORS, WRITE SECTORS) durch Schreiben in ATA-Register, die in den Adressraum von SDIO-Funktion 1 über CMD52 abgebildet sind, und Überträgt Sektordaten über CMD53. 3-stufige Wiederholungslogik auf CMD-, Daten- und ATA-Ebene.
PIO SDIO (sdio_pio.c, sdio.pio) — Hardwarebeschleunigtes SDIO mithilfe des RP2040-PIO-Peripheriegeräts (4-Bit-Bus bei 10 MHz, 4 PIO-Zyklen pro Bit mit umgangenen Eingangssynchronisierern). Drei PIO-Programme teilen sich eine einzige Zustandsmaschine durch dynamischen Programmwechsel:
Pin/Power (sdio_hw.c) — GPIO-Initialisierung und HDD-Stromsteuerung. Die gesamte SDIO-Kommunikation verwendet PIO.
Menschliche Notizen: Interessanterweise zögerte Claude wirklich, SDIO in PIO zu implementieren, und es wurden viele Entwicklungszyklen damit verschwendet, zwischen PIO und Bit-Banging hin und her zu wechseln.
Das MK4001MTD präsentiert sich als SDIO-Karte mit einer I/O-Funktion. Die standardmäßige SDIO-Karteninitialisierung (CMD5/CMD3/CMD7) richtet den Bus ein, dann werden ATA-Register über SDIO-Befehle angesprochen:
Registerzugriff (CMD52): Jedes ATA-Register ist auf eine Funktion-1-Adresse abgebildet:
Datentransfer (CMD53): Sektordaten werden durch Ausgabe von CMD53 im Blockmodus übertragen, der auf das DATA-Register (Adresse 0x00) abzielt. Bei Multi-Sektor-Lesevorgängen überträgt ein einzelner CMD53 mit block_count=N N × 512 Bytes in einer einzigen SDIO-Multiblock-Transaktion.
Interrupt-Signalisierung: Das Laufwerk signalisiert die Sektorbereitschaft durch Auslösen eines SDIO-Interrupts (INT_PENDING Bit 1 im CCCR-Register 0x05). Das Lesen des ATA-STATUS-Registers löscht den Interrupt.
Für einen 16-Sektor-Lesevorgang:
1. ATA-Register über PIO CMD52 schreiben:
SECCOUNT=16, LBA_LO/MID/HI, DEV/HEAD=0xE0, CMD=0x20
2. STATUS über CMD52 abfragen, bis DRQ (Bit 3) gesetzt ist
3. PIO zum DAT-Leseprogramm wechseln
4. CMD53 senden: block_mode=1, fn=1, addr=0x0000, block_count=16
5. PIO DAT lesen: für jeden der 16 Blöcke:
a. Auf Startbit warten (alle DAT-Leitungen niedrig)
b. DMA 1024 Nibbles (512 Bytes) vom PIO RX FIFO zum Puffer
c. Warten, bis SM das Taktgeben der CRC+End-Nibbles abgeschlossen hat (SM PC abfragen)
d. Nibbles → Bytes in-place neu packen
6. PIO zurück zum CMD-Programm wechseln
Für einen 16-Sektor-Schreibvorgang:
1. ATA-Register über PIO CMD52 schreiben:
SECCOUNT=16, LBA, DEV/HEAD=0xE0, CMD=0x30
2. STATUS über CMD52 abfragen, bis DRQ (Bit 3) gesetzt ist
(STATUS 0xD8 = BSY+DRQ wird als DRQ-bereit behandelt, gemäß N91-Spur)
3. PIO zum DAT-Schreibprogramm wechseln
4. CMD53 senden: block_mode=1, fn=1, addr=0x0000, block_count=16
5. PIO DAT schreiben: für jeden der 16 Blöcke:
a. CRC16-CCITT pro DAT-Leitung vorberechnen (4 unabhängige CRCs)
b. Nibble-Strom aufbauen: start(0x0) + data(1024 Nibbles) + CRC(16) + end(0xF)
c. DMA Nibble-Strom zum PIO TX FIFO
d. PIO taktet alle Nibbles aus, dann:
- Schaltet DAT auf Eingang
- Takted 16 Zyklen für CRC-Status von der Karte
- Pollt DAT0, bis die Karte Busy freigibt
- Löst IRQ 0 aus, um Blockabschluss zu signalisieren
6. PIO zurück zum CMD-Programm wechseln
Der RP2040 PIO hat 32 Befehlsslots pro Block. Unsere drei Programme haben insgesamt 55 Befehle, sodass sie nicht nebeneinander existieren können. Stattdessen wird eine einzelne SM0 auf PIO0 verwendet, und Programme werden durch direktes Schreiben in den PIO-Befehlsspeicher ausgetauscht:
static void load_program_raw(const pio_program_t *program) {
for (uint i = 0; i < program->length; i++)
pio->instr_mem[FIXED_OFFSET + i] = program->instructions[i];
}
Dies umgeht den Allokator pio_add_program/pio_remove_program des SDKs. Der Programmwechsel dauert etwa 1 µs. Jedem Wechsel folgt eine programmspezifische Reinitialisierung, die Pin-Zuordnungen, Schieberichtung und Taktteiler setzt.
Die Analyse der Logikspuren des Nokia N91 zeigt eine aggressive Energieverwaltung:
Die Firmware repliziert dieses Verhalten mit einem konfigurierbaren Leerlauf-Timeout:
#define IDLE_STANDBY_MS 5000 // in main.c
Zwei Pfade lösen die HDD-Stromabschaltung aus:
Beide Pfade senden ATA STANDBY SOFORT (0xE0), um den Schreibcache zu leeren und die Köpfe zu parken, und schalten dann den Strom über GP9 ab.
Aufwecksequenz (ausgelöst durch ersten LESE/SCHREIBVORGANG nach Abschaltung):
Wenn eine Multi-Sektor-Übertragung auf einen fehlerhaften Sektor trifft:
STATUS/ERROR-Bits zu erfassen, anstatt den Fehler auf ein generisches DRQ-Timeout zu reduzierenMEDIUM ERROR (Lesen: 03/11/00, Schreiben: 03/0C/00)Punkt 6 ist nicht akademisch: Dieses Laufwerk hatte einen langanhaltenden nicht lesbaren Sektor bei LBA 1952 (READ: ST=0x51 ERR ERR=0x40 UNC). Sobald die Brücke einem Schreibvorgang erlaubte, ihn tatsächlich zu erreichen, überschrieb das Laufwerk den Sektor und er liest sich seitdem sauber:
[ATA] FAST-RD: ST=0x51 ERR ERR=0x40 UNC LBA=1952
[MSC] BAD SECTOR read LBA=1952
[MSC] Bad sector LBA=1952 repaired by write
arm-none-eabi-gcc)Die SDK-Version ist festgelegt: Wenn PICO_SDK_PATH gesetzt ist (Umgebungs- oder CMake-Variable), wird es verwendet und seine Version gegen die Festlegung geprüft – eine Abweichung führt zu einem Konfigurationsfehler mit Anweisungen (überschreibbar mit -DMK4001_ALLOW_SDK_MISMATCH=ON). Ohne PICO_SDK_PATH wird die festgelegte SDK-Version zur Konfigurationszeit automatisch von GitHub geholt, sodass ein einfaches git clone && cmake && make vollständig reproduzierbar ist.
Die Firmware benötigt einen gepatchten TinyUSB-MSC-Klassentreiber (App-Sense-Daten bleiben bei Lese-/Schreibfehlern erhalten + eine Caching-Modusseite mit WCE=1). Diese Datei ist in diesem Repo eingebunden unter lib/tinyusb_patched/msc_device.c – der Build kompiliert sie automatisch anstelle der SDK-Kopie, sodass nie ein SDK-Eingriff erforderlich ist. Der Diff gegen das ursprüngliche TinyUSB (0.18.0, wie im pico-sdk 2.2.0 enthalten) befindet sich in lib/tinyusb_patched/; die SDK-Festlegung existiert genau deshalb, weil diese eingebundene Datei das TinyUSB des SDKs verfolgen muss.
cd /home/pi/mk4001_bridge/build
cmake ..
make -j4
sudo openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg \
-c "adapter speed 1000" -c "init" -c "reset halt" -c "sleep 200" \
-c "program /home/pi/mk4001_bridge/build/mk4001_bridge.elf verify" \
-c "reset run" -c "exit"
Hinweis: GP0 und GP1 sind an diesem speziellen Pico-Modul tot. Alle SDIO-Pinzuweisungen sind um +2 verschoben.
Menschliche Notizen: Claude lag hier falsch, weil es nicht erkannte, dass GP0 und GP1 in seiner Build-Konfiguration für das UART-Terminal verwendet wurden. Es vergaß dies ständig, bis ich die SDIO-GPIOs einfach von diesem UART wegbewegt habe.
HDD_PWR ist nicht notwendig. Sie müssen das Laufwerk nicht stromlos schalten, um es zu verwenden; es ist eher eine Entwicklungserleichterung zum Zurücksetzen der HDD, wenn viele Dinge fest codiert sind. Wenn Sie jedoch Strom sparen möchten, können Sie dieses Signal verwenden, aber es kann Warm-Reset problemlos handhaben.
Sie sehen Debug-Nachrichten über UART. Sie gehen nicht über USB-CDC, weil es für Claude einfacher war, eine separate UART-zu-USB-Logging-Verbindung einzurichten, die während der frühen Entwicklung nicht getrennt oder instabil wird.
Das UART-Protokoll meldet auch alle 30 Sekunden, während das Laufwerk aktiv ist, die Laufwerkstemperatur ([TEMP] drive temperature: 29 C). Der Sensor wurde durch Reverse-Engineering des Toshiba-Herstellerbefehls 0xC2 entdeckt – das N91 liest ihn zu Beginn jeder Laufwerkssitzung, um seine HDD-Betriebstemperaturgrenzen durchzusetzen. Details in docs/N91_TRACE_ANALYSIS.md §4.
Hier ein Beispiel des Logs:
========================================
MK4001MTD USB Bridge v0.11
SDIO-ATA → USB Mass Storage (PIO)
========================================
[MAIN] Pre-delay 5000ms...
[PIO] Init OK: clkdiv=3.12 (~10.0 MHz), CMD@0
[MAIN] Power cycling HDD...
[SDIO] HDD power OFF
[SDIO] HDD power ON
[MAIN] SDIO init (PIO)...
[SDIO] CMD5 ready (OCR=0x901F8000)
[SDIO] RCA=0x0001
[SDIO] fn1 ready (attempt 0)
[MAIN] ATA IDENTIFY...
[ATA] IDENTIFY complete
Model: [TOSHIBA MK4001MTD]
Serial: [ 763B004HA]
Firmware: [VH173A]
Sectors: 7862400 (3839 MB)
SMART: not supported (supported=0, enabled=0)
IDENTIFY: W0=0040 W47=0000 W49=0000 W59=0000
ATA W80=0000 Cmd W82=0000 W83=0000 W84=0000
En W85=0000 W86=0000 W87=0000 W89=0008 W128=0001
[DIAG] === Drive Diagnostics ===
[DIAG] Standard SMART: not supported (IDENTIFY W82 bit0 = 0)
[DIAG] Toshiba vendor CMD 0xC2:
FEAT=0x01 unknown_01 → SC=00 LBA=02/00/00 ST=50
FEAT=0x02 unknown_02 â SC=00 LBA=02/00/00 ST=50
FEAT=0x03 unknown_03 → SC=00 LBA=02/00/00 ST=50
FEAT=0x04 unknown_04 → SC=00 LBA=02/00/00 ST=50
FEAT=0x10 diag_10 (LBA_LO varies) → SC=00 LBA=00/00/00 ST=50
FEAT=0x11 diag_11 → SC=00 LBA=00/00/00 ST=50
FEAT=0x12 diag_12 (LBA_LO varies) → SC=00 LBA=01/00/00 ST=50
FEAT=0x20 query_20 (N91: SC=0xFF always) → SC=FE LBA=00/FF/00 ST=50
FEAT=0x21 query_21 (N91: SC varies per boot) → SC=1B LBA=00/FF/00 ST=50
[MAIN] MBR: valid 0x55AA
[MAIN] Warming up...
[MAIN] PIO OK, STATUS=0x50
[MAIN] Drive: 7862400 sectors (3839 MB)
[MAIN] Ready.
[PWR] Idle 5000ms → STANDBY + power gate
[PWR] STANDBY IMMEDIATE → power gate
[SDIO] HDD power OFF
Abschließend hier die Verdrahtung zum tatsächlichen Laufwerk.
Hier ein Ausschnitt aus dem N91-Schaltplan, Sie können auch die Pinnummer zuordnen.

Anmerkung: Dieses Laufwerk arbeitet mit 3V, aber ich denke, 3,3V ist in Ordnung, hauptsächlich um Pegelanpassungsarbeit zu sparen.
Speziell für dieses Laufwerk entworfene Hardware befindet sich unter /hardware!

# Prüfen, ob Gerät erschienen ist
lsblk -dno NAME,MODEL | grep MK4001
# Dateisystemtest – mounten, Dateien kopieren, überprüfen
sudo mount /dev/sdX1 /mnt/mk4001
cp /tmp/testfile /mnt/mk4001/
sync
md5sum /tmp/testfile /mnt/mk4001/testfile # sollte übereinstimmen
sudo umount /mnt/mk4001
# Geschwindigkeitsbenchmarks (rohes Gerät, NICHT mounten – beschädigt das Dateisystem)
# Verwenden Sie einen sicheren Offset jenseits des Dateisystems oder ein nicht partitioniertes Laufwerk
sudo dd if=/dev/sdX of=/dev/null bs=64k count=128 iflag=direct # lesen
sudo dd if=/dev/zero of=/dev/sdX bs=64k count=64 oflag=direct seek=1024 # schreiben (Offset jenseits FS)
Menschliche Notizen hier, lustige Tatsache: Als es zum ersten Mal mit Geschwindigkeitstests begann, hat es tatsächlich dd direkt auf das Laufwerk angewendet und die Dateisysteme beschädigt. Zum Glück spielt das während der Entwicklung keine große Rolle, aber denken Sie immer daran, wenn Sie OpenClaw handhaben, Ihre Einrichtung zu sichern.
Ist mir egal.
| Metrik | Wert |
|---|
| Lesegeschwindigkeit | ~985 kB/s (USB Full-Speed begrenzt) |
| Schreibgeschwindigkeit | ~920 kB/s (USB Full-Speed begrenzt, beworbener Schreibcache) |
| Rohe SDIO-Seitengeschwindigkeit | ~2,35 MB/s Lesen / ~2,15 MB/s Schreiben (laufwerksbegrenzt) |
| Kapazität | 3,75 GB (7.862.400 Sektoren) |
| Dateisystem | FAT32 verifiziert (mount/unmount/fsck sauber) |
| Datenintegrität | Schreiben+Rücklesen verifiziert; CRC16 pro Block auf allen 4 DAT-Leitungen |
| Leerlauf-Standby | 5 s Leerlauf oder USB-Suspend → STANDBY SOFORT + Stromabschaltung |
| Adresse | Register | Verwendung |
|---|
| 0x00 | DATA | CMD53-Ziel für Sektordaten |
| 0x01 | ERR/FEAT | Fehler (lesen) / Feature (schreiben) |
| 0x02 | SECCOUNT | Sektoranzahl |
| 0x03 | LBA_LO | LBA-Bits 0-7 |
| 0x04 | LBA_MID | LBA-Bits 8-15 |
| 0x05 | LBA_HI | LBA-Bits 16-23 |
| 0x06 | DEV/HEAD | Gerät/Kopf + LBA-Bits 24-27 |
| 0x07 | CMD/STATUS | Befehl (schreiben) / Status (lesen) |
| Pico GPIO | Funktion | Hinweise |
|---|
| GP2 | SDIO_CLK | Host-Taktausgang |
| GP3 | SDIO_CMD | Bidirektionale Befehlsleitung |
| GP4 | SDIO_DAT0 | Datenbit 0 |
| GP5 | SDIO_DAT1 | Datenbit 1 |
| GP6 | SDIO_DAT2 | Datenbit 2 |
| GP7 | SDIO_DAT3 | Datenbit 3 |
| GP9 | HDD_EN | Laufwerksstromfreigabe (HIGH=ein) |
| GP12 | UART TX | Debug-Ausgabe @ 115200 |
| GP13 | UART RX | Debug-Eingang |
| GP16 | LED: HDD-Strom | Aktiv niedrig |
| GP17 | LED: HDD-Gesund | Aktiv niedrig |
| GP18 | LED: Lesen | Aktiv niedrig |
| GP19 | LED: Schreiben | Aktiv niedrig |
| Datei | Zeilen | Zweck |
|---|
main.c | 210 | Initialisierung, Leerlauf-Standby, USB-Suspend/Resume |
msc_device.c | 400 | USB-MSC-Callbacks, Aufwecken der Stromabschaltung, Cache für defekte Sektoren |
ata_sdio.c | 390 | ATA-Befehle, Fehlerbehebung, Herstellerdiagnose |
sdio_pio.c | 635 | PIO SDIO: CMD52, CMD53 lesen/schreiben, Programmwechsel, CRC16 |
sdio_hw.c | 45 | Pin-Initialisierung + HDD-Stromsteuerung |
sdio.pio | 200 | PIO-Assembler + C-SDK-Initialisierungshilfen |
led.h | 37 | LED-Hilfsfunktionen (GP16–GP19, aktiv niedrig) |
usb_descriptors.c | 77 | USB-Geräte-/Konfigurations-/String-Deskriptoren |
tusb_config.h | 20 | TinyUSB-Konfiguration (MSC, 32KB EP-Puffer) |
| Version | Lesen | Schreiben | Wichtige Änderung |
|---|
| v0.1–v0.3 | 105 kB/s | 93 kB/s | Bit-Bang SDIO, CRC16, Wiederholungslogik |
| v0.5 | 374 kB/s | — | Einzel-SM PIO, direkter Befehlsspeicherwechsel |
| v0.6 | 583 kB/s | 93 kB/s | Multi-Block CMD53-Lesevorgänge, CRC-Taktabflussbehebung |
| v0.8 | 588 kB/s | 274 kB/s | PIO-Schreibvorgänge, OSR-Leerungsbehebung |
| v0.9 | 475 kB/s | 371 kB/s | 64-Sektor-Blöcke, CRC16-Leseüberprüfung |
| v0.10 | 453 kB/s | 329 kB/s | LED-Umbelegung, HDD-EN-Pin, UART auf GP12/GP13 |
| v0.11 | ~450 kB/s | ~340 kB/s | HDD-Stromabschaltung, PIO-Aufwecken, Defekter-Sektor-Erkennung, USB-Suspend |
| v0.12 | ~985 kB/s | ~920 kB/s | Laufwerk/USB-Überlappung (Leseprefetch + beworbener Schreibcache mit Write-Behind), pipelineartige PIO-Blöcke, bswap DMA, 4-Zyklus-PIO-Schleifen, SBC-artige Semantik für defekte Sektoren (Schreibreparatur), eingebundener TinyUSB-MSC-Treiber |