Как то раз мне приспичило управлять несколькими RGB светодиодами одновременно, а более конкретно, стояла такая задача:

  • Минимальное количество занятых пинов микроконтроллера
  • Дополнительное железо (сдвиговики, ШИМ контроллеры) использовать нельзя!
  • Простой набор цветов (скажем 8 цветов)
  • Индивидуальное управление каждым светодиодом

Как вы знаете, один RGB светодиод требует 3 пина для управления, а нам нужно например подключить 5 штук. 5*3 == 15 пинов? Жирно! Вспомним про такую замечательную штуку, как динамическая индикация: “одинаковые” ноги управляемых устройств соединяются вместе, а общие ноги подключаются отдельно (а не наоборот, как в классическом подключении). Теперь можно контролировать одновременно все линии данных, но включать только выбранную общую ногу, т.е. переключаясь между общими ногами очень быстро, включать нужный цвет на нужном светодиоде. Если делать это достаточно быстро, глаз не заметит мерцаний! Итак, схема:

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

  • Ноги R, G, B соединены параллельно и через резисторы заведены на пины 10, 11, 12 (пины неважно какие, в скетче можно изменить). Примечание: индикация динамическая, т.е. светодиоды горят по очереди, и чем их больше – тем меньше по времени горит каждый. В принципе можно уменьшать сопротивление резистора хоть до 100 Ом, светодиоды не должны сгореть! Я по привычке поставил 220 Ом и всё работало.
  • Общие ноги (катоды) подключены опять же на любые пины, в схеме это A0, A1, A2, A3, A4

В скетче используются прерывания по таймеру 1, я использовал свою библиотеку GyverTimer012 (скачать можно здесь), без неё скетч не заведётся. Собственно сам код:

#define LED_AMOUNT 5  // сколько RGB ледов подключено
byte COM_pins[] = {A0, A1, A2, A3, A4};   // общие пины диодов
byte RGB_pins[] = {10, 11, 12};   // пины цветов (R, G, B)

#include "GyverTimer1.h"

// цвета
enum colors {
  D_BLACK,
  D_BLUE,
  D_GREEN,
  D_CYAN,
  D_RED,
  D_MAGENTA,
  D_YELLOW,
  D_WHITE,
};

volatile colors color[LED_AMOUNT];

void setup() {
  // настройка динамического переключения
  dynamicSetup();

  // задаём цвета
  color[0] = D_RED;
  color[1] = D_GREEN;
  color[2] = D_BLUE;
  color[3] = D_WHITE;
  color[4] = D_MAGENTA;

  // задержка (смотрим цвета)
  delay(3000);
}

void loop() {
  // включаем выключаем зажигаем разными цветами
  color[0] = D_BLACK;
  delay(100);
  color[1] = D_BLACK;
  delay(100);
  color[2] = D_BLACK;
  delay(100);
  color[3] = D_BLACK;
  delay(100);
  color[4] = D_BLACK;
  delay(100);

  color[0] = D_RED;
  delay(100);
  color[1] = D_GREEN;
  delay(100);
  color[2] = D_BLUE;
  delay(100);
  color[3] = D_WHITE;
  delay(100);
  color[4] = D_MAGENTA;
  delay(100);
}

void dynamicSetup() {
  // катоды врубаем
  for (byte i = 0; i < LED_AMOUNT; i++) {
    pinMode(COM_pins[i], OUTPUT);
    writePin(COM_pins[i], 1);
  }

  // аноды вырубаем
  for (byte i = 0; i < 3; i++) {
    pinMode(RGB_pins[i], OUTPUT);
    writePin(RGB_pins[i], 0);
  }
  // таймер на 4 миллисекунды
  timer1_setPeriod(4000);
  timer1_ISR(isr);
  timer1_start();
}

void isr() {
  static volatile int8_t counter = 0;
  // выключаем предыдущий катод
  writePin(COM_pins[ (counter == 0) ? (LED_AMOUNT - 1) : (counter - 1) ], 1);

  // выставляем цвет
  for (byte i = 0; i < 3; i++)
    writePin(RGB_pins[i], bitRead(color[counter], i));

  // включаем следующий катод
  writePin(COM_pins[counter], 0);

  // зацикливаем счётчик
  if (++counter >= LED_AMOUNT) counter = 0;
}

void writePin(uint8_t pin, uint8_t x) {
  if (pin < 8) bitWrite(PORTD, pin, x);
  else if (pin < 14) bitWrite(PORTB, (pin - 8), x);
  else if (pin < 20) bitWrite(PORTC, (pin - 14), x);
  else return;
}

Что делает код? Давайте разберём. Начнём с цветов: цвета у меня записаны как перечисления (enum):

// цвета
enum colors {
  D_BLACK,
  D_BLUE,
  D_GREEN,
  D_CYAN,
  D_RED,
  D_MAGENTA,
  D_YELLOW,
  D_WHITE,
};

Как вы знаете из урока про enum, каждый добавленный в список член получает “значение” на единицу больше предыдущего, т.е.

D_BLACK == 0
D_BLUE == 1
D_GREEN == 2
D_CYAN == 3
D_RED == 4
D_MAGENTA == 5
D_YELLOW == 6
D_WHITE == 7

И что? Действительно. Давайте запишем в бинарном виде:

D_BLACK == 0b000
D_BLUE == 0b001
D_GREEN == 0b010
D_CYAN == 0b011
D_RED == 0b100
D_MAGENTA == 0b101
D_YELLOW == 0b110
D_WHITE == 0b111

Вооот оно что, я думаю вы уже уловили, откуда берутся цвета и почему их всего 8. Цвета светодиодов хранятся в массиве colors и задаются вот так:

// задаём цвета
color[0] = D_RED;
color[1] = D_GREEN;
color[2] = D_BLUE;
color[3] = D_WHITE;
color[4] = D_MAGENTA;

Это ладно, самое интересное происходит в прерывании таймера!

void isr() {
  static volatile int8_t counter = 0;
  // выключаем предыдущий катод
  writePin(COM_pins[ (counter == 0) ? (LED_AMOUNT - 1) : (counter - 1) ], 1);

  // выставляем цвет
  for (byte i = 0; i < 3; i++)
    writePin(RGB_pins[i], bitRead(color[counter], i));

  // включаем следующий катод
  writePin(COM_pins[counter], 0);

  // зацикливаем счётчик
  if (++counter >= LED_AMOUNT) counter = 0;
}

Что тут происходит? Функция writePin является очень быстрым аналогом digitalWrite и работает только для ATmega328 (UNO, Nano, Pro Mini), если вам нужно запустить код на другом чипе – замените writePin на digitalWrite. Но может подтормаживать.

Со включением и выключением катодов я думаю всё понятно, а вся хитрость с установкой цвета происходит тут:

for (byte i = 0; i < 3; i++)
    writePin(RGB_pins[i], bitRead(color[counter], i));

Мы берём тот самый цвет, который хранится в массиве color, это где мы смотрели бинарник, и при помощи функции-макроса bitRead вынимаем нужную единичку/нолик и подаём её на цветной пин текущего светодиода. Вот и вся магия =) Видео с демонстрацией данного скетча/схемы можно посмотреть ниже.