SD-карта на AVR: на USI и без FS

Как видно из заглавия, будем обходиться без «железного» SPI — на тиньках его нет. Зато есть USI, его и заюзаем. Можно, конечно, и программный SPI написать, но там нужен таймер, а их вечно мало. Также хочу отметить, что вряд ли этот опус можно считать полноценной статьей по SD-картам — скорее, некоторые наброски на тему подводных камней (потому и находится в личном разделе). В общем, отмазки закончились, к делу!

1. Щас я, по-быстрому...
… На десятом часу непрерывных измерений в химической лаборатории голова уже шла кругом: через пять минут закрывался универ, проскока так и не наступило, эксперимент загублен, все начинать сначала! Вот если бы оставить работающую установку еще на 2-3 часа… Но кто будет ночью записывать показания? Тогда-то и пришла мысль, что пора апгрейдить свой самопальный измеритель концентрации спиртов на attiny24 с датчиком MQ-3.

Пора бы запилить туда внутреннюю память — чтобы показания датчика сами каждую секунду писались, куда надо. Сначала думал в сторону EEPROM, но читать потом ее с компьютера… нет уж, увольте, SD-карта тут вне конкуренции! Конечно, для самой простой библиотеки FAT у 24-й тиньки никакой памяти не хватит, но мы люди не гордые — главное, записать, а уж сольем данные любой HEX-программой (лично у меня исторически сложилась любовь к WinHex).

Делов-то… на пару дней, думал я. Тема-то… должно быть, сто раз обсосана. Берем готовый код, берем для примера Attiny2313 и… Ничего не работает!

2. MOSI не MOSI, а MISO не MISO
Внимание, вопрос: как соединяются устройствами по SPI? Правильно, CS-CS, MOSI-MOSI… ну и так далее (да, есть варианты с односторонней передачей, но это не наш случай). На тиньках есть USI — полуфабрикат для SPI/I2C со своим блэкджеком и таймером. Выводы USI совпадают с пинами прошивки: MISO, MOSI, SCK… И вот скажите, не особо углубляясь в даташиты, как было бы логичней соединить мк с SD-картой? Лично я взял MOSI с тиньки с соединил с MOSI карты памяти — и так далее, в соответствии со схемой:



И вот еще вопрос: что означают DI и DO? Если MOSI — он везде MOSI (как и MISO), то назначение DI и DO зависит от того, в каком режиме работает устройство. В режиме Slave выводы DI/DO — это, соответственно, MOSI/MISO. А вот в режиме Master наоборот: DI — это MISO, а DO — это MOSI. Все становится логично, если знать расшифровку.

Подводный камень состоит как раз в том, что выводы подписанные в даташите, как MISO и MOSI, в USI-режиме выполняют функции DO и DI соответственно. То есть, если мы используем тиньку, как мастер, то вывод MISO — это на самом деле MOSI, а вывод MOSI — это MISO! То есть, при использовании USI в режиме Master соединять нужно так:



В качестве CS может выступать любой свободный пин мк. И да, не забываем, что напряжение на карте (как питающее, так и SPI) не должно превышать 3,6 В.

3. Пытаемся писать код
Понятно, что код писать я начал еще до того, как разобрался с MOSI/MISO и только потом укурился мануалами и гайдами — в конце статьи найдете ссылки по теме. В качестве полигона у нас будет Attiny2313.

3.1. Обозначаем ноги
Чтобы больше не путаться, заменим обозначения: MOSI на DI и MISO на DO. Соединение с картой памяти будет (см. выше): DI-DO, DO-DI, SCL-SCLK, CS-CS. В качестве CS у нас будет нога PB4. Поехали:

// Ножки интерфейса SPI
#define SPI_DDR DDRB
#define SPI_PORT PORTB
#define SPI_PIN PINB
#define DI 5
#define DO 6
#define SCK 7
#define CS 4 //любой свободный — я выбрал PB4

//отладочный светодиод
#define LED_DDR DDRB
#define LED_PORT PORTB
#define LED_TEST 3
#define CS_LOW() (SPI_PORT &= ~(1<<CS))
#define CS_HIGH() (SPI_PORT |= (1<<CS))


3.2. Инициализация ножек SPI
Обернем ее в функцию spi_init().

void spi_init() {
  SPI_DDR |=  (1<<SCK) | (1<<CS) | (1<<DO);
  SPI_DDR &=  ~(1<<DI); //вход без PullUp-подтяжки 
}


Интересный момент, если карта памяти и мк питаются от одного источника (напр., 3.3 В), то все выводы можно настроить на выход. Однако, если питание карты и мк ведется от разных источников (напр, 3 и 5 В), то работает только вариант выше. С чем связано, я так и не понял, может кто-то в комментах прояснит такое поведение, а пока не парьтесь и юзайте вариант, где DI (который был MOSI в режиме прошивки) настроен на вход.

3.3. Отладочная функция
Напишем простейшую функцию, которая будет мигать диодиком нужное количество раз, сигнализируя об ошибке или, наоборот, об успешном выполнении части программы:

void test(int j) {
  for (int i = 0; i<j; i++) {
   LED_PORT |= (1<<LED_TEST);
     _delay_ms(200);
   LED_PORT &= ~(1<<LED_TEST);
     _delay_ms(200);
  }
}


3.4. Посылка байта и текста
Тут я не буду углубляться в дебри USI, если кому интересно, в конце статьи приведена ссылка на годный гайд. Отмечу только, что паузы в 100 и 10 мкс внесены мною, потому что SD-карты не умеют работать на высоких скоростях, обычно для них предел 200-300 кГц. В общем, с параметрами _delay_us() в функции ниже можно поиграться:

unsigned int _spi_sendbyte(unsigned char b) {
  USIDR=b;
  USISR|=(1<<USIOIF);
  while((USISR&(1<<USIOIF))==0){
   USICR=(1<<USIWM0)|(1<<USICS1)|(1<<USICLK)|(1<<USITC);
   _delay_us(10); 
  }
_delay_us(100);
return USIDR;
}


Текст посылается проще некуда:

void _spi_sendtext(char *str) {
  int i = 0;  while (str[i]) {
    _spi_sendbyte(str[i++]); 
  }
}


Хинт: конструкцию (char *str) можно заменить на (char __flash *str), тогда строка будет храниться не в оперативке (которой у тиньки почти нету), а во флеше. Работает только для IAR, в AVR Studio свои приемы.

3.5. Main!
Примерно в этой части статьи мне надоело разбивать ее по главам
Теперь приступаем к основной части программы — main()


void main() {
   LED_DDR |= (1<<LED_TEST); //Порт светодиода на выход
   unsigned char status_; //сюда будем писать ответ карты
   spi_init(); //Инициализация ножек
   SPI CS_HIGH(); //Высокий уровень на CS 

   _delay_ms(100); //Ждем, пока SD-карта выйдет на режим


Перед началом работы с картой, ее надо перевести в SPI-режим. А перед этим — инициализировать — послать минимум 74 импульса на SCK, при этом MOSI и CS должны иметь высокий уровень. Чуть выше мы уже переключили CS. Что касается MOSI и SCK, то просто пошлем 10 раз байт FF по SPI. В байте 8 бит (sic!), в результате получится 8×10 — 80 импульсов. После этого устанавливаем на CS низкий уровень.

for (int i = 0; i < 10; i++) {
    _spi_sendbyte(0xFF); 
  } 
 
CS_LOW();


Идем дальше — переход в SPI-режим. Для этого служит команда CMD0. Вообще, команды состоят из 6 байт: байт на код команды, 4 байта на аргумент (если его нет, он все равно передается — четыре раза 0×00) и последний — байт контрольной суммы (CRC), который важен только для CMD0 (0×95) и CMD8 (0×87). После перехода в SPI контрольная сумма никак не используется и можно посылать что угодно (да хоть 0xFF).

//команда CDM0
  _spi_sendbyte(0x40); //индекс команды cmd0
  _spi_sendbyte(0x00); //первый байт пустого аргумента
  _spi_sendbyte(0x00); //второй байт пустого аргумента
  _spi_sendbyte(0x00); //третий байт пустого аргумента
  _spi_sendbyte(0x00); //четвертый байт пустого аргумента
  _spi_sendbyte(0x95); // CRC
  while ((status_=_spi_sendbyte(0xFF))!=0x01); //ожидание сигнала о принятия команды
  test(4);


В принципе, уже можно компилировать и проверять: если диодик мигнет 4 раза, то команда CMD0 прошла успешно, карта перешла в SPI-режим, можно проверять тип карты (CMD8). Если нет, проверяем все соединения и т.д. Вообще, на первых порах советую питать карту и микроконтроллер от одного источника 3–3,5 В, потому что согласование уровней — лишняя возможность допустить ошибку.

Проверяем тип карты (первая версия — старая, до 2 Гб, вторая — новая (SDHC, SDXC) — от 2 Гб и более):

_spi_sendbyte(0x48); //индекс команды cmd8
  _spi_sendbyte(0x00); //первый байт пустого аргумента
  _spi_sendbyte(0x00); //второй байт пустого аргумента
  _spi_sendbyte(0x01); //третий байт пустого аргумента
  _spi_sendbyte(0xAA); //четвертый байт пустого аргумента
  _spi_sendbyte(0X00); // CRC
    
  do { //ожидание сигнала о принятия команды (ответ R7 - 5 байт (1-ый байт это R1, далее 4-е байта нам не интересны))
      status_=_spi_sendbyte(0xFF);
        
      //если ответ R1 0x04 - "com are error", значит SD v.1 
      if (status_==0x04 || status_==5) { 
          test(1);                
      }                
      //если ответ R1 0x01 - "in idle state", значит SD v.2
      if (status_==0x01) { 
          test(2);        
      }
      
  } while((status_!=0x01) && (status_!=0x04) && (status_!=0x05));

 


И тут опять интересный момент: если перелопатить кучу разных примеров, везде проверяется ответ карты 0×01 (т.е. SD v.2) и 0×04 (SD v.1). Однако, то ли мне так повезло, то ли еще что, но моя карта упорно не хотела инициализироваться (следующий этап после проверки). Подставил в цикл

if (status != 0xFF && status != 0x01) test(status_);

В результате диодик мигнул пять раз! Дописал (последнее условие выше) проверку на ответ 0×05 — процесс пошел. Возможно, если углубиться в даташиты, надо проверять не 0×04, а совпадение по маске, тоже буду рад, если кто в комментариях ткнет носом.

Следующий шаг — инициализация microSD (до этого была другая инициализация — spi), которая для разных карт отличается. Для новых карт (SDHC, SDXC — ответ 0×01) можно прочитать здесь. Для моих целей хватает карты старого образца, потому рассмотрю только ее.

Итак, чуть выше функции main() пишем такой код:

void sd_init(void) {
  CS_LOW();

  while (1) {
   _spi_sendbyte(0x41); //CMD1
   _spi_sendbyte(0x00);
   _spi_sendbyte(0x00);
   _spi_sendbyte(0x00);
   _spi_sendbyte(0x00);
   _spi_sendbyte(0xFF);
   _spi_sendbyte(0xFF); //байт ожидания
   status_=_spi_sendbyte(0xFF); //проверяем статус
   if (status_ != 0xFF) {
    if (status_ == 0x0) break;
   }
  }

  CS_HIGH();
}


Ну и после проверки версии microSD (внутри функции main()) пишем одну-единственную команду:

sd_init();


Теперь создадим переменную, в которой будет храниться позиция (в байтах), с которой будем писать/читать.


  unsigned long int bpos;
  bpos = 0;


Операции чтения/записи осуществляются блоками. По умолчанию один блок — 512 байт. Для карт нового образца этот параметр изменить нельзя, а вот на старых SD-шках _теоретически_ можно задавать размер блока программно. Теоретически, потому что, как было разжевано во второй статье lleeloo , для этого нужно, чтобы были выставлены определенные биты CSD-регистра (это такая область памяти объемом аж 128 бит, где хранится почти вся инфа о SD-картах). Биты эти только для чтения и вы можете, конечно, попробовать заюзать команду CMD16, но лично на моей карте писать блоками менее 512 байт оказалось нельзя.

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

Теперь об операциях чтения-записи. Опять на время выходим за пределы main() и дописываем две (точнее три, но об этом позже) функции.

Запись на карту (первый аргумент принимает текст, второй — позицию, с которой пишем):

void sd_write(char *str, unsigned long int bpos) {
  CS_LOW(); //опускаем CS
  
  _spi_sendbyte(0xFF);
  
  //А тут определяем, с какого байта будем писать на карту
  
  
  _spi_sendbyte(0x58);
  _spi_sendbyte(((bpos & 0xFF000000)>>24));
  _spi_sendbyte(((bpos & 0x00FF0000)>>16)); 
  _spi_sendbyte(((bpos & 0x0000FF00)>>8));
  _spi_sendbyte(((bpos & 0x000000FF)));
  _spi_sendbyte(0x00); //CRC
 
  while (_spi_sendbyte(0xFF)!=0x0);
  
  test(2);
  
  _spi_sendbyte(0xFF); //посылаем один байт ожидания
  _spi_sendbyte(0xFE); //посылаем метку о начале пакета данных
//  for (int i=0; i<512; i++) 
//  _spi_sendbyte(0xAA);
  _spi_sendtext(str);
  _spi_sendbyte(0x00); //1-ый байт CRC
  _spi_sendbyte(0x00); //2-ой байт CRC
  
  while (_spi_sendbyte(0xFF)&0x5 != 0x5); //ожидание сигнала о принятия команды
  
  test(2);
  
  while (_spi_sendbyte(0xFF)!=0x00); //ожидание сигнала о принятия команды
  
  test(2);
  
  CS_HIGH(); //поднимаем CS
  
}


У меня тут натыкано тестовых вызовов, можете при желании поменять их значение, либо вообще убрать.

Теперь чтение (возвращать будем только 4 байта в переменную ret4b; кому нужно больше — поправите SIG_LEN). В качестве единственного аргумента — позиция чтения (кратно 512):

#define SIG_LEN 4
 

char * ret4b;

char * sd_read(unsigned long int bpos) { //возвращает только 4 байта из прочитанного блока
  CS_LOW();
  
  _spi_sendbyte(0xFF);
  
  //А тут определяем, с какого блока будем читать карту
  
  
  
  _spi_sendbyte(0x51);
  _spi_sendbyte(((bpos & 0xFF000000)>>24));
  _spi_sendbyte(((bpos & 0x00FF0000)>>16)); 
  _spi_sendbyte(((bpos & 0x0000FF00)>>8));
  _spi_sendbyte(((bpos & 0x000000FF)));
  _spi_sendbyte(0x00); //CRC
 

  while (_spi_sendbyte(0xFF)!=0x0);
  
  while (_spi_sendbyte(0xFF)!=0xFE); //ожидание сигнала о начале пакета данных
  

  for (int i=0; i<SIG_LEN; i++) {
    ret4b[i] = _spi_sendbyte(0xFF);
  }
  
  for (int i=0; i<(512-SIG_LEN); i++) {
    _spi_sendbyte(0xFF);
  }
  

  _spi_sendbyte(0x00); //1-ый байт CRC
  _spi_sendbyte(0x00); //2-ой байт CRC
  
  //while (_spi_sendbyte(0xFF)!=0x00); //ожидание сигнала о принятия команды

  
  CS_HIGH();
  test(2);
  return ret4b;
  
}


И, наконец, третья функция:

int chk_sig(char * r, char * sig) {
  for (int i = 0; i < SIG_LEN; i++) {
    if (r[i] != sig[i]) return 0;
  }
  return 1;
}


Эта функция сравнивает две строки. Для чего это нужно, чуть ниже.

Итак, возвращаемся обратно в функцию main(). После инициализации sd_init() можно писать на карту. Но с какой позиции вести запись? Логично, что с той, где нет важных данных.

Варианты есть разные, лично мне нравится такой вариант: каждый блок будем начинать с 4-байтной сигнатуры. Далее, при каждом новом включении, программа читает всю карту на наличие сигнатуры (например, банально «ZZZZ») и если в начале блока нет нужных 4-х байт, то запись будет вестись оттуда.

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

В общем, тупим в цикле, пока не найдем позицию, доступную для чтения:

while (chk_sig(sd_read(bpos), "ZZZZ")) {
    sd_init();
    bpos += 512;
  }


Обратите внимание, что sd_init() надо вызывать перед _каждым_ вызовом чтения/записи. Иногда может повезти, но если прозевать sd_init(), то программа вполне может зависнуть.

Ну вот, теперь, наконец-то, можем записать:

sd_init();
  test(2);
  sd_write("ZZZZКу! ЖПНР! Полный и непроходящий!", bpos);


Если вы все сделали правильно, то при каждом сбросе контроллер будет записывать 512 байт на новой позиции. Да, записывать можно меньше 512 байт, остальное будет автоматически заполнено 0xFF:



Исходники (для IAR) ниже. Источники:

1. Развернутая статья по SD-картам (не все рецепты применимы, но все же хорошая пища для размышлений).
2. SD и AVR от lleeloo : раз и два.
3. Неплохая статья по USI и SPI
4. Работа с SD — в 3х частях, описано согласование уровней (советую использовать CD4050 — простая, дешевая и надежная микруха для таких случаев).

За сим все, задавайте вопросы.

P.S. Ссылка на гитхаб™: github.com/DySprozin/avr/tree/master/SD_USI
P.P.S. Архив также прикрепляю.
Файлы в топике: SD_USI.zip

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

RSS свернуть / развернуть
Индикацию ошибок можно сделать так:

static u08 error_id;

void set_error_id (u08 a)
{
   error_id = a;
   _show_id_errors = 1;
}
//------------------------------------------------------------------------
void set_show_id_errors_off (void)
{
   err_led_off ();
   _show_id_errors = 0;
}
//------------------------------------------------------------------------
void show_id_errors (void)
{
   static u08 tmp_error_id;

   switch (_show_id_errors)
   {
      case 0:
         break;

      case 1:
         err_led_off ();
         tmp_error_id = error_id;
         set_timer (ST_PROC_ERRORS, LED_PAUSE);
         _show_id_errors = 2;
         break;

      case 2:
         if (wait (ST_PROC_ERRORS))
         {
            if (ERR_LED_METHOD & (1<<ERR_LED))
            {
               if (!--tmp_error_id)
               {
                  _show_id_errors = 1;
                  return;
               }
            }

            err_led_switch ();
            set_timer (ST_PROC_ERRORS, LED_TIME);
         }
         break;

      default:
         break;
   }
}

      case PROC_NRF24L01_ERROR:
         if (Get_Event (EV_ID_KEY_PRESSED))
         {
            Clr_Event (EV_ID_KEY_PRESSED);

            set_show_id_errors_off ();
            _proc_nrf24l01 = PROC_NRF24L01_INIT_4;
            return;
         }

         show_id_errors ();
         break;
0
_show_id_errors — эта переменная не должна быть глобальной?

set_timer — что за функция?
0

static u08 _show_id_errors;


set_timer, wait — программный таймер. Это из старых библиотек, которые я переписал. Пример смотри тут: www.avrfreaks.net/projects/soft-timers-0

Предложенный мной способ подходит, если: автоматное программирование, конечные автоматы. Никаких зацикленностей. Вход, проверка условий, действие в зависимости от условий, выход. Системный таймер настраивается на 1 мс. Итерация основного цикла должна с запасом впаковаться в системный тик.
Пример основного цикла:

__C_task main (void)
{
   wdt_enable (WDTO_15_MS);

   init_sys_timer ();

   Init_Events ();

   __enable_interrupt ();

   while (1)
   {
      __watchdog_reset ();

      kbd_drv ();

      proc_nrf24l01 ();

      Process_Events ();
   }
}
0
Я немного не в теме, но не окажется ли так, что в новых карточках (в свете последних тенденций с носителями вообще) минимальный размер считываемого/записываемого блока будет 4к?
+1
Ну это вопрос уже к файловым системам. ИМХО на низком уровне ничего не поменяется.
+1
SPI_DDR &=  ~(0<<DI); //вход без PullUp-подтяжки 

У вас ошибка(хотя наверное скорее опечатка) — правильно будет так (1 << DI). Кстати, регистры DDR забиты нулями после включения мк, можно было для такого однократного применения при старте вообще не писать эту строчку)
+1
> У вас ошибка(хотя наверное скорее опечатка) — правильно будет так (1 << DI)
ага, опечатка, спасибо, поправил.

> Кстати, регистры DDR забиты нулями
тож согласен — просто так наглядней, видно, что мы порт настраиваем на вход.
0
И тут опять интересный момент: если перелопатить кучу разных примеров, везде проверяется ответ карты 0×01 (т.е. SD v.2) и 0×04 (SD v.1). Однако, то ли мне так повезло, то ли еще что, но моя карта упорно не хотела инициализироваться (следующий этап после проверки). Подставил в цикл

if (status != 0xFF && status != 0x01) test(status_);

В результате диодик мигнул пять раз! Дописал (последнее условие выше) проверку на ответ 0×05 — процесс пошел. Возможно, если углубиться в даташиты, надо проверять не 0×04, а совпадение по маске, тоже буду рад, если кто в комментариях ткнет носом.
Тут короче дело такое, карты v1 standard capacity не знают команду CMD8, поэтому отвечают 0х05(код ошибки), видимо в тех статьях, которые вы читали, не заморачивались с совместимостью со старыми картами. А вот в этой статье показано правильное дерево инициализации we.easyelectronics.ru/blog/AVR/1077.html
+1
хм… а код 0x04 — это разве не код ошибки? Или в разных картах код ошибки тоже разный?
Кстати, вот тут человек проанализировал несколько карт, ответ на cmd8:

SanDisk TransFlash 256MB — CMD8 illegal (0x7F)
SanDisk 2GB — CMD8 illegal (0x7F)
Kingston 1GB — CMD8 ok
Kingston 2GB — CMD8 illegal (0x05)
Hama 128MB — CMD8 illegal (0x05)
Transcend MMCPlus 512MB — CMD8 illegal (0x05)

то есть, ответом может вроде быть вообще 0x7F… В общем, с универсальностью та еще головная боль получается.
0
Ну неспроста же производители всякой электроники часто указывают рекомендованные карточки.
+1
В общем, с универсальностью та еще головная боль получается.
Судя по статье, на которую дали ссылку выше — в ответе нужно проверять значение бита illegal command. Т.е. if(status & 0x04) {SD} else {SDHC/SDXC}.
0
тоже об этом подумал, но эту проверку ошибочно пройдет ответ 0xFF — фильтровать отдельно?
0
А это разве валидный ответ на команду?
0
Цикл был такой:

do {
<...>
  } while((status_!=0x01) && (status_!=0x04) && (status_!=0x05));


станет таким:

do {
<...>
  } while((status_!=0x01) && !(status & 0x04));


В результате, если вначале карта пошлет ответом 0xFF (а она вроде так и сделает, потому что тормозная и ей нужно время обработать команду), то выполнится условие (status & 0x04) и мы выйдем из цикла, так и не дождавшись ответа.
0
А что карта может посылать, если еще занята, кроме 0xFF? Если только 0xFF, то возможно стоит цикл переделать на do {} while(status==0xff)?
0
возможно, однако, такой конструкции не встречал. И это странно, учитывая, что она проще — видимо, и тут есть с подводные камни.
0
Кстати, вон по линку ниже упомянуто, что ждать нужно ответ со сброшенным седьмым битом.
Каких-либо специальных задержек там не нужно. Искать в ответе байт, отличный от 0xFF, неправильно. Если в течение восьми передач нуля в седьмом бите не последовало, значит нет ответа на команду.
0
то есть, ответом может вроде быть вообще 0x7F…
Может быть причина такая
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.