[베릴로그 RTL 예제] 탁구게임기
0. 개요
베릴로그 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까지 모두 세면 한 화면의 모두 그려진 셈이다. 가로 축 좌표 9에서 15 사이의 좌표점에 점을 찍어 탁구대 벽을 그리는 베릴로그 묘사는 다음과 같다.
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 <verilated_vcd_sc.h>
#endif
{
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_trace_file* fp; // VCD file
#endif
VerilatedVcdSc* tfp; // Verilator VCD
#endif
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);
// 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
// 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"
{
reset.write(true);
wait(clk.posedge_event());
wait(clk.posedge_event());
wait(clk.posedge_event());
reset.write(false);
{
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();
1-4. 시뮬레이터 빌드
1-5. Makefile
베릴로그 HDL

댓글 없음:
댓글 쓰기