Увлёкся я изучением протоколов. Про реализацию протокола I2C у меня уже была статья на Датагоре. Сегодня поговорим о протоколе SPI. Сразу к делу. Заглянем в Wiki за ёмким определением.
SPI (англ. Serial Peripheral Interface, SPI bus — последовательный периферийный интерфейс, шина SPI) — последовательный синхронный стандарт передачи данных в режиме полного дуплекса, предназначенный для обеспечения простого и недорогого высокоскоростного сопряжения микроконтроллеров и периферии. SPI также иногда называют четырёхпроводным (англ. four-wire) интерфейсом.
В отличие от стандартного последовательного порта (англ. standard serial port), SPI является синхронным интерфейсом, в котором любая передача синхронизирована с общим тактовым сигналом, генерируемым ведущим устройством (процессором). Принимающая (ведомая) периферия синхронизирует получение битовой последовательности с тактовым сигналом. К одному последовательному периферийному интерфейсу ведущего устройства-микросхемы может присоединяться несколько микросхем. Ведущее устройство выбирает ведомое для передачи, активируя сигнал «выбор кристалла» (англ. chip select) на ведомой микросхеме. Периферия, не выбранная процессором, не принимает участия в передаче по SPI.
В реализации протокола SPI используют четыре цифровых линии для передачи сигналов (в скобках даны вариации обозначений): В отличие от стандартного последовательного порта (англ. standard serial port), SPI является синхронным интерфейсом, в котором любая передача синхронизирована с общим тактовым сигналом, генерируемым ведущим устройством (процессором). Принимающая (ведомая) периферия синхронизирует получение битовой последовательности с тактовым сигналом. К одному последовательному периферийному интерфейсу ведущего устройства-микросхемы может присоединяться несколько микросхем. Ведущее устройство выбирает ведомое для передачи, активируя сигнал «выбор кристалла» (англ. chip select) на ведомой микросхеме. Периферия, не выбранная процессором, не принимает участия в передаче по SPI.
CLK (CLOCK, SCLK) — по этой линии передаются тактовые импульсы для ведомых устройств.
DO (MISO, SDO, DO, DOUT, SO) — вход данных МК, эта линия нужна для приема данных от периферии.
DI (MOSI, SDI, DI, DIN, SI) — выход данных МК, передаём данные к ведомому устройству.
CS (SS, nCS, CS, CSB, CSN) — выбор микросхемы, выбор ведомого, в народе «чипселект». Выводов CS может быть несколько, и количество их соответствует тому количеству устройств, с которыми мы хотим общаться на шине.
Хочу особо отметить, что в большинстве случаев этот сигнал (CS) инвертирован. Т.е. если на нем логический «0» — устройство выбрано и с ним можно работать. И наоборот, «1» говорит, что мы не хотим общаться с этим устройством.
С теорией вроде как всё, переходим к практике. Далее раcсказ пойдет только об одном ведомом устройстве на шине SPI. Подключать сразу несколько устройств пока не было необходимости.
Забегая вперед, скажу: для каждого нового устройства на шине нужно будет добавить две небольшие процедуры начала/конца общения.
Я, как обычно, для тестов использую Arduino Nano. Под неё и пишу.
Распишем для начала все дефайны (#define):
// Chip type ATmega328P at 16 MHz
// Пример реализации протокола SPI
// Для примера использована память MX25L8005
#include // нужно для атмегушки
#include // нужно для задержек
// пропишем пины
#define spi_cs PORTB.0 // это D8 ардуино
#define DDR_spi_cs DDRB. 0
#define spi_do PINB. 1 // это D9 ардуино
#define DDR_spi_do DDRB. 1
#define spi_di PORTB.2 // это D10 ардуино
#define DDR_spi_di DDRB. 2
#define spi_clk PORTB.3 // это D11 ардуино
#define DDR_spi_clk DDRB. 3
// задержка
#define SPI_time 10
Сразу можно прописать инициализацию портов:
// главная функция типа:)
void main () {
DDRB.5=1; // для тестовой лампочки
// инициализация линий.....
DDR_spi_clk = 1; // на выход
DDR_spi_do = 0; // на вход
DDR_spi_di = 1; // на выход
DDR_spi_cs = 1; // на выход
spi_cs=1; // задираем cs
delay_us(100); // ждем устаканивания питания
// тут напишем потом
}
Далее про тактирование и передачу данных. Приведу для примеру картинку, которая раскрывает некоторые понятия, чтоб можно было проще воспринимать коментарии в коде.
На картинке изображена форма тактирующего сигнала CLK. Теперь нужно передать один байт в шину. Тут ничего сложного нет:
// пишем байт в шину
void SPI_wr(char byte) {
char i; // счетчик для цикла
for (i=0; i<8; i++) { // цикл на 8 бит
delay_us(SPI_time/2); // формируем половину паузы
if (byte & 0x80) spi_di=1; else spi_di=0; // выставляем один бит
byte<<=1; // двигаем байт
delay_us(SPI_time/2); // завершаем паузу
spi_clk = 1; // тактовый фронт
delay_us(SPI_time); // формируем импульс
spi_clk = 0; // тактовый спад
}
}
Дальше надо научиться принимать байт:
// читаем байт из шины
unsigned char SPI_rd() {
char i, byte=0; // переменные для счетчика и байта
for (i=0; i<8; i++) { // цикл на 8 бит
delay_us(SPI_time); // формируем паузу
spi_clk = 1; // формируем фронт
delay_us(SPI_time/2); // половина импульса
if (spi_do) byte++; // читаем бит и записываем его
if (i!=7) byte<<=1; // двигаем байт
delay_us(SPI_time/2); // завершаем импульс
spi_clk = 0;
}
return byte; // вернули, что прочли
}
Линии CLK, DI и DO теперь у нас работают нормально. Не стоит забывать и про линию CS. Многие устройства по этой линии начинают и заканчивают процедуру обмена данными. Пропишем:
// начинаем общение
void spi_go() {
delay_us(SPI_time);
spi_clk=0;
spi_cs =0;
delay_us(SPI_time);
}
// заканчиваем общение
void spi_end() {
delay_us(SPI_time);
spi_cs = 1;
delay_us(SPI_time);
}
У нас готовы основные функции работы с протоколом. Теперь, чтобы передать пару байт в шину, необходимо выполнить четыре функции. Пишу чисто для примера, в основной код это не включается:
spi_go(); // говорим устройству, что начинаем общаться с ним
spi_wr(0xAA); // записали в шину байт 0xAA
spi_wr(0x55); // записали в шину байт 0x55
spi_end(); // завершили процедуру обмена данными
Как оказалось, ничего сложного нет.
Теперь надо придумать, на чём всё это дело проверить. Порывшись в своих запасах, нашёл микросхему памяти MX25L8005, да ещё и в удобном DIP8 корпусе. Данный чип представляет собой память с интерфейсом SPI на целый мегабайт.
Тест будем проводить по светодиоду, припаянному к D13 ножке Arduino. Алгоритм такой: мы просто считаем ID данной микросхемы и если он совпадет с тем, что указано в даташите, мы включаем тестовый светодиод.
Т.е. передав четыре байта 0×90, 0×00, 0×00 и 0×00 мы должны прочитать с шины два байта, а именно 0xC2 и 0×13. Данный алгоритм охватит все процедуры и функции, описанные ранее. И так, приступим:
while(1) {
char t=0;
delay_ms(100); // ждем немного
PORTB.5=0; // гасим тестовую лампочку
// читаем ID микросхемы памяти
spi_go(); spi_wr(0x90); spi_wr(0x00); spi_wr(0x00); spi_wr(0x00);
if (spi_rd()==0xC2) t++; // сразу сравниваем с нужным
if (spi_rd()==0x13) t++; // сразу сравниваем с нужным
spi_end();
if (t==2) PORTB.5=1; // включаем тестовую лампочку
}
Далее закидываем наш исходный код в CodeVision, компилируем и смотрим возможные ошибки. Сразу без ошибок у меня редко выходит написать, но с опытом их становится всё меньше. Вышло три ошибки/опечатки.
Теперь тестим это дело в Протеусе (ISIS 7 Proteus). К сожалению, не оказалось в Протеусе модели/прототипа моей микросхемы памяти. Просто подключил виртуальный осциллограф, чтобы форму сигналов посмотреть:
На картинке видно, что все сигналы около дела.
Обнаружил один недочет — забыл задрать в самом начале чипселект CS. Он ведь инверсный. Поправил.
Теперь настала пора подумать над схемой включения. Из дефайнов видно, что CLK — D8, DO — D9, DI — D10, CS — D11. Нарисовал схему:
Переходим к тесту в железе. Воткнул в беспаечную макетную плату (breadboard) платку ARDUINO NANO (всё ещё пахнет недавним горелым диодом) и микросхему MX25L8005, соединил всё проводками. Теперь долгожданный момент — включаем и… И забыл прошить!
Открываю мой любимый Xloader, заливаю прошивку. И вижу, что светодиод сразу загорелся.
Для проверки менял значения 0xC2 и 0×13 в коде на другие. Результат совпал с ожидаемым — светодиод не загорался.
Вывод. Данный код можно считать полностью рабочим и положить в папку примеров.
Эх, даже ничего не сломалось и не сгорело в этот раз!
Спасибо за внимание!
Камрад, рассмотри датагорские рекомендации
🌼 Полезные и проверенные железяки, можно брать
Опробовано в лаборатории редакции или читателями.