2026년 3월 15일 일요일

[베릴로그 RTL 예제] 탁구 게임기 -6편: 탁구대, 움직이는 공 그리고 탁구채

[베릴로그 RTL 예제] 탁구 게임기 -6편: 탁구대, 움직이는 공 그리고 탁구채-

목차:

1. RTL 베릴로그로 "탁구대" 그리기
2. 그래픽 LCD 구동 칩의 시뮬레이션 모델
3. "탁구대"의 그래픽 테스트 벤치
4. 움직이는 탁구공
5. GLCD의 버스 기능 모델

6. 탁구대, 움직이는 공 그리고 탁구채

6-1. "탁구대"와 "움직이는 공" 시현
6-2. "탁구채"
6-3. 대화형 하드웨어 시뮬레이터
6-4. 실습 및 과제

----------------------------------------------------------------------------------------------------------------
오픈-소스 반도체 설계 도구가 설치된 "내 칩 디자인 킷"의 WSL 가상 디스크 이미지  [링크]
----------------------------------------------------------------------------------------------------------------

6. 탁구대, 움직이는 공 그리고 탁구채

알고리즘을 하드웨어로 구현하면서 얻는 가장 큰 장점을 꼽자면 낮은 속도(=긴 주기)의 클럭으로 높은 처리율(스루풋)을 얻을 수 있다는 점이다. 물론 단점 또한 만만치 않다. 장단점에 관하여 따로 논의해보기로 한다. 이에 덧붙여 병렬성은 매우 강력한 개발 수단이다. 소프트웨어에서 구현하기 어려운 병렬 처리를 단숨에 해결할 수 있다.

6-1. "탁구대"와 "움직이는 공" 시현

앞서 영상 데이터 시현을 위해 좌표의 생성을 디지털 카운터로 단숨에 해결 했었다. "탁구대" 그리기는 비교문 하나면 충분 했다. 탁구공을 시현하고 움직이기 위해 다소의 좌표변환(탁구공 이미지 좌표를 화면 좌표계 내로 변환)을 필요로 했지만 그래픽 알고리즘 중에서도 워낙 단순해서 그리 큰 어려움은 없었다. "탁구대"와 "공"을 생성하는 하드웨어는 그래픽 좌표 생성과 GLCD 인터페이스용 FSM을 공유하면서 독립적으로(또는 병렬로) 작동한다. 두 병렬처리 하드웨어에서 생성된 이미지를 한 화면에 합치려면 논리 합 연산 하나로 족하다.

"탁구대"와 "공"을 그리기 위한 화소 값을 각각 pixel_table 과 pixel_ball 로 선언하고 할당 한다.

    // Table --------------------------------------------------------
    wire pixel_table = ((x_pos>5) && (x_pos<15))? 1:0;

    // Ball ---------------------------------------------------------
    reg pixel_ball;
    always @*
        if ((x_ball<=x_pos) && ((x_ball+7)>=x_pos) &&
            (y_ball<=y_pos) && ((y_ball+7)>=y_pos))
            pixel_ball = rom_data[rom_bit];
        else
            pixel_ball = 0;

두 화소를 논리 합 연산 만으로 "탁구대"와 "움직이는 공"을 한화면에 시현할 수 있다. 여기에 약간의 기교를 부려 화소의 겹침을 보여주기 위해 배타적 논리합 연산으로 처리하였다.

    // Pixel --------------------------------------------------------
    assign pixel = pixel_table ^ pixel_ball;

예제를 실행 해보자. 소스 파일의 구성은 이전편에서 다뤘던 구성과 동일하다. 다만 베릴로그의 소스 파일 pong_SbS.v 에 탁구대와 공의 화소 겹침 연산 구문이 추가되었을 뿐이다.

    $ cd ~/ETRI050_DesignKit/Projects/RTL/pong_SbS/06_Table_Ball

    $ tree
    .
    ├── _Docs_
    ├── pong_SbS
    │   └── pong_SbS.v
    └── simulation
        ├── Makefile
        ├── sc_glcd128x64_TLM.cpp
        ├── sc_glcd128x64_TLM.h
        ├── sc_pong_SbS_TB.cpp
        ├── sc_pong_SbS_TB.h
        └── sc_main.cpp

    $ cd simulation

    $ make build

    $ make run

6-2. 탁구채

탁구 게임기를 구성하는 객체는 "탁구대", "공" 그리고 "채"다. "탁구공"은 내부의 규정(벽 또는 채에 부딪치면 반사)대로 움직이지만 "채"는 외부 버튼의 누름 여부에 따라 상하로 이동해야 한다. 버튼 입력이 추가된 베릴로그 모듈은 다음과 같다.

//
// Filename: pong_SbS.v
//

module pong_SbS(clk,reset,x_pos,y_pos,pixel,p_tick,busy,up,down);
    input           clk;
    input           reset;
    output [6:0]    x_pos;
    output [5:0]    y_pos;
    output          pixel;
    output          p_tick;
    input           busy;
    input           up;
    input           down;

    ......

endmodule

탁구채의 위치는 6비트 레지스터 paddle 이다. 화면이 갱신 될 때 버튼의 눌림을 탐지하여 채의 위치를 변경한다. 화면이 갱신 된다는 제어신호는 FSM에서 생성되는 v_sync 다.

    // Paddle Postion -----------------------------------------------
    reg [5:0]   paddle;

    always @(posedge clk or posedge reset)
    begin
        if (reset)
            paddle <= 0;
        else
        begin
            if (up && paddle > 0 && v_sync)
                paddle <= paddle - 1;
            if (down && paddle < 44 && v_sync)
                paddle <= paddle + 1;
        end
    end

탁구채의 위치에 맞춰 그려주는 화소 생성 구문은 다음과 같다. 채의 크기는 20으로 잡았다,

    // Paddle -------------------------------------------------------
    wire pixel_paddle;
    assign pixel_paddle = ((x_pos>122) && (y_pos>paddle) && (y_pos<(paddle+20)))? 1:0;

이제 "탁구 게임기"를 구성하는 세 객체를 합쳐 한 화면에 시현되도록 한다.

    // Pixel --------------------------------------------------------
    assign pixel = (pixel_table ^ pixel_ball) | pixel_paddle;

6-3. 대화형 하드웨어 시뮬레이터

게임기는 플레이어가 누르는 버튼에 따라 동적으로 작동하는 자동기계다. 플레이어가 버튼을 누르는 순간이 예정되어 있지 않다. 대화형 시뮬레이터를 제작하여 규칙에 맞춰 게임이 작동하는지 검증하기 로 한다. 이미 칩으로 제작된 하드웨어는 수정이 불가하다. 따라서 칩이 응용될 실제 상황에 접근된 검증이 이뤄져야 할 것이다. 앞서 SystemC로 작성했던 시현 장치(GLCD)의 시뮬레이션 모델에 키보드 입력을 받아 시험 입력으로 전달하는 스레드를 추가했다. 키보드 입력 스레드가 추가된 시스템 수준 모델의 외형은 다음과 같다.

//
// Filename: sc_glcd128x64_TLM.h
//

#ifndef _SC_GLCD128x64_TLM_H_
#define _SC_GLCD128x64_TLM_H_

#include <systemc.h>
#include <SDL2/SDL.h>

SC_MODULE(sc_glcd128x64_TLM)
{
    sc_in<bool>             reset;
    sc_in<sc_uint<7> >      x_pos;
    sc_in<sc_uint<6> >      y_pos;
    sc_in<bool>             pixel;
    sc_in<bool>             p_tick;
    sc_out<bool>            busy;

    sc_out<bool>            up;
    sc_out<bool>            down;

    void Display_Thread(void);
    void Button_Thread(void);

    // SDL2--------------------------
    ......

    SC_CTOR(sc_glcd128x64_TLM)
    {
        SC_THREAD(Display_Thread);
        sensitive << p_tick;

        SC_THREAD(Button_Thread);

        // SDL2--------------------------
        ......
    }

};

#endif

키보드 입력을 탐지하여 버튼 up 과 down 의 사건으로 출력하는 스레드 함수 Button_Thread()에 사건 감응이 지정될 필요 없다. 사건 감응 없이 일반적인 함수처럼 작동 하도록 기술한 경우 "시스템 수준" 모델이라 한다.

키보드 탐지 스레드 함수 Button_Thread()는 다음과 같다. 게임을 비록하여 멀티미디어 응용프로그램 제작용 오픈소스 라이브러리 SDL(Simple Directmedia Layer)가 사용되었다.

//
// Filename: sc_glcd128x64_TLM.cpp
//

#include <unistd.h>
#include "sc_glcd128x64_TLM.h"

void sc_glcd128x64_TLM::Button_Thread(void)
{
    SDL_Event event;
    bool quit = false;

    up.write(false);
    down.write(false);

    while(!quit)
    {
        if (SDL_PollEvent(&event))
        {
            switch (event.type)
            {
            case SDL_QUIT:
                quit = true;
                break;
            case SDL_KEYDOWN:
                switch( event.key.keysym.sym )
                {
                    case SDLK_UP:
                        up.write(true);
                        break;
                    case SDLK_DOWN:
                        down.write(true);
                        break;
                    case SDLK_r:
                        goto EXIT;
                        break;
                    default:
                        break;
                }
                break;
            case SDL_KEYUP:
                switch( event.key.keysym.sym )
                {
                    case SDLK_UP:
                        up.write(false);
                        break;
                    case SDLK_DOWN:
                        down.write(false);
                        break;
                    default:
                        break;
                }
                break;
            default:
                break;
            }
        }
        else
            wait(100, SC_NS);
    }

    EXIT:
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    sc_stop();
}

void sc_glcd128x64_TLM::Display_Thread(void)
{
    ......
}

키보드 눌림 SDL_KEYDOWN 을 탐지하여 윗 화살표 키 SDLK_UP 이면 up 출력에 true를 내보낸다. 아래 화살표 키 SDLK_DOWN 이면 down 출력에 true를 내보낸다. 키보드 뗌 SDL_KEYUP 이 탐지되면 해당 출력에 false 를 내보낸다. SDL의 키보드 사건이 없다면 wait(100, SC_NS)로 쓰레드가 시뮬레이터를 점유하는 시간을 미루고 실행을 양보한다. SystemC의 하드웨어 시뮬레이션 커널은 비 선점형 다중처리(Non-Preemptive Multi-Processing)다. 

6-4. 실습 및 과제

예제의 시뮬레이터 빌드와 실행은 모두 make 유틸리티로 수행한다. 소스 파일의 구성은 앞장에서와 동일하다. "탁구채"를 묘사하기 위해 베릴로그와 키보드 입력이 추가된 "시스템수준" 테스트벤치가 변경 되었다.

    $ cd ~/ETRI050_DesignKit/Projects/RTL/pong_SbS/07_Table_Ball_Paddle

    $ tree
    .
    ├── _Docs_
    ├── pong_SbS
    │   └── pong_SbS.v
    └── simulation
        ├── Makefile
        ├── sc_glcd128x64_TLM.cpp
        ├── sc_glcd128x64_TLM.h
        ├── sc_pong_SbS_TB.cpp
        ├── sc_pong_SbS_TB.h
        └── sc_main.cpp

a. 실습

시뮬레이터를 빌드하고 실행,

    $ make build

    $ make run

탁구 게임기를 구성하는 세 객체가 모두 시현되는 화면을 보게될 것이다. 키보드 입력에 반응하여 탁구채가 이동한다.

b. 과제

[과제1] 예제의 소스로 보여주는 "탁구 게임기"는 미완성이다. 다양한 게임의 규칙이 적용되도록 베릴로그 설계를 변경해 보자. 예를들어 공이 탁구채의 상단 혹은 하단에 부딪칠 경우 공의 속도가 바뀌면 게임이 한층 재미를 더할 수 있다.

[과제2] "시스템 수준 모델링"을 공부해보자.

구글 인공지능에 "시스템 수준 모델링"을 문의[링크]하면 이렇게 답한다.

"시스템 수준 모델링(System-Level Modeling)은 복잡한 시스템의 구성 요소, 상호작용, 행위를 추상화하여 전체적인 관점에서 기능과 구조를 정의하는 과정입니다. 주로 초기 설계 단계에서 요구사항 분석, 아키텍처 설계, 빠른 시뮬레이션을 통해 시스템의 동작을 검증하고, 시제품 제작 전 비용과 시간을 절감하는 데 목적이 있습니다."

적용 분야로는,

"임베디드 시스템: 하드웨어와 소프트웨어의 통합 동작 검증."
"시스템 아키텍처: 논리적, 물리적 구성을 정의하여 시스템 인터페이스 설계."

위의 인공지능 답변을 근거로 "탁구 게임기"의 테스트벤치가 "시스템 수준"에서 작성된 측면에 대하여 논하라.


[베릴로그 RTL 예제] 탁구 게임기 -5편: GLCD의 버스 기능 모델-

[베릴로그 RTL 예제] 탁구 게임기 -5편: GLCD의 버스 기능 모델 -

목차:

1. RTL 베릴로그로 "탁구대" 그리기
2. 그래픽 LCD 구동 칩의 시뮬레이션 모델
3. "탁구대"의 그래픽 테스트 벤치
4. 움직이는 탁구공

5. GLCD의 버스 기능 모델

5-1. 버스 기능 모델
5-2. BFM 수준으로 작성된 GLCD 모델
5-3. 실습 및 과제

----------------------------------------------------------------------------------------------------------------
오픈-소스 반도체 설계 도구가 설치된 "내 칩 디자인 킷"의 WSL 가상 디스크 이미지  [링크]
----------------------------------------------------------------------------------------------------------------

5. GLCD의 버스 기능 모델

디지털 파형을 보며 그래픽 게임기를 설계하기는 불가능에 가깝다. 앞의 편에서 RTL 베릴로그로 작성한 설계를 검증하기 위해 그래픽 LCD의 시뮬레이션 모델을 제작하고 이를 테스트벤치에 활용하였다. 하지만 명색이 비디오 게임기 인데 제아무리 시뮬레이션이라지만 실행 속도가 너무 느렸다.

5-1. 버스 기능 모델

처리율과 클럭율이 각각 다른 두 모듈 사이에 정보전달에 핸드쉐이크 기법이 사용되었다. 이(인터페이스)를 검증하기 위해 양 모듈 모두 동등한 추상화 수준에서 기술하고 시뮬레이션 했지만 속도가 느리다. 사건 구동 시뮬레이션에서 속도를 올리려면 사건 발생을 줄여야 한다. 설계 대상은 RTL 이지만 시뮬레이션 모델의 내부 동작 역시 이에 준하는 추상화 수준일 필요는 없다. 두 모듈 사이의 인터페이스 동작(핸드 쉐이크)가 확인된 이상 그래픽 LCD 모델의 추상화 수준을 높임으로써 시뮬레이션 속도를 올려보기로 한다. "버스 기능 모델(BFM, Bus Functional Model)"을 구글 인공지능으로 찾아보면 다음과 같이 답해주었다[링크].

정의: 트랜잭션 검증 모델(TVM)이라고도 하며, 컴퓨터나 집적 회로(IC) 설계에서 외부 버스와 통신하는 구성 요소를 검증하기 위한 합성 불가능한 소프트웨어 모델 입니다. 하드웨어 시뮬레이션에서 버스 프로토콜(예: AMBA, PCIe)의 트랜잭션 수준 동작을 모방하는 검증 모델입니다.

목적: RTL(Register Transfer Level) 설계가 완료되기 전에, 상위 수준에서 기능 검증을 수행하기 위해 사용합니다. 즉, 실제 하드웨어의 동작을 소프트웨어적으로 시뮬레이션하여 데이터 전송, 인터럽트 등 버스 기반 동작을 미리 테스트합니다.

구조: 일반적인 버스 프로토콜의 신호 수준 동작을 모델링하여 복잡한 로직을 대신 수행합니다.

활용: 트랜잭션 검증 모델(TVM)이라고도 불리며, 복잡한 SoC(System on Chip) 설계에서 버스 인터페이스의 기능적인 정확성을 확인하는 데 필수적입니다.

5-2. BFM 수준으로 작성된 GLCD 모델

GLCD 모델이 바로 "합성 불가능한 소프트웨어 모델"이다. BFM 수준으로 GLCD 모델을 작성해보자. "탁구 게임기"의 RTL과 GLCD 사이에 전송이 이뤄지는 버스 인터페이스 신호를 살펴보자. GLCD 상에 그림을 그릴 정보는 좌표 x_pos, y_pos와 픽셀 값 pixel이며 p_tick 과 busy로 두 모듈 사이의 핸드쉐이크 동작이 이뤄진다. 두 모듈 사이에 연결된 신호들과 전송규약을 묶어 "버스"라 한다.

GLCD 모델에서 좌표를 받아 그림을 그리는 절차를 RTL 보다 높은 추상화 수준을 변경해 주도록 한다. 먼저 "탁구 게임기" RTL 과 연결되는 GLCD의 외형에는 큰 변화 없다. GLCD 내부에 적용된 클럭이 빠져 있는 점에 주목한다. 디지털 회로의 RTL에서 벗어났음을 의미한다.

//
// Filename: sc_glcd128x64_TLM.h
//

#ifndef _SC_GLCD128x64_TLM_H_
#define _SC_GLCD128x64_TLM_H_

#include <systemc.h>
#include <SDL2/SDL.h>

SC_MODULE(sc_glcd128x64_TLM)
{
    sc_in<bool>             reset;
    sc_in<sc_uint<7> >      x_pos;
    sc_in<sc_uint<6> >      y_pos;
    sc_in<bool>             pixel;
    sc_in<bool>             p_tick;
    sc_out<bool>            busy;

    void Display_Thread(void);

    // SDL2--------------------------
    SDL_Window* window;
    SDL_Renderer* renderer;
    SDL_Event event;

    SC_CTOR(sc_glcd128x64_TLM)
    {
        SC_THREAD(Display_Thread);
        sensitive << p_tick;

        // SDL2--------------------------
        window = NULL;
        renderer = NULL;
        if (SDL_Init(SDL_INIT_VIDEO) < 0)
            return;

        window = SDL_CreateWindow("SDL2 Window",
                              SDL_WINDOWPOS_UNDEFINED,
                              SDL_WINDOWPOS_UNDEFINED,
                              128, 64,
                              SDL_WINDOW_SHOWN);

        if (!window)
        {
            SDL_Quit();
            return;
        }

        SDL_SetWindowTitle(window, "GLCD 128x64");
        SDL_SetWindowResizable(window, SDL_FALSE);

        renderer = SDL_CreateRenderer(window, -1,
                                SDL_RENDERER_ACCELERATED);
    }
};

#endif

그림을 그리는 사건 구동함수 Display_Thread() 가 단지 p_tick에 감응 지정되었다. 사건구동함수는 다음과 같다. RTL 수준의 동작이 모두 배제 되었으므로 매우 단순하다. 핸드쉐이크를 위해 p_tick에 반응하여 busy 신호를 내고 있을 뿐이다. 정작 점을 찍는 동작은 사건 구동에 무관하다.

//
// Filename: sc_glcd128x64_TLM.cpp
//

#include <unistd.h>
#include "sc_glcd128x64_TLM.h"

void sc_glcd128x64_TLM::Display_Thread(void)
{
    int x, y;

    busy.write(false);

    while(true)
    {
        // SDL QUIT event
        if (SDL_PollEvent(&event) && (event.type == SDL_QUIT))
        {
            SDL_DestroyRenderer(renderer);
            SDL_DestroyWindow(window);
            SDL_Quit();
            sc_stop();
        }

        wait(p_tick.posedge_event());

        busy.write(true);

        wait(p_tick.negedge_event());

        x = x_pos.read();
        y = y_pos.read();
        if (pixel.read())
            SDL_SetRenderDrawColor(renderer, 255, 255, 255,
                            SDL_ALPHA_OPAQUE);
        else
            SDL_SetRenderDrawColor(renderer,0,0,0,SDL_ALPHA_OPAQUE);

        SDL_RenderDrawPoint(renderer, x, y);

        if (x==127 && y==63)
            SDL_RenderPresent(renderer);                

        busy.write(false);
    }
}

5-3. 실습 및 과제

버스 기능 모델 GLCD가 적용된 "빠른 탁구공"의 실습 예제 소스 구성은 다음과 같다.

    $cd ~/ETRI050_DesignKit/Projects/RTL/pong_SbS/05_Ball_Fast_GLCD

    $ tree
    .
    ├── _Docs_
    ├── pong_SbS
    │   └── pong_SbS.v
    └── simulation
        ├── Makefile
        ├── sc_glcd128x64_TLM.h
        ├── sc_glcd128x64_TLM.cpp
        ├── sc_pong_SbS_TB.h
        ├── sc_pong_SbS_TB.cpp
        └── sc_main.cpp

a. 실습

예제의 시뮬레이터 빌드와 실행은 모두 make 유틸리티로 수행한다. 소스 파일의 구성은 앞장에서와 동일하므로 Makefile의 변화는 없다.

    $ make build

시뮬레이터를 실행해보자.

    $ make run

빠르게 움직이는 "탁구공"을 보게될 것이다. 파형을 보면 핸드쉐이크가 일어난 후 점을 찍기까지 GLCD 모델 내에서 사건발생이 일어나지 않는다.

    $ make wave

"탁구공" RTL의 클럭이 상승 사건이 발생하면 델타 싸이클 내에 핸드쉐이크가 완료되는 모습을 볼 수 있다.

b. 과제

[과제1] RTL의 입력 busy와 출력 p_tick 가 clk의 델타 싸이클 내에 변화하는 모습이 시뮬레이션으로 얻은 디지털 파형에 나타나고 있지 않은점에 유의해야 한다. 사건 구동 시뮬레이션에서 "델타 싸이클"의 의미에 대하여 토론해 보자.

[과제2] 하드웨어 설계 자동화 도구(EDA Tool)는 반도체 관련 산업의 중요한 축이다. 소프트웨어로 하드웨어를 모사하는 사건 구동 시뮬레이터의 작동에 대하여 이해해보자. 아래의 관련문서(디자인 킷에 포함되어 있음)가 도움이 될 것이다.

    "C++ 템플릿 크래스와 SystemC의 최소한 이해"[링크]

[과제3] 불필요한 rom_bit의 계산을 최소화 할 수 있다.

    reg [2:0]  rom_bit;

    always @*
        if ((x_ball<=x_pos) && ((x_ball+7)>=x_pos) &&
            (y_ball<=y_pos) && ((y_ball+7)>=y_pos))
        begin
            rom_bit = x_pos - x_ball;
            pixel = rom_data[rom_bit];
        end
        else
        begin
            rom_bit = 0;
            pixel = 0;
        end

움직이는 "탁구공" 생성 RTL에서 이미지 출력부분을 위와 같이 변경 한후 시뮬레이션 출력 파형을 검토해보라. 아울러 계산 최소화로 얻는 잇점도 있으나 단점에 대해서도 논의해 보자.


2026년 3월 14일 토요일

[베릴로그 RTL 예제] 탁구 게임기 -4편: 움직이는 탁구공-

[베릴로그 RTL 예제] 탁구 게임기 -4편: 움직이는 탁구공-

목차:

1. RTL 베릴로그로 "탁구대" 그리기
2. 그래픽 LCD 구동 칩의 시뮬레이션 모델
3. "탁구대"의 그래픽 테스트 벤치

4. 움직이는 탁구공

4-1. 탁구공 이미지 비트-맵
4-2. 임의 위치에 탁구공 그리기
4-3. 움직이는 탁구공
4-4. 실습 및 과제
----------------------------------------------------------------------------------------------------------------
오픈-소스 반도체 설계 도구가 설치된 "내 칩 디자인 킷"의 WSL 가상 디스크 이미지  [링크]
----------------------------------------------------------------------------------------------------------------

4. 움직이는 탁구공

고정된 "탁구대" 그리기는 쉬웠다. 조금 난이도를 높여 움직이는 탁구공이다. 이 공은 사방의 벽에 부딪쳐 방향을 바꾼다. 단순한 비트-맵으로 탁구공 이미지를 임의의 좌표상에 표시할 것이다. 이는 흔히 문자 단말기(text terminal)에서 문자 이미지를 표시하는 기법과 같다.

4-1. 탁구공 이미지 비트-맵

탁구공 이미지의 비트-맵을 베릴로그의 case 문으로 기술하면 다음과 같다. 이는 비동기식 ROM 메모리를 기술하는 기법이기도 하다. 또는 값을 주고 출력을 얻어오는 룩-업 테이블(LUT, Look-Up Table)이라고 한다.

    // Ball Image ROM -----------------------------------------------
    reg  [7:0]  rom_data;
    always @*
    begin
        case(rom_addr)
            3'b000 :    rom_data = 8'b00111100; //   ****  
            3'b001 :    rom_data = 8'b01111110; //  ******
            3'b010 :    rom_data = 8'b11000011; // **    **
            3'b011 :    rom_data = 8'b11000011; // **    **
            3'b100 :    rom_data = 8'b11000011; // **    **
            3'b101 :    rom_data = 8'b11000011; // **    **
            3'b110 :    rom_data = 8'b01111110; //  ******
            3'b111 :    rom_data = 8'b00111100; //   ****
        endcase
    end

연산장치는 LUT로 구현 되기도 한다. 예를 들어 A+B=C 라고 할 경우 주소로 [A+B] 를 주고 해당 주소에 C 에 해당하는 값을 저장해 두면 바로 덧셈 연산기가 된다. LUT의 장점은 임의의 연산기를 쉽게 만들 수 있지만 경우에 따라 메모리 용량이 불필요하게 커질 수 있다는 점이다. 논리식에서 돈-캐어(Don't-Care) 조건을 적용하여 최적화를 수행할 있지만 고정된 비트폭을 가지는 입력을 가지게 되므로 논리식 최적화의 의미가 없다. 미리 하드웨어가 정해져 있는 프로그래머블(programmable 또는 configurable) 반도체(FPGA, PLA, ROM 등)에서 조합 논리식은 LUT 로 구현한다.

4-2. 임의 위치에 탁구공 그리기

임의의 그래픽 좌표 x_pos 와 y_pos에 탁구공 이미지를 그려보자. 그래픽 화면을 연속적으로 훓는 좌표가 탁구공 이미지 좌표 (x_ball, y_ball)에 이르면 이미지 비트를 표시한다.

이미지 ROM에서 읽은 8비트 데이타 중 조건에 맞는 픽셀 값을 얻어 화면에 출력하기를 베릴로그로 표현하면 다음과 같다. 모든 입력 신호가 always의 감응 리스트에 적용 하였다. 따라서 아래 구문의 always 구역에 감응되는 신호는 x_pos, y_pos, x_ball, y_ball 그리고 rom_bit와 rom_data다. 아울러 출력 pixel이 if ~ else 문에 모두 할당(완결된 if 문)되었다. 따라서 조합회로다.

    always @*
        if ((x_ball<=x_pos) && ((x_ball+7)>=x_pos) &&
            (y_ball<=y_pos) && ((y_ball+7)>=y_pos))
            pixel = rom_data[rom_bit];
        else
            pixel = 0;

탁구공 이미지가 담긴 ROM 의 주소와 데이터 내 비트 위치를 계산하는 구문은 단순하다. 그래픽 훓기 좌표에서 공의 위치를 빼면 표시할 공의 이미지 정보가 된다. 단순하지만 기하학에서 말하는 상대좌표 변환 알고리즘이다. 높은 추상화 수준의 컴퓨팅 언어를 사용하면 알고리즘을 용이하게 기술할 수 있다.

    // Ball rom address ---------------------------------------------
    wire [2:0]  rom_addr;
    assign rom_addr = y_pos-y_ball;

    // Ball rom bit-position ----------------------------------------
    wire [2:0]  rom_bit;
    assign rom_bit = x_pos - x_ball;


4-3. 움직이는 탁구공

탁구공의 죄표를 변경하여 움직임을 표현할 수 있다. 탁구공 좌표는 화면 훓기가 새로 시작될 때마다 갱신되어야 한다. 수직 축 좌표 y_pos 가 63에 이르면 새로 화면이 갱신 되었음을 표시하는 v_sync 를 제어기 FSM 에 추가한다.

    // FSM //////////////////////////////////////////////////////////
    reg [1:0]   State;
    parameter sWait  = 2'b01;
    parameter sPixel = 2'b10;

    reg v_sync;

    always @(posedge clk or posedge reset)
    begin
        if (reset)
        begin
            x_pos  <= 127;
            y_pos  <= 63;
            p_tick <= 0;
            v_sync <= 0;
            State <= sWait;
        end
        else
            case(State)
            sWait:
                begin
                    if (!busy)
                    begin
                        x_pos <= x_pos + 1;
                        if (x_pos==127)
                        begin
                            y_pos <= y_pos + 1;
                            if(y_pos==63)
                               v_sync <= 1;
                        end
                        p_tick <= 1'b1;
                        State <= sPixel;
                    end
                end
            sPixel:
                begin
                    v_sync <= 0;
                    if (busy)
                    begin
                        p_tick <= 1'b0;
                        State <= sWait;
                    end
                end
            default:
                State <= sWait;
            endcase
    end

화면 훓기가 새로 시작되기 전에 탁구공의 좌표를 수정한다. 벽에 부딪칠 때 좌표의 변위만큼 더하거나 빼주므로써 탁구공의 위치를 변경하면 새로 화면이 그려질 때마다 탁구공이 움직이는 것처럼 보일 것이다.

    // Update Ball position -----------------------------------------
    reg [6:0] x_ball;
    reg [5:0] y_ball;
    always @(posedge clk or posedge reset)
    begin
        if (reset)
        begin
            x_ball <= 0;
            y_ball <= 50;
        end
        else
        begin
            if (v_sync)
            begin
                if (sign_x) x_ball <= x_ball - 1;
                else        x_ball <= x_ball + 1;
                if (sign_y) y_ball <= y_ball - 1;
                else        y_ball <= y_ball + 1;
            end;
        end
    end

탁구공이 벽에 부딪쳤음을 아는 방법은 간단하다. 공의 죄표가 벽의 상하좌우 벽을 넘어가는지 알아내서 그에 따라 다음 화면 갱신때 변위 만큼 가감해준다. 공이 화면영역 내에 있는지 검사하는 베릴로그는 다음과 같다. 완결된 if~else 가 아니므로 sign_x와 sign_y는 1비트 플립플롭 레지스터로서 always 구역은 순차회로가 된다.

    reg sign_x;
    reg sign_y;

    always @(posedge clk or posedge reset)
    begin
        if (reset)
        begin
            sign_x <= 0;
            sign_y <= 0;
        end
        else if (v_sync)
        begin
            if (x_ball==119)    sign_x <= 1;
            else if (x_ball==0) sign_x <= 0;
            if (y_ball==55)     sign_y <= 1;
            else if (y_ball==0) sign_y <= 0;
        end
    end

4-4. 실습 및 과제

움직이는 "탁구공"의 실습 예제 소스 구성은 다음과 같다.

    $ cd ~/ETRI050_DesignKit/Projects/RTL/pong_SbS/04_Ball

    $ tree
    .
    ├── _Docs_
    │   └── 04_Ball.jpg
    ├── pong_SbS
    │   └── pong_SbS.v
    └── simulation
        ├── Makefile
        ├── glcd128x64_defs.h
        ├── sc_glcd128x64.h
        ├── sc_glcd128x64.cpp
        ├── sc_glcd128x64_TB.h
        ├── sc_glcd128x64_TB.cpp
        ├── sc_pong_SbS_TB.h
        ├── sc_pong_SbS_TB.cpp
        └── sc_main.cpp

a. 실습

예제의 시뮬레이터 빌드와 실행은 모두 make 유틸리티로 수행한다. 소스 파일의 구성은 앞장에서와 동일하므로 Makefile의 변화는 없다.

    $ ROTATE_SCREEN=YES make build

시뮬레이터를 실행해보자.

    $ make run


b. 과제

시뮬레이션 속도가 매우 느리다. 화면을 한번 갱신하는데 수초가 걸린다. 실습으로 얻은 파형을 관찰해 보라. 그리고 사건 구동 시뮬레이터(event-driven simulator)에서 수행 속도가 느린 이유에 대하여 토론해보자.

    $ make wave

처리 속도가 느리고 처리율(클럭 레이턴시)이 낮은 GLCD와 핸드쉐이크를 위해 "탁구공" 하드웨어가 여러 클럭 동안 대기하고 있다.

과제1]  시뮬레이션 속도는 곧 개발 지연을 초래한다. 정작 설계하려는 대상에 비해 시뮬레이션 테스트벤치 모델로 인하여 개발 시간이 오래 걸린다면 합리적일까? 이를 극복할 수 있는 방법으로 버스 기능 모델(BFM, Bus Functional Modeling)또는 트랜잭션 모델링(TLM, Transaction Level Modeling)기법이 동원된다. BFM과 TLM 에 대하여 공부해보자.

과제2] 시뮬레이션 파형을 관찰해 보면 불필요하게 rom_bit 가 계산되고 있다. 저전력 설계의 여지를 찾아보자.

"탁구공" 이미지 출력을 아래와 같이 두었을 때,

    assign pixel = rom_data[rom_bit];

그래픽 화면 가득 공을 그린다.

조건문을 써서 가득찬 공 중에서 한개만 골라냈지만 가감산 계산은 계속 이뤄지고 있음을 보여준다.

설계단계에서 저전력을 이루려면 불필요한 계산을 없애야 한다.

과제3] FPGA의 구조에서 LUT의 역할에 대하여 토론해 보자.

[출처]https://www.geeksforgeeks.org/digital-logic/programming-array-logic/

[출처] The HLS Book: Parallel Programing for FPGA , https://github.com/KastnerRG/pp4fpgas.git


---------------------------------------------------------------------------

<5편: GLCD의 버스 기능 모델>

2026년 3월 2일 월요일

[베릴로그 RTL 예제] 탁구 게임기 -3편: 탁구대의 그래픽 테스트 벤치-

[베릴로그 RTL 예제] 탁구 게임기 -3편: 탁구대의 그래픽 테스트 벤치-

목차:

1. RTL 베릴로그로 "탁구대" 그리기
2. 그래픽 LCD 구동 칩의 시뮬레이션 모델

3. "탁구대"의 그래픽 테스트 벤치
    3-1. 핸드 쉐이크
    3-2. RTL 베릴로그 "탁구대"
    3-3. 그래픽 LCD 인터페이스 모델
    3-4. 실습 및 과제

----------------------------------------------------------------------------------------------------------------
오픈-소스 반도체 설계 도구가 설치된 "내 칩 디자인 킷"의 WSL 가상 디스크 이미지  [링크]
----------------------------------------------------------------------------------------------------------------

3. "탁구대"의 그래픽 테스트 벤치

베릴로그로 기술한 설계의 시뮬레이션의 출력은 디지털 파형으로 기록되곤 한다. 멀티 미디어, 디지털 신호처리 같은 대량의 입출력을 취급하는 설계에서 클럭마다 일일이 확인하여 그 결과를 판단하기는 어렵다. "탁구 게임기"는 작은 규모지만 비디오 신호를 다룬다. 클럭 마다 생성되는 화소 정보를 2차원 그래픽 화면에 시현하므로써 효과적으로 비디오 게임기의 동작을 검증 할 수 있다. RTL 베릴로그로 기술한 "탁구대"를 그래픽 LCD를 갖춘 테스트벤치로 시뮬레이션 해보자.

3-1. 핸드 쉐이크

앞서 기술한 RTL "탁구 게임기"는 매 클럭 마다 그래픽 정보(좌표 및 화소값)를 출력한다. 이와는 달리 "그래픽 LCD"는 좌표를 받아 한 화소를 표시하기 위해 여러 클럭에 걸쳐 순차적으로 동작한다. 핸드쉐이크(hand-shake)는 서로다른 입출력 처리율(throughput)을 가진 두 장치 사이의 정보 전달을 위한 규약이다. 처리율이 낮은 외부 장치가 준비되었을 때 비로서 출력을 낼 수 있는 핸드쉐이크 동작을 디지털 회로로 구현하는 유용한 기법은 유한상태 머신(FSM, Finite State Machine)이다.

핸드 쉐이크 FSM은 그래픽 LCD에서 표시 절차가 진행 중 임을 표시하는 busy 가 0일 때  이 작동하여 p_tick으로 유효한 픽셀 값의 출력을 알린다. FSM의 상태도는 다음과 같다.

3-2. RTL 베릴로그 "탁구대"

그래픽 신호를 출력하는 "탁구 게임기"에 그래픽 LCD의 낮은 처리율을 맞추기 위한 인터페이스 FSM을 추가한 RTL 베릴로그는 다음과 같다.

- 화소의 좌표는 매 클럭 마다 카운트 하여 얻는 대신 LCD가 준비 되었을 때(busy=0) x_pos가 증가한다.
- 표시할 화소가 준비되었음을 알리기 위해 p_tick을 내보낸다.
- 그래픽 신호를 생성하는 "탁구대" 베릴로그 모듈과 그래픽 LCD 인터페이스 모듈 사이에 양방향 핸드쉐이크가 작동한다.

//
// Filename: pong_SbS.v
//

module pong_SbS(clk, reset, x_pos, y_pos, pixel, p_tick, busy);
input           clk;
input           reset;
output [6:0]    x_pos;
output [5:0]    y_pos;
output          pixel;
output          p_tick;
input           busy;

    reg [6:0]   x_pos;
    reg [5:0]   y_pos;
    reg         pixel;
    reg         p_tick;

    // FSM ////////////////////////
    reg [2:0]   State;
    parameter sWait  = 3'b001;
    parameter sPixel = 3'b010;

    always @(posedge clk or posedge reset)
    begin
        if (reset)
        begin
            x_pos <= 127;
            y_pos <= 63;
            p_tick <= 0;
            State <= sWait;
        end
        else
            case(State)
                sWait:
                begin
                    if (!busy)
                    begin
                        x_pos += 1;
                        if (x_pos==0)   y_pos += 1;
                        p_tick <= 1'b1;
                        State <= sPixel;
                    end
                end

                sPixel:
                begin
                    if (busy)
                    begin
                        p_tick <= 1'b0;
                        State <= sWait;
                    end
                end

                default:
                    State <= sWait;

            endcase
    end

    assign pixel = (x_pos>9 && x_pos<15)? 1'b1:1'b0;

endmodule

3-3. 그래픽 LCD 인터페이스 모델

레지스터 트랜스퍼 수준(RTL)으로 기술한 "탁구대"의 베릴로그 출력을 그림으로 표시해야 하므로 그래픽 LCD의 입출력 역시 이와 동일한 추상화 수준으로 변경 되어야 한다. 그래픽 LCD 시뮬레이션 모델 sc_glcd128x64 을 하위 모듈로 둔 RTL 인터페이스용 SystemC 모듈은 다음과 같다.

//
// Filename: sc_glcd128x64_TB.h
//

#include <systemc.h>

#include "sc_glcd128x64.h"      // GLCD
#include "glcd128x64_defs.h"

SC_MODULE(sc_glcd128x64_TB)
{
    sc_in<bool>             reset;
    sc_in<sc_uint<7> >      x_pos;
    sc_in<sc_uint<6> >      y_pos;
    sc_in<bool>             pixel;
    sc_in<bool>             p_tick;
    sc_out<bool>            busy;

    sc_clock                clk;

    sc_signal<bool>         RS; // Register Mode: Inst(L), Data(H)
    sc_signal<bool>         RW; // Read(H), Write(L)
    sc_signal<bool>         E;  // Enable @ Posedge
    sc_signal<sc_uint<8> >  DBi; // Data Bus (Input)
    sc_signal<sc_uint<8> >  DBo; // Data Bus (Output)
    sc_signal<bool>         CS1;    // Chip-Select #1
    sc_signal<bool>         CS2;    // Chip-Select #2
    sc_signal<bool>         RST;    // Reset(L)

    sc_glcd128x64*  u_sc_glcd128x64;

    void Test_Gen(void);

    SC_CTOR(sc_glcd128x64_TB):
                clk("clk", 305, SC_NS, 0.5, 0.0, SC_NS, false)

    {
        SC_THREAD(Test_Gen);
        sensitive << clk;

        // DUT
        u_sc_glcd128x64 = new sc_glcd128x64("u_sc_glcd128x64");
        u_sc_glcd128x64->RS(RS);    // Register Mode Select
        u_sc_glcd128x64->RW(RW);    // Read(H), Write(L)
        u_sc_glcd128x64->E(E);      // Enable @ Posedge
        u_sc_glcd128x64->DBi(DBi);  // Data Bus
        u_sc_glcd128x64->DBo(DBo);  // Data Bus
        u_sc_glcd128x64->CS1(CS1);  // Chip-Select #1
        u_sc_glcd128x64->CS2(CS2);  // Chip-Select #2
        u_sc_glcd128x64->RST(RST);  // Reset(L)
    }
};

콜백 함수 Test_Gen()은 sc_glcd128x64_TB.cpp 에 작성되었다. 앞서 그래픽 LCD 모델의 테스트 벤치[링크]는 콜백 함수에 사건 감응 신호 지정이 없었다. SC_THREAD 로 지정한 콜백 함수[링크]는 시간 대기 wait(100, SC_NS) 함수를 사용하여 LCD 구동 수순을 진행 했었다. 이와 달리, 추상화 수준을 "탁구대"의 RTL에 맞추기 위해 클럭의 상승 엣지 사건에 반응하여 그래픽 LCD 시뮬레이션 모델을 구동하는 신호 생성 수순을 진행한다.

//
// Filename: sc_glcd128x64_TB.cpp
//

#include <unistd.h>
#include "sc_glcd128x64_TB.h"

#define SET_INST(_CS1_,_CS2_,_INST_)    \
{                                       \
    RS.write(false);                    \
    RW.write(false);                    \
    ......
    DBi.write(_INST_);                  \
    wait(clk.posedge_event());          \
    E.write(true);                      \
    wait(clk.posedge_event());          \
    E.write(false);                     \
    wait(clk.posedge_event());          \
}

#define SET_DATA(_CS1_,_CS2_,_DATA_)    \
{                                       \
    RS.write(true);                     \
    RW.write(false);                    \
    ......
    DBi.write(_DATA_);                  \
    wait(clk.posedge_event());          \
    E.write(true);                      \
    wait(clk.posedge_event());          \
    E.write(false);                     \
    wait(clk.posedge_event());          \
}

#define GET_DATA(_CS1_,_CS2_,_DATA_)    \
{                                       \
    RS.write(true);                     \
    RW.write(true);                     \
    CS1.write(_CS1_? false:true);       \
    CS2.write(_CS2_? false:true);       \
    wait(clk.posedge_event());          \
    E.write(true);                      \
    wait(clk.posedge_event());          \
    E.write(false);                     \
    wait(clk.posedge_event());          \
    _DATA_ = DBo.read();                \
}

#define SET_PIXEL(_X_,_Y_,_01_)         \
{                                       \
    SET_INST(true, true, INST_SET_Z_ADDRESS|0x00) \
    SET_INST((_Y_<64? true:false), (_Y_>63? true:false), INST_SET_Y_ADDRESS|(_Y_%64)) \
    SET_INST((_Y_<64? true:false), (_Y_>63? true:false), INST_SET_X_ADDRESS|(_X_/8)) \
    sc_uint<8>  _GD_DATA_; \
    GET_DATA((_Y_<64? true:false), (_Y_>63? true:false), _GD_DATA_) \
    if (_01_) _GD_DATA_ |=  (0x01<<(x%8)); \
    else      _GD_DATA_ &= ~(0x01<<(x%8)); \
    SET_INST((_Y_<64? true:false), (_Y_>63? true:false), INST_SET_Y_ADDRESS|(_Y_%64)) \
    SET_DATA((_Y_<64? true:false), (_Y_>63? true:false), _GD_DATA_) \
}

void sc_glcd128x64_TB::Test_Gen(void)
{
    int x, y;

    while(true)
    {
        wait(clk.posedge_event());
        if (reset.read())
        {
            // Reset Sequence
            busy.write(true);
            ......
            busy.write(false);
            continue;
        }

        if (p_tick.read())
            ......
    }
}

RTL "탁구대"에서 좌표와 화소 값이 준비되었다는 p_tick 를 받게되면 busy를 올려 LCD 구동 수순이 진행 되고 있음을 표시한다. 화소 표시 절차를 마치면 busy를 내려 핸드쉐이크를 마친다.

void sc_glcd128x64_TB::Test_Gen(void)
{
    int x, y;

    while(true)
    {
        wait(clk.posedge_event());
        if (reset.read())
        {
            ......
            continue;
        }

        if (p_tick.read())
        {
            busy.write(true);
            y = x_pos.read();
            x = y_pos.read();
            if (pixel.read())
                SET_PIXEL( x, y, true)
            else
                SET_PIXEL( x, y, false)
           busy.write(false);
        }
    }
}

콜백 함수 Test_Gen()을 SC_THREAD 로 지정하면서 클럭 객체 clk 의 사건에 감응 시켰다. 그래픽 LCD 모듈을 구동하는 클럭은 "탁구대"모듈과 동기되지 않는다. "탁구대"와 "그래픽 LCD"는 서로 다른 입출력 처리율을 가질 뿐만 아니라 클럭율(주기)도 서로 독립적이다. 처리율(throughput)은 물론 클럭율(clock rate)도 상이한 두 디지털 장치 사이의 통신에 핸드쉐이크(handshake)가 필요하다.

3-4. 실습

베릴로그로 작성한 "탁구대"의 출력을 그래픽 LCD 시뮬레이션 모델 테스트벤치에 물려 RTL 시뮬레이션한다. 시뮬레이션 테스트벤치의 파일 구성은 다음과 같다.

    $ cd ~/ETRI050_DesignKit/Projects/RTL/pong_SbS/03_TableDraw

    $ tree
    .
    ├── pong_SbS
    │   └── pong_SbS.v
    └── simulation
        ├── Makefile
        ├── glcd128x64_defs.h
        ├── sc_glcd128x64.cpp
        ├── sc_glcd128x64.h
        ├── sc_glcd128x64_TB.cpp
        ├── sc_glcd128x64_TB.h
        ├── sc_pong_SbS_TB.cpp
        ├── sc_pong_SbS_TB.h
        └── sc_main.cpp

a. 따라하기

실습을 위한 테스트 벤치와 Makefile이 준비되어있는 예제 시뮬레이션 디렉토리로 이동한다.

    $ pwd

    ~/ETRI050_DesignKit/Projects/RTL/pong_SbS/03_TableDraw/simulation

    $ make

        Makefile for Co-Simulation of Verilog-RTL example, pong_SbS

        TOP_MODULE=pong_SbS make lint
        TOP_MODULE=pong_SbS VCD_TRACE=[YES]|NO ROTATE_SCREEN=YES|[NO] make build
        make run
        make wave
        make clean

모델을 시험해 볼 수 있는 테스트벤치와 함께 시뮬레이터를 빌드한다.

    $ ROTATE_SCREEN=YES make build

        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 \
                    -CFLAGS -DVCD_TRACE_GLCD \
                    -CFLAGS -DROTATE_SCREEN \
                    -LDFLAGS -lm \
                    -LDFLAGS -lSDL2 \
                    ../pong_SbS/pong_SbS.v \
                    ./sc_main.cpp \
                    ./sc_glcd128x64.cpp \
                    ./sc_glcd128x64_TB.cpp \
                    ./sc_pong_SbS_TB.cpp

시뮬레이션 모델 테스트벤치를 실행 한다.

    $ make run

시뮬레이션을 그림으로 확인 할 수 있다. 디지털 파형을 보면 아래와 같다.

    $ make wave

b. 과제

예제 디렉토리에 시뮬레이션 모델과 테스트벤치 그리고 Makefile 까지 모두 준비되어 있다. 단지 따라해보기를 넘어 소스 코드를 읽고 토론을 통해 모델링과 테스트에 활용된 기법들을 이해하기 바란다. 클럭 주기와 처리율이 상이한 두 디지털 장치 사이의 핸드쉐이크의 필요성에 대하여 토론해 보자. 아울러 디지털 회로의 FSM을 베릴로그 RTL로 기술한 기법을 이해한다. 베릴로그로 RTL과 추상화 수준을 맞추기 위해 SystemC로 작성한 시뮬레이션 모델이 변경된 사항을 비교 설명해 보자.

핸드 쉐이크 시작:

핸드 쉐이크 종료:


----------------------------------------------------------------------------

<4편: 움직이는 탁구공>