[Из песочницы] DIY-контроллер LED панели на CPLD с использованием BAM модуляции

Некоторое время назад участвовал в обсуждении DIY проекта матричных светодиодных часов
И что меня удивило — в качестве устройства отображения использовались древние одноцветные светодиодные матрицы 8×8 с шагом 5 миллиметров. Причём под них разводились сложные печатные платы, делалась софтовая динамическая индикация. И это в то время, когда уже давно доступны по цене в районе 10–20$ готовые полноцветные LED панели 64×32 с шагом 3 мм. А общий ассортимент подобных панелей очень большой и имеет шаг пикселя от 2 до 10 мм и практически любой размер.
В то же время использовать такие панели в DIY конструкциях достаточно непросто — готовые контроллеры стоят довольно больших денег и не имеют нормального API. Сделать же достаточно быстрое сканирование панели на обычно используемых в DIY микроконтроллерах достаточно сложно. Причём временные интервалы должны выдерживаться с высокой точностью — иначе начинается заметная неравномерность яркости.

Есть неплохие решения на Adafruit, но они все достаточно дорогие и сложные.

После некоторого раздумья возникла мысль —, а почему бы не сделать предельно недорогую плату, которая будет мостом между обычной копеечной платой типа ардуино и LED панелью? После какой-то пары месяцев возни родилось что-то работающее.

В данной статье описана вторая, улучшенная версия контроллера.

Задача


В качестве базовой задачи хотелось иметь возможность управлять сборной панелью общим размером хотя бы 64×64, имея при этом возможность работы хотя бы в Highcolor (RGB565) с сохранением приемлемой частоты обновления экрана (не менее 50Гц). В первой версии контроллера базовая задача была полностью реализована, но возникла идея реализации задачи другим, весьма многообещающим методом, из чего и родилась вторая версия.

Базовые пояснение по устройству типовой LED панели


На входе интерфейс HUB75:

2dyngtqqelfmxsb9iseipe2c29u.jpeg


На каждом входе цвета стоит цепочка регистров типа HC595 (но специальные 16-ти канальные версии для светодиодов). Регистров столько, чтобы хватало на ширину панели. Входы клока, параллельной загрузки и разрешения выхода общие для всех регистров. Входы ABCDE — это выбор ряда — идут на обычный дешифратор.

Принцип работы:

  • выставляем данные на RGB входы, щелкаем клоком CLK. Повторяем, пока не загрузим всю строку
  • выключаем выходы OE = 1 (чтобы помех не было)
  • выдаём на дешифратор номер загруженного ряда
  • щелкаем параллельной загрузкой LAT — данные строки переносятся в выходные регистры
    включаем выходы OE = 0
  • повторяем для следующего ряда


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

Для того, чтобы получить градации яркости классическим PWM, такой цикл приходится повторять N-1 раз, где N — число градаций яркости (256 для RGB888). А учитывая, что при этом это всё еще и мерцает — всё это надо делать очень-очень быстро.

Есть обходной вариант — Bit Angle Modulation (BAM). При этом время свечения в каждом цикле пропорционально весу отображаемого бита. То есть для RGB888 надо всего 8 циклов отображения. Чуть более детально — здесь.

В первой версии контроллера использовался классический PWM, что накладывало жёсткое ограничение на количество циклов сканирования. Во второй версии реализован BAM, что дало огромный выигрыш в скорости.

Реализация


Совершенно очевидным было то, что обычный микроконтроллер тянет только маленькие панели — на большие просто не хватает скорости. Поэтому без CPLD или FPGA здесь не обойтись — выдавать десятки МБ/сек на недорогих микроконтроллерах физически невозможно.

В качестве памяти мне на форуме IXBT порекомендовали очень интересную FIFO память Averlogic AL422B, которая имеет примерно 400 кбайт памяти и может работать на частотах до 50МГц.

Учитывая, что моим основным требованием была максимальная дешевизна компонентов, чтобы готовая платка была доступна самодельщикам — была выбрана Altera EPM3064 — CPLD c 64 мя макроячейками. В то же время столь малое количество макроячеек не позволяет сделать динамически конфигурируемую плату — конфигурацию необходимо компилировать непосредственно в CPLD.

→ Получившаяся схема лежит здесь

Детали:

  • CPLD EPM3064ATC44–10 — цена на Ali примерно 13–15$ за десяток
  • FIFO RAM AL422B — цена на Ali примерно 15$ за десяток
  • Кварцевый генератор на 50МГц. На плате предусмотрена установка в корпусах DIP14/DIP8/7050. Цена на Ali примерно 6–7$ за десяток
  • Стабилизатор на 3.3В в корпусе SOT223. Цена в Чип и Дип — 40р за штуку
  • Разъем IDC-10MS. Цена в Чип и Дип — 3 р/штуку
  • Разъем IDC-16MS. Цена в Чип и Дип — 8 р/штуку
  • Разъём IDC-14MS. Цена в Чип и Дип — 7 р/штуку
  • Конденсаторы 1 мкФ 0805 — 8 штук примерно по 1 р/штуку
  • Конденсатор 0,1 мкФ 0805 — примерно по 1 р/штуку
  • Резистор 10к 0805 — копейки


Итого по деталям получается 1,5+1,5+0,7=3,7$ и 40+3+8+7+8×1+1=67 р. Всё вместе в пределах 5$ — копейки.

→ Исходный рисунок платы лежит здесь

→ Подготовленные gerber файлы для заказа

Плата подготовлена для первой версии, в которой не было управления RE. Для использования её со второй версией надо разрезать перемычку между выводами 23 и 24 AL422B и бросить проводок от вывода 28 EPM3064 (он выведен на контактную площадку) на вывод 24 AL422B.

При пайке платы не забыть запаять перемычки на питание на обратной стороне платы.

y6hyi6obrqqyyybfvractvmtuqy.jpeg

u1n1jr2zzs7sqfwpzxji2xpg2pg.jpeg

Расчёты


Расчёты нужных параметров довольно сложны.

Дело в том, что в контроллере параллельно выполняются два процесса — загрузка данных следующей строки / индикация уже загруженной строки.

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

→ Для расчёта была сделана Excel-табличка

Исходные данные:

  • CRYSTAL_FRQ (MHz) — частота генератора (50 МГц)
  • PIXEL_COUNT — количество пикселей в строке загрузки. Более детально в разделе коммутации
  • RGB_INPUTS — количество RGB входов, используемых в HUB75E интерфейсе используемой панели. 1 или 2
  • BYTES_PER_PIXEL — байт на пиксель. В нашем случае всегда 3 — RGB888
  • SCAN_LINES — количество линий сканирования в используемой панели. 8/16/32


Подбираемые параметры:

  • PRE_DELAY — задержка от сигнала LAT до включения OE, задаётся в тактах
  • PRESCALER — прескейлер для основного счётчика. То есть если прейскейлер 8 и вес текущего бита 4, то OE будет включено на 8×4 = 32 такта
  • POST_DELAY — минимальная задержка от выключения OE до следующего сигнала LAT, задаётся в тактах


Например, у нас панель 32×32, имеющая 8 линий сканирования и 2 RGB входа. Такая панель имеет два HUB75E разъема, то есть физически это две панели 32×16. Мы соединяем эти панели последовательно, то есть логически эта панель будет выглядеть как 64×16.

PRE_DELAY и POST_DELAY — это интервалы гашения до и после output enable (OE) для того, чтобы мультиплексоры успели переключить выходы и ключи открыться/закрыться. Без них будут «тянучки» от горящих пикселей на соседние строки. Значения подбираются экспериментально для конкретной панели. Обычно достаточно 15 тактов (задаются в тактах).

Тут возникает вопрос выбора prescaler — как его выбрать.

Малое значение прескейлера даёт малое время отображения кадра, но уменьшает общую яркость. Большое значение прескейлера увеличивает время отображения кадра, то есть при переборе приводит в мерцанию экрана.

Попробуем PRESCALER = 1

Получим:

OE_EFFICIENCY — 8,3% то есть панель будет работать всего на 8.3% от возможной максимальной яркости
FRAMES_PER_SECOND — 2034 к/с — зато частота обновления картинки будет огромной — больше 2000 к/с.

Потеря яркости уж очень большая.

Попробуем PRESCALER = 16

Получим:

OE_EFFICIENCY — 72,9% то есть панель будет работать на 72,9% от возможной максимальной яркости
FRAMES_PER_SECOND — 1117 — и частота обновления картинки очень хорошая — больше 1000 к/с.
Ну вот, вполне нормально — эффективность больше 50% вполне нормальная и частота кадров очень хороша.

Общее эмпирическое правило — PRESCALER примерно в 8 раз меньше произведения PIXEL_COUNT*RGB_INPUTS

Ну и дальше считать и проверять.

Коммутация LED панелей


Все панели соединяются последовательно. Схема соединения: сперва справа-налево, потом снизу-вверх. То есть сперва соединяем горизонтали последовательно, потом выход нижнего ряда ко входу второго снизу ряда и т.д. до верхнего ряда.

Контроллер цепляется к правой нижней панели.

Бывают панели, которые имеют по два входных и два выходных разъёма. Такие панели по сути являются просто механической сборкой двух панелей по вертикали. Коммутируются как две независимые панели.

После сборки надо посчитать общую длину цепочки в пикселях — для этого смотрим — сколько всего панелей получилось в цепочке и умножаем это число на ширину панели в пикселях. Это число потом надо будет вбить в значение PIXEL_COUNT при конфигурации CPLD и в калькулятор таймингов.

Прошивка FPGA


Все необходимые файлы лежат на github. Скачивать нужно прямо папкой.

С сайта Altera после регистрации необходимо скачать и установить Quartus II 13.0sp1. Качать надо ИМЕННО ЭТУ версию — более новые версии уже не поддерживают серию MAX3000. Ломать её не надо — достаточно Web edition (free) версии. При скачивании не забудьте поставить галочки на поддержке MAX3000 и Programmer. На всякий случай предупреждаю — пакет большой, порядка двух гигов. Еще понадобится Altera USB Blaster — обычная цена на ali порядка 3$.

Открываем проект al422_bam.qpf. Слева открываем закладку файл и открываем файл al422_bam.v — это основной файл проекта. В нём надо настроить параметры:

Сколько RGB входов на панели — на панелях со входом HUB75 может быть 1 или 2 входа RGB. Выяснить — сколько именно входов можно таким способом — берём количество пикселей на панели по вертикали. Делим её на число линий сканирования (указано в обозначении панели как 8S, например). Делим на количество входных разъёмов (1 или 2). Например — у меня панель 32×32, сканирование 8S и два входных разъёма — 32/8/2=2 — значит два входа RGB.

`define RGB_outs     2


Сколько линий сканирования на панели — так как поддерживается стандарт HUB75E, то может быть до 32х. Количество линий сканирования обычно есть в названии панели в виде 8S/16S/32S соответственно.

Должна быть раскомментирована только одна нужная строка:

`define SCAN_x8      1
//`define SCAN_x16      1
//`define SCAN_x32      1


Общее число пикселей по горизонтали в цепочке. Считаются пиксели во всей цепочке панелей — см. раздел выше «Коммутация LED панелей»

`define PIXEL_COUNT  64


Фазы выходных сигналов. Наиболее типичная конфигурация такая — OE активен по низкому уровню (коммент снят), CLK работает по фронту (коммент стоит), LAT активен по высокому уровню (коммент стоит). Возможны всякие странные варианты. Выяснять какой именно у вас только экспериментальным способом или снятием схемы и поиском даташитов на используемые микросхемы).

//`define LED_LAT_ACTIVE_LOW 1
`define LED_OE_ACTIVE_LOW       1
//`define LED_CLK_ON_FALL               1


Пред и пост задержки сигнала OE относительно LAT и прескейлер для основного счётчика. См. выше.

`define OE_PRESCALER 16
`define OE_PREDELAY     31
`define OE_POSTDELAY    31


Всё, нажимаем ctrl-L — проект компилируется. Если нигде не напортачили — будет несколько warnings, но не должно быть никаких ошибок. Дальше цепляем спаянную плату к USB Blaster, подаём питание на плату. В Quartus идём в tools — programmer. Выбираем в Hardware setup USB-blaster, нажимаем Start. Всё, CPLD запрограммирована.

Микроконтроллерная часть


Выдача данных на контроллер, в общем-то, предельно проста — сбрасываем адрес записи и потом последовательно выдаём байты данных, стробируя их сигналом WCLK. И вроде бы даже банальной ардуинки хватит для работы. Но есть две проблемы:

а) Надо много памяти. Даже небольшая панель 32×32 в режиме RGB888 требует 3кБайта памяти под экранный буфер. Обычные ардуино на базе Atmega328 содержат всего 2 кбайта оперативки. Можно, конечно, использовать плату Mega на базе Atmega2560, которая содержит аж 8 кБайт оперативки, но даже этого мало для панелей нормального размера — панель 128×64 в режиме RGB565 требует 16кБайт памяти.

б) В процессе работы с AL422B вылез не документированный нигде глюк — при записи данных со скоростью меньше 2МБ/сек счётчик адресов работает некорректно и пишет данные «не туда». Возможно это глюк имеющейся у меня партии. Возможно нет. Но этот глюк приходится обходить. Учитывая, что AVR8 работает на 16МГц, то получить с неё данные на нужных скоростях почти нереально.

Предлагаемое решение — уйти на дешёвые платки на базе 32-х битного контроллера STM32F103C8T6. Такая платка стоит на Ali примерно 2.5$ поштучно или около 1.7$ при покупке десятком, то есть дешевле даже Arduino Nano. При этом мы получаем полноценный 32-х битный микроконтроллер, работающий на 72 МГЦ и имеющий 20 кБ оперативной памяти и 64 кБ флеша (сравните с 2кБ/8кБ Atmega328, которая стоит на Nano).

При этом такие платы вполне успешно программируются в среде Arduino. Про это есть неплохая статья на гиктаймс, поэтому я не буду её дублировать. В общем — делайте всё, как описано в статье.

В среде ардуино выбирайте плату Generic STM32F103C, variant STM32F103C8. Данные идут через DMA, поэтому можно использовать любой вариант оптимизации.

Коммутация происходит по следующей схеме:

Жёстко прибиты в библиотеке:
A0…A7 → DI0…DI7 AL422B
B0 → WCLK AL422B
B1 → WRST AL422B

Назначается в скетче на контроллер:
B10 → WE AL422B

Общий провод:
G → GND

Ну и не забудьте подать питание 5В/GND от панели на соответствующие пины контроллера.

Распиновку разъема на контроллере брать со схемы.

nhp7_ygm9hqmkawess06ypt1oy4.jpeg

Программная часть


Так как ставилась задача сделать всё максимально просто и доступно, то весь софт сделан под среду Arduino и оформлен в виде библиотеки LED_PANEL.

Библиотека LED-PANEL активно использует библиотеку Adafruit GFX, поэтому она должна быть инсталлирована.

Я настоятельно рекомендую не ставить библиотеку LED_PANEL в каталог libraries, а оставить её в папке со скетчем. Дело в том, что там много железно привязанных параметров и если вы захотите перенести работу на более «жирный» микроконтроллер, то придётся много чего менять в самом коде.

Инициализация идёт примерно в таком виде:

#include "LED_PANEL.h"
 
#define width 32
#define height 32
#define bpp 3
 
#define scan_lines  8
#define RGB_inputs  2
 
#define we_out_pin PB10
 
LED_PANEL led_panel = LED_PANEL(width, height, bpp, scan_lines, RGB_inputs, we_out_pin);


то есть создаём экземпляр класса LED_PANEL, для которого указываем параметры:

width — общая ширина панели в пикселях (всего)
height — общая высота панели в пикселях (всего)
bpp — байт на пиксель, 3 для RGB888. BAM версия работает только в RGB888
scan_lines — количество линий сканирования — 8/16/32. Должно соответствовать режиму, прошитому в контроллер.
RGB_inputs — количество RGB входов в разъеме HUB75 — ½. Должно соответствовать режиму, прошитому в контроллер.
we_out_pin — пин, к которому подцеплен вывод WE

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

Запуск и очистка экрана в разделе setup:

  led_panel.begin();
  led_panel.clear();


begin инициализирует нужные пины на выход, подключает таймер и DMA
clear очищает буфер

Для рисования можно использовать все стандартные процедуры библиотеки Adafruit GFX — от простейшего drawPixel до вывода текста. Для выдачи нарисованного в буфер используется процедуры:

led_panel.show();


В таком виде show инициирует передачу данных на контроллер по DMA и немедленно возвращает управление. Узнать — закончилась ли передача можно с помощью функции led_panel.OutIsFree () — если она говорит true, то значит передача закончилась. Есть особенность — если вызвать show, когда передача еще не завершилась — она будет просто проигнорирована.

led_panel.show(false);


аналог show (), но если вызвать show (false), а передача еще не завершилась, то процедура подождёт завершения передачи, потом начнёт новую передачу и вернёт управление:

led_panel.show(true);


аналог show (false), но если вызвать show (true), то после начала новой передачи процедура не вернёт управления до завершения передачи.

В общем-то — всё.

pn-aofhwnhuuzszuk_mcf9upqje.jpeg

Некоторые замечания по софту:

а) Гамма-коррекция вводится при пересчёте цвета из RGB565 (который использует библиотека) функцией ExpandColor. Во всех прочих случаях используется линейная трансфер-функция, то есть яркость прямо пропорциональна значению.
б) Софт позволяет подключать несколько LED-контроллеров к одной микроконтроллерной плате. Для этого надо отдать на контроллеры параллельно шину данных, линии RST и CLK. Нужный контроллер выбирается через линию WE. В софте нужно создать отдельный экземпляр класса LED_PANEL для каждого контроллера, при этом у каждого экземпляра при инициализации должны быть указаны разные линии WE (последний параметр).

TO DO


— Разобраться с «наводкой» цветов на соседние ряды. Похоже на плохую разводку самой панели (ключи мусорят), но надо проверить. Только приехала новая панель — буду проверять;
— Сделать новую версию платы — с уже разведённым RE и добавлением выходных преобразователей уровней в 5В;
— Сделать класс META_LED_PANEL, который позволит объединять несколько LED_PANEL в один виртуальный экран — это даст возможность создавать очень большие экраны с несколькими контроллерами;
— В перспективе уйти на более мощную серию CPLD, например CycloneIV. Это бы существенно расширило возможности при сохранении невысокой стоимости (EP4CE6E22 стоит у китайцев порядка 5$ штука, при этом там в 100 раз больше макроячеек и порядка 32 кБ встроенной памяти). Но этим я буду заниматься когда-нибудь потом. Если захочу. Так как подобные разработки отнимают уж слишком много времени.

© Geektimes