Pull to refresh

Как сделать очень заметный информер из светодиодного модуля для наружной рекламы и Arduino

Reading time 8 min
Views 32K

Как сделать очень заметный информер из светодиодного модуля P10 и Arduino.


Цель: Быстро подключить большую светодиодную матрицу P10 (16х32см) к ПК или другому устройству, превратив все это в очень заметный и яркий информер с динамической сменой выводимой информации. Применений такой вещи можно найти массу и как показывает практика она очень привлекает внимание. Вы только представьте, теперь точно все будут знать что у вас работает кондиционер и дверь предполагается закрывать!



Засветка не от окна, а от панели ;)


Однажды посмотрев на унылые матрицы 8х8 светодиодов для Arduino и на их стоимость мне стало грустно. Было решено капнуть в сторону готовых бегущих строк для наружной рекламы и каково было мое удивление когда все они оказались стандартные и все как одна не знали ничего про динамическое обновление информации через внешний порт. Копнув глубже обнаружилось что во всей подобной продукции используются типовые светодиодные модули (светодиодные матрицы).


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




Эти светодиодных модулей довольно дешевы. Порядка 6$, цена зависит от типоразмера и цветности. С контроллерами сложнее. Самые простые по цене сравнимы с одной светодиодной панелью. Однако большинство из них "заточено" на работу в режиме демонстрации заранее заготовленных "презентаций" и не имеют возможности динамически менять выводимую информацию. Признаюсь что только бегло ознакомился с функционалом самых простых контроллеров, но этого оказалось достаточно чтобы понять — дешевле и быстрее сделать необходимое на универсальном контроллере Arduino. Это позволит подключить совершенно спокойно несколько модулей. Но мы начнем с одного.


Светодиодный модуль (Р10) выглядит примерно так:



http://digital-wizard.net/avr_projects/p10_led_display_panel_interface


По сути это просто матрица светодиодов которые припаяны к выходам сдвиговых регистров, не будем вдаваться в дебри схемотехники, модули все стандартные и отличаются лишь размерами. Р10 означает что между двумя соседними диодами 10мм. Бываю панели одноцветные, двухцветные и даже RGB. Для интересующихся подробностями тут уже есть аналогичная статья, но с уклоном в более низкий уровень.


Аппаратная часть


А для тех кто хочет быстрее получить результат, который можно "пощупать", потребуется:


  1. Одна светодиодная матрица.
  2. Arduino. (Я использовал mini, но удобнее будет nano чтобы не использовать доп. переходники для связи с ПК).
  3. Блок питания на 5В/3А. Матрица прожорлива, если зажечь все диоды то питания надо много.
  4. Шлейф подключения. (Обычно он идет в комплекте с матрицей.)
  5. Желание довести дело до конца.

Будем делать монолитную конструкцию которую достаточно лишь воткнуть в розетку и ПК чтобы отображать нашу драгоценную информацию (например курс биткоина).


Необходимо взять шлейф от матрицы, разрезав его напополам припаять по простой схеме к Arduino.



Если использовать Arduino Mini или UNO то паять надо к соответствующим пинам по аналогии.


Если по какой-то причине у вас не оказалось шлейфа то его можно заменить на MIDI кабель которым в старых звуковых картах подключается к самой плате MIDI разъем (вариант для старожил) или заменить на два штекера Dupont (мама) по 8 контактов. В принципе разъем стандартный, ищется довольно легко.



У меня же за 10 минут получилась следующая конструкция, болтается еще дополнительный USBtoUART переходник которого у вас не будет если использовать не Arduino Mini.




Аппаратная часть готова, необходимо только подключить дополнительный БП на 5В/3А для питания матрицы. Для этого на ней есть отдельный разъем, плюс к плюсу, минус к минусу. Наоборот можно, но работать не будет. Хотя странно, так как всем известно что электроны, по обыкновению, текут от плюса к минусу. А если учесть что электроны отрицательно заряженные то вообще не понятно почему они текут к отрицательному полюсу… ;) Ерунды навыдумывали короче.



Я надеюсь что вы подключите питание более надежно, а мой способ отлично годится только если у вас много запасных БП. Ардуину можно прикрутить болтом (не саморезом!) к самой панели, в панели есть монтажные резьбовые отверстия.


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


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


Если вы подумали что нужно будет разбираться как программировать эту штуку из трех букв (SPI интерфейс) то могу вас разочаровать — все значительно проще. Как всегда все велосипеды придуманы до нас и не единожды. Собственно как и эта статья. Есть готовые библиотеки DMD и DMD2 и рисование сводится к тому что в скетче нужно просто указать что, как и куда выводить, все — Профит!


Немного поигравшись с формой квадрата этими библиотеками я пришел к выводу что скролинг текста во DMD2 выглядит "печально", хотя там есть больше возможностей и управление яркостью. В итоге остановился на первой и вспомнив что "ты ж программист" добавил в нее примитивное управление яркостью. Тут стоит упомянуть что светят эти матрицы очень ярко. К концу отладки выжжет роговицу.


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


Для большей наглядности я добавил датчик температуры 18в20. Если вы такой не подключите то просто не будет выводится температура. У меня по задумке там будет выводится некий вес в граммах, вы же можете выводить просто число. Чтобы изменить текст который прокручивается снизу нужно его просто "вгрузить" в COM порт Arduino используя специальный формат, некое подобие ESC последовательности. Прилагается скрипт на Python который просто посылает на панель текущее время.


Формат управляющих команд:


  1. 0x1B (ESC) + 0x40 ("@") // Опознание, возвращает строку ID устройства «P10_DISPLAY_V01\r\n»
  2. 0xOC (CLR) // Очистка экрана.
  3. 0x1F (US) + 0x58 («X») + 0x80 // Задать якрость экрана, 0x80 максимальная яркость.
  4. 0x1F (US) + 0x52 («B») + Строка // Задать нижнюю строку которая скролится.
  5. 0x1F (US) + 0x0A («LF») + 0x31, 0x32, 0x33, 0x34, 0x35 // Передать пятизначно число отображаемое вверху «12345»

Я не нашел готового и пришлось нарисовать русский шрифт 5*7. Наверно велосипед, но разве мы делаем всегда все рационально?


Скетч:


Код
//====================== OneWire ====================================
#include <OneWire.h>
OneWire ds2  (2);
String res = "";
byte type_s;
byte data[12];
byte addr_ds2[8];
bool temp_sensor_2_found = false;
float temp1 = 999;
//===================================================================

#include <SPI.h>        
#include "DMD.h"       
#include "TimerOne.h"   
#include "SystemFont5x7rus.h"

#define CLR 0x0C
#define US 0x1F
#define ESC 0x1B
#define LF 0x0A

static const char ID[] = "P10_DISPLAY_V01\r\n";    // Device ID

#define DISPLAYS_ACROSS 1
#define DISPLAYS_DOWN 1
DMD dmd(DISPLAYS_ACROSS, DISPLAYS_DOWN); // Конфигурация экрана, панелей может быть не одна.
#define max_char1 6
#define max_char2 176
unsigned char message1[max_char1] = {0x20,0x30,0x30,0x30,0x30,0x00};    // stores you message
unsigned char message2[max_char2];    // stores you message

unsigned char r_char;               
byte index1 = 4;            
byte index2 = 176;            
int i;
long scrollSpeed = 25;            

char test[] = { 0x84,0x80,0x20,0x87,0x84,0x90,0x80,0x82,0x91,0x92,0x82,0x93,0x85,0x92,0x20,0x92,
                0x8E,0x20,0x81,0x8B,0x80,0x83,0x8E,0x84,0x80,0x90,0x9F,0x20,0x97,0x85,0x8C,0x93,
                0x20,0x8C,0x9B,0x20,0x8D,0x85,0x91,0x8C,0x8E,0x92,0x90,0x9F,0x20,0x8D,0x88,0x20,
                0x8D,0x80,0x20,0x97,0x92,0x8E,0x20,0x88,0x20,0x82,0x8E,0x8F,0x90,0x85,0x8A,0x88,
                0x20,0x82,0x91,0x85,0x8C,0x93,0x21,0x00};

//=============================================================================
bool get_sensor(byte addr[8], OneWire ds) {

  // Ищем адрес датчика
  if  (! ds.search  (addr)) {
    ds.reset_search  ();
    delay  (250);
    return false;
  }

  // Проверяем не было ли помех при передаче
  if (OneWire::crc8(addr, 7) != addr[7])
  {
    Serial.println("get_sensor:CRC is not valid!");
    return false;
  }

  // Определяем серию датчика
  switch (addr[0])
  {
    case 0x10:
      //Chip = DS18S20
      type_s = 1;
      break;
    case 0x28:
      //Chip = DS18B20
      type_s = 0;
      break;
    case 0x22:
      //Chip = DS1822
      type_s = 0;
      break;
    default:
      Serial.println("get_sensor:Device is not a DS18x20 family device.");
      return false;
  }
  return true;
}

//=============================================================================

double get_temp(byte addr[8], OneWire ds) {

  double celsius;

  ds.reset  ();
  ds.select  (addr);   // Выбираем адрес
  ds.write  (0x44, 1); // Производим замер, в режиме паразитного питания
  delay  (750); 
  ds.reset  ();
  ds.select  (addr);
  ds.write  (0xBE); // Считываем оперативную память датчика

  for  (int i = 0; i < 9; i++) { // Заполняем массив считанными данными
    data[i] = ds.read  ();
  }

  // Данные о температуре содержатся в первых двух байтах, переведем их в одно значение и преобразуем в шестнадцатиразрядное число
  int raw =  (data[1] << 8) | data[0];
  if  (type_s) {
    raw = raw << 3; // 9 bit resolution default
    if  (data[7] == 0x10) {
      raw =  (raw & 0xFFF0) + 12 - data[6];
    }
  }
  else
  {
    byte cfg =  (data[4] & 0x60);
    if  (cfg == 0x00) raw = raw << 3; 
    else if  (cfg == 0x20) raw = raw << 2; 
    else if  (cfg == 0x40) raw = raw << 1; 
  }

  celsius =  (double)raw / 16.0;
  return celsius;
};

//=============================================================================
void massage2BufClear ()
{
  for(i=0; i<max_char2; i++)
        {
            message2[i] = '\0';
        }        
        index2=0;
}
//--------------------------------------------------------------
void massage1BufClear ()
{

        message1[0] = 0x20;
        message1[1] = 0x30;
        message1[2] = 0x30;
        message1[3] = 0x30;
        message1[4] = 0x30;
        message1[5] = 0x00;    
        index1=0;        
}

//--------------------------------------------------------------
void massage1Normalization ()
{

  char buf[5];

  buf[0] = 0x20;
  buf[1] = 0x30;
  buf[2] = 0x30;
  buf[3] = 0x30;
  buf[4] = 0x30;

  int char_count = index1;

   for(i = char_count - 1; i >= 0; i--)
   {
      buf[i] = message1[i];      
   }

   massage1BufClear();

   for(i = char_count - 1; i >= 0; i--)
   {
      message1[i+5-char_count] = buf[i];      
   }        
}
//------------------------------------------------------------------
void ScanDMD()
{ 
  dmd.scanDisplayBySPI();
}
//------------------------------------------------------------------
void setup(void)
{
   Timer1.initialize( 500 );          
   Timer1.attachInterrupt( ScanDMD );   
   dmd.clearScreen( true );   
   Serial.begin(9600);
   strcpy(message2,test);
   dmd.brightness = 70;
   temp_sensor_2_found = get_sensor(addr_ds2, ds2);
   dmd.selectFont(SystemFont5x7);
}
//------------------------------------------------------------------
void loop(void)
{    
   dmd.drawMarquee(message2 ,index2,(32*DISPLAYS_ACROSS)-1 ,8);

    if (temp_sensor_2_found)                  // !!!!!!!
    {                                         // !!!!!!!
     res = String(get_temp(addr_ds2, ds2));   // !!!!!!!
     res.toCharArray(message1, 5);            // !!!!!!!
     message1[4] = 0xF8;                      // !!!!!!!
    }                                         // !!!!!!!

   long start=millis();
   long timer=start;
   boolean ret=false; 
   while(!ret)
   {
     if ((timer + scrollSpeed) < millis()) {
        dmd.drawFilledBox( 0,0,31,7, GRAPHICS_INVERSE);
        ret=dmd.stepMarquee(-1,0);
        timer=millis();
        dmd.drawString(  1,  0, message1, 5, GRAPHICS_NORMAL );      
        if(Serial.available())
          break;        
     }
   }
}
//-----------------------------------------------------------------------------------
void serialEvent() {

      delay(25); // Wait all data
      r_char = Serial.read();

      if(r_char < 0x20)
      {
          switch (r_char) 
          {
              case ESC:
                r_char = Serial.read();
                if(r_char=='@')
                {
                  Serial.write(ID);
                }
                break;             

              case CLR:
                massage1BufClear();
                massage2BufClear();              
                break;

              case US:
                r_char = Serial.read();                
                if(r_char=='X')
                {
                  dmd.brightness = Serial.read();   
                }       

                if(r_char=='B'){
                  massage2BufClear();

                  while(Serial.available() > 0)
                  {
                    r_char = Serial.read();
                    if(index2 < (max_char2-1)) 
                      {         
                        if(r_char > 0x1F)
                        {
                            message2[index2] = r_char;     
                            index2++;                     
                        }
                      }               
                  }
                }

                  if(r_char==LF){
                          massage1BufClear();

                          while(Serial.available() > 0)
                                  {
                                    r_char = Serial.read();         
                                    if(index1 < (max_char1-1)) 
                                      {                                 
                                        if((r_char > 0x29) and (r_char < 0x3A))
                                        {
                                            message1[index1] = r_char;     
                                            index1++;                     
                                        }
                                      }

                                  }
                          massage1Normalization();   
                }               
                dmd.drawFilledBox( 0,8,31,15, GRAPHICS_INVERSE);
                break;

              default:
                break;
            }       
          }

   while (Serial.available()) Serial.read();
   dmd.writePixel(0,0,GRAPHICS_NORMAL,1);
}

Работа на полной яркости:



Работа с человеческой яркостью:



Работа с динамической подгрузкой времени с ПК при помощи скрипта на Python:



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


Стоит отметить что совершенно свободно можно соеденить несколько модулей последовательно и всего лишь указав это в скетче получить экран большего размера. Ограничения начинаются лишь после определенного количества панелей и связанны они с ограниченной производительностью Arduino и ее SPI интерфейса. Спасибо ElectricFromUfa за подробнейшее и развернутый обзор панелей и не только!


Возможно в будущем все тоже самое я проделают на STM32F103C8T6 и на ESP-12F, там все должно шевелится побыстрее.


Ссылки:


1. Ссылка на архив со скетчем и сопутствующим.
2. Еще ссылка на BitBucket


Tags:
Hubs:
+9
Comments 27
Comments Comments 27

Articles