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

Ну и видео

  • Eugene

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

    • ruX

      Спасибо :)

  • Олег

    Спасибо!