[베릴로그 RTL 예제] 탁구 게임기 -2편: 그래픽 LCD 모델링-
목차:
2. 그래픽 LCD 구동 칩의 시뮬레이션 모델
2-1. 도트 매트릭스 그래픽 LCD 구동 칩
2-2. 인터페이스 프로토콜
2-3. 그래픽 데이터 메모리
2-4. SystemC 모델
a. 리셋 동작
b. 명령 또는 데이터 구분
c. 명령 해석
d. 그림 데이터 접근(읽기 또는 쓰기)
2-5. 실습
a. 따라하기
b. 과제
--------------------------------------------------------
2. 그래픽 LCD 구동 칩의 시뮬레이션 모델
비디오 게임기를 설계하려던 참이다. 화면은 가로 128, 세로 64개의 점을 찍을 수 있는 크기로 정했다. 한 화면은 총 8192개의 점으로 구성된다. 공과 패들이 움직이는 모습을 보여 주려면 초당 10개 화면이 만들어져야 한다. 간단해 보이지만 무려 초당 8만 1천여개(=128x64x10)의 점을 쏟아내는 하드웨어를 설계하는 것이 목표다. 게임기의 비디오 화면을 확인하기 위해 한 화면에 해당하는 8천여개의 클럭 신호를 일일이 확인하는 것은 불합리 하다. 앞서 화면 좌표 생성과 점을 찍는 기본 디지털 회로(카운터)를 구상해 봤었다. 그래픽 LCD 구동 칩의 시뮬레이션 모델을 만들어 베릴로그로 기술한 하드웨어로 그래픽 신호를 생성하고 이를 시현하려고 한다. 게임기 하드웨어는 아직 개발중이므로 디지털 회로의 시뮬레이션 결과를 그래픽 화면으로 보면서 확인할 수 있도록 그래픽 LCD 장치의 모델을 작성해보자.
SDL(Simple Direct Layer)은 컴퓨터의 멀티미디어 입출력 장치를 쉽게 다룰 수 있도록 공개된 오픈-소스 라이브러리다. C++ 로 작성되어 게임, 에뮬레이터, 오디오 재생 등 다양한 응용 프로그램에 널리 활용되고 있다.
도트 매트릭스 그래픽 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"
#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);
}
}
















