Инструменты пользователя

Инструменты сайта


prog:sd:sdio_vc1

Работа с картой SD через интерфейс SDIO в 1901ВЦ1

Существует два способа работать с SD (Security Digital) картами, через "родной" интерфейс SDIO и через SPI. Данная статья будет посвящена интерфейсу SDIO, который реализован в процессоре 1901ВЦ1Т.

(Для справки, вот ссылка на пример взаимодействия с картой SD через SPI, который реализовал RMax на форуме. Причем, сделал он это через реализацию СMSIS (Cortex Microcontroller Software Interface Standard) драйвера SPI, что позволяет использовать этот "стандартизованный" драйвер не только с файловой системой, но и в прочих middleware приложениях от Keil.)

В текущем примере мы разберем реализацию блока SDIO в микроконтроллере 1901ВЦ1Т и подключим этот блок к файловой системе FatFs от elm-chan.org. В проекте использована последняя на текущий момент версия FatFs R0.13b от Apr 07, 2018. За основу проекта взят демо-пример от производителя опубликованный на форуме. Этот пример работает с той же библиотекой файловой системы, только более старой версии, проигрывает mp3 и прочее. Но оказалось, что в примере не работают файловые операции с современными SD картами. Точнее, выяснилось, что если дослушать mp3 трек до конца, то тест файловой системы проходит. А если сразу идти к операциям с файлами, или прервать исполнение музыки, то карточка не читается. Пример был написан достаточно давно, в 2012 году, и как удалось выяснить у автора, работал тогда только с картами стандартного объема до 2ГБ.

Какие бывают карты можно посмотреть в Википедии. Для изучения SDIO была куплена самая дешевая SD карта в Ашане за ~350 рублей. Это SDHC (Secure Digital High Capacity) карта высокой емкости на 8ГБ. На ней и отработан данный проект.

При работе с картой я опирался на описание SD стандарта в переводе "Упрощенное описание стандарта физического уровня карт SD" и в некоторых местах англоязычное было все-таки более понятно SD Card Association.

Получившийся проект доступен на GitHub. В проекте запускается тест драйвера "диска" от автора библиотеки файловой системы. Этот тест проверяет драйвер и говорит будет ли данный драйвер работать с файловой системой FatFs или нет. Собственно цель проекта состояла в том, чтобы тест прошел успешно. Для этого потребовалось реализовать всего 5 функций:

  • disk_status - Get device status
  • disk_initialize - Initialize device
  • disk_read - Read sector(s)
  • disk_write - Write sector(s)
  • disk_ioctl - Control device dependent functions
  • get_fattime - Get current time, (используется реализация по умолчанию).

Итого, пример работает следующим образом:

  • При запуске мигает светодиод LED_CYCLE (PB13) - показывая, что исполняется основной цикл while.
  • Кнопка Down - выполнение теста FatFs, результат выводится на светодиоды.
  • Кнопка Select - тест идентификации карты, результат выводится на светодиоды.
  • Результат:
    • LED_OK (PB15) - Тест пройден успешно
    • LED_FAULT (PB14) - Тест провален.

Pack для 1901ВЦ1Т

В официальном паке от производителя, по прежнему нет поддержки для двух-ядерного микроконтроллера 1901ВЦ1Т. В этом микроконтроллере RISC ядро полностью идентично серии 1986ВЕ9х, и поэтому все драйвера периферийных блоков от 1986ВЕ9х могут быть использованы для программирования 1901ВЦ1Т. Кроме этого, в директории библиотеки уже лежат файлы для модулей SDIO и DSP. Они просто не подключены.

C:\Keil_v5\ARM\PACK\Keil\MDR1986BExx\1.5\Libraries\MDR32F9Qx_StdPeriph_Driver\src\

  • MDR32F9Qx_sdio.c
  • MDR32F9Qx_dsp.c

Чтобы исправить положение, я взял официальный пак 1.5 с сайта Миландр и дописал туда поддержку процессора 1901ВЦ1Т с драйверами от 1986ВЕ9х. Дополнительно подключил для него модули SDIO и DSP. Все это делается в одном файле Keil.MDR1986BExx.pdsc, который находится внутри архива c расширением *.pack. На самом деле *.pack - это обычный zip архив, его можно открыть и легко поменять содержимое.

Получившийся pack находится внутри примера на GitHub. Я поменял название пака на 1.51, чтобы он устанавливался в отдельную директорию C:\Keil_v5\ARM\PACK\Keil\MDR1986BExx\1.51 и не затирал пак установленный от производителя, тот что стоит в директории 1.5. В своем паке 1.51 я удалил все примеры, чтобы архив был поменьше. На основании этого пака и собран пример для SDIO. Это удобнее, чем заводить пример для Cortex-M3 based device, как это предлагается в данной статье - Создаем проект для 1901ВЦ1Т

Установить Pack можно простым дабл-кликом на него - откроется PackInstaller, либо через сам PackInstaller в Keil.

Блок SDIO в 1901ВЦ1Т

В ссылке на спецификацию SD написано, какие сигналы и как необходимо подавать на карту, при работе с ней. Вкратце, SDIO должно:

  1. Подавать частоту на линию CLK, которая тактирует обмен по линиям CMD и данных.
  2. На линию CMD подавать команды для карты
  3. С линии CMD считывать ответы от карты
  4. По линиям данных DAT0-DAT3 посылать и принимать данные

Вся логика работы с модулем SDIO реализована в файле brdSDIO.c. Модуль реализует все функции, необходимые для обмена сигналами с SD картой, но сам протокол обмена реализован в отдельном файле - brdSD_Card.c.

(Планируется, что при подключении карты по интерфейсу SPI, логика brdSD_Card.c останется почти неизменной, а вместо brdSDIO.c подключится реализация обмена сигналами по интерфейсу SPI. Таким образом, текущий проект можно будет легко модифицировать для работы с SD картой по интерфейсу SPI.)

Частота на CLK нужна только для обмена, она не является тактовой для работы самой карты. Внутри карты есть свой контроллер, который работает там на своей какой-то частоте. Данные могут передаваться либо только по линии DAT0, либо по всем четырем DAT0-DAT3, что обеспечивает большую скорость. В нашем примере используются все линии.

Для начала, на карту необходимо подать напряжение питания. В демоплате 1901ВЦ1 питание на карту подано всегда, поэтому каких-либо действий не требуется. В функции включения питания просто инициализируется регистр управления блоком MDR_SDIO→CR. Все режимы работы блока задаются этим регистром, поэтому для наглядности влияния бит я вынес маски значений регистра отдельно:

#define SDIO_CR__PowerOn      SDIO_CR_SDE | SDIO_CR_CLKOE | SDIO_CR_CRC_EN_DATA
#define SDIO_CR__SendCMD      SDIO_CR_CRC_EN_CMD | SDIO_CR_SBITCMD | SDIO_CR_DIRCMD
#define SDIO_CR__ReadResp     SDIO_CR_CRC_EN_CMD | SDIO_CR_SBITCMD
#define SDIO_CR__SendCycles   SDIO_CR_DIRCMD

#define SDIO_CR__ReadBlock    SDIO_CR_CRC_EN_CMD | SDIO_CR_SBITDAT
#define SDIO_CR__WriteBlock   SDIO_CR_CRC_EN_CMD | SDIO_CR_SBITDAT | SDIO_CR_DIRDATA

#define SDIO_CR__ModeClear   (SDIO_CR_CRC_EN_CMD | SDIO_CR_SBITCMD | SDIO_CR_DIRCMD  \
                               | SDIO_CR_SBITDAT | SDIO_CR_DIRDATA)

void BRD_SDIO_PowerOn (void)
{
  MDR_SDIO->CR = SDIO_CR__PowerOn;
}

Биты регистра при включении питания:

  • SDIO_CR_SDE - включение блока SDIO.
  • SDIO_CR_CLKOE - разрешение генерации тактовых сигналов в линию CLK.
  • SDIO_CR_CRC_EN_DATA - автоматически считать CRC посылаемых данных и выводить в линии данных.

Посылка команды

После подачи питания, карту необходимо опросить и проинициализировать, поэтому вся настройка идет через две линии - CLK и CMD. Для посылки команд, в соответствии со спецификацией на 1901ВЦ1Т реализована функция BRD_SDIO_SendCMD(cmd, arg).

static inline void SDIO_CR_SetMode(uint32_t CR_Flags)
{
  uint32_t regCR = MDR_SDIO->CR;
  regCR &= ~(SDIO_CR__ModeClear);
  MDR_SDIO->CR = regCR | CR_Flags;
}

#define SD_TX_BIT   0x40

BRD_SD_CMD_Result BRD_SDIO_SendCMD(uint32_t cmd, uint32_t arg)
{
  cmd &= 0x7F;

  WAIT_CMD_STOPPED();

  SDIO_CR_SetMode(SDIO_CR__SendCMD);
  //  Запись команды с аргументом
  MDR_SDIO->CMDDR  = ((cmd | SD_TX_BIT) & 0x0000007F) |
                        (arg >> 16 & 0x0000FF00) |
                        (arg       & 0x00FF0000) |
                        (arg << 16 & 0xFF000000) ;
  MDR_SDIO->CMDDR  = arg & 0x000000FF;
  MDR_SDIO->CMDCRC = 0x00000000;
  MDR_SDIO->CMD_TRANSFER = SD_CMD_LEN;
  MDR_SDIO->CR |= SDIO_CR_WORK2;                                //  Запуск
  
  //  Ожидание окончания передачи c таймаутом
  if (!WaitCMDCompleted(_SDIO_Timeouts.SD_SendCMD_Timout_ms))
    return BRD_SD_CMD_TimeoutSend;
  
  // Передача закончена
  return BRD_SD_CMD_OK;
}

static inline bool WaitCMDCompleted(uint32_t timeout_ms)
{
  DelayInt_ms = timeout_ms;
  while ((MDR_SDIO->CR & SDIO_CR_WORK2) != 0) 
  { 
    //  Timeout истек, выход с ошибкой
    if (!DelayInt_ms)
      return false;
  }
  
  return true;
}

Как видно из реализации функции, для посылки команды необходимо:

  1. Выставить необходимый режим в регистре CR
    1. SDIO_CR_CRC_EN_CMD - генерировать CRC для посылаемых команд и выводить в линию CMD.
    2. SDIO_CR_SBITCMD - Выводить в линию CMD команду и аргумент.
    3. SDIO_CR_DIRCMD - Направление линии CMD
      1. 0: CMD - вход
      2. 1: CMD - выход
  2. В FIFO команд (регистр CMDDR) записать данные, которые будут выводиться на линию CMD. В данном случае это команда и 32-битный аргумент. Интересно, что в команде мы сами прописываем бит TX, определяющий что за данные находятся на линии:
    1. 1 - Команда в карту
    2. 0 - Ответ из карты
  3. В регистр расчета CMDCRC записать 0, поскольку при выставленном бите SDIO_CR_CRC_EN_CMD, само CRC посчитается контроллером автоматически и выведется на линию вслед за командой и аргументом.
  4. В регистре CMD_TRANSFER указать длину посылки, столько тактовых сигналов выведет блок на линию.
    1. Длина команды к карте всегда 48 бит.
    2. Ответ от карты может быть 48 бит или 138, именно столько битовых значений на линии CMD будет ждать блок в режиме считывания ответа от карты. Clock будет подаваться постоянно пока необходимое количество бит не будет принято. Начало же отсчета бит начнется по стартовому биту в канале CMD. Некоторые команды не имеют ответа от карты.
  5. Выставить бит SDIO_CR_WORK2 - запуск передачи.
  6. После окончания вывода команды, бит SDIO_CR_WORK2 сбросится в 0.

Использование таймаута, на мой взгляд, здесь излишне. Ведь контроллер тут играет активную роль - вывел данные на линию и готов. Т.е. нет условий, которые помешали бы биту SDIO_CR_WORK2 сбросится. Но у автора на форуме таймаут использовался, поэтому я решил оставить все как есть.

Диаграммы исполнения функции BRD_SDIO_SendCMD(cmd, arg) представлены на этой картинке:

Здесь представлено несколько вариантов команды и аргумента, чтобы понять какие байты в каком месте посылки выводятся. Пояснения:

  • Тактовые импульсы подаются только на время вывода команды. Их столько, сколько задано в CMD_TRANSFER + 1, т.е. для посылки команды к карте 48 + 1. (Предположение почему выводится +1 ниже.)
  • Сигнал CDM начинается со стартового бита, который всегда равен "0", и заканчивается стоповым битом, который всегда равен "1". Это логично, ведь если посылки идут непрерывно друг за другом, то стоповый 1, четко отделяется от следующего пакета начинающегося с 0. Т.е. вне зависимости от данных, есть четкое переключение уровней сигнала, разграничивающее соседние посылки.
  • Второй бит, сразу за стартовым, определяет направление передачи по линии CMD.
    • 1 - Передача команды от хоста (МК) к SD карте.
    • 0 - Ответ от карты в хосту.
  • Далее идет 6 бит команды, затем старший байт аргумента и остальные байты.
  • После аргумента на линию выводится значение CRC, оно считается аппаратно при установленном бите SDIO_CR_CRC_EN_CMD.
  • Завершается посылка выставлением стопового бита.

Проблема с тактированием

С тактированием здесь есть небольшая проблема. Согласно спецификации на SD, пункт "4.4 Clock Control" тактирование по линии CLK может подаваться постоянно, либо выключаться, например, для снижения энергопотребления в простое (отсутствии обмена). Но есть несколько исключений:

  1. Необходимо дождаться чтобы карта закончила текущую операцию, т.е. вышла из режима Busy. Для этого карту необходимо опрашивать с периодом не реже 50мс, командой ACMD41 (SD_APP_OP_COND) которая возвращает биты статуса. Тактирование при этом можно держать постоянно, либо включать только на время опроса, но с учетом п.3.
  2. Но по стандарту, допускается отключить тактирование и не дожидаясь выхода карты из режима Busy. Текущая операция в карточке все равно закончится. Но в данном случае, необходимо подать на карту дополнительный тактовый перепад, чтобы снять сигнал занятости по линии данных DAT0. Если этого не сделать, то линия DAT0 останется в 0. (Вероятно именно этим требованием объясняется наличие дополнительного 49-го тактового импульса в диаграмме посылки команды - BRD_SDIO_SendCMD(cmd, arg).)
  3. Но самое главное это, цитата:

Имеется очевидное требование, что тактовая частота должна выдаваться все время, когда карта выводит данные или токены ответа (response). По окончанию последней транзакции по шине SD хосту требуется предоставить 8 тактовых циклов для карты, чтобы завершить операцию перед выключением тактов. Ниже приведен список различных транзакций по шине:

  1. Команда без ответа. 8 тактов подается хостом после завершающего бита команды.
  2. Команда с ответом карты (response). 8 тактов после завершающего бита ответа карты.
  3. Транзакция чтения данных. 8 тактов по завершении последнего бита последнего блока данных.
  4. Транзакция записи данных. 8 тактов после токена статуса CRC.

Т.е. на сколько я понял, необходимо чтобы любая передача по линиям DAT0-DAT3 и CMD всегда заканчивалась 8-ю дополнительными тактами на линии CLK. И только после этих 8 тактов сигнал тактирования на CLK может быть остановлен. Но вот такого режима работы блока SDIO мне и не удалось достичь. Как было видно на диаграмме функции BRD_SDIO_SendCMD(cmd, arg), такты CLK заканчиваются сразу же после вывода стопового бита и дополнительного 49-го бита для сброса статуса на DAT0. Тактовый сигнал выключается без вывода дополнительных 8 тактов.

Я попробовал дописать лишние 8 бит в регистр CMD_TRANSFER, чтобы вывелось на 8 тактов больше. Но эти такты ушли в длину команды, т.е. CRC было высчитано с этими дополнительными 8-ю битами, результат вышел не тот, что требовалось:

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

Между посылкой команды и дополнительными 8-ю тактами есть промежуток, связанный с переинициализацией блока SDIO, но используемая SD карточка отнеслась к нему спокойно. Забегая вперед скажу, что неработоспособность примера с форума оказалась связана с тем, что после команды CMD0, которая не требует ответа, не выводились эти 8 дополнительных тактов. По крайней мере, как только я дописал вывод этих 8 тактов к командам не требующим ответа, то пример сразу же заработал.

bool BRD_SDIO_Send_ClockCycles(uint32_t clockCycles)
{
  WAIT_CMD_STOPPED();
 
  SDIO_CR_SetMode(SDIO_CR__SendCycles);
  MDR_SDIO->CMDDR  = 0x00FF;
  MDR_SDIO->CMD_TRANSFER = clockCycles;          // Количество циклов
  MDR_SDIO->CMDCRC = 0x00000000;

  MDR_SDIO->CR |= SDIO_CR_WORK2;

  if (!WaitCMDCompleted(_SDIO_Timeouts.SD_SendCMD_Timout_ms))
    return false;
  
  MDR_SDIO->CR &= ~SDIO_CR_DIRCMD;              // Command RX
  return true;
}

Если посмотреть код, то видно что он практически такой-же как и в BRD_SDIO_SendCMD(cmd, arg). Различие только в управляющих битах регистра CR. Здесь сброшены биты:

  1. SDIO_CR_SBITCMD = 0, данные на линию CMD не выводятся. В регистр CMDDR кладется любое число, чтобы FIFO не было пустым, иначе передача не начнется.
  2. SDIO_CR_CRC_EN_CMD = 0, значение CRC не считается и на линию не выводится.
  3. SDIO_CR_DIRCMD = 0, линия CMD не используется и остается в режиме входа, состояние по умолчанию.

Для справки приведу диаграмму вывода команды при сброшенном бите SDIO_CR_SBITCMD. Практической пользы этот режим не имеет, и приведен здесь просто для понимания назначения этого бита. На картинке видно, что нет стартового бита, команда и аргумент на линию CMD не выводились, выведено только значение CRC.

Чтение ответа от карты

Код чтения ответа от карты такой-же как в спецификации.

BRD_SD_CMD_Result BRD_SDIO_ReadResponce(SD_ResponceLength respLength, uint32_t *response)
{
  uint32_t tmpBuff[5];
  
  //  Чтение ответа от SD карты
  SDIO_CR_SetMode(SDIO_CR__ReadResp);
  
  MDR_SDIO->CMD_TRANSFER = (uint32_t) respLength;
  MDR_SDIO->CMDCRC = 0x00000000;
  MDR_SDIO->CR |= SDIO_CR_WORK2;

  //  Ожидание приема ответа с таймаутом
  if (!WaitCMDCompleted(_SDIO_Timeouts.SD_ReadResp_Timout_ms))
  {
    // Принудительная остановка транзакции
    MDR_SDIO->CR &= ~(SDIO_CR_WORK2 | SDIO_CR_SBITCMD);
    return BRD_SD_CMD_TimeoutSend;
  }

  //  Чтение принятых данных из FIFO
  tmpBuff[0] = MDR_SDIO->CMDDR;                 // Read the response words 0
  tmpBuff[1] = MDR_SDIO->CMDDR;                 // Read the response words 1

  response[0] = ((tmpBuff[0]<<8) & 0xFFFFFF00) | ((tmpBuff[1]>>8) & 0x000000FF);
  if (respLength == SD_Resp_138) 
  {
    tmpBuff[2] = MDR_SDIO->CMDDR;               // Read the response words 2
    tmpBuff[3] = MDR_SDIO->CMDDR;               // Read the response words 3
    tmpBuff[4] = MDR_SDIO->CMDDR;               // Read the response words 4
    
    response[1] = ((tmpBuff[1]<<8) & 0xFFFFFF00) | ((tmpBuff[2]>>8) & 0x000000FF);
    response[2] = ((tmpBuff[2]<<8) & 0xFFFFFF00) | ((tmpBuff[3]>>8) & 0x000000FF);
    response[3] = ((tmpBuff[3]<<8) & 0xFFFFFF00) | ((tmpBuff[4]>>8) & 0x000000FF);
  }
  
  return BRD_SD_CMD_OK;
}

В регистре CR выставляются теже биты, что и для посылки команды, сброшен только бит:

  • SDIO_CR_DIRCMD = 0, линия CMD работает как вход. Сигналами на нем управляет карточка.

В этом режиме блок SDIO бесконечно генерирует тактовые импульсы и после появления на линии CMD стартового бита считывает данные в количестве бит, указанных в регистре CMD_TRANSFER. При окончании чтения линии бит SDIO_CR_WORK2 сбросится в 0, и полученные данные вычитываются из регистров блока. Операция чтения выполняется с таймаутом, поскольку если ответа нет, то бесконечное чтение необходимо прервать. Это говорит о том, что карточка не приняла команду - т.е. не послала ответ, либо самой карточки в разъеме нет. По этой причине, при срабатывании таймаута необходимо вручную сбросить биты SDIO_CR_WORK2 и SDIO_CR_SBITCMD, чтобы остановить транзакцию.

Это всё были операции управления, которые работают через линию CMD. Функции работы с данными такие-же, как и в спецификации. Назначение бит для линий DAT принципиально такое-же, только в названиях бит и регистров вместо CMD используется DAT. Поскольку проблем с чтением данных у меня не возникло, то описывать реализацию этих функций не буду.

Таймауты

В спецификации все таймауты указаны в миллисекундах. Для того чтобы их отработать в примере используется системный таймер, который является составной частью ядра - т.е. есть в любом Cortex-M. Таймер настраивается на генерацию прерывания каждую секунду. В обработчике же прерывания декрементируются две переменных, которые отсчитывают таймаут. Значение переменной таймаута задается в определенном месте в коде, и затем там же происходит ожидание обнуления этой переменной. Если переменная обнулилась - значит сработал таймаут.

В модуле декрементируются две переменных таймаута. Одна используется внутренними функциями модуля brdSDIO.c, а вторая используется функциями более высокого уровня, реализованными в модуле brdSD_Card.c. Декремент переменных выделен в отдельную функцию BRD_SDIO_DelayHandler(). Это сделано на случай, если основной программе потребуется свой обработчик прерывания системного таймера. Тогда обработчик прерывания необходимо реализовать отдельно и в нем вызывать BRD_SDIO_DelayHandler() для отсчета таймаутов. Если основной программе обработчик прерывания системного таймера не нужен, то обработчик по умолчанию SysTick_Handler() уже реализован в brdSDIO.c и активируется макроопределением USE_SYS_TIMER_HANDLER = 1, в файле brdSDIO.h.

volatile uint32_t DelayInt_ms, DelayExt_ms;

void BRD_SDIO_DelayHandler(void)
{
  volatile uint32_t n;
  
	n = DelayExt_ms;
	if (n) 
    DelayExt_ms = --n;
	n = DelayInt_ms;
	if (n) 
    DelayInt_ms = --n;  
}

#if USE_SYS_TIMER_HANDLER
void SysTick_Handler(void)
{
  BRD_SDIO_DelayHandler();
}
#endif

В текущем примере прерывание от системного таймера используется в main.c для мигания светодиодом LED_CYCLE, поэтому определение USE_SYS_TIMER_HANDLER = 0, а в SysTick_Handler() вызывается BRD_SDIO_DelayHandler().

Работа с SD картой

Работа с SD картой подразумевает посылку команд, принятие ответов и, собственно, обмен данными. Все это реализуется функциями SDIO, которые мы рассмотрели выше. Согласно стандарта, некоторые команды требуют предварительного переключения SD карты в режим восприятия следующей команды не в стандартном ее понимании, а в неком специфичном. В описании они называются "командами специфичными для приложения", что по русски звучит несколько коряво, а по английски это Application Specific Command. Такие команды начинаются с префикса "A", т.е. это ACMDx, и их исполнение состоит из двух посылок, на примере ACMD41 это:

  1. CMD55 (APP_CMD) - воспринимать следующую команду в специфическом понимании, а не в прямом.
  2. CMD41 - сама команда.
    • Сама по себе команда CMD41 зарезервирована, и если послать ее по шине - то, карточка ничего не ответ и никак не воспримет эту команду.
    • А если же посылку CMD41 предварить командой CMD55, то это сочетание SD карта воспринимает как команду вернуть статус.

Битов в команде всего 6, т.е. это всего 64 возможных команды, поэтому видимо со временем команд перестало хватать и пришлось пойти на такое "расширение" адресного пространства.

Для удобства, чтобы не разбираться в коде, обычная команда посылается или специфичная, реализована отдельная функция SD_ExecACMD(), которая на вход принимает оба типа команд и посылает предварительную команду CMD55 если это требуется.

// Пример определения команд
#define CMD38   (38)          // ERASE
#define ACMD41  (41|0x80)     // SEND_OP_COND (SDC)
#define CMD55   (55)          // APP_CMD

// Унифицированная функция для посылки CMD и ACMD
bool SD_ExecACMD(uint32_t cmd, uint32_t arg, SD_ResponceLength respLength, uint32_t *response)
{
  SD_ResponceR1 respCMD55;
  BRD_SD_CMD_Result result;
  
  if (cmd & 0x80) 
  {
    //  Переключение карты в режим ожидания приема команды ACMD
    result = SD_ExecCMD(CMD55, CardRCA << 16, SD_Resp_48, &respCMD55.value);
    if (result != BRD_SD_CMD_OK)
      return false;
    
    //  Если карта не вернула статус ожидания APP_CMD - выход
    if (!(respCMD55.bits.SD_R1_APP_CMD))
      return false;
  }  
  
  //  Сама команда Application-Specific Command
  return (SD_ExecCMD(cmd, arg, respLength, response) == BRD_SD_CMD_OK);
}

Через эту функцию SD_ExecACMD(), реализована вся логика работы с картой.

Работа с картой делится на два этапа:

  1. Идентификация карты и настройка рабочего режима.
  2. Работа с данными - запись и чтение данных на карте.

Идентификация карты

Идентификация карты происходит после включения питания. Опрашиваются внутренние регистры карты, определяется ее тип, объем, рабочее напряжение, максимальная скорость работы и т.д. В текущий момент на шине SDIO может быть активной только одна карта, поэтому карта назначается активной и затем с ней происходит обмен данными.

Важно, что идентификация проходит на маленькой скорости 100-400КГц и высоком диапазоне напряжений питания 2.7-3.6 В, это необходимо для обратной совместимости со старыми картами.

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

  1. Включение питания - на плате постоянно подано.
  2. Выставление скорости обмена в битах BRG регистра CR. Сама частота не подается, это просто установка делителя для предстоящего обмена.
  3. Задержка в 74 такта на инициализацию карты. Поскольку питание подано всегда, то нет возможности отследить необходимо ли тут тактирование на линии CMD, или имеется ввиду просто задержка выраженная в тактах CLK.
  4. CMD0 (GO_IDLE_STATE) - Перевод карты в состояние IDLE. Карта после подачи питания и так должна быть в IDLE. Но если идентификация вызывается не после подачи питания, а при карте уже инициализированной и находящейся в каком-то режиме - то без CMD0 не обойтись.
  5. CMD8 (SEND_IF_COND) - Команда добавлена с версии спецификации 2.00, для карт которые могут работать на меньших напряжениях питания. Здесь наша карта выдает ответ, что она поддерживает указанный диапазон напряжений 2,7-3,6В. С платы подается 3,3В.
  6. ACMD41 (SEND_OP_COND) - Опрос статуса карты в цикле, пока она не вернет бит готовности после PowerUp. Бит CCS в ответе говорит что наша карта высокой емкости (High Capacity SD Memory). В ответе карта вернула регистр OCR (Operation Conditions Register).
  7. CMD2 (ALL_SEND_CID) - Чтение регистра CID (Card Identification Data).
  8. CMD3 (SEND_RELATIVE_ADDR) - Получение адреса карты RCA (Relative Card Address). Используется далее для адресации карты.
  9. CMD9 (SEND_CSD) - Чтение регистра CSD (Card Specific Data).
  10. CMD7 (SELECT_CARD) - Здесь карта становится активной, состояние Transfer. С ней будет идти обмен по линиям данных. Остальные карты, если есть на шине, будут в состоянии StandBy.
  11. ACMD6 (SET_BUS_WIDTH) - Устанавливаем обмен по всем 4-м линиям DAT.
  12. Идентификация закончена, устанавливаем делитель частоты BRG на высокую скорость обмена. Теперь можно обмениваться данными.

Настраивать напряжение питание нам нет необходимости. Ведь, согласно англоязычной спецификации на SD карты, по напряжению питания карты делятся на

  • высоковольтные, с диапазоном Vdd1: 2,7-3,6В. Как наша карта.
  • с двумя диапазонами. Сейчас это карты ультра-скоростного диапазона UHS_II.
    • Vdd1: 2,7-3,6 В
    • Vdd2: 1,7-1,95 В

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

Работа с данными

После идентификации и ответа карты о поддерживаемых скоростях, МК может выставить более высокую скорость общения с картой. Скорость определяется классом карточки, для нашей карты указан 10 класс - это значит, что карточка поддерживает скорость не меньше 10МБ/сек. Максимальная же скорость обмена по стандарту 2.00 составляет 25МБ/сек, т.е. 50МГц.

Работа с данными происходит блоками по 512 байт. Карты поддерживают одно-блочные операции чтения/записи и много-блочные. При записи данных сразу в несколько блоков рекомендуется задать команду на предварительное стирание этих блоков, тогда запись данных произойдет быстрее.

Последовательность чтения данных:

  1. CMD13 - проверка, что карточка в состоянии Transfer.
  2. Запрос на проведение операции чтения, проверка ответа на отсутствие ошибок обмена данными.
    • CMD17 - READ_SINGLE_BLOCK
    • CMD18 - READ_MULTIPLE_BLOCK
  3. Цикл чтения блока/блоков данных через SDIO функцией BRD_SDIO_ReadBlock().
  4. После окончания считывания подаем дополнительные 8 тактов CLK - BRD_SDIO_Send_ClockCycles(8).
  5. Если чтение много-блочное вызываем CMD12 (STOP_TRANISSION) для выхода из режима выдачи данных. В случае одно-блочного режима, карта сама вернется в состояние Transfer после выдачи одного блока данных.
  6. Подача дополнительных 8 бит тактовой частоты.

Последовательность записи данных:

  1. CMD13 - проверка, что карточка в состоянии Transfer.
  2. Если запись мульти-блочная стираем сектора.
    • CMD23 - SET_BLK_COUNT для карт MMC.
    • ACMD23 - SET_WR_BLK_ERASE_COUNT для карт SD.
  3. Запрос на проведение операции записи, проверка ответа на отсутствие ошибок обмена данными.
    • CMD24 - WRITE_BLOCK
    • CMD25 - WRITE_MULTIPLE_BLOCK
  4. Цикл записи данных:
    • CMD13 - Проверка, что карточка в состоянии Transfer и выставлен бит готовности к приему данных.
    • Запись блока данных через функцию BRD_SDIO_WriteBlock().
  5. Если запись много-блочная - вызываем CMD12 (STOP_TRANISSION) для выхода из режима приема данных. В случае одно-блочного режима, карта сама вернется в состояние Transfer после приема одного блока данных.
  6. Подача дополнительных 8 бит тактовой частоты.

Последовательности чтения и записи я заканчиваю подачей дополнительных 8-ми тактов на линии CLK. С ними тест начал, хоть и со сбоями, проходить на частоте 10 МГц (см. дальше).

Тест файловой системы

Как уже упоминалось, в проекте используется библиотека файловой системы FatFs от elm-chan.org. Она качается с сайта и подключается к проекту, Download. В файле diskio.c уже заведены прототипы функций, которые необходимо реализовать для работы файловой системы.

Тут просто берем и подключаем получившиеся функции работы с SD картой. Т.е. иерархия получается такая:

  • diskio.c - Функции IO файловой системы, работают через:
    • brdSD_Card.c - Протокол обмена с SD картой (команды и обмен данными), работают через:
      • brdSDIO.c - Функции работы с блоком SDIO (генерация сигналов взаимодействия)
Функция IO драйвера FatFs Функция brdSD_Card.c
DSTATUS disk_initialize (BYTE pdrv) BRD_SD_CardDetect() - идентификация карты
DSTATUS disk_status (BYTE pdrv) Возвращаем переменную статуса
DRESULT disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count) BRD_SD_CardRead() - чтение секторов
DRESULT disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count) BRD_SD_CardWrite() - запись секторов
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff) BRD_SD_IO_Ctrl() - Вспомогательные операции IO, реализацию этой функции берем из примеров.

В разделе ресурсы можно скачать архив с примерами FatFs под разные платформы - Раздел "Resources", ссылка "FatFs sample projects for various platforms". Наиболее близкая - это реализация FatFs под STM.

Там же на сайте предлагается модуль для теста функций IO - "A function checker for low level disk I/O module is available here". Модуль вызывает ранее перечисленные функции IO и если тест проходит успешно, то выдается заключение о том, что реализация драйвера IO выполнена правильно и файловая система работоспособна.

Я сохранил этот тест в функции TestFileSystem(), файл test_funcIO.c. Он вызывается в проекте по нажатию кнопки Down на отладочной плате. Если тест проходит успешно, то загорается светодиод LED_OK (PB15). Если тест провален, то загорается светодиод LED_FAULT (PB14).

В процессе исполнения TestFileSystem() отладочная информация выводится через функцию printf(). Для того чтобы получать эти логи, я настроил вывод printf через ITM. Как это сделать описано тут - Printf через ITM. Я использовал отладчик Ulink2.

Рабочая скорость

Если посмотреть файл main.c, то видно что вся настройка для работы с SD картой сведена в две структуры, которые передаются в функцию BRD_SD_Init(&sdioCardCfg, &sdioTimeouts);

  • BRD_SDDrv_Timeouts sdioTimeouts - таймауты для работы функций brdSDIO.c.
  • BRD_SD_ClockCfg sdioCardCfg - настройки для brdSD_Card.c.

Основное внимание здесь надо уделить структуре sdioCardCfg и полям:

  * .SysTimerPeriod_1ms   = PERIOD_1MS,       // Value to get 1ms timer period  
  * .SD_CLK_BRG_Detect    = SDIO_BRG_div256,  // 80MHz/256 = 312,5 KHz < 400KHz
  * .SD_CLK_BRG_Transfer  = SDIO_BRG_div16,   // 80MHz/16  = 5 MHz - Работает

Последние два поля - это делитель тактовой частоты, задающий частоту обмена с картой. Для отладки проекта я использовал один делитель SDIO_BRG_div256 в обоих полях. Важно было добиться принципиальной работоспособности примера. После того как тест FatFs прошел успешно, я постепенно повышал скорость обмена.

  • SysTimerPeriod_1ms - это период системного таймера для получения прерываний каждую миллисекунду.
  • SD_CLK_BRG_Detect - это делитель частоты ядра, для получения низкой частоты обмена с картой на этапе идентификации. Частота должна быть в диапазоне 100-400КГц. Делитель SDIO_BRG_div256 обеспечивает данную частоту, но на тестах я ставил делитель SDIO_BRG_div32, идентификация все-равно проходила успешно. Но на каких-нибудь древних картах высокая скорость при идентификации может не сработать.
  • SD_CLK_BRG_Transfer - это делитель частоты ядра, для получения рабочей частоты обмена данными. Здесь мне удалось поднять частоту только до 5МГц, т.е. при частоте ядра 80МГц используется делитель на 16. Далее обмен данными становится не стабильным.
    • При частоте CPU_CLK/8 обмен ломается стабильно на каждый 9-й запуск. Это очень странно, ведь цикличность сбоя говорит о какой-то логической ошибке, а не о влиянии помех или качестве сигналов. Лог файл при этом показывает, что сбой происходит в функции disk_read() уже практически в конце теста - функция выходит по таймауту, не дождавшись от карты необходимого количества данных.
    • При частоте CPU_CLK/4 обмен не работает совсем. Лог показывает, что считанные данные, не совпадают с записанными. Даже если понизить частоту CPU в 2 раза, чтобы итоговая частота CLK осталась как при делителе на 8. Это говорит о том, что сам блок SDIO не успевает обрабатывать сигнал при таком делителе. Не хватает ему тактов частоты для правильной обработки сигнала. (Так, например максимальная частота блока UART в микроконтроллерах "Миландр" ограничена значением CPU_CLK/16. Т.е. для одного бита требуется 16 тактов CPU). Но возможно дело и в чем-то другом.
//=======  .SD_CLK_BRG_Transfer   = SDIO_BRG_div4 =====

test_diskio(0, 3, 0x20000E08, 0x00000800)
**** Test cycle 1 of 3 start ****
 disk_initalize(0) - ok.
**** Get drive size ****
 disk_ioctl(0, GET_SECTOR_COUNT, 0x20000DD0) - ok.
 Number of sectors on the drive 0 is 15605760.
**** Get block size ****
 disk_ioctl(0, GET_BLOCK_SIZE, 0x20000DC4) - ok.
 Size of the erase block is 2048 sectors.
**** Single sector write test 1 ****
 disk_write(0, 0x20000E08, 0, 1) - failed.                     ERROR!
Sorry the function/compatibility test failed. (rc=6)
FatFs will not work with this disk driver.


//=======  .SD_CLK_BRG_Transfer   = SDIO_BRG_div8 =====
test_diskio(0, 3, 0x20000E08, 0x00000800)
**** Test cycle 1 of 3 start ****
 disk_initalize(0) - ok.
...
**** Test cycle 2 of 3 start ****
...
**** Test cycle 3 of 3 start ****
...
**** Multiple sector write test ****
 disk_write(0, 0x20000E08, 1, 4) - ok.
 disk_ioctl(0, CTRL_SYNC, NULL) - ok.
 disk_read(0, 0x20000E08, 1, 4) - failed.                      ERROR!
Sorry the function/compatibility test failed. (rc=13)
FatFs will not work with this disk driver.

При частоте обмена CPU_CLK/16 тест проходит успешно, сколько бы я его ни запускал. Не буду приводить всю простыню логов, приведу только самое окончание.

//=======  .SD_CLK_BRG_Transfer   = SDIO_BRG_div16 =====

test_diskio(0, 3, 0x20000E08, 0x00000800)
**** Test cycle 1 of 3 start ****
...

**** Test cycle 3 of 3 start ****
 disk_initalize(0) - ok.
**** Get drive size ****
 disk_ioctl(0, GET_SECTOR_COUNT, 0x20000DD0) - ok.
 Number of sectors on the drive 0 is 15605760.
**** Get block size ****
 disk_ioctl(0, GET_BLOCK_SIZE, 0x20000DC4) - ok.
 Size of the erase block is 2048 sectors.
**** Single sector write test 1 ****
 disk_write(0, 0x20000E08, 0, 1) - ok.
 disk_ioctl(0, CTRL_SYNC, NULL) - ok.
 disk_read(0, 0x20000E08, 0, 1) - ok.
 Data matched.
**** Multiple sector write test ****
 disk_write(0, 0x20000E08, 1, 4) - ok.
 disk_ioctl(0, CTRL_SYNC, NULL) - ok.
 disk_read(0, 0x20000E08, 1, 4) - ok.
 Data matched.
**** Single sector write test (misaligned address) ****
 disk_write(0, 0x20000E0B, 5, 1) - ok.
 disk_ioctl(0, CTRL_SYNC, NULL) - ok.
 disk_read(0, 0x20000E0D, 5, 1) - ok.
 Data matched.
**** 4GB barrier test ****
 disk_write(0, 0x20000E08, 6, 1) - ok.
 disk_write(0, 0x20001008, 8388614, 1) - ok.
 disk_ioctl(0, CTRL_SYNC, NULL) - ok.
 disk_read(0, 0x20000E08, 6, 1) - ok.
 disk_read(0, 0x20001008, 8388614, 1) - ok.
 Data matched.
**** Test cycle 3 of 3 completed ****

Congratulations! The disk driver works well.

Собственно небольшой лог, я сохранил в проекте и его можно посмотреть целиком.

Итого

В целом, обмен с картой SD состоялся, тест файловой системы прошел успешно. Исходный проект с форума не работал по причине отсутствия 8-ми дополнительных тактов на линии CLK, после окончания передач по линиям CLK и DAT. SDIO не позволяет добавить эти 8-мь тактов к непрерывной посылке, но отдельная подача 8 тактов после окончания операции тоже помогла. Если конкретно:

  1. 8 тактов к CMD0 - сделали проект работоспособным.
  2. 8 тактов к окончанию чтения/записи блоков данных, позволило поднять скорость обмена до 10МГц. Но при такой частоте фиксируются сбои, поэтому максимальная частота обмена оставлена 5МГц.

В перспективе планируется добавить в проект поддержку работы через SPI. Тогда уже можно будет вернуться и к вопросу получения максимальной скорости обмена, чтобы сравнить, какой же интерфейс окажется быстрее.

prog/sd/sdio_vc1.txt · Последние изменения: 2018/08/21 18:05 — vasco