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 обязательно вешать? Или все же можно на любые свободные?
Желательно. Если очень хочется, можно использовать пару других ног, но тогда вам придётся их настроить самостоятельно, а также использовать функциюshiftOut() для записи в регистр.
То есть придется отказаться от класса SPI? Требуется сразу задействовать ввод и вывод. Хотя, как я понимаю, они не будут друг другу мешать. Для вывода, например, к MOSI и SCK используется еще и 8 пин, а для ввода MISO, SCK и пин 9. По идее все должно нормально работать?
Всё верно: на каждое SPI-устройство выделяешь по отдельной линии SS — и они друг другу не мешают.
Благодарю 🙂 Теперь можно окончательно доделывать схему и печатную плату устройства 🙂
Верно ли я понимаю, что драйвер светодиодов MBI5028 — такой же 16-битный сдвиговый регистр и подключается он так же — за исключением линии яркости и резистора на ту же тему? Или у него как-то по-другому реализовано?
А можно ли передать ШИМ по spi?
Только если организуете ШИМ вручную — дёргая выводы сдвигового регистра в нужные моменты (по прерываниям от тайпера, например). Всё-таки, сдвиговые регистры — не полноценные входы/выходы (:
Помогите, есть платка с жк, а на обратной стороне 8 штук hef4015bt. Вот и нужно запустить это все а на эти регистры нету никаких примеров, и как включать их я тоже не знаю
спасибо! самое понятное с примерами разъяснение SPI и работа с 71HC595
у меня по ходу вопрос: а почему, при включении 71HC595, светодиоды некоторые зажигаются? почему сам регистр не очищается при включении?
может есть какой-то аппаратный метод включения для самоочистки?
74HC595 *
видимо, нужно допаять сброс с конденсатором и резистором на SCLR?
Хорошая идея.
Товарищи, хочу сделать что то типо этогоhttp://www.youtube.com/watch?v=JFKPlNoBpfk , но не могу с кодом разобраться, читал читал так и не получается. не могу заставить бегать бит от изменений на А0.
Огромное спасибо за статью!
Решил поэкспериментировать с выходным сдвиговым регистром. Возникла проблемка, так как я новичок в этом, как записать значения в регистр из массива?
Заранее спасибо!!!
Я не понимаю сути вопроса. У вас проблемы со сдвиговым регистром, с массивами или с битовыми операциями? Допустим, у вас есть массив 8 значений — 0 или 1, и вы хотите, чтобы каждый элемент массива «управлял» соответствующим выводом регистра, тогда код будет таким:
Спасибо! Примерно этого я и хотел добиться! Если что, извиняйте, если не понятно суть вопроса изложил. Проблемы в целом, с программированием…:) Только учусь.
«reg |= lamp_state_array[i] << i» эта строка возвращает не те значения, которые нужно.
С каждым нажатием кнопки в переменной reg увеличивается число. И соответственно контроллер этого уже не понимает. Я хочу управлять нагрузкой через сдвиговый регистр. И мне нужно в определенном элементе массива менять с 0 на 1 либо наоборот. С массивом то я разобрался…:) только вот как теперь значения в регистр загнать ни как не могу допетрить..) Тот, вариант который вы предложили рабочий, но до второго нажатия кнопки. Потом не работает.
Вот поэтому я и говорю, что пользователям Arduino всё равно нужно знать C++, чтобы сделать что-либо, кроме стандартных примеров (:
В случае обработки нажатия кнопки вам нужно делать так:
Дело в том, что запись
эквивалентна записи
Естественно, что вам нужно обнулять значение reg перед циклом на каждое нажатие кнопки.
Попытайтесь более понятно объяснить, чего хотите добиться.
Это кусок выставляет в единичку i-тый бит. Следите за значениями в array. Они должны быть строго 1 или 0.
Более надёжно:
Есть ещё интересная схемотехника Daisy Chaining совмещения (зацикливания) выходных (74HC595) и входных (74HC165) решистров:
robots.freehostia.com/Software/ShiftRegister/ShiftRegisterBody.html
Подскажите можно ли подключить каскадом 8 таких регистров?
Можно, написано же что хоть 10 подключай 🙂
Для определения на какой ноге находится SS, MOSI, MISO и SCK, есть глобальные переменные
Товарищи, есть код управления бегущим огоньком с помощью потенциометра:
Проблема такая, мне мало каскада из двух 595ых, как мне здесь добавить больше, чтобы все были на общих линиях data SS CLK… немогу понять как мне добавить в ход еще один shiftOut и что в него передевать…