LoginSignup
2
0

NSLで回路を作ろう#01: 24時間時計

Last updated at Posted at 2023-12-20

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を使うのが基本的(?)

他の部分でパラメータ変えて使いたいのでこの部分はVerilogで実装した。

  • NSLで作るという話だったが申し訳ない。ここは妥協できなかった。
    • 他のHDLで記述されたモジュールを組み合わせて回路作ることができるという機能の紹介ということで…
en_period.v
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言語のヘッダファイルみたいなのでインクルードガードを設定しておく
en_period.nsh
#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文をネストして作られた分岐
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を使った実装
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進カウンタ

ヘッダファイル

cnt60.nsh
#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

モジュール

cnt60.nsl
#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進カウンタ

ヘッダファイル

cnt24.nsh
#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

モジュール

cnt24.nsl
#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

NSLファイル

7セグメントLEDデコーダ回路

ヘッダファイル

seg7dec.nsh
#ifndef _SEG7DEC_NSH
#define _SEG7DEC_NSH

declare seg7dec {
	input din[4];
	output nHEX[7];
	func_in decode(din): nHEX;
}

#endif // _SEG7DEC_NSH

モジュール

seg7dec.nsl
#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上でできる。
ボタン入力モジュール

ヘッダファイル

btn_in.nsh
#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

モジュール

btn_in.nsl
#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点滅させる機能も実装。
点滅する周期はチャタリング防止のカウントと同様、パラメータ上書きで設定。

時刻調整

ヘッダファイル

adjust_time.nsh
#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

モジュール

adjust_time.nsl

#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つの機能をまとめていく。

  • 作ってきた部品を配線する部分。
トップモジュール

ヘッダファイル

clock_24h.nsh
#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

モジュール

clock_24h.nsl
#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. 動作チェック

ModelSim使って波形を見てみる。
波形全体表示

長いので一部を拡大表示

カウントアップしていくのを見ていくと、ちゃんとカウントできてるようだ。
カウントアップの確認

あと23:59:59の次は00:00:00に戻るのも確認。
時刻が1周することの確認

MODEボタン押したらモードチェンジして7セグメントLEDが点滅しているのと
その点滅がSELECTボタン押すたびに代わるかチェック。
時刻調整動作確認1

そして、ADJUSTボタンを押すと時刻調整できるかどうかチェック。
時刻調整動作確認2

見た感じできてそう。

4. まとめ

以下の機能をもった24時間時計を作成した

  • 24時間を計る機能
  • ボタンを押して時刻調整する機能
ここから拡張するなら・・・(アイディア)
  • ゼロサプレス(テキストより)
    • 0~9時の間は0X時と表示されている状態をX時のみにさせる機能
    • 10未満の間に10時間の桁に使われる7セグメントLEDを消灯させる
  • 12/24時切り替え(テキストより)
    • スイッチ入力で12/24時の表示を切り替える
      • スライドスイッチ
      • NORMAL時のADJUST入力
    • テキストのヒント:状態はMODE24信号で管理してそこで切り替える
  • 時報(テキストより)
    • 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の順番を表示
        • 一番左が点灯したら日曜でその隣から月火水木金土となる
    • 時刻調整機能のステートマシンに状態追加してこのカウンターを調整する必要がありそう。
  • カレンダー
    • 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時間時計を作ってみる。

  • 今回はハードウェアだけの作成だが、次回はハードウェアに組込むソフトウェアも作成する。

参考文献

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0