22 июня 2012 г.

Соединяем две Arduino через SPI

Маразм крепчал, и поэтому сегодня мы будем соединять две платы Arduino через шину SPI. Сразу вас разочарую, передавать инфу мы будем только в одну сторону. Дуплексное соединение организуется чуть-чуть сложнее, чем то, что мы натворим сейчас. Но зато на простом примере разберемся, как работать с SPI не через библиотеку, а через регистры процессора. И, что главное, хорошенько изучим шину на низком уровне.

Вспоминаем SPI

Про SPI я уже писал миллиард раз, однако вспомним ключевые детали. SPI (Serial Peripheral Interface) - это последовательный синхронный интерфейс, который предназначен для общения контроллера с периферийными устройствами, или контроллеров друг с другом (жизненная целесообразность последнего кажется неочевидной в виду наличия UART).

Шина SPI состоит из четырех линий:
MOSI (Master Out Slave In) – передача данных от ведущего к ведомому.
MISO (Master In Slave Out) – передача данных от ведомого к ведущему.
SS (Slave Select) – выбор ведомого устройства.
SCK (Serial ClocK– передача тактового сигнала от ведущего к ведомому.




Одно из устройств на шине называется ведущим (master), а все остальные - ведомыми (slave). Master инициализирует передачу со slave, выставляя низкий уровень на его линии SS, после чего производится одновременная двухсторонняя передача данных по линиям MOSI и MISO с тактированием по линии SCK. 

В Arduino DE есть прекрасная библиотека SPI.h, которая превращает использование шины в посиделки с блинами: 

//подключаем библиотеку
#include <SPI.h>
//задаем пин slave select
#define SS_PIN 10
//определяем переменную для хранения передаваемого байта
byte byte2send;

void setup() {
//инициализируем SPI
  SPI.begin(); 
}

void loop() {
  //опускаем линию SS - начинаем передачу
  digitalWrite(SS_PIN, LOW);
  //передаем байт (или скока угодно байт)
  SPI.transfer(byte2send);
  //поднимаем линию SS - завершаем передачу
  digitalWrite(SS_PIN, HIGH);

}
И SPI работает, пока ты отдыхаешь.

Однако, чтобы запилить SPI-slave на Arduino, придется залезть поглубже в SPI и поизучать матчасть.

SPI уровнем ниже

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

Вот и для SPI, естественно, есть свои регистры: регистр управления SPI (SPCR), регистр состояния SPI (SPSR) и регистр данных SPI (SPDR). Когда мы разрешаем работу SPI в регистре управления, мы как бы говорим: "давай-ка, дружочек, твои ноги 10, 11, 12 и 13 будут отвечать за шину SPI, перебинди-ка их функции, да побыстрее!"

Состав управляющего регистра:
7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit 
SPCRSPIESPE DORD MSTR CPOL CPHA SPR1 SPR0 

SPIE (SPI Interrupt Enable) - разрешение прерывания SPI
SPE (SPI Enable) - разрешение работы SPI
DORD (Data Order) - направление передачи. 1 - LSB вперед, 0 - MSB вперед
MSTR (Master/Slave select) - выбор ведущего. 1 - ведущий, 0 - ведомый
CPOL (Clock Polarity) - выбор полярности тактирования
CPHA (Chock Phase) - выбор фазы тактирования
SPR1,0 (SPI clock Rate select) - выбор делителя частоты процессора (вместе с SPI2X)

Состав регистра состояния:
7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit 
SPSRSPIFWCOL SPI2X 


SPIF (SPI Interrupt Flag) - флаг окончания передачи (только для чтения, снимается, когда выполнился обработчик прерывания)
WCOL (Write COLlision flag) - флаг факта записи в регистр данных во время передачи (только для чтения, снимается при чтении регистра данных)
SPI2X (Double SPI speed) - выбор делителя частоты процессора (вместе с SPR1,0)

Состав регистра данных:
7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 bit 
SPDRMSBLSB 

Тут все понятно: MSB - старший бит, LSB - младший бит, между ними еще 6 штук.

Теперь мы видим, что если мы хотим инициализировать SPI в режиме ведущего, не пользуясь библиотекой, нам достаточно записать единицы в четвертый (SPE) и шестой (MSTR) биты управляющего регистра:

  SPCR = (1<<SPE)|(1<<MSTR);
здесь << - это битовый сдвиг, а | - побитовое ИЛИ. Данная запись означает, что мы берем пустой регистр SPCR, берем единицу и сдвигаем ее на позицию бита SPE (01000000); потом мы берем еще одну единицу и сдвигаем ее на позицию бита MSTR (00010000); сделав между ними операцию побитового ИЛИ, мы получаем 01010000.

Для инициализации SPI в режиме ведомого достаточно просто написать

  SPCR = (1<<SPE);
На рисунке ниже, вырезанном из оригинального даташита ATmega328, показано соединение ведущего и ведомого устройства:


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

Ёжики-треножики! Теперь все понятно!

Собираем установку

Сделаем такой проект: пусть у ведущей Arduino будет 8 кнопок, которые можно произвольно натыкать, и еще одна кнопка, которая называется "сделать магию": по ее нажатию состояние кнопок будет упаковываться в байт, пересылаться по SPI ведомой Arduino и отображаться светодиодами.

Пусть ведущей бордой будет Arduino UNO. Приляпаем девять кнопок к ее цифровым пинам 2,3,4,5,6,7,8,9 и 0. Схема включения каждой кнопки вот такая:

Это схема с подтягивающим к земле резистором. Так делается для того, чтобы с кнопки не читались шумы. Когда кнопка не нажата, то пин прижат к земле и стабильно читает 0. Когда кнопка нажимается, она соединяет питание с пином, который читает 5В.

Свой странный, на первый взгляд, выбор пинов для кнопок я могу объяснить следующим образом. Если внимательно посмотреть на Arduino UNO сверху, то можно увидеть, что рядом с цифровыми пинами 0 и 1 написано RX и TX, что означает передачу и прием по UART. Если подключить туда кнопки, то программа тупо не зальётся в контроллер, потому что эти ноги просто-напросто притянуты к земле! Далее, когда вы будете использовать монитор последовательного соединения (а вы его будете использовать, когда придется дебажить программу), вам опять понадобится TX. Поэтому было бы логично подключить кнопки к пинам 2-10, что я и сделал. До того, как прикрутил SPI... Судьба такова, что нога 10 занята как линия SS, поэтому пришлось кнопку "сделать магию" перенести на нулевой пин, все время выдирая проводок, когда хочется перезалить программу.

Вот, что у меня получилось:

В качестве ведомой борды будем использовать Arduino Pro Mini, о которой я рассказывал несколько дней назад. Тут бесхитростно берем 8 белых светодиодов, которые через токоограничивающие резисторы подключаем к пинам 2-9.

Примерно вот так:

Организуем шину SPI: соединим SS с SS, MOSI с MOSI и SCK с SCK. Теперь все в шоколаде, и наше чудовище готово:


Пишем

Хоть мы сегодня научились инициализировать SPI по-грамотному, для ведущей Arduino оставим все в "традиционном" стиле. Конечно, большая часть программы состоит из того, что мы подавляем дребезг кнопок с помощью библиотеки Bounce. Как это делать, я недавно писал. Я решил, что здесь это критично, потому что из-за дребезга по SPI будет посылаться всякое барахло. Да и вообще, когда нет дребезга - это круто!


//MASTER

//Подключаем нужные библиотеки
#include <Bounce.h>
#include <SPI.h>
//Задаем пин Slave select для SPI
#define SS_PIN 10
//Создаем объекты класса подавления дребезга
Bounce button0 = Bounce(2, 5);
Bounce button1 = Bounce(3, 5);
Bounce button2 = Bounce(4, 5);
Bounce button3 = Bounce(5, 5);
Bounce button4 = Bounce(6, 5);
Bounce button5 = Bounce(7, 5);
Bounce button6 = Bounce(8, 5);
Bounce button7 = Bounce(9, 5);
Bounce button8 = Bounce(0, 5);
//определяем переменную для передаваемого байта
byte byte2send=0x00;

void setup() {
  //определяем пин slave select как выход
  pinMode(SS_PIN, OUTPUT);
  //определяем пины с кнопками как входы
  for(int i=2;i<11;i++){
    pinMode(i, INPUT);
  }
  //инициализируем последовательное соединение
  Serial.begin(9600);
  //инициализируем SPI
  SPI.begin(); 
}

void loop() {
  //если нажата кнопка
  if ( button0.update() ) {
    if ( button0.read() == HIGH) {
  //пишем единицу в соответствующий бит в байте для передачи
      byte2send = byte2send | (1 << 0);
    }
  }
  //то же самое еще 7 раз
  if ( button1.update() ) {
    if ( button1.read() == HIGH) {
      byte2send = byte2send | (1 << 1);
    }
  }
  if ( button2.update() ) {
    if ( button2.read() == HIGH) {
      byte2send = byte2send | (1 << 2);
    }
  }
  if ( button3.update() ) {
    if ( button3.read() == HIGH) {
      byte2send = byte2send | (1 << 3);
    }
  }
  if ( button4.update() ) {
    if ( button4.read() == HIGH) {
      byte2send = byte2send | (1 << 4);
    }
  }
  if ( button5.update() ) {
    if ( button5.read() == HIGH) {
      byte2send = byte2send | (1 << 5);
    }
  }
  if ( button6.update() ) {
    if ( button6.read() == HIGH) {
      byte2send = byte2send | (1 << 6);
    }
  }
  if ( button7.update() ) {
    if ( button7.read() == HIGH) {
      byte2send = byte2send | (1 << 7);
    }
  }
  //если нажата кнопка "сделать магию"
  if ( button8.update() ) {
    if ( button8.read() == HIGH) {
      //смотрим в мониторе наш байт
      Serial.println(byte2send,BIN);
      //отправляем байт по SPI
      digitalWrite(SS_PIN, LOW);
      SPI.transfer(byte2send);
      digitalWrite(SS_PIN, HIGH);
      //сбрасываем байт
      byte2send = 0x00;
    }
  }
}
Тут все понятно (кому не понятно, пишите комментарии, присылайте письма и т.д.), переходим к ведомой части. 

Раз мы решили не пользоваться библиотекой, то ручками определяем все нужные пины:

#define MOSI_PIN 11 
#define MISO_PIN 12 
#define SCK_PIN  13
#define SS_PIN 10


void setup() {
  pinMode(MOSI_PIN, INPUT);
  pinMode(MISO_PIN, OUTPUT);
  pinMode(SCK_PIN, INPUT);
  pinMode(SS_PIN, INPUT);
}

Потом мы определим функцию spi_receive(), которая собственно, будет читать данные с шины в буфер.


byte spi_receive()
{
  while (!(SPSR & (1<<SPIF))){};
  return SPDR;                    
}
Эта функция ничего не делает, пока не окончится сеанс передачи по SPI, а потом ХРЯСЬ! и вываливает данные из регистра SPDR. Узнает она об окончании передачи по флагу SPIF, который при этом поднимается хардверно в регистре SPSR.

Все остальное станет понятно по ходу листинга:

//SLAVE

//определяем пины SPI
#define MOSI_PIN 11 
#define MISO_PIN 12 
#define SCK_PIN  13
#define SS_PIN 10
//определяем переменную для получаемого байта
byte recievedByte;

void setup() {
  //обнуляем регистр управления SPI
  SPCR = B00000000;
  //разрешаем работу SPI
  SPCR = (1<<SPE);
  //определяем пины со светодиодами как выходы
  for(int i=2;i<10;i++){
    pinMode(i, OUTPUT);
  }
  //инициализируем последовательное соединение
  Serial.begin(9600);
  //определяем пины для работы с SPI
  pinMode(MOSI_PIN, INPUT);
  pinMode(MISO_PIN, OUTPUT);
  pinMode(SCK_PIN, INPUT);
  pinMode(SS_PIN, INPUT);
}

void loop() {
  //пока пин slave select опущен
  while (digitalRead(SS_PIN)==LOW){
    //принимаем байт и записываем его в переменную
    recievedByte=spi_receive();
    //смотрим в мониторе полученный байт
    Serial.println(recievedByte,BIN);
    //зажигаем светодиоды, которые соответствуют единицам в полученном байте
    digitalWrite(2,recievedByte & (1 << 0));
    digitalWrite(3,recievedByte & (1 << 1));
    digitalWrite(4,recievedByte & (1 << 2));
    digitalWrite(5,recievedByte & (1 << 3));
    digitalWrite(6,recievedByte & (1 << 4));
    digitalWrite(7,recievedByte & (1 << 5));
    digitalWrite(8,recievedByte & (1 << 6));
    digitalWrite(9,recievedByte & (1 << 7));
  }
}

//функция для приема байта
byte spi_receive()
{
  //пока не выставлен флаг окончания передачи, принимаем биты
  while (!(SPSR & (1<<SPIF))){};
  //позвращяем содержимое регистра данных SPI
  return SPDR;                    
}

Смотрим видео



Ваши вопросы оставляйте в комментариях, присылайте по e-mail или задавайте в группе вконтактеПодписывайтесь на твиттер GreenOakStudio, чтобы раньше всех узнавать о новых проектах Green Oak Studio

Успехов в ваших проектах!

2 комментария:

  1. Спасибо, отличная статья- редко когда на ардуине кто-то что-то показывает полезного. Обычно "подцепил библиотеку помигал разобрал ничего не понял"

    ОтветитьУдалить
  2. Описано прекрасно, но почему дурацкий сленг ? борда- вместо board, "приляпаем" вместо "припаяем" и т. д. Не надо извращаться, пишите литературно.

    ОтветитьУдалить