Verilog의 always와 reg, wire가 SystemVerilog의 always_comb, always_latch, always_ff와 logic으로 어떻게 바뀌었는지 비교한다. 바뀐 이유와 장단점은 simulation과 합성 결과로 확인한다.
case 글에 이은 문법 연재 두 번째 글이다. Verilog에 있던 것을 먼저 보고 SystemVerilog가 바꾼 것을 뒤에 본다. 예제는 iverilog 13.0과 Verilator 5.052, 그리고 내가 만들고 있는 오픈소스 RTL 시뮬레이터 vitamin의 vita(공개 저장소 751dd35)로 돌렸다. vita를 만든 과정은 vitamin 연재에 적었다. 합성은 오픈소스 합성 도구인 yosys 0.69로 했다. 상용 도구의 검사 항목과 메시지는 이와 다를 수 있다.
Verilog에는 always가 하나뿐이고, 무엇이 될지는 코드 모양이 정한다
Verilog에서는 combinational logic도, latch도, FF도 모두 always 하나로 쓴다. 합성 도구는 sensitivity list에 posedge나 negedge가 있으면 FF를 만들고, 없으면 block 안의 식을 보고 combinational logic이나 latch를 만든다. 이때 combinational logic의 모양은 block 안의 식이 정하고, sensitivity list는 영향을 주지 않는다.
값을 받는 신호에도 규칙이 있다. always 안에서 값을 받는 신호는 reg로, assign이나 instance의 출력에서 값을 받는 신호는 wire로 선언해야 한다. reg라는 이름은 register를 떠올리게 하지만 hardware와는 관계가 없다. 아래 두 module의 y는 output reg로 선언했는데, 합성하면 FF 없이 mux 하나가 된다. reg는 procedural assignment의 왼쪽에 올 수 있는 variable이라는 뜻일 뿐이다.
sensitivity list를 손으로 쓰면 빠뜨린 신호에서 RTL과 netlist가 갈린다
// (a) Verilog-1995: the sensitivity list is written by hand, and sel is missing
module mux_list (input a, b, sel, output reg y);
always @(a or b)
y = sel ? b : a;
endmodule
// (b) Verilog-2001: @(*) builds the list from what the block reads
module mux_star (input a, b, sel, output reg y);
always @(*)
y = sel ? b : a;
endmodule(a)는 Verilog-1995 방식으로 sensitivity list를 손으로 적었고 sel을 빠뜨렸다. (b)는 Verilog-2001의 @(*)를 썼다. 둘을 yosys로 합성하면 똑같이 다음 한 줄이 된다.
// yosys netlist of (a) and (b): the same single line
assign y = sel ? b : a;
(a)와 (b)의 합성 결과다. sel이 0이면 a를, 1이면 b를 y로 내보낸다. sensitivity list에 없는 sel도 mux의 select로 그대로 연결된다.
합성 도구는 block 안의 식만 보고 logic을 만든다. simulator는 다르다. (a)의 always는 a나 b가 바뀔 때만 깨어나므로 sel만 바뀌면 y를 다시 계산하지 않는다. 두 module과 (a)의 netlist를 한 testbench에 넣고 a, b, sel을 10 ns마다 바꿔 봤다.
$ vita --top tb_sens tb_sens.sv sens.v mux_list.net.v
time a b sel (a)list (b)star gate(a)
0 0 1 0 0 0 0
10 0 1 1 0 1 1
20 1 1 1 1 1 1
30 1 0 1 0 0 0
40 1 0 0 0 1 1
simulation ended (Finish) at time 50
errors=0 warnings=0 notes=0
파형으로 보면, sel이 1이 되는 10에서 (b)와 netlist는 b를 따라 1이 되는데 (a)는 0에 머문다. 20에서 a가 바뀌어 always가 깨어나고 나서야 1이 된다. 40에서 sel이 0으로 돌아가도 (a)는 0에 남는다. iverilog와 Verilator도 같은 표를 낸다.
RTL simulation과 실제 칩이 다르게 움직이는 경우다. Mills와 Cummings는 SNUG 1999 논문에서 합성 도구가 식으로 logic을 만든 뒤 sensitivity list와 비교해 빠진 신호를 경고해 준다고 썼다. 그런데 Cummings의 2016년 논문을 보면 Design Compiler는 이 경고(ELAB-292)를 없앴다. Synopsys는 이런 code quality 검사를 lint 도구의 몫이라고 답했다(SNUG 2016). 이번에 쓴 네 도구도 (a)에 아무 말이 없었고, Verilator의 -Wall lint도 조용했다.
Verilog-2001의 @(*)는 목록을 simulator가 만든다
Verilog-2001은 이 문제를 풀려고 @*를 넣었다. block이 읽는 신호로 sensitivity list를 자동으로 만드는 구문이다. Cummings는 simulator와 합성 도구가 combinational logic과 latch의 완전한 목록을 스스로 만들게 하려는 것이 목적이었다고 설명한다. 같은 개정에서 or 대신 쉼표로 신호를 구분하는 @(a, b)도 들어왔다.
@*와 @(*)는 같은 구문이다. 표준 문법에 두 표기가 모두 올라 있고 뜻은 같다. 네 도구 모두 @*, @(*), @ ( * ), @(a, b, sel), @(a or b or sel)을 받아들였고 yosys의 합성 결과도 한 줄도 다르지 않았다. 이 글은 괄호가 있는 @(*)로 쓴다.
@(*)에는 time 0과 function이라는 두 구멍이 남았다
@(*)가 만드는 것도 결국 sensitivity list다. 목록의 신호가 바뀌어야 block이 돈다는 성질은 그대로여서 두 경우를 놓친다. 첫째는 time 0이다. always @(*)는 목록의 신호가 바뀔 때까지 기다리기만 하고, 먼저 한 번 돌지 않는다. 아래 cfg의 mode는 통합할 때 상수로 묶어 두고 바꾸지 않는 설정 핀이라고 보면 된다. tie는 localparam만 읽는다.
// mode is a strap: tied to a constant at integration and never changes
module cfg (input logic [1:0] mode, output logic [3:0] mask_star, mask_comb);
always @(*)
case (mode)
2'd0: mask_star = 4'b0001;
2'd1: mask_star = 4'b0011;
default: mask_star = 4'b1111;
endcase
always_comb
case (mode)
2'd0: mask_comb = 4'b0001;
2'd1: mask_comb = 4'b0011;
default: mask_comb = 4'b1111;
endcase
endmodule
// a block that reads nothing but constants
module tie (output logic [3:0] id_star, id_comb);
localparam logic [3:0] ID = 4'hA;
always @(*) id_star = ID;
always_comb id_comb = ID;
endmodule아래 testbench는 cfg를 u0과 u1 두 개로 instance하고 tie를 u2로 넣었다. u0의 mode는 선언할 때 2’d1로 초기화한 variable mode_v에, u1의 mode는 상수 2’d1에 바로 연결했다. time 10에 세 instance의 출력을 찍은 뒤 mode_v만 0으로 바꾸고, time 20에 u0을 한 번 더 찍는다.
`timescale 1ns/1ns
// inputs that never change after time 0
module tb_t0;
logic [1:0] mode_v = 2'd1; // variable with a declaration initializer
logic [3:0] ms0, mc0, ms1, mc1, is, ic;
cfg u0 (.mode(mode_v), .mask_star(ms0), .mask_comb(mc0));
cfg u1 (.mode(2'd1), .mask_star(ms1), .mask_comb(mc1)); // constant port connection
tie u2 (.id_star(is), .id_comb(ic));
initial begin
$dumpfile("t0.vcd");
$dumpvars(0, tb_t0);
#10;
$display("time @(*) always_comb");
$display(" 10 u0 (var init) %b %b", ms0, mc0);
$display(" 10 u1 (2'd1) %b %b", ms1, mc1);
$display(" 10 tie %b %b", is, ic);
mode_v = 2'd0;
#10;
$display(" 20 u0 (mode_v=0) %b %b", ms0, mc0);
$finish;
end
endmodule$ vita --top tb_t0 tb_t0.sv cfg.sv
time @(*) always_comb
10 u0 (var init) xxxx 0011
10 u1 (2'd1) 0011 0011
10 tie xxxx 1010
20 u0 (mode_v=0) 0001 0001
simulation ended (Finish) at time 20
errors=0 warnings=0 notes=0
u0의 @(*)는 파형에서도 10까지 x이고, mode가 바뀐 뒤에야 0001이 된다. tie의 @(*)는 끝까지 x다. always_comb는 두 경우 모두 처음부터 값이 있다.
tie의 @(*)는 읽는 신호가 하나도 없어서 한 번도 돌지 않는다. iverilog는 compile할 때 “@* found no sensitivities so it will never trigger”라는 경고로 이것을 알려 준다. u0이 x인 이유는 선언의 초기값에 있다. IEEE 1800은 선언에서 준 초기값이 어떤 initial이나 always보다 먼저 들어가고 event를 만들지 않는다고 정한다. @(*)가 깨어날 변화가 없는 것이다. u1은 상수를 연결한 port의 값이 time 0에 한 번 바뀌면서 @(*)도 돌았다. 다만 time 0에 여러 process가 어떤 순서로 도는지는 표준이 정하지 않았으므로 이 결과에 기대면 안 된다.
둘째는 function이다. @(*)는 block에서 부르는 function의 argument만 목록에 넣고, function 안에서 읽는 신호는 넣지 않는다.
// inv is read inside the function, not passed in as an argument
module fsens (input logic [3:0] a, input logic inv,
output logic [3:0] y_star, y_comb);
function automatic logic [3:0] f(input logic [3:0] v);
f = inv ? ~v : v;
endfunction
always @(*) y_star = f(a);
always_comb y_comb = f(a);
endmodulef는 argument로 a만 받고, inv는 module의 신호를 바로 읽는다. 이 module과 yosys netlist를 한 testbench에 넣었다.
$ vita --top tb_fsens tb_fsens.sv fsens.sv fsens.net.v
time a inv @(*) always_comb gate:@(*) gate:comb
0 0011 0 0011 0011 0011 0011
10 0011 1 0011 1100 1100 1100
20 0101 1 1010 1010 1010 1010
30 0101 0 1010 0101 0101 0101
simulation ended (Finish) at time 40
errors=0 warnings=0 notes=0
파형에서 inv만 바뀐 10과 30을 보면 @(*)의 y는 그대로이고, always_comb와 netlist의 y는 바로 바뀐다. a가 바뀐 20에서야 @(*)도 따라온다.
합성하면 y_star와 y_comb는 같은 XOR gate 네 개가 되고, yosys는 둘을 한 신호로 묶었다. Cummings는 @(*)가 function 안까지 들어가 보지 않는다고 지적했고, Sutherland와 Mills는 이것을 always @(*)의 bug라고 불렀다(SNUG 2013). Verilog-2001만 쓸 수 있다면 function이 읽는 신호를 모두 argument로 넘겨야 @(*)가 볼 수 있다.
Verilator는 두 실험 모두에서 @(*)도 netlist와 같은 값을 냈다. u0과 tie의 @(*)가 처음부터 0011과 1010이고, fsens의 @(*)도 inv를 따라간다. 표준이 정한 동작과는 다르지만 합성 결과와는 맞는다. vita와 iverilog는 표준대로 x와 옛 값을 보여 준다. 어느 simulator를 쓰느냐에 따라 같은 RTL의 불일치가 보이기도 하고 안 보이기도 한다.
SystemVerilog는 always를 always_comb, always_latch, always_ff로 나눴다
SystemVerilog는 always에 의도를 붙인 keyword 세 개를 더했다. always_comb는 combinational logic을, always_latch는 latch를, always_ff는 clock edge로 움직이는 logic을 쓰는 block이다. always_comb와 always_latch는 sensitivity list를 스스로 만들고, always_ff는 여전히 사람이 적는다. block의 본문만 봐서는 clock의 이름과 edge, reset이 synchronous인지 asynchronous인지 알 수 없기 때문이다. always @(*)와 always_comb의 차이는 다음과 같다.
| 항목 | always @(*) | always_comb |
|---|---|---|
| time 0 | 입력이 바뀔 때까지 기다린다 | time 0에 한 번 돈다 |
| function 안에서 읽는 신호 | sensitivity list에 없다 | sensitivity list에 있다 |
| 같은 variable을 다른 process가 씀 | 허용 | 금지 |
| # delay, @ event control | 쓸 수 있다 | 쓸 수 없다 |
| latch가 생겼을 때 | 의도한 것인지 도구가 알 수 없다 | 의도와 다르다고 도구가 경고할 수 있다 |
앞의 두 실험에서 always_comb가 맞는 값을 낸 것이 이 표의 첫 두 줄이다. 같은 variable을 다른 process가 쓰지 못하게 한 것은 두 block이 한 신호를 구동하는 실수를 막기 위해서다. Cummings는 RTL의 combinational block에는 always_comb를, clock으로 움직이는 block에는 always_ff를 쓰고 always @(*)는 그만 쓰라고 권한다. Sutherland와 Mills도 일반 always는 합성하지 않는 model과 testbench에만 쓰라고 한다. lowRISC의 coding style guide는 always @* 대신 always_comb를 쓰도록 정해 두었다.
세 keyword는 netlist를 바꾸지 않는다
// Verilog-2001
module ff_v (input clk, rst_n, en, d, output reg q);
always @(posedge clk or negedge rst_n)
if (!rst_n) q <= 1'b0;
else if (en) q <= d;
endmodule
// SystemVerilog
module ff_sv (input logic clk, rst_n, en, d, output logic q);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) q <= 1'b0;
else if (en) q <= d;
endmodule
module lat_v (input en, d, output reg q);
always @(*)
if (en) q = d;
endmodule
module lat_sv (input logic en, d, output logic q);
always_latch
if (en) q = d;
endmodule같은 FF와 latch를 Verilog와 SystemVerilog로 하나씩 썼다. yosys로 합성하면 ff_v와 ff_sv는 enable과 active-low asynchronous reset이 달린 FF cell($_DFFE_PN0P_) 하나가 된다. lat_v와 lat_sv도 latch cell($_DLATCH_P_) 하나로 같은 netlist가 된다. $로 시작하는 이름은 yosys가 technology mapping 전에 쓰는 내장 gate cell이다. $_DFFE_PN0P_의 네 글자는 차례로 clock의 rising edge(P), active-low reset(N), reset 값 0, active-high enable(P)을 뜻한다. $_DLATCH_P_의 P는 enable이 1일 때 transparent라는 뜻이다. 앞의 fsens에서도 @(*)와 always_comb는 같은 XOR gate 네 개였다.

그림은 ff_sv의 합성 결과를 enable이 없는 FF와 mux로 풀어 그렸다. en이 1이면 mux가 d를, 0이면 q를 다시 D로 넣어 값을 유지한다. rst_n이 0이 되면 clock과 상관없이 CLR이 q를 0으로 만든다. yosys는 이 mux와 FF를 enable이 있는 FF cell 하나로 합쳤다.
case 글에서 unique는 합성 결과를 바꿨지만, 이 세 keyword는 hardware를 바꾸지 않는다. 바뀌는 것은 simulation의 의미(time 0, function)와 도구가 할 수 있는 검사다.
검사는 도구마다 다르고, 표준이 강제하는 것은 일부다
keyword가 약속하는 검사를 도구가 실제로 하는지 보려고, 흔한 실수를 하나씩 넣은 module을 만들어 네 도구에 넣었다. 아래는 그 가운데 다섯 개다.
module md_comb (input logic a, b, output logic y, z);
always_comb y = a & b;
always_comb y = a | b;
endmodule
module l_comb (input logic en, d, output logic q);
always_comb if (en) q = d;
endmodule
// always_latch, but every path assigns q: no latch
module l_nolatch (input logic en, d, output logic q);
always_latch q = en & d;
endmodule
// no edge: this is combinational logic written as always_ff
module f_noedge (input logic a, b, output logic y);
always_ff @(a or b) y <= a & b;
endmodule
// two event controls in one always_ff
module f_twoev (input logic clk, d, output logic q);
always_ff @(posedge clk) begin
q <= d;
@(posedge clk) q <= ~d;
end
endmodulevita와 iverilog는 compile과 simulation을 했고, Verilator는 –lint-only -Wall로 lint를, yosys는 합성을 했다. 표준 열은 IEEE 1800이 error로 정한 것인지, 도구에 맡긴 것인지다.
| 코드 | 표준 | vita | iverilog | Verilator lint | yosys |
|---|---|---|---|---|---|
| always @(*) 두 개가 같은 variable을 씀 | 허용 | 통과 | 통과 | 경고 | 경고 |
| always_comb 두 개가 같은 variable을 씀 (md_comb) | error | error | 통과 | 경고 | 경고 |
| always @(*)에 빈 분기 | 허용(latch) | 통과 | 통과 | 경고 | 경고 |
| always_comb에 빈 분기 (l_comb) | 경고는 선택 | 통과 | 통과 | 경고 | error |
| always_latch인데 latch가 없음 (l_nolatch) | 경고는 선택 | 통과 | 통과 | 경고 | error |
| always_ff에 edge가 없음 (f_noedge) | 경고는 선택 | 통과 | 경고 | 통과 | error |
| always_ff 안에 event control 둘 (f_twoev) | error | error | error | 통과 | parse error |
| always_ff의 variable을 always @(*)도 씀 | error | error | 통과 | 경고, error | 경고 |
| always_ff 안의 blocking assignment | 허용 | 통과 | 통과 | 경고 | 통과 |
표준이 error로 정한 것은 always_comb와 always_ff의 variable을 다른 process가 쓰는 것과, always_ff 안에 event control을 둘 이상 두는 것이다. latch가 생기는지, FF가 생기는지는 도구가 경고해도 되고 안 해도 된다. Sutherland와 Mills는 이 경고가 표준이 요구하지 않는 선택 사항이라고 적었고, 2013년 당시에는 경고하는 simulator가 없고 lint와 합성 도구만 경고한다고 했다. 이번 결과도 크게 다르지 않다. latch 검사는 Verilator lint와 yosys만 했고, simulator 가운데서는 iverilog가 edge 없는 always_ff에 경고한 것이 전부였다.
$ yosys -p "read_verilog -sv l_comb.sv; synth -top l_comb"
ERROR: Latch inferred for signal `\l_comb.\q' from always_comb process `\l_comb.$proc$l_comb.sv:2$1'.
$ yosys -p "read_verilog -sv l_nolatch.sv; synth -top l_nolatch"
ERROR: No latch inferred for signal `\l_nolatch.\q' from always_latch process `\l_nolatch.$proc$l_nolatch.sv:2$1'.
$ yosys -p "read_verilog -sv f_noedge.sv; synth -top f_noedge"
ERROR: Found non edge/level sensitive event in always_ff process `\f_noedge.$proc$f_noedge.sv:2$1'.yosys는 always_comb의 latch, latch가 없는 always_latch, edge가 없는 always_ff를 error로 막는다. 같은 빈 분기를 always @(*)에 쓰면 경고만 낸다. keyword가 의도를 알려 줬기 때문에 경고가 error로 올라간 것이다. Cummings는 Design Compiler가 이런 경우에 경고(ELAB-974, 975, 976)만 낸다며 error로 막아야 한다고 주장했다. Verilator는 lint를 돌려야 경고가 나온다. 표에 나온 경고의 이름은 MULTIDRIVEN, MULTIDRIVENPROC, LATCH, NOLATCH, BLKSEQ다(Verilator 경고 목록).
도구 사이의 차이는 표준이 error로 정한 세 줄에서 가장 컸다. always_comb 두 개가 같은 variable을 쓰는 것, always_ff 안의 두 번째 event control, always_ff의 variable을 다른 process가 쓰는 것이다. 셋을 모두 error로 막은 것은 vita였다. iverilog는 event control만 잡았고, Verilator lint는 나머지 둘을 경고하면서 event control은 놓쳤다. yosys는 event control에서 parse error로 멈췄고 나머지 둘에는 경고만 냈다. 표준이 정한 검사라도 도구가 구현하지 않으면 keyword를 바꾼 효과가 없다. always_ff 안의 blocking assignment는 표준이 막지 않으므로 Verilator만 BLKSEQ로 경고했다. blocking과 non-blocking assignment는 따로 다룬다.
reg와 wire는 logic 하나로 합쳐졌고, 여러 driver가 필요한 곳에만 wire가 남았다
Verilog에서는 신호마다 reg와 wire 가운데 하나를 골라야 했고, 틀리면 compile error가 났다. iverilog를 Verilog-2005 모드로 돌려 reg를 assign으로 구동해 봤다. “Variable ‘y’ cannot be driven by a continuous assignment/module.”라는 error와 함께 SystemVerilog에서는 허용된다는 안내가 나온다.
SystemVerilog는 logic이라는 4-state data type을 더했다. IEEE 1800의 6.11.2는 reg라는 keyword가 hardware register로 읽힐 수 있어 사용자의 의도를 늘 정확히 나타내지는 못한다고 적는다. 같은 절은 logic이 의도를 더 잘 드러내는 이름이고, logic과 reg는 같은 type이라고 덧붙인다(IEEE 1800-2017). Rich는 합성 도구가 발전하면서 reg가 hardware storage보다 software의 variable처럼 쓰이게 됐다고 본다. SystemVerilog가 reg를 logic으로 바꾼 이유도 거기서 찾는다(DVCon). logic은 대부분의 자리에서 variable이 되지만 input과 inout port에서는 net이 된다. 덕분에 port와 신호마다 wire로 할지 reg로 할지 고를 필요가 없어졌다.
variable의 규칙도 넓어졌다. SystemVerilog의 variable은 procedural assignment 여러 개로 쓰거나, continuous assignment 하나로 쓸 수 있다. 둘을 섞거나 continuous assignment를 두 개 두면 error다(IEEE 1800의 6.5). 그래서 logic 신호는 assign으로도 always로도 쓸 수 있고, 실수로 두 곳에서 구동하면 compile할 때 걸린다. wire는 여러 driver의 값을 resolution으로 합치므로 이런 실수를 조용히 넘긴다.
| 코드 | 표준 | vita | iverilog | Verilator lint | yosys |
|---|---|---|---|---|---|
| Verilog: reg를 assign으로 구동 | error | 경고 | error | 통과 | 경고 |
| SystemVerilog: reg를 assign으로 구동 | 허용 | 통과 | 통과 | 통과 | 경고 |
| wire를 always에서 씀 | error | error | error | error | 경고 |
| logic을 assign과 always가 같이 씀 | error | error | error | 경고 | 경고 |
| logic을 assign 두 개가 씀 | error | error | error | 통과 | error |
| wire를 assign 두 개가 씀(tri-state) | 허용 | 통과 | 통과 | 통과 | tribuf pass 필요 |
SystemVerilog에서는 vita와 iverilog가 표준대로 동작했다. reg에 assign을 받아 주고, logic에 driver가 둘이면 error를 낸다. Verilog에서는 둘이 갈렸다. iverilog는 Verilog-2005 모드에서 error를 냈고, vita는 .v 파일의 reg를 assign으로 구동하면 Verilog-2005 도구를 위해 wire로 선언하라는 경고를 냈다. Verilator lint는 assign과 always가 섞인 경우만 MULTIDRIVEN 경고로 잡았다. yosys는 표준보다 느슨해서 wire를 always에서 써도 경고만 하고 합성한다.

wire가 여전히 필요한 경우다. tri-state buffer 두 개가 한 wire y를 같이 구동하고, en0과 en1 가운데 켜진 쪽의 값이 y에 실린다. vita와 iverilog로 돌리면 둘 다 켜져 값이 다를 때 y는 x가, 둘 다 꺼지면 z가 된다. Verilator는 2-state라 x 대신 1을, z 대신 0을 냈다. yosys는 tribuf pass를 넣어야 $_TBUF_ cell 두 개가 y를 같이 구동하는 netlist를 만든다. lowRISC style guide도 logic을 기본으로 쓰되, inout port에 연결되는 net처럼 logic이 맞지 않는 곳은 wire로 선언하라고 정한다.
다섯 가지가 바뀐 이유와 장단점
| 바뀐 것 | 바뀐 이유 | 장점 | 단점 |
|---|---|---|---|
| always @(a or b)에서 @(*)로 (Verilog-2001) | 손으로 쓴 목록에서 신호가 빠지면 RTL과 netlist가 달라진다 | 목록을 빠뜨릴 수 없다 | time 0에 돌지 않고, function 안에서 읽는 신호를 놓친다 |
| always @(*)에서 always_comb로 | @(*)의 두 구멍을 막고 combinational logic이라는 의도를 도구에 알린다 | time 0에 한 번 돌고 function 안까지 보며, 한 process만 쓸 수 있고 latch를 검사할 수 있다 | latch 검사는 선택 사항이고, 다른 process의 쓰기를 잡지 않는 simulator가 있다 |
| latch용 always @(*)에서 always_latch로 | 의도한 latch를 의도하지 않은 latch와 구분한다 | always_comb의 latch 경고와 겹치지 않고, latch가 없으면 도구가 알려 줄 수 있다 | 검사하는 도구가 적다. 이번에는 Verilator lint와 yosys뿐이었다 |
| always @(posedge clk)에서 always_ff로 | clock으로 움직이는 logic이라는 의도를 도구에 알린다 | event control이 하나로 제한되고 한 process만 쓸 수 있으며, FF가 없으면 도구가 알려 줄 수 있다 | sensitivity list는 여전히 손으로 쓰고, blocking assignment는 막지 않는다 |
| reg와 wire에서 logic으로 | reg가 register로 오해되고, 신호마다 reg와 wire를 골라야 했다 | 선언 하나로 assign과 always를 모두 쓰고, driver가 둘이면 compile error가 난다 | 여러 driver가 필요한 곳은 wire로 써야 하고, input port의 logic은 net이 된다는 예외를 알아야 한다 |
단점 열을 보면 always_comb와 always_latch의 단점은 언어가 아니라 도구 쪽에 있다. 그래서 RTL을 SystemVerilog keyword로 바꿀 때는, 쓰는 simulator와 lint가 어떤 실수를 잡는지 이번처럼 작은 module로 한 번 확인해 두는 게 좋을 것 같다.
참고 자료
- vitamin, open-source RTL simulator (vita)
- Don Mills, Clifford E. Cummings, RTL Coding Styles That Yield Simulation and Synthesis Mismatches (SNUG 1999)
- Clifford E. Cummings, SystemVerilog Logic Specific Processes for Synthesis, Benefits and Proper Usage (SNUG 2016)
- Stuart Sutherland, Don Mills, Synthesizing SystemVerilog, Busting the Myth that SystemVerilog is only for Verification (SNUG 2013)
- Dave Rich, The Life of a SystemVerilog Variable (DVCon)
- IEEE 1800-2017, SystemVerilog Unified Hardware Design, Specification, and Verification Language
- lowRISC Verilog Coding Style Guide
- Verilator, Errors and Warnings (LATCH, NOLATCH, MULTIDRIVEN, BLKSEQ)