Verilog(with SV)문법 – blocking과 non-blocking assignment

Verilog의 blocking(=)과 non-blocking(<=) assignment가 simulation과 합성 결과를 어떻게 바꾸는지, race가 생기는 곳과 함께 네 도구로 확인한다.

case 글, always 글에 이은 문법 연재 세 번째 글이다. 예제는 iverilog 13.0과 Verilator 5.052, 그리고 내가 만들고 있는 오픈소스 RTL 시뮬레이터 vitamin의 vita로 돌렸다. 처음에는 공개 저장소의 751dd35로 돌렸고, 10월 15일에 그 뒤에 빌드한 vita 0.2.1로 모든 예제를 다시 돌려 같은 출력을 얻었다. 합성은 yosys 0.69로 했고, 합성한 netlist도 같은 testbench에 넣어 RTL과 나란히 돌렸다. 상용 도구의 결과와 메시지는 이와 다를 수 있다.

=은 그 자리에서 바꾸고, <=는 NBA region까지 미룬다

blocking assignment(=)는 RHS를 계산하고 LHS까지 바꾼 뒤에야 다음 문장으로 넘어간다. non-blocking assignment(<=)는 RHS만 계산해 두고, LHS를 바꾸는 일은 같은 time step의 NBA region으로 미룬 채 다음 문장으로 간다. IEEE 1800-2017은 4.9.3과 4.9.4에서 두 assignment를 이렇게 나눠 정의한다. 한 time step이 어떤 region으로 나뉘고 어떤 순서로 도는지는 RTL 시뮬레이터 연재의 이벤트 큐 편에 적었다. 여기서는 Active와 NBA 두 region만 쓴다.

같은 edge에서 cnt <= cnt + 1과 snap <= cnt가 Active region에서 RHS 4와 3을 계산하고, NBA region에서 cnt = 4, snap = 3으로 바뀌는 그림

같은 clk edge에서 깨어나는 always block 두 개가 cnt와 snap을 <=로 쓰는 경우다. cnt가 3일 때 edge가 오면 두 block은 Active region에서 RHS만 계산한다. 위 block은 4를 얻고, 아래 block은 cnt를 읽어 3을 얻는다. 어느 block이 먼저 돌든 Active region에서 cnt는 아직 3이다. NBA region에 가서야 cnt가 4로, snap이 3으로 바뀐다.

한 always block 안에서는 =의 순서가 회로를 정한다

=은 앞 문장이 바꾼 값을 다음 문장이 바로 읽는다. 한 block 안에 =을 이어 쓰면 그 순서가 곧 계산 순서다. 이벤트 큐 편에서는 두 단 shift register의 <=를 =로 바꾸면 한 단이 되는 것을 봤다. 이번에는 네 단짜리 chain을 =로 쓰고 문장 순서만 바꿨다. q1부터 q3까지는 module 안에만 있고 q4만 밖으로 나간다.

Verilog
// One always block, four blocking assignments. Only the statement order differs.
// q1..q3 stay inside the module; only q4 leaves it.
module chain_fwd (input clk, input d, output reg q4);
    reg q1, q2, q3;
    always @(posedge clk) begin
        q1 = d;
        q2 = q1;
        q3 = q2;
        q4 = q3;
    end
endmodule

module chain_rev (input clk, input d, output reg q4);
    reg q1, q2, q3;
    always @(posedge clk) begin
        q4 = q3;
        q3 = q2;
        q2 = q1;
        q1 = d;
    end
endmodule

module chain_mix (input clk, input d, output reg q4);
    reg q1, q2, q3;
    always @(posedge clk) begin
        q2 = q1;
        q1 = d;
        q4 = q3;
        q3 = q2;
    end
endmodule

module chain_nb (input clk, input d, output reg q4);
    reg q1, q2, q3;
    always @(posedge clk) begin
        q1 <= d;
        q2 <= q1;
        q3 <= q2;
        q4 <= q3;
    end
endmodule

testbench는 d에 한 clock 동안만 1을 넣고, 그 1이 몇 번째 edge에서 q4에 나오는지 본다. 네 module과, 앞의 세 module을 yosys로 합성한 netlist를 한 testbench에 넣었다.

Terminal
$ vita --top tb_chain tb_chain.v chain.v ../syn/chain_fwd.net.v ../syn/chain_rev.net.v ../syn/chain_mix.net.v
time d  fwd rev mix nb | fwd rev mix (netlist)
  10 0   0   x   x   x |  0   x   x
  20 0   0   x   x   x |  0   x   x
  30 0   0   x   0   x |  0   x   0
  40 0   0   0   0   0 |  0   0   0
  50 1   1   0   0   0 |  1   0   0
  60 0   0   0   0   0 |  0   0   0
  70 0   0   0   1   0 |  0   0   1
  80 0   0   1   0   1 |  0   1   0
  90 0   0   0   0   0 |  0   0   0
 100 0   0   0   0   0 |  0   0   0
 110 0   0   0   0   0 |  0   0   0
simulation ended (Finish) at time 110
errors=0 warnings=0 notes=0
d가 40부터 50까지 1일 때 q4가 chain_fwd는 45, chain_mix는 65, chain_rev와 chain_nb는 75에서 1이 되는 파형

d는 40에서 1이 되고, 45의 edge가 그 1을 잡는다. chain_fwd의 q4는 그 edge에서 바로 1이 되고, chain_mix는 65에서, chain_rev와 chain_nb는 75에서 1이 된다. netlist도 RTL과 같은 edge에서 1이 됐다. iverilog와 Verilator의 출력도 vita와 같았다. Verilator는 2-state라 처음 몇 줄의 x를 0으로 찍은 것만 달랐다.

module문장 순서q4가 1이 되는 edge지연(clock)yosys FF 개수
chain_fwdq1, q2, q3, q44511
chain_mixq2, q1, q4, q36533
chain_revq4, q3, q2, q17544
chain_nbq1, q2, q3, q4 (<=)7544

yosys가 만든 FF의 개수는 d가 q4까지 가는 데 걸린 clock 수와 같다. chain_fwd에서는 q1에 들어간 d를 다음 문장이 곧바로 q2로 옮기고, 그다음 문장들이 q3과 q4로 옮긴다. 네 문장이 끝나면 넷 모두 이번 edge의 d를 갖고, q1부터 q3까지의 값을 다음 edge까지 들고 있을 이유가 없다. 그래서 FF 하나만 남았다. chain_rev는 각 문장이 아직 바뀌지 않은 앞 단을 읽도록 순서를 뒤집었으므로 FF가 넷이다. chain_mix에서는 q3 = q2가 첫 줄에서 q1의 옛 값을 받은 q2를 읽는다. q2를 FF로 둘 필요가 없어져 FF가 셋으로 줄었다.

<=로 쓴 chain_nb는 모든 RHS를 edge 직전의 값으로 읽으므로 문장 순서가 결과를 바꾸지 않는다. Cummings는 SNUG 2000 논문에서 세 단 pipeline으로 같은 비교를 했다. 정순의 =은 register 하나로 합성된다고 했고, 역순의 =은 pipeline대로 동작하지만 그 역시 나쁜 coding style이라고 적었다.

Verilator의 -Wall lint는 =을 쓴 세 module의 = 줄마다 BLKSEQ 경고를 냈다. always 글의 비교 표에서 Verilator만 경고한 always_ff 안의 blocking assignment도 q1 = d; q2 = q1;로 된 두 단 chain이었다.

always block을 둘로 나누면 둘 중 무엇이 먼저 도는지 정해져 있지 않다

이번에는 =을 쓴 두 문장을 서로 다른 always block에 나눴다. 한 block은 cnt를 1씩 올리고, 다른 block은 같은 edge에서 cnt를 snap으로 옮긴다. race_b는 race_a와 두 block의 순서만 다르고, race_nb는 두 block 모두 <=로 썼다.

Verilog
// Two always blocks wake on the same clk edge. Both use blocking assignments.
module race_a (input clk, input rst, output reg [3:0] cnt, output reg [3:0] snap);
    always @(posedge clk)
        if (rst) cnt = 4'd0;
        else     cnt = cnt + 4'd1;

    always @(posedge clk)
        snap = cnt;
endmodule

// The same two blocks, written in the other order.
module race_b (input clk, input rst, output reg [3:0] cnt, output reg [3:0] snap);
    always @(posedge clk)
        snap = cnt;

    always @(posedge clk)
        if (rst) cnt = 4'd0;
        else     cnt = cnt + 4'd1;
endmodule

// Nonblocking assignments in both blocks.
module race_nb (input clk, input rst, output reg [3:0] cnt, output reg [3:0] snap);
    always @(posedge clk)
        if (rst) cnt <= 4'd0;
        else     cnt <= cnt + 4'd1;

    always @(posedge clk)
        snap <= cnt;
endmodule

두 block은 같은 edge에서 함께 깨어나 Active region에 들어간다. IEEE 1800-2017의 4.7은 Active region의 event를 어떤 순서로 꺼내 처리해도 된다고 정한다. snap = cnt가 먼저 돌면 snap은 옛 cnt를 받고, cnt를 올리는 block이 먼저 돌면 새 cnt를 받는다. 어느 쪽이 나와도 표준을 어긴 것이 아니다. vita와 iverilog로 돌린 결과다.

Terminal
$ vita --top tb_race tb_race.v race.v ../syn/race_a.net.v ../syn/race_b.net.v
time  cnt/snap: race_a race_b race_nb | netlist_a netlist_b
  10            0/0    0/x    0/x   |   0/x       0/x
  20            1/1    1/0    1/0   |   1/0       1/0
  30            2/2    2/1    2/1   |   2/1       2/1
  40            3/3    3/2    3/2   |   3/2       3/2
  50            4/4    4/3    4/3   |   4/3       4/3
simulation ended (Finish) at time 50
errors=0 warnings=0 notes=0
Terminal
$ iverilog -g2012 -o race.vvp -s tb_race tb_race.v race.v ../syn/race_a.net.v ../syn/race_b.net.v
$ vvp -n race.vvp
VCD info: dumpfile race.vcd opened for output.
time  cnt/snap: race_a race_b race_nb | netlist_a netlist_b
  10            0/x    0/0    0/x   |   0/x       0/x
  20            1/1    1/0    1/0   |   1/0       1/0
  30            2/1    2/2    2/1   |   2/1       2/1
  40            3/3    3/2    3/2   |   3/2       3/2
  50            4/3    4/4    4/3   |   4/3       4/3
tb_race.v:30: $finish called at 50 (1ns)

vita는 같은 region 안의 process를 source에 나온 순서대로 돌린다. 그래서 race_a의 snap은 cnt와 같고, race_b의 snap은 한 clock 늦다. iverilog는 같은 race_a에서 edge마다 결과가 바뀌었다. snap이 새 cnt를 받는 edge와 옛 cnt를 받는 edge가 번갈아 나왔고, race_b는 그 반대 순서로 번갈아 나왔다. iverilog가 이 순서를 어떻게 정하는지는 소스까지 확인하지 않았다. Verilator는 두 module 모두 snap이 한 clock 늦었다.

cnt register의 Q가 +1을 거쳐 자기 D로 돌아가고 같은 Q가 snap register의 D로 들어가며, rst가 cnt의 R pin에, clk가 두 register의 clock pin에 들어가는 회로

race_a와 race_b를 yosys로 합성하면 둘 다 이 회로가 된다. 4-bit register인 cnt의 Q가 +1을 거쳐 자기 D로 돌아가고, 같은 Q가 snap register의 D로 들어간다. R은 rst가 1일 때 cnt를 0으로 만드는 synchronous reset이다. 두 register가 같은 edge에서 값을 잡으므로 snap에는 edge 직전의 cnt가 들어간다. netlist에서 +1은 XOR, AND, NOT gate 6개로 풀려 있다.

cnt가 10 ns마다 0, 1, 2, 3, 4로 늘 때 vita의 race_a snap은 cnt와 같은 칸에서 바뀌고, race_b와 netlist의 snap은 한 칸 늦게 바뀌며, iverilog의 race_a snap은 1과 3에 두 칸씩 머무는 파형

vita의 race_a는 snap이 cnt와 같은 칸에서 바뀌고, race_b와 netlist는 한 칸 늦다. iverilog의 race_a는 15부터 1, 35부터 3으로 두 칸씩 머문다.

합성된 회로는 언제나 옛 cnt를 잡는다. 그러니 RTL simulation이 netlist와 맞을지는 simulator가 고른 순서에 달렸다. vita에서는 block을 적은 순서가, iverilog에서는 edge가 결과를 바꿨다. 두 block을 <=로 쓴 race_nb는 세 simulator 모두 netlist와 같은 값을 냈고, 앞 그림에서 본 경우다.

posedge block 안의 임시 변수는 누가 읽느냐에 따라 FF가 된다

sequential block에는 <=를 쓴다는 규칙에도 흔한 예외가 있다. 긴 식을 나눠 쓰려고 posedge block 안에서 임시 변수에 =로 값을 넣고, 그 값을 <=로 register에 넘기는 코드다. always_ff를 쓰는 SystemVerilog라면 tmp를 block 안에 선언할 수도 있는데, 그 경우는 뒤에서 본다.

Verilog
// tmp is written and read inside one always block only.
module tmp_local (input clk, input [3:0] a, b, output reg [4:0] q);
    reg [4:0] tmp;
    always @(posedge clk) begin
        tmp = a + b;
        q  <= tmp;
    end
endmodule

// A second always block also reads tmp.
module tmp_shared (input clk, input [3:0] a, b, output reg [4:0] q, output reg [4:0] r);
    reg [4:0] tmp;
    always @(posedge clk) begin
        tmp = a + b;
        q  <= tmp;
    end

    always @(posedge clk)
        r <= tmp;
endmodule

// The same two blocks, written in the other order.
module tmp_shared_b (input clk, input [3:0] a, b, output reg [4:0] q, output reg [4:0] r);
    reg [4:0] tmp;
    always @(posedge clk)
        r <= tmp;

    always @(posedge clk) begin
        tmp = a + b;
        q  <= tmp;
    end
endmodule

tmp_local의 tmp는 같은 block에서 쓰고 바로 읽는다. yosys로 합성하면 tmp는 adder의 출력 wire가 되고, FF는 q의 5개뿐이다. tmp_shared에서는 다른 always block이 tmp를 읽는다. tmp가 다음 edge까지 값을 들고 있어야 하므로 FF가 되고, 합성 결과의 FF는 10개로 늘었다. q와 tmp는 같은 값을 잡으므로 yosys가 둘을 FF 5개로 합쳤고, 나머지 5개가 r이다.

Terminal
$ vita --top tb_tmp tb_tmp.v tmp.v ../syn/tmp_local.net.v ../syn/tmp_shared.net.v
time a+b | local q | shared q r | shared_b q r | netlist local q, shared q r
  10   2  |    2    |    2  2    |     2  x     |       2,          2  x
  20   3  |    3    |    3  3    |     3  2     |       3,          3  2
  30   4  |    4    |    4  4    |     4  3     |       4,          4  3
  40   5  |    5    |    5  5    |     5  4     |       5,          5  4
  50   6  |    6    |    6  6    |     6  5     |       6,          6  5
simulation ended (Finish) at time 50
errors=0 warnings=0 notes=0

같은 block에서만 쓴 tmp_local은 세 simulator와 netlist가 모두 같았다. 다른 block이 읽는 r은 앞의 snap과 똑같이 갈렸다. vita에서는 block 순서에 따라 q와 같거나 한 clock 늦었고, iverilog에서는 edge마다 둘 사이를 오갔다. Verilator와 netlist에서는 한 clock 늦었다.

Verilator 5.052의 -Wall lint는 tmp_local과 tmp_shared에 똑같이 BLKSEQ 하나씩을 냈다. 다른 block이 tmp를 읽는지는 가리지 않았다. Verilator 문서는 BLKSEQ를 code style 경고로 분류한다. 경고 설명에는 만든 값을 같은 block의 뒤에서만 쓴다면 그렇게 써도 되지만, 실수하기 쉬워 권하지 않는다고 적혀 있다. Cummings도 tmp_local과 같은 모양의 예제가 simulation과 합성 모두 맞게 동작한다고 하면서, 한 always block에 =과 <=를 섞는 것은 권하지 않았다. FSM 글에서 1-always FSM의 중간값을 =로 계산하자 BLKSEQ가 붙은 것도 같은 경우다.

combinational block의 <=는 한 번 늦은 값을 남긴다

반대로 combinational logic을 쓰는 always block에 <=를 쓰면 어떻게 될까. 아래는 4-bit 두 값을 더해 15를 넘으면 15로 자르는 saturating adder다. s에 합을 넣고 그 s로 y를 정한다. (a)는 sensitivity list를 손으로 쓰면서 s를 빠뜨렸고, (b)는 @(*)를 썼다. (c)는 =로 쓴 기준이다.

Verilog
// Saturating add: s is the 5-bit sum, y clips it to 4'hf.
// (a) nonblocking, hand-written list without s
module sat_list (input [3:0] a, b, output reg [3:0] y);
    reg [4:0] s;
    always @(a or b) begin
        s <= a + b;
        y <= s[4] ? 4'hf : s[3:0];
    end
endmodule

// (b) nonblocking, @(*)
module sat_star (input [3:0] a, b, output reg [3:0] y);
    reg [4:0] s;
    always @(*) begin
        s <= a + b;
        y <= s[4] ? 4'hf : s[3:0];
    end
endmodule

// (c) blocking, @(*)
module sat_bl (input [3:0] a, b, output reg [3:0] y);
    reg [4:0] s;
    always @(*) begin
        s = a + b;
        y = s[4] ? 4'hf : s[3:0];
    end
endmodule
Terminal
$ vita --top tb_sat tb_sat.v sat.v ../syn/sat_list.net.v
time  a  b  want | (a) <= list  (b) <= @(*)  (c) = @(*) | netlist (a)
  0   3  4   7  |    x        7       7     |    7
 10   9  8  15  |    7       15      15     |   15
 20   2  2   4  |   15        4       4     |    4
 30  15  1  15  |    4       15      15     |   15
 40   5  5  10  |   15       10      10     |   10
simulation ended (Finish) at time 50
errors=0 warnings=0 notes=0
a와 b가 10 ns마다 바뀔 때 (a)의 y는 한 칸 앞의 합인 7, 15, 4, 15를 내고, (b)와 netlist의 y는 7, 15, 4, 15, 10을 내는 파형

(a)의 y는 a와 b가 바뀐 칸에서 한 칸 앞의 합을 보인다. 10에서 9와 8이 들어왔는데 y는 3 + 4인 7이다. (b)와 netlist는 처음부터 맞는 값을 낸다.

(a)가 깨어나면 s <=는 새 합을 NBA region에 예약하고, 다음 줄의 y <=는 아직 바뀌지 않은 s로 y를 계산한다. NBA region에서 s가 바뀌어도 s가 sensitivity list에 없으니 block은 다시 돌지 않는다. (b)의 @(*)는 block에서 읽는 s도 목록에 넣는다. s가 바뀌면 block이 한 번 더 돌고, 그때 y가 맞는 값이 된다. 시간은 흐르지 않고 delta cycle만 하나 더 돈다.

yosys는 (a), (b), (c)를 모두 같은 gate 20개로 만들었고, 이 netlist는 처음부터 맞는 값을 냈다. RTL과 netlist가 다른 것은 (a)뿐이다. iverilog와 Verilator의 (a)도 vita처럼 한 번 늦었다. Cummings는 AND-OR logic으로 같은 실험을 하고, 목록을 채워도 block을 두 번 돌아야 하니 combinational logic에는 =을 쓰라고 권했다. 그런데 Verilator lint는 (b)에만 COMBDLY 경고를 냈고, 결과가 틀린 (a)에는 아무 말이 없었다.

testbench가 clk edge에서 =로 값을 바꾸면 FF가 무엇을 잡을지 모른다

race는 RTL 안에서만 생기지 않는다. testbench가 DUT 입력을 바꾸는 process도 DUT의 FF와 같은 edge에서 깨어나면 같은 일이 생긴다.

testbench가 d를 DUT 안 FF의 D pin으로 보내고, clk 하나가 testbench와 FF의 clock pin에 함께 들어가는 블록도

testbench와 DUT의 FF가 같은 clk를 본다. testbench가 posedge clk에서 d를 바꾸면 FF도 같은 edge에서 d를 읽는다. 아래 testbench는 같은 4-bit register 네 개에 d를 네 가지 방법으로 넣는다. (1)은 posedge 뒤에 =로, (2)는 <=로, (3)은 negedge에서 =로 바꾼다. (4)는 뒤에서 볼 clocking block이다.

SystemVerilog
// The same 4-bit register, driven four ways by the testbench.
module dff4 (input logic clk, input logic [3:0] d, output logic [3:0] q);
    always @(posedge clk)
        q <= d;
endmodule

// (1) blocking assignment right after the rising edge
initial begin
    d_bl = 0;
    for (int i = 1; i <= 4; i++) begin
        @(posedge clk);
        d_bl = i;
    end
end

// (2) nonblocking assignment right after the rising edge
initial begin
    d_nb = 0;
    for (int i = 1; i <= 4; i++) begin
        @(posedge clk);
        d_nb <= i;
    end
end

// (3) blocking assignment on the falling edge
initial begin
    d_neg = 0;
    @(posedge clk);
    for (int i = 1; i <= 4; i++) begin
        @(negedge clk);
        d_neg = i;
        @(posedge clk);
    end
end

clocking block을 지원하지 않는 simulator가 있어서, 세 simulator 모두 clocking block을 빼고(-DNO_CLOCKING) 돌렸다. 표는 10부터 50까지 negedge마다 찍은 q다.

simulator(1) =(2) <=(3) negedge
vita0 1 2 3 40 1 2 3 40 1 2 3 4
iverilog1 1 3 3 40 1 2 3 40 1 2 3 4
Verilator1 2 3 4 40 1 2 3 40 1 2 3 4

세 simulator로 돌려 보니 (1)의 q가 모두 달랐다. vita에서는 q가 한 clock 늦게 d를 따라갔고, Verilator에서는 같은 edge의 새 d를 잡았다. iverilog에서는 두 경우가 번갈아 나와 q가 1, 1, 3, 3, 4가 됐다. testbench의 d = 와 FF가 d를 읽는 일이 같은 Active region에 들어가고, 4.7은 그 순서를 정하지 않는다. 그래서 셋 중 어느 결과도 표준을 어긴 것이 아니다. (2)와 (3)은 세 simulator가 같은 값을 냈다. (2)의 d는 NBA region에서 바뀌므로 FF는 언제나 옛 d를 읽는다. (3)은 d가 바뀌는 시각과 FF가 d를 잡는 시각이 반 clock 떨어져 있다.

d가 5부터 10 ns마다 1, 2, 3, 4로 바뀔 때 (1)의 q가 vita에서는 한 칸 늦게 따라가고 iverilog에서는 1과 3에 두 칸씩 머물며, (2)의 q는 한 칸 늦게 따라가는 파형

vita의 (1)과 (2)는 같은 모양이다. iverilog의 (1)은 5와 25의 edge에서 새 d를 잡고, 15와 35의 edge에서는 옛 d를 잡았다.

같은 변수에 =과 <=를 섞으면 simulator와 합성 도구가 다른 회로를 본다

Verilog는 한 always block 안에서 같은 변수에 =과 <=를 함께 쓰는 것을 막지 않는다. 나는 같은 q를 두 방식으로 쓰는 module 셋을 만들었다. mix_rst는 reset만 =로 쓰고, mix_clr은 <=로 d를 넣은 뒤 clr일 때 =로 0을 넣는다. nb_clr은 mix_clr의 clear를 <=로 바꾼 것이다.

Verilog
// '=' and '<=' on the same q, in one always block.
// (1) reset with '=', load with '<='
module mix_rst (input clk, input rst_n, input d, output reg q);
    always @(posedge clk or negedge rst_n)
        if (!rst_n) q = 1'b0;
        else        q <= d;
endmodule

// (2) load with '<=', then clear with '='
module mix_clr (input clk, input clr, input d, output reg q);
    always @(posedge clk) begin
        q <= d;
        if (clr) q = 1'b0;
    end
endmodule

// (3) the same clear written with '<=' only
module nb_clr (input clk, input clr, input d, output reg q);
    always @(posedge clk) begin
        q <= d;
        if (clr) q <= 1'b0;
    end
endmodule
Terminal
$ vita --top tb_mix tb_mix.v mix.v ../syn/mix_rst.net.v ../syn/mix_clr.net.v ../syn/nb_clr.net.v
time rst_n clr | mix_rst mix_clr nb_clr | netlist: mix_rst mix_clr nb_clr
  10   1    0  |    1       1       1    |          1       0       1
  20   1    0  |    1       1       1    |          1       0       1
  30   1    1  |    1       1       0    |          1       0       0
  40   1    1  |    1       1       0    |          1       0       0
  50   1    0  |    1       1       1    |          1       0       1
simulation ended (Finish) at time 50
errors=0 warnings=0 notes=0

d는 계속 1이고, clr은 25와 35의 edge에서만 1이다. mix_clr을 쓴 사람은 그때 q가 0이 되기를 바랐을 것이다. 세 simulator에서 q는 clr과 상관없이 1에 머물렀다. q = 0이 Active region에서 q를 0으로 바꿔도, 그보다 앞서 예약된 q <= d가 NBA region에서 다시 1을 쓴다. 표준이 정한 순서라 simulator 사이에 차이가 없었다. nb_clr처럼 둘 다 <=로 쓰면 같은 변수의 NBA는 실행된 순서대로 반영되므로(IEEE 1800-2017 4.6) 나중의 0이 남는다.

yosys는 mix_clr을 전혀 다르게 읽었다. 합성 log에는 경고가 하나도 없었는데, netlist의 q는 상수 0이었다. yosys가 읽어 들인 process를 dump해 보면 d가 한 번도 나오지 않는다.

Terminal
$ grep "assign q" ../syn/mix_clr.net.v
  assign q = 1'h0;

$ yosys -p "read_verilog ../hdl/mix.v; hierarchy -top mix_clr; dump mix_clr" \
    | awk '/^  process/{on=1} on{print} /^  end$/{if(on)exit}' | grep -v attribute
  process $proc$../hdl/mix.v:11$3
    assign { } { }
    assign { } { }
    assign $0\q[0:0] $1\q[0:0]
    switch \clr
      case 1'1
        assign { } { }
        assign $1\q[0:0] 1'0
      case 
        assign $1\q[0:0] \q
    end
    sync posedge \clk
      update \q $0\q[0:0]
  end

clr이면 0을, 아니면 q를 그대로 두는 식만 남아 있다. 0밖에 받을 수 없는 FF이니 뒤의 최적화가 q를 상수 0으로 바꿨다. RTL simulation은 clr을 무시하고, netlist는 d를 무시한다. yosys가 q <= d를 어느 단계에서 버리는지는 따라가 보지 않았다. mix_rst는 yosys가 보통의 asynchronous reset FF로 합성해 simulation과 netlist가 같았다. 그런데 Cummings는 mix_rst와 같은 모양의 코드에 Synopsys 도구가 syntax error를 낸다고 적었다.

Verilator에는 같은 변수에 =과 <=가 섞이면 내는 BLKANDNBLK라는 error가 있다. 그런데 5.052의 -Wall lint는 mix_rst와 mix_clr에 BLKSEQ만 냈다. 문서에 따르면 5.038부터는 =이 combinational logic 쪽에 있고 두 assignment가 겹치지 않는다고 증명할 수 없을 때만 이 error를 낸다. clocked block의 <=와 always @(*)의 =이 한 q를 나눠 쓰는 mix_proc에서야 BLKANDNBLK가 나왔다.

Verilog
// (4) '<=' in a clocked block and '=' in a combinational block, same q
module mix_proc (input clk, input clr, input d, output reg q);
    always @(posedge clk)
        q <= d;
    always @(*)
        if (clr) q = 1'b0;
endmodule
Terminal
$ verilator --lint-only -Wall -Wno-DECLFILENAME --timing mix_proc.v 2>&1 | grep '^%'
%Warning-MULTIDRIVENPROC: mix_proc.v:5:18: Variable written to in always block also written by another always block: 'q'
%Warning-LATCH: mix_proc.v:4:5: Latch inferred for signal 'q' (not all control paths of combinational always assign a value)
%Error-BLKANDNBLK: mix_proc.v:1:60: Unsupported: Blocking and non-blocking assignments to potentially overlapping bits of same packed variable: 'q'
%Error: Exiting due to 1 error(s), 2 warning(s)
코드표준vitaiverilogVerilator lintyosys
mix_rst: reset은 =, load는 <=허용통과통과BLKSEQasynchronous reset FF
mix_clr: load는 <=, clear는 =허용(q는 d를 따름)통과통과BLKSEQ경고 없이 q가 상수 0
mix_proc: clocked <=와 @(*)의 =허용통과통과BLKANDNBLK errorlatch, driver 충돌 경고
mix_bits: 같은 vector의 다른 bit허용통과통과통과통과

mix_bits는 v[3:1]을 clocked block의 <=로, v[0]을 always @(*)의 =로 쓴 module이다. 겹치지 않는 bit를 나눠 쓴 경우라 Verilator도 BLKANDNBLK를 내지 않았고, 네 도구 모두 아무 말이 없었다. vita와 iverilog는 네 module을 모두 그대로 돌렸다. Verilog 표준이 막지 않는 코드이니 simulator로서는 틀린 동작이 아니다. Verilator 문서도 BLKANDNBLK가 SystemVerilog에서 불법은 아니고 좋은 coding practice를 어긴 것이라고 적는다.

SystemVerilog의 always_ff와 always_comb도 =과 <=를 골라 주지 않는다

SystemVerilog에서는 always_ff에 <=를, always_comb에 =을 쓰는 것이 관례다. always 글에서 본 것처럼 두 keyword는 다른 process가 같은 variable을 쓰지 못하게 막는다. 하지만 always_ff 안의 =이나 always_comb 안의 <=는 막지 않는다. IEEE 1800-2017 9.2.2.2의 always_comb 예제에는 d <= #1ns b & c;가 그대로 나온다.

SystemVerilog
// '=' inside always_ff: legal
module ff_bl (input logic clk, input logic [3:0] a, b, output logic [4:0] q);
    always_ff @(posedge clk) begin
        logic [4:0] tmp;
        tmp = a + b;
        q  <= tmp;
    end
endmodule

// '=' inside always_ff to a module-level variable
module ff_bl_mod (input logic clk, input logic [3:0] a, b, output logic [4:0] q);
    logic [4:0] tmp;
    always_ff @(posedge clk) begin
        tmp = a + b;
        q  <= tmp;
    end
endmodule

// '<=' inside always_comb: legal
module comb_nb (input logic [3:0] a, b, output logic [4:0] y);
    always_comb
        y <= a + b;
endmodule

ff_bl은 tmp_local을 always_ff로 옮기면서 tmp를 block 안에 선언했다. 이름 없는 block에 선언한 변수는 hierarchical reference로도 부를 수 없으므로(6.21), tmp_shared 같은 race가 생길 자리가 없다. Verilator lint도 tmp를 module에 선언한 ff_bl_mod에만 BLKSEQ를 내고, ff_bl에는 내지 않았다. comb_nb에는 Verilator가 COMBDLY를, iverilog가 경고를 냈다.

always_comb 안의 <=는 @(*)처럼 다시 돌지 않는다

앞의 saturating adder를 always_comb로 옮기면 결과가 또 달라진다. always_comb의 sensitivity list는 9.2.2.2.1이 정하는데, block 안에서 쓰는 변수는 읽더라도 목록에서 뺀다. s는 block 안에서 쓰므로 목록에 없다. 표준대로라면 NBA region에서 s가 바뀌어도 block은 다시 돌지 않고, y는 (a)처럼 한 번 늦어야 한다. iverilog로 돌린 결과다.

SystemVerilog
// (d) nonblocking inside always_comb
module sat_comb (input logic [3:0] a, b, output logic [3:0] y);
    logic [4:0] s;
    always_comb begin
        s <= a + b;
        y <= s[4] ? 4'hf : s[3:0];
    end
endmodule

// (e) blocking inside always_comb
module sat_comb_bl (input logic [3:0] a, b, output logic [3:0] y);
    logic [4:0] s;
    always_comb begin
        s = a + b;
        y = s[4] ? 4'hf : s[3:0];
    end
endmodule
Terminal
$ vvp -n satc.vvp
VCD info: dumpfile satc.vcd opened for output.
time  a  b  want | (d) always_comb <=  (e) always_comb =  (b) <= @(*) | netlist (d)
  1   3  4   7  |       x             7           7     |    7
 11   9  8  15  |       7            15          15     |   15
 21   2  2   4  |      15             4           4     |    4
 31  15  1  15  |       4            15          15     |   15
 41   5  5  10  |      15            10          10     |   10
tb_satc.sv:29: $finish called at 51 (1ns)

iverilog의 (d)는 한 번 늦었고, always_comb 안에 <=를 쓰지 말라는 경고도 냈다. 같은 식을 =로 쓴 (e)와, <=를 쓴 @(*)의 (b)는 맞는 값이다. (d)를 합성한 netlist는 처음부터 맞는 값을 냈다. @(*)에서 맞던 코드가 keyword만 always_comb로 바꾸자 netlist와 달라졌다. Verilator는 (d)에서도 netlist와 같은 값을 냈다. Verilator 문서는 COMBDLY를 무시하면 합성 도구처럼 <=를 =로 바꿔 처리하고, 그래서 다른 simulator와 결과가 다를 수 있다고 적었다.

Terminal
$ vita --top tb_satc tb_satc.sv sat_sv.sv sat.v ../syn/sat_comb.net.v
time  a  b  want | (d) always_comb <=  (e) always_comb =  (b) <= @(*) | netlist (d)
  1   3  4   7  |       7             7           7     |    7
 11   9  8  15  |      15            15          15     |   15
 21   2  2   4  |       4             4           4     |    4
 31  15  1  15  |      15            15          15     |   15
 41   5  5  10  |      10            10          10     |   10
simulation ended (Finish) at time 51
errors=0 warnings=0 notes=0

vita도 (d)에서 netlist와 같은 값을 냈다. 이것은 해석의 차이가 아니고 vita가 표준과 다르게 돈 것이다. 9.2.2.2.1의 b)는 block 안에서 쓰는 식을 sensitivity list에서 뺀다고 적는다. <=로 쓴 s도 block 안에서 쓴 식이다. 어디서 갈리는지 보려고 block이 돌 때마다 $display를 찍었다.

SystemVerilog
`timescale 1ns/1ns
module comb_nb_runs;
    logic [3:0] a = 4'd0, s, y;
    always_comb begin
        $display("%0t run: a=%0d s=%0d", $time, a, s);
        s <= a + 4'd1;
        y <= s;
    end
    initial begin
        #1 a = 4'd5;
        #1 $display("%0t y=%0d s=%0d", $time, y, s);
        $finish;
    end
endmodule
Terminal
$ vita comb_nb_runs.sv
0 run: a=0 s=x
0 run: a=0 s=1
1 run: a=5 s=1
1 run: a=5 s=6
2 y=6 s=6
simulation ended (Finish) at time 2
errors=0 warnings=0 notes=0

$ iverilog -g2012 -o comb_nb_runs.vvp comb_nb_runs.sv 2>/dev/null
$ vvp -n comb_nb_runs.vvp
0 run: a=0 s=x
1 run: a=5 s=1
2 y=1 s=6

iverilog는 a가 바뀐 1 ns에 block을 한 번만 돌렸고, y는 옛 s인 1을 받았다. vita는 NBA region에서 s가 6으로 바뀌자 block을 한 번 더 돌렸고, 그래서 y도 6이 됐다. vita가 s를 sensitivity list에서 빼지 않은 것이다. 이 부분은 표준을 따른 iverilog의 결과가 맞다. Verilator의 6도 표준과 다르지만, 앞에서 본 대로 문서에 적어 둔 동작이다.

automatic 변수에는 <=를 쓸 수 없다

표준이 <=를 금지하는 곳도 있다. IEEE 1800-2017 10.4.2는 ‘It shall be illegal to make nonblocking assignments to automatic variables.’라고 적는다. block 안에 automatic으로 선언한 변수와 automatic task의 변수가 여기에 해당한다.

SystemVerilog
// '<=' to an automatic variable: illegal (IEEE 1800-2017 10.4.2)
module auto_nb (input logic clk, input logic [3:0] a, output logic [3:0] q);
    always_ff @(posedge clk) begin
        automatic logic [3:0] t;
        t <= a + 4'd1;
        q <= t;
    end
endmodule

// '<=' to a variable of an automatic task: illegal (IEEE 1800-2017 10.4.2)
module auto_task (input logic clk, input logic [3:0] a, output logic [3:0] q);
    task automatic inc(input logic [3:0] x, output logic [3:0] y);
        logic [3:0] t;
        t <= x + 4'd1;
        y = t;
    endtask
    always_ff @(posedge clk) begin
        logic [3:0] v;
        inc(a, v);
        q <= v;
    end
endmodule
Terminal
$ verilator --lint-only -Wall -Wno-DECLFILENAME --timing auto_nb.sv 2>&1 | grep '^%'
%Error: auto_nb.sv:4:9: Automatic lifetime variable not allowed in nonblocking assignment (IEEE 1800-2023 6.21): 't'
%Error: Exiting due to 1 error(s)
$ iverilog -g2012 -o /dev/null auto_task.sv
auto_task.sv:4: error: automatically allocated variables may not be assigned values using non-blocking assignments.
1 error(s) during elaboration.

Verilator는 두 module 모두 IEEE 1800-2023의 6.21을 들어 error를 냈다. iverilog는 auto_task에서 같은 뜻의 error를 냈지만, auto_nb에서는 block 안의 automatic 선언을 아직 지원하지 않는다는 sorry 메시지에서 멈췄다. vita도 두 module 모두 error로 멈췄다. 다만 메시지는 <=를 금지한다는 내용이 아니라 아직 지원하지 않는 구문이라는 내용이었다. yosys는 둘 다 경고 없이 합성했고, netlist의 q는 4’hx였다.

코드표준vitaiverilogVerilator lintyosys
always_ff 안에서 module의 tmp에 = (ff_bl_mod)허용통과통과BLKSEQ통과
always_ff 안에서 선언한 tmp에 = (ff_bl)허용통과통과통과통과
always_comb 안의 <= (comb_nb)허용통과경고COMBDLY통과
automatic 변수에 <= (auto_nb)errorerror(미지원)sorry(미지원)error통과, q = x
automatic task 변수에 <= (auto_task)errorerror(미지원)errorerror통과, q = x

clocking block은 testbench의 drive를 DUT가 읽은 뒤로 미룬다

SystemVerilog의 clocking block은 testbench 쪽 race를 문법으로 다룬다. 14.16에 따르면 skew가 0인 output은 clocking event가 일어난 time step의 Re-NBA region에서 바뀐다. Re-NBA region은 DUT의 always가 도는 Active, NBA region보다 뒤에 온다. 그래서 DUT의 FF는 언제나 옛 d를 잡는다. input은 기본 skew가 1step이라 clocking event 바로 앞 time step이 끝날 때의 값을 읽는다(14.3, 14.4). 다만 14.4의 NOTE는 program block 밖에서 같은 clock을 기다린 process가 clocking block의 값을 읽으면 race가 남는다고 적는다.

SystemVerilog
clocking cb @(posedge clk);
    default input #1step output #0;
    output d_cb;
endclocking

// (4) clocking block drive
initial begin
    d_cb = 0;
    for (int i = 1; i <= 4; i++) begin
        @(cb);
        cb.d_cb <= i;
    end
end

앞의 testbench에 (4)로 넣은 부분이다. default input #1step output #0은 이 clocking block의 input과 output 기본 skew를 정한다. (4)는 @(cb)로 clocking event를 기다렸다가 cb.d_cb <=로 d를 바꾼다. 이번에는 -DNO_CLOCKING 없이 돌렸다.

Terminal
$ verilator --binary --timing --no-stop-fail -Wno-fatal -Wno-lint -Wno-style --Mdir obj_drive --top tb_drive tb_drive.sv
$ ./obj_drive/Vtb_drive
-Info: tb_drive.sv:72: $dumpvar ignored, as Verilated without --trace
time  q:  (1) =  (2) <=  (3) negedge  (4) clocking
  10         1      0        0             0
  20         2      1        1             1
  30         3      2        2             2
  40         4      3        3             3
  50         4      4        4             4
- tb_drive.sv:80: Verilog $finish
- S i m u l a t i o n   R e p o r t: Verilator 5.052 2026-09-05
- Verilator: $finish at 51ns; walltime 0.000 s; speed 1.500 ms/s
- Verilator: cpu 0.000 s on 1 threads; allocated 2 MB

$ iverilog -g2012 -o drive.vvp -s tb_drive tb_drive.sv 2>&1 | head -2
tb_drive.sv:53: syntax error
tb_drive.sv:53: error: Invalid module item.

Verilator는 (4)에서 (2), (3)과 같은 값을 냈다. iverilog 13.0은 clocking 구문에서 syntax error를 냈다. vita는 “clocking skew `#0` on `d_cb` is unsupported in this subset”이라는 error로 멈췄다. 같은 메시지는 지금 받는 skew가 #1step뿐이라고 알려 준다. 이번 testbench로 clocking block의 drive까지 돌려 결과를 얻은 simulator는 Verilator 하나였다.

그런데 output의 skew를 적지 않으면 vita는 error 없이 돈다. 14.3에 따르면 skew를 적지 않은 output의 기본 skew는 0이고, 14.4는 #0을 적은 output과 같은 Re-NBA region에서 drive한다고 적는다. 아래 code는 cb.d에 drive를 한 번도 하지 않고 d만 찍는다.

SystemVerilog
`timescale 1ns/1ns
module cb_default_skew;
    logic clk = 0;
    logic [3:0] d = 0;
    always #5 clk = ~clk;
    clocking cb @(posedge clk);
        output d;   // no skew: default output skew 0
    endclocking
    initial begin   // no drive to cb.d anywhere
        #1 $display("%0t d=%0d", $time, d);
        @(cb);
        $display("%0t d=%0d", $time, d);
        #1 $display("%0t d=%0d", $time, d);
        $finish;
    end
endmodule
Terminal
$ vita cb_default_skew.sv
1 d=0
5 d=x
6 d=x
simulation ended (Finish) at time 6
errors=0 warnings=0 notes=0

$ verilator --binary --timing --Mdir obj_cb --top cb_default_skew cb_default_skew.sv
$ ./obj_cb/Vcb_default_skew
1 d=0
5 d=0
6 d=0

vita에서는 drive가 없었는데도 첫 clocking event인 5 ns에 d가 x로 바뀌었다. 14.16에 따르면 대상 signal은 synchronous drive가 있어야 바뀐다. 대상이 net이어도 clocking block이 만드는 driver는 ‘z로 시작해 첫 drive 전까지는 영향이 없다. 그러니 d는 0에 머물러야 하고, Verilator가 낸 0이 표준대로의 결과다. vita는 #0을 적으면 지원하지 않는다며 멈추지만, 같은 뜻인 기본 skew는 받아들이고 틀린 값을 냈다.

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다