MMC(SD) и AVR. Часть 2. Работа с картой.

AVR
В прошлой статье мы с вами узнали основные определения и получили минимально необходимые знания для работы с картами памяти формата MMC, SDSC,SDHC. Теперь настало время познакомиться с ними поближе. Давайте посмотрим на основные операции, которые сы можем выполнять с этими картами, после того, как они инициализированы и находятся в режиме SPI:

— Чтение (нескольких байтов, одного блока и последовательности блоков)
— Запись (нескольких байтов, одного блока и последовательности блоков)
— Чтение регистров карты памяти


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

void MMC_SendCommand(unsigned char ucCommand, unsigned long ulParam, unsigned char ucRespType)
{
  unsigned char uc_MaxErrors = 0xFF;
  
  SPI_WriteByte(0xFF);
  SPI_WriteByte(ucCommand);
  SPI_WriteByte((unsigned char) (ulParam>>24));
  SPI_WriteByte((unsigned char) (ulParam>>16));
  SPI_WriteByte((unsigned char) (ulParam>>8));
  SPI_WriteByte((unsigned char) (ulParam));
  SPI_WriteByte(m_ucCrc);

  // Responce is R1 must read allways
  do
  {
    ucRespData[0] = SPI_ReadByte();
  }while((uc_MaxErrors--)&&(ucRespData[0] & 0x80));
  

  switch(ucRespType)
  {
  case ertR1:
    
    return;
  
  // Responce R2 is 2 bytes long  
  case ertR2:
    ucRespData[1] = SPI_ReadByte();
    return;
    
  // Responces R3, R7 are 5 bytes long
  case ertR3:
  case ertR7:
    ucRespData[4] = SPI_ReadByte();
    ucRespData[3] = SPI_ReadByte();
    ucRespData[2] = SPI_ReadByte();
    ucRespData[1] = SPI_ReadByte();
    return;
  }
}


Итак, параметр ucCommand это, собственно, команда, которую необходимо отправить карте памяти, ulParam – параметр для данной команды, а ucRespType – это тип ответа, который карта должна предоставить нам на эту команду. Допустим, эта команда CMD0. Из datasheet’а видно, что параметров эта команда не имеет, а ответ на нее R1. Тогда отправка CMD0 быдут выглядеть следующим образом:

MMC_SendCommand(CMD0, 0x00, ertR1);

В ходе выполнения этой функции будет выполнена следующая последовательность операций:
1) Отправлена команда CMD0 (1 байт)
2) Отправлен параметр для CMD0 равный 0х00 (4 байта)
3) Отправлен CRC для этой команды(1 байт)
4) Чтение ответа от карты (карта может ответить на команду не сразу)
Напомню, что общий вид команды для карты памяти выглядит следующим образом:

{{0x40 + CMDx}, {0x????????}, {0x??}}
Номер команды(1 байт) Аргумент команды(4 байта, тип целое 32 бита) CRC(1 байт)

Так же мы помним, что ответ от карты памяти мы можем отличить от потока байтов 0xFF по 7-му биту прочитанного байта (он всегда равен 0). В свою очередь любой ответ содержит в себе первым байтом ответ R1. Например, ответ R2 есть ответ (R1, 1 байт данных R2). Таким образом, после каждой команды мы выделяем ответ R1, а затем, в зависимости от параметра ucRespType, читаем остальные байты ответа(если он отличен от R1). Стоит пояснить, что в этой функции используется переменная m_ucCrc, которую я сделал глобальной. Мы помним, что поле CRC в режиме SPI игнорируется для всех команд, кроме CMD0 и CMD8. Поэтому перед вызовом этой функции для этих двух команд, мы должны присвоить переменной m_ucCrc верное значение. Стоит заметить, что перед отсылкой команды, мы отправляем байт данных равный 0xFF. Это делается согласно datasheet’а, т.к. минимальная задержка между последним байтом ответа от предыдущей команды и первым байтом следующей должно пройти не менее 8 клоков по SCLK.
Вот вроде бы теперь нам понятно как отправлять команды, но есть еще один маленький моментик. В картах SD любого типа есть команды типа ACMDx. Эти команды имеют одну особенность: они отправляются непосредственно после команды CMD55(APP CMD). Таким образом, если нам необходимо отправить команду ACMD41(а ее индекс совпадает с индексом CMD41), то первой отсылается команда CMD55, а затем CMD41. Такое сочетание воспримется картой как команда ACMD41. О командах вроде как все.
Теперь пришло время рассказать об операции чтения данных с карты памяти. Как упоминалось ранее, читать (писать) можно байтами, блоками и последовательностью блоков. Так вот, все эти карты памяти поддерживают чтение (запись) блоками и последовательностью блоков. Возможность чтения (записи) с карты памяти байтами или несколькими байтами можно выяснить из регистра CSD. Значит первое, что мы должны узнать, как прочитать эти заветные регистры. Для этого предусмотрены соответствующие команды:
 CMD9 (SEND_CSD) – прочитать регистр CSD (card specific data)
 CMD10 (SEND_CID) – прочитать регистр CID (card identification data)

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



Вот теперь я поясню. Отправлять команду мы умеем, принимать ответ тоже знаем как, но вот теперь научимся читать данные. Между ответом на команду и данными карта может немного подумать и мы будем принимать от ее 0xFF (хотя можем и не принять). Так вот, первый прочитанный байт после ответа, равный 0xFE и будет маркером начала блока данных. Последующие после него байты и будут запрашиваемыми нами данные. Теперь приведу функцию, которая читает регистр CSD карты памяти.

void MMC_WaitDataToken()
{
  unsigned char uc_Answer;
  unsigned char uc_MaxErrors = 0xFF;

  do
  {
    uc_Answer = SPI_ReadByte();
  }while((uc_MaxErrors--)&&(uc_Answer != MMC_START_TOKEN_SINGLE));

  if (uc_Answer != MMC_START_TOKEN_SINGLE)
  {
    m_CardStatus = ecsERROR;
  }
}

void MMC_GetCSD(unsigned char* ucBuffer)
{
  if (m_CardStatus != ecsOK)
  {
    return;
  }
  
  MMC_ACTIVATE();
  MMC_SendCommand(MMC_SEND_CSD, 0x00, ertR1);
  if (0x00 == ((CCardR1*)&ucRespData)->ucData)
  {
    MMC_WaitDataToken();
    SPI_ReadBuffer(ucBuffer, 16);
    SPI_SendConst(0xFF, 2);
  }
  else
  {
    m_CardStatus = ecsERROR;
  }

  MMC_Finish();
}


Здесь функция WaitDataToken() как раз и ищет маркер начала данных. Как только мы нашли этот байт, мы читаем содержимое регистра CSD. Отмечу также, что после того, как мы прочитали все запрашиваемые данные, карта добавит к последнему байту еще 2 байта CRC, которые мы тоже должны прочитать. Надеюсь теперь прочитать регистр CID для Вас не составит труда=)
Да, чуть не забыл. Содержимое регистров для карт MMC, SDSC и SDHC разнятся. Их значения можно взять из датащитов, которые я прикрепил к топику.
Прочитав регистр CSD, мы точно знаем, что может карта (почти все ее характеристики). Но мы говорим про чтение, а значит нас интересуют следующие биты:
READ_BL_PARTIAL — чтение блока данных размером меньше блока карты памяти
READ_BLK_MISALIGN — чтение с нарушение границ блока
READ_BL_LEN — максимальный размер блока чтения данных (насколько я знаю, максимум для
любых карт 512 байт)

Теперь, если бит READ_BL_PARTIAL установлен, то мы можем читать блоки данных, меньшие по размеру чем 512 байт (READ_BL_LEN), но главное чтобы чтение данных не пересекало границу блока (скажем так нельзя прочитать 10 байт начиная с адреса 503). Команда CMD16 (SET_BLOCKLEN) в этом случае устанавливает размер блока для чтения. Если READ_BLK_MISALIGN установлен, можем читать как нам приспичит=) с нарушением границ блока. Стоит, однако, помнить, что только что сказанное не относится к картам SDHC, где размер блока чтения всегда равен 512.
Уточнив для себя все необходимые параметры, мы можем приступать к чтению непосредственно данных, записанных на карту памяти. Я приведу пример функции для чтения блока данных с карты памяти равного 512 байт, но отмечу заранее, что адресация дискового пространства для карт SDHC(SDXC) отличается от других карт.

void MMC_ReadBlock(unsigned long ulAddress, unsigned char* ucBuffer)
{
  if (m_CardStatus != ecsOK)
  {
    return;
  }

  MMC_ACTIVATE();
  MMC_SendCommand(MMC_READ_SINGLE_BLOCK, ulAddress, ertR1);

  if (0x00 == ((CCardR1*)&ucRespData)->ucData)
  {
    MMC_WaitDataToken();
    SPI_ReadBuffer(ucBuffer, MMC_BLOCK_SIZE);
  }
  else
  {
    m_CardStatus = ecsERROR;
  }

  MMC_Finish();
}


Эту функцию можно использовать для любых карт. Следует только помнить, что ulAddress для карт MMC, SDSC это линейный адрес начала данных, а в случае с SDHC(SDXC) это LBA (логический адрес блока, т.е. если нужно прочитать блок данных с адреса 1024, то ulAddress должен быть равен 1024/512 = 2).
Лиха беда начало. Осталось допилить самое малое – запись данных на карту памяти=) В принципе, для нас с вами на текущем уровне это не составит труда. Из регистра CSD имеем значение битов:
WRITE_BL_PARTIAL — запись блока данных размером меньше блока карты памяти
WRITE_BLK_MISALIGN — запись с нарушение границ блока
WRITE_BL_LEN — максимальный размер блока записи данных (насколько я знаю, максимум для
любых карт 512 байт)

Теперь, если бит WRITE_BL_PARTIAL установлен, то мы можем записывать блоки данных, меньшие по размеру чем 512 байт (WRITE_BL_LEN), но главное чтобы чтение данных не пересекало границу блока (скажем так нельзя записать 10 байт начиная с адреса 503). Команда CMD16 (SET_BLOCKLEN) в этом случае устанавливает размер блока для записи. Если WRITE_BLK_MISALIGN установлен, можем писать как нам приспичит=) с нарушением границ блока. Стоит, однако, помнить, что только что сказанное не относится к картам SDHC, где размер блока записи всегда равен 512.

void MMC_WriteBlock(unsigned long ulAddress, unsigned char* ucBuffer)
{
  if (m_CardStatus != ecsOK)
  {
    return;
  }

  MMC_ACTIVATE();
  MMC_SendCommand(MMC_WRITE_SINGLE_BLOCK, ulAddress, ertR1);

  if (0x00 == ((CCardR1*)&ucRespData)->ucData)
  {
    SPI_WriteByte(0xFF);
    SPI_WriteByte(0xFF);
    SPI_WriteByte(MMC_START_TOKEN_SINGLE);
    
    SPI_WriteBuffer(ucBuffer, MMC_BLOCK_SIZE);
    
    SPI_WriteByte(0xFF);
    SPI_WriteByte(0xFF);
    while(SPI_ReadByte() == 0x00);
  }
  else
  {
    m_CardStatus = ecsERROR;
  }

  MMC_Finish();
}


Эту функцию можно использовать для любых карт. Следует только помнить, что ulAddress для карт MMC, SDSC это линейный адрес начала данных, а в случае с SDHC(SDXC) это LBA (логический адрес блока, т.е. если нужно записать блок данных с адреса 1024, то ulAddress должен быть равен 1024/512 = 2).
Вроде бы основное что хотел рассказать – рассказал. Надеюсь статьи были интересными и полезными. Ниже файлы проекта прошивки и программа для PC. Отмечу, что программка набросана на скорую руку и сейчас расписывает дынные для SDSC (CSD версии 1.0). Остальные лень было прикручивать, хотя если кому оч нужно будет, то допилю. Старался от души. Буду рад Вашим комментариям, критике, советам и предложения. Жду…

Upd 1.
Обновил прошивочку и програмку для сабжа. Прикреплено v1.1 ;)
Upd 2.
Как верно поправил меня тов duke_pm вот здесь, команда записи страницы в прикрепленном коде может не дожидаться окончания записи. Потому нужно прочесть DataResponce от карточки перед ожиданием окончания условия Busy. Спасибо за замечание!
  • +4
  • 14 сентября 2011, 01:17
  • lleeloo
  • 2
Файлы в топике: mmc_sd.zip, mmc_sd_1.1.zip

Комментарии (50)

RSS свернуть / развернуть
Спасибо за статью и — отдельно — за исходники.
скажем так нельзя записать 10 байт начиная с адреса 502
Разве? 10 байт с адреса 502 как раз укалдываются в 512 (если READ_BL_LEN равен 512).
0
Если бы счет адресов шел с 1 тогда бы 502+10=512, да — получилось бы, но тут то счет с 0, потому максимум 0x01FF = 511.
-1
Хм. Только что вот для проверки открыл WinHex. Встал на offset 502 и записал блок размером в 10 байт. Блок соориентирован как раз до границы сектора.
0
Тут не совсем все так, как может показаться. Winhex работает с картой памяти через драйвера системы. Я больше чем уверен, что на карте было перезаписано 2 блока. Хотя я понял про что ты говоришь, сча чтоп небыло кофликту поправлю =)
0
10 байт с адреса 502 не будут пересекать границу блока. Примерно также, как не будут пересекать 2 байта с адреса 510. Это будут байты 510 и 511. Ну ты понял)
0
тупанул, согласен. Писал ночью, сонный=) полность согласен, но уже раскаялся и поправил!)
0
Выставил бы статью наконкурс
+1
  • avatar
  • Geban
  • 14 сентября 2011, 11:57
Кстати да. Вполне достойный труд
0
Я бы попробывал, но не в курсе как =( Не примите за наглость, но в вопросе пользования форумом все еще трудно… Если не трудно, ткните носом=)
0
Вверху сайта, в шапке, есть раздел справочная. Там про вступление в блоги. Там же, в шапке, условия конкурса и правила конкурсных постов.
0
Ди! А что за призы-то в этот раз? :)
0
Загляни в раздел конкурса. Там все призы указаны. Ну не все, только первый и третий. Второй еще не выкристаллизовался.
0
Видимо в вопросе конкурса я не в лодке =( Не смогу разместить внешнюю ссылку. Ну ничего, главное чтобы интересныз статей было бы побольше.
0
даже на вконтакте не асилишь? )
0
Не имею учетной записи. Да и наверное мои труды не особо подходят для конкурса…
0
Что, даже на форумах никаких не зареган чтоли? Вполне можно баннер в подпись запихнуть, у него и размер подходящий.
Да и наверное мои труды не особо подходят для конкурса…
Да уж подостойнее иных. Вон того «учебного курса по микропаскалю» например.
0
  • avatar
  • Vga
  • 15 сентября 2011, 15:08
Перенес в блог авр. Просьба следующую статью цикла (если будет) выкладывать туда
0
А почему в авр-то? MMC/SD прекрасно и из другого МК юзать мона, тока переписать работу с SPI.
0
  • avatar
  • Vga
  • 14 сентября 2011, 19:52
Я использую щас на LPC. Давай в конкурс, может дадут чего нить :)
0
Код на авр приведен.
0
По большей части код тут от хардвера абстрагирован. Точнее не вижу вообще ни одной AVR-specific строчки.
Хотя другого блога для таких статей все равно нету.
0
  • avatar
  • Vga
  • 16 сентября 2011, 06:16
В заголовке статьи явно написано «и AVR» :)
0
И да. Если будете править, то статя улетит в ваш персональный блог. Вам надо подключиться к блогу авр. Чтобы не таскать запись туда сюда. Т.к. адреса на статью меняются, а это не есть хорошо
0
А по какому алгоритму считается CRC команд и данных? Мож можно ее считать для очередного байта, пока следущий принимается.
0
  • avatar
  • Vga
  • 16 сентября 2011, 06:17
Если необходимо посчитать CRC команды, то алгоритм следующий:
unsigned char calc_crc7(unsigned char *ptr, signed char count ){
  char crc=0;
  unsigned char i,data;
  while (count--) {
    data=*ptr++; 
    for (i=0;i<8;i++) { 
      crc <<= 1; 
      if ((data & 0x80)^(crc & 0x80)) crc ^=0x09; 
      data <<= 1; 
    } 
  } 
  return((crc<<1)|1); 
}


здесь передается массив из: команда(1байт), аргумент(0х??,0x??,0x??,0x??)

значение полученное на выходе этой функции является последним байтом команды (CRC)

Для данных используется алгоритм вычисления CRC16. Но я не вижу смысла его изпользовать в режиме SPI.
0
Ну, во первых, некоторые карточки, говорят, CRC по умолчанию включают. Ну и ИМХО не проблема его побайтно посчитать параллельно с отправкой команды.

Аналогично и для данных. Понадежней будет, если CRC проверять, а вычисление его вроде не столь уж и накладно.
0
  • avatar
  • Vga
  • 16 сентября 2011, 14:31
Не проблема его(CRC) выключить. Он используется в протоколе MMS(SD), где присутствует битовый поток и высокие скорости передачи данных. SPI сам по себе надежен. Вы же не считаете CRC при работе с другой переферией по SPI =)
0
Она его просто не держит)
Ну бывают еще грабли с контактом в слоте или внезапным выпадением карты.
SPI сам по себе не более надежен чем «битовый поток», никаких средств обеспечения целостности инфы он не предоставляет, и даже никаких мер противодействия помехам (как, например, дифференциальный сигнал у USB) не обеспечивает. Спасает малая длина проводов и то, что МК не потянет высокие частоты SPI (хотя и 10МГц не так уж мало).
0
  • avatar
  • Vga
  • 16 сентября 2011, 14:52
Для включенной по умолчанию CRC делают финт ушами и тупо подставляют зарание посчитанное. 0x95 вроде. карта её принимает и отключает проверку црц, последующие команду идут с той же самой црц (для них не правильной) но проверки больше нерт и она благополучно игнорируется.
0
я же вроде расписал про црц. 0х95 валидно только для цмд0! Если вы вниматель гляните на инициализацию карты, то заметите что для цмд8 црц другой. И это не финт ушами а так написано в стандарте. При переходе в спи режим, проверка црц отключена по умолчанию, поэтому можете в режиме спи передавать место срс любой байт;)
0
Спасибо большое за статью. Прописал Ваши алгоритмы на Bascom-e, все вроде работает, но иногда в в произвольные моменты времени карта виснет и вывести ее из этого состояния можно только коммутацией питания. У Вас в софте есть функция MMC_PowerDown(), в комментариях стоит «Setup _CS pin and switch off power from card», планировалось дергать питание карты? Это нормальная ситуацияя или я что-то не догоняю?
0
Тут это сугубо для экономии питания в предыдущем проекте использовалось. В этом проекте убрано и не нужно. Странно, что карточка вешается. У меня ни разу такого не было.
0
Спасибо, еще вопрос:
READ_BL_LEN — максимальный размер блока чтения данных (насколько я знаю, максимум для
любых карт 512 байт)
У меня для теста одна карточка 512 Mb и две по 2Gb. При чтении CSD, READ_BL_LEN и WR_BL_LEN 512 байт только для 512Mb карты, для 2-х гиговых указан 1024 байт (0A hex). Попробую писать — читать по килобайту. Если не установлен WRITE_BL_PARTIAL, а я пытаюсь писать не полностью блок (512 байт), потом поднимаю CS, может — ли карта после этого работать как-то неправильно?
0
Если бит WRITE_BL_PARTIAL не установлен, то информация записана не будет если пишете не полный блок. Вообще можно прочитать статус карты, или анализировать ответ от команды. Вполне возможно, что после такой операции карта и вешается.
0
Есть команда устанавливающая размер блока. посмотрите в примерах ElmChan'а.
0
WRITE_BL_PARTIAL не установлен, значит запись ведется блоками по WR_BL_LEN. Команда SET_BLOCK_LEN в таком случае игнорируется =( Для карт SDHC размер блока чтения записи фиксированный и, насколько знаю я, не изменяется(могу ошибиться, лень лезть в спецификацию).
0
Вроде немножко разобрался, в спецификации для 2Gb карт есть следующее:
To make 2GByte card, the Maximum Block Length (READ_BL_LEN=WRITE_BL_LEN) shall be set to
1024 bytes. However, the Block Length, set by CMD16, shall be up to 512 bytes to keep consistency
with 512 bytes Maximum Block Length cards (Less than and equal 2GByte cards).
Так, что писать — читать приходится блоками по 512 байт.
0
Исправил ошибки в программе и прошивке. Добавлена функциональность.
0
while(SPI_ReadByte() == 0x00);

Прошу прощения за идиотский вопрос, но не должно ли здесь быть
while(SPI_ReadByte() != 0x00);

?
Ответом карты на команду записи служит R1, а значит, что только значение 0x00 свидетельствует об успешном прожёвывании команды и отсутствии ошибок.
Или я гоню?
0
хороший вопрос!
в этой статье
как раз ловят ненулевое значение. и еще одно отличие ответ R1 они ждут 0x05!!! хотя по чану положительный отвтет 0х00!!!
короче полная неразбериха
0
Если надоест пытаться понять что хотел сказать автор статьи, всегда можно почитать даташит.
0
в том то и дело что расхождение с даташитом есть, и как после этого пример работает?!
0
Блин, тов. Lifelover дал вам обоим(в том числе и newboy) дельный совет: читайте даташит!) Есть еще время с условием BUSY. Короче читать даташит и смотреть временные диаграммы…
0
А хотя хрен со мной, поясню…
При окончании записи если все ОК, то R1 = 0x00 и BUSY будет нулями пока карта занята записью данный на физ. носитель, отсюда растут и ноги. Как бы так=)
0
Во-первых спасибо вам большое за доступное описание работы с SD/MMC!
До этого в моем понимании протокола было несколько темных моментов.
Однако должен не согласится с этим утверждением про запись. После того как карта примет блок данных и 2 байта crc она отвечает не R1!!!
Она отвечает Data Response который гарантированно не 0. Таким образом ваш код для записи блока не будет дожидаться окончания записи.
Код окончания записи блока должен выглядеть так:
SPI_WriteByte(0xFF);
SPI_WriteByte(0xFF);
unsigned char DataResponse = SPI_ReadByte();
while(SPI_ReadByte() == 0x00);
0
Полностью согласен с Вами. Ответ карты после блока данных и 2-х байт CRC будет DataResponce(1 байт) который гарантированно не 0. Спасибо за замечание, но поправить статью уже не могу, слишком давно это было, движок не позволяет. Если Модератор позволит внести изменения, то обязательно дополню, ссылаясь на Вашу поправку. Еще раз спасибо за внимательность!
+1
Все поправил, ссылка на Ваш «Патч» оставил в Update.
0
Все описанное, это если заливать необходимое содержание через МК посредством внешнего интерфейса или данными, полученными в процессе «работы» МК. Я правильно понял? Если мне необходимо на карту заранее, что-то положить, то надо развертывать файловую систему?
0
  • avatar
  • DVF
  • 28 августа 2014, 17:01
Это всего лишь описание процедур записи.чтения сектора на карту. Как вариант можно использовать как низкоуровневые функции для всяких драйверов фат и прочих систем
0
А как записать свои значения в регистр CSD? Там ведь некоторый поля доступны для записи.
Как можно записать свой CID? Насколько я знаю, некоторые производители для регистров используют память EEPROM.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.