1절. 소개

1.1절. 이 문서에 대해서

이 문서는 The Linux Kernel Module Programming Guide을 참고했으며, 많은 부분 원문을 그대로 번역하였다. 그러나 본문을 실제 테스트 하면서 내용이 미흡한 부분을 보완하였으며, 몇몇 틀린 부분에 대한 수정도 이루어졌다.


1.2절. 커널 모듈이란 ?

커널 모듈에 대해서 이해하고 프로그래밍을 하기 위해서 당연히 여러분은 C언어와 리눅스 시스템에 대한 기본적인 이해를 하고 있어야 한다. 이 문서는 리눅스(유닉스) 시스템과 C에 대한 기본 이해를 하고 있다는 가정하에 작성될 것이다.

커널 모듈이란 필요에 따라 커널에 로드하거나 언로드 할 수 있는 특정한 기능을 수행하는 코드(프로그램)이다. 이렇게 하므로써 쉽게 커널의 기능을 확장할 수 있을 뿐만 아니라 운영체제를 리부팅 하지 않고도 원하는 기능을 수행할 수 있도록 만들 수 있다.

예를 들어서 어떤 하드웨어를 제어하기 위한 문자 장치(device drive)를 작성해야 한다고 생각해보자. 만약 모듈기능을 제공하지 않는 커널이라면 커널을 직접수정하는 방식을 동원해서 커널에 필요한 기능을 추가시켜야 할 것이다. 프로그램 자체가 어려워지는 것은 물론이고 기능을 테스트 하기 위해서는 계속적인 리부팅 작업이 필요하게 되므로 개발기간 역시 극적으로 늘어날 수 밖에 없을 것이다. 또한 커널에 필요한 기능이 추가될 때마다 커널에 계속해서 코드가 추가 됨으로 커널의 크기도 매우 커지게 될것이다. 사운드카드를 위한 기능을 추가했는데 해당 사운드카드를 가지지 않는 유저도 있을 것이다. 이럴 경우는 그야말로 쓸데 없는 자원낭비가 되는 셈이다. ` 커널 모듈로써 작동하도록 만들었다면 쓸데없는 기능을 하는 모듈은 언로드 시키면 그만이다.

이 문서는 리눅스 커널 2.4를 기준으로 작성되었다.


2절. 커널 모듈 프로그래밍의 기본

2.1절. 커널에 모듈 적재시키기

현재 커널에서 작동중인 모듈의 목록은 lsmod 명령을 통해서 확인할 수 있다.

# 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 
			
lsmod는 /proc/modules 파일의 내용을 그대로 출력한다.

그럼 커널은 이러한 모듈을 어떻게 찾아서 적재시키는 걸까. 커널이 어떤 모듈을 포함하고자 할때 해당 모듈이 아직 커널에 적재되어 있지 않다면 모듈 데몬(daemon)인 kmod가 modprobe를 실행시켜서 모듈을 읽어들이게 된다. 이때 modprobe는 다음중 하나의 방법을 이용해서 읽어들여야할 모듈을 찾게 된다.

  • softdog, ppp와 같은 모듈이름을 직접 찾는다.

  • char-major-10-30 과 같은 일반적인 식별자(generic identifier)를 이용한다.

만약 modprobe가 식별자를 이용할 경우, 해당 식별자에 대한 진짜 모듈이름을 알아와야 할것이다. 이에 대한 정보는 /etc/modules.conf에 저장되어 있다.

alias char-major-10-30 softdog
			
별칭목록을 확인함으로써 식별자를 위해서 softdog.o모듈을 적재시켜야 된다는 정보를 얻을 수 있게된다.

다음 modprobe는 /lib/modules/version/module.dep파일을 검사한다. 여기에는 해당모듈이 실행되기위해 필요한 다른 모듈들 즉 모듈의존성에 관한 정보들이 있어서 softdog.o를 적재하기 위해서 다른 모듈이 필요한지 확인하고 미리 적재시킨다. 이 파일은 depmod -a명령으로 생성시킬 수 있다. 예를 들어 msdos.o 모듈은 fat.o모듈이 우선적으로 적재되어 있어야만 한다. modprobe는 modeule.dep파일을 참조해서 의존성을 검사하게 된다.

마지막으로 modprobe는 insmod를 이용해서 원하는 모듈을 적재하기 위해서 우선적으로 필요한 모듈을 적재시키게 된다. insmod는 /lib/modules/version/을 직접참조해서 모듈을 적재한다. 최종적으로 여러분이 msdos 모듈을 올리기를 원한다면 다음과 같이 하면 된다.

# insmod /lib/modules/2.5.1/kernel/fs/fat/fat.o
# insmod /lib/modules/2.5.1/kernel/fs/msdos/msdos.o
			
그러나 위와 같이 할경우 모듈 의존성을 직접 검사해줘야 하는데, 이럴 경우 modprobe를 이용하면 된다.
 
# modprobe -a msdos
			

리눅스에서 사용되는 modprobe, insmod, depmod와 같은 프로그램은 modutils(혹은 mod-utils) 패키지에 포함된다.

그럼 /etc/modules.conf를 간략하게 살펴보고 이번장을 끝마치도록 하겠다.

# This file is automatically generated by update-modules

path[misc]=/lib/modules/2.4.?/local
alias eth0 ne2k-pci
alias eth1 ne2k-pci
			
'#'은 주석을 위해서 사용되며 공백라인은 무시된다.

path[misc]는 misc모듈을 찾을 경로의 지정을 위해서 사용된다.

alias 는 kmode가 식별자 eth0을 호출 했을 때 ne2k-pci를 호출하도록 한다. alias는 꽤 중요하게 사용될 수 있는데 하나의 시스템에 동일한 장치가 2개 이상 붙어 있을때 이를 식별할 수 있도록 해준다.


2.2절. 초간단 모듈제작 : Hello World

어떤 역사적인 이유가 있는지 모르겠지만 대부분 프로그래밍입문 을 하는데 있어서 가장 먼저 "Hello World"를 출력하는 코드를 장성하는 데에서 부터 시작한다. Hello World 출력 코드와 관련된 재미있는 글이 있는데 한번 읽어 보기 바란다. Hello World의 변천사

여기에서도 "Hello World"를 출력하는 코드를 만드는 것으로 모듈 프로그래밍으로의 발걸음을 내딛도록 하겠다. 이것은 매우 간단한 모듈인데, 아직 컴파일 방법을 다루지는 않을 것이다. 모듈 컴파일은 2.3절에서 다루도록 하겠다.

#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개의 함수를 가지고 있어야만 한다. 하나는 init_module()라는 이름의 시작(초기화)함수로써 insmod에 의해서 커널로 적재될때 호출된다. 다른 하나는 cleanup_module()라는 이름의 종료함수로써 rmmod를 호출해서 모듈을 삭제할때 호출된다.


2.2.1절. printk()에 대해서

일반적으로 printk를 이용하면 (함수이름의 어감 때문에) 특정한 메시지를 표준출력할 것으로 생각하는 경우가 많은데 printk는 유저를 위한 어떤 출력도 하지 않는다. 이름과는 달리 로그나 경고 메시지를 남기기 위한 커널로깅 목적으로 사용된다.

일반적인 로그관련 라이브러리나 함수들이 그렇듯이 printk도 우선순위(priority)를 가진다. 모두 8단계의 우선순위를 가지며 <1> KERN_ALERT 와 같은 방식으로 결정할 수 있다. 이들 우선순위에 대한 선언정보는 linux/kernel.h에서 확인할 수 있다. 만약 우선순위를 정하기 귀찮거나 정할 수 없다면 기본 우선순위 DEFAULT_MESSAGE_LOGLEVEL을 사용하면 된다.

만약 syslogd와 klogd가 실행중이라면 메시지는 /var/log/messages에 추가 된다. 다음은 실제 저장된 로그들이다.

Oct  6 01:15:39 localhost kernel: Hello World 1.
Oct  6 01:16:11 localhost kernel: Goodbye world 1.
				


2.3절. 커널 모듈 컴파일 하기

커널 모듈을 컴파일하기 위해서는 특별한 gcc 옵션과 더불어 몇가지 값들의 정의(symbols define)가 필요하다. 이유는 커널모듈 컴파일시 사용되는 커널 헤더들이 커널버젼에 매우 의존적일 수 있기 때문이다.

이러한 정의는 gcc의 -D옵션을 이용하거나 혹은 #define 선행처리자를 이용하면 된다. 이번 장에서는 커널컴파일을 하기 위해서 필요한 내용들에 대해서 다룰 것이다.

  • -c : 커널모듈은 독립적으로 실행되지 않으며 (main함수 자체를 포함하고 있지 않다) object파일 형태로 커널에 링크되어서 실행된다. 결과적으로 -c 옵션을 이용해서 오브젝트 형태로 만들어 주어야 한다.

  • -O2 : 커널은 inline함수를 매우 많이 사용하며, 그런 이유로 모듈은 반드시 최적화(optimization) 옵션을 사용해서 컴파일 되어야 한다. 최적화 옵션을 사용하지 않을 경우 어셈블러 매크로등을 사용하는데 있어서 문제가 생길수 있다. 이럴경우 모듈의 적제가 실패하게 될것이다.

  • -D__KERNEL__ : 이 코드가 유저 프로세스가 아닌 커널모드에서 작동할 것이라는걸 커널헤더에 알려준다.

  • -W -Wall : 모듈 프로그램은 커널에 매우 민감한 영향을 끼칠 수 있으며 커널을 다운 시킬 수도 있다. 그러므로 가능한한 모든 종류의 경고메시지를 검사해야할 필요가 있다. 이 옵션을 사용하면 컴파일러가 발생시킬수 있는 모든 경고메시지를 출력한다.

  • -DMODULE : 커널모듈로 작성되는 코드라는걸 알려주기 위해서 사용한다.

이외에도 컴파일에 사용될 헤더파일을 찾기 위해서 -I대신에 -isystem을 사용하며 "unused varaiable"과 같은 경고 메시지의 출력을 위해서 -W -Wall을 이용할 것이다. -isystem은 gcc-3.x이상에서 지원되는 옵션이다.

참고: -isystem 도 -I 처럼 헤더파일의 경로 지정을 위해서 사용된다는 점에서 비슷하다. -I의 경우 표준 (헤더파일_시스템 경로를 검사하기 전에 -I로 지정된 경로를 먼저 검사하는 반면 -isystem은 가장 마지막에 지정된 경로에 대한 검사를 한다.

다음은 커널 모듈을 컴파일하기 위한 전형적인 Makefile이다.

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
			
쉽게 이해 가능할 것이다. make를 실행하면 hello.c를 컴파일하고 그결과 커널 모듈(오브젝트) 파일인 hello.o를 생성해낸다. 생성된 커널 모듈은 insmod ./hello.o를 통해서 적재 할 수 있다. 이걸로 당신은 최초의 커널 모듈작성에 성공했다. 예상외로 간단하지 않은가 ? 적재된 커널모듈은 rmmod hello로 제거할 수 있다. printk()출력은 /var/log/message에 쌓일 것이다. 확인해 보기 바란다.

2.2절에 있는 예제를 보면 init_module()에서 0을 리턴하고 있다. 그런데 다른 값을 리턴하도록 하면 어떻게 될까 ? 지금한번 테스트 해보기 바란다.


2.4절. Hello World 2

init함수와 cleanup함수의 이름이 반드시 init_module()와 cleanup_module()로 작성되어야 한다는 것은 (비록 혼동을 줄여주긴 하겠지만) 왠지 이치에 맞지 않는것 같다.

리눅스 커널 2.4부터는 이들 고정된 이름대신 다른 이름으로 사용가능하며, 이를 위해서 module_init()와module_exit()함수를 제공한다.

예제 : hello_re.c

#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 : 라이센스와 모듈에 대한 정보

커널모듈은 다른 시스템/유저프로그램에 비해 운영체제에 더욱민감한 영향을 끼칠 수 있다. 그런이유로 최소한 커널모듈에는 커널작성자에 대한 정보가 들어가도록 작성하는게 좋을 것이다.

리눅스 커널 2.4이상에서 지금까지 우리가 작성한 커널 모듈을 적재하려고 하면 다음과 경고 메시지를 출력할 것이다.

# 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
			

참고: 라이센스정보관련 경고메시지 출력은 커널 옵션을 어떻게 주고 컴파일 했느냐에 따라 출력되지 않을 수도 있다. 몇몇 배포판의 경우 경고메시지가 출력되지 않을 것이다.

특히 많은 개발자들은 해당 모듈이 GPL(혹은 이와 비슷한)과 같은 공개된 라이스정책을 따르는지 그렇지 않은지에 대해서 민감할 수 있는데, MODULE_LICENSE() 매크로를 이용해서 라이센스를 명시할 수 있다. 이러한 라이센스에 대한 메커니즘은 linux/module.h에 정의되어 있다.

이와 비슷하게 MODULE_DESCRIPTION()과 MODULE_AUTHOR()매크로를 이용해서 모듈의 원저작자와 모듈에 대한 간단한 설명을 곁들일 수도 있다.

이러한 모든 매크로는 linux/module.h에 정의 되어있다. 이들 매크로 값들은 커널에 의해서 직접 이용되지는 않지만objdump와 같은 도구를 이용할때 모듈에 대한 정보를 얻는데 도움을 준다.

# 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.           
			
다음은 이들 메크로를 포함시킨 예이다.

예제 : hello_li.c

#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절. 명령행 인자의 처리

커널 모듈도 명령행 인자를 받아들일 수 있다. 그러나 일반적으로 이용하는 argc/argv 기법을 사용할 수는 없다.

모듈로의 아규먼트 전달은 MODULE_PARM()매크로를 통해서 이루어진다. MODULE_PARM()매크로는 2개의 인자를 가진다. 첫 번째 인자는 값이 저장될 변수명이고, 두번째 인자는 저장될 데이터의 타입을 나타낸다. 데이터 타입은 "b" : 바이트, "h": short int, "i": integer, "l": long int, "s":string(문자열)가 있다. 문자열은 char * 타입이며 insmod로 호출될때 메모리가 할당된다. 다음은 간단한 활용예이다.

int myint = 3;
char *mystr;

MODULE_PARM (myint, "i");
MODULE_PARM (mystr, "s");
			

배열도 지원되는데, '-'를 이용해서 배열의 최소크기와 최대크기를 지정할 수 있다. 이는 주어질수 있는 인자의 최소와 최대 갯수를 정할 수 있음을 의미한다.

int myshortArray[4];
MODULE_PARM(myintArray, "2-4i");
			

이제 실제 모듈을 실행시키면서 인자를 넘기는 방법을 알아보도록 하자. 인자는 [변수명]=[값]의 형태로 넘어간다. 만약 모듈 코드상에 MODULE_PARM(myint, "i"); 로 되어 있다면 다음과 같은 방법으로 인자를 넘긴다.

# insmod ./hello.o myint=50
			
꽤나 독특한 방법으로 넘기고 있음을 알 수 있다.

배열의 경우에는 인자가 지정한 최대/최소의 범위를 벗어날 경우 에러메시지를 출력하며 모듈이 적재되지 않는다. 일반 애플리케이션에서 수행하는 argc를 통한 아규먼트 갯수 검사와 비슷한 형태라고 보면 된다. 배열의 각 요소는 ','를 통해서 구분되어 진다.

int myarray[4];

MODULE_PARM(myarray, "2-4");
			
와 같이 되어 있다고 할때, 다음과 같은 방법으로 값을 넘길 수 있다.
# insmod ./hello.o myarray=1,4,3
			
다음은 간단한 예제코드이다.

예제 : hello_arg.c

#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절. 모듈별 분할 컴파일

보통 조금이라도 규모가 있는 시스템/유저 애플리케이션을 작성할 때는 소스의 관리를 위해서 함수/기능별로 소스를 분할해서 컴파일한다.

커널 모듈역시 이러한 분할 컴파일을 지원하는데, 아래의 형식을 따라주어야 한다.

  1. 모든 소스파일 혹은 하나 이상의 소스파일에 #define _NO_VERSION__ 이 포함되어 있어야 한다. 모듈 컴파일을 위해서 포함시키는 module.h 내에 커널 버젼정보가 포함되어 있으며 이 정보는 모듈에 전역적으로 사용되므로 _NO_VERSION__의 사용은 꽤나 중요해진다. 만약에 version.h를 직접 포함시켜야 되는 경우가 생긴다면 _NO_VERSION__을 정의하기 바란다. module.h에는 이게 정의되어 있지 않기 때문이다.

  2. 일반적인 방법으로 컴파일한다.

  3. 만들어진 여러개의 오브젝트파일을 하나로 만들어 줘야 한다. x86하에서는 d -m elf_i386 -r -o <module name.o> <1st src file.o> <2nd src file.o>

다음은 모듈분할 방식으로 작성된 커널 모듈 예제들이다.

예제 : start.c

#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;
}
			

예제 : stop.c

#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");
}
			

다음은 컴파일을 위한 Makefile이다.

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
			

출 처
  커널 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");

 // Makefile
# 변수 설정...
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 *.


  커널 2.6 버젼...
 //소스 파일
#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");

 // Makefile
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


 모듈 작성은 2.4 커널과 2.6 커널이 다르다. 실제로 2.6 커널의 모듈 작성 방법은 2.4.20 이후 버젼의 커널에서도 동일하게 사용할 수 있다.

 2.6 커널에서는 linux/init.h 헤더파일이 추가되었다. 그리고 init_module()과 cleanup_module()이라는 함수 이름 규약을 더 이상 따르지 않아도 된다. 초기화 함수와 종료 함수는 자신이 원하는 이름으로 작성하면 되고, module_init()과 module_exit() 매크로에서 초기화 함수와 종료 함수의 이름만 지정해주면 된다. 물론, 2.4 커널의 init_module(), cleanup_module()을 사용해도 되지만, 현재 커널 해커들은 이들 함수를 전혀 사용하고 있지 않다. 2.4.20 이후 버전의 커널에서는 위와 같은 2.6 형식의 코드를 사용하고, gcc를 사용해서 모듈로 빌드할 수 있다. 2.4.20 이후 버젼과 2.6 커널의 모듈은 동일한 코드를 사용할 수 있으며, 빌드 과정만 다르다.

 2.6 커널부터는 커널 빌드에 대해서 kbuild 시스템을 사용합니다. 따라서 모듈을 빌드하기 위해서 gcc를 사용할 수 없고, Makefile을 정의하고, kbuild 시스템으로 모듈을 빌드해야 한다.

 모듈을 로드하기 위해 insmod 명령을 실행한다. 모듈에 2.4 커널은 .o 형식을 사용하지만, 2.6 커널은 .ko 형식을 사용한다.
2.4 커널의 경우
# insmod hello.o
2.6 커널의 경우
# insmod hello.ko

 모듈이 로드된 것을 확인하기 위해 lsmod 명령을 사용한다.

 rmmod 명령으로 모듈을 제거한다. 커널 버전에 관계없이 동일하며, 제거할 모듈 이름만 지정한다.
# rmmod hello

Jtag을 구성하기 위해서 제일 먼저 하는것이 바로 병렬포트와 연결되는 Jtag인터페이스(동글) 이다. 이는 Jtag신호를 TTL레벨의 신호로 알맞게 CPU에 전달하기 위해서 인데, 그동안의 편의성으로 인하여 병렬포트에 연결되는것이 많았다. 요즈음은 USB나 시리얼 형도 많이 나와 있으나 현재까지는 대세가 병렬포트이므로 병렬포트의 구조를 알아보려고 한다.

실제 병렬포트는 취미로 하는 자작에서 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 병렬포트의 정확한 스펙문서를 찾아야 할것이다.


프로젝트 목적
- 소리의 발생 원리와  주파수 등을 이해한다.
- 임베디드 컴퓨터의 소리 발생 방법을 익힌다.
- 키패드 장치와 소리 발생 장치의 상호 연동 프로그램을 구현한다.

프로젝트 개요
 실습용 임베디드 컴퓨터는 모두 12개의 다이얼패드를 가지고 있다. 임의의 키를 눌렀을 때 그 키에 해당되는 다이얼 톤을 발생시키도록 한다. 다이얼 톤은 임베디드 컴퓨터의 Phone 단자에 이어폰을 꽂아서 발생여부를 확인하게 된다. 12개의 다른 키 값에 대해 각기 다른 주파수의 다이얼 톤이 발생되어야 한다. 톤 발생과 더블어 당연히 키패드 드라이버의 활용도 필요하다. 즉, 어떤 키가 눌려졌는지, 또 눌려진 시간은 얼마인지 등을 알게 하는 드라이버를 작성해야하며, 응용 프로그램은 키패드 드라이버와 톤 발생기 드라이버를 동시에 사용하도록 해야 한다.

* 키패드 드라이버 구현
 - 어느 키가 얼마 동안 눌러졌는지를 확인할 수 있어야 한다.
 - 톤은 특정키가 눌러져 있는 동안 계속 유지되어야 한다.
* 톤 발생기
 - 주어진 입력 값에 따라 각기 다른 주파수를 갖는 톤을 발생시킨다.
 - 임베디드 컴퓨터의 phone 단자로 실제 출력이 이루어져야 한다.


프로젝트 진행

- 키패드 드라이버
 우리가 개발 장비로 사용한 PXA255-PRO2의 키패드는 인터럽트에 의해 작동된다. 즉, 평소에는 아무런 일이 일어나지 않다가 키패드의 특정키를 누르면 인터럽트가 발생하여 인터럽트 처리가 일어나야 한다는 뜻이다. 여기서 키패드는 GPIO0번 핀을 인터럽트 입력 핀으로 사용한다.
 인터럽트 처리를 위해서는 먼저 set_GPIO_IRQ_edge() 함수를 사용하여 인터럽트의 감지 상태 등을 설정하고, 인터럽트 처리 루틴을 작성, 이 루틴을 request_irq() 함수를 사용하여 등록해야 한다.
 어느 키가 얼마 동안 눌러졌는지 확인할 수 있기 위해서 인터럽트의 Rising Edge와 Falling Edge를 모두 감지하는 디바이스 드라이버를 작성하였다. 즉, 인터럽트의 Rising Edge에서 키의 누름을 감지하고, Falling Edge에서 키의 때어짐을 감지하는 것이다. 이를 위해 먼저 다비이스 open시 인터럽트의 Rising Edge를 감지하도록 설정하고, 인터럽트가 Rising Edge에서 발생하면 이데 따른 인터럽트 처리 루틴에서 다시 Falling Edge를 감지하도록 설정 하는 방식으로 구현하였다.

- 톤 발생기
 키패드의 눌러진 키에 따라 각기 다른 주파수를 가지는 톤이 발생되어야 한다. 이를 위해서 일정한 주파수를 가진 사인파형의 디지털 신호를 만들어주는 프로그램을 작성하였다. 또한 이 일정한 주파수의 톤이 임베디드 컴퓨터의 phone 단자로 출력하기 위해서 이미 커널에 올려진 AC97 디바이스 드라이버(/dev/dsp)를 이용하였다.
 사인 웨이브를 만드는 방법으로는 룩업 테이블을 이용하는 방식, 보간법 그리고 다항식법 등 여러 가지 방법이 있다. 본 프로젝트에서는 순환형 필터를 사용하여 일정한 주파수를 가진 신호를 생성했다. 이 디지털 신호를 만들어 내는 수식은 다음과 같다.


 나이키스트 이론에 의해 톤 주파수는 0에서 (샘플링 주파수/2)사이의 값만을 가질 수 있다. 또한 초기 값으로

으로 설정하였다.

- 간략적인 코드

// 키패드
// 인터럽트 처리 루틴 1
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);
}

// 인터럽트 처리 루틴 2
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;
}

// SinGenerator(톤 발생기)
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을 이용하여 생활에서 많이 사용하는 디지털 시계를 구현해 봄으로써 Clock의 활용과 7 Segment의 제어를 익히고 디지털 시계의 구현을 통해 VHDL의 응용 능력을 기른다.
- 입력 장치인 Push Button Switch, 출력 장치인 FND(7 Segment)의 특성을 이해한다.

프로젝트 개요
- 본 프로젝트에서는 7 Segment LED를 이용한 디지털 시계를 VHDL을 이용하여 구현하고자 한다.
- 프로젝트에서 사용하는 7 Segment LED는 6개의 Segment LED가 Dynamic 구동방식으로 동작한다.
- 디지털 시계는 6개의 7 Segment LED에 시, 분, 초 각각 2자리씩 표현한다. 그리고 시계를 리셋 시키는 기능과 시간을 맞추는 작업은 FPGA 보드에 있는 스위치를 이용한다.
- 본 프로젝트에서 사용하는 FPGA 보드의 기본 주파수는 33Mhz를 사용한다. 디지털 시계의 1초를 구하기 위해서는 33Mhz의 Main Clock 주파수를 변환 작업을 통하여 1초로 만들고 1초를 60분주하여 1분을 만든다.

PXA255-FPGA 보드

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


- 디지털 시계에서는 7 Segment LED 제어를 위하여 EP1C6 칩 11~18의 데이터 라인 8핀과 19~25까지의 COM 라인 6핀을 출력 PORT로 사용한다.
- PXA255-FPGA 보드에서 사용하는 FND는 공통으로 사용하는 데이터 라인 8개와 7 Segment의 출력을 결정하는 COM라인 6개로 구성된다.
- FND 동작은 6개라인(FCOM[5..0])의 6비트 중 비트 값이 0인 Segment에 데이터(FDATA[7..0])를 출력한다.
- 각각 다른 데이터를 7 Segment LED에 출력 하려면 6개의 FCOM의 비트들을 쉬프트 시켜가면서 FCOM의 값이 바뀌는 시점에 데이터 라인을 세팅하면 선택된 7 Segment LED에 데이터를 출력한다. 이와 같은 방법을 Dynamic Display 방식이라고 한다.
- Dynamic Display 방식은 빠른 속도로 쉬프트 시킴으로서 사람의 눈의 잔상효과를 이용하여 마치 6개의 7 Segment가 동시에 출력되는 것처럼 보여준다.

디지털 시계 프로그램의 블록도


- 위의 블럭도에서는 입력으로 Main Clock과 리셋 버튼, 그리고 시, 분, 초를 제어할 수 있는 스위치 3개를 가진다.
- 디지털 시계의 출력으로는 6개의 7 Segment를 제어하기 위한 8개의 데이터 라인 FDATA_SM[7..0]과 출력 7 Segment 선택 라인인 FCOM[5..0]의 6개 출력을 가진다.

구현
- clk_div.vhd -
library ieee;
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;

- DigitalTimer.vhd -
library ieee;
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에 올려 보고 제대로 동작을 하는지 확인해봄으로써, 평소 시뮬레이션만 해보는 것과 다르게 회로설계에 대한 자신감을 가질 수 있었다.





 학교 다닐 때 했던 프로젝트

 프로젝트 목적
- 다수의 컴퓨터의 효율적 관리
- 컴퓨터 사용 기록 관리
- 비 인가자의 접근 제한

 개발 내용
- 1(server)대 다(client) 관리
- 서버 클라이언트 간 메신져
- 사용자 정보 관리
- 사용 기록 관리
- Time-out 기능

 개발 방법
- CAsyncSocket 사용
- Gina 및 전역 후킹으로 시스템 키 차단
- MySQL을 사용한 DB 연동






결과 화면

- server 화면 -




- client 화면 -




 이 프로젝트에서 UI 구성, DB관련 프로그래밍,  Gina/전역후킹 등을 이용한 시스템 키를 차단하는 부분을 맡아서 진행했다. MFC를 이용하여 간단하게 UI를 만들고, DBMS로는 MySQL를 이용했다. Ctrl-alt-del 키를 차단 하기 위해서 Gina를 이용했으며, 나머지 시스템 키들을(alt-f4, alt-tab 등) 차단 하기 위해서 전역 후킹을 이용했다.


전체적인 시스템 부팅 과정은 아래와 같다.


 

■ 부트로더 : 부트섹터(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)

       가 동작함

 부팅 과정 중 커널이 적재되고 장치 드라이버가 초기화되면 커널은 /etc, /bin 이나 /sbin에 있는 init 프로그램을 실생시킨다. init은 다목적 프로그램으로, 새로운 프로세스를 생성하거나 프로그램이 종료되었을 때 이를 다시 시작하는 기능이 있다. 또한 init은 여러 프로그램을 실행하고 시스템이 부팅할 때 스크립트를 실행할 책임을 진다. init의 모든 동작은 /etc/inittab 파일을 통해 제어한다.

 /etc/inittab 파일을 이해하기 위해서는 우선 실행 레벨이라는 개념을 이해해야 한다. init에서 실행 레벨은 현재 시스템 상태를 명시하는 숫자 또는 문자다. 예를 들어, 시스템 실행 레벨이 3으로 변경되면 /etc/inittab의 모든 항목 중 실행 레벨이 3이라고 되어 있는 것을 실행한다. 실행 레벨은 /etc/inittab 안의 항목을 그룹으로 묶는 유용한 방법이다.


현재 나의 데비안 시스템 inittab 설정 파일....

run level 2로 설정 되어 있다.. 주석을 보면
run level 0 : halt
run level 1 : single-user mode
run level 2-5 : multi-user mode
run level 6 : reboot 
으로 설정 되어 있는 것을 알 수 있다.

여기서 run level 0을 실행 시켜 보기 위해 
# init 0 
명령을 주자 시스템이 꺼졌다.

위 파일의 각 필드는 콜론(:)으로 구분한다. 
첫번째 필드는 임의의 식별자로 어떤 것이든 상관없으며 파일 내에서 다른 것과 총돌하지 않으면 된다. 
둘째 필드는 명령이 실행될 실행 레벨이다.
셋째 필드는 init이 그 항목을 어떻게 처리할 것인지 지시한다.
넷째 필드는 init이 실제 실행할 명령이다.

id : 2 : initdefault     -> 디폴티 run level 2로 설정

si : : sysinit : /etc/init.d/rcS     -> 시스템을 부팅할 때 init에게 /etc/init.d/rcS을 실행 하도록 한다. 이 파일은 기본 시스템 초기화를 처리해주는 명령을 담은 간단한 셸 스크립트다. 예를 들면, 스와핑 긴능을 켜고 파일시스템을 점검하고, 마운트하며, 시스템 시간을 CMOS 시간에 맞춘다.

10 : 0 : wait : /etc/init.d/rc 0
11 : 1 : wait : /etc/init.d/rc 1
12 : 2 : wait : /etc/init.d/rc 2
13 : 3 : wait : /etc/init.d/rc 3
14 : 4 : wait : /etc/init.d/rc 4
15 : 5 : wait : /etc/init.d/rc 5
16 : 6 : wait : /etc/init.d/rc 6
-> 실행 레벨 0부터 6까지, 어디로 들어가든 /etc/init.d/rc 스크립트에 적절한 실행 레벨 인수를 사용하여 실행한다. rc는 다목적 시동 스크립트로서, 각 실행 레벨에 알맞은 스크립트를 실행시켜 준다. 여기서 action 필드는 wait다. init에게 주어진 command를 실행하고, 종료할 때까지 기다린 후 다음 작업으로 넘어가라는 지시다.
-> 각 실행 레벨의 스크립트는 (데비안의 경우) /etc/rcN.d에 있다. 여기서 N은 시작할 실행 레벨 번호다. 따라서 실행 레벨 2라면 /etc/rc2.d의 스크립트를 사용한다. 이 디렉토리들의 내용을 살펴 보면 Snnxxxx나 Knnxxxx 형태의 파일을 볼 수 있는데 K로 시작하는 서비스를 죽이는 스크립트이며, S로 시작하는 스크립트는 서비스를 시작할 때 사용하는 스크립트다. 이름에 들어 있는 nn은 서로의 실행 순서를 맞추려는 것으로 낮은 번호의 스크립트는 높은 번호의 스크립트보다 먼저 실행된다.
-> 같은 서비스를 서로 다른 실행 레벨에서 시작하고 중지하기 때문에 같은 스크립트를 여러 곳에 복사하는 것보다 심볼릭 링크를 사용하고 있다. 따라서 각각의 S, K 파일은 모든 서비스의 시동/셧다운 스크립트를 포함한 중앙 디렉토리를 가리키는 심볼릭 링크일 뿐이다. 데비안의 경우 이 중앙 디렉토리는 /etc/init.d 이다. 그러므로 특정 서비스를 시동 시키거나 중지시키고 싶을 때는 /etc/init.d에 위치한 스크립트를 이용하면 쉽게 할 수 있다.

/etc/init.d/networking stop  : 네트워크 서비스 중단
/etc/init.d/networking start  : 네트워크 서비스 시작

또 다른 중요 시스템 설정 스크립트는 /etc/init.d/rc.local이 있다. 이 스크립트는 다른 시스템 초기화 스크립트를 실행한 뒤에 실행된다. (보통 각각의 /etc/rcN.d에 S99rc.local이라는 심볼릭 링크를 만들어 둔다. 99라는 숫자는 s 스크립트가 가질 수 있는 가장 큰 숫자이므로 가장 나중에 실행된다) 부팅 중 다른 특별한 시스템 명령을 실행하길 원하거나 어디에서 실행시켜야 할지 모를 때는 rc.local 파일을 편집하여 사용하면 된다.

ca : 12345 : ctrlaltdel : /sbin/shutdown -t1 -a -r now
-> 이 항목은 콘솔에서 Ctrl-Alt-Del 을 눌렀을 때 실행된다. 이 키조합을 누르면 보통 시스템은 재부팅하는 인터럽트가 발생한다. 리눅스에서는 이 인터럽트를 받아 init에게 보내고 init은 ctrlaltdel의 action 필드에 있는 항목을 실행한다. 여기서는 /sbin/shutdown -t1 -a -r now로 안전한 시스템 재부팅을 진행시킨다.

1: 2345 : respawn : /sbin/getty 38400 tty1
2: 23 : respawn : /sbin/getty 38400 tty2
3: 23 : respawn : /sbin/getty 38400 tty3
4: 23 : respawn : /sbin/getty 38400 tty4
5: 23 : respawn : /sbin/getty 38400 tty5
6: 23 : respawn : /sbin/getty 38400 tty6
-> 마지막으로 inittab 파일에는 처음 나오는 6개의 가상 콘솔을 /sbin/getty에서 실행하는 항목이 있다. 이 프로그램은 터미널의 로그인을 받아준다. 이 프로그램이 없다면 터미널은 그야말로 죽은 상태나 다름없으며 키보드나 마우스에 반응하지 않는다. getty 명령은 터미널 장치를 열고 터미널 드라이버에 다양한 매개변수를 설정하고 /bin/login을 실행하여 로그인 세션을 연다.
-> getty는 '보율(baud rate)'과 '장치'라는 두 개의 인수를 받는다. 리눅스 가상 콘솔의 포트명은 /dev/tty1, /dev/tty2등이다. 가상 콘솔의 보율은 일반적으로 38400이다.
-> getty 항목의 action 필드는 respawn인데 이는 해당 명령이 죽으면 init이 다시 명령을 실행한다는 뜻으며, 사용자가 로그아웃 할 때마다 getty 프로세스가 죽으며 이에 따라 getty 프로세스를 다시 실행시켜 로그인을 준비한다.





+ Recent posts