はじめに
前回の記事(SystemVerilogをtwo-process methodで書く)では
two process method(TP)の概要を説明しました。
前回の基本概念の説明がメインで単純なカウンタを例にしていたので
今回はもう少し複雑な処理をTPで書いてみます。
DFのコード
今回は元となるコードとして次の記事のものを使わせていただきました。
Qiita - Verilogでシリアル通信入力
SystemVerilogで書き直したものが次のコードです。
定数宣言、三項演算子への置き換えなど一部変更していますが基本は同じです。
コードの読みやすさを比べるためにコメントはあえて削除しています。
interface uart_interface;
logic [7:0] data;
logic enable;
modport receiver(
output data, enable
);
endinterface
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
以下は全体のブロック図とタイミングチャートです。
読み出し制御のブロックが少し複雑になっており
コード上でも少し読みにくくなっていることがわかります。
TPへの書き換え
処理を変えずにTPにする
ひとまず、処理を変えずに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
の処理より先に書くように強制される
- 元のコードでは
関連する処理をまとめる
上記のコードにおいてbusy
、div
、cnt
はそれぞれ読み出し制御を行うためのレジスタですが
それぞれの関係が見た目ではわかりません。
それぞれのif文を1つにまとめてみます。
このとき、if文中にstart
、dt_get
、fin
があると読みにくいので合わせて展開します。
整理した結果が次のコードです。
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
の立下りでdiv
、cnt
の初期値を設定してbusy状態になる
-
- busy状態のとき
-
div
は常にカウントダウンしている -
cnt
はdiv
が0のときだけカウントアップする -
div
とcnt
が条件を満たしたらbusy状態を終了する
-
ここで改めて整理したコードと次のstart
、dt_get
、fin
の式を見ると
共通の条件が多数あることがわかります。
start = (r.rx[2:1] == 2'b10) & (~r.busy);
dt_get = (r.busy) & (r.div == 8'd0);
fin = (r.cnt == 4'd9) & (dt_get);
文脈的にもstart
、dt_get
、fin
はbusy
、div
、cnt
から生成される状態を
読みやすくするための中間変数として使われているのでまとめてしまってもよさそうです。
まとめて整理した結果が次のコードです。
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
このコードからは以下のことがすぐ読み取れます。
-
start
、dt_get
、fin
はラッチしない - busyでない状態のとき
-
rx
が立ち下がると開始(start
)
-
- busyでない状態のとき
-
div
が0になるたびにデータをラッチする(dt_get
) - 必要なビット数をラッチしたら終了(
fin
)
-
以上の結果をまとめると全体のコードは次のようになります。
start
はどの処理でも使われていないので削除しました。
処理の流れと意味のまとまりが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 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 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にまとめる。
一方で次のような問題点もあります。
-
合成結果の問題
TPでは1つのprocessで処理を記述するため、論理合成の結果が合成ツールに依存しやすくなります。
タイミングや回路規模がシビアな設計には向いていないかもしれません。普段は~100MHzクロック程度までしか設計していませんが、
その範囲では特に困ったことはありません。 -
制約の問題
今回作成したコードではメタステーブル対策の2段FFがありますが、
1段目と2段目のFF間の配線は短いほうが望ましいです。
このような制約を入れる方法についてはまだ把握していません。現状、制約が必要なものはDFで書いています。