はじめに
ニキシー管時計づくりにチャレンジしてみました。
前回、12V を 170~200V にする昇圧チョッパを作りました。
今回はそれを用いてニキシー管を点灯し、デコーダ IC や ESP32 による数字の制御もしてみます。
他の記事はこちらから
- part 0:事前学習&準備編
- part 1:昇圧チョッパ編
- part 2:ニキシー管点灯編
- part 3:NTPとRTCで時間管理編
- part 4:プログラム完成編
- part 5:comming soon...(?)
ニキシー管を光らせる
単純な点灯
早速光らせます。
高圧電源さえ用意してしまえば、ニキシー管を点灯させることは非常に簡単なようです。
以下の単純な回路で光ります。
ニキシー管は一般に、アノードに高圧(今回使用する IN-12B の場合 170~200V )を印加し、対応する数字のカソードを GND に繋ぐだけで光ります。
必要な電流はかなり小さいので、大きな制限抵抗が必須になります。
IN-12B に流す電流は 3.5mA なので、オームの法則から 170V / 3.5mA ≒ 48.6kΩ の抵抗が必要です。50kΩ( 消費電力を考え 100kΩ を 2 つ並列しました)を接続します。
0~9 のカソードを手動で切り替え、順に光らせた様子です。
ようやく光った姿を見れました。めちゃくちゃ嬉しい。
光る数字を制御する
さて、時計を作るのであれば手動でピンを入れ替えるわけにはいきません。
マイコンからの出力( GPIO )で制御することが必須です。
しかし直接繋げてしまうとピンが 170V に耐えられないため、トランジスタ等によるスイッチングが必要になります。
同様にすべてのピンにトランジスタを通じて GPIO を接続し...
てもいいのですが、ニキシー管の制御にはもっと楽な方法があります。デコーダ IC「K155ID1」です。
リンク(Amazon):K155ID1
ニキシー管の数字は複数同時に光ることはないので、点灯パターンは 10 通り( 0~9 )です。つまり 4 bit( 16 通り)の入力で制御できるはずです(詳しくは part 0 を参照)。
K155ID1 はニキシー管の特性を活かして高圧を制御できる内部構造をしており、まさしくニキシー管のためのデコーダ IC と言えます。
デコーダ IC を使うことで、4 ピンで 1 つのニキシー管の光る数字を制御することができます。
(※ただし「消灯」はできないので、消す場合はアノードを制御します)
ESP32 と K155ID1 の動作を確認するため、次のような回路を作成します。
ジャンパワイヤで見づらいですが...
ESP32 に書き込むプログラムです。
( GitHub のリンク)
/* カソード制御ピン */
const int PIN_A = 32;
const int PIN_B = 25;
const int PIN_C = 26;
const int PIN_D = 33;
/* 指定した数字を点灯させる関数 */
void lightup(int n) {
digitalWrite(PIN_A, n&B0001);
digitalWrite(PIN_B, n&B0010);
digitalWrite(PIN_C, n&B0100);
digitalWrite(PIN_D, n&B1000);
}
void setup() {
pinMode(PIN_A, OUTPUT);
pinMode(PIN_B, OUTPUT);
pinMode(PIN_C, OUTPUT);
pinMode(PIN_D, OUTPUT);
digitalWrite(PIN_A, HIGH);
digitalWrite(PIN_B, HIGH);
digitalWrite(PIN_C, HIGH);
digitalWrite(PIN_D, HIGH);
}
void loop() {
/* 0~9まで順に光らせる */
for(int n = 0; n < 10; ++n) {
lightup(n);
delay(300);
}
}
ポイントは lightup() 関数です。引数に整数を受け取って、ニキシー管をその数字で点灯させます。
K155ID1 の ON になるピンは、入力 A ~ D を 2 進数 → 10 進数変換した番号のものです( LOW=0 、HIGH=1 と解釈します)。
D | C | B | A | ON になるピン番号 |
---|---|---|---|---|
L | L | L | L | 0 |
L | L | L | H | 1 |
L | L | H | L | 2 |
L | L | H | H | 3 |
L | H | L | L | 4 |
L | H | L | H | 5 |
L | H | H | L | 6 |
L | H | H | H | 7 |
H | L | L | L | 8 |
H | L | L | H | 9 |
なので引数 n を B0001~B1000 でマスクすれば、A~D の HIGH( 1 )と LOW( 0 )が得られます。
連続点灯させた様子です。
スムーズに切り替わっています!
複数の管を制御する(ダイナミック点灯)
最後に、複数のニキシー管をダイナミック点灯で制御します。
(ダイナミック点灯についても part 0 で少し解説しています)
ざっくりまとめれば、複数のニキシー管のうち、1 つの管のアノードだけに電圧を印加することで、カソード側の回路を共用する方式です。
高速に切り替えればすべての管が同時に光っているように見えますが、実際は 1 つの管しか光っていないので、回路の小型化と省電力化につながります。
回路が小さくなるのがメリットとだけあり、回路図はかなり簡単です。
(実際のニキシー管は 4 つですが、2 つまでしか書いていません)
フォトカプラには TLP627-4 を使います。
入力側の抵抗の計算方法です。
次の表はデータシート(上記リンクからダウンロード可能)より転載しています。
電気的特性の一覧から、発光側の順電圧 VF を標準の 1.15V とします。電源が 3.3V なので、抵抗の両端電圧は 3.3 - 1.15 = 2.15V です。
推奨される順電流 IF = 16mA 以上を流すため、オームの法則から 2.15V / 16mA ≒ 134Ω くらいの抵抗を使います。
今回は 120Ω 抵抗をつなぎ、18mA 程度を流すことにしました。
制作した回路です。仕方が無いとはいえジャンパワイヤが生い茂っています。
プログラムは 4 つのファイルに分かれています。
( GitHub のリンク)
書き込むと、4 つのニキシー管に、0 ~ 9 の数字が順に流れていくように表示されます。
( 0123 → 1234 → 2345 → ... → 6789 → 7890 → 8901 → 9012 → 0123 → ... )
#pragma once
#include <Arduino.h>
/* ピン情報をまとめる構造体 */
struct NixiePinInfo {
uint8_t anode;
uint8_t cathode_a;
uint8_t cathode_b;
uint8_t cathode_c;
uint8_t cathode_d;
};
/* ニキシー管を表すクラス */
class Nixie {
private:
const NixiePinInfo PIN; // ピン情報
uint8_t num; // 表示する数字
public:
/* コンストラクタ */
Nixie(NixiePinInfo _pin_info): PIN(_pin_info), num(0) {
pinMode(PIN.anode, OUTPUT);
pinMode(PIN.cathode_a, OUTPUT);
pinMode(PIN.cathode_b, OUTPUT);
pinMode(PIN.cathode_c, OUTPUT);
pinMode(PIN.cathode_d, OUTPUT);
digitalWrite(PIN.anode, HIGH);
digitalWrite(PIN.cathode_a, HIGH);
digitalWrite(PIN.cathode_b, HIGH);
digitalWrite(PIN.cathode_c, HIGH);
digitalWrite(PIN.cathode_d, HIGH);
}
/* 数字を設定 */
void setNum(uint8_t _num) {
num = _num;
}
/* 点灯 */
void lightOn() const {
digitalWrite(PIN.cathode_a, num & B0001);
digitalWrite(PIN.cathode_b, num & B0010);
digitalWrite(PIN.cathode_c, num & B0100);
digitalWrite(PIN.cathode_d, num & B1000);
digitalWrite(PIN.anode, LOW);
}
/* 消灯 */
void lightOff() const {
digitalWrite(PIN.anode, HIGH);
}
};
#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 NixieController {
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:
NixieController(const dgt_t_umap<NixiePinInfo> _dgt_pinf_umap); // コンストラクタ
void setup(); // 初期設定
void setNums(const dgt_t_umap<uint8_t> dgt_num_umap); // すべての管に数字をセット
void lightup(); // すべての管を点灯
};
#include "NixieController.h"
/* コンストラクタ */
NixieController::NixieController(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 NixieController::setup() {
dgt_nix_iter = dgt_nix_umap.begin(); // 最初の桁
dgt_nix_iter->second.lightOn(); // 点灯
switched_time = micros(); // 点灯時刻を記録
on_flag = true; // フラグを立てる
}
/* すべての管に数字をセットする */
void NixieController::setNums(const dgt_t_umap<uint8_t> dgt_num_umap) {
for (auto [dgt, num] : dgt_num_umap) {
dgt_nix_umap.at(dgt).setNum(num);
}
}
/* すべての管を点灯させる */
void NixieController::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(); // 切り替え時刻更新
}
}
}
#include "NixieController.h"
/* ニキシー管のピン情報 */
const uint8_t PIN_ANODE_HR_L = 23; // それぞれの管のアノードのピン
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; //
/* すべての管のピン情報をまとめたmap */
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}}, // 分の左側
};
NixieController nixie_controller(dgt_pinf_umap); // コントローラ生成
dgt_t_umap<uint8_t> dgt_num_umap = { // 表示する数字のunordered_map
{Digit::HR_L, 0},
{Digit::HR_R, 1},
{Digit::MIN_L, 2},
{Digit::MIN_R, 3}
};
uint32_t prev_time;
const uint32_t DURATION = 1000;
void setup() {
nixie_controller.setup();
nixie_controller.setNums(dgt_num_umap); // 数字を設定
prev_time = millis();
}
void loop() {
nixie_controller.lightup(); // 光らせる
if (millis() - prev_time > DURATION) { // 一定時間が経過していれば
for(auto [dgt, num] : dgt_num_umap){
dgt_num_umap.at(dgt) = (num + 1) % 10; // 数字を更新
}
nixie_controller.setNums(dgt_num_umap); // 数字を設定
prev_time = millis();
}
}
なんか長い。
ダイナミック点灯では回路のシンプルさをプログラムがカバーするので、ある程度長くなるのは仕方がありません(それにしても機能の少なさのわりに長いとは思います)。
順番に解説していきます。
Nixie.h
ニキシー管 1 つを表す Nixie クラスを定義しているファイルです。(短いので実装も同じファイルに書いてます)
まず最初に、1 つのニキシー管のピン情報をまとめる構造体 NixiePinInfo を宣言しています。
後に説明する、1 つのニキシー管を表す Nixie クラスがメンバとしてこの構造体を持ちます。
ダイナミック点灯なら全ニキシー管でカソードピン A ~ D は同じ値になりますが、一応今後スタティック点灯にするかもしれないので、念のため各々の管がこの構造体を持つようにしています。
/* ピン情報をまとめる構造体 */
struct NixiePinInfo {
uint8_t anode;
uint8_t cathode_a;
uint8_t cathode_b;
uint8_t cathode_c;
uint8_t cathode_d;
};
クラス Nixie は、1 つのニキシー管を表すクラスです。
/* ニキシー管を表すクラス */
class Nixie {
private:
const NixiePinInfo PIN; // ピン情報
uint8_t num; // 表示する数字
public:
/* コンストラクタ */
Nixie(NixiePinInfo _pin_info): PIN(_pin_info), num(0) {
pinMode(PIN.anode, OUTPUT);
pinMode(PIN.cathode_a, OUTPUT);
pinMode(PIN.cathode_b, OUTPUT);
pinMode(PIN.cathode_c, OUTPUT);
pinMode(PIN.cathode_d, OUTPUT);
digitalWrite(PIN.anode, HIGH);
digitalWrite(PIN.cathode_a, HIGH);
digitalWrite(PIN.cathode_b, HIGH);
digitalWrite(PIN.cathode_c, HIGH);
digitalWrite(PIN.cathode_d, HIGH);
}
/* 数字を設定 */
void setNum(uint8_t _num) {
num = _num;
}
/* 点灯 */
void lightOn() const {
digitalWrite(PIN.cathode_a, num & B0001);
digitalWrite(PIN.cathode_b, num & B0010);
digitalWrite(PIN.cathode_c, num & B0100);
digitalWrite(PIN.cathode_d, num & B1000);
digitalWrite(PIN.anode, LOW);
}
/* 消灯 */
void lightOff() const {
digitalWrite(PIN.anode, HIGH);
}
};
1 つのニキシー管を点灯させた際のコードに少し手を加えてクラス化した感じです。
メンバ変数としてピン情報構造体 NixiePinInfo 、自身が表示する数字 num を持ち、それぞれの値に従って点灯や消灯をする関数を持ちます。
コンストラクタは構造体 NixiePinInfo を引数に取り、メンバ PIN を初期化します。
前のプログラムと比較すると、ダイナミック点灯のためのアノードの制御が加わっています。
点灯時にはアノードピンを LOW にして、消灯させるときはアノードピンを HIGH にします。
NixieController.h と NixieController.cpp
これらのファイルは、4 つのニキシー管を扱う NixieController クラスを定義しています。
まずヘッダファイルで、「桁」を表す列挙型 Digit と、それに基づくエリアステンプレート dgt_t_umap を定義しています。
/* ニキシー管の桁を表す列挙型 */
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>;
enum class やエリアステンプレート、unordered_map については、多くの解説記事があるため説明を省きます。
いずれもそこまで深く知っていなくて大丈夫です(自分もまだまだ使いこなせないので...)
とりあえず重要なのは、定義した dgt_t_umap という型が、列挙型 Digit の 4 つの要素をキーとして、それぞれに対応する任意の型のデータを保持できるということです。
つまり、dgt_t_umap は「 4 つの桁に対応するデータ」全般を扱うためのテンプレートです。例えば 4 つのニキシー管そのもの、あるいは各桁に点灯させる数字の集合、などなど。
列挙型 Digit は、この dgt_t_umap にアクセスするためだけに使います。
HR = 時( HouR )
MIM = 分( MINute )
L = 左( Left )
R = 右( Right )
を表します。
次に定義しているのが今回の目玉である NixieController クラスです。このクラスでは 4 つのニキシー管をまとめて制御します。
/* 4つのニキシー管を制御するクラス */
class NixieController {
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:
NixieController(const dgt_t_umap<NixiePinInfo> _dgt_pinf_umap); // コンストラクタ
void setup(); // 初期設定
void setNums(const dgt_t_umap<uint8_t> dgt_num_umap); // すべての管に数字をセット
void lightup(); // すべての管を点灯
};
このクラスの中心となるのは、dgt_t_umap による 4 つのニキシー管を格納する連想配列 dgt_nix_umap と、そのイテレータ dgt_nix_iter です。
コンストラクタではこの dgt_nix_umap を、NixiePinInfo 構造体をデータに持つ dgt_t_umap から初期化します。
/* コンストラクタ */
NixieController::NixieController(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))},
}
{}
初期化子リストの部分がすごいことになっていますが、
・NixiePinInfo 構造体を unordered_map から取り出し( _dgt_pinf_umap.at( Digit 型のキー) )
・それを Nixie クラスのコンストラクタに与えてインスタンス化( Nixie( 構造体 NixiePinInfo ) )
・そしてそれを dgt_nix_umap の各桁のキーに登録
という処理を 4 つやっているだけです。
これで、dgt_nix_umap に Digit のキーでアクセスすると、対応する Nixie クラスのインスタンスにアクセスできます。
ダイナミック点灯ではこの Nixie クラスのインスタンスを次々と処理したいので、イテレータ dgt_nix_iter を用います。
注意が必要なのは、この dgt_nix_iter が指すのは Nixie クラスのインスタンスでなく、列挙型 Digit のキーとのペア(コンテナでいう pair )です。dgt_nix_iter->first とすると Digit のキー、dgt_nix_iter->second とすると Nixie クラスのデータにアクセスできます。
さて、このクラスのメインの処理である、4 つのニキシー管を順に光らせる lightup() 関数を見ていきます。
/* すべての管を点灯させる */
void NixieController::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(); // 切り替え時刻更新
}
}
}
すべての管を順に光らせるだけなのに、条件分岐がいくつもあって少し複雑です。
確かにダイナミック点灯ですべての管を光らせるだけなら、HR_L を光らせて少し待機、次に HR_R を光らせて少し待機...と書いておけばいいだけです。例えばこんな感じです。
void lightup() {
for (dgt_nix_iter = dgt_nix_umap.begin(); dgt_nix_iter != dgt_nix_umap.end(); ++dgt_nix_iter){
dgt_nix_iter->second.lightOn(); // 点灯
delayMicroseconds(ON_TIME_MICRO); // 待つ
dgt_nix_iter->second.lightOff(); // 消灯
delayMicroseconds(OFF_TIME_MICRO); // 待つ
}
}
unordered_map の範囲 for 文を使えばもっとシンプルに書けます。
void lightup() {
for (auto [dgt, nix] : dgt_nix_umap){
nix.lightOn(); // 点灯
delayMicroseconds(ON_TIME_MICRO); // 待つ
nix.lightOff(); // 消灯
delayMicroseconds(OFF_TIME_MICRO); // 待つ
}
}
しかし、気になるのは delayMicroseconds() 関数です。
delay() 関数などはマイコン内部の処理まで止めてしまうので、割り込み処理との相性が良くありません。
今回の最終目標は時計であり、点灯や消灯をして待っている間にも時間は変化します。
その変化を割り込みで感知して表示の更新を行いたいので、処理を止めてしまう delay() 関数などは使えません。
もっと言えば、1 つの関数内で長い繰り返し文を回すことなども避けたいです。
そのため、点灯(消灯)からの経過時間をメンバ変数で記録し、「あるニキシー管を点灯(消灯)し続けるか判断する」という短い処理を、loop() 内で繰り返すことを考えました。
元のプログラムの lightup() 関数をフローチャートに直したものがこちらです。
まず、現在扱っている管が点灯しているか消灯しているかで処理を分岐します。
点灯している場合、一定時間点灯したかを判断し、経過していれば消灯をします。
消灯している場合、一定時間点灯していれば扱う管を別の管に変え、その管を点灯させます。
これを繰り返し、lightup() 関数をなるべく早く抜けるようにしています。
NixieController クラスのメンバ関数は、あとは初期設定を行う setup() 関数と
/* 初期設定 */
void NixieController::setup() {
dgt_nix_iter = dgt_nix_umap.begin(); // 最初の桁
dgt_nix_iter->second.lightOn(); // 点灯
switched_time = micros(); // 点灯時刻を記録
on_flag = true; // フラグを立てる
}
すべての管に数字を設定する setNums() 関数です。
/* すべての管に数字をセットする */
void NixieController::setNums(const dgt_t_umap<uint8_t> dgt_num_umap) {
for (auto [dgt, num] : dgt_num_umap) {
dgt_nix_umap.at(dgt).setNum(num);
}
}
setNums() は、引数として Digit と uint8_t を結び付けた dgt_t_umap を受け取り、範囲 for 文で dgt_nix_umap の setNum() を呼び出しています。
nxie_dynamic
これまでに定義したクラスの実体などを使ったメインの処理です。
まず、ニキシー管につながるピンを定義します。
/* ニキシー管のピン情報 */
const uint8_t PIN_ANODE_HR_L = 23; // それぞれの管のアノードのピン
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; //
それらを使って NixiePinInfo 構造体の dgt_t_umap を作ります。
/* すべての管のピン情報をまとめたmap */
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}}, // 分の左側
};
さらにその dgt_t_umap を使って NixieController クラスのインスタンスを生成します。
NixieController nixie_controller(dgt_pinf_umap); // コントローラ生成
あとは、4 つのニキシー管に表示する数字をまとめた dgt_t_umap や諸々の変数を定義して
dgt_t_umap<uint8_t> dgt_num_umap = { // 表示する数字のunordered_map
{Digit::HR_L, 0},
{Digit::HR_R, 1},
{Digit::MIN_L, 2},
{Digit::MIN_R, 3}
};
uint32_t prev_time;
const uint32_t DURATION = 1000;
setup() 関数で初期設定
void setup() {
nixie_controller.setup();
nixie_controller.setNums(dgt_num_umap); // 数字を設定
prev_time = millis();
}
loop() 関数では、ニキシー管の点灯と、一定時間( DURATION = 1000ms = 1s )ごとに dgt_num_umap の各要素を 1 ずつ増やす処理をしています。
void loop() {
nixie_controller.lightup(); // 光らせる
if (millis() - prev_time > DURATION) { // 一定時間が経過していれば
for(auto [dgt, num] : dgt_num_umap){
dgt_num_umap.at(dgt) = (num + 1) % 10; // 数字を更新
}
nixie_controller.setNums(dgt_num_umap); // 数字を設定
prev_time = millis();
}
}
やはりクラスを使うことで、loop() 内などの処理は簡単になります。
時計システムを作るときも、基本的にはこのクラスがそのまま使えると思います。
さて、実際に点灯させた様子です。
完璧...!
スタティックな点灯よりはやはりだいぶ暗いですが、それでも 4 桁分の点灯は見ていて楽しいです。
おわりに
やはり上手くまとめられない...
久々にプログラムを書きました(情報系なのに)。
なぜか次回も半分くらいは回路をやっているような気がします。
次回は NTP と RTC による時間の取得と保存にチャレンジします。
次回:ESP32でニキシー管時計を作る part 3:NTPとRTCで時間管理編
回路図などの参考:「最新のチップで動かすニキシー管 増補版」(黒井宏一)
あと、ダイナミック点灯のプログラムにおいて、本文中では脱線しすぎるので言わなかったことを下にまとめておきます。
テンプレート dgt_t_umap を作った理由
簡単に言えば、リストや配列を使うことで生じる、番号や順番の暗黙のルールが嫌いだから、です。
詳しく説明します。
NixieController クラスを書き始めて、最初に問題になったのが Nixie クラス 4 つをどんなデータ構造で管理するのか、でした。
ダイナミック点灯ではすべての管を順に処理するため、最初にリスト( std::list )で書いてみました。
Nixie クラス 4 つのデータは難なくリストに格納できました。ニキシー管に 1 つずつ順にアクセスして光らせる、というだけなら格納された順番は関係ありません。
しかし、4 つの管に対応する数字やピン情報といったデータはそういうわけにはいきません。その格納された順番がニキシー管とずれてしまうと問題が生じるからです。
結局、Nixie クラスや様々なデータのリストに、「時左(begin)→時右→分左→分右」という順番があることを暗黙のルールとし、データをその順番に格納したり取り出したりしました。
それに嫌気がさして配列( std::array )も検討しましたが、結局ダメでした。
列挙型を使えば添字に意味を持たせることができるため、ニキシー管とそれに対応するデータに桁を意識してアクセスできます。
しかし、列挙型で問題となるのは範囲 for 文との相性の悪さです。というか、実はそもそも列挙型を範囲 for 文で回すことができません(これは非常に意外でした)。せいぜい、列挙型の全要素を持つ配列を定義し、それを範囲で回す、という感じです。
そうなればやはり配列の本来の使い方である「番号」でのアクセスに落ち着き、桁での管理が希薄になります。
汚いしダサいしもう嫌...そしてたどり着いたのが map です。
列挙型をキーとする map を使うことで、桁を中心とした書き方ができました。またテンプレート化し、ほかのあらゆる型のデータにも適用できます。
なお定義で列挙型をキーに指定すれば、その列挙型以外でアクセスはできないため安全で、範囲 for 文との相性も良いです。
そんなわけで、プログラムは長くなりますし、いちいち「 Digit::○○ 」などとしてアクセスしなければなりませんが、代わりに自分好みの「桁」を意識したアクセスが徹底できました。
安全性とか可読性とか、他の人から見てどうなのかはわかりませんので、やはりこれは私の趣味、と言わざるを得ません。
「消灯」を維持する時間
さて、NixieController クラスの lightup() 関数では、1 つの管を 4000μs 点灯させた後、消灯させ、380μs それを維持します。
つまりある管を点灯させた後、ニキシー管が 1 つも光っていない状態が必ず 380μs 続くわけです。
理由の説明は後にして、とりあえずこの消灯時間を 0s にして実行してみましょう。
constexpr static uint32_t ON_TIME_MICRO = 4000; // オン時間
constexpr static uint32_t OFF_TIME_MICRO = 0; // オフ時間
先ほどの消灯時間が 380μs の時と比べると、違いは一目瞭然です。
消灯状態を維持していない下の方の写真では、
0 の管に「 3 」
1 の管に「 0 」
2 の管に「 1 」
3 の管に「 2 」
が薄く表示されています。
これが、ニキシー管をダイナミック点灯する際によくみられてしまう「ゴースト」と呼ばれる現象です(名前はカッコ良い)。
ゴーストは、ダイナミック点灯による高速な切り替えに、アノード電圧の立ち下がりが付いていけないことによって生じます。
先ほどの画像をよく見ると、ゴーストで表示されている数字は、自身の左隣の管(左端の管だけは 1 番右の管)の数字です。
アノードピンへの 170V の印加は 4000μs という短い間隔で切り替わりますが、電圧の印加を止めた後すぐに 0V になるわけではありません。
ダイナミック点灯ではカソードを共用しているため、前の管のアノード電圧が下がりきっていない状態で次の管を点灯させてしまうと 2 つの管が同じ数字に光ってしまいます。
これがゴーストの発生原因です。
上の画像では、例えば「 1 」の管の点灯が終わり、「 1 」の管のアノードの電圧が高いまますぐに「 0 」の管を点灯してしまったため、「 1 」の管にも「 0 」が表示されてしまっています。
(ゴーストの現れ方から、どうやら unordered_map のイテレータは MIN_R→MIN_L→HR_R→HR_L の順に切り替わっていることが分かります。てっきり逆だと思っていましたが、ゴーストのおかげで気づけて面白かったです)
もうお分かりかと思いますが、ゴーストを防ぐ方法は「待つ」ことです。消灯後、アノードピンの電圧が下がりきるまで待つしかありません。
実験の結果、大体 380μs くらい待てば気にならない程度になったので、そうしています。
それとついでに、4000μs のほうをどのように決めたかも書いておきます。
まずは人間の目の時間的分解能を考えました。
例えば西日本の蛍光灯は 60Hz 、東日本の蛍光灯は 50Hz で点滅を繰り返していますが、私の目には常に光っているように見えます。
ダイナミック点灯においても 1 つの管を 1 秒間に 50 ~ 60 回ほど点灯させなければならず、これを下回るとチカチカと点滅していることが分かってしまいます。
すべての桁が点灯し消灯するまでを 1 周期として、60Hz で光らせるのであれば、1 周期の時間は 1 / 60 ≒ 16666μs です。
4 桁なので 4 で割って、1 桁あたりの点灯と消灯の合計は 4167μs くらいになります。
後は、ゴーストを確認しながら消灯時間を決め、ちらつきが気にならない程度に点灯時間を長くする、という感じです。
周波数はあまり上げすぎない方がいいでしょう。というのも、ゴースト対策用の消灯時間はおそらく一定なので、周波数を上げるほど点灯時間に対する消灯時間の割合が増します。PWM のような考え方で見かけ上暗く見えてしまうので、50 ~ 60Hz あたりが良いと思います。