
Firmware RP2040 faisant le pont entre un microdrive SDIO Toshiba MK4001MTD 0,85" et un périphérique de stockage de masse USB, implémentant la pile de protocole SDIO-ATA complète à partir de zéro avec des lectures/écritures accélérées par PIO et la récupération des secteurs défectueux.
Micrologiciel pour RP2040 Pico qui fait le pont entre un microdrive Toshiba MK4001MTD 0,85" SDIO et un périphérique de stockage de masse USB.

Le MK4001MTD est un microdrive de 4 Go initialement utilisé dans le Nokia N91 (téléphone musical) et certains autres appareils, comme des lecteurs MP3 ou des clés USB, à une époque où le stockage flash était encore assez cher.
Vous avez peut-être vu des présentations affirmant que ce disque utilise le protocole MMC, mais c'est en fait incorrect. J'ai enquêté là-dessus un moment : j'ai essayé de construire un lecteur de carte MMCplus 8 bits et testé différents lecteurs SD/MMC sans succès. En dernier recours, j'ai acheté un Nokia N91 pour capturer des traces logiques et confirmer quel protocole il utilise réellement.
Voici la photo de ma tentative d'utilisation avec ma carte lecteur 8-bit-MMCPlus, et il s'avère que ce n'est pas du MMC :(

J'ai donc fini par obtenir un N91 pour collecter des traces :

Contrairement aux microdrives ATA/CF standard, il utilise une interface SDIO avec des commandes ATA tunnelisées via CMD52/CMD53. Aucun pilote existant ne prend en charge ce protocole, donc ce micrologiciel implémente la pile complète à partir de zéro.
Cela m'a surpris, car il existe une norme SDIO vers ATA appelée CE-ATA. Mais si l'on regarde attentivement la chronologie des versions, CE-ATA est arrivée après ce disque. Par conséquent, ce disque repose entièrement sur les commandes SDIO, et CE-ATA n'est pas disponible. CE-ATA a deux nouvelles commandes CMD60/CMD61 et utilise CMD12/39, mais on voit d'après les traces qu'il n'en utilise aucune.
Le second point matériel à mentionner est qu'une autre désinformation qui circule — affirmant qu'il s'agit d'une carte MMCPlus 8 bits — est non seulement fausse, mais en plus le brochage ne correspond pas non plus à la norme MMC. Vous pouvez trouver le manuel de service du Nokia N91 avec une documentation sur le brochage : bien que la numérotation des broches suive la norme MMCPlus, le mappage des broches ne la suit pas. C'est un détail important si vous câblez vous-même : il utilise le même connecteur MMC, mais le mappage des broches est différent, plus d'informations dans la section Matériel.
Enfin, notez que ce projet est co-développé avec Claude/OpenClaw. J'ai collecté les traces logiques manuellement et mis en place une station de test en boucle fermée pour qu'OpenClaw puisse itérer sur le développement — analyser les traces et implémenter les fonctionnalités. La documentation sera principalement rédigée par Claude ; j'ajouterai mes notes en ligne également. J'ai moi-même lu et vérifié la documentation, elle devrait être fiable et facile à suivre.
Pour des informations sur l'analyse de la trace N91, consultez /docs/N91_TRACE_ANALYSIS.md. J'ai également mis le manuel de service N91 ainsi que les traces logiques brutes.
Voir plus dans l'article de blog ici : https://www.willwhang.dev/Reading-MK4001MTD/
Voir en action ici : https://youtu.be/GC4xil3_Bbc
Stockage de masse USB entièrement fonctionnel avec lectures/écritures accélérées par PIO et gestion de l'alimentation en veille.
Hôte USB ←→ USB MSC (TinyUSB) ←→ Couche ATA ←→ Couche SDIO (PIO) ←→ MK4001MTD
Le micrologiciel comporte quatre couches :
USB MSC (msc_device.c) — Classe de stockage de masse TinyUSB. Traduit SCSI READ(10)/WRITE(10) en opérations de secteurs ATA. Tampon EP de 32 Ko, regroupement jusqu'à 64 secteurs par transfert USB. Les E/S du disque sont chevauchées avec l'USB dans les deux sens, comme un véritable pont ATA-USB avec un disque à cache : un préfetcheur de lecture séquentielle récupère le bloc suivant pendant que le précédent est envoyé à l'hôte, et les écritures sont mises en scène et vidées pendant que l'USB reçoit le morceau suivant. Le périphérique annonce son cache d'écriture (page de mode Caching, WCE=1 — les hôtes rapportent « Write cache: enabled » et émettent SYNCHRONIZE CACHE à fsync/démontage/suspension, ce que le micrologiciel respecte). Un vidage en arrière-plan qui échoue se manifeste par une MEDIUM ERROR sur le prochain WRITE ou SYNCHRONIZE CACHE ; les écritures sur des secteurs connus comme défectueux empruntent un chemin synchrone strict.
ATA sur SDIO (ata_sdio.c) — Implémente les commandes ATA (IDENTIFY, READ SECTORS, WRITE SECTORS) en écrivant dans les registres ATA mappés dans l'espace d'adressage de la fonction SDIO 1 via CMD52, et en transférant les données de secteur via CMD53. Logique de nouvelle tentative à 3 niveaux (CMD, données et ATA).
PIO SDIO (sdio_pio.c, sdio.pio) — SDIO accéléré par matériel utilisant le périphérique PIO du RP2040 (bus 4 bits à 10 MHz, 4 cycles PIO par bit avec synchroniseurs d'entrée contournés). Trois programmes PIO partagent une seule machine d'état via un échange dynamique de programmes :
Notes humaines : Fait intéressant, Claude était vraiment réticent à implémenter le SDIO en PIO, et de nombreux cycles de développement ont été perdus à faire des allers-retours entre PIO et bit-bang.
Le MK4001MTD se présente comme une carte SDIO avec une fonction d'E/S. L'initialisation standard de carte SDIO (CMD5/CMD3/CMD7) configure le bus, puis les registres ATA sont accédés via des commandes SDIO :
Accès aux registres (CMD52) : Chaque registre ATA est mappé à une adresse de la fonction 1 :
Transfert de données (CMD53) : Les données de secteur sont transférées en émettant CMD53 en mode bloc, ciblant le registre DATA (adresse 0x00). Pour des lectures multi-secteurs, un seul CMD53 avec block_count=N transfère N × 512 octets en une seule transaction multi-bloc SDIO.
Signalisation d'interruption : Le disque signale la disponibilité d'un secteur en activant une interruption SDIO (bit INT_PENDING 1 dans le registre CCCR 0x05). La lecture du registre ATA STATUS efface l'interruption.
Pour une lecture de 16 secteurs :
1. Écrire les registres ATA via PIO CMD52 :
SECCOUNT=16, LBA_LO/MID/HI, DEV/HEAD=0xE0, CMD=0x20
2. Interroger STATUS via CMD52 jusqu'à ce que DRQ (bit 3) soit mis à 1
3. Échanger PIO vers le programme de lecture DAT
4. Envoyer CMD53 : block_mode=1, fn=1, addr=0x0000, block_count=16
5. Lecture DAT PIO : pour chacun des 16 blocs :
a. Attendre le bit de départ (toutes les lignes DAT à l'état bas)
b. DMA de 1024 nibbles (512 octets) depuis la FIFO RX PIO vers le tampon
c. Attendre que la SM ait fini d'horloger les nibbles CRC+fin (interroger PC de la SM)
d. Réorganiser les nibbles → octets sur place
6. Échanger PIO vers le programme CMD
Pour une écriture de 16 secteurs :
1. Écrire les registres ATA via PIO CMD52 :
SECCOUNT=16, LBA, DEV/HEAD=0xE0, CMD=0x30
2. Interroger STATUS via CMD52 jusqu'à ce que DRQ (bit 3) soit mis à 1
(STATUS 0xD8 = BSY+DRQ traité comme DRQ prêt, d'après la trace N91)
3. Échanger PIO vers le programme d'écriture DAT
4. Envoyer CMD53 : block_mode=1, fn=1, addr=0x0000, block_count=16
5. Écriture DAT PIO : pour chacun des 16 blocs :
a. Précalculer CRC16-CCITT par ligne DAT (4 CRC indépendants)
b. Construire le flux de nibbles : start(0x0) + data(1024 nibbles) + CRC(16) + end(0xF)
c. Envoyer par DMA le flux de nibbles vers la FIFO TX PIO
d. Le PIO horloge tous les nibbles, puis :
- Passe DAT en entrée
- Horloge 16 cycles pour l'état CRC de la carte
- Interroge DAT0 jusqu'à ce que la carte libère le signal d'occupé
- Déclenche IRQ 0 pour signaler la fin du bloc
6. Échanger PIO vers le programme CMD
Le PIO du RP2040 dispose de 32 emplacements d'instructions par bloc. Nos trois programmes totalisent 55 instructions, ils ne peuvent donc pas coexister. On utilise plutôt une seule SM0 sur PIO0, et les programmes sont échangés en écrivant directement dans la mémoire d'instructions PIO :
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];
}
Cela contourne l'allocateur pio_add_program/pio_remove_program du SDK. L'échange de programme prend environ 1 µs. Chaque échange est suivi d'une réinitialisation spécifique au programme qui définit les mappages de broches, la direction de décalage et le diviseur d'horloge.
L'analyse des traces logiques du Nokia N91 révèle une gestion agressive de l'alimentation :
Le micrologiciel reproduit ce comportement avec un délai d'inactivité configurable :
#define IDLE_STANDBY_MS 5000 // dans main.c
Deux chemins déclenchent la coupure d'alimentation du disque dur :
Les deux chemins envoient ATA STANDBY IMMEDIATE (0xE0) pour vider le cache d'écriture et garer les têtes, puis coupent l'alimentation via GP9.
Séquence de réveil (déclenchée par la première lecture/écriture après coupure) :
Lorsqu'un transfert multi-secteurs rencontre un secteur défectueux :
STATUS/ERROR au lieu de réduire l'échec à un DRQ timeout génériqueMEDIUM ERROR (lecture : 03/11/00, écriture : 03/0C/00)Le point 6 n'est pas théorique : ce disque avait un secteur illisible de longue date au LBA 1952 (READ: ST=0x51 ERR ERR=0x40 UNC). Une fois que le pont a permis à une écriture de l'atteindre, le disque a réécrit le secteur et il est relu proprement depuis :
[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)La version du SDK est verrouillée : si PICO_SDK_PATH est défini (variable d'environnement ou CMake), il est utilisé et sa version est vérifiée par rapport à l'épingle — une incompatibilité fait échouer la configuration avec des instructions (remplacer par -DMK4001_ALLOW_SDK_MISMATCH=ON). Sans PICO_SDK_PATH du tout, la version épinglée du SDK est téléchargée automatiquement depuis GitHub au moment de la configuration, donc un simple git clone && cmake && make est parfaitement reproductible.
Le micrologiciel a besoin d'un pilote de classe MSC TinyUSB patché (données de sense d'application préservées en cas d'erreurs de lecture/écriture + une page de mode Caching avec WCE=1). Ce fichier est fourni dans ce dépôt à lib/tinyusb_patched/msc_device.c — la compilation l'utilise automatiquement à la place de la copie du SDK, donc aucune modification du SDK n'est jamais nécessaire. Le diff par rapport à TinyUSB amont (0.18.0, tel qu'inclus dans pico-sdk 2.2.0) se trouve dans lib/tinyusb_patched/ ; l'épingle du SDK existe précisément parce que ce fichier fourni doit suivre le TinyUSB du SDK.
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"
Remarque : GP0 et GP1 sont morts sur cette unité Pico spécifique. Toutes les affectations des GPIO SDIO sont décalées de +2.
Notes humaines : Claude s'est trompé ici car il n'a pas réalisé que GP0 et GP1 étaient utilisés pour le terminal UART dans sa configuration de construction. Il n'arrêtait pas de l'oublier, au point que j'ai simplement déplacé les GPIO SDIO hors de cet UART.
HDD_PWR n'est pas nécessaire. Vous n'avez pas besoin de couper l'alimentation du disque pour l'utiliser ; c'est surtout une commodité de développement pour réinitialiser le disque dur quand beaucoup de choses sont codées en dur. Cela dit, si vous souhaitez économiser de l'énergie, vous pouvez utiliser ce signal, mais il supporte la réinitialisation à chaud sans problème.
Vous verrez des messages de débogage sur l'UART. Ils ne passent pas par USB-CDC car il était plus facile pour Claude de mettre en place une liaison de journalisation UART-vers-USB séparée qui ne se déconnecte pas ou ne devient pas instable pendant le développement précoce.
Le journal UART rapporte également la température du disque toutes les 30 secondes lorsque le disque est actif ([TEMP] drive temperature: 29 C). Le capteur a été découvert par rétro-ingénierie de la commande constructeur Toshiba 0xC2 — le N91 la lit au début de chaque session disque pour imposer ses limites de température de fonctionnement du disque dur. Détails dans docs/N91_TRACE_ANALYSIS.md §4.
Voici un exemple du journal :
========================================
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
Enfin, voici le câblage vers le disque réel.
Voici un extrait du schéma N91, vous pouvez également mapper le numéro de broche.

Note secondaire : ce disque est en 3 V mais je pense que 3,3 V convient, surtout pour éviter du travail de décalage de niveau.
Un matériel spécifiquement conçu pour ce disque se trouve dans /hardware !

# Vérifier que le périphérique est apparu
lsblk -dno NAME,MODEL | grep MK4001
# Test du système de fichiers — monter, copier des fichiers, vérifier
sudo mount /dev/sdX1 /mnt/mk4001
cp /tmp/testfile /mnt/mk4001/
sync
md5sum /tmp/testfile /mnt/mk4001/testfile # doit correspondre
sudo umount /mnt/mk4001
# Tests de vitesse (périphérique brut, NE PAS monter d'abord — cela corromprait le système de fichiers)
# Utiliser un décalage sûr après le système de fichiers ou un disque non partitionné
sudo dd if=/dev/sdX of=/dev/null bs=64k count=128 iflag=direct # lecture
sudo dd if=/dev/zero of=/dev/sdX bs=64k count=64 oflag=direct seek=1024 # écriture (décalage après le système de fichiers)
Notes humaines ici, anecdote amusante : Quand il a commencé les tests de vitesse, il a en fait utilisé dd directement sur le disque et endommagé les systèmes de fichiers... Heureusement, cela n'a pas eu d'importance majeure pendant le développement, mais gardez toujours cela à l'esprit lorsque vous manipulez OpenClaw et votre configuration.
Je m'en fiche.
| Métrique | Valeur |
|---|
| Vitesse de lecture | ~985 ko/s (limité par la pleine vitesse USB) |
| Vitesse d'écriture | ~920 ko/s (limité par la pleine vitesse USB, cache d'écriture annoncé) |
| Vitesse côté SDIO brute | ~2,35 Mo/s en lecture / ~2,15 Mo/s en écriture (limité par le disque) |
| Capacité | 3,75 Go (7 862 400 secteurs) |
| Système de fichiers | FAT32 vérifié (montage/démontage/fsck propre) |
| Intégrité des données | Écriture+relecture vérifiée ; CRC16 par bloc sur les 4 lignes DAT |
| Veille inactif | 5 s d'inactivité ou suspension USB → STANDBY IMMEDIATE + coupure d'alimentation |
Broches/Alimentation (sdio_hw.c) — Initialisation des GPIO et commande d'alimentation du disque dur. Toute la communication SDIO utilise le PIO.
| Adresse | Registre | Usage |
|---|
| 0x00 | DATA | Cible CMD53 pour les données de secteur |
| 0x01 | ERR/FEAT | Erreur (lecture) / Feature (écriture) |
| 0x02 | SECCOUNT | Nombre de secteurs |
| 0x03 | LBA_LO | LBA bits 0-7 |
| 0x04 | LBA_MID | LBA bits 8-15 |
| 0x05 | LBA_HI | LBA bits 16-23 |
| 0x06 | DEV/HEAD | Device/Head + LBA bits 24-27 |
| 0x07 | CMD/STATUS | Commande (écriture) / Statut (lecture) |
| GPIO Pico | Fonction | Remarques |
|---|
| GP2 | SDIO_CLK | Sortie d'horloge hôte |
| GP3 | SDIO_CMD | Ligne de commande bidirectionnelle |
| GP4 | SDIO_DAT0 | Bit de données 0 |
| GP5 | SDIO_DAT1 | Bit de données 1 |
| GP6 | SDIO_DAT2 | Bit de données 2 |
| GP7 | SDIO_DAT3 | Bit de données 3 |
| GP9 | HDD_EN | Activation de l'alimentation du disque (HIGH=marche) |
| GP12 | UART TX | Sortie de débogage à 115200 bauds |
| GP13 | UART RX | Entrée de débogage |
| GP16 | LED : Alimentation disque | Active basse |
| GP17 | LED : Disque sain | Active basse |
| GP18 | LED : Lecture | Active basse |
| GP19 | LED : Écriture | Active basse |
| Fichier | Lignes | Objectif |
|---|
main.c | 210 | Initialisation, veille inactive, suspension/reprise USB |
msc_device.c | 400 | Callbacks USB MSC, réveil par coupure d'alimentation, cache de secteurs défectueux |
ata_sdio.c | 390 | Commandes ATA, récupération d'erreur, diagnostics constructeur |
sdio_pio.c | 635 | SDIO PIO : CMD52, CMD53 lecture/écriture, échange de programme, CRC16 |
sdio_hw.c | 45 | Initialisation des broches + contrôle d'alimentation du disque dur |
sdio.pio | 200 | Assembleur PIO + helpers d'initialisation du SDK C |
led.h | 37 | Helpers pour LED (GP16–GP19, actif bas) |
usb_descriptors.c | 77 | Descripteurs USB de périphérique/configuration/chaîne |
tusb_config.h | 20 | Configuration TinyUSB (MSC, tampon EP de 32 Ko) |
| Version | Lecture | Écriture | Changement clé |
|---|
| v0.1–v0.3 | 105 ko/s | 93 ko/s | SDIO en bit-bang, CRC16, logique de nouvelle tentative |
| v0.5 | 374 ko/s | — | PIO mono-SM, échange direct de mémoire d'instructions |
| v0.6 | 583 ko/s | 93 ko/s | Lectures CMD53 multi-blocs, correction du drain d'horloge CRC |
| v0.8 | 588 ko/s | 274 ko/s | Écritures PIO, correction du vidage OSR |
| v0.9 | 475 ko/s | 371 ko/s | Blocs de 64 secteurs, vérification de lecture CRC16 |
| v0.10 | 453 ko/s | 329 ko/s | Réaffectation LED, broche HDD EN, UART sur GP12/GP13 |
| v0.11 | ~450 ko/s | ~340 ko/s | Coupure d'alimentation disque dur, réveil PIO, sense de secteur défectueux, suspension USB |
| v0.12 | ~985 ko/s | ~920 ko/s | Chevauchement disque/USB (préfetcheur de lecture + cache d'écriture annoncé avec écriture en différé), blocs PIO pipeline, DMA d'inversion d'octets, boucles PIO 4 cycles, sémantique de secteur défectueux style SBC (réparation par écriture), pilote MSC TinyUSB fourni |