22 ноября 2011 г.

Arduino: Параллельное и последовательное подключение ведомых устройств к шине SPI

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

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

Параллельное подключение устройств к шине SPI

При параллельном подключении несколько ведомых устройств используют общие провода SCLK, MOSI и MISO, при этом каждый ведомый имеет свою линию SS.  Ведущий определяет устройство, с которым осуществляется обмен, путем формирования низкого сигнала на его SS.
Видно, что для подключения n устройств требуется n линий SS, то есть для функционирования SPI-среды с n ведомыми нужно выделить под это n+3 ноги микроконтроллера.

Последовательное подключение устройств к шине SPI

При последовательном подключении устройств они используют общие провода SCLK и SS, а выход одного подсоединяется во вход другого. MOSI ведущего подключается к первому устройству, а MISO - к последнему. То есть для ведущего на шине SPI это как бы одно устройство.

Такое подключение позволяет построить, например, из двух 8-битных сдвиговых регистров один 16-битный, чем мы сейчас и займемся.
Остается отметить прелесть такого подключения: подключи хоть 3, хоть 8 устройств, это займет всего 4 ноги на контроллере.

Последовательное соединение двух сдвиговых регистров

Еще раз взглняем на сдвиговый регистр 74HC595:

Мы помним, что DS - есть пин последовательного ввода, а Q0-Q7 пины последовательного вывода. Q7S, который мы не использовали, когда у нас был всего один регистр в схеме, - это последовательный вывод регистра. Он находит свое применение, когда мы передаем больше 1 байта в регистры. Через этот пин последовательно протолкнутся все байты, предназначенные для последующих регистров, а последний передаваемый байт останется в первом регистре.


Подсоединяя пин Q7S одного первого регистра к пину DS второго (и так далее, если это необходимо), получаем двойной (тройной и т.д.) регистр.

Подключение двойного 7-сегментного дисплея

Двойной 7-семисегментный дисплей это, как правило, устройство с 18-ю ногами, по 9 на каждый символ. Взглянем на схему (мой дисплей имеет маркировку LIN-5622SR и есть большая вероятность того, что его схема подключения окажется уникальна):

Это дисплей с общим анодом, что говорит о необходимости подачи на com1 и com2 высокого уровня ТТЛ, а для зажигания диода - низкий уровень на соответствующей ноге. Если у вас дисплей с общим катодом, делать нужно наоборот!

Подключим дисплей, как показано на схеме:

Левый дисплей подключаем к первому регистру: 1A к ноге Q0, 1B к ноге Q1, 1C к ноге Q2 и т.д. Общий контакт com1 подключаем на землю. Точно так же поступаем с правым дисплеем: 2A к ноге Q0, 2B к ноге Q1, и т.д., общий контакт com2 - на землю.

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

Простая змейка на двойном 7-сегментном дисплее

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

Наш цикл будет состоять из восьми кадров, на каждом из которых будет зажигаться определенные три светодиода. На первом кадре будут гореть 1E, 1F, 1A (см. схему), на втором - 1F, 1A, 2A, на третьем - 1A, 2A, 2B и так далее, на восьмом - 1D, 1E, 1F.

Снова, для удобства, составим табличку байтов, помня, что по умолчанию биты передаются, начиная со старшего, т.е. 2h.

Кадр
1 abcd efgh
2 abcd efgh
hex
1
   0111 0011
   1111 1111
EC FF
2
   0111 1011
   0111 1111
ED EF
3
   0111 1111
   0011 1111
EF CF
4
   1111 1111
   0001 1111
FF 8F
5
   1111 1111
   1000 1111
FF 1F
6
   1110 1111
   1100 1111
7F 3F
7
   1110 0111
   1110 1111
7E 7F
8
   1110 0011
   1111 1111
7C FF


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


#include <SPI.h> //подключаем библиотеку SPI
enum { reg = 9 }; //выбираем линию SS регистра на 9-м пине Arduino
void setup(){
  SPI.begin(); //инициализируем SPI
    //переводим выбранный для передачи пин в режим вывода
  pinMode(reg, OUTPUT);
}


void loop(){
  //Заполняем массив байтами, которые будем передавать
  static uint8_t digit[16] = 
    {0xFF,0xCE,0xFF,0xDE,0xFC,0xFE,0xF8,0xFF,
     0xF1,0xFF,0xF3,0xF7,0xF7,0xE7,0xFF,0xC7};
    //передаем по два байта из массива и защелкиваем регистры 
  for (int i=0;i<16;i+=2){
    digitalWrite(reg, LOW);
    SPI.transfer(digit[i]);
    SPI.transfer(digit[i+1]);
    digitalWrite(reg, HIGH);
    delay(80); //пауза между кадрами
  }
}

Видео работы программы:



Параллельные процессы в  Arduino 

Почему разработчики Arduino уделяют особое внимание примеру Blink without delay?

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

Стержнем нашей новой конструкции станет таймер. Таймер будет считать время, а мы заставим происходить то или иное событие через определенные промежутки времени. Например, каждую секунду будет тикать дисплей с часами, а каждые 0,86 секунды будет мигать светодиод.

В Arduino есть штука, которая отсчитывает время с начала работы программы, называется она millis(). С ее-то помощью и организуется "распараллеливание" задач.

Итоговый проект: часы и хитрая змейка


Соберем такую схему:


Левый и средний регистры у нас работают  с точки зрения ведущего как одно устройство, а правый регистр - как другое. Видно, что эти два устройства используют один и тот же провод SCLK (13-й пин Arduino, провод показан оранжевым) и MOSI (11-й пин, желтый цвет),   SS используются разные (пины 8 и 9, зеленый цвет). Подключение 7-сегментных дисплеев к регистрам показано для моих конкретных моделей и, вероятно, не будет совпадать с вашим.


В этот раз сделаем нашу змейку более хитрой: она будет пробегать по всем сегментам так же,  как ездил на мотоцикле по дорожной развязке волк в серии "Ну Погоди!", которая начинается с того, что он этот самый мотоцикл выкатывает из гаража и надевает каску.

Последовательность байтов для этой змейки будет такая:

   static uint8_t snake[32] = 
     {0xFF,0x9E,0xFF,0xDC,0xFF,0xF8,0xFF,0xF1,
      0xFF,0xE3,0xFF,0xA7,0xBF,0xAF,0xBD,0xBF,
      0xBC,0xFF,0xDC,0xFF,0xCE,0xFF,0xC7,0xFF,
      0xE3,0xFF,0xB3,0xFF,0xBB,0xBF,0xBF,0x9F};

Теперь суть: функция millis() сидит и считает миллисекунды от начала начал. В начале каждого цикла loop мы запоминаем значение millis() в переменную timer. Заводим переменные snakeTimerPrev и digitTimerPrev, которые будут хранить в себе момент предыдущего события: для snakeTimerPrev - это включение предыдущего кадра анимации змейки, для digitTimerPrev - включение предыдущей цифры. Как только разница текущего времени (timer) и предыдущего (snakeTimerPrev или digitTimerPrev) становится равна заданному периоду (в нашем случае - 80 и 1000 мс, соответственно), мы производим передачу следующего кадра/байта.

Таким образом,

  • каждые 80 мс контроллер будет опускать сигнал на линии SS двойного дисплея, передавать два байта и отпускать линию.
  • каждую секунду контроллер будет опускать сигнал на линии SS одиночного дисплея, передавать один байт и отпускать линию.

Реализуем это на Arduino. Я уже все подробно описывал до этого, думаю, нет смысла комментировать.

#include <SPI.h>


enum { snakePin = 9, digitPin = 8 };
unsigned long timer=0, snakeTimerPrev=0, digitTimerPrev=0;
int i=0, j=0;



void setup(){
  SPI.begin();
  pinMode(digitPin, OUTPUT);
  pinMode(snakePin, OUTPUT);
}


void loop(){
   static uint8_t digit[16] = 
     {0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
      0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E};
   static uint8_t snake[32] = 
     {0xFF,0x9E,0xFF,0xDC,0xFF,0xF8,0xFF,0xF1,
      0xFF,0xE3,0xFF,0xA7,0xBF,0xAF,0xBD,0xBF,
      0xBC,0xFF,0xDC,0xFF,0xCE,0xFF,0xC7,0xFF,
      0xE3,0xFF,0xB3,0xFF,0xBB,0xBF,0xBF,0x9F};


   timer=millis();


   if (timer-snakeTimerPrev>80){
      digitalWrite(snakePin, LOW);
      SPI.transfer(snake[j]);
      SPI.transfer(snake[j+1]);
      digitalWrite(snakePin, HIGH);
      j<30 ? j+=2 : j=0;
      snakeTimerPrev=timer;
      }
   if (timer-digitTimerPrev>1000){
      digitalWrite(digitPin, LOW);
      SPI.transfer(digit[i]);
      digitalWrite(digitPin, HIGH);
      i<9 ? i++ : i=0;
      digitTimerPrev=timer;
      }
 }

Вот я использовал выражение j<30 ? j+=2 : j=0; Кто забыл, это логическое выражение типа А ? B : C, оно означает: ЕСЛИ А, ТО B, ИНАЧЕ C. Крайне полезная штука!

Ну и вкусное виде о том, что получилось: