티스토리 뷰

 

지난 시간에 ModelSim을 다운로드 받고, 실행하는 방법까지 다뤄봤었다.

이번에는 간단한 디지털 회로를 설계하고 ModelSim을 이용하여 simulation을 진행하는 방법에 관해 이야기 하고자 한다.

 

만약, ModelSim을 다운받고 설치하는 방법을 알고 싶다면

▼아래 링크를 클릭하길 바란다.

https://dreamsailor.tistory.com/3?category=856131

 

 

 

 

 

 

전자공학이라는 학문을 배우기 시작한 사람 중에

회로 설계를 세부 전공으로 선택하는 사람들이 반드시 직면한다는 고민은 다음과 같다.

 

아날로그 설계냐 아니면 디지털 설계냐

 

현업에서 설계 업무를 하다 보니, 두 가지 서로 다른 background를 가진 사람들을 많이 만나게 된다.

이러한 사람들과 함께 일을 하면서 느꼈던 것을 돌아보면,

예전에 전공 수업 때 한 교수님이 해주셨던 말씀이 떠오른다.

 

"아날로그 설계를 잘하는 뇌, 디지털 설계를 잘하는 뇌는 서로 배타적(exclusive)이다."

 

어렵게 이야기했지만, 한마디로 둘 다 잘하는 사람은 드물다는 것이다.

아날로그 설계가 익숙하고 적성에 맞는 사람이 있는 반면,

디지털 설계가 본인의 업(業)으로 삼기에 더 적합한 사람들도 있다.

 

나는 석사 때, 아날로그 설계 쪽을 전공했지만

역시나 나의 뇌는 디지털에 더 어울리는 녀석이었다.

디지털 설계는 결국 0과 1이다. 매우 명확한 결과를 출력한다.

이에 반해 아날로그 설계의 결과는 0부터 1 사이 어떠한 값도 가능하다.

해석하기에 복잡한 아날로그 설계보다 딱 뿌러지는 디지털이 체질에 맞았나보다.

 

명확한 결과를 얻기 위해서는 spec과 timing이 중요하다.

spec이 제대로 주어지지 않거나, 설계 도중 계속 바뀌는 것만큼 고역이 없다.

 

비록 이번 포스팅에서는 간단한 예제를 다룰 예정이지만,

디지털 설계의 과정을 ModelSim을 이용해 간단히 맛볼 수 있도록 구성하고자 노력하였다.

spec도 간략히 명시해보고 block diagram도 그려보았다.

아쉽게도 sequential logic을 설계하는 것이 아니기에 timing diagram은 여기서 다루지 않았다.

 

자, 그럼 0과 1의 세상으로 발을 내디뎌 보자.

 

 

 

 

오늘 우리가 만들어 볼 디지털 회로는 하나의 Half adder와 3개의 Full adder로 구성된 4bit ripple carry adder이다.

combinational logic이므로 clock이나 timing은 여기서는 고려할 필요가 없다.

 

[spec]

input port : input_a[3:0], input_b[3:0]

output port : sum[3:0], carry_out

 

이에 맞는 회로도는 아래와 같다.

 

 

 

Half adder와 Full adder는 Carry_in의 유무에 따라 나뉜다.

Carry_in을 받아, 자리 올림을 받아 계산에 고려하는 것이 Full_adder이다.

Half adder는 결과에 따라, 자리 올림 수를 출력하지만 연산 시에는 자리 올림 수 Carry_in을 받지 않는다.

 

Half adder는 아래와 같은 logic gate로 구현된다.

상단의 gate가 XOR 기능을 하고, 하단의 gate는 AND 연산을 담당한다.

 

 

 

Full adder는 다음 그림과 같은데, 자세히 살펴보면 Half adder가 2단으로 구성된 형태임을 알 수 있다.

 

 

목표로 하고 있는, 4bit ripple carry adder를 설계하기 위해서

Half adder module과 Full adder module을 각각 설계하였다.

이렇게 top에서 각 module을 instatiation 하는 것이 일반적인 설계 방식이다.

물론, 하나의 파일에 각 부분을 구현하는 형식으로 코딩해도 무방하지만,

reusability가 떨어지고, 가독성이 좋지 않다는 단점이 있다.

 

 

 

Verilog 파일의 확장자는 .v이다.

설계보다는 검증을 위해 주로 사용되는 System verilog의 확장자는 .sv이다.

현업보다는 학계에서 많이 사용하는 Vhdl 파일의 확장자는 .vhd이다.

사실 Vhdl은 논리회로를 배울 때, 잠깐 써보고 그 이후로 본 적이 없어서 기억이 가물가물하다.

 

나는 Linux 환경에서 설계 작업을 하기에,

Gvim 등과 같은 editor로 코딩 작업을 진행한다.

사실, 어떠한 editor를 사용하는지는 지극히 개인 취향의 영역이다.

여기서는 ModelSim 자체 editor를 사용해서 코딩하였다.

 

그럼 먼저 ModelSim을 실행시키자.

여러 가지 tip을 알려주는 창이 팝업으로 뜰 텐데, 그냥 닫으면 된다.

물론 해당 tool에는 다양한 고급 기능들이 있겠지만

우리는 아주 기본적인 부분만 확실히 알고 넘어가도록 한다.

 

 

학부 시절에는 아래 transcript 부분이 어떠한 기능을 하는지 몰랐는데,

간단히 설명하자면, console 창과 같이 직접 명령어를 넣어서 여러 기능을 동작시키는 부분이다.

GUI가 익숙한 사람들은 굳이 사용하지 않아도 되지만

이러한 명령어를 잘 다룬다면, 더욱 빠르게 simulation을 진행할 수 있다.

 

일단, project를 만들어 보자.

File -> New -> Project.. 를 클릭하면

아래와 같은 창이 뜰 것이다.

 

 

Project 이름과 저장될 위치를 입력할 수 있다.

기본 library 이름도 설정 가능한데, 여기서 작업하는 .v 파일들은 모두 이 directory에 저장된다.

이 외에 Modelsim에서 기본으로 제공하는 library들을 설정할 수 있다.

굳이 바꿀 필요 없이 OK를 누르자.

 

 

 

이러한 창이 떴다면, 아직 아무런 .v 파일들이 없으므로

먼저 Create New File을 눌러서 코딩을 시작해보자.

 

 

 

File Name에 원하는 파일명과 .v 확장자를 빼먹지 말고 기재하자.

File type은 VHDL이 아닌 Verilog를 입력한다.

내부에 다른 Folder들이 존재하지 않으므로, Top Level을 수정하지 않고 그대로 사용한다.

 

 

 

Verilog 파일이 하나 생성된 것을 확인할 수 있다.

파일을 우클릭하여 Edit을 눌러 내용을 채워보자.

아직 Half Adder와 Full Adder를 설계하지 않았기에,

해당 부분은 비워둔채 껍데기만 만들어 놓은 형상인 것을 알 수 있다.

 

왼쪽 창에서 우클릭 후,

Add to project -> New File을 눌러

HalfAdder와 FullAdder도 추가하자.

소스 파일은 아래 부분을 참고하면 된다.

 

▼ CarryRippleAdder (Top)

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
module CarryRippleAdder(
    a_in,
    b_in,
    sum,
    carry_out
);
 
    input [3:0] a_in;
    input [3:0] b_in;
    output [3:0] sum;
    output carry_out;
 
// Half Adder
    wire [3:0] carry;
    HalfAdder HA(
        .a_in(a_in[0]),
        .b_in(b_in[0]),
        .sum(sum[0]),
        .c_out(carry[0])
    );
// Full Adder_1
    FullAdder FA1(
        .a_in(a_in[1]),
        .b_in(b_in[1]),
        .c_in(carry[0]),
        .sum(sum[1]),
        .c_out(carry[1])
    );
// Full Adder_2
    FullAdder FA2(
        .a_in(a_in[2]),
        .b_in(b_in[2]),
        .c_in(carry[1]),
        .sum(sum[2]),
        .c_out(carry[2])
    );
// Full Adder_3
    FullAdder FA3(
        .a_in(a_in[3]),
        .b_in(b_in[3]),
        .c_in(carry[2]),
        .sum(sum[3]),
        .c_out(carry[3])
    );
    assign carry_out = carry[3];
 
endmodule
cs

 

▼ HalfAdder (Sub_module_1)

1
2
3
4
5
6
7
8
9
10
11
12
13
module HalfAdder(
    a_in,
    b_in,
    sum,
    c_out
);
    input a_in;
    input b_in;
    output sum;
    output c_out;
    assign sum = a_in ^ b_in; // xor
    assign c_out = a_in & b_in; // and
endmodule
cs

 

▼ FullAdder (Sub_module_2)

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
module FullAdder(
    a_in,
    b_in,
    c_in,
    sum,
    c_out
);
 
    input a_in;
    input b_in;
    input c_in;
    output sum;
    output c_out;
 
    wire a_xor_b;
    wire a_and_b;
    wire ab_and_c;
 
    assign a_xor_b = a_in ^ b_in; // xor
    assign a_and_b = a_in & b_in; // and
    assign ab_and_c = a_xor_b & c_in; // (a ^ b) & c
    assign sum = a_xor_b ^ c_in;
    assign c_out = ab_and_c | a_and_b; // or
 
endmodule
cs

 

 

필요한 .v 파일을 모두 생성하였으니,

컴파일을 진행할 차례이다.

컴파일을 통해 해당 design에 bug나 syntax error가 있는지 검사를 진행한다.

 

아래 그림은 HDL code를 컴파일하는 일련의 design cycle을 나타낸 도식이다.

조금 더 디테일하게 나아가면 Elaboration 하는 과정까지 포함 해야 한다고 할 수 있겠지만,

여기서는 간략한 HDL design의 과정을 나타내고자 한듯하다.

 

 

 

왼쪽 창에서 우클릭 후, Compile -> Compile All

일단은 상기 명령어를 사용해서 컴파일을 진행하면 된다.

만약 설계량이 방대해져서 모든 .v 파일을 컴파일하는 시간을 아끼고 싶으면,

원하는 파일을 선택하고 Compile -> Compile Selected를 클릭하면 선택한 파일만 컴파일하는 것이 가능하다.

 

만약, 컴파일 과정에서 Error가 발생한다면

다음과 같은 화면을 만나게 될 것이다.

초록색 체크 표시 사이에 빨간 엑스(X) 표시와 함께 log에서 어떤 파일에서 error가 발생했는지 알려준다.

 

 

 

당황하지 않고, 아래 error message를 double click 하면

어떠한 유형의 error인지 설명을 볼 수 있다.

 

 

여기에서 발생한 error는 아주 단순한 syntax error로

code를 적고 마지막 부분에 ;를 빼먹었다는 것이다.

수정해서 다시 진행하니, 정상적으로 컴파일되었다는 결과를 확인할 수 있었다.

 

 

 

 

 

 

 

설계가 끝났으니 stimulus를 인가해줄 testbench를 만들어야 한다.

testbench란, 직접 특정 입력을 input port에 넣어주고,

결과가 제대로 출력되는지를 확인하기 위한 환경이라고 보면 이해하기 쉽다.

 

여기서는 단순히 선형으로 시간이 증가하는 상태에서

몇몇 입력값을 변화시키고

그때마다 출력을 waveform 형태로 확인하는 것을 통해 검증을 진행할 것이다.

 

▼ testbench

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
`timescale 1ns/10ps
 
module tb_CRA();
    
    reg [3:0] test_in_a;
    reg [3:0] test_in_b;
    wire [3:0] test_out_sum;
    wire test_out_carry;
 
    CarryRippleAdder CRA(
    .a_in(test_in_a),
    .b_in(test_in_b),
    .sum(test_out_sum),
    .carry_out(test_out_carry)
    );        
 
    initial begin
    test_in_a = 4'b0000;
    test_in_b = 4'b0000;
    
    #100
    test_in_a = 4'b1100;
    test_in_b = 4'b0001;

    #10

    $display("in_a : %b, in_b : %b => sum : %b, carry : %b"
          test_in_a, test_in_b, test_out_sum, test_out_carry);
    #100
    test_in_a = 4'b0111;
    test_in_b = 4'b1011;

 

    #10 

    $display("in_a : %b, in_b : %b => sum : %b, carry : %b"
          test_in_a, test_in_b, test_out_sum, test_out_carry);
    #100
    test_in_a = 4'b1010;
    test_in_b = 4'b0101;

    #10 

    $display("in_a : %b, in_b : %b => sum : %b, carry : %b"
          test_in_a, test_in_b, test_out_sum, test_out_carry);
 
    end
endmodule
#cs

 

 

구현한 testbench는 아주 단순한 구조로 되어 있다.

timescale은 현재 simulation의 기준 단위를 1ns로 하고,

10ps의 해상도를 갖는다는 의미이다.

 

즉, #100 -> 100ns의 delay를 가지게 되며,

소수점으로 10ps까지 시간 표현의 세밀함의 정도를 조정할 수 있다.

 

testbench는 입출력을 따로 가지지 않는다.

내가 검증하고 싶어 하는 대상(DUT - Design Under Test)을 껍데기처럼 둘러싸고 있는 module이기 때문이다.

testbench는 아래 두 가지 기능을 잘 만드는 것이 핵심이다.

원하는 입력을 DUT로 밀어 넣어줄 수 있어야 하며,

DUT의 출력을 받아올 수 있어야 한다.

 

본 testbench에서는 $display task를 사용하여 입력과 출력을 log로 뿌려주게 하였다.

물론, waveform으로 확인하는 방법도 있지만,

이렇게 log로 결과를 바로 볼 수 있게 하는 것도 때로는 검증 시간을 단축하는 효과가 있다.

 

 

 

 

 

waveform 및 testbench에 추가한 $display를 통해 결과를 확인하기 위해,

직접 simulation을 진행해 보도록 하자.

 

Simulate -> Start Simulation...을 클릭하면 다음과 같은 화면을 마주하게 된다.

 

 

 

work directory에 testbench를 선택하고 OK 버튼을 클릭한다.

왼쪽에 simulation 창이 떴으면, 원하는 signal을 wave로 뽑기 위해,

DUT를 선택하여, 우클릭한다.

다양한 메뉴 중, Add wave를 클릭하자.

 

 

 

이제 wave 창이 떴으니, 모든 준비는 끝났다.

 

1st step. 원하는 만큼 simulation time을 조정해준다. 그리고 run 버튼을 클릭하자.

2nd step. zoom 버튼을 클릭하고 wave 창에서 우클릭 후, Zoom Full을 눌러서 전체 waveform을 한눈에 쉽게 확인할 수 있도록 만들자. 

 

 

해당 signal이 binary로 나오지 않는다면,

wave창의 Msgs 칸 내부에서 우클릭 후, radix를 원하는 형태로 변경하면 된다.

 

wave 결과 및 log를 통해 원하는 결과를 출력하는 것을 확인할 수 있었다.

물론, 해당 logic의 경우에는 overflow나 signed/unsigned를 고려하지는 않았기에

두 input을 더 해준 sum의 결과가 돌아가는 현상이 있는 것을 볼 수 있다.

 

이러한 모든 corner case들을 고려해서 설계하면 가장 좋은 일이겠지만,

예상하지 못한 오동작을 잡아내기 위해

다양한 case로 검증을 진행하는 것도 이 못지않게 중요한 일이다.

 

 

 

추억에 잠겨,

먼지 묻은 오래된 tool을 꺼내고

감상에 젖어 시간 가는 줄 모르고 설계를 하였다.

 

문득, 학부 시절 나의 모습이

오버랩 되고는 했다.

 

모든 것이 낯설기도 했고

많은 시행착오도 겪으면서

전전긍긍했던 젊은 날의 초상.

 

그래도

즐거웠다.

설계를 한다는 것.

무언가를 만들어 낸다는 것.

그 자체에 매료되었었다.

 

나는 지금도

디지털 회로설계라는 분야를 선택한 것을 후회하지 않는다.

 

 

부족한 포스팅이지만,

젊은 날의 내 모습처럼 디지털 설계라는 세계에

조심스럽게 문을 두드리고 있는 누군가를 위해

자그마한 도움이 되었으면 하는 바람이다.

 

- Fin.

 

댓글
공지사항