2026년 3월 14일 토요일

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

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

목차:

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

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_ball과 와 y_ball에 탁구공 이미지를 그려보자. 그래픽 화면을 연속적으로 훓는 좌표가 탁구공 이미지 좌표에 이르면 이미지 비트를 표시할 수 있다.


베릴로그로 표현하면 다음과 같다. 모든 입력 신호가 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;

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 가 아니므로 x_ball과 y_ball 은 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. 실습 및 과제

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


a. 실습

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

$ make build

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

$ make run


b. 과제

시뮬레이션 속도가 매우 느리다. 화면을 한번 갱신하는데 수초가 걸린다. 실습으로 얻은 파형을 관찰해 보라. 그리고 사건 구동 시뮬레이터(event-driven simulator)에서 수행 속도가 느린 이유에 대하여 토론해보자. 시뮬레이션 속도는 곧 개발 지연을 초래한다. 이를 극복할 수 있는 방법으로 버스 기능 모델(BFM, Bus Functional Modeling)또는 트랜잭션 모델링(TLM, Transaction Level Modeling)기법이 동원된다.

* 불필요하게 rom_bit 가 계산되고 있다. 저전력 설계의 여지를 토론해보자.



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로 작성한 시뮬레이션 모델이 변경된 사항을 비교 설명해 보자.

핸드 쉐이크 시작:

핸드 쉐이크 종료:



2026년 3월 1일 일요일

[베릴로그 RTL 예제] 탁구 게임기 -2편: 그래픽 LCD 모델링-

[베릴로그 RTL 예제] 탁구 게임기 -2편: 그래픽 LCD 모델링-

목차:

1. RTL 베릴로그로 탁구대 그리기

2. 그래픽 LCD 구동 칩의 시뮬레이션 모델
    2-1. 도트 매트릭스 그래픽 LCD 구동 칩
    2-2. 인터페이스 프로토콜
    2-3. 그래픽 데이터 메모리
    2-4. SystemC 모델
        a. 리셋 동작
        b. 명령 또는 데이터 구분
        c. 명령 해석
        d. 그림 데이터 접근(읽기 또는 쓰기)
    2-5. 실습
        a. 따라하기
        b. 과제

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

2. 그래픽 LCD 구동 칩의 시뮬레이션 모델

비디오 게임기를 설계하려던 참이다. 화면은 가로 128, 세로 64개의 점을 찍을 수 있는 크기로 정했다. 한 화면은 총 8192개의 점으로 구성된다. 공과 패들이 움직이는 모습을 보여 주려면 초당 10개 화면이 만들어져야 한다. 간단해 보이지만 무려 초당 8만 1천여개(=128x64x10)의 점을 쏟아내는 하드웨어를 설계하는 것이 목표다. 게임기의 비디오 화면을 확인하기 위해 한 화면에 해당하는 8천여개의 클럭 신호를 일일이 확인하는 것은 불합리 하다. 앞서 화면 좌표 생성과 점을 찍는 기본 디지털 회로(카운터)를 구상해 봤었다. 그래픽 LCD 구동 칩의 시뮬레이션 모델을 만들어 베릴로그로 기술한 하드웨어로 그래픽 신호를 생성하고 이를 시현하려고 한다. 게임기 하드웨어는 아직 개발중이므로 디지털 회로의 시뮬레이션 결과를 그래픽 화면으로 보면서 확인할 수 있도록 그래픽 LCD 장치의 모델을 작성해보자.

SDL(Simple Direct Layer)은 컴퓨터의 멀티미디어 입출력 장치를 쉽게 다룰 수 있도록 공개된 오픈-소스 라이브러리다. C++ 로 작성되어 게임, 에뮬레이터, 오디오 재생 등 다양한 응용 프로그램에 널리 활용되고 있다.

2-1. 도트 매트릭스 그래픽 LCD 구동 칩

도트 매트릭스 그래픽 LCD 는 가장 단순한 그래픽 표시 장치다. 내부의 메모리에 저장해 놓은 디지털 그림 데이터를 보고 가로와 세로로 화소를 배치한 LCD 패널에 점을 찍는다. KS0108은 도트 매트릭스 LCD 구동 칩으로 잘 알려져 있다. 1990년대에 삼성전자에서 출시한 이 칩은 단순 그래픽 LCD 구동 칩의 "사실상 표준"으로 지금도 여러 곳에서 생산 되고 있다[참고]. 이 칩을 채택한 LCD 장치를 구동하는 각종(아듀이노를 포함하여) 소프트웨어 라이브러리 코드들을 쉽게 구할 수 있지만 하드웨어 시뮬레이션 모델은 찾을 수 없어서 제작하기로 한다. 시뮬레이션 모델은 하드웨어로 제작하려는 목적이 아니므로 굳이 베릴로그로 작성할 필요없다. 더구나 그래픽을 시현할 LCD 패널의 모델링을 HDL로 작성하기는 불가능에 가깝다. 하드웨어의 행위는 C++의 크래스 라이브러리 SystemC로, LCD 그래픽 시현은 SDL을 활용하여 KS0108 칩[데이터쉬트]을 모델링 한다.

위 그림은 64x64 화소를 구동할 수 있는 KS0108 칩의 내부 구성을 보여준다. 128x64 화소를 가진 LCD 패널을 구동하기 위해  두개의 칩을 사용하고 있다. GLCD 모듈 [데이터 쉬트]의 내부 구성은 아래와 같다. 

2-2. 인터페이스 프로토콜

그래픽 LCD는 MPU(또는 CPU)등 디지털 계산 장치의 출력을 시현하는 주변장치다. MPU는 정해진 입출력 규격과 절차(protocol)에 따라 주변 장치에 접근하여 제어한다. GLCD 모듈의 사양서[데이터 쉬트]에 입출력 신호선의 용도를 다음과 같이 기술하고 있다. 전원(VDD, VSS, VEE)을 제외한 대부분 제어 핀들은 MPU로부터 입력되지만 DB0-DB7까지 8개의 데이터 핀은 양방향이다. MPU는 LCD 모듈 구동 칩 내에 그림 데이터를 저장하거나 읽어갈 수 있다. 

LCD에 그림이 나타나게 하려면 데이터 메모리(Display Data RAM)에 그림 데이터를 넣어 주어야 한다. 위의 핀 구성표를 보면 가로세로 위치를 지정하고 화소 값을 써넣기 위한 데이터 메모리의 주소 핀이 보이지 않는다. 입출력 핀의 수를 늘이면 전력 소모면에서 불리할 뿐만 아니라 주변 장치를 장착하기 위한 배선의 수가 증가한다. 이는 상품 제조 비용이 높아지는 등 시장성이 떨어진다. 더구나 빠른 데이터 전송이 필요하지 않은 사용자 인터페이스용 주변장치를 감안 하면 주소와 데이터 버스가 별도로 존재하는 고속의 인터페이스를 가질 필요 없다. 게다가 메모리에 접근은 어짜피 두 단계 순서를 거친다. 먼저 주소를 지정하고 읽기 또는 쓰기 제어 신호를 주어야 비로서 데이터 값을 참조할 수 있다.

그래픽 LCD 모듈에 내장된 데이터 메모리에 접근 하는 방법으로 데이터 버스 DB0-DB7의 용도를 주소와 데이터 용으로 겸한다. 데이터 버스에 실린 8비트 값이 주소를 지정하는지 또는 그림 데이터를 의미하는지 구분하기 위해 D/I 핀을 두고 있다. 위의 기능 설명에 따르면 D/I 핀이 0(L)일때 데이터 버스에 실린 값은 명령(또는 주소지정)이며 1(H)일때 그림 데이터다. CS는 두개의 구동 칩 중 하나를 선택한다. 그래픽 LCD 모듈의 제어 명령표는 아래와 같다. 그래픽 LCD 모듈의 RS 핀은 구동 칩의 D/I 에 해당한다.

그래픽 LCD를 구동하는 칩(KS0108)의 내부 구성에서 MPU와 인터페이스 부분은 디지털 회로다. 데이터 버스에 실린 값을 구동 칩의 내부 레지스터(또는 그래픽 메모리)에 저장하려면 클럭의 동기를 맞춰야 한다. 디지털 회로의 D-플립플롭에서 클럭에 해당하는 신호가 E 다. 그래픽 LCD 모듈의 입출력 타이밍은 다음과 같다.

MPU에서 GLCD로 데이터(또는 명령)을 써넣는 타이밍이다. R/W=L을 준 후 E 가 상승 엣지 일때 데이터 버스의 값을 구동 칩 내부의 레지스터에 저장한다.

MPU가 GLCD의 데이터를 읽는 타이밍이다. R/W=H로 준 후 E 가 상승 엣지 이면 데이터 버스에 구동 칩의 내부 값이 실리고 MPU는 이를 읽는다.

2-3. 그래픽 데이터 메모리

GLCD의 화면은 가로 128, 세로 64로 화소점의 수는 총 8192개다. 흑백 LCD 모듈 이므로 화소점 당 1비트가 할당된다. 임의의 위치에 점을 찍거나 지우기 위해 각 화소점마다 주소를 지정하려면 무려 13(가로 주소 비트 폭 7, 세로 주소 비트폭 6)의 선이 필요하다. 소요 선폭이 넓으면 회로가 복잡해지고 배선 소요가 많아 매우 불리하다. 선폭이 넓이는 디지털 회로의 큰 단점으로 꼽힌다. GLCD 모듈의 핀 설명서에 세로축 주소(Y Address)에 6비트를 할당 하지만 0에서 127까지 표현해야하는 가로축(X Address)으로는 단 3비트에 불과하다.

위의 설명에 따르면 가로 주소(X Address)를 지정하는 용도라면서 "페이지"라는 표현을 쓰고 있다. 화소점의 주소가 아니라는 것을 짐작할 수 있다. "페이지"는 8비트로 구성된 바이트(byte)를 의미한다. 페이지 당 8 비트 이므로 8 페이지는 64개의 화소점을 지정할 수 있다.  두개의 구동 칩을 사용하여 각각 64개씩 도합 128개 점을 표현할 수 있다. CS1=0, CS2=1 일때 0부터 63까지 가로축 화소를 지정하며 CS1=1, CS2=0 일때 64부터 127까지 가로축 화소 점을 지정한다.

흑백 GLCD의 그림 데이터 메모리 구조에서 임의의 화소점에 점을 찍으려면 가로축 주소(Y Address)와 페이지 주소(X Address) 준 후 써넣는 데이타(Data)의 비트 위치에 1 또는 0을 넣는다. 예를 들어 좌표 (10, 67)에 위치한 화소에 점을 찍으려면 CS1=1, CS2=0, Y Address=0x03, X Address=0x01에 Data=0x04를 써넣는다.

2-4. SystemC 모델링

그래픽 LCD의 시뮬레이션 모델을 SystemC로 작성해보자. 크래스 헤더 파일 sc_glcd128x64.h 에 모듈의 외형을 기술 한다. SystemC 모듈 크래스 명은 sc_gcld128x64 다. GLCD 모듈에 맞춰 입출력 포트를  기술하면 다음과 같다. 양방향 데이터 버스는 입력과 출력으로 분리하였다.

//
// Filanema: sc_glcd128x64.h
//

#ifndef _SC_GLCD128x64_H_
#define _SC_GLCD128x64_H_

#include <systemc.h>

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

    void Renderer_Method(void);

    SC_CTOR(sc_glcd128x64)
    {
        SC_METHOD(Renderer_Method);
        sensitive << E << RST;

        ......

    }

    ~sc_glcd128x64()
    {}
};

#endif

구성자 SC_CTOR()은 크래스와 동일한 이름을 가져야 한다. 구성자 내에 SC_METHOD 형식의 콜백 함수를 입력 신호 E 와 RST의 사건에 감응 되도록 지정하였다. 콜백 함수 Renderer_Method() 는 모듈 크래스의 소속 함수로 입력 인수와 되돌림은 허용되지 않는다. 콜백 함수를 크래스를 정의한 헤더 파일과 별도로 sc_glcd128x64.cpp 에 작성 했다.

a. 리셋 동작

GLCD 모듈의 사양서에 따라 입출력 핀의 기능에 맞춰 내부 행동의 기술 한다. 콜백 함수 Renderer_Method() 는 E 와 RST의 사건에 의하여 호출된다. 먼저 RST에 대한 행동은 아래와 같다. RST=0(L)일때 리셋이다. 모듈의 내부 기능을 수행하는 모든 변수들을 초기화 한다.

//
// Filename: sc_glcd128x64.cpp
//

#include "sc_glcd128x64.h"
#include "glcd128x64_defs.h"

void sc_glcd128x64::Renderer_Method(void)
{
    if (!RST.read())    // RESET
    {
        opWrite = false;
        opInst = false;
        opCS1 = opCS2 = false;
        bDisplay = false;
        x0_address = y0_address = z0_address = 0;
        x1_address = y1_address = z1_address = 0;
    }
    else if (RST.read())    // NON-RESET
    {
        ......
    }
}

리셋이 해제되면 E의 상승 엣지 일때 CS1, CS2, RW, RS 등 입력 핀 값을 받아 상태를 지역변수에 저장해 둔다.

    if (!RST.read())    // RESET
        ......
    else if (RST.read())    // NON-RESET
    {
        if (E.read())   // Pos-Edge of E
        {
            opWrite  = RW.read()?  false:true;
            opInst   = RS.read()?  false:true;
            opCS1    = CS1.read()? false:true;
            opCS2    = CS2.read()? false:true;
            bDisplay = RST.read()? true:false;
        }
        else if (!E.read()) // Neg-Edge of E
        {
            ......
        }
    }

b. 명령 또는 데이터 구분

E의 하강 엣지 일때 데이터 버스에 실린 값을 받아 이를 명령 또는 데이터로 구분하고 내부동작을 수행한다.  앞서 E의 상승 엣지 일때 RW=0(L), RS=0(L) 이었다면 MPU에서 명령을 내린 것이다. 데이터 버스에 실린 값은 명령이다.

    if (!RST.read())    // RESET

        ......
    else if (RST.read())    // NON-RESET
    {
        if (E.read())   // Pos-Edge of E
            ......
        else if (!E.read()) // Neg-Edge of E
        {
            if (opWrite)  // Write Operation
            {
                DataBus = DBi.read();
                if (opInst)  // Instruction Write
                {
                    ......
                }
                else if (!opInst) // Data Write
                {
                    ......
                }
            }
            ......
        }
    }

c. 명령 해석

데이터 입력 버스 DBi 의 값을 명령으로 취하여 이를 해석하고 해당 동작을 수행한다. 데이터 입력 버스의 값에서 상위 두 비트의 의해 명령이 구분된다. 

해석한 X Address와 Y Address 설정 명령에 따라 8비트 데이터 버스에서 각각 하위 3비트 또는 6비트를 취하여 그림 데이터 메모리 주소로 저장해 두었다.

    if (opWrite)  // Write Operation
    {
        DataBus = DBi.read();
        if (opInst)  // Instruction Write
        {
            switch(DataBus & 0xC0)
            {
                case (INST_DISPLAY & 0xC0):
                    if (!DataBus[0])    // DISPLAY OFF
                        bDisplay = false;
                    else                // DISPLAY ON
                        bDisplay = true;
                        Renderer();
                        break;
                case (INST_SET_Y_ADDRESS & 0xC0):  // Set Y_ADDRESS
                    if (opCS1)
                        y0_address = DataBus.range(5,0);
                    if (opCS2)
                        y1_address = DataBus.range(5,0);
                    break;
                case (INST_SET_X_ADDRESS & 0xC0):  // Set X_ADDRESS
                    if (opCS1)
                        x0_address = DataBus.range(2,0);
                    if (opCS2)
                        x1_address = DataBus.range(2,0);
                    break;
                case (INST_SET_Z_ADDRESS & 0xC0):  // Set Z_ADDRESS
                    if (opCS1)
                        z0_address = DataBus.range(5,0);
                    if (opCS2)
                        z1_address = DataBus.range(5,0);
                    break;
                default:    break;
            }
        }
        else if (!opInst) // Data Write
            ......
    }

d. 그림 데이터 접근(읽기 또는 쓰기)

E의 상승 엣지에서 읽어둔 RS 값이 1(H)이면 데이터 입력 버스 DBi 의 값은 LCD 에 시현될 그림 데이터다.

앞서 명령 해석 단계에서 X와 Y 주소를 지역변수로 저장해 두었다. 해당 주소의 메모리에 데이터 입력 버스 DBi 값을 저장(쓰기)하거나 메모리에서 읽어 데이터 출력 버스 DBo로 내보낸다.

    if (opWrite)  // Write Operation
    {
        DataBus DBi.read();
        if (opInst)  // Instruction Write
            ......
        else if (!opInst) // Data Write
        {
            if (opCS1)
            {
                gMemory[0][x0_address][y0_address] = DataBus;
                y0_address++;
            }
            if (opCS2)
            {
                gMemory[1][x1_address][y1_address] = DataBus;
                y1_address++;
            }
            if (opCS1 || opCS2) // Render Screen when Display Memory written!
                Renderer();
        }
    }
    else if (!opWrite)  // Read Operation
    {
        if (opCS1)
        {
            DBo.write(gMemory[0][x0_address][y0_address]);
            y0_address++;
        }
        if (opCS2)
        {
            DBo.write(gMemory[1][x1_address][y1_address]);
            y1_address++;
        }
    }

그림 데이터 메모리에 새 값이 쓰이게 되면 즉시 LCD 화면을 갱신하기 위해 Render() 함수를 호출한다. 이 함수는 그림 데이터 메모리 gMemory[2][][]에서 값을 읽어 SDL의 그래픽 라이브러리를 통해 화면에 그림을 그려준다.

void sc_glcd128x64::Renderer(void)
{
    if (!bDisplay)  // DISPLAY OFF
    {
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
        SDL_RenderClear(renderer);
        SDL_RenderPresent(renderer);
    }
    else if (bDisplay)  // DISPLAY ON
    {
        for(int x=0; x<64; x++)
        {
            for(int y=0; y<128; y++)
            {
                int cs    = y/64;
                int page  = x/8;
                int y_pos = y%64;
                int x_pos = x%8;

                if (gMemory[cs][page][y_pos] & (0x01<<x_pos))
                    SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
                else
                    SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);

                #ifdef ROTATE_SCREEN
                SDL_RenderDrawPoint(renderer, y, x);
                #else
                SDL_RenderDrawPoint(renderer, x, y);
                #endif
            }
        }
        SDL_RenderPresent(renderer);
    }
}

2-5. 실습

그래픽 LCD 구동 칩의 시뮬레이션 모델을 테스트 해본다. SystemC 모델과 시뮬레이션 테스트벤치의 파일 구성은 다음과 같다.

    $ cd ~/ETRI050_DesignKit/Projects/RTL/pong_SbS/02_glcd128x64

    $ tree
    .
    ├── _Docs_
    │   ├── glcd-128x64-datasheet.pdf
    │   └── KS0108B.pdf
    └── simulation
        ├── Makefile
        ├── glcd128x64_defs.h
        ├── sc_glcd128x64.h
        ├── sc_glcd128x64.cpp
        ├── sc_glcd128x64_TB.h
        ├── sc_glcd128x64_TB.cpp
        └── sc_main.cpp

a. 따라하기

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

    $ pwd

   ~/ETRI050_DesignKit/Projects/RTL/pong_SbS/02_glcd128x64/simulation

    $ make

    Makefile for sc_glcd128x64, SystemC model of "GLCD 128x64"

    TOP_MODULE=sc_glcd128x64 VCD_TRACE=YES|[NO] DEBUG_MSG=YES|[NO] ROTATE_SCREEN=YES|[NO] make build
    make run
    make wave
    make clean

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

    $ ROTATE_SCREEN=YES VCD_TRACE=YES make build

    clang++  -I/opt/systemc/include \
                -L/opt/systemc/lib \
                -o sc_glcd128x64_TB \
                 -DVCD_TRACE -DROTATE_SCREEN \
                ./sc_main.cpp \
                ./sc_glcd128x64.cpp \
                ./sc_glcd128x64_TB.cpp \
                -lSDL2 -lsystemc

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

    $ make run

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

    $ make wave

b. 과제

예제 디렉토리에 시뮬레이션 모델과 테스트벤치 그리고 Makefile 까지 모두 준비되어 있다. 단지 따라해보기를 넘어 소스 코드를 읽고 토론을 통해 모델링과 테스트에 활용된 기법들을 이해하기 바란다. 시뮬레이션 결과로 보여주는 SDL 그래픽 시현과 디지털 파형을 테스트벤치와 비교하며 설명해보라. GLCD 시뮬레이션 모델을 시험하는 테스트벤치에서 #define 매크로를 적극적으로 사용하고 있다. 특히 SET_PIXEL(_X_,_Y_,_01_) 매크로를 눈여겨 살펴보라. 임의의 화소점 좌표에 점을 찍기 위해 그래픽 데이터 메모리 쓰기 SET_DATA(_CS1_,_CS2_,_DATA_) 전에 먼저 읽기 GET_DATA(_CS1_,_CS2_,_DATA_)를 수행하는 이유를 설명할 수 있어야 그래픽 LCD 모듈의 동작을 제대로 이해했다고 할 수 있다.

//
// Filename: sc_glcd128x64_TB.cpp
//

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

#define SET_INST(_CS1_,_CS2_,_INST_)    \
{                                       \
    ......
}

#define SET_DATA(_CS1_,_CS2_,_DATA_)    \
{                                       \
    ......
}

#define GET_DATA(_CS1_,_CS2_,_DATA_)    \
{                                       \
    ......
}

#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)
{
    RS.write(false);    // Register Mode
    RW.write(true);     // Read(H), Write(L)
    E.write(false);     // Enable @ Posedge
    DBi.write(0x00);    // Data Bus
    CS1.write(true);    // Chip-Select #1
    CS2.write(true);    // Chip-Select #2
    RST.write(true);    // Reset
    wait(100, SC_NS);
    RST.write(false);   // Reset(L)
    wait(100, SC_NS);
    usleep(2000);
    RST.write(true);
    wait(100, SC_NS);
    usleep(1000);

    SET_INST(true,true,INST_DISPLAY|0x01) // DISPLAY ON

    while(true)
    {
        for(int x=0; x<64; x++)
            for(int y=0; y<128; y++)
                SET_PIXEL( x, y, true)
        for(int y=0; y<128; y++)
        {
            int x = (int)(31*sin(y*2*M_PI/128)+32);
            SET_PIXEL(x, y, false)
        }
        for(int y=0; y<128; y++)
        {
            int x = (int)(31*sin(y*2*M_PI/64)+32);
            SET_PIXEL(x, y, false)
        }
        for(int y=0; y<128; y++)
        {
            int x = (int)(31*sin(y*2*M_PI/32)+32);
            SET_PIXEL(x, y, false)
        }
        sleep(1);
        SET_INST(true, true, INST_DISPLAY)  // Display OFF
        sleep(1);
        SET_INST(true, true, INST_DISPLAY|0x01) // Display ON
        sleep(1);
        for(int y=0; y<128; y++)
            for(int x=0; x<64; x++)
                SET_PIXEL( x, y, false)
        for(int y=0; y<128; y++)
        {
            int x = (int)(31*sin(y*2*M_PI/128)+32);
            SET_PIXEL(x, y, true)
        }
        for(int y=0; y<128; y++)
        {
            int x = (int)(31*sin(y*2*M_PI/64)+32);
            SET_PIXEL(x, y, true)
        }
        for(int y=0; y<128; y++)
        {
            int x = (int)(31*sin(y*2*M_PI/32)+32);
            SET_PIXEL(x, y, true)
        }
        sleep(1);
        SET_INST(true, true, INST_DISPLAY)  // Display OFF
        sleep(1);
        SET_INST(true, true, INST_DISPLAY|0x01) // Display ON
        sleep(1);
    }
}

<3편: 탁구대의 그래픽 테스트 벤치>