How memory cards work: OS point of view

Денис Дерюгин
8 min readDec 7, 2019

--

Where to begin

Unluckily, there are not so many resources to tell about it without tons of technical details (even osdev.org wiki doesn’t have any page for MMC on the 7-th Dec 2019), so I decided to create such article on my own. I hope it will help you to implement your own MMC subsystem or some MMC driver (or just become more familiar with MMC subsystem).

All stuff written here is based on my experience with Embox RTOS where I implemented a couple of memory card drivers:

  • PL181
  • SDHCI

Inspecting Linux or u-boot source is a good option, but it can be a bit complicated as well if you are not familiar with some basics.

In this article I’ll try to cover some basic concepts of handling memory cards if you develop OS (or want to interact with it baremetal).

I won’t cover all the details (e.g. I’ll talk only about single block read/write operations).

Check out some links at the end of this article for specifications and other helpful resources.

Historical overview

If you want some historical details, refer to the Wikipedia article. Important aspect of MMC 20+ year history is that it was a closed standard for most part of its life.

Now you can access specification online. However full specification (including DRM-related stuff) is still not available to everyone, you should pay like $ 1 000 to be a member of SD card community to be able to download it.

MMC vs SD vs SDIO vs SDHC vs SDXC vs …

MMC and SD are standards for devices which operate similar concepts but are different in details.

SDHC means “SD High Capacity”, SDXC means “SD eXtended Capacity” and are backward compatible with SD host controllers.

Remember, not every card controller is able to operate on every device, even if slot is physically compatible! Host driver should check this manually every time card is inserted.

MMC subsystem in OS

Trust me, it’s a good idea to create a general MMC interface even if you currently only want to implement a baremetal driver for your hobby project. Splitting it later will be pretty painful.

Although SD and MMC are different types of devices, they usually are being handled by the same subsystem (for example it’s done in that way Linux and u-boot).

Basically, there are two levels:

  • Common MMC system
  • Drivers for host controllers
MMC system layers in operation system

Host controller

OS can interact with MMC/SD cards in two ways:

  • SPI mode
  • Using host controller

In this article I’ll talk about host controller way, because usually direct SPI access is not an option as it requires specific hardware support.

Commands

There are various host controllers (SDHC, PL180/181, etc), they may differ in some details, but here are two essential parts every host controller should implement:

  • Sending commands
  • Receiving responses

Commands are enumerated and listed in specification, each one of them has corresponding response type and some flags (e.g. “Data transfer” flag).

Here are some basic examples for MMC:

| Command | Argument     | Abbreviation        | Response type |
| ------- | ------------ | ------------------- | ------------- |
| CMD0 | | GO_IDLE_STATE | |
| CMD2 | OCR | ALL_SEND_CID | R2 |
| CMD9 | RCA | SEND_CSD | R2 |
| CMD12 | | STOP_TRANSMISSION | R1b |
| CMD13 | RCA | SEND_STATUS | R1 |
| CMD17 | Data address | READ_SINGLE\_BLOCK | R1 |

Response type

So what exactly is a response type?
Basically it defines data length and some fields.

Sadly, usually you can’t just write response type to some register, instead of that you should operate with bit-fields:

  • Response presents
  • Long response (sometimes it’s referred as 136-bit response)
  • Checking response cmd code (opcode)
  • Checking response CRC
  • Checking if card is busy

Table below shows corresponding flags for each response type

| Response type | Host controller flags       |
| ------------- | --------------------------- |
| No response | All fields are zero |
| R1 | Present, CRC, opcode |
| R1b | Present, CRC, opcode, busy |
| R2 | Present, long response, CRC |
| R3 | Present |
| R4 | Present |
| R5 | Present, CRC, opcode |
| R6 | Present, CRC, opcode |
| R7 | Present, CRC, opcode |

You can figure out why it’s done this way by analyzing corresponding specifications (links at the end of the article).

From OS point of view, response type defines which registers will be filled with response and which bit-fields should be set in corresponding registers.
As you can see from table above, same response type doesn’t mean same data (compare: CMD2 and CMD9; CMD 13 and CMD17).

Handle command types in your driver

When you process some command in host controller driver, you can say for each command: “I should expect response of this kind, and those flags should be set in some registers”, but generally it’s a better idea to pass those flags manually from the MMC subsystem, so it will look like this.

Sorry for bad looking code block, I really don’t know how to make it look better on Medium (e.g. add syntax highlight).

/* mmc.c */
void mmc_send_cmd(uint32_t cmd_idx, uint32_t arg, uint32_t *response) {
int flags = 0;
switch (cmd_idx) {
case 2:
flags |= CMD_RESP | CMD_RESP_LONG | CMD_RESP_CRC;
break;
case 3:
/* ... */
}
mmc_host_controller_cmd(cmd_idx, arg, flags, response);
}
/* host_controller_driver.c */
void mmc_host_controller_cmd(
uint32_t cmd_idx,
uint32_t arg,
int flags,
uint32_t *resp) {
/* Perform device-specific operations to send
* given command with given args */
if (flags & CMD_RESP) {
/* Setup HC for response */
/* ... */
if (flags & CMD_LONG_RESP) {
/* Setup long response */
/* ... */
}
}
/* Setup registers for cmd_idx and arg */
/* ... */
/* Send cmd */
/* ... */
/* Write back response to buffer */
/* ... */
}

Card initialization

All types of memory cards support CMD0 which puts card to idle state.

Different card types allow different sets of commands in the idle state, so refer to according specification for details.

Here is how it’s done for MMC:

MMC initialization sequence

In your code it will go like this:

/* mmc.c */
int try_mmc() {
uint32_t resp[4];
/* Arguments for mmc_send_cmd() are
* 1) cmd id
* 2) cmd arg
* 3) response type
* 4) memory for response */
mmc_send_cmd(0, 0, 0, resp); /* Go idle */
mmc_send_cmd(1, 0, 0, resp); /* Go identification state */
mmc_send_cmd(2, 0, MMC_RSP_R2, resp); /* Read CID register */
/* Now `resp' contains information about manufacturer,
* serial number, manufacture date and so on.
*
* Refer to function mmc_dump_cid() in this file
* https://github.com/embox/embox/blob/master/src/drivers/mmc/core/mmc_host.c
* for more info how to parse this register
*/
mmc_send_cmd(3, 0, 0, resp); /* Go standy mode */ mmc_send_cmd(7, rca, 0, resp); /* Put card to ready state */ /* Now you can do read/write commands */
}

SD initialization is a little bit more complicated, so I’ll skip it.

You may wonder, what is “rca” parameter in that code example? Let’s find out.

Multiple cards on the same bus (and what is RCA?)

Most modern devices have a single card slot for a one host controller (so if there are multiple physical slots, there are likely to be multiple host controllers), but it’s possible to connect multiple cards to single MMC bus.

RCA (Relative Card Address) is used to address some particular memory card on such bus. You should know two things about RCA’s: SD cards have their own RCA-s (so you just read it during identification), MMC cards don’t have them on reset (so you have to assign it manually). RCA is used as an argument for some commands.

You need to handle RCA-s even if you have a single memory card (because card itself is not aware if there are other cards around).

Data transfers

There are two general ways for host controller to transfer data:

  • FIFO (First in — first out)
  • DMA (Direct Memory Access)

These two mechanisms are used widely in variaty of devices. If you’re interested in osdev, you probably know it already, but I’ll cover it a little bit in following code examples.

Let’s see how to read and write data from/to SD cards.

/* mmc.c */
/* For the sake of simplicity let's use a single data buffer for all transfers. */
uint8_t data_buffer[512];
int mmc_block_read(struct mmc_host *mmc, int block_num) {
uint32_t arg;
if (mmc->is_high_capacity) {
/* For high capacity cards argument is a block number */
arg = block_num
} else {
/* For low capacity cards argument is a byte offset */
arg = block_num * 512; /* Suppose block size is 512 */
}
/* Arguments for mmc_send_cmd() are
* 1) cmd id
* 2) cmd arg
* 3) response type
* 4) memory for response */
/* Read single block */
mmc_send_cmd(17, arg, MMC_RSP_R1B | MMC_DATA_READ, resp);
return 0;
}
int mmc_block_write(struct mmc_host *mmc, int block_num) {
uint32_t arg;
if (mmc->is_high_capacity) {
arg = block_num
} else {
arg = block_num * 512;
}
/* Write single block operations */
mmc_send_cmd(24, arg, MMC_RSP_R1B | MMC_DATA_WRITE, resp);
return 0;
}

Now let’s add some code to mmc_host_controller_cmd():

extern uint8_t data_buffer[512];
/* host_controller_driver.c */
void mmc_host_controller_cmd(
uint32_t cmd_idx,
uint32_t arg,
int flags,
uint32_t *resp) {
/* Perform device-specific operations to send
* given command with given args */
if (flags & CMD_RESP) {
/* Setup HC for response */
/* ... */
if (flags & CMD_LONG_RESP) {
/* Setup long response */
/* ... */
}
}
if (flags & (MMC_DATA_READ | MMC_DATA_WRITE)) {
/* Setup data address, usually it's just some register. */
/* ... */
/* If we go DMA way, it will take care of data transfer on
* it's own */
if (we_use_fifo() && (flags & MMC_DATA_WRITE)) {
for (int i = 0; i < 512; i++) {
/* Push i-th byte of data_buf[] to fifo */
/* ... */
}
}
}
/* Setup registers for cmd_idx and arg */
/* ... */
/* Send cmd */
/* ... */
/* Write back response to buffer */
/* ... */
if (flags & (MMC_DATA_READ | MMC_DATA_WRITE)) {
/* Make sure transfer is finished. Usually you can
* check it with some bit field in a status register or
* specific IRQ */
while (transfer_is_not_over(...)) {
}
if (we_use_fifo() && (flags & MMC_DATA_READ)) {
for (int i = 0; i < 512; i++) {
/* Get i-th byte from FIFO to data_buf[] */
/* ... */
}
}
}
}

And that’s it, these operations allow you to read and write data from your memory card.

Now driver handlers look kinda overif()-ed, but you need to make those checks anyway. For some specific pieces of hardware it may be neccessary to handle voltage setup in special if () { } else { } constructions; some devices need to handle stop transfer ops in a special way and so on…

Feedback

If you find some mistakes/typos here or want to suggest topic for a new article, contact me:

Resources

--

--