SNSへはこちら

PYNQ-Z1ボードでFPGA体験(6) - ラーメンタイマーの仮実装

しばらく日が空きましたが、ラーメンタイマー を作ってみます。今回は途中まで

最終的な仕様目標

  • ボタンで設定値入力
  • カウントダウンをしていき、00:00 になったら以下を実行
    • カウントストップ
    • 電子ブザー鳴動
  • スライドスイッチによってカウントイネーブル/ディスエーブル

今回は太字の部分のみをひとまず実装です。クロックに関して適当に扱っていたため、結構実装に時間がかかってしまいました。

プロジェクト配布

ということで意味があるかわからないこの配布ですが、まぁ備忘録も兼ねているブログなので何してもいいでしょう(?)。こちらからダウンロードできます。

ここから学んだ点

ただアップロードするだけではなく、作ることで分かったことをまとめていきたいと思います。似たようなのを作ってみたい人向けに。

クロック同期の大切さ

いまさら何だ、と思われるでしょうが、御覧ください。
今回の回路は 1Hz クロックでカウントアップしていく回路です。この前上げたストップウォッチ回路 (stopwatch.v) では、カウンタ回路のクロックを 100Hz としていました。回路そのものは 125MHz クロックで動いているのに

これは何がいけないかというと、回路ごとに同期して動くクロックが異なるということです。更に言うと、

  • 元となる高速クロックと分周したクロックが必ずしも同期しているとは限らない
  • 単純なカウントならいいが、スイッチ入力などを考えるときに非常にややこしくなる
    • 例えばスイッチ入力を チャタリング除去回路→エッジ検出回路 に入れるとする。
    • このときエッジ検出はエッジ検出回路の回路クロック1周期分しか出力されない。すると、低速クロックで動く回路に入力したときに動くとは限らない
    • たまたま動けばいいが、別クロックで動いている他の回路に入力するときは?パルス幅を調整する?センシティビティリストはどうする??

このように、ややこしくなるし、クロックが無駄に増えるので誤動作の素です。わかりやすく極端な例を考えてみましょう。といっても今回うまくいかなくてハマった話。

1Hz でカウントする回路にスイッチ入力を入れた例

まずは基本から作っていこうと 1Hz の分周クロックを回路の動作クロックに入れました。この時点で大間違い
当たり前ですが、回路は 1Hz という超低速クロックをベースに動作します。

ではここにスタート/ストップ機能を入れて拡張することを考えます。当然ながらスイッチを使うわけです。スイッチはチャタリングしますので除去が必要です。
その回路の動作クロックは流石に 1Hz では遅いので、高速な 125MHz とします。そしてチャタリングも除去します。概ね以下の図でしょうか。

これで無事ストップウォッチ回路にスイッチ信号が入力...されません。125MHz のクロック幅を持つパルス信号は 1Hz クロック幅より小さすぎるのです。タイミングチャートを見てください。

チャタリングを含んだスイッチ信号はチャタリング除去、ポジティブエッジ検出を通って時間幅の小さいパルスになります。するとこのパルスは 1Hz クロックが立ち上がるまで待てずすぐに立ち下がってしまいます。これでは Stopwatch 回路から見たら信号が入力されていないのと同じです。

ここでなんとか動かそうと頭を捻りましたがだめでした。以下考えたこととだめだった理由(の推測)です。

  • Stopwatch の動作クロックを2種類としてしまえばよい!1Hz クロックと START/STOP 信号を always 文のセンシティビティリストに書き、どのクロックが立ち上がったを見れば良い
    • 実際にやるとシミュレーションでは可能だが、FPGA として論理合成をするとできない。そもそもクロックを2つ使うべきではない。
  • それでは Stopwatch の動作クロックをいっそ 125MHz にすれば良い!そして 1Hz も入力して、回路内部カウンタは 1Hz で動かすようにすれば良い。125MHz はスイッチ監視用。
    • でも実際に内部カウンタに指示を出すのは幅の小さいスイッチのエッジ信号。結局上と一緒になる。
  • ではパルス幅を 1Hz に増やしてしまえ!スイッチパルスで High になり、1Hz クロックの立ち下がりで Low になれば...
    • この時点で複数クロックを使っている。こちらも論理合成時に怒られる。
  • じゃあトップモジュール中で125MHz で監視し、スイッチパルスが立ち上がったらその値を 1Hz 分だけ保持しておけば良い
    • できるけど反応が遅れて話にならない。実際に書いてみればわかるが、記述に必要な reg 変数や wire 変数が増えて明らかに汚い。これは明らかに不適である。

いずれも複数クロック使用については書き方によっては怒られずにすむのですが、大抵うまく動きませんメタステーブルが原因だと思います(最近勉強して知った)。

発想を変える必要があります。僕はここまで来てやっと最初の「1Hz クロックを用いる」考えが正しくないことに気が付きました。そして実際にうまくいった考え方はこちらです。

  • 回路はすべて均一に 125MHz の高速クロックで動かす
  • 1Hz の経過はこれまで通り分周回路を作る。ただし...
    • Duty 50% の模範的なクロック波形ではなく、高速クロック1周期分のパルスを 1sec ごとに発生させる回路に改変
    • こうすることで、125MHz クロックにたいしてこのパルスは2つめのクロックではなく、ただの入力信号になる
  • スイッチ入力からエッジ検出までもこれまで通り
    • 高速クロックで何も工夫をしなくてもこの信号を監視できるというのが大きなメリット。こちらもただの入力信号となるため見逃しがない。

...ちょっと分かりにくいですかね?これ以上かんたんかつ具体的に説明できる語彙力がありません。
もっと言うと、上のような構成にすることで 1Hz 信号やスイッチ入力がエッジセンシティブではなくレベル検知のみで利用できるようになります。

スイッチ入力を受け入れる回路の設計

今回はストップウォッチ回路ではないのですが、以上の考え方を応用して設計にこぎつけました。大変だったのよ。

どう書くかですが、ズバリ入力パルス波形が High だったら、という if 文を書くだけ。単純にできるんですよねこれ。
具体的には以下のコードがあります。

// 一部省略
module counter60_bcd_down_with_en( // カウントダウン回路
    input wire nRST,
    input wire CLK, // CLKは回路システム全体と同じ高速クロックにすること
    input wire EN, // ここがHighならカウントダウン
    input wire INCR, // スイッチ入力。カウントを1増やす
    input wire DECR, // スイッチ入力。カウントを1減らす
    output reg [3:0] NUMBER_1,
    output reg [2:0] NUMBER_10,
    output wire FULL
);
// 略
    always @(posedge CLK or negedge nRST) begin
        if( ~nRST ) begin
            NUMBER_1  <= 4'd0;
            NUMBER_10 <= 3'd0;
        end
        else if ( INCR ) begin // CLKが高速クロックなので、スイッチ入力はただのレベル信号である
            NUMBER_1 <= (NUMBER_1 + 4'd1) % 10;
            NUMBER_10 <= (NUMBER_1 == 4'd9) ? (NUMBER_10 + 3'd1) % 6 : NUMBER_10;
        end
        else if ( DECR ) begin
            NUMBER_1 <= (NUMBER_1 - 4'd1 + 4'd10) % 10;
            NUMBER_10 <= (NUMBER_1 == 4'd0) ? (NUMBER_10 - 3'd1 + 3'd6) % 6 : NUMBER_10;
        end
        else if ( EN ) begin // 1Hzごと(今回は1Hzの極狭パルスがHighのとき。こちらもただのレベル信号)に通常のカウント動作をする
            if( NUMBER_1 == 4'd0 && NUMBER_10 == 3'd0 ) begin
                NUMBER_1 <= 4'd9;
                NUMBER_10 <= 3'd5;
            end
            else if( NUMBER_1 == 4'd0 ) begin // (x:0) (x != 0)
                NUMBER_1 <= 4'd9;
                NUMBER_10 <= (NUMBER_10 - 3'd1) % 10;
            end
            else begin
                NUMBER_1 <= (NUMBER_1 - 4'd1) % 10;
                NUMBER_10 <= NUMBER_10;
            end
        end
// 以下略

さ、あとは追加の機能を実装するだけだ!
今回はだいぶハマったおかげで、同期回路の大切さが分かってきました。