С чего начать

К сожалению, не так много ресурсов, чтобы рассказать об этом без тонны технических подробностей (даже в вики osdev.org нет страницы для MMC на 7 декабря 2019 года), поэтому я решил создать такую ​​статью на мой собственный. Я надеюсь, что это поможет вам реализовать свою собственную подсистему MMC или какой-нибудь драйвер MMC (или просто лучше познакомиться с подсистемой MMC).

Все написанное здесь основано на моем опыте работы с Embox RTOS, где я реализовал пару драйверов карт памяти:

  • PL181
  • SDHCI

Проверка исходного кода Linux или u-boot — хороший вариант, но он также может быть немного сложным, если вы не знакомы с некоторыми основами.

В этой статье я попытаюсь осветить некоторые основные принципы работы с картами памяти, если вы разрабатываете ОС (или хотите взаимодействовать с ней на «голом металле»).

Я не буду описывать все детали (например, я расскажу только об операциях чтения/записи отдельных блоков).

Ознакомьтесь с некоторыми ссылками в конце этой статьи для ознакомления со спецификациями и другими полезными ресурсами.

Исторический обзор

Если вам нужны некоторые исторические подробности, обратитесь к статье в Википедии. Важным аспектом более чем 20-летней истории MMC является то, что большую часть своей жизни он был закрытым стандартом.

Теперь вы можете получить доступ к спецификации онлайн. Однако полная спецификация (включая материалы, связанные с DRM) по-прежнему доступна не всем, вы должны заплатить около 1000 долларов, чтобы стать членом сообщества SD-карт, чтобы иметь возможность загрузить ее.

MMC, SD, SDIO, SDHC, SDXC, …

MMC и SD — это стандарты для устройств, которые используют схожие концепции, но отличаются в деталях.

SDHC означает «SD High Capacity», SDXC означает «SD eXtended Capacity» и обратно совместимы с хост-контроллерами SD.

Помните, что не каждый контроллер карты может работать на каждом устройстве, даже если слот физически совместим! Драйвер хоста должен проверять это вручную каждый раз, когда вставляется карта.

Подсистема MMC в ОС

Поверьте мне, хорошая идея создать общий интерфейс MMC, даже если в настоящее время вы хотите реализовать только драйвер для своего хобби. Разделение его позже будет довольно болезненным.

Хотя SD и MMC — это разные типы устройств, они обычно обрабатываются одной и той же подсистемой (например, так это делается в Linux и u-boot).

По сути, есть два уровня:

  • Общая система ММС
  • Драйверы для хост-контроллеров

Хост-контроллер

ОС может взаимодействовать с картами MMC/SD двумя способами:

  • SPI-режим
  • Использование хост-контроллера

В этой статье я расскажу о способе хост-контроллера, потому что обычно прямой доступ через SPI не вариант, так как он требует специальной аппаратной поддержки.

Команды

Существуют различные хост-контроллеры (SDHC, PL180/181 и т. д.), они могут отличаться в некоторых деталях, но вот две основные части, которые должен реализовать каждый хост-контроллер:

  • Отправка команд
  • Получение ответов

Команды пронумерованы и перечислены в спецификации, каждая из них имеет соответствующий тип ответа и некоторые флаги (например, флаг «Передача данных»).

Вот несколько основных примеров для 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            |

Тип ответа

Так что же такое тип ответа?
По сути, он определяет длину данных и некоторые поля.

К сожалению, обычно вы не можете просто записать тип ответа в какой-то регистр, вместо этого вы должны работать с битовыми полями:

  • Ответ представляет
  • Длинный ответ (иногда его называют 136-битным ответом)
  • Проверка кода ответа cmd (код операции)
  • Проверка ответа CRC
  • Проверка занята ли карта

В таблице ниже показаны соответствующие флаги для каждого типа ответа.

| 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        |

Понять, почему так сделано, можно, проанализировав соответствующие спецификации (ссылки в конце статьи).

С точки зрения ОС тип ответа определяет, какие регистры будут заполнены ответом и какие битовые поля должны быть установлены в соответствующих регистрах.
Как видно из таблицы выше, один и тот же тип ответа не означает одинаковые данные ( сравните: CMD2 и CMD9; CMD 13 и CMD17).

Обработка типов команд в вашем драйвере

Когда вы обрабатываете какую-либо команду в драйвере хост-контроллера, вы можете сказать для каждой команды: «Я должен ожидать ответа такого рода, и эти флаги должны быть установлены в некоторых регистрах», но обычно лучше передавать эти флаги вручную из Подсистема MMC, поэтому это будет выглядеть так.

Извините за плохо выглядящий блок кода, я действительно не знаю, как сделать его лучше на Medium (например, добавить подсветку синтаксиса).

/* 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 */
    /* ... */
}

Инициализация карты

Все типы карт памяти поддерживают CMD0, которая переводит карту в состояние ожидания.

Карты разных типов допускают разные наборы команд в состоянии ожидания, поэтому подробности см. в соответствующих спецификациях.

Вот как это делается для MMC:

В вашем коде это будет выглядеть так:

/* 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 немного сложнее, поэтому я ее пропущу.

Вы можете задаться вопросом, что такое параметр «rca» в этом примере кода? Давайте разберемся.

Несколько карт на одной шине (и что такое RCA?)

Большинство современных устройств имеют один слот для карты для одного хост-контроллера (поэтому, если есть несколько физических слотов, вероятно, будет несколько хост-контроллеров), но можно подключить несколько карт к одной шине MMC.

RCA (относительный адрес карты) используется для адресации конкретной карты памяти на такой шине. Вы должны знать две вещи о RCA: карты SD имеют свои собственные RCA (так что вы просто читаете это при идентификации), карты MMC не имеют их при сбросе (поэтому вам нужно назначить его вручную). RCA используется в качестве аргумента для некоторых команд.

Вам нужно обращаться с RCA-картами, даже если у вас есть одна карта памяти (потому что сама карта не знает, есть ли поблизости другие карты).

Передача данных

Существует два основных способа передачи данных хост-контроллером:

  • FIFO (первым пришел — первым ушел)
  • DMA (прямой доступ к памяти)

Эти два механизма широко используются в различных устройствах. Если вас интересует osdev, вы, вероятно, уже знаете его, но я немного расскажу о нем в следующих примерах кода.

Давайте посмотрим, как читать и записывать данные с/на SD-карты.

/* 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;
}

Теперь добавим код в 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[] */
                /* ... */
            }
        }
    }
}

И все, эти операции позволяют читать и записывать данные с карты памяти.

Теперь обработчики драйверов выглядят несколько перегруженными, но вам все равно нужно выполнять эти проверки. Для некоторых конкретных аппаратных средств может потребоваться настройка напряжения в специальных if () { } else { } конструкциях; некоторые устройства должны особым образом обрабатывать операции остановки передачи и так далее…

Обратная связь

Если вы нашли здесь ошибки/опечатки или хотите предложить тему для новой статьи, свяжитесь со мной:

Ресурсы