воскресенье, 7 мая 2017 г.

Проект часов для телевидения на основе Arduino

Часы на ТВ.


Часы на телевидении, являются вторым по важности устройством, необходимым для точности вещания, а так-же синхронной работы нескольких подразделений. Особенностью таких часов является их точность и связность.
По сути часы на ТВ - это сеть, через которую таблички с цифровыми дисплеями, получают информацию о времени от центрального устройства, которое в свою очередь может синхронизироваться от внешних источников, типа GPS, NTP, ну или на худой конец - устанавливаться в ручную.
В основном, существующие на рынке часы основаны на LTC - по сути - кодированный 80-ти битный SMPTE таймкод, передающийся в виде симметричного аудио сигнала с частотой 48 кГц. Но не будем углубляться.
К этому проекту можно приделать и NTP сервер точного времени с GPS...

Пролог.

При создании проекта телеканала, передо мной встала задача выбора часов для компании. По скольку всё оборудование канала (вещательное) располагается в серверной - в одной комнате, то нет задачи выносить LTC за пределы серверной, как и нет задачи пользоваться LTC, так-как единственным устройством имеющим LTC вход/выход, является видеомикшер.
По этой причине отпадает надобность LTC в принципе. В данном проекте, часы больше нужны для визуального контроля времени. И тем более, существующие на рынке LTC часы стоят неоправданно дорого.
А что тогда использовать?
Первым в голову пришло применение микроконтроллера на базе Arduino. Дёшево и достаточно просто. Так и было принято решение о разработке. Надо заметить, что на данный момент, проект почти завершён, и полностью работоспособен. Но прийти к результату было не совсем просто.  
Первоначально пришла в голову мысль - передавать данные между микроконтроллерами по SPI, но дальность связи при применении такого интерфейса - маловата. В теории длина всей трассы (провода) между дисплеями - 100 - 200 метров. Нужно было применить что-то универсальное и доступное к подключению ардуины, в том числе доступное в продаже. Самым подходящим оказался промышленный интерфейс CAN, и дисплеем - MAX7219. С этого и началась разработка.

Разработка

Первая задача сводилась к синхронизации часов Arduino и ПК (эфирной машины) через USB и выводу времени на цифровой дисплей MAX7219. За основу была взята плата Arduino Nano. 
Для ардуино есть все необходимые примеры, при установке правильных библиотек.
И так!
Подгружаем в скетч ардуины библиотеку времени, библиотеку MAX7219, запускаем скетч в Processing, нажимаем на окошко Processing-a, и тут-же видим синхронизированное время. Даже с миллисекундами! 
Таким образом мы видим на дисплее MAX7219 внутреннее время ардуины, которое было синхронизировано в момент нажатия клавиши мыши.
Так,- да не так! 
Первое что бросилось в глаза - миллисекундная задержка времени от системных часов. Сначала возникло предположение что часы отображаемые на экране монитора отображаются с задержкой в связи с задержкой преобразования времени в картинку. Можно было бы этим пренебречь, до той поры пока клавиша мыши не была нажата ближе к концу окончания системной секунды, и задержка стала очевидной.
Загвоздка кроется в скетче Processing-a. 
При  нажатии кнопки мыши для синхронизации, Processing отправляет синхронизационный пакет времени в ардуину в формате Unix time. А это означает - что в момент времени, например в 11:59:59:995 была нажата клавиша мыши, и Processing отправит в Arduino время 11:59:59. Всё правильно. Только без миллисекунд. И визуально задержка будет почти секунду!
Пришлось исправить скетч Processing-a. По скольку миллисекунды в ардуину не передаются, а точность отображения времени от эфирного компьютера критична, было принято решение поправить скетч таким образом, чтобы после нажатия кнопки мыши (для синхронизации) скрипт ждал пока в системе не изменится секунда, и как только она изменяется, скрипт отправляет текущее время на Arduino. И это оказалось очень эффективно! Так-же в скрипт добавил индикацию времени, для наглядности.

 Далее рабочие скетчи:




Скетч Processing - TimeRTCSet.pde :

/**
 * SyncArduinoClock. 
 *
 * portIndex must be set to the port connected to the Arduino
 * 
 * The current time is sent in response to request message from Arduino 
 * or by clicking the display window 
 *
 * The time message is 11 ASCII text characters; a header (the letter 'T')
 * followed by the ten digit system time (unix time)
 */


import processing.serial.*;

import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;

public static final short portIndex = 0;  // select the com port, 0 is the first port

public static final String TIME_HEADER = "T"; //header for arduino serial time message 
public static final char TIME_REQUEST = 7;  // ASCII bell character 
public static final char LF = 10;     // ASCII linefeed
public static final char CR = 13;     // ASCII linefeed
Serial myPort;     // Create object from Serial class

void setup() {  

  size(200, 200);
  println(Serial.list());
  println(" Connecting to -> " + Serial.list()[portIndex]);
  myPort = new Serial(this,Serial.list()[portIndex], 115200);
  println(getTimeNow());
}

void draw()

{
  background(200);
  textSize(20);
  textAlign(CENTER);
  fill(0);
  int m=minute(),s=second(),h=hour();

float sy=map(millis()%1000,0,1000,0,120);

text(h,50,30);
text(m,100,30);
text(s,150,30);
  
  text("Click to send\nTime Sync\n", 0, 75, 200, 175);
  if ( myPort.available() > 0) {  // If data is available,
    char val = char(myPort.read());         // read it and store it in val
    if(val == TIME_REQUEST){
       long t = getTimeNow();
       sendTimeMessage(TIME_HEADER, t);   
    }
    else
    { 
       if(val == LF)
           ; //igonore
       else if(val == CR)           
         println();
       else  
         print(val); // echo everying but time request
    }
  }
 if(millis()%1000==0){
     sendTimeMessage( TIME_HEADER, getTimeNow());
 }
   
}

void mousePressed() {  

  
  
  long millis = 1000 - System.currentTimeMillis() % 1000;

  try {

  Thread.sleep(millis);
  }
  catch (Exception e){/*Ignore*/}
  
  sendTimeMessage( TIME_HEADER, getTimeNow());
   
}


void sendTimeMessage(String header, long time) {  

  String timeStr = String.valueOf(time);  
  myPort.write(header);  // send header and time to arduino
  myPort.write(timeStr); 
  myPort.write('\n');  
}

long getTimeNow(){

  // java time is in ms, we want secs    
  Date d = new Date();
  Calendar cal = new GregorianCalendar();
  long current = d.getTime()/1000;
  long timezone = cal.get(cal.ZONE_OFFSET)/1000;
  long daylight = cal.get(cal.DST_OFFSET)/1000;
  return current + timezone + daylight; 
}


Скетч Arduino - NanoRtcLcdSet.ino - центральный модуль :

/*

 * Этот скрипт выполняет функции - синхронизация времени с ПК, запись времени в RTC модуль, вывод времени на дисплей max7219, 
 * сравнение времени ардуины и RTC каждую секунду (коррекция), формирование из времени CAN сообщения и отправка в CAN шину.
 * TimeRTCSet.pde
 * example code illustrating Time library with Real Time Clock.
 *
 * RTC clock is set in response to serial port time message 
 * A Processing example sketch to set the time is included in the download
 * On Linux, you can use "date +T%s > /dev/ttyACM0" (UTC time zone)
 */
#include <mcp_can.h>
#include <SPI.h>
#include <TimeLib.h>
#include <Wire.h>
#include <DS3231RTC.h>  // a basic DS1307 library that returns time as a time_t
#include "LedControl.h" // подключается библиотека MAX7219

// следующий абзац - для диспля.. the cs pin of the version after v1.1 is default to D9

// v0.9b and v1.0 is default D10
const int SPI_CS_PIN = 10;
const int LED=8;
boolean ledON=1;
LedControl lc=LedControl(6,8,9,1); //Tells LedControl where our hardware is connected.
//(6,8,9,1) for nano with busy spi bus
int hourTen; //tens of hours
int hourUnit; //units of hours
int minTen; // you get the idea..
int minUnit;
int secTen;
int secUnit;
int msTen;
int msUnit;
int sec;
int rtcSec;
unsigned long timer =0;

MCP_CAN CAN(SPI_CS_PIN);                                    // Set CS pin


#define TIME_HEADER  "T"   // Header tag for serial time sync message

#define TIME_REQUEST  7    // ASCII bell character requests a time sync message 

void setup()  {

  Serial.begin(115200);
   pinMode(LED,OUTPUT); //для дисплея
//  while (!Serial) ; // Needed for Leonardo only
  setSyncProvider(RTC.get);   // the function to get the time from the RTC
  if (timeStatus() != timeSet) 
     Serial.println("Unable to sync with the RTC");
  else
     Serial.println("RTC has set the system time");      
//setup для can шины
       while (CAN_OK != CAN.begin(CAN_500KBPS))              // init can bus : baudrate = 500k
    {
        Serial.println("CAN BUS Shield init fail");
        Serial.println(" Init CAN BUS Shield again");
        delay(100);
    }
    Serial.println("CAN BUS Shield init ok!");

             // setup для дисплея MAX7219

  lc.shutdown(0,false); // Wake up the MAX 72xx controller 
  lc.setIntensity(0,8); // Set the display brightness
  lc.clearDisplay(0); //Clear the display
}

void loop()

{
  //ожидаем сообщения синхронизации времени из COM порта
  if (Serial.available()) {
    time_t t = processSyncMessage();
    if (t != 0) {
      RTC.set(t);   // set the RTC and the system time to the received value
      setTime(t);   // устанавливаем время в ардуину   
    }
  }  
    
  rtcSec = RTC.get(); // зпаисать в переменную rtcSec, время из RTC модуля - с точностью до секунды
  delay(1) ;// ждём 1 мс 
    displayTime(); // показать время ардуины на светодиодном дисплее - это тоже занимает время
  if (rtcSec != RTC.get())  {   //сравниваем время записанное в переменную rtcSec с текущим временем RTC, и если оно не равно, выполнить функции
    setSyncProvider(RTC.get);   // the function to get the time from the RTC - коррекция времени ардуины из RTC
    }
    canMsg(); // формирование сообщения для CAN шины, из времени ардуины
    digitalClockDisplay() ;  // время в com порт
}


void canMsg(){

  // формирование сообщения для CAN шины, из времени ардуины
  unsigned char stmp[3] = {hour(), minute(), second()};//, 3, 4, 5, 6, 7}; // 3-7 - запасные ячейки
    
  // send data:  id = 0x00, standrad frame, data len = 3, stmp: data buf
  CAN.sendMsgBuf(0x00, 0, 3, stmp);
}

void digitalClockDisplay(){

  // digital clock display of the time
  Serial.print(hour());
  printDigits(minute());
  printDigits(second());
    printDigits(millis()%100);

  Serial.println(); 

}

void printDigits(int digits){

  // utility function for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if(digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

/*  code to process time sync messages from the serial port   */

#define TIME_HEADER  "T"   // Header tag for serial time sync message

unsigned long processSyncMessage() {

  unsigned long pctime = 0L;
  const unsigned long DEFAULT_TIME = 1357041600; // Jan 1 2013 

  if(Serial.find(TIME_HEADER)) {

     pctime = Serial.parseInt();
     return pctime;
     if( pctime < DEFAULT_TIME) { // check the value is a valid time (greater than Jan 1 2013)
       pctime = 0L; // return 0 to indicate that the time is not valid
     }
  }
  return pctime;
}

// Displays the time on our LEDs - отображаем время на дисплее

void displayTime() { 

   // timer = millis(); // reset the serial comms timer

    hourUnit = (hour()%10);
    hourTen = ((hour()/10)%10);
    minUnit = (minute()%10);
    minTen = ((minute()/10)%10);
    secUnit = (second()%10);
    secTen = ((second()/10)%10);
    msUnit = (millis()%10);
    msTen = ((millis()/10)%10);
    lc.setDigit (0,7,hourTen,false);
    lc.setDigit (0,6,hourUnit,false);
    lc.setDigit (0,5,minTen,false);
    lc.setDigit (0,4,minUnit,false);
    lc.setDigit (0,3,secTen,false);
    lc.setDigit (0,2,secUnit,false);
    lc.setDigit (0,1,msTen,false);
    lc.setDigit (0,0,msUnit,false);
}


Скетч Arduino - DisplayCanReceive.ino клиент-дисплей :


// Этот скрипт принимает сообщение по CAN шине (через MCP2515), и если оно принято,
// отправляет в последовательный порт инфомацию о принятом сообщении,
// и информацию из buf [0,1,2], и отображает инфомацию
// из buf [0,1,2] на led дисплей max7219.


#include <SPI.h> // подключается библиотека SPI

#include "mcp_can.h" // подключается библиотека SPI для MCP
#include "LedControl.h" // подключается библиотека MAX7219

// Далее - установки инициализации переменных для CAN шины и MAX7219

// the cs pin of the version after v1.1 is default to D9
// v0.9b and v1.0 is default D10
const int SPI_CS_PIN = 10;
const int LED=8;
boolean ledON=1;
MCP_CAN CAN(SPI_CS_PIN);                                    // Set CS pin

float br=(5 - analogRead(A0)) * 2.5 + 5; //делитель установить 12в на 5в, в итоге если вход 12 и выше - яркость 5 ; если 10 то яркость 8


LedControl lc=LedControl(6,8,9,1); //Tells LedControl where our hardware is connected.(6,8,9,1) for nano with busy spi bus


unsigned long timer =0;


void setup()

{
  // setup для инициализации MCP2515
    Serial.begin(115200);
    pinMode(LED,OUTPUT);

    while (CAN_OK != CAN.begin(CAN_500KBPS))              // init can bus : baudrate = 500k

    {
        Serial.println("CAN BUS Shield init fail");
        Serial.println(" Init CAN BUS Shield again");
        delay(100);
    }
    Serial.println("CAN BUS Shield init ok!");

    // setup для дисплея MAX7219

  lc.shutdown(0,false); // Wake up the MAX 72xx controller 
  lc.setIntensity(0,br); // Set the display brightness
  lc.clearDisplay(0); //Clear the display
}


void loop()

{
    unsigned char len = 0;
    unsigned char buf[8];

    if(CAN_MSGAVAIL == CAN.checkReceive())            // check if data coming

    {
        CAN.readMsgBuf(&len, buf);    // read data,  len: data length, buf: data buf

        unsigned char canId = CAN.getCanId();


      // выводим инфу в последовательный порт

        Serial.println("-----------------------------");
        Serial.println("get data from ID: ");
        Serial.println(canId);

              Serial.print(buf[0]);

              Serial.print(":");
              Serial.print(buf[1]);
              Serial.print(":");
              Serial.print(buf[2]);

        Serial.println();


        //и выводим на led дисплей max7219

    lc.setDigit (0,7,(buf[0]/10)%10,false);
    lc.setDigit (0,6,buf[0]%10,false);
    lc.setDigit (0,5,(buf[1]/10)%10,false);
    lc.setDigit (0,4,buf[1]%10,false);
    lc.setDigit (0,3,(buf[2]/10)%10,false);
    lc.setDigit (0,2,buf[2]%10,false);
    
    }
    

   //    displayTime(); // показать время на светодиодном дисплее

}

// Displays the time on our LEDs - отображаем время на дисплее


/*********************************************************************************************************

  Конец
*********************************************************************************************************/

C программной частью закончили - переходим к практике.

Центральный модуль.
Чтобы долго не ломать голову, центральный модуль было решено собрать в корпусе старого дохлого свитча. 
В этом корпусе разместились устройства:
Arduino nano
Max7219
LTC модуль ds3132
CAN модуль
Блок питания 19 В / 4 А
Стабилизатор на 5 В

Выглядит в корпусе это так:

Сзади - 2 гнезда rj-45 (они параллельны), выход CAN, USB - для подключения к компьютеру и синхронизации времени.

Дисплей - клиент.

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

Получилась основа 17.5 см * 71 см  - это с учётом толщины стенок корпуса - рамки.
Первым делом отдаём чертёж в полиграфический центр - для вырезания маски из чёрной плёнки (1:1) Маску вырезать зеркально. Так же понадобится белая самоклейка.
Всё это клеится на стекло размером 17.5см * 71см.
Стекло нужно хорошо очистить чтобы не оставалось разводов.
Сначала размечается маска по распечатанному чертежу (ставятся отметки на углах чертежа и маски). Маска с чертежом совмещается через вырезанные сегменты цифр. Далее на стекло клеится белая самоклейка. Клеить лучше вдвоём и с помощью воды. Так будет проще выгонять пузыри. Воду лучше нанести на стекло с помощью распылителя, а выгонять капли-пузыри  пластиковой картой.



Пузырей быть не должно. Отрезаем всё лишнее.
Клеим маску. Есть особенность: нужно клеить стекло к плёнке.
Отрываем клеящий слой от маски, укладываем липким слоем вверх, поливаем водой.
На весу совмещаем намеченные углы маски со стеклом, и аккуратно накладываем  стекло белой самоклейкой вниз на маску. Отрезаем лишнее.


Готово.
 Изготавливаем корпус для получившейся заготовки из деревянного бруса 20*30 мм. Делаем пропилы на циркулярной пиле. Стекло крепим внутрь рамки без возможности регулировки и замены. Важно чтобы расстояние пропила между стеклом и задней стенкой было 22 мм. Высота перегородок 20 мм + проводка ~2мм.


В результате получим рамку с маской.

Клеим перегородки из белого ПВХ уголка 20*20 мм. Разрезаем его вдоль по углу, получаем полоски. Из них и соорудим "колодцы" для света.
Перегородки клеить плотно и строго по маске. Процесс хоть и кропотливый, но занимает не меньше часа.

Готовим заднюю стенку - основу.
Распечатываем чертёж 1:1, накладываем на лицевую сторону нашей рамки. Я распечатал чертёж из 3-х листов, и мне пришлось делать подгон листов под маску, подсвечивая снизу лампой.
Когда чертёж подогнан, его нужно склеить со стеклом маленьким кусочком двухстороннего скотча. Чертёж должен быть полностью совмещён сегментами на просвет с маской.
Загибаем внешние углы чертежа по рамке.
Отклеиваем чертёж, переворачиваем рамку, накладываем чертёж с задней стороны совмещая углы рамки, и загибаем их в другую сторону.

Заднюю стенку из двп изготавливаем в размер стекла 17,5*71 см. Приклеиваем вертикально по середине к ней полосу двухстороннего скотча. Защитный слой скотча разрезать пополам и прилепить на края этого-же скотча, оставив в середине липкий слой.
 
Аккуратно и ровно приклеиваем заднюю стенку к нашему чертежу установленному в заготовку - рамку. Загибаем края чертежа вокруг приклеенной дсп, вынимаем дсп с чертежом, переворачиваем и приглаживаем середину.

На этом этапе нужно быть особенно точным, чтобы сегменты совпадали наверняка.
Снимаем защитный слой скотча, и доклеиваем середину. Дальше -проще. лепим полосы 2ст. скотча вдоль доски и приклеиваем чертёж целиком.



Дальше работаем только с задней стенкой.

Лепим светодиодные сегменты на чертёж и запаиваем провода.
Особенность следующая - в моём случае с дисплейным модулем 3641AH - с общим анодом - спаивать сегменты нужно начиная с +, и сегменты при приклеивании нужно лепить + в центр для удобства пайки. А ещё, нужно выпаять резистор из светодиодных сегментов и поставить на его место перемычку! На каждом сегменте.


Дальше придётся попотеть с электроникой.
Max7219 не может работать со светодиодной лентой с напряжением 12 вольт. Дисплей у этой микросхемы не рассчитан на такие задачи.
По этому, нужно изготовить драйвер для светодиодной ленты. На официальном сайте Maxim есть предложение по сборке драйвера. Опираясь на предложенные схемы и оценивая то что есть в местном радиомаге, разработал следующую схему драйвера.
По скольку у меня на плате max7219 был установлен дисплей 3641AH - с общим анодом то анодных цепеи на дисплей 4 а сегментных - 8. Работает схема последовательно по цифрам - цифры загораются по порядку с первого дисплея 1-2-3-4, далее второй дисплей 1-2-3-4. Нам нужно 6 цифр, значит 6 анодных цепей и 7 сегментных. Не буду углубляться в то - как работает схема - заострю внимание что полевик должен быть с логическим входом.

Делаем плату драйвера.

Тут проще - рисуем схему в Sprint Layout - отдаём текстолит и чертёж в полиграфию, и за 100 рублей получаем плату готовую к травлению. Плату нужно делать небольших размеров, чтобы она влезла в корпус. Например 10*5 см.
Из платы с max7219 выпаять дисплеи - вместо них установить шпильки проводники и припаять к готовой и собранной плате микросхемой вверх.



Дальше - самое лёгкое - закоммутировать модули ардуины, уложить проводку, выпилить лобзиком 2 отверстия под разъём RJ-45 (по которому приходит питание и CAN сообщения). Остаётся исхитриться уместить в оставшееся место радиаторы для стабилизаторов 5 и 12 вольт.

Собираем в корпус и отдыхаем!) Наслаждаемся готовым изделием и готовимся к запуску телестудии...









convert colors to midi note for midi controllers

I recently (2 years ago) designed and built a simple MIDI controller for my intercom system using an Arduino Leonardo, USB hub, sound card, ...