Учимся работать с SDHC/SDXC-картами по протоколу SPI

4 июня 2018

Сегодня SD-карты используются повсюду. Они втыкаются в ноутбуки, планшеты, телефоны, видеокамеры, роутеры, фоторамки, диктофоны, электронные книги, mp3-плееры, одноплатные компьютеры и даже квадрокоптеры — словом, они везде. Часто о них думают, как об относительно медленных устройствах, способных хранить пару гигабайт информации. Однако в наши дни уже доступны SD-карты объемом 512 Гб и скоростью чтения-записи 90 Мбайт/сек (не мегабит!). Теоретически же объем хранимой информации ограничен 2 Тб. А чем еще прекрасны SD-карты, это тем, что с ними можно работать по незамысловатому протоколу, основанному на SPI.

Немного матчасти

«SD» расшифровывается как «Secure Digital». Причем тут безопасность не знает никто. Внутри SD-карты находится обычная flash-память и микроконтроллер, осуществляющий общение с внешним миром. То есть, в первом приближении, это точно такая же non-volatile память, как и SPI flash.

SD-карты бывают трех типов. Карты SDSC (SC = Standard Capacity) позволяют хранить до 2 Гб информации и используют файловую систему FAT12 или FAT16. Эти карты морально устарели, в магазинах их найти непросто, да и по цене они сопоставимы с картами большего объема. Кроме того, они используют протокол, несколько отличающийся от протокола SDHC/SDXC-карт. В силу названных причин, с этого момента про существование SDSC мы забудем. К современным типам карт относятся SDHC (HC = High Capacity), использующие файловую систему FAT32 и способные хранить до 32 Гб данных, а также SDXC (XC = eXtended capacity), использующие exFAT и имеющие объем до 2 Тб. С точки зрения протокола эти карты неотличимы. Разница заключается только в файловой системе, выбор которой диктуется спецификацией.

Разумеется, ничто не мешает отформатировать SDHC карту под exFAT, или SDXC карту под какой-нибудь ZFS. Но ваш смартфон или фотоаппарат, вероятно, не сможет работать с такой картой.

Fun fact! Встречаются поддельные SDHC карты, которые на самом деле являются SDSC. В обычном магазине вы такие, скорее всего, не найдете, а вот на eBay налететь можно. Если вам предлагают купить типа SDHC карту объемом всего лишь 1 Гб, она наверняка на самом деле является SDSC.

Карты разделяют на различные классы, в зависимости от минимальной последовательной скорости записи (обратите внимание на выделение курсивом). Класс скорости обозначают в стиле C4 (class 4) или V30 (class 30). В обоих случаях цифра означает скорость в Мбайт/сек. Отличие C от V заключается только в том, что V намекает на пригодность карты для записи видео высокого разрешения. Еще встречаются маркировки U1 и U3 для 10 Мбайт/сек и 30 Мбайт/сек соответственно, где U означает Ultra High Speed. C10, V10 и U1 — это одно и то же.

SD-карты бывают разных форм-факторов — SD, MiniSD и MicroSD. MiniSD сегодня практически не встречаются. Многие карты выпускаются в форме MicroSD с переходником в обычный SD-формат. Это позволяет покупателям использовать карты с различными устройствами.

На следующем фото изображена моя небольшая коллекция SD и MicroSD-карт, а также модулей для подключения их к отладочным платам (Arduino, Nucleo и подобным):

SD-карты и модули для работы с ними

Все представленные здесь модули работают одинаково хорошо. Если сомневаетесь, какой брать — берите тот, что изображен слева внизу. Он позволяет работать как с SD, так и с MicroSD-картами (через переходник), а также имеет дополнительные пины для подключения логического анализатора. Модуль не составляет труда найти на eBay. Еще встречаются модули вообще без резисторов, стабилизаторов напряжения и так далее, имеющие только слот для подключения карты и пины. С ними некоторые карты работать не будут! Далее станет понятно, почему. Наконец, модуль легко спаять из адаптера для MicroSD-карт. Далее будет рассказано, как.

Подключение SD-карты

Ниже изображена распиновка SD и MicroSD-карт:

Расположение пинов на SD и MicroSD-картах

Наибольший интерес для нас представляет правая колонка. На первый вгляд, все просто — смотрим на картинку, хоть напрямую припаиваемся к карте проводами, и начинаем слать и принимать байты по SPI. Но есть ряд важных моментов:

  • Ни в коем случае не подавайте 5 В на пин VDD! Все SD-карты гарантировано работают от 3.3 В. Некоторые при этом также могут работать и от 5 В, но это не гарантируется. Если подать 5 В, вы рискуете спалить вашу дорогую карточку на 128 Гб, после чего ее останется только выкинуть;
  • По тем же соображениям, если ваш проект использует пятивольтовую логику, крайне рекомендуется использовать конвертер уровней, например TXS0108E (даташит [PDF]);
  • Платы Arduino имеют пин 3V3, но не могут подавать на него большой ток. Если запитать SD-карту от этого пина, можно словить забавные глюки. Например, карта будет нормально работать в одиночестве, но переставать работать при подключении к плате TFT-экранчика на базе ST7735, чья подсветка также питается от 3V3. Поэтому, если вы проектируете модуль или Arduino-шилд, используйте понижающий стабилизатор напряжения на 3.3 В вроде AMS1117;
  • Пин DO (он же MISO) должен быть обязательно подтянут к плюсу через резистор на 10 кОм или около того. Некоторые карты просто не будут стартовать без этого резистора. Например, я наблюдал такое поведение на картах производства Sony;

Теперь становится понятно, почему простые модули, имеющие только слот для подключения карты, не очень подходят. Также теперь ясно, как сделать модуль для подключения MicroSD-карт из адаптера. Отмечу, что пины с землей (VSS1 и VSS2) в адаптере, как правило, уже соединены между собой, поэтому дополнительно соединять их проводочком не требуется. На всякий случай стоит перепроверить, соединены ли пины, прозвонив их мультиметром.

Тонкости протокола

Хорошее описание протокола было найдено в статье How to Use MMC/SDC на сайте elm-chan.org. Здесь я не вижу смысла ее пересказывать. Заинтересованные читатели могут ознакомиться с оригиналом, а также с полной реализацией протокола для микроконтроллеров STM32 в исходниках к данному посту. Вместо пересказа я лишь пробегусь по основным моментам. Также отмечу, что в статье я не нашел упоминание нескольких крайне важных нюансов, про которые будет рассказано далее.

Итак, типичная команда выглядит как-то так:

Протокол SD-карт в PulseView

Команды всегда имеют формат 01xxxxxx, и в соответствии со значением битов xxxxxx называются CMD0, CMD1, и так далее до CMD63. Следом за командой идут 4 байта аргумента, за которыми идет байт в формате yyyyyyy1 с семибитным CRC. Контрольные суммы по умолчанию выключены и проверяются только для первых нескольких команд на этапе инициализации. В остальных же случаях CRC заполняется единицами.

Большинство команд получают в ответ один байт, так называемый R1:

/*
R1: 0abcdefg
     ||||||`- 1th bit (g): card is in idle state
     |||||`-- 2th bit (f): erase sequence cleared
     ||||`--- 3th bit (e): illigal command detected
     |||`---- 4th bit (d): crc check error
     ||`----- 5th bit (c): error in the sequence of erase commands
     |`------ 6th bit (b): misaligned addres used in command
     `------- 7th bit (a): command argument outside allowed range
             (8th bit is always zero)
*/

Если старший бит ответа равен единице, значит SD-карта еще обрабатывает запрос. Иногда за R1 следуют дополнительные данные. Также в определенных ситуациях в протоколе фигурируют data tokens (байты 0xFC, 0xFE), stop transaction token (0xFD), error token и data response. Детали не слишком захватывающие, к тому же, они хорошо описаны на elm-chan.org и их можно изучить по коду. Куда интереснее то, чего в статье нет или обозначено не слишком явно.

Во-первых, вы можете помнить, что в SPI за один такт SCLK одновременно принимается и передается один бит информации. Так вот, оказывается, что если при чтении ответа от SD-карты случайно послать по SPI что-то отличное от единиц, некоторым SD-картам это рвет башню. Поэтому прием данных от карты выглядит как-то так:

static int SDCARD_ReadBytes(uint8_t* buff, size_t buff_size) {
    // make sure FF is transmitted during receive
    uint8_t tx = 0xFF;
    while(buff_size > 0) {
        HAL_SPI_TransmitReceive(&SDCARD_SPI_PORT, &tx, buff, 1,
                                HAL_MAX_DELAY);
        buff++;
        buff_size--;
    }

    return 0;
}

Во-вторых, в статье верно описано, что в определенных случаях карта может помечать себя занятой (busy), притягивая MISO к земле. В таких ситуациях нужно дождаться готовности карты. Но на практике оказалось, что проверку на готовность нужно выполнять перед каждой командой, даже если в текущих обстоятельствах карта не может быть занятой. То есть, по сути, перед каждой командой нужно посылать 0xFF (на иллюстрации с форматом команд этот момент опущен). Иначе некоторые карты отказываются работать. Я наблюдал такое поведение у карт производства SanDisc.

Соответствующая проверка:

static int SDCARD_WaitNotBusy() {
    uint8_t busy;
    do {
        if(SDCARD_ReadBytes(&busy, sizeof(busy)) < 0) {
            return -1;
        }
    } while(busy != 0xFF);

    return 0;
}

Fun fact! Понять я это смог, подглядев в Arduino-библиотеку SD. Пользуясь случаем, отмечу, что библиотека эта в целом довольно скверная. Мне не кажется очень хорошей идеей мешать в одну кучу код для SDSC и SDHC/SDXC карт, как сделано в этой библиотеке. Также я заметил, что в ней почему-то отсутствует поддержка CMD18 (READ_MULTIPLE_BLOCK), несмотря на то, что CMD25 (WRITE_MULTIPLE_BLOCK) реализована. И еще библиотека отказалась работать с некоторыми имеющимися у меня картами, несмотря на то, что код, написанный мной с нуля, прекрасно с ними работает. Вот и пользуйся после этого готовыми библиотеками!

Наконец, в третьих, карта может делить SPI-шину с другими устройствами. Понятно, что в этом случае первым делом после запуска прошивки нужно пометить все устройства, как неактивные, подав соответствующее напряжение, обычно высокое, на пины CS. После чего уже можно спокойно общаться с каждым устройством по отдельности, не беспокоясь, что какое-то другое устройство по ошибке решит, что обращались с нему. Но проблема заключается в том, что SD-карта определенным образом интерпретирует данные, передаваемые по SPI, даже не являясь выбранным устройством. Если конкретнее, то при инициализации карты нужно передать 74 или больше единицы (например, 10 байт 0xFF) с высоким напряжением на CS. По этой причине карта либо должна жить на отдельной шине, либо инициироваться перед всеми остальными устройствами. Иначе карта может отказаться работать, я проверял.

Получившаяся библиотека

В ходе изучения мной протокола SD-карт была написана библиотека для STM32, реализующая этот протокол. Библиотека основана на HAL и имеет следующий интерфейс:

#define SDCARD_SPI_PORT      hspi1
#define SDCARD_CS_Pin        GPIO_PIN_5 // Arduino shield: D4
#define SDCARD_CS_GPIO_Port  GPIOB
extern SPI_HandleTypeDef SDCARD_SPI_PORT;

// call before initializing any SPI devices
void SDCARD_Unselect();

// all procedures return 0 on success, < 0 on failure
// size of block == 512 bytes

int SDCARD_Init();
int SDCARD_GetBlocksNumber(uint32_t* num);
int SDCARD_ReadSingleBlock(uint32_t blockNum, uint8_t* buff);
int SDCARD_WriteSingleBlock(uint32_t blockNum, const uint8_t* buff);

// Read Multiple Blocks
int SDCARD_ReadBegin(uint32_t blockNum);
int SDCARD_ReadData(uint8_t* buff);
int SDCARD_ReadEnd();

// Write Multiple Blocks
int SDCARD_WriteBegin(uint32_t blockNum);
int SDCARD_WriteData(const uint8_t* buff);
int SDCARD_WriteEnd();

SD-карты могут реализовывать дополнительные функции, такие, как очистка блоков и защита блоков от записи. Но они поддерживаются не всеми картами, и потому не реализованы. Также протокол позволяет включить проверку контрольных сумм. Но эта возможность не реализована, поскольку данные могут испортиться не только во время передачи, и потому их целостность должна проверяться выше уровня протокола, на уровне конкретного приложения. Дополнительно вычисляя и проверяя CRC на уровне протокола мы, скорее всего, только зря скушаем миллиамперы и займем flash-память. Да и вообще, я не убежден в надежности семибитных CRC.

Заключение

В качестве источников дополнительной информации я бы рекомендовал следующие:

Полную версию исходников к этому посту вы найдете на GitHub. Обратите внимание, что тамошний пример кода пишет на карту на уровне блоков, ничего не зная ни о каких файловых системах. Поэтому, если решите его запускать, советую выбрать SD-карту без особо ценных данных.

Вооружившись полученными сегодня знаниями, можно реализовать много безумных идей. Например, можно сделать RAID из SD-карточек, или устройство с интерфейсом SD-карты, сжимающее и/или шифрующее данные. Или вообще отправляющее их на сервер по беспроводной связи. Конечно же, совершенно не был затронут вопрос работы с файловыми системами. Ему будет посвящена одна из следующих заметок.

Дополнение: Работа с FAT32 и exFAT с помощью библиотеки FatFs

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.