SNSへはこちら

Tang Nanoでお手軽FPGA入門(3) - 3種のLチカ

さて、毎回恒例、Lチカです。普段はマイコンを弄っているので、割と単純なように思えますが、今回ばかりは違います。

FPGA ですので、論理回路を生成するということで、回路を意識して作らねばなりません。1つの LED を使うLチカは単純ですが。
また、C ではなく Verilog で動作を記述するというのが楽しみですねえ。

基本のLチカ

LED 1つをチカチカさせていきます。

方針

クロックが 24MHz で出ているらしいので、これを遅延回路に通せばよいことが分かります。
論理回路で遅延といえばカウンタに他ならないので、これをクロック入力時に設定すればよいわけです。まあマイコンのLチカと同じですね。

(余談)カウンタの役割について

カウンタといったらタイマー等に内蔵されている加算用のバッファですが、これを使うことで 分周、遅延が実現できます。

分周はクロックをカウンタに入れて、カウンタの出力を取り出すことで実現できます。例えばカウンタの1ビット目を見るとクロックが2分周されているといった感じです。

遅延は超単純脳筋で、最初のカウントの際にサブカウンタを用意して、それが規定数に溜まるまでメインのカウンタをインクリメントしないことで実現できます。

話を戻して

ということで、以下が設計した回路です。1クロックごとにカウンタをインクリメントしていき、規定数(この場合は max) に達したら1bit出力の outp をトグルするという動作です。

localparam integer max = 32'd1200_0000;

module counter(input wire clk, input wire rstn, output reg outp);
    reg[31:0] cnt;
    always @(posedge clk or negedge rstn) begin
        if( !rstn ) begin
            cnt <= 0;
            outp <= 1;
        end
        else begin
            cnt <= cnt + 32'd1;
            if( cnt == max ) begin
                cnt <= 0;
                outp <= ~outp;
            end
        end
    end
endmodule

ご覧の通り、max までに至る時間を考えると周期は 500msec であることが分かると思います。
IO の接続ですが、この記事にあるように、以下で行きます。
このサイトを参考に、ドラッグアンドドロップでピン配置を設定してやってください。
ややこしいことですが、その際に出来る .cst ファイルは プロジェクト名/src に保存してください。保存ダイアログのデフォルトのパスが変なとこになっていることがありますので要注意です。

ピン名 ピン番号
clk 35
rstn 15(Aボタン、Active low)
outp 18(Red LED, Active low)

動作はこんな感じ。いいですね。

LED3つのLチカ

続いて RGB すべての LED を使っていきます。具体的には3つの LED の点滅タイミングをずらしてやりたいと思います。
ただし、FPGA でのハードウェア設計ということで、3つの LED チカチカ専用回路を作るのではなく、あくまで汎用的に使えるカウンタを積んだハードウェアを作ることを(個人的に)追求したいので、工夫する必要があります。

方針

先程生成した counter モジュールですが、内部に位相遅延成分を追加します。つまり初期状態で一定時間クロックによるカウントアップをさせないことによってこれを実装できます。
カウントアップをさせない時間は、内部のサブカウンタで処理します。つまり、最初はクロック入力に対してサブカウンタのみをカウントアップさせ、これが既定値に達したら今度はサブカウンタのカウントをストップ、メインのカウンタをやっとこさ使い始めるという仕組みです。

その一定時間を外部ピンで(FPGA全体としてみたら内部信号に他ならないが)設定することによって、汎用的なハードウェアを3つ並べてLチカを実現します。

localparam integer max = 32'd1200_0000;

module led(input wire clk, input wire rstn, output reg[0:2] outp);
    counter red(clk, 32'd0, rstn, outp[0]);
    counter green(clk, 32'd400_0000, rstn, outp[1]);
    counter blue(clk, 32'd800_0000, rstn, outp[2]);
endmodule

module counter(input wire clk, input wire[31:0] delay, input wire rstn, output reg outp);
    reg[31:0] cnt;
    reg[31:0] subcnt; // for phase delay generation
    always @(posedge clk or negedge rstn) begin
        if( !rstn ) begin
            cnt <= 0;
            subcnt <= 0;
            outp <= 1;
        end
        else begin
            if( subcnt < delay) subcnt <= subcnt + 1;
            else begin
                cnt <= cnt + 32'd1;
                if( cnt == max ) begin
                    cnt <= 0;
                    outp <= ~outp;
                end
            end
        end
    end
endmodule

汎用的な counter モジュールを定義し、そのインスタンスを led モジュール内で宣言することで3つ分の LED を制御することが出来るというわけです。つまり最終的に配置すべき IO は led で記述している clk, rstn, outp[0], outp[1], outp[2] です。

ピン名 ピン番号
clk 35
rstn 15(Aボタン、Active low)
outp[0] 18(Red LED, Active low)
outp[1] 16(Green LED, Active low)
outp[2] 17(Blue LED, Active low)

まぁだいたいこんな感じです。

LEDボヤァ

ソフトでは割と簡単ですが、ハードウェアではちょっと考えを変える必要があって難しかったです。だってハードウェア記述は上から順に実行されるものではないですからね。ロジックを気にしつつ、かつラッチができないように気を使う必要があります。

数10分格闘したところ、何とかできました。考えをお見せしましょう。

ハードウェア的な考え方

単にコードをお見せするのではなく、どう考えたかをお教えします。
まず、ハードウェアはクロックをベースに動きます。そして出力レベルは High or Low に限られます。
なので原則 LED の明るさを制御するために用いられるのは PWM 制御 なわけです。図で表すと以下のような感じですかね。

時間でこの Duty が比例的に変わっていくのですね。これってあるカウンターで実現できそうですね。クロックのナマの値を積算していくととても高速にカウンター値が変化していくのですが、クロックをある程度大きな値で分周すると目に見える速さで Duty が変化しそうです。誤解を恐れずに言えば、この Duty が波形の包絡線になるのです。説明ベタですみません。。。

では、ある Duty 値の部分をズームアップしてみましょう。そこにはナマのクロックカウンターが見えます。

上側を ON、下側を OFF にするために、 Duty値(実際には Duty x カウンタMax値)と常に比較して、出力を入れる必要があるのです。この考えを Verilog に盛り込みました。

コード

以上の考えをコードにすると以下です。

module counter_with_max(input wire clk, input wire rstn, input wire[15:0] div, output reg[15:0] outp);
    reg[15:0] internal_cnt;
    always @(posedge clk or negedge rstn) begin
        if( !rstn ) begin
            internal_cnt <= 0;
            outp <= 0;
        end
        else begin
            internal_cnt <= internal_cnt + 1;
            if( internal_cnt == div ) begin
                outp <= outp + 1;
                internal_cnt <= 0;
            end
        end
    end
endmodule

localparam integer div_for_duty = 16'd240;
module blur(input wire clk, input wire rstn, output reg outp);
    wire[15:0] div_cnt;
    reg[15:0] cnt;
    counter_with_max cmx(clk, rstn, div_for_duty, div_cnt); // Duty generator

    always @(posedge clk or negedge rstn) begin
        if( !rstn ) begin
            outp <= 0;
            cnt <= 0;
        end
        else begin
            cnt <= cnt + 1;
            if( cnt < div_cnt ) outp <= 0; // ON time
            else outp <= 1; // OFF time
        end
    end
endmodule

動きはこんな感じ。やったね!