이 문서는 The Linux Kernel Module Programming Guide을 참고했으며, 많은 부분 원문을 그대로 번역하였다. 그러나 본문을 실제 테스트 하면서 내용이 미흡한 부분을 보완하였으며, 몇몇 틀린 부분에 대한 수정도 이루어졌다. 커널 모듈에 대해서 이해하고 프로그래밍을 하기 위해서 당연히 여러분은 C언어와 리눅스 시스템에 대한 기본적인 이해를 하고 있어야 한다. 이 문서는 리눅스(유닉스) 시스템과 C에 대한 기본 이해를 하고 있다는 가정하에 작성될 것이다. 커널 모듈이란 필요에 따라 커널에 로드하거나 언로드 할 수 있는 특정한 기능을 수행하는 코드(프로그램)이다. 이렇게 하므로써 쉽게 커널의 기능을 확장할 수 있을 뿐만 아니라 운영체제를 리부팅 하지 않고도 원하는 기능을 수행할 수 있도록 만들 수 있다. 예를 들어서 어떤 하드웨어를 제어하기 위한 문자 장치(device drive)를 작성해야 한다고 생각해보자. 만약 모듈기능을 제공하지 않는 커널이라면 커널을 직접수정하는 방식을 동원해서 커널에 필요한 기능을 추가시켜야 할 것이다. 프로그램 자체가 어려워지는 것은 물론이고 기능을 테스트 하기 위해서는 계속적인 리부팅 작업이 필요하게 되므로 개발기간 역시 극적으로 늘어날 수 밖에 없을 것이다. 또한 커널에 필요한 기능이 추가될 때마다 커널에 계속해서 코드가 추가 됨으로 커널의 크기도 매우 커지게 될것이다. 사운드카드를 위한 기능을 추가했는데 해당 사운드카드를 가지지 않는 유저도 있을 것이다. 이럴 경우는 그야말로 쓸데 없는 자원낭비가 되는 셈이다. ` 커널 모듈로써 작동하도록 만들었다면 쓸데없는 기능을 하는 모듈은 언로드 시키면 그만이다. 이 문서는 리눅스 커널 2.4를 기준으로 작성되었다. 현재 커널에서 작동중인 모듈의 목록은 lsmod 명령을 통해서 확인할 수 있다. 그럼 커널은 이러한 모듈을 어떻게 찾아서 적재시키는 걸까. 커널이 어떤 모듈을 포함하고자 할때 해당 모듈이 아직 커널에 적재되어 있지 않다면 모듈 데몬(daemon)인 kmod가 modprobe를 실행시켜서 모듈을 읽어들이게 된다. 이때 modprobe는 다음중 하나의 방법을 이용해서 읽어들여야할 모듈을 찾게 된다. softdog, ppp와 같은 모듈이름을 직접 찾는다. char-major-10-30 과 같은 일반적인 식별자(generic identifier)를 이용한다. 만약 modprobe가 식별자를 이용할 경우, 해당 식별자에 대한 진짜 모듈이름을 알아와야 할것이다. 이에 대한 정보는 /etc/modules.conf에 저장되어 있다. 다음 modprobe는 /lib/modules/version/module.dep파일을 검사한다. 여기에는 해당모듈이 실행되기위해 필요한 다른 모듈들 즉 모듈의존성에 관한 정보들이 있어서 softdog.o를 적재하기 위해서 다른 모듈이 필요한지 확인하고 미리 적재시킨다. 이 파일은 depmod -a명령으로 생성시킬 수 있다. 예를 들어 msdos.o 모듈은 fat.o모듈이 우선적으로 적재되어 있어야만 한다. modprobe는 modeule.dep파일을 참조해서 의존성을 검사하게 된다. 마지막으로 modprobe는 insmod를 이용해서 원하는 모듈을 적재하기 위해서 우선적으로 필요한 모듈을 적재시키게 된다. insmod는 /lib/modules/version/을 직접참조해서 모듈을 적재한다. 최종적으로 여러분이 msdos 모듈을 올리기를 원한다면 다음과 같이 하면 된다. 리눅스에서 사용되는 modprobe, insmod, depmod와 같은 프로그램은 modutils(혹은 mod-utils) 패키지에 포함된다. 그럼 /etc/modules.conf를 간략하게 살펴보고 이번장을 끝마치도록 하겠다. path[misc]는 misc모듈을 찾을 경로의 지정을 위해서 사용된다. alias 는 kmode가 식별자 eth0을 호출 했을 때 ne2k-pci를 호출하도록 한다. alias는 꽤 중요하게 사용될 수 있는데 하나의 시스템에 동일한 장치가 2개 이상 붙어 있을때 이를 식별할 수 있도록 해준다. 어떤 역사적인 이유가 있는지 모르겠지만 대부분 프로그래밍입문 을 하는데 있어서 가장 먼저 "Hello World"를 출력하는 코드를 장성하는 데에서 부터 시작한다. Hello World 출력 코드와 관련된 재미있는 글이 있는데 한번 읽어 보기 바란다. Hello World의 변천사 여기에서도 "Hello World"를 출력하는 코드를 만드는 것으로 모듈 프로그래밍으로의 발걸음을 내딛도록 하겠다. 이것은 매우 간단한 모듈인데, 아직 컴파일 방법을 다루지는 않을 것이다. 모듈 컴파일은 2.3절에서 다루도록 하겠다. 일반적으로 printk를 이용하면 (함수이름의 어감 때문에) 특정한 메시지를 표준출력할 것으로 생각하는 경우가 많은데 printk는 유저를 위한 어떤 출력도 하지 않는다. 이름과는 달리 로그나 경고 메시지를 남기기 위한 커널로깅 목적으로 사용된다. 일반적인 로그관련 라이브러리나 함수들이 그렇듯이 printk도 우선순위(priority)를 가진다. 모두 8단계의 우선순위를 가지며 <1> KERN_ALERT 와 같은 방식으로 결정할 수 있다. 이들 우선순위에 대한 선언정보는 linux/kernel.h에서 확인할 수 있다. 만약 우선순위를 정하기 귀찮거나 정할 수 없다면 기본 우선순위 DEFAULT_MESSAGE_LOGLEVEL을 사용하면 된다. 만약 syslogd와 klogd가 실행중이라면 메시지는 /var/log/messages에 추가 된다. 다음은 실제 저장된 로그들이다. 커널 모듈을 컴파일하기 위해서는 특별한 gcc 옵션과 더불어 몇가지 값들의 정의(symbols define)가 필요하다. 이유는 커널모듈 컴파일시 사용되는 커널 헤더들이 커널버젼에 매우 의존적일 수 있기 때문이다. 이러한 정의는 gcc의 -D옵션을 이용하거나 혹은 #define 선행처리자를 이용하면 된다. 이번 장에서는 커널컴파일을 하기 위해서 필요한 내용들에 대해서 다룰 것이다. -c : 커널모듈은 독립적으로 실행되지 않으며 (main함수 자체를 포함하고 있지 않다) object파일 형태로 커널에 링크되어서 실행된다. 결과적으로 -c 옵션을 이용해서 오브젝트 형태로 만들어 주어야 한다. -O2 : 커널은 inline함수를 매우 많이 사용하며, 그런 이유로 모듈은 반드시 최적화(optimization) 옵션을 사용해서 컴파일 되어야 한다. 최적화 옵션을 사용하지 않을 경우 어셈블러 매크로등을 사용하는데 있어서 문제가 생길수 있다. 이럴경우 모듈의 적제가 실패하게 될것이다. -D__KERNEL__ : 이 코드가 유저 프로세스가 아닌 커널모드에서 작동할 것이라는걸 커널헤더에 알려준다. -W -Wall : 모듈 프로그램은 커널에 매우 민감한 영향을 끼칠 수 있으며 커널을 다운 시킬 수도 있다. 그러므로 가능한한 모든 종류의 경고메시지를 검사해야할 필요가 있다. 이 옵션을 사용하면 컴파일러가 발생시킬수 있는 모든 경고메시지를 출력한다. -DMODULE : 커널모듈로 작성되는 코드라는걸 알려주기 위해서 사용한다. 참고: -isystem 도 -I 처럼 헤더파일의 경로 지정을 위해서 사용된다는 점에서 비슷하다. -I의 경우 표준 (헤더파일_시스템 경로를 검사하기 전에 -I로 지정된 경로를 먼저 검사하는 반면 -isystem은 가장 마지막에 지정된 경로에 대한 검사를 한다. 다음은 커널 모듈을 컴파일하기 위한 전형적인 Makefile이다. 2.2절에 있는 예제를 보면 init_module()에서 0을 리턴하고 있다. 그런데 다른 값을 리턴하도록 하면 어떻게 될까 ? 지금한번 테스트 해보기 바란다. init함수와 cleanup함수의 이름이 반드시 init_module()와 cleanup_module()로 작성되어야 한다는 것은 (비록 혼동을 줄여주긴 하겠지만) 왠지 이치에 맞지 않는것 같다. 리눅스 커널 2.4부터는 이들 고정된 이름대신 다른 이름으로 사용가능하며, 이를 위해서 module_init()와module_exit()함수를 제공한다. 예제 : hello_re.c 커널모듈은 다른 시스템/유저프로그램에 비해 운영체제에 더욱민감한 영향을 끼칠 수 있다. 그런이유로 최소한 커널모듈에는 커널작성자에 대한 정보가 들어가도록 작성하는게 좋을 것이다. 리눅스 커널 2.4이상에서 지금까지 우리가 작성한 커널 모듈을 적재하려고 하면 다음과 경고 메시지를 출력할 것이다. 참고: 라이센스정보관련 경고메시지 출력은 커널 옵션을 어떻게 주고 컴파일 했느냐에 따라 출력되지 않을 수도 있다. 몇몇 배포판의 경우 경고메시지가 출력되지 않을 것이다. 이와 비슷하게 MODULE_DESCRIPTION()과 MODULE_AUTHOR()매크로를 이용해서 모듈의 원저작자와 모듈에 대한 간단한 설명을 곁들일 수도 있다. 이러한 모든 매크로는 linux/module.h에 정의 되어있다. 이들 매크로 값들은 커널에 의해서 직접 이용되지는 않지만objdump와 같은 도구를 이용할때 모듈에 대한 정보를 얻는데 도움을 준다. 예제 : hello_li.c 커널 모듈도 명령행 인자를 받아들일 수 있다. 그러나 일반적으로 이용하는 argc/argv 기법을 사용할 수는 없다. 모듈로의 아규먼트 전달은 MODULE_PARM()매크로를 통해서 이루어진다. MODULE_PARM()매크로는 2개의 인자를 가진다. 첫 번째 인자는 값이 저장될 변수명이고, 두번째 인자는 저장될 데이터의 타입을 나타낸다. 데이터 타입은 "b" : 바이트, "h": short int, "i": integer, "l": long int, "s":string(문자열)가 있다. 문자열은 char * 타입이며 insmod로 호출될때 메모리가 할당된다. 다음은 간단한 활용예이다. 배열도 지원되는데, '-'를 이용해서 배열의 최소크기와 최대크기를 지정할 수 있다. 이는 주어질수 있는 인자의 최소와 최대 갯수를 정할 수 있음을 의미한다. 이제 실제 모듈을 실행시키면서 인자를 넘기는 방법을 알아보도록 하자. 인자는 [변수명]=[값]의 형태로 넘어간다. 만약 모듈 코드상에 MODULE_PARM(myint, "i"); 로 되어 있다면 다음과 같은 방법으로 인자를 넘긴다. 배열의 경우에는 인자가 지정한 최대/최소의 범위를 벗어날 경우 에러메시지를 출력하며 모듈이 적재되지 않는다. 일반 애플리케이션에서 수행하는 argc를 통한 아규먼트 갯수 검사와 비슷한 형태라고 보면 된다. 배열의 각 요소는 ','를 통해서 구분되어 진다. 예제 : hello_arg.c 보통 조금이라도 규모가 있는 시스템/유저 애플리케이션을 작성할 때는 소스의 관리를 위해서 함수/기능별로 소스를 분할해서 컴파일한다. 커널 모듈역시 이러한 분할 컴파일을 지원하는데, 아래의 형식을 따라주어야 한다. 모든 소스파일 혹은 하나 이상의 소스파일에 #define _NO_VERSION__ 이 포함되어 있어야 한다. 모듈 컴파일을 위해서 포함시키는 module.h 내에 커널 버젼정보가 포함되어 있으며 이 정보는 모듈에 전역적으로 사용되므로 _NO_VERSION__의 사용은 꽤나 중요해진다. 만약에 version.h를 직접 포함시켜야 되는 경우가 생긴다면 _NO_VERSION__을 정의하기 바란다. module.h에는 이게 정의되어 있지 않기 때문이다. 일반적인 방법으로 컴파일한다. 만들어진 여러개의 오브젝트파일을 하나로 만들어 줘야 한다. x86하에서는 d -m elf_i386 -r -o <module name.o> <1st src file.o> <2nd src file.o> 예제 : start.c 예제 : stop.c 다음은 컴파일을 위한 Makefile이다.1절. 소개
1.1절. 이 문서에 대해서
1.2절. 커널 모듈이란 ?
2절. 커널 모듈 프로그래밍의 기본
2.1절. 커널에 모듈 적재시키기
lsmod는 /proc/modules 파일의 내용을 그대로 출력한다.
# lsmod
Module Size Used by Tainted: P
via82cxxx_audio 18304 1 (autoclean)
uart401 6560 0 (autoclean) [via82cxxx_audio]
ac97_codec 9504 0 (autoclean) [via82cxxx_audio]
sound 59052 0 (autoclean) [via82cxxx_audio uart401]
soundcore 4324 4 (autoclean) [via82cxxx_audio sound]
autofs 10948 0 (autoclean) (unused)
ne2k-pci 5568 1
8390 6736 0 [ne2k-pci]
ipchains 37704 0
ide-scsi 8192 0
scsi_mod 95848 1 [ide-scsi]
ide-cd 27360 0 (autoclean)
cdrom 28480 0 (autoclean) [ide-cd]
usb-uhci 21764 0 (unused)
usbcore 51744 1 [usb-uhci]
ext3 61568 4
별칭목록을 확인함으로써 식별자를 위해서 softdog.o모듈을 적재시켜야 된다는 정보를 얻을 수 있게된다.
alias char-major-10-30 softdog
그러나 위와 같이 할경우 모듈 의존성을 직접 검사해줘야 하는데, 이럴 경우 modprobe를 이용하면 된다.
# insmod /lib/modules/2.5.1/kernel/fs/fat/fat.o
# insmod /lib/modules/2.5.1/kernel/fs/msdos/msdos.o
# modprobe -a msdos
'#'은 주석을 위해서 사용되며 공백라인은 무시된다.
# This file is automatically generated by update-modules
path[misc]=/lib/modules/2.4.?/local
alias eth0 ne2k-pci
alias eth1 ne2k-pci
2.2절. 초간단 모듈제작 : Hello World
커널모듈은 최소한 2개의 함수를 가지고 있어야만 한다. 하나는 init_module()라는 이름의 시작(초기화)함수로써 insmod에 의해서 커널로 적재될때 호출된다. 다른 하나는 cleanup_module()라는 이름의 종료함수로써 rmmod를 호출해서 모듈을 삭제할때 호출된다.
#include <linux/module.h>
#include <linux/kernel.h>
int init_module(void)
{
printk("<1>Hello World 1.\n");
return 0;
}
void cleanup_module(void)
{
printk(KERN_ALERT "Goodbye world 1.\n");
}
2.2.1절. printk()에 대해서
Oct 6 01:15:39 localhost kernel: Hello World 1.
Oct 6 01:16:11 localhost kernel: Goodbye world 1.
2.3절. 커널 모듈 컴파일 하기
이외에도 컴파일에 사용될 헤더파일을 찾기 위해서 -I대신에 -isystem을 사용하며 "unused varaiable"과 같은 경고 메시지의 출력을 위해서 -W -Wall을 이용할 것이다. -isystem은 gcc-3.x이상에서 지원되는 옵션이다.
쉽게 이해 가능할 것이다. make를 실행하면 hello.c를 컴파일하고 그결과 커널 모듈(오브젝트) 파일인 hello.o를 생성해낸다. 생성된 커널 모듈은 insmod ./hello.o를 통해서 적재 할 수 있다. 이걸로 당신은 최초의 커널 모듈작성에 성공했다. 예상외로 간단하지 않은가 ? 적재된 커널모듈은 rmmod hello로 제거할 수 있다. printk()출력은 /var/log/message에 쌓일 것이다. 확인해 보기 바란다.
TARGET := hello
WARN := -W -Wall -Wstrict-prototypes -Wmissing-prototypes
INCLUDE := -isystem /lib/modules/`uname -r`/build/include
CFLAGS := -O2 -DMODULE -D__KERNEL__ ${WARN} ${INCLUDE}
CC := gcc
${TARGET}.o: ${TARGET}.c
.PHONY: clean
clean:
rm -rf ${TARGET}.o
2.4절. Hello World 2
#include <linux/module.h>
#include <linux/tty.h>
#include <linux/init.h>
#include <linux/kernel.h>
int hello_init(void)
{
printk(KERN_ALERT "HELLO, World\n");
return 0;
}
void hello_exit(void)
{
printk(KERN_ALERT "bye bye\n");
}
module_init(hello_init);
module_exit(hello_exit);
2.5절. Hello World 3 : 라이센스와 모듈에 대한 정보
# insmod ./hello.o
Warning: loading ./hello.o will taint the kernel: no license
See http://www.tux.org/lkml/#export-tainted for information about tainted modules
다음은 이들 메크로를 포함시킨 예이다.
# objdump -s hello_li.o
...
0000 6b65726e 656c5f76 65727369 6f6e3d32 kernel_version=2
0010 2e342e32 30006c69 63656e73 653d4750 .4.20.license=GP
0020 4c000000 00000000 00000000 00000000 L...............
0030 00000000 00000000 00000000 00000000 ................
0040 61757468 6f723d79 756e6472 65616d20 author=yundream
0050 3c79756e 64726561 6d406a6f 696e632e <yundream@joinc.
0060 636f2e6b 723e0064 65736372 69707469 co.kr>.descripti
0070 6f6e3d41 2073696d 706c6520 64726976 on=A simple driv
0080 65720064 65766963 653d7465 73746465 er.device=testde
0090 76696365 00 vice.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#define DRIVER_AUTHOR "yundream <yundream@joinc.co.kr>"
#define DRIVER_DESC "A simple driver"
int init_hello_3(void);
void cleanup_hello_3(void);
static int init_hello_4(void)
{
printk("<2>Hello, world 4\n");
return 0;
}
static void cleanup_hello_4(void)
{
printk("<2>Goodbye, world 4\n");
}
module_init(init_hello_4);
module_exit(cleanup_hello_4);
MODULE_LICENSE("GPL");
MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_SUPPORTED_DEVICE("testdevice");
2.6절. 명령행 인자의 처리
int myint = 3;
char *mystr;
MODULE_PARM (myint, "i");
MODULE_PARM (mystr, "s");
int myshortArray[4];
MODULE_PARM(myintArray, "2-4i");
꽤나 독특한 방법으로 넘기고 있음을 알 수 있다.
# insmod ./hello.o myint=50
와 같이 되어 있다고 할때, 다음과 같은 방법으로 값을 넘길 수 있다.
int myarray[4];
MODULE_PARM(myarray, "2-4");
다음은 간단한 예제코드이다.
# insmod ./hello.o myarray=1,4,3
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yundream@joinc.co.kr");
static short int myshort = 1;
static int myint = 420;
static long int mylong = 9999;
static char *mystring = "blah";
MODULE_PARM (myshort, "h");
MODULE_PARM (myint, "i");
MODULE_PARM (mylong, "l");
MODULE_PARM (mystring, "s");
static int __init hello_5_init(void)
{
printk(KERN_ALERT "Hello, world 5\n=============\n");
printk(KERN_ALERT "myshort is a short integer: %hd\n", myshort);
printk(KERN_ALERT "myint is an integer: %d\n", myint);
printk(KERN_ALERT "mylong is a long integer: %ld\n", mylong);
printk(KERN_ALERT "mystring is a string: %s\n", mystring);
return 0;
}
static void __exit hello_5_exit(void)
{
printk(KERN_ALERT "Goodbye, world 5\n");
}
module_init(hello_5_init);
module_exit(hello_5_exit);
2.7절. 모듈별 분할 컴파일
다음은 모듈분할 방식으로 작성된 커널 모듈 예제들이다.
#include <linux/kernel.h> /* We're doing kernel work */
#include <linux/module.h> /* Specifically, a module */
int init_module(void)
{
printk("Hello, world - this is the kernel speaking\n");
return 0;
}
#if defined(CONFIG_MODVERSIONS) && ! defined(MODVERSIONS)
#include <linux/modversions.h> /* Will be explained later */
#define MODVERSIONS
#endif
#include <linux/kernel.h> /* We're doing kernel work */
#include <linux/module.h> /* Specifically, a module */
#define __NO_VERSION__ /* It's not THE file of the kernel module */
#include <linux/version.h> /* Not included by module.h because of
__NO_VERSION__ */
void cleanup_module()
{
printk("<1>Short is the life of a kernel module\n");
}
CC=gcc
MODCFLAGS := -O -Wall -DMODULE -D__KERNEL__
hello.o: start.o stop.o
ld -m elf_i386 -r -o hello.o start.o stop.o
start.o: start.c
${CC} ${MODCFLAGS} -c start.c
stop.o: stop.c
${CC} ${MODCFLAGS} -c stop.c
Trace
- 1장. 리눅스 커널 컴파일하기 (1.1) 2010.09.11
- 임베디드 시스템 엔지니어를 위한 리눅스 커널 분석 2010.09.10
- 리눅스 커널 모듈 프로그래밍 - 2.4 2010.09.09
- 리눅스 모듈 작성법 2010.09.09
- 병렬 포트 구조 2010.09.06
- 다이얼 톤 발생기 2010.09.05
- VHDL을 이용한 디지털 시계 프로그램 구현 2010.09.05 4
- 피씨 관리 프로그램 2010.09.05
- 리눅스 부팅 과정 2010.09.05
- /etc/inittab 2010.09.05
1장. 리눅스 커널 컴파일하기 (1.1)
임베디드 시스템 엔지니어를 위한 리눅스 커널 분석
리눅스 커널 모듈 프로그래밍 - 2.4
리눅스 모듈 작성법
/* hellomodule.c */
// #define MODULE
#include <linux/module.h>
#include <linux/kernel.h>
int init_module()
{
printk("Module init\n");
printk("Hello Linux Module!\n");
return 0;
}
void cleanup_module()
{
printk("Module Cleaned up\n");
}
MODULE_LICENSE("GPL");
# 변수 설정...
KERNELDIR=/lib/modules/$(shell uname -r)/build
CFLAGS=-D__KERNEL__ -DMODULE -I$(KERNELDIR)/include -O
# CFLAGS=-D__KERNEL__ -I$(KERNELDIR)/include -O
CC=gcc
all : test.o
# $(CC) $(CFLAGS) -c test.c
rebuild : clean all
insert : all
insmod test.o
remove :
rmmod test
clean :
rm -rf *.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
int init_hello()
{
printk("Module init\n");
printk("Hello Linux Module\n");
return 0;
}
void exit_hello()
{
printk("Module Cleaned up\n");
}
module_init(init_hello);
module_exit(exit_hello);
MODULE_LICENSE("GPL");
KERENLPATH=/usr/src/linux
obj-m := hello.o
KDIR := /usr/src/linux
PWD := $(shell pwd)
default :
$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
clean :
rm -rf *.o
# insmod hello.o
2.6 커널의 경우
# insmod hello.ko
병렬 포트 구조
실제 병렬포트는 취미로 하는 자작에서 PC쪽의 프로그램과 자작 하드웨어간의 통신을 위해서도 많이 쓰이는데, 그만큼 간단하게 외부기기와 인터페이스 될수 있다는 뜻이겠다.
위는 병렬포트의 물리적인 형상이다. 1번부터 25번까지 25개의 핀이 존재하며, 이중 18번에서 25번까지는 그라운드이다. 2번에서 부터 9번까지 8개의 핀은 PC에서 외부로 신호를 내보내는, 즉 Output으로 사용되며, 10번에서 15번까지 6개의 핀은 외부에서 PC로 신호를 보내는, 즉 Input으로 사용된다. 기타 나머지 핀들은 각각의 정해진 기능및 Input/Output을 모두 수행하는 Bidirectional Input/Output으로 동작한다.
자세한 핀별 구성은 아래와 같다.
1번핀: (Strobe)
2번핀: (Data0)
3번핀: (Data1)
4번핀: (Data2)
5번핀: (Data3)
6번핀: (Data4)
7번핀: (Data5)
8번핀: (Data6)
9번핀: (Data7)
10번핀: (Acknoledge)
11번핀: (Busy)
12번핀: (PaperEnd)
13번핀: (Select)
14번핀: (AutoFeed)
15번핀: (Error)
16번핀: (Init)
17번핀: (SelectInput)
18번핀: (Ground)
19번핀: (Ground)
20번핀: (Ground)
21번핀: (Ground)
22번핀: (Ground)
23번핀: (Ground)
24번핀: (Ground)
25번핀: (Ground)
위의 핀 구성은 프린터에 규격화 되어 있으며, 프린터 포트도 모드(EPP,SPP등)에 따라 각각의 명칭이 달라지거나 접근하는 어드레스가 다르다. 일단 이정도만으로도 Jtag의 구성이 가능하므로 자세한것은 PC 병렬포트의 정확한 스펙문서를 찾아야 할것이다.
다이얼 톤 발생기
void key_handler(int irq, void *dev_id, struct pt_regs *regs) {
unsigned long *keyport;
keyport = (unsigned long *)ioremap(KEY_BASE, 0x10);
key_value = *(keyport) & 0xf;
iounmap(keyport);
// FALLING EDGE Detect...
free_irq(KEY_IRQ, NULL);
set_GPIO_IRQ_edge(0, GPIO_FALLING_EDGE);
request_irq(KEY_IRQ, key_handler1, SA_INTERRUPT, "Keypad", NULL);
wake_up_interruptible(&wq);
}
void key_handler1(int irq, void *dev_id, struct pt_regs *regs) {
key_value = 0xff;
// Rising Edge Detect...
free_irq(KEY_IRQ, NULL);
set_GPIO_IRQ_edge(0, GPIO_RISING_EDGE);
request_irq(KEY_IRQ, key_handler, SA_INTERRUPT, "Keypad", NULL);
wake_up_interruptible(&wq);
}
int dev_open(struct inode *inode, struct file *fp) {
// set keypad interrupt handler
GPDR0 &= ~(GPIO_0);
GAFR0_L &= ~(0x3);
set_GPIO_IRQ_edge(0, GPIO_RISING_EDGE);
return request_irq(KEY_IRQ, key_handler, SA_INTERRUPT, "Keypad", NULL);
}
ssize_t dev_read(struct file *fp, char *buf, size_t len, loff_t *pos) {
interruptible_sleep_on(&wq); // wait until key pressed
copy_to_user(buf, &key_value, 1);
return len;
}
ssize_t dev_write(struct file *fp, const char *buf, size_t len, loff_t *pos) {
return 0;
}
int dev_close(struct inode *inode, struct file *fp) {
free_irq(KEY_IRQ, NULL);
return 0;
}
void SinGenerator(double *buffer, int BUFFSIZE, double f0, double fs, short ampl)
{
double w0;
double m_2cosW0;
const double PI = 4.0 * atan(1.0);
int i;
w0 = 2 * PI * f0 / fs; // fs: sampling frequency, f0 : tone frequency
m_2cosW0 = 2 * cos(w0);
buffer[BUFFSIZE-2] = (double)(ampl * -sin(w0));
buffer[BUFFSIZE-1] = 0;
buffer[0] = (double)(m_2cosW0 * buffer[BUFFSIZE-1] - buffer[BUFFSIZE-2]);
buffer[1] = (double)(m_2cosW0 * buffer[0] - buffer[BUFFSIZE-1]);
for(i = 2 ; i < BUFFSIZE ; i++)
buffer[i] = (double)(m_2cosW0 * buffer[i-1] - buffer[i-2]);
}
먼저 키패드 드라이버 작성은 교수님이 제공해 주신 키패드 드라이버 관련 문서를 보고 작성하였다. 이를 바탕으로 작성된 디바이스 드라이버를 올리고 프로그램 내에서 open한 후 read를 하여 결과를 출력 하는데, 프로그램을 처음 실행 할 때는 아무 문제없이 잘 작동하였다. 하지만 이 프로그램의 두 번째 실행에서 부터는 인터럽트 발생이 폭주 하는 듯, 키를 누르지도 않았는데 이상한 결과가 계속해서 출력이 되는 것이었다. 문제는 프로그램 실행에서 open시 인터럽트를 활성화 시키는데 반해 close시 이 인터럽트를 다시 비활성화 시키지 않는데서 비롯된 것이었다. 첫 번째 실행에서 활성화된 인터럽트를 두 번째 실행에서 다시 활성화함으로써 발생한 것이었다. 이에 close 시스템 콜 구현 부분에서 free_irq()함수를 이용하여 활성화된 인터럽트를 비활성화 시킴으로써 해결할 수 있었다.
톤 발생기 구현은 첨에 막막한 상태였다. 어디서부터 시작해야 될지 몰랐다. 여러 자료를 검색하던 중 MultiMedia Sound Programming 이라는 영진닷컴에서 출판된 책에 특정 주파수 신호 발생에 대한 내용을 찾을 수 있었다. 이 책을 참조하여 특정주파수 신호를 만들어 내는 모듈을 작성할 수 있었다.
첨에 신호를 만들어 호스트 컴퓨터에서 소리를 발생 시켰다. 원하는 결과가 잘 나왔다. 하지만 임베디드 컴퓨터에서는 소리가 나오지 않는 것이었다. 이것 때문에 상당한 시간을 허비하였는데, 알고 보니 임베디드 컴퓨터의 mixer 초기 설정 상태가 mute(음소거)상태였던 것이었다. "mixer vol unmute", "mixer pcm unmute" 명령으로 mute 상태를 해제하니 소리가 잘 나왔다.
마지막으로 키패드 마다 각기 다른 주파수 톤을 발생시키기 위해 처음에는 일반 전화기에서 사용하는 DTMF(Dual Tone Multi-Frequency)를 이용하여 구현하였으나, 약간의 문제로 인해 그냥 각 키마다 임의로 다른 주파수를 발생시키도록 구현하였다. 여기서 DTMF 란 여러 개의 주파수를 가진 이중톤이라는 의미로 서로 다른 2개의 주파수를 합성하여 하나의 주파수로 할당되게 된다. 약간의 문제란 DTMF 원리를 이용하여 신호를 발생시키면, 전화기 버튼을 눌렀을 때 발생하는 톤과 비슷하였지만, 서로 다른 주파수 신호를 합성하는 과정에서 잡음이 상당히 많이 발생해서 듣기가 별로 좋진 않았다. 또한 서로 다른 2개의 신호를 생성하고 합성하는데 상당한 시간이 걸려서, 키패드 버튼을 빠르게 누르고 땠을 경우 Falling Edge 인터럽트를 놓치는 경우가 발생하여, 오동작하는 경우도 생겼다. 물론 키를 누르기 전에 미리 신호를 다 생성시켜 놓으면 되지만, 그렇게 되면 각 키마다 신호를 저장해야 될 버퍼가 필요하게 되므로 비효율적이다. 이러한 문제로 인해 DTMF를 사용하지 않고 각 키마다 임의로 다른 주파수의 신호가 발생되게 하였다.
VHDL을 이용한 디지털 시계 프로그램 구현
FPGA |
Altera Cyclon EP1C6 |
Logic Elements |
5980 |
Logic Gate |
120000 Logic Gate |
RAM Bit |
92160 Bit |
외부 CPU |
PXA255 |
외부 인터페이스 |
32 Bit Address/Data Bus |
입력 I/O |
Push SW, Dip SW, ADC, Image Sensor |
출력 I/O |
Text LCD, LED, 7 Segment, Buzzer, DotMatrix,
VGA |
기타 I/O |
EEPROM |
use ieee.std_logic_1164.all;
entity clk_div is
generic(count : integer range 0 to 30000 := 2);
port(CLK_IN : in std_logic;
SM_CLK : out std_logic );
end clk_div;
architecture sample of clk_div is
begin
process(CLK_IN)
variable tmp : integer range 0 to 30000 := 0;
begin
if(CLK_IN'event and CLK_IN = '1') then
if(tmp = count-1) then
tmp := 0;
SM_CLK <= '1';
else
tmp := tmp + 1;
SM_CLK <= '0';
end if;
end if;
end process;
end sample;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.std_logic_arith.all;
entity DigitalTimer is
port( CLK : in std_logic;
rset : in std_logic;
ih_key : in std_logic;
im_key : in std_logic;
is_key : in std_logic;
FDATA_SM : out std_logic_vector(7 downto 0) := "00111111";
FCOM : out std_logic_vector(5 downto 0) := "001111" );
end DigitalTimer;
architecture rt1 of DigitalTimer is
function fnd_disnum(cnt : integer range 0 to 9) return std_logic_vector is
variable seg_decode : std_logic_vector(7 downto 0);
begin
case cnt is
when 0 => seg_decode := "00111111";
when 1 => seg_decode := "00000110";
when 2 => seg_decode := "01011011";
when 3 => seg_decode := "01001111";
when 4 => seg_decode := "01100110";
when 5 => seg_decode := "01101101";
when 6 => seg_decode := "01111101";
when 7 => seg_decode := "00100111";
when 8 => seg_decode := "01111111";
when 9 => seg_decode := "01100111";
when others => seg_decode := "00000000";
end case;
return (seg_decode);
end fnd_disnum;
-- clock division
component clk_div
generic(count : integer range 0 to 30000);
port( CLK_IN : in std_logic;
SM_CLK : out std_logic );
end component;
-- used signal
signal temp : integer range 0 to 5;
signal cnt_sec1 : integer range 0 to 10 := 0;
signal cnt_min : integer range 0 to 10 := 0;
signal cnt_min1 : integer range 0 to 10 := 0;
signal seg0 : std_logic_vector(7 downto 0) := "00111111";
signal seg1 : std_logic_vector(7 downto 0) := "00111111";
signal seg2 : std_logic_vector(7 downto 0) := "00111111";
signal seg3 : std_logic_vector(7 downto 0) := "00111111";
signal seg4 : std_logic_vector(7 downto 0) := "00111111";
signal seg5 : std_logic_vector(7 downto 0) := "00111111";
signal sclk0, sclk1, sclk2, sclk3 : std_logic := '0';
signal temp_clk : std_logic;
signal fnd_clk : std_logic;
signal sec_clk : std_logic;
begin
LED_CLK_DIV : clk_div generic map(330) port map(CLK, fnd_clk); -- fnd_clk : 100Khz
TEMP_CLK_DIV : clk_div generic map(3300) port map(CLK, temp_clk); -- temp_clk : 10Khz
SEC_CLK_DIV : clk_div generic map(10000) port map(temp_clk, sec_clk); -- sec_clk : 1hz
-- FND Output Process
process(fnd_clk)
begin
if(fnd_clk'event and fnd_clk = '1') then
case temp is
when 0 => temp <=1; FCOM <= "111110"; FDATA_SM <= seg5;
when 1 => temp <=2; FCOM <= "111101"; FDATA_SM <= seg4;
when 2 => temp <=3; FCOM <= "111011"; FDATA_SM <= seg3;
when 3 => temp <=4; FCOM <= "110111"; FDATA_SM <= seg2;
when 4 => temp <=5; FCOM <= "101111"; FDATA_SM <= seg1;
when 5 => temp <=0; FCOM <= "011111"; FDATA_SM <= seg0;
when others => FCOM <= "111111"; FDATA_SM <= "00111111";
end case;
end if;
end process;
-- 1 second Process
process(sec_clk, rset, is_key)
variable cnt : integer range 0 to 10 := 0;
begin
if(rset = '1' or is_key = '1') then
cnt := 0;
sclk0 <= '0';
seg0 <= fnd_disnum(0);
elsif(sec_clk'event and sec_clk = '1') then
if(cnt = 9) then
cnt := 0;
sclk0 <= '0';
elsif(cnt = 8) then
sclk0 <= '1';
cnt := cnt + 1;
else
cnt := cnt + 1;
sclk0 <= '0';
end if;
seg0 <= fnd_disnum(cnt);
end if;
end process;
-- 10 second process
process(sec_clk, rset, is_key)
begin
if(rset = '1' or is_key = '1') then
cnt_sec1 <= 0;
elsif(sec_clk'event and sec_clk = '1') then
if(sclk0 = '1') then
if(cnt_sec1 = 5) then
cnt_sec1 <= 0;
else
cnt_sec1 <= cnt_sec1 + 1;
end if;
end if;
end if;
seg1 <= fnd_disnum(cnt_sec1);
end process;
sclk1 <= '1' when(cnt_sec1 = 5 and sclk0 = '1') else '0';
-- 1 miniute process
process(sec_clk, rset, im_key)
begin
if(rset = '1') then
cnt_min <= 0;
elsif(sec_clk'event and sec_clk = '1') then
if(im_key = '1') then
if(cnt_min = 9) then
cnt_min <= 0;
else
cnt_min <= cnt_min + 1;
end if;
elsif(sclk1 = '1') then
if(cnt_min = 9) then
cnt_min <= 0;
else
cnt_min <= cnt_min + 1;
end if;
end if;
end if;
seg2 <= fnd_disnum(cnt_min);
end process;
sclk2 <= '1' when(cnt_min = 9 and sclk1 = '1') else
'1' when(cnt_min = 9 and im_key = '1') else '0';
-- 10 miniute process
process(sec_clk, rset)
begin
if(rset = '1') then
cnt_min1 <= 0;
elsif(sec_clk'event and sec_clk = '1') then
if(sclk2 = '1') then
if(cnt_min1 = 5) then
cnt_min1 <= 0;
else
cnt_min1 <= cnt_min1 + 1;
end if;
end if;
end if;
seg3 <= fnd_disnum(cnt_min1);
end process;
sclk3 <= '1' when(cnt_min1 = 5 and sclk2 = '1') else '0';
-- hour process
process(sec_clk, rset, ih_key)
variable cnt : integer range 0 to 24 := 0;
variable a, b : integer range 0 to 50 := 0;
begin
if(rset = '1') then
cnt := 0;
elsif(sec_clk'event and sec_clk = '1') then
if(ih_key = '1') then
if(cnt = 23) then
cnt := 0;
else
cnt := cnt + 1;
end if;
elsif(sclk3 = '1') then
if(cnt = 23) then
cnt := 0;
else
cnt := cnt + 1;
end if;
end if;
end if;
a := cnt / 10;
b := cnt mod 10;
seg4 <= fnd_disnum(b);
seg5 <= fnd_disnum(a);
end process;
end rt1;
이번 프로젝트에서는 디지털시계를 HDL언어인 VHDL를 사용하여 설계하여 실습 보드인 PXA255 FPGA에 직접 올려보았다.
디지털시계를 구현하기 위해서 33Mhz의 메인 클럭에서 1hz의 클럭을 얻기 위한 묘듈이 필요했다. 이 묘듈은 카운터를 이용하여 원하는 수만큼 카운트 한 후 신호를 내보내는 원리로 동작한다.
디지털시계는 1초, 10초, 1분, 10분, 시간을 각각 처리하는 프로세스 문으로 작성되며, 모든 동작은 1hz인 second clock에 동기 되어 동작하게 된다. 그 외 sclk0, sclk1, sclk2, sclk3 의 클럭들이 내부에서 발생 된다. sclk0은 9초에서 10초로 넘어 갈 때, sclk1은 59초에서 1분으로 넘어 갈 때, sclk2는 9분에서 10분으로 넘어 갈 때, sclk3는 59분에서 1시간으로 넘어 갈 때 사용 되는 신호이다.
이번 프로젝트를 통해서 타이밍에 대한 이해와 VHDL의 활용능력 및 쿼터스 같은 툴 사용에 대해 익숙해 진 것 같다. 또한 자신이 작성한 디지털시계를 직접 FPGA에 올려 보고 제대로 동작을 하는지 확인해봄으로써, 평소 시뮬레이션만 해보는 것과 다르게 회로설계에 대한 자신감을 가질 수 있었다.
피씨 관리 프로그램
리눅스 부팅 과정
전체적인 시스템 부팅 과정은 아래와 같다.
■ 부트로더 : 부트섹터(MBR, Master Boot record)에 적재되어 있는 프로그램.
CentOS에는 GRUB
부트로더에 의해 선택된 운영체제로 부팅 시작
■ 커널 : 부트로더에 의해 운영체제 부팅이 시작되면 맨 먼저 커널(kernel)이 동작됨.
부팅 시 시스템 내의 하드웨어를 인식하여 그에 대한 정보를 보여주는데 이를 커널 메시지라
한다.
■ 부트스플래시 : 부팅 진행 상황을 그래픽으로 보여주는 것.
(BootSplash) CentOS에서는 RHGB(RedHat Graphical Boot)이 기능 제공
■ 루트 파일 시스템 마운트
커널 동작 후 initrd.gz 또는 initrd.img로 된 램 초기 디스크가 동작하여
리눅스가 설치된 파티션을 루트 파일시스템으로 읽기 모드(Only-read)로 마운트됨
운영체제 적재 끝나면 rc.sysinit 스크립트에 의해 로컬 디스크가 읽기/쓰기 모드로 다시 마운트됨
* 응급복구모드에서는 루트 파일시스템이 읽기 모드로만 마운트되어 있으므로, 읽기/쓰기 모드로
재 마운트 시켜 주어야 시스템 파일 수정 가능(mount -o remount,rw /)
■ 시스템 초기화 프로세스(init, inittab)
1. 루트 파일 시스템이 마운트된 후 프로세스 ID(pid) 1번 값을 가지는 init 프로세스에 의해
시스템 초기화 실행
* init 프로세스는 로그인 프롬프트 나오기 전까지 파일 시스템 점검, 서비스 프로세스 관리, 가
상 콘솔 접속 관리, 실행 레벨 관리 등 리눅스 사용환경을 위한 초기화 작업 실행
2. init 프로세스가 실행되면 가장 먼저 /etc/inittab 파일을 읽어들임
3. inittab 파일은 맨 처음 /etc/rc.d/rc.sysinit 스크립트 구동
(네트워크 설정, 호스트이름 설정, /proc 마운트, 시스템 시간 설정, 시스템 폰트 구동, 스왑 구
동, 루트 파일 시스템 및 리눅스 파일 시스템 마운트 및 점검, 하드 디스크 최적화, Raid 디바
이스 구동 등의 작업 진행)
4. default로 지정되어 있는 /etc/rc.d/rc 디폴트번호 가 실행됨
○ 런레벨
<런레벨 정의> : 런레벨은 총 7가지가 있음
- 0 : 시스템종료(halt)
- 1 : 싱글유저(윈도우의 안전모드와 유사, root의 비밀번호 변경 가능)
- 2 : 멀티유저(NFS(Network File System) 기능 제외)
- 3 : 멀티유저(모든기능 포함, 콘솔 부팅)
- 4 : 사용 안함
- 5 : x윈도우
- 6 : 재부팅
5. 시스템 재시작 Ctrl-Alt-Del키 설정
재시작키 설정을 없애려면 주석 처리
만약 명시된 사용자만이 이 조합키를 사용할 수 있게 하려면 -a 옵션 사용
-> /etc/shutdown.allow 파일 만들고 여기에 사용자 추가
6. UPS 전원 부족 시에 자동으로 셧다운하기
UPS 파워가 부족할 경우 2분 후에 자동으로 셧다운
UPS 전원이 충전되어 전원 공급에 문제가 없을 경우 실행한 셧다운 명령을 취소
7. 가상 콘솔 접속을 위한 mingetty 설정
리눅스에서는 6개의 가상 콘솔을 제공
가상 콘솔을 제공하는 프로토콜 : getty (주로 mingetty와 mgetty가 많이 사용됨)
respawn은 커널 메모리 상에서 프로세스가 실행되었다가 죽으면 다시 실행되어 살아날 수 있도록
해 주는 명령. 로그아웃 후에도 계속 같은 로그인 프롬프트가 뜨는 이유.
8. X window 실행
실행레벨이 5인 경우 preefdm 스크립트에 의해 엑스 윈도우로 부팅 이루어짐
/etc/sysconfig/desktop 파일에 명시되어 있는 오픈 데스크톱 유형에 따라 엑스 디스플레이 관
리자가 실행되어 엑스 로그인 화면 나타남
CentOS에서는 GNOME을 기본 데스크톱 환경으로 지원하므로, gdm(Gnome Display Manager)
가 동작함