24時間時計をハードウェア記述言語NSLで作る
【改訂2版】FPGAボードで学ぶ 組込みシステム開発入門[Intel FPGA編] (以下、テキスト)の第3章に載っている24時間時計をハードウェア記述言語のNSLで作ってみる。
1.回路について
24時間時計の機能としてはこんな感じ(テキストから抜粋)
- 時間を計る機能
- 6桁の7セグメントLEDに数字が表示される
- 上の2桁ずつから時分秒の表示
- 時刻調整機能:3つのボタン{MODE,SELECT,ADJUST}を使って時刻調整する機能
- MODEボタンで時刻調整開始して、SELECTボタンでどこを調整するか選んで、ADJUSTボタンで実際に調整する。
- MODEボタンを押すと・・・
- 秒の表示が点滅される
- SELECTボタンを押すと・・・
- 押すたびに点滅する桁が変わる
- 秒→時→分→秒といった感じに変化
- ADJUSTボタンを押すと・・・
- 秒を選んでいるとき: 0秒になる
- 分や時をえらんでいるとき: +1分もしくは+1時間
2. 作ってみよう
1.でまとめた以下の機能をNSLにする。
- 時間を計る機能
- 時刻調整機能
時間を計る機能
時間を計る機能を以下のように分割していく。
- 1秒を計る
- 時分秒のカウンタ
- 7セグメントLEDに表示する
1秒を計る
1秒を計るにはクロック入力を使う
- クロック入力: 0と1の値を繰り返している信号
今回使うFPGAボードでは50MHzのクロック入力がされているので0~49,999,999まで数えたら1秒になる。
- 49,999,999になったら
1秒経過
の信号を出力する
こういうものは、49,999,999の値をパラメータ化して、
インスタンス生成時に、1秒経過のほかに0.5秒経過に設定を変更できたら色々便利だが、
NSLではパラメータがうまく使えない。
- コンパイル時にパラメータの設定値がハードコーディングされる。
- NSLではパラメータの代わりに
#define
を使うのが基本的(?)
- NSLではパラメータの代わりに
他の部分でパラメータ変えて使いたいのでこの部分はVerilogで実装した。
- NSLで作るという話だったが申し訳ない。ここは妥協できなかった。
- 他のHDLで記述されたモジュールを組み合わせて回路作ることができるという機能の紹介ということで…
module en_period (
m_clock, p_reset, en
);
input m_clock;
input p_reset;
output en;
parameter CNT_BIT = 26;
parameter CNT_MAX = 49_999_999;
reg [CNT_BIT-1:0]cnt;
assign en = (cnt == CNT_MAX);
always @(posedge m_clock) begin
if (p_reset) begin
cnt <= 0;
end else begin
if ( en ) begin
cnt <= 0;
end else begin
cnt <= cnt + 1;
end
end
end
endmodule
Verilogのモジュールでも、declare文で入出力を定義すればNSLのモジュール側で参照できる。
- 扱いとしてはC言語のヘッダファイルみたいなのでインクルードガードを設定しておく
#ifndef _EN_PERIOD_NSH
#define _EN_PERIOD_NSH
declare en_period{
param_int CNT_MAX = 49999999;
param_int CNT_BIT = 26;
func_out en();
}
#endif // _EN_PERIOD_NSH
時分秒のカウンタ
時は24進、分秒は60進のカウンタを作ればOK。
秒は1秒を計る部分からくる信号でカウントアップ
- 59秒からカウントアップするときは0にもどしながら、繰り上がり信号を出して分を1増やすようにする
00:00:59 -> 00:01:00
- 同じように59分で繰り上がって時が1増える
00:59:59 -> 01:00:00
- 23時の次は0時
23:59:59 -> 00:00:00
NSLのfunc構文を使うとカウンタを1増やすとか0クリアするとかの制御は、C言語の関数を定義する感覚で実装できる。
- if文で分岐させるより直観的でわかりやすい!(と個人的な感覚)
if(count_up){
if(q0 == 9){
q0 := 0;
if(q1 == 5){
q1 := 0;
on_carry_out = 1;
}else{
q1++;
on_carry_out = 0;
}
}else{
q0++;
on_carry_out = 0;
}
}
func count_up{
if(q0 == 9) {
q0 := 0;
count_hi();
}else {
q0++;
}
}
func count_hi{
q0 := 0;
if((q0 == 9) && (q1 == 5)) {
q1 := 0;
on_carry_out();
} else {
q1++;
}
}
if文をネストして作られた分岐
では別々の処理が混ざっている
- 全部が一緒にまとめられてわかりづらい
funcを使った実装
では処理がわかれている - 行われていることが分割されてわかりやすい
- func宣言しても中身は信号なので値を使った条件分岐もできる。
- 実行していないときは0、実行しているときは1。
- 特に定義していなくても不定値にならないのが安心。
- でも不定値検出→バグ特定、ってできないからそこはデメリット(?)
それぞれの桁を参照するので10の位、1の位に分けて出力
- 60進は10進カウンタを2つ使って59でカウントアップしたら次の値は0
- 24進はテキストでは全列挙でごり押してたのでそれはそのまま
NSLファイル
60進カウンタ
ヘッダファイル
#ifndef _CNT60_NSH
#define _CNT60_NSH
declare cnt60 {
output cnt_lo[4];
output cnt_hi[4];
func_in clr();
func_in count_up();
func_out on_carry_out();
}
#endif // _CNT60_NSH
モジュール
#include "cnt60.nsh"
module cnt60{
reg q0[4] = 0;
reg q1[3] = 0;
func_self count_hi;
wire c0, c1;
func clr{
q0 := 0;
q1 := 0;
}
func count_up{
if(c0 == 1) {
q0 := 0;
count_hi();
}else {
q0++;
}
}
func count_hi{
q0 := 0;
if(c1 == 1) {
q1 := 0;
on_carry_out();
} else {
q1++;
}
}
c0 = (q0 == 4'd9);
c1 = c0 && (q1 == 3'd5);
cnt_lo = q0;
cnt_hi = {0b0, q1};
}
24進カウンタ
ヘッダファイル
#ifndef _CNT24_NSH
#define _CNT24_NSH
declare cnt24 {
output cnt_lo[4];
output cnt_hi[4];
func_in clr();
func_in count_up();
func_out on_carry_out();
}
#endif // _CNT24_NSH
モジュール
#include "cnt24.nsh"
module cnt24 {
reg cnt[5] = 0;
func clr{
cnt := 0;
}
func count_up{
if(cnt == 5'd23) {
cnt := 0;
on_carry_out();
}else {
cnt++;
}
}
any {
cnt == 5'd0 : {cnt_hi = 4'd0; cnt_lo=4'd0;}
cnt == 5'd1 : {cnt_hi = 4'd0; cnt_lo=4'd1;}
cnt == 5'd2 : {cnt_hi = 4'd0; cnt_lo=4'd2;}
cnt == 5'd3 : {cnt_hi = 4'd0; cnt_lo=4'd3;}
cnt == 5'd4 : {cnt_hi = 4'd0; cnt_lo=4'd4;}
cnt == 5'd5 : {cnt_hi = 4'd0; cnt_lo=4'd5;}
cnt == 5'd6 : {cnt_hi = 4'd0; cnt_lo=4'd6;}
cnt == 5'd7 : {cnt_hi = 4'd0; cnt_lo=4'd7;}
cnt == 5'd8 : {cnt_hi = 4'd0; cnt_lo=4'd8;}
cnt == 5'd9 : {cnt_hi = 4'd0; cnt_lo=4'd9;}
cnt == 5'd10 : {cnt_hi = 4'd1; cnt_lo=4'd0;}
cnt == 5'd11 : {cnt_hi = 4'd1; cnt_lo=4'd1;}
cnt == 5'd12 : {cnt_hi = 4'd1; cnt_lo=4'd2;}
cnt == 5'd13 : {cnt_hi = 4'd1; cnt_lo=4'd3;}
cnt == 5'd14 : {cnt_hi = 4'd1; cnt_lo=4'd4;}
cnt == 5'd15 : {cnt_hi = 4'd1; cnt_lo=4'd5;}
cnt == 5'd16 : {cnt_hi = 4'd1; cnt_lo=4'd6;}
cnt == 5'd17 : {cnt_hi = 4'd1; cnt_lo=4'd7;}
cnt == 5'd18 : {cnt_hi = 4'd1; cnt_lo=4'd8;}
cnt == 5'd19 : {cnt_hi = 4'd1; cnt_lo=4'd9;}
cnt == 5'd20 : {cnt_hi = 4'd2; cnt_lo=4'd0;}
cnt == 5'd21 : {cnt_hi = 4'd2; cnt_lo=4'd1;}
cnt == 5'd22 : {cnt_hi = 4'd2; cnt_lo=4'd2;}
cnt == 5'd23 : {cnt_hi = 4'd2; cnt_lo=4'd3;}
else : {cnt_hi = 4'bx; cnt_lo=4'bx;}
}
}
7セグメントLEDに表示する
時分秒のカウンタの値のままでは7セグメントLEDに表示できないので変換(デコード)する必要がある。
7セグメントLEDは色々オンオフする所があって、
数字毎にオンオフする所が決まっているのでそれを列挙する。
- Verilogではこういうのはcase文使うけどNSLだとanyもしくはaltを使う。
- このany,altは適当に使うと不具合になる(ソースは自分)
- 個人的な使い分けのイメージ
- case文代わりに使うならany
- ifたくさん並べる代わりに使うならany
- if-else代わりに使うならalt
- 使い始めたころはこの時にany使ってやらかしました...orz
- 個人的な使い分けのイメージ
- このany,altは適当に使うと不具合になる(ソースは自分)
NSLファイル
7セグメントLEDデコーダ回路
ヘッダファイル
#ifndef _SEG7DEC_NSH
#define _SEG7DEC_NSH
declare seg7dec {
input din[4];
output nHEX[7];
func_in decode(din): nHEX;
}
#endif // _SEG7DEC_NSH
モジュール
#include "seg7dec.nsh"
module seg7dec{
func decode {
wire tmp[7];
any {
din==4'h0 : tmp = 7'b1000000;
din==4'h1 : tmp = 7'b1111001;
din==4'h2 : tmp = 7'b0100100;
din==4'h3 : tmp = 7'b0110000;
din==4'h4 : tmp = 7'b0011001;
din==4'h5 : tmp = 7'b0010010;
din==4'h6 : tmp = 7'b0000010;
din==4'h7 : tmp = 7'b1011000;
din==4'h8 : tmp = 7'b0000000;
din==4'h9 : tmp = 7'b0010000;
din==4'ha : tmp = 7'b0001000;
din==4'hb : tmp = 7'b0000011;
din==4'hc : tmp = 7'b1000110;
din==4'hd : tmp = 7'b0100001;
din==4'he : tmp = 7'b0000110;
din==4'hf : tmp = 7'b0001110;
}
return tmp;
}
}
時刻調整機能
時刻調整機能については、ボタン入力を使うわけで入力の処理が必要なのと、
色々制御するからステートマシンを使っていく。
なので以下についてそれぞれ考えていく。
- ボタン入力
- ステートマシンを使った時刻調整
ボタン入力
ボタン入力はチャタリングを考慮する必要があるので、
その除去をする処理を必要がある。
- チャタリング: 入力を切り替えたときにオンとオフの値が振動してしまう現象。
チャタリング除去は、以下の方式でできる。
- カウンタ信号等でイネーブル信号を定期的に出す。
- イネーブル信号のタイミングでボタンの値を取込む。
- システムクロック(50MHz)の1周期分の長さに調整する。
でこの1秒に50回信号を出す部分を前述の1秒を計る回路のパラメータを上書きして使い回す。
- パラメータの上書きはNSL上でできる。
ボタン入力モジュール
ヘッダファイル
#ifndef _BTN_IN_NSH
#define _BTN_IN_NSH
#include "en_period.nsh"
#define EN50HZ_CNT_BIT 20
#define EN50HZ_CNT_MAX 999999
declare btn_in {
input n_bin[3];
output btn_out[3];
}
#endif // _BTN_IN_NSH
モジュール
#include "btn_in.nsh"
module btn_in {
en_period en50hz(
CNT_BIT = EN50HZ_CNT_BIT,
CNT_MAX = EN50HZ_CNT_MAX
);
reg ff1[3] = 0, ff2[3] = 0;
wire btn_edge[3];
reg tmp[3];
func en50hz.en {
ff2 := ff1;
ff1 := n_bin;
}
btn_edge = ~ff1 & ff2 & 3{en50hz.en};
tmp := btn_edge;
btn_out = tmp;
}
ステートマシンを使って時刻調整
ステートマシンを使うといっても、NSLは専用の構文があるので、
全ての状態をstate_name
で列挙して、それぞれのstate
内で遷移する条件にマッチしたらgoto
で遷移するだけ。便利ですね。
- この
goto
はC言語等で実装されている指定されたラベルに飛ぶgoto
とは別物。一応ラベルにジャンプするgoto
も作れる(非推奨と思われるが)。
あと、どこの桁を調整しているかわかるように7セグメントLED点滅させる機能も実装。
点滅する周期はチャタリング防止のカウントと同様、パラメータ上書きで設定。
時刻調整
ヘッダファイル
#ifndef _ADJUST_TIME_NSH
#define _ADJUST_TIME_NSH
#include "en_period.nsh"
#define EN4HZ_CNT_BIT 24
#define EN4HZ_CNT_MAX 12499999
struct tag_btn {
mode;
select;
adjust;
};
struct tag_hms {
hour;
min;
sec;
};
declare adjust_time {
input bt_in[3];
output seg_on_out[3];
func_out sec_clr();
func_out min_inc();
func_out hour_inc();
}
#endif // _ADJUST_TIME_NSH
モジュール
#include "adjust_time.nsh"
module adjust_time {
state_name normal, sec, min, hour;
en_period en4hz(
CNT_BIT = EN4HZ_CNT_BIT,
CNT_MAX = EN4HZ_CNT_MAX
);
func_self blink_sec();
func_self blink_min();
func_self blink_hour();
reg sig2hz = 1;
tag_btn wire btn;
tag_hms wire seg_on;
btn = bt_in;
func en4hz.en {
sig2hz := ~sig2hz;
}
state normal{
any{
btn.mode : {
goto sec;
}
}
}
state sec{
any{
btn.mode : {
goto normal;
}
btn.select : {
goto hour;
}
btn.adjust : {
sec_clr();
}
}
blink_sec();
}
state min{
any{
btn.mode : {
goto normal;
}
btn.select : {
goto sec;
}
btn.adjust : {
min_inc();
}
}
blink_min();
}
state hour{
any{
btn.mode : {
goto normal;
}
btn.select : {
goto min;
}
btn.adjust : {
hour_inc();
}
}
blink_hour();
}
seg_on.sec = if(blink_sec) sig2hz else 1;
seg_on.min = if(blink_min) sig2hz else 1;
seg_on.hour = if(blink_hour) sig2hz else 1;
seg_on_out = seg_on;
}
トップモジュール
ここまでで作成した2つの機能をまとめていく。
- 作ってきた部品を配線する部分。
トップモジュール
ヘッダファイル
#ifndef _CLOCK_24H_NSH
#define _CLOCK_24H_NSH
#include "btn_in.nsh"
#include "adjust_time.nsh"
#include "cnt60.nsh"
#include "cnt24.nsh"
#include "seg7dec.nsh"
declare clock_24h {
input KEY[3];
output HEX0[7], HEX1[7], HEX2[7], HEX3[7], HEX4[7], HEX5[7];
}
#endif // _CLOCK_24H_NSH
モジュール
#include "clock_24h.nsh"
module clock_24h {
seg7dec dec[6];
en_period cnt_up;
cnt60 sec, min;
cnt24 hour;
btn_in btn;
adjust_time adj;
tag_hms wire seg_on;
btn.n_bin = KEY;
adj.bt_in = btn.btn_out;
seg_on = adj.seg_on_out;
// func_out の処理をまとめる
any {
adj.sec_clr : {
sec.clr();
}
cnt_up.en : {
sec.count_up();
}
adj.min_inc || sec.on_carry_out : {
min.count_up();
}
adj.hour_inc || min.on_carry_out : {
hour.count_up();
}
p_reset : {
sec.clr();
min.clr();
hour.clr();
}
}
HEX0 = if(seg_on.sec) dec[0].decode(sec.cnt_lo) else 7'b111_1111;
HEX1 = if(seg_on.sec) dec[1].decode(sec.cnt_hi) else 7'b111_1111;
HEX2 = if(seg_on.min) dec[2].decode(min.cnt_lo) else 7'b111_1111;
HEX3 = if(seg_on.min) dec[3].decode(min.cnt_hi) else 7'b111_1111;
HEX4 = if(seg_on.hour) dec[4].decode(hour.cnt_lo) else 7'b111_1111;
HEX5 = if(seg_on.hour) dec[5].decode(hour.cnt_hi) else 7'b111_1111;
}
3. 動作チェック
長いので一部を拡大表示
カウントアップしていくのを見ていくと、ちゃんとカウントできてるようだ。
MODEボタン押したらモードチェンジして7セグメントLEDが点滅しているのと
その点滅がSELECTボタン押すたびに代わるかチェック。
そして、ADJUSTボタンを押すと時刻調整できるかどうかチェック。
見た感じできてそう。
4. まとめ
以下の機能をもった24時間時計を作成した
- 24時間を計る機能
- ボタンを押して時刻調整する機能
ここから拡張するなら・・・(アイディア)
- ゼロサプレス(テキストより)
- 0~9時の間は0X時と表示されている状態をX時のみにさせる機能
- 10未満の間に10時間の桁に使われる7セグメントLEDを消灯させる
- 12/24時切り替え(テキストより)
- スイッチ入力で12/24時の表示を切り替える
- スライドスイッチ
-
NORMAL
時のADJUST
入力
- テキストのヒント:状態は
MODE24
信号で管理してそこで切り替える
- スイッチ入力で12/24時の表示を切り替える
- 時報(テキストより)
- 00分とか正午ににブザーを鳴らしたり単色LEDを点滅させたりしてお知らせする機能
- 30時間表示
- 12/24時間表示ができたら30時間制も作ったら面白そう(?)
- 深夜0~5時を24~29時として午前6時から元に戻る
- 24進カウンタを2桁に分離している部分を別回路にしてステートマシン追加して12/24/30を切り替える(?)
- 曜日表示
- 7進カウンターを追加して今日の曜日を表示する機能
- 日曜が0で1増やす度月火水木金土と見なす
-
23:59:59
から00:00:00
になるときに曜日レジスタを1増やす - 表示はどうする
- SELECTおしたら7セグメントLEDに表示
- デコーダ回路を追加する
- 単色LEDの順番を表示
- 一番左が点灯したら日曜でその隣から月火水木金土となる
- SELECTおしたら7セグメントLEDに表示
- 時刻調整機能のステートマシンに状態追加してこのカウンターを調整する必要がありそう。
- 7進カウンターを追加して今日の曜日を表示する機能
- カレンダー
-
NORMAL
状態でSELECT
おしたら7セグメントLEDに日付を表示 - カレンダーモジュールを追加して12進と日付カウンターを追加
- 日付のカウントアップは時の繰り上がり信号
- 月のカウントアップは月の値によって分岐
- 日付のカウンタの繰り上がり信号は実装せず、外部で管理
- 次の月になる瞬間にクリア信号
- うるう年の2月
- 年のレジスタ追加する
- 2bitが00ならうるう年
-
- 万年カレンダー
- カレンダーに年表示を追加
- 年数のレジスタを管理
- 4の倍数でうるう年
5.感想
最初の回路として24時間時計を作ってみたわけだがここまでで少し感想をまとめていく。
- パラメータ値を設定できるモジュールを作れない仕組みなのは不便
- 別言語で実装されたモジュールのパラメータ値を上書きすることはできる
- 内部でハードコーディングされることさえなければ…
- ステートマシン専用の構文は便利
- テキストのVerilogファイルより見やすいはず(?)
func構文の余談
制御信号として用いるfunc
の構文の理解がNSLを学ぶ上で感じる壁の1つらしい。
- 個人的にはC言語の関数使う感覚とあまり変わらないイメージ。
- 関数の宣言には
func_in
,func_out
,func_self
の3つがあって、それらの使い分けは難しいかも? - 個人的な使い分けの感覚はこんな感じ。
-
func_in
: 外部入力で関数呼び出しする。- オブジェクト指向的には: public メソッド(?)
-
func_out
: 呼ぶタイミングを設定、処理は外部に委ねる。- オブジェクト指向的には: abstruct メソッド(?)
-
func_self
: 内部の制御で関数使う。- オブジェクト指向的には: private メソッド(?)
-
次回
次回はPlatform Designer上で提供されている, Nios II CPUを組込み、プログラムとハードウェアを組合わせた24時間時計を作ってみる。
- 今回はハードウェアだけの作成だが、次回はハードウェアに組込むソフトウェアも作成する。