
Firmware RP2040 que faz a ponte entre um microdrive SDIO Toshiba MK4001MTD de 0,85" como um dispositivo de armazenamento em massa USB, implementando toda a pilha de protocolo SDIO-ATA do zero com leituras/escritas aceleradas por PIO e recuperação de setores defeituosos.
Firmware RP2040 Pico que faz a ponte de um microdrive SDIO Toshiba MK4001MTD de 0,85" como dispositivo de armazenamento em massa USB.

O MK4001MTD é um Microdrive de 4 GB originalmente usado no telefone musical Nokia N91 e em alguns outros dispositivos, como tocadores de MP3 ou unidades USB, na época em que o armazenamento flash ainda era bastante caro.
Você pode ter visto introduções afirmando que este drive usa o protocolo MMC, mas isso está incorreto. Estou investigando isso há algum tempo: tentei construir um leitor de cartão MMCplus de 8 bits e testei diferentes leitores SD/MMC sem sucesso. Como último recurso, comprei um Nokia N91 para capturar traces lógicos e confirmar qual protocolo ele realmente usa.
Aqui está a foto quando eu estava tentando usá-lo com minha placa leitora 8bit-MMCPlus, e descobriu-se que não é MMC :(

Então acabei comprando um N91 para coletar traces:

Ao contrário dos microdrives ATA/CF padrão, ele usa uma interface SDIO com comandos ATA tunelados através do CMD52/CMD53. Nenhum driver existente suporta esse protocolo, então este firmware implementa toda a pilha do zero.
Isso me surpreendeu, porque existe um padrão SDIO-para-ATA chamado CE-ATA. Mas se você observar atentamente a linha do tempo de lançamento, o CE-ATA veio depois deste drive. Como resultado, este drive depende inteiramente de comandos SDIO, e o CE-ATA não está disponível. O CE-ATA tem dois novos comandos CMD60/CMD61 e utiliza CMD12/39, mas você pode ver pelos traces que ele não está usando nenhum deles.
O segundo ponto de hardware a mencionar é que outra desinformação circulando—afirmando que é um cartão MMCPlus de 8 bits—não é apenas falsa, mas a pinagem também não segue o padrão MMC. Você pode encontrar o manual de serviço do Nokia N91 com alguma documentação sobre a pinagem: embora a numeração dos pinos siga o padrão MMCPlus, o mapeamento dos pinos não. Este é um detalhe importante se você estiver fazendo a fiação você mesmo: ele usa o mesmo conector MMC, mas o mapeamento dos pinos é diferente, mais na seção Hardware.
Finalmente, note que isso foi desenvolvido em conjunto com Claude/OpenClaw. Eu coletei os traces lógicos manualmente e montei uma estação de teste em malha fechada para o OpenClaw iterar no desenvolvimento—analisando os traces e implementando funcionalidades. A documentação será escrita principalmente por Claude; também adicionarei minhas anotações inline. Também li e verifiquei a documentação pessoalmente, e ela deve ser confiável e fácil de seguir.
Para insights sobre as análises do trace do N91, está em /docs/N91_TRACE_ANALYSIS.md, também coloquei o manual de serviço do N91 lá junto com os traces lógicos brutos.
Veja mais no post do blog aqui: https://www.willwhang.dev/Reading-MK4001MTD/
Veja em atividade aqui: https://youtu.be/GC4xil3_Bbc
Armazenamento em massa USB totalmente funcional com leituras/escritas aceleradas por PIO e gerenciamento de energia ociosa.
USB Host ←→ USB MSC (TinyUSB) ←→ ATA Layer ←→ SDIO Layer (PIO) ←→ MK4001MTD
O firmware tem quatro camadas:
USB MSC (msc_device.c) — TinyUSB Mass Storage Class. Traduz SCSI READ(10)/WRITE(10) em operações de setor ATA. Buffer EP de 32 KB, agrupando até 64 setores por transferência USB. A E/S do drive é sobreposta com USB em ambas as direções, como uma ponte ATA-USB real com um disco de cache: um pré-buscador de leitura sequencial busca o próximo bloco enquanto o anterior é transmitido ao host, e as escritas são enfileiradas e liberadas enquanto a USB recebe a próxima parte. O dispositivo anuncia seu cache de escrita (Caching mode page, WCE=1 — os hosts reportam "Write cache: enabled" e emitem SYNCHRONIZE CACHE em fsync/desmontagem/suspensão, que o firmware honra). Uma liberação de fundo com falha surge como MEDIUM ERROR no próximo WRITE ou SYNCHRONIZE CACHE; escritas em setores conhecidamente ruins seguem um caminho estritamente síncrono.
ATA-over-SDIO (ata_sdio.c) — Implementa comandos ATA (IDENTIFY, READ SECTORS, WRITE SECTORS) escrevendo em registradores ATA mapeados no espaço de endereço da função 1 SDIO via CMD52, e transferindo dados de setor via CMD53. Lógica de retry em 3 níveis nos níveis CMD, dados e ATA.
PIO SDIO (sdio_pio.c, sdio.pio) — SDIO acelerado por hardware usando o periférico PIO do RP2040 (barramento de 4 bits a 10 MHz, 4 ciclos PIO por bit com sincronizadores de entrada desviados). Três programas PIO compartilham uma única máquina de estado via troca dinâmica de programas:
Pin/Power (sdio_hw.c) — Inicialização de GPIO e controle de energia do HDD. Toda comunicação SDIO usa PIO.
Notas humanas: Curiosamente, Claude estava realmente relutante em implementar SDIO em PIO, e muitos ciclos de desenvolvimento foram desperdiçados indo e voltando entre PIO e bit-banging.
O MK4001MTD se apresenta como um cartão SDIO com uma função I/O. A inicialização padrão do cartão SDIO (CMD5/CMD3/CMD7) configura o barramento, então os registradores ATA são acessados através de comandos SDIO:
Acesso a registradores (CMD52): Cada registrador ATA é mapeado para um endereço da função 1:
Transferência de dados (CMD53): Os dados do setor são transferidos emitindo CMD53 em modo bloco visando o registrador DATA (endereço 0x00). Para leituras de múltiplos setores, um único CMD53 com block_count=N transfere N × 512 bytes em uma transação multi-bloco SDIO.
Sinalização de interrupção: O drive sinaliza prontidão do setor afirmando uma interrupção SDIO (bit INT_PENDING 1 no registrador CCCR 0x05). A leitura do registrador ATA STATUS limpa a interrupção.
Para uma leitura de 16 setores:
1. Escrever registradores ATA via PIO CMD52:
SECCOUNT=16, LBA_LO/MID/HI, DEV/HEAD=0xE0, CMD=0x20
2. Poll STATUS via CMD52 até DRQ (bit 3) ser definido
3. Trocar PIO para programa de leitura DAT
4. Enviar CMD53: block_mode=1, fn=1, addr=0x0000, block_count=16
5. Leitura PIO DAT: para cada um dos 16 blocos:
a. Aguardar bit de início (todas as linhas DAT baixas)
b. DMA de 1024 nibbles (512 bytes) do FIFO RX do PIO para o buffer
c. Aguardar SM terminar de clockear nibbles CRC+fim (poll SM PC)
d. Reempacotar nibbles → bytes no local
6. Trocar PIO de volta para programa CMD
Para uma escrita de 16 setores:
1. Escrever registradores ATA via PIO CMD52:
SECCOUNT=16, LBA, DEV/HEAD=0xE0, CMD=0x30
2. Poll STATUS via CMD52 até DRQ (bit 3) ser definido
(STATUS 0xD8 = BSY+DRQ tratado como DRQ-pronto, conforme trace N91)
3. Trocar PIO para programa de escrita DAT
4. Enviar CMD53: block_mode=1, fn=1, addr=0x0000, block_count=16
5. Escrita PIO DAT: para cada um dos 16 blocos:
a. Pré-calcular CRC16-CCITT por linha DAT (4 CRCs independentes)
b. Construir fluxo de nibbles: start(0x0) + dados(1024 nibbles) + CRC(16) + end(0xF)
c. DMA do fluxo de nibbles para FIFO TX do PIO
d. PIO clockea todos os nibbles, então:
- Alterna DAT para entrada
- Clockea 16 ciclos para status CRC do cartão
- Poll DAT0 até cartão liberar busy
- Dispara IRQ 0 para sinalizar conclusão do bloco
6. Trocar PIO de volta para programa CMD
O RP2040 PIO tem 32 slots de instrução por bloco. Nossos três programas totalizam 55 instruções, então não podem coexistir. Em vez disso, uma única SM0 no PIO0 é usada, e os programas são trocados escrevendo diretamente na memória de instrução 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];
}
Isso contorna o alocador pio_add_program/pio_remove_program do SDK. A troca de programa leva ~1 µs. Cada troca é seguida por uma reinicialização específica do programa que define mapeamentos de pinos, direção de deslocamento e divisor de clock.
A análise dos traces lógicos do Nokia N91 revela gerenciamento de energia agressivo:
O firmware replica esse comportamento com um tempo limite configurável para inatividade:
#define IDLE_STANDBY_MS 5000 // in main.c
Dois caminhos acionam o corte de energia do HDD:
Ambos os caminhos enviam ATA STANDBY IMMEDIATE (0xE0) para liberar o cache de escrita e estacionar as cabeças, depois cortam a energia via GP9.
Sequência de ativação (acionada pelo primeiro READ/WRITE após o corte):
Quando uma transferência de múltiplos setores atinge um setor ruim:
STATUS/ERROR em vez de colapsar a falha em um DRQ timeout genéricoMEDIUM ERROR (leitura: 03/11/00, escrita: 03/0C/00)O ponto 6 não é acadêmico: este drive tinha um setor ilegível de longa data no LBA 1952 (READ: ST=0x51 ERR ERR=0x40 UNC). Depois que a ponte permitiu que uma escrita realmente o alcançasse, o drive reescreveu o setor e ele tem sido lido limpo desde então:
[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)A versão do SDK é travada: se PICO_SDK_PATH estiver definida (variável de ambiente ou CMake) ela é usada e sua versão é verificada contra o travamento — uma incompatibilidade falha a configuração com instruções (substituir com -DMK4001_ALLOW_SDK_MISMATCH=ON). Sem PICO_SDK_PATH alguma, a versão travada do SDK é baixada do GitHub automaticamente no momento da configuração, então um simples git clone && cmake && make é totalmente reproduzível.
O firmware precisa de um driver de classe MSC TinyUSB corrigido (dados sense preservados em erros de leitura/escrita + uma página de modo Caching com WCE=1). Esse arquivo está fornecido neste repositório em lib/tinyusb_patched/msc_device.c — a construção automaticamente o compila em vez da cópia do SDK, portanto nenhuma cirurgia no SDK é necessária. O diff contra o TinyUSB upstream (0.18.0, como incluído no pico-sdk 2.2.0) está em lib/tinyusb_patched/; o travamento do SDK existe precisamente porque este arquivo fornecido deve rastrear o TinyUSB do 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"
Nota: GP0 e GP1 estão mortos nesta unidade Pico específica. Todas as atribuições de pinos SDIO estão deslocadas +2.
Notas humanas: Claude estava errado aqui porque não percebeu que GP0 e GP1 estavam sendo usados para o terminal UART em sua configuração de construção. Ele continuava esquecendo isso, a tal ponto que eu simplesmente movi os GPIOs SDIO para fora daquela UART.
O HDD_PWR não é necessário. Você não precisa desligar e ligar o drive para usá-lo; é mais uma conveniência de desenvolvimento para resetar o HDD quando muitas coisas estão codificadas. Dito isso, se você quiser economia de energia, pode usar esse sinal, mas ele pode lidar com reset a quente sem problemas.
Você verá mensagens de depuração pela UART. Elas não passam pelo USB-CDC porque foi mais fácil para Claude configurar um link de registro UART-para-USB separado que não desconecta ou se torna instável durante o desenvolvimento inicial.
O log UART também reporta a temperatura do drive a cada 30 segundos enquanto o drive está ativo ([TEMP] drive temperature: 29 C). O sensor foi descoberto por engenharia reversa do comando proprietário Toshiba 0xC2 — o N91 o lê no início de cada sessão do drive para impor seus limites de temperatura operacional do HDD. Detalhes em docs/N91_TRACE_ANALYSIS.md §4.
Aqui está um exemplo do log:
========================================
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
Finalmente, aqui está a fiação para o drive real.
Aqui está um recorte do esquema N91, você pode mapear o número do pino também.

Nota lateral: este é um drive de 3V, mas acho que 3.3V é aceitável, principalmente para economizar trabalho de mudança de nível.
HW especificamente projetado para este drive está em /hardware!

# Verificar se o dispositivo apareceu
lsblk -dno NAME,MODEL | grep MK4001
# Teste de sistema de arquivos — montar, copiar arquivos, verificar
sudo mount /dev/sdX1 /mnt/mk4001
cp /tmp/testfile /mnt/mk4001/
sync
md5sum /tmp/testfile /mnt/mk4001/testfile # deve coincidir
sudo umount /mnt/mk4001
# Benchmarks de velocidade (dispositivo bruto, NÃO monte primeiro — corromperá o sistema de arquivos)
# Use um deslocamento seguro além do sistema de arquivos ou um drive não particionado
sudo dd if=/dev/sdX of=/dev/null bs=64k count=128 iflag=direct # leitura
sudo dd if=/dev/zero of=/dev/sdX bs=64k count=64 oflag=direct seek=1024 # escrita (deslocamento além do FS)
Notas humanas aqui, fato divertido: Quando começou a fazer testes de velocidade, ele na verdade executou dd diretamente no drive e danificou os sistemas de arquivos..... Felizmente isso não importa muito durante os desenvolvimentos, mas sempre tenha em mente ao lidar com o OpenClaw sua configuração.
Eu não me importo.
| Metric | Value |
|---|
| Velocidade de leitura | ~985 kB/s (limitado pela USB full-speed) |
| Velocidade de escrita | ~920 kB/s (limitado pela USB full-speed, cache de escrita anunciado) |
| Velocidade bruta do lado SDIO | ~2,35 MB/s leitura / ~2,15 MB/s escrita (limitado pelo drive) |
| Capacidade | 3,75 GB (7.862.400 setores) |
| Sistema de arquivos | FAT32 verificado (mount/unmount/fsck limpo) |
| Integridade dos dados | Escrita+releitura verificada; CRC16 por bloco em todas as 4 linhas DAT |
| Espera ociosa | 5 s inativo ou suspensão USB → STANDBY IMMEDIATE + corte de energia |
| Endereço | Registrador | Uso |
|---|
| 0x00 | DATA | Alvo CMD53 para dados de setor |
| 0x01 | ERR/FEAT | Erro (leitura) / Feature (escrita) |
| 0x02 | SECCOUNT | Contagem de setores |
| 0x03 | LBA_LO | LBA bits 0-7 |
| 0x04 | LBA_MID | LBA bits 8-15 |
| 0x05 | LBA_HI | LBA bits 16-23 |
| 0x06 | DEV/HEAD | Dispositivo/Cabeça + LBA bits 24-27 |
| 0x07 | CMD/STATUS | Comando (escrita) / Status (leitura) |
| Pico GPIO | Função | Notas |
|---|
| GP2 | SDIO_CLK | Saída de clock do host |
| GP3 | SDIO_CMD | Linha de comando bidirecional |
| GP4 | SDIO_DAT0 | Bit de dados 0 |
| GP5 | SDIO_DAT1 | Bit de dados 1 |
| GP6 | SDIO_DAT2 | Bit de dados 2 |
| GP7 | SDIO_DAT3 | Bit de dados 3 |
| GP9 | HDD_EN | Ativação de energia do drive (HIGH=ligado) |
| GP12 | UART TX | Saída de depuração @ 115200 |
| GP13 | UART RX | Entrada de depuração |
| GP16 | LED: Energia HDD | Ativo baixo |
| GP17 | LED: HDD Saudável | Ativo baixo |
| GP18 | LED: Leitura | Ativo baixo |
| GP19 | LED: Escrita | Ativo baixo |
| File | Lines | Purpose |
|---|
main.c | 210 | Inicialização, standby inativo, suspensão/retomada USB |
msc_device.c | 400 | Callbacks USB MSC, ativação de corte de energia, cache de setor ruim |
ata_sdio.c | 390 | Comandos ATA, recuperação de erros, diagnósticos de fornecedor |
sdio_pio.c | 635 | PIO SDIO: CMD52, CMD53 leitura/escrita, troca de programa, CRC16 |
sdio_hw.c | 45 | Inicialização de pinos + controle de energia do HDD |
sdio.pio | 200 | Montagem PIO + auxiliares de inicialização SDK C |
led.h | 37 | Auxiliares de LED (GP16–GP19, ativo baixo) |
usb_descriptors.c | 77 | Descritores de dispositivo/configuração/string USB |
tusb_config.h | 20 | Configuração TinyUSB (MSC, buffer EP de 32KB) |
| Version | Read | Write | Key Change |
|---|
| v0.1–v0.3 | 105 kB/s | 93 kB/s | SDIO bit-bang, CRC16, lógica de retry |
| v0.5 | 374 kB/s | — | PIO SM único, troca direta de memória de instrução |
| v0.6 | 583 kB/s | 93 kB/s | Leituras CMD53 multi-bloco, correção de dreno de clock CRC |
| v0.8 | 588 kB/s | 274 kB/s | Escritas PIO, correção de liberação OSR |
| v0.9 | 475 kB/s | 371 kB/s | Blocos de 64 setores, verificação de leitura CRC16 |
| v0.10 | 453 kB/s | 329 kB/s | Remapeamento de LED, pino HDD EN, UART em GP12/GP13 |
| v0.11 | ~450 kB/s | ~340 kB/s | Corte de energia HDD, ativação PIO, sense de setor ruim, suspensão USB |
| v0.12 | ~985 kB/s | ~920 kB/s | Sobreposição Drive/USB (pré-busca de leitura + cache de escrita anunciado com escrita posterior), blocos PIO em pipeline, DMA bswap, loops PIO de 4 ciclos, semântica de setor ruim estilo SBC (reparo por escrita), driver MSC TinyUSB fornecido |