Cветодиодная линейка на MAX7219

Попалась мне в руки светодиодная линейка неизвестного происхождения из 52 двухцветных светодиодов – красный и зелёный. Которая управлялась двумя микросхемами maxim max7919(драйвер). У этой платки было 8 пинов для подпайки, один из которых ни к чему не подключался.

Цель – разобраться что она из себя представляет, как работает и сделать что-нибудь с ней – погонять “бегущие огни” будет вполне достаточно.

Предыстория

Конечно, сама она мне в руки не упала :). Дал мне её преподаватель нового предмета – Основы проектирования. Он очень много знает и желает делится знаниями со студентами, предлагая создать различные проекты.

Внутри

Понятия не имею откуда эта линейка и для чего она предназначалась. Поэтому пришлось разбираться. Единственное что было на плате – цифры 8485-01, видимо какой то серийный номер – ровным счётом ничего не дало. Осмотр схемы + чтение даташита – час ушёл на то, чтоб понять как она примерно работает.

Каждые 4 диода общим анодом к выходам “номерам цифр” драйвера, и к каждому цвету светодиода по ноге из сегментов. В итоге на первый драйвер приходится 8 “номеров цифр”, на второй – 5. Под номерами цифр я подразумеваю номера цифр на 7ми сегментных дисплеев

Схему в общем виде линейку можно обрисовать так:

Ага, рисовал карандашиком. Жутко не хотелось в каде. Посмотрел библиотеки компонентов gEDA, KiCad и даже eagle – там не было драйвера. Пробежался по гуглу – тоже нет. Рисовать – лень, да и не нужно это для одного раза. Вот карандашиком.. как на начерталке ))

Разводку платы знать и не нужно – главное,что есть интерфейс. На рисунке – он слева. Пару слов о нём:

  1. Пусто.. видимо для отсчёта
  2. GND – земля. Общая для двух драйверов
  3. DIN – данные. Также общий для драйверов – разумеется данные предназначаются тому, у которого активен CS
  4. CS1 – chip select для первой микросхемы. Выставляем в 0, когда хотим что-то сказать ей. На ней 5 сегментов(5 * 4 = 20светодиодов)
  5. CS2 – аналочино, для второго драйвера. На этом драйвере висят 8 сегментов(8 * 4 = 32светодиода)
  6. CLK – clock. Тактируем..
  7. Iset – установка максимальной яркости led. НЕ ЯРКОСТИ, как многие(судя по форумам) понимают. Яркость задаётся ШИМом и устанавливается значением регистра. Для NONAME светодиодов которые попались – 37кОм слишком много – еле видны, а 31кОм – самое то. Значение устанавливается в зависимости от мощности нагрузки – читаем даташит.
  8. VCC – питание 5v

Поэксперементировав, с зелёным цветом становится быстро понятно – определяется каждым нечётным номером бита. С красным – даже не то что ровно на оборот – они смещенные(но при этом чётные). Вдаваться в подробности не буду, т.к. пока делал – знал какие биты соответствуют какому номеру светодиода – написал прошивку и забыл. Ниже прошивка, в которой не требуется знать какой бит, номер сегмента и микросхемы отвечает за конкретный номер led.

Для эксперементов подключил atmega8, вот таким способом:

Разумеется, можно было без кварца, для такой фигни-то. Просто он был на “отладочной плате” – ну и пусть. Также, не отображён коннектор для прошивки. Питалось это всё от USB.
Забыл нарисовать резистор 10кОм подключённый между VCC и ногой RESET – без него мега если и запустится, то будет от каждого чиха перезагружаться

Прошивка

Сначала хотел управлять через usb – но раздумал, не стоит это того, всё равно скоро отдавать, да и практической пользы мало.

Не смотря на то, что как описано в даташите многие значения регистров по дефолту совпадают с тем, что мне нужно, я очень долго бился пока не понял – их все нужно сначала инициализировать. После инициализации вычислил (посылая разные комбинации драйверам) как драйверы подключены к диодам, и уже по из этого вычислил схему подключения :)

I/O

Не буду пересказывать даташит. Комманда состоит из 2 байт – в старшем: старшие 4 бита – незначащие, младшие – комманда. Второй байт(младший) – если можно так выразится – аргументы для комманды, ну или значения которые нужно положить в определённый регистр драйвера.

Несколько вспомогательных функций для работы с CS, DIN и CLK линиями.

#define LOW 0
#define HIGH 1

void led_cs1(char x) {
PORTC = x ? PORTC | _BV(4) : PORTC & ~_BV(4);
}
void led_cs2(char x) {
PORTC = x ? PORTC | _BV(5) : PORTC & ~_BV(5);
}
void led_clk(char x) {
PORTD = x ? PORTD | _BV(7) : PORTD & ~_BV(7);
}
void led_din(char x) {
PORTD = x ? PORTD | _BV(6) : PORTD & ~_BV(6);
}

Аргумент у них – должен отвечать на вопрос: установить ли 1 на линии?

Когда умеем дёргать линии, можно посылать данные:

void outputByte(unsigned char val) {
for(signed char i = 7; i >= 0; i--) {
led_din(val & (1 << i)); _delay_ms(CLOCK_TIME_MS /2); led_clk(HIGH); _delay_ms(CLOCK_TIME_MS); led_clk(LOW); _delay_ms(CLOCK_TIME_MS); } } void outBytes(char pin, unsigned char reg, unsigned char dat) { PORTC &= ~_BV(pin); outputByte(reg); outputByte(dat); PORTC |= _BV(pin); }

Т.е. отправка комманды комманды(слова) заключается в опускании CS(установки в активное состояние) для нужного драйвера, и последовательной отправки 2 байт.

Для отправки какой то одной или сразу двум вынес отдельно:

#define outBytesChip1(reg, dat) outBytes(PIN_CS1, reg, dat);
#define outBytesChip2(reg, dat) outBytes(PIN_CS2, reg, dat);

void outBytesChips(unsigned char reg, unsigned char dat) {
outBytesChip1(reg, dat);
outBytesChip2(reg, dat);
}

outBytesChipN() - через дефайн, т.к. эксперементально определил что прошивка меньше места занимает :)

Инициализация

Как я уже писал выше - промучился, пока жостко не проинициализировал.

void init() {
// configure pins to output
DDRC |= _BV(5) | _BV(4);
DDRD |= _BV(6) | _BV(7);

// pre-init drivers line
led_cs1(HIGH);
led_cs2(HIGH);
led_clk(LOW);

// setup clock interrupt and prescaler
TCCR0 |= (1<
Для чего нужны прерывания по таймеру? Ниже.

Управление светодиодами

Весьма сложно вычислять адрес определённого цвета и светодиода. Он складывается из номера драйвера, номера сегмента, номера "цифры" в сегменте в зависимости от цвета. Сделать тупо бегущий огонь - это уже куча перерасчётов.

Я сделал нечто "видеопамяти". Некоторый буффер, в котором хранятся значения цветов для каждого светодиода. Элементарное обращение - через несколько функций, которые знают в какую часть массива "видеопамяти" полезть и какие биты прочитать/изменить.

Каждая ячейка состоит из 2х бит - цвета.

enum LedStatus{
lsOff, lsGreen, lsRed, lsOrange
};

Говорящие названия - комментировать не нужно.

Выделяем память под массив "видеопамяти" и флажок - признак того, что содержимое памяти изменилось и нужно снова посылать новые значения в драйверы.

char buffman_changed = 0;
char buffman_data[13];

Функции для манипуляции с массивом таковы:

LedStatus buffman_get(unsigned int num) {
if (num > 52) return lsOff;
int bitcount = (num * 2);
int off_bit = bitcount % 8;
return (LedStatus) ((buffman_data[bitcount/8] & (3 << off_bit)) >> off_bit);
}
void buffman_setAll(LedStatus stat) {
unsigned char mask = stat << 0 | stat << 2 | stat << 4 | stat << 6; for(int i = 0; i < 13; i++) buffman_data[i] = mask; buffman_changed = 1; } void buffman_set(unsigned char num, LedStatus stat) { if (num > 51) return;
int bitcount = (num * 2);
int off_byte = num >> 2, off_bit = bitcount % 8;
buffman_data[off_byte] = (buffman_data[off_byte] & ~(3 << off_bit)) | (((char)stat) << off_bit); buffman_changed = 1; }

Суть одна - умеют работать с "видеобуфером".

Самое интересное - то как эта "видеопамять" отображается на массив. По таймеру.

ISR(TIMER0_OVF_vect) {
int mask = 0;
if (!buffman_changed) return;

for(int i = 0; i < 15; i++) { mask = 0; for(int j = 0; j < 4; j++) { switch(buffman_get(i*4+j)) { case lsOff: continue; case lsOrange: mask |= 2 << redMatrix[j] | 1 << greenMatrix[j]; break; case lsRed: mask |= 2 << redMatrix[j]; break; case lsGreen: mask |= 1 << greenMatrix[j]; break; } } if (i < 8) { outBytesChip2(i+1, mask); } else { outBytesChip1(i-7, mask); } } }
По прерыванию в цикле пробегается по всем сегментам - группам светодиодов(по 4), в зависимости от значения в "видеобуфере" формируется с помощью масок формируется отправляемое значение.

При том, маска для каждого номера светодиода и цвета в определённой позиции лежит в

static unsigned char redMatrix[] = {4, 2, 0, 6},
greenMatrix[] = {6, 4, 2, 0};

А теперь можно в цикле рисовать в массиве через через buffman_set() с определённой задержкой - и эти изменения практически моментально отразятся на светодиодной линейке.

Наример вот такая прошивка:

int main(void)
{
init();

buffman_setAll(lsOff);

int dir = 1, i = 1;
LedStatus ls = lsRed;
do {
buffman_set(i-1, lsOrange);
buffman_set(i, lsRed);
buffman_set(i+1, lsRed);
buffman_set(i+2, lsGreen);
if (i == 0 || i == 49) dir = - dir;
i+=dir;
wait(25);
if (i%2) buffman_set(0, (buffman_get(0) == lsOff ? lsRed : lsOff ));
if (i%2==0) buffman_set(51, (buffman_get(51) == lsOff ? lsRed : lsOff ));

} while( 1 );

return 0;
}

Делает вот такое:

Ещё вот баловался:

На этом всё

На этом всё. Поиграл - и хватит. Делюсь файлами.
Полный текст прошивки:

#include
#include
#include
#include
#include

#define PIN_CS1 4
#define PIN_CS2 5

// Clock duration
#define CLOCK_TIME_MS 0.0005

#define LOW 0
#define HIGH 1

static unsigned char redMatrix[] = {4, 2, 0, 6}, greenMatrix[] = {6, 4, 2, 0};

enum LedStatus{
lsOff, lsGreen, lsRed, lsOrange
};

char buffman_changed = 0;
char buffman_data[13];

LedStatus buffman_get(unsigned int num) {
if (num > 52) return lsOff;
int bitcount = (num * 2);
int off_bit = bitcount % 8;
return (LedStatus) ((buffman_data[bitcount/8] & (3 << off_bit)) >> off_bit);
}
void buffman_setAll(LedStatus stat) {
unsigned char mask = stat << 0 | stat << 2 | stat << 4 | stat << 6; for(int i = 0; i < 13; i++) buffman_data[i] = mask; buffman_changed = 1; } void buffman_set(unsigned char num, LedStatus stat) { if (num > 51) return;
int bitcount = (num * 2);
int off_byte = num >> 2, off_bit = bitcount % 8;
buffman_data[off_byte] = (buffman_data[off_byte] & ~(3 << off_bit)) | (((char)stat) << off_bit); buffman_changed = 1; } void led_cs1(char x) { PORTC = x ? PORTC | _BV(4) : PORTC & ~_BV(4); } void led_cs2(char x) { PORTC = x ? PORTC | _BV(5) : PORTC & ~_BV(5); } void led_clk(char x) { PORTD = x ? PORTD | _BV(7) : PORTD & ~_BV(7); } void led_din(char x) { PORTD = x ? PORTD | _BV(6) : PORTD & ~_BV(6); } //#define led_din(x) PORTD = x ? PORTD | _BV(6) : PORTD & ~_BV(6); void outputByte(unsigned char val) { for(signed char i = 7; i >= 0; i--) {
led_din(val & (1 << i)); _delay_ms(CLOCK_TIME_MS /2); led_clk(HIGH); _delay_ms(CLOCK_TIME_MS); led_clk(LOW); _delay_ms(CLOCK_TIME_MS); } } void outBytes(char pin, unsigned char reg, unsigned char dat) { PORTC &= ~_BV(pin); outputByte(reg); outputByte(dat); PORTC |= _BV(pin); } #define outBytesChip1(reg, dat) outBytes(PIN_CS1, reg, dat); #define outBytesChip2(reg, dat) outBytes(PIN_CS2, reg, dat); void outBytesChips(unsigned char reg, unsigned char dat) { outBytesChip1(reg, dat); outBytesChip2(reg, dat); } void init() { // configure pins to output DDRC |= _BV(5) | _BV(4); DDRD |= _BV(6) | _BV(7); // pre-init drivers line led_cs1(HIGH); led_cs2(HIGH); led_clk(LOW); // setup clock interrupt and prescaler TCCR0 |= (1< 0; ms-=1) _delay_ms(1);
}

int main(void)
{
init();

buffman_setAll(lsOff);

int dir = 1, i = 1;
LedStatus ls = lsRed;
do {
buffman_set(i-1, lsOrange);
buffman_set(i, lsRed);
buffman_set(i+1, lsRed);
buffman_set(i+2, lsGreen);
if (i == 0 || i == 49) dir = - dir;
i+=dir;
wait(25);
if (i%2) buffman_set(0, (buffman_get(0) == lsOff ? lsRed : lsOff ));
if (i%2==0) buffman_set(51, (buffman_get(51) == lsOff ? lsRed : lsOff ));

} while( 1 );

return 0;
}


А также запакованный вариант - вместе с мейкфайлом и прошивкой: led-line-firmware.tar.gz

UPD

Выкладываю ещё одну демо-прошивку и видео к ней. Есть три функции, выполняющие каждая свой эффект, вызываются циклически по очереди, с плавным переходом(после каждого эффекта уменьшается яркость с помощью встроенного в драйверы ШИМа)


inline void wait(int ms) {
for(; ms > 0; ms-=1) _delay_ms(1);
}

void e1() {
int dir = 1, i = 1;
for(int j = 0; j < 300 ; j++) { buffman_set(i-1, lsOrange); buffman_set(i, lsRed); buffman_set(i+1, lsRed); buffman_set(i+2, lsGreen); if (i == 0 || i == 49) dir = - dir; i+=dir; wait(25); if (i%2) buffman_set(0, (buffman_get(0) == lsOff ? lsRed : lsOff )); if (i%2==0) buffman_set(51, (buffman_get(51) == lsOff ? lsRed : lsOff )); } } void e2() { LedStatus ls = lsRed; int i = 1, len=6, end = 52;; int delay = 14; while(end > 0) {
for(i = 0; i < end;i++) { for(int k = 0; k < len; k++) { buffman_set(k+i, ls); } buffman_set(i-1, lsOff); wait(delay); } delay++; end--;end--; if (ls == lsRed) ls = lsGreen; else if (ls == lsGreen) ls = lsOrange; else if (ls == lsOrange) ls = lsRed; } } void e3() { for(int dir = 1, i = 1, j = 0; j < 400; j++) { buffman_set(i, lsRed); buffman_set(dir>0?i+1:i-1, lsRed);
buffman_set(i-dir, lsOff);

buffman_set(52-i, lsGreen);
buffman_set(52-i-(dir>0?+1:-1), lsGreen);
buffman_set(52-i+dir, lsOff);

if(i > 52/2-10) {
for(int k = 0; k < i-20; k++) { buffman_set(k, lsOrange); buffman_set(52-k, lsOrange); } } i += dir; if (i == 0 || i == 52/2) dir = - dir; wait(31); } } void (*examples[]) (void) = { &e3, &e2, &e1}; int main(void) { init(); buffman_setAll(lsOff); for(int i = 0; ; i = ++i % (sizeof(examples)/sizeof(examples[0]))) { examples[i](); for(int b = 15; b > 1; b--) {
outBytesChips(0xa, (b)); // intensity
wait(150);
}
buffman_setAll(lsOff);
wait(300);
outBytesChips(0xa, (15)); // intensity

}

return 0;
}

Обожаю си за возможность писать такие выверты :)
Всю прошивку можно скачать с исходниками: ledline-firmware-3-effects.tar

Ну и видео

  • Eugene

    Реально круто! Молодцом!!!

    • ruX

      Спасибо :)

  • Олег

    Спасибо!