Перевод статьи Arduino Round Segment NeoPixel ClockRajesh, Circuit Digest.

Обычный семисегментный индикатор — вещь надёжная, но скучная. А что, если взять ту же идею семи сегментов, но выложить их не прямыми палочками, а изогнутыми дугами по кругу и подсветить адресными светодиодами? Получаются часы, которые не только показывают время, но и служат ярким настольным украшением: цвет цифр меняется каждую минуту, между часами и минутами мигает двоеточие, а раз в минуту устройство коротко показывает температуру в комнате.

В основе — плата Arduino Nano R4, модуль часов реального времени DS3231 и 30 адресных светодиодов WS2812 (они же NeoPixel). В этом руководстве разберём, как устроен индикатор, как всё соединить и как работает прошивка.

Что нам понадобится#

  • Arduino Nano R4 — основной контроллер;
  • модуль RTC DS3231 — отсчёт времени и встроенный датчик температуры;
  • 30 × адресных светодиодов WS2812 (NeoPixel);
  • 2 тактовые кнопки — подстройка часов и минут;
  • переключатель ON/OFF;
  • Li-ion аккумулятор 3,7 В;
  • модуль зарядки;
  • повышающий преобразователь (boost);
  • двухпиновые разъёмы и гребёнки.

Идея индикатора#

Классическая цифра из семи сегментов — это семь прямых палочек. Здесь автор перерисовал сегменты в круглые: семь изогнутых дуг, расставленных по окружности вместо прямоугольника. Разметку удобно нарисовать в любом векторном редакторе (в оригинале использовалась Canva).

Каждая цифра — это семь сегментов, а каждый сегмент подсвечивается одним светодиодом WS2812. На четыре цифры уходит 28 светодиодов, ещё два отведены под двоеточие — итого 30.

Почти всё держится на одном приёме: таблице соответствия. Для каждой цифры (0–9) заранее задано, какие из семи сегментов горят (1), а какие погашены (0). Дальше код просто пробегает по этой таблице и зажигает нужные светодиоды.

Плата и корпус#

Автор развёл собственную плату в EasyEDA: светодиоды NeoPixel — на лицевой стороне, а всё остальное (Arduino, RTC, кнопки, схема питания) — на обратной. Корпус спроектирован в Tinkercad и напечатан на 3D-принтере в два цвета: белый пластик работает как рассеиватель света, чёрный даёт жёсткость и контраст.

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

Схема подключения#

КомпонентВывод Arduino
Линия данных NeoPixelD6 (LED_PIN)
DS3231 SDAA4 (SDA)
DS3231 SCLA5 (SCL)
Кнопка «часы»D2 (HUR_PIN)
Кнопка «минуты»D3 (MIN_PIN)
Кнопка DST (перевод на летнее время)D5 (DST_PIN)
Датчик освещённостиD4 (BRI_PIN)

Модуль DS3231 общается с Arduino по шине I²C (SDA/SCL). Кнопки и датчик света подключены к цифровым входам с подтяжкой к питанию (INPUT_PULLUP), поэтому нажатие «замыкает на землю» и читается как LOW.

Как это работает#

DS3231 непрерывно ведёт время и заодно измеряет температуру. Arduino по I²C читает эти данные, по таблице сегментов определяет, какие светодиоды зажечь, и обновляет ленту через библиотеку FastLED.

Логика простая:

  • время переводится в одно число вида ЧЧММ (например, 0155);
  • число разбирается по одной цифре с конца, и каждая цифра «раскладывается» на семь сегментов;
  • каждую секунду переключается состояние двоеточия — получается мигание;
  • как только меняется последняя цифра минут, проигрывается радужная анимация и выбирается новый случайный цвет из палитры на 21 оттенок;
  • на 27-й секунде каждой минуты часы ненадолго показывают температуру.

Яркость подстраивается автоматически: если датчик света видит темноту — светодиоды притухают, на свету — светят ярче.

Библиотеки#

Перед компиляцией установите три библиотеки через менеджер библиотек Arduino IDE:

  • TimeLib
  • DS3232RTC
  • FastLED

Ключевые функции скетча#

  • GetTime() — читает время из RTC, применяет перевод на летнее время (DST), переводит из 24- в 12-часовой формат и возвращает время числом ЧЧММ.
  • TimeToArray() — раскладывает цифры времени по светодиодным сегментам через таблицу соответствия; здесь же управляется двоеточие и запускается смена цвета.
  • TempToArray() — на 27-й секунде минуты выводит температуру (с обработкой знака «минус»).
  • BrightnessCheck() — регулирует яркость по датчику освещённости.
  • DSTcheck() и TimeAdjust() — обрабатывают нажатия кнопок: переключение летнего времени и подстройку часов/минут.
  • cylon() — радужный «прогон» по ленте, тот самый эффект при смене минуты.

Полный код#

//EDISON SCIENCE CORNER
#include <DS3232RTC.h>
#include <TimeLib.h>
#include <Wire.h>
#include <FastLED.h>
DS3232RTC myRTC(false);
#define NUM_LEDS 30
#define COLOR_ORDER RGB  // Порядок цветов вашей ленты
#define LED_PIN 6 // Вывод данных для светодиодов
#define DST_PIN 5  // Кнопка перевода на летнее время
#define MIN_PIN 3  // Кнопка подстройки минут
#define HUR_PIN 2  // Кнопка подстройки часов
#define BRI_PIN 4 // Вывод датчика освещённости
CRGB leds[NUM_LEDS];
byte digits[12][7] = {
 {1,0,1,1,1,1,1},
 {1,0,1,0,0,0,0},
 {0,1,1,1,0,1,1},
 {1,1,1,1,0,0,1},
 {1,1,1,0,1,0,0},
 {1,1,0,1,1,0,1},
 {1,1,0,1,1,1,1},
 {1,0,1,1,0,0,0},
 {1,1,1,1,1,1,1},
 {1,1,1,1,1,0,0},
 {0,0,0,1,1,1,1},
 {0,1,1,1,1,0,0}};


bool Dot = true;  // Состояние двоеточия
bool DST = true; // Состояние летнего времени
bool TempShow = false;
int last_digit = 0;
long ledColor = CRGB::DarkOrchid; // Текущий цвет
// Палитра случайных цветов
long ColorTable[21] = {
 CRGB::Amethyst,
 CRGB::Aqua,
 CRGB::Blue,
 CRGB::Chartreuse,
 CRGB::DarkGreen,
 CRGB::DarkMagenta,
 CRGB::DarkOrange,
 CRGB::DeepPink,
 CRGB::Fuchsia,
 CRGB::Gold,
 CRGB::GreenYellow,
 CRGB::LightCoral,
 CRGB::Tomato,
 CRGB::Salmon,
 CRGB::Red,
 CRGB::Orchid,
 CRGB::Sienna,
 CRGB::Purple,
 CRGB::DarkOrange,
 CRGB::FloralWhite,
 CRGB::Yellow
};

void setup(){
 Serial.begin(9600);
 Wire.begin();
 myRTC.begin();
 FastLED.addLeds<WS2812B, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
 FastLED.setBrightness(75); // Стартовая яркость
 pinMode(DST_PIN, INPUT_PULLUP);
 pinMode(MIN_PIN, INPUT_PULLUP);
 pinMode(HUR_PIN, INPUT_PULLUP);
 pinMode(BRI_PIN, INPUT_PULLUP);
 TempShow = false; // Пока не показываем температуру
}
// Возвращает время одним числом. Если часы — одна цифра,
// время показывается как 155 вместо 0155.
int GetTime(){
 tmElements_t Now;
 myRTC.read(Now);
 int hour=Now.Hour;
 int minute=Now.Minute;
 int second =Now.Second;
 if (second % 2==0)
  {Dot = false;}
   else {Dot = true;};

 // Учитываем летнее время до перевода в 12-часовой формат
 if (DST){
   hour = (hour + 1) % 24;
 };
 if (hour >= 12) {
   hour -= 12;
 }
 if (hour == 0) {
   hour = 12;
 }
 return (hour*100+minute);
};
// Читаем датчик света и выставляем яркость
void BrightnessCheck(){
 const byte sensorPin = BRI_PIN; // Вывод датчика света
 const byte brightnessLow = 20; // Низкая яркость
 const byte brightnessHigh = 75; // Высокая яркость
 int sensorValue = digitalRead(sensorPin);
 if (sensorValue == 0) {FastLED.setBrightness(brightnessHigh);}
 else {FastLED.setBrightness(brightnessLow);}
 };
// Раскладываем время по массиву светодиодов
void TimeToArray(){
 int Now = GetTime();  // Получаем время

 int cursor = 30; // Номер последнего светодиода

 if (Dot){leds[28]=ledColor;
          leds[29]=ledColor;}
  else  {leds[28]=0x000000;
          leds[29]=0x000000;
   };

 for(int i=1;i<=4;i++){
   int digit = Now % 10; // Последняя цифра времени
   if (i==1){
     cursor = 21;
     for(int k=0; k<=6;k++){
       if (digits[digit][k]== 1){leds[cursor]=ledColor;}
        else if (digits[digit][k]==0){leds[cursor]=0x000000;};
        cursor ++;
       };
     if (digit != last_digit)
     {
       cylon();
       ledColor =  ColorTable[random(21)];
     }
     last_digit = digit;
     }
   else if (i==2){
     cursor =14;
     for(int k=0; k<=6;k++){
       if (digits[digit][k]== 1){leds[cursor]=ledColor;}
        else if (digits[digit][k]==0){leds[cursor]=0x000000;};
        cursor ++;
       };
     }
    else if (i==3){
     cursor =7;
     for(int k=0; k<=6;k++){
       if (digits[digit][k]== 1){leds[cursor]=ledColor;}
        else if (digits[digit][k]==0){leds[cursor]=0x000000;};
        cursor ++;
       };
     }
   else if (i==4){
     cursor =0;
     if(digit !=0){
        for(int k=0; k<=6;k++){
       if (digits[digit][k]== 1){leds[cursor]=ledColor;}
        else if (digits[digit][k]==0){leds[cursor]=0x000000;};
        cursor ++;
       };
     }
     if(digit ==0){
     for(int k=0; k<7;k++){
       leds[k]=0x000000;
       };
     }
   }
    Now /= 10;
 };
};
// Раскладываем температуру по массиву светодиодов
void TempToArray(){
 tmElements_t tm;
 myRTC.read(tm);
 if (tm.Second != 27) {
   TempShow = false;
   return;
 }
 TempShow = true;
 int t = myRTC.temperature();
 int celsius = t * 25; // Эквивалент (t / 4.0) * 100 в целочисленной арифметике
 bool isNegative = (celsius < 0);
 celsius = abs(celsius);
 Serial.print("Temp is: ");Serial.println(celsius);

 int cursor = 30;

 leds[28]=0x000000;
 leds[29]=0x000000;

 for(int i=1;i<=4;i++){
   int digit = celsius % 10;
   if (i==1){
     cursor = 21;
     for(int k=0; k<=6;k++){
       if (digits[10][k]== 1){leds[cursor]=ledColor;}
        else if (digits[10][k]==0){leds[cursor]=0x000000;};
        cursor ++;
       };
   }
   else if (i==2){
     cursor =14;
     for(int k=0; k<=6;k++){
       if (digits[11][k]== 1){leds[cursor]=ledColor;}
        else if (digits[11][k]==0){leds[cursor]=0x000000;};
        cursor ++;
       };
     }
   else if (i==3){
     cursor =7;
     for(int k=0; k<=6;k++){
       if (digits[digit][k]== 1){leds[cursor]=ledColor;}
        else if (digits[digit][k]==0){leds[cursor]=0x000000;};
        cursor ++;
       };
     }
   else if (i==4){
     cursor =0;
     if (isNegative) {
       // Знак «минус» (горит только сегмент G, индекс 1)
       for(int k=0; k<=6; k++){
         if (k == 1) { leds[cursor] = ledColor; }
         else { leds[cursor] = 0x000000; }
         cursor++;
       }
     } else {
       // Гашение ведущего нуля
       if(digit != 0){
         for(int k=0; k<=6;k++){
           if (digits[digit][k]== 1){leds[cursor]=ledColor;}
            else if (digits[digit][k]==0){leds[cursor]=0x000000;};
            cursor ++;
           };
       }
       else if(digit ==0){
         for(int k=0; k<7;k++){
           leds[k]=0x000000;
         };
       }
     }
   }
   celsius /= 10;
 };
};

void DSTcheck(){
  int buttonDST = digitalRead(DST_PIN);
  if (buttonDST == LOW){
   if (DST){
     DST=false;
     }
     else if (!DST){
       DST=true;
     };
  delay(500);
  };
 }
void TimeAdjust(){
 int buttonH = digitalRead(HUR_PIN);
 int buttonM = digitalRead(MIN_PIN);
 if (buttonH == LOW || buttonM == LOW){
   delay(500);
   tmElements_t Now;
   myRTC.read(Now);
     if (buttonH == LOW){
       if (Now.Hour== 23){Now.Hour=0;}
         else {Now.Hour += 1;};
       }else {
         if (Now.Minute== 59){Now.Minute=0;}
         else {Now.Minute += 1;};
         };
   myRTC.write(Now);
   }
 }
/* Красивый эффект */
void fadeall() { for(int i = 0; i < NUM_LEDS; i++) { leds[i].nscale8(250); } }

void cylon () {
 static uint8_t hue = 0;
 // Прогон в одну сторону
 for(int i = 0; i < NUM_LEDS; i++) {
   leds[i] = CHSV(hue++, 255, 255);
   FastLED.show();
   fadeall();
   delay(10);
 }
 // Прогон в обратную сторону
 for(int i = (NUM_LEDS)-1; i >= 0; i--) {
   leds[i] = CHSV(hue++, 255, 255);
   FastLED.show();
   fadeall();
   delay(10);
 }
}

void loop()  // Главный цикл
{
 BrightnessCheck(); // Проверяем яркость
 DSTcheck(); // Проверяем летнее время
 TimeAdjust(); // Проверяем подстройку времени
 TimeToArray(); // Готовим массив светодиодов
 TempToArray();
 FastLED.show(); // Выводим на ленту
 if (TempShow == true)
 delay (8000);
}

Что в итоге#

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