[베릴로그 RTL 예제] 탁구 게임기 -1편: RTL 베릴로그로 탁구대 그리기-
목차:
개요
1. 탁구대 그리기
1-1. 래스터 스캔 방식 비디오 시현
1-2. 베릴로그 HDL로 탁구대 그리기
1-3. C++ 로 작성하는 하드웨어 시뮬레이션 테스트벤치
1-4. 시뮬레이터 빌드
1-5. Makefile
1-6. 실습
----------------------------------------------------------------------------------------------------------------
오픈-소스 반도체 설계 도구가 설치된 "내 칩 디자인 킷"의 WSL 가상 디스크 이미지 [링크]
----------------------------------------------------------------------------------------------------------------
베릴로그 HDL로 화려하지는 않은 비디오 게임기를 만들어본다. 1인 탁구 게임기다. 마이크로 컨트롤러 보드에서 C++언어로 이정도 게임기를 만들기는 그리 어렵지 않을지 모른다. 하지만 이런 동작을 하는 디지털 회로를 설계하기는 좀더 난해할 수 있다.
디지털 회로 설계에도 컴퓨팅 언어가 사용된다. 베릴로그(Verilog)라는 HDL(Hardware Description)언어다. 탁구 게임기가 제대로 묘사 되었는지 확인도 하지 않고 무턱대고 디지털 회로를 꾸밀 수는 없다. 하드웨어는 매우 단단해서 한번 잘못 만들어 놓으면 고치기는 매우 어렵다. 사실 불가능 하다. 디지털 회로를 실제 하드웨어로 꾸미기 전에 먼저 시뮬레이션을 해볼 텐데 프로그래밍 언어 C++와 하드웨어 묘사용 크래스 라이브러리 SystemC 를 사용한다. 동작이 확인 되면 디지털 논리 소자들로 구성된 회로를 FPGA 에 넣어 작동 시켜볼 것이다. 이에 만족하면 한걸음 더 나가 "내 칩"으로 만들어 보기로 하자. 세상에서 유일한 내가 만든 게임기 IC를 내놓을 꿈을 꾸면서 말이다. 나만의 반도체 IC 부품을 제작하려면 큰 비용이 들지만 우리나라 정부에서 학생들에게 무료로 제공하는 "내 칩 제작 서비스"를 통해 "내 칩"을 만들어 볼 수 있다. SOP 28 핀 패키지까지 무료다. 내 유일한 IC를 받아 아래 동영상 처럼 게임기를 만들어 보자. 왼쪽에 노란 딱지가 붙여진 칩이 탁구 게임기 "내 칩"이다.
1. 탁구대 그리기
비디오 게임기를 목표로 하는 만큼 디지털 그래픽 시현을 알아보자.
1-1. 래스터 스캔 방식 비디오 시현
2차원 평면에 그림을 그리는 방식은 다양하지만 그중 가장 단순하면서 직관적인 것이 래스터 스캔(Raster Scan)이다. 가로와 세로로 좌표를 훑어가며 그림이 될 지점에 점을 찍는다. 그래픽 평면은 가로 128, 세로 64개의 점으로 정했다. 한 화면은 총 8192개의 점으로 구성된다. 공과 패들이 움직이는 모습을 보여 주려면 초당 10개 화면이 만들어져야 한다. 간단해 보이지만 무려 초당 8만 1천여개(=128x64x10)의 점을 쏟아내는 하드웨어를 설계하는 것이 목표다.
1-2. 베릴로그 HDL로 탁구대 그리기
움직이는 화면을 보여주려면 끊임 없이 가로 축 좌표와 세로축 좌표를 생성하고 그림의 위치에 점을 찍어 주어야 한다. 2차원 그래픽의 좌표점을 생성하고 탁구대의 벽을 그리는 하드웨어의 베릴로그 묘사는 다음과 같다. 디지털 회로의 카운터는 클럭을 받아 숫자를 센다. 가로축 좌표 x_pos 를 생성하기 위해 0부터 127까지 센다. 세로축 카운터의 숫자 범위는 0부터 63까지다. 각각 7 비트와 6 비트로 선언되었다.
// Filename: pong_SbS.v
// Purpose: Draw Table
//
module pong_SbS(clk, reset, x_pos, y_pos, pixel);
input clk;
input reset;
output [6:0] x_pos;
output [5:0] y_pos;
output pixel;
reg [5:0] y_pos;
reg pixel;
always @(posedge clk or posedge reset)
begin
if (reset)
begin
x_pos <= 0;
y_pos <= 0;
end
else
begin
x_pos += 1;
if (x_pos==0) y_pos += 1;
end
end
가로 축 x_pos 는 7비트로 선언되었으므로 127(=1111111b)까지 센 후 다음은 0이다. 세로 축 y_pos는 가로 선을 모두 찍고난 x_pos가 0일 때 1씩 증가한다. 6비트 이므로 63(=111111b)다음은 0이다. 세로 축 카운트가 63까지 모두 세면 한 화면의 모두 그려진 셈이다. 가로 축 좌표 10에서 14 사이의 좌표에 점을 찍어 탁구대 벽을 그리는 베릴로그 묘사는 다음과 같다.
assign pixel = (x_pos>9 && x_pos<15)? 1'b1:1'b0;1-3. C++ 로 작성하는 하드웨어 시뮬레이션 테스트벤치
탁구대 평면 그림을 그리는 베릴로그를 시뮬레이션 해보자. 테스트 벤치는 SystemC 로 작성 하였다.
//
// Filename: sc_pong_SbS_TB.h
//
#ifndef _SC_PONG_SBS_TB_H_
#define _SC_PONG_SBS_TB_H_
#include <systemc.h>
#ifdef VCD_TRACE_DUT_VERILOG
#include <verilated_vcd_sc.h>
#endif
#include "Vpong_SbS.h"
SC_MODULE(sc_pong_SbS_TB)
{
sc_clock clk;
sc_signal<bool> reset;
sc_signal<bool> pixel;
sc_signal<sc_uint<7> > x_pos;
sc_signal<sc_uint<6> > y_pos;
Vpong_SbS* u_pong_SbS;
#ifdef VCD_TRACE_TEST_TB
sc_trace_file* fp; // VCD file
#endif
#ifdef VCD_TRACE_DUT_VERILOG
VerilatedVcdSc* tfp; // Verilator VCD
#endif
void Test_Gen(void);
SC_CTOR(sc_pong_SbS_TB):
clk("clk", 100, SC_NS, 0.5, 0.0, SC_NS, false)
{
SC_THREAD(Test_Gen);
sensitive << clk;
// Instantiate DUT ----------
u_pong_SbS = new Vpong_SbS("u_pong_SbS");
u_pong_SbS->clk(clk);
u_pong_SbS->reset(reset);
u_pong_SbS->x_pos(x_pos);
u_pong_SbS->y_pos(y_pos);
u_pong_SbS->pixel(pixel);
#ifdef VCD_TRACE_TEST_TB
// VCD Trace
fp = sc_create_vcd_trace_file("sc_pong_SbS_TB");
fp->set_time_unit(100, SC_PS);
sc_trace(fp, clk, "clk");
sc_trace(fp, reset, "reset");
sc_trace(fp, x_pos, "x_pos");
sc_trace(fp, y_pos, "y_pos");
sc_trace(fp, pixel, "pixel");
#endif
#ifdef VCD_TRACE_DUT_VERILOG
// Trace Verilated Verilog internals
Verilated::traceEverOn(true);
tfp = new VerilatedVcdSc;
sc_start(SC_ZERO_TIME);
u_pong_SbS->trace(tfp, 99); // Trace levels of hierarchy
tfp->open("Vpong_SbS.vcd");
#endif
}
};
#endif
SystemC는 C++ 로 하드웨어를 묘사하기 위해 만들어진 크래스 라이브러리다. 만일 SystemC가 생소하다면 유튜브 동영상 "Learn SystemC"[링크]를 추천한다. 10여분짜리 6편으로 구성된 이 동영상은 SystemC를 간략하게 핵심만 짚어 설명한다. 프로그래밍 언어를 이해한다면 처음 세편 정도만 들어보는 것으로도 시작하기에 충분하다.
탁구대 그리기를 기술한 베릴로그 pong_SbS.v의 테스트벤치를 살펴보자. C++의 크래스를 사용하기 위해 먼저 SystemC를 인클루드 한다.
#include <systemc.h>
베릴로그 HDL로 묘사된 하드웨어를 SystemC의 테스트벤치로 불러오려면 언어변환이 되어야 한다. 베릴레이터(Verilator)는 오픈-소스 언어 변환기로서 베릴로그를 SystemC의 C++ 크래스로 변환해준다. C++의 SystemC 크래스로 변환된 베릴로그의 파일들은 현재 작업 디렉토리의 obj_drv에 저장된다. 언어 변환한 C++ 헤더 파일 명은 베릴로그 파일 앞에 대문자 V 가 붙는다. SystemC 테스트벤치에서 C++로 변환된 베릴로그의 헤더 파일을 인클루드 한다.
#include "Vpong_SbS.h"
테스트벤치 모듈은 SystemC의 크래스 sc_pong_SbS_TB 다. 크래스와 동일한 이름의 구성자(constructor)가 존재한다.
SC_MODULE(sc_pong_SbS_TB)
{
SC_CTOR(sc_pong_SbS_TB):
clk("clk", 100, SC_NS, 0.5, 0.0, SC_NS, false)
{
......
}
};
구성자가 수행할 주요 임무 중 하나는 사건(event)과 이에 반응할 콜백(call-back) 함수의 지정이다. 클럭 신호 clk에 발생하는 사건에 감응되어 반응할 콜백 함수 Test_Gen() 를 아래와 같이 지정하였다.
SC_THREAD(Test_Gen);
sensitive << clk;
시험의 대상 DUT(Design Under Test)는 포인터 크래스로 선언되었다.
Vpong_SbS* u_pong_SbS;
DUT를 사례화(instantiate)한 후 지역 하드웨어 객체에 연결(mapping)도 구성자 내에서 이뤄진다.
// Instantiate DUT ----------
u_pong_SbS = new Vpong_SbS("u_pong_SbS");
u_pong_SbS->clk(clk);
u_pong_SbS->reset(reset);
u_pong_SbS->x_pos(x_pos);
u_pong_SbS->y_pos(y_pos);
u_pong_SbS->pixel(pixel);
모듈 내의 지역 하드웨어 객체(베릴로그의 wire 또는 reg에 해당)들은 미리 선언 되었다.
sc_clock clk;
sc_signal<bool> reset;
sc_signal<bool> pixel;
sc_signal<sc_uint<7> > x_pos;
sc_signal<sc_uint<6> > y_pos;
주기적인 클럭을 선언하는 객체형은 sc_clock 이다. 주기(period)와 비율(duty ratio)등 클럭의 규격은 구성자와 함께 초기화 한다. 테스트벤치의 구성자 SC_CTOR 에서 주기 100 나노 초, 듀티 비 50% 인 클럭 발생기의 초기화 선언은 다음과 같다.
SC_CTOR(sc_pong_SbS_TB):
clk("clk", 100, SC_NS, 0.5, 0.0, SC_NS, false)
하드웨어 신호 clk에 발생하는 사건에 감응된 콜백 함수 Test_Gen()는 별도의 sc_pong_SbS_TB.cpp에 기술하였다.
//
// Filename: sc_pong_SbS_TB.cpp
//
#include "sc_pong_SbS_TB.h"
void sc_pong_SbS_TB::Test_Gen()
{
reset.write(true);
wait(clk.posedge_event());
wait(clk.posedge_event());
wait(clk.posedge_event());
reset.write(false);
while(true)
{
wait(clk.posedge_event());
if (x_pos.read()==127 && y_pos.read()==63)
{
wait(5000, SC_NS);
sc_stop();
}
}
}
콜백 함수의 형식은 사건에 대기 시킬 수 있는 SC_THREAD 다. 사건 또는 시간 지연의 방식으로 테스트 절차를 기술 할 수 있다. 시뮬레이션이 개시되면 DUT에 reset 을 준 후 3회에 걸쳐 clk의 상승 엣지 사건을 기다린 후 리셋을 풀어준다. 이어 while(true)의 무한 반복문으로 이어진다. 실행 선점권을 이 무한 반복문이 점유하지 않도록 wait(clk.posedge_event()) 를 두고 있는 점에 유의한다.
SystemC는 C++다. 변환으로 얻은 하드웨어 DUT와 테스트벤치 크래스를 가지고 실행 파일을 만들려면 main()에 사례화 한다.
/******************************************************************
Filename: sc_main.cpp
Purpose : Testbench
Author : goodkook@gmail.com
History : Mar. 2025, First release
*******************************************************************/
#include "sc_pong_SbS_TB.h"
int sc_main(int argc, char** argv)
{
sc_pong_SbS_TB u_sc_pong_SbS_TB("u_sc_pong_SbS_TB");
sc_start();
return 0;
}
DUT를 가지고 있는 테스트벤치 크래스를 인클루드 하고 sc_main() 내에 사례화 하였다. sc_main()은 매크로 정의한 것으로 C++ 의 main()이다.
sc_pong_SbS_TB u_sc_pong_SbS_TB("u_sc_pong_SbS_TB");하드웨어가 준비되면 비로서 시뮬레이터를 시작한다.
sc_start();
시뮬레이션은 사건 발생이 없거나 진행 시간이 만료될때까지 지속된다. sc_start()의 인수로 지속 시간을 줄 수 있다. 지속 시간을 지정하지 않았으므로 지속 시간을 무한히 진행된다. sc_clock 으로 클럭 사건이 무한 반복되므로 시뮬레이터는 종료하지 않는다.
1-4. 시뮬레이터 빌드
C++ 컴파일러로 DUT와 테스트벤치를 컴파일 하여 시뮬레이터를 빌드한다. 베릴레이터를 통한 빌드 명령은 다음과 같다.
$ verilator --sc -Wno-WIDTHTRUNC -Wno-WIDTHEXPAND \
--trace --timing --pins-sc-uint \
--top-module pong_SbS --exe --build \
-CFLAGS -g -CFLAGS -I../../c_untimed \
-CFLAGS -I/opt/systemc/include \
-CFLAGS -DVCD_TRACE_TEST_TB \
-CFLAGS -DVCD_TRACE_DUT_VERILOG \
-LDFLAGS -lm -LDFLAGS -lgsl -LDFLAGS -lSDL2 \
../pong_SbS/pong_SbS.v \
./sc_main.cpp ./sc_pong_SbS_TB.cpp
명령 verilator 는 베릴로그를 C++ 크래스 모듈로 변환하고 SystemC로 작성한 테스트 벤치를 묶어 실행파일로 컴파일 한다. 베릴로그에서 변환된 C++ 소스 파일은 현재 디렉토리에서 하위 디렉토리 obj_dir를 만들어 저장한다. 다수의 C++ 소스 파일을 일일이 컴파일 하고 실행 파일을 생성 하려면 표준 외에 별도의 라이브러리들이 추가되어야 한다. 이를 일괄적으로 처리하기 위한 메이크 스크립트도 이곳에 만들어진다. 메이크 스크립트 ./obj_dir/Vpong_SbS.mk 에 의해 시뮬레이터를 빌드하는 과정은 다음과 같다. 언어 변환 후 컴파일과 링크는 매우 난해하며 그 과정에서 수많은 빌드 옵션이 동원된다. 화면에 뿌려지는 로그들은 매우 난해하다. 어지러운 메시지들을 일일이 살펴볼 필요는 없지만 어떤 절차를 수행하는지 간략히 살펴보기로 한다.
언어변환기, 컴파일-링크 등과 같은 자동화 도구들의 작동을 이해하면 향후 고급 활용이 필요할 때 도움이될 것이다. 자동화 도구의 개선이나 전용 도구 사유화 개발에 참여 할 수도 있다. 자동화 도구의 개발 또한 반도체 산업의 중요한 부분을 차지한다. 무었보다도 설계 오류에 대처할 수 있는 단서가 된다. 게다가 도구를 잘 이해하고 다룰수 있다면 능력치를 뽐내기 매우 좋다!
make: Entering directory './obj_dir'
ccache g++ -I. -MMD ...... \
-I/usr/local/share/verilator/include \
-I/usr/local/share/verilator/include/vltstd \
......
-I/opt/systemc/include \
-DVCD_TRACE_TEST_TB -DVCD_TRACE_DUT_VERILOG \
-I/opt/systemc/include \
-Os -c -o sc_main.o ../sc_main.cpp
ccache g++ -I. -MMD ...... \
......
-Os -c -o sc_pong_SbS_TB.o ../sc_pong_SbS_TB.cpp
하위 디렉토리 obj_dir로 이동 후 테스트벤치를 컴파일 한다. GNU의 C++ 컴파일러 g++ 를 ccache 에 씌워 실행 하고 있다. 다수의 소스 파일을 독립적으로 컴파일 하여 빌드 시간을 줄일 수 있도록 해준다. 특히 병렬 코어를 가진 CPU에 병렬처리가 가능한 현대적 컴퓨팅 운영체제에서 매우 유용하다. 이어 베릴레이터의 API 소스들을 컴파일 한다.
ccache g++ -Os -I. -MMD ...... \
......
-c -o verilated.o \
/usr/local/share/verilator/include/verilated.cpp
ccache g++ -Os -I. \
......
-c -o verilated_vcd_c.o \
......
/usr/local/share/verilator/include/verilated_vcd_c.cpp
ccache g++ -Os -I. -MMD ...... \
......
-c -o verilated_threads.o \
......
/usr/local/share/verilator/include/verilated_threads.cpp
베릴로그에서 SystemC로 변환할 때 작은 단위(베릴로그의 always블럭을 SystemC의 사건-콜백 함수)로 분할되었던 C++ 소스들을 한 파일로 묶는다. 이때 베릴레이터에서 제공한 파이썬 스크립트가 사용된다.
python3 /usr/local/share/verilator/bin/verilator_includer \
......
Vpong_SbS.cpp ... > Vpong_SbS__ALL.cpp
베릴로그에서 변환된 C++ 파일을 컴파일 한다.
......
-I/opt/systemc/include \
-c -o Vpong_SbS__ALL.o Vpong_SbS__ALL.cpp
테스트벤치와 베릴레이터 API 그리고 DUT의 오브젝트 파일들을 모두 묶어 한 실행 파일로 합친다. 이때 systemc, pthread 등의 라이브러리가 동원되었다. 각각 컴파일된 오브젝트 파일을 모두 묶는 링크 단계에서 g++의 실행은 ccache 하지 않는다.
g++ -L/opt/systemc/lib \
sc_main.o \
sc_pong_SbS_TB.o \
verilated.o \
verilated_vcd_c.o \
verilated_threads.o \
Vpong_SbS__ALL.a \
-pthread -lpthread -latomic -lsystemc \
-o Vpong_SbS
빌드하는 동안 만들어졌던 중간 파일들을 청소하고 작업을 수행 하던 obj_dir 디렉토리를 떠나면서 수행 리포트를 보여준다. 베릴레이터의 버젼, 도구가 작동하면서 점유했던 메모리 량, 수행 시간 등을 보여 준다. 참고로 봐두자.
rm Vpong_SbS__ALL.verilator_deplist.tmp
make: Leaving directory '/obj_dir'
- V e r i l a t i o n R e p o r t: Verilator 5.045 devel rev v5.044-137-g4aa0ea3f2
- Verilator: Built from 0.029 MB sources in 2 modules, into 0.040 MB in 8 C++ files needing 0.000 MB
- Verilator: Walltime 0.770 s (elab=0.009, cvt=0.022, bld=0.677); cpu 0.019 s on 1 threads; allocated 30.348 MB
1-5. Makefile
시뮬레이터를 빌드하기 위해 사용한 verilator의 명령줄 옵션이 매우 길다. 개발하는 도중 이를 매번 입력하기가 번거롭다. 심지어 이 명령들을 전부 외우고 있기도 어려울 뿐더러 설계마다 달라지는 옵션을 명령줄에 적용할 수 없다. 시간이 지난후 재 빌드할 때 예전의 기억을 모두 더듬기는 불가능하다. 자동화 도구의 명령과 사용된 옵션 그리고 여러 단계의 빌드 절차를 스크립트로 작성해 두면 언재든 재연 할 수 있다. Make 유틸리티는 빌드 절차를 쳬계적으로 기술할 수 있는 유용한 도구다. 탁구 게임기 시뮬레이터를 빌드하는 Makefile 은 다음과 같다.
# SystemC Environments -----------------------------------------
export SYSTEMC = /opt/systemc
export SYSTEMC_HOME = $(SYSTEMC)
export SYSTEMC_INCLUDE = $(SYSTEMC_HOME)/include
export SYSTEMC_LIBDIR = $(SYSTEMC_HOME)/lib
export LD_LIBRARY_PATH :=$(LD_LIBRARY_PATH):$(SYSTEMC_LIBDIR)
export CXX = clang++
#---------------------------------------------------------------
TOP_MODULE = pong_SbS
# SystemC testbench Reuse --------------------------------------
SC_SRCS = \
./sc_main.cpp \
./sc_$(TOP_MODULE)_TB.cpp
SC_HDRS = \
./sc_$(TOP_MODULE)_TB.h
# Verilator vars -----------------------------------------------
RTL_PATH = \
../$(TOP_MODULE)
lint: VERILOG_SRCS = \
$(RTL_PATH)/pong_SbS.v
build: VERILOG_SRCS = \
$(RTL_PATH)/pong_SbS.v
run: VERILOG_SRCS = \
$(RTL_PATH)/pong_SbS.v
#---------------------------------------------------------------
VERILATOR = verilator
VL_WARNING = -Wno-WIDTHTRUNC -Wno-WIDTHEXPAND
VCFLAGS += -CFLAGS -g
VCFLAGS += -CFLAGS -I../../c_untimed
VCFLAGS += -CFLAGS -I$(SYSTEMC_INCLUDE)
ifeq ($(VCD_TRACE),)
VCFLAGS += -CFLAGS -DVCD_TRACE_TEST_TB
VCFLAGS += -CFLAGS -DVCD_TRACE_DUT_VERILOG
endif
#VCFLAGS += -CFLAGS -D$(MODE)
VCFLAGS += -LDFLAGS -lm
VCFLAGS += -LDFLAGS -lgsl
VCFLAGS += -LDFLAGS -lSDL2
#VCFLAGS += -CFLAGS -fPIC
#VCFLAGS += -LDFLAGS -shared
# Targets ------------------------------------------------------
TARGET = V$(TOP_MODULE)
TARGET_DIR = obj_dir
# Build Rules --------------------------------------------------
all :
@clear
@if [ ! -n "$(TOP_MODULE)" ]; then \
echo "*********************************"; \
echo "!!! TOP_MODULE not declared !!!"; \
echo "*********************************"; \
exit 1; \
fi
@echo
@echo 'Makefile for Co-Simulation of Verilog-RTL example, $(TOP_MODULE) in MODE=$(MODE)'
@echo
@echo ' TOP_MODULE=$(TOP_MODULE) make lint'
@echo ' TOP_MODULE=$(TOP_MODULE) VCD_TRACE=[YES]|NO make build'
@echo ' make run'
@echo ' make wave'
@echo ' make clean'
@echo
@echo 'CC BY-NC, by GoodKook, goodkook@gmail.com'
@echo
lint :
$(VERILATOR) --sc --timing $(VL_WARNING) --pins-sc-uint \
--top-module $(TOP_MODULE) $(VERILOG_SRCS)
build : $(TARGET_DIR)/$(TARGET)
$(TARGET_DIR)/$(TARGET) : $(VERILOG_SRCS) $(SC_SRCS) $(SC_HDRS)
$(VERILATOR) --sc $(VL_WARNING) --trace --timing --pins-sc-uint \
--top-module $(TOP_MODULE) $(VERILOG_DEF) --exe --build \
$(VCFLAGS) $(VERILOG_SRCS) $(SC_SRCS)
run : $(TARGET_DIR)/$(TARGET)
./$(TARGET_DIR)/$(TARGET)
wave : V$(TOP_MODULE).vcd sc_$(TOP_MODULE)_TB.vcd
gtkwave V$(TOP_MODULE).vcd --save=V$(TOP_MODULE).gtkw &
gtkwave sc_$(TOP_MODULE)_TB.vcd --save=sc_$(TOP_MODULE)_TB.gtkw &
clean :
rm -rf $(TARGET_DIR)
rm -f *.vcd
rm -f $(TOP_MODULE)
rm -f sc_$(TOP_MODULE)_TB.txt
debug : $(TARGET_DIR)/$(TARGET)
ddd $(TARGET_DIR)/$(TARGET)
C++의 표준 크래스 외에 SystemC 라이브러리를 사용하고 있으므로 이에 맞는 환경을 갖춰야 한다. 환경 변수로 파일시스템 내에 SystemC의 헤더 파일과 라이브러리가 존재하는 디렉토리들을 변수로 선언하였다. 환경변수 LD_LIBRARY_PATH는 실행형 동적 라이브러리(마이크로소프트의 윈도우즈 운영체제에서 DLL, Dynamic Linking Library, 리눅스에서는 so, Shared Object)가 존재하는 디렉토리 위치를 지정한다. 시뮬레이터가 실행 될 때 SystemC의 시뮬레이션 엔진(커널)을 필요로 한다. 이때 실행형 동적 라이브러리를 불러오기 위해 탐색할 디렉토리 위치다. 현재 수행중인 스크립트 이외 실행 명령에서도 이 환경변수를 유효화 하기 위해 export 하였다.
# SystemC Environments -----------------------------------------
export SYSTEMC = /opt/systemc
export SYSTEMC_HOME = $(SYSTEMC)
export SYSTEMC_INCLUDE = $(SYSTEMC_HOME)/include
export SYSTEMC_LIBDIR = $(SYSTEMC_HOME)/lib
export LD_LIBRARY_PATH :=$(LD_LIBRARY_PATH):$(SYSTEMC_LIBDIR)
export CXX = clang++
베릴레이터 도구의 다양한 명령줄 옵션들을 모두 Makefile 내에서 사용할 변수로 지정하였다. 변수 할당에 += 는 이어붙이기(concatenation)다. 베릴레이터의 실행 옵션은 [링크]에서 확인할 수 있다.
VERILATOR = verilator
VL_WARNING = -Wno-WIDTHTRUNC -Wno-WIDTHEXPAND
VCFLAGS += -CFLAGS -g
VCFLAGS += -CFLAGS -I../../c_untimed
VCFLAGS += -CFLAGS -I$(SYSTEMC_INCLUDE)
ifeq ($(VCD_TRACE), )
VCFLAGS += -CFLAGS -DVCD_TRACE_TEST_TB
VCFLAGS += -CFLAGS -DVCD_TRACE_DUT_VERILOG
endif
VCFLAGS += -LDFLAGS -lm
VCFLAGS += -LDFLAGS -lgsl
VCFLAGS += -LDFLAGS -lSDL2
Makefile 은 스크립트오서 언어에 준하는 명령 체계를 갖추고 있다. 환경 변수를 문자열로 비교하는 명령은 ifeq 이다. 환경 변수 VCD_TRACE 의 문자열 값이 YES인 경우 변수 VCFLAGS에 문자열을 추가하는 명령문은 다음과 같다.
ifeq ($(VCD_TRACE), YES)
VCFLAGS += -CFLAGS -DVCD_TRACE_TEST_TB
VCFLAGS += -CFLAGS -DVCD_TRACE_DUT_VERILOG
endif
다른 인수 없이 make 를 실행 하면 현재 디렉토리에서 Makefile 을 찾아 이를 처리한다. 기본 처리 목표(target)은 all: 이다. 위의 Makefile에서 목표 all 은 도움말을 출력하도록 작성되었다.
all :
@clear
@echo
@echo 'Makefile for Co-Simulation of Verilog-RTL example, \
$(TOP_MODULE) in MODE=$(MODE)'
@echo
@echo ' TOP_MODULE=$(TOP_MODULE) make lint'
@echo ' TOP_MODULE=$(TOP_MODULE) VCD_TRACE=[YES]|NO make build'
@echo ' make run'
@echo ' make wave'
@echo ' make clean'
@echo
@echo 'CC BY-NC, by GoodKook, goodkook@gmail.com'
@echo
위의 Makefile에는 별도로 여러 목표를 지정하고 있다. 목표 lint는 베릴로그의 무결성을 검사한다. 베릴레이터는 베릴로그를 읽어 C++로 변환해 주는 도구로서 무결성 검사를 매우 효과적으로 수행한다. 달러 문자와 소괄호 $() 사이에 앞서 선언한 변수를 지정하여 해당 문자열 값을 불러온다. 백슬러쉬 문자는 명령줄이 연속됨을 뜻한다.
lint :
$(VERILATOR) --sc --timing $(VL_WARNING) --pins-sc-uint \
--top-module $(TOP_MODULE) $(VERILOG_SRCS)
목표 build 는 베릴로그에서 변환한 모듈과 테스트벤치를 합쳐 C++ 컴파일러로 시뮬레이터를 빌드한다.
build : $(TARGET_DIR)/$(TARGET)
Makefile에서 콜론(:)은 매우 중요한 의미를 가진다. 왼편의 목표에 도달하려면 오른편의 파일이 존재해야 하는 의존 관계를 표현한 것이다. 앞서 변수 TARGET_DIR과 TARGET 이 각각 obj_dir과 Vpong_SbS 이었다. 따라서 ./obj_dir/Vpong_SbS 가 없다면 이를 만드는 규칙을 찾는다.
$(TARGET_DIR)/$(TARGET) : $(VERILOG_SRCS) $(SC_SRCS) $(SC_HDRS)
$(VERILATOR) --sc $(VL_WARNING) --trace --timing --pins-sc-uint \
--top-module $(TOP_MODULE) $(VERILOG_DEF) --exe --build \
$(VCFLAGS) $(VERILOG_SRCS) $(SC_SRCS)
목표 $(TARGET_DIR)/$(TARGET)는 콜론의 오른편에 놓은 파일들에 의존하고 있다. 의존관계에 있는 파일들을 재료로 삼아 왼편의 파일을 만드는 규칙은 바로 이어지는 명령에 따른다. 위의 예에서 각 변수를 앞서 선언한 변수들에 대입해 보면 결국 베릴레이터로 소스 파일들을 읽어 시뮬레이터를 빌드하는 긴 명령임을 알 수 있다. Makefile의 목표는 빌드 뿐만 아니라 다양한 규칙을 기술할 수 있다. 목표 run 은 ./obj_dir/Vpong_SbS 가 만들어 졌는지 확인하고 이를 실행 한다.
run : $(TARGET_DIR)/$(TARGET)
./$(TARGET_DIR)/$(TARGET)
시뮬레이션을 수행하여 얻은 VCD 파일을 디지털 파형 보기 유틸리티 gtkwave 로 보기위한 목표는 wave 에 작성 되었다.
gtkwave sc_$(TOP_MODULE)_TB.vcd --save=sc_$(TOP_MODULE)_TB.gtkw &
1-6. 실습
앞서 설명한 예제의 탁구대 그리기 베릴로그와 SystemC 테스트벤치 그리고 Makefile은 깃 허브 저장소[링크]에서 올려져 있다. 향후 합성으로 변환될 베릴로그와 테스트벤치 파일을 각각 다른 디렉토리에 용도별로 나누어 저장해 놓았다. 예제의 디렉토리 구조는 다음과 같다.
.
├── _Docs_
├── pong_SbS
│ └── pong_SbS.v
└── simulation
├── Makefile
├── sc_main.cpp
├── sc_pong_SbS_TB.cpp
├── sc_pong_SbS_TB.gtkw
└── sc_pong_SbS_TB.h
시뮬레이션 디렉토리로 이동하여 make를 실행하면 현재 디렉토리의 Makefile에 목표 all을 작성한 대로 다음과 같은 도움말을 출력한다.
$ cd ~/ETRI050_DesignKit/Projects/RTL/pong_SbS/01_Table/simulation
$ make
Makefile for Co-Simulation of Verilog-RTL example,
make run
make wave
make clean
CC BY-NC, by GoodKook, goodkook@gmail.com
리눅스 명령줄에 make를 실행하기 전에 특정 환경 변수를 별도로 넣을 수 있다. 예제의 Makefile 에 VCD_TRACE 변수 조건을 아래와 같이 두었다.
VCFLAGS += -CFLAGS -DVCD_TRACE_TEST_TB
VCFLAGS += -CFLAGS -DVCD_TRACE_DUT_VERILOG
endif
시뮬레이터를 빌드 하면서 VCD 파일 생성을 원치 않는다면 VCD_TRACE 변수를 NO로 해준다.
$ VCD_TRACE=NO make build
변수 VCD_TRACE 를 비워두고 시뮬레이터 빌드 한다.
$ make build
성공적으로 빌드가 이뤄지면 ./obj_dir 디렉토리에 실행파일이 만들어진다.
$ ls -l ./obj_dir/Vpong_SbS
-rwxr-xr-x 1 mychip mychip 4944776 Feb 28 19:03 ./obj_dir/Vpong_SbS
시뮬레이터를 실행한다.
$ make run
./obj_dir/Vpong_SbS
SystemC 3.0.2-Accellera --- Feb 5 2026 16:58:41
Copyright (c) 1996-2025 by all Contributors,
ALL RIGHTS RESERVED
Info: (I703) tracing timescale unit set: 100 ps (sc_pong_SbS_TB.vcd)
Info: /OSCI/SystemC: Simulation stopped by user.
SystemC 테스트벤치 sc_pong_SbS_TB.h 를 컴파일 할 때 VCD 파일을 생성하도록 C++ 컴파일러의 명령줄 옵션으로 -DVCD_TRACE_TEST_TB 와 -DVCD_TRACE_DUT_VERILOG가 전달 되었으므로 시뮬레이션의 결과가 VCD 파일에 기록된다.
$ ls -l *.vcd
-rw-r--r-- 1 mychip mychip 395818 Feb 28 19:05 sc_pong_SbS_TB.vcd
-rw-r--r-- 1 mychip mychip 294923 Feb 28 19:05 Vpong_SbS.vcd
디지털 파형을 보자.
$ make wave
탁구대의 128x64 개의 픽셀로 구성된 한 화면 분량의 시뮬레이션 결과다. 8192개에 이르는 클럭 clk 가 한 화면에 담겼다. 탁구대를 그리기 위한 베릴로그의 동작은 가로 좌표 x_pos 와 세로 좌표 y_pos 의 생성이다. 파형의 일부분을을 확대하여 카운터 동작을 살펴보자.
x_pos가 0에서 127까지 증가하며 가로 좌표를 출력하고 있다. x_pos=0 일때 세로 좌표 y_pos가 증가하는 모습을 볼 수 있다.
가로 좌표가 10에서 14 사이에 pixel이 1을 출력하여 화면에 점을 찍는다. 세로 좌표 값이 63 에 이르면 다시 0부터 카운트를 시작하여 화면 그림을 재생산한다. 래스터 스캔 방식의 그림 그리기 동작을 수행하고 있는 것을 알 수 있다.
탁구 게임기의 비디오 화면을 확인하기 위해 한 화면에 해당하는 8천여개의 클럭 신호를 일일이 확인하는 것은 불합리 하다. 디지털 회로의 시뮬레이션 결과를 설계 목적에 맞게 그래픽 화면으로 보면서 확인할 수 있는 방법을 찾아보자.



