frame buffer 이야기
Linux/Linux 일반 :
2009. 2. 7. 15:52
반응형
출처 : http://kelp.or.kr/korweblog/stories.php?story=02/11/09/8557035&topic=29
이규명님의 강좌
== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.
== 글을 쓴 동기
이곳 KELP 사이트 질문/답변 란에 가끔씩 올라오는 질문 중에 하나는 frame buffer 디바이스에 관련된 내용입니다. 이를 테면 StrongARM 보드에 LCD 드라이버를 다 올렸는데 그 LCD에 bmp와 같은 그림 파일을 읽어서 나타내고 싶은데 어떻게 하는지 모르겠다는 내용이죠. 그것에 대한 답변을 짧게 주고는 있으나 많은 사람들이 아직 잘 모를 것 같아서 정리해 보려고 합니다.
== frame buffer 란?
우선 모든 이야기에 앞서 Frame buffer가 무엇인지 알아야 하겠죠. Frame buffer란 linux 시스템에서 그래픽을 표현할 수 있는 하드웨어를 말합니다. 즉, PC라면 그래픽 카드, StrongARM 같으면 LCD controller를 frame buffer 장치라고 할 수 있습니다. 그 하드웨어를 user level application이 제어할 수 있도록 만들어진 device driver를 frame buffer driver라고 말할 수 있습니다. 그런데 마구 잡이로 만드는 device driver가 아니 application 작성자가 코딩을 할 수 있어야 하므로 어떠한 표준화된 interface를 가지고 있습니다. 사실 이 글의 내용은 그 interface를 배우는 것입니다.
(사실 frame buffer는 말 그대로 frame을 저장하고 있는 buffer를 의미합니다. Buffer가 무슨 뜻인지는 아실 테고, frame은 한 장의 그림을 나타낸다고 보시면 편합니다. 단지 여기서 말하는 frame은 LCD나 CRT등의 표시 장치의 화면에 출력될 그림입니다.)
== 글의 내용 및 구성
전체 글의 내용은 frame buffer 디바이스를 사용하여 화면에 이미지를 뿌리는 방법에 관한 것입니다. Frame buffer 디바이스 드라이버를 포팅하거나 하는 내용은 전혀 없습니다. 또한 모든 예제는 PC 기반의 linux system에서 수행할 수 있도록 작성되었습니다. 이곳 KELP 사이트가 embedded linux system을 주로 다루고 있지만, 특정 target(예를 들어 640x480x16bit LCD가 달려 있는 StrongARM 보드)에서 진행하지 않는 이유는 다음과 같습니다.
(1) PC 기반의 linux가 접하기 쉽다.
(2) 그런 보드를 가지고 있지 않다.
(3) 그런 target에서 글을 진행할 필요가 별로 없다.
(1)번과, (2)번은 쉽게 이해하겠죠. (3)에 대해서는 잠시 언급하도록 하겠습니다. 어짜피 우리는 linux라는 시스템에서 frame buffer 디바이스를 사용하는 프로그램을 작성합니다. 위에서 언급한대로 frame buffer driver는 어떤 표준화된 interface를 제공합니다. 따라서 어떤 linux 시스템에서 application을 작성하고 테스트하든지 큰 차이는 없습니다.
(사실 약간의 차이는 존재합니다. 지원하는 해상도나 픽셀당 비트수(bit per pixel) 등은 하드웨어 마다 다르죠. 하지만 현재 셋팅되어 있는 값을 읽어 올 수 있는 interface도 존재하고 지원하지 않는 해상도로 바꾸려고 했을 때 에러를 리턴한다거나 해서 어느 정도의 차이는 극복할 수 있습니다. 사실 이러한 차이는 모든 하드웨어에서도 마찬가지로 존재하죠. 예를 들어 Serial port가 9600bps까지 지원하는 하드웨어가 있을 수 있는 반면 그보다 빠른 속도로 동작할 수 있는 하드웨어도 존재하죠. 그렇다고 시리얼 프로그래밍의 방법이 바뀐다든지 하지 않는 것처럼 frame buffer driver 프로그래밍도 하드웨어마다 크게 바뀌지 않습니다.)
이번 글은 PC 기반의 linux 시스템에서 frame buffer device를 프로그래밍하기 위한 준비에 대한 글이고 다음 글부터 작성된 예제 소스코드를 살펴보고 직접 수행해 보는 형식으로 진행될 예정입니다.
== xdm을 끄기
PC linux 시스템이 부팅되고 나서 화면에 나타나는 로그인 메시지가 보이십니까? 그냥 text 기반의 메시지입니까? 아니면 그래픽 환경에서 로그인 메시지가 나옵니까? 대부분은 그래픽 환경에서 로그인 메시지가 보일 겁니다. 그 로그인 프로그램을 xdm(GNOME 환경은 gdm, KDE 환경은 kdm)이라고 하는데 그 프로그램은 X server를 이용하여 화면에 내용을 나타냅니다. 그런데 X server는 frame buffer device와 동일한 하드웨어, 즉 PC 상의 그래픽 카드를 사용하고 있기 때문에, frame buffer 프로그래밍을 위해서는 X server가 뜨지 말아야 합니다. 뭐 가장 간단한 방법으로 text 환경으로 나오려면 Ctrl+Alt+F2(2번째 virtual console)를 누르면 되지만 역시 X server가 back ground에서 수행되고 있는 상태이므로 이 정도로는 안됩니다. 그냥 부팅 당시부터 X server가 아예 뜨지 않도록 해야 합니다. 수단과 방법을 가리지 말고 X server가 뜨지 않게 만드시기 바랍니다.
그렇게 만드는 일이 어려운 분들이 있을까 봐 확실하고 원초적인(?) 방법을 공개합니다. 우선 /etc/inittab 파일을 editor로 여십시오.(물론 root권한이 있어야 바꿀 수 있으므로 root 권한 획득이 선행되어야 겠죠.) RedHat 기반의 linux 시스템(Wowlinux, Hancomlinux도 Redhat 기반일 겁니다.)이라면 다음과 같이 보일 겁니다.
=================================
<생략>
# Default runlevel. The runlevels used by RHS are:
# 0 - halt (Do NOT set initdefault to this)
# 1 - Single user mode
# 2 - Multiuser, without NFS (The same as 3, if you do not have networking)
# 3 - Full multiuser mode
# 4 - unused
# 5 - X11
# 6 - reboot (Do NOT set initdefault to this)
#
id:5:initdefault:
<생략>
==================================
그럼 주석 부분을 살펴보면 “5 – X11”라고 되어 있고, “3 – Full multiuser mode”라고 되어있는 것을 확인합니다. (이 말은 5번이면 X11을 수행하여 xdm을 수행하고 3 이면 보통 multiuser 모드로 만든다는 이야기입니다.) 그런 다음 “id:5:initdefault”를 “id:3:initdefault”로 바꾸십시오. 그 다음 파일을 저장하고 rebooting합니다. 이제 xdm이 뜨지 않고 그냥 text 기반의 login 메시지가 보입니까? 지금까지 한 일을 좀 있어 보이는(?) 말로 default run-level을 5에서 3으로 바꾸었다라고 표현할 수 있습니다. 부팅하고 나서 다시 xdm이 실행되도록 하고 싶으면 당연히 원래대로 바꾸면 됩니다.
(사실 그냥 run time에 run-level을 바꿀 수도 있습니다. telinit이라는 프로그램이 있는데 위와 같은 변환을 telinit 3이라고 하면 그냥 3번 run-level로 바꿀 수 있습니다. 당연히 다시 run-level 5로 바꾸려면 telinit 5를 하면 되겠죠.)
== frame buffer module 올리기
(혹시 부팅화면 중에 펭귄 로고가 보입니까? 그렇다면 linux에 이미 frame buffer device driver가 올라가 있는 상태이므로 이 부분을 건너 뛰시기 바랍니다.)
이제 frame buffer module을 올릴 차례입니다. 당연히 module 안에는 frame buffer device driver가 있겠죠. 우선 자신이 가지고 있는 그래픽 카드가 무엇인지 알아야 합니다. 혹 모르는 사람은 쉘 상에 다음과 같은 명령어를 쳐서 나오는 화면을 보고 잘 생각해서 알아냅니다.
$ cat /proc/pci
(당연히 $은 쉘 프롬프트이므로 타이핑을 하지 말아야겠죠.)
자 시스템에 설치되어 있는 그래픽 카드가 무엇인지 알았으면 다음 차례로 넘어가죠.
어떤 frame buffer module 들이 있나 살펴보려면 /lib/modules/커널버젼/kernel/drivers/video 아래(하위 디렉토리 포함) 있는 모든 object 파일들을 살펴보면 됩니다. RedHat 8.0에 기본적으로 설치되는 frame buffer module은 다음과 같습니다.
==================================
aty/atyfb.o
matrox/i2c-matroxfb.o
matrox/g450_pll.o
matrox/matroxfb_DAC1064.o
matrox/matroxfb_Ti3026.o
matrox/matroxfb_accel.o
matrox/matroxfb_base.o
matrox/matroxfb_crtc2.o
matrox/matroxfb_maven.o
matrox/matroxfb_misc.o
riva/rivafb.o
aty128fb.o
clgenfb.o
fbcon-hga.o
fbcon-mfb.o
fbgen.o
hgafb.o
sis/sisfb.o
sstfb.o
mdacon.o
neofb.o
pm2fb.o
pm3fb.o
radeonfb.o
tdfxfb.o
==================================
각 파일들이 어떤 그래픽 카드와 매칭되는지 알아야 겠죠.
aty/atyfb.o : ATI mach64와 3D Rage 칩 기반의 카드
matrox/*.o : Matrox Millenium I, II, Mystique, G100, G200, G400, G450 칩 기반의 카드
riva/rivafb.o : nVidia RIVA 128/TNT/TNT2, Geforce 1, 2, 3 칩 기반의 카드
aty128fb.o : ATI Rage128 칩 기반의 카드
clgenfb.o : Cirrus Logic 칩 기반의 카드
sis/sisfb.o : SiS 300/630/730/540/315/550/650/740 칩 기반의 카드
sstfb.o : VOODOO 1, 2 칩 기반의 카드
neofb.o : NeoMagic Framebuffer(무엇인지 모름)
pm2fb.o: Permedia2 칩 기반의 카드
pm3fb.o : Permedia3 칩 기반의 카드
radeonfb.o : ATI Radeon(8500까지) 칩 기반의 카드
tdfxfb.o : VOODOO 3, 5, Banshee 칩 기반의 카드
나머지 파일은 무시해도 좋습니다.
이제 어떤 파일이 각자의 시스템에 달려 있는 그래픽 카드에 해당되는지 알았죠. 혹 가지고 있는 카드가 위의 리스트에 없다면 다음에 나올 “지원하는 frame buffer 없음”을 읽으시길 바랍니다.
글쓴이는 ATI mach64 칩을 사용한 그래픽 카드를 가지고 있으므로 다음과 같은 명령어로 module을 올리면 됩니다.(당연히 root 권한이 있어야 되겠죠.)
$ modprobe atyfb
(directory와 확장자는 떼어내야 합니다. matrox는 object가 많은데 어쩌냐구요? 저도 모릅니다. Matrox 그래픽 카드가 달려있는 linux 시스템에 없으므로 해 볼 수도 없고, 암튼 잘 해보시길… )
각자의 시스템에서 시스템에 맞는 module을 올려 보길 바랍니다. 그럼 화면이 깜박할 겁니다. 그럼 frame buffer module이 올라갔다고 보시면 됩니다.
== 지원하는 frame buffer 없음
가지고 있는 그래픽 카드가 위에서 언급한 칩에 해당되지 않는다면 이제는 어쩔 수 없이 VESA frame buffer를 올려야 합니다. 아주 아주 오래된 그래픽 카드가 아니라면 VESA BIOS 2.0 호환 그래픽 카드일 겁니다. VESA BIOS 2.0 호환 그래픽 카드를 지원하는 frame buffer driver는 보통 linux kernel에 built-in되어 있습니다. Kernel argument로 “vga=옵션”을 넘겨 주어서 초기화시킬 수 있다고 합니다. 자세한 내용은 다음과 같은 문서를 참고하시기 바랍니다.
(1) kernel 소스(Document/svga.txt)
(2) kernel 소스(Document/fb/vesafb.txt)
(3) Linux Framebuffer HOWTO(http://www.tldp.org/HOWTO/Framebuffer-HOWTO.html)
당연히 kernel argument를 바꾸려면 사용하고 있는 로더(보통 LILO 혹은 Grub이겠죠.)의 매뉴얼을 참고하시기 바랍니다. vesafb는 화면 해상도와 픽셀당 bit수 등을 run-time에 바꿀 수 없으니 픽셀당 bit수는 16bpp(bit per pixel)로 세팅하시기 바랍니다. (640x480@16bpp 권장)
(ATI mach 64 카드도 VESA BIOS 2.0 호환 카드로 알고 있는데 “vga=옵션”으로 vesafb를 올리는 시도를 한 결과 잘 되지 않더군요. 카드에 문제가 있을 수도 있지만 커널 컴파일을 새로 해야 할 지도 모른다는 생각이 듭니다. 그 vga kernel argument를 처리하는 부분은 arch/i386/boot/video.S 파일인 것으로 보이는데 RedHat 8.0 커널에서 그 파일의 처음 부분을 보면 “#undef CONFIG_VIDEO_SVGA”라고 되어 있습니다. 그런데 640x480x16bpp는 Standard VGA에서 지원하는 해상도와 bpp가 아니므로(즉 SuperVGA에서만 지원하므로) 잘 안 되는 것 같다는 의심이 듭니다. 암튼 정 안되면 여러가지를 바꿔보며 kernel compile을 새로 해보는 길 밖에 없을 것 같습니다. 뭐 이 문제로 질문이나 항의(?)가 많이 들어온다면 직접 해볼 수도 있겠지만 kernel compile하면 initrd도 새로 만들고 module도 새로 만들어야 하는데 상당히 귀찮습니다.)
== 마치며
이번 글에서는 각 시스템에 맞는 frame buffer module을 올리는 일을 알아봤습니다. 이제 다음 시간부터는 직접 프로그래밍을 해 볼 예정입니다. 우선 frame buffer driver의 정보(해상도와 bpp)를 알아보고 바꾸는 프로그램을 작성해 볼 생각이고, 점을 찍거나 읽는 루틴은 그 다음 글에서 알아볼 예정입니다.
== 관련 문서 및 소스 코드
(1) kernel 소스(Document/svga.txt)
(2) kernel 소스(Document/fb/*.txt)
(3) kernel 소스(drivers/video/*)
(4) Linux Framebuffer HOWTO(http://www.tldp.org/HOWTO/Framebuffer-HOWTO.html)
== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.
== 시작하기 전에
전 글에서 frame buffer driver를 올리는 방법을 알아봤습니다. 이번 글에서는 frame buffer driver가 올라가 있는 시스템에서 frame buffer driver가 올라가 있는 상태에서 frame buffer device의 기본적인 정보를 살펴볼 수 있는 프로그램을 작성해보도록 하겠습니다. 우선 frame buffer device special 파일을 보도록 하죠. 다음과 같이 shell 상에 입력합니다.
$ ls –l /dev/fb
lrwxrwxrwx 1 root root 3 9월 8 03:15 /dev/fb -> fb0
다음과 같은 결과를 보입니다.(이 결과는 RedHat 7.3의 결과입니다.)
$ ls –l /dev/fb0
crw------- 1 root root 29, 0 4월 11 2002 /dev/fb0
결과를 보면 /dev/fb는 /dev/fb0의 symbolic link이고 /dev/fb0는 root만 쓰고 읽을 수 있는 character device임을 알 수 있습니다.(물론 permission은 바꿀 수 있습니다. 그러나 그냥 root만 읽고 쓸 수 있도록 놔두도록 하죠. 대신 /dev/fb를 여는 모든 프로그램은 root 권한이 있을 때만 수행해야 합니다.)
== fb_var_screeninfo, fb_fix_screeninfo
이제 직접 소스코드를 만날 차례입니다.
===============================
1 /*
2 * fbinfo.c : Frame buffer information viewer
3 *
4 * Copyright(C) 2002 holelee
5 *
6 */
7 #include <stdio.h>
8 #include <stdlib.h> /* for exit */
9 #include <unistd.h> /* for open/close .. */
10 #include <fcntl.h> /* for O_RDWR */
11 #include <sys/ioctl.h> /* for ioctl */
12 #include <linux/fb.h> /* for fb_var_screeninfo, FBIOGET_VSCREENINFO */
13 #define FBDEVFILE "/dev/fb"
14 int main()
15 {
16 int fbfd;
17 int ret;
18 struct fb_var_screeninfo fbvar;
19 struct fb_fix_screeninfo fbfix;
20 fbfd = open(FBDEVFILE, O_RDWR);
21 if(fbfd < 0)
22 {
23 perror("fbdev open");
24 exit(1);
25 }
26 ret = ioctl(fbfd, FBIOGET_VSCREENINFO, &fbvar);
27 if(ret < 0)
28 {
29 perror("fbdev ioctl(FSCREENINFO)");
30 exit(1);
31 }
32 ret = ioctl(fbfd, FBIOGET_FSCREENINFO, &fbfix);
33 if(ret < 0)
34 {
35 perror("fbdev ioctl(FSCREENINFO)");
36 exit(1);
37 }
38 printf("x-resolution : %d\n", fbvar.xres);
39 printf("y-resolution : %d\n", fbvar.yres);
40 printf("x-resolution(virtual) : %d\n", fbvar.xres_virtual);
41 printf("y-resolution(virtual) : %d\n", fbvar.yres_virtual);
42 printf("bpp : %d\n", fbvar.bits_per_pixel);
43 printf("length of frame buffer memory : %d\n", fbfix.smem_len);
44 close(fbfd);
45 exit(0);
46 return 0;
47 }
===============================
이 것을 모두 타이핑하고 컴파일하여 수행해 보시기 바랍니다.
우선 수행 중에 생길 수 있는 에러 메시지는 다음과 같습니다.
(1) fbdev open: Permission denied
=> root 권한이 없어서 생기는 메시지입니다.
(2) fbdev open: No such device
=> frame buffer driver가 제대로 올라가지 않아서 생기는 에러 메시지입니다.
혹 fbdev ioctl…어쩌구 하는 에러 메시지가 나온다면 그것은 frame buffer driver가 정상적인 동작을 하지 않아서 그런 것 같습니다.
수행시키면 다음과 같은 결과를 볼 수 있습니다.
x-resolution : 640
y-resolution : 480
x-resolution(virtual) : 640
y-resolution(virtual) : 480
bpp : 8
length of frame buffer memory : 8388608
(이 결과는 RedHat 7.3 Permedia2 8MB 그래픽 카드가 있는 시스템에서 보여주는 결과입니다.)
화면 해상도는 640x480이고 가상화면 해상도도 640x480입니다. 픽셀당 bits 수는 8이고 frame buffer memory의 크기는 8MB라고 해석할 수 있습니다. 가장 중요한 정보는 해상도하고 bpp정보입니다. 가상화면을 쓰는 경우가 있는지 모르겠지만 가상화면 정보는 혹시나 해서 출력해 보았고 Frame buffer memory 크기도 크게 중요한 부분은 아닙니다.
헤더 파일들을 간략히 살펴보면 unstd.h는 open/close 시스템 콜이 선언되어 있기 때문에 include한 것이고, fcntl.h는 open의 argument인 O_RDWR이 #define인 되어 있을 겁니다. Sys/ioctl.h는 ioctl 시스템 콜이 선언되어 있는 파일입니다. linux/fb.h가 제일 중요한 include 파일인데요, 이 내부에 fb_var_screeninfo 구조체, fb_fix_screeninfo 구조체, FBIOGET_VSCREENINFO, FBIOGET_FSCREENINFO 등이 정의되어 있습니다.
소스코드가 너무 짧기 때문에 설명할 내용도 없습니다. 단지 fb_var_screeninfo 정보를 얻어오기 위해 FBIOGET_VSCREENINFO ioctl을 이용하고 fb_fix_screeninfo 정보를 얻어오기 위해 FBIOGET_FSCREENINFO ioctl을 이용한다고 기억하시면 됩니다.
fb_fix_screeninfo는 frame buffer상에 고정(fix)된 정보입니다. fb_var_screeninfo는 frame buffer상에서 바꿀 수 있는 정보입니다. 즉 fb_var_screeninfo 안에 있는 정보, 예를 들어 해상도나 bpp는 사용자의 요구에 의해서 바꿀 수도 있습니다. 각각의 정의는 linux/fb.h에 있고 구조체의 멤버에 대해서 짤막한 설명이 들어 있으므로 살펴보시기 바랍니다.
== 16bpp로 바꾸기
다음의 소스코드를 살펴보도록 하죠.
===============================
1 /*
2 * fb16bpp.c : Change bpp of frame buffer to 16
3 *
4 * Copyright(C) 2002 holelee
5 *
6 */
7 #include <stdio.h>
8 #include <stdlib.h> /* for exit */
9 #include <unistd.h> /* for open/close .. */
10 #include <fcntl.h> /* for O_RDONLY */
11 #include <sys/ioctl.h> /* for ioctl */
12 #include <linux/fb.h> /* for fb_var_screeninfo, FBIOGET_VSCREENINFO */
13 #define FBDEVFILE "/dev/fb"
14 int main()
15 {
16 int fbfd;
17 int ret;
18 struct fb_var_screeninfo fbvar;
19 fbfd = open(FBDEVFILE, O_RDWR);
20 if(fbfd < 0)
21 {
22 perror("fbdev open");
23 exit(1);
24 }
25 ret = ioctl(fbfd, FBIOGET_VSCREENINFO, &fbvar);
26 if(ret < 0)
27 {
28 perror("fbdev ioctl(GET)");
29 exit(1);
30 }
31 fbvar.bits_per_pixel = 16;
32 ret = ioctl(fbfd, FBIOPUT_VSCREENINFO, &fbvar);
33 if(ret < 0)
34 {
35 perror("fbdev ioctl(PUT)");
36 exit(1);
37 }
38 close(fbfd);
39 exit(0);
40 return 0;
41 }
===============================
위의 소스코드는 fb_var_screeninfo를 읽어온 후에 bits_per_pixel 필드만 바꾼 후에 FBIOPUT_VSCREENINFO ioctl을 이용하여 kernel에 넘겨준 것입니다. 프로그램을 컴파일하여 수행할 때 혹시 생길 수 있는 에러 메시지는 위에서 언급된 두 가지 뿐 아니라 다음과 같은 메시지가 나올 수 있습니다.
fbdev ioctl(PUT): Invalid argument
이 메시지는 frame buffer device가 16 bpp로 바꿀 수 없다는 메시지입니다. 아무래도 하드웨어가 지원하는 해상도와 bpp에는 한계가 있을 수 있으므로 하드웨어에 따라서 달라집니다.
(RedHat 8.0 ATI mach 64 CT 2MB 시스템의 경우 에러가 나더군요. atyfb의 버그인 것으로 보입니다. fbinfo의 결과에서 yres_virtual이 엄청나게 이상한 값을 가지고 있고 따라서 그것을 다시 480으로 바꾼후에 FBIOPUT_VSCREENINFO를 해주면 문제가 해결됩니다.)
이 프로그램을 에러없이 수행하고 난 다음에 처음의 프로그램을 다시 수행하면 다음과 같은 결과를 볼 수 있습니다.
x-resolution : 640
y-resolution : 480
x-resolution(virtual) : 640
y-resolution(virtual) : 480
bpp : 16
length of frame buffer memory : 8388608
bpp가 제대로 바뀌었습니다. 나머지는 물론 변화가 없습니다. 이제 해상도를 800x600으로 바꾸는 프로그램도 작성할 수 있겠죠. 또 32bpp나 24bpp로 바꿀 수도 있겠죠.(단 하드웨어와 frame buffer driver가 지원만 한다면.)
== 마치며
이 이후부터 진행되는 모든 글에 있는 소스코드는 16bpp를 기준으로 작성할 예정입니다. 굳이 한정하는 이유는 모든 bpp를 지원할 수 있도록 하면 소스코드가 너무 길어지기도 하고 기타 등등의 이유가 있으므로 그렇게 하겠습니다. 요즘 PDA나 Handphone도 16bpp가 많이 있고 StrongARM에도 보통 16bpp LCD를 다는 추세이므로 16bpp가 크게 문제가 되리라 보지 않습니다. 다른 bpp의 경우는 모든 글이 완료되었을 때 각 bpp별 특성에 대해서 짤막한 글을 적도록 하겠습니다.
다음 글에서는 드뎌 “점찍기”를 해보도록 하겠습니다. 단 그것을 위한 준비로 이번 글에 있는 내용을 읽고 해상도는 관계 없이 frame buffer setting을 16bpp로 만들어 놓으시길 바랍니다.
== 참고 자료
fbset-2.1(http://home.tvd.be/cr26864/Linux/fbdev/)
=> 이 프로그램 컴파일 해서 수행해 보길 바랍니다.
== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.
== 시작
이번 글에서는 드디어 점(pixel) 찍기를 해보도록 하겠습니다. 점 찍기는 당연히 모든 그래픽을 표현하는 기본이 됩니다. 점을 어떻게 찍는 지만 알면 선이나 네모를 그리거나 bmp를 화면에 출력하는 일은 그냥 그것의 응용일 뿐입니다.(물론 선을 그리거나 원이나 호를 그리는 것은 약간의 알고리즘 지식이 필요하기는 합니다만 이 글의 목적상 그런 부분을 다루지는 않겠습니다.) 저번 글에서 언급한대로 16bpp환경에서 진행합니다.
글을 진행하기에 앞서 한가지 밝혀 둘 것이 있는데 이번 글에서 나오는 모든 소스는 좀 무식한(?) 방법으로 구현되었습니다. 좀더 깔끔하고 빠르게 동작하는 소스 코드는 글이 좀더 진행되면 만날 수 있을 겁니다.
== 16bpp에서 한 pixel 데이터
우선 한 pixel의 데이터가 어떤 식으로 표현되는지 이해하고 있어야 합니다. 당연히 한 pixel은 Red, Green, Blue(RGB)의 값을 가지고 있어야 합니다. Red=0,Green=0,Blue=0인 경우는 검정색이 되고 모두 maximum 값을 가지고 있으면 하얀색 점이 됩니다. 그럼 16bpp에서 RGB값은 묶여 있는 형식은 다음 그림(?)과 같습니다.
-------------------------------
| R(5bit) | G(6bit) | B(5bit) |
-------------------------------
MSB LSB
Red와 Blue는 각각 5비트씩이고 G는 6비트 정보를 가지게 됩니다. 그리고 MSB(Most Significant Bit)에서 LSB(Least Significant Bit)쪽으로 R, G, B 순으로 저장됩니다.
(이 부분에 다소 문제가 있음 나중에 밝혀졌습니다. 문제가 되는 부분은 "frame buffer 이야기(7)"에서 확인할 수 있습니다.
== 좌표계
사실 frame buffer 에서 좌표를 따진다는 것은 사실 별로 무의미할지도 모릅니다. 그러나 이미 fb_var_screeninfo에서 xres, yres라는 좌표형식이 등장하였고 앞으로 있을 설명의 편의상 그냥 좌표계를 정하겠습니다.
좌표의 origin은 당연히 화면 왼쪽 상단에 있고 좌우축은 x축에 해당하고 상하축은 y축에 해당한다고 사회적 통념상 무리가 없고 fb_var_screeninfo의 xres, yres의 결과와도 부합합니다. 좌표계를 그림(?)으로 표현하면 다음과 같겠죠.
화면 상단쪽
(0,0)...............(x,0)
.........................
.........................
...
.........................
.........................
(0,y)...............(x,y)
화면 하단쪽
== pixel 찍는 방법(?)
이번에 소개할 pixel을 찍는 방법은 아주 간단합니다. 그냥 device를 열고 write하면 pixel이 찍히게 됩니다. pixel을 원하는 위치에 찍기 위해서는 그냥 lseek 시스템 호출을 이용하여 offset을 바꾸면 됩니다. 640x480@16bpp frame buffer라고 하면 640*480*(16/8)개의 바이트가 들어있는 파일이라고 생각하면 쉽습니다. 좌표 (0, 0)에서부터 시작하여 (1, 0), (2, 0), (3, 0)의 순으로 (639, 0)까지 진행되다가 (0, 1)로 계속 되는 순서를 가지는 파일이라고 보면 딱 맞습니다. 그럼 pixel 찍는 예제 소스 코드를 살펴보도록 하죠.
== pixel 찍는 프로그램 소스코드
다시 한번 얘기하지만 좀 원초적인 방법으로 구현된 소스 코드입니다. 그냥 동작한다는 것이 의미가 있을 뿐 실제로 쓰기에는 조금 무리가 있는 소스 코드입니다.
16bpp가 아니면 실행이 되지 않도록 체크 코드가 들어 있습니다.
===============================
1 /*
2 * fbpixel.c : Frame buffer draw pixel example
3 *
4 * Copyright(C) 2002 holelee
5 *
6 */
7
8 #include <stdio.h>
9 #include <stdlib.h> /* for exit */
10 #include <unistd.h> /* for open/close .. */
11 #include <fcntl.h> /* for O_RDONLY */
12 #include <sys/ioctl.h> /* for ioctl */
13 #include <sys/types.h>
14 #include <linux/fb.h> /* for fb_var_screeninfo, FBIOGET_VSCREENINFO */
15
16 #define FBDEVFILE "/dev/fb"
17
18 typedef unsigned char ubyte;
19
20 unsigned short makepixel(ubyte r, ubyte g, ubyte b)
21 {
22 return (unsigned short)(((r>>3)<<11)|((g>>2)<<5)|(b>>3));
23 }
24
25 int main()
26 {
27 int fbfd;
28 int ret;
29 struct fb_var_screeninfo fbvar;
30 unsigned short pixel;
31 int offset;
32
33 fbfd = open(FBDEVFILE, O_RDWR);
34 if(fbfd < 0)
35 {
36 perror("fbdev open");
37 exit(1);
38 }
39
40 ret = ioctl(fbfd, FBIOGET_VSCREENINFO, &fbvar);
41 if(ret < 0)
42 {
43 perror("fbdev ioctl");
44 exit(1);
45 }
46
47 if(fbvar.bits_per_pixel != 16)
48 {
49 fprintf(stderr, "bpp is not 16\n");
50 exit(1);
51 }
52
53 /* red pixel @ (0,0) */
54 if(lseek(fbfd, 0, SEEK_SET) < 0)
55 {
56 perror("fbdev lseek");
57 exit(1);
58 }
59 pixel = makepixel(255,0,0); /* red pixel */
60 write(fbfd, &pixel, 2); /* write 2byte(16bit) */
61
62 /* green pixel @ (100,50) */
63 offset = 50*fbvar.xres*(16/8)+100*(16/8);
64 if(lseek(fbfd, offset, SEEK_SET) < 0)
65 {
66 perror("fbdev lseek");
67 exit(1);
68 }
69 pixel = makepixel(0,255,0); /* green pixel */
70 write(fbfd, &pixel, 2); /* write 2byte(16bit) */
71
72
73 /* blue pixel @ (50,100) */
74 offset = 100*fbvar.xres*(16/8)+50*(16/8);
75 if(lseek(fbfd, offset, SEEK_SET) < 0)
76 {
77 perror("fbdev lseek");
78 exit(1);
79 }
80 pixel = makepixel(0,0,255); /* blue pixel */
81 write(fbfd, &pixel, 2); /* write 2byte(16bit) */
82
83 /* white pixel @ (100,100) */
84 offset = 100*fbvar.xres*(16/8)+100*(16/8);
85 if(lseek(fbfd, offset, SEEK_SET) < 0)
86 {
87 perror("fbdev lseek");
88 exit(1);
89 }
90 pixel = makepixel(255,255,255); /* white pixel */
91 write(fbfd, &pixel, 2); /* write 2byte(16bit) */
92
93 close(fbfd);
94 exit(0);
95 return 0;
96 }
===============================
(0,0)에 빨간 pixel, (100, 50)에 녹색 pixel, (50, 100)에 파란 pixel, (100, 100)에 하얀 pixel을 순서대로 찍는 프로그램입니다. 실행시켜 보면 네 개의 점을 볼 수 있습니다. 간단히 설명하면 makepixel() 함수는 r,g,b 세 개의 byte값(0~255)을 받아서 16bit pixel 값을 만들어 내는 함수 입니다. 원하는 위치에 pixel을 찍기 위해 사용하는 방법은 단순히 file descriptor의 처음(SEEK_SET)에 해당 위치의 offset을 계산한 후 lseek()을 호출하여 file offset을 바꾸고 2 byte pixel 데이터를 write하는 것 뿐입니다. 당연히 offset은 바이트 단위로 계산합니다. (xpos, ypos)에 점을 찍고 싶으면 오프셋 계산 식은 다음과 같습니다.
offset = ypos*{한줄의바이트수} + xpos*{한픽셀당바이트수}
한픽셀당바이트수 = 16/8(16bpp이므로)
한줄의바이트수 = {한줄의픽셀수}*{한픽셀당바이트수} = xres*(16/8)
결국, offset = ypos*xres*(16/8)+xpos*(16/8) 이 됩니다.
그럼 원하는 위치에 pixel을 찍는 함수를 다음과 같이 구현해 볼 수 있겠습니다.
===============================
1: void put_pixel(struct fb_var_screeninfo *fbvar, int fbfd, int xpos, int ypos, unsigned short pixel)
2: {
3: int offset = ypos*fbvar->xres*(16/8)+xpos*(16/8);
4: lseek(fbfd, offset, SEEK_SET);
5: write(fbfd, &pixel, 2);
6: }
===============================
한 가지 기억해 둘 것은 한 개의 점을 원하는 위치에 찍기 위해서 두 개의 시스템 호출(lseek, write)을 해야 한다는 것입니다. 이것이 원초적이고 무식한(?) 이유입니다. 나중에 배울 새로운 점 찍기 루틴을 보고, 성능 비교를 해보면 왜 무식한지 알 수 있을 겁니다.
== pixel 읽기
화면에 있는 pixel을 읽으려면 어떻게 해야 할까요? 그냥 유추해 볼 수 있는 가장 간단한 방법은 똑같이 offset을 계산해서 write를 하지 않고 이번에는 read를 하는 것입니다. 뭐 마땅한 예제를 만들기가 힘들어서 넘어가지만 예상대로라면 제대로 pixel을 읽을 수 있을 겁니다.
== random number 찍기
혹 점이 몇 개 없어서 심심하게 생각하는 사람들을 고려해 서비스 차원에서 다음 소스코드를 작성했습니다. Ctrl+C를 누르면 중지하는 무한 루프입니다. 특별히 설명할 내용은 당연히 없습니다.
===============================
1 /*
2 * fbrandpixel.c : Frame buffer draw random pixel example
3 *
4 * Copyright(C) 2002 holelee
5 *
6 */
7
8 #include <stdio.h>
9 #include <stdlib.h> /* for exit */
10 #include <unistd.h> /* for open/close .. */
11 #include <fcntl.h> /* for O_RDONLY */
12 #include <sys/ioctl.h> /* for ioctl */
13 #include <sys/types.h>
14 #include <linux/fb.h> /* for fb_var_screeninfo, FBIOGET_VSCREENINFO */
15
16 #define FBDEVFILE "/dev/fb"
17
18 int main()
19 {
20 int fbfd;
21 int ret;
22 struct fb_var_screeninfo fbvar;
23
24 fbfd = open(FBDEVFILE, O_RDWR);
25 if(fbfd < 0)
26 {
27 perror("fbdev open");
28 exit(1);
29 }
30
31 ret = ioctl(fbfd, FBIOGET_VSCREENINFO, &fbvar);
32 if(ret < 0)
33 {
34 perror("fbdev ioctl");
35 exit(1);
36 }
37
38 if(fbvar.bits_per_pixel != 16)
39 {
40 fprintf(stderr, "bpp is not 16\n");
41 exit(1);
42 }
43
44 while(1)
45 {
46 int xpos, ypos;
47 int offset;
48 int rpixel;
49
50 /* random number between 0 and xres-1 */
51 xpos = (int)((fbvar.xres*1.0*rand())/(RAND_MAX+1.0));
52 /* random number between 0 and yres-1 */
53 ypos = (int)((fbvar.yres*1.0*rand())/(RAND_MAX+1.0));
54
55 offset = ypos*fbvar.xres*(16/8)+xpos*(16/8);
56
57 /* random number between 0 and 65535(2^16-1) */
58 rpixel = (int)(65536.0*rand()/(RAND_MAX+1.0));
59
60 if(lseek(fbfd, offset, SEEK_SET) < 0)
61 {
62 perror("fbdev lseek");
63 exit(1);
64 }
65 write(fbfd, &rpixel, 2); /* write 2byte(16bit) */
66 }
67
68 return 0;
69 }
===============================
== random 네모 그리기
똑같이 그냥 데모용 random 네모 그리기 소스 코드입니다.
===============================
1 /*
2 * fbrandrect.c : Frame buffer draw random rectangular example
3 *
4 * Copyright(C) 2002 holelee
5 *
6 */
7
8 #include <stdio.h>
9 #include <stdlib.h> /* for exit */
10 #include <unistd.h> /* for open/close .. */
11 #include <fcntl.h> /* for O_RDONLY */
12 #include <sys/ioctl.h> /* for ioctl */
13 #include <sys/types.h>
14 #include <linux/fb.h> /* for fb_var_screeninfo, FBIOGET_VSCREENINFO */
15
16 #define FBDEVFILE "/dev/fb"
17
18 int main()
19 {
20 int fbfd;
21 int ret;
22 struct fb_var_screeninfo fbvar;
23
24 fbfd = open(FBDEVFILE, O_RDWR);
25 if(fbfd < 0)
26 {
27 perror("fbdev open");
28 exit(1);
29 }
30
31 ret = ioctl(fbfd, FBIOGET_VSCREENINFO, &fbvar);
32 if(ret < 0)
33 {
34 perror("fbdev ioctl");
35 exit(1);
36 }
37
38 if(fbvar.bits_per_pixel != 16)
39 {
40 fprintf(stderr, "bpp is not 16\n");
41 exit(1);
42 }
43
44 while(1)
45 {
46 int xpos1, ypos1;
47 int xpos2, ypos2;
48 int offset;
49 int rpixel;
50 int t, tt;
51
52 /* random number between 0 and xres-1 */
53 xpos1 = (int)((fbvar.xres*1.0*rand())/(RAND_MAX+1.0));
54 xpos2 = (int)((fbvar.xres*1.0*rand())/(RAND_MAX+1.0));
55
56 /* random number between 0 and yres */
57 ypos1 = (int)((fbvar.yres*1.0*rand())/(RAND_MAX+1.0));
58 ypos2 = (int)((fbvar.yres*1.0*rand())/(RAND_MAX+1.0));
59
60 if(xpos1 > xpos2)
61 {
62 t = xpos1;
63 xpos1 = xpos2;
64 xpos2 = t;
65 }
66
67 if(ypos1 > ypos2)
68 {
69 t = ypos1;
70 ypos1 = ypos2;
71 ypos2 = t;
72 }
73
74 /* random number between 0 and 65535(2^16-1) */
75 rpixel = (int)(65536.0*rand()/(RAND_MAX+1.0));
76
77 for(t = ypos1; t <= ypos2; t++)
78 {
79 offset = t*fbvar.xres*(16/8)+xpos1*(16/8);
80
81 if(lseek(fbfd, offset, SEEK_SET) < 0)
82 {
83 perror("fbdev lseek");
84 exit(1);
85 }
86 for(tt = xpos1; tt <= xpos2; tt++)
87 write(fbfd, &rpixel, 2);
88 }
89 }
90
91 return 0;
92 }
===============================
== 마치며
이번 글에서는 16bpp 픽셀이 어떤 형식으로 구성되는 지 알아보았고 lseek, write를 이용하여 원하는 위치에 pixel을 찍는 법을 살펴보았습니다. lseek, read를 이용하면 원하는 위치의 pixel값을 읽어올 수도 있다는 예측도 해보았습니다. 다음 글에서는 mmap() 시스템 호출(system call)에 대해서 알아볼 예정입니다.
== 글을 올리고 나서
어떤 방식으로 글을 올리던지 이 KELP 사이트의 게시판에서는 소스코드가 이쁘게 나오지 않네요.
== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.
== 시작
저번 글에서는 다소 무식한(?) pixel 찍기를 해 보았습니다. 왜 무식한지는 나중에 따지도록 하고 전형적인 보통 파일 쓰기의 방법에 기초한 방법이었습니다. open하고 lseek으로 offset 조정하고 write로 값을 써 넣었습니다. 그런데 보통의 파일 쓰기의 경우 그냥 주르륵 써 넣기만 하지 쓸 때마다 매번 lseek으로 offset을 조정하거나 하는 것은 매우 드믄 경우가 됩니다. 이렇게 매번 lseek으로 offset을 조정하는 것은 상당히 프로그래밍을 하기가 피곤합니다. 좀더 좋은 방법이 없을까요? mmap 시스템 호출이라는 좋은 방법이 있습니다. 이번 글에서는 일반적인 mmap 시스템 호출에 대해서 알아보고 다음 글에서는 mmap을 frame buffer에 이용하는 법을 알아보도록 하겠습니다.
== mmap 시스템 호출
역사적 배경이야 잘 모르겠고, “파일 읽기 쓰기를 메모리 읽기 쓰기와 동일하게 할 수 없을까?”라는 소박한(?) 요구에 부응하는 것이 mmap 시스템 호출입니다. mmap 시스템 호출은 file descriptor로 표현(?)되는 object(C++도 아니니 객체로 번역하기도 껄끄럽고 그렇다고 컴파일할 때 나오는 object 코드를 이야기하는 것도 아닙니다. 그냥 개체)를 application의 virtual address상에 매핑(mapping)시켜주는 시스템 호출입니다. 그냥 쉽게 생각해서 파일을 열어서 mmap하면 read/write를 포인터로 할 수 있다고 알아두면 쉽습니다. 이러한 시스템 호출은 linux상에도 있고 Windows도 비슷한 일을 하는 시스템 호출이 있는 것으로 보아 많은 현대적인 OS에 존재하는 것 같습니다. 사용법을 알아보죠.
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
이것이 mmap의 man page에 나오는 mmap 시스템 호출의 prototype입니다. 우선 그냥 특별한 경우가 아니면 start는 0을 넣는다고 외우세요.(즉 일반적인 경우 크게 의미가 없는 argument입니다.) length는 메모리로 매핑할 크기가 됩니다. prot, flags는 우선 무시하고 fd는 매핑할 file descriptor입니다. 그리고 offset은 fd로 표현되는 object상의 offset입니다. 리턴 값은 어드레스구요. 제가 한마디로, 그러나 어설프게 mmap이 무엇을 해주는 가를 말해보도록 하겠습니다. object 대신 파일이라는 말을 쓰면 이해하기 쉬우니 파일을 써서 하겠습니다.
“mmap 시스템 호출은 fd로 지정된 파일에서 offset을 시작으로 length바이트만큼을 메모리에 매핑하고 그 어드레스를 리턴한다.”(사실 한글 man page에서 적당히 베껴서 편집한 말입니다.)
argument prot은 메모리 매핑이 읽기/쓰기를 어떤 형식으로 허용할지를 나타내고 flags는 이 메모리와 파일 사이의 관계(?)를 나타내는 flag값인 것 같습니다.
모든 자세한 사항은 man page를 참고로 하시기 바랍니다.
예제로 무엇을 할까 많은 고민 끝에 가장 간단하면서도 아무 쓸모가 없는 것으로 정했습니다.(마땅한 예제가 없네요.) 우선 mmap 예제를 돌리기에 앞서서 test file을 만들어 내는 프로그램 소스코드를 먼저 올립니다.
===============================
/*
* tvector.c : test vector generator for mmaptest
*
* Copyright(C) 2002 holelee
*
*/
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fstream;
int i;
fstream = fopen("mmaptest.tst", "w+b");
if(fstream == NULL)
{
fprintf(stderr, "Cannot open mmaptest.tst\n");
exit(1);
}
for(i = 0; i < 1024; i++)
fwrite(&i, 4, 1, fstream);
fclose(fstream);
exit(0);
return 0;
}
===============================
그냥 C standard library에 있는 함수로 작성해 봤습니다. 내용은 간단하죠. 그냥 mmaptest.tst라는 파일 안에 0부터 1023까지 정수 값을 저장하는 프로그램입니다.(총 32비트 machine에서는 총 4096바이트에 해당하겠죠.)
이제 mmaptest.c 파일을 살펴보도록 하겠습니다.
===============================
/*
* mmaptest.c : mmap example
*
* Copyright(C) 2002 holelee
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int main()
{
int fd;
int *pmmap;
int i;
fd = open("mmaptest.tst", O_RDWR);
if(fd < 0)
{
perror("open");
exit(1);
}
pmmap = (int *)mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if((unsigned)pmmap == (unsigned)-1)
{
perror("mmap");
exit(1);
}
for(i = 0; i < 1024; i++)
{
printf("%d\n", pmmap[i]); /* 읽어서 출력 */
pmmap[i] = pmmap[i]+1024; /* 값을 바꿈 */
}
munmap(pmmap, 4096);
close(fd);
exit(0);
return 0;
}
===============================
내용을 보면 동일한 파일(mmaptest.tst)를 open하고 offset 0부터 4096byte만큼을 READ/WRITE가 가능하도록 mmap합니다. mmap이 리턴한 address는 integer 포인터 pmmap에 assign하고 1024개 만큼 그 포인터에서 읽어서 출력하면서 포인터로 지정된 어드레스에 있는 값을 바꾸는 프로그램입니다. 값을 바꾸었으므로 당연히 파일에 저장된 값이 바뀔겁니다. 그 다음 munmap이라고 하여 mmap의 역함수(?)와 같은 함수를 부릅니다. munmap에 대해서 자세히 알고 싶으면 man page를 보길 바랍니다. 암튼 이제 동작을 살펴봐야 겠죠.
우선 tvector를 수행하여 파일을 만듭니다. 그 다음 mmaptest를 수행시키면 0부터 1023까지의 정수를 차례로 출력할겁니다.(눈깜짝할 사이에 지나가 버리겠지만요.) 다시 mmaptest를 수행하면 이제는 1024부터 2047까지의 값을 찍게 됩니다. 전에 수행시켰던 mmaptest가 pmmap 포인터를 이용하여 파일에 저장된 내용을 바꾸었으니까 당연한 것이죠. 계속 수행해 보면 1024씩 증가합니다.
암튼 파일 내용을 write 시스템 호출을 이용하지 않고 포인터를 이용하여 바꾸는데 성공하였습니다.
** 여기서 잠깐
mmap이 리턴하는 어드레스는 virtual address입니다. 그 address 값 자체는 큰 의미를 지니지 못합니다. CPU의 MMU가 그것을 physical address로 바꿀 때 어떤 일이 일어나겠지만요.
== file descriptor로 지정된 object
file descriptor로 지정될 수 있는 object는 상당히 많이 있습니다. 당연히 일반적인 파일이 지정될 수 있죠. 또한 socket도 file descriptor로 지정될 수 있고, IPC(Inter Process Communication)에서 사용하는 pipe, fifo 등도 역시 file descriptor로 지정될 수 있습니다. 또한 device special file(mknod로 만들어진)을 open하면 device도 지정될 수 있겠죠. 사실 file descriptor로 지정된 모든 것이 mmap을 지원하는 것은 아닙니다. 예를 들어 serial port device와 같은 경우는 모든 데이터가 순서대로 읽어지고 써지는 구조를 가지고 있는데 이런 경우에는 lseek도 의미가 없고 mmap도 되지 않을 겁니다. Serial device를 mmap하여 접근하는 것 자체가 우스운 일이죠.(혹 가능하면 글쓴이만 우스워지므로 가능하다는 사실을 알고 있으면 조용히 메일을 보내줄 것.) mmap을 지원하지 않은 object라면 mmap 시스템 호출에 -1를 리턴하고 errno를 적당한 값으로 채워줄 겁니다.
== 왜 mmap 이야기를 꺼냈는가?
Frame buffer 이야기를 하다가 왜 일반적인 mmap 이야기가 나왔을까요? 이쯤 되면 짐작이 되겠지만 frame buffer device도 file descriptor로 지정될 수 있고 일반적으로 mmap을 지원합니다. 따라서 lseek/write 시스템 호출에서 벗어나서 그냥 포인터로 접근을 해서 화면에 출력될 pixel 값을 바꾼다던지 화면에 출력된 pixel 값을 읽어 올 수 있습니다. lseek/write 시스템 호출을 사용하는 것은 나중에 살펴보겠지만 엄청 무식한 방법입니다. 단, 모든 frame buffer가 mmap을 지원하지는 않는 것 같습니다. 커널 소스의 Documentation/fb/tgafb.txt를 보면 DECChip 21030 칩 기반 그래픽 카드는 mmap을 지원하지 않는 것으로 나와 있습니다. 아마도 시스템의 구조상 버퍼 메모리를 CPU의 virtual address로 매핑할 수 없기 때문일 것으로 생각됩니다. 하지만 일반적인 frame buffer라면 대부분 mmap을 지원하고 mmap을 해서 사용하는 것이 일반적입니다. StrongARM의 LCD도 당연히 mmap을 지원할 겁니다. 왜냐하면 시스템 메모리의 일부를 LCD 버퍼로 이용하므로 virtual address로의 매핑이 쉽게 가능하기 때문이죠.
== 마치며
이번 글에서는 일반적인 mmap 시스템 호출에 대해서 알아보았습니다. 다음 글에서는 좀 유식한(?) mmap을 이용한 pixel 찍는 방법에 대해서 알아보겠습니다. 그리고 이번 글부터는 소스코드에 nl을 이용하여 줄 숫자를 적는다거나 하는 작업을 하지 않을 겁니다. 어짜피 KELP에서 잘 보이지 않네요. 잘 알아서 보길 바랍니다.
== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.
== 시작
예고한대로 mmap을 이용한 무식하지 않은(?) pixel 찍는 법을 알아 볼 차례입니다. 방법은 아주 간단합니다. frame buffer device를 open하고 open이 리턴한 file descriptor를 이용하여 mmap을 합니다. 당연히 offset은 0이고 length는 화면의 해상도와 bpp를 통해 계산된 byte수 만큼이죠. mmap이 리턴한 address와 pixel을 찍기를 원하는 좌표를 이용하여 새로운 address를 계산하고 그 address에 값을 쓰면 pixel이 찍히게 되고 그 address에서 값을 읽으면 화면상에 있는 pixel 값을 읽을 수 있는 겁니다. 자 준비되었으면 소스를 보도록 하죠.
== 다시 나온 pixel 찍기 소스
===============================
/*
* fbpixel2.c : Frame buffer draw pixel example(using mmap)
*
* Copyright(C) 2002 holelee
*
*/
#include <stdio.h>
#include <stdlib.h> /* for exit */
#include <unistd.h> /* for open/close .. */
#include <fcntl.h> /* for O_RDWR */
#include <sys/ioctl.h> /* for ioctl */
#include <sys/mman.h> /* for mmap */
#include <linux/fb.h> /* for fb_var_screeninfo, FBIOGET_VSCREENINFO */
#define FBDEVFILE "/dev/fb"
typedef unsigned char ubyte;
unsigned short makepixel(ubyte r, ubyte g, ubyte b)
{
return (unsigned short)(((r>>3)<<11)|((g>>2)<<5)|(b>>3));
}
int main()
{
int fbfd;
int ret;
struct fb_var_screeninfo fbvar;
unsigned short pixel;
int offset;
unsigned short *pfbdata;
fbfd = open(FBDEVFILE, O_RDWR);
if(fbfd < 0)
{
perror("fbdev open");
exit(1);
}
ret = ioctl(fbfd, FBIOGET_VSCREENINFO, &fbvar);
if(ret < 0)
{
perror("fbdev ioctl");
exit(1);
}
if(fbvar.bits_per_pixel != 16)
{
fprintf(stderr, "bpp is not 16\n");
exit(1);
}
pfbdata = (unsigned short *)
mmap(0, fbvar.xres*fbvar.yres*16/8,
PROT_READ|PROT_WRITE, MAP_SHARED, fbfd, 0);
if((unsigned)pfbdata == (unsigned)-1)
{
perror("fbdev mmap");
exit(1);
}
/* red pixel @ (0,0) */
pixel = makepixel(255,0,0); /* red pixel */
*pfbdata = pixel;
/* green pixel @ (100,50) */
offset = 50*fbvar.xres+100;
pixel = makepixel(0,255,0); /* green pixel */
*(pfbdata+offset) = pixel; /* draw pixel */
/* blue pixel @ (50,100) */
offset = 100*fbvar.xres+50;
pixel = makepixel(0,0,255); /* blue pixel */
*(pfbdata+offset) = pixel; /* draw pixel */
/* white pixel @ (100,100) */
offset = 100*fbvar.xres+100;
pixel = makepixel(255,255,255); /* white pixel */
*(pfbdata+offset) = pixel; /* draw pixel */
munmap(pfbdata, fbvar.xres*fbvar.yres*16/8);
close(fbfd);
exit(0);
return 0;
}
===============================
먼저 소개한 fbpixel.c처럼 (0,0)에 빨간 pixel, (100, 50)에 녹색 pixel, (50, 100)에 파란 pixel, (100, 100)에 하얀 pixel을 순서대로 찍는 간단한 프로그램입니다. fbvar.xres*fbvar.yres*(16/8)만큼의 바이트만큼이 mmap으로 virtual address에 매핑되고 있습니다.(당연히 mmap을 지원하지 않는 특이한 frame buffer에서는 에러를 내고 죽게 됩니다.) 그 address로부터 픽셀이 저장된(될) address라고 보면 됩니다. 픽셀이 저장된 순서는 (0,0)부터 시작하여 (1,0), (2,0)의 순서로 (xres-1, 0)까지 진행되고 그 다음 address는 (0, 1)이 되고 다시 (1,1), (2,1),… 이런 식으로 진행됩니다. 쉽게 이해할 수 있겠죠. lseek 때의 경우와 똑같습니다. 당연히 픽셀 offset은 바이트 단위로는 먼저 소개한 fbpixel.c와 같지만 unsigned short 포인터 pfbdata와 더하므로 offset을 바이트 단위로 계산하지 않고 2byte(sizeof(unsigned short))단위로 하고 있습니다. 따라서 더 이상 (18/6)과 같은 것은 offset 계산에 필요가 없습니다. 그럼 좌표 (xpos, ypos)에 pixel을 찍는 함수를 한번 작성해 보죠.
===============================
1: void put_pixel(fb_var_screeninfo *fbvar, unsigned short *pfbdata, int xpos, int ypos, unsigned short pixel)
2: {
3: int offset = ypos*fbvar->xres+xpos;
4: pfbdata[offset] = pixel;
5: }
===============================
pfbdata에 mmap이 리턴한 address를 주면 제대로 동작할 것으로 기대됩니다.
== address에 값을 쓰면 어떤 일이 벌어지나?
mmap이 리턴한 address는 virtual address로 실제 물리적인 것과는 전혀 관계가 없음을 저번 글에서 언급했습니다. 그럼 위의 fbpixel2.c 소스 코드에서 *(fbdata+offset) = pixel이라는 수식(expression)이 수행되면 실제 PC에서는 어떤 일이 벌어질까요? 알고 있다시피 실제 화면에 나올 픽셀이 저장된 버퍼는 PC에서는 그래픽카드에 있는 RAM에 존재합니다. 그럼 C 언어의 수식하나로 그 그래픽 카드에 있는 RAM 값이 update되는 셈인데 어떤 일이 벌어지는 지 궁금하지 않나요? 제가 어떤 일이 벌어지는지 구성해 보도록 하겠습니다.
우선 CPU에서 저 수식이 수행되면 address 계산(fbdata+offset)을 하게 됩니다. 그 address에 pixel 값을 쓰는 instruction 수행되겠죠.(보통 RISC는 그런 instruction을 store라고 부르고 x86에서는 mov입니다.) 그럼 address를 CPU의 MMU가 살펴보고 physical address로 바꾸어서 CPU의 버스에 그 physical address에 pixel이라는 값을 쓰겠다고 신호를 보냅니다. 그럼 CPU 버스에 물려있는 North bridge라는 녀석이 그 address를 살펴보고 시스템에 설치된 DRAM의 어드레스가 아님을 판단하고 PCI 버스(혹은 AGP포트)에 address와 pixel 데이터로 다시 신호를 보내게 됩니다.(이 때 address translation이 다시 일어날 수도 있습니다. PCI버스와 CPU버스가 addressing이 다르다면.) 그럼 PCI 버스(혹은 AGP포트)에 달려있는 그래픽 카드가 그 address를 보고 자기 자신에 관한 일임을 알아채고 그에 상응하는 일을 해주게 됩니다. ATI mach 64 CT처럼 PCI 버스에서 동작하는 그래픽카드를 가정하고 환상적인 그림솜씨(?)를 뽐내보면 다음과 같은 그림으로 시스템을 나타내 볼 수 있습니다.
CPU(MMU)
|/
|/ <CPU버스>
|/
North Bridge --- DRAM
|/
|/ <PCI버스>
|/
그래픽카드
당연히 버스에는 latency가 존재하므로 DRAM에 접근하는 것보다는 훨씬 오래 걸립니다. 또한 Cache에 저장되면 문제가 일어나게 되므로(Cache만 update되고 화면은 바뀌지 않는 식으로) Cache를 이용할 수도 없어서 latency가 상당하죠.
(실제로 요즘 최신 PC 시스템에서는 North Bridge에 직접 PCI버스가 물려있는 것이 아니고 North Bridge에는 Point-to-Point 버스로 South Bridge가 물려있고 South Bridge에 PCI 버스가 물려 있는 것으로 알고 있습니다. 그러한 구조상의 변화 때문에 Intel에서는 Bridge라는 말을 더 이상 사용하지 않고 HUB라는 말을 쓴다고 하는군요.)
== 마치며
이번 글에서는 mmap을 이용한 깔끔한 pixel 찍기에 대해서 살펴봤습니다. 뭐 별로 어려울 것은 없을 것으로 생각됩니다. 그럼 다음 글에서는 왜 그 전에 lseek/write를 이용한 pixel 찍기가 “다소 무식한(?)” 방법이었는지 생각해 보는 시간을 갖도록 하겠습니다.
== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.
== 시작
lseek/write를 이용하는 pixel 찍기는 우선 코딩상 귀찮습니다. lseek, write 시스템 호출이 에러를 리턴하는지 체크해야 하고 그에 따른 error 처리 루틴도 만들어야 하죠.(물론 mmap이 리턴한 address로 pixel을 찍거나 읽을 때도 잘못하면 에러가 발생할 수 있습니다. 그러나 에러가 Segmentation Fault나 Bus Error로 프로그램이 그냥 죽을 겁니다.) 그러나 그런 것만 가지고는 “다소 무식한(?)”이라는 표현이 조금 과한 표현입니다. 이제 왜 “다소 무식한(?)”지 알아보기 위해서 벤치마크를 해보도록 하겠습니다.
== 벤치마크 시스템
벤치마크를 담당할 시스템은 본인이 집에 가지고 있는 linux machine입니다. 컴퓨터 사양은 다음과 같습니다.
OS : RedHat 8.0(default로 깔리는 kernel)
CPU : PentiumPro 200Mhz
Graphic Card : ATI mach 64 CT 2MB
Frame buffer driver : atyfb
== 벤치마크 프로그램 : 다시 나온 random 네모 그리기
random 네모 그리기를 mmap을 이용하여 다시 작성했습니다. 이번에 작성된 프로그램을 fbrandrect2라고 부르고 전에 lseek/write를 이용하여 작성된 프로그램을 fbrandrect1이라고 부르겠습니다. 소스의 골격이나 구성은 fbrandrect1과 fbrandrect2 모두 동일합니다. 다만 무한 루프를 돌면 곤란하므로 두 프로그램 모두 그냥 1000개의 네모만 그리고 프로그램이 종료되도록 하였습니다. 그리고 rand 함수가 똑 같은 값을 리턴해야 두 프로그램의 동작이 똑같아 지므로 srand 함수를 이용하여 rand 함수의 seed도 주었습니다.
===============================
/*
* fbrandrect2.c : Frame buffer draw random rectangular example unsing mmap
*
* Copyright(C) 2002 holelee
*
*/
#include <stdio.h>
#include <stdlib.h> /* for exit */
#include <unistd.h> /* for open/close .. */
#include <fcntl.h> /* for O_RDONLY */
#include <sys/ioctl.h> /* for ioctl */
#include <sys/mman.h>
#include <linux/fb.h> /* for fb_var_screeninfo, FBIOGET_VSCREENINFO */
#define FBDEVFILE "/dev/fb"
int main()
{
int fbfd;
int ret;
struct fb_var_screeninfo fbvar;
unsigned short *pfbdata;
int count = 1000;
fbfd = open(FBDEVFILE, O_RDWR);
if(fbfd < 0)
{
perror("fbdev open");
exit(1);
}
ret = ioctl(fbfd, FBIOGET_VSCREENINFO, &fbvar);
if(ret < 0)
{
perror("fbdev ioctl");
exit(1);
}
if(fbvar.bits_per_pixel != 16)
{
fprintf(stderr, "bpp is not 16\n");
exit(1);
}
pfbdata = (unsigned short *)
mmap(0, fbvar.xres*fbvar.yres*16/8,
PROT_READ|PROT_WRITE, MAP_SHARED, fbfd, 0);
if((unsigned)pfbdata == (unsigned)-1)
{
perror("fbdev mmap");
exit(1);
}
srand(1); /* seed for rand */
while(0 < count--)
{
int xpos1, ypos1;
int xpos2, ypos2;
int offset;
int rpixel;
int t, tt;
/* random number between 0 and xres */
xpos1 = (int)((fbvar.xres*1.0*rand())/(RAND_MAX+1.0));
xpos2 = (int)((fbvar.xres*1.0*rand())/(RAND_MAX+1.0));
/* random number between 0 and yres */
ypos1 = (int)((fbvar.yres*1.0*rand())/(RAND_MAX+1.0));
ypos2 = (int)((fbvar.yres*1.0*rand())/(RAND_MAX+1.0));
if(xpos1 > xpos2)
{
t = xpos1;
xpos1 = xpos2;
xpos2 = t;
}
if(ypos1 > ypos2)
{
t = ypos1;
ypos1 = ypos2;
ypos2 = t;
}
/* random number between 0 and 65535(2^16-1) */
rpixel = (int)(65536.0*rand()/(RAND_MAX+1.0));
for(t = ypos1; t <= ypos2; t++)
{
offset = t*fbvar.xres;
for(tt = xpos1; tt <= xpos2; tt++)
*(pfbdata+offset+tt) = rpixel;;
}
}
munmap(pfbdata, fbvar.xres*fbvar.yres*16/8);
close(fbfd);
exit(0);
return 0;
}
===============================
소스는 굳이 설명을 하지 않겠습니다.
== 벤치 마크 결과
fbrandrect1, fbrandrect2 모두 gcc의 “–O2” 옵션을 주고 컴파일 하였고 time 명령어로 수행되는 시간을 측정해 보았습니다.
time ./fbrandrect2
real 0m4.736s
user 0m4.730s
sys 0m0.006s
time ./fbrandrect1
real 3m53.784s
user 0m34.213s
sys 3m19.572s
어떻습니까? 차이가 보입니까? fbrandrect2는 5초도 되지 않은 시간에 끝이 났고, fbrandrect1은 4분 가까이나 걸렸네요. 눈으로 네모가 그려지는 모습을 보더라도 확연히 성능 차이를 확인할 수 있습니다. 이제 왜 무식한 방법이라고 얘기했는지 이해할 수 있겠죠?
== fbrandrect1은 왜 성능이 떨어지는가?
Frame buffer 이야기와는 크게 상관은 없지만 fbrandrect1이 성능이 떨어지는 이유에 대해서 짤막하게 살펴 보겠습니다. 그 이유는 lseek/write 시스템 호출을 많이 했기 때문입니다. 시스템 호출(system call)은 software interrupt라는 instruction을 수행하게 되어 있습니다.(instruction 이름은 CPU architecture마다 다르게 부르고 instruction 자체도 CPU architecture마다 다르죠.) 그 instruction이 수행되면 CPU는 interrupt를 받은 경우와 동일하게 동작합니다. Interrupt를 받으면 우선 CPU안에 있는 레지스터를 지정된 위치에 저장하고 어떤 이유로 interrupt가 걸렸는지 살펴보고 그것에 따라서 kernel 내부에서 그 interrupt를 서비스하는 함수를 호출합니다. write 시스템 호출을 했다면 linux에서 서비스 함수는 sys_write일 겁니다. 이 sys_write는 file descriptor를 살펴서 그 파일이 frame buffer device인지 판단하고 다시 그 frame buffer driver의 write함수를 호출하게 됩니다. 그 함수의 수행이 끝나게 되면 schedule을 다시 해야 할 필요가 있는지 계산해보고 그렇지 않다면 다시 저장된 레지스터를 복원해 놓고 user application으로 리턴합니다. User application으로 돌아오게 되면 CPU의 cache가 비어있으므로 cache miss가 발생하여 DRAM으로부터 cache의 내용을 다시 채우게 됩니다. fbrandrect1이 한 pixel을 찍을 때마다 write 시스템 호출을 하므로 이 모든 일을 반복하게 됩니다. lseek 시스템 호출도 마찬가지죠. 따라서 성능은 엄청 떨어지죠.
== 마치며
다음 글의 내용은 아직 미정입니다. 다음 글을 적을 때까지는 시간이 좀 걸릴 것 같습니다.
== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.
== 시작
pixel 찍기(쓰기)와 pixel 읽기를 알아보았으므로 사실 frame buffer에 대한 프로그래밍은 다 알아봤다고 볼 수 있습니다. 적어도 지금까지 테스트한 16bpp 환경에서는 말이죠. 그런데 random pixel 찍기나 random 네모 그리기는 그야말로 데모 수준이지 그렇게 쓸모가 있는 프로그램은 아니죠. 또한 원하는 색깔의 pixel이 제대로 나타나는지도 정확하게 알 수 없죠. 그래서 지금까지 알아본 내용을 바탕으로 작성된 bmp 파일 display 프로그램을 소개합니다. 사실 bmp 파일은 MS와 IBM이 만들었으므로 linux와 친하다고 볼 수는 없습니다.(상식 : GNU/linux와 가장 친하지 않은 그래픽 파일형식은 GIF죠.) 그래도 굳이 bmp 파일형식을 사용한 이유는 파일 구조와 압축 방식이 비교적 간단하고 쉽게 구할 수 있는 이미지 파일형식이기 때문입니다.
== fbbmp 프로그램
= 사용법
그냥 압축 풀고 make로 build하면 fbbmp라는 프로그램이 만들어 집니다. bmp 파일 이름을 인자로하여 수행하면 화면에 그 파일을 display합니다.(당연히 그 전에 frame buffer driver가 제대로 올라가 있어야 하고 16bpp로 셋팅되어 있어야 합니다.) 네 개의 bmp 파일이 포함되어 있으니 테스트하면 됩니다.
= 지원 환경
(1) 16bpp mmap이 가능한 frame buffer
=> mmap이 지원되지 않으면 lseek/write로 pixel을 찍도록 코딩을 할 수도 그런 경우가 많지도 않고 귀찮으므로 그냥 작성하였습니다.
(2) little endian machine
=> bmp 파일 형식이 little endian으로 되어 있는데 big endian machine까지 지원하도록 만들기 위해서는 byte를 swap해야 하는데 귀찮아서 그냥 두었습니다.
= 지원하는 bmp 파일 형식
BI_RLE4 압축을 한 4bpp 파일을 제외한 모든 bmp 파일을 지원합니다.(코딩하기 귀찮고 어짜피 테스트도 못하기 때문에 BI_RLE4 4bpp를 제외했습니다.)
= 테스트된 bmp 파일 형식
(1) 1bpp 파일 : kelp_logo_1bpp.bmp
(2) 4bpp(BI_RGB : 즉 압축되지 않은) 파일 : kelp_logo_4bpp.bmp
(3) 8bpp(BI_RGB : 즉 압축되지 않은) 파일 : tux_resize_8bpp.bmp
(4) 24bpp 파일 : lixgreen_24bpp.bmp
지원하는 모든 bmp 파일 형식에 대해서 테스트하고 싶었으나 bmp 파일을 구할 수 없어서 어쩔 수 없이 4가지 형식에 대해서만 테스트를 진행했습니다. 포함되어 있는 모든 bmp 파일은 인터넷을 돌아다니면서 몇 가지 골라온 것을 적당한 툴로 변환한 것입니다.
당연히 다른 형식에서 바르게 작동하는지 검사하지 못했으므로 버그가 있을 가능성이 큽니다. 혹 디스플레이가 되지 않는 bmp 파일이 있으면 메일을 보내 주길 바랍니다.
= 약간의 문제
급히 작성한 소스코드라서 error처리가 완전하지 않습니다. 따라서 손상된 bmp파일을 입력하면 어떻게 될지 모르겠습니다.
== 소스코드 설명
소스코드 설명은 없습니다. 소스코드를 설명하려면 bmp 파일 구조부터 설명해야 하는데 그것은 “frame buffer 이야기”라는 제목과 어울리지 않아서 그만 둡니다. bmp 파일 구조에 대해서는 reference에 나와있는 사이트에서 있는 관련 문서에 자세히 나와 있습니다. 관심있는 사람은 bmp 파일형식에 대해 알아본 후 소스코드를 직접 살펴보길 바랍니다.
== 중대한 에러 발견
모든 프로그래밍을 끝내고 Permedia2 frame buffer에 디스플레이를 해보고 잘된다고 생각했습니다. 집에 가서 글을 적으며 ATI mach64 frame buffer에서 테스트를 해보았더니 색깔이 깨지며 원하는 색상이 나오지 않는 문제가 발견되었습니다. 그래서 어떤 문제인지 알아보았더니 16bit pixel encoding 문제가 있었습니다.
“Frame buffer 이야기(3)”에서 16bpp의 pixel은 Red 5bit, Green 6bit, Blue 5bit이 MSB에서부터 순서대로 붙어 있다고 말했습니다. 이렇게 구성된 16bpp를 보통은 high color라고 부릅니다. 6만5천(정확하게는 2^16=65536) color를 표현할 수 있죠. 하지만 특별한 시스템에서는 16bpp에서의 pixel이 그렇게 표현되지 않고 다음과 같이 표현되는 경우도 있습니다.
============================================
| NotUsed(1) | Red(5) | Green(5) | Blue(5) |
============================================
즉 최상위비트(MSB)는 사용하지 않고 Red, Green, Blue가 각각 5bit씩 사용됩니다. 어찌보면 15bpp라고 할 수도 있겠지만 결국 1 pixel이 16bit로 표현되므로 16bpp입니다. 이런 경우 당연히 6만5천 color를 표현하지 못하고 3만2천(정확하게는 2^15=32768) color를 표현할 수 있습니다.
일반적인 PC 시스템에서는 보통 16bpp라고 하면 R(5), G(6), B(5)로 coding된 경우를 일컫는 것으로 알고 있어서 그렇게 계속 이야기를 진행하고 나중에 다른 bpp에 관한 이야기를 할 때 이 문제를 잠시만 언급하려고 했습니다. 그런데 ATI mach64 frame buffer driver(RedHat 8.0)상의 16bpp는 R(5),G(5),B(5)인 것으로 나타났습니다. 그래서 색상이 깨진 것이죠. 이 문제를 해결할 수 있는 방법을 언급하고 넘어가도록 하겠습니다.
== 어떻게 구분하는가?
문제점을 해결하기 위해서 가장 간단한 방법은 프로그램을 두 가지 버전으로 만들어보고 잘 되는 것을 사용하는 것이겠지만, 이런 식의 해결은 많은 사람들이 좋아하는 방법이 아니죠. 그럼 하나의 프로그램이 두 가지 pixel encoding 형식을 모두 지원하도록 만드는 방법이 좋은데 어떻게 frame buffer driver가 어떤 pixel encoding 형식을 사용하는 지를 알아낼 수 있는가가 핵심이 되겠습니다.
실제로 알아내는 방법은 아주 간단합니다. ioctl(FBIOGET_VSCREENINFO)를 통해 얻은 fb_var_screeninfo 구조체에 모든 내용이 들어 있습니다. 그 구조체 안에 있는 bits_per_pixel이라는 member는 두 가지 pixel encoding 방식 모두 16의 값을 가지므로 구분을 위해 사용할 수 없습니다. 구조체 member중에 struct fb_bitfield type의 red, green, blue, transp라고 하는 것들이 있는데 그것을 통해서 알 수 있습니다.(/usr/include/linux/fb.h나 직접 kernel 소스의 include/linux/fb.h를 살펴보길 바랍니다.) transp의 경우는 나중에 32bpp에 관한 설명을 할 때 설명하도록 하고 지금은 무시하기로 하겠습니다.(보통 16bpp상에는 투명도는 잘 사용되지 않으므로. 이 가정도 잘못될 수 있기는 하진만요.)
struct fb_bitfield는 다음과 같습니다.
struct fb_bitfield {
__u32 offset; /* beginning of bitfield */
__u32 length; /* length of bitfield */
__u32 msb_right; /* != 0 : Most significant bit is right*/
};
struct fb_bitfield red가 { 11, 5, 0 }의 값을 가진다면 이 의미는 pixel encoding에서 색상 Red가 11bit의 위치에서 시작하여 5 bit의 크기를 가지고 색상 Red를 나타내는 bit열의 MSB가 left(즉 전체 pixel encoding상 MSB쪽)임을 나타냅니다. msb_right는 정말 특수한 경우가 아니면 0이 되므로 무시하기로 합니다.
이제 R(5),G(6),B(5) 형식의 pixel encoding을 생각해 보면 fb_var_screeninfo내의 red, green, blue가 다음과 같은 값을 가지게 됩니다.
struct fb_bitfield red = { 11, 5, 0 };
struct fb_bitfield green = { 5, 6, 0 };
struct fb_bitfield blue = { 0, 5, 0 };
그리고 R(5),G(5),B(5) 형식의 pixel encoding의 경우는 다음과 같습니다.
struct fb_bitfield red = { 10, 5, 0 };
struct fb_bitfield green = { 5, 5, 0 };
struct fb_bitfield blue = { 0, 5, 0 };
이제 구분할 수 있겠죠.
사실 구분할 필요가 없습니다. Red, Blue, Green 각각의 색상이 연속적인(contiguous) bit열로 encoding되어 있는 모든 16bpp에 대해서 위의 세 개의 값을 통해서 나타낼 수 있습니다. 즉 16bpp라고 해서 pixel encoding이 꼭 R(5),G(6),B(5) 또는 R(5),G(5),B(5)의 경우가 아닐 수도 있고 그런 경우라고 해도 위의 세 개의 값을 이용하면 모두 encoding을 표현하는 것이 가능합니다. 즉 엽기적으로 R(14),G(1),B(1)의 pixel encoding도 표현할 수 있다는 말이죠.(물론 그야말로 엽기적인 경우일 뿐, 일반적인 상식으로는 별로 가능성을 고려할 필요가 없죠.)
== 수정 사항
fbpixel.c와 fbpixel2.c 에서 나온 makepixel이라는 함수를 바꾸어 16bpp의 경우 모두 지원할 수 있도록 만들어 보았습니다. 저번 makepixel 함수에 비해 길어졌지만 그냥 보기 쉽게 만들려고 한 것일 뿐, bit의 length와 offset을 고려한 것을 제외하면 더 복잡해 졌다고 볼 수는 없습니다.
===============================
typedef unsigned char ubyte;
static unsigned short makepixel(struct fb_var_screeninfo *pfbvar, ubyte r, ubyte g, ubyte b)
{
unsigned short rnew, gnew, bnew;
rnew = r >> (8-pfbvar->red.length);
gnew = g >> (8-pfbvar->green.length);
bnew = b >> (8-pfbvar->blue.length);
return (unsigned short) ((rnew << pfbvar->red.offset)
| (gnew << pfbvar->green.offset)
| (bnew << pfbvar->blue.offset));
}
===============================
이제 함수의 인자로 FBIOGET_VSCREENINFO ioctl을 통해 전달 받은 fb_var_screeninfo를 넘기고 있습니다. 그것의 red, green, blue의 값을 이용하여 pixel을 encoding하는 모습을 볼 수 있습니다. (fbbmp.c에서도 동일한 함수를 사용하였습니다.)
사실 좀더 정확하게 하려면 함수의 인자인 r, g, b가 unsigned char 형식이 아니라 unsigned short여야 가능한 모든 16bpp pixel encoding 형식을 지원할 수 있겠지만 일반적으로 16bpp에서 한 색상이 8bit이상은 되지 않을 것이라는 가정을 하고 그냥 사용합니다. 또한 8-length의 값이 음수가 될 수도 있으므로 그것을 고려해야 하겠지만 역시 같은 이유로 그냥 사용합니다.(shift 연산의 우측 operand가 음수일 때 C 언어의 behavior가 어떤지는 C 언어 reference 매뉴얼을 찾아봐야 하지만 귀찮으므로 넘어갑니다.)
== reference
(1) www.wotsit.com
=> 갖가지 파일 형식에 관한 문서들을 모아놓은 사이트로 bmp 파일 형식에 관한 문서도 다운로드 가능합니다.
(2) ezfb(www.akrobiz.com/ezfb/)
=> 소스코드를 다운로드하여 수행해 보길 바랍니다. 역시 bmp를 frame buffer에 display할 수 있는 프로그램뿐 아니라 frame buffer에서 pixel값을 읽어서 bmp로 저장하는 프로그램 등도 들어 있는 것으로 보입니다. 개인적으로 좋아하지 않는 코딩 스타일이라서 참고로 하지 않았습니다.
== 마치며
원래 이번 글은 소스코드만 올리고 그냥 짧게 넘어가려고 했는데 예기치 않은 문제가 발생하여 길어졌습니다. 물론 한 수 배운 셈이죠. 그리고 이번 글에서는 참 “귀찮은 것”이 많았는데 그냥 양해하길 바랍니다. 다음 글은 16bpp가 아닌 다른 bpp에서의 프로그래밍에 대해서 짤막하게 알아보도록 하겠습니다. 이제 “frame buffer 이야기”도 끝날 때가 머지 않았습니다.
첨부 파일: bmp.tgz (471 KiB(482,221 Bytes))
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.
== 시작
저번 글에서 bmp 파일을 frame buffer상에 display하는 프로그램을 작성하였으므로 이제 16bpp frame buffer에 대한 모든 사항을 알아보았다고 볼 수 있습니다. 물론 pixel 읽기는 말로 넘어갔으므로 조금 의심스러운 부분이 남아 있을 지라도 적어도 display에 대해서는 모든 사항을 알아보았습니다. 오늘 이야기는 16bpp가 아닌 다른 bpp(bits per pixel)에 관한 프로그래밍에 대해서 짤막하게 알아보도록 하겠습니다.
PC 기반의 linux 시스템에서 frame buffer는 보통 4, 8, 16, 24, 32bpp를 사용합니다. 모두 color를 나타낼 수 있습니다. 그런데 embedded system의 경우 gray LCD를 사용하는 경우도 있고 color LCD를 사용한다고 해도 PC와는 다른 bpp를 사용하는 경우가 있습니다. 예를 들어 Compaq(HP에 인수되었지만)에서 생산한 PDA인 iPAQ의 초기 모델의 경우 12bpp를 사용하여 총 4096 color를 나타낼 수 있었습니다. 이렇게 다양한 frame buffer 하드웨어에 대하여 모든 조사를 진행할 수도 없고 테스트도 불가능 합니다. 따라서 이 글은 제 개인적인 경험(보잘 것 없기는 하지만)에 바탕으로 한 추측을 기반으로 작성되었습니다. 이 글의 내용을 모두 곧이 곧대로 사실로 받아들이지 말고 그냥 가이드 정도의 수준으로 생각하고 읽기 바랍니다. 자 시작할 까요?
== 1 bpp
pixel 하나를 1 bit로 표현할 수 있습니다. 어떻게 가능할까요? 당연히 bit가 ‘1’이면 1번 pixel이 나타나고 bit가 ‘0’이면 0번 pixel이 나타나도록 할 수밖에 없습니다. 보통은 0번 pixel은 그냥 pixel이 없는 상태로 결국 bit가 ‘1’이면 점이 있고, bit가 ‘0’이면 점이 없는 그냥 말로 “흑백”이죠. 무슨 gray level이 있고 하는 것이 아니라 그야말로 pixel의 유무를 1bit로 나타낸 것입니다. 얼마 전까지 주로 사용되는 핸드폰용 흑백 LCD가 이런 형태를 지니고 있었죠.(물론 핸드폰은 linux가 아직 널리 쓰이지 않는 분야이기는 하지만요.) 이런 종류의 frame buffer가 많이 쓰이지는 않을 것으로 보입니다. 간단한 정보를 display하기 위해서는 character LCD를 더 선호할 테니깐요.
프로그래밍은 간단합니다. 우선 Frame buffer상의 좌표를 모두 byte와 bit단위로 계산을 해야 합니다. lseek/read/write를 이용하거나 mmap을 이용하거나 하더라도 우리가 기본적으로 읽을 수 있는 최소 단위는 byte(char)이기 때문에 읽고 쓰기는 byte단위로 이루어져야 합니다. 그럼 예를 들어 320x240 사이즈의 frame buffer에서 (99, 50)에 pixel을 찍고 싶다고 가정하죠. 그럼 (99,50)의 위치는 byte 단위로는 (320*50+99)/8 = 2012가 됩니다. 그럼 offset 2012에서 byte만큼의 값을 먼저 읽어 prev_pixel(char형)에 저장합니다. 그 다음 bit offset은 (320*50+99)%8 = 3이 됩니다. 그럼 prev_pixel의 MSB부터 3비트 떨어진 비트 위치를 ‘1’로 바꾸고(prev_pixel |= 0x10 으로 하면 되죠.) 다시 그 값을 offset 2012에 다시 적어주면 됩니다. 아주 간단하죠. 이런 식으로 byte단위 offset과 그 byte내부의 bit 단위 offset을 계산해서 값을 읽거나 쓸 수 있겠습니다.
옛날 이야기 좋아하는 사람들을 위해서 한마디 적으면, PC상에서 사용되던 그래픽 카드중에 Hercules(요즘도 Hercules라는 상표를 달고 나오는 그래픽 카드가 있는데 그런 녀석 말고.) 라고 하는 녀석도 1bpp를 사용했었죠. simcga라고 하는 CGA emulation 프로그램을 램 상주 시키고 게임을 했던 기억이 있습니다.(simvga라고 하는 희대의 사기극이 온라인계를 강타하고 그랬었죠. simvga를 다운로드 받으려고 노력했던 기억이 아련합니다.) 놀라운 것은 linux kernel source에 이 Hercules 그래픽 카드를 지원하는 frame buffer driver가 존재한다는 겁니다. DOS시절 turbo C 2.0으로 Hercules 카드에 그림을 그려본 경험으로 보면 Hercules 카드의 그래픽 메모리는 linear하지 않고 interleaving되어 있어서 offset 계산을 특별하게 했던 기억이 있는데, linux kernel source에 포함되어 있는 Hercules frame buffer는 어떻게 프로그래밍 해야 하는지 궁금합니다. 아마도 mmap을 지원하지 않거나 지원하더라도 linear하지 않을 것으로 보입니다. (도저히 그 그래픽 카드를 구할 수 있는 방법이 없으므로 테스트 못합니다. 혹 구한다고 해도 테스트하기 싫죠. -.-! 또한 kernel source code를 분석해야 할 필요성 조차 느끼지 못한답니다.)
== 2 bpp
pixel 하나를 2 bpp로 나타내는 경우는 gray level LCD에서 사용되는 것으로 보입니다. 한 점이 4개의 gray level로 나타내어 지게 됩니다. pixel값이 “00b”(이진수 00)라면 pixel이 없는 상태(즉 가장 밝은(?, 흰색에 가까운) pixel)이고 “11b”라면 가장 어두운(?, 검정색에 가까운) pixel을 나타냅니다. Pixel값을 이 어두운(?) 순서로 나열하면 “11b” > “10b” > “01b” > “00b”이 됩니다.
프로그래밍은 당연히 1bpp의 경우처럼 byte단위 offset과 bit단위의 offset으로 나누어 계산해야 합니다. 한번 더 예를 들어서 살펴보죠. 320x240사이즈의 2 bpp frame buffer에서 (99, 50)의 위치에 점 “11b”를 쓰고 싶다고 하죠. 그럼 (320*50*2 + 99*2)/8 = 4024의 byte offset을 가집니다. 그 byte 내부의 bit offset은 (320*50*2 + 99*2)%8 = 6이 됩니다. 그럼 한 byte에 총 4개의 2 bpp pixel이 들어가므로 MSB에서 6bit 만큼 떨어져 있는 위치는 LSB에 있는 2bit을 나타내게 됩니다. 따라서 byte offset에서 한 byte를 읽은 후에 LSB의 2 bit를 “11b”로 바꾸고 다시 값을 써 주면 되겠습니다.
한 byte에 4개의 pixel이 들어가므로 다음과 같이 계산 할 수도 있습니다. (320*50 + 99)/4 = 4024, (320*50+99)%4 = 3이 됩니다. 여기서 3이라는 의미는 byte내부에서 3번째 pixel 위치를 나타냅니다.(당연히 LSB 2bit죠.)
== 4 bpp
4 bpp가 사용되는 경우는 크게 color와 gray level로 나누어 볼 수 있습니다. 우선 gray level(주로 LCD가 되겠죠)을 사용하는 시스템은 16 gray level을 나타낼 수 있다는 점만 빼고는 2bpp의 경우와 비슷합니다. pixel값이 “0000b”라면 pixel이 없는 상태(즉 가장 밝은(?) pixel)이고 “1111b”라면 가장 어두운(?) pixel을 나타냅니다.
그럼 color는 어떻게 될까요? pixel 값이 “0000b”이면 어떤 색의 pixel일까요? 혹 3bpp라면 R(1),G(1),B(1)로 나타낼 수 있을 지도 모르는 데 4bpp라면 그렇게 나누기도 쉽지 않습니다. 이 이야기는 아래 palette 이야기에서 살펴보도록 하겠습니다.
프로그래밍은 설명 없이도 알겠죠. 1bpp와 2bpp의 경우와 비슷하게 byte, bit offset을 각각 찾아서 프로그래밍하면 됩니다.
== palette(스펠링이 어려워요.)
palette는 우리말(?)로는 팔레트라고 하죠. 왜 그 미술시간에 튜브에 들어있는 물감을 짜서 쓸 수 있도록 구획이 있는 플라스틱으로 된 판(?) 있잖아요. 환상적인(?) 그림 솜씨를 뽐내며 설명하고 싶지만 KELP사이트의 제약으로 인해 잘 안됩니다.(혹 아직도 모르겠으면 주변 사람들에게 물어 보면 됩니다.) 컴퓨터 그래픽에서도 이 palette라는 것이 등장합니다. 컴퓨터 그래픽에서 등장하는 palette도 미술시간에 사용하던 팔레트와 마찬가지로 지금 사용하고 있는 색깔을 담아두는 공간(실제로는 memory)입니다.
Color 4bpp에서 pixel 값이 “0000b”라면 어떤 색의 pixel이 나올지 궁금했죠. 모든 사람들이 비웃을지 모르지만 답을 공개 합니다. “0번 색깔 pixel”입니다. 그럼 pixel값이 “0011b”라면 “3번 색깔 pixel”이 되고 “1111b”라면 “15번 색깔 pixel”이 됩니다. 상당히 우스운 답이죠? 그럼 이제 “x번 색깔”은 도대체 무슨 색깔인지가 궁금해 집니다.
Color 4bpp에서는 총 16개의 색의 물감을 채울 수 있는 palette가 자동으로 주어집니다. 팔레트의 구획이 총 16개이고 각각의 구획에 0번부터 15번까지의 번호를 부여했다고 가정합니다. 각 구획에 무언가 물감이 들어 있고 그 물감의 색깔을 구획의 번호에 맞추어 “x번 색깔”이라고 부르도록 합니다. 위에서 말한 “0번 색깔”은 말 그대로 palette에 있는 “0번 색깔”을 의미합니다. 그럼 이제 palette의 “0번 색깔”이 무엇인지 알아볼 수 있는 방법만 있다면 pixel값이 “0000b”일 때 어떤 색깔의 pixel인지 알 수 있습니다.
(컴퓨터에서 개수가 정해져 있는 데이터를 관리하기에 가장 편한 것은 array입니다. 따라서 palette도 array로 구현되는 경우가 일반적입니다.)
Linux frame buffer에서는 palette라는 말을 사용하지 않고 대신 colormap이라는 말을 사용합니다. colormap이라는 말이 좀더 그럴싸하게 들리기는 합니다만 아무튼 다음과 같은 ioctl()로 colormap(palette)의 내용을 확인 할 수 있습니다.
struct fb_cmap fbcmap;
….
ioctl(fd, FBIOGETCMAP, &fbcmap);
당연히 frame buffer driver가 colormap을 지원하지 않는다면 ioctl()은 에러를 리턴하게 됩니다.
/usr/include/linux/fb.h(kernel source include/linux/fb.h)를 보면 struct fb_cmap은 다음과 같이 되어 있습니다.
struct fb_cmap {
__u32 start; /* first entry */
__u32 len; /* Number of entries */
__u16 *red; /* Red values */
__u16 *green;
__u16 *blue;
__u16 *transp; /* transparency, can be NULL */
};
start는 보통 0이고 4bpp의 경우 len은 16이 됩니다. red, green, blue는 특정 array나 아니면 malloc된 address를 가리키고 있어야 합니다.(transp는 나중에 설명하죠. 그냥 NULL) 그런 다음 위의 ioctl()을 수행하면 red, green, blue가 가리키고 있는 address에서 현재 frame buffer가 사용하고 있는 palette의 값을 읽어오게 됩니다.
그러면 “0000b” pixel의 색은 RED=fbcmap.red[0], GREEN=fbcmap.green[0], BLUE=fbcmap.blue[0]의 색을 가지게 됩니다. “1111b”는 RED=fbcmap.red[15], GREEN=fbcmap.green[15], BLUE=fbcmap.blue[15]의 색을 가지게 됩니다.
palette에 들어 있는 색을 바꿀 수 있으면 더욱 좋겠죠. 지금 셋팅되어 있는 palette를 사용하는 것이 아니라 우리가 직접 16개의 물감을 짜 놓은 팔레트를 사용하는 것이 그림 그릴 때 더 좋을 것 아닙니까? 그 방법도 있습니다. FBIOPUTCMAP ioctl을 사용하면 됩니다.(당연히 frame buffer driver가 지원하지 않는다면 error를 리턴합니다.)
이제 palette를 좀 정리해서 학구적인(?) 말로 적어보도록 하겠습니다. 어떤 computer graphic에서 palette mode를 사용한다고 하면 pixel의 값은 R,G,B값으로 직접 encoding되어 있지 않고, palette 안에 있는 색을 가리키는 index역할을 하게 됩니다. 실제 색을 알아내기 위해서는 pixel값으로 palette를 indexing하면 됩니다. Linux frame buffer에서는 palette라는 말 대신 colormap이라는 말을 사용하고 palette내용을 알아내거나 바꿀 때 FBIOGETCMAP, FBIOPUTCMAP ioctl()을 사용합니다.(정리끝 : 이렇게 간단한 이야기를 저렇게 길게 써 놓았다니..)
== 8bpp
8bpp의 경우는 gray level LCD에 사용되는 경우는 거의 없고 대부분 256 color를 나타낼 수 장치에 사용됩니다. Color 8bpp는 color 4bpp에서와 마찬가지로 palette mode를 사용합니다. 즉 pixel값에 직접 R,G,B가 encoding되어 있는 것이 아니라 pixel값으로 palette를 index해야 pixel의 색상을 알 수 있습니다. 단지 8bpp이므로 256개의 entry를 가지는 palette를 사용한다는 점이 다릅니다. 프로그래밍 방법은 아주 간단합니다. 이제는 Byte offset만 계산해서 byte의 값만 update하면 pixel이 바뀌게 됩니다. 다시 한번 예로 320x240 사이즈의 8bpp frame buffer에서 (99, 50)에 0xf0라는 pixel을 찍는다고 가정하면 byte offset은 (320*50+99)가 됩니다.
특별한 예외로, 삼성에서 나온 ARM MCU의 경우 8bpp color LCD를 지원하는 데 palette mode를 사용하는 것이 아니라 pixel값에 직접 R,G,B값이 encoding되어 있는 것으로 보입니다.(MSB부터 Red 3bit, Green 3bit, Blue 2bit라고 합니다.)
== 15bpp
16bpp에서 한 픽셀이 dummy(1bit),R(5bit),G(5bit),B(5bit)으로 나타내어질 때 보통 15bpp라고 칭하기도 합니다. Dummy bit가 있다고 해도 한 pixel이 16bit로 나타내어지므로 16bpp라고 해야 정확하다고 볼 수 있습니다.
== 16bpp
넘어 갑니다.
== 24bpp
24bpp이상은 작은 embedded system에서 쓰이는 경우는 거의 없습니다. 아무래도 bpp가 커질수록 메모리에 대한 부담이 크게 작용하기 때문입니다. Embedded system에 사용된다면 대부분 TV(settop)와 같이 display를 주로 이용하는 system의 경우가 많습니다. 24bpp를 보통 true color라고 부릅니다. 24bpp에서 한 pixel의 값은 R,G,B가 각각 8bit(1byte)씩인 모여서 24bit를 이루게 됩니다. 따라서 프로그래밍을 위해서는 offset을 계산한 후에 R,G,B 1byte씩을 써 주면 됩니다.
(R,G,B가 1 byte씩이라는 것은 일반적인 경우입니다. 정확하게 R,G,B가 어떤 bit를 점유하고 있는지는 16bpp에서와 마찬가지로 struct fb_var_screeninfo 내부의 struct fb_bitfield타입의 red, green, blue를 각각 참조를 해야 알 수 있습니다.)
== 32bpp
32bpp도 24bpp와 마찬가지로 true color라고 부릅니다. 24bpp와 동일하게 R,G,B값이 각각 8bit(1byte)로 coding됩니다. 나머지 8bit를 우리는 보통 alpha channel(이하 alpha)이라고 부릅니다. alpha는 그 pixel의 투명도(transparency)를 나타냅니다. 위의 colormap에서 나오는 transp라는 포인터와 struct fb_var_screeninfo내부의 struct fb_bitfield transp라고 하는 부분도 이에 해당합니다. 투명도에 대해서 감이 없는 분은 http://www.directfb.org/screenshots/gimp.png 을 열어보기 바랍니다. 반투명한 image들이 막 섞여 있는 것이 보일 겁니다. 보통 Alpha와 R,G,B를 합쳐서 ARGB라고 부르는 경우도 있습니다. 프로그래밍 방법은 역시 크게 다르지 않습니다. 한 pixel이 4 byte의 크기를 가지므로 4 byte단위의 offset을 계산한 후에 4 byte를 적어 주면 됩니다. 쉽게는 그냥 unsigned형 데이터를 직접 쓰면 되겠습니다.
(A,R,G,B가 1byte씩이라는 것은 일반적인 경우를 나타내며 정확히 A,R,G,B에 대한 bitoffset을 계산하기 위해서는 struct fb_bitfield red, green, blue, transp를 참조하면 됩니다.)
== 몇 가지 남은 이야기
왜 9bpp라는 것은 잘 사용되지 않을까요? R,G,B를 각각 3bit씩 coding하면 아주 쉬울 것 같은데 말이죠. 그 이유는 9bpp로 하면 offset계산과 9bit를 읽고 쓰기가 불편하기 때문일 겁니다. offset계산이야 당연히 byte단위와 bit단위로 나누어 계산한다고 하더라도 항상 2byte를 읽어와서 bit masking을 하고 그 값을 다시 적는 것이 까다롭겠죠. 잘 이해가 안되는 사람은 직접 코딩을 해보면 쉽게 이해할 수 있을 겁니다.
16bpp 이상의 그래픽 시스템에서는 일반적으로 palette mode를 사용하지 않습니다. 그 이유는 어짜피 palette도 메모리의 어딘가에 저장하고 있어야 하는데 bpp가 커질수록 palette의 사이즈가 기하급수적(?)으로 증가하기 때문입니다. 예를 들어 320x240 16bpp 그래픽 시스템에서 palette mode를 사용한다고 가정하죠. 그리고 각 palette안에 들어 있는 pixel은 24bpp로 코딩되어 있다고 할 때 palette를 위해서 필요한 메모리의 크기는 24bit*2^16 = 196608byte가 됩니다. 또한 pixel값을 저장할 메모리의 크기는 320*240*16bit=153600byte이므로 시스템은 총 350208byte의 메모리를 사용합니다. 그런데 320x240 24bpp 그래픽 시스템이 palette mode를 사용하지 않으면 pixel값을 저장할 메모리만 필요하므로 총 320*240*24bit = 230400byte만 필요합니다. 당연히 320x240 16bpp palette mode에서 display할 수 있는 image는 모두 화질 저하 없이 320x240 24bpp 시스템에서 display할 수 있습니다. 이런 결과를 보고 누가 16bpp 이상의 그래픽 시스템이 palette mode를 사용하겠습니까?
bmp파일을 16bpp frame buffer상에 display하는 프로그램에서 24bpp로 encoding된 bmp파일도 display가 가능했습니다. 24bpp pixel을 16bpp pixel로 변환할 때 R,G,B각각의 하위 2~3비트씩을 떼어내었다는 것을 코드를 살펴본 사람은 알 수 있을 겁니다. 당연히 24bpp frame buffer에 display하는 것보다 화질 저하가 생겼지만 어쩔 수 없습니다. 16bpp로 나타낼 수 있는 색상의 수가 24bpp보다는 적으므로 피할 수 없습니다. 실제로 화질 저하를 좀더 줄이려면 bmp파일 내의 각 pixel의 R,G,B값의 통계적인 분포를 살펴보고 그에 따라서 하위 비트를 떼어낼지 아니면 상위 비트를 떼어내어 saturation시킬 지 결정해야 합니다. 예를 들어 24bpp로 encoding된 bmp파일이 R,G,B값을 하위 두 비트만 사용하여 매우 어두운 image였다면 제가 수행한 방법으로는 완전히 검은 image를 얻을 수 밖에 없습니다.
그럼 24bpp로 encoding된 bmp파일을 8bpp(palette mode) frame buffer상에 display하려면 어떻게 해야 할까요? 16bpp의 경우는 그래도 pixel의 값이 R,G,B 값 자체로 코딩되어 있으므로 적당히 bit를 떼어내면 쉽게 가능합니다. 하지만 Palette mode를 사용하면 256가지 색상은 원하는 색상을 마음대로 골라서 사용할 수 있지만 다른 색깔은 어떻게 표현할 방법이 없습니다. 혹시 24bpp로 encoding된 bmp파일이 256색 이하만 사용한다면 그 사용된 색으로 palette를 업데이트하고 display하면 화질 저하가 전혀 없는 image를 얻을 수 있습니다. 하지만 그럴 가능성은 일반적으로 매우 적죠. 이런 경우 bmp파일을 살펴봐서 bmp파일에서 적당히 256가지 색상을 찾은 다음 그 256가지 색상으로 bmp파일의 각 pixel을 이미 고른 256개의 색상중에 가장 가까운 색상으로 모두 바꾸어서 화질 저하를 최소화하려고 노력합니다. 당연히 화질 저하가 최소화되려면 256가지 색상을 잘 선택해야 합니다. 이런 과정을 전문 용어로는 dithering(디더링)이라고 표현한다고 합니다. 사실 전 dithering의 구체적인 방법은 전혀 아는 바가 없습니다. 그래서 처음부터 frame buffer이야기를 16bpp로 한정한 것입니다.(아! 이 잔머리 놀랍지 않습니까?) bmp파일 display 예제로 올렸던 압축 파일 내부의 kelp_logo_1bpp.bmp와 kelp_logo_4bpp.bmp는 KELP 사이트의 logo(24bpp)를 PC용 프로그램으로 dithering해서 만든 이미지입니다. kelp_logo_1bpp는 당연히 흑백이니까 차이가 나고 kelp_logo_4bpp도 잘 살펴보면 원본 이미지와 차이가 나는 것을 볼 수 있습니다.
마지막으로 24bpp나 32bpp를 왜 true color라고 부를까요? 이건 학교 다닐 때 수업시간에 얼핏 들은 이야기인데 보통 사람의 눈으로는 256개 이상의 gray level로 encoding된 image를 256개의 gray level image와 구별하기 힘들다고 합니다. 즉 똑 같은 image를 512(9bit)개의 gray level로 나타내든지, 1024(10bit)개의 gray level로 나타내든 256 gray level로 나타낸 모습과의 화질 차이는 보통 사람 눈에는 안 보인다고 합니다. 어찌 보면 사람의 가청 주파수가 20Hz~20kHz라는 것과 같은 사람의 한계라고 볼 수 있습니다. 물론 백마 타고 온 초인이 간혹 존재해 구분해 내겠지만요. 따라서 R,G,B를 각각 256(8bit)단계로 표현한 24bpp와 32bpp를 true color라고 부릅니다. 마케팅적인 요소가 있는지 아니면 초인들이 늘어났는지 알 수는 없지만 DVD player에서 사용하는 DAC(Digital to Analog Converter)은 보통 R,G,B(또는 Y,Cb,Cr)를 각각 10bit씩 사용하고 PC 시스템에서도 최근에 나온 matrox Parhelia와 같은 그래픽 카드는 R,G,B값을 8bit이상으로 표현할 수 있는 경우가 있습니다. Matrox Parhelia 그래픽 카드는 너무 비싸서 선뜻 사기도 힘들고 저와 같은 막눈으로는 화질이 좋아졌다고 판단할 수 없죠.
== 마치며
간단히 적으려고 했는데 많이 길어졌습니다. 너무 길어져서 그냥 휙 넘어간 부분도 많은데 그냥 이해하기 바랍니다. 이번 글에서 제일 중요한 이야기는 palette에 관한 것이었고 상식으로 alpha channel이나 이런 것들을 기억해 두면 됩니다. 이번 글은 사실 frame buffer뿐 아니라 대부분의 그래픽 시스템과 그래픽 파일에도 동일하게 적용되는 부분입니다.
다음 글이 frame buffer 이야기의 마지막 글일 겁니다. frame buffer를 사용하는 몇 가지 프로그램들에 대한 소개하고 frame buffer의 한계 등에 대해서 다룰 예정입니다. 빨라도 다음 주말에나 글을 쓸 수 있지 않을까 생각이 듭니다.
(푸념 한마디) 지금 사용하고 있는 MS Windows PC의 그래픽 카드(matrox G400)가 맛이 가려나 봅니다. 툭하면 화면에 줄이 가고 다운 되고 그러네요. 드라이버 문제라면 다시 설치해 보겠지만 잘 모르겠네요. 가뜩이나 궁핍한 생활에 그래픽 카드마저 속을 썩이니…혹 Hercules 그래픽 카드를 아직까지 골동품(?)으로 소장(?)하고 있는 분은 저한테 택배로 부쳐 주시기 바랍니다. PC에 ISA slot이 있었나? 앗, 그러고 보니 MS Windows가 Hercules 카드를 지원하지 않을 듯. Linux 만세! 후후…
== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.
== 시작
이번 글이 frame buffer 이야기의 마지막 글이 될 겁니다. 이제까지 linux frame buffer를 이용하여 화면에 그림을 그리는 것에 대한 내용을 모두 마쳤다고 볼 수 있습니다. 현실적인 제약으로 인해 모든 frame buffer에 대해서 테스트를 해보거나 할 수는 없었지만 그래도 어느 정도 프로그래밍을 위한 기초적인 정보는 모두 살펴봤다고 볼 수 있습니다. 이번 글에서는 “frame buffer 프로그래밍으로 무엇을 할 수 있나?”에 대해서 알아본 후에 frame buffer가 실제로 사용되고 있는 프로그램들에 대해서 짤막하게 소개하도록 하겠습니다. 그 다음 linux frame buffer의 한계에 대해서 생각해 보는 시간을 갖도록 하겠습니다. 준비가 되었다면 시작하도록 하죠.
== frame buffer 응용
이제 까지 frame buffer를 이용하여 화면에 나타날 pixel을 찍거나 이미 화면에 있는 pixel을 읽을 수 있다는 것을 알아보았습니다. 그럼 이런 방법론을 이용하여 실제로 응용할 수 있는 분야는 어떤 것이 있을까요? 당연히 2차원 컴퓨터 그래픽을 전부를 할 수 있습니다. 2차원 컴퓨터 그래픽을 사용하고 있는 분야는 상당히 많이 있죠. 하지만 실제 embedded system에서 사용할 만한 가능성이 있는 것들만 추려보면 다음과 같습니다.
(1) 그래픽 파일 viewer나 동영상 player
(2) GUI(Graphic User Interface)
더 생각이 나지 않는 군요.(더 좋은 응용 분야를 알고 있는 사람은 리플을 달아주기 바랍니다.)
(1) 번이야 쉽게 이해가 되죠? 예를 들어 요즘 지하철 열차 내에 설치되어 있는 LCD를 이용한 광고 장치(?, OS로는 MS Windows를 사용하는 것으로 보입니다. 따라서 열차 내의 어딘가에는 PC가 있을 것으로 보입니다. 오로지 출력만 여러 LCD로 연결했을 가능성이 큽니다.)와 같이 일방적으로 사람들에게 화상 정보를 보내는 장치는 frame buffer를 이용해서 프로그래밍하면 될 겁니다.(지하철 역사에는 PDP와 프로젝션이 설치되고 있는 추세더군요.)
GUI의 경우도 역시 2차원 컴퓨터 그래픽을 이용하는 분야이므로 frame buffer를 이용할 수 있습니다. 물론 GUI는 어디까지나 사용자의 입력도 받아야 하므로 사용자의 입력을 받을 수 있는 장치가 필수죠. 대부분 마우스나 터치스크린과 같은 pointing device와 같이 이용합니다. (1)이 GUI위에 올라갈 수도 있죠.
== linux frame buffer의 장점
linux 시스템에서 frame buffer를 이용할 때 장점은 딱 한가지 있습니다. 포팅의 용의성입니다. 예를 들어 frame buffer에 결과를 출력하는 동영상 player를 만들었다고 하면 그 player는 오로지 linux kernel(frame buffer driver포함)에만 의존적(dependent)입니다. 또한 비교적 frame buffer 프로그래밍 interface가 잘 되어 있고(문서는 어디 있는지 찾기 어렵지만), 그것이 하드웨어(그래픽 카드)에 의존적이지 않습니다. 결국 의존성(dependency)이 작으므로 한번 작성된 프로그램을 여러 embedded target으로 포팅하기 쉽게 됩니다.
== linux frame buffer를 이용하고 있는 프로그램들
linux에서 frame buffer를 이용하고 있는 프로그램들의 범주 역시 위에서 살펴본 “응용”과 별반 다르지 않습니다. 실제로는 더욱 많이 이용되고 있겠지만 사람들의 입에 자주 오르내리는 몇가지 프로그램만 소개하도록 하겠습니다.
= 동영상 player
(*) Xine(http://xine.sourceforge.net/)
(*) MPlayer(http://www.mplayerhq.hu/)
Linux에서 가장 유명한 두 개의 동영상 player입니다. 두 프로그램 모두 많은 동영상 코덱(codec)을 지원하고 많은 video output driver와 audio output driver를 지원합니다. 지원하는 video output driver 중에 linux frame buffer가 있습니다.
= GUI
(*) X server(http://www.xfree86.org)
X 를 빼 놓고는 linux의 GUI를 이야기할 수 없죠. XFree86은 사실상 PC를 주된 target으로 합니다. 사이트에서 살펴보면 PC에서 사용되는 많은 그래픽 카드를 나열해 놓고 지원한다고 말하고 있습니다. 특정 그래픽 카드에 맞게 최적화된 드라이버를 계속 만들어 내고 있습니다. 그 드라이버가 하드웨어를 직접 제어해서 화면을 구성합니다.
XFree86에서 지원하는 드라이버 중에 linux frame buffer도 존재합니다. 보통 linux frame buffer를 이용하도록 컴파일된 X server를 TinyX라고 부릅니다.
(*) Qt/Embedded(http://www.trolltech.com/products/embedded/)
KDE에 주로 이용되고 있는 toolkit인 Qt는 trolltech에서 만들었습니다. 그 toolkit을 embedded system으로 포팅한 것을 Qt/Embedded라고 한다고 합니다. PC에서는 Qt toolkit이 화면에 무엇인가를 나타내려고 할 때 X를 사용했겠지만, X가 올라가 있지 않는 embedded system에서는 frame buffer를 이용할 수 밖에 없습니다.(위에서 설명한 의존성이 가장 작은 interface이기 때문이죠.)
참고로 Qt/Embedded는 license를 받아야 하는 상용 프로그램으로 알고 있습니다.
(*) microwindows(http://www.microwindows.org/)
Opensource GUI에서 유명한 녀석이죠. 역시 linux frame buffer를 지원합니다.
== linux frame buffer의 한계
frame buffer를 이용하여 그래픽을 출력하는 프로그램이 과연 성능이 좋을까요? 성능이 좋다면 XFree86에서 계속 그래픽 카드에 포팅하고 있지도 않고 PC에서 동작하는 X도 frame buffer를 이용할 겁니다. 그런데 여전히 새로 나오는 그래픽 카드에 포팅은 진행하고 있고, PC linux에서는 frame buffer가 없이도 X는 잘 동작합니다. 결국 frame buffer는 성능이 좋은 그래픽 interface는 아니라는 말입니다.
현재 시장에 나오고 있는 그래픽 카드는 3D 가속기능을 가지고 있습니다. Frame buffer야 3차원과는 거리가 있으니까 무시해도 됩니다. 그런데 MS Windows 3.1이나 MS Windows95가 시장에서 힘을 쓰고 있던 시절에는 그래픽 카드들이 모두 “Windows Acceleration”이란 광고를 하고 있었습니다. 이 “Windows Acceleration”이라는 기능이 2차원 그래픽 가속기능인데 현재 시장에 있는 그래픽 카드는 모두 이런 기능을 가지고 있습니다. 이 기능을 간단히 이야기하면 bitblt(bit-blit이라고 발음되고 bit block transfer의 약자임)이라고 하여 그래픽 카드가 스스로 bitmap을 화면(그래픽 메모리)의 이곳 저곳으로 여러 가지 연산과정과 함께 옮길 수 있는 기능입니다. 또한 그래픽 카드가 PCI나 AGP의 bus mastering(일종의 DMA)을 이용하여 시스템 메모리에 있는 bitmap을 화면(그래픽 메모리)으로 전송할 수도 있습니다. 그리고 rectangular를 특정 색깔로 채우거나 하는 것도 그래픽 카드가 스스로 수행하죠. 이러한 2차원 그래픽 가속능력은 GUI의 속도를 개선하는데 필요한 핵심 기능 중에 하나입니다.
Frame buffer를 이용한 프로그램은 모두 CPU가 직접 해 줄 수밖에 없습니다. Bmp viewer 예제에서 보았듯이 pixel 하나 하나를 CPU가 직접 그래픽 메모리로 옮겨야 합니다. 또한 그래픽 메모리의 일정 부분을 다른 곳으로 옮길 때도 CPU가 해줄 수밖에 없습니다. 문제는 테스트에서 사용된 ATI mach64나 Permedia2와 같은 그래픽 카드도 그러한 가속기능을 가지고 있는데도 Frame buffer interface로는 그러한 가속 기능을 이용할 수 없다는 점입니다. 즉 하드웨어의 문제가 아니라 interface의 문제인 것이죠. 물론 StrongARM에 있는 LCD controller는 bitblt가 같은 것을 수행하지 못합니다. 그렇다고 embedded linux에서 사용되는 그래픽 하드웨어가 모두 2차원 가속 기능을 가지지 않는다고 단정지을 수는 없습니다. Embedded linux에 국한 시켜봐도 frame buffer interface가 완전하다고 말할 수는 없습니다.
(linux frame buffer driver 내부에서 console(console on FB)을 scroll할 때 그래픽 카드의 가속 기능을 이용하는 경우가 있기는 합니다. 하지만 유저 프로그램이 그런 기능을 직접 이용하는 방법은 제가 아는 한 없습니다.)
완전한 interface라면 그래픽 하드웨어가 가지고 있는 모든 가속기능을 이용할 수 있도록 해야 합니다. StrongARM의 LCD controller와 같이 그런 기능이 없으면 그것을 CPU가 직접 소프트웨어로 처리하고, 있으면 최대한 하드웨어를 이용할 수 있는 interface가 필요합니다.
(*) DirectFB(http://www.directfb.org)
아마도 이름은 MS의 DirectX에서 따온 것으로 보입니다. 위에서 설명한 완벽한 interface를 제공하기 위해 열심히 뛰고 있는 사람들입니다. Linux frame buffer와 그래픽 하드웨어용 루틴을 합쳐서 동일한 interface를 제공하도록 라이브러리화한 것으로 보입니다. 이 DirectFB위 GTK+ toolkit을 포팅한 것도 찾아볼 수 있습니다.(참고로 GTK+는 free입니다.)
== 글을 모두 마치며
이제까지 linux frame buffer에 관련된 많은 내용을 알아봤습니다. Frame buffer는 PC에서도 사용할 수 있기는 하지만 전체적으로 embedded target을 위한 interface라고 볼 수 있습니다. Linux Framebuffer HOWTO와 같은 문서를 보면 PC에서는 결국 부팅할 때 “펭귄”을 보려고 삽질을 한다고 나옵니다. 아무래도 PC는 X를 사용하기 때문에 별로 의미가 있는 그래픽 interface라고 하기는 어렵습니다. Embedded target에서 frame buffer 프로그램을 작성할 때 도움이 되고 사람들이 frame buffer 프로그래밍이 별 것 아니구나 하는 생각이 들었으면 좋겠다는 조그만 바람으로 시작한 글이었는데, 결과가 어떨지는 잘 모르겠습니다.
그리고 전체적으로 급히 작성을 해서 글의 완성도가 여러모로 떨어지고, 약간은 장난기가 섞여 있는 것으로 보입니다. 그냥 이해하고 봐주셨으면 좋겠습니다. 아무튼 여기까지 읽어 주신 분들에게 감사 드립니다.
다음 기회에는 좀더 좋은 주제로 글을 잘 적겠습니다.
혹, 다루었으면 하는 좋은 주제가 있으면 리플이나 메일로 알려주시기 바랍니다. 물론 제가 아는 것이 별로 없으므로, 모든 주제에 대해서 글을 적을 수 있다고 할 수는 없습니다. 그리고 하드웨어에 의존적인 부분에 대해서는 테스트를 할 수 없으니 그런 주제도 피해야 하구요. 또한 제 관심사에 벗어난 일(예를 들어 GUI 프로그래밍)이라면 당연히 안되겠죠. 푸후…이렇게 하고 나니 글을 적을 만한 주제가 없겠네요. 다시 질답란에서만 활동하며 글을 적을 만한 주제를 찾아야겠습니다.
반응형
'Linux > Linux 일반' 카테고리의 다른 글
우분투 8.10 에서 프레임 버퍼 띄우기 (0) | 2009.02.08 |
---|---|
gcc 이야기 (0) | 2009.02.07 |
Linux 시스템 부팅과정 (0) | 2009.02.07 |