티스토리 뷰

 

어느새 Testbench 연재 3번째 포스팅이다. 

크게 생각해보면, 아래 3가지 정도 내용이 더 남았고

 

  • Random configuration
  • Reference model
  • Monitor & Scoreboard

Test case나 coverage, 그리고 regression 환경(Tcl, Script 등)은 일단 뒤로 미루어 두려고 한다.

 

이번 글은 Driver 2번째 시간이다.

 

 


 

 

 

 

DUT에 맞게 stimulus를 넣어주는 부분을 소개하자니, 어플리케이션에 따라 천차만별일 것이라는 생각이 들었다. CPU, AP처럼 OP code와 Bus 입력 등이 필요할 수도 있고, display, sensor처럼 영상 바탕의 입력이 요구될 수도 있다. 여기서는 간단히 특정 프로토콜에 맞춰서 영상 스트림을 뒤로 출력하는 image driver를 구현해 보았다. 

 

대부분의 경우, DUT가 필요로하는 입력을 spec 문서와 함께 받게 되지만 이번 예제는 임의로 spec을 규정하여 프로토콜을 정하였다. 복잡할 수 있는 부분들을 모두 걷어내고 최대한 간단한 구조로 구현하고자 했다. 많은 시간 공들이지 않았기에 허술한 부분이 있을 수 있고, 이 방법이 정답은 아니니 참고만 하길 바란다.

 

 

DUT가 정상적으로 동작하기 위한 Input 프로토콜

 

 

Digital system에서 가장 중요한 것은 아무래도 clock speed(frequency)와 영상의 resolution이겠지만, 여기서는 별다른 제약을 두지는 않았다. 위와 같은 사양들이 중요한 이유는 이 값들에 의해서 frame rate가 정해지기 때문인데, 이 지표가 높을수록 부드럽고 잔상이 적은 영상을 얻을 수 있기 때문이다.

 

 

frame rate에 대한 간단한 예시 (출처 : jpvideopro.com)

 

 

 

여기서 정의한 Input protocol들을 설명하면 다음과 같다.

 

  • frm_start: 첫 frame이 시작되기 전에 SW reset을 수행하는 신호
  • data_in: 1 channel 입력으로, 실제 영상의 data 값 (여기서는 10bit로 설정)
  • hsync: Horizontal 크기만큼 입력이 들어올 때 enable되는 신호로 1 line마다 high를 유지하고 blank 구간에서 low로 내려간다.
  • h_start: 1 line의 첫번째 data임을 알려주는 신호
  • h_last: 1 line의 마지막 data임을 알려주는 신호
  • vsync: Vertical 크기만큼 입력이 들어올 때 enable 되는 신호로 모든 line에서 high를 유지하고 마지막 data가 출력되면서 low로 내려간다.
  • v_last: 현재 출력이 마지막 line임을 알려주는 신호

 

복잡한 시스템일수록 더 많은 프로토콜들이 요구되겠지만, 가장 기본이 되는 골격은 이와 비슷할 것이다. module의 I/O는 아래와 같고, parameter를 통해 resolution(H/V size)과 blank 크기 및 대기 cycle 등을 설정할 수 있게 구성했다. 많은 사람들이 알고 있는 것일 테지만, 아래와 같이 코드를 짤 경우 파라미터 오버라이딩이 가능하므로 더욱 유연한 사용이 가능하게 된다.

 

 

심플 그 자체인 block diagram을 보라

 

▼ image driver module 선언부 및 파라미터 오버라이딩 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module img_drv
#(    
    parameter H_SIZE = 640,
    parameter V_SIZE = 480,
    parameter H_BLANK = 64,
    parameter V_BLANK = 48,
    parameter FRM_CYCLE = 64,
    parameter WAIT_CYCLE = 320                                
)
(
    // input
    input clk,
    input rst_n,
    // output
    output reg hsync,
    output reg vsync,
    output reg frm_start,
    output reg h_first,
    output reg h_last,
    output     v_last,
    output [9:0] data_in
);
cs

 

1
2
3
4
5
6
7
8
9
10
img_drv img_drv
#(    
    .H_SIZE(320),
    .V_SIZE(240),
    .H_BLANK(32),
    .V_BLANK(48),
    .FRM_CYCLE(64),
    .WAIT_CYCLE(32)                                      
)
(.*);
cs

 

 

자, 그럼 이제 본격적으로 프로토콜을 만들어 준 부분을 아래에서 살펴보자.

 

 

 


 

 

 

 

예전에 선배에게 직접 짠 Testbench를 검토 받은 적이 있었다.

 

당시, 선배曰 "야, 그거 굳이 그렇게 RTL스럽게 짜야하니? 어차피 모델인데, SW 짜듯이 해."

 

그럴 만도 한 게 어차피 합성할 코드도 아니고 C 코딩하듯이 만들어도 출력만 멀쩡하면 되기 때문이었다. 그러나 RTL 설계를 먼저 시작한 나에게는 습관처럼 RTL같이 Testbench를 구현하는 버릇이 아직도 남아있다.

 

Image driver도 기본은 3개의 counter를 통해 컨트롤하게 구현했다. reference counter를 통해 전반적인 신호를 생성하고, 유효한 영역(H/V 방향으로 data가 출력되는 부분과 blank 부분까지 모두 포함한 영역)은 data_en 신호를 통해 구분 짓는다. h_cnt와 v_cnt가 각각 horizontal 방향과 vertical 방향의 프로토콜들을 생성하기 위한 토대(base)가 된다. 

 

 

▼ 3개의 counter로 각 프로토콜을 생성한 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
    reg [31:0] ref_cnt;
    reg [15:0] h_cnt;
    reg [15:0] v_cnt;
    reg [9:0] mem[0:H_SIZE * V_SIZE - 1];
    reg data_en;
 
    // data_enable
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            data_en <= 'b0;
        end
        else if (ref_cnt == (WAIT_TIME - 2)) begin
            data_en <= 'b1;
        end
        else if (ref_cnt == (WAIT_TIME + TOTAL_FRM - 2)) begin
            data_en <= 'b0;
        end
    end
    
    // reference counter
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            ref_cnt <= 'h0;
        end
        else begin
            ref_cnt <= ref_cnt + 'h1;
        end
    end
    
    // frame_start
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            frm_start <= 'h0;
        end
        else begin
            frm_start <= (ref_cnt == (FRM_CYCLE - 1)) ? 'h1 : 'h0;
        end
    end
 
    // h counter
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            h_cnt <= 'h0;
        end
        else if (h_cnt == (H_CYCLE - 1)) begin
            h_cnt <= 'h0;
        end
        else begin
            h_cnt <= data_en ? h_cnt + 'h1 : 'h0;
        end
    end
    // hsync & data_in
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            hsync <= 'b0;
            data_in <= 'h0;
        end
        else if ((h_cnt >= 0&& (h_cnt < H_SIZE) && (v_cnt < V_SIZE) && data_en) begin
            hsync <= 'b1;
            data_in <= mem[H_SIZE*v_cnt + h_cnt];
        end
        else begin
            hsync <= 'b0;
            data_in <= 'h0;
        end
    end
    // h_first, h_last
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            h_first <= 'b0;
            h_last <= 'b0;
        end
        else begin
            h_first <= ((h_cnt == 0&& (v_cnt < V_SIZE) && data_en) ? 'b1 : 'b0;
            h_last <= ((h_cnt == H_SIZE-1&& (v_cnt < V_SIZE) && data_en) ? 'b1 : 'b0;
        end
    end
 
    // v counter
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            v_cnt <= 'h0;
        end
        else if ((v_cnt == (V_CYCLE - 1)) && (h_cnt == (H_CYCLE - 1)) && data_en) begin
            v_cnt <= 'h0;
        end
        else if ((h_cnt == (H_CYCLE - 1)) && data_en) begin
            v_cnt <= v_cnt + 'h1;
        end
    end
    // vsync & data_in
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            vsync <= 'b0;
        end
        else if ((v_cnt >= 0&& (v_cnt < V_SIZE) && data_en) begin
            vsync <= ((v_cnt == (V_SIZE - 1)) && (h_cnt > (H_SIZE - 1))) ? 'b0 : 'b1;
        end
        else begin
            vsync <= 'b0;
        end
    end
 
    assign v_last = (v_cnt == (V_SIZE - 1)) & h_first;
 
cs

 

 

이런식으로 구현하지 않고, FSM을 통해 만드는 방법도 있을 것 같은데, 다음에 기회가 되면 한 번 다뤄보는 것도 괜찮을 것 같다. 혹은 조금 더 추상화하여 TLM(Transaction Level Modeling)으로 선배 말마따나 SW 스럽게 짜보는 것도 재미있는 경험이 될 것이다(정말...?).

 

 

 


 

 

 

 

영상을 연속적으로 출력하기 위해서는 메모리를 할당하여 미리 담아둘 필요가 있다. 앞선 코드(60번째 줄)를 봐도, mem이라는 1차원 array에 접근하여 순차적으로 값을 읽어나가는 것을 확인할 수 있다. SystemVerilog에서는 시스템 테스크인 $readmemh, $readmemb 등을 통해 손쉽게 메모리에 값을 넣어둘 수 있다.

 

사용법 : $readmemh("파일 경로", 값을 담을 메모리 array)

 

▼ $readmemh 및 vcd dump 구현부

1
2
3
4
5
6
7
8
9
    string imgFile = "./test_img.hex";
    string dumpFile = "./test.vcd";
 
    // vcd dump & img dump 
    initial begin
        $readmemh(imgFile, mem);                             
        $dumpfile(dumpFile);
        $dumpvars(1);
    end
cs

 

 

 


 

 

 

 

코드를 구현만 하고 끝이면 얼마나 좋을까? 이제는 디버깅의 영역인데 직접 log를 뿌리는 방법과 waveform을 확인하는 방법이 있다. 내가 어떤 simulator tool을 사용하냐에 따라서 waveform의 포맷도 달라지는데, 크게 3가지 정도로 나눌 수 있다.

 

  1. Cadence社의 simvision - *.shm
  2. Synopsys社의 verdi - *.fsdb
  3. VCD(Value Change Dump) 

 

이 중에서 나는 VCD를 통해 simulation 결과를 확인하였다. VCD는 IEEE에서 지원하는 ASCII 기반의 waveform standard로서, 대부분의 simulator에서 지원하는 규격이므로 호환성 측면에서 가장 뛰어나다. 다만, text 형식이기 때문에 용량이 크다는 단점이 존재한다. VCD를 통해 waveform dump를 받는 방법은 아래와 같이 매우 쉽다.

 

사용법: $dumpfile("waveform dump 파일 경로")

          $dumpvars(dump 옵션)  <levels>, <module_or_variable> 

          *dump 옵션을 주지 않을 경우 모든 variable을 dump 한다.

          *$dumpvars( <levels>, <module_or_variables> ) : 직접 module 지정 가능

          level 0: 하위 모든 sub module까지 dump

          level 1: 지정된 module만 dump

          level 2: 1단계 하위 module까지만 dump

 

VCD를 display 해주는 오픈소스 프로그램인 GTKWave를 사용하여 dump받은 waveform 결과를 확인할 수 있었다. 상용 tool과 비교해서 편의성이 조금 부족하고, 지원하는 기능에 제한이 있었지만 그래도 무료라는 점에서 꽤 쓸만한 tool이라고 느꼈다. EDA playground에서 제공하는 EPWare 같은 tool보다는 훨씬 좋으니, 혹시 simultor license가 없는 사람들은 꼭 한번 이용해 보길 권한다.

 

 

gtkwave.sourceforge.net/

 

GTKWave

Welcome to GTKWave GTKWave is a fully featured GTK+ based wave viewer for Unix, Win32, and Mac OSX which reads LXT, LXT2, VZT, FST, and GHW files as well as standard Verilog VCD/EVCD files and allows their viewing. You can grab version 3.3.107 here. Docume

gtkwave.sourceforge.net

 

자, 그럼 waveform을 통해서 Image driver가 제대로 동작하는지 확인해보자.

 

신호들이 이쁘게 잘 만들어졌음을 알 수 있다.

 

여러 frame을 구현하지 않았지만 기능 추가를 대비하여 뒷부분까지 확실하게 처리했다.

 

사실 코드가 한 번에 완벽하게 동작하지는 않았다. 의식의 흐름대로 코드를 만들다 보니, 사소한 timing 실수들이 있었고, 몇 번의 수정 끝에 원하는 결과를 얻어낼 수 있었다.

 

맺음글

 

예전에 좋은 설계자가 되기 위해서는 반드시 본인만의 Testbench를 꾸밀 수 있어야 한다고 블로그에 글을 남긴 적이 있다. 지금도 그 생각에 변함이 없으며, 중급 설계자로 넘어가기 위한 첫 번째 관문이 제로 베이스에서 검증 환경을 구축할 수 있는지 여부라고 생각한다. 

 

비록 나의 역량도 아직은 많이 부족하지만, 어제의 나 자신보다 조금은 더 성장했을 거라 믿는다.

부족한 이 포스팅이 어디선가 고군분투하고 있을 디지털 설계자들에게 작은 도움이 되었기를 빌며...

 

이만 글을 줄인다.

 

 

댓글
공지사항