Передача данных spi avr между atmega. SPI Arduino – подключение устройств к ардуино

10.09.2021

Всем коллегам и согражданам привет!
Увлёкся я изучением протоколов. Про реализацию на Датагоре. Сегодня поговорим о протоколе SPI. Сразу к делу. за ёмким определением.

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

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

В реализации протокола 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; // включаем тестовую лампочку }

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

Синхронный последовательный интерфейс SPI предназначен для ввода- вывода данных в интерфейсах "точка-точка" с одним ведущим (SPI-master) и одним ведомым (SPI-slave) устройством (рис. 1.24). Схема управления SPI- master формирует тактовые импульсы SCK, по которым одновременно производится передача сигналов на выходе MOSI и прием сигналов на входе MISO. Эти же тактовые импульсы SCK, поступая в SPI-slave, управляют приемом сигналов на его входе MOSI и формированием сигналов на его выходе MISO. Раздельные сигнальные цепи MOSI и MISO позволяют легко реализовать полнодуплексный режим обмена данными.

Форматы данных, параметры сигналов, временные характеристики и т.п. в интерфейсе не регламентируются, например, скорость обмена данными определяется только частотой тактовых импульсов SCK, формируемых SPI- master. Максимальное расстояние зависит от уровня искажения сигналов в линиях связи, предполагается, что надежный обмен данными возможен при расстояниях до нескольких метров.

По-существу, полноценным интерфейсом даже для физического уровня не является. Фактически, SPI реализует стандартную процедуру ввода-вывода данных в регистрах сдвига, никаких алгоритмов контроля работы, контроля передаваемых данных не предусмотрено. Все необходимые процедуры контроля должен выполнять SPI-master. Это, с одной стороны, требует применения дополнительных средств контроля, а с другой стороны, максимально упрощает средства реализации самого интерфейса SPI. SPI- slave – это стандартный регистр сдвига с требуемым числом разрядов данных.

Например, микроконтроллеры семейства AVR фирмы ATMEL поддерживают ввод-вывод данных в режиме и SPI-master, и SPI-slave. Стандартный цикл обмена предполагает одновременную передачу в обоих направлениях по одному байту данных (рис. 1.24). При передаче многобайтовых сообщений SPI-slave должен содержать регистр сдвига соответствующей разрядности, а SPI-master должен производить управление обменом требуемой последовательности байтов данных, обрабатывая каждый байт после очередного стандартного цикла работы интерфейса и обеспечивая запуск следующего стандартного цикла обмена.

применяется не только для обмена данными между микроконтроллерами, но и для сопряжения микроконтроллеров с внешними АЦП (ADC) и ЦАП (DAC), микросхемами памяти – SRAM, FRAM, SEERAM и многими другими устройствами. Благодаря последовательному формату данных и простой логической организации интерфейса SPI эти микросхемы производятся в компактных 8 – 16 выводных корпусах. В табл. 1.6 приведены примеры микросхем различного функционального назначения и разных производителей с интерфейсом SPI. Эти примеры показывают, что последовательный формат интерфейса позволяет существенно сократить требуемое число линий ввода-вывода.

Таблица 1.6

Тип микросхемы

Основные параметры

Тип корпуса

Производитель

24-Bit, Delta-Sigma, 15 Гц

24-Bit, Delta-Sigma, 41 кГц

16-Bit, SAR, 100 кГц

16-Bit, PulSAR, 500 кГц

12-Bit, U-out, 2.5 мкс

16-Bit, U-out, 10 мкс

14-Bit, I-out, 0.04 мкс

12-Bit, I-out, 0.6 мкс

16-Bit, U-out, 1 мкс

www.maxim- ic.com

Тип микросхемы

Основные параметры

Тип корпуса

Производитель

16 кБит, 1 трлн

4 кБит, неогр.

64 кБит, неогр.

256K, 32768×8, 0.1млн Cycles

8К, 1024×8, 0.1млн Cycles

16K, 8192×8, 0.1млн Cycles

128K, 16384×8, 0.1млн Cycles

256K, 32768×8, 0.1млн Cycles

Termosensor

13-bit, -40 to +150 C o (±0.5C o)

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

Например, интерфейс SPI может оказаться полезным для считывания информации о состоянии большого числа двухпозиционных датчиков или для ввода многобитовых данных, поступающих в параллельном формате. Для этих целей удобно использовать отдельные регистры с параллельной записью и последовательным считыванием (8-Bit Parallel-In/Serial-Out Shift Register), например CD74HCT166 (рис. 1.25).

Схема подключения шестнадцати двухпозиционных датчиков (S1 – S16) через SPI-интерфейс микроконтроллера показана на рис. 1.26. Следует отметить, что перед стартом работы SPI-интерфейса необходимо сформировать сигнал записи информации в регистры с параллельных входов D0-D7. Для 38 этого можно использовать один из выходов микроконтроллера, в данном примере PC0.

Рис. 1.25. Функциональная схема регистра CD74HCT166

Рис. 1.26. Подключение двухпозиционных датчиков к SPI-интерфейсу

Рис. 1.27. Подключение шестиразрядного индикатора к SPI-интерфейсу

Применяя регистры с последовательной записью и параллельной выдачей информации (8-Bit Serial-In, Parallel-Out Shift Register) – SN74HC595 , SPI-интерфейс можно использовать и для многобайтовой параллельной выдачи информации. В качестве примера на рис. 1.27 приведена схема подключения шестиразрядного семисегментного индикатора к микроконтроллеру. В отличие от предыдущей схемы, сигнал параллельного вывода (PB1) необходимо сформировать после окончания передачи данных интерфейсом SPI средствами, выходящими за рамки интерфейса. Например, алгоритм взаимодействия с интерфейсом должен предусматривать контроль количества переданных байтов данных, а после завершения передачи последнего байта необходимо дополнительно передать сигнал параллельного вывода.

Теперь вы имеете общее представление о последовательном периферийном интерфейсе и можно перейти к рассмотрению SPI модуля.
SPI модуль микроконтроллера AVR atmega16 использует для своей работы 4 вывода - MOSI, MISO, SCK и SS. Когда модуль не задействован, эти выводы являются линиями портов ввода/вывода общего назначения. Когда модуль включен, режим работы этих выводов переопределяются согласно следующей таблице.

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

Регистры SPI модуля

В микроконтроллере atmega16 для работы с модулем SPI используются три регистра:

Управляющий регистр SPCR,
- статусный регистр SPSR,
- регистр данных SPDR.

Все три регистра восьмиразрядные.

Кофигурация модуля SPI устанавливается с помощью регистра SPCR (SPI Control Register).

SPIE – разрешает /запрещает прерывания от модуля SPI. Если бит установлен в 1, прерывания от SPI разрешены.

SPE – включает/выключает модуль SPI. Если бит установлен в 1, модуль SPI включен.

DORD – определяет порядок передачи данных. Когда бит установлен в 1, содержимое регистра данных передается младшим битом вперед. Когда бит сброшен, то старшим битом вперед.

MSTR – определяет режим работы микроконтроллера. Если бит установлен в 1, микроконтроллер работает в режиме Master (ведущий). Если бит сброшен – в режиме Slave (ведомый). Обычно микроконтроллер работает в режиме master.

CPOL и CPHA – определяют в каком режиме работает SPI модуль. Требуемый режим работы зависит от используемого периферийного устройства.


SPR1
и SPR0 – определяют частоту тактового сигнала SPI модуля, то есть скорость обмена. Максимально возможная скорость обмена всегда указывается в спецификации периферийного устройства.


Статусный регистр SPSR (SPI Status Register) предназначен для контроля состояния SPI модуля
, кроме того он содержит дополнительный бит управления скоростью обмена.


SPIF
– флаг прерывания от SPI. Он устанавливается в 1 по окончании передачи байта данных. Если разрешены прерывания модуля, одновременно с установкой этого флага генерируется прерывание от SPI. Также этот флаг устанавливается в 1 при переводе микроконтроллера из режима master в режим slave с помощью вывода SS.
Сброс флага происходит аппаратно, при вызове подпрограммы обработки прерывания или после чтения регистра SPSR с последующим обращением к регистру данных SPDR.

WCOL - флаг конфликта записи. Флаг устанавливается в 1, если во время передачи данных выполняется попытка записи в регистр данных SPDR. Флаг сбрасывается аппаратно после чтения регистра SPSR с последующим обращением к регистру данных SPDR.

SPI2X - бит удвоения скорости обмена. Установка этого разряда в 1 удваивает частоту тактового сигнала SCK. Микроконтроллер при этом должен работать в режиме master.

Взаимосвязь между битами SPR0, SPR1, SPI2X и частотой тактового сигнала SCK показана в таблице.

Где Fosc - тактовая частота микроконтроллера AVR.


Для передачи и приема данных предназначен регистр SPDR (SPI Data Register)
. Запись данных в этот регистр инициирует передачу данных SPI модулем. При чтении этого регистра, считывается содержимое буфера сдвигового регистра SPI модуля.

Программный код

Минимальный программный код для работы с SPI модулем состоит из двух функций:

Функции инициализации.
- функции передачи/приема байта данных

Инициализация SPI модуля

Инициализация включает в себя конфигурирование выводов SPI модуля и управляющего регистра SPCR.


#define SPI_PORTX PORTB
#define SPI_DDRX DDRB

#define SPI_MISO 6
#define SPI_MOSI 5
#define SPI_SCK 7
#define SPI_SS 4

/*инициализация SPI модуля в режиме master*/
void SPI_Init(void)
{

/*настройка портов ввода-вывода
все выводы, кроме MISO выходы*/
SPI_DDRX |= (1< SPI_PORTX |= (1<

/*разрешение spi,старший бит вперед,мастер, режим 0*/
SPCR = (1< SPSR = (0<}

Передача/прием данных

Процесс передачи/приема данных с помощью SPI модуля, работающего в режиме Master, состоит из следующей последовательности действий:

1. установка низкого логического уровня на линии SS
2. загрузка данных в регистр SPDR
3. ожидание окончания передачи (проверка флага SPIF)
4. сохранение принятых данных (чтение SPDR), если требуется
5. возврат на 2-ой шаг, если переданы не все данные
6. установка высокого логического уровня на линии SS

Ниже приведено несколько вариантов функции передачи/приема данных.

Передача одного байта данных по SPI


void SPI_WriteByte(uint8_t data)
{
SPI_PORTX &= ~(1< SPDR = data;
while(!(SPSR & (1< SPI_PORTX |= (1<}

Передача и прием одного байта данных по SPI

uint8_t SPI_ReadByte(uint8_t data)
{
uint8_t report;
SPI_PORTX &= ~(1< SPDR = data;
while(!(SPSR & (1< report = SPDR;
SPI_PORTX |= (1< return report;
}

Передача нескольких байтов данных по SPI
*data – указатель на массив передаваемых данных, а num – размерность массива


void SPI_WriteArray(uint8_t num, uint8_t *data)
{
SPI_PORTX &= ~(1< while(num--){
SPDR = *data++;
while(!(SPSR & (1< }
SPI_PORTX |= (1<}

//Пример использования:
uint8_t buf = {12, 43, 98};


SPI_WriteArray(3, buf);

Передачи и прием нескольких байтов данных по SPI
*data – указатель на массив передаваемых данных, а num – размерность массива.
Принятые данные будут сохраняться в том же массиве.


void SPI_ReadArray(uint8_t num, uint8_t *data)
{
SPI_PORTX &= ~(1< while(num--){
SPDR = *data;
while(!(SPSR & (1< *data++ = SPDR;
}
SPI_PORTX |= (1<}

Файлы

Простой SPI драйвер, в котором сведены все описанные выше функции -

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

0. Вместо введения

Что это за статья?
Эта статья представляет собой компиляцию информации из различных источников, вольный перевод некоторых частей документации, а также мои собственные комментарии, дополнения и описания возникших проблем.

Для кого эта статья?
В первую очередь, для новичков, каковым являюсь и я. На форумах по embedded Linux очень часто можно встретить вопрос: «А как на этой плате работать с SPI?». Именно на него я и попытаюсь дать ответ. В качестве примера, я приведу код написанный для работы с моим тестовым SPI устройством.

Структура статьи
По причине того, что информации получилось достаточно много, статья разбита на несколько подразделов:

  1. Что такое SPI?
  2. Обзор SPI подсистемы в Linux
  3. Разработка userspace протокольного SPI драйвера с использованием spidev
  4. Разработка протокольного SPI драйвера уровня ядра
  5. Документация
Первые два пункта войдут в первую часть статьи, оставшиеся во вторую.

Первый подраздел описывает работу шины SPI, данная часть статьи конкретно к Linux никак не привязана, поэтому её можно читать тем, кому Linux не интересен, а нужно лишь получить информацию об этом интерфейсе.

Второй подраздел описывает структуры и механизмы лежащие в основе работы с SPI в Linux, его нужно прочесть для понимания того, о чём пойдёт речь в третьей и четвёртой частях.

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

Ошибки
Я не волшебник, я только учусь. Если найдёте какие-либо ошибки или неточности, пожалуйста, сообщите мне.

1. Что такое SPI?

Аббревиатура SPI означает «Serial Peripheral Interface» или в русском варианте «последовательный периферийный интерфейс». Название говорит само за себя, данный интерфейс используется для работы с различными периферийными устройствами. Например, это могут быть различные ЦАП/АЦП, потенциометры, датчики, расширители портов ввода/вывода (GPIO), различная память и даже более сложная периферия, такая как звуковые кодеки и контроллеры Ethernet.

С технической точки зрения SPI - это синхронная четырёхпроводная шина. Она представляет собой соединение двух синхронных сдвиговых регистров, которые является центральным элементом любого SPI устройства. Для соединения используется конфигурацию ведущий/ведомый. Только ведущий может генерировать импульсы синхронизации. В схеме всегда только один ведущий (в отличие от той же шины I2C, где возможен вариант с более чем одним ведущим), количество ведомых может быть различно. В общем случае выход ведущего соединяется со входом ведомого, и наоборот, выход ведомого соединяется со входом ведущего. При подаче импульсов синхронизации на выход SCK, данные выталкиваются ведущим с выхода MOSI, и захватываются ведомым по входу MISO. Таким образом если подать количество импульсов синхронизации соответствующее разрядности сдвигового регистра, то данные в регистрах обменяются местами. Отсюда следует что SPI всегда работает в полнодуплексном режиме. А вот нужны ли нам данные, полученные от устройства при записи какого-либо параметра, это уже другой вопрос. Часто бывает что данные полученные от устройства при записи в него данных являются мусором, в таком случае их просто игнорируют, но мы их получим вне зависимости от нашего желания.

Контроллер SPI, как правило, реализуется периферийным блоком в MCU или eMPU. В большинстве чипов он может работать как в режиме ведущего, так и в режиме ведомого. Но на данный момент Linux поддерживает только режим ведущего (Master).

Существует несколько способов включения SPI устройств.

Простейший из них вы видите на рисунке выше (спасибо Wikipedia за рисунки под свободной лицензией GFDL). В данном случае к ведущему все ведомые подключаются параллельно, за исключением сигнала выбора ведомого (~CS). Для каждого ведомого необходим отдельный сигнал выбора ведомого (на рисунке они обозначены как SSx). Для сигналов выбора ведомого могут использоваться как специально предназначенные для этого выходы SPI-контроллера, так и порты ввода/вывода общего назначения (GPIO) микроконтроллера.

Два проводника используются для передачи данных, один для подачи тактовых импульсов и по одному сигналу выбора ведомого для каждого из ведомых.
Описание используемых сигналов:

  • MOSI - Master Output, Slave Input (выход ведущего, вход ведомого). Данный сигнал предназначен для последовательной передачи данных от ведущего к ведомому. Также может называться SDO, DO и т.п.
  • MISO - Master Input, Slave Output (вход ведущего, выход ведомого). Данный сигнал предназначен для последовательной передачи данных от ведомого к ведущему. Может называться SDI, DI и т.п.
  • SCK - Serial Clock (сигнал синхронизации). Используется для синхронизации при передаче данных. Также может иметь название SCLK, CLK и др.
  • ~CS - Chip Select (выбор микросхемы). С помощью данного сигнала происходит активация ведомого устройства. Обычно он является инверсным, то есть низкий уровень считается активным. Иногда его называют ~SS (Slave Select, рус. «выбор ведомого»).

Частным случаем независимого подключения является вариант с одним единственным ведомым. В таком случае может возникнуть желание подтянуть сигнал ~CS к земле, чтобы устройство всегда было в активном состоянии. Но делать это крайне не рекомендуется, так как ведомое устройство может использовать сигнал CS для инициализации или для других служебных целей.

Основное неудобство при независимом подключении ведомых в том, что для каждого из ведомых необходим отдельный сигнал ~CS. Каскадная схема подключения, в зарубежной литературе называемая «daisy-chain» (можно перевести как «гирлянда»), лишена такого недостатка.

Как видно из рисунка выше, здесь используется общий сигнал выбора ведомого для всех ведомых. Выход каждого из ведомых соединяется со входом следующего. Выход последнего ведомого соединяется со входом ведущего, таким образом образуется замкнутая цепь. При таком подключении можно считать что последовательно соединённые устройства образуют один большой сдвиговый регистр. Соответственно, данные можно записать во все устройства «за один присест», предварительно собрав нужный пакет, объединяющий данные для каждого из устройств в порядке соответствующем физическому порядку соединения. Но тут есть один тонкий момент. Во-первых, все микросхемы должны поддерживать такой тип подключения; во-вторых, ядро Linux не поддерживает такой тип подключения, так что если всё же захотите его использовать, то вам придётся модифицировать существующие драйвера, либо же написать собственные.

Существует четыре режима работы SPI устройств. Как правило, именно они вызывают больше всего путаницы у новичков. Данные четыре режима представляют собой комбинацию двух бит:

  • CPOL (Clock Polarity) - определяет начальный уровень (полярность) сигнала синхронизации.
    CPOL=0 показывает, что сигнал синхронизации начинается с низкого уровня, так что передний фронт является нарастающим, а задний - падающим.
    CPOL=1, сигнал синхронизации начинается с высокого уровня, таким образом передний фронт является падающим, а задний - нарастающим.
  • CPHA (Clock Phase) - фаза синхронизации, определяет по какому из фронтов синхронизирующего сигнала производить выборку данных.
    CPHA=0 показывает что необходимо производить выборку по переднему фронту, а
    CPHA=1 показывает что выборку данных необходимо производить по заднему фронту.
Эти два бита и образуют номер режима. CPOL является старшим битом, а CPHA - младшим. Иногда в документации к устройству явно не указывают номер режима, но его всегда можно легко определить по временным диаграммам. Также важно понимать, что выборка и установка данных всегда происходят по противоположенным фронтам синхронизирующего сигнала. Например, пусть наше устройство работает в режиме 0 (наиболее распространённый вариант), в таком случае ведомое устройство будет считывать бит данных со входа MOSI по переднему нарастающему фронту синхронизирующего сигнала, а ведущее устройство будет считывать данные от ведомого на входе MISO также по переднему нарастающему фронту. Для большей наглядности я приведу осциллограммы для всех четырёх режимов работы:

На этом рисунке показаны сигналы MOSI (синяя линия) и SCK (жёлтая линия). Во всех случаях передаётся число 0x64. Светлые вертикальные линии показывают момент выборки данных. Рассмотрим режим 2, для которого, как мы помним, CPOL=1, а CPHA=0. Таким образом мы видим что синхронизирующий сигнал изначально имеет высокий уровень, а выборка производится по переднему фронту (в данном случае спадающему). Так как осциллограф у меня имеет только два канала, сигналы ~CS и MISO не показаны. Но в данном случае они не так интересны, например, сигнал ~CS представляет собой просто «провал» на всём протяжении передачи данных.

2. Обзор SPI подсистемы в Linux

Драйверы SPI в Linux делятся на две части. Первая - это драйверы SPI контроллеров, которые работают непосредственно с железом конкретно взятого контроллера. Такие драйверы определяют как настроить контроллер, какие действия предпринять при переходе в режим пониженного энергопотребления (suspend) и выходе из него(resume), выбор следующей передачи (spi_transfer) из очереди передач в сообщении (spi_message, об очередях чуть ниже) и отправка его непосредственно в порт, также определяется как активировать/деактивировать конкретное устройство посредством CS (функции cs_activate/cs_deactivate). В этой статье я не буду описывать данный тип драйверов. Как правило, они уже реализованы для тех MCU/eMPU на которые существует порт Linux, и лезть в них руками надо только в том случае, если вам нужна какая-то специфичная функция, вроде Chip Select Decoding, для возможности активации нужного ведомого устройства посредством внешней логики. Иногда это бывает полезно, например, в случае недостатка GPIO.

Вторая часть - это протокольные драйверы, используемые для работы с различными ведомыми устройствами, которые подключены к шине SPI. Данные драйверы называют «протокольными», потому что они лишь отправляют и получают различные данные от ведомых устройств, при этом не работая напрямую с каким-либо оборудованием. Именно данный тип драйверов нам наиболее интересен, так как позволяет добавить поддержку интересующего ведомого устройства в систему, его то мы и рассмотрим.

Большинство протокольных драйверов представляет собой модули ядра. Например, если устройство представляет собой аудиокодек подключаемый по SPI, то драйвер будет также использовать функции предоставляемые ALSA, а программы (например, madplay) смогут работать с ним посредством символьного устройства /dev/audio, не имея ни малейшего понятия о том как он аппаратно устроен и к какой шине подключен.

Также ядро предоставляет протокольный драйвер общего назначения, называемый spidev, с интерфейсом в виде символьного устройства. Он позволяет совершать полудуплексные обращения к ведомому SPI-устройству посредством стандартных системных вызовов read() и write(), устанавливать режим работы, а также производить полнодуплексный обмен данными посредством ioctl() вызовов.

Таким образом протокольные драйверы для SPI устройств можно разделить на два типа:

  • userspace драйверы, работающие в пространстве пользователя и представляющие собой обычные программы на любом языке, работающие с SPI устройством посредством чтения/записи соответствующего символьного устройства spidev.
  • драйверы, работающие в пространстве ядра и предоставляющие интерфейс для userspace посредством файлов устройств в каталоге /dev, либо с помощью атрибутов в каталоге устройства в sysfs.
Все обращения к SPI устройствам Linux ставит в очередь. Протокольные драйверы SPI оперируют явно или не явно сообщениями представленными структурой struct spi_message, которая является мультисегментной SPI транзакцией.
struct spi_message { struct list_head transfers; struct spi_device *spi; unsigned is_dma_mapped:1; void (*complete)(void *context); void *context; unsigned actual_length; int status; struct list_head queue; void *state; };
transfers - связанный список передаваемых сегментов в транзакции (передач);
spi - указатель на spi устройство, в очереди которого стоит данное сообщение;
is_dma_maped - если данный флаг «истина», то предоставлены оба, dma и cpu виртуальные адреса для каждого буфера передачи;
complete - обратный вызов, вызываемый для извещения об окончании транзакции;
context - аргумент для обратного вызова complete();
actual_length - полное число байт, которые были переданы во всех успешных предачах;
status - 0 в случае успеха, либо отрицательное значение с errno в случае ошибки;

Теги:

  • linux
  • spi
  • spidev
  • embedded
  • kernel module
Добавить метки

SPI - Serial Peripheral Interface или «Последовательный периферийный интерфейс» - это синхронный протокол передачи данных для сопряжения ведущего устройства (Master) с периферийными устройствами (Slave) . Ведущим устройством часто является микроконтроллер. Связь между устройствами осуществляется по четырём проводам, поэтому SPI иногда называют «четырёхпроводной интерфейс». Вот эти шины:

Существует четыре режима передачи данных (SPI_MODE0, SPI_MODE1, SPI_MODE2, SPI_MODE3 ), обусловленные сочетанием полярности тактовых импульсов (работаем по уровню HIGH или LOW), Clock Polarity, CPOL , и фазой тактовых импульсов (синхронизация по переднему или заднему фронту тактового импульса), Clock Phase, CPHA . В последнем столбце таблицы приведены поясняющие иллюстрации. На них Sample обозначены моменты, когда данные на линии должны быть готовы и считываются устройствами. Буквой Z отмечено, что состояние данных на линии неизвестно или не важно.

Интерфейс SPI предусматривает несколько вариантов подключения ведомых устройств: независимое и каскадное . При независимом подключении к шине SPI ведущее устройство обращается к каждому ведомому устройству индивидуально. При каскадном подключении ведомые устройства срабатывают поочерёдно, как бы каскадом.


Виды подключения устройств для работы по интерфейсу SPI: независимое и каскадное

2 Реализация интерфейса SPI на платах семейства Arduino

В Arduino шины интерфейса SPI находятся на определённых портах. У каждой платы своё соответствие выводов. Для удобства выводы продублированы и вынесены также на отдельный разъём ICSP (In Circuit Serial Programming, программирование устройства, включённого в цепь, по последовательному протоколу). Обратите внимание, что на разъёме ICSP отсутствует пин выбора ведомого - SS, т.к. подразумевается, что Arduino будет использоваться как ведущее устройство в сети. Но при необходимости вы можете назначить любой цифровой вывод Ардуино в качестве SS.

На рисунке приведено стандартное соответствие выводов шинам SPI для Arduino UNO и Nano.


3 Стандартная библиотека для работы по интерфейсу SPI

Для Arduino написана специальная библиотека , которая реализует протокол SPI . Она устанавливается вместе со средой разработки Arduino IDE . Подключается она так: в начале программы добавляем #include SPI.h .

Чтобы начать работу по протоколу SPI , нужно задать настройки и затем инициализировать протокол с помощью процедуры SPI.beginTransaction() . Можно выполнить это одной инструкцией: SPI.beginTransaction(SPISettings(14000000, MSBFIRST, SPI_MODE0))

Это значит, что мы инициализируем протокол SPI на частоте 14 МГц, передача данных идёт, начиная с MSB (наиболее значимого бита), в режиме SPI_MODE0 .

После инициализации выбираем ведомое устройство, переводя соответствующий пин SS в состояние LOW . Затем передаём ведомому устройству данные командой SPI.transfer() . После передачи возвращаем SS в состояние HIGH .


Работа с протоколом завершается командой SPI.endTransaction() .

Желательно минимизировать время выполнения передачи между инструкциями SPI.beginTransaction() и SPI.endTransaction(), чтобы не возникло накладок, если другое устройство попробует инициализировать передачу данных, используя другие настройки.

Если вы планируете в своём скетче использовать стандартные пины Arduino, можно не описывать их в начале программы, т.к. они уже определены в самой библиотеке и имеют следующие имена:

#define PIN_SPI_SS (10) #define PIN_SPI_MOSI (11) #define PIN_SPI_MISO (12) #define PIN_SPI_SCK (13)

Данные пины определены в файле pins_arduino.h , который находится по пути %programfiles%\arduino-(версия)\hardware\arduino\avr\variants\ (если вы устанавливали программу в стандартное расположение). То есть, например, чтобы опустить пин выбора ведомого в состояние "0", можно написать:

DigitalWrite(PIN_SPI_SS, LOW);

4 Подключение сдвигового регистра к Arduino

Рассмотрим практическое применение интерфейса SPI . Будем зажигать светодиоды, управляя 8-битным сдвиговым регистром по шине SPI . Подключим к Arduino сдвиговый регистр 74HC595 . К каждому из 8-ми выходов регистра через ограничительный резистор подключим по светодиоду номиналом 220 Ом. Схема приводится на рисунке.


5 Скетч для управления сдвиговым регистром по интерфейсу SPI

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

#include const int pinSelect = 8; // пин выбора регистра void setup() { SPI.begin(); // инициализация интерфейса SPI pinMode(pinSelect, OUTPUT); // digitalWrite(pinSelect, LOW); // выбор ведомого устройств (регистра) SPI.transfer(0); // очищаем содержимое регистра digitalWrite(pinSelect, HIGH); // конец передачи Serial.begin(9600); } void loop() { for (int i=0; i }

Сначала подключим библиотеку SPI и инициализируем интерфейс SPI . Определим пин 8 как пин выбора ведомого устройства SS . Очистим сдвиговый регистр, послав в него значение "0". Инициализируем последовательный порт.

Чтобы зажечь определённый светодиод с помощью сдвигового регистра, нужно подать на его вход 8-разрядное число. Например, чтобы загорелся первый светодиод - подаём двоичное число 00000001, чтобы второй - 00000010, чтобы третий - 00000100, и т.д. Эти двоичные числа при переводе в десятичную систему счисления образуют такую последовательность: 1, 2, 4, 8, 16, 32, 64, 128 и являются степенями двойки от 0 до 7.

Соответственно, в цикле loop() по количеству светодиодов делаем пересчёт от 0 до 7. Функция pow(основание, степень) возводит 2 в степень счётчика цикла. Микроконтроллеры не очень точно работают с числами типа "double", поэтому для преобразования результата в целое число используем функцию округления round() . И передаём получившееся число в сдвиговый регистр. Для наглядности в монитор последовательного порта выводятся значения, которые получаются при этой операции: единичка «бежит» по разрядам - светодиоды загораются волной.

6 «Бегущая волна» из светодиодов

Светодиоды загораются по очереди, и мы наблюдаем бегущую «волну» из огоньков. Управление светодиодами осуществляется с помощью сдвигового регистра, к которому мы подключились по интерфейсу SPI . В результате для управления 8-ю светодиодами задействованы всего 3 вывода Arduino. Если бы мы подключали светодиоды напрямую к цифровым портам Arduino, нам бы потребовалось для каждого светодиода использовать отдельный порт.

Мы изучили самый простой пример работы Arduino с шиной SPI . Более подробно рассмотрим работу нескольких сдвиговых регистров при независимом и каскадном подключениях в отдельной статье.