ESP32でニキシー管時計を作る part 4：プログラム完成編

# はじめに

ニキシー管時計づくりにチャレンジしてみました。

[前回](https://qiita.com/Gen-Gen-0504/items/b6a2d755978719bf9be1)、NTP と RTC による時刻の管理を行いました。
今回は、それと[前々回](https://qiita.com/Gen-Gen-0504/items/8547e912f89edc2cce9d)のニキシー管のダイナミック点灯のプログラムと組み合わせ、ニキシー管時計プログラムを完成させます。


>他の記事はこちらから
>- [part 0：事前学習＆準備編](https://qiita.com/Gen-Gen-0504/items/2e57ea467f9f76910bff)
>- [part 1：昇圧チョッパ編](https://qiita.com/Gen-Gen-0504/items/9f885674f90cadf54797)
>- [part 2：ニキシー管点灯編](https://qiita.com/Gen-Gen-0504/items/8547e912f89edc2cce9d)
>- [part 3：NTPとRTCで時間管理編](https://qiita.com/Gen-Gen-0504/items/b6a2d755978719bf9be1)
>- part 4：プログラム完成編
>- part 5：comming soon...(？)


# 回路図

回路図はこんな感じです。


>2025/06/02 追記
VDDとVBATに0.1μFのバイパスコンデンサを追加。
VBATに電流制限抵抗を追加。
SCL , SDA , /INT にプルアップ抵抗を追加。
値は全てアプリケーションマニュアルを参考にしました。


<img width="900" alt="part004-RTC＋ニキシー管回路図_修正版_20250602.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821099/a221e3c2-05bc-4e69-93a3-94afdae9b379.png">


前回と前々回の回路をそのまま合体させただけです。例のごとくニキシー管は 2 つまでしか描いていません。

作った回路はこんな感じ。
<img width="500" alt="part003-回路.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821099/ead119b9-cabc-8851-d2cc-5952eb3ef17c.jpeg">


# ニキシー管時計プログラム

以下が作成したプログラムです。
前回までで作成した NTP.h 、RTC.h および RTC.cpp もそのまま使用します（これらのファイルの説明は[前回の記事](https://qiita.com/Gen-Gen-0504/items/b6a2d755978719bf9be1)）。
（ [GitHub のリンク](https://github.com/Gen-Gen-0504/nixie-clock/tree/master/nixie_clock)）


```cpp:NixieClock.h
#pragma once
#include <Arduino.h>
#include <unordered_map>
#include "Nixie.h"

/* 時計の桁を表す列挙型 */
enum class Digit : uint8_t {
  HR_L, HR_R, MIN_L, MIN_R
};

/* Digitの各要素と、任意の型のデータを紐づける辞書型のエイリアステンプレート */
template<typename T>
using dgt_t_umap = std::unordered_map<Digit, T>;

/* 4つのニキシー管を制御するクラス */
class NixieClock {
  private:
    constexpr static uint32_t  ON_TIME_MICRO = 4000; // オン時間
    constexpr static uint32_t OFF_TIME_MICRO =  380; // オフ時間
    uint32_t switched_time; // 点灯消灯の切り替え時刻
    bool     on_flag;       // 点灯フラグ
    dgt_t_umap<Nixie>                 dgt_nix_umap; // 4桁分のニキシー管を扱うunordered_map
    dgt_t_umap<Nixie>::const_iterator dgt_nix_iter; // 現在扱っている桁

  public:
    NixieClock(const dgt_t_umap<NixiePinInfo> _dgt_pinf_umap); // コンストラクタ
    void setup();                                              // 初期設定
    void setTime(const struct tm *tm);                         // すべての管に時刻をセット
    void lightup();                                            // すべての管を点灯
    void shuffle(const uint8_t n);                             // シャッフル点灯
};
```


```cpp:NixieClock.cpp
#include <random>
#include "NixieClock.h"

/* コンストラクタ */
NixieClock::NixieClock(const dgt_t_umap<NixiePinInfo> _dgt_pinf_umap) :
  dgt_nix_umap{
    {Digit::HR_L,  Nixie(_dgt_pinf_umap.at(Digit::HR_L))},
    {Digit::HR_R,  Nixie(_dgt_pinf_umap.at(Digit::HR_R))},
    {Digit::MIN_L, Nixie(_dgt_pinf_umap.at(Digit::MIN_L))},
    {Digit::MIN_R, Nixie(_dgt_pinf_umap.at(Digit::MIN_R))},
  }
{}

/* 初期設定 */
void NixieClock::setup() {
  dgt_nix_iter = dgt_nix_umap.begin(); // 最初の桁
  dgt_nix_iter->second.lightOn();      // 点灯
  switched_time = micros();            // 点灯時刻を記録
  on_flag = true;                      // フラグを立てる
}

/* すべての管に時刻をセット */
void NixieClock::setTime(const struct tm *tm) {
  dgt_nix_umap.at(Digit::HR_L ).setNum(tm->tm_hour/10);
  dgt_nix_umap.at(Digit::HR_R ).setNum(tm->tm_hour%10);
  dgt_nix_umap.at(Digit::MIN_L).setNum(tm->tm_min/10);
  dgt_nix_umap.at(Digit::MIN_R).setNum(tm->tm_min%10);
}

/* すべての管を点灯 */
void NixieClock::lightup() {
  if (on_flag) { // オン時
    if (micros() - switched_time > ON_TIME_MICRO) { // 点灯時間終了
      dgt_nix_iter->second.lightOff(); // 消灯
      on_flag = false;                 // フラグを降ろす
      switched_time = micros();        // 切り替え時刻更新
    }
  }
  else { // オフ時
    if (micros() - switched_time > OFF_TIME_MICRO) { // 消灯時間終了
      if(++dgt_nix_iter == dgt_nix_umap.end()){
        dgt_nix_iter = dgt_nix_umap.begin();
      }
      dgt_nix_iter->second.lightOn(); // 点灯
      on_flag = true;                 // フラグを立てる
      switched_time = micros();       // 切り替え時刻更新
    }
  }
}

/* シャッフル点灯 */
void NixieClock::shuffle(const uint8_t n = 1) {
  const uint8_t  NUM_OF_NUMBERS     = 10;
  const uint32_t SHUFFLE_SPAN_MILLI = 30;
  uint32_t previous;
  static dgt_t_umap<std::array<uint8_t, NUM_OF_NUMBERS>> dgt_ary_umap = { 
    {Digit::HR_L,  {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
    {Digit::HR_R,  {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
    {Digit::MIN_L, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
    {Digit::MIN_R, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
  }; // ランダム表示させる数列のunordered_map

  std::random_device rd; // 乱数生成エンジン
  std::mt19937 mt(rd()); // 疑似乱数生成エンジン
  for(uint8_t _n = 0; _n < n; ++_n) {
    for(auto& [dgt, ary] : dgt_ary_umap){
      std::shuffle(ary.begin(), ary.end(), mt); // 数列をシャッフル
    }
    for(uint8_t i = 0; i < NUM_OF_NUMBERS; ++i) {
      for(auto& [dgt, ary] : dgt_ary_umap){
        dgt_nix_umap.at(dgt).setNum(ary[i]);
      }
      previous = millis();
      while(millis() - previous < SHUFFLE_SPAN_MILLI) {
        lightup();
      }
    }
  }
}
```

```cpp:nixie_clock
#include "NTP.h"
#include "RTC.h"
#include "NixieClock.h"

const char*   SSID = "YOUR_SSID";     // SSID
const char*   PASS = "YOUR_PASSWORD"; // パスワード
const uint8_t PIN_SCL         = 32;   // I2C通信用のピン
const uint8_t PIN_SDA         = 33;   //
const uint8_t PIN_N_INT       = 27;   // 割り込みを検知するピン
const uint8_t PIN_ANODE_HR_L  = 23;   // 4つのニキシー管のアノードのピン
const uint8_t PIN_ANODE_HR_R  = 22;   //
const uint8_t PIN_ANODE_MIM_L = 21;   //
const uint8_t PIN_ANODE_MIM_R = 19;   //
const uint8_t PIN_CATHODE_A   = 18;   // カソードの制御ピン
const uint8_t PIN_CATHODE_B   = 16;   //
const uint8_t PIN_CATHODE_C   = 4;    //
const uint8_t PIN_CATHODE_D   = 17;   //

NTP ntp(SSID, PASS); // NTPクラスのインスタンスを生成

const RTCPinInfo rtcPinInfo {
  PIN_SCL,
  PIN_SDA,
  PIN_N_INT
};
RTC rtc(rtcPinInfo); // RTCクラスのインスタンスを生成

const dgt_t_umap<NixiePinInfo> dgt_pinf_umap = {
  {Digit::HR_L,  {PIN_ANODE_HR_L,  PIN_CATHODE_A, PIN_CATHODE_B, PIN_CATHODE_C, PIN_CATHODE_D}}, // 時の左側
  {Digit::HR_R,  {PIN_ANODE_HR_R,  PIN_CATHODE_A, PIN_CATHODE_B, PIN_CATHODE_C, PIN_CATHODE_D}}, // 時の右側
  {Digit::MIN_L, {PIN_ANODE_MIM_L, PIN_CATHODE_A, PIN_CATHODE_B, PIN_CATHODE_C, PIN_CATHODE_D}}, // 分の左側
  {Digit::MIN_R, {PIN_ANODE_MIM_R, PIN_CATHODE_A, PIN_CATHODE_B, PIN_CATHODE_C, PIN_CATHODE_D}}, // 分の左側
};
NixieClock nixie_clock(dgt_pinf_umap); // NixieClockクラスのインスタンスを生成

bool tick_flag = false;                  // 時間更新割り込み発生フラグ
void setupTickFlag() {tick_flag = true;} // 時間更新割り込み時に実行する関数
struct tm tm;                            // 時刻保存用の構造体


void setup() {
  ntp.setup();
  rtc.setup();
  rtc.setISR(setupTickFlag);
  if(ntp.getIsConfigured()) { // Wi-Fi接続成功時
    ntp.getTime(&tm);
    rtc.setDateTime(&tm); // RTCに時刻を保存
    ntp.disconnect();
  }
  else { // Wi-Fi接続失敗時
    // 何もしない
  }
  
  nixie_clock.setup();
  nixie_clock.shuffle(3);
  rtc.getDateTime(&tm);
  nixie_clock.setTime(&tm);
}

void loop() {
  nixie_clock.lightup();

  if(tick_flag) { // 割り込み発生（「分」が更新）
    rtc.getDateTime(&tm);
    if(tm.tm_min == 0) { // 「時」が変わったらシャッフル
      nixie_clock.shuffle(1);
    }
    nixie_clock.setTime(&tm);
    tick_flag = false; // フラグを降ろす
  }
}
```

***

NixieClock クラスは、[前々回](https://qiita.com/Gen-Gen-0504/items/8547e912f89edc2cce9d)の NixieController クラスを少し書き換えたものです。
具体的には 2 つの関数が異なります。

まず setNums() 関数が setTime() 関数に代わっています。
この関数は構造体 tm を受け取り、その時と分で 4 つのニキシー管の表示する数字を書き換えます。

```cpp
/* すべての管に時刻をセット */
void NixieClock::setTime(const struct tm *tm) {
  dgt_nix_umap.at(Digit::HR_L ).setNum(tm->tm_hour/10);
  dgt_nix_umap.at(Digit::HR_R ).setNum(tm->tm_hour%10);
  dgt_nix_umap.at(Digit::MIN_L).setNum(tm->tm_min/10);
  dgt_nix_umap.at(Digit::MIN_R).setNum(tm->tm_min%10);
}
```

もう 1 つは shuffle() 関数です。完全に趣味のために追加しました。

```cpp
/* シャッフル点灯 */
void NixieClock::shuffle(const uint8_t n = 1) {
  const uint8_t  NUM_OF_NUMBERS     = 10;
  const uint32_t SHUFFLE_SPAN_MILLI = 30;
  uint32_t previous;
  static dgt_t_umap<std::array<uint8_t, NUM_OF_NUMBERS>> dgt_ary_umap = { 
    {Digit::HR_L,  {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
    {Digit::HR_R,  {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
    {Digit::MIN_L, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
    {Digit::MIN_R, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
  }; // ランダム表示させる数列のunordered_map

  std::random_device rd; // 乱数生成エンジン
  std::mt19937 mt(rd()); // 疑似乱数生成エンジン
  for(uint8_t _n = 0; _n < n; ++_n) {
    for(auto& [dgt, ary] : dgt_ary_umap){
      std::shuffle(ary.begin(), ary.end(), mt); // 数列をシャッフル
    }
    for(uint8_t i = 0; i < NUM_OF_NUMBERS; ++i) {
      for(auto& [dgt, ary] : dgt_ary_umap){
        dgt_nix_umap.at(dgt).setNum(ary[i]);
      }
      previous = millis();
      while(millis() - previous < SHUFFLE_SPAN_MILLI) {
        lightup();
      }
    }
  }
}
```

各桁にそれぞれ 0 ～ 9 が入れられた配列をあてがい、配列内をランダムに入れ替えます。
30 ミリ秒の間隔でそれらを順に表示し、これを引数で指定された回数繰り返します。

***

nixie_clock では、これまでの集大成として NTP クラス、RTC クラス、NixieClock クラスそれぞれのインスタンスを生成し、適宜機能を呼び出しています。

プログラムの機能は以下のような感じです。

- 電源投入直後
    - ネットワークに接続
        - 成功：NTP により ESP32 の時刻合わせを行う
        - 失敗：何もしない
    - RTC に時刻を保存
        - ネットワーク接続成功時：現在時刻に更新
        - ネットワーク接続失敗時：すでに保存された時刻のまま
    - ニキシー管を 3 回シャッフル点灯
    - ニキシー管の表示時刻を更新
- メインループ
    - ニキシー管を点灯
    - 割り込み（分の更新）を監視
- 割り込み発生時
    - ニキシー管の表示時刻を更新
    - 更新後の分が「 0 」（時も変更）の場合、シャッフル点灯

# 動作確認

実行してみます。
スマホの画面に表示されているのは、例のごとく [NICT の日本標準時](https://www.nict.go.jp/JST/JST5.html)です。


#### 電源投入直後

<iframe width="100%" height="400" src="https://www.youtube.com/embed/-4HPKojSmEg" title="ニキシー管時計 電源投入" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

しっかり時刻の取得ができています。
なぜシャッフルするのか？ ***かっこいいから***。

#### 分の更新

<iframe width="100%" height="400" src="https://www.youtube.com/embed/MNEX-Bo4EQE" title="ニキシー管時計 分の更新" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

分の更新もばっちりです。
遅延も見られず、NTP によって精度よく時刻を取得できていることが分かります。


#### 時の更新

<iframe width="100%" height="400" src="https://www.youtube.com/embed/ErmzhCqlchw" title="ニキシー管時計 時の更新" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

こちらのシャッフルも問題ないですね。
なぜシャッフルするのか？ かっk


# おわりに

これは...「「 時計 」」...！

次回から、地獄の本体設計編が開幕します。

次回：comming soon...(？)