По роду деятельности, мне часто приходится разрабатывать всевозможные системы управления световыми эффектами. В случаях, когда для заказчика избыточен функционал представленных на рынке контроллеров, приходится под конкретный объект изобретать простенький контроллер практически на коленке. Задача каждый раз уникальна, начиная от одного канала, пульсирующего словно сердце до управления «гирляндой» декодеров работающих по протоколу DMX-512 или на микросхемах WS28хх:

Простейший DMX декодер


Стоят копейки у китайцев. Я очень люблю эти декодеры. С их помощью легко можно масштабировать систему от трех до 512 каналов.

Для меня самый удобный вариант всем известная плата Arduino. Очень удобно держать в ящике десяток-другой клонов Arduino Pro Mini и по мере надобности использовать их в подобных проектах. Люблю его за то что на его базе можно быстро собрать достаточно компактное устройство и стоят эти клоны в Китае, примерно столько же, как и голая Atmaga328.

И вот при разработке эффектов для очередного контроллера на 16 каналов, работающего по протоколу DMX-512, я окончательно замучился раскладывать на столе паутину проводов на скрутках ради того, что бы один раз отладить эффекты и снова все это разобрать. Я озадачился поиском приборов для анализа DMX пакетов, но оказалось, что это весьма редкие и довольно дорогостоящие приборы. Это и подтолкнуло меня к созданию собственного DMX-тестера.

Изначально я решил, что нужен прибор, который будет уметь отображать уровни каналов в реальном времени и этого будет достаточно, но немного пораздумав, я определил следующий список задач:

  • Отображение уровней каналов в режиме реального времени в относительных единицах;
  • Отображение общего количества каналов, на которые контроллер выдает данные;
  • Отображение уровня одного выбранного канала в абсолютных единицах (0-255);

Выводить уровни каналов, было принято в виде столбиков, т. е. если использовать символьный LCD-дисплей 16х2, то в одной строке уже можно наблюдать 16 каналов одновременно. Т.к. каналов может быть до 512, то хорошо бы было иметь возможность прокрутки отображаемого диапазона каналов.

Ну раз задача поставлена, перейдем к железу. В основе, как я уже писал, у меня клон Arduino Pro Mini:

Данные мы будем получать через простейший переходник TTL to RS-485, основанный на MAX485.
"

Ну и выводить информацию будем на LCD-дисплей 16х2 (сразу оговорюсь, что в момент разработки уже заказал аналогичный дисплей 40х2 который встанет на свое место в конечном устройстве). Управлять всем этим буду посредством клавиатуры, описанной здесь товарищем kumbr_87.

Сказано — сделано.

На макетке собрал прототип


Написал скетч
#include <Wire.h>
#include <LiquidCrystal.h>
#include <Conceptinetics.h>

#define DMX_SLAVE_CHANNELS   512

#define LCD_W   16  //количество выводимых на дисплей каналов (по идее соответствует символьной ширине дисплея)

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

DMX_Slave dmx_slave ( DMX_SLAVE_CHANNELS );

unsigned long       lastFrameReceivedTime = 0; //Время последнего получения пакета
unsigned long       lastFrameTranceivedTime = 0;  // Время последнего отображенного пакета

byte qa[8] = 
{
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B11111
};
byte ws[8] =
{
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B11111,
  B11111
};
byte ed[8] =
{
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B11111,
  B11111,
  B11111
};
byte rf[8] =
{
  B00000,
  B00000,
  B00000,
  B00000,
  B11111,
  B11111,
  B11111,
  B11111
};
byte tg[8] =
{
  B00000,
  B00000,
  B00000,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};
byte yh[8] =
{
  B00000,
  B00000,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};
byte uj[8] =
{
  B00000,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};
byte ik[8] =
{
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};

byte outAr [LCD_W]; // Значения каналов, выводимые на дисплей
unsigned short chRx; // количество принятых каналов 

#define KEY_BUTTON_1_PIN A2  //пин к которому подключена клавиатура
unsigned int  KeyButton1Value=0;  //значение с клавиатуры
unsigned long KeyButton1TimePress=0;  //последнее время когда не было нажатых кнопок
unsigned long KeyButton1Latency=100;  //задержка перед считыванием состояния панели управления после нажатия
unsigned int  KeyButton1WasChecked=0;  //метка означающая что нажатие кнопки было обработано
unsigned long KeyButton1RepeatLatency=1500;  //время после которого удерживание кнопки начинает засчитываться как многократные быстрые нажатия
unsigned long KeyButton1RepeatTimePress=0;  //вспомогательная переменная для обработки повторных нажатий
unsigned long KeyButton1TimeFromPress=0;  //переменная для хранения времени между временем когда не было зажатых кнопок и временем проверки
unsigned long KeyBoardTime1=0;                //
unsigned long KeyBoardTime2=0;                // Переменные для обработки времени для обработки событий клавиатуры
unsigned long KeyBoardTimeInterval=25;        //

byte start = 0;  // +1 номер канала, который выводится на экран первым

void setup() {
  lcd.begin(LCD_W, 2);
  dmx_slave.enable ();  
  dmx_slave.setStartAddress (1);
  dmx_slave.onReceiveComplete ( OnFrameReceiveComplete );
 
  lcd.createChar(0,qa);
  lcd.createChar(1,ws);
  lcd.createChar(2,ed);
  lcd.createChar(3,rf);
  lcd.createChar(4,tg);
  lcd.createChar(5,yh);
  lcd.createChar(6,uj);
  lcd.createChar(7,ik);
  
  pinMode (KEY_BUTTON_1_PIN, INPUT);
  pinMode (10, OUTPUT);
  pinMode (9, OUTPUT);
  digitalWrite(9, LOW);
}

void loop() 
{
  //проверка таймера для обработки нажатий клавиатуры
  KeyBoardTime2=millis();
  if ((KeyBoardTime2-KeyBoardTime1)>KeyBoardTimeInterval) 
    {
      KeyBoardTime1=KeyBoardTime2;
      KeyBoardCalculate();
    }
    
  if (lastFrameReceivedTime > lastFrameTranceivedTime){ //если получен новый пакет
    printLevel (outAr);  // Вывод значений на дисплей
    lastFrameTranceivedTime = millis();
  }
  else if ((lastFrameReceivedTime==0 && lastFrameTranceivedTime ==0)||(KeyBoardTime2-lastFrameReceivedTime>2000)) {
    lcd.clear();
    delay (500);
    lcd.setCursor(0, 0);
    lcd.print("NO SIGNAL");
    delay (500);
  }
  
}

void OnFrameReceiveComplete (unsigned short channelsReceived) // функция, вызываемая после получения очередного пакета
{
  chRx = channelsReceived;  // количество калалов, на которые пришли данные в пакете
  for (byte i=0; i<LCD_W; i++){ // цикл запиди уровней каналов в массив для отображения на дисплее
      outAr[i]=dmx_slave.getChannelValue (i+start+1);
    }
  lastFrameReceivedTime = millis();
}

void printLevel(byte lv[LCD_W])  // функция вывода значений на дисплей
{
  byte dispLv[LCD_W];
  for (byte i=0; i<LCD_W; i++){
    switch (lv[i]/32) {    //// добавить регулировку чувствительности
      case 0:
        dispLv[i]=0;
        break;
      case 1:
        dispLv[i]=1;
        break;
      case 2:
        dispLv[i]=2;
        break;
      case 3:
        dispLv[i]=3;
        break;
      case 4:
        dispLv[i]=4;
        break;
      case 5:
        dispLv[i]=5;
        break;
      case 6:
        dispLv[i]=6;
        break;
      case 7:
        dispLv[i]=7;
        break;
    }
  }
  lcd.setCursor(0, 0);
  for (byte i=0; i<LCD_W; i++){ //
    lcd.write(dispLv[i]);       // Вывод уровней на дисплей
  }                             //
  lcd.setCursor(0, 1);
  lcd.print("^");                                  //
  if ((start+1)<10) {                              //
    lcd.print((start+1));                          //
    lcd.print("  ");                               // Отображение номера первого отображаемого канала
  } else if ((start+1)<100 && (start+1)>9) {       //
           lcd.print((start+1));                   //
           lcd.print(" ");                         //
  } else lcd.print((start+1));                     //
  lcd.setCursor(5, 1);
  lcd.print("V:");                       //
  if (lv[0]<10) {                        //
    lcd.print("00");                     //
    lcd.print(lv[0]);                    // Отображение уровня первого отображаемого канала в абсолютных единицах
  } else if (lv[0]<100 && lv[0]>9) {     //
           lcd.print("0");               //
           lcd.print(lv[0]);             //
  } else lcd.print(lv[0]);               //
  lcd.setCursor(11, 1);
  lcd.print("T:");              // Отображение общего количества полученных каналов
  lcd.print(chRx);              //
}


void ButtonPress() // Распознаем, какая кнопка нажата
{
  if ((KeyButton1Value>200) and (KeyButton1Value<500))
    {
      if((start) < (chRx-LCD_W) && chRx>LCD_W) start++;   // обработка нажатия первой кнопки
    }
  if ((KeyButton1Value>500) and (KeyButton1Value<1000)) 
    {
       if(start > 0) start--; // Обработка нажатия второй кнопки
    }
}

void KeyBoardCalculate()
{
  //Часть отработки нажатия клавиши
  KeyButton1Value=analogRead(KEY_BUTTON_1_PIN); 
  //если сигнал с кнопки нулевой то обнуляем метку обработки нажатия
  if ((KeyButton1Value<=50) or (KeyButton1Value>=1000))
    {
      //Сохраняем время последнего сигнала без нажатой кнопки
      KeyButton1TimePress=millis(); 
      KeyButton1WasChecked=0;
      KeyButton1RepeatTimePress=0;
    } 
        
  KeyButton1TimeFromPress=millis()-KeyButton1TimePress;
  //исключаем шумы
  if ((KeyButton1Value>50) and (KeyButton1Value<1000))
    {
      //отработка первого нажатия
      if ( ((KeyButton1TimeFromPress)>KeyButton1Latency) and (KeyButton1WasChecked==0))
        {
           KeyButton1Value=analogRead(KEY_BUTTON_1_PIN);  
           ButtonPress();
           KeyButton1WasChecked=1;
           KeyButton1RepeatTimePress=0;
        }
      
      //отработка повторных нажатий  
      if ( ((KeyButton1TimeFromPress)>(KeyButton1RepeatLatency+KeyButton1RepeatTimePress)) and (KeyButton1WasChecked==1))
        {
           KeyButton1Value=analogRead(KEY_BUTTON_1_PIN);  
           ButtonPress();
           KeyButton1RepeatTimePress=KeyButton1RepeatTimePress+100;
        }
    }
  
}


И как ни странно, все заработало! Столбики «прыгали» в соответствии с получаемыми данными. К этому времени, как раз пришел дисплей 40х2. Подключил его, переделал скетч под дисплей на 40 символов, залил, и тут произошло необъяснимое.

В момент запуска микроконтроллера в первом отображаемом кадре вся информация верна, при отображении последующих кадров, arduino куда-то съедает несколько каналов. Т.е. если контроллер выдает данные на 60 каналов, то в момент включения или перезагрузки arduino, отображается общее количество каналов 60 и первый канал на дисплее является действительно первым. Но сразу после смены кадров, отображается количество кадров 57, а первым отображается, в реальности пятый. Пробовал еще загонять сигнал на 30 каналов — тоже самое, только сначала все правильно, а потом показывает общее количество каналов 29, а на месте первого показывает вообще непонятно что.

Все остальное работает без проблем — прокрутка листает. Может кто-нибудь свежим взглядом увидит… Да, кстати, если выдавать сигнал на <17 каналов, то отображается все корректно.

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

Комментарии (2)


  1. vp7
    14.11.2015 15:27

    В момент запуска микроконтроллера в первом отображаемом кадре вся информация верна, при отображении последующих кадров, arduino куда-то съедает несколько каналов.

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

    Для проверки можно попробовать в основном цикле дважды вызывать
    printLevel (outAr); // Вывод значений на дисплей

    Если и тут начнутся потери, то получаем явный конфликт по таймингу библиотеки DMX_Slave и функции вывода данных.
    Уж не знаю как именно работает DMX_Slave (использует ли она прерывания или отрабатывает по завершении каждого цикла void loop()), но можно попробовать начать с вывода за один цикл не более 16 значений (т.е. на 40 строчный дисплей данные будут выводиться за 3 цикла loop()).


    1. siplix
      16.11.2015 10:43

      Да, действительно. Посмотрел логическим анализатором — при ширине выводимой строки 40 символов, вывод на дисплей занимает 30,21мс, а на получение одного пакета данных — 26,43. Получается без изменений кода, удается вывести строку около 30 символов…