
이번 포스팅에서는 가장 기초적인 Half Adder를 통해 RTL 설계의 기본기와 일반적인 검증 워크플로우를 정리해 보겠습니다.
Half Adder를 첫 예제로 고른 이유는 간단합니다. 로직 자체가 단순해서 설계보다 "내가 만든 게 정말 맞게 동작하는지 어떻게 확인하지?" 라는 검증의 흐름에 집중하기 좋기 때문입니다.
처음 RTL을 접했을 때 Verilog 문법이나 시뮬레이터 사용법부터 막막하게 느껴졌습니다.
이 글이 같은 고민을 하고 있는 분들께 조금이나마 도움이 됐으면 좋겠습니다.
RTL(Register Transfer Level) 설계는 단순히 코드를 짜는 것에 그치지 않고, 의도한 대로 동작하는지 증명하는 과정이 핵심입니다. 일반적인 순서는 다음과 같습니다.
1. RTL 작성 (Design): 하드웨어 로직 구현
2. 테스트벤치 작성 (Testbench): 입력 시나리오 및 검증 로직 작성
3. 컴파일 (Compilation): 설계 및 테스트벤치 코드의 문법 체크 및 실행 파일 생성
4. 검증 (Verification): 파형(Waveform) 분석 또는 로그 확인
반가산기는 가장 기본적인 산술 연산 블록으로, 2진수 1비트 두 개를 더해 합(Sum)과 올림수(C_out)를 출력합니다.
`timescale 1ns / 1ps
module half_adder (
input a, b,
output c_out, sum
);
// XOR 게이트를 이용한 Sum 구현
xor g1 (sum, a, b);
// AND 게이트를 이용한 Carry 구현
and g2 (c_out, a, b);
endmodule
테스트벤치는 설계한 모듈(DUT, Design Under Test)에 가상의 입력을 넣어주는 실험 환경입니다. 여기서는 두 가지 방식을 비교합니다.
입력값을 하나하나 직접 인가하고, 터미널의 로그나 파형을 보고 눈으로 직접 결과를 확인하는 방식입니다.
장점: 직관적이며 소규모 회로의 동작을 빠르게 확인할 때 유리합니다.
단점: 입력 조합이 많아질수록 누락되는 케이스가 생기기 쉽고, 하나하나 확인하기 번거롭습니다.
// directed testbench for half adder
`timescale 1ns / 1ps
module test_half_adder ();
reg t_a, t_b;
wire t_c_out, t_sum;
half_adder uut (
.a(t_a),
.b(t_b),
.c_out(t_c_out),
.sum(t_sum)
);
initial #100 $finish;
initial begin
$dumpfile("sim/build/output/iverilog/test_half_adder.vcd");
$dumpvars(0, test_half_adder);
$display("a b | c_out sum");
$display("-------------");
t_a = 0;
t_b = 0;
#10;
$display("%b %b | %b %b", t_a, t_b, t_c_out, t_sum);
t_a = 0;
t_b = 1;
#10;
$display("%b %b | %b %b", t_a, t_b, t_c_out, t_sum);
t_a = 1;
t_b = 0;
#10;
$display("%b %b | %b %b", t_a, t_b, t_c_out, t_sum);
t_a = 1;
t_b = 1;
#10;
$display("%b %b | %b %b", t_a, t_b, t_c_out, t_sum);
end
endmodule
입력값을 랜덤으로 인가하거나 모든 조합을 돌리면서, Reference Model(정답지)과 실제 출력값을 비교하여 자동으로 PASS/FAIL을 판정하는 방식입니다.
보통 6개의 블럭으로 구성됩니다.
// self-checking testbench for half_adder
`timescale 1ns / 1ps
module test_half_adder;
reg t_a, t_b;
reg [1:0] ref_out;
wire t_c_out, t_sum;
integer i;
integer err_cnt;
reg [3:0] seen; // 입력 조합 관측 체크용
// 1. DUT 연결
half_adder u_dut (
.a(t_a),
.b(t_b),
.c_out(t_c_out),
.sum(t_sum)
);
// 3. reference model
function [1:0] ref_half_adder;
input a_i, b_i;
begin
ref_half_adder = a_i + b_i; // {c_out, sum}
end
endfunction
initial begin
err_cnt = 0;
seen = 4'b0000;
// ── 2. 입력 인가 ──
repeat (50) begin
t_a = $urandom_range(0, 1);
t_b = $urandom_range(0, 1);
#1;
seen[{t_a, t_b}] = 1'b1;
// ── 4. 자동 비교: 랜덤 50회 ──
ref_out = ref_half_adder(t_a, t_b);
if ({t_c_out, t_sum} !== ref_out) begin
$error("Mismatch: a=%0b b=%0b got=%0b%0b exp=%0b%0b", t_a, t_b, t_c_out, t_sum, ref_out[1],
ref_out[0]);
err_cnt = err_cnt + 1;
end
end
// ── 미커버 조합 강제 인가 (전수 커버리지 보장) ─────────
for (i = 0; i < 4; i = i + 1) begin
if (!seen[i]) begin
{t_a, t_b} = i[1:0];
#1;
seen[{t_a, t_b}] = 1'b1;
ref_out = ref_half_adder(t_a, t_b);
if ({t_c_out, t_sum} !== ref_out) begin
$error("Mismatch: a=%0b b=%0b got=%0b%0b exp=%0b%0b", t_a, t_b, t_c_out, t_sum,
ref_out[1], ref_out[0]);
err_cnt = err_cnt + 1;
end
end
end
// ── 5. 최종 판정 ──
if (seen !== 4'b1111) $fatal(1, "FAIL: not all input combos covered, seen=%b", seen);
if (err_cnt == 0) $display("PASS: self-checking random TB");
else $fatal(1, "FAIL: err_cnt=%0d", err_cnt);
$finish;
end
initial begin
#1000;
$fatal(1, "TIMEOUT");
end
endmodule
컴파일 단계에서는 iverilog를 사용합니다. 각 옵션의 의미를 정확히 아는 것이 중요합니다.
// iverilog 컴파일 명령어 예시
iverilog -g2012 -Wall -s test_half_adder -o sim/build/output/iverilog/test_half_adder.vvp \
rtl/half_adder.v sim/test/test_half_adder.v
옵션 상세 설명
-g2012: iverilog가 사용할 Verilog 표준을 지정합니다.
g2005, g2001 등 여러 버전이 있는데, -g2012는 IEEE 1800-2012(SystemVerilog) 표준으로 $urandom_range 같은 최신 시스템 함수를 사용하려면 이 옵션이 필요합니다.
명시하지 않으면 iverilog 기본값으로 컴파일되어 최신 문법에서 에러가 날 수 있습니다.
-Wall: 모든 경고(Warning) 메시지를 출력합니다. 사소한 문법 실수나 포트 연결 누락처럼 에러는 아니지만 잠재적으로 문제가 될 수 있는 부분을 알려줍니다.
-s test_half_adder: 시뮬레이션의 최상위 모듈(Top-level module)을 지정합니다. 보통 테스트벤치 모듈 이름인 test_half_adder가 들어갑니다.
-o [출력파일경로]: 컴파일 결과로 생성될 실행 파일의 경로와 이름을 지정합니다. iverilog의 출력물은 .vvp 확장자를 사용하며, 이후 vvp 명령어로 실행합니다. 경로를 지정하지 않으면 현재 디렉토리에 a.out으로 생성됩니다.
소스 파일 나열: 마지막에는 컴파일에 필요한 모든 .v 파일을 나열합니다. 설계 파일(half_adder.v)을 먼저, 테스트벤치 파일(test_half_adder.v)을 그 뒤에 적는 것이 일반적입니다. 파일을 빠뜨리면 "module not found" 에러가 발생하니 주의하세요.
컴파일이 성공했다면 vvp 명령어로 시뮬레이션을 실행합니다.
// 시뮬레이션 실행
vvp sim/build/output/iverilog/test_half_adder.vvp
// 출력 예시
PASS: self-checking random TB


// gtkwave를 이용하여 파형 확인
gtkwave sim/build/output/iverilog/test_half_adder.vcd
아래 메시지가 뜨면 모든 테스트를 통과한 것입니다.
PASS: self-checking random TB

만약 실패했다면 error와 $fatal 메시지가 함께 출력됩니다. error는 어떤 입력값에서 출력이 틀렸는지 알려주고, $fatal은 최종적으로 몇 개의 오류가 발생했는지 요약해줍니다. 로그를 위에서부터 읽으면서 첫 번째 ERROR가 찍힌 시점을 찾는 것이 디버깅의 시작입니다.
ERROR: Mismatch: a=1 b=1 got=01 exp=10
FAIL: err_cnt=1
이번 포스팅에서는 Half Adder를 통해 RTL 설계부터 iverilog를 이용한 컴파일, 검증까지 전체 흐름을 정리해봤습니다. 단순한 회로지만 막상 직접 테스트벤치를 짜고 시뮬레이션을 돌려보면 생각보다 알아야 할 것이 많다고 느꼈습니다.
다음 포스팅에서는 베릴레이터를 이용하여 64bit adder를 검증해 볼 예정입니다.