문자 디바이스 드라이버 프로그래밍의 예로써 LED 장치 디바이스 드라이버를 설명한다.

 

 

#include "linux/kernel.h"
#include "LINUX/module.h"
#include "LINUX/init.h"
#include "LINUX/types.h"
#include "LINUX/ioport.h"
#include "UNISTD.H"
#include "LINUX/slab.h"
#include "LINUX/mm.h"
#include "ASM/hardware.h"
......

#define LED_ADDR0xF3000C00  // 베이스 주소+LED offset 값 0x0C00

static int led_major = 63;

 

 

 

led-drv.h에는 linux/kernel.h, linux/module.h 등 디바이스 드라이버 구현에 필요한 각종 헤더 파일들이 포함되어 있다. LED 디바이스 파일의 디바이스 주 번호로 63이 정의되어 있다.

 

참고로 linux/module.h는 커널 모듈 프로그래밍에서 필요한 정의, 함수 등이 선언되어 있다.

 

linux/init.h는 module_init()과 module_exit(), 매크로등이 선언되어 있다. 

 

 

디바이스 드라이버 실제 구현부분인 led-drv.c 의 소스는 다음과 같다

#include "led-drv.h"

int led_open(struct inode *inode, struct file *filp)
{
MOD_INC_USE_COUNT;
printk("LED device open \n");

return 0;
}

static ssize_t led_write(struct file *file, const char *buffer, size_t length, loff_t *offset){

unsigned char *led_port;
size_t len = length;
int value;

get_user(value, (int *)buffer);
printk("data from application program : %d\n",value);

led_port = (unsigned char *)(LED_ADDR);
*led_port = value;
return len;
}

int led_release(struct inode *inode, struct file *filp)
{
MOD_DEC_USE_COUNT;
return 0;
}

struct file_operations led_fops = {
open : led_open,
write : led_write,
release : led_release,
};

static int __init led_init(void)
{
int result;

result = register_chrdev(led_major,"led_device",&led_fops);
if(result<0)
{
printk(KERN_WARNING "register_chrdev() FAIL!\n");
return result;
}

if(!check_region(LED_ADDR,2))
  request_region(LED_ADDR,2,"led_device");
else
printk(KERN_WARNING "check_region() FAIL!\n");

return result;
}

static void __exit led_cleanup(void)
{
   release_region(LED_ADDR, 2);
if(unregister_chrdev(led_major, "led_device"))
printk(KERN_WARNING "unregister_chrdev() FAIL!\n");

}

module_init(led_init);
module_exit(led_cleanup);


 

 

 

 

 

 

위 소스에서 마지막 줄의 module_init(led_init)와 module_exit(led_cleanup) 문에서는 커널 모듈이 로드될 때 호출되는 함수가 led_init()이고 언로드 될 때 호출되는 함수가 led_cleanup()임을 나타낸다.

 

led_init 시에 check_region(LED_ADDR,2) 문을 사용해 LED_ADDR이 가리키는 영역이 현재 입출력(I/O) 포트로 사용할 수 있는 영역인지 확인한다. check_region() 함수의 포맷은 다음과 같다.

 

int check_region(unsigned int from,unsigned int extent)

 

위에서 from 인수는 입출력 포트에 해당하는 영역의 시작 주소이고, extent는 그영역의 범위를 나타낸다.

 

check_region() 함수가 성공하면, request_region(LED_ADDR, 2, "led_device") 문을 사용해 LED_ADDR이 가리키는 메모리 영역을 입출력 포트로 사용하기 위해 할당한다.

 

할당 후에는 LED_ADDR을 사용해 디바이스에 대한 접근을 할 수 있다.

 

 

request_region() 함수의 포맷은 다음과 같다.

 

void request_region(unsigned int from, unsigned int extent, const char *name);

 

위에서 from 인수는 입출력 포트에 해당하는 메모리 영역의 시작 주소이고, extent는 그 영역의 범위를 나타내며, name은 디바이스의 이름이다. 즉 request_region() 함수는 디바이스의 입출력 포트를 위해 특정 주소의 메모리 영역을 커널에서 확보하는데 사용한다.

 

led_write() 함수는 실제 LED로 출력을 하는데 다음 문에서는 get_user()함수를 이용해 사용자 메모리 영역(buffer)의 데이터를 커널 영역의 메모리(value)로 옮기고 있다. copy_from_user() 함수를 사용해도 동일한 작업을 할 수 있다.

 

get_user(value, (int *)buffer);

 

다음 문에서는 request_region() 함수로 확보한 메모리 영역에 접근하여 LED_ADDR을 사용하여 이를 통해 사용자로 부터 받은 값을 LED 디바이스로 직접 출력하고 있다.

 

led_ port = (unsigned char*)(LED_ADDR)

*led_port = value;

 

led_release() 함수는 사용자 응용 프로그램에서 close() 함수를 사용했을 때 호출되는 함수이며, 여기서는 단지 MOD_DEC_USE_COUNT 매크로를 이용하여 이 디바이스를 이용하는 프로그램 개수(usage count)를 줄이는 기능만 수행한다.

 

사용자 응용 프로그램 led-drv-app.c 소스는 다음과 같다

 

 

#include 
#include 
#include 

int main() {

int fd;
int value;

if((fd = open("/dev/led_device",O_RDWR))<0){
printf(" open() FAIL! \n");
exit(-1);
}

for(value=0; value<64; value++){
write(fd,&value,sizeof(int));
usleep(8000);
}

close(fd);
return 0;
}



 

 

 

 

 

다음 Makefile은 위에서 설명한 디바이스 드라이버 소스 led-drv.c와 사용자 응용 프로그램 led-drv-app.c를 각각 컴파일 하여 커널 모듈인 led-drv.o와 사용자 실행 프로그램 led-drv-app를 생성하기 위한 것이다.

CC = arm-linux-gcc
STRIP = arm-linux-strip

INCLUDEDIR = /src/linux-2.4.21/include

CFLAGS = -D__KERNEL__ -DMODULE -02 -Wall -I$(INCLUDEDIR)

EXECS = led-drv-app
MODULE_OBJ = led-drv.o
SRC = led-drv.c
ARRP_SRC = led-drv-app.c
HDR = led-drv.h

all : $(MODULE_OBJ) $(EXECS)

$(MODULE_OBJ) : $(SRC) $(HDR)
 $(CC) $(CFLAGS)  -c $(SRC)

$(EXECS) : $(APP_SRC)
$(CC)  -o $@   $(APP_SRC)
$(STRIP) $@

clean : 
rm -f *.o  $(EXECS)

 

 

 

 

위에서 CFLAGS는 커널 모듈을 컴파일 하기 위해 필요한 컴파일 옵션들은 선언하고 있다. 여기서 옵션 -Wall은 'Warning All'의 의미로 모든 경고 메시지를 표시하라는 것으로 이는 커널 모듈은 커널 레벨에서 동작하므로 응용 레벨 프로그램과는 달리 에러 발생 시 시스템 전체에 영향을 미칠 수 있는 상황을 막기 위한 보호 장치가 없기 때문에 조심하기 위한 것이다.

 

컴파일 옵션 -O2는 최적화 레벨을 나타낸다. 이 옵션은 컴파일 시 최적화하지 않는 경우 인라인 함수를 외부로 공개하는 심벌로 간주해 에러가 발생할 수 있기 때문에 사용한다. 옵션 -O를 사용해도 된다.

 

컴파일 옵션 -D__KERNEL__은 코드가 커널 모드에서 실행됨을 알려주며, 옵션 -DMODULE은 헤더파일에게 소스가 모듈임을 알려 준다. INCLUDEDIR은 커널 모듈을 생성하기 위해서 필요로 하는 헤더 파일 위치를 나타낸다.

 

커널 모듈 컴파일 시는 링크 과정이 없으므로 옵션 -c를 사용하였고, 사용자 프로그램 컴파일 시는 링크 과정을 통해 실행파일이 생성되어야 하므로 옵션 -o가 사용되었다. 위 Makefile 파일에 대해 make 명령을 실행하면 커널 모듈 led-drv.o와 사용자 실행프로그램 led-drv-app가 생성된다.

 

디바이스 드라이버 실행 준비를 위해 먼저 위에서 얻은 디바이스 드라이버 파일 led-drv.o와 실행파일 led-drv-app을 임베디드 보드로 전송해야 한다. 이를 위해서 이 책에서는 임베디드 보드가 부팅 과정에서 NFS를 사용하여 마운트하는 루트파일 시스템이 호스트 컴퓨터의 /test/nfs 디렉터리라고 설정하였으므로 다음과 같이 cp 명령을 사용하여 임베디드 보드의 '/root' 디렉터리, 즉 root 계정의 홈디렉터리로 보낸다.

 

cp led-drv.o /test/nfs/root

cp led-drv-app /test/nfs/root

 

이후 호스트에서 시리얼 통신 프로그램을 실행시켜 임베디드 보드와 연결 후 사용자 계정 root로 임베디드 보드에 로그인 하면 위에서 cp명령으로 전송했던 파일이 root의 홈 디렉터리에 있는 것이 보인다.

  LED 디바이스 드라이버 실행을 위해 먼저 LED 디바이스 파일을 만들어야 한다. 이를 위해 다음 명령을 수행하여 디바이스 주(major) 번호 63으로 LED디바이스 파일을 생성한다.

 

mknod /dev/led_device c 63 0

 

다음 명령을 수행하여 LED 디바이스 드라이버 모듈인 led-drv.o를 메모리에 로드한다.

 

insmod led-drv.o

 

insmod 명령을 사용하여 LED 디바이스 드라이버를 메모리에 로드한 후에는 다음과 같이 하여 사용자 응용 프로그램 led-drv-app을 실행할 수 있다.

 

/led-drv-app

 

실행 결과로 임베디드 보드의 8개 LED에 0~64에 해당 하는 2진수 값이 차례로 표시된다.

 

 

블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

댓글을 달아 주세요

사용자 응용프로그램에서 디바이스 드라이버와 서로 데이터를 주고받기 위해서는 보통 read()나 write()  함수를 사용한다.

반면 응용프로그램에서 ioctl() 함수는 복잡한 기능의 디바이스를 제어하기 위해 사용한다.

 

응용 프로그램에서 호출하는 ioctl 함수 포맷 : int ioctl(int d, int request, ....)

 

여기서 ioctl 함수의 첫 번째 인수는 open 함수로 오픈된 디바이스 파일을 가리키는 파일 디스크립터이다. 두 번째 인수는 디바이스 드라이버 내에서 사용하기 위해 정의된 명령어 번호이다. ioctl 함수의 세번째 인수가 '....'인 것은 세번째 인수가 char 타입, long타입, pointer 타입 등 여러가지 데이터 타입을 가질 수 있어 컴파일 시 인수 타입 불일치로 인한 에러 발생을 막기 위한 것이다.

 

위의 ioctl() 함수의 두 번째 인수인 명령어 번호는 보통 디바이스 드라이버 개발자가 할당한다. 리눅스 커널에서는 이 명령어 번호를 쉽게 관리할 수 있는 매크로를 제공한다.

 

 

매크로 

기능 

_IO(type,nr) 

ioctl() 함수의 세 번째 인수를 사용하지 않을 때 사용 

_IOR(type, nr, size) 

ioctl() 함수의 세 번째 인수가 디바이스 드라이버에서 read될 때 사용 

)IOW(type, nr, size) 

ioctl() 함수의 세 번째 인수가 디바이스 드라이버로 write될 때 사용 

_IOWR(type, nr, size)

ioctl() 함수의 세 번째 인수가 읽기/쓰기/동작에 모두 사용될 때 사용 

 

 

아래에 LCD 디스플레이 디바이스 드라이버에서 매직넘버 및 ioctl() 함수의 명령어를 정의하는 예를 보였다.

 

#define LCD_DEV_MAGIC 'Y'

 

#define LCD_INIT _IO(LCD_DEV_MAGIC,0)

#define LCD_CMD _IOW(LCD_DEV_MAGIC, 1, unsigned char)

#define LCD_FILL _IOW(LCD_DEV_MAGIC, 2, unsigned char)

 

위 예에서 ioctl() 함수의 LCD_INIT 명령은 사용자 응용 프로그램과 디바이스 드라이버 사이에서 데이터 전달이 필요 없는 명령이어서 _IO 매크로를 사용하였다. LCD_CMD, LCD_FILL 명령은 사용자 응용 프로그램에서 디바이스 드라이버로 데이터를 보내는(write) 기능이므로 _IOW() 매크로를 이용하였다. 이때 전달하는 데이터는 1바이트 문자로 하였다. 

 

사용자 실행 프로그램에서 위와 같이 ioctl() 함수를 호출하면 해당 디바이스 드라이버에서 file_operations 구조체가 가리키는 ioctl 함수가 호출된다

 

file_operations->(*ioctl) (struct inode*, struct file *, unsigned int, unsigned long)

 

 

 

블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

댓글을 달아 주세요

static int __init chrdev_init(void)
{
int result;
result = register_chrdev(63, "chrdev_dd",&chrdev_fops);
if(result<0)
{
printk("chrdev_dd: 에러!\");
return result;
}
return result;
}

int chrdev_open(struct inode *inode, struct file *filp){
 MOD_INC_USE_COUNT;
 return 0;
}

static ssize_t chrdev_read(struct file * file, char *buffer, size_t, count, loff_t *offset){
 chrdev_buff = *chrdev_addr;
 copy_to_user(buffer, &chrdev_buff, size);
}

static ssize_t chrdev_write(struct file *file, const char *buffer, size_t length, loff_t *offset){
  int value, *keyReg;
  ......생략...........
  get_user(value,(int*)buffer);
  *chardev_wr_addr = ~value;
 .......생략..........
}

int chrdev_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
{
    return 0;
}

int chrdev_release(struct inode *inode,struct file *filp){
MODE_DEC_USE_COUNT;
return 0;
}

struct file_operations chrdev_fops = {
open : chrdev_open;
read : chrdev_read;
write : chrdev_write,
ioctl : chrdev_ioctl,
release : chrdev_release,
};

module_init(chrdev_init)l
module_exit(chrdev_cleanup);


 

 

 

 

 

위에서 module_init(chrdev)init);문에 의해 디바이스 드라이버가 메모리에 로드될 때 chrdev_init()함수가 실행되며 이때 등록함수 register_chrdev()가 호출된다. 함수에서 인수로 디바이스 주번호 63 디바이스명은 chrdev_dd file_operations 구조체 포인터는 chrdev_fops를 사용한다. 커널 레벨에서의 문자 출력 함수인 printk()문은 등록 실패시 실행된다.

 

 

응용프로그램에서 open("/dev/chrdev_dd", O_RDWR) 식으로 open()함수를 호출하면, 커널 내부에서는 /dev 디렉터의 디바이스 파일 chrdev_dd에 할당된 디바이스 주(major) 번호를 가지고 chardevs[] 배열에서 해당하는 문자 디바이스 드라이버의 file_operations 구조체 멤버 (*open)이 가리키는 chrdev_open()함수를 호출한다.

 

디바이스 파일 chrdev_dd의 오픈에 성공하면 파일 디스크립터를 얻게 되고 이후 응용프로그램에서는 이 파일 디스크립터를 사용해 해당 디바이스에 접근(read/write)할 수 잇다. 이 open()함수는 디바이스를 초기화하고 필요한 자원을 할당받으며 MOD_INT_USE_COUNT 매크로를 이용해서 카운트 값을 1 증가시켜 디바이스 드라이버가 사용중임을 표시한다.

 

 

여기서 file_operations 구조체 chrdev_fops에는 응용프로그램의 open(), read(), write(), ioctl(), release()에 대응하는 디바이스 드라이버 내부 함수 chrdev_open(),chrdev_read(), chrdev_write, chrdev_ioctl(), chrdev_release()를 각각 정의하고 있다.

 

chrdev_read() 함수에서는 실제 디바이스 주소로부터 데이터를 읽어온 후 이를 copy_to_user 매크로를 사용해 to포인터가 가리키는 사용자 메모리로 from 포인터가 가리키는 커널 메모리에 있는 내용을 복사한다.

 

디바이스에 데이터를 출력하기 위해 응용프로그램에서 write() 함수를 호출하면 이때 문자 디바이스 드라이버 내의 함수 chrdev_write()가 실행된다. chrdev_write()에서는 디바이스에 데이터를 쓰기 전에 copy_from_user(to, from, n) 매크로를 사용해 from 포인터가 가리키는 사용자 메모리 영역의 내용을 to 포인터가 가리키는 커널 메모리 영역으로 복사한다.

 

응용프로그램에서 ioctl() 함수를 호출하면 디바이스 드라이버 내의 함수 chrdev_ioctl()가 실행된다. 응용프로그램에서 read()나 write() 함수는 디바이스와 데이터를 주고 받는데 주로 사용하며, 이를 통해 디바이스의 기능 자체를 제어하기는 적합치 않은 경우가 있다. 이런 경우 디바이스 자체를 제어하는 특수 기능은 ioctl()을 통해서 한다.

블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

댓글을 달아 주세요


* 리눅스 디바이스 드라이버


 리눅스 디바이스 드라이버는 리눅스 커널의 일부분으로 포함되어 컴퓨터나 임베디드 시스템의 하드웨어를 제어하는 기능 및 이에 대한 인터페이스를 제공한다.

임베디드 시스템에서 주변장치를 제어하고자 하는 응용프로그램은 리눅스 디바이스 드라이버가 제공하는 인터페이스를 통하여 해당 장치를 제어할 수 있다. 


 리눅스 디바이스 드라이버는 리눅스 커널 자체에서 이미 지원하는 것도 있고 임베디드 시스템 개발 과정에서 개발자가 직접 작성해야 하는 경우도 있다. 


- 문자 디바이스 


 문자 디바이스는 바이트 단위로 동작하며 파일처럼 바이트 스트림으로 읽거나 쓸 수 있는 디바이스이며, 블록 디바이스는 블록 단위로 동작하며 하드디스크처럼 내부에 파일시스템을 가질 수 있는 디바이스이다. 


- 블록 디바이스 


 블록 디바이스는 하드디스크나 USB 메모리 처럼 내부에 파일시스템을 가질 수 있는 디바이스다


* 문자 디바이스 드라이버


 디바이스 드라이버도 디바이스 종류와 마찬가지로 문자 디바이스 드라이버, 블록 디바이스 드라이버 및 네트워크 디바이스 드라이버로 분류할 수 있다.. 임베디드 시스템 개발 시 가장 많이 사용하는 문자 디바이스 드라이버의 작성 시에는 응용 프로그램에서 이 디바이스를 사용할 수 있도록 하기 위해 open(), close(), read(), write(), ioctl() 같은 시스템 호출 함수들을 구현해야 한다. 



* 리눅스 커널 모듈 


 리눅스 시스템에서 디바이스 드라이버는 커널 모듈 형태로 만들어진다. 커널 모듈은 커널의 재컴파일 및 설치가 필요 없이 동적으로 리눅스 커널에 모듈 형태로 커널 기능의 추가나 제거를 가능하게 해준다. 따라서 리눅스 부팅 이후 커널이 실행중인 동안에도 필요 시에 커널에 기능을 추가할 수 있다.


커널 모듈 코드에는 init-module() 함수와 cleanup_module() 함수가 반드시 포함되는데, init_module() 함수는 커널 모듈이 메모리에 적재될 때 호출되며 cleanup_module()은 커널 모듈이 제거될 때 호출된다. 커널 모듈은 실행파일이 아닌 오브젝트 파일 형태이므로 커널 모듈을 컴파일할 때는 C 컴파일러에게 오브젝트 코드만을 생성하도록 하는 '-c' 옵션을 주면 된다. 


커널 모듈이 실제로 링크되는 시점은 메모리에 적재될 때이다. 커널 모듈을 메모리에 적재할 때는 insmod명령을 사용하고, 제거할 때에는 rmmod 명령을 사용한다.

커널 모듈소스에는 헤더 파일로서 linux/module.h가 포함되어야 한다. 




간단한 커널 모듈프로그램

#define MODULE

#include "linux/module.h"
#include "linux/kernel.h"

int init_module(void)
{
printk(" Linux Kernel Module Loading ... \n");
return 0;
}

void cleanup_module(void)
{
printk(" Linux Kernel Module Unloading ...\n");
}



커널 모듈이 메모리에 적재될 때(insmod 명령 사용) init_module()함수에 의해 적재된 커널 모듈이 초기화되면 자신을 현재 실행중인 커널에 등록한다. 이때 등록에 사용하는 함수 이름은 문자 디바이스의 경우 register_chrdev(), 블록 디바이스의 경우 register_blkdev() 이다.


rmmod 명령에 의해서 커널 모듈이 제거될 때에는 cleanup_module()함수가 호출되어 init_module() 함수에서 할당받은 자원을 반환하고, 문자 디바이스의 경우 unregister_chrdev() 함수를 사용하여 커널 모듈의 등록을 해제한다. 블록 디바이스의 경우 등록 해제에 unregister_blkdev()함수를 사용한다.



* file_operations 구조체 


file_operations 구조체는 사용하려는 응용 프로그램과 디바이스 드라이버를 서로 연결해 주는 기능을 하며, 디바이스 드라이버 내에서 구현된 함수들에 대한 포인터들로 구성되어 있다.  file_operations 구조체는 include/linux/fs.h 헤더 파일에 정의되어 있으며 다음에 이 내용의 일부를 보였다.










블로그 이미지

종환 Revolutionist-JongHwan

github.com/alciakng 항상 겸손하자.

댓글을 달아 주세요