SD/MMC карта памяти и микроконтроллер AVR (часть 1) Базовые операции.
Работа SD/MMC карты с микроконтроллером AVR
Как-то давно хотел себе сделать логгер температуры, возникла необходимость использовать внешнюю EEPROM память. Прикинув, что в наличие имеется 512кб память с i2c интерфейсом, сделал на ней экспериментальный логгер. Но когда возникла необходимость скинуть данные на компьютер для последующей обработки, то вылезли трудности с написанием софтины, которая бы считывала данные с EEPROM и писала бы их в текстовый файл.
Софтину я написал, только работала она как-то корявенько и медленно, для меня сойдет, а вот для массового производства корявости недопустимы. Захотелось писать данные на карту памяти, которую можно потом вставить в карт-ридер и через проводник перекинуть на компьютер, где их уже можно обрабатывать чем удобно. Для этого нужно уметь собственно писать данные на карту памяти, и знать файловую систему FAT, чтобы карточка распозналась компьютером. С этого вступления хочу начать небольшой цикл из 3-х статей, в котором я напишу про то, как работать с SD/MMC картой памяти и файловой системой FAT.
Карточку SD я выбрал из-за того, что внутри ее есть встроенный контроллер и что с ней можно работать в SPI 0 режиме (положительный синхроимпульс, защёлкивание по переднему фронту, сдвиг по заднему фронту), с которым довольно просто совладать. Посмотрим на распиновку(цоколевку) SD карты:
Видно, что карточка имеет 9 контактов, предназначение их следующее.
При работе с микроконтроллерами режима SPI за глаз хватает по скорости, потому использовать родной режим работы карты памяти будет нецелесообразно. Нам понадобятся пины DI, DO, CLK, CS для передачи данных и VSS, VDD для питания. Теперь немного про питание, SD карта требует от 2,7В до 3,6В, и тока до 100мА. Также если планируется горячее подключения, нужно предусмотреть просадку питающего напряжения при подсоединении карты. Иначе питание может просесть до такого уровня, когда BOD детектор сработает и ресетнет микроконтроллер. Этого можно избежать, налепив по питания конденсаторов и индуктивность. Учитывая все это, была слеплена стендовая платка для работы с SD картой:
Схема платки показана ниже:
Для питания карты памяти предусмотрен стабилизатор на 3,3В - LP2980-3.3. Его обвязка из конденсаторов C1,C3 – 100мкф, танталовые; C2,C4 – 0,1мкф, керамика; L1 – индуктивность на 22мкГн. Для сопряжения TTL уровней сигналов предусмотрены резистивные делители R2-R4 по 5,6кОм; R5-R7 по 10кОм. Светодиод D1 – сигнальный, для него ограничивающий резистор R1 – 470 Ом. Также на разъеме оказались выводы WP и INS, отслеживая состояния которых можно понять, защищена ли карта от записи механической защелкой, и присутствует ли карта в разъеме соответственно. Далее дело за подключением, все сигналы с карты подключил к PortB микроконтроллера. Нежелание использовать аппаратный SPI микроконтроллера буду аргументировать плохой переносимостью кода на разные модели микроконтроллеров. Для работы с SD картой сразу буду использовать ATmega32 (при работе с FAT нам понадобится около 20кб флеша). Хотя можно использовать хоть Atmega8, благо код для этого переделывать не нужно. Схема подключения к микроконтроллеру показана ниже:
Тактовый генератор – встроенный RC на 8Мгц, хотя можно использовать любой, но с 8Мгц работает шустро. Конденсаторы C5,C7 – по 100мкФ, электролиты, С4,С6 – 0,1мкф, керамика. Так как мы будем передавать большие объемы данных (стандартный блок – 512 байт), то выводить их будем по UART на компьютер в программу Terminal 1.9. Про сопряжение UART микроконтроллера с компьютером я писал уже неоднократно вот здесь, здесь и даже здесь.
Теперь у нас есть все железо для экспериментов с картой памяти. Но прежде чем перейти к программной части, упомянем, что карта типа MMC также может работать в SPI режиме. Для ее управления также стоит использовать выводы DI, DO, CLK, CS. Схемотехнические рассуждения для MMC такие же, как и для SD карты. Распиновка MMC карты показана ниже:
Перейдем к программной части. Рассмотрим, как инициализировать карту, и писать/читать из нее. Для перехода в режим SPI нужно дождаться, пока питающее напряжения на карте достигнет 3В (несколько миллисекунд, после включения) и подать более 74 импульса на выводе CLK, при высоком уровне на CS и DI выводах.
После нужно выставить на CS нулевой уровень, далее карта входит в режим SPI, теперь для успешной работы следует подать команды сброса и инициализации CMD0, CMD1 прежде, чем мы сможем писать/читать данные из карты. Команд для карты довольно много, часть из них приведена в таблице ниже:
Команд много, но основанная масса работы производится командами CMD0, CMD1 (сброс и инициализация) CMD17 (чтение), CMD24 (запись). Весь перечень команд и как с ними работать, можно просмотреть в родной спецификации SD карты на английском.
Рассмотрим формат команды для SD карты.
Сперва идет индекс команды. Индекс команды в десятичном виде определяется как 64+имя команды. Далее следует 4 байта аргументов (данные, адрес), после следует 7-ми битная контрольная сумма. После успешной отправки команды следует послать байтовую паузу из 8*N тактовых импульсов (N - целое), после чего карта ответит. Ответ может быть типа R1,R2,R3. В нашем случае, ответы будут только типа R1. Поэтому рассмотрим только его.
Старший бит R1 всегда равен 0. Назначения остальных битов хорошо видно с рисунка.
Рассмотрим процесс инициализации карты памяти командами CMD0,CMD1. Сперва, при высоком уровне на выводах CS и DI подаем 80 тактовых импульсов на вывод CLK. Далее на все время работы с картой сажаем CS на землю, подаем команду CMD0, контрольная сумма для которой равнa 0x95 (контрольная сумма в нашем случае нужна только для команды CMD0, в остальных случаях она не проверяется, поэтому все время будем использовать 0х95 как контрольную сумму). Далее, после байтовой паузы, карточка должна ответить 0х01, что означает, что она вошла в SPI режим и готова принимать команды. Теперь подаем команды CMD1, и после паузы ожидаем от карточки ответа 0х00, которые говорит о том, что карта готова к обмену данными.
Обмен данными между картой памяти и микроконтроллером будет производиться стандартными блоками по 512 байт. Адресация карты побайтная, начиная с нуля, но считывать данные можно только блоками. Адресом блока служит первый его байт. То есть 0-й блок имеет адрес 0х0000, 1-й блок - 0х0200, 2-й блок – 0х400 и т.д. (справедливо для размера блока 512 байт). В SDHC картах адресация поблочная, адресом блока служит его номер. Операция чтения блока производится в следующем порядке. Подается команда CMD17, байтовая пауза, если принимается ответ 0х00, то после еще одной байтовой паузы принимается блок данных, структура которого показана ниже.
Некое подобие временной диаграммы операции чтения можно посмотреть на рисунке ниже
Как видно, блок начинается с байта 0хFE (для команд CMD17/18, CMD24), далее идет 512 байт информации и 2 байта контрольной суммы (которая по умолчанию не используется). Операция записи производиться похоже, команда CMD24, пауза, ответ карты 0х00, и блок данных (так как контрольная сумма не проверяется, то ее поле можно заполнить случайно ). Далее следует ответ карты про прием блока данных.
После чего busy состояние, когда карта записывает полученные данные. Временная диаграмма операции записи приведена ниже.
Для подтверждения вышесказанного хочу привести пример работы с картой. Запишем в блок под номером 1 (адрес 0х0200) какие-то циклически повторяющиеся данные, например чередующиеся цифры от 0 до 9, а потом считаем этот же блок и посмотрим, записались ли наши данные. Для этого была написана небольшая программка :
#include <avr/io.h> //Cтандартная библиотека ввода/вывода #include <string.h> //Библиотека для работы с строками #define DI 0 #define DO 1 #define CLK 2 #define CS 3 #define INS 4 #define WP 5 char buffer [512]={}; //Буфер данных для записи/чтения //Программа инициализации UART void uart_init(void) { UBRRH = 0x00; //256000 битрейт, 1 стоп бит, без проверки четности UBRRL = 0x01; UCSRA = 0x00; UCSRB = (1<<RXEN)|(1<<TXEN); //Прием и передача разрешена UCSRC = (1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0); } //Программа передачи байта по UART void uart_transmit(unsigned char data) { while ( !( UCSRA & (1<<UDRE)) ); UDR = data; } //Программа приема байта по UART unsigned char uart_receive (void) { while ( !(UCSRA & (1<<RXC)) ); return UDR; } //Программа передачи строки по UART void uart_transmit_message(char* msg) { unsigned char i; i=0; //Начальное значение переменной //Цикл до перебора всех элементов строки while ((i<256)&(msg[i]!=0x00) ) { //Отправка поэлементно символов строки uart_transmit(msg[i]); i++; //Увеличиваем номер элемента строки } } //Программа передачи байта карте SD|MMC void spi_transmit (unsigned char data) { unsigned char i; for (i=0;i<8;i++) //Цикл перебора битов отправляемого байта { if ((data&0x80)==0x00) //Если все данные переданы PORTB&=~_BV(DI); //Выставить бит данных else PORTB|=_BV(DI); data=data<<1; PORTB|=_BV(CLK); //Импульс asm("nop"); //Пауза в 1 такт PORTB&=~_BV(CLK); } } //Программа приема байт от карты SD|MMC unsigned char spi_receive (void) { //Декларация переменных unsigned char i, res=0; for(i=0;i<8;i++) { PORTB|=_BV(CLK); //Фронт импульса res=res<<1; if ((PINB&_BV(DO))!=0x00) res=res|0x01; //Считать бит данных PORTB&=~_BV(CLK); //Спад испульса asm("nop"); } return res; } unsigned char sd_cmd(char b0, char b1, char b2, char b3, char b4, char b5) //Отправка команды карте SD|MMC { unsigned char res; long int count; spi_transmit (b0); //Передать индекс команды spi_transmit (b1); //Передать аргумент spi_transmit (b2); spi_transmit (b3); spi_transmit (b4); spi_transmit (b5); //Передать CRC count=0; do { //Подождпть R1 ответа res=spi_receive(); count=count+1; } while ( ((res&0x80)!=0x00)&&(count<0xffff) ); return res; } unsigned char sd_card_init(void) { unsigned char i,temp; long int count; if ((PINB&_BV(INS))!=0x00) return 1; //Проверка карты в слоту for (i=0;i<10;i++) //80 импульсов spi_transmit (0xff); PORTB&=~_BV(CS); //CS опустить temp=sd_cmd (0x40,0x00,0x00,0x00,0x00,0x95); //CMD0 if (temp!=0x01) return 3; //Выйти, если ответ не 0х01 spi_transmit (0xff); count=0; do{ temp=sd_cmd (0x41,0x00,0x00,0x00,0x00,0x95); //CMD1 spi_transmit (0xff); count=count+1; } while ( (temp!=0x00)&&(count<0xffff) ); //Ждем 0x01 ответа R1 if (count>=0xffff) return 4; return 0; } unsigned char read_block (char* buff, unsigned char a1, unsigned char a2, unsigned char a3, unsigned char a4) { unsigned char temp; long int count; if ((PINB&_BV(INS))!=0x00) return 1; //Проверка карты в слоту temp=sd_cmd (0x51,a1,a2,a3,a4,0x95); //CMD17 if (temp!=0x00) return 5; //Выйти, если ответ не 0x00 spi_transmit (0xff); count=0; do{ //Ждем начала пакета данных temp=spi_receive(); count=count+1; } while ( (temp!=0xfe)&&(count<0xffff) ); if (count>=0xffff) return 5; for (count=0;count<512;count=count+1) //Сохраняем данные buff[count]=spi_receive(); spi_receive(); //Сохраняем CRC spi_receive(); return 0; } unsigned char write_block (char* buff, unsigned char a1, unsigned char a2, unsigned char a3, unsigned char a4) { unsigned char temp; long int count; if ((PINB&_BV(INS))!=0x00) return 1; //Проверка карты в слоту if ((PINB&_BV(WP))!=0x00) return 2; //Проверка write protect temp=sd_cmd(0x58,a1,a2,a3,a4,0x95); //CMD24 if (temp!=0x00) return 6; //Выйти, если ответ не 0x00 spi_transmit (0xff); spi_transmit (0xfe); //Начало пакета данных for (count=0;count<512;count=count+1) //Отослать данные spi_transmit(buff[count]); spi_transmit (0xff); //Отослать CRC spi_transmit (0xff); temp=spi_receive(); if ((temp&0x05)!=0x05) return 6; //Выйти, если данные не приняты count=0; do { //Ждем окончания busy состояния temp=spi_receive(); count=count+1; }while ( (temp!=0xff)&&(count<0xffff) ); if (count>=0xffff) return 6; return 0; } int main(void) { unsigned char temp; int i; PORTB=_BV(CS)|_BV(DO)|_BV(DI)|_BV(WP)|_BV(INS); //Инициализация портов DDRB=_BV(CS)|_BV(DI)|_BV(CLK); uart_init(); //Инициализация UART temp=sd_card_init(); //Инициализация карты if (temp==0x00) { uart_transmit_message("sd_card_init: initialization succes\r\n"); for (i=0;i<512;i=i+1) buffer[i]=0x30+(i%10); //Заполнить буфер 12345... temp=write_block(buffer,0x00,0x00,0x02,0x00);//Записать буфер if(temp==0x00) uart_transmit_message("write_block: block writte succes\r\n"); else if (temp==1) uart_transmit_message("write_block: fail, no card in the slot\r\n"); else if (temp==2) uart_transmit_message("write_block: fail, card write protected\r\n"); else uart_transmit_message("read_block: CMD24 fail\r\n"); temp=read_block(buffer,0x00,0x00,0x02,0x00); //Считать буфер if(temp==0x00) { uart_transmit_message("read_block: data block read succes:\r\n"); for (i=0;i<512;i=i+1) //Выслать буфер по UART uart_transmit(buffer[i]); } else if (temp==0x01) uart_transmit_message("read_block: fail, no card in the slot\r\n"); else uart_transmit_message("read_block: CMD17 fail\r\n"); } else if (temp==0x01) uart_transmit_message("sd_card_init: fail, no card in the slot\r\n"); else if (temp==0x03) uart_transmit_message("sd_card_init: CMD0 fail\r\n"); else uart_transmit_message("sd_card_init: CMD1 fail\r\n"); while (1); return 1; }
Код программы прост, название функций говорит само за себя. Вначале инициализируем UART, посредством которого будем связываться с компьютером на скорости 256кбод/с, с одним стоп битом, без проверки четности (если такая скорость покажется большой по ряду причин, то ее легко подредактировать в строке 16). Функция void uart_init(void) инициализирует UART; void uart_transmit(unsigned char х) – пересылает байт х по UART; unsigned char uart_receive(void) – принимает байт; void uart_transmit_message(char * msg) – пересылает строку, на которую ссылается msg по UART. Подключение карты к микроконтроллера определяется директивами define в начале программы. Рассмотрим функции работы c картой SD/MMC. Функции void spi_transmit(unsigned char x), unsigned char spi_receive(void) отвечают за пересылку байт по SPI; unsigned char sd_cmd(unsigned char b0, unsigned char b1, unsigned char b2, unsigned char b3, unsigned char b4, unsigned char b5) – отвечает за пересылку команды карте памяти.
b0 – индекс команды, b1-b4 – аргумент команды, b5 – контрольная сумма, функция возвращает ответ R1 от карточки. Пользовательские функции: unsigned char sd_card_init(void) служит для инициализации карты командами CMD0, CMD1, при успешном выполнении возвращает результат 0х00. Функции unsigned char read_block (char* buff, unsigned char a1, unsigned char a2, unsigned char a3, unsigned char a4) и unsigned char write_block (char* buff, unsigned char a1, unsigned char a2, unsigned char a3, unsigned char a4) служат для чтения/записи блока, на который указывает buff, по адресу a1-a4, в случае выполнения возвращают 0х00. Во всех пользовательских функция предусмотрена проверка наличия карты в слоте и режима Write Protect, который устанавливается механическим ползунком на самой карте памяти.
При внутрисхемном программировании следует вынимать карту из слота, иначе программатор не сможет залить прошивку в микроконтроллер (сигналы WP, INS при вставленной карте садят на землю выводы нужные для внутрисхемного программирования). Выполнение приведенной выше программы приведет к тому, что целостность файловой системы (существовавшей до этого на карточке) будет нарушена, так что перед экспериментами не забудьте скопировать с карточки всю важную информацию.
Вот что я увидел в Terminal 1.9 при запуске программы:
Решил перепроверить с помощью утилиты WinHex содержание первого блока карты памяти:
Результаты совпадают. Запись чтение удалось. Теперь смело можно писать/читать данные на карту. Про то, как писать данные в файлы и про файловую систему FAT я напишу чуть позже.
Cледующая статья цикла: "SD/MMC карта памяти и микроконтроллер AVR (часть 2) Система FAT, Petit FatFs."
Последняя стать цикла: "SD/MMC карта памяти и микроконтроллер AVR (часть 3) Система FAT, FatFs."
Код программы в формате проекта для AVR Studio 4.
Разводка печатной платы в формате .lay для программы Sprint Layout 5
Отличная статья,
Отличная статья, Респект!
Хотел спросить, вместо LP2980-3.3 можно поставить LP2981AIM5-3.3? и возможны ли допуски с индуктивностью и в каких диапазонах, а то у нас нет 22мкГн, есть 10 и 33 и какие можно использовать?
Спасибо!
Подойдет любой регулятор
Подойдет любой регулятор напряжения, который на выходе дает 3,3 Вольта. По поводу индуктивности 33мкГн будет не критично.
SDHC 16Gb
Уважаемый автор.
Я проштудировал всю родную спецификацию, ссылку на которую Вы дали (http://avrlab.com/upload_files/SD_card_Specification_Ver3.01_Final_10051...), но так и не понял, где Вы взяли ту последовательность действий инициализации SD карты, которую указываете:
"1. Сперва, при высоком уровне на выводах CS и DI подаем 80 тактовых импульсов на вывод CLK.
2. Далее на все время работы с картой сажаем CS на землю.
3. Подаем команду CMD0, контрольная сумма для которой равнa 0x95 (контрольная сумма в нашем случае нужна только для команды CMD0, в остальных случаях она не проверяется, поэтому все время будем использовать 0х95 как контрольную сумму).
4. Далее, после байтовой паузы, карточка должна ответить 0х01, что означает, что она вошла в SPI режим и готова принимать команды.
5. Теперь подаем команды CMD1, и после паузы ожидаем от карточки ответа 0х00, которые говорит о том, что карта готова к обмену данными."
Более того, в этом документе указано, что CMD1 зарезервировано и вместо нее рекомендуется использовать ACMD41, но не сразу после CMD0, а в такой последовательности CMD0 -> CMD8 -> ACMD41.
Причина моей критики заключается в том, что задача инициализации SD карты для меня сейчас является насущной, но что-то она пока не решается ни с помощью рекомендаций из английского оригинала, ни, к сожалению, с помощью Ваших.
Коммент увидел, завтра
Коммент увидел, завтра отвечу развернуто.
Эту последовательность команд
Эту последовательность команд уважаемый автор видимо взял не из указанного родного даташита на SD-карты, а из описания использования SD/MMS карт мистера ChaN'a (elm-chan.org/docs/mmc/mmc_e.html)
Здравствуйте! Почитал Вашу
Здравствуйте!
Почитал Вашу статью и пытаюсь повторить на ATmega16L (фиксированное питание 3.3 Вольта)
Взял карточку miro-SD с адаптером, но не знаю как подключить на основе Вашей статьи.
Подскажите пожалуйста! В секции Вашей программы
скажите, что именно необходимо написать вместо следующих строк,т.к. такие выводы на карточке( INS и WP) я не нашёл
Заранее Вам всем спасибо за ответ!
Чтобы не переделывать код,
Чтобы не переделывать код, просто подаем на эти пины 0В и работаем с карточной дальше. Можно убрать эти дефайны, сэкономить 2 пина, но придется убрать все упоминание из mmc.c и подредактитовать функцию disk_timerproc.
Нет 512 байт памяти
Очень занимательно, а если в контроллере ВСЕГО 512 байт памяти?
Сам с таким столкнулся. Возможное решение. В железо еще не закатал..
Народ, конструктивная критика приветствуется.
Логично, если нет целого
Логично, если нет целого куска памяти - нужно разбить на части :)
Только не совсем понятно зачем массив disk[N][MMC_BLOCK_SIZE]. Или это только для демонстрации?
От себя добавлю, если интересует последовательно чтение/запись - то легко реализуется - SPI протокол можно стопнуть в любой момент. Т.е. считали небольшой блок данных, заморозили состояние SPI, обработали данные, снова считали блок и т.д.
Интересовал произвольный доступ
disk - это демонстрация. Пример алгоритма так сказать.
Инетересовал произвольный доступ кратный блокам DATA_BLOK_SIZE. Как я понимаю, нельзя непрочитав весь сектор, начать считывать из другого?
И еще вопрос к автору:
unsigned char a4 - в функциях write_block, read_block всегда равен 0. Так? Тоесть как бы нет смысла его передавать и следить за ним.
Угу, сектор нужно дочитывать
Угу, сектор нужно дочитывать до конца.
В данном примере a4 все время остается равной 0х00. Но размер блока в некоторых устройствах может быть например 128 байт. Т.е. насовсем я бы ее не убирал.
Глюк или косяк?
День добрый..попробовал поиграться с подключением SD к STM8...на CMD0 получаю нормальный ответ 0х01, на следущую за ней CMD1 - опять 0х01 вместо 0х00...из-за чего это может происходить?
Это происходит из-за того,
Это происходит из-за того, что карте нужно время на инициализацию. Попробуй периодически спамить ее коммандой CMD1 до того времени, как она вернет 0х00. У меня это сделано так:
так и делал
Собственно, как раз так и делал - ответ перманентно 0х01...сегодня попробую с другой карточкой - может эта просто глючит...
Запись в SD
Добрый день !!!
Подскажите пожалуйста, как сделать так чтобы данные записывались в продолжении имеющихся данных на карте (без указания адреса).
Нужно постоянно отслеживать
Нужно постоянно отслеживать текущий адрес блока. Т.е.
запись(блок,адрес блока);
адрес блока = адрес блока + размер блока;
запись(блок,адрес блока);
...
Я бы загнал адрес в глобальную переменную, в функции write_block увеличивал эту глобальную переменную на 512 (стандартный блок) при записи каждого блока. И соответственно избавился от переменных а1,а2,а3,а4 в функции write_block.
Если я не ошибаюсь, на
Если я не ошибаюсь, на карточку 1Gb можно записать ровно 2000 блоков по 512 байт. Да?
Это 1Мб... В 1Гб 2000000
Это 1Мб... В 1Гб 2000000 блоков по 512байт
SD/MMC карта памяти и микроконтроллер AVR
spi_transmit и spi_receive проще объединить в одну функцию (ведь аппаратный spi именно так и работает)
Можно, но мне привычней с
Можно, но мне привычней с разными функциями для приема/передачи работать.
Хорошая статья, все понятно
Хорошая статья, все понятно написано без лишних заморочек.
Есть вопрос: для повторения данной схемы подойдет любая SD карта, или есть какие-то особые требования (производитель, объем и т.д.)?
Подойдет SD карта, каких-то
Подойдет SD карта, каких-то предпочтений производителя и объема нету. С картами SDHC не тестил, но думаю что заработает.
Респектище тебе!!! Очень
Респектище тебе!!! Очень полезный мануал
Здравствуйте! у меня есть
Здравствуйте!
у меня есть вопрос по схеме
Я не нашел слота для SD карты и подсоединяю на прямую к контроллеру и не пойму выводы WP и IN5 от контроллера должны идти на линию GND?
Да. При установленной карте
Да.
При установленной карте памяти на INS 0В.
При разрешенной записи на выводе WP 0В.