Часы на Arduino с круглыми сегментами из NeoPixel
Перевод статьи Arduino Round Segment NeoPixel Clock — Rajesh, 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 |
|---|---|
| Линия данных NeoPixel | D6 (LED_PIN) |
| DS3231 SDA | A4 (SDA) |
| DS3231 SCL | A5 (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);
}
Что в итоге#
Часы точно показывают время круглыми сегментами собственного дизайна, цвет цифр живо меняется каждую минуту, а кнопки позволяют быстро подстроить часы и минуты и переключить летнее время. Раз в минуту на экране мелькает температура. Получается устройство, которое одинаково хорошо работает и как настольные часы, и как небольшой декоративный светильник.