
RP2040 펌웨어로, 도시바 MK4001MTD 0.85인치 SDIO 마이크로드라이브를 USB 대용량 저장 장치로 브리징하며, PIO 가속 읽기/쓰기 및 불량 섹터 복구 기능을 갖춘 전체 SDIO-ATA 프로토콜 스택을 처음부터 구현합니다.
Toshiba MK4001MTD 0.85인치 SDIO 마이크로드라이브를 USB 대용량 저장 장치로 연결하는 RP2040 Pico 펌웨어.

MK4001MTD는 4GB 마이크로드라이브로, 원래 Nokia N91 뮤직폰 및 일부 MP3 플레이어나 USB 드라이브 같은 기기에 사용되었으며, 당시 플래시 저장 장치는 꽤 비쌌습니다.
이 드라이브가 MMC 프로토콜을 사용한다는 소개를 본 적이 있을 수 있지만, 이는 사실 잘못된 정보입니다. 저는 이 문제를 한동안 조사해 왔습니다. 8비트 MMCplus 카드 리더기를 만들어 보기도 하고 다양한 SD/MMC 리더기를 테스트해 보았지만 성공하지 못했습니다. 최후의 수단으로 Nokia N91을 구입하여 로직 트레이스를 캡처하고 실제로 어떤 프로토콜을 사용하는지 확인했습니다.
아래는 제 8비트 MMCPlus 리더 보드와 함께 사용하려고 시도했을 때의 사진입니다. 결과적으로 MMC가 아니었습니다 :(

그래서 결국 N91을 구입하여 트레이스를 수집했습니다:

표준 ATA/CF 마이크로드라이브와 달리, SDIO 인터페이스를 사용하며 ATA 명령이 CMD52/CMD53을 통해 터널링됩니다. 이 프로토콜을 지원하는 기존 드라이버가 없기 때문에, 이 펌웨어는 처음부터 전체 스택을 구현합니다.
이것은 저에게 놀라운 점이었습니다. SDIO-to-ATA 표준인 CE-ATA가 존재하기 때문입니다. 하지만 출시 타임라인을 자세히 살펴보면, CE-ATA는 이 드라이브보다 나중에 나왔습니다. 결과적으로 이 드라이브는 전적으로 SDIO 명령에 의존하며 CE-ATA는 사용할 수 없습니다. CE-ATA는 두 개의 새로운 명령 CMD60/CMD61과 CMD12/39를 사용하지만, 트레이스에서 볼 수 있듯이 이 드라이브는 이러한 명령을 전혀 사용하지 않습니다.
두 번째 하드웨어 관련 사항은, 돌아다니는 또 다른 잘못된 정보—즉, 이 드라이브가 8비트 MMCPlus 카드라는 주장—이 사실이 아닐 뿐만 아니라 핀 배치도 MMC 표준을 따르지 않는다는 점입니다. Nokia N91 서비스 매뉴얼에서 핀 배치에 대한 일부 문서를 찾을 수 있습니다. 핀 번호는 MMCPlus 표준을 따르지만 핀 매핑은 그렇지 않습니다. 직접 배선할 때 중요한 세부 사항입니다. 동일한 MMC 커넥터를 사용하지만 핀 매핑이 다릅니다. 하드웨어 섹션에서 자세히 설명합니다.
마지막으로, 이 프로젝트는 Claude/OpenClaw와 공동 개발되었습니다. 제가 로직 트레이스를 수동으로 수집하고 OpenClaw가 개발을 반복할 수 있도록 폐쇄 루프 테스트 스테이션을 설정했습니다. 트레이스를 분석하고 기능을 구현한 것입니다. 문서는 주로 Claude가 작성할 예정이며, 제가 인라인으로 노트를 추가하겠습니다. 또한 제가 직접 문서를 읽고 다시 확인했으며, 신뢰할 수 있고 따라하기 쉬울 것입니다.
N91 트레이스 분석에 대한 통찰은 /docs/N91_TRACE_ANALYSIS.md에 있습니다. N91 서비스 매뉴얼과 원시 로직 트레이스도 함께 넣었습니다.
블로그 게시물에서 더 자세한 내용을 확인하세요: https://www.willwhang.dev/Reading-MK4001MTD/ 실제 동작 영상: https://youtu.be/GC4xil3_Bbc
PIO 가속 읽기/쓰기 및 아이들 전원 관리 기능을 갖춘 완전한 기능의 USB 대용량 저장 장치.
| 지표 | 값 |
|---|---|
| 읽기 속도 | ~985 kB/s (USB full-speed 제한) |
| 쓰기 속도 | ~920 kB/s (USB full-speed 제한, 광고된 쓰기 캐시) |
| 순수 SDIO 측 속도 | ~2.35 MB/s 읽기 / ~2.15 MB/s 쓰기 (드라이브 제한) |
| 용량 | 3.75 GB (7,862,400 섹터) |
| 파일 시스템 | FAT32 확인됨 (마운트/언마운트/fsck 깨끗함) |
| 데이터 무결성 | 쓰기+읽기백 확인됨; 모든 4개 DAT 라인에 블록별 CRC16 |
| 아이들 대기 | 5초 아이들 또는 USB 서스펜드 → STANDBY IMMEDIATE + 전원 게이트 |
USB 호스트 ←→ USB MSC (TinyUSB) ←→ ATA 레이어 ←→ SDIO 레이어 (PIO) ←→ MK4001MTD
펌웨어는 네 개의 레이어로 구성됩니다:
USB MSC (msc_device.c) — TinyUSB Mass Storage Class. SCSI READ(10)/WRITE(10)를 ATA 섹터 연산으로 변환합니다. 32KB EP 버퍼, USB 전송당 최대 64섹터 일괄 처리. 드라이브 I/O는 양방향으로 USB와 중첩되어, 실제 ATA-USB 브리지와 캐싱 디스크처럼 동작합니다. 순차 읽기 프리페처는 이전 청크가 호스트로 스트리밍되는 동안 다음 청크를 가져오고, 쓰기는 USB가 다음 조각을 수신하는 동안 스테이징되어 플러시됩니다. 장치는 쓰기 캐시(캐싱 모드 페이지, WCE=1 — 호스트는 "쓰기 캐시: 활성화됨"을 보고 fsync/언마운트/서스펜드 시 SYNCHRONIZE CACHE를 발행하며, 펌웨어는 이를 처리합니다)를 광고합니다. 실패한 백그라운드 플러시는 다음 WRITE 또는 SYNCHRONIZE CACHE에서 MEDIUM ERROR로 표시됩니다. 알려진 불량 섹터에 대한 쓰기는 엄격한 동기 경로를 취합니다.
ATA-over-SDIO (ata_sdio.c) — ATA 명령(IDENTIFY, READ SECTORS, WRITE SECTORS)을 구현하며, ATA 레지스터를 CMD52를 통해 SDIO 함수 1 주소 공간에 매핑하고 CMD53을 통해 섹터 데이터를 전송합니다. CMD, 데이터, ATA 레벨에서 3단계 재시도 로직.
PIO SDIO (sdio_pio.c, sdio.pio) — RP2040의 PIO 주변 장치를 사용한 하드웨어 가속 SDIO (4비트 버스, 10MHz, 입력 동기화기 우회로 비트당 4 PIO 사이클). 동적 프로그램 스와핑을 통해 단일 상태 머신을 공유하는 세 가지 PIO 프로그램:
핀/전원 (sdio_hw.c) — GPIO 초기화 및 HDD 전원 제어. 모든 SDIO 통신은 PIO를 사용합니다.
인간 노트: 흥미롭게도 Claude는 PIO에서 SDIO를 구현하는 것을 매우 꺼려했고, PIO와 비트뱅잉 사이를 왔다 갔다 하면서 많은 개발 주기가 낭비되었습니다.
MK4001MTD는 하나의 I/O 함수를 가진 SDIO 카드로 자신을 식별합니다. 표준 SDIO 카드 초기화(CMD5/CMD3/CMD7)가 버스를 설정한 후, ATA 레지스터는 SDIO 명령을 통해 접근됩니다:
레지스터 접근 (CMD52): 각 ATA 레지스터는 함수 1 주소에 매핑됩니다:
| 주소 | 레지스터 | 용도 |
|---|---|---|
| 0x00 | DATA | 섹터 데이터용 CMD53 대상 |
| 0x01 | ERR/FEAT | 오류 (읽기) / 기능 (쓰기) |
| 0x02 | SECCOUNT | 섹터 수 |
| 0x03 | LBA_LO | LBA 비트 0-7 |
| 0x04 | LBA_MID | LBA 비트 8-15 |
| 0x05 | LBA_HI | LBA 비트 16-23 |
| 0x06 | DEV/HEAD | 장치/헤드 + LBA 비트 24-27 |
| 0x07 | CMD/STATUS | 명령 (쓰기) / 상태 (읽기) |
데이터 전송 (CMD53): 섹터 데이터는 DATA 레지스터(주소 0x00)를 대상으로 블록 모드에서 CMD53을 발행하여 전송됩니다. 다중 섹터 읽기의 경우, block_count=N인 단일 CMD53이 한 번의 SDIO 다중 블록 트랜잭션으로 N × 512바이트를 전송합니다.
인터럽트 신호: 드라이브는 SDIO 인터럽트(CCCR 레지스터 0x05의 INT_PENDING 비트 1)를 어설션하여 섹터 준비를 알립니다. ATA STATUS 레지스터를 읽으면 인터럽트가 지워집니다.
16섹터 읽기의 경우:
1. PIO CMD52를 통해 ATA 레지스터 쓰기:
SECCOUNT=16, LBA_LO/MID/HI, DEV/HEAD=0xE0, CMD=0x20
2. CMD52를 통해 STATUS 폴링, DRQ(비트 3)가 설정될 때까지
3. PIO를 DAT 읽기 프로그램으로 교체
4. CMD53 전송: block_mode=1, fn=1, addr=0x0000, block_count=16
5. PIO DAT 읽기: 16개 블록 각각에 대해:
a. 시작 비트(모든 DAT 라인 로우) 대기
b. PIO RX FIFO에서 버퍼로 1024 니블(512바이트) DMA
c. SM이 CRC+종료 니블 클록을 마칠 때까지 대기(SM PC 폴링)
d. 니블 → 바이트 인플레이스 재패킹
6. PIO를 CMD 프로그램으로 교체
16섹터 쓰기의 경우:
1. PIO CMD52를 통해 ATA 레지스터 쓰기:
SECCOUNT=16, LBA, DEV/HEAD=0xE0, CMD=0x30
2. CMD52를 통해 STATUS 폴링, DRQ(비트 3)가 설정될 때까지
(STATUS 0xD8 = BSY+DRQ는 N91 트레이스에 따라 DRQ-준비로 처리)
3. PIO를 DAT 쓰기 프로그램으로 교체
4. CMD53 전송: block_mode=1, fn=1, addr=0x0000, block_count=16
5. PIO DAT 쓰기: 16개 블록 각각에 대해:
a. DAT 라인별로 CRC16-CCITT 사전 계산 (4개의 독립 CRC)
b. 니블 스트림 구축: 시작(0x0) + 데이터(1024 니블) + CRC(16) + 종료(0xF)
c. 니블 스트림을 PIO TX FIFO로 DMA
d. PIO가 모든 니블을 클록 아웃한 후:
- DAT를 입력으로 전환
- 카드의 CRC 상태를 위해 16사이클 클록
- 카드가 busy를 해제할 때까지 DAT0 폴링
- 블록 완료를 알리기 위해 IRQ 0 발생
6. PIO를 CMD 프로그램으로 교체
RP2040 PIO는 블록당 32개의 명령어 슬롯이 있습니다. 세 프로그램은 총 55개의 명령어이므로 공존할 수 없습니다. 대신 PIO0의 단일 SM0를 사용하고, 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];
}
이것은 SDK의 pio_add_program/pio_remove_program 할당자를 우회합니다. 프로그램 교체는 약 1µs가 소요됩니다. 각 교체 후에는 핀 매핑, 시프트 방향 및 클록 분배기를 설정하는 프로그램별 재초기화가 이어집니다.
Nokia N91 로직 트레이스 분석 결과, 공격적인 전원 관리가 드러났습니다:
펌웨어는 구성 가능한 아이들 타임아웃으로 이 동작을 복제합니다:
#define IDLE_STANDBY_MS 5000 // main.c
두 가지 경로가 HDD 전원 게이팅을 트리거합니다:
두 경로 모두 ATA STANDBY IMMEDIATE (0xE0)를 전송하여 쓰기 캐시를 플러시하고 헤드를 주차한 다음 GP9를 통해 전원을 차단합니다.
깨우기 시퀀스 (게이트 후 첫 READ/WRITE에 의해 트리거):