여기서는 인터럽트를 사용하는 응용으로써 키패드 디바이스 드라이버를 설명한다. 키패드는 내부에 디코더를 가지고 있어 눌려진 키에 대한 4비트 BCD 데이터를 출력하며 이출력은 4비트 래치를 거쳐 프로세서의 데이터 버스에 입력된다. 4비트를 위해 사용되는 데이터 버스의 비트 위치는 비트 15 ~ 비트 12로 가정하였다.


키패드 회로에서는 주소 0x0C00 0000에서 래치 동작신호를 발생시키기 위하여 주소 디코더에 칩 선택 신호 CS3를 사용하고있다. 또 키패드에서 발생하는 인터럽트를 처리하기 위해 인터럽트 요청 신호가 PXA 프로세서의 GPIO핀에 연결되어 있다.


key-drv.h 소스 


#include "linux/kernel.h"
#include "linux/module.h"
#include "linux/init.h"
#include "linux/types.h"
#include "linux/ioport.h"
#inlcude "unistd.h"
#include "linux/slab.h"
#include "linux/mm.h"
......


#define KEY_ADDR 0xF3000000

#define KEY_IRQ IRQ_GPIO(19)

static int KEY_MAJOR = 67;

static DECLARE_WAIT_QUEUE_HEAD(key_queue);

key_drv.h의 처음 부분에는 디바이스 드라이버 구현에 필요한 각종 헤더파일들과 인터럽트 사용에 필요한 헤더파일들이 있다.


키패드 디바이스 파일의 주번호로 67이 정의되어 있다.


다음문에서는 인터럽트가 발생할 때까지 프로세스를 대기상태로 두는 대기큐를 선언한다.


DECLARE_WAIT_QUEUE_HEAD(key_queue);


다음 문은 리눅스 커널 소스에서 해당 임베디드 보드에 대한 물리주소와 가상주소를 서로 연결해주기 위한 것이다.


#define KEY_ADDR 0xF3000000



다음은 디바이스 드라이버 실제 구현 부분인 key-drv.c 부분이다.




#include "key-drv.h"

static unsigned short *key;
static int key_data =0;

static void key_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
  key_data = (*(key) & 0xf000) >> 12;

  printk("Interrupt Service : Input Data from Keypad : %d \n",key_data);

 wake_up_interruptible(&key_queue);
}

static ssize_t key_read(struct file *filp, char *buf,size_t count,loff_t *l)
{
  int tmp;

 tmp = key_data;

  intteruptible_sleep_on(&key_queue);

 copy_to_user(buf,&key_data,sizeof(tmp));
 return 0;
}

static int key_open(struct inode *inode, struct file *filp)
{
  MOD_INC_USE_COUNT;
  printk("keypad Device Open\n");
  return 0;
}

static int key_release(Struct inode *inode, struct file *filp)
{
 MOD_DEC_USE_COUNT;
 printk("Keypad Device Released\n");
 return 0;
}
 
static struct file_operations key_fops = {
 open : key_open,
 read : key_read,
 release : key_release,


};


static int __init key_init(void)
{
  int res, result;

   if((result = register_chrdev(KEY_MAJOR,"key",&key_fops))<0){
     printk(" register_chrdev() FAIL! \n");
     return result;
   }

   if(!check_region(KEY_ADDR,2))
    request_region(KEY_ADDR, 2, "key");

  key = (unsigned short *)KEY_ADDR;

  if((res = request_irq(KEY_IRQ, &key_interrupt, SA_INTERRUPT, "key", NULL))<0)
     printk(KERN_ERR, "request_irq() FAIL!\n");

  set_GPIO_IRQ_edge(IRQ_TO_GPIO_2_80(KEY_IRQ), GPIO_FALLING_EDGE);

  return 0;
}

void __exit key_exit(void)
{
   disable_irq(KEY_IRQ);
         free_irq(KEY_IRQ,NULL);

   release_region(KEY_ADDR, 2);
   unregister_chrdev(KEY_MAJOR,"key");
}

module_init(key_init);
module_exit(key_exit);

}


블로그 이미지

종환 Revolutionist-JongHwan

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

댓글을 달아 주세요

 

* 인터럽트 제어레지스터

 

pxa 계열 프로세서의 인터럽트 제어 방식 및 관련 레지스터는 거의 유사하다. pxa계열 프로세서의 인터럽트 컨트롤러는 내부 유닛에서 발생하는 각종 인터럽트 및 GPIO 핀을 통한 외부 장치로 부터의 인터럽트 요청(IRQ)을 모두 처리한다.

 

PXA 계열 프로세서에서의 인터럽트는 상위레벨과 하위레벨의 두 단계 구조를 가진다.

상위레벨은 인터럽트를 발생하는 장치들이며 하위레벨은 각 장치 내부의 개별적인 인터럽트 소스들이다. 이들 내부 인터럽트 소스의 구분은 각 장치 내부의 상태 레지스터를 읽으므로써 알 수 있다.

 

 

예를 들어 DMA 컨트롤러는 상위레벨 인터럽트 발생 장치인데, DMA 컨트롤러 내부에는 16개의 채널이 있고 이들 개별 채널이 각각 하위레벨 인터럽트 소스가 된다. 따라서 DMA 컨트롤러는 16개의 하위레벨 인터럽트 소스를 가진다. 또 UART는 상위레벨 인터럽트 발생 장치인데, UART 내부에서는 송신 인터럽트, 수신 인터럽트, 에러 인터럽트 등 여러개의 하위레벨 인터럽트 소스를 가지는 것이다.

 

인터럽트 기능을 제어하는 레지스터들은 다음과 같은 것들이 있다

 

Interrupt Controller Pending Register(ICPR)

 

Interrupt Controller IRQ Pending Register(ICIP)

 

Interrupt Controller FIQ Pending Register(ICFP)

 

Interrupt Controller Mask Register(ICMR)

 

Interrupt Controller Level Register(ICLR)

 

Interrupt Controller Control Register(ICCR)

 

 

위의 인터럽트 컨트롤러 레지스터 중 ICPR 레지스터는 인터럽트 발생 시 해당 비트가 "1"로 된다.

 

인터럽트 소스로 IS 8 ~ IS 31 까지가 정의되어 있고 해당 인터럽트 장치(소스)에서 인터럽트 발생 시 해당 비트가  "1"로 되며 커널에서는 이를 보고 해당 장치의 인터럽트 핸들러를 찾을 수 있게 된다. 해당 장치의 인터럽트 핸들러에서는 해당 인터럽트 장치의 내부 레지스터를 접근해서 실제 인터럽트를 발생한 하위레벨 인터럽트 소스를 찾은 다음 여기에 대한 처리를 한다.

 

인터럽트 소스 IS 11은 USB 장치이며 하위레벨 인터럽트를 7개 가지는 것을 알 수 있고, 인터럽트 소스 IS 10 PXA 프로세서의 GPIO 핀 2~80으로부터의 외부 인터럽트이며 각 핀마다 인터럽트를 가지므로 총 79개의 하위레벨 인터럽트를 가짐을 알 수 있다. GPIO 총 79개 핀 중 하나라도 인터럽트가 발생하면 인터럽트 소스 IS 10은 "1"로 되며, 인터럽트 소스 IS 10에 대한 인터럽트 핸들러에서 GPIO 어느 핀에서 인터럽트가 발생하였는지 검색하여 여기에 대한 인터럽트 처리를 하는 것이다.

 

ICLR 레지스터는 발생한 인터럽트가 일반 인터럽트인 IRQ인지 고속 인터럽트인 FIQ인지 나타낸다. 해당 비트가 '0'이면 IRQ이고 "1"이면 FIQ 이다....

 

 

* 인터럽트 처리 및 등록

 

처리 : IRQ 인터럽트가 발생하면 현 프로그램 카운터 값을 링크 레지스터에 저장하고 CPSR(Current Processor Status Register) 값을 SPSR_irq(Saved Processor Status Register)에 각각 저장한다.

 

그리고 IRQ의 exception 벡터 주소로 점프하여 인터럽트 처리가 시작된다. 인터럽트 처리 완료 후에는 링크 레지스터에 저장된 복귀 주소로 리턴되고 SPSR_irq 레지스터 내용도 다시 CPSR로 복구하여 인터럽트 이전 상태로 돌아간다.

 

등록 : 인터럽트를 사용하고자 하는 디바이스 드라이버는 인터럽트 서비스 루틴을 등록하고 이는 리눅스 커널의 전역변수 irq_desc에 등록된다. 인터럽트가 발생하면 커널은 ICPR 레지스터에서 "1"인 비트를 조사해 인터럽트를 발생한 장치 번호를 얻고, do_IRQ() 함수에서 전역변수 irq_desc를 조사해 해당 인터럽트 서비스 루틴을 찾는 방식으로 동작한다.

 

* GPIO 핀을 인터럽트 입력으로 설정하기

 

PXA 계열 프로세서의 GPIO 핀들은 외부장치로부터의 인터럽트 요청을 받는 입력신호로 사용할 수 있다. 이와 같이 설정하기 위해서 필요한 PXA 계열 프로세서의 GPIO 관련 레지스터들 및 그 기능은 다음과 같다.

 

인터럽트 입력으로 설정할 때

 

GPDR 레지스터 : 인터럽트 입력으로 사용할 핀의 동작 방향을 입력으로 설정한다.

 

GAFR 레지스터 : 인터럽트 입력으로 사용할 핀은  부가기능을 OFF 시켜야 하므로 이에 대응한 비트를 "00"으로 설정한다.

 

GRER 레지스터 : 인터럽트 입력으로 사용할 핀에서 신호가 상승 에지 시 인터럽트를 검출하고자 할 때 설정한다.

 

GFER 레지스터 : 인터럽트 입력으로 사용할 핀에서 신호가 하강 에지 시 인터럽트를 검출하고자 할 때 설정한다.

 

GEDR 레지스터 : 인터럽트 신호가 들어오면 해당 핀에 대응하는 비트가 "1"로 된다.

 

예를 들어 GPIO 40번 핀을 하강 에지로 설정하려면 위의 GFER1 레지스터의 비트 8을 "1"로 해주면 되고 GPIO 28핀을 상승 에지로 설정하려면 GRER레지스터의 28을 "1"로 해주면 된다. 이와 같은 기능을 대신해주는 함수로 set_GPIO_IRQ_edge()이 있다.

 

첫 번째 인수는 인터럽트 입력으로 설정하려는 GPIO 핀 번호이고 두 번째 인수는 플랙으로서 해당 핀을 상승에지로 설정하려면 GPIO_RISING_EDGE를 하강에지로 설정하려면 GPIO_FALLING_EDGE를 각각 넣으면 된다.

 

 

블로그 이미지

종환 Revolutionist-JongHwan

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

댓글을 달아 주세요

문자 디바이스 드라이버 프로그래밍의 예로써 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 항상 겸손하자.

댓글을 달아 주세요