こんにちは.
今回は初めてのこういったサイトへの投稿ということで,乱文等読みにくい場面もあるかと思いますが何卒ご容赦ください.
使用するVFDと仕様決定
さて,今回投稿する作品は表題の通り,Arduinoを用いたVFD時計ということでメインとなるVFDには次のものを用いました.
https://eleshop.jp/shop/g/gEB4411/
こちらのサイトにおける動作例は以下のようになっていました.
- 9桁表示
- ダイナミック点灯方式
- フィラメント電圧:約DC 4 V
- フィラメント電流:約DC 0.085 A
- セグメントグリッド点灯電圧:約DC 20~24 V
- スキャン周波数:5 kHz(1桁当たり200 us)
簡単にダイナミック点灯方式について説明します.
ダイナミック点灯方式とは上図のように複数桁の表示を持つ表示器においてその各桁の表示を同時に行うのではなく,各桁とセグメントの制御を順番に行うことによって1つの数値入力と各桁のスイッチによって複数桁の表示を行う点灯表示方式です.
各桁の表示を順番に行うことにより,実際には高速で各桁の表示はON,OFFを繰り返しているのですが,それらが高速であるために人の目には数字表示が連続して点灯しているように見えます.
メインとなるVFDが決まったところで,今回の時計を作るにあたって決めた仕様を以下に示します.
- 表示は 時間,分,秒とする
- 時刻は起動時(または調整時)に時刻を手動でセットしその後は内部でカウントする.
- 電源はArduinoの動作電源であるDC 5 VのDCアダプタを用いる.
- 各セグメントの点灯時間は(100 us)とする.
前述の購入サイトにおいては電源電圧として24 Vを用いていましたが,今回は電源を5 Vにすることによってフィラメントの電流制御抵抗を1/4 Wのもので済み,電力的にも24 Vからリニアレギュレータ等で降圧するより有利に働くと思います.
また,今回は以下の2種類のICを使ってArduinoの使用IOを極力減らしてみました.
使用IC | 用途 |
---|---|
TC4511BP | 7セグメントドライバ(2進7セグ変換) |
TC4017BP | 10進カウンタIC(ダイナミック点灯用) |
ブロック線図
こちらの仕様を満たすようにまずはブロック線図を考えます.
なお赤線は電源線を表しています.
それぞれのICはトランジスタアレイへと接続しており,トランジスタアレイからVFDの各グリッド,各セグメントへの信号が供給されています.
今回昇圧コンバータに関しては秋月電子の昇圧型スイッチング電源モジュールを用いました.
使用ICの説明
ブロック線図を示したところで10進カウンタと7セグメントドライバの役割について詳しく説明します.
VFDによる表示器においては,セグメント,桁グリッドをHレベルにしたときにその桁の入力したセグメントが光るようになっています.
ダイナミック点灯を行うためには,それぞれ各桁を順にHレベルへとしていく必要があります.そこで用いるのが10進カウンタICです.
10進カウンタICとはクロック端子に入力された電圧の立ち上がりや,立下りに応じてQ0からQ9までの端子の出力を順にHレベル(電源電圧)へと切り替えてくれるICです.またHレベルとなっている端子以外の出力端子はLレベル(0V)となります.
よって,この10進カウンタICの出力Q0~Q8を,VFDの各桁グリッドに接続し,10進カウンタICのクロック端子にArduinoのデジタル出力をつなぎ,各桁の点灯時間(今回は100 us)の周期でON,OFFのdigitalWriteを行えば各桁を100 usずつ順に表示を行ってくれる回路が出来上がります.
しかしこのままだとQ9の端子がONになってしまう時間が無駄になってしまいます.そこで使うのがリセット端子です.
このリセット端子にHレベルを入力することで現在の状態にかかわらずQ0をHにすることができます.
最初はQ9端子とリセット端子を短絡することにより,自己の信号でリセットを行うことはできないみたいだったので,リセット端子もArduinoによって制御することにしました.
次に7セグメントドライバICです.このICは2進数の入力を0から9の7セグメントを点灯させる信号へと変換してくれるICです.使い方等はデータシートを見ればわかると思うのですが,ダイナミック表示において便利な機能として,BI端子があります.この端子をLにすることによってデータ入力端子(A~D)に無関係にすべての出力端子(a~g)をLとしてくれます.
ダイナミック点灯方式における問題として,ゴーストというものがあります.ゴーストとは桁を移動するときに,前の桁で表示していたデータが残ってしまい,うっすらと表示されてしまう現象です.
これを防ぐためには,桁の移動する前に少しだけ全部の表示をOFFにするという時間が必要となります.この時に役に立つのがこのBI端子です.桁を移動する少し前にこのBI端子をLにして,桁を移動,データ入力をしてからBI端子をHにすることでゴーストを防ぐことができます.
また,今回はゴースト対策としてだけでなく,9桁表示のVFDを6桁の表示として用いる際にも利用しています.
今回作成した時計においては時間,分,秒の計6桁のみ使用するため,残りの3桁は使いません.また,時間,分,秒が連続して表示されては見づらくなってしまうため,時間,分,秒の間に一桁空白を入れています.
具体的には1,2桁目に時間,3桁目は空白,4,5桁目に分,6桁目は空白,7,8桁目には秒を表示させています.そのため3桁目,6桁目,9桁目においてはBI端子をLにすることによってこの空白になるようにしています.
今回の時計おいては6桁のみの表示なので実際には6桁の7セグを表示してくれるこちらのIC(https://akizukidenshi.com/catalog/g/gI-13224/)を用いれば1つのICで済むと思うのですが,今回は今後の拡張性を考えて9桁すべて表示できるようにこのような方式にしました.
実装したプログラム
ここまでで回路的な話は終わりましたので次にプログラムの話となります.まずは以下に今回実装したプログラムを示します.
VFD時計
const int CLK = 0;
const int reset = 1;
const int Data0 = 2;
const int Data1 = 3;
const int Data2 = 4;
const int Data3 = 5;
const int ghost = 6;
const int LED = 7;
const int SW5 = 8;
const int SW1 = 9;
const int SW2 = 10;
const int SW3 = 11;
const int SW4 = 12;
int n = 0;
int set = 1;
int pos = 0;
int Current_state = 0;
int SW5_state = 0;
int flag = 0;
int HMS[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
unsigned long Prev_t = micros();
unsigned long Curr_t;
unsigned long tnext = 999031; //1000000usを初期値とし,1日程度置いてじかんを補正する
unsigned long Prev_scan = micros();
unsigned long Curr_scan;
unsigned long tmp;
void setup()
{
pinMode(Data0, OUTPUT);
pinMode(Data1, OUTPUT);
pinMode(Data2, OUTPUT);
pinMode(Data3, OUTPUT);
pinMode(ghost, OUTPUT);
pinMode(reset, OUTPUT);
pinMode(CLK, OUTPUT);
pinMode(LED, OUTPUT);
pinMode(SW1, INPUT_PULLUP);
pinMode(SW2, INPUT_PULLUP);
pinMode(SW3, INPUT_PULLUP);
pinMode(SW4, INPUT_PULLUP);
pinMode(SW5, INPUT_PULLUP);
digitalWrite(LED, LOW);
digitalWrite(CLK, LOW);
digitalWrite(ghost, HIGH);
digitalWrite(reset, HIGH);
delay(1);
digitalWrite(reset, LOW);
Current_state = 0;
}
//7セグICへと送る2進数
void num0() {
digitalWrite(Data0, LOW);
digitalWrite(Data1, LOW);
digitalWrite(Data2, LOW);
digitalWrite(Data3, LOW);
}
void num1() {
digitalWrite(Data0, HIGH);
digitalWrite(Data1, LOW);
digitalWrite(Data2, LOW);
digitalWrite(Data3, LOW);
}
void num2() {
digitalWrite(Data0, LOW);
digitalWrite(Data1, HIGH);
digitalWrite(Data2, LOW);
digitalWrite(Data3, LOW);
}
void num3() {
digitalWrite(Data0, HIGH);
digitalWrite(Data1, HIGH);
digitalWrite(Data2, LOW);
digitalWrite(Data3, LOW);
}
void num4() {
digitalWrite(Data0, LOW);
digitalWrite(Data1, LOW);
digitalWrite(Data2, HIGH);
digitalWrite(Data3, LOW);
}
void num5() {
digitalWrite(Data0, HIGH);
digitalWrite(Data1, LOW);
digitalWrite(Data2, HIGH);
digitalWrite(Data3, LOW);
}
void num6() {
digitalWrite(Data0, LOW);
digitalWrite(Data1, HIGH);
digitalWrite(Data2, HIGH);
digitalWrite(Data3, LOW);
}
void num7() {
digitalWrite(Data0, HIGH);
digitalWrite(Data1, HIGH);
digitalWrite(Data2, HIGH);
digitalWrite(Data3, LOW);
}
void num8() {
digitalWrite(Data0, LOW);
digitalWrite(Data1, LOW);
digitalWrite(Data2, LOW);
digitalWrite(Data3, HIGH);
}
void num9() {
digitalWrite(Data0, HIGH);
digitalWrite(Data1, LOW);
digitalWrite(Data2, LOW);
digitalWrite(Data3, HIGH);
}
//一つの関数で表示する数字を7セグドライバへと送る
void disp(int a) {
switch (a) {
case 0:
num0();
break;
case 1:
num1();
break;
case 2:
num2();
break;
case 3:
num3();
break;
case 4:
num4();
break;
case 5:
num5();
break;
case 6:
num6();
break;
case 7:
num7();
break;
case 8:
num8();
break;
case 9:
num9();
break;
}
}
//時間の繰り上げ方
void HMS_trans(int *p) {
if (p[7] == 10) {
p[6] = p[6] + 1;
p[7] = 0;
}
if (p[6] == 6) {
p[4] = p[4] + 1;
p[6] = 0;
}
if (p[4] == 10) {
p[3] = p[3] + 1;
p[4] = 0;
}
if (p[3] == 6) {
p[1] = p[1] + 1;
p[3] = 0;
}
if (p[1] == 10) {
p[0] = p[0] + 1;
p[1] = 0;
}
if (p[0] == 2 && p[1] == 4) {
p[1] = 0;
p[0] = 0;
}
}
//ダイナミック点灯方式のために各桁の表示を送る
void scan() {
Curr_scan = micros();
if (Curr_scan < Prev_scan) {
tmp = 4294967295 - Prev_scan;
if (Curr_scan + tmp > 100) {
Prev_scan = Curr_scan;
if (Current_state == 0) {
digitalWrite(ghost, LOW);
digitalWrite(CLK, HIGH);
n = n + 1;
Current_state = 1;
} else {
digitalWrite(CLK, LOW);
Current_state = 0;
}
}
} else if (Curr_scan - Prev_scan > 100) {
Prev_scan = Curr_scan;
if (Current_state == 0) {
digitalWrite(ghost, LOW);
digitalWrite(CLK, HIGH);
n = n + 1;
Current_state = 1;
} else {
digitalWrite(CLK, LOW);
Current_state = 0;
}
}
if (n >= 9) {
digitalWrite(reset, HIGH);
n = 0;
digitalWrite(reset, LOW);
}
if (n == 2 || n == 5 || n == 8) {
}
else {
disp(HMS[n]);
digitalWrite(ghost, HIGH);
}
}
//設定をする
void setting() {
int i = 0;
int SW1_state = 0;
int SW2_state = 0;
int SW3_state = 0;
int SW4_state = 0;
int SW5_state = 0;
int Prev_SW_state = 1;
int Prev_SW_state2 = 0;
digitalWrite(LED, HIGH);
while (1) {
//設定完了するまでループ
SW1_state = digitalRead(SW1);
SW2_state = digitalRead(SW2);
SW3_state = digitalRead(SW3);
SW4_state = digitalRead(SW4);
SW5_state = digitalRead(SW5);
tmp = SW1_state + SW2_state + SW3_state + SW4_state;
if (Prev_SW_state2 == 0) {
if (SW2_state == LOW) {
switch (i) {
case 7:
if (HMS[i] < 9) {
HMS[i] = HMS[i] + 1;
}
break;
case 6:
if (HMS[i] < 5) {
HMS[i] = HMS[i] + 1;
}
break;
case 4:
if (HMS[i] < 9) {
HMS[i] = HMS[i] + 1;
}
break;
case 3:
if (HMS[i] < 5) {
HMS[i] = HMS[i] + 1;
}
break;
case 1:
if (HMS[0] == 2 && HMS[1] < 4) {
HMS[i] = HMS[i] + 1;
} else if (HMS[0] < 2 && HMS[1] < 9) {
HMS[i] = HMS[i] + 1;
}
break;
case 0:
if (HMS[i] < 2) {
HMS[i] = HMS[i] + 1;
}
break;
}
Prev_SW_state2 = 1;
} else if (SW3_state == LOW && HMS[i] > 0) {
HMS[i] = HMS[i] - 1;
Prev_SW_state2 = 1;
} else if (SW4_state == LOW) {
if (i == 1 || i == 4 ) {
i = i + 2;
}
else if (i == 0 || i == 3 || i == 6) {
i = i + 1;
}
Prev_SW_state2 = 1;
} else if (SW1_state == LOW) {
if (i == 1 || i == 4 || i == 7) {
i = i - 1;
} else if (i == 3 || i == 6) {
i = i - 2;
}
Prev_SW_state2 = 1;
}
} else if (tmp == 4) {
Prev_SW_state2 = 0;
}
if (SW5_state == LOW && Prev_SW_state == 0) {
Prev_t = micros();
digitalWrite(LED, LOW);
break;
} else if (SW5_state == HIGH && Prev_SW_state == 1) {
Prev_SW_state = 0;
}
scan();
}
}
// the loop function runs over and over again forever
void loop()
{
//時間の取得
Curr_t = micros();
if (Curr_t < Prev_t) {
tmp = 4294967295 - Prev_t;
if (Curr_t + tmp >= tnext) {
tnext = 1998062 - (Curr_t + tmp);//一日程度動作させ時間を補正する
HMS[7] = HMS[7] + 1;
Prev_t = Curr_t;
}
} else if (Curr_t - Prev_t >= tnext) {
tnext = 1998062 - (Curr_t - Prev_t);//一日程度動作させ時間を補正する
HMS[7] = HMS[7] + 1;
Prev_t = Curr_t;
}
HMS_trans(HMS);
scan();
set = digitalRead(SW5);
if (flag == 0 && set == LOW) {
flag = 1;
setting();
} else if (flag == 1 && set == HIGH) {
flag = 0;
}
}
プログラムは以上となります.普段はあまりデジタル物の工作をしないこともあってあまりきれいなプログラムとは言えないと思いますがご容赦ください.また,自分の勉強のためにグローバル変数を使っているにもかかわらずわざわざポインタ形式で記述している関数(HMS_trans)もありますが,もしこちらを参考に時計を組むこと等があればここはわざわざポインタで書かずに直接変数を書き換えてしまってよいと思います.
さて,プログラム部分についてはいくつかのポイントのみ説明したいと思います.
時間の計測とオーバーフローの対策
まずは時間の計測についてです.
時間の計測
//時間の取得
Curr_t = micros();
if (Curr_t < Prev_t) {
tmp = 4294967295 - Prev_t;
if (Curr_t + tmp >= tnext) {
tnext = 2000000 - (Curr_t + tmp);
HMS[7] = HMS[7] + 1;
Prev_t = Curr_t;
}
} else if (Curr_t - Prev_t >= tnext) {
tnext = 2000000 - (Curr_t - Prev_t);
HMS[7] = HMS[7] + 1;
Prev_t = Curr_t;
}
}
時間の計測には,Curr_t,Prev_tという変数を用いています.まずはPrev_tという関数にmicros()関数を用いてArduino内部で計測しているus単位の時間を読み込みます.次にCurr_tという変数に再度micros()関数で時間を書き込み比較をします.このCurr_tとPrev_tの差分をとることによって,前回取得した時間からどれだけの秒数が経ったのかを取得することができます.基本的にはこの差が1000000usの以上になった時にHMS[7]に保存している秒数のカウンタを+1しています.
しかしそのままでは毎回数usずつオーバーしてしまいます.そこで,
tnext = 2000000 - (Curr_t - Prev_t);
を実行し次の桁の繰り上げまでの秒数から,今回オーバーした時間を引いてあげることでこのオーバーした時間分次の繰り上げまでの時間を短くしてあげることで,時間の補正をかけています.
また,micros()という関数は内部で起動してからの時間を図っているのですが4294967296usで内部のカウンタがオーバーフローしてしまい0へと戻っていしまいます.こうすると次の時刻の取得がうまくいかなくなってしまいます.
そこで,オーバーフローの対策として,下記のプログラムを実行しています.
if (Curr_t < Prev_t) {
tmp = 4294967295 - Prev_t;
if (Curr_t + tmp >= tnext) {
tnext = 2000000 - (Curr_t + tmp);
HMS[7] = HMS[7] + 1;
Prev_t = Curr_t;
}
}
この関数は前回取得時刻Prev_tより,現在の取得時刻Curr_tが小さいとき,つまりオーバーフローによりカウンタが0に戻ってしまったとき,最大の測定時刻4294967295とPrev_tとの差をとり,その時間を現在の時刻に足して,経過時間の判断をすることでこのオーバーフローの対策をとっています.
設定スイッチの処理
次にスイッチの判別についてです.スイッチの判別についてはFlagやPrev_SW_state2という変数を用いてスイッチが初めて押されたのか,押し続けられているのかを判別しています.
例えば設定関数に入ったときや,設定画面から出るときに1つのスイッチを用いて
- メインのループ関数において設定ボタンを押したら設定に入る.
- 設定関数内において設定ボタンを押したら設定から抜けてメインループの関数に入る
という機能を実装したいとします.
これをそれぞれflagを用いずに実装してしまうとメインループで押された設定ボタンの状態を設定関数内でもすぐさま読み取ってしまい,メインループと設定関数内を行ったり来たりしてしまうことになります.
そこで,
スイッチの判別
void set(){
while(1){
SW = digitalRead(SettingSW);
//処理の記述
if(flag ==1 && SW == LOW){
flag = 0;
}
if(flag == 0 && SW == HIGH){
flag = 1;
break
}
}
}
void loop(){
SW = digitalRead(SettingSW);
if(flag == 0 && SW == HIGH){
flag = 1;
set();
}
if(flag == 1 && SW == LOW){
flag =0;
}
}
とすることで,はじめて押されたのか,継続して押し続けられているのかを判別し,設定ボタンを押し続けてもメインループと関数を勝手に行き来することがなくなり,1つのボタンで設定に入る.設定から抜けるといった処理が可能になります.
また,このプログラムを応用することで,スイッチの長押し判別等も可能となりますので是非考えてみてください.
実装と動作確認
ここまでの回路とプログラムで実装します.
なお,回路図中にはVFDと昇圧コンバータはコネクタとして表しています.
今回はほとんどIOピンとICの線を直結するだけのあまり複雑な回路にはならないため,実体配線図は電源とグランドのみを描き,スズメッキ線で実装し,その他の配線はビニール単線を用いました.しかし配線の数も多いため,あればポリウレタン銅線を用いたほうが実装も楽で配線もすっきりするでしょう.
また,今回はスイッチに(INPUT_PULLUP)を用いていることにより,外部抵抗を不要とし,プログラムにおいてはスイッチON時にはLOW,スイッチOFF時にはHIGHとなっていることに注意します.
実際にケースに収めて動作させているところです.
micros()で1000000usを測って秒数をカウントする方式を用いて秒数をカウントしていたのですが,この方法ですと3~4時間で数秒ずれていしまいました.
そこで動作させた状態で1日程度放置し,そのずれから補正をかけました.今回はmicros()のカウント時間を今回は999031usとしました.(tnext=1000000-ずれた時間/計測時間)
最後に
以上で作成記事は終了となります.個人的には久々のArduinoでの工作でしたので,面白かったです.
今回デジタル時計ということで作成してみましたが,Arduinoの基礎的なところがかなりさらえる内容となってはいるのではないでしょうか.Lチカなどから一歩踏み込んだ応用例としては実用的で楽しく作れると思います.
-
JunkAudio
さんが
2021/01/04
に
編集
をしました。
(メッセージ: 初版)
ログインしてコメントを投稿する