
Прошивка RP2040, которая превращает SDIO микронакопитель Toshiba MK4001MTD 0.85" в USB-устройство массового хранения, реализуя полный стек протоколов SDIO-ATA с нуля с ускоренными PIO чтением/записью и восстановлением битых секторов.
Прошивка для RP2040 Pico, которая подключает Toshiba MK4001MTD 0,85" SDIO микродиск как USB-накопитель.

MK4001MTD — это 4-гигабайтный микродиск, изначально использовавшийся в музыкальном телефоне Nokia N91 и других устройствах, таких как MP3-плееры или USB-флешки, в те времена, когда флеш-память была еще довольно дорогой.
Возможно, вы видели утверждения, что этот накопитель использует протокол MMC, но это на самом деле неверно. Я некоторое время изучал этот вопрос: пытался построить 8-битный кардридер MMCplus и тестировал различные SD/MMC-ридеры безрезультатно. В качестве последней меры я купил Nokia N91, чтобы захватить логические сигналы и подтвердить, какой протокол он на самом деле использует.
Вот фото, когда я пытался использовать его с моей платой 8bit-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, но назначение контактов другое, подробнее в разделе Hardware.
Наконец, отмечу, что этот проект разрабатывался совместно с Claude/OpenClaw. Я вручную собирал логические сигналы и настроил замкнутый испытательный стенд для OpenClaw, чтобы он мог итеративно разрабатывать — анализировать сигналы и реализовывать функции. Документацию в основном пишет Claude; я также добавлю свои заметки прямо в текст. Я прочитал и перепроверил документацию самостоятельно, она должна быть надежной и легко читаемой.
Для анализа сигналов N91 смотрите /docs/N91_TRACE_ANALYSIS.md; я также поместил туда сервисное руководство N91 вместе с сырыми логическими сигналами.
Подробнее в блоге: https://www.willwhang.dev/Reading-MK4001MTD/
Посмотрите в работе: https://youtu.be/GC4xil3_Bbc
Полностью функциональный USB-накопитель массового хранения с ускоренным PIO чтением/записью и управлением питанием в простое.
USB Host ←→ USB MSC (TinyUSB) ←→ ATA Layer ←→ SDIO Layer (PIO) ←→ MK4001MTD
Прошивка состоит из четырех слоев:
USB MSC (msc_device.c) — TinyUSB Mass Storage Class. Преобразует SCSI READ(10)/WRITE(10) в операции с секторами ATA. Буфер EP 32 КБ, пакетирование до 64 секторов за одну передачу USB. Ввод/вывод накопителя перекрывается с USB в обоих направлениях, как в реальном мосту ATA-USB с кэширующим диском: последовательный предварительный выборщик чтения подгружает следующий фрагмент, пока предыдущий передается на хост, а запись ставится в очередь и сбрасывается, пока USB принимает следующую часть. Устройство рекламирует свой кэш записи (Caching mode page, WCE=1 — хосты сообщают «Write cache: enabled» и выдают SYNCHRONIZE CACHE при fsync/размонтировании/приостановке, что прошивка соблюдает). Неудачный фондрайн сбрасывания отображается как MEDIUM ERROR при следующей операции WRITE или SYNCHRONIZE CACHE; запись в заведомо плохие сектора идет строго синхронным путем.
ATA-поверх-SDIO (ata_sdio.c) — Реализует команды ATA (IDENTIFY, READ SECTORS, WRITE SECTORS) путем записи в регистры ATA, отображенные в адресное пространство функции 1 SDIO через CMD52, и передачи данных секторов через CMD53. 3-уровневая логика повторов на уровнях CMD, данных и ATA.
PIO SDIO (sdio_pio.c, sdio.pio) — Аппаратно-ускоренный SDIO с использованием периферии PIO RP2040 (4-битная шина на 10 МГц, 4 цикла PIO на бит с отключенными синхронизаторами входа). Три программы PIO совместно используют одну конечную машину через динамическую замену программ:
Пин/Питание (sdio_hw.c) — Инициализация GPIO и управление питанием HDD. Вся связь SDIO осуществляется через PIO.
Заметки человека: Интересно, что Claude очень не хотел реализовывать SDIO на PIO, и многие циклы разработки были потрачены на метания между PIO и бит-бэнгом.
MK4001MTD представляется как SDIO-карта с одной функцией ввода/вывода. Стандартная инициализация SDIO-карты (CMD5/CMD3/CMD7) настраивает шину, затем регистры ATA доступны через команды SDIO:
Доступ к регистрам (CMD52): Каждый регистр ATA отображается на адрес функции 1:
Передача данных (CMD53): Данные секторов передаются путем выдачи CMD53 в блочном режиме с целью регистра DATA (адрес 0x00). Для многосекторного чтения один CMD53 с block_count=N передает N × 512 байт за одну мультиблочную транзакцию SDIO.
Сигнализация прерывания: Накопитель сигнализирует о готовности сектора, устанавливая прерывание SDIO (бит INT_PENDING 1 в регистре CCCR 0x05). Чтение регистра STATUS ATA сбрасывает прерывание.
Для чтения 16 секторов:
1. Запись регистров ATA через PIO CMD52:
SECCOUNT=16, LBA_LO/MID/HI, DEV/HEAD=0xE0, CMD=0x20
2. Опрос STATUS через CMD52 до установки бита DRQ (бит 3)
3. Переключение PIO на программу чтения DAT
4. Отправка CMD53: block_mode=1, fn=1, addr=0x0000, block_count=16
5. PIO чтение DAT: для каждого из 16 блоков:
a. Ожидание стартового бита (все линии DAT низкий уровень)
b. DMA 1024 полубайт (512 байт) из PIO RX FIFO в буфер
c. Ожидание завершения тактирования CRC+конечных полубайт конечной машиной (опрос PC конечной машины)
d. Переупаковка полубайт → байты на месте
6. Переключение PIO обратно на программу CMD
Для записи 16 секторов:
1. Запись регистров ATA через PIO CMD52:
SECCOUNT=16, LBA, DEV/HEAD=0xE0, CMD=0x30
2. Опрос STATUS через CMD52 до установки бита DRQ (бит 3)
(STATUS 0xD8 = BSY+DRQ рассматривается как готовность DRQ, согласно трассе N91)
3. Переключение PIO на программу записи DAT
4. Отправка CMD53: block_mode=1, fn=1, addr=0x0000, block_count=16
5. PIO запись DAT: для каждого из 16 блоков:
a. Предварительное вычисление CRC16-CCITT для каждой линии DAT (4 независимых CRC)
b. Формирование потока полубайт: start(0x0) + data(1024 полубайта) + CRC(16) + end(0xF)
c. DMA потока полубайт в PIO TX FIFO
d. PIO тактирует все полубайты, затем:
- Переключает DAT на ввод
- Тактирует 16 циклов для получения статуса CRC от карты
- Ожидает, пока DAT0 не отпустит занятость
- Запускает IRQ 0 для сигнализации завершения блока
6. Переключение PIO обратно на программу CMD
PIO RP2040 имеет 32 слота инструкций на блок. Наши три программы в сумме 55 инструкций, поэтому они не могут существовать одновременно. Вместо этого используется одна SM0 на PIO0, а программы переключаются записью непосредственно в память инструкций 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];
}
Это обходит аллокатор pio_add_program/pio_remove_program SDK. Переключение программы занимает ~1 мкс. После каждого переключения выполняется повторная инициализация, специфичная для программы, с установкой назначения пинов, направления сдвига и делителя тактовой частоты.
Анализ логических сигналов Nokia N91 показывает агрессивное управление питанием:
Прошивка повторяет это поведение с настраиваемым тайм-аутом простоя:
#define IDLE_STANDBY_MS 5000 // в main.c
Два пути запускают отключение питания HDD:
Оба пути отправляют ATA STANDBY IMMEDIATE (0xE0) для сброса кэша записи и парковки головок, затем отключают питание через GP9.
Последовательность пробуждения (запускается первой операцией READ/WRITE после отключения):
При сбое многосекторной передачи на плохом секторе:
Пункт 6 не теоретический: этот накопитель имел давний нечитаемый сектор на LBA 1952 (READ: ST=0x51 ERR ERR=0x40 UNC). Как только мост позволил запись до него, накопитель перезаписал сектор, и после этого он читается чисто:
[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)Версия SDK заблокирована: если PICO_SDK_PATH задан (переменная окружения или CMake), он используется, и его версия проверяется на соответствие фиксации — несоответствие вызывает ошибку настройки с инструкциями (можно переопределить с помощью -DMK4001_ALLOW_SDK_MISMATCH=ON). Если PICO_SDK_PATH вообще не задан, фиксированная версия SDK загружается из GitHub автоматически во время настройки, поэтому простая последовательность git clone && cmake && make полностью воспроизводима.
Прошивке требуется пропатченный драйвер класса MSC TinyUSB (данные дополнительного смысла сохраняются при ошибках чтения/записи + страница режима кэширования с WCE=1). Этот файл включен в данный репозиторий по пути lib/tinyusb_patched/msc_device.c — при сборке автоматически компилируется он, а не копия из SDK, поэтому никаких правок SDK не требуется. Разница с upstream TinyUSB (0.18.0, входящий в pico-sdk 2.2.0) находится в lib/tinyusb_patched/; фиксация SDK существует именно потому, что этот вендорный файл должен отслеживать TinyUSB из 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"
Примечание: GP0 и GP1 мертвы на этом конкретном экземпляре Pico. Все назначения пинов SDIO сдвинуты на +2.
Заметки человека: Claude здесь ошибся, потому что не понял, что GP0 и GP1 используются для терминала UART в его конфигурации сборки. Он постоянно это забывал, до такой степени, что я просто перенес SDIO GPIO с этого UART.
HDD_PWR не обязателен. Вам не нужно перезапускать питание накопителя для его использования; это скорее удобство для разработки при сбросе HDD, когда многое жестко закодировано. Тем не менее, если вы хотите экономить энергию, вы можете использовать этот сигнал, но накопитель отлично выдерживает горячий сброс без отключения питания.
Вы увидите отладочные сообщения через UART. Они идут не через USB-CDC, потому что для Клода было проще настроить отдельную UART-USB линию для логирования, которая не отключалась бы и не становилась нестабильной на ранних этапах разработки.
Лог UART также сообщает температуру накопителя каждые 30 секунд, пока накопитель активен ([TEMP] drive temperature: 29 C). Датчик был обнаружен при обратной разработке заводской команды Toshiba 0xC2 — N91 считывает её при каждом сеансе работы с накопителем для соблюдения ограничений рабочей температуры HDD. Подробности в docs/N91_TRACE_ANALYSIS.md §4.
Пример лога:
========================================
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
Наконец, вот подключение к самому накопителю.
Вот фрагмент схемы N91, по нему можно сопоставить номера контактов.

Попутное замечание: это 3-вольтовый накопитель, но я думаю, 3,3 В тоже подойдет, в основном чтобы сэкономить на преобразовании уровней.
Аппаратная часть, специально разработанная для этого накопителя, находится в папке /hardware!

# Проверка появления устройства
lsblk -dno NAME,MODEL | grep MK4001
# Тест файловой системы — монтирование, копирование файлов, проверка
sudo mount /dev/sdX1 /mnt/mk4001
cp /tmp/testfile /mnt/mk4001/
sync
md5sum /tmp/testfile /mnt/mk4001/testfile # должно совпасть
sudo umount /mnt/mk4001
# Тесты скорости (сырое устройство, НЕ монтируйте сначала — испортит файловую систему)
# Используйте безопасное смещение за пределами файловой системы или неразмеченный накопитель
sudo dd if=/dev/sdX of=/dev/null bs=64k count=128 iflag=direct # чтение
sudo dd if=/dev/zero of=/dev/sdX bs=64k count=64 oflag=direct seek=1024 # запись (смещение за ФС)
Заметка человека, забавный факт: когда он впервые начал тестирование скорости, он напрямую сделал dd на накопитель и повредил файловые системы... К счастью, во время разработки это не имело большого значения, но всегда имейте в виду, когда будете настраивать стенд вместе с OpenClaw.
Мне всё равно.
| Метрика | Значение |
|---|
| Скорость чтения | ~985 кБ/с (ограничение USB full-speed) |
| Скорость записи | ~920 кБ/с (ограничение USB full-speed, рекламируется кэш записи) |
| Сырая скорость на стороне SDIO | ~2,35 МБ/с чтение / ~2,15 МБ/с запись (ограничено накопителем) |
| Емкость | 3,75 ГБ (7 862 400 секторов) |
| Файловая система | FAT32 проверена (mount/unmount/fsck чисто) |
| Целостность данных | Проверено чтение+обратное чтение; для всех 4 линий DAT используется CRC16 на блок |
| Ожидание в простое | 5 с бездействия или USB suspend → STANDBY IMMEDIATE + отключение питания |
| Адрес | Регистр | Использование |
|---|
| 0x00 | DATA | Цель CMD53 для данных секторов |
| 0x01 | ERR/FEAT | Ошибка (чтение) / Feature (запись) |
| 0x02 | SECCOUNT | Количество секторов |
| 0x03 | LBA_LO | LBA биты 0-7 |
| 0x04 | LBA_MID | LBA биты 8-15 |
| 0x05 | LBA_HI | LBA биты 16-23 |
| 0x06 | DEV/HEAD | Device/Head + LBA биты 24-27 |
| 0x07 | CMD/STATUS | Команда (запись) / Статус (чтение) |
| Pico GPIO | Функция | Примечания |
|---|
| GP2 | SDIO_CLK | Выход тактового сигнала хоста |
| GP3 | SDIO_CMD | Двунаправленная линия команд |
| GP4 | SDIO_DAT0 | Бит данных 0 |
| GP5 | SDIO_DAT1 | Бит данных 1 |
| GP6 | SDIO_DAT2 | Бит данных 2 |
| GP7 | SDIO_DAT3 | Бит данных 3 |
| GP9 | HDD_EN | Включение питания накопителя (HIGH=вкл) |
| GP12 | UART TX | Вывод отладки @ 115200 |
| GP13 | UART RX | Ввод отладки |
| GP16 | LED: Питание HDD | Активный низкий уровень |
| GP17 | LED: HDD исправен | Активный низкий уровень |
| GP18 | LED: Чтение | Активный низкий уровень |
| GP19 | LED: Запись | Активный низкий уровень |
| Файл | Строк | Назначение |
|---|
main.c | 210 | Инициализация, ожидание в простое, USB приостановка/возобновление |
msc_device.c | 400 | Обратные вызовы USB MSC, пробуждение от отключения питания, кэш плохих секторов |
ata_sdio.c | 390 | Команды ATA, восстановление после ошибок, диагностика производителя |
sdio_pio.c | 635 | PIO SDIO: CMD52, CMD53 чтение/запись, переключение программ, CRC16 |
sdio_hw.c | 45 | Инициализация пинов + управление питанием HDD |
sdio.pio | 200 | Ассемблер PIO + вспомогательные функции инициализации C SDK |
led.h | 37 | Вспомогательные функции для светодиодов (GP16–GP19, активный низкий уровень) |
usb_descriptors.c | 77 | Дескрипторы USB устройства/конфигурации/строк |
tusb_config.h | 20 | Конфигурация TinyUSB (MSC, буфер EP 32 КБ) |
| Версия | Чтение | Запись | Ключевое изменение |
|---|
| v0.1–v0.3 | 105 кБ/с | 93 кБ/с | Бит-бэнг SDIO, CRC16, логика повторов |
| v0.5 | 374 кБ/с | — | Один SM PIO, прямая замена памяти инструкций |
| v0.6 | 583 кБ/с | 93 кБ/с | Мультиблочные CMD53 чтения, исправление слива тактов CRC |
| v0.8 | 588 кБ/с | 274 кБ/с | PIO запись, исправление сброса OSR |
| v0.9 | 475 кБ/с | 371 кБ/с | Чанки по 64 сектора, верификация чтения CRC16 |
| v0.10 | 453 кБ/с | 329 кБ/с | Переназначение светодиодов, пин HDD EN, UART на GP12/GP13 |
| v0.11 | ~450 кБ/с | ~340 кБ/с | Управление питанием HDD, пробуждение PIO, датчик плохих секторов, USB приостановка |
| v0.12 | ~985 кБ/с | ~920 кБ/с | Перекрытие накопителя/USB (предвыборка чтения + рекламируемый кэш записи с отложенной записью), конвейерная обработка блоков PIO, DMA с перестановкой байт, циклы PIO по 4 такта, семантика плохих секторов в стиле SBC (восстановление записью), вендорный драйвер MSC TinyUSB |