Chapter 66: SD card and eMMC deep dive¶
Lab vs production: Do not burn fuses, enroll production keys, or sign release images while following the lab. Use throwaway keys and back up the unsigned image plus the key directory before testing irreversible security flows.
What: the MMC subsystem that backs both SD cards and eMMC. Speed modes (DS, HS, HS200, HS400, SDR104), the EXT_CSD register (the eMMC’s metadata block), boot partitions, RPMB, wear monitoring, and the “removable card in a production product” antipattern. Three configurations compared: µSD on a card slot, soldered eMMC at HS200, soldered eMMC with secure boot via RPMB.
Why: for most i.MX6ULL products with > 32 MB storage need, the choice is “SD or eMMC.” Picking wrong leads to field failures or wasted engineering effort. EMMC is the right choice for production. SD is fine for dev boards. This chapter is mostly the why behind that statement, plus the bring-up + monitoring details. Compare: removable SD (cheap, accessible, dies first), soldered eMMC HS200 (200 MB/s, rated for 5+ years of continuous service in industrial-grade parts), eMMC with RPMB (~10 % overhead, replay-protected for secure boot).
66.1 SD card vs eMMC, the production reality¶
SD cards fail often, in ways that surprise engineers used to flash chips:
No wear levelling guarantees: The card’s controller does some, but cheap cards (the kind whose price you negotiate down) skimp on it.
Power-loss corruption: A write in progress when power drops can corrupt the entire FAT/ext4 metadata, not just the in-flight sector. EMMC has better resilience (built-in caching with battery backup in some packages), but SD cards are notoriously fragile.
Temperature: Industrial-grade SD exists (-40 to +85 °C) but consumer ones spec 0–70.
Counterfeit: The single biggest reliability issue. A “32 GB SanDisk” purchased on a forum reseller might be a 4 GB chip with the controller faked to report 32 GB. Writes beyond 4 GB silently fail. Even reputable distributors get hit with fakes.
For a dev board or a product where the user expects removable media (a kiosk that takes SD-card content updates), SD is fine. For any product that’s supposed to “just work” for years untouched, solder an eMMC.
eMMC advantages:
Soldered = no socket reliability issues.
Built-in wear leveling, bad-block management, controller-level health monitoring (EXT_CSD).
Faster (HS200 ~200 MB/s vs SD DDR50 ~50 MB/s).
Authenticated boot partitions (RPMB).
Industrial grade available, AEC-Q100 for automotive.
The cost difference is < $1 in volume. Decide between SD and eMMC at schematic time, not after the first field unit fails.
66.2 i.MX6ULL MMC hardware¶
The SoC has two uSDHC controllers: uSDHC1 and uSDHC2. Each supports:
SD cards up to SDR104 (~104 MB/s).
eMMC up to HS200 (~200 MB/s).
SDIO (used by SDIO WiFi modules, Ch 91).
1, 4, or 8-bit data widths.
Typically Point Atom boards wire uSDHC1 to the SD slot, uSDHC2 to onboard WiFi or eMMC.
66.3 Speed modes¶
Mode |
Clock |
Data lines |
Bandwidth (theoretical) |
Required pinmux |
|---|---|---|---|---|
DS (Default Speed) |
25 MHz |
4-bit |
12.5 MB/s |
basic |
HS (High Speed) |
50 MHz |
4-bit |
25 MB/s |
basic |
DDR50 |
50 MHz double-rate |
4-bit |
50 MB/s |
basic |
SDR50 |
100 MHz |
4-bit |
50 MB/s |
basic |
SDR104 |
200 MHz |
4-bit |
100 MB/s |
UHS-I voltage switch |
HS200 (eMMC) |
200 MHz SDR |
8-bit |
200 MB/s |
8-bit, 1.8 V signal |
HS400 (eMMC) |
200 MHz DDR |
8-bit |
400 MB/s |
strobe-aware (i.MX6ULL does NOT support HS400) |
i.MX6ULL supports up to HS200 (200 MB/s on 8-bit eMMC) and SDR104 (104 MB/s on 4-bit SD). HS400 is i.MX8M+ territory.
66.4 Device tree, uSDHC2 with eMMC¶
&usdhc2 {
pinctrl-names = "default", "state_100mhz", "state_200mhz";
pinctrl-0 = <&pinctrl_usdhc2>;
pinctrl-1 = <&pinctrl_usdhc2_100mhz>;
pinctrl-2 = <&pinctrl_usdhc2_200mhz>;
bus-width = <8>;
non-removable;
mmc-hs200-1_8v;
keep-power-in-suspend;
no-sd;
no-sdio;
vmmc-supply = <®_emmc_3v3>;
vqmmc-supply = <®_emmc_1v8>;
status = "okay";
};
Critical pieces:
bus-width = <8>: eMMC’s 8 data lines. Gets HS200’s full bandwidth.non-removable: kernel knows not to poll for card removal.mmc-hs200-1_8v: declares HS200 mode is supported (requires switching VQMMC to 1.8 V).vqmmc-supply: the I/O voltage rail. Needs to support 1.8 V for HS200.pinctrl-1and-2: different pin slew rates at higher speeds (pull strengths change).no-sd,no-sdio: speeds up probing. We know this is eMMC.
For an SD slot on uSDHC1:
&usdhc1 {
pinctrl-names = "default", "state_100mhz", "state_200mhz";
pinctrl-0 = <&pinctrl_usdhc1>;
pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
bus-width = <4>;
cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
keep-power-in-suspend;
vmmc-supply = <®_sd1_vmmc>;
sd-uhs-sdr104;
no-mmc;
status = "okay";
};
cd-gpios declares the card-detect line (a switch in the SD socket).
66.5 The MMC protocol, what’s actually on the wire¶
Unlike QSPI and EEPROM (Ch 64/65), where a from-scratch driver was tractable in ~200 lines, an MMC/SD host controller driver is genuinely a different scale. The SD spec is ~700 pages. The eMMC spec is ~400. There are ~60 commands, multi-stage state machines, signal-voltage switching, tuning windows, CRC validation, and physical-layer subtleties. Writing one from scratch in a single chapter is not realistic.
What we can do is trace a single read() through the layers so you understand exactly what the kernel does, and you can read the existing host driver’s source after.
The protocol vocabulary¶
MMC/SD commands are 48-bit packets:
bit: 47 46 45–40 39–8 7–1 0
┌────┬────┬──────────┬───────────┬───────┬─────┐
│ 0 │ 1 │ index │ argument │ CRC7 │ 1 │
└────┴────┴──────────┴───────────┴───────┴─────┘
▲
direction: 1 = host→card, 0 = card→host (response)
A handful of commands you’ll see in dmesg or in driver code:
Cmd |
Name |
Purpose |
|---|---|---|
CMD0 |
GO_IDLE_STATE |
Reset card to idle |
CMD8 |
SEND_IF_COND |
Check SD 2.0 voltage range (SD vs eMMC discrimination) |
ACMD41 |
SD_SEND_OP_COND |
SD initialization, voltage window |
CMD1 |
SEND_OP_COND |
eMMC initialization equivalent |
CMD2 |
ALL_SEND_CID |
Read card identification |
CMD3 |
SET_RELATIVE_ADDR |
Assign a relative address (RCA) |
CMD7 |
SELECT_CARD |
Select the addressed card for I/O |
CMD8 (eMMC) |
SEND_EXT_CSD |
Read 512-byte EXT_CSD block |
CMD17 |
READ_SINGLE_BLOCK |
Read one block (512 B) |
CMD18 |
READ_MULTIPLE_BLOCK |
Read consecutive blocks until CMD12 |
CMD23 |
SET_BLOCK_COUNT |
Pre-declare a multi-block count (skip CMD12) |
CMD24 |
WRITE_BLOCK |
Write one block |
CMD25 |
WRITE_MULTIPLE_BLOCK |
Write consecutive blocks |
CMD12 |
STOP_TRANSMISSION |
End a CMD18/CMD25 sequence |
CMD13 |
SEND_STATUS |
Read card status register |
CMD6 |
SWITCH |
Set EXT_CSD fields (mode change, partition switch) |
CMD21 (eMMC) |
SEND_TUNING_BLOCK |
HS200/HS400 tuning pattern |
Data transfers happen on the data lines in parallel with commands on the CMD line. A successful read of 4 blocks is roughly:
Host → Card: CMD17 (READ_SINGLE_BLOCK) | start_block_addr
Card → Host: R1 response (status, OK)
Card → Host: DAT lines: 512 bytes of data + CRC16
For multi-block (CMD18), one command triggers a stream of consecutive blocks until the host issues CMD12 to stop, much more efficient than per-block CMD17s.
The state machine¶
Cards live in one of these states: Idle → Ready → Identification → Standby → Transfer → Sending-data / Receive-data / Programming / Disconnected. Commands move the card between states. Only certain commands are valid in each state. The host’s job is to track this state and never issue an illegal command.
This is why a from-scratch MMC driver isn’t tractable in a chapter, it’s not the wire protocol (relatively simple), it’s the state-machine management plus the voltage switching plus the tuning plus the CRC plus the error recovery.
66.6 The kernel’s MMC subsystem, three layers¶
user-space: cat /dev/mmcblk1p1
│ read() syscall
▼
┌──────────────────────────────────────────────────┐
│ block layer (Ch 55D) │
│ builds a struct bio → struct request │
└──────────────────────────────────────────────────┘
│ mmc_blk_request_fn (queue_rq callback)
▼
┌──────────────────────────────────────────────────┐
│ MMC block driver (drivers/mmc/core/block.c) │
│ - converts bio offsets → MMC block addresses │
│ - builds struct mmc_request │
└──────────────────────────────────────────────────┘
│ mmc_wait_for_req(host, mrq)
▼
┌──────────────────────────────────────────────────┐
│ MMC core (drivers/mmc/core/core.c) │
│ - state-machine tracking │
│ - CMD/data sequencing │
│ - retry / error recovery │
└──────────────────────────────────────────────────┘
│ host->ops->request(host, mrq)
▼
┌──────────────────────────────────────────────────┐
│ Host driver (drivers/mmc/host/sdhci-esdhc-imx.c) │
│ - programs hardware registers │
│ - configures DMA / PIO │
│ - waits for completion IRQ │
│ - reads response register, returns to core │
└──────────────────────────────────────────────────┘
│ MMIO + IRQ
▼
uSDHC peripheral → physical bus → card
The mmc_host_ops contract¶
A host driver provides this struct of callbacks:
/* drivers/mmc/host/sdhci-esdhc-imx.c — simplified */
static const struct mmc_host_ops sdhci_esdhc_ops = {
.request = sdhci_request, /* execute one command + opt. data */
.set_ios = sdhci_set_ios, /* change clock, bus width, voltage */
.get_cd = sdhci_esdhc_get_cd, /* card-present? */
.get_ro = sdhci_esdhc_get_ro, /* write-protect? */
.enable_sdio_irq = sdhci_enable_sdio_irq, /* for SDIO devices */
.execute_tuning = sdhci_esdhc_executing_tuning, /* HS200 tuning */
.start_signal_voltage_switch = sdhci_start_signal_voltage_switch, /* 3.3→1.8 V */
/* ... */
};
That is the abstraction. The core asks the host driver to “execute this request” or “switch to bus width 8” without caring whether the controller is uSDHC, sdhci-pci, dw-mshc, or anything else. The host driver translates abstract requests into specific MMIO writes for its hardware.
MMIO: memory-mapped I/O, where software accesses peripheral registers through normal load and store instructions.
Tracing a single 4-KB read¶
You run dd if=/dev/mmcblk1p1 bs=4096 count=1 of=/tmp/x. Here’s what happens:
VFS sends
read(fd, buf, 4096)to the block-device file’s chardev.Block layer receives a 4 KB bio at logical-block-address (LBA) = wherever in p1. Splits into
struct request(one or more, depending on splitting policy).mmc_blk_request_fn(inblock.c) is called as the queue’squeue_rq. It:Calculates the MMC block address (LBA / 512, since MMC blocks are 512 B even when the FS uses larger).
Decides between single-block (CMD17) for 1 block or multi-block (CMD18 / CMD23+CMD18) for 2+ blocks. For 4 KB = 8 blocks: multi-block with CMD23 prefix.
Builds a
struct mmc_requestcontaining the command, data-direction, sg-list for the DMA destination, and a completion callback.
MCU bridge: Think of DMA like the MCU DMA controller you used for UART or SPI, but with cache coherency, scatter-gather descriptors, and kernel ownership rules added. DMA: Direct Memory Access. Hardware moves data to or from memory without the CPU copying each byte.
mmc_wait_for_req(host, mrq)in the core: sends to host_ops->request, waits on the completion.sdhci_requestin the host driver:Programs the eSDHC’s command register (CMD18 opcode), argument register (start block), block-count register (8).
Sets up SDMA: scatter-gather list pointing into the user-space buffer’s pinned pages.
Enables IRQs for “command complete” and “transfer complete.”
Writes the “start” bit. Hardware now drives the bus.
Hardware drives CMD18 onto the CMD line. EMMC responds with R1. EMMC begins streaming 8 × 512 bytes on the 8-bit DAT bus at 200 MHz (HS200) into DDR via DMA.
DDR: external DRAM that must be configured and trained before most software can run from it.
IRQ “command complete” fires after the CMD line transaction. Host reads R1 response register, captures status.
MCU bridge: Think of an IRQ like an EXTI/NVIC interrupt path, except Linux splits the hard interrupt from deferred work and must share lines across drivers. IRQ: interrupt request, the signal path that tells the CPU or interrupt controller that hardware needs service.
IRQ “transfer complete” fires after the data phase finishes. Host driver calls
mmc_request_done(host, mrq).MMC core wakes the waiter.
mmc_blk_request_fnchecks for errors, retries if needed, callsblk_mq_end_request(req, status).Block layer returns to user-space.
End-to-end at HS200, 4 KB takes about ~25 µs (10 µs of bus time, ~15 µs of kernel + IRQ overhead). For a properly-sized buffer (~64 KB or larger), the kernel overhead amortises and you approach raw bus bandwidth.
A host driver’s skeleton¶
If you ever did need to write one (say, porting to a new SoC):
static int sdhci_xxx_probe(struct platform_device *pdev)
{
struct sdhci_host *host;
struct resource *iomem;
/* SDHCI is the common framework — most SDHCI-compatible host drivers
reuse the SDHCI core and only set quirks + IOMEM */
host = sdhci_pltfm_init(pdev, &sdhci_xxx_pdata, sizeof(struct sdhci_xxx));
if (IS_ERR(host)) return PTR_ERR(host);
/* Get clocks, regulators, IOMEM */
sdhci_get_of_property(pdev);
/* Custom: i.MX has non-standard register layout for some bits */
host->ops = &sdhci_xxx_ops; /* override SDHCI defaults */
host->quirks |= SDHCI_QUIRK_BROKEN_DMA;
host->quirks2 |= SDHCI_QUIRK2_NO_1_8_V;
host->mmc->caps |= MMC_CAP_NONREMOVABLE | MMC_CAP_8_BIT_DATA;
return sdhci_add_host(host);
}
The SDHCI framework (drivers/mmc/host/sdhci.c) handles the standard cases. Vendor drivers like sdhci-esdhc-imx.c add quirks and override specific operations. That’s why the i.MX driver is ~1500 lines (mostly quirks) rather than 5000+, the heavy lifting is in sdhci.c.
For a completely custom controller (not SDHCI-compatible), you’d implement mmc_host_ops from scratch, that’s more work but the structure is the same.
66.7 EXT_CSD, the eMMC’s health report card¶
eMMCs maintain a 512-byte Extended Card-Specific Data register full of metadata. Read it from userspace:
[root@pa-mini:~]# mmc extcsd read /dev/mmcblk1
=============================================
Extended CSD rev 1.7 (MMC 5.0)
=============================================
Card supported command sets [S_CMD_SET: 0x01]
HPI Features [HPI_FEATURE: 0x01]: implementation based on CMD13
Background operations support [BKOPS_SUPPORT: 0x01]
...
Device life time estimation type A [DEVICE_LIFE_TIME_EST_TYP_A: 0x01]
Device life time estimation type B [DEVICE_LIFE_TIME_EST_TYP_B: 0x01]
Pre EOL information [PRE_EOL_INFO: 0x01]
eMMC Life Time Estimation A: 0%–10% device life time used
eMMC Life Time Estimation B: 0%–10% device life time used
Pre EOL information: Normal
The three key fields:
DEVICE_LIFE_TIME_EST_TYP_A: SLC-cell wear, 0–9 representing 10% bands (0 = 0–10% used).DEVICE_LIFE_TIME_EST_TYP_B: MLC-cell wear, same scale.PRE_EOL_INFO: 0x01 = Normal, 0x02 = Warning (80%+ wear), 0x03 = Urgent (replacement needed).
Production firmware should periodically read these, log via MQTT/syslog to a fleet management system. When you see PRE_EOL warning across many units of the same age, you’ve quantified your hardware lifetime, invaluable for warranty planning.
66.8 eMMC boot partitions¶
eMMCs typically have:
Boot partition 1 (typically 4 MB).
Boot partition 2 (typically 4 MB).
RPMB (Replay-Protected Memory Block, ~4 MB).
User partition (the bulk).
[root@pa-mini:~]# ls /dev/mmcblk1*
/dev/mmcblk1 /dev/mmcblk1p1
/dev/mmcblk1boot0 /dev/mmcblk1boot1
/dev/mmcblk1rpmb
mmcblk1boot0 is the active boot partition, i.MX6ULL boots from it (with the right fuse setting) instead of looking at the main partition’s MBR. Use it for U-Boot:
MCU bridge: Think of U-Boot like a much larger boot stub plus debug monitor: it initializes hardware, loads the next image, and gives you commands before Linux starts. U-Boot: the bootloader that initializes enough hardware to load and start the Linux kernel.
# Force write to mmcblk1boot0
[root@pa-mini:~]# echo 0 > /sys/block/mmcblk1boot0/force_ro
[root@pa-mini:~]# dd if=u-boot.imx of=/dev/mmcblk1boot0
[root@pa-mini:~]# echo 1 > /sys/block/mmcblk1boot0/force_ro
# Select boot partition 1 as the active partition
[root@pa-mini:~]# mmc bootpart enable 1 1 /dev/mmcblk1
Why? Two boot partitions enable atomic boot-loader updates: write to boot0 while booting from boot1. On success, swap. Crash mid-update = boot1 still works.
66.9 RPMB, replay-protected secure storage¶
RPMB is a small (~4 MB) partition that requires HMAC authentication for every write. The eMMC controller verifies that a key (programmed once at factory) matches before allowing writes. Reads are authenticated too, you know the data hasn’t been tampered with.
Use cases:
Secure-boot rollback prevention (store “minimum allowed firmware version”).
DRM keys.
Counters that need replay protection (anti-replay nonce).
Programming the RPMB key is one-shot, once written, that’s it forever. Don’t experiment with RPMB on production boards.
66.10 Performance test¶
[root@pa-mini:~]# dd if=/dev/zero of=/data/big.bin bs=1M count=512 conv=fsync
512+0 records in
512+0 records out
536870912 bytes (537 MB) copied, 4.5 s, 119 MB/s
[root@pa-mini:~]# fio --name=randwr --filename=/data/test.bin --rw=randwrite \
--bs=4k --runtime=30 --time_based --ioengine=psync --iodepth=1 --size=100M
write: IOPS=2500, BW=10MiB/s
HS200 eMMC: 100–150 MB/s sequential, ~2500 IOPS random 4k write. Compare to a budget SD card: 30 MB/s sequential, ~200 IOPS random.
66.11 Lab¶
Privilege boundary: $ means normal user. # or sudo means root and can change host or target state. After a privileged command, verify the expected device, service, or file appears before continuing. Roll back by undoing the config change or stopping the service you just enabled.
Trace a read with ftrace.
echo function > /sys/kernel/debug/tracing/current_tracer. echo 'mmc_*' > set_ftrace_filter. cat /dev/mmcblk1p1 > /dev/null. cat trace. Seemmc_blk_request_fn,mmc_wait_for_req,sdhci_requestfire in order, exactly the layered call chain described in §66.6.Inspect EXT_CSD. Run
mmc extcsd read /dev/mmcblk1. Identify the eMMC’s life-time estimation.Benchmark. dd + fio at sequential and random. Compare against an SD card.
Boot partition write.
ddU-Boot tommcblk1boot0. Activate it. Boot.Force HS mode change. In DT, remove
mmc-hs200-1_8v. Reboot. Benchmark, confirm slower.Wear monitoring script. Daily cron: read EXT_CSD, log to /var/log/wear.log. After running for weeks, plot.
Pull-the-plug test. With dd writing a large file, yank power. Reboot. Observe whether
fsckfinds errors. Compare eMMC vs SD card resilience (eMMC much better).Read the host driver. Skim
drivers/mmc/host/sdhci-esdhc-imx.c. Findsdhci_esdhc_ops. Find the i.MX-specific quirks (HS400 absence on i.MX6ULL, the tuning callback). With the layer model in §66.6 you should be able to navigate it.
66.12 Pitfalls¶
bus-width = <4>on a chip with 8 data lines wired. You get DS or HS speeds at best. HS200 needs 8-bit. Check schematic ↔ DT.Missing
vqmmc-supplyfor HS200. Driver can’t switch to 1.8 V signaling. Falls back to HS50. Look for “fall back” messages in dmesg.non-removableon an SD slot. The card-detect signal is ignored, so the system keeps trying to talk to the slot after the card is removed.cd-gpiospolarity wrong. Empty slot reports as “card present” (or vice versa).GPIO_ACTIVE_LOWis typical for card-detect switches.eMMC tuning fails. HS200 requires per-card calibration (“tuning”). Some eMMCs require specific tuning patterns. Mainline supports this. If you see “tuning failed” in dmesg, the eMMC chip is buggy (not common but happens, usually fixable by
mmc-ddr-1_8vinstead of HS200).fsync slow. eMMC
fsyncdoes a real flush-to-flash, which takes 10 to 100 ms. If your application callsfsyncafter every write, throughput drops sharply. Batch your writes.Write amplification. Even with TRIM, eMMC’s GC writes amplify your data ~2–5×. A 10 GB/day app actually writes 30 GB/day to flash. Plan lifetime accordingly.
force_roon boot partitions: Default is RO. You must clear it to write. Don’t forget to re-arm.Power-fail mid-erase. eMMC’s internal erase block is invisible. A power loss can corrupt a wider area than you actually wrote. Industrial eMMCs (Micron, KIOXIA) include PFAIL protection. Consumer parts do not.
66.13 Going deeper¶
Documentation/mmc/: MMC subsystem documentation.drivers/mmc/core/core.c: MMC core. Look atmmc_wait_for_req,mmc_attach_sd,mmc_attach_mmcfor the state-machine code.drivers/mmc/core/block.c: the block-driver glue.mmc_blk_mq_issue_rqis the per-request entry point.drivers/mmc/host/sdhci.c: SDHCI common framework.drivers/mmc/host/sdhci-esdhc-imx.c: i.MX uSDHC driver. ~1500 lines, mostly quirks oversdhci.c.Documentation/devicetree/bindings/mmc/: MMC bindings.JEDEC eMMC 5.1 standard (JESD84-B51): the eMMC specification.
SD Physical Layer Specification: at sdcard.org (free Simplified version, ~250 pages).
mmc-utils:mmc,mmc extcsd, RPMB tools.fio: the storage benchmarking tool.
End of Group A, Storage (Ch 64–66). You now have the three storage stacks covered: QSPI NOR for small boot, EEPROM for tiny metadata, eMMC/SD for bulk. Pick by capacity + speed + reliability requirements.
Next chapter: Chapter 67: Temperature / humidity / pressure sensors. Group B opens with the environmental sensors trio, BME280, SHT3x, AHT20, and the IIO drivers that expose them. IIO: Industrial I/O, Linux’s subsystem for sensors, ADCs, DACs, and buffered sampled data.