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
а передача дынных производится по заднему (падающему) фронту«Дынных») Четыре раза) Копипаст такой копипаст.
if (d & 0x80)А чем не угодила битовая математика? Ну и оно было бы короче и читабельнее в виде SPI_SDI = (d & 0x80)? 1: 0;
{
SPI_SDI = 1; // передать 1
}
else
{
SPI_SDI = 0; // передать 0
}
if (SPI_SDO == 1) { spiReadData |= 0x01; }Аналогично, чем не угодила битовая математика? spiReadData |= SPI_SDO.
Ну и поскольку SPI — шина дуплексная, то обычно делают одну функцию — обмен данными. Она одновременно передает и принимает байт. А дальше для при передаче выкидывается принятый байт, при приеме — передается пустой байт. Кроме того, бывают случаи, когда нужны оба одновременно.
По умолчанию вся память организована по 264 байта на страницуА при этом страниц те же 4096 (т.е. общий размер 8.25 Мбит) или меньше (3972 с неполной последней страницей)? Вообще, эта фича явно предназначена для wear leveling. Там как раз нужно вместе с страницей хранить немного данных о ее использовании.
Проверочная программа.А ты ее случаем не забыл приаттачить? Алсо, какой компилятор используешь?
Копипаст такой копипаст.— это редкостный копипаст. Много раз везде и всеми повторенные теоретические знания о SPI, причем не мной придуманные!!! О чем я и упомянул первой же строчкой.
А чем не угодила битовая математика?— ничем!!! По данному вопросу есть какая то такая умная фраза, точно не помню но что то типа:
— я могу писать ++i, но буду писать i = i + 1, обо так вспоминать легче.
Ну и поскольку… когда нужны оба одновременно.— полностью согласен, но в данном случае только полудуплекс и нужен. Ну и просто «разделить и упростить» чтобы проще воспринимать.
А при этом страниц те же 4096 ...По даташиту так – 4,096 Pages (256/264-Bytes/Page) Main Memory. Следовательно страниц всегда 4096, а вот байт в них переменное количество. Ну а сколько там места опять же следует из названия микрухи — 8-megabit Atmel DataFlash.
Приаттачить действительно забыл!!! Спасибо за напоминание!!! Исправился!!!
— это редкостный копипаст.Да не, я про орфографию.
Ну и просто «разделить и упростить» чтобы проще воспринимать.От добавления пары инструкций в цикл оно бы сложнее не стало. Скорее даже проще — избавилось бы от дублированного между send и recv кода.
— я могу писать ++i, но буду писать i = i + 1, обо так вспоминать легче.Битовая математика в данном случае вопрос оптимальности, а не читаемости. Хотя я не уверен, что оно будет быстрее (не знаю как пик, а на AVR инструкция вида SPI_SDI = (d & 0x80) >> 7 врядли скомпилируется оптимально, if может оказаться оптимальней).
P.S. Ты слишком резко реагируешь.
Ты слишком резко реагируешь.:))))))))))))
Да не нисколько, наоборот попытался максимально ответить на вопросы.
скомпилируется оптимально— это вопрос компилятора, а не PICvsAVR. Хотя проверял несколько раз проверял, что длинные битовые операции И разбитые на несколько простых компилируются одинаково (ну или с разницей в 1-2 комнаты). А вот смысл длинных битовых строк иногда теряется )))).
Нет, я о том, что в AVR сдвиг есть только на 1 бит. И команда d >> 7, если ее перевести тупо в лоб, превратится в 7 команд сдвига вправо на один бит. Или в цикл на 7 итераций с этой же командой внутри. Хотя на ассемблере можно было бы просто сдвинуть старший бит в С и оттуда перебросить в регистр GPIO. А вот как реализованы сдвиги на PIC я не в курсе. Ну и оптимальность зависит от компилятора.
длинные битовые операции И разбитые на несколько простых компилируются одинаково (ну или с разницей в 1-2 комнаты)Это если разбитые. Но у тебя оно не только разбито на несколько строк, но и использует другие операции — if вместо битовой арифметики.
Добрый день. Хотел бы задать Вам вопрос
У меня DataFlash AT45DB041D
После того как я стёр всю память из DataFlash у меня прекратилась какая либо запись(всегда читает только 0xFF)
По каким причинам это могло возникнуть?
У меня DataFlash AT45DB041D
После того как я стёр всю память из DataFlash у меня прекратилась какая либо запись(всегда читает только 0xFF)
По каким причинам это могло возникнуть?
- Egor-Zobnin
- 12 января 2017, 14:14
- ↓
Что значит «прекратилась какая либо запись»?
всегда читает только 0xFFДля стертой флешки это нормально.
До этого я записывал данные и они читались
Но после того как я очистил всю память по страницам
после записи все значения читает 0xFF
Но после того как я очистил всю память по страницам
после записи все значения читает 0xFF
- Egor-Zobnin
- 12 января 2017, 14:21
- ↑
- ↓
Прошу прощения.
Какое то время контроллер побыл обесточенным (минут 20 точно), и опять стала читать…
Вопрос в принципе решён.
Но с чем это могло быть связано?
Какое то время контроллер побыл обесточенным (минут 20 точно), и опять стала читать…
Вопрос в принципе решён.
Но с чем это могло быть связано?
- Egor-Zobnin
- 12 января 2017, 14:28
- ↑
- ↓
- Egor-Zobnin
- 12 января 2017, 14:31
- ↑
- ↓
Комментарии (16)
RSS свернуть / развернуть