목차:
0. 개요
1. 탁구대 벽 그리기
2. 움직이는 공
3. 탁구대와 움직이는 공
4. 탁구채
5. 과제
2. 움직이는 공
3. 탁구대와 움직이는 공
4. 탁구채
5. 과제
MyChip-on-MyDesk, "내 칩 제작 서비스" 오픈-소스 디자인 킷 사용자 그룹
--------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------
0. 개요
"남의 칩"으로 탁구 게임기를 제작해본다. 여기서 "남의 칩"은 AVR계열 마이크로 컨트롤러 ATmega328p다. "미니 오실로스코프[링크]"를 만들어 봤던 바로 그 칩이다. 마이크로 컨트롤러는 써넣는 코드(응용 프로그램)에 따라 기능이 바꿀 수 있는 "범용" 칩이다.
마이크로 컨트롤러가 범용이라지만 이 칩 하나로는 아무것도 못한다. 최소한 전원은 넣어 줘야 할 것 이다. USB 를 통하거나 전용 DC-DC 전압 변환기 칩을 통해 배터리로 5V 전원을 안정적으로 공급해 줘야한다. 전원 칩이 없으면 시스템 반도체도 무용지물이다. 전자 정보화 시대가 도래하던 무렵 "도란스 내려!"라고 외치던 패기는 이제 전원 칩 없으면 시스템 반도체(인공지능 반도체) "무쓸모"라는 구호로 옮겨왔다. "전원 칩"이 비메모리 반도체 이기는 하다. (MLCC는?) 이쨌든 범용 마이크로 컨트롤러에 전원과 크리스털 발진 소자(이상한 컨덴서) 그리고 커넥터를 붙여 놓은 "아두이노 프로 미니"보드[그림]를 활용키로 한다. 여기에 비디오 게임기를 만들려고 하는 만큼 시현장치도 있어야 한다[회로도]. 사용할 비디오 시현장치는 가로 128, 세로 64의 그래픽 OLED 모듈이다. 비디오 컨트롤러로는 SSD1306[가술문서]이다. 마이크로 컨트롤러와는 I2C 버스로 연결 되었다.
1. 탁구대 벽 그리기
1인 탁구 게임기의 상대가 되는 벽(wall)을 그려보자. 소스코드는 01_TableDraw.ino [링크]다.
그래픽 장치를 구동하기 위해 이미 만들어 놓은 다양한 라이브러리들을 활용하기로 한다. 각 라이브러리에 속한 함수(API, Application Programing Interface)들을 사용하려면 이들을 정의한 헤더 파일(header file)을 들여온다.
각 라이브러리들의 헤더 파일은 다음과 같다.
- Wire.h: I2C 장치와 통신에 필요한 라이브러리다.
- Adafruits_GFX.h: Adafruit 사에서 개발하여 공개하는 범용 그래픽 라이브러리다. 점찍기, 줄긋기, 사각형 그리기, 원 그리기, 비트맵 문자 표시 등 다양한 함수들을 제공한다. API 문서는 아래링크에서 받을 수 있다.
- DIYables_OLED_SSD1309.h: 특정 OLED 칩의 드라이버 라이브러리다. 수많은 개발자들에 의해 드라이버가 개발되어 공개됐다. 이 드라이버는 SSD1309(2.4"), SSD1315(0.96"), SSD1306(0.96") 등과 호환된다.
- EEPROM.h: ATmega328P 에 내장된 플래시 메모리는 프로그램 코드를 저장하는 공간이다. 이 영역을 고정 데이터로 사용하기 위한 라이브러리다.
사용할 그래픽 장치의 특성(가로세로 화소크기)과 버스의 주소등을 정의한 후 장치 구동 객체(크래스)를 사례화한다. 앞으로 그래픽 장치로 접근은 모두 이 객체를 통한다.
아듀이노의 개발환경에서 사용하는 코딩 언어는 C++ 다. C/C++ 언어의 시작은 main() 인데 이를 포함하여 각 마이크로 컨트롤러를 장착한 보드(모듈)에 맞게 기본 설정을 미리 해놓고 이를 스타트-업 라이브러리로 제공한다. 굳이 C/C++ 의 기준을 따르면 다음과 같다.
int main(int argc, char** argv)
{
// 보드 특성에 따른 초기화
setup(); // 사용자 초기화
while(true)
loop(); // 사용자 어플리케이션
return 0;
}
무한 반복이 사용된 이유는 아듀이노 개발환경의 용도가 별도 운영체제(저장소 및 메모리 관리)가 없는 소규모 컴퓨팅 환경을 목표로 하기 때문이다. main() 이 종료되면 갈곳이 없다!
사용자 초기화는 다음과 같다. 먼저 사용할 그래픽 장치를 초기화 한 후 시작 화면을 꾸미고 탁구대 벽을 그렸다.
고정된 탁구대를 그리고 나면 사용자 어플리케이션은 할일은 없다. 그저 LED만 깜빡인다. 그리고 그려놓은 탁구대를 그래픽 장치로 전송한다.
그래픽 장치는 매우 느리다. 컴퓨터의 시현장치(스크린 모니터)의 갱신율(refresh rate)이 보통 60Hz다. 이는 초당 겨우 60번 화면을 갱신 한다는 뜻이다. 수 기가 헤르츠의 속도로 동작하는 CPU의 논리회도에 비하면 터무니 없다. 전체 시스템의 성능이 가장 느린 장치에 종속되지 않도록 고속의 CPU와 속도가 느린 입출력 장치를 직접 붙이지 않고 버스를 통하면 어느정도 입출력 병목(IO device bottleneck)을 해소할 수 있다. 하지만 고속 디지털 회로들 사이의 연결(CPU과 메모리)과 저속 입출력 장치의 연결을 별도의 버스로 분리하는 것 만으로는 충분치 않다.
OLED 시현장치는 마이크로 컨트롤러의 직렬 버스(I2C) 제어기를 통하여 연결되었다. OLED 패널에 점을 찍으려면 CPU는 그래픽 램(GRAM)에 값을 써넣는다. 그래픽 컨트롤러(SSD1309)가 GRAM의 값을 그대로 OLED 패널에 시현한다. 문제는 그래픽 패널의 반응 속도가 매우 느리다는 점이다.
가로 폭 5, 높이 64인 탁구대 벽을 시현하려면 총 320개의 점을 찍어야 한다. OLED 그래픽 컨트롤러의 사양에 따르면 최대 화면 갱신율이 50Hz라고 하는데 약 20ms 가 소용된다. 결국 벽을 하나 그리는데 6.4초(~320x20ms)가 걸린다.
점하나 찍을 때마다 그래픽 컨트롤러의 화면을 갱신 시키는 방법은 매우 불합리하다. 그래픽 구동 소프트웨어들은 별도로 그림판 메모리(캔바스, Canvas)를 확보해둔다. 빠른 속도의 데이터 램에 확보한 캔바스에 한 화면 분량을 그림을 그려 놓고 이를 시현장치의 GRAM에 일괄적으로 전송한 후 OLED 패널을 갱신한다. 탁구대 벽을 그리는 API인 fillRect()는 데이터 램에 확보해둔 메모리에 그림 데이터를 채울 뿐이다. 이어 display() 를 호출 하여 시현 장치에 전송하면 OLED 컨트롤러는 화면 갱신을 수행한다.
빠른 데이터 메모리에 그리고 이를 일괄 전송하는 방식은 모든 컴퓨터 그래픽에 적용되는 기법이다. 예를 들어 파이썬의 matplotlib를 보자. 다음은 MNIST 데이터를 시현하는 파이썬 코드 plot_data.py의 일부다. 마지막 줄에 show()를 호출해야 비로서 그림이 화면에 나타난다.
import matplotlib.pyplot
all_values = data_list[1].split(',')
image_array = numpy.asfarray(all_values[1:]).reshape((28,28))
# Plot that array!
matplotlib.pyplot.imshow(image_array)
matplotlib.pyplot.show()
응용 프로그램의 첫 화면에 문자열을 출력하는 함수는 다음과 같다.
메모리 버퍼(캔바스라고 부르는)에 문자 이미지를 그려놓고 이를 시현하기 위해 display()를 호출하고 있다.
2. 움직이는 공
움직이는 탁구공을 그리고 움직여보자. 소스 파일은 02_Ball.ino [링크]다.
앞서 벽을 그리기 위해 일일이 점을 찍지 않고 안을 채운 사각형 그리기 API를 활용 했었다. 이번에는 작은 공을 비트맵 이미지를 미리 제작하고 이를 화면에 뿌린다. 이 방법은 화면에 문자 표시하는 방법과 같다. 컴퓨터 게임 그래픽에서 움직이는 캐릭터 이미지를 시현할 때 흔히 사용하는 비트 블릿(BitBlit)기법이다.
공의 비트맵 이미지는 다음과 같다. 공의 이미지는 변경되지 않을 것이므로 프로그램 롬 영역에 넣어둔다. 마이크로 컨트롤러의 제한적인 램 영역을 아낄 수 있다.
비트맵 이미지를 화면의 임의 위치에 그려넣는 API를 활용하여 공을 그려 넣는다. 이미지의 움직임을 고려하여 그리거나 지울 수 있도록 하였다.
이제 공을 움직여 보자. 이전에 그렸던 공을 지우고 새 좌표를 계산하여 그린다. 공이 화면 밖으로 나가지 않도록 움직이는 범위를 제한 하였다. 탁구대(화면 영역)에 부딪치면 공은 방향을 바꾼다.
과제: ++yPos와 yPos++의 차이에 대하여 프로그래밍 언어의 순차실행 구문의 관점에서 설명해 보라. 하드웨어 기술 언어(베릴로그)의 병렬 구문 실행과 비교해보자. 아울러 베릴로그 하드웨어 언어에서 단항 연산자 ++, -- 등을 둘 경우 그 위험성(?)을 설명해 보자.
3. 탁구대와 움직이는 공
앞서 움직이는 공은 탁구대의 벽을 감안하지 않았다. 벽을 다시 그리지 않아서 공이 지날 때 벽을 손상 시켰다.
공은 벽과 부딪치면 방향을 바꾸므로 움직이는 범위를 재조정 해야 한다. 공이 벽 위를 지날일이 없으므로 매번 벽을 다시 그릴 필요는 없다. 소스 파일은 03_Table_Ball.ino [링크]다.
고정된 탁구대 벽의 크기를 매크로(define macro)로 정의해 놓으면 소스 코드의 가독성(readability)과 재사용성(re-usablity)을 높인다.
4. 탁구채
탁구채(paddle)까지 넣어 게임기를 완성해보자. 소스 파일은 04_Table_Ball_Paddle.ino [링크]다. 탁구채는 가로축은 고정된채 버튼의 눌림에 따라 상하로 움직인다. 탁구채가 화면의 밖으로 벗어나지 않도록 세심하계 계산 되었다.
이어 공의 움직임을 묘사한다. 공이 벽에 부딪치거나 탁구채에 맞았을 때 방향을 바꾼다.
게임의 흥미를 돗우기 위해 점수를 메겨본다. 탁구채가 공을 맞추지 못하면 게임이 끝나고 점수를 보여준다.
움직이는 탁구채 그리기는 아래와 같다. 상하로 움직이므로 지우고 다시 그릴 수 있도록 하였다.
5. 과제
탁구 게임기를 "남의 칩" 마이크로 컨트롤러의 소프트웨어로 구현하였다. 아래의 내용을 토론해보자.
- 게임기의 행동을 프로그래밍 언어 C++로 기술하였다. 순차적으로 실행되는 구문만을 가지는 소프트웨어 언어와 병렬 실행이 추가된 하드웨어 언어의 차이를 토론해보자.
- 시현장치(CRT, 디스플레이 패널)를 다루는 설명에 흔히 언급되는 비월 주사(raster scan)에 대하여 알아보라. 화소점을 순차적으로 제어하기 위해 VSYNC, HSYNC 신호의 역활에 대하여 토론해보자.
- 위의 탁구 게임기에서 버그(소프트웨어 오류)를 찾아 수정하라.
- "내 칩"으로 탁구게임기를 설계할 참이다. 소프트웨어로도 충분히 구현 가능함에도 불구하고 굳이 전용 칩(하드웨어)이 필요한 이유에 대하여 토론해 보라.
