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<<CS02)|(1<<CS00);
TIMSK |= (1<<TOIE0);
TCNT0 = 0;

// initialize drivers
outBytesChips(0xc, (1)); // shutdown
outBytesChips(0xa, (15)); // intensity
outBytesChips(0x9, 0); // decode mode
outBytesChips(0xf, 0); // test mode
outBytesChips(0xb, 7); // scan limit

// finally, enable interrupts
sei();
}

Для чего нужны прерывания по таймеру? Ниже.

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

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

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

Каждая ячейка состоит из 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 <avr/io.h>
#include <avr/eeprom.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <util/delay.h>

#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<<CS02)|(1<<CS00);
TIMSK |= (1<<TOIE0);
TCNT0 = 0;

// initialize drivers
outBytesChips(0xc, (1)); // shutdown
outBytesChips(0xa, (15)); // intensity
outBytesChips(0x9, 0); // decode mode
outBytesChips(0xf, 0); // test mode
outBytesChips(0xb, 7); // scan limit

// finally, enable interrupts
sei();
}

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);
}
}
}

inline void wait(float ms) {
for(; ms > 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

Ну и видео