Verilog 문법 – case의 종류들

Verilog의 case, casez, casex와 SystemVerilog의 case inside, unique, unique0, priority가 item을 어떻게 비교하고 어떤 logic으로 합성되는지를 simulation과 합성 결과로 정리한다.

Verilog의 case, casez, casex와 SystemVerilog의 case inside, unique, unique0, priority가 item을 어떻게 비교하고 어떤 logic으로 합성되는지를 simulation과 합성 결과로 정리한다.

case 문은 RTL에서 가장 자주 쓰는 구문이다. FSM 글에서는 next-state logic을 case로 썼고, clock gating 글에서는 빠진 분기가 latch를 만든다는 것을 봤다. 이 글은 case 문 자체를 다룬다. Verilog에 원래 있던 것을 먼저 보고, SystemVerilog가 더한 것을 그 뒤에 본다. 예제는 내가 만들고 있는 오픈소스 RTL 시뮬레이터 vitamin의 vita 0.2.0과 iverilog 13.0, Verilator 5.052로 돌렸고, 합성은 오픈소스 합성 도구인 yosys 0.69로 했다(vita를 만든 과정은 vitamin 연재에 적었다). 상용 합성 도구의 결과는 cell library와 최적화 옵션에 따라 달라지므로, 여기 나오는 gate 수는 구조를 비교하는 용도로만 보면 된다.

case는 위에서부터 비교해 처음 맞는 item 하나만 실행한다

Verilog
case (sel)                 // case expression
    2'b00:   y = a;        // case item : statement
    2'b01:   y = b;
    2'b10,
    2'b11:   y = c;        // several items can share one statement
    default: y = 1'b0;     // runs when no item matches
endcase

괄호 안의 식을 case expression, 콜론 앞의 값을 case item이라고 부른다. simulator는 case expression을 위에서부터 item과 차례로 비교하고, 처음 맞는 item의 문장 하나만 실행한 뒤 endcase로 빠져나간다. C의 switch와 달리 break가 없어도 아래 item으로 흘러내리지 않는다. Cummings는 이 동작이 if (expr === item1) … else if (expr === item2) … 로 이어지는 if-else-if와 같다고 설명한다(SNUG 1999).

이 설명에서 두 가지가 따라 나온다. 비교가 ===이므로 x와 z도 하나의 값으로 보고 글자 그대로 비교한다. case expression에 x가 섞이면 0과 1로만 쓴 item에는 맞지 않는다. 그리고 차례로 비교하므로 item 사이에는 순서, 곧 우선순위가 있다. item이 상수일 필요도 없다. Verilog는 item 자리에 신호나 식을 쓸 수 있어서, 뒤에 나오는 case (1’b1) 같은 형태가 가능하다.

casez와 casex는 z와 x를 don’t-care로 본다

instruction decoder처럼 일부 비트만 보고 분기할 때는 나머지 비트를 비교에서 빼야 한다. casez는 z를, casex는 x와 z를 don’t-care로 취급한다. 숫자 literal에서 ?는 z의 다른 표기라서 4’b1?00은 4’b1z00과 같은 값이다. 그래서 casez의 item 4’b1?00은 bit 2를 보지 않고 4’b1000과 4’b1100에 모두 맞는다. Cummings는 don’t-care가 필요하면 casez를 쓰고 item에는 z 대신 ?를 쓰라고 권한다.

조심할 점은 don’t-care가 양쪽에 다 적용된다는 것이다. item에 쓴 ?뿐 아니라 case expression 쪽에 들어온 z도(casex라면 x도) 그 비트를 비교에서 뺀다. 아래 testbench는 item 4’b1?00 하나를 case, casez, casex, case inside 네 가지로 비교한다. case inside는 뒤에서 다룰 SystemVerilog 구문인데, 한 표에서 보려고 같이 넣었다.

SystemVerilog
`timescale 1ns/1ns
// Which case flavor matches the item 4'b1?00 for a given case expression?
module tb_match;
    logic [3:0] v;
    logic       m_case, m_casez, m_casex, m_inside;

    always_comb begin
        case (v)
            4'b1?00: m_case = 1'b1;
            default: m_case = 1'b0;
        endcase
    end

    always_comb begin
        casez (v)
            4'b1?00: m_casez = 1'b1;
            default: m_casez = 1'b0;
        endcase
    end

    always_comb begin
        casex (v)
            4'b1?00: m_casex = 1'b1;
            default: m_casex = 1'b0;
        endcase
    end

    // case (v) inside 4'b1?00 compares with the one-sided wildcard ==?
`ifdef HAS_CASE_INSIDE
    always_comb begin
        case (v) inside
            4'b1?00: m_inside = 1'b1;
            default: m_inside = 1'b0;
        endcase
    end
`else
    always_comb begin
        if (v ==? 4'b1?00) m_inside = 1'b1;
        else               m_inside = 1'b0;
    end
`endif

    task automatic show(input logic [3:0] val);
        v = val;
        #1;
        $display("%b     %b     %b      %b      %b", v, m_case, m_casez, m_casex, m_inside);
    endtask

    initial begin
        $display("v        case  casez  casex  inside");
        show(4'b1000);
        show(4'b1100);
        show(4'b0100);
`ifndef TWO_STATE
        show(4'b1z00);
        show(4'b1x00);
        show(4'bz100);
        show(4'bx100);
        show(4'bzzzz);
        show(4'bxxxx);
`endif
        #1 $finish;
    end
endmodule
Terminal
$ vita tb_match.sv
v        case  casez  casex  inside
1000     0     1      1      1
1100     0     1      1      1
0100     0     0      0      0
1z00     1     1      1      1
1x00     0     1      1      1
z100     0     1      1      0
x100     0     0      1      0
zzzz     0     1      1      0
xxxx     0     0      1      0
simulation ended (Finish) at time 10
errors=0 warnings=0 notes=0

case 열은 v가 1z00일 때만 1이다. 보통의 case에서 ?는 z라는 값일 뿐이어서, 4’b1000도 4’b1100도 이 item에 맞지 않는다. wildcard를 쓴 줄 알았는데 실제 회로에서는 한 번도 선택되지 않는 item이 되는 것이다. Verilator는 이 줄에 CASEWITHX 경고를 내고 casex나 casez를 쓰려던 것이 아니냐고 묻는다(Verilator 경고 목록).

casez 열에서 1×00이 맞는 것은 item이 bit 2를 보지 않기 때문이고, 의도한 동작이다. 문제는 z100과 zzzz다. case expression의 z가 그 비트를 비교에서 빼 버려서, 맨 위 비트가 1인지 확인하지도 않고 맞았다고 한다. casex는 여기에 x100과 xxxx까지 맞는다. reset 전의 register나 구동되지 않은 신호처럼 x는 simulation에서 흔하게 생기므로, 값이 정해지지 않았는데도 처음 맞는 item이 조용히 실행되고 그 뒤로는 멀쩡한 값이 흘러간다. Cummings의 지침은 합성할 코드에는 casex를 쓰지 말고, casez도 조심해서 쓰라는 것이다. Verilator도 casex를 보면 CASEX 경고로 casez와 ?를 권한다.

inside 열은 item 쪽의 ?만 don’t-care로 보고 case expression의 x와 z는 그대로 비교한다. 그래서 z100, x100, zzzz, xxxx가 모두 0이다. 다만 case inside를 그대로 받아 준 것은 Verilator뿐이었다. vita 0.2.0과 iverilog 13.0은 이 구문에서 parse error를 내고 멈춘다. 그래서 위 testbench는 HAS_CASE_INSIDE macro가 없으면 같은 비교 규칙을 가진 wildcard equality 연산자 ==?로 inside 열을 만든다. Verilator는 2-state simulator라 x와 z가 든 행을 돌릴 수 없어서 TWO_STATE macro로 앞의 세 행만 돌렸고, 세 행의 결과는 위와 같다. iverilog의 출력은 vita와 같다.

wildcard로 쓴 비트는 합성하면 비교 logic에서 빠진다

v[3]은 그대로, v[1]과 v[0]은 inverter를 거쳐 3-input AND gate로 들어가 match를 만들고, v[2]는 어디에도 연결되지 않은 4'b1?00 비교 logic

4’b1?00과 비교하는 logic이다. v[3]은 그대로, v[1]과 v[0]은 inverter를 거쳐 AND gate로 들어가고, ?로 쓴 v[2]는 어디에도 연결되지 않는다.

합성 도구에는 x나 z라는 값이 없다. wire에는 0 아니면 1이 흐르므로, casez든 casex든 item의 don’t-care 자리를 비교에서 빼라는 뜻으로만 읽는다. 그래서 같은 item을 쓴 casez와 casex는 같은 netlist가 되고, 둘의 차이는 simulation에서만 나타난다. simulation에서만 나타나는 차이는 RTL과 netlist가 다르게 움직인다는 뜻이다. casex를 쓰지 말라는 지침은 여기서 나온다.

CPU의 instruction decoder가 casez를 쓰는 방식

ARM 계열 core의 decode logic을 읽다 보면 4’b1?00 같은 item이 줄지어 선 casez를 자주 만난다. instruction encoding이 그렇게 생겼기 때문이다. ARMv6-M Architecture Reference Manual의 Table A5-1은 16-bit Thumb instruction을 맨 위 6비트(opcode, bit 15~10)로 나누는데, 00xxxx는 shift, add, subtract, move, compare이고 010000은 data processing, 01001x는 literal pool에서 읽는 LDR, 1011xx는 miscellaneous인 식으로 확인해야 하는 비트 수가 제각각인 pattern이 섞여 있다. 이 표를 그대로 옮기면 다음과 같다.

Verilog
// ARMv6-M 16-bit Thumb instruction classes. opcode = instr[15:10]
// (ARMv6-M Architecture Reference Manual, Table A5-1)
module thumb_dec (
    input  wire [5:0] opcode,
    output reg  [3:0] cls
);
    localparam [3:0] C_SHIFT_ADD  = 4'd0,   // shift, add, subtract, move, compare
                     C_DATA_PROC  = 4'd1,
                     C_SPECIAL_BX = 4'd2,
                     C_LDR_LIT    = 4'd3,
                     C_LOAD_STORE = 4'd4,
                     C_ADR        = 4'd5,
                     C_ADD_SP     = 4'd6,
                     C_MISC       = 4'd7,
                     C_STM        = 4'd8,
                     C_LDM        = 4'd9,
                     C_BCOND_SVC  = 4'd10,
                     C_BRANCH     = 4'd11,
                     C_32BIT      = 4'd12;  // first halfword of a 32-bit instruction

    always @* begin
        casez (opcode)
            6'b00????: cls = C_SHIFT_ADD;
            6'b010000: cls = C_DATA_PROC;
            6'b010001: cls = C_SPECIAL_BX;
            6'b01001?: cls = C_LDR_LIT;
            6'b0101??,
            6'b011???,
            6'b100???: cls = C_LOAD_STORE;
            6'b10100?: cls = C_ADR;
            6'b10101?: cls = C_ADD_SP;
            6'b1011??: cls = C_MISC;
            6'b11000?: cls = C_STM;
            6'b11001?: cls = C_LDM;
            6'b1101??: cls = C_BCOND_SVC;
            6'b11100?: cls = C_BRANCH;
            default:   cls = C_32BIT;       // 6'b11101?, 6'b1111??
        endcase
    end
endmodule

열네 개의 item은 서로 겹치지 않는다. 어떤 opcode도 두 item에 동시에 맞지 않고, default까지 합치면 64개 opcode를 모두 덮는다. 64개를 전부 넣어 class마다 몇 개가 들어가는지 세는 testbench를 돌리면 16, 1, 1, 2, 20, 2, 2, 4, 2, 2, 4, 2, 6개이고 합이 64다. vita, iverilog, Verilator의 결과가 같고 Verilator의 -Wall lint도 경고 없이 지나간다. yosys로 합성하면 cell 25개(AND 9, OR 8, NOT 5, MUX 3)이고 가장 긴 경로는 cell 5개다. item이 겹치지 않으므로 순서를 따지는 logic 없이, cls의 각 비트가 opcode 비트 몇 개의 조합으로 바로 만들어진다.

맞는 item이 없는 값이 있으면 latch가 생긴다

Cummings는 case 문의 성질 두 가지에 이름을 붙였다. case expression이 가질 수 있는 모든 값이 어떤 item이나 default에 맞으면 full이고, 어떤 값도 둘 이상의 item에 동시에 맞지 않으면 parallel이다. 합성 결과는 이 두 성질이 정한다. full부터 본다.

Verilog
// (a) full and parallel: every sel value has exactly one item
module mux4_full (input wire [1:0] sel, input wire [3:0] d, output reg y);
    always @* begin
        case (sel)
            2'b00: y = d[0];
            2'b01: y = d[1];
            2'b10: y = d[2];
            2'b11: y = d[3];
        endcase
    end
endmodule

// (b) not full: sel == 2'b11 has no item and no default
module mux3_nodef (input wire [1:0] sel, input wire [2:0] d, output reg y);
    always @* begin
        case (sel)
            2'b00: y = d[0];
            2'b01: y = d[1];
            2'b10: y = d[2];
        endcase
    end
endmodule

// (c) default drives a constant
module mux3_def0 (input wire [1:0] sel, input wire [2:0] d, output reg y);
    always @* begin
        case (sel)
            2'b00:   y = d[0];
            2'b01:   y = d[1];
            2'b10:   y = d[2];
            default: y = 1'b0;
        endcase
    end
endmodule

// (d) default drives x: a don't-care for synthesis
module mux3_defx (input wire [1:0] sel, input wire [2:0] d, output reg y);
    always @* begin
        case (sel)
            2'b00:   y = d[0];
            2'b01:   y = d[1];
            2'b10:   y = d[2];
            default: y = 1'bx;
        endcase
    end
endmodule

// (e) not full, but the pragma tells synthesis to treat it as full
module mux3_fullcase (input wire [1:0] sel, input wire [2:0] d, output reg y);
    always @* begin
        case (sel) // synopsys full_case
            2'b00: y = d[0];
            2'b01: y = d[1];
            2'b10: y = d[2];
        endcase
    end
endmodule

(a)는 full이고 parallel이다. sel의 네 값마다 item이 정확히 하나씩 있다. yosys는 이것을 2:1 MUX cell 세 개, 곧 4:1 mux 하나로 만든다.

d[0]부터 d[3]까지 네 입력이 00, 01, 10, 11 자리에 들어가고 sel[1:0]이 아래에서 들어와 y를 내보내는 4:1 mux

(a)의 합성 결과다. sel이 네 입력 가운데 하나를 골라 y로 내보낸다. 입력 사이에 순서가 없고, 어느 입력에서 오든 y까지의 경로 길이가 같다.

(b)는 sel이 2’b11일 때 맞는 item이 없다. 그때 always block은 y에 아무것도 쓰지 않으므로 y는 직전 값을 그대로 가지고 있어야 한다. 값을 기억하는 combinational logic은 없으니 합성 도구는 latch를 넣는다. yosys의 출력에서 해당 줄만 옮기면 다음과 같다.

Terminal
$ yosys -p "read_verilog mux.v; synth -top mux3_nodef; abc -g simple; opt_clean; stat"
...
Warning: Latch inferred for signal `\mux3_nodef.\y' from process `\mux3_nodef.$proc$mux.v:15$2': $auto$proc_dlatch.cc:547:proc_dlatch$33
...
       14 cells
        7   $_AND_
        1   $_DLATCH_N_
        2   $_NOT_
        4   $_OR_
d[0], d[1], d[2]를 고르는 3:1 mux의 출력이 latch의 D로 들어가고, sel[1]과 sel[0]의 NAND 출력이 latch의 EN으로 들어가며 Q가 y로 나가는 회로

(b)의 합성 결과다. 3:1 mux의 출력이 latch의 D로 들어가고, sel[1]과 sel[0]의 NAND가 latch의 EN을 만든다. sel이 11이 아니면 latch가 열려 mux 출력이 그대로 y가 되고, 11이면 닫혀서 직전 값을 붙잡는다.

나머지 셋은 이 빈자리를 채우는 방법이다. (c)는 default에서 상수를 준다. (d)는 default에서 x를 준다. 합성 도구는 x를 어떤 값이 나와도 좋다는 don’t-care로 읽는다. (e)는 코드는 (b)와 같고, 합성 도구에게 이 case를 full로 취급하라고 말하는 // synopsys full_case 주석만 붙였다. 이 주석을 pragma 또는 directive라고 부르는데, yosys도 이 주석을 읽는다. RTL module 네 개와 netlist 두 개를 한 testbench에 넣고 sel을 00, 01, 11, 11, 10, 11 순서로 바꿔 봤다. netlist는 yosys가 write_verilog로 쓴 파일에서 module 이름에 _gate만 붙인 것이다.

Terminal
$ vita --top tb_mux tb_mux.sv mux.v mux_gates.v
sel  d    nodef  def0  defx  full_case  gate:nodef  gate:full_case
00   110  0      0     0     0          0           0
01   110  1      1     1     1          1           1
11   110  1      0     x     1          1           0
11   001  1      0     x     1          1           1
10   001  0      0     0     0          0           0
11   001  0      0     x     0          0           1
simulation ended (Finish) at time 60
errors=0 warnings=0 notes=0
sel이 11인 구간에서 no default와 full_case의 RTL은 직전 값을 유지하고, default 0은 0, default x는 x가 되며, full_case의 netlist는 d[0]을 따라가는 파형

같은 결과를 파형으로 옮긴 것이다. sel이 11인 구간에서 no default의 RTL과 netlist는 d가 바뀌어도 직전 값을 유지한다. default: 0은 0으로 내려가고 default: x는 x가 된다. full_case는 RTL에서는 no default와 똑같이 값을 유지하는데, netlist에서는 0, 1, 1로 d[0]을 따라간다.

full_case의 RTL과 netlist가 서로 다르다. simulator에게 pragma는 주석일 뿐이라 (e)의 RTL은 (b)와 똑같이 값을 붙잡는다. 합성 도구는 sel이 11인 경우를 don’t-care로 받아들였고, yosys는 그 자리를 d[0]으로 채웠다. RTL simulation에서 본 동작과 칩의 동작이 달라지는 것이다. Cummings가 이 pragma는 효과가 있을 때 가장 위험하다고 쓴 이유다. yosys에서는 (d)와 (e)가 완전히 같은 netlist가 됐다. 다만 (d)의 RTL은 x를 내므로 simulation에서 그 x가 뒤로 퍼져 눈에 띈다.

gate 수도 볼 만하다. (c)는 cell 9개(AND 3, OR 4, NOT 2)인데 (d)와 (e)는 12개(AND 6, OR 4, NOT 2)다. don’t-care를 줬는데 오히려 커졌다. full_case가 설계를 작고 빠르게 만든다는 믿음은 틀렸다는 Cummings의 지적과 같은 방향의 결과다. 물론 이것은 yosys 한 도구의 결과이고 다른 도구에서는 다르게 나올 수 있다.

Verilator는 이 pragma를 assertion으로 바꿔서 돌린다. 같은 testbench를 Verilator로 돌리면 sel이 11이 되는 순간 synthesis full_case, but non-match found for ‘2’h3′ 이라는 메시지와 함께 멈춘다. 반대로 lint에서는 pragma가 경고를 지운다. (b)에는 CASEINCOMPLETE와 LATCH 경고가 나오는데 (e)에는 둘 다 나오지 않는다. lowRISC의 coding style guide는 full_case와 parallel_case pragma를 절대 쓰지 말고, 모든 값을 덮었더라도 default를 항상 쓰라고 정해 두었다.

default를 썼다고 latch가 다 없어지는 것은 아니다. item마다 값을 쓰는 신호가 다르면, 어떤 item에서 쓰지 않은 신호에 latch가 생긴다. 이 경우는 case 앞에서 모든 출력에 default assignment를 해 두는 것으로 막는다. FSM 글과 clock gating 글에서 쓴 방법이다.

여러 item이 동시에 맞으면 priority logic이 생긴다

Verilog
// (f) overlapping items: the first match wins
module sel4_pri (input wire [3:0] req, input wire [3:0] d, output reg y);
    always @* begin
        y = 1'b0;
        casez (req)
            4'b???1: y = d[0];
            4'b??1?: y = d[1];
            4'b?1??: y = d[2];
            4'b1???: y = d[3];
        endcase
    end
endmodule

// (g) same function, items rewritten so that they never overlap
module sel4_excl (input wire [3:0] req, input wire [3:0] d, output reg y);
    always @* begin
        y = 1'b0;
        casez (req)
            4'b???1: y = d[0];
            4'b??10: y = d[1];
            4'b?100: y = d[2];
            4'b1000: y = d[3];
        endcase
    end
endmodule

// (h) overlapping items plus the parallel_case pragma
module sel4_par (input wire [3:0] req, input wire [3:0] d, output reg y);
    always @* begin
        y = 1'b0;
        casez (req) // synopsys parallel_case
            4'b???1: y = d[0];
            4'b??1?: y = d[1];
            4'b?1??: y = d[2];
            4'b1???: y = d[3];
        endcase
    end
endmodule

(f)는 req에 1이 둘 이상 있으면 여러 item이 동시에 맞는다. 처음 맞는 item이 실행되므로 req[0]의 우선순위가 가장 높고 req[3]이 가장 낮다. yosys가 쓴 netlist는 이 순서를 그대로 담고 있다.

Verilog
assign _0_ = req[3] & d[3];
assign _1_ = req[2] ? d[2] : _0_;
assign _2_ = req[1] ? d[1] : _1_;
assign y = req[0] ? d[0] : _2_;
req[3]과 d[3]의 AND에서 시작해 req[2], req[1], req[0]이 select인 2:1 mux 세 개가 차례로 이어지고 마지막 mux가 y를 내는 priority mux chain

(f)의 합성 결과다. 2:1 mux 세 개가 줄줄이 이어진다. y 바로 앞의 mux는 req[0]이 1이면 d[0]을 고르고, 아니면 앞 단의 결과를 받는다. 우선순위가 낮은 d[3]은 AND gate 하나와 mux 세 개를 지나야 y에 닿는다.

item 수가 늘어나면 이 chain도 그만큼 길어진다. cell은 MUX 3개와 AND 1개, 가장 긴 경로는 cell 4개다. (g)는 item을 겹치지 않게 다시 쓴 것이다. 기능은 (f)와 같고, yosys의 결과도 같은 구조다. Cummings의 논문에도 parallel하게 썼는데 priority encoder logic이 나오는 예가 있다. item을 겹치지 않게 쓰는 것과 합성 결과가 나란한 구조가 되는 것은 서로 다른 문제다.

(h)는 (f)에 // synopsys parallel_case를 붙였다. item이 겹치지 않는다고 합성 도구에게 말해 주는 pragma다.

Verilog
assign _00_ = d[0] & req[0];
assign _01_ = d[1] & req[1];
assign _02_ = _00_ | _01_;
assign _03_ = d[2] & req[2];
assign _04_ = d[3] & req[3];
assign _05_ = _03_ | _04_;
assign y = _02_ | _05_;
req[0]과 d[0], req[1]과 d[1], req[2]와 d[2], req[3]과 d[3]을 받는 AND gate 네 개의 출력이 OR gate 하나로 모여 y를 내는 AND-OR 구조

(h)의 합성 결과다. req와 d를 비트마다 AND한 뒤 OR로 모은다. 순서가 없고, 네 입력 모두 AND 하나와 OR을 지나면 y에 닿는다.

cell은 AND 4개와 OR 3개, 가장 긴 경로는 cell 3개로 줄었다(그림의 4-input OR은 2-input OR 세 개다). 작고 빨라졌지만 기능이 달라졌다. req가 0110이면 RTL은 d[1]을 고르는데 이 netlist는 d[1] | d[2]를 낸다. Cummings는 priority encoder로 짠 case에 parallel_case를 붙였다가 RTL simulation은 모두 통과하고 칩에서 틀린 사례를 전한다. gate-level simulation도 이 문제를 잡지 못했고, ASIC을 다시 만드는 데 수십만 달러와 몇 달이 들었다고 한다. 이 차이는 뒤에서 SystemVerilog keyword와 함께 simulation으로 확인한다.

Verilog
// the same item written twice: the second one can never run
case (sel)
    2'b00:   y = d[0];
    2'b01:   y = d[1];
    2'b01:   y = d[2];
    default: y = d[3];
endcase

// "reverse case": a constant case expression, signals as items
case (1'b1)
    req[0]: y = d[0];
    req[1]: y = d[1];
    req[2]: y = d[2];
    req[3]: y = d[3];
endcase

겹침의 극단은 같은 item을 두 번 쓰는 것이다. 위쪽 case에서 두 번째 2’b01은 절대 실행되지 않는다. yosys가 만든 netlist에는 d[2]가 아예 나오지 않고, Verilator는 CASEOVERLAP 경고로 두 줄을 짚어 준다. 아래쪽은 case expression에 상수를, item에 신호를 쓰는 형태로 reverse case라고 부른다. one-hot FSM에서 자주 쓰고, 합성하면 (f)와 같은 netlist가 나온다. item이 상수가 아니어서 Verilator의 CASEOVERLAP이 이 겹침은 잡지 못한다. Cummings는 의도한 priority encoder는 case 대신 if-else-if로 쓰라고 권한다. 읽는 사람이 순서가 있다는 것을 바로 알아보기 때문이다.

SystemVerilog의 case inside는 item 쪽 wildcard만 인정한다

여기까지가 Verilog에 있는 것이다. SystemVerilog는 비교 방식 하나와 keyword 세 개를 더했다. 비교 방식이 case (expr) inside다. item에 쓴 x, z, ?만 don’t-care로 보고, case expression 쪽의 x와 z는 don’t-care로 보지 않는다. 앞의 표에서 inside 열이 z100, x100, zzzz, xxxx에 맞지 않았던 이유다. Mills는 case inside가 casez와 casex를 모두 대신하며, casex보다 casez를 쓰라던 예전 권고는 이제 거둬야 한다고 썼다(SNUG 2012). lowRISC도 wildcard가 필요 없으면 case, 필요하면 case inside, Verilog-2001 호환이 필요할 때만 casez를 쓰고 casex는 쓰지 말라고 정한다. item에 [6’b010100:6’b100111]처럼 범위를 쓸 수도 있다.

SystemVerilog
// SystemVerilog version: enum, always_comb, unique case ... inside
module thumb_dec_sv (
    input  logic [5:0] opcode,
    output logic [3:0] cls
);
    typedef enum logic [3:0] {
        C_SHIFT_ADD, C_DATA_PROC, C_SPECIAL_BX, C_LDR_LIT, C_LOAD_STORE,
        C_ADR, C_ADD_SP, C_MISC, C_STM, C_LDM, C_BCOND_SVC, C_BRANCH, C_32BIT
    } cls_e;

    always_comb begin
        unique case (opcode) inside
            6'b00????: cls = C_SHIFT_ADD;
            6'b010000: cls = C_DATA_PROC;
            6'b010001: cls = C_SPECIAL_BX;
            6'b01001?: cls = C_LDR_LIT;
            6'b0101??,
            6'b011???,
            6'b100???: cls = C_LOAD_STORE;
            6'b10100?: cls = C_ADR;
            6'b10101?: cls = C_ADD_SP;
            6'b1011??: cls = C_MISC;
            6'b11000?: cls = C_STM;
            6'b11001?: cls = C_LDM;
            6'b1101??: cls = C_BCOND_SVC;
            6'b11100?: cls = C_BRANCH;
            default:   cls = C_32BIT;       // 6'b11101?, 6'b1111??
        endcase
    end
endmodule

앞의 Thumb decoder를 SystemVerilog로 옮긴 것이다. class 이름은 localparam 대신 enum으로, always @*는 always_comb로, casez는 unique case … inside로 바뀌었다. Verilator에서 64개 opcode를 모두 넣어 Verilog 버전과 비교하면 출력이 전부 같고, unique가 거는 검사에도 한 번도 걸리지 않는다.

아쉬운 것은 도구 지원이다. 내가 쓴 네 도구 가운데 case inside를 받은 것은 Verilator 하나다. vita와 iverilog는 parse error를 내고, yosys 0.69는 다음 error로 멈춘다. 오픈소스 도구로 simulation과 합성을 모두 하려면 아직은 casez 버전이 필요하다.

Terminal
$ yosys -p "read_verilog -sv thumb_dec_sv.sv; synth -top thumb_dec_sv"
thumb_dec_sv.sv:13: ERROR: Cast operation must be applied on sized constants, e.g. <ID>'d0, while 6'b00???? is not a sized constant.

unique, unique0, priority는 full과 parallel을 simulation에서 검사한다

pragma의 문제는 합성 도구에게만 말하고 simulator에게는 말하지 않는다는 데 있다. SystemVerilog는 같은 내용을 주석이 아니라 keyword로 만들어서 simulator도 알게 했다. Mills의 정리에 따르면 SystemVerilog 2005가 unique와 priority를 넣었고 2009가 unique0를 더했다. 2009부터는 조건이 깨졌을 때 나오는 메시지를 violation report라고 부르고, 같은 시각 안에서 값이 잠깐 바뀌었다가 돌아오는 glitch로는 보고하지 않게 했다.

keyword여러 item이 맞을 때맞는 item이 없을 때합성 도구가 읽는 뜻
uniqueviolationviolationfull_case와 parallel_case
unique0violation허용parallel_case
priority허용, 처음 맞는 item 실행violationfull_case

세 keyword는 if에도 붙일 수 있다. default가 있으면 맞는 item이 없는 경우가 생기지 않으므로 unique와 priority의 그 검사는 의미가 없어진다. Cummings는 2005년 논문에서 keyword와 pragma의 이 대응을 표로 정리했고(SNUG 2005), yosys 문서도 이 keyword들을 full_case, parallel_case attribute로 바꿔서 처리한다고 적고 있다. 앞의 (f)에 keyword만 붙여 본다.

SystemVerilog
// (i) unique: items must not overlap, and one of them must match
module sel4_unique (input logic [3:0] req, input logic [3:0] d, output logic y);
    always_comb begin
        y = 1'b0;
        unique casez (req)
            4'b???1: y = d[0];
            4'b??1?: y = d[1];
            4'b?1??: y = d[2];
            4'b1???: y = d[3];
        endcase
    end
endmodule

// (j) unique0: items must not overlap, no match is allowed
module sel4_unique0 (input logic [3:0] req, input logic [3:0] d, output logic y);
    always_comb begin
        y = 1'b0;
        unique0 casez (req)
            4'b???1: y = d[0];
            4'b??1?: y = d[1];
            4'b?1??: y = d[2];
            4'b1???: y = d[3];
        endcase
    end
endmodule

// (k) priority: overlap is allowed, but one of the items must match
module sel4_priority (input logic [3:0] req, input logic [3:0] d, output logic y);
    always_comb begin
        y = 1'b0;
        priority casez (req)
            4'b???1: y = d[0];
            4'b??1?: y = d[1];
            4'b?1??: y = d[2];
            4'b1???: y = d[3];
        endcase
    end
endmodule

세 module을 한 testbench에 넣고 req를 0010(요청 하나), 0110(요청 둘), 0000(요청 없음) 순서로 줬다. d는 1010이다. 먼저 vita의 출력이다.

Terminal
$ vita tb_qual.sv sel_sv.sv
sel_sv.sv:5:16: warning[VITA-W4031] W-RUN-UNIQUE-VIOLATION: value is unhandled for priority or unique case statement [in tb_qual.u_u] [at time 0]
sel_sv.sv:31:18: warning[VITA-W4031] W-RUN-UNIQUE-VIOLATION: value is unhandled for priority or unique case statement [in tb_qual.u_p] [at time 0]
t=10 req=0010  unique=1 unique0=1 priority=1
t=20 req=0110  unique=1 unique0=1 priority=1
sel_sv.sv:5:16: warning[VITA-W4031] W-RUN-UNIQUE-VIOLATION: value is unhandled for priority or unique case statement [in tb_qual.u_u] [at time 20]
sel_sv.sv:31:18: warning[VITA-W4031] W-RUN-UNIQUE-VIOLATION: value is unhandled for priority or unique case statement [in tb_qual.u_p] [at time 20]
t=30 req=0000  unique=0 unique0=0 priority=0
simulation ended (Finish) at time 30
errors=0 warnings=4 notes=0

vita는 맞는 item이 없을 때 unique(u_u)와 priority(u_p)에 경고를 낸다. unique0(u_u0)에는 내지 않는다. time 0의 두 줄은 req에 아직 값을 넣기 전이라 xxxx였기 때문이고, time 20의 두 줄이 req가 0000이 된 때다. iverilog 13.0도 time 20에 같은 두 경고를 낸다. 그런데 요청이 둘이라 item이 겹치는 0110에는 vita도 iverilog도 아무 말이 없다. iverilog는 compile할 때 Case unique/unique0 qualities are ignored 라고 미리 알려 준다. Verilator는 다르다.

Terminal
$ verilator --binary --timing --no-stop-fail -Wno-fatal --top tb_qual tb_qual.sv sel_sv.sv
$ ./obj_dir/Vtb_qual 2>&1 | grep -v '^- ' | awk '!seen[$0]++'
t=10 req=0010  unique=1 unique0=1 priority=1
[10] %Error: sel_sv.sv:18: Assertion failed in tb_qual.u_u0: unique0 case, but multiple matches found for '4'h6'
[10] %Error: sel_sv.sv:5: Assertion failed in tb_qual.u_u: unique case, but multiple matches found for '4'h6'
t=20 req=0110  unique=1 unique0=1 priority=1
[20] %Error: sel_sv.sv:5: Assertion failed in tb_qual.u_u: unique case, but none matched for '4'h0'
[20] %Error: sel_sv.sv:31: Assertion failed in tb_qual.u_p: priority case, but non-match found for '4'h0'
t=30 req=0000  unique=0 unique0=0 priority=0
[30] %Error: sel_sv.sv:5: Assertion failed in tb_qual.u_u: unique case, but none matched for '4'h0'
[30] %Error: sel_sv.sv:31: Assertion failed in tb_qual.u_p: priority case, but non-match found for '4'h0'

Verilator는 0110에서 unique와 unique0 둘 다 multiple matches found를 내고, 0000에서는 unique와 priority에 맞는 item이 없다는 메시지를 낸다. 같은 줄이 두 번씩 나와서 awk로 한 번만 남겼고, Verilator가 끝에 붙이는 보고 줄은 grep으로 뺐다. –no-stop-fail은 첫 위반에서 멈추지 않게 하는 옵션이다. Verilator는 돌리기 전 lint에서도 CASEOVERLAP과 CASEINCOMPLETE로 같은 자리를 짚는다. keyword를 붙였다고 겹침이 다 잡히는 것은 아니고, 쓰는 simulator가 그 검사를 구현했는지에 달려 있다는 것을 기억해 둘 만하다.

unique를 붙이면 합성 결과도 바뀐다

keyword는 검사만 하는 것이 아니다. 합성 도구는 unique를 보면 겹침이 없고 빈자리도 없다고 믿고 logic을 만든다. (f)부터 (k)까지를 yosys로 합성한 결과다. 가장 긴 경로는 입력에서 y까지 지나는 cell 수다.

코드cell가장 긴 경로
(f) 겹치는 casezMUX 3, AND 14
(g) 겹치지 않게 쓴 casezMUX 3, AND 14
(h) (f)에 parallel_caseAND 4, OR 33
(i) unique casezAND 5, OR 4, NOT 24
(j) unique0 casezAND 4, OR 33
(k) priority casezMUX 33

(j) unique0의 netlist는 (h) parallel_case와 완전히 같다. (i)와 (k)는 다음과 같다.

Verilog
// (i) unique casez
assign _09_ = ~req[3];
assign _00_ = req[1] | req[2];
assign _01_ = ~_00_;
assign _02_ = d[0] & _09_;
assign _03_ = _01_ & _02_;
assign _04_ = req[2] & d[2];
assign _05_ = req[3] & d[3];
assign _06_ = req[1] & d[1];
assign _07_ = _05_ | _06_;
assign _08_ = _04_ | _07_;
assign y = _03_ | _08_;

// (k) priority casez
assign _1_ = req[2] ? d[2] : d[3];
assign _0_ = req[1] ? d[1] : _1_;
assign y = req[0] ? d[0] : _0_;

(k)에서는 마지막 item의 조건인 req[3]이 사라졌다. 반드시 하나는 맞는다고 했으니, 앞의 셋이 아니면 넷째라고 본 것이다. (i)에서는 d[0]을 고르는 조건에서 req[0]이 빠지고, 나머지 셋이 모두 0이면 d[0]을 고르게 됐다. (f)의 RTL과 다섯 netlist를 한 testbench에 넣어 비교했다. d는 1101이다.

Terminal
$ vita --top tb_gate tb_gate.sv sel.v sel_gates.v
req   RTL  casez  parallel_case  unique  unique0  priority
0100  1    1      1             1       1        1
0110  0    0      1             1       1        0
0000  0    0      0             1       0        1
1000  1    1      1             1       1        1
simulation ended (Finish) at time 40
errors=0 warnings=0 notes=0
req가 0100, 0110, 0000, 1000으로 바뀔 때 RTL의 y와 casez, parallel_case, unique, unique0, priority netlist의 y를 비교한 파형. 0110과 0000 구간에서 netlist마다 값이 갈린다

같은 결과를 파형으로 옮긴 것이다. 요청이 하나뿐인 0100과 1000에서는 여섯 줄이 모두 같다. 0110에서는 parallel_case, unique, unique0의 netlist가 RTL과 다르고, 0000에서는 unique와 priority의 netlist가 RTL과 다르다.

요청이 둘인 0110에서 RTL은 d[1]인 0을 내는데 parallel_case, unique, unique0의 netlist는 1을 낸다. item이 겹치지 않는다는 약속이 깨진 입력이다. 요청이 없는 0000에서는 RTL이 case 앞의 default assignment대로 0을 내는데, unique의 netlist는 d[0]인 1을, priority의 netlist는 d[3]인 1을 낸다. 반드시 하나는 맞는다는 약속이 깨진 입력이다. iverilog와 Verilator로 돌려도 표는 같다.

여기서 조심할 것이 하나 나온다. 요청이 없는 상태가 정상 입력인 설계에서는 case 앞에 default assignment를 했더라도 unique나 priority를 쓰면 안 된다. simulation은 경고를 내면서도 default 값을 그대로 보여 주는데, 합성 도구는 그 경우를 don’t-care로 처리하기 때문이다. 맞는 item이 없어도 되는 case에는 unique0를 쓰거나 default를 넣는다. Mills도 default assignment를 case 앞에 두고 unique case를 쓰면 합성 도구가 그 default를 무시한다는 같은 예를 들고, 그 자리에 unique0 case를 쓰라고 권한다.

빈자리가 있는 case를 always_comb 안에 쓰면 도구가 한 번 더 막아 준다. 앞의 (b)를 always_comb와 unique0 case로 바꿔 yosys에 넣으면 Latch inferred for signal … from always_comb process 라는 error로 합성이 멈춘다. always @*에서는 경고였던 것이 error가 된다. 같은 자리에 unique case나 priority case를 쓰면 latch 없이 합성되고, 결과는 full_case를 붙인 (e)와 같다.

wildcard가 필요 없으면 case를, 필요하면 case inside나 casez를 쓴다

쓴 것합성 결과RTL과 netlist
full이고 parallel인 casemux 또는 decoder같다
맞는 item이 없는 값이 있고 default가 없음mux와 latch같다(의도하지 않은 latch)
default에서 상수mux같다
default에서 xdon’t-care로 최적화RTL만 x를 낸다
full_case pragmadon’t-care로 최적화빈자리 입력에서 다르다
겹치는 itempriority mux chain같다
겹치는 item에 parallel_case, unique, unique0AND-OR겹치는 입력에서 다르다
default 없는 unique, priority빈자리를 don’t-care로 최적화빈자리 입력에서 다르다

지금까지 본 것을 한 표로 모았다. 합성 결과를 정하는 것은 어떤 keyword를 썼느냐보다 그 case가 full인지, parallel인지다. pragma와 keyword는 그 성질을 도구에게 약속해 주는 수단이고, 약속이 사실과 다르면 RTL과 netlist가 갈라진다. 자료들이 권하는 바를 모으면 순서는 이렇다. wildcard가 필요 없으면 case를 쓰고, 필요하면 case inside를, 도구가 받지 못하면 casez와 ?를 쓴다. casex와 pragma는 쓰지 않는다. case 앞에서 모든 출력에 default assignment를 하고 default item도 넣는다. item이 겹치지 않아야 하는 case에는 unique0나 unique를 붙여 simulation이 검사하게 하되, 쓰는 simulator가 겹침까지 검사하는지는 직접 확인해 둔다. 순서가 필요한 선택은 case 대신 if-else-if로 쓴다.

댓글 남기기

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