Help us understand the problem. What is going on with this article?

TPのSystemVerilogでシリアル通信入力

はじめに

前回の記事(SystemVerilogをtwo-process methodで書く)では
two process method(TP)の概要を説明しました。

前回の基本概念の説明がメインで単純なカウンタを例にしていたので
今回はもう少し複雑な処理をTPで書いてみます。

DFのコード

今回は元となるコードとして次の記事のものを使わせていただきました。
Qiita - Verilogでシリアル通信入力

SystemVerilogで書き直したものが次のコードです。
定数宣言、三項演算子への置き換えなど一部変更していますが基本は同じです。
コードの読みやすさを比べるためにコメントはあえて削除しています。

interface
interface uart_interface;
    logic [7:0] data;
    logic       enable;
    modport receiver(
        output data, enable
    );
endinterface
UART_RX.sv(DF)
module UART_RX #(
    parameter DIV_WID = 8,
    parameter DIV_CNT = 8'hAE
) (
    input logic             i_rst_n,
    input logic             i_clk,
    input logic             i_UART_RX,
    uart_interface.receiver uart_if
);

    localparam START_BIT = 1'b0;
    localparam STOP_BIT = 1'b1;

    logic[2:0]         rx;
    logic              start;
    logic              fin;
    logic              busy;
    logic[DIV_WID-1:0] div;
    logic[4:0]         cnt;
    logic              dt_get;
    logic[9:0]         sp_ff;
    logic              chk_trg;
    logic[7:0]         dat;
    logic              ena;

    always_ff @(posedge i_clk, negedge i_rst_n) begin
        if (~i_rst_n) begin
            rx <= '1;
        end else begin
            rx <= {rx[1:0], i_UART_RX};
        end
    end

    assign start = (rx[2:1] == 2'b10) & (busy == 1'b0);

    assign fin = (cnt == 5'd9) & (dt_get == 1'b1);

    always_ff @(posedge i_clk, negedge i_rst_n) begin
        if (~i_rst_n)
            busy <= 1'b0;
        else if (start)
            busy <= 1'b1;
        else if (fin)
            busy <= 1'b0;
    end

    always_ff @(posedge i_clk, negedge i_rst_n) begin
        if (~i_rst_n) begin
            div <= '0;
        end else if (start) begin
            div <= {1'b0, DIV_CNT[DIV_WID-1:1]};
        end else if (busy) begin
            div <= (div == 8'd0) ? DIV_CNT : div <= div-1;
        end else begin
            div <= '0;
        end
    end

    always_ff @(posedge i_clk, negedge i_rst_n) begin
        if (~i_rst_n) begin
            cnt <= '0;
        end else if (start) begin
            cnt <= '0;
        end else if (dt_get) begin
            cnt <= cnt+1;
        end
    end

    assign dt_get = (busy) & (div == 8'd0);

    always_ff @(posedge i_clk, negedge i_rst_n) begin
        if (~i_rst_n) begin
            sp_ff <= '1;
        end else if (dt_get) begin
            sp_ff <= {rx[2], sp_ff[9:1]};
        end
    end

    always_ff @(posedge i_clk, negedge i_rst_n) begin
        if (~i_rst_n) begin
            chk_trg <= 1'b0;
        end else begin
            chk_trg <= fin;
        end
    end

    always_ff @(posedge i_clk, negedge i_rst_n) begin
        if (~i_rst_n) begin
            dat <= 8'h00;
            ena <= 1'b0;
        end else if ((chk_trg) & (sp_ff[0] == START_BIT) & (sp_ff[9] == STOP_BIT)) begin
            dat <= sp_ff[8:1];
            ena <= 1'b1;
        end else begin
            dat <= dat;
            ena <= 1'b0;
        end
    end

    assign uart_if.dat = dat;
    assign uart_if.ena = ena;

endmodule

以下は全体のブロック図とタイミングチャートです。
読み出し制御のブロックが少し複雑になっており
コード上でも少し読みにくくなっていることがわかります。

block-diagram.jpg

timing-chart.jpg

TPへの書き換え

処理を変えずにTPにする

ひとまず、処理を変えずにDFをTPの形式にしたものが次のコードです。

UART_RX.sv(DF->TP)
module UART_RX #(
    parameter DIV_WID = 8,
    parameter DIV_CNT = 8'hAE
) (
    input logic i_rst_n,
    input logic i_clk,
    input logic i_UART_RX,
    uart_interface.receiver uart_if
);

    localparam START_BIT = 1'b0;
    localparam STOP_BIT = 1'b1;

    logic start;
    logic fin;
    logic dt_get;

    typedef struct packed{
        logic                        busy;
        logic [2:0]                  rx;
        logic unsigned [DIV_WID-1:0] div;
        logic unsigned [3:0]         cnt;
        logic [9:0]                  sp_ff;
        logic                        chk_trg;
        logic [7:0]                  dat;
        logic                        ena;
    } reg_type;

    localparam reg_type INIT = '{
        busy : 1'b0,
        rx : '1,
        div : '0,
        cnt : '0,
        sp_ff : '1,
        chk_trg : 1'b0,
        dat : '0,
        ena : 1'b0
    };

    reg_type rin, r = INIT;

    always_comb begin

        rin = r;

        rin.rx = {r.rx[1:0], i_UART_RX};

        start = (r.rx[2:1] == 2'b10) & (~r.busy);
        dt_get = (r.busy) & (r.div == 8'd0);
        fin = (r.cnt == 4'd9) & (dt_get);

        if (start) begin
            rin.busy = 1'b1;
        end else if (fin) begin
            rin.busy = 1'b0;
        end

        if (start) begin
            rin.div = {1'b0, DIV_CNT[DIV_WID-1:1]};
        end else if (r.busy) begin
            rin.div = (r.div == 8'd0) ? DIV_CNT : r.div - 1;
        end else begin
            rin.div = 8'd0;
        end

        if (start) begin
            rin.cnt = 4'd0;
        end else if (dt_get) begin
            rin.cnt = r.cnt + 1;
        end

        if (dt_get) begin
            rin.sp_ff = {r.rx[2], r.sp_ff[9:1]};
        end

        rin.chk_trg = fin;

        rin.ena = 1'b0;
        if ((r.chk_trg) & (r.sp_ff[0] == START_BIT) & (r.sp_ff[9] == STOP_BIT)) begin
            rin.dat = r.sp_ff[8:1];
            rin.ena = 1'b1;
        end

        uart_if.data = r.dat;
        uart_if.enable = r.ena;

    end

    always_ff @(posedge i_clk, negedge i_rst_n) begin
        if (~i_rst_n) begin
            r <= INIT;
        end else begin
            r <= rin;
        end
    end

endmodule

この時点で読みやすくなった点は次の通りです。

  • regが明示される
    • レジスタは構造体になる
  • リセット処理が分離される
    • リセット処理をいちいち読み飛ばさなくてよくなる
    • レジスタのリセット漏れがないかどうかはINITを見るだけでよくなる
      • 今回はリセット時に全てのレジスタが初期化されるため一括代入できる
      • リセット時も保存しておきたいレジスタがある場合はレジスタ毎にリセットを書く必要がある
  • 処理を上から読むことができる
    • 元のコードではfinの条件にdt_getが含まれるが、dt_getの処理は終盤のほうに書かれているため、探す必要がある
    • TPでは処理が上から順番に行われるため、dt_getの処理をfinの処理より先に書くように強制される

関連する処理をまとめる

上記のコードにおいてbusydivcntはそれぞれ読み出し制御を行うためのレジスタですが
それぞれの関係が見た目ではわかりません。

それぞれのif文を1つにまとめてみます。
このとき、if文中にstartdt_getfinがあると読みにくいので合わせて展開します。

整理した結果が次のコードです。

        if (~r.busy) begin
            rin.div = 8'd0;
            if (r.rx[2:1] == 2'b10) begin
                rin.div = {1'b0, DIV_CNT[DIV_WID-1:1]};
                rin.cnt = 4'd0;
                rin.busy = 1'b1;
            end
        end else begin
            rin.div = (r.div == 8'd0) ? DIV_CNT : r.div - 1;
            if (r.div == 8'd0) begin
                rin.cnt = r.cnt + 1;
                if (r.cnt == 4'd9) begin
                    rin.busy = 1'b0;
                end
            end
        end

ネストは深くなってしまいますが、
if文を読むだけでよくなるので信号を探し回る必要がなくなります。

この時点でコードからは以下のことがすぐ読み取れます。

  • busy状態でないとき
    • divだけ固定値が入れられている
      • divに固定値を入れる必要はないかも?
      • cntに固定値を入れないのはバグかも?
    • rxの立下りでdivcntの初期値を設定してbusy状態になる
  • busy状態のとき
    • divは常にカウントダウンしている
    • cntdivが0のときだけカウントアップする
    • divcntが条件を満たしたらbusy状態を終了する

ここで改めて整理したコードと次のstartdt_getfinの式を見ると
共通の条件が多数あることがわかります。

    start = (r.rx[2:1] == 2'b10) & (~r.busy);
    dt_get = (r.busy) & (r.div == 8'd0);
    fin = (r.cnt == 4'd9) & (dt_get);

文脈的にもstartdt_getfinbusydivcntから生成される状態を
読みやすくするための中間変数として使われているのでまとめてしまってもよさそうです。

まとめて整理した結果が次のコードです。

        start = 1'b0;
        dt_get = 1'b0;
        fin = 1'b0;
        if (~r.busy) begin
            rin.div = 8'd0;
            if (r.rx[2:1] == 2'b10) begin
                start = 1'b1;
                rin.div = {1'b0, DIV_CNT[DIV_WID-1:1]};
                rin.cnt = 4'd0;
                rin.busy = 1'b1;
            end
        end else begin
            rin.div = (r.div == 8'd0) ? DIV_CNT : r.div - 1;
            if (r.div == 8'd0) begin
                dt_get = 1'b1;
                rin.cnt = r.cnt + 1;
                if (r.cnt == 4'd9) begin
                    fin = 1'b1;
                    rin.busy = 1'b0;
                end
            end
        end

このコードからは以下のことがすぐ読み取れます。

  • startdt_getfinはラッチしない
  • busyでない状態のとき
    • rxが立ち下がると開始(start)
  • busyでない状態のとき
    • divが0になるたびにデータをラッチする(dt_get)
    • 必要なビット数をラッチしたら終了(fin)

以上の結果をまとめると全体のコードは次のようになります。
startはどの処理でも使われていないので削除しました。
処理の流れと意味のまとまりがDFよりも見やすくなっていると思います。

UART_RX.sv(TP)
module UART_RX #(
    parameter DIV_WID = 8,
    parameter DIV_CNT = 8'hAE
) (
    input logic i_rst_n,
    input logic i_clk,
    input logic i_UART_RX,
    uart_interface.receiver uart_if
);

    localparam START_BIT = 1'b0;
    localparam STOP_BIT = 1'b1;

    logic fin;
    logic dt_get;

    typedef struct packed{
        logic                        busy;
        logic [2:0]                  rx;
        logic unsigned [DIV_WID-1:0] div;
        logic unsigned [3:0]         cnt;
        logic [9:0]                  sp_ff;
        logic                        chk_trg;
        logic [7:0]                  dat;
        logic                        ena;
    } reg_type;

    localparam reg_type INIT = '{
        busy : 1'b0,
        rx : '1,
        div : '0,
        cnt : '0,
        sp_ff : '1,
        chk_trg : 1'b0,
        dat : '0,
        ena : 1'b0
    };

    reg_type rin, r = INIT;

    always_comb begin

        rin = r;

        rin.rx = {r.rx[1:0], i_UART_RX};

        dt_get = 1'b0;
        fin = 1'b0;
        if (~r.busy) begin
            rin.div = 8'd0;
            if (r.rx[2:1] == 2'b10) begin
                rin.div = {1'b0, DIV_CNT[DIV_WID-1:1]};
                rin.cnt = 4'd0;
                rin.busy = 1'b1;
            end
        end else begin
            rin.div = (r.div == 8'd0) ? DIV_CNT : r.div - 1;
            if (r.div == 8'd0) begin
                dt_get = 1'b1;
                rin.cnt = r.cnt + 1;
                if (r.cnt == 4'd9) begin
                    fin = 1'b1;
                    rin.busy = 1'b0;
                end
            end
        end

        if (dt_get) begin
            rin.sp_ff = {r.rx[2], r.sp_ff[9:1]};
        end

        rin.chk_trg = fin;

        rin.ena = 1'b0;
        if ((r.chk_trg) & (r.sp_ff[0] == START_BIT) & (r.sp_ff[9] == STOP_BIT)) begin
            rin.dat = r.sp_ff[8:1];
            rin.ena = 1'b1;
        end

        uart_if.data = r.dat;
        uart_if.enable = r.ena;

    end

    always_ff @(posedge i_clk, negedge i_rst_n) begin
        if (~i_rst_n) begin
            r <= INIT;
        end else begin
            r <= rin;
        end
    end

endmodule

実際にはコメントが入るのでもう少し読みやすくなります。

UART_RX.sv(TP)
//-------------------------------------------------------------------
// uart receiver
//   * clock             : 20[MHz]                   => 50[ns]
//   * baudrate          : 115200[bps]               => 8.68[us]
//   * count             : clock[Hz] / baudrate[bps] => 174(0xAE)[/bit]
//   * parity            : none
//   * stop bit          : 1 bit
//   * metastable filter : 2-FF
//   * noise filter      : none (connect to buffer IC only)
//-------------------------------------------------------------------

interface uart_interface;

    logic [7:0] data;
    logic       enable;

    modport receiver(
        output data, enable
    )

endinterface

module UART_RX #(
    parameter DIV_WID = 8,    // frequency division counter bits
    parameter DIV_CNT = 8'hAE // sampling period (clocks per sampling)
) (
    input logic i_rst_n,
    input logic i_clk,
    input logic i_UART_RX,
    uart_interface.receiver uart_if
);

    localparam START_BIT = 1'b0;
    localparam STOP_BIT = 1'b1;

    // wire
    logic fin;    // finish pulse
    logic dt_get; // data latch trigger

    // reg
    typedef struct packed{
        logic                        busy;    // busy
        logic [2:0]                  rx;      // edge detection([2:1]) + metastable filter([1:0])
        logic unsigned [DIV_WID-1:0] div;     // frequency division counter
        logic unsigned [3:0]         cnt;     // received bit count
        logic [9:0]                  sp_ff;   // serial to parallel ff
        logic                        chk_trg; // check trigger
        logic [7:0]                  dat;     // received data
        logic                        ena;     // received data enable
    } reg_type;

    localparam reg_type INIT = '{
        busy : 1'b0,
        rx : '1,
        div : '0,
        cnt : '0,
        sp_ff : '1,
        chk_trg : 1'b0,
        dat : '0,
        ena : 1'b0
    };

    reg_type rin, r = INIT;

    always_comb begin

        // default assignment
        rin = r;

        // metastable filter + edge detection
        rin.rx = {r.rx[1:0], i_UART_RX};

        // timing control
        dt_get = 1'b0;
        fin = 1'b0;
        if (~r.busy) begin
            rin.div = 8'd0;
            if (r.rx[2:1] == 2'b10) begin
                rin.div = {1'b0, DIV_CNT[DIV_WID-1:1]}; // shift 1/2 period
                rin.cnt = 4'd0; // counter reset
                rin.busy = 1'b1;
            end
        end else begin
            rin.div = (r.div == 8'd0) ? DIV_CNT : r.div - 1; // frequency division counter
            if (r.div == 8'd0) begin
                dt_get = 1'b1; // data latch trigger
                rin.cnt = r.cnt + 1; // bit counter
                if (r.cnt == 4'd9) begin
                    fin = 1'b1; // finish pulse
                    rin.busy = 1'b0;
                end
            end
        end

        // serial to parallel (LSB first)
        if (dt_get) begin
            rin.sp_ff = {r.rx[2], r.sp_ff[9:1]};
        end

        // gerate check trigger
        rin.chk_trg = fin;

        // check data
        rin.ena = 1'b0;
        if ((r.chk_trg) & (r.sp_ff[0] == START_BIT) & (r.sp_ff[9] == STOP_BIT)) begin
            rin.dat = r.sp_ff[8:1];
            rin.ena = 1'b1;
        end

        // output
        uart_if.data = r.dat;
        uart_if.enable = r.ena;

    end

    always_ff @(posedge i_clk, negedge i_rst_n) begin
        if (~i_rst_n) begin
            r <= INIT;
        end else begin
            r <= rin;
        end
    end

endmodule

まとめ

TPで書くことでコードを上から順に読むことができ
DFよりも処理の流れが追いやすくなります。

次の方法を使えば、ここからさらに読みやすくすることもできるでしょう。

  • busyのかわりにtypedef enum {IDLE, RECEIVE} state_typeのようなユーザ定義型を使う。
  • 特定の処理をSubprogramにまとめる。

一方で次のような問題点もあります。

  1. 合成結果の問題

    TPでは1つのprocessで処理を記述するため、論理合成の結果が合成ツールに依存しやすくなります。
    タイミングや回路規模がシビアな設計には向いていないかもしれません。

    普段は~100MHzクロック程度までしか設計していませんが、
    その範囲では特に困ったことはありません。

  2. 制約の問題

    今回作成したコードではメタステーブル対策の2段FFがありますが、
    1段目と2段目のFF間の配線は短いほうが望ましいです。
    このような制約を入れる方法についてはまだ把握していません。

    現状、制約が必要なものはDFで書いています。

参考

ryo_i6
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away