디바이스 드라이버8 - 인터럽트

kenGwon·2024년 2월 27일
0

[Embedded Linux] BSP

목록 보기
31/36

인터럽트를 적용하는 이유


폴링으로 키입력을 조사하고 있으면 CPU 로드가 100%가 찍히면서 금방 CPU가 과열된다. 프로세스가 계속 준비상태랑 실행상태를 왔다갔다 하면서(문맥교환 하면서) 키입력을 감지하고 있는 상황이기 때문이다.

이렇게 할 필요가 없다. 여기에 인터럽트를 적용해야 한다. 인터럽트를 적용하면 인터럽트가 들어오기 전까지 프로세스의 상태를 대기(sleep)상태로 만들어 놓는 식으로 진행하게 때문에 CPU가 100% 사용될 일이 없다. 대기상태에 들어간 프로세스를 깨우는 역할(Wakeup)을 하는게 바로 인터럽트 핸들러인 것이다.

1단계: "dev.c"에서 key 입력 여부 검사방식을 폴링에서 인터럽트로 변경한다.
2단계: "app.c"에서 do~while()에 blocking I/O를 적용한다.

위 두단계가 모두 적용되어야 CPU 로드 100%가 풀린다.


인터럽트를 적용한 ledkey

우리는 인터럽트 등록/해제 하는 것은 insmod 할때랑 rmmod 할 때 하도록 할 것이지만, 보통은 open(), close() 할 때 해주는게 보통이다.

gpio핀을 외부 인터럽트 핀으로 설정해줘야 한다.
1. gpio 핀을 먼저 input 상태로 만든다.
2. irq 등록을 해준다.

아래는 전역변수를 쓰지 않고 void *data를 사용하여 매개변수를 사용하여 값을 넘겨주는 방식까지 적용된 코드이다.

동적메모리 할당 및 구조체를 이용한 인터럽트 서비스루틴과의 정보공유를 위해 open() close()에 수정이 발생한 경위

모든 시스템 콜 함수에는 파일포인터(struct file* filp)를 넘기도록 되어있다.
즉 그것은 다시 말해 파일포인터를 이용하면 모든 시스템 콜 함수에서 동일한 메모리 영역에 접근할 수 있다는 것이다. 즉 동적할당을 하려면 파일포인터를 접근해야만 한다.
근데 insmodrmmod에 대한 init()과 exit()에는 인자로 파일포인터가 없다.(당연한 것이다. 미래의 열리지도 않은 파일포인터를 어떻게 인자로 주겠는가.) 그래서 우리는 open()과 close()에서 모든 인터럽트 등록/해제를 진행함으로써 모든 시스템 콜 함수에서 파일포인터에 접근할 수 있도록 하겠다.

우리는 file 구조체(struct file *filp)의 멤버변수인 void *private_data;에다가 지역변수로 선언된 동적할당 지역변수 포인터를 넘겨주면 그렇게 함으로써 모든 디바이스 드라이버 함수에서 *filp를 통해서 해당 동적할당 메모리에 접근할 수 있게 되는 것이다.

dev.c

코드 바탕은 p238_ledkey이다.

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/gpio.h> // gpio
#include <linux/interrupt.h>
#include <linux/irq.h>

#define DEBUG 1

#define LEDKEY_DEV_NAME			"ledkey_dev"
#define LEDKEY_DEV_MAJOR		230 // 디바이스 주번호

#define OFF 0
#define ON 1
#define GPIOLEDCNT 8
#define GPIOKEYCNT 8

static int gpioLed[GPIOLEDCNT] = {6,7,8,9,10,11,12,13};
static int gpioKey[GPIOKEYCNT] = {16,17,18,19,20,21,22,23};

// 이건 전역변수가 아니다. 그저 구조체의 틀을 선언했을 뿐이다. 자료형을 선언했을 뿐이다.
// 구조체를 사용해야지만 인터럽트 서비스 루틴에다가 정보를 넘겨줄 때 여러개의 정보를 한번에 넘겨줄 수 있기 때문에 구조체를 선언한 것이다.
typedef struct {
	int key_irq[8];
	int keyNumber;
} keyData;

static int gpioLedInit(void);
static void gpioLedSet(long);
static void gpioLedFree(void);
static int gpioKeyInit(void);
//static int gpioKeyGet(void);
static void gpioKeyFree(void);
static int gpioKeyIrqInit(keyData* pKeyData);
static void gpioKeyIrqFree(keyData* pKeyData);
irqreturn_t key_isr(int irq, void *data);

irqreturn_t key_isr(int irq, void *data) // 이 함수는 '커널'에서 호출하는 함수이다.
{
	int i;
	keyData* pKeyData = (keyData*)data; // void포인터로 받아오기 때문에  크기정보가 없다. 그래서 크기정보를 주기 위해 형변환 해준다.

	for(i = 0; i < GPIOKEYCNT; i++)
	{
		if(irq == pKeyData->key_irq[i])
		{
			pKeyData->keyNumber = i + 1; // 전역변수, 키 번호를 0~7이 아니라 1~8번 범위로 보기 위함
			break;
		}
	}
#if DEBUG
	printk(KERN_DEBUG "key_isr() irq: %d, keyNumber: %d\n", irq, pKeyData->keyNumber);
#endif
	return IRQ_HANDLED;
}

static int gpioLedInit(void)
{
	int i;
	int ret=0;
	char gpioName[10];
	for(i=0;i<GPIOLEDCNT;i++)
	{
		sprintf(gpioName,"led%d",i);
		ret = gpio_request(gpioLed[i],gpioName);
		if(ret < 0) {
			printk("Failed gpio_request() gpio%d error \n",i);
			return ret;
		}

		ret = gpio_direction_output(gpioLed[i],OFF);
		if(ret < 0) {
			printk("Failed gpio_direction_output() gpio%d error \n",i);
			return ret;
		}
	}
	return ret;
}

static void gpioLedSet(long val)
{
	int i;
	for(i=0;i<GPIOLEDCNT;i++)
	{
		gpio_set_value(gpioLed[i],(val>>i) & 0x1);
	}
}
static void gpioLedFree(void)
{
	int i;
	for(i=0;i<GPIOLEDCNT;i++)
	{
		gpio_free(gpioLed[i]);
	}
}
static int gpioKeyInit(void)
{
	int i;
	int ret=0;
	char gpioName[10];
	for(i=0;i<GPIOKEYCNT;i++)
	{
		sprintf(gpioName,"key%d",gpioKey[i]);
		ret = gpio_request(gpioKey[i], gpioName);
		if(ret < 0) {
			printk("Failed Request gpio%d error\n", 6);
			return ret;
		}
	}
	for(i=0;i<GPIOKEYCNT;i++)
	{
		ret = gpio_direction_input(gpioKey[i]);
		if(ret < 0) {
			printk("Failed direction_output gpio%d error\n", 6);
       	 	return ret;
		}
	}
	return ret;
}
/* <이제 폴링방식으로 key를 조사하지 않을 것이므로 필요없어진 함수...>
static int gpioKeyGet(void)
{
	int i;
	int ret;
	int keyData=0;
	for(i=0;i<GPIOKEYCNT;i++)
	{
//		ret=gpio_get_value(gpioKey[i]) << i;
//		keyData |= ret;
		ret=gpio_get_value(gpioKey[i]);
		keyData = keyData | ( ret << i );
	}
	return keyData;
}
*/
static void gpioKeyFree(void)
{
	int i;
	for(i=0;i<GPIOKEYCNT;i++)
	{
		gpio_free(gpioKey[i]);
	}
}

static int gpioKeyIrqInit(keyData* pKeyData)
{
	int i;
	int result;
	char * irqName[8] = {
		"IrqKey0","IrqKey1","IrqKey2","IrqKey3",
		"IrqKey4","IrqKey5","IrqKey6","IrqKey7"
	};

	for(i = 0; i < GPIOKEYCNT; i++)
	{
		// gpio핀의 기능을 외부 인터럽트 기능으로 설정하면서 그 값을 irq로 설정
		pKeyData->key_irq[i] = gpio_to_irq(gpioKey[i]); 

		if (pKeyData->key_irq[i] < 0)
		{
			printk("gpioKeyIrq() Failed gpio %d\n", gpioKey[i]);
			return pKeyData->key_irq[i]; // 오류 예외처리
		}
	}

	for (i = 0; i < GPIOKEYCNT; i++)
	{
		// 인터럽트 등록: request_irq(인터럽트 번호, 핸들러함수, 인터럽트타이밍(라이징엣지/폴링엣지)설정, 인터럽트 이름, 인터럽트서비스루틴에 전달할 변수)
		result = request_irq(pKeyData->key_irq[i], key_isr, IRQF_TRIGGER_RISING, irqName[i], pKeyData);
		if(result < 0) 
		{
			printk(KERN_ERR "request_irq() failed irq %d\n", pKeyData->key_irq[i]);
			return result;
		}
	}

	return 0; // 정상종료
}

static void gpioKeyIrqFree(keyData* pKeyData)
{
	int i;
	for(i = 0; i < GPIOKEYCNT; i++)
	{
		free_irq(pKeyData->key_irq[i], pKeyData); // 커널 지원 함수 사용
	}
}

/////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////

static int ledkey_open(struct inode *inode, struct file *filp)
{
	int num0 = MINOR(inode->i_rdev); 
	int num1 = MAJOR(inode->i_rdev);
	int result;

	// 동적 메모리 할당(36바이트의 keyData) 및 인터럽트 관련 각종 초기화 등록
	keyData* pKeyData = (keyData*)kmalloc(sizeof(keyData), GFP_KERNEL); 
	pKeyData->keyNumber = 0;
	if(!pKeyData) return -ENOMEM; 
	// GFP_KERNEL: 항상 메모리를 할당하도록 하는 매크로
	// 우리가 목적하는 것은 현재 이 함수의 지역변수로 선언된 pKeyData의 내용을 다른 시스템 콜 함수에서 사용할 수 있도록 하는 것이다. 그것을 위해서 *filp를 사용할 것이다.

	result = gpioLedInit();
	if(result < 0) return result;
	result = gpioKeyInit();
	if(result < 0) return result;
	result = gpioKeyIrqInit(pKeyData); // 동적할당 된 메모리를 넘겨준다.
	if(result < 0) return result;

	filp->private_data = pKeyData;
	// 우리는 file 구조체(`struct file *filp`)의 멤버변수인 `void  *private_data;`에다가 지역변수로 선언된 동적할당 지역변수 포인터를 넘겨주면 그렇게 함으로써 모든 디바이스 드라이버 함수에서 `*filp`를 통해서 해당 동적할당 메모리에 접근할 수 있게 되는 것이다.(file 구조체 내부는 fs.h 파일에 있다)

#if DEBUG
	printk("ledkey open-> minor : %d\n", num0);
	printk("ledkey open-> major : %d\n", num1);
#endif
	try_module_get(THIS_MODULE);

	return 0;
}

static ssize_t ledkey_read(struct file *filp, char *buf, size_t count, loff_t *f_pos)
{
	char kbuff;
	keyData* pKeyData = (keyData*)filp->private_data;

	//kbuff = gpioKeyGet(); // 이렇게 폴링으로 하지 않고 인터럽트를 적용해야하는 부분
	kbuff = pKeyData->keyNumber; // 전역변수 keyNumber 
	put_user(kbuff, buf);
	if(pKeyData->keyNumber)
		pKeyData->keyNumber = 0;

#if DEBUG
	printk("%d\n", kbuff);
	printk("ledkey read -> buf : %08X, count : %08X \n", (unsigned int)buf, count);
#endif
	return count;
}

static ssize_t ledkey_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos)
{
	char kbuff;

	get_user(kbuff, buf); 
	gpioLedSet(kbuff);

#if DEBUG
	printk("ledkey write -> buf : %08X, count : %08X \n", (unsigned int)buf, count);
#endif
	return count; // 실제로 읽은 바이트 수를 리턴해야 한다. 
}

static long ledkey_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	printk("ledkey ioctl -> cmd : %08X, arg : %08X\n", cmd, (unsigned int)arg);
	return 0x53;
}

static int ledkey_release(struct inode *inode, struct file *filp)
{
	keyData* pKeyData = (keyData*)filp->private_data;

	printk("ledkey release \n");
	module_put(THIS_MODULE);

	gpioKeyIrqFree(pKeyData); // 파일구조체 포인터가 들고 있던 동적메모리 값을 인자로 던져줌
	gpioKeyFree();
	gpioLedFree();
	if (pKeyData) // 주소가 할당되어있다면..(nullptr이 아니라면..)
		kfree(pKeyData); // 혹시 다른 곳에서 동적메모리를 해제할 수도 있기 때문에 이렇게 구성함.

	return 0;
}

struct file_operations ledkey_fops = {
	//.owner			= THIS_MODULE,
	.read			= ledkey_read,
	.write			= ledkey_write,
	.unlocked_ioctl	= ledkey_ioctl,
	.open			= ledkey_open,
	.release		= ledkey_release, // 저수준 입출력함수에서 close()가 .release에 대응한다.
};

static int ledkey_init(void)
{
	int result;

	printk("ledkey ledkey_init \n");

	result = register_chrdev(LEDKEY_DEV_MAJOR, LEDKEY_DEV_NAME, &ledkey_fops); 
	if (result < 0) return result;

	return 0;
}

static void ledkey_exit(void)
{
	printk("ledkey ledkey_exit \n");

	unregister_chrdev(LEDKEY_DEV_MAJOR, LEDKEY_DEV_NAME);
}

module_init(ledkey_init);
module_exit(ledkey_exit);
MODULE_AUTHOR("KCCI");
MODULE_DESCRIPTION("Device Driver pracite~!");
MODULE_LICENSE("Dual BSD/GPL");

app.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define DEVICE_FILENAME  "/dev/ledkey_dev"

void print_OX(unsigned char);
int main(int argc,char * argv[])
{
    char buff = 0;
	char oldBuff = 0;
    int ret;
    int dev;
	unsigned long val;

	if(argc < 2)
	{
        printf("Usage : %s ledValue[0x00~0xff]\n",argv[0]);
		return 1;
	}

	val = strtoul(argv[1],0,16);
	if(val < 0 || 0xff < val)
    {
        printf("Usage : %s ledValue[0x00~0xff]\n",argv[0]);
        return 2;
    }
	buff = val;

    dev = open( DEVICE_FILENAME, O_RDWR|O_NDELAY );
	if(dev<0)
	{
		perror("open()");
		return 2;
	}
    ret = write(dev,&buff,sizeof(buff));
	if(ret < 0)
	{
		perror("write()");
		return 3;
	}
	
	buff = 0;
	do {
		read(dev,&buff,sizeof(buff));
		if(oldBuff != buff)
		{
			if(buff != 0)
			{
				printf("key : %d\n",buff);
				print_OX(buff);
    			write(dev,&buff,sizeof(buff));
				if(buff == 8) //key:8
					break;
			}
			oldBuff = buff;
		}
	} while(1);


    close(dev);
    return 0;
}
void print_OX(unsigned char led)
{
	int i;
	led = 1 << led-1; // 커널에서 키 넘버를 보낼 때 1~8범위로 보냈기 때문에 0~7 범위로 바꿔준 뒤 비트시프트
	puts("1:2:3:4:5:6:7:8");
	for(i=0;i<=7;i++)
	{
		if(led & (0x01 << i))
			putchar('O');
		else
			putchar('X');
		if(i < 7 )
			putchar(':');
		else
			putchar('\n');
	}
	return;
}
profile
스펀지맨

0개의 댓글