Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
MK4001MTD-USB-Bridge | Kitploit
Tools/GitHubGitHub/will127534/mk4001mtd-usb-bridge
Embedded Systems SecurityReverse EngineeringData RecoveryHardware HackingHardware SecurityHardware & IoT SecurityFirmware Analysis
GitHubwill127534/mk4001mtd-usb-bridge

MK4001MTD-USB-Bridge

View Repository
42121 month agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

MK4001MTD USB Bridge

RP2040 Pico firmware that bridges a Toshiba MK4001MTD 0.85" SDIO microdrive as a USB Mass Storage device. _DSC1170 _DSC1354

The MK4001MTD is a 4 GB Microdrive originally used in the Nokia N91 music phone and some other devices, like MP3 players or USB drives, back when flash storage was still quite expensive.

You might have seen introductions claiming this drive uses the MMC protocol, but that’s actually incorrect. I’ve been investigating this for a while: I tried building an 8-bit MMCplus card reader and tested different SD/MMC readers without success. As a last resort, I bought a Nokia N91 to capture logic traces and confirm what protocol it actually uses.

Here is the photo when I was trying to use it with my 8bit-MMCPlus reader board, and it turns out it is not MMC :( _DSC0484

So I end up getting N91 to collect traces: _DSC1093 _DSC1131

Unlike standard ATA/CF Microdrives, it uses an SDIO interface with ATA commands tunneled through CMD52/CMD53. No existing driver supports this protocol, so this firmware implements the full stack from scratch.

This surprised me, because there is an SDIO-to-ATA standard called CE-ATA. But if you look closely at the release timeline, CE-ATA came later than this drive. As a result, this drive relies entirely on SDIO commands, and CE-ATA is not available. CE-ATA has two new cmd CMD60/CMD61 and utilize CMD12/39, but you can see from the traces it is not using any of those.

The second hardware point to mention is that another piece of misinformation floating around—claiming it’s an 8-bit MMCPlus card—is not only untrue, but the pinout doesn’t follow the MMC standard either. You can find the Nokia N91 service manual with some documentation on the pinout: while the pin numbering follows the MMCPlus standard, the pin mapping does not. This is an important detail if you’re wiring it yourself: it uses the same MMC connector, but the pin mapping is different, more in the Hardware section.

Finally, note that this is co-developed with Claude/OpenClaw. I collected the logic traces manually and set up a closed-loop test station for OpenClaw to iterate on development—analyzing the traces and implementing features. The documentation will mostly be written by Claude; I’ll add my notes inline as well. I’ve also read and double-checked the documentation myself, and it should be reliable and easy to follow.

For insights into the analytics on N91 trace, it is under /docs/N91_TRACE_ANALYSIS.md, I've also put the N91 service manual there along with the raw logic traces.

See more in the blog post here: https://www.willwhang.dev/Reading-MK4001MTD/
See it in activity here: https://youtu.be/GC4xil3_Bbc

Status

Fully functional USB mass storage with PIO-accelerated reads/writes and idle power management.

How It Works

Architecture

root@kitploit:~
USB Host ←→ USB MSC (TinyUSB) ←→ ATA Layer ←→ SDIO Layer (PIO) ←→ MK4001MTD

The firmware has four layers:

  1. USB MSC (msc_device.c) — TinyUSB Mass Storage Class. Translates SCSI READ(10)/WRITE(10) into ATA sector operations. 32 KB EP buffer, batching up to 64 sectors per USB transfer. Drive I/O is overlapped with USB in both directions, like a real ATA-USB bridge with a caching disk: a sequential-read prefetcher fetches the next chunk while the previous one streams to the host, and writes are staged and flushed while USB receives the next piece. The device advertises its write cache (Caching mode page, WCE=1 — hosts report "Write cache: enabled" and issue SYNCHRONIZE CACHE at fsync/unmount/suspend, which the firmware honors). A failed background flush surfaces as MEDIUM ERROR on the next WRITE or SYNCHRONIZE CACHE; writes to known-bad sectors take a strict synchronous path.

  2. ATA-over-SDIO (ata_sdio.c) — Implements ATA commands (IDENTIFY, READ SECTORS, WRITE SECTORS) by writing to ATA registers mapped into SDIO function 1 address space via CMD52, and transferring sector data via CMD53. 3-tier retry logic at CMD, data, and ATA levels.

  3. PIO SDIO (sdio_pio.c, sdio.pio) — Hardware-accelerated SDIO using RP2040's PIO peripheral (4-bit bus at 10 MHz, 4 PIO cycles per bit with input synchronizers bypassed). Three PIO programs share a single state machine via dynamic program swapping:

    • CMD tx/rx (24 instructions) — sends SDIO commands and receives responses
    • DAT read (12 instructions) — reads data blocks from 4-bit DAT bus via byte-swapping DMA (no CPU repack); block N's CRC verifies while block N+1 streams
    • DAT write (14 instructions) — writes data blocks to 4-bit DAT bus via DMA, with built-in CRC status reception and busy-wait; block N+1's nibble stream builds while block N transfers
  4. Pin/Power (sdio_hw.c) — GPIO initialization and HDD power control. All SDIO communication uses PIO.

Human notes: Interestingly, Claude was really reluctant to implement SDIO in PIO, and a lot of development cycles were wasted bouncing back and forth between PIO and bit-banging.

The SDIO-ATA Protocol

The MK4001MTD presents itself as an SDIO card with one I/O function. Standard SDIO card initialization (CMD5/CMD3/CMD7) sets up the bus, then ATA registers are accessed through SDIO commands:

Register access (CMD52): Each ATA register is mapped to a function 1 address:

Data transfer (CMD53): Sector data is transferred by issuing CMD53 in block mode targeting the DATA register (address 0x00). For multi-sector reads, a single CMD53 with block_count=N transfers N × 512 bytes in one SDIO multi-block transaction.

Interrupt signaling: The drive signals sector readiness by asserting an SDIO interrupt (INT_PENDING bit 1 in CCCR register 0x05). Reading the ATA STATUS register clears the interrupt.

Read Path (Multi-Block PIO)

For a 16-sector read:

root@kitploit:~
1. Write ATA registers via PIO CMD52:
     SECCOUNT=16, LBA_LO/MID/HI, DEV/HEAD=0xE0, CMD=0x20

2. Poll STATUS via CMD52 until DRQ (bit 3) is set

3. Swap PIO to DAT read program
4. Send CMD53: block_mode=1, fn=1, addr=0x0000, block_count=16

5. PIO DAT read: for each of the 16 blocks:
   a. Wait for start bit (all DAT lines low)
   b. DMA 1024 nibbles (512 bytes) from PIO RX FIFO to buffer
   c. Wait for SM to finish clocking CRC+end nibbles (poll SM PC)
   d. Repack nibbles → bytes in-place

6. Swap PIO back to CMD program

Write Path (Multi-Block PIO)

For a 16-sector write:

root@kitploit:~
1. Write ATA registers via PIO CMD52:
     SECCOUNT=16, LBA, DEV/HEAD=0xE0, CMD=0x30

2. Poll STATUS via CMD52 until DRQ (bit 3) is set
   (STATUS 0xD8 = BSY+DRQ treated as DRQ-ready, per N91 trace)

3. Swap PIO to DAT write program
4. Send CMD53: block_mode=1, fn=1, addr=0x0000, block_count=16

5. PIO DAT write: for each of the 16 blocks:
   a. Precompute CRC16-CCITT per DAT line (4 independent CRCs)
   b. Build nibble stream: start(0x0) + data(1024 nibbles) + CRC(16) + end(0xF)
   c. DMA nibble stream to PIO TX FIFO
   d. PIO clocks out all nibbles, then:
      - Switches DAT to input
      - Clocks 16 cycles for CRC status from card
      - Polls DAT0 until card releases busy
      - Fires IRQ 0 to signal block completion

6. Swap PIO back to CMD program

PIO Program Swapping

The RP2040 PIO has 32 instruction slots per block. Our three programs total 55 instructions, so they can't coexist. Instead, a single SM0 on PIO0 is used, and programs are swapped by writing directly to PIO instruction memory:

root@kitploit:~
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];
}

This bypasses the SDK's pio_add_program/pio_remove_program allocator. Program swap takes ~1 µs. Each swap is followed by a program-specific reinit that sets pin mappings, shift direction, and clock divider.

Power Management

Analysis of Nokia N91 logic traces reveals aggressive power management:

  • Idle mode: STANDBY IMMEDIATE (0xE0) every ~7.5 seconds, even with no I/O. Each standby triggers full SDIO re-initialization (CMD5 retry → CMD3 → CMD7 → CCCR setup). 28 standby cycles observed in one idle session.
  • Active mode: Standby issued between bursts of I/O (24 cycles during USB drive file operations).
  • No other power commands (IDLE, SLEEP, CHECK POWER MODE) or CCCR power register accesses observed.

The firmware replicates this behavior with a configurable idle timeout:

root@kitploit:~
#define IDLE_STANDBY_MS 5000  // in main.c

Two paths trigger HDD power gating:

  1. Idle timeout (5 seconds) — main loop detects no I/O activity
  2. USB suspend — host suspends the USB port

Both paths send ATA STANDBY IMMEDIATE (0xE0) to flush the write cache and park heads, then cut power via GP9.

Wake sequence (triggered by first READ/WRITE after gate):

  1. Power on HDD, wait 500ms for rail settling
  2. PIO-based SDIO reinit: CMD5 (OCR) → 10ms settle → CMD3 (RCA) → CMD7 (select)
  3. Switch to fast PIO clock, configure CCCR via CMD52 (4-bit bus, 512B blocks, fn1 enable)
  4. Poll fn1 ready (CCCR IO_READY bit 1)
  5. N91-style 30ms DRDY status poll until drive is ATA-ready

Bad Sector Handling

When a multi-sector transfer hits a bad sector:

  1. Chunk read fails → error recovery (IO_ABORT + fn1 reset, ~500ms)
  2. Falls back to per-sector I/O to identify the failing block precisely
  3. The ATA layer waits long enough to capture the final STATUS/ERROR bits instead of collapsing the failure into a generic DRQ timeout
  4. Any unrecovered block fails the SCSI command immediately with MEDIUM ERROR (read: 03/11/00, write: 03/0C/00)
  5. Bad-sector LBA cached → repeat reads fail quickly without re-hammering the drive (anti-hammer protection against host retry storms)
  6. Writes always touch the medium, per SBC — a successful write to a cached-bad LBA clears it from the cache, exactly like a drive clearing a pending sector

Point 6 is not academic: this drive had a long-standing unreadable sector at LBA 1952 (READ: ST=0x51 ERR ERR=0x40 UNC). Once the bridge allowed a write to actually reach it, the drive rewrote the sector and it has read back cleanly since:

root@kitploit:~
[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

Building

Prerequisites

  • Raspberry Pi Pico SDK — stock, unmodified, pinned to 2.2.0
  • ARM toolchain (arm-none-eabi-gcc)
  • CMake

The SDK version is locked: if PICO_SDK_PATH is set (environment or CMake variable) it is used and its version is checked against the pin — a mismatch fails the configure with instructions (override with -DMK4001_ALLOW_SDK_MISMATCH=ON). With no PICO_SDK_PATH at all, the pinned SDK release is fetched from GitHub automatically at configure time, so a bare git clone && cmake && make is fully reproducible.

The firmware needs a patched TinyUSB MSC class driver (app sense data preserved on read/write errors + a Caching mode page with WCE=1). That file is vendored in this repo at lib/tinyusb_patched/msc_device.c — the build automatically compiles it instead of the SDK's copy, so no SDK surgery is ever needed. The diff against upstream TinyUSB (0.18.0, as bundled with pico-sdk 2.2.0) is in lib/tinyusb_patched/; the SDK pin exists precisely because this vendored file must track the SDK's TinyUSB.

Build & Flash

root@kitploit:~
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"

Hardware Wiring

Note: GP0 and GP1 are dead on this specific Pico unit. All SDIO pin assignments are shifted +2.

Human notes: Claude was wrong here because it didn’t realize that GP0 and GP1 were being used for the UART terminal in its build configuration. It kept forgetting this, to the point where I just moved the SDIO GPIOs off that UART.

HDD_PWR isn’t necessary. You don’t have to power-cycle the drive to use it; it’s more of a development convenience for resetting the HDD when a lot of things are hard-coded. That said, if you want power savings, you can use that signal, but it can handle warm reset without any issue.

You’ll see debug messages over UART. They’re not going through USB-CDC because it was easier for Claude to set up a separate UART-to-USB logging link that doesn’t disconnect or become unstable during early development.

The UART log also reports the drive temperature every 30 seconds while the drive is active ([TEMP] drive temperature: 29 C). The sensor was discovered by reverse-engineering the Toshiba vendor command 0xC2 — the N91 reads it at the start of every drive session to enforce its HDD operating-temperature limits. Details in docs/N91_TRACE_ANALYSIS.md §4.

Here is a example of the log:

root@kitploit:~
========================================
  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

Finally, here’s the wiring to the actual drive.
_DSC1176-2 Here is a crop from the N91 schematic, you can map the pin number also. image

Side note that this is a 3V drive but I think 3.3V is fine, mostly it is to save some level shifting work.

HW specifically designed for this drive is under /hardware! image

Source Files

Version History

Testing

root@kitploit:~
# Check device appeared
lsblk -dno NAME,MODEL | grep MK4001

# Filesystem test — mount, copy files, verify
sudo mount /dev/sdX1 /mnt/mk4001
cp /tmp/testfile /mnt/mk4001/
sync
md5sum /tmp/testfile /mnt/mk4001/testfile    # should match
sudo umount /mnt/mk4001

# Speed benchmarks (raw device, do NOT mount first — will corrupt filesystem)
# Use a safe offset past the filesystem or an unpartitioned drive
sudo dd if=/dev/sdX of=/dev/null bs=64k count=128 iflag=direct     # read
sudo dd if=/dev/zero of=/dev/sdX bs=64k count=64 oflag=direct seek=1024  # write (offset past FS)

Human notes here, fun fact: When it first starts to do speed testing, it actually dd directly to the drive and damage the filesystems..... Thankfully it doesn't matter that much here during the developments but always keep in mind when you handle OpenClaw your setup.

License

I don't care.

Download Tool
MetricValue
Read speed~985 kB/s (USB full-speed limited)
Write speed~920 kB/s (USB full-speed limited, advertised write cache)
Raw SDIO-side speed~2.35 MB/s read / ~2.15 MB/s write (drive-limited)
Capacity3.75 GB (7,862,400 sectors)
FilesystemFAT32 verified (mount/unmount/fsck clean)
Data integrityWrite+readback verified; per-block CRC16 on all 4 DAT lines
Idle standby5 s idle or USB suspend → STANDBY IMMEDIATE + power gate
AddressRegisterUsage
0x00DATACMD53 target for sector data
0x01ERR/FEATError (read) / Feature (write)
0x02SECCOUNTSector count
0x03LBA_LOLBA bits 0-7
0x04LBA_MIDLBA bits 8-15
0x05LBA_HILBA bits 16-23
0x06DEV/HEADDevice/Head + LBA bits 24-27
0x07CMD/STATUSCommand (write) / Status (read)
Pico GPIOFunctionNotes
GP2SDIO_CLKHost clock output
GP3SDIO_CMDBidirectional command line
GP4SDIO_DAT0Data bit 0
GP5SDIO_DAT1Data bit 1
GP6SDIO_DAT2Data bit 2
GP7SDIO_DAT3Data bit 3
GP9HDD_ENDrive power enable (HIGH=on)
GP12UART TXDebug output @ 115200
GP13UART RXDebug input
GP16LED: HDD PowerActive low
GP17LED: HDD HealthyActive low
GP18LED: ReadActive low
GP19LED: WriteActive low
FileLinesPurpose
main.c210Init, idle standby, USB suspend/resume
msc_device.c400USB MSC callbacks, power gate wake, bad sector cache
ata_sdio.c390ATA commands, error recovery, vendor diagnostics
sdio_pio.c635PIO SDIO: CMD52, CMD53 read/write, program swap, CRC16
sdio_hw.c45Pin init + HDD power control
sdio.pio200PIO assembly + C SDK init helpers
led.h37LED helpers (GP16–GP19, active low)
usb_descriptors.c77USB device/config/string descriptors
tusb_config.h20TinyUSB config (MSC, 32KB EP buffer)
VersionReadWriteKey Change
v0.1–v0.3105 kB/s93 kB/sBit-bang SDIO, CRC16, retry logic
v0.5374 kB/s—Single-SM PIO, direct instruction memory swap
v0.6583 kB/s93 kB/sMulti-block CMD53 reads, CRC clock drain fix
v0.8588 kB/s274 kB/sPIO writes, OSR flush fix
v0.9475 kB/s371 kB/s64-sector chunks, CRC16 read verification
v0.10453 kB/s329 kB/sLED remap, HDD EN pin, UART on GP12/GP13
v0.11~450 kB/s~340 kB/sHDD power gate, PIO wake, bad sector sense, USB suspend
v0.12~985 kB/s~920 kB/sDrive/USB overlap (read prefetch + advertised write cache with write-behind), pipelined PIO blocks, bswap DMA, 4-cycle PIO loops, SBC-style bad-sector semantics (write-repair), vendored TinyUSB MSC driver