Arduino: светодиодные часы с синхронизацией времени

Захотелось к себе в комнату повесить цифровые часы на стену. Покупать готовые было не интересно, хотелось собрать что-нибудь самому. Встал вопрос об отображении цифр. Было два варианта: либо покупать семисегментные индикаторы, либо паять матрицу из светодиодов. Первый вариант отпал по причине отсутствия больших индикаторов в магазинах, а заказывать и ждать долго не хотелось. К тому же они недешёвые. Второй вариант требовал большого количества светодиодов и огромного времени на их последующую пайку.

Но тут я увидел в магазине светодиодные ленты и всё сразу решилось. В статье фотоотчёт по изготовлению таких часов и схемы с исходным кодом.

Итак, вот она, светодиодная лента:

Питается от постоянного напряжения 12 вольт. Лента состоит из соединённых между собой светодиодов размещённых на полимерной основе, с обратной стороны — самоклейка.

Ленту можно резать! И это самое главное. Места реза обозначены меткой и имеют с двух сторон контакты для пайки. Каждое такое звено состоит из трёх светодиодов и резистора, соединённых последовательно. Звенья же соединяются вместе параллельно. Таким образом, каждое звено питается от напряжения 12 вольт. Длина отрезка ленты равна 5 сантиметрам.

Ленты есть разные по мощности. Соответственно, если нужны часы для уличного применения, то необходимо брать помощнее. Для домашнего — самые тусклые, иначе в комнате будет светло как днём (не шучу).

Список того, что нам понадобится для сборки часов:

  • корпус;
  • 29 отрезков светодиодной ленты;
  • Arduino;
  • RTC (часы реального времени) на микросхеме DS1307;
  • Ethernet-shield Wiznet W5100;
  • 4 микросхемы PCF8574;
  • 4 микросхемы ULN2003A;
  • 2 резистора по 1.5 кОм;
  • 1 резистор на 3 кОм;
  • npn-транзистор, любой на напряжение питания 12 вольт и более;
  • блок питания 12 вольт;
  • макетная плата (либо можете вытравить свою), монтажные провода;
  • синяя изолента 🙂

Теперь надо всё подключить по следующим схемам:

Ethernet-shield нам понадобится для подключения к Интернету, чтобы можно было синхронизировать часы с NTP-сервером.

Микросхемы PCF8574 — 8-битные расширители для шины I2C:

Их распиновка следующая:

  • VDD — питание 5 вольт;
  • SDA, SCL — шина I2C;
  • A0..2 — пины для установки адреса микросхемы;
  • P0..7 — входы/выходы;
  • VSS — земля.

Адресация PCF8574:

ВходАдрес на шине I2C
A2A1A0DECHEX
000320x20
001330x21
010340x22
011350x23
100360x24
101370x25
110380x26
111390x27

ULN2003A — сборка Дарлингтона. Распиновка:

  • COM — питание 12 вольт;
  • E — земля;
  • 1..7B — входы;
  • 1..7C — выходы.

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

Корпус часов я сделал из дерева:

Светодиодную ленту нарезал на кусочки и приклеил в виде восьмёрок на пластик:

Примеряем:

Теперь собираем схему и припаиваем «восьмёрки». Плюсовые выводы ленты подключаем все вместе к +12 вольт, минусовые к ULN2003A:

Пробуем включить:

Всё светится, отлично.

Для подключения точек нам понадобится любой npn-транзистор на напряжение не менее 12 вольт. Я взял первый попавшийся под руку. Это оказался C5027F-R. Ищем даташит на него, определяем местоположение коллектора, эмиттера и базы. Подключаем по схеме. Светодиоды LED1, LED2, LED3 и резистор R4 — расположены на отрезке ленты. Плюсовой вывод подключаем к питанию 12 вольт, минусовой — к коллектору транзистора. Средний светодиод заклеиваем чёрной изолентой, чтобы его не было видно.

Чтобы в комнате ночью не стало светло от таких часов, я заклеил все светодиоды двумя слоями синей изоленты и листом полупрозрачного синего пластика:

А затем всё сверху закрыл стеклом. Получились вот такие часы:

Ну и конечно исходный код скетча:

// Скетч для Arduino. Светодиодные часы с синхронизацией.
// Версия 1.0 (29.07.2014)
//
// Автор: Гладышев Дмитрий (2014)
// http://student-proger.ru/2014/07/arduino-svetodiodnye-chasy-s-sinhronizaciej-vremeni/

#include <SPI.h>         //для Ethernet-shield
#include <Ethernet.h>    //   --//--
#include <EthernetUdp.h>
#include <Wire.h>        //I2C для RTC
#include <DS1307RTC.h>   //RTC
#include <Time.h>
#include <Metro.h>       //для задания интервалов времени (аля Cron)

//****************************************************************************************
byte mac[] = {  0xDE, 0xAD, 0x00, 0x00, 0x00, 0x00 }; //MAC-адрес Arduino
byte clock_addr[] = {0x20, 0x21, 0x22, 0x23}; //адреса PCF8574

IPAddress timeServer(132, 163, 4, 101);   // IP-адрес NTP сервера
const int timeZone = 8;                   // TimeZone
//****************************************************************************************
unsigned int localPort = 53;
Metro TimeSync = Metro(86400000); //Частота синхронизации (1 сутки)

char clockbuf[4];

EthernetClient client2;
EthernetUDP Udp;

unsigned long lastTimeUpdate = 0;

bool DotTimeState = false;

void setup() {

  for (int i=0; i<4; i++)
  {
    clockbuf[i]='-';
  }
  UpdateClock();

  // Ethernet connection:
  do 
  {
    delay(1000);
  } while (Ethernet.begin(mac) == 0);
  
  Wire.begin();
  Udp.begin(localPort);
  
  //Запрашиваем время с NTP сервера
  setSyncProvider(getNtpTime);
  //Если время получили успешно, то записываем данные в RTC
  if (timeStatus() != timeNotSet)
  {
    RTC.set(now());
  }
}

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

void loop()
{
  tmElements_t tm;
  
  if (TimeSync.check() == 1) //Синхронизация по времени
  {
    //Запрашиваем время с NTP сервера
    setSyncProvider(getNtpTime);
    //Если время получили успешно, то записываем данные в RTC
    if (timeStatus() != timeNotSet)
    {
      RTC.set(now());
    }
  }    
    
  if (client2.available())
  {
    char c = client2.read();
  }

  if (abs(millis() - lastTimeUpdate) > 500)
  {
    lastTimeUpdate = millis();
    
    if (RTC.read(tm))
    {
      DotTimeState=!DotTimeState;
      
      clockbuf[0] = (char)(tm.Hour/10)+'0';
      if (tm.Hour<10)
      {
        clockbuf[0] = ' ';
      }
      clockbuf[1] = (char)(tm.Hour%10)+'0';
      clockbuf[2] = (char)(tm.Minute/10)+'0';
      clockbuf[3] = (char)(tm.Minute%10)+'0';
      UpdateClock();
    }

  }

}

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

void reverse(char s[])
{
  int i, j;
  char c;
  
  for (i = 0, j = strlen(s)-1; i<j; i++, j--) {
    c = s[i];
    s[i] = s[j];
    s[j] = c;
  }
}

void itoa(int n, char s[])
{
  int i, sign;
  
  if ((sign = n) < 0)  /* записываем знак */
  n = -n;          /* делаем n положительным числом */
  i = 0;
  do {       /* генерируем цифры в обратном порядке */
    s[i++] = n % 10 + '0';   /* берем следующую цифру */
  } while ((n /= 10) > 0);     /* удаляем */
  if (sign < 0)
    s[i++] = '-';
  s[i] = '\0';
  reverse(s);
}

int len(char *buf)
{
  int i=0;
  do
  {
    i++;
  } while (buf[i]!='\0');
  return i;
}

void UpdateClock()
{
  for (int i=0; i<4; i++)
  {
    Wire.beginTransmission(clock_addr[i]);
    Wire.write(SegmentValue(clockbuf[i]));
    Wire.endTransmission();
  }
}

byte SegmentValue(char k)
{
  byte u;
  switch (k)
  {
    case '0':
      u=B1111110;
      break;
    case '1':
      u=B0110000;
      break;
    case '2':
      u=B1101101;
      break;
    case '3':
      u=B1111001;
      break;
    case '4':
      u=B0110011;
      break;
    case '5':
      u=B1011011;
      break;
    case '6':
      u=B1011111;
      break;
    case '7':
      u=B1110000;
      break;
    case '8':
      u=B1111111;
      break;
    case '9':
      u=B1111011;
      break;
    case '-':
      u=B0000001;
      break;
    case ' ':
      u=B0000000;
      break;
    case 'E':
      u=B1001111;
      break;
    case 'U':
      u=B0111110;
      break;
    case 'u':
      u=B0011100;
      break;
    case 'I':
      u=B0110000;
      break;
    case 'i':
      u=B0010000;
      break;
    case 'O':
      u=B1111110;
      break;
    case 'o':
      u=B0011101;
      break;
    case 'P':
      u=B1100111;
      break;
    case 'A':
      u=B1110111;
      break;
    case 'S':
      u=B1011011;
      break;
    case 'F':
      u=B1000111;
      break;
    case 'H':
      u=B0110111;
      break;
    case 'J':
      u=B0111100;
      break;
    case 'L':
      u=B0001110;
      break;
    case 'C':
      u=B1001110;
      break;
    case 'B':
      u=B1111111;
      break;
    case 'b':
      u=B0011111;
      break;
    case 'd':
      u=B0111101;
      break;
    case '°':
      u=B1100011;
      break;
    default:
      u=B0000000;
  }
  
  byte t=0;
  
  //переворачиваем биты
  for (byte i=0; i<7; i++)
  {
    if ((u&(1<<i))!=0)
    {
      t=t|(1<<(6-i));
    }
  }
  u=t;
  
  u=u<<1;
  if (DotTimeState)
  {
    u=u+1;
  }
    
  return u;
}


/*-------- NTP code ----------*/

const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets

time_t getNtpTime()
{
  while (Udp.parsePacket() > 0) ; // discard any previously received packets 
  sendNTPpacket(timeServer);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) 
  {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) 
    {
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }  
  return 0; // return 0 if unable to get the time
}

// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

/*-------- NTP code END ----------*/

Смотрим строки 16-20:

byte mac[] = {  0xDE, 0xAD, 0x00, 0x00, 0x00, 0x00 }; //MAC-адрес Arduino
byte clock_addr[] = {0x20, 0x21, 0x22, 0x23}; //адреса PCF8574
 
IPAddress timeServer(132, 163, 4, 101);   // IP-адрес NTP сервера
const int timeZone = 8;                   // TimeZone

mac — MAC-адрес Ethernet-shield. В принципе, можете оставить таким же.

clock_addr — адреса микросхем PCD8574 на шине I2C. Если не меняли, то должны быть такими же. Определить адреса можно с помощью скетча i2c_scanner, который есть в конце статьи.

timeServer — IP-адрес NTP-сервера.

timeZone — номер часового пояса.

Видео посетителей сайта, оставленные ими в комментариях:
Павел:

AlexSoft:

Скетч (7.15 KB)
Библиотеки:
DS1307RTC (4.8 KB)
Time (23.2 KB)
Metro (76.11 KB)
I2C scanner (1.02 KB)
Работа проверялась с Arduino IDE v1.0.5-r2.
Библиотеки являются собственностью их авторов!

Предупреждение!
Автор не несёт ответственности за возможную порчу оборудования. Всё, что вы делаете — вы делаете на свой страх и риск!