SPI и Arduino:
Рассмотрим классический сдвиговый регистр 74HC595, модель M74HC595B1 от STMicroelectronics. По сути, это преобразователь последовательного интерфейса в параллельный: получает данные по SPI, а потом разом выставляет уровни на 8 ножках согласно полученным битам. Биты, выставляемые ведущим на выводе SI, проталкиваются по цепочке D-триггеров с каждым тактовым импульсом (от ведущего) на ноге SCK. Одновременный вывод на ножки параллельного интерфейса обеспечивается так называемой защёлкой (latch) RCK, которая «не пускает» переданные биты на выводы раньше времени. Вывод G управляет состоянием выводов — включает их либо переводит в состояние Hi-Z:
А вот и сам регистр в DIP-корпусе:
Выводы микросхемы имеют следующее назначение:
- Vcc — питание, от 2 до 6 В
- GND — земля
- QA-QH — эти выводы соответствуют битам, записанными по SPI
- SI — вход ведомого, MOSI (SPI)
- G — Output Enabe; когда на этом выводе низкий уровень, выводы включены (подключены к «защёлкам»), когда высокий — выводы переходят в состояние Hi-Z
- RCK — защёлка, SS (SPI); при установке низкого уровня выводы регистра защёлкиваются
- SCK — тактовый вход, SCLK (SPI)
- SCLR — Shift Register Clear Input; если на этом выводе низкий уровень, очищает все триггеры по фронту тактового сигнала на SCLK. С нашей точки зрения это банальный RESET: прижал к земле — сбросил все биты регистра
- QH’ — на этом выводе будет появляться старший переданный бит
Для начала попробуем помигать светодиодами через сдвиговый регистр. Для этого подключим выводы по следующей схеме:
- Vcc ⇨ +5 В
- GND ⇨ GND
- QA-QH ⇨ светодиоды через резисторы на 510 Ом
- SI ⇨ пин 11 Arduino (MOSI)
- G ⇨ GND (выводы включены)
- RCK ⇨ пин 8 (SS)
- SCK ⇨ пин 13 (SCLK)
- SCLR ⇨ +5 В (сброс неактивен)
- QH’ оставим неподключенным
У меня получилось так:
Мигать будем не как в классическом скетче Blink, а пробегая огоньком по линейке светодиодов, начиная с первого. Для этого напишем следующий код:
#include <SPI.h>
enum { REG_SELECT = 8 }; // пин, управляющий защёлкой (SS в терминах SPI)
void setup()
{
/* Инициализируем шину SPI. Если используется программная реализация,
* то вы должны сами настроить пины, по которым будет работать SPI.
*/
SPI.begin();
pinMode(REG_SELECT, OUTPUT);
digitalWrite(REG_SELECT, LOW); // выбор ведомого - нашего регистра
SPI.transfer(0); // очищаем содержимое регистра
/* Завершаем передачу данных. После этого регистр установит
* на выводах QA-QH уровни, соответствующие записанным битам.
*/
digitalWrite(REG_SELECT, HIGH);
}
/* Эта функция сдвигает биты влево на одну позицию, перемещая старший бит
* на место младшего. Другими словами, она "вращает" биты по кругу.
* Например, 11110000 превращается в 11100001.
*/
void rotateLeft(uint8_t &bits)
{
uint8_t high_bit = bits & (1 << 7) ? 1 : 0;
bits = (bits << 1) | high_bit;
}
void loop()
{
static uint8_t nomad = 1; // это наш бегающий бит
/* Записываем значение в сдвиговый регистр */
digitalWrite(REG_SELECT, LOW);
SPI.transfer(nomad);
digitalWrite(REG_SELECT, HIGH);
/* И вращаем биты влево - в следующий раз загорится другой светодиод */
rotateLeft(nomad);
delay(1000 / 8); // пробегаем все 8 светодиодов за 1 секунду
}
Здорово. А если нам нужно больше выводов? Можно подсоединить ещё один сдвиговый регистр к той же шине SPI:
Как-то так, например:
Здесь у обоих регистров линии SCLK и MOSI общие, но у каждого своя линия SS. В результате мы получаем независимое управление двумя регистрами по одной шине SPI. Код аналогичен первому примеру:
#include <SPI.h>
enum { REG1 = 8, REG2 = 7 };
/* Копипаста не для нас, писать в регистр теперь будем так */
void writeShiftRegister(int ss_pin, uint8_t value)
{
digitalWrite(ss_pin, LOW);
SPI.transfer(value);
digitalWrite(ss_pin, HIGH);
}
void setup()
{
SPI.begin();
/* Всё то же, что и в первом примере, только для двух регистров */
pinMode(REG1, OUTPUT);
pinMode(REG2, OUTPUT);
writeShiftRegister(REG1, 0);
writeShiftRegister(REG2, 0);
}
void rotateLeft(uint8_t &bits)
{
uint8_t high_bit = bits & (1 << 7) ? 1 : 0;
bits = (bits << 1) | high_bit;
}
void rotateRight(uint8_t &bits)
{
uint8_t low_bit = bits & 1 ? (1 << 7) : 0;
bits = (bits >> 1) | low_bit;
}
void loop()
{
static uint8_t nomad1 = 1, nomad2 = 0x80;
writeShiftRegister(REG1, nomad1);
rotateLeft(nomad1);
writeShiftRegister(REG2, nomad2);
/* Для разнообразия погоняем биты во втором регистре в обратную сторону */
rotateRight(nomad2);
delay(1000 / 8);
}
Но и это ещё не всё, как любят говорить в «магазинах на диване» — есть ещё каскадное подключение сдвиговых регистров. При таком подключении биты из первого регистра будут проталкиваться в следующий в каскаде регистр, а из него — в следующий, и так далее. Таким образом, каскад из двух 8-битных регистров будет работать, как один 16-битный, а каскад из 10 регистров — как один 80-битный (схему можно печатать на рулонах — получится оригинальный подарок электронщику).
Мы не мажоры, нам хватит и 16-битного. Нужно подсоединить вывод QH’ первого регистра к пину SI (MOSI) второго, и оба регистра повесить на общую линию SS, чтобы активировать их оба за раз:
#include <SPI.h>
enum { REG = 8 };
/* Теперь шлём по 16 бит. Важный момент: так как по умолчанию
* данные передаются, начиная со старшего бита, сначала нужно
* послать старший байт, затем - младший - тогда всё 16 бит
* передадутся в правильном порядке.
*/
void writeShiftRegister16(int ss_pin, uint16_t value)
{
digitalWrite(ss_pin, LOW);
/* Фокус вот в чём: сначала шлём старший байт */
SPI.transfer(highByte(value));
/* А потом младший */
SPI.transfer(lowByte(value));
digitalWrite(ss_pin, HIGH);
}
void setup()
{
SPI.begin();
pinMode(REG, OUTPUT);
writeShiftRegister16(REG, 0);
}
/* Слегка изменим функцию для работы с 16-битными значениями */
void rotateLeft(uint16_t &bits)
{
uint16_t high_bit = bits & (1 << 15) ? 1 : 0;
bits = (bits << 1) | high_bit;
}
void loop()
{
static uint16_t nomad = 1;
writeShiftRegister16(REG, nomad);
rotateLeft(nomad);
delay(1000 / 8);
}
Заметьте, в функции writeShiftRegister16() сначала пишется старший байт — это потому что мы используем порядок бит MSBFIRST. Если смените порядок бит на LSBFIRST, придётся функцию поменять — слать сначала младший байт.
Исходники примеров доступны для скачивания напрямую и на GitHub в репозитории RoboCraft:
Картинки из статьи лежат в альбоме на Яндекс.Фотках.
Использовалось железо:
- Сдвиговые регистры 74HC595, модель M74HC595B1
- Arduino-совместимая плата CraftDuino, удобные провода и макетка
- Светодиоды, резисторы 510 Ом
Следующая тема — SPI и Arduino: ввод









38 комментариев на «“SPI и Arduino: плодим выходы”»
вопрос в следующем: хочу повесить на атмегу 328 управление 24ю сервами, но у самой микросхемы нет столько выходов.
надо либо делать все на 1280/2560 либо использовать сдвиговые регистры.
собственно, пробовал ли кто-нибудь второй вариант?
О втором варианте я как раз статью пишу, и библиотеку Servo под это дело допиливаю. Пару дней потерпишь? (:
Ха, отличная новость!
буду ждать 🙂
Спасибо за статью, расширила мой кругозор.
Пока делал по ней столкнулся с неожиданностью: на картинке с фоткой сверху микросхемы выходы обозначены как надо, но на схеме ниже уже нет 🙂 Видимо потому что разные микросхемы. Меня это сбило с толку на некоторое время.
Например где на верхней QA, на нижней GND.
На схеме расположение выводов не нормированно — как рисовать удобней — обозначения и номерера совпадают же=)
Zoltberg дело говорит (:
Если бы я расположил выводы на схеме так же, как и на реальной микросхеме, схема получилась бы достаточно запутанной. Собственно, я сначала так и делал, пока не стал путаться сам.
я дополню, что для микросхем обычно есть некое стандартное схемотехническое обозначение, иногда даже без номеров выводов, просто с названиями
А чем можно расширить число выходов с PWM?
Ну что бы например подключить 3-4 RGB-светодиода?
Хм, это тоже можно сделать через сдвиговый регистр при желании. Я тут пишу статью по подключению разной периферии через регистры — попробую что-нибудь придумать для PWM (:
А так-то, для этих целей используют специальные микросхемы вроде SG1525A.
Иностранцы пишут, что для LED годится TLC5940, которая через PWM управляет 16 светодиодами. То есть одной микросхемы хватит на 5 RGB-светодиодов.
В последнем примере объявлена процедура
void writeShiftRegister16(int ss_pin, uint16_t value)
А после она вызывается с недостающим параметром, что вызывает ошибку
writeShiftRegister16(nomad);
Спасибо, исправил. Похоже, ранее я поправил эту ошибку у себя локально и забыл обновить пост.
Какая разница между 74HC595B1 и 74HC595D? Второй стоит в 2,5 раза дешевле!
У 74HC595B1 корпус — DIP-16 (можно ставить в макетку), а у 74HC595D — SO-16 (для поверхностного монтажа, SMD, нужно разводить плату).
Запоздалое спасибо за ответ! Купил осенью себе пару штук от NXP по 50+ рублей, а на eBay такие микрухи от ST в DIP-корпусе обошлись мне по 7,5 р 🙂
Ребята, а подскажите, а на ноги MOSI и SCK обязательно вешать? Или все же можно на любые свободные?
Желательно. Если очень хочется, можно использовать пару других ног, но тогда вам придётся их настроить самостоятельно, а также использовать функцию для записи в регистр.
То есть придется отказаться от класса SPI? Требуется сразу задействовать ввод и вывод. Хотя, как я понимаю, они не будут друг другу мешать. Для вывода, например, к MOSI и SCK используется еще и 8 пин, а для ввода MISO, SCK и пин 9. По идее все должно нормально работать?
Всё верно: на каждое SPI-устройство выделяешь по отдельной линии SS — и они друг другу не мешают.
Благодарю 🙂 Теперь можно окончательно доделывать схему и печатную плату устройства 🙂
Верно ли я понимаю, что драйвер светодиодов MBI5028 — такой же 16-битный сдвиговый регистр и подключается он так же — за исключением линии яркости и резистора на ту же тему? Или у него как-то по-другому реализовано?
А можно ли передать ШИМ по spi?
Только если организуете ШИМ вручную — дёргая выводы сдвигового регистра в нужные моменты (по прерываниям от тайпера, например). Всё-таки, сдвиговые регистры — не полноценные входы/выходы (:
Помогите, есть платка с жк, а на обратной стороне 8 штук hef4015bt. Вот и нужно запустить это все а на эти регистры нету никаких примеров, и как включать их я тоже не знаю
спасибо! самое понятное с примерами разъяснение SPI и работа с 71HC595
у меня по ходу вопрос: а почему, при включении 71HC595, светодиоды некоторые зажигаются? почему сам регистр не очищается при включении?
может есть какой-то аппаратный метод включения для самоочистки?
74HC595 *
видимо, нужно допаять сброс с конденсатором и резистором на SCLR?
Хорошая идея.
Товарищи, хочу сделать что то типо этого , но не могу с кодом разобраться, читал читал так и не получается. не могу заставить бегать бит от изменений на А0.
Огромное спасибо за статью!
Решил поэкспериментировать с выходным сдвиговым регистром. Возникла проблемка, так как я новичок в этом, как записать значения в регистр из массива?
Заранее спасибо!!!
Я не понимаю сути вопроса. У вас проблемы со сдвиговым регистром, с массивами или с битовыми операциями? Допустим, у вас есть массив 8 значений — 0 или 1, и вы хотите, чтобы каждый элемент массива «управлял» соответствующим выводом регистра, тогда код будет таким:
uint8_t array[8] = ...; uint8_t reg_value = 0; for (int i = 0; i < 8; ++i) reg_value |= array[i] << i; digitalWrite(REG_SELECT, LOW); SPI.transfer(reg_value); digitalWrite(REG_SELECT, HIGH);Спасибо! Примерно этого я и хотел добиться! Если что, извиняйте, если не понятно суть вопроса изложил. Проблемы в целом, с программированием…:) Только учусь.
«reg |= lamp_state_array[i] << i» эта строка возвращает не те значения, которые нужно.
С каждым нажатием кнопки в переменной reg увеличивается число. И соответственно контроллер этого уже не понимает. Я хочу управлять нагрузкой через сдвиговый регистр. И мне нужно в определенном элементе массива менять с 0 на 1 либо наоборот. С массивом то я разобрался…:) только вот как теперь значения в регистр загнать ни как не могу допетрить..) Тот, вариант который вы предложили рабочий, но до второго нажатия кнопки. Потом не работает.
Вот поэтому я и говорю, что пользователям Arduino всё равно нужно знать C++, чтобы сделать что-либо, кроме стандартных примеров (:
В случае обработки нажатия кнопки вам нужно делать так:
Дело в том, что запись
эквивалентна записи
Естественно, что вам нужно обнулять значение reg перед циклом на каждое нажатие кнопки.
Попытайтесь более понятно объяснить, чего хотите добиться.
Это кусок выставляет в единичку i-тый бит. Следите за значениями в array. Они должны быть строго 1 или 0.
Более надёжно:
... if(arry[i]) reg_value |= 1 << i;Есть ещё интересная схемотехника Daisy Chaining совмещения (зацикливания) выходных (74HC595) и входных (74HC165) решистров:
Подскажите можно ли подключить каскадом 8 таких регистров?
Можно, написано же что хоть 10 подключай 🙂
Для определения на какой ноге находится SS, MOSI, MISO и SCK, есть глобальные переменные
Serial.print('SS: '); Serial.println(SS); Serial.print('MOSI: '); Serial.println(MOSI); Serial.print('MISO: '); Serial.println(MISO); Serial.print('SCK: '); Serial.println(SCK);Товарищи, есть код управления бегущим огоньком с помощью потенциометра:
#define SER 9 //mosi #define LATCH 10 // ss #define CLK 11 #define POW_LED 13 #define FLASH_LED 12 #define TO_MOTHER_BOARD 8 unsigned int vals[13] = {0, 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047,4095}; void setup() { Serial.begin(9600); pinMode(TO_MOTHER_BOARD, OUTPUT); // на мать импульс pinMode(FLASH_LED, OUTPUT);// синхронизирован с исмпульсом на мать pinMode(POW_LED, OUTPUT); // есть питание pinMode(SER, OUTPUT); pinMode(LATCH, OUTPUT); pinMode(CLK, OUTPUT); } void loop() { //в лупе у нас гистограмный индикатор на 12 диодов через 2 74hc595 int pot = analogRead(2); Serial.println(pot); pot = constrain(pot, 5, 1000); pot = map(pot, 5, 1000, 0, 12); digitalWrite(LATCH, LOW);//низкий - начало отправки shiftOut(SER, CLK, MSBFIRST, highByte(vals[pot])); shiftOut(SER, CLK, MSBFIRST, lowByte(vals[pot])); digitalWrite(LATCH, HIGH); delay(100); }Проблема такая, мне мало каскада из двух 595ых, как мне здесь добавить больше, чтобы все были на общих линиях data SS CLK… немогу понять как мне добавить в ход еще один shiftOut и что в него передевать…