Table of Contents

여기서는 리눅스 디바이스 드라이버의 가장 기본이라고 할 수 있는 LED 제어를 해보려고 한다.
우선은 커널 2.4 대에서 해보고, 나중에 커널 2.6 대에서 시도해 볼 것이다. 여기서 나오는 소스 파일을 분석해보면 디바이스 드라이버의 어느정도 감을 잡을 수 있지 않을까 생각한다.

준비물

참고로 나의 환경은 다음과 같다. 주인공이라고 할 수 있는 LED는 반드시 있어야 한다! 또 한가지를 꼽자면, 프린터 포트이다. 왠만한 컴퓨터에 프린터 포트는 있기 때문에 별도로 명시하지 않았다.

CPU AMD
OS 와우 리눅스 7.1
커널 2.4.2
그 외 LED 1개

위의 준비물을 갖추었으면 본격적으로 시작해보자! 다시 말하지만 굳이 나의 환경에 맞출 필요는 없다. 한가지 당부하고 싶은 것은 커널을 2.4 대로 한다. 참고로 커널 2.4.26 으로 테스트해본 결과 정상적으로 동작하였다.

시작하기

우선 디바이스 드라이버를 만들 파일을 만들어야 한다.

#vi prnioport.c

언뜻 보기에는 복잡해 보이지만 알고보면 간단하다.

#include <linux/ioport.h>
#include <asm/uaccess.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/io.h>
 
 
#define PRNIOPORT_MAJOR 88 // 프린터 IO를 위한 메이저 번호
#define PRNIOPORT_NAME "PRINT IO PORT" // 프린터 장치명 이름
#define PRNIOPORT_MODULE_VERSION "PRINT IO PORT V0.2" // 프린터 장치명 이름 및 버전 번호
#define PRNIOPORT_ADDRESS 0x0378 // 대부분의 PC에서 LPT1은 이 주소를 사용한다.
#define PRNIOPORT_ADDRESS_RANGE 3 // 대부분의 PC에서 LPT1은 이 주소 범위를 사용한다.
//
// 광역 변수 정의
//
//*******************************************************************
 
static int prnioport_usage=0; // 디바이스 오픈 상태 1 이면 사용 / 0 이면 미사용 상태
 
//*******************************************************************
//
// 함수 정의
//
//*******************************************************************
//-------------------------------------------------------------------
// 설명 : 어플리 케이션에서 디바이스를 open 했을 때 호출되는 함수
// 매계 : 없음
// 반환 : 정상이면 0을 반환한다.
// 주의 : 이 함수의 구성은 오렐리가 가장 싫어하는 형태다.
// 즉 디바이스를 오픈할 수 있는 기회는 단 한번으로 제약을 가하고 있다.
//
//
//  즉 한 어플리케이션에서 오픈을 하여 점유하면 다른 어플리케이션은 점유하지 못한다.
// 
//-------------------------------------------------------------------
int prnioport_open(struct inode *minode, struct file *mfile)
{
// 이미 사용중이면 사용중이라는 값을 되돌린다.
if( prnioport_usage != 0 ) return -EBUSY;
 
MOD_INC_USE_COUNT; // 모듈 사용 증가 카운터 매크로
 
prnioport_usage = 1; // 사용 중
 
printk("PRN IOPORT DRIVE OPEN");
 
return(0);
}
 
//-------------------------------------------------------------------
// 
// 설명 : 어플리케이션에서 디바이스를 close를 하였을 때 호출되는 함수
// 매계 : 없음 
// 반환 : 정상이면 0을 반환한다.
// 주의 : 이 함수의 구성은 오렐리가 가장 싫어하는 형태다.
// 즉 디바이스를 오픈 할 수 있는 기회는 단 한번으로 제약을 가하고 있다.
// 즉 한 어플리케이션에서 오픈을 하여 점유하면 다른 어플리케이션은 점유하지 못한다.
// 
// 
//-------------------------------------------------------------------
int prnioport_release(struct inode *minode, struct file *mfile)
{
MOD_DEC_USE_COUNT; // 모듈 사용 횟수를 감소시킨다.
 
prnioport_usage = 0; // 사용하지 않음을 표시
 
printk("PRN IOPORT DRIVE CLOSE");
 
return 0;
}
//-------------------------------------------------------------------
//
// 설명 : 어플리케이션에서 디바이스를 write 를 하였을 때 호출되는 함수
// 매계 : gdata : 어플리케이션 영역의 버퍼주소
// length : 버퍼크기
// off_what : ?
// 반환 : 정상이면 처리된 바이트 수를 반환한다.
// 주의 : 없음
//-------------------------------------------------------------------
ssize_t prnioport_write_byte(struct file *inode, const char *gdata,
size_t length, loff_t *off_what)
{
int i;
const char *data;
char c;
 
data=gdata;
 
for (i=0; i<length; i++)
{
get_user( c, (char *)(data+i) ); // 어플리케이션 영역에서 한 바이트를 얻는다.(uaccess.h에 정의)
outb( c , PRNIOPORT_ADDRESS ); // 프린터 포트에 쓴다.(io.h에 정의)
printk( "write %0X",c & 0xff );
}
 
// 사용된 길이만큼 돌려준다.
return(length);
}
 
//-------------------------------------------------------------------
// 설명 : insmod 함수에 의해서 호출되는 함수
// 매계 : 없음
// 반환 : 정상이면 0을 반환한다.
// 주의 : 없음
//-------------------------------------------------------------------
int init_module(void)
{
//
// 드라이버에 사용되는 접근함수에 대한 함수 포인터 구조체를 정의 한다. 
// 이 구조체는 fs.h 에 정의
static struct file_operations lcd_fops =
{
write:prnioport_write_byte, // ssize_t (*write) (struct file *, const
open:prnioport_open, // int (*open) (struct inode *, struct
release:prnioport_release, // int (*release) (struct inode *, struct
};
 
//
// 장치를 등록한다.
//
if( !register_chrdev( PRNIOPORT_MAJOR, // 장치 메이저 번호
PRNIOPORT_NAME , // 장치명
&lcd_fops // 장치 접근 함수 구조체 주소
) )
{
printk(" register_chrdev %s Ok", PRNIOPORT_MODULE_VERSION );
 
// 영역이 이미 사용되고 있는 지를 검사한다.
if (!check_region( PRNIOPORT_ADDRESS, // 사용되는 시작어드레스
PRNIOPORT_ADDRESS_RANGE // 시작 어드레스 부터의 갯수
))
{
// 사용되지 않고 있다면 IO 영역을 등록한다. ( ioport.h 에 정의 되어 있다.)
request_region(
PRNIOPORT_ADDRESS, // 사용되는 시작 어드레스
PRNIOPORT_ADDRESS_RANGE , // 시작 어드레스 부터의 갯수
PRNIOPORT_NAME // 장치명
);
printk(" got %d addresses from %#x",PRNIOPORT_ADDRESS_RANGE, PRNIOPORT_ADDRESS );
 
printk(" %s DRIVE REGISTER OK", PRNIOPORT_NAME );
 
}
else
{
// 이미 사용되었다면 등록된 장치를 제거한다.(ioport.h 에 정의되어 있다.)
unregister_chrdev( PRNIOPORT_MAJOR, // 장치 메이저 번호
PRNIOPORT_NAME // 장치명
);
printk(" could not get %d addresses from %#x",PRNIOPORT_ADDRESS_RANGE, PRNIOPORT_ADDRESS );
}
}
else
{
// 등록되지 못했다.
printk("unable to get major %d for %s", PRNIOPORT_MAJOR,PRNIOPORT_NAME );
}
 
return 0;
 
}
//-------------------------------------------------------------------
// 설명 : rmmod 함수에 의해서 호출되는 함수
// 매계 : 없음
// 반환 : 없음
// 주의 : 없음
//-------------------------------------------------------------------
void cleanup_module(void)
{
 
// 커널에 등록된 IO 어드레스 영역을 해제한다.
release_region( PRNIOPORT_ADDRESS, // 사용되는 시작 어드레스
PRNIOPORT_ADDRESS_RANGE // 시작 어드레스 부터의 갯수
);
 
// 등록된 장치 함수 구조체를 해제한다.
if( !unregister_chrdev( PRNIOPORT_MAJOR, // 장치 메이저 번호
PRNIOPORT_NAME // 장치명
))
{
printk("%s DRIVER CLEANUP O", PRNIOPORT_NAME);
}
else
{
printk("%s DRIVER CLEANUP FAILED", PRNIOPORT_NAME);
}
 
} 

이제는 컴파일할 Makefile 을 만들 차례다. Makefile 을 만드는 이유는 여러가지가 있겠지만, 내가 생각하기에는, 다음과 같다.

  1. 일일이 컴파일 옵션을 적어주는 번거로움을 덜 수 있다.
  2. 복수의 수정된 파일을 컴파일 할때, 바뀐 파일만 컴파일 할 수 있다.

또한 앞으로 맞닥들이게 될 프로그램들은 하나의 파일로 이루어진 것보다는 여러개의 모듈로 구성된 프로그램들이다. 여기서는 가장 쉽게 해볼 수 있는 예제를 든 것이지만…
여러개의 모듈의 프로그램을 컴파일 할 때 Makefile 의 중요성은 커진다. 서문이 너무 길었다. 만들어 보자!

#vi Makefile

Makefile 을 만들 때 주의할 점이 있다. 나 또한 그랬지만, 직접 타이핑하지 않고 복사&붙여넣기를 하게되면, 나중에 뒤에서 컴파일이 십중팔구 안 될 것이다. 이것은 바로 Makefile 의 특징을 잘 몰라서 저지르는 일반적인 실수이다.
Makefile 을 만들 때는 라벨(Label)을 제외한 모든 줄의 처음에 반드시 탭(Tab)를 눌러서 칸을 띄어 주어야 한다. 그렇지 않으면 구문 에러가 날 것이다.

# 디바이스 드라이버를 디버깅하기 위한 디버그 코드가 삽입되게 컴파일하기 위해서는
# 아래 라인을 살리면 된다.
#DEBUG = y
 
# 헤더 파일 디렉토리를 여기서 지정하거나 "make"의 명령라인에서 수정한다.
INCLUDEDIR = /usr/src/linux/include
 
# 디버그를 설정했을때의 컴파일 환경을 처리하는 부분
ifeq ($(DEBUG),y)
DEBFLAGS = -O -g -DJIT_DEBUG -DJIQ_DEBUG -DALL_DEBUG
else
DEBFLAGS = -O2
endif
 
CFLAGS = -D__KERNEL__ -DMODULE -Wall $(DEBFLAGS)
 
 
CFLAGS += -I$(INCLUDEDIR)
 
OBJS = prnioport.o
 
all: $(OBJS)
    $(CC) $(CFLAGS) -c $^ -o $@
 
clean:
     rm -f *.o *.~* 

이제 기다리던 직접 컴파일을 해보자!

#make

에러없이 수행되었다면, 컴파일에 성공한 것이다. 또한 디렉토리를 보면 *.o 오프젝트 파일이 생성되었을 것이다. 이것은 바로 커널 모듈이다. 어렵게 만든 모듈을 커널에 직접 올려보자!!

#insmod prnioport.o
#lsmod

모듈 목록에 방금 전에 올린 모듈이 있을 것이다. 뭔가 허전하지 않은가? 그렇다. User Application 을 만들지 않았다. -_-; 어서 만들어 주자!

#vi test.c

다음을 적어준다.

//
//******************************************************************************
//
// 헤더 정의
//
//******************************************************************************
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
 
//******************************************************************************
//
// 함수 정의
//
//******************************************************************************
//------------------------------------------------------------------------------
// 설명 : 디바이스 드라이버를 열고 닫고 쓰는 시험을 한다.
// 매계 : 없음
// 반환 : 정상이면 0을 반환한다.
// 주의 : 없음
//------------------------------------------------------------------------------
int main(int argc, char **argv)
{
int dev;
int loop;
char buff[2];
 
// 화일을 연다.
dev = open("/dev/prnioport", O_WRONLY|O_NDELAY );
if (dev != -1)
{
printf( "PRNIOPORT OPEN OK");
for( loop = 0; loop < 10; loop++ )
{
sleep(1); // 1 초 대기 한다.
buff[0] = 0x00; // LED를 끈다.
write(dev,buff,1); // 프린터 포트에 쓴다.
sleep(1); // 1 초 대기 한다.
buff[0] = 0xff; // LED를 켠다.
write(dev,buff,1); // 프린터 포트에 쓴다.
}
close(dev);
printf( "PRNIOPORT CLOSE
");
}
else
{
// 화일 열기 실패
printf( "PRNIOPORT OPEN FAIL");
exit(-1);
}
 
return(0);
} 

곧바로 컴파일 하자! 이것은 User Application 이기 때문에 바로 해주면 된다.

#gcc -o test test.c

여기서 잠깐 실행하기 전에 해주어야 하는 것이있다. 그것은 바로 디바이스 파일을 만들어주어야 하는데, 눈치 빠른 사람이라면 벌써 만들었는지도 모르겠다. 위의 소스를 보면, /dev/prnioport 파일을 open 하고 있다. 당연히 해당 파일이 없다면, 에러가 날 것이다. 그래서 디바이스 파일을 만들어 주어야 한다.

#mknod /dev/prnioport c 88 0

실행하기

이제야 처음에 준비한 LED를 써먹을 때가 왔다. LED를 보면 다리가 한쪽이 긴 것과 짧은 것이 있다. 긴쪽은 (+)이고, 짧은 쪽은 (-)이다.
긴쪽을 프린터 포트 2번 구멍에 꼽고 짧은 쪽을 18번과 25번 사이에 꼽아 보자!
그리고 방금 위에서 컴파일한 test 파일을 실행해보자!!

#./test

어떤가? LED가 깜박깜박 거리지 않는가!!
이번에는 2.6 대 커널에서 한번 해보자~