Освой Arduino играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Плата Arduino содержит ограниченное число выводов и при сложном проекте их не хватает для полноценной работы. К примеру, для подключения сегментного индикатора необходимо задействовать восемь выводов, два индикатора займут уже 16 выводов. Сдвиговый регистр позволяет сэкономить число используемых выводов, беря часть управления выводами на себя.
В электронике регистром называют устройство, которое может хранить небольшой объем данных для быстрого доступа к ним. Они есть внутри каждого контроллера и микропроцессора, включая и микроконтроллер Atmega328, который входит в состав платы Arduino Uno. Как правило регистры представляют собой сборку из D-триггеров — элементарных ячеек памяти. Записывать данные в регистр можно либо последовательно, либо параллельно. Регистры первого типа называются сдвиговыми, второго типа — параллельными.
Считывать данные из регистра можно одновременно из всех ячеек. Именно это его свойство помогает нам работать с кучей светодиодов.
Регистр называется сдвиговым, потому что при добавлении каждого нового бита в него, мы как бы сдвигаем все остальные в сторону. Вспомним, что один бит позволяет нам хранить ноль или единицу, истину или ложь. Посмотрим на диаграмме, как это происходит.
Пусть в начальном состоянии регистр уже заполнен какими-то восемью битами. Попробуем «задвинуть» в него восемь новых бит: 11011010.
Как видно, после двух итераций, в начале регистра оказалось два новых бита, а два бита в последних ячейках «вывалились» через край и пропали. На восьмом шаге весь регистр оказался заполнен новыми битами.
Регистры можно соединять в цепочку. В таком случае, вытесненный бит не будет пропадать без следа, а отправится в начало следующего регистра. При этом увеличивается число доступных выводов.
Самым популярным является восьмиразрядный (8 управляемых выходов) сдвиговый регистр 74HC595 (отечественный аналог КР1564ИР52), который можно встретить в стартовых наборах или купить отдельно.
Схема
+---------+
1 -|Q1 VCC|- 16
2 -|Q2 Q0|- 15
3 -|Q3 DS|- 14
4 -|Q4 OE|- 13
5 -|Q5 ST_CP|- 12
6 -|Q6 SH_CP|- 11
7 -|Q7 MR|- 10
8 -|GND Q7'|- 9
+---------+
74HC595 — восьмиразрядный сдвиговый регистр с последовательным вводом, последовательным или параллельным выводом информации, с триггером-защёлкой и тремя состояниями на выходе. Другими словами этот регистр позволяет контролировать 8 выходов, используя всего несколько выходов на самом контроллере. При этом несколько таких регистров можно объединять последовательно для каскадирования.
74HC595 может отдавать сигналы не только параллельно, но и последовательно. Это необходимо при объединении нескольких регистров, для получения 16 и более выходов. В этом случае первые 8 бит сигнала передаются на следующий регистр для параллельного вывода на нём.
Соберём схему, для которой понадобится сдвиговый регистр и восемь светодиодов с резисторами. При этом обратите внимание, что в нашем распоряжении восемь выводов регистра для светодиодов, а на плате используем только три цифровых вывода (экономия пяти выводов).
Установите сдвиговый регистр в центре макетной платы, чтобы ножки разделяла центральная разделительная дорожка.
Подключим контакты 16 (VCC) и 10 (MR) к выводу 5V на Arduino.
Соединяем контакты 8 (GND) и 13 (OE) с выводом GND на Arduino.
Соединяем три контакта, которыми мы будем управлять сдвиговым регистром:
Далее подключаем все восемь светодиодов с резисторами. Обратите внимание, что у регистра с одной стороны идут семь выводов подряд, а восьмой находится на выводе 15.
Вариант подключения (используются другие выводы платы).
Попробуем включить один светодиод. Сначала указываем используемые выводы платы (тактовая линия - clockPin, данные - dataPin, защёлка - latchPin).
В setup() устанавливаем для них режим OUTPUT и ставим защёлке высокий уровень, чтобы регистр не принимал сигналов.
В loop() попробуем что-нибудь отправить на регистр. Сначала ставим LOW на защёлку (начинаем передачу данных. Теперь регистр принимает сигналы с Arduino). Далее отправляем данные в двоичном виде. Например, отправим байт 0b10000000 (должен будет загореться первый светодиод). В конце выставляем HIGH на защёлку (заканчиваем передавать данные).
int dataPin = 9; // к выводу 14 регистра SD
int clockPin = 11; // к выводу 11 регистра (SH_CP)
int latchPin = 12; // к выводу 12 регистра (ST_CP)
void setup() {
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);
digitalWrite(latchPin, LOW);
}
void loop() {
digitalWrite(latchPin, LOW); // начинаем передачу данных
shiftOut(dataPin, clockPin, LSBFIRST, 0b10000000);
digitalWrite(latchPin, HIGH); // прекращаем передачу данных
}
Если в shiftOut() поменять LSBFIRST на MSBFIRST, то включится не первый, а последний светодиод в цепочке схемы.
При работе с несколькими светодиодами не очень удобно постоянно писать три строчки кода для каждого светодиода в отдельности. Поэтому оформим код в виде функции и будем мигать третьим светодиодом.
int dataPin = 9; // к выводу 14 регистра SD
int clockPin = 11; // к выводу 11 регистра (SH_CP)
int latchPin = 12; // к выводу 12 регистра (ST_CP)
void setup() {
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);
digitalWrite(latchPin, LOW);
}
void loop() {
setByte(0b00100000);
delay(1000);
setByte(0b00000000);
delay(1000);
}
void setByte(byte value) {
digitalWrite(latchPin, LOW); // начинаем передачу данных
// устанавливаем нужный байт
shiftOut(dataPin, clockPin, LSBFIRST, value);
digitalWrite(latchPin, HIGH); // прекращаем передачу данных
}
Другой скетч.
int dataPin = 9; // к выводу 14 регистра
int clockPin = 11; // к выводу 11 регистра (SH_CP)
int latchPin = 12; // к выводу 12 регистра (ST_CP)
byte path[4] = {
B11000011,
B00111100,
B00100100,
B00011000
};
void setup() {
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);
}
void loop() {
for (int i = 0; i < 4; i++) {
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, LSBFIRST, path[i]);
digitalWrite(latchPin, HIGH);
delay(250);
}
for (int i = 0; i < 4; i++) {
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, LSBFIRST, path[3 - i]);
digitalWrite(latchPin, HIGH);
delay(250);
}
}
В path[] мы указываем последовательность включённых и выключенных светодиодов. Между этими последовательностями будет происходит анимация.
Для анимации бегущих огней можно реализовать задачу через функцию bitWrite().
int dataPin = 9; // к выводу 14 регистра
int clockPin = 11; // к выводу 11 регистра (SH_CP)
int latchPin = 12; // к выводу 12 регистра (ST_CP)
void setup() {
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);
}
void loop() {
byte byteToSend = 0; //Создаем пустой байт B00000000
for (int bitPos = 0; bitPos < 8; bitPos++) { // В переменной хранится позиция изменяемого бита
byteToSend = 0; // Обнуляем байт при каждом проходе
bitWrite(byteToSend, bitPos, HIGH); // При bitPos=0 получим B00000001, при bitPos=1 - B00000010, при bitPos=2 - B00000100 и т.д.
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, MSBFIRST, byteToSend); // Инвертируем сигнал при помощи MSBFIRST, грузим с первого бита
digitalWrite(latchPin, HIGH);
delay(50);
}
}
Код попроще, чтобы лучше понять происходящее.
int dataPin = 9; // к выводу 14 регистра
int clockPin = 11; // к выводу 11 регистра (SH_CP)
int latchPin = 12; // к выводу 12 регистра (ST_CP)
byte leds = 0;
int currendLed = 0;
void setup() {
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);
leds = 0;
}
void loop() {
leds = 0;
if (currendLed == 7) {
currendLed = 0;
}
else {
currendLed++;
}
bitSet(leds, currendLed);
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, LSBFIRST, leds);
digitalWrite(latchPin, HIGH);
delay(250);
}
В методе setup() мы просто инициализируем режимы выводов и переменную светодиодов.
В методе loop() очищаем биты в переменной leds в начале каждой итерации, так что все биты устанавливаются в 0, так как мы хотим только включать один светодиод за раз. После этого мы увеличиваем или перезапускаем текущую переменную currentLed, чтобы затем опять включать правильный светодиод.
После этих двух операций мы переходим к смещению бит. Начинаем с вызова метода bitSet(), которому передаём байт, что хранит биты, и переменную currentLed.
Этот метод позволяет нам установить отдельные биты байта, указав их положение. Например, если мы хотим вручную установить байт в 10010, мы могли бы использовать следующие вызовы, поскольку биты, которые нам нужно установить в 1, являются вторыми справа (это позиция 1, когда мы начинаем в позиции 0) и пятый справа, который находится в положении 4:
bitSet(leds, 1);
bitSet(leds, 4);
Таким образом, каждый раз, когда мы увеличиваем текущую переменную currentLed и передаем ее методу bitSet(), мы каждый раз устанавливаем бит слева от предыдущего до 1 и, таким образом сообщаем сдвиговому регистру активировать вывод слева от предыдущего.
После установки бит мы записываем на контакт защёлки указание сдвиговому регистру, что собираемся отправить ему данные. Как только мы это сделаем, мы вызываем метод shiftOut(). Метод позволяет сдвигать биты за один вызов. Для этого мы передаём данные и синхронизацию в качестве первых двух параметров, затем передаём константу LSBFIRST, которая сообщает методу, что первый бит должен быть наименее значимым, а затем мы проходим через байт, содержащий биты, которые мы действительно хотим перенести в регистр сдвига.
Как только мы закончим смещение битов, мы снова обращаемся на контакт защёлки (используя HIGH в этот раз), чтобы указать, что мы отправили все данные. После того, как операция записи будет завершена, загорится соответствующий светодиодный индикатор, а затем задержится на 250 миллисекунд, прежде чем всё повторится.
Для последовательного подключения большого количества сдвиговых регистров используется Q7 регистра — по нему данные продавливаются по мере поступления. Выходы 11 (SH_CP, задающий тактовые импульсы) и 10 (ST_CP, «защелка») подключаются параллельно и управляются синхронно.
Читайте на сайте Codius
Как же использовать ШИМ, ведь мы же часто управляем при помощи регистра светодиодами, а выходы регистра могут иметь только 3 состояния — логический ноль LOW, логическая единица HIGH и высокоимпедансное состояние (пин не имеет физического контакта с электрической цепью). И действительно ШИМ сдвиговым регистром не поддерживается, но есть одна небольшая хитрость — мы можем использовать выход регистра OE (Output Enable input) — он отвечает за переключение из высокомного состояния в ноль. Выход OE — можно назвать логическим нолем для всех выходов. Таким образом, если мы подключим этот пин к ШИМ-выходу Arduino, то сможем таким образом смещать логический ноль, тем самым имитировать ШИМ на светодиодах.
Читайте на сайте Codius
int dataPin = 9; //Пин подключен к DS входу 74HC595
int latchPin = 10; //Пин подключен к ST_CP входу 74HC595
int clockPin = 11; //Пин подключен к SH_CP входу 74HC595
int pwmPin = 6; //Пин подключен к OE входу 74HC595 для управления ШИМ
void setup() {
//устанавливаем режим OUTPUT
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);
pinMode(pwmPin, OUTPUT);
digitalWrite(latchPin, LOW); // устанавливаем синхронизацию "защелки" на LOW
shiftOut(dataPin, clockPin, LSBFIRST, B11110001);
digitalWrite(latchPin, HIGH); //"защелкиваем" регистр, тем самым устанавливая значения на выходах
}
void loop() {
for (int i=0; i<256; i++) {
analogWrite(pwmPin,i); // Назначаем выходу ШИМ разные значения
delay(2); // Делаем паузу, чтобы не мигало слишком быстро
}
}
Минус данного подхода заключается в том, что в этом случае регулируется яркость всех светодиодов, подключённых к одному сдвиговому регистру. А что же делать, если нам нужно показать разную яркость светодиодов, подключённых к одному сдвиговому регистру. Здесь снова нужно будет пойти на хитрость — создать карту яркостей светодиодов, и зажигать каждую группу со своей яркостью по очереди, так быстро, чтобы создавалось ощущение постоянного свечения:
int dataPin = 9; //Пин подключен к DS входу 74HC595
int latchPin = 10; //Пин подключен к ST_CP входу 74HC595
int clockPin = 11; //Пин подключен к SH_CP входу 74HC595
int pwmPin = 6; //Пин подключен к OE входу 74HC595 для управления ШИМ
byte _map[2]= {
B01010101,
B10101010
};
void setup() {
//устанавливаем режим OUTPUT
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);
pinMode(pwmPin, OUTPUT);
digitalWrite(pwmPin, LOW);
}
void loop() {
for (int i=0; i<=255; i++) {
showLed(_map[0],i);
showLed(_map[1],255-i);
}
for (int i=255; i>=0; i--) {
showLed(_map[0],i);
showLed(_map[1],255-i);
}
}
void showLed(byte mapLed, int brightness) {
digitalWrite(pwmPin, LOW);
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, MSBFIRST, mapLed); // Инвертируем сигнал при помощи MSBFIRST, грузим с первого бита
digitalWrite(latchPin, HIGH);
analogWrite(pwmPin, brightness);
delay(5);
}
Но если вы начнёте экспериментировать с задержками и большим количеством карт яркости, то столкнётесь с очень неприятным эффектом мерцания — это связно с большим временем исполнения стандартных для языка Arduino функций-обёрток типа digitalWrite, digitalRead, analogWrite, analogRead и т.д.
Другие регистры, например, STP16C596 могут управлять 16 светодиодами одновременно без использования дополнительных резисторов.
Множим выходы с помощью сдвигового регистра 74HC595 (перевод официальной документации).
74hc595 arduino: сдвиговый регистр, основы использования