Светодиодная матрица ардуино

Сообщества ›
Arduino для автомобиля ›
Блог ›
❺✚ DEMO Подключаем MAX7219 + Arduino + 7 segment display (Video+2)

Привет всем любителям Arduino.

Полный размерhello
Введение и напутственные слова (для новичков).

На сегодняшний момент представленно очень много проектов связанных с ардуино, есть сложные и есть простые. Самый первый и простой это пример blink.
Любой из вас кто впервые взял плату ардуино, залил этот скетч, МОЛОДЦЫ, все работает.

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

Вижу много вопросов: Помогите написать скетч-код и т.д. все мы хотим готовый и 100% работающий код, которые удовлетворяет своим потребностям, а нет хер там, ищешь на просторах google готовые скетчи, заливаешь, а он не работает или не корректно отображает данные и т.д.
Пока сам не разберешься, готовенькое на блюдечке ни кто тебе не принесет, а если и отдаст, то за вознаграждение за большие проекты.

Не в обиду, но оно так и есть, на своем опыте это прошел.

Как работать с драйверами индикаторов MAX7219 и MAX7221 через стандартную библиотеку LedControl с 7 segment индикатором?

Для выполнения своего проекта искал драйвер управления 7 segment индикатором, лучшим и удобным из них с минимальной обвязкой становится

MAX7219

.На просторах интернета полным полно скетчей использующий этот драйвер, но принципы и объяснения его работы в коде программы для него толком нет.

Попробую, как говорится «на пальцах объяснить», как он работает и что использует в коде программы, через библиотеку LedControl.
Примитивно, минимально, но работает это точно.

Смотрим видео.

ВИДЕО DEMO MAX7219 + Arduino + 7 segment

Для воплощения потребуется детали:
1. MAX7219 Display driver в любом корпусе dip или SO.
2. Резистор 10kom, если MAX7219 используется отдельно от готовой платы
3. Естественно ардуинку, чем меньше тем лучше, я собирал на нано.
4. 7 segment display индикатор с общим катодом(минус)
или взять готовую стоит около 160 рублей

Полный размерготовая плата

По подключению особо говорить нечего подробно и с комментариями

Типовая схема подключения MAX7219.

Фото взято с сайта, все понятно и доходчиво.

Работа с кодом.

DEMO sketch MAX7219 + Arduino + 7 segment
скачиваем на здоровье на github.com

Копируем и вставляем в свой скетч.

Опишем код

В кратце…

#include «LedControl.h» // Подключаем библиотеку
LedControl lc=LedControl(12,11,10,1); // используемые пины ардуины для подключения, и сколько драйверов в цепочке.
// pin 12 is connected to the CS (CS)(LOAD)
// pin 11 is connected to the CLK (CLK)
// pin 10 is connected to LOAD(DIN)

Шапка скетча, Подключаем библиотеку LedControl.h и прописываем порты к которым подключается MAX7219,
LedControl(12,11,10,1) последняя цифра говорит о том сколько у вас будет подключено драйверов в одной цепочке, их может быть любое количество, и в комментарии напоминание пины ардуино.

Далее…

void setup()
{
//Инициируем MAX7219
lc.shutdown(0,false);// включаем дисплей энергосбережение дисплей
lc.setIntensity(0,8);// устанавливаем яркость (0-минимум, 15-максимум)
lc.clearDisplay(0);// очищаем дисплей
}

Этот код тоже обязателен, в нем настраиваем энергосбережение, яркость светодиодов от 0 до 15, я выбрал 8 и мне этого достаточно, экспериментируйте и выбирайте нужный параметр.
Последняя строка, очистка дисплея очень нужная вещь, незабываем ей пользоваться, между кодами она часта используется, обратите внимание.

Ну и на по следок, как управлять индикатором и выводить на них хоть что-нибудь.

Более подробнее описание и библиотека LedControl на английском.

Первый
Зажигаем определенный LED * Установите статус одного Led
void setLed(int addr, int row, int col, boolean state);
* Params :
* addr адрес дисплея драйвера (количество драйверов подключенных последовательно)
* row сегмент-row(seg) (0.7)
* col цифра-col(dig) (0.7)
* boolean state : true включен led, false выключен led
* at col, row (0,цифра-col(dig), сегмент-row(seg), true) включен led
* at col, row (0,цифра-col(dig), сегмент-row(seg), false) выключен led
* в помощь картинка ниже для определения параметров led

Каждый отдельный светодиод сегмента зажигаем
Схема 4 digital 7 segment indicator/

схема есть в коде программы

Поможет вам определится с сегментом (пользуемся).

Второй

lc.setChar(0,3,’h’, false);
void setChar(int addr, int digit, char value, boolean dp);
(адрес драйвера, 0 для 7 индикатора номер цифры, hex значения символа, dp вкл и выкл);
отображение символов букв и цифр, а также формате (HEX).
Конечно на 7 сегментом индикаторе все буквы правильно отобразить не получится, хорошо читаются и понимаются
* ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’, ‘0’,
* ‘A’, ‘b’, ‘c’, ‘d’, ‘E’, ‘F’, ‘H’, ‘L’, ‘P’,
* ‘.’, ‘-‘, ‘_’, ‘ ‘

Третий

void setDigit(int addr, int digit, byte value, boolean dp);
отображение цифр.

Теперь фантазируем и придумываем интересные эффекты, есть идеи пишем в комментариях.

Всем удачи в ваших начинаниях.
С уважением serdgos.

Кто дочитал до конца, тот молодец. BONUS. (советую посмотреть видео).

PS В ближайшее время будет размещена статья о реализации часов с датчиком температуры и влажности DHT11, DS3231, MAX7219, Arduino Nano как это работает можно увидеть в видео.

Как говорится лучше один раз увидеть чем 100 раз услышать.

Код

Данный код просто прокручивает заданный текст. При желании вы можете изменить буквы или вставить вместо них другие символы.

/* This code is a public example. dil = 11 cs = 10 clk = 13 */ #include <MaxMatrix.h> #include <avr/pgmspace.h> PROGMEM prog_uchar CH = { 3, 8, B00000000, B00000000, B00000000, B00000000, B00000000, // space 1, 8, B01011111, B00000000, B00000000, B00000000, B00000000, // ! 3, 8, B00000011, B00000000, B00000011, B00000000, B00000000, // » 5, 8, B00010100, B00111110, B00010100, B00111110, B00010100, // # 4, 8, B00100100, B01101010, B00101011, B00010010, B00000000, // $ 5, 8, B01100011, B00010011, B00001000, B01100100, B01100011, // % 5, 8, B00110110, B01001001, B01010110, B00100000, B01010000, // & 1, 8, B00000011, B00000000, B00000000, B00000000, B00000000, // ‘ 3, 8, B00011100, B00100010, B01000001, B00000000, B00000000, // ( 3, 8, B01000001, B00100010, B00011100, B00000000, B00000000, // ) 5, 8, B00101000, B00011000, B00001110, B00011000, B00101000, // * 5, 8, B00001000, B00001000, B00111110, B00001000, B00001000, // + 2, 8, B10110000, B01110000, B00000000, B00000000, B00000000, // , 4, 8, B00001000, B00001000, B00001000, B00001000, B00000000, // — 2, 8, B01100000, B01100000, B00000000, B00000000, B00000000, // . 4, 8, B01100000, B00011000, B00000110, B00000001, B00000000, // / 4, 8, B00111110, B01000001, B01000001, B00111110, B00000000, // 0 3, 8, B01000010, B01111111, B01000000, B00000000, B00000000, // 1 4, 8, B01100010, B01010001, B01001001, B01000110, B00000000, // 2 4, 8, B00100010, B01000001, B01001001, B00110110, B00000000, // 3 4, 8, B00011000, B00010100, B00010010, B01111111, B00000000, // 4 4, 8, B00100111, B01000101, B01000101, B00111001, B00000000, // 5 4, 8, B00111110, B01001001, B01001001, B00110000, B00000000, // 6 4, 8, B01100001, B00010001, B00001001, B00000111, B00000000, // 7 4, 8, B00110110, B01001001, B01001001, B00110110, B00000000, // 8 4, 8, B00000110, B01001001, B01001001, B00111110, B00000000, // 9 2, 8, B01010000, B00000000, B00000000, B00000000, B00000000, // : 2, 8, B10000000, B01010000, B00000000, B00000000, B00000000, // ; 3, 8, B00010000, B00101000, B01000100, B00000000, B00000000, // < 3, 8, B00010100, B00010100, B00010100, B00000000, B00000000, // = 3, 8, B01000100, B00101000, B00010000, B00000000, B00000000, // > 4, 8, B00000010, B01011001, B00001001, B00000110, B00000000, // ? 5, 8, B00111110, B01001001, B01010101, B01011101, B00001110, // @ 4, 8, B01111110, B00010001, B00010001, B01111110, B00000000, // A 4, 8, B01111111, B01001001, B01001001, B00110110, B00000000, // B 4, 8, B00111110, B01000001, B01000001, B00100010, B00000000, // C 4, 8, B01111111, B01000001, B01000001, B00111110, B00000000, // D 4, 8, B01111111, B01001001, B01001001, B01000001, B00000000, // E 4, 8, B01111111, B00001001, B00001001, B00000001, B00000000, // F 4, 8, B00111110, B01000001, B01001001, B01111010, B00000000, // G 4, 8, B01111111, B00001000, B00001000, B01111111, B00000000, // H 3, 8, B01000001, B01111111, B01000001, B00000000, B00000000, // I 4, 8, B00110000, B01000000, B01000001, B00111111, B00000000, // J 4, 8, B01111111, B00001000, B00010100, B01100011, B00000000, // K 4, 8, B01111111, B01000000, B01000000, B01000000, B00000000, // L 5, 8, B01111111, B00000010, B00001100, B00000010, B01111111, // M 5, 8, B01111111, B00000100, B00001000, B00010000, B01111111, // N 4, 8, B00111110, B01000001, B01000001, B00111110, B00000000, // O 4, 8, B01111111, B00001001, B00001001, B00000110, B00000000, // P 4, 8, B00111110, B01000001, B01000001, B10111110, B00000000, // Q 4, 8, B01111111, B00001001, B00001001, B01110110, B00000000, // R 4, 8, B01000110, B01001001, B01001001, B00110010, B00000000, // S 5, 8, B00000001, B00000001, B01111111, B00000001, B00000001, // T 4, 8, B00111111, B01000000, B01000000, B00111111, B00000000, // U 5, 8, B00001111, B00110000, B01000000, B00110000, B00001111, // V 5, 8, B00111111, B01000000, B00111000, B01000000, B00111111, // W 5, 8, B01100011, B00010100, B00001000, B00010100, B01100011, // X 5, 8, B00000111, B00001000, B01110000, B00001000, B00000111, // Y 4, 8, B01100001, B01010001, B01001001, B01000111, B00000000, // Z 2, 8, B01111111, B01000001, B00000000, B00000000, B00000000, // 3, 8, B00000010, B00000001, B00000010, B00000000, B00000000, // hat 4, 8, B01000000, B01000000, B01000000, B01000000, B00000000, // _ 2, 8, B00000001, B00000010, B00000000, B00000000, B00000000, // ` 4, 8, B00100000, B01010100, B01010100, B01111000, B00000000, // a 4, 8, B01111111, B01000100, B01000100, B00111000, B00000000, // b 4, 8, B00111000, B01000100, B01000100, B00101000, B00000000, // c 4, 8, B00111000, B01000100, B01000100, B01111111, B00000000, // d 4, 8, B00111000, B01010100, B01010100, B00011000, B00000000, // e 3, 8, B00000100, B01111110, B00000101, B00000000, B00000000, // f 4, 8, B10011000, B10100100, B10100100, B01111000, B00000000, // g 4, 8, B01111111, B00000100, B00000100, B01111000, B00000000, // h 3, 8, B01000100, B01111101, B01000000, B00000000, B00000000, // i 4, 8, B01000000, B10000000, B10000100, B01111101, B00000000, // j 4, 8, B01111111, B00010000, B00101000, B01000100, B00000000, // k 3, 8, B01000001, B01111111, B01000000, B00000000, B00000000, // l 5, 8, B01111100, B00000100, B01111100, B00000100, B01111000, // m 4, 8, B01111100, B00000100, B00000100, B01111000, B00000000, // n 4, 8, B00111000, B01000100, B01000100, B00111000, B00000000, // o 4, 8, B11111100, B00100100, B00100100, B00011000, B00000000, // p 4, 8, B00011000, B00100100, B00100100, B11111100, B00000000, // q 4, 8, B01111100, B00001000, B00000100, B00000100, B00000000, // r 4, 8, B01001000, B01010100, B01010100, B00100100, B00000000, // s 3, 8, B00000100, B00111111, B01000100, B00000000, B00000000, // t 4, 8, B00111100, B01000000, B01000000, B01111100, B00000000, // u 5, 8, B00011100, B00100000, B01000000, B00100000, B00011100, // v 5, 8, B00111100, B01000000, B00111100, B01000000, B00111100, // w 5, 8, B01000100, B00101000, B00010000, B00101000, B01000100, // x 4, 8, B10011100, B10100000, B10100000, B01111100, B00000000, // y 3, 8, B01100100, B01010100, B01001100, B00000000, B00000000, // z 3, 8, B00001000, B00110110, B01000001, B00000000, B00000000, // { 1, 8, B01111111, B00000000, B00000000, B00000000, B00000000, // | 3, 8, B01000001, B00110110, B00001000, B00000000, B00000000, // } 4, 8, B00001000, B00000100, B00001000, B00000100, B00000000, // ~ }; int data = 11; // 8, DIN pin of MAX7219 module int load = 10; // 9, CS pin of MAX7219 module int clock = 13; // 10, CLK pin of MAX7219 module int maxInUse = 1; //change this variable to set how many MAX7219’s you’ll use MaxMatrix m(data, load, clock, maxInUse); // define module byte buffer; // active sentenses char string1 = » ABCDEFGHIJKLMNOPQRSTUVWXYZ «; void setup(){ m.init(); // module initialize m.setIntensity(0); // dot matix intensity 0-15 Serial.begin(9600); // serial communication initialize } void loop(){ byte c; // this is the code if you want to entering a message via serial console while (Serial.available() > 0){ byte c = Serial.read(); Serial.println(c, DEC); printCharWithShift(c, 100); } delay(100); m.shiftLeft(false, true); // 1st block — print the active sentences // comment this block when using the 2nd messages block printStringWithShift(string1, 100); //printStringWithShift(string2, 100); //printStringWithShift(string3, 100); //printStringWithShift(string4, 100); //printStringWithShift(string5, 100); //printStringWithShift(string6, 100); // 2nd block — print sentences just for tests // uncomment this block to use it /* printStringWithShift(string7, 100); printStringWithShift(string8, 100); printStringWithShift(string9, 100); printStringWithShift(string10, 100); printStringWithShift(string11, 100); */ } void printCharWithShift(char c, int shift_speed){ if (c < 32) return; c -= 32; memcpy_P(buffer, CH + 7*c, 7); m.writeSprite(maxInUse*8, 0, buffer); m.setColumn(maxInUse*8 + buffer, 0); for (int i=0; i<buffer+1; i++) { delay(shift_speed); m.shiftLeft(false, false); } } void printStringWithShift(char* s, int shift_speed){ while (*s != 0){ printCharWithShift(*s, shift_speed); s++; } } void printString(char* s) { int col = 0; while (*s != 0) { if (*s < 32) continue; char c = *s — 32; memcpy_P(buffer, CH + 7*c, 7); m.writeSprite(col, 0, buffer); m.setColumn(col + buffer, 0); col += buffer + 1; s++; } }

Тестирование

После загрузки кода мы можем в полной мере насладиться полученным результатом. Фото не передают всю картину в полной мере, поэтому лучше посмотреть видео.

На этом возможности использования матрицы не заканчиваются. Одновременно можно подключить до восьми матриц одновременно. И для этого понадобиться всего 3 лишних контакта. CS, CLK, 5В и Gnd всех матриц можно соединить вместе, а DATAOUT первой матрицы подключить к DATAIN следующей матрицы и т.д.

Статья является авторским переводом с сайта instructables.com.

Данная статья является собственностью Amperkot.ru. При перепечатке данного материала активная ссылка на первоисточник, не закрытая для индексации поисковыми системами, обязательна.

На предыдущих уроках мы научились управлять группой из восьми светодиодов при помощи сдвигового регистра. Это оказалось немного сложнее, чем зажигать 1-2 светодиода напрямую с выводов общего назначения. Проблема, которую нам тогда предстояло решить, заключалась в ограниченном количестве управляемых выводов у контроллера Ардуино. Апогеем же наших изысканий стало использование динамической индикации для управления сразу тремя индикаторами-цифрами. Теперь пришло время еще немного усложнить задачу: учимся работать со светодиодной матрицей.

1. Матричный индикатор

Как мы уже знаем, сегментные индикаторы, будь то шкалы или цифры, состоят из отдельных светодиодов, соединенных вместе. Например, у группы светодиодов могут быть соединены все катоды. Такой индикатор имеет приписку «с общим катодом», в противном случае — «с общим анодом». А что будет, если мы разместим светодиоды не в виде цифры или шкалы, а в виде сетки? Получится уже вполне себе графический индикатор. То есть такой, на котором можно отобразить не только число, но и какое-то изображение. Такая сетка называется матричным индикатором, а в случае использования светодиодов — светодиодной матрицей. Разрешение матричного индикатора — это количество точек по горизонтали и вертикали. Например, самые распространенные индикаторы имеют разрешение 8×8 точек.Если требуется светодиодная матрица с большим разрешением, то её просто-напросто составляют из нескольких 8×8 индикаторов. Как это делать, мы увидим позже. А пока разберемся как соединяются все 64 светодиода внутри матрицы. Конечно, можно бы было как и в случае семисегментного индикатора соединить все светодиоды общим катодом или анодом. В этом случае нам бы потребовалось либо 64 вывода контроллера, либо 8 сдвиговых регистров. Оба варианта весьма расточительны. Более правильный вариант — объединить светодиоды в группы по 8 штук с общим катодом. Пусть это будут столбцы матрицы. Затем, параллельные светодиоды в этих столбцах объединить снова в группы по 8 штук уже с общим анодом. Получится вот такая схема:Предположим, стоит задача зажечь светодиод R6C3. Для этого нам потребуется подать высокий уровень сигнала на вывод R6, а вывод C3 соединить с землей.Не выключая эту точку, попробуем зажечь другую — R3C7. Положительный контакт питания соединим с R3 и землю с C7. Но в таком случае строки R6 и R3 будут пересекаться с колонками C3 и C7 не в двух, а в четырех местах! Следовательно и зажжется не две, а четыре точки. Проблема!

Очевидно, что помочь сможет всё та же динамическая индикация. Если мы будем включать точки R6C3 и R3C7 по-очереди очень быстро, то сможем использовать персистентность зрения — способность интерпретировать быстро сменяющиеся изображения как одно целое.

2. Светодиодная матрица и сдвиговые регистры

В нашем уроке мы будем подключать к Ардуино Уно самую простую светодиодную матрицу 8×8 красного свечения. Нумерация выводов начинается с нижнего левого угла. При этом, нумерация ног 1-16 не связана никакой логикой с нумерацией колонок и строк C и R.Ориентируясь на урок про динамическую индикацию, попробуем использовать в схеме управления матричным индикатором 8-битные сдвиговые регистры. Один регистр подключим к выводам индикатора, отвечающим за колонки, а второй к выводам строк. Принципиальная схемаВажное замечание №1. Необходимо, чтобы резисторы в этой схеме были на линиях, идущих от первого сдвигового регистра. Этот сдвиговый регистр отвечает за колонки. При таком подключении, каждый резистор будет задавать ток только для одного светодиода на каждом шаге динамического алгоритма. Следовательно, все светодиоды будут светиться равномерно. Важное замечание №2. Указанная выше схема носит сугубо ознакомительный характер. Правильнее будет включить в разрыв между вторым регистром и матрицей дополнительную силовую микросхему, например транзисторную сборку ULN2003.

3. Программа

Чтобы было веселей, попробуем высветить на индикаторе смайлик. Как уже было сказано, для вывода изображения на матрицу воспользуемся динамической индикацией. А именно, будем высвечивать нашу картинку построчно. Сначала зажжем нужные колонки в самой верхней строке, затем во второй, в третьей, и так все 8 строк.За колонки у нас будет отвечать первый сдвиговый регистр, а за строки второй. Следовательно, вывод строки будет состоять из двух последовательных записей в регистр: сначала передаем код строки, затем код точек в этой строке. В этой программе мы также воспользуемся ускоренной версией функции digitalWrite. Это необходимо для того, чтобы процесс динамической индикации проходил очень быстро. В противном случае, мы увидим заметное мерцание матрицы. Исходный код const byte data_pin = PD2; const byte st_pin = PD3; const byte sh_pin = PD4; unsigned long tm, next_flick; const unsigned int to_flick = 500; byte line = 0; const byte data = { 0b00111100, 0b01000010, 0b10100101, 0b10000001, 0b10100101, 0b10011001, 0b01000010, 0b00111100 }; void latchOn(){ digitalWriteFast(st_pin, HIGH); digitalWriteFast(st_pin, LOW); } void fill( byte d ){ for(char i=0; i<8; i++){ digitalWriteFast(sh_pin, LOW); digitalWriteFast(data_pin, d & (1<<i)); digitalWriteFast(sh_pin, HIGH); } } void setPinFast(byte pin){ DDRD |= _BV(pin); } void digitalWriteFast(byte pin, byte sig){ if( sig ) PORTD |= _BV(pin); else PORTD &= ~_BV(pin); } void setup() { setPinFast(data_pin); setPinFast(st_pin); setPinFast(sh_pin); setPinFast(oe_pin); } void loop() { tm = micros(); if( tm > next_flick ){ next_flick = tm + to_flick; line++; if( line == 8 ) line = 0; // передаем код строки fill( ~(1<<(7-line)) ); // зажигаем точки в строке № line fill( data ); // открываем защелку latchOn(); } } Основная часть этой программы, включая переменные data_pin, sh_pin, st_pin, next_flick, to_flick и функцию fill уже известны нам из уроков про сдвиговый регистр и про динамическую индикацию. Массив data хранит восемь строк нашей картинки. Для экономии памяти мы записали каждую комбинацию точек в бинарном виде. Функция latchOn открывает защелку регистра. Это нужно делать только после заполнения обоих сдвиговых регистров. После загрузки программы на Ардуино, на индикаторе появится смайл.

4. Анимация на светодиодной матрице

А теперь доработаем программу таким образом, чтобы изображение на индикаторе менялось каждые пол секунды. Для этого вспомним еще раз урок про выполнение действий по таймеру. const byte data_pin = PD2; const byte st_pin = PD3; const byte sh_pin = PD4; unsigned long tm, next_flick, next_switch; const unsigned int to_flick = 500; const unsigned long to_switch = 500000; byte line = 0; byte frame = 0; const byte data = { { 0b00111100, 0b01000010, 0b10100101, 0b10000001, 0b10100101, 0b10011001, 0b01000010, 0b00111100 }, { 0b00111100, 0b01000010, 0b10100101, 0b10000001, 0b10000001, 0b10111101, 0b01000010, 0b00111100 }}; void latchOn(){ digitalWriteFast(st_pin, HIGH); digitalWriteFast(st_pin, LOW); } void fill( byte d ){ for(char i=0; i<8; i++){ digitalWriteFast(sh_pin, LOW); digitalWriteFast(data_pin, d & (1<<i)); digitalWriteFast(sh_pin, HIGH); } } void setPinFast(byte pin){ DDRD |= _BV(pin); } void digitalWriteFast(byte pin, byte sig){ if( sig ) PORTD |= _BV(pin); else PORTD &= ~_BV(pin); } void setup() { setPinFast(data_pin); setPinFast(st_pin); setPinFast(sh_pin); } void loop() { tm = micros(); if( tm > next_flick ){ next_flick = tm + to_flick; line++; if( line == 8 ) line = 0; fill( ~(1<<(7-line)) ); fill( data ); latchOn(); } tm = micros(); if( tm > next_switch ){ next_switch = tm + to_switch; frame = !frame; } } Загружаем программу на Ардуино и наблюдаем результат.

Задания

  • Гипноз. Запрограммировать контроллер таким образом, чтобы на светодиодной матрице с периодом в 1 секунду появлялись концентрические окружности с постоянно увеличивающимся радиусом.
  • Игра змейка. Реализовать на светодиодной матрице 8×8 такую известную игру, как змейка. В схему необходимо добавить четыре кнопки для управления направлением движения, а также зуммер для сигнализации события съедания яблок (или что там ест змея…).
  • Электронный уровень. Добавить в схему акселерометр. Написать программу, которая будет отображать на светодиодной матрице точку, координаты которой зависят от наклона всего устройства. Например, когда устройство зафиксировано параллельно земле (перпендикулярно вектору гравитации), то точка находится в центре. При наклоне электронного уровня влево, точка пропорционально смещается право.

Собрав схему управления матрицей, у многих может возникнуть вопрос: «Ну неужели за 30 лет никто не придумал более простого способа работы с матрицей?» На самом деле, придумали. Существуют специализированные микросхемы для работы с разными типами дисплеев, в том числе и для работы со светодиодной матрицей. В одном из следующих уроков мы научимся управлять индикатором с помощью микросхемы MAX7219. Такой способ позволит нам легко объединять несколько матриц с один большой дисплей, без необходимости сильно усложнять электрическую схему. Вконтакте Facebook Twitter Google+ 1+


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

Алгоритм индикации следующий: сначала устанавливаем на линиях требуемые логические уровни в зависимости от того какие сегменты надо включить на первом индикаторе (индикация слево направо), при этом высокий логический уровень для включения, низкий для выключения сегмента. Далее подаем высокий логический уровень на базу транзистора VT1, тем самым общий катод первого индикатора подключается к общему проводу, в этот момент загораются те сегменты, на анодах которых присутствует логическая единица. Через определенное время (пауза) индикатор отключаем, подав низкий логический уровень на базу транзистора, затем снова меняем логические уровни на линиях в соответствии с выводимой информацией, предназначенной для второго индикатора, и подаем сигнал включения на транзистор VT2. Таким образом, по порядку в круговом цикле коммутируем все индикаторы, вот и вся динамическая индикация.

Для получения цельного изображения без мерцаний, переключение необходимо выполнять с большой скоростью, для исключения мерцания светодиодов частоту обновления необходимо устанавливать от 70 Гц и более, я обычно устанавливаю 100 Гц. Для вышерассмотренной конструкции пауза рассчитывается следующим образом: для частоты в 100 Гц период равен 10 мс, всего 4 индикатора, соответственно время свечения каждого индикатора устанавливаем на уровне 10/4=2,5 мс. Существуют многоразрядные семисегментные индикаторы в одном корпусе, в которых одноименные сегменты соединены внутри самого корпуса, естественно для их использования необходимо применять динамическую индикацию.

Для реализации динамической индикации необходимо воспользоваться прерываниями по переполнению одного из таймеров. Ниже представлен код с использованием таймера TMR0:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;Реализация динамической индикации для 4-х семисегментных индикаторов ;Частота тактового генератора для примера 4 МГц, машинный цикл 1 мкс org 0000h ;начать выполнение программы с адреса 0000h goto Start ;переход на метку Start ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;Подпрограмма обработки прерываний org 0004h ;начать выполнение подпрограммы с адреса 0004h movwf W_TEMP ;сохранение значений ключевых регистров swapf STATUS,W ; clrf STATUS ; movwf STATUS_TEMP ; ; bcf ind1 ;выключение 1-го индикатора bcf ind2 ;выключение 2-го индикатора bcf ind3 ;выключение 3-го индикатора bcf ind4 ;выключение 4-го индикатора ; incf shet,F ;инкремент регистра shet movlw .5 ;проверка содержимого регистра shet xorwf shet,W ;на равенство числу 5 btfss STATUS,Z ; goto met1 ;число в регистре shet не равно 5 movlw .1 ;число в регистре shet равно 5: запись числа 1 movwf shet ;в регистр shet ; met1 movlw .1 ;проверка содержимого регистра shet xorwf shet,W ;на равенство числу 1 btfss STATUS,Z ; goto met2 ;число в регистре shet не равно 1: переход на met2 movf datind1,W ;число в регистре shet равно 1: копирование movwf PORTB ;содержимого регистра datind1 в регистр PORTB bsf ind1 ;включение 1-го индикатора goto exxit ;переход на метку exxit met2 movlw .2 ;проверка содержимого регистра shet xorwf shet,W ;на равенство числу 2 btfss STATUS,Z ; goto met3 ;число в регистре shet не равно 2: переход на met3 movf datind2,W ;число в регистре shet равно 2: копирование movwf PORTB ;содержимого регистра datind2 в регистр PORTB bsf ind2 ;включение 2-го индикатора goto exxit ;переход на метку exxit met3 movlw .3 ;проверка содержимого регистра shet xorwf shet,W ;на равенство числу 3 btfss STATUS,Z ; goto met4 ;число в регистре shet не равно 3: переход на met4 movf datind3,W ;число в регистре shet равно 3: копирование movwf PORTB ;содержимого регистра datind3 в регистр PORTB bsf ind3 ;включение 3-го индикатора goto exxit ;переход на метку exxit met4 movf datind4,W ;копирование содержимого регистра datind3 movwf PORTB ;в регистр PORTB bsf ind4 ;включение 4-го индикатора ; exxit bcf INTCON,T0IF ;сброс флага прерывания по переполнению TMR0 movlw .100 ;запись числа 156 в регистр таймера TMR0 movwf TMR0 ; ; swapf STATUS_TEMP,W ;восстановление содержимого ключевых регистров movwf STATUS ; swapf W_TEMP,F ; swapf W_TEMP,W ; ; retfie ;выход из подпрограммы прерывания ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;Основная программа Start …………….. ;первоначальная настройка регистров …………….. ;специального назначения …………….. bsf STATUS,RP0 ;запись двоичного числа 11010011 в регистр movlw b’11010011′ ;OPTION_REG, тем самым устанавливаем внутренний movwf OPTION_REG ;источник тактового сигнала для TMR0 bcf STATUS,RP0 ;включаем предделитель перед TMR0 ;устанавливаем коэффициент предделителя 1:16 ; clrf shet ;обнуление регистра shet, перед запуском ;прерываний по переполнению TMR0, выполняется ;однократно, после включения питания ; clrf datind1 ;очистка регистров вывода информации на clrf datind2 ;индикаторы, равнозначно выключению clrf datind3 ;индикаторов, так как индикаторы с общим clrf datind4 ;катодом ; bcf INTCON,T0IF ;сброс флага прерывания по переполнению TMR0 bsf INTCON,T0IE ;разрешение прерываний по переполнению TMR0 bsf INTCON,GIE ;разрешение глобальных прерываний ; movlw b’00000110′ ;пример вывода числа 13,52 movwf datind1 ; movlw b’11001111′ ; movwf datind2 ; movlw b’01101101′ ; movwf datind3 ; movlw b’01011011′ ; movwf datind4 ; ; …………….. ; …………….. ; …………….. ; ; end ;конец всей программы ;

В основной программе сначала настраиваем таймер с помощью регистра OPTION_REG, ранее я рассказывал про использование таймеров для временной задержки. Далее очищаем регистр shet, предназначенный для введения счета от 1 до 4, для каждого индикатора. Этот регистр инкрементируется в подпрограмме обработки прерываний и там же корректируется (он будет считать от 1 до 4), поэтому данная очистка выполняется однократно после включения питания. По этому регистру будем определять, какой индикатор включать и выдавать данные соответствующие ему. Следующим шагом будет очистка регистров хранения информации, четыре регистра dataind1,2,3,4 соответствующие четырем индикаторам. Очистка равнозначна выключению индикаторов, так как в подпрограмме обработки прерываний, содержимое этих регистров передается в регистр PORTB, к которому подключены аноды индикаторов. Это необходимо для того чтобы на индикаторах не высвечивался всякий мусор после разрешения прерываний, в принципе этого можно и не делать, если сразу записывать правильную информацию для вывода. Далее сбрасываем флаг прерывания по переполнению таймера, разрешаем прерывания по переполнению TMR0, и наконец, разрешаем глобальные прерывания.

В подпрограмме обработки прерываний, первым делом выключаем все индикаторы (подав низкие логические уровни на базы транзисторов), потому что неизвестно какой из них включен. Производим инкремент регистра shet, с проверкой на равенство числу 5, при наличии такого совпадения записываем в регистр число 1, так как необходимо вести счет от 1 до 4. Далее проверяем, какое именно число лежит в регистре shet, по которому загружаем в PORTB данные из регистров хранения информации (dataind) для соответствующего индикатора и включаем его. После чего сбрасываем флаг прерывания по переполнению TMR0, записываем число 100 в таймер (ниже приведен расчет этого значения), для временной задержки и выходим из обработчика прерываний. При первом прерывании включается первый индикатор, во втором прерывании второй и так по круговому циклу. В основной программе остается только загружать данные в регистры хранения информации для каждого индикатора. В подпрограмме прерываний не забываем сохранять и восстанавливать значения ключевых регистров, об этом я писал в статье про прерывания.

Для вывода чисел лучше использовать знакогенератор в виде таблицы данных. Например, чтобы вывести число 3456 на индикаторы, его необходимо разбить на разряды, при этом лучше использовать отдельные регистры для хранения чисел разрядов (от 0 до 9), далее прогнать эти регистры через знакогенератор, получив тем самым правильные байты (загружаемые в регистры dataind) для зажигания соответствующих сегментов.

Частоту тактового генератора примем за 4 МГц, машинный цикл 1 мкс. Частота обновления каждого индикатора пускай составит 100 Гц (период T=10 мс), соответственно необходимая временная задержка равна 10/4 = 2,5 мс. Коэффициент предделителя для TMR0 устанавливаем равным 1:16, при этом максимально возможная задержка равна 256х16 = 4096 мкс, а нам требуется пауза в 2,5 мс. Рассчитаем число для записи в TMR0: 256-((256х2,5)/4,096) = 256-156,25 = 99,75. После округления получим число 100.

Ниже можно скачать модель для программы Proteus, прошивку и исходник с реализацией динамической индикации на 4-х разрядный индикатор с общим катодом с применением микроконтроллера PIC16F628A. Для примера на индикатор выводятся числа 0000; 0001; 0002; 13,52; 9764.
Прошивка МК и исходник + файл проекта Proteus_7.7

Теперь рассмотрим подключение матрицы с разрешением 8х8 точек (светодиодов). Структуру матрицы обычно рассматривают в виде строк и столбцов. На картинке ниже в каждом столбце соединены катоды всех светодиодов, а в каждой строке аноды. Строки (8 линий, аноды светодиодов) через резисторы подключают к микроконтроллеру. Каждый столбец (катоды светодиодов) подключают к микроконтроллеру через 8 транзисторов. Алгоритм индикации такой же, сначала устанавливаем необходимые логические уровни на строках, в соответствии с тем, какие светодиоды должны гореть в столбце, далее подключаем первый столбец (индикация слева направо). Через определенную паузу выключаем столбец, и изменяем логические уровни на строках для отображения второго столбца, далее подключаем второй столбец. И так поочередно коммутируем все столбцы. Ниже представлена схема подключения матрицы к микроконтроллеру.

Всего для подключения такой матрицы потребуется 16 выводов микроконтроллера, что весьма немало, поэтому для сокращения управляющих линий лучше использовать последовательные сдвиговые регистры.

Самым распространенным последовательным регистром является микросхема 74НС595, которая содержит в себе сдвиговый регистр для загрузки данных, и регистр хранения через который данные передаются на выходные линии. Загружать данные в него просто, устанавливаем логический 0 на входе тактирования SH_CP, далее устанавливаем требуемый логический уровень на входе данных DS, после чего переключаем вход тактирования в 1, при этом происходит сохранение значения уровня (на входе DS) внутри сдвигового регистра. Одновременно с этим происходит сдвиг данных на один разряд. Снова сбрасываем вывод SH_CP в 0, устанавливаем требуемый уровень на входе DS и поднимаем SH_CP в 1. После полной загрузки сдвигового регистра (8 бит), устанавливаем в 1 вывод ST_CP, в этот момент данные передаются в регистр хранения и поступают на выходные линии Q0…Q7, после чего сбрасываем вывод ST_CP. Во время последовательной загрузки, данные сдвигаются от Q0 до Q7. Вывод Q7’ подключен к последнему разряду сдвигового регистра, этот вывод можно подключить на вход второй микросхемы, таким образом можно загружать данные сразу в две и более микросхемы. Вывод OE переключает выходные линии в третье (высокоомное) состояние, при подаче на него логической 1. Вывод MR предназначен для сброса сдвигового регистра, то есть установка низких логических уровней на выходах триггеров регистра, что эквивалентно загрузке восьми нулей. Ниже представлена диаграмма загрузки данных в микросхему 74НС595, установка значения 11010001 на выходных линиях Q0…Q7, при условии, что изначально там были нули:

Рассмотрим подключение матрицы 8×8 к микроконтроллеру PIC16F628A с помощью двух сдвиговых регистров 74HC595, схема представлена ниже:

Данные загружаются в микросхему DD2 (управление логическими уровнями на строках, аноды светодиодов) затем через вывод Q7’ передаются в DD3 (управление столбцами), соответственно сначала загружаем байт для включения столбца, затем байт с логическими уровнями на строках. К выходным линиям DD3 подключены транзисторы коммутирующие столбцы матрицы (катоды светодиодов). Ниже приведен код программы для вывода изображения на матрицу:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;Реализация динамической индикации для матрицы с разрешением 8х8 ;Частота тактового генератора для примера 4 МГц, машинный цикл 1 мкс org 0000h ;начать выполнение программы с адреса 0000h goto Start ;переход на метку Start ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;Подпрограмма обработки прерываний org 0004h ;начать выполнение подпрограммы с адреса 0004h movwf W_TEMP ;сохранение значений ключевых регистров swapf STATUS,W ; clrf STATUS ; movwf STATUS_TEMP ; ; movf FSR,W ;сохранение текущего значения регистра FSR movwf FSR_osn ;в регистр FSR_osn movf FSR_prer,W ;восстановление ранее сохраненного значения movwf FSR ;регистра FSR из регистра FSR_prer ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;загрузка содержимого регистра stolb в микросхему ;74HC595 (последовательный сдвиговый регистр) movf stolb,W ;копирование содержимого регистра stolb movwf var ;в регистр var movlw .8 ;запись числа 8 в регистр scetbit, для отсчета movwf scetbit ;переданных битов met2 btfsc var,0 ;устанавливаем вывод ds в соответствии с bsf ds ;значением 7-го бита регистра var btfss var,0 ; bcf ds ; bsf sh_cp ;тактируем вывод sh_cp, для защелкивания данных bcf sh_cp ; rrf var,F ;сдвиг регистра var вправо, для подготовки ;следующего бита decfsz scetbit,F ;декремент с условием регистра scetbit goto met2 ;scetbit не равен нулю: переход на метку met2 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;загрузка содержимого регистра INDF в микросхему ;74HC595 (последовательный сдвиговый регистр) movf INDF,W ;копирование содержимого регистра INDF movwf var ;в регистр var movlw .8 ;запись числа 8 в регистр scetbit, для отсчета movwf scetbit ;переданных битов met1 btfsc var,7 ;устанавливаем вывод ds в соответствии с bsf ds ;значением 7-го бита регистра var btfss var,7 ; bcf ds ; bsf sh_cp ;тактируем вывод sh_cp, для защелкивания данных bcf sh_cp ; rlf var,F ;сдвиг регистра var влево, для подготовки ;следующего бита decfsz scetbit,F ;декремент с условием регистра scetbit goto met1 ;scetbit не равен нулю: переход на метку met1 ; bsf st_cp ;тактируем вывод st_cp, для передачи загруженных bcf st_cp ;байтов на выходные линии микросхем 74HC595 ; bcf STATUS,C ;сброс бита C регистра статус перед сдвигом rrf stolb,F ;сдвиг влево регистра stolb ; incf FSR,F ;инкремент регистра FSR, подготовка следующего ;регистра для отправки данных на 74HC595 decfsz shet,F ;декремент с условием регистра shet goto exxit ;регистр shet не равен 0: переход на exxit movlw data1 ;регистр shet равен 0: запись адреса первого movwf FSR ;регистра хранения иннформации в регистр FSR movlw .8 ;запись числа 8 в регистр shet, для ведения movwf shet ;счета столбцов movlw b’10000000′ ;запись двоичного числа 10000000 в movwf stolb ;регистр stolb, для включения 1-го столбца ; exxit bcf INTCON,T0IF ;сброс флага прерывания по переполнению TMR0 movlw .124 ;запись числа 124 в регистр таймера TMR0 movwf TMR0 ; ; movf FSR,W ;сохранение текущего значения регистра FSR movwf FSR_prer ;в регистр FSR_prer movf FSR_osn ,W ;восстановление ранее сохраненного значения movwf FSR ;регистра FSR из регистра FSR_osn ; swapf STATUS_TEMP,W ;восстановление содержимого ключевых регистров movwf STATUS ; swapf W_TEMP,F ; swapf W_TEMP,W ; ; retfie ;выход из подпрограммы прерывания ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;Основная программа Start …………….. ;первоначальная настройка регистров …………….. ;специального назначения …………….. bsf STATUS,RP0 ;запись двоичного числа 11010011 в регистр movlw b’11010010′ ;OPTION_REG, тем самым устанавливаем внутренний movwf OPTION_REG ;источник тактового сигнала для TMR0 bcf STATUS,RP0 ;включаем предделитель перед TMR0 ;устанавливаем коэффициент предделителя 1:8 ; movlw .8 ;запись числа 8 в регистр shet, перед запуском movwf shet ;прерываний по переполнению TMR0, выполняется ;однократно, после включения питания movlw b’10000000′ ;запись двоичного числа 10000000 в movwf stolb ;регистр stolb, для включения 1-го столбца ;выполняется однократно, после включения питания ; movlw data1 ;запись адреса первого регистра (регистры хранения movwf FSR_prer ;информации) в регистр FSR_prer, выполняется ;однократно, после включения питания ; movlw .8 ;очистка 8-ми регистров вывода информации на movwf tmp ;матрицу, равнозначно выключению movlw data1 ;матрицы movwf FSR ; met3 clrf INDF ; incf FSR,F ; decfsz tmp,F ; goto met3 ; ; bcf INTCON,T0IF ;сброс флага прерывания по переполнению TMR0 bsf INTCON,T0IE ;разрешение прерываний по переполнению TMR0 bsf INTCON,GIE ;разрешение глобальных прерываний ; m1 movlw data1 ;пример вывода буквы R movwf FSR ; movlw b’00000000′ ; movwf INDF ; incf FSR,F ; movlw b’01111111′ ; movwf INDF ; incf FSR,F ; movlw b’00001001′ ; movwf INDF ; incf FSR,F ; movlw b’00011001′ ; movwf INDF ; incf FSR,F ; movlw b’00101001′ ; movwf INDF ; incf FSR,F ; movlw b’01000110′ ; movwf INDF ; incf FSR,F ; movlw b’00000000′ ; movwf INDF ; incf FSR,F ; movlw b’00000000′ ; movwf INDF ; ; …………….. ; …………….. ; …………….. ; ; end ;конец всей программы ;

В основной программе настраиваем таймер, записываем число 8 в регистр shet. Этот регистр используется в качестве счетчика при выводе информации на столбцы, регистр декрементируется от 8 до 0 в подпрограмме обработки прерываний, и там же корректируется, поэтому первоначальная запись выполняется однократно, после подачи питания, перед разрешением прерываний. Далее записываем двоичное число 10000000 в регистр stolb, в подпрограмме обработки прерываний содержимое этого регистра передается в микросхему DD3 для коммутации соответствующего столбца. В данном случае двоичное число 10000000 включает первый столбец, для включения остальных столбцов, содержимое регистра сдвигается на один бит. Запись в регистр stolb также выполняется однократно, корректировка производится в обработчике прерываний.

Информация с логическими уровнями для строк каждого столбца, хранится в 8-ми регистрах хранения информации, обращение к которым выполняется через косвенную адресацию. Адресу первого регистра присвоено название data1. Кроме первоначальной записи регистров shet и stolb, необходимо записать в регистр FSR_prer адрес первого регистра хранения информации (регистр – data1, запись в FSR_prer выполняется однократно, далее корректируется в обработчике), только после этого разрешать прерывания по переполнению TMR0.

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

В подпрограмме обработки прерываний загружаем в микросхему DD2 содержимое регистра stolb (при первом входе в обработчик после разрешения прерываний, в регистре лежит число 10000000, как было сказано выше). Загрузка начинается с младшего бита регистра stolb, который сдвигается в направлении от Q0 к Q7 (внутри микросхемы DD2) по мере загрузки, алгоритм загрузки был рассмотрен выше, так что думаю, разобраться в коде не составит труда. Далее загружаем в DD2 содержимое регистра INDF, это один из регистров хранения информации, адрес которого находится в FSR (при первом входе в обработчик после разрешения прерываний в FSR лежит адрес первого регистра хранения информации с названием data1). Загрузка начинается со старшего бита регистра INDF. После загрузки рассмотренных 2-х байтов, тактируем вывод st_cp, тем самым загруженные данные передаются на выходные линии микросхем DD2, DD3. Таким образом, при первом входе в обработчик коммутируется первый столбец матрицы, в котором загораются светодиоды, на анодах которых присутствует высокий логический уровень, в соответствии с содержимым регистра data1 (первый регистр хранения информации).

Далее сдвигаем регистр stolb вправо на один бит, для того чтобы подготовить к коммутации второй столбец матрицы при следующем входе в обработчик прерываний. Перед сдвигом необходимо очистить флаг C регистра STATUS, так как сдвиг происходит через этот флаг, и его состояние не известно на момент сдвига. После сдвига, инкрементируем регистр FSR, подготавливая следующий регистр хранения информации (после регистра data1) с логическими уровнями строк для второго столбца. Далее декрементируем с условием регистр shet, и если он не равен нулю, сбрасываем флаг прерывания по переполнению TMR0, производим запись числа в таймер, и выходим из обработчика прерываний.

При следующем входе в обработчик включится второй столбец матрицы и так далее. При обнулении регистра shet (после коммутации 8-го столбца), в него записывается число 8 для очередного цикла коммутации столбов, кроме этого корректируется значение регистра stolb, в регистр FSR записывается адрес первого регистра хранения информации (data1).

Выполним расчет временной задержки для таймера TMR0, частота тактового генератора 4 МГц, машинный цикл 1 мкс. Чтобы избежать мерцания светодиодов, примем частоту обновления каждого столбца в 100Гц (период T=10 мс), временная задержка равна 10/8 = 1,25 мс. Коэффициент предделителя TMR0 установим равным 1:8, при этом максимально возможная задержка равна 256х8 = 2048 мкс. Для паузы в 1,25 мс таймер должен отсчитать (256х1,25)/2,048 = 156,25 раз, округляя получим 156 отсчетов. Соответственно в таймер необходимо записать число 256-156 = 100. Но это не совсем правильное значение, так как на выполнение подпрограммы обработки прерываний затрачивается некоторое время, в данном случае на это уходит около 190 мкс, в перерасчете с учетом коэффициента предделителя получаем 190/8 = 23,75 или 24 отсчета. Правильное значение для записи в TMR0 равно: 100+24=124.

В основной программе записываем 8 регистров хранения информации, в соответствии с тем, что хотим вывести на матрицу. Ниже представлена схема поясняющая вывод информации на матрицу для вышерассмотренного кода.

Кроме сдвиговых регистров существуют специализированные драйвера для вывода информации на семисегментные индикаторы и светодиодные матрицы, в этом случае динамическую индикацию реализует сам драйвер, остается только отправлять на него данные для отображения. Один из таких популярных драйверов я рассмотрел в статье про подключение MAX7219.

Ниже по ссылке можно скачать прошивку и исходник для микроконтроллера PIC16F628A, с реализацией динамической индикации на матрице 8х8 с применением двух сдвиговых регистров 74HC595, схема подключения была рассмотрена выше. На матрицу поочередно выводятся буквы R, L, цифра 46, смайлик, и просто узор в виде креста, эта анимация продемонстрирована в видеоролике ниже.
Прошивка МК и исходник
Даташит на светодиодную матрицу FYM-23881

Последние записи:

  • Часы на газоразрядных индикаторах
  • Стробоскоп своими руками
  • Часы на индикаторах ИВ-11
  • Солнечный трекер

Вот приспичило вам сделать себе могучую светодиодную хреновину, чтобы моргала и переливалась. Да еще в RGB и плавненько так. Собрали вы это дело, поглядели на количество каналов которыми нужно рулить и призадумались…

▌А что не так с ШИМ?
Да все с ним хорошо, только аппаратных каналов обычно всего несколько штук. А программный ШИМ имеет ряд недостатков. Да, можно взять и на базе алгоритма управления кучей сервомашинок, используя всего один таймер собрать многоканальный ШИМ, но сколько у нас будет вызовов прерываний?

Каждый отдельный фронт потребует своего прерывания на смену уровня. А представьте, что у нас этих каналов будет не 4, а 40? Или 400? Да контроллер из прерываний вылезать не будет. Прерывания будут налезать друг на друга, порождая джиттер. Не говоря уже о том, что все эти каналы надо будет при любом изменении скважности заново сортировать по длительности. В общем, тупилово будет еще то.

▌Нас спасет BAM
Но решение есть. Зовется этот метод BAM. Суть его в том, что мы включаем нагрузку импульсами, поразрядно, с длительностью равной весу разряда.

В результате мы имеем высокую дискретность, но при этом у нас всего 7 прерываний на любое число каналов. Соответственно разрядам.

Интегрируется все аналогично обычному ШИМу. Но есть ряд нюансов:

  1. Частота плавает и на малых разрядах она повышается. Для светодиода или грелки это наплевать. А вот двигатель или еще какую нагрузку с реактивными элементами вроде обмоток или емкостей я бы таким сигналом питать не стал.
  2. При переходе с малых весов к одному большому наблюдается мерцание. Но с этим можно бороться, подробности ниже.
  3. Выдавать вес лучше с большего к меньшему, так меньше заметно влияние второго пункта.

▌Код
Концепция ясна, попробуем замутить прогу которая это реализует.

Начнем с автомата который формирует временные интервалы. Все реализации которые я видел, использовали массив выдержек, что загружались в таймер. Но мне это не нравится, я предпочитаю максимально аппаратные решения. Ведь чему равен вес разряда? Да тому же байту у которого всего одна единичка в соответствующем бите. И байт с битом в нужном разряде автоматически становится правильной временной выдержкой.

Поэтому нам для всего этого хватит таймера в режиме СТС и одного прерывания OCR. Поглядим даташит на ATMega16A, раздел 8-bit Timer/Counter0 with PWM.

Там есть такая вот картинка:

То что нужно!

Дальше работать будет до смешного просто. Мы загружаем в OCR0 число 0b10000000 и запускаем таймер. А в прерывании по OCR сдвигаем это число по кругу направо, получая автоматом 0b01000000 -> 0b00100000 -> 0b00010000 -> 0b00001000 и так далее. Попутно у нас получается маска для выбора бита из массива. И все это само. Красота же 🙂

Вот код автомата времени:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <avr/io.h> #include <avr/interrupt.h> #include <avrlibtypes.h> #include <avrlibdefs.h> // Макросы циклических сдвигов #define ROL(x) ((x << 1) | (x >> 7)) #define ROR(x) ((x >> 1) | (x << 7)) // Прерывание по сравнению ISR(TIMER0_COMP_vect) { // Сдвигаем содержимое регистра сравнения. Это автоматом // Уменьшает выдержку до следующего прерывания // А также формирует маску выборки бита. OCR0 = ROR(OCR0); // Тут будет самая движуха } void main(void) { volatile u08 a; sei(); // Инициализируем таймер TCNT0 = 0; OCR0 = 0b10000000; // Настраиваем в режим СТС и запускаем. TIMSK |= 1<<OCF0; TCCR0 |= 1<<WGM01 | 4<<CS00; // Пустой цикл. Должна же где то висеть основная программа 🙂 while(1) { a++; } }

Можно поглядеть в отладчике как работает все это 🙂 Заодно посмотреть временные интервалы, хватает ли нам скорости и так далее.

Осталось за малым, наладить копирование битов и загрузку этого всего в порты. Будет у меня 8 каналов. Не ахти какое число, но для примера сойдет. На крайняк, желающие копипастой налепят себе сколько не жалко.

Забахаем массивчик и сразу накидаем туда циферок:

1 u08 BRG= {1,33,65,97,129,161,193,225};

Это яркости каждого канала.

Выборка битов из него будет следующая:

Сначала обнуляем нужный бит порта, чтобы ничего лишнего там не было.

1 PORTA &=~(1<<3);

А дальше берем нашу маску, по совместительству регистр OCR0, лепим ее на значение яркости, чтобы вычленить нужный бит. Если этот бит равен нулю, то записываем в бит порта ноль, если не нулю, то единицу.

1 PORTA |= ((BRG & OCR0)?1:0)<<3;

Увы в битовых операциях Си (да и остальные высокие языки) ведет себя подобно карьерному бульдозеру во дворе. Остается лишь надеяться, чтобы компилятор сам применил всю мощь процессорных команд для работы с битами.

Посмотрим что получилось:

9 команд на операцию. Не сказать, что сильно много, но делая это вручную, на ассемблере, можно было бы вообще обойтись без сравнений. А просто копировать бит командами BST/BLD получилось бы быстрей и за меньшее число операций. Ну и в других архитектурах может быть более оптимально, особенно если есть возможность просто копировать биты.

Обернем эту процедурку в макрос, для пущей наглядности и удобства.

1 2 3 4 5 #define COPYBIT(dst,bit,src) \ do { \ (dst) &=~(1<<(bit)); \ (dst) |=(((src) & OCR0)?1:0)<<(bit); \ } while(0)

Весь код получился крошечный и простой:

Опа и получили градиентик:

Причем, как вы понимаете, совершенно не обязательно пихать данные сразу же в порт. А если у вас нет столько портов? Ничего страшного, пихать их можно куда угодно. Например затолкать их в буфер, а на последнем байте весь этот паровоз через какой-нибудь SPI протокол загнать в сдвиговые регистры и отправить на светодиодную линейку феерической длины.

А вот так он выглядит на логическом анализаторе:

▌Косяки
Дальше начинаются приколы. Самый безобидный заключается в том, что напряжением яркость светодиода регулируется весьма нелинейно. Т.е. если плавно менять яркость от 0 до 255, то вначале она резко наростает, а начиная со второй половины и не видно толком. Радости добавляет еще и нелинейность восприятия света глазом. В общем, если захотите прям ровненький такой переход яркости, то кривые зависимостей придется очень тщательно подбирать.

Еще один прикол обнаружился в динамике. В статике то оно все замечательно. Горит себе на 100 герцах и не жалуется. Но стоило мне начать гонять яркости по кругу в цикле, как начались странные моргания. Долго не мог понять что это за фигня, искал ошибку в алгоритме. Нерегулярная природа их намекала на некий случайный процесс. Как то, например, период изменения яркости относительно скорости автомата на прерываниях.

Решил делать смену яркости только на начало автомата. Реализовал это следующим образом:

1 2 3 4 5 6 7 while(1) { if(OCR0 == 128) { BRG+=1; _delay_ms(10); }

Т.е. мы бесимся в цикле пока не ухватим за начальную стадию автомата, а там и подождать в тупой задержке можно.

И стал ловить переход подбирая числа. Выловил, оказалось, что при смене с 127 на 128 хорошо так мерцает. А вот в статике разница в яркости между 127 и 128 на глаз не различается почти. Что за мистика? Ткнулся осциилографом и увидел следующую картину:

В месте перехода со 128 на 127 возникает стыковка двух байтов. Она дает двойной интервал. Казалось бы, ну двойной интервал. Единичный, на 120ГЦ. Но блин, на глаз его видно.

Поставил смену на конец цикла:

1 2 3 4 5 6 7 while(1) { if(OCR0 == 1) { BRG+=1; _delay_ms(10); }

Картинка изменилась. Теперь у нас вспышка. Да епт…

Взял и воткнул смену на середину цикла.

1 2 3 4 5 6 7 while(1) { if(OCR0 == 64) { BRG+=1; _delay_ms(10); }

Стало намного лучше. Переход есть, но это надо оочень сильно всматриваться.

Вообще развлекаясь с этой штукой заметил интересную вещь — глаз отлично видит изменения, а разницу в статике практически не замечает. Т.е. когда яркость меняется на 1/256 — это заметно. А когда два рядом горят, отличаясь на тот же 1/256 разницы практически не видно.

▌Производительность
В отличии от ШИМ, который молотит равномерно, BAM имеет периоды загрузки и периоды практически полного бездействия. Анализируя состояние BAM автомата можно планировать тяжелые вычисления. Например между 128м весом и 64 времени ну просто завались, можно посчитать что-то очень тяжелое. А если у нас молотилка вышла на малые веса 8 и меньше, то лучше тормознуть все процессы и дать индикации нормально добить эти периоды, благо это недолго.

▌Outro
Ну и вот вам немного лулзов на тему синих светодидов выжигающих глаза, что так любят китайцы втыкать в лицевые панели разным девайсам. Была у меня на колонке эта пакость, на индикаторе питания. Жарила шо гиперболоид, даром что обычный дешманский светодиод на 3мм. Ну заклеил я ее изолентой. А через год полез зачем-то к колонке. Оторвал изоленту эту. А там…