SPI (программный). Память Atmel DataFlash AT45DB081D.

Немного теории с просторов интернета.

SPI (англ. Serial Peripheral Interface, SPI bus — последовательный периферийный интерфейс, шина SPI) — последовательный синхронный стандарт передачи данных в режиме полного дуплекса, разработанный компанией Motorola для обеспечения простого и недорогого сопряжения микроконтроллеров и периферии. SPI также иногда называют четырёхпроводным (англ. four-wire) интерфейсом.

SPI является синхронным интерфейсом, в котором любая передача синхронизирована с общим тактовым сигналом, генерируемым ведущим устройством (процессором, контроллером). Принимающая (ведомая) периферия синхронизирует получение битовой последовательности с тактовым сигналом. К одному последовательному периферийному интерфейсу ведущего устройства-микросхемы может присоединяться несколько микросхем. Ведущее устройство выбирает ведомое для передачи, активируя сигнал «выбор кристалла» (англ. chip select) на ведомой микросхеме. Периферия, не выбранная контроллером, не принимает участия в передаче по SPI.




В SPI используются четыре цифровых сигнала:

MOSI (SI, DO, SDO, DOUT) — выход ведущего, вход ведомого (англ. Master Out Slave In). Служит для передачи данных от ведущего устройства ведомому.

MISO (SO, DI, SDI, DIN) — вход ведущего, выход ведомого (англ. Master In Slave Out). Служит для передачи данных от ведомого устройства ведущему.

SCLK (DCLOCK, CLK, SCK) — последовательный тактовый сигнал (англ. Serial Clock). Служит для передачи тактового сигнала для ведомых устройств.

CS или SS — выбор микросхемы, выбор ведомого (англ. Chip Select, Slave Select).

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

CPOL или CKP (Clock Polarity) — определяет начальный уровень (полярность) сигнал синхронизации.
  • CPOL=0 показывает, что сигнал синхронизации начинается с низкого уровня, так что передний фронт является нарастающим, а задний — падающим.
  • CPOL=1, показывает, что сигнал синхронизации начинается с высокого уровня, таким образом передний фронт является падающим, а задний — нарастающим.

CPHA или CKE (Clock Phase) — фаза синхронизации, определяет по какому из фронтов синхронизирующего сигнала производить выборку данных.
  • CPHA=0 показывает, что необходимо производить выборку по переднему фронту
  • CPHA=1 показывает, что выборку данных необходимо производить по заднему фронту.

 
Пунктирной линией показывает момент передачи данных.

Режим 0 / Mode 0



В этом режиме пассивным уровнем линии SCK является «0», а передача дынных производится по заднему (падающему) фронту.

Режим 1 / Mode 1



В этом режиме пассивным уровнем линии SCK является «0», а передача дынных производится по переднему (нарастающему) фронту.

Режим 2 / Mode 2



В этом режиме пассивным уровнем линии SCK является «1», а передача дынных производится по переднему (нарастающему) фронту.

Режим 3 / Mode 3



В этом режиме пассивным уровнем линии SCK является «1», а передача дынных производится по заднему (падающему) фронту.

От теории перейдем к практике.

Как всегда начнем с объявления необходимых переменных.


// режим SPI / SPI MODE = 0
 
// назначение выводов порта
#define SPI_SCK RB1 // выход - SCLK
#define SPI_SDI RB2 // выход - MOSI
#define SPI_SDO RB3 // вход  - MISO
#define SPI_SS  RC5 // выход - Chip Select
 
// настройка выводов порта
#define TRIS_SPI_SCK TRISB1 // выход - SCLK
#define TRIS_SPI_SDI TRISB2 // выход - MOSI
#define TRIS_SPI_SDO TRISB3 // вход  - MISO
#define TRIS_SPI_SS  TRISC5 // выход - Chip Select


Для начала проведем настройку линий интерфейса. В принципе все это можно сделать в первоначальной настройке контроллера, но для примера вынесем все в отдельную функцию. Данную функцию можно запустить один раз при настройке контроллера.


// инициализация SPI
void _spi_init(void)
{
   // первоначаотное положение линий SPI
   SPI_SS       = 1;
   TRIS_SPI_SS  = 0; // выход - Chip Select
   SPI_SCK      = 0;
   TRIS_SPI_SCK = 0; // выход - SCLK
   SPI_SDI      = 0;
   TRIS_SPI_SDI = 0; // выход - MOSI
   SPI_SDO      = 1;
   TRIS_SPI_SDO = 1; // вход  - MISO
}


Для начала обмена необходимо перевести линию CS (SS) в «0».
Окончанием связи служит перевод линии CS в «1».


// СТАРТ последовательность
void _spi_start(void)
{
   SPI_SCK = 0; // SPI_MODE = 0
   SPI_SS  = 0; // Chip Select - Enable
}
 
 
// СТОП последовательность
void _spi_stop(void)
{
   SPI_SS  = 1; // Chip Select - Disable
   SPI_SCK = 0; // SPI_MODE = 0
}


Ну и конечно же функции Передачи и Приема.


// ПОСЛАТЬ байт
void _spi_sendbyte(unsigned char d)
{
   unsigned char i;
 
   // отправить 1 байт
   for(i=0; i<8; i++)
   {
      CLRWDT();
 
      // проверить старш бит = 1
      if (d & 0x80)
      {
         SPI_SDI = 1; // передать 1
      }
      else
      {
         SPI_SDI = 0; // передать 0
      }
 
      SPI_SCK = 1; // синхроимпульс
      d <<= 1;     // сдвиг для передачи след бита
      //NOP();
      //NOP();
      SPI_SCK = 0; // синхроимпульс
   }
}

// ПРОЧИТАТЬ байт
unsigned char _spi_readbyte(void)
{
   unsigned char i, spiReadData=0;
 
   for(i=0; i<8; i++)
   {
      CLRWDT();
 
      spiReadData <<= 1; // сдвиг для передачи след бита
 
      SPI_SCK = 1; // синхроимпульс
      NOP();
      if (SPI_SDO == 1) { spiReadData |= 0x01; } // читаем бит
      //NOP();
      //NOP();
      SPI_SCK = 0; // синхроимпульс
   }
 
   return spiReadData;
}


Память Atmel DataFlash AT45DB081D.


Основные характеристики:
  • Страничная Flash память;
  • Емкость — 8 Мбит;
  • Организация — 4096 страниц по 256 или 264 байта;
  • Высокоскоростной SPI интерфейс обмена;
  • Напряжение питания от 2,5 или 2,7 до 3,6 В;
  • Максимальное входное напряжение до 6,25 В, что позволяет подключать ее напрямую к 5-ти вольтовому контроллеру;
  • Два буфера SRAM памяти по 256 или 264 байта каждый;
  • Принцип работы «чтение-модификация-запись»;
  • Программная и аппаратная защита от записи.



Несколько особенностей при работе с данной памятью.

1. По умолчанию вся память организована по 264 байта на страницу. Ее можно реорганизовать на 256 байт на страницу, НО это возможно сделать только один раз и обратно на 264 сменить не удастся. Так что не будем на этом заморачиваться, тем более что это никак нам не мешает. Также у памяти есть специальные 4 байта в которые можно записать идентификатор (например серийный номер изделия), но запись в них так же однократная и перезаписи не подлежит.

2. Микросхема имеет 2 специальные ноги:
  • RESET — аппаратный сброс микросхемы;
  • WP (Write Protection) — аппаратная защита от перезаписи.
На данном этапе они нам не нужны, поэтому их нужно подключить к линии питания.

3. Обмен данными происходит по 4-х проводному интерфейсу SPI. Микросхема может работать в режимах SPI 0 и 3, а также двух собственных режимах Atmel. Чтобы во всем этом не разбираться будем использовать обычный SPI mode 0.

4. Так как память страничная, то как такового доступа к отдельным ячейкам нет. Все операции чтения/записи происходят по принципу «чтение-модификация-запись» с использованием 2 специальных буферов. Использование 2 буферов помогает ускорить и упростить некоторые операции (пока один буфер записывается в память, второй заполняется новыми данными).

5. После подачи питания и перед обращением к памяти необходимо сделать гарантированную задержку в 20 миллисекунд.

Особенности страничной памяти.

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

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

 
Запись в память производится по принципу «чтение-модификация-запись»:
  • первоначально нужно определить страницу памяти в которую нужно записать данные, и расположение начала записи на странице. Так же нужно удостовериться что массив данных не выйдет за пределы страницы (если выходит то массив нужно разбить на 2 и записать на разные страницы памяти);
  • считать содержимое нужной страницы памяти во внутренний буфер;
  • начиная с определенного места произвести «модификацию» в буфере;
  • записать буфер в страницу памяти.

Здесь также придется следить за окончанием буфера/страницы.

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

Адрес байта в памяти записывается тремя байтами:
x x x P P P P P P P P P P P P B B B B B B B B B
где x x x — резерв (не используется, =0);
P P P P P P P P P P P P — адрес страницы (12 бит), может принимать значения от 0 до 4095;
B B B B B B B B B — адрес байта на странице (9 бит), может принимать значения от 0 до 263/255.

В качестве примера вычислим расположение байта под номером 353 246.
Для определения номера страницы разделим число на 264:
353246 / 264 = 1338,053 = 1338 = 01010 0111010
А теперь определим адрес байта на странице:
353246 — (1338*264) = 353246 — 353232 = 14 = 000001110
Таким образом получается адрес: 00001010 01110100 00001110 или 0Ah 74h 0Eh.

 

Ну что же приступаем, глядишь на деле все и проясниться.

Итак, первая команда которую необходимо рассмотреть — это Status Register Read (D7h). Она необходима для определения готовности памяти. Так как внутренний процесс записи может занимать продолжительное время, то для определения его окончания нужно считать Status регистр и проверить в нем старший бит RDY, если там «1» то память готова.

Оформим это в отдельную функцию.

// (AT45DB081D) чтение Status регистра
unsigned char Memory_Read_Status(void)
{
   unsigned char temp;
 
   _spi_start();
   _spi_sendbyte(0xD7);    // Command - Status Register Read
   temp = _spi_readbyte(); // чтение Status регистра
   _spi_stop();
 
   return temp;
}


Использовать ее можно вот так:
unsigned char MEM_status = Memory_Read_Status();


А для определения окончания записи, вот так:

// Delay (Check Status.RDY)
do
{
   MEM_status = Memory_Read_Status();
} while (!(MEM_status & 0x80));


 
Команда очистки памяти.

// ОЧИСТКА ПАМЯТИ AT45DB081D   
// AT45DB081D - Chip Erase
// СТИРАНЕЕ ВСЕЙ ПАМЯТИ
// (C7H 94H 80H 9AH)
_spi_start();
_spi_sendbyte(0xC7); // Command
_spi_sendbyte(0x94); // Command
_spi_sendbyte(0x80); // Command
_spi_sendbyte(0x9A); // Command
_spi_stop();
// Delay Tce = 7-22S  (Check Status.RDY)
do
{
   MEM_status = Memory_Read_Status();
} while (!(MEM_status & 0x80));


Этот процесс достаточно длительный и может занимать от 7 до 22 секунд.

 

Рассмотрим остальные команды на примере чтения пяти байт из памяти, начиная с адреса 353246 (адрес страницы — 1338, адрес байта — 14) в массив unsigned char MEMbuff[5];. Работать будем через Buffer 1.

1. Считать необходимую страницу во внутренний буфер. Адрес страницы 1338 = 00001010 01110100 или 0Ah 74h, остальные биты «0».

// Переслать страницу памяти в буфер
// AT45DB081D - Main Memory Page to Buffer Transfer
// (53H for buffer 1 or 55H for buffer 2)
_spi_start();
_spi_sendbyte(0x53); // Command - Buffer 1
_spi_sendbyte(0x0A); // Address - x x x P P P P P
_spi_sendbyte(0x74); // Address - P P P P P P P x 
_spi_sendbyte(0x00); // Address - x x x x x x x x 
_spi_stop();
// Delay Txfr = 200uS  (Check Status.RDY)
do
{
   MEM_status = Memory_Read_Status();
} while (!(MEM_status & 0x80));


2. Прочитать из внутреннего буфера необходимые данные, начиная с адреса 14 = 00001110 или 0Eh, остальные биты «0».

// Чтение из буфера
// AT45DB081D - Buffer Read
// (D1H for buffer 1 or D2H for buffer 2)
_spi_start();
_spi_sendbyte(0xD1); // Command - Read Buffer 1
_spi_sendbyte(0x00); // Address - x x x x x x x x
_spi_sendbyte(0x00); // Address - x x x x x x x B 
_spi_sendbyte(0x0E); // Address - B B B B B B B B 
_spi_sendbyte(0x00); // 1 Dummy Byte
MEMbuff[0] = _spi_readbyte(); // первый байт
MEMbuff[1] = _spi_readbyte(); // второй байт
MEMbuff[2] = _spi_readbyte(); // третий байт
MEMbuff[3] = _spi_readbyte(); // четвертый байт
MEMbuff[4] = _spi_readbyte(); // пятый байт
_spi_stop();


1 Dummy Byte — 1 пустой (произвольный) байт необходим памяти для подготовки данных, этакая небольшая временная задержка.

После этих двух операций пять считанных байт, начиная с адреса 353246, будут записаны в массив MEMbuff.

 

А теперь попробуем записать те же пять байт в ту же область памяти. На этот раз работать будем через Buffer 2.

1. Считать необходимую страницу во внутренний буфер. Адрес страницы 1338 = 00001010 01110100 или 0Ah 74h, остальные биты «0».

// Переслать страницу памяти в буфер
// AT45DB081D - Main Memory Page to Buffer Transfer
// (53H for buffer 1 or 55H for buffer 2)
_spi_start();
_spi_sendbyte(0x55); // Command - Buffer 2
_spi_sendbyte(0x0A); // Address - x x x P P P P P
_spi_sendbyte(0x74); // Address - P P P P P P P x 
_spi_sendbyte(0x00); // Address - x x x x x x x x 
_spi_stop();
// Delay Txfr = 200uS  (Check Status.RDY)
do
{
   MEM_status = Memory_Read_Status();
} while (!(MEM_status & 0x80));


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

2. Начиная с адреса 14 = 00001110 или 0Eh, произвести «модификацию» в буфере 2.

// Запись в буфер
// AT45DB081D - Buffer Write
// (84H for buffer 1 or 87H for buffer 2)
_spi_start();
_spi_sendbyte(0x87); // Command - Write Buffer 2
_spi_sendbyte(0x00); // Address - x x x x x x x x
_spi_sendbyte(0x00); // Address - x x x x x x x B
_spi_sendbyte(0x0E); // Address - B B B B B B B B
_spi_sendbyte(0x00); // ??? 1 Dummy Byte ???
_spi_sendbyte(0x12); // первый байт
_spi_sendbyte(0x23); // второй байт
_spi_sendbyte(0x34); // третий байт
_spi_sendbyte(0x45); // четвертый байт
_spi_sendbyte(0x56); // пятый байт
_spi_stop();


Хотя в даташите ничего не сказано про 1 Dummy Byte, правильно память заработала только с ним.

3. Записать буфер 2 в страницу памяти.

// Переслать буфер в страницу памяти с встроенным стиранием
// AT45DB081D - Buffer to Main Memory Page Program with Built-in Erase
// (83H for buffer 1 or 86H for buffer 2)
_spi_start();
_spi_sendbyte(0x86); // Command - Buffer 2
_spi_sendbyte(0x0A); // Address - x x x P P P P P
_spi_sendbyte(0x74); // Address - P P P P P P P x 
_spi_sendbyte(0x00); // Address - x x x x x x x x 
_spi_stop();
// Delay Tep = 14-35mS  (Check Status.RDY)
do
{
   MEM_status = Memory_Read_Status();
} while (!(MEM_status & 0x80));


 

Проверка.


Так как модели для протеуса не нашлось, будем экспериментировать в железе.

Контроллер — PIC16F876A-I/SO, питание 5В.
Память — AT45DB081D, питание 3,3В.
Подключение
// (5) WP# — +3.3V
// (3) RESET# — +3.3V
// (2) SCK — SPI_SCK — (22) RB1
// (8) SO — SPI_SDO — (24) RB3
// (1) SI — SPI_SDI — (23) RB2
// (4) CS# — SPI_SS — (16) RC5
// SPI_SS (подтяжка к питанию — 10К)

Для индикации работы подключим к RC2 анод светодиода, катод на землю.

Проверочная программа.
1. Зажигает светодиод. Выполняет очистку памяти. После окончания очистки гасит светодиод.
2. Записывает 5 байт в память.
3. Читает 5 байт из памяти.
4. Считанные данные записываются в EEPROM память контроллера. Светодиод начинает мигать.

Для проверки необходимо прошить программу в контроллер, дождаться когда начнет мигать светодиод, выключить контроллер и считать EEPROM.
  • +3
  • 09 февраля 2013, 20:41
  • Frankie
  • 1
Файлы в топике: spisoft-AT45DB081D.zip

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

RSS свернуть / развернуть
а передача дынных производится по заднему (падающему) фронту
«Дынных») Четыре раза) Копипаст такой копипаст.
if (d & 0x80)
{
SPI_SDI = 1; // передать 1
}
else
{
SPI_SDI = 0; // передать 0
}
А чем не угодила битовая математика? Ну и оно было бы короче и читабельнее в виде SPI_SDI = (d & 0x80)? 1: 0;
if (SPI_SDO == 1) { spiReadData |= 0x01; }
Аналогично, чем не угодила битовая математика? spiReadData |= SPI_SDO.
Ну и поскольку SPI — шина дуплексная, то обычно делают одну функцию — обмен данными. Она одновременно передает и принимает байт. А дальше для при передаче выкидывается принятый байт, при приеме — передается пустой байт. Кроме того, бывают случаи, когда нужны оба одновременно.
По умолчанию вся память организована по 264 байта на страницу
А при этом страниц те же 4096 (т.е. общий размер 8.25 Мбит) или меньше (3972 с неполной последней страницей)? Вообще, эта фича явно предназначена для wear leveling. Там как раз нужно вместе с страницей хранить немного данных о ее использовании.
Проверочная программа.
А ты ее случаем не забыл приаттачить? Алсо, какой компилятор используешь?
0
  • avatar
  • Vga
  • 09 февраля 2013, 22:17
Копипаст такой копипаст.
— это редкостный копипаст. Много раз везде и всеми повторенные теоретические знания о SPI, причем не мной придуманные!!! О чем я и упомянул первой же строчкой.

А чем не угодила битовая математика?
— ничем!!! По данному вопросу есть какая то такая умная фраза, точно не помню но что то типа:
— я могу писать ++i, но буду писать i = i + 1, обо так вспоминать легче.

Ну и поскольку… когда нужны оба одновременно.
— полностью согласен, но в данном случае только полудуплекс и нужен. Ну и просто «разделить и упростить» чтобы проще воспринимать.

А при этом страниц те же 4096 ...
По даташиту так – 4,096 Pages (256/264-Bytes/Page) Main Memory. Следовательно страниц всегда 4096, а вот байт в них переменное количество. Ну а сколько там места опять же следует из названия микрухи — 8-megabit Atmel DataFlash.

Приаттачить действительно забыл!!! Спасибо за напоминание!!! Исправился!!!
0
— это редкостный копипаст.
Да не, я про орфографию.
Ну и просто «разделить и упростить» чтобы проще воспринимать.
От добавления пары инструкций в цикл оно бы сложнее не стало. Скорее даже проще — избавилось бы от дублированного между send и recv кода.
— я могу писать ++i, но буду писать i = i + 1, обо так вспоминать легче.
Битовая математика в данном случае вопрос оптимальности, а не читаемости. Хотя я не уверен, что оно будет быстрее (не знаю как пик, а на AVR инструкция вида SPI_SDI = (d & 0x80) >> 7 врядли скомпилируется оптимально, if может оказаться оптимальней).
P.S. Ты слишком резко реагируешь.
0
Ты слишком резко реагируешь.
:))))))))))))
Да не нисколько, наоборот попытался максимально ответить на вопросы.

скомпилируется оптимально
— это вопрос компилятора, а не PICvsAVR. Хотя проверял несколько раз проверял, что длинные битовые операции И разбитые на несколько простых компилируются одинаково (ну или с разницей в 1-2 комнаты). А вот смысл длинных битовых строк иногда теряется )))).
0
1-2 комнаты
))))))))))))))
1-2 команды
0
Нет, я о том, что в AVR сдвиг есть только на 1 бит. И команда d >> 7, если ее перевести тупо в лоб, превратится в 7 команд сдвига вправо на один бит. Или в цикл на 7 итераций с этой же командой внутри. Хотя на ассемблере можно было бы просто сдвинуть старший бит в С и оттуда перебросить в регистр GPIO. А вот как реализованы сдвиги на PIC я не в курсе. Ну и оптимальность зависит от компилятора.
длинные битовые операции И разбитые на несколько простых компилируются одинаково (ну или с разницей в 1-2 комнаты)
Это если разбитые. Но у тебя оно не только разбито на несколько строк, но и использует другие операции — if вместо битовой арифметики.
0
Добрый день. Хотел бы задать Вам вопрос
У меня DataFlash AT45DB041D
После того как я стёр всю память из DataFlash у меня прекратилась какая либо запись(всегда читает только 0xFF)
По каким причинам это могло возникнуть?
0
Что значит «прекратилась какая либо запись»?
всегда читает только 0xFF
Для стертой флешки это нормально.
0
До этого я записывал данные и они читались
Но после того как я очистил всю память по страницам
после записи все значения читает 0xFF
0
Показывай код.
0
Прошу прощения.
Какое то время контроллер побыл обесточенным (минут 20 точно), и опять стала читать…
Вопрос в принципе решён.
Но с чем это могло быть связано?
0
Возможно, ты просто выставил бит, который запрещает запись?
0
Это исключено в схеме он не подведён ни к одной из ног
0
Кроме пина WP (который, я надеюсь, ты не оставил висеть?) есть еще бит в регистре. Пин просто запрещает этот бит сбросить.
0
Ясно, спасибо
0
Я использовал библиотеку, которая была с контроллером…
Разбираю её
yadi.sk/d/06qDnOz_38aBTK
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.