SPI и Arduino: плодим выходы


SPI и Arduino:

  1. Теория
  2. Вывод
  3. Ввод

Рассмотрим классический сдвиговый регистр 74HC595, модель M74HC595B1 от STMicroelectronics. По сути, это преобразователь последовательного интерфейса в параллельный: получает данные по SPI, а потом разом выставляет уровни на 8 ножках согласно полученным битам. Биты, выставляемые ведущим на выводе SI, проталкиваются по цепочке D-триггеров с каждым тактовым импульсом (от ведущего) на ноге SCK. Одновременный вывод на ножки параллельного интерфейса обеспечивается так называемой защёлкой (latch) RCK, которая «не пускает» переданные биты на выводы раньше времени. Вывод G управляет состоянием выводов — включает их либо переводит в состояние Hi-Z:

А вот и сам регистр в DIP-корпусе:

Выводы микросхемы имеют следующее назначение:

  1. Vcc — питание, от 2 до 6 В
  2. GND — земля
  3. QA-QH — эти выводы соответствуют битам, записанными по SPI
  4. SI — вход ведомого, MOSI (SPI)
  5. G — Output Enabe; когда на этом выводе низкий уровень, выводы включены (подключены к «защёлкам»), когда высокий — выводы переходят в состояние Hi-Z
  6. RCK — защёлка, SS (SPI); при установке низкого уровня выводы регистра защёлкиваются
  7. SCK — тактовый вход, SCLK (SPI)
  8. SCLR — Shift Register Clear Input; если на этом выводе низкий уровень, очищает все триггеры по фронту тактового сигнала на SCLK. С нашей точки зрения это банальный RESET: прижал к земле — сбросил все биты регистра
  9. 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:

Картинки из статьи лежат в альбоме на Яндекс.Фотках.

Использовалось железо:

Следующая тема — SPI и Arduino: ввод


38 комментариев на «“SPI и Arduino: плодим выходы”»

  1. вопрос в следующем: хочу повесить на атмегу 328 управление 24ю сервами, но у самой микросхемы нет столько выходов.

    надо либо делать все на 1280/2560 либо использовать сдвиговые регистры.

    собственно, пробовал ли кто-нибудь второй вариант?

    • О втором варианте я как раз статью пишу, и библиотеку Servo под это дело допиливаю. Пару дней потерпишь? (:

  2. Спасибо за статью, расширила мой кругозор.
    Пока делал по ней столкнулся с неожиданностью: на картинке с фоткой сверху микросхемы выходы обозначены как надо, но на схеме ниже уже нет 🙂 Видимо потому что разные микросхемы. Меня это сбило с толку на некоторое время.
    Например где на верхней QA, на нижней GND.

    • На схеме расположение выводов не нормированно — как рисовать удобней — обозначения и номерера совпадают же=)

    • Zoltberg дело говорит (:
      Если бы я расположил выводы на схеме так же, как и на реальной микросхеме, схема получилась бы достаточно запутанной. Собственно, я сначала так и делал, пока не стал путаться сам.

    • я дополню, что для микросхем обычно есть некое стандартное схемотехническое обозначение, иногда даже без номеров выводов, просто с названиями

    • Хм, это тоже можно сделать через сдвиговый регистр при желании. Я тут пишу статью по подключению разной периферии через регистры — попробую что-нибудь придумать для PWM (:
      А так-то, для этих целей используют специальные микросхемы вроде SG1525A.

    • Иностранцы пишут, что для LED годится TLC5940, которая через PWM управляет 16 светодиодами. То есть одной микросхемы хватит на 5 RGB-светодиодов.

  3. В последнем примере объявлена процедура
    void writeShiftRegister16(int ss_pin, uint16_t value)
    А после она вызывается с недостающим параметром, что вызывает ошибку
    writeShiftRegister16(nomad);

    • Спасибо, исправил. Похоже, ранее я поправил эту ошибку у себя локально и забыл обновить пост.

    • У 74HC595B1 корпус — DIP-16 (можно ставить в макетку), а у 74HC595D — SO-16 (для поверхностного монтажа, SMD, нужно разводить плату).

    • Запоздалое спасибо за ответ! Купил осенью себе пару штук от NXP по 50+ рублей, а на eBay такие микрухи от ST в DIP-корпусе обошлись мне по 7,5 р 🙂

    • Желательно. Если очень хочется, можно использовать пару других ног, но тогда вам придётся их настроить самостоятельно, а также использовать функцию shiftOut() для записи в регистр.

    • То есть придется отказаться от класса SPI? Требуется сразу задействовать ввод и вывод. Хотя, как я понимаю, они не будут друг другу мешать. Для вывода, например, к MOSI и SCK используется еще и 8 пин, а для ввода MISO, SCK и пин 9. По идее все должно нормально работать?

    • Всё верно: на каждое SPI-устройство выделяешь по отдельной линии SS — и они друг другу не мешают.

    • Благодарю 🙂 Теперь можно окончательно доделывать схему и печатную плату устройства 🙂

  4. Верно ли я понимаю, что драйвер светодиодов MBI5028 — такой же 16-битный сдвиговый регистр и подключается он так же — за исключением линии яркости и резистора на ту же тему? Или у него как-то по-другому реализовано?

    • Только если организуете ШИМ вручную — дёргая выводы сдвигового регистра в нужные моменты (по прерываниям от тайпера, например). Всё-таки, сдвиговые регистры — не полноценные входы/выходы (:

  5. Помогите, есть платка с жк, а на обратной стороне 8 штук hef4015bt. Вот и нужно запустить это все а на эти регистры нету никаких примеров, и как включать их я тоже не знаю

  6. спасибо! самое понятное с примерами разъяснение SPI и работа с 71HC595

    у меня по ходу вопрос: а почему, при включении 71HC595, светодиоды некоторые зажигаются? почему сам регистр не очищается при включении?
    может есть какой-то аппаратный метод включения для самоочистки?

  7. Огромное спасибо за статью!
    Решил поэкспериментировать с выходным сдвиговым регистром. Возникла проблемка, так как я новичок в этом, как записать значения в регистр из массива?
    Заранее спасибо!!!

    • Я не понимаю сути вопроса. У вас проблемы со сдвиговым регистром, с массивами или с битовыми операциями? Допустим, у вас есть массив 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_value = 0;
      reg |= lamp_state_array[i] << i;
      

      Дело в том, что запись

      reg |= x;

      эквивалентна записи

      reg = reg | x;

      Естественно, что вам нужно обнулять значение reg перед циклом на каждое нажатие кнопки.

    • Попытайтесь более понятно объяснить, чего хотите добиться.

       reg_val = 0;
      ...
       reg_val |= 1 << i;
      ...
      

      Это кусок выставляет в единичку i-тый бит. Следите за значениями в array. Они должны быть строго 1 или 0.
      Более надёжно:

      ...
         if(arry[i])
           reg_value |= 1 << i;
      
    • Можно, написано же что хоть 10 подключай 🙂

  8. Для определения на какой ноге находится 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);
    
  9. Товарищи, есть код управления бегущим огоньком с помощью потенциометра:

    #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 и что в него передевать…

Добавить комментарий

Arduino

Что такое Arduino?
Зачем мне Arduino?
Начало работы с Arduino
Для начинающих ардуинщиков
Радиодетали (точка входа для начинающих ардуинщиков)
Первые шаги с Arduino

Разделы

  1. Преимуществ нет, за исключением читабельности: тип bool обычно имеет размер 1 байт, как и uint8_t. Думаю, компилятор в обоих случаях…

  2. Добрый день! Я недавно начал изучать программирование под STM32 и ваши уроки просто бесценны! Хотел узнать зачем использовать переменную типа…

3D-печать AI Android Arduino Bluetooth CraftDuino DIY IDE iRobot Kinect OpenCV Open Source Python Raspberry Pi RoboCraft ROS swarm ИК автоматизация андроид балансировать бионика версия видео военный датчик дрон интерфейс камера кибервесна манипулятор машинное обучение наше нейронная сеть подводный пылесос работа распознавание робот робототехника светодиод сервомашинка собака телеприсутствие управление ходить шаг за шагом шаговый двигатель шилд юмор

OpenCV
Робототехника
Будущее за бионическими роботами?
Нейронная сеть - введение