Вспоминаем SPI
Шина 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 работает, пока ты отдыхаешь.#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-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 | |
SPCR | SPIE | SPE | 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 | |
SPSR | SPIF | WCOL | - | - | - | - | - | 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 | |
SPDR | MSB | LSB |
Тут все понятно: MSB - старший бит, LSB - младший бит, между ними еще 6 штук.
Теперь мы видим, что если мы хотим инициализировать SPI в режиме ведущего, не пользуясь библиотекой, нам достаточно записать единицы в четвертый (SPE) и шестой (MSTR) биты управляющего регистра:
SPCR = (1<<SPE)|(1<<MSTR);
Для инициализации SPI в режиме ведомого достаточно просто написать
SPCR = (1<<SPE);
Видно, что на выходах устройств стоят восьмибитные сдвиговые регистры. Во время сеанса передачи битики перетекают из регистра ведущего устройства в регистр ведомого устройства, выталкивая оттуда родные битики, которым ничего не остается, как пойти на освободившееся место. На рисунке, который я не вырезал, под сдвиговым регистром еще нарисован буфер чтения, в который сваливаются пришедшие байты.
Ёжики-треножики! Теперь все понятно!
Собираем установку
Пусть ведущей бордой будет Arduino UNO. Приляпаем девять кнопок к ее цифровым пинам 2,3,4,5,6,7,8,9 и 0. Схема включения каждой кнопки вот такая:
Свой странный, на первый взгляд, выбор пинов для кнопок я могу объяснить следующим образом. Если внимательно посмотреть на 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);
}
#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;
}
Все остальное станет понятно по ходу листинга:
//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;
}
//определяем пины 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.
Успехов в ваших проектах!
Спасибо, отличная статья- редко когда на ардуине кто-то что-то показывает полезного. Обычно "подцепил библиотеку помигал разобрал ничего не понял"
ОтветитьУдалитьОписано прекрасно, но почему дурацкий сленг ? борда- вместо board, "приляпаем" вместо "припаяем" и т. д. Не надо извращаться, пишите литературно.
ОтветитьУдалить