Часы Nixie Clock: инструкция, примеры использования и документация

Используйте часы Nixie Clock в качестве стильного гаджета на своём столе дома, мастерской или на работе. Часы не просто показывают текущее время, а делает это максимально зрелищно благодаря прозрачному циферблату, который имитирует знаменитые газоразрядные индикаторы Nixie Tube. Только вместо олдскульных газоразрядных ламп наподобие ИН-12, ИН-14, ИН-18 — современная управляемая LED-подсветка на WS2812B.

Что такое Nixie Clock?

В конце 90-х было модно собирать часы на газоразрядных лампах Nixie Tube моделей ИН-12, ИН-14, ИН-18.

Мы решили дать возможность каждому собрать модные олдскульные часы. Однако вместо дефицитных газоразрядных ламп используем акриловые пластины с выгравированными цифрами, которые подсветим адресными светодиодами WS2812B.

Каждый разряд часов состоит из десяти вырезанных лазерным резаком прямоугольников из прозрачного акрила.

На каждом прямоугольнике выполнена гравировка цифры от 0 до 9.

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

С теорией разобрались, вперёд собирать стильные часы.

Как собрать?

Комплектация

Шаг 1

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

Шаг 2

Вставьте батарейку типоразмера CR1225 в батарейный отсек платы Nixie Clock. Батарейка идет в комплекте с часами.

В часах Nixie Clock батарейка используется для хранения пользовательских настроек цвета, анимации и отсчета времени при отключении часов от питания.

Шаг 3

Установите пластины А, B и С снизу платы Nixie Clock, используя пять винтов M3×25.

Шаг 4

Вставьте секундный рассеиватель в пластину Е. Установите пластины E и D сверху платы Nixie Clock и закрепите их пятью алюминиевыми стойками.

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

Шаг 5

Через пластину F установите в часы четыре набора цифр. Один набор цифр состоит из 10 пластиковых деталей с цифрами от 0 до 9. Цифры устанавливаются по возрастанию. В каждом разряде цифра 0 находится ближе всего к лицевой стороне часов.

Для удобства установки цифр, пластину F можно временно закрепить винтами к алюминиевым стойкам.

Шаг 6

Установите сверху часов пластину G и закрепите её пятью винтами М3×10.

Шаг 7

Вуаля часы собраны, остаётся только подать питания. Подключите 5 В к часам через разъём Micro-USB. В качестве источника питания подойдёт USB-порт компьютера или наш сетевой адаптер 5 В.

Вот и всё, часы тикают и радуют ваш взгляд.

Если стандартной работы часов вам мало и чувствуете что можете улучшить гаджет, переходите в раздел разботчика.

Раздел разработчика

По умолчанию на Nixie Clock установлена заводская прошивка. Но вы можете запрограммировать гаджет по-своему.

  1. Подключите Nixie Clock к компьютеру

Часы выполнены на микроконтроллере ATmega328P с зашитим загрузчиком от Arduino Uno. Однако на плате Nixie Clock распаяна дополнительная переферия: часы реального времени, кнопки и светодиоды.

Особенности распиновки вы найдёте ниже. И не бойтесь экспеременитировать, штатный код часов так же приложен ниже.

Распиновка платы Nixie Clock

Для создания собственной прошивки часов вам понадобится информация о задействованных пинах микроконтроллера ATmega328P.

Прошивка платы

Часы Nixie Clock поставляются прошитыми, однако при желании вы можете изменить или дополнить оригинальную прошивку.

Необходимые библиотеки

В прошивке платы Nixie Clock используются следующие Arduino библиотеки:

  • Adafruit NeoPixel Library - библиотека для управления с адресными светодиодами WS2812B.
  • TroykaButton - библиотека для работы с кнопками.
  • TroykaRTC - библиотека для работы с часами реального времени DS1307.

Убедитесь что эти библиотеки установлены в ваше рабочее пространство Arduino IDE.

Штатный код часов

Clock.ino
#include <Adafruit_NeoPixel.h>
#include <TroykaButton.h>
#include <TroykaRTC.h>
#include <Wire.h>
 
// Uncomment the line below to source time from an MCU circuit rather than RTC.
// Use one having best precision in hardware.
// #define TIME_SOURCE_MCU
 
// Pin definition
 
constexpr uint8_t BUTTON_MODE_PIN = A3;
constexpr uint8_t BUTTON_OK_PIN = A2;
constexpr uint8_t BUTTON_UP_PIN = A1;
constexpr uint8_t BUTTON_DOWN_PIN = A0;
constexpr uint8_t WS2812_DOT_PIN = 9;
constexpr uint8_t WS2812_MATRIX_PIN = 2;
 
// Class objects
 
Adafruit_NeoPixel dot = Adafruit_NeoPixel(1, WS2812_DOT_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel matrix = Adafruit_NeoPixel(40, WS2812_MATRIX_PIN, NEO_GRB + NEO_KHZ800);
RTC clock;
TroykaButton button_mode(BUTTON_MODE_PIN, 1000, true, 200);
TroykaButton button_ok(BUTTON_OK_PIN, 1000, true, 200);
TroykaButton button_up(BUTTON_UP_PIN, 1000, true, 200);
TroykaButton button_down(BUTTON_DOWN_PIN, 1000, true, 200);
 
// Time variables
 
uint8_t current_hour = 0;
uint8_t current_minute = 0;
uint8_t target_hour = 0;
uint8_t target_minute = 0;
 
#ifdef TIME_SOURCE_MCU
volatile uint16_t isr_counter = 0; // Used for clock source from AVR hardware timer
volatile uint8_t isr_current_hour = 0; // Used for clock source from AVR hardware timer
volatile uint8_t isr_current_minute = 0; // Used for clock source from AVR hardware timer
#endif
 
// LED variables
 
uint32_t now_time = 0;
 
uint32_t digit_color[4] = { 0, 0, 0, 0 };
uint32_t digit_previous_color[4] = { 0, 0, 0, 0 };
 
uint8_t digit_current_led[4] = { 255, 255, 255, 255 };
uint8_t digit_previous_led[4] = { 255, 255, 255, 255 };
uint8_t digit_old_led[4] = { 255, 255, 255, 255 };
 
bool dot_blinking_state = false;
 
#ifndef TIME_SOURCE_MCU
uint32_t dot_blinking_last_time = 0;
#endif
 
constexpr uint16_t MATRIX_BLINKING_INTERVAL_MS = 250;
uint32_t matrix_blinking_last_time = 0;
bool matrix_blinking_state = false;
 
// Modes variables and definitions
 
enum WorkingMode {
    WORKING_MODE_CLOCK = 1,
    WORKING_MODE_SET_TIME_HOURS = 2,
    WORKING_MODE_SET_TIME_MINUTES = 3,
    WORKING_MODE_MENU_1 = 4,
    WORKING_MODE_MENU_2 = 5,
    WORKING_MODE_MENU_3 = 6,
};
 
uint8_t current_working_mode = WORKING_MODE_CLOCK;
bool new_working_mode = false;
 
uint8_t current_setting_1_value = 0;
uint8_t current_setting_2_value = 0;
uint8_t current_setting_3_value = 0;
 
uint8_t target_setting_1_value = 0;
uint8_t target_setting_2_value = 0;
uint8_t target_setting_3_value = 0;
 
constexpr uint8_t TOTAL_SETTING_1_VALUES = 17;
constexpr uint8_t TOTAL_SETTING_2_VALUES = 4;
constexpr uint8_t TOTAL_SETTING_3_VALUES = 3;
 
// Color variables
 
int16_t delta_hue[4] = { 0, 0, 0, 0 };
uint8_t digit_brightness[4] = { 255, 255, 255, 255 };
uint8_t digit_previous_brightness[4] = { 255, 255, 255, 255 };
 
// Animation variables
 
enum ColorAnimation {
    COLOR_ANIMATION_NONE = 0,
    COLOR_ANIMATION_PENDULUM = 1,
    COLOR_ANIMATION_WAVE_PENDULUM = 2,
    COLOR_ANIMATION_DEEPNESS = 3,
};
 
constexpr float COLOR_ANIMATION_RATE_MS = 12.0 / 60000.0; // Changes in ms
constexpr float COLOR_ANIMATION_PHASE_SHIFT = 0.125;
uint32_t previous_color_animation_time = 0;
float color_animation_timer = 0;
 
enum DigitAnimation {
    DIGIT_ANIMATION_NONE = 0,
    DIGIT_ANIMATION_RUNNING_LED = 1,
    DIGIT_ANIMATION_FADE = 2,
};
 
bool digit_animation_is_preview = false;
uint32_t previous_digit_animation_fade_preview_time = 0;
uint32_t previous_digit_animation_running_led_preview_time = 0;
constexpr uint32_t DIGIT_ANIMATION_RUNNING_LED_PREVIEW_TIME_MS = 4000;
constexpr uint32_t DIGIT_ANIMATION_FADE_PREVIEW_TIME_MS = 6500;
 
uint8_t running_led[4] = { 255, 255, 255, 255 };
constexpr uint32_t DIGIT_ANIMATION_RUNNING_LED_RATE_MS = 30;
uint32_t previous_digit_animation_running_led_time = 0;
 
constexpr float FADE_ANIMATION_RATE_MS = 3000.0; // 3 sec
uint32_t previous_fade_animation_time[4] = { 0, 0, 0, 0 };
float fade_animation_timer[4] = { 0, 0, 0, 0 };
 
// Auxiliary
 
uint16_t degree_hue_to_uint16_hue(int16_t degree) {
    return (float)((degree % 360) / 360.0 * 65535.0);
}
 
// CLOCK
 
void read_time() {
    clock.read();
    current_hour = clock.getHour();
    current_minute = clock.getMinute();
#ifdef TIME_SOURCE_MCU
    set_volatile_data();
#endif
}
 
void read_ram_data() {
    uint8_t data = clock.getRAMData(0x0A); // Menu 1 setting
    current_setting_1_value = ((data > 0) && (data < TOTAL_SETTING_1_VALUES)) ? data : 0;
    data = clock.getRAMData(0x0B); // Menu 2 setting
    current_setting_2_value = ((data > 0) && (data < TOTAL_SETTING_2_VALUES)) ? data : 0;
    data = clock.getRAMData(0x0C); // Menu 3 setting
    current_setting_3_value = ((data > 0) && (data < TOTAL_SETTING_3_VALUES)) ? data : 0;
}
 
void set_ram_data() {
    clock.setRAMData(0x0A, current_setting_1_value); // Menu 1 setting
    clock.setRAMData(0x0B, current_setting_2_value); // Menu 2 setting
    clock.setRAMData(0x0C, current_setting_3_value); // Menu 3 setting
}
 
// MODE button
 
void button_mode_handler() {
    button_mode.read();
 
    if (button_mode.isClick()) {
 
        switch (current_working_mode) {
        case WORKING_MODE_CLOCK:
            current_working_mode = WORKING_MODE_SET_TIME_HOURS;
            break;
        case WORKING_MODE_SET_TIME_HOURS:
            clock.setHour(target_hour);
            clock.setSecond(0);
            read_time();
            current_working_mode = WORKING_MODE_SET_TIME_MINUTES;
            break;
        case WORKING_MODE_SET_TIME_MINUTES:
            clock.setMinute(target_minute);
            clock.setSecond(0);
            read_time();
            current_working_mode = WORKING_MODE_CLOCK;
            break;
        case WORKING_MODE_MENU_1:
            current_setting_1_value = target_setting_1_value;
            set_ram_data();
            current_working_mode = WORKING_MODE_MENU_2;
            break;
        case WORKING_MODE_MENU_2:
            current_setting_2_value = target_setting_2_value;
            set_ram_data();
            current_working_mode = WORKING_MODE_MENU_3;
            break;
        case WORKING_MODE_MENU_3:
            current_setting_3_value = target_setting_3_value;
            set_ram_data();
            current_working_mode = WORKING_MODE_CLOCK;
            break;
        default:
            break;
        }
        new_working_mode = true;
    } else if (button_mode.isHold()) {
 
        switch (current_working_mode) {
        case WORKING_MODE_CLOCK:
            current_working_mode = WORKING_MODE_MENU_1;
            break;
        default:
            break;
        }
        new_working_mode = true;
    }
}
 
// UP button
 
void button_up_handler() {
    button_up.read();
 
    if (!button_up.isClickOnHold() && !button_up.justPressed())
        return;
 
    switch (current_working_mode) {
    case WORKING_MODE_SET_TIME_HOURS:
        target_hour = (target_hour + 1) % 24;
        break;
    case WORKING_MODE_SET_TIME_MINUTES:
        target_minute = (target_minute + 1) % 60;
        break;
    case WORKING_MODE_MENU_1:
        target_setting_1_value = (target_setting_1_value + 1) % TOTAL_SETTING_1_VALUES;
        break;
    case WORKING_MODE_MENU_2:
        target_setting_2_value = (target_setting_2_value + 1) % TOTAL_SETTING_2_VALUES;
        break;
    case WORKING_MODE_MENU_3:
        target_setting_3_value = (target_setting_3_value + 1) % TOTAL_SETTING_3_VALUES;
        break;
    default:
        break;
    }
}
 
// DOWN button
 
void button_down_handler() {
    button_down.read();
 
    if (!button_down.isClickOnHold() && !button_down.justPressed())
        return;
 
    switch (current_working_mode) {
    case WORKING_MODE_SET_TIME_HOURS:
        target_hour = (target_hour - 1 + 24) % 24;
        break;
    case WORKING_MODE_SET_TIME_MINUTES:
        target_minute = (target_minute - 1 + 60) % 60;
        break;
    case WORKING_MODE_MENU_1:
        target_setting_1_value = (target_setting_1_value - 1 + TOTAL_SETTING_1_VALUES) % TOTAL_SETTING_1_VALUES;
        break;
    case WORKING_MODE_MENU_2:
        target_setting_2_value = (target_setting_2_value - 1 + TOTAL_SETTING_2_VALUES) % TOTAL_SETTING_2_VALUES;
        break;
    case WORKING_MODE_MENU_3:
        target_setting_3_value = (target_setting_3_value - 1 + TOTAL_SETTING_3_VALUES) % TOTAL_SETTING_3_VALUES;
        break;
    default:
        break;
    }
}
 
// OK button
 
void button_ok_handler() {
    button_ok.read();
 
    if (!button_ok.isClick())
        return;
 
    switch (current_working_mode) {
    case WORKING_MODE_SET_TIME_HOURS:
    case WORKING_MODE_SET_TIME_MINUTES:
        clock.setHour(target_hour);
        clock.setMinute(target_minute);
        clock.setSecond(0);
        read_time();
        current_working_mode = WORKING_MODE_CLOCK;
        break;
    case WORKING_MODE_MENU_1:
    case WORKING_MODE_MENU_2:
    case WORKING_MODE_MENU_3:
        current_setting_1_value = target_setting_1_value;
        current_setting_2_value = target_setting_2_value;
        current_setting_3_value = target_setting_3_value;
        set_ram_data();
        current_working_mode = WORKING_MODE_CLOCK;
        break;
    default:
        break;
    }
    new_working_mode = true;
}
 
void working_mode_handler() {
    if (!new_working_mode)
        return;
 
    switch (current_working_mode) {
    case WORKING_MODE_SET_TIME_HOURS:
        target_hour = current_hour;
        target_minute = current_minute;
        break;
    case WORKING_MODE_MENU_1:
        target_setting_1_value = current_setting_1_value;
        break;
    case WORKING_MODE_MENU_2:
        target_setting_2_value = current_setting_2_value;
        break;
    case WORKING_MODE_MENU_3:
        target_setting_3_value = current_setting_3_value;
        break;
    default:
        break;
    }
    new_working_mode = false;
}
 
uint32_t nth_preset_color(uint8_t current_color_setting, int16_t hue_change = 0, uint8_t brightness = 255) {
    return (current_color_setting < 16 && current_color_setting > 0) ? Adafruit_NeoPixel::ColorHSV(degree_hue_to_uint16_hue((current_color_setting - 1) * 24 + hue_change), 255, brightness) : 0xFFFFFFFF;
}
 
void dot_handler() {
 
    uint32_t dot_color;
 
    switch (current_working_mode) {
    case WORKING_MODE_CLOCK:
        if (dot_blinking_state) {
            dot_color = nth_preset_color(current_setting_1_value);
            dot.setPixelColor(0, dot_color);
        } else {
            dot.setPixelColor(0, 0);
        }
        break;
    default:
        dot.setPixelColor(0, 0);
        break;
    }
    dot.show();
}
 
float saw_wave(float x) {
    float fract = fmod(x, 1);
    float y = (fract < 0.5) ? (fract / 0.5) : (((fract - 0.5) / -0.5) + 1.0);
    return y = 2 * y - 1;
}
 
void color_animation_handler() {
 
    uint32_t dt = now_time - previous_color_animation_time;
    color_animation_timer = color_animation_timer + (dt * COLOR_ANIMATION_RATE_MS);
 
    float a = 0;
 
    previous_color_animation_time = now_time;
 
    if (target_setting_2_value == COLOR_ANIMATION_PENDULUM || current_setting_2_value == COLOR_ANIMATION_PENDULUM) {
        a = saw_wave(color_animation_timer);
        for (uint8_t i = 0; i < 4; i++)
            delta_hue[i] = 24 * a;
    } else if (target_setting_2_value == COLOR_ANIMATION_WAVE_PENDULUM || current_setting_2_value == COLOR_ANIMATION_WAVE_PENDULUM) {
        for (uint8_t i = 0; i < 4; i++) {
            a = saw_wave(color_animation_timer + (i + 1) * COLOR_ANIMATION_PHASE_SHIFT);
            delta_hue[i] = 24 * a;
        }
    } else if (target_setting_2_value == COLOR_ANIMATION_DEEPNESS || current_setting_2_value == COLOR_ANIMATION_DEEPNESS) {
        for (uint8_t i = 0; i < 4; i++)
            delta_hue[i] = (digit_current_led[i] - 10 * i) * 24.0 / 10.0;
    } else // COLOR_ANIMATION_NONE
        for (uint8_t i = 0; i < 4; i++)
            delta_hue[i] = 0;
}
 
void animation_running_led_handler() {
 
    if (now_time - previous_digit_animation_running_led_time > DIGIT_ANIMATION_RUNNING_LED_RATE_MS) {
        for (uint8_t i = 0; i < 4; i++)
            if (running_led[i] > digit_old_led[i])
                running_led[i]--;
        previous_digit_animation_running_led_time = now_time;
    }
 
    for (uint8_t i = 0; i < 4; i++) {
        if (digit_current_led[i] != digit_old_led[i]) {
            running_led[i] = 10 * (i + 1) - 1;
            digit_old_led[i] = digit_current_led[i];
        }
    }
 
    // Preview
 
    if (digit_animation_is_preview && (now_time - previous_digit_animation_running_led_preview_time > DIGIT_ANIMATION_RUNNING_LED_PREVIEW_TIME_MS)) {
        for (uint8_t i = 0; i < 4; i++)
            running_led[i] = 10 * (i + 1) - 1;
        previous_digit_animation_running_led_preview_time = now_time;
    }
 
    // Show
 
    for (uint8_t i = 0; i < 4; i++)
        digit_current_led[i] = running_led[i];
}
 
float fade_wave(float x) {
    float fract = fmod(x, 1);
    return pow(fract == 0 ? 1 : fract, 2);
}
 
void animation_fade_handler() {
 
    for (uint8_t i = 0; i < 4; i++) {
        if (digit_current_led[i] != digit_old_led[i]) {
            digit_previous_led[i] = digit_old_led[i];
            if (fade_animation_timer[i] >= 1.0) {
                fade_animation_timer[i] = 0;
            }
        }
    }
 
    for (uint8_t i = 0; i < 4; i++) {
        if (fade_animation_timer[i] < 1.0) {
            uint32_t dt = now_time - previous_fade_animation_time[i];
            fade_animation_timer[i] = fade_animation_timer[i] + (dt * (1.0 / FADE_ANIMATION_RATE_MS));
        }
        if (fade_animation_timer[i] >= 1.0) {
            fade_animation_timer[i] = 1.0;
            digit_old_led[i] = digit_current_led[i];
        }
        previous_fade_animation_time[i] = now_time;
    }
 
    // Preview
 
    if (digit_animation_is_preview && (now_time - previous_digit_animation_fade_preview_time > DIGIT_ANIMATION_FADE_PREVIEW_TIME_MS)) {
        for (uint8_t i = 0; i < 4; i++) {
            digit_previous_led[i] = 255;
            fade_animation_timer[i] = 0;
        }
        previous_digit_animation_fade_preview_time = now_time;
    }
 
    // Show
 
    for (uint8_t i = 0; i < 4; i++) {
        float a = fade_wave(fade_animation_timer[i]);
        digit_brightness[i] = 255 * a;
        digit_previous_brightness[i] = 255 * (1 - a);
    }
}
 
void matrix_handler() {
    matrix.clear();
 
    memset(digit_brightness, 255, 4 * sizeof(digit_brightness[0]));
 
    // Choose digits
 
    switch (current_working_mode) {
    case WORKING_MODE_SET_TIME_HOURS:
    case WORKING_MODE_SET_TIME_MINUTES:
        digit_current_led[0] = target_hour / 10;
        digit_current_led[1] = (target_hour % 10) + 10;
        digit_current_led[2] = (target_minute / 10) + 20;
        digit_current_led[3] = (target_minute % 10) + 30;
        break;
    case WORKING_MODE_CLOCK:
        digit_current_led[0] = current_hour / 10;
        digit_current_led[1] = (current_hour % 10) + 10;
        digit_current_led[2] = (current_minute / 10) + 20;
        digit_current_led[3] = (current_minute % 10) + 30;
        break;
    case WORKING_MODE_MENU_1:
        digit_current_led[0] = 1; // Menu 1
        digit_current_led[1] = 10;
        digit_current_led[2] = target_setting_1_value >= 9 ? (((target_setting_1_value + 1) / 10) + 20) : 20;
        digit_current_led[3] = ((target_setting_1_value + 1) % 10) + 30;
        break;
    case WORKING_MODE_MENU_2:
        digit_current_led[0] = 2; // Menu 2
        digit_current_led[1] = 10;
        digit_current_led[2] = target_setting_2_value >= 9 ? (((target_setting_2_value + 1) / 10) + 20) : 20;
        digit_current_led[3] = ((target_setting_2_value + 1) % 10) + 30;
        break;
    case WORKING_MODE_MENU_3:
        digit_current_led[0] = 3; // Menu 3
        digit_current_led[1] = 10;
        digit_current_led[2] = target_setting_3_value >= 9 ? (((target_setting_3_value + 1) / 10) + 20) : 20;
        digit_current_led[3] = ((target_setting_3_value + 1) % 10) + 30;
        break;
    default:
        break;
    }
 
    // Color animation
 
    color_animation_handler();
 
    switch (current_working_mode) {
    case WORKING_MODE_CLOCK:
        digit_animation_is_preview = false;
        if (current_setting_3_value == DIGIT_ANIMATION_RUNNING_LED)
            animation_running_led_handler();
        else if (current_setting_3_value == DIGIT_ANIMATION_FADE)
            animation_fade_handler();
        break;
    case WORKING_MODE_MENU_3:
        digit_animation_is_preview = true;
        if (target_setting_3_value == DIGIT_ANIMATION_RUNNING_LED)
            animation_running_led_handler();
        else if (target_setting_3_value == DIGIT_ANIMATION_FADE)
            animation_fade_handler();
        break;
    default:
        break;
    }
 
    // Choose colors
 
    switch (current_working_mode) {
    case WORKING_MODE_SET_TIME_HOURS:
    case WORKING_MODE_SET_TIME_MINUTES:
    case WORKING_MODE_CLOCK:
        for (uint8_t i = 0; i < 4; i++) {
            if (current_setting_1_value == 16) {
                digit_color[i] = Adafruit_NeoPixel::ColorHSV(degree_hue_to_uint16_hue(i * 48 + delta_hue[i]), 255, digit_brightness[i]);
                if (current_setting_3_value == DIGIT_ANIMATION_FADE)
                    digit_previous_color[i] = Adafruit_NeoPixel::ColorHSV(degree_hue_to_uint16_hue(i * 48 + delta_hue[i]), 255, digit_previous_brightness[i]);
            } else {
                digit_color[i] = nth_preset_color(current_setting_1_value, delta_hue[i], digit_brightness[i]);
                if (current_setting_3_value == DIGIT_ANIMATION_FADE)
                    digit_previous_color[i] = nth_preset_color(current_setting_1_value, delta_hue[i], digit_previous_brightness[i]);
            }
        }
        break;
    case WORKING_MODE_MENU_1:
    case WORKING_MODE_MENU_2:
    case WORKING_MODE_MENU_3:
        for (uint8_t i = 0; i < 4; i++)
            if (target_setting_1_value == 16)
                digit_color[i] = Adafruit_NeoPixel::ColorHSV(degree_hue_to_uint16_hue(i * 48 + delta_hue[i]), 255, digit_brightness[i]);
            else
                digit_color[i] = nth_preset_color(target_setting_1_value, delta_hue[i], digit_brightness[i]);
        break;
    default:
        break;
    }
 
    // Show digits
 
    switch (current_working_mode) {
    case WORKING_MODE_SET_TIME_HOURS:
    case WORKING_MODE_SET_TIME_MINUTES:
 
        if ((now_time - matrix_blinking_last_time) >= MATRIX_BLINKING_INTERVAL_MS) {
            matrix_blinking_last_time = now_time;
            matrix_blinking_state = !matrix_blinking_state;
        }
 
        if (button_up.isPressed() || button_down.isPressed())
            matrix_blinking_state = true;
 
        if (current_working_mode == WORKING_MODE_SET_TIME_HOURS) {
            matrix.setPixelColor(digit_current_led[0], matrix_blinking_state ? digit_color[0] : 0);
            matrix.setPixelColor(digit_current_led[1], matrix_blinking_state ? digit_color[1] : 0);
            matrix.setPixelColor(digit_current_led[2], digit_color[2]);
            matrix.setPixelColor(digit_current_led[3], digit_color[3]);
        } else if (current_working_mode == WORKING_MODE_SET_TIME_MINUTES) {
            matrix.setPixelColor(digit_current_led[0], digit_color[0]);
            matrix.setPixelColor(digit_current_led[1], digit_color[1]);
            matrix.setPixelColor(digit_current_led[2], matrix_blinking_state ? digit_color[2] : 0);
            matrix.setPixelColor(digit_current_led[3], matrix_blinking_state ? digit_color[3] : 0);
        }
        break;
    case WORKING_MODE_CLOCK:
        for (uint8_t i = 0; i < 4; i++) {
            matrix.setPixelColor(digit_current_led[i], digit_color[i]);
            if (current_setting_3_value == DIGIT_ANIMATION_FADE)
                matrix.setPixelColor(digit_previous_led[i], digit_previous_color[i]);
        }
        break;
    case WORKING_MODE_MENU_1:
        matrix.setPixelColor(digit_current_led[0], digit_color[0]);
        matrix.setPixelColor(digit_current_led[2], digit_color[2]);
        matrix.setPixelColor(digit_current_led[3], digit_color[3]);
        break;
    case WORKING_MODE_MENU_2:
        matrix.setPixelColor(digit_current_led[0], digit_color[0]);
        matrix.setPixelColor(digit_current_led[2], digit_color[2]);
        matrix.setPixelColor(digit_current_led[3], digit_color[3]);
        break;
    case WORKING_MODE_MENU_3:
        matrix.setPixelColor(digit_current_led[0], digit_color[0]);
        matrix.setPixelColor(digit_current_led[2], digit_color[2]);
        matrix.setPixelColor(digit_current_led[3], digit_color[3]);
        break;
    default:
        break;
    }
 
    matrix.setBrightness(255);
    matrix.show();
}
 
// 1Hz hardware timer
 
#ifdef TIME_SOURCE_MCU
ISR(TIMER1_COMPA_vect) {
    isr_counter++;
 
    if (isr_counter >= 60) {
        isr_counter = 0;
        isr_current_minute++;
    }
    if (isr_current_minute >= 60) {
        isr_current_minute = 0;
        isr_current_hour++;
    }
    if (isr_current_hour >= 24)
        isr_current_hour = 0;
 
    dot_blinking_state = !dot_blinking_state;
}
 
void start_blink_timer() {
    cli();
    TCCR1A = 0; // Set entire TCCR1A register to 0
    TCCR1B = 0; // Same for TCCR1B
    TCNT1 = 0; // Initialize counter value to 0
    // Set compare match register for 1hz increments
    OCR1A = 15624; // = (16*10^6) / (1*1024) - 1 (must be <65536)
    TCCR1B |= _BV(WGM12); // Turn on CTC mode
    TCCR1B |= _BV(CS12) | _BV(CS10); // Set CS12 and CS10 bits for 1024 prescaler
    TIMSK1 |= _BV(OCIE1A); // Enable blinking
    TIMSK1 |= _BV(OCIE1A);
    sei();
}
 
void stop_blink_timer() {
    TCCR1A = 0; // Set entire TCCR1A register to 0
    TCCR1B = 0; // Same for TCCR1B
    TCNT1 = 0; // Initialize counter value to 0
}
 
void fetch_volatile_data() {
    cli();
    current_hour = isr_current_hour;
    current_minute = isr_current_minute;
    sei();
}
 
void set_volatile_data() {
    cli();
    isr_current_hour = current_hour;
    isr_current_minute = current_minute;
    sei();
}
#endif
 
// Setup
 
void setup() {
    button_mode.begin();
    button_ok.begin();
    button_up.begin();
    button_down.begin();
 
    clock.begin();
    read_time();
    clock.set(current_hour, current_minute, 0, 3, 1, 2022, 1);
 
    read_ram_data();
 
    dot.begin();
    matrix.begin();
    dot.clear();
    dot.setBrightness(50);
    matrix.clear();
 
#ifdef TIME_SOURCE_MCU
    start_blink_timer();
#endif
}
 
void loop() {
    now_time = millis();
 
#ifdef TIME_SOURCE_MCU
    fetch_volatile_data();
#else
    read_time();
    if (now_time - dot_blinking_last_time >= 1000) {
      dot_blinking_state = !dot_blinking_state;      
      dot_blinking_last_time = now_time;
    }
#endif
 
    button_mode_handler();
    button_ok_handler();
    button_up_handler();
    button_down_handler();
    working_mode_handler();
    matrix_handler();
    dot_handler();
}

Тест светодиодов

Если вам кажется что какой-то из светодиодов на плате Nixie Clock перестал работать как положено, вы можете проверить работу всех светодиодов загрузив на плату следующий код:

TestLed.ino
#include <Adafruit_NeoPixel.h>
 
constexpr uint8_t DELAYVAL = 50;
 
Adafruit_NeoPixel matrix(40, 2, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel dot(1, 9, NEO_GRB + NEO_KHZ800);
 
void setup() {
  matrix.begin();
  dot.begin();
}
 
void loop() {
  matrix.clear();
  dot.clear();
  dot.setPixelColor(0, dot.Color(0, 150, 0));
  dot.show();
  for (int i = 0; i < 10; i++) {
    matrix.setPixelColor(i, matrix.Color(150, 150, 0));
    matrix.show();
    delay(DELAYVAL);
  }
  for (int i = 10; i < 20; i++) {
    matrix.setPixelColor(i, matrix.Color(150, 0, 150));
    matrix.show();
    delay(DELAYVAL);
  }
  for (int i = 20; i < 30; i++) {
    matrix.setPixelColor(i, matrix.Color(0, 150, 150));
    matrix.show();
    delay(DELAYVAL);
  }
  for (int i = 30; i < 40; i++) {
    matrix.setPixelColor(i, matrix.Color(0, 150, 0));
    matrix.show();
    delay(DELAYVAL);
  }
  delay(10000);
}

Ресурсы

Часы Nixie Clock в магазине