리눅스 캐릭터 디바이스 작성 및 인터럽트

Posted by Lee Yunjin on June 06, 2025 · 27 mins read

리눅스 캐릭터 디바이스란?

캐릭터 디바이스는, 말 그대로 문자 기반으로 시스템과 상호작용한다는 뜻이다. 동일한 디바이스 드라이버가 관리하는 디바이스들은 같은 메이저 번호를 지니고, 각각의 디바이스들은 서로 다른 마이너 번호를 가진다.

주요 특성

  • 스트림 기반 순차 I/O
  • 유저-디바이스 간 버퍼링 없는 직접 상호작용

할당 순서

  • 메이저, 마이너 번호 할당
  • 캐릭터 디바이스 리전 할당
  • 캐릭터 디바이스 구조체 초기화
  • 캐릭터 디바이스 클래스 생성
  • 캐릭터 디바이스 생성

주요 사용처

사용자와 버퍼링 없이 순차적으로 소통한다는 특성은 시리얼 포트 등에 적합하다. 이러한 시리얼 포트를 통해 터미널, 키보드/마우스, 라인 프린터 등의 상호작용을 할 수가 있다.

간단한 인터럽트가 포함된 캐릭터 디바이스를 실제로 구현하며 알아보도톡 하자.

기본 구조

캐릭터 디바이스는 Init, Exit, 그리고 사용자 공간-> 커널 메모리, 디바이스 드라이버 Open, Close, 커널 메모리->사용자 공간이라는 두 개의 메모리 IO 흐름으로 대표된다. 필요한 경우 IO 명령을 지정하는 IOCTL(I/O ConTroL)을 지정 가능하며, 이것은 꽤 자주 사용된다. 이것과 관련해서도 추후 안내할 것이다.


기본 헤더/하드웨어 연결

코드

#include<linux/init.h>
#include<linux/delay.h>
#include<linux/workqueue.h>
#include<linux/module.h>
#include<linux/wait.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<linux/err.h>
#include<linux/interrupt.h>
#include<linux/time.h>
#include<linux/gpio.h>
#define ECHO 536 // gpio-536 (GPIO-24) in /sys/kernel/debug/info
#define ECHO_LABEL "GPIO_24"
#define TRIG 535 // GPIO 23 // gpio-535 (GPIO-23) in /sys/kernel/debug/info
#define TRIG_LABEL "GPIO_23"
static wait_queue_head_t waitqueue; //waitqueue for wait and wakeup

dev_t dev = 0; // device driver's major/minor number

int IRQ_NO; //variabe for storing echo pin irq
_Bool echo_status; //for checking ECHO pin status, needed for identifying RISING/FALLING
uint64_t sr04_send_ts, sr04_recv_ts, duration;

헤더 선언

#include<linux/init.h>

모듈의 Init과 Exit을 위해 필요하다.

#include<linux/workqueue.h>
#include<linux/wait.h>

작업 큐를 위해 필수적이다.

#include<linux/module.h>

커널 모듈의 필수 기능들이 포함되어있다.

#include<linux/cdev.h>
#include<linux/device.h>

캐릭터 디바이스의 구조체와 클래스를 사용하기 위해 필수적이다.

#include<linux/err.h>

오류 처리를 위한 정의들이다. IS_ERR와 같은 것들이 있다.

#include<linux/interrupt.h>
#include<linux/time.h>

타임스탬프를 가져오기 위한 정의들을 포함한다.

#include<linux/gpio.h>

GPIO를 사용하기 위한 헤더이다.

#define ECHO 536 // gpio-536 (GPIO-24) in /sys/kernel/debug/info
#define ECHO_LABEL "GPIO_24"
#define TRIG 535 // GPIO 23 // gpio-535 (GPIO-23) in /sys/kernel/debug/info
#define TRIG_LABEL "GPIO_23"

아래에서 설명하도록 하겠다.

static wait_queue_head_t waitqueue; //waitqueue for wait and wakeup

작업 큐이다. Init에서 초기화할 것이다.

dev_t dev = 0; // device driver's major/minor number

디바이스 드라이버의 메이저, 마이너 넘버를 정수로 저장한다. 단일 정수로 저장하며 구조체가 아니다.

int IRQ_NO; //variabe for storing echo pin irq

정수로 ECHO 핀의 인터럽트 요청 번호를 저장한다.

_Bool echo_status; //for checking ECHO pin status, needed for identifying RISING/FALLING
uint64_t sr04_send_ts, sr04_recv_ts, duration;

에코 핀의 상태, 핀 인터럽트 시작/종료 시 타임스탬프, 그리고 그 차를 저장할 변수를 선언한다.


하드웨어 연결

우선 라즈베리 파이를 준비하고, 다음과 같이 연결한다. TRIG 핀은 23번, ECHO 핀은 24번이다.

이러한 GPIO 핀 번호를 그대로 쓰는 것은, 아쉽게도 6.x 커널에서 지원하지 않는다.

cat /sys/kernel/debug/info

를 통해 핀 테이블을 확인한다. 내가 사용하는 RPi 4B 기종의 경우 ECHO 핀이 gpio-536, TRIG 핀이 gpio-535이다.

또한, 나중에 언급하겠지만, GPIO 핀을 시스템에 요청할 때는 레이블이 필요하다. 이 경우 간단하고 알아보기 쉽게 GPIO 핀 번호를 쓰도록 하겠다.

#define ECHO 536 // gpio-536 (GPIO-25) in /sys/kernel/debug/info
#define ECHO_LABEL "GPIO_24"

#define TRIG 535 // GPIO 23 // gpio-535 (GPIO-23) in /sys/kernel/debug/info 
#define TRIG_LABEL "GPIO_23"

이제 핀 할당을 위한 매크로가 선언되었다.

인터럽트 설정

핀을 인터럽트 wake-up 신호로 쓰려면 RISING인지, FALLING인지 등을 정교하게 설정해야 한다. 전압이 올라가는 순간과 떨어지는 순간 모두가 트리거로 사용될 수 있으나, 하드웨어가 이를 감지할 수 없을 가능성이 있다. 둘 모두를 커널에 말해주는 편이 좋다.

	IRQ_NO = gpio_to_irq(ECHO); // GPIO pin as interrupt pin
	if(request_irq(IRQ_NO, echo_irq_triggered, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "hc-sr04", (void *) echo_irq_triggered)) { //request irq function is for measurement... see the top of the code.
		_printk("cannot register Irq...");
		free_irq(IRQ_NO, (void *) echo_irq_triggered);
	}

거리 공식

이제, 거리 공식을 되새겨 보자. 보통은 이것을 중학교 시간에 배웠다.

거리 = 속력 * 시간

커널 타이머가 나노초임을 안다면 쉽게 할 수 있지만, 센티미터 단위여서 100을 곱해야 한다는 것 외에도 핵심이 있다. 초음파가 발사된 후 돌아오기까지의 시간은 거리를 2배로 측정하며, 소리의 속도는 343m/s이다. (그러나 소리의 속도는 기온에 따라 편차도 있으니 대략 340m/s로 사용할 수 있다.)

그렇다면 초음파의 속도만큼 나누면서도, 미터가 아닌 센티미터 단위로 측정할 것이라면 계산 공식은 duration*(340/2)/(1000000000/100)이 될 것인데, 이것을 간추리면 duration*170/10000000이다.

그렇다면 duration은 언제 산출해야 하는가?

앞에 설정한 인터럽트의 시점은 ECHO핀에 초음파가 돌아와서 전압으로 출력되는(혹은 출력이 다시 그라운드로 떨어지는) 시점이다. 이 인터럽트 함수를 초음파의 왕복 시간을 구하는 시간으로 정의하고 duration이라는 변수에 저장하면 된다.


핀 설정

ECHO는 항상 인터럽트의 트리거로만 사용된다. 그 말은, 이것은 항상 입력용 핀이라는 뜻이다. 그렇다면 이 ECHO가 연결된 GPIO는 입력 전용, 항상 측정을 시작할 때 전압을 출력하기만 한다. 그렇다면 출력 전용 핀으로 설정해야 한다.

이것은 코드로 다음과 같이 나타내진다.

gpio_direction_output(TRIG,0);
gpio_direction_input(ECHO);

메모리 할당과 해제

할당 해제는 할당의 역순이다. 하지만 특정 단계에서 실패 시 상위 단계들로 차근차근 할당 해제하는 것을 깔끔하게 하기 쉽지 않다.

이 때 다익스트라가 쓰지 말라고 엄포한 goto를 쓰게 될 것이다. 물론 권위자의 판단이니만큼 개별 케이스에 대해 일일이 함수를 선언하거나, 분기문을 달 것이라면 그것도 괜찮다고 생각한다.

하지만 이 경우는 goto의 가능한 활용법을 보이는 것도 나쁘지 않다고 판단했다. cdev(Character Device), class, Major/Minor 등의 할당을 유심히 보며 따라와 주길 바란다.

static int __init sr04_driver_init(void) {
    int major = MAJOR(dev);
    int minor = MINOR(dev);
	if(alloc_chrdev_region(&dev, minor, 1, "sr04")<0) { /* NOTE: DEV_T ALLOC */
		_printk("Cannot allocate chrdev region, \n find comment \"NOTE: DEV_T ALLOC \"\n, \
			Quitting without driver ins...\n");
		goto chrdev_error;
	}
	_printk("Major = %d, Minor = %d", MAJOR(dev),MINOR(dev));
	cdev_init(&sr04_cdev,&fops);
	if((cdev_add(&sr04_cdev,dev,1)) < 0) { /* NOTE: ADDING CDEV */
		_printk("Cannot add cdev: find comment \"NOTE: ADDING CDEV\"\n,\
			 Quitting without driver ins...\n");\
		goto cdev_error;
	}
	if(IS_ERR(sr04_class = class_create("sr04_class"))) { /*NOTE: CREATING DEV CLASS */
		_printk("Cannot create class structure,\n \
			  find comment \"NOTE: CREATING DEV CLASS\",\
			  Quitting without driver ins..\n");
		goto class_error;
	}

	if(IS_ERR(device_create(sr04_class, NULL, dev, NULL, "sr04"))) {
		_printk("Cannot create the device,\n find comment \"NOTE: DEV CREATION\", \nQuitting without driver ins...\n");
		goto device_creation_error;
	}


	//gpio availability check
	if(!gpio_is_valid(ECHO)) {
		_printk("SR04 ECHO PIN IS NOT WORKING\n");
	}
	if(!gpio_is_valid(TRIG)) {
		_printk("SR04 TRIG PIN IS NOT WORKING\n");
	}

	if(gpio_request(TRIG,TRIG_LABEL)<0) {
		_printk("ERROR ON TRIG REQUEST");
		gpio_free(TRIG);
		return -1;
	}
	if(gpio_request(ECHO,ECHO_LABEL)<0) {
		_printk("ERROR ON ECHO REQUEST");
		gpio_free(TRIG); //if the program has executed until now, trig is available, and requested succesfully
		gpio_free(ECHO);
		return -1;
	}

	IRQ_NO = gpio_to_irq(ECHO); // GPIO pin as interrupt pin
	if(request_irq(IRQ_NO, echo_irq_triggered, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "hc-sr04", (void *) echo_irq_triggered)) { //request irq function is for measurement... see the top of the code.
		_printk("cannot register Irq...");
		free_irq(IRQ_NO, (void *) echo_irq_triggered);
	}

	gpio_direction_output(TRIG,0);
	gpio_direction_input(ECHO);
	init_waitqueue_head(&waitqueue); // waitqueue init
	_printk("SR04 Dev. Driver inserted.");
	return 0;


chrdev_error:
	unregister_chrdev_region(dev,1);
	_printk("SR04 Dev. Driver failed");
	return -1;
cdev_error:
	cdev_del(&sr04_cdev);
	goto chrdev_error;
class_error:
	class_destroy(sr04_class);
	goto cdev_error;

device_creation_error:
	device_destroy(sr04_class,dev);
	goto class_error;
}
  • device_creation_error는 동작 후 class_error를 호출
  • class_error는 동작 후 cdev_error를 호출
  • cdev_error는 동작 후 chrdev_error를 호출
  • chrdev_error는 goto 없이 종료

이러한 흐름으로 불필요한 코드 중복을 막을 수 있다.

그리고 정상적으로 모듈의 적재를 해제할 시의 메모리 할당 해제 코드는 역순으로 단순하게 진행하면 된다.

static void __exit sr04_driver_exit(void) {
	free_irq(IRQ_NO, (void *) echo_irq_triggered);
	gpio_free(ECHO);
	gpio_free(TRIG);
	device_destroy(sr04_class,dev);
	class_destroy(sr04_class);
	cdev_del(&sr04_cdev);
	unregister_chrdev_region(dev,1);
	_printk( "SR04 Dev. Driver removed.\n" );
}

인터럽트 후 읽기

거리는 인터럽트가 트리거된 후, ECHO 핀이 꺼진 후 왕복 시간을 읽어서 계산해야 오차가 없다. 즉, 읽기 작업은 항상 인터럽트가 끝난 후에 동작해야 한다는 절차를 따른다. 여기서 C의 절차 지향적 철학이 빛을 발한다.

인터럽트 코드

/* start of IRQ Handler */
static irqreturn_t echo_irq_triggered(int irq, void *dev_id) {
	echo_status = (_Bool)gpio_get_value(ECHO);
	if(echo_status == 1) {
		sr04_send_ts = ktime_get_ns();
		_printk("ECHO INTERRUPT\n");
	} else {
		_printk("SUCCEED TO GET sr04_recv_ts%llu\n", sr04_recv_ts);
		sr04_recv_ts = ktime_get_ns();
		duration = sr04_recv_ts-sr04_send_ts;
		wake_up_interruptible(&waitqueue);
	}

	return IRQ_HANDLED;
}

인터럽트는 ECHO핀의 현재 값을 echo_status라는 불리언으로, 또한 나노초 단위의 타임스탬프 차를 구해서 duration에 저장한다. 또한, Wake Up 시점 역시 ECHO핀이 1일 때로 직관적이다.

이제 이것을 아래에서 어떻게 읽는지를 살펴보자.

ssize_t sr04_read(struct file *file, char __user *buf, size_t len, loff_t * off) {
	gpio_set_value(TRIG,1);
	wait_event_interruptible(waitqueue,echo_status == 0); //wait for interrupt pin
	gpio_set_value(TRIG,0);
	if(duration<=0) { //if duration is invalid
		_printk("SR04 Distance measurement: failed to get ECHO.. : duration is %llu\n", duration);
		return 0;
	} else {
		char dist[16];
		memset(dist,0,sizeof(dist));
		sprintf(dist, "%llu", duration*170/10000000);
		_printk("duration : %llu\n", duration);
		int copied_bytes=copy_to_user(buf,dist,16);  //returning value as character
		if(copied_bytes>0) {
			_printk("Distance hasn't copied to user...remained bytes: %d", copied_bytes);
		}
		return sizeof(dist);
	}
	return 0;
}

wait_event_interruptible에 ECHO핀의 상태가 0(그라운드 값)일때까지 작업큐로부터 인터럽트 종료를 기다린다.


전체 코드

이제 이러한 흐름을 이해했다면 전체 코드를 통해 어떻게 조합되는지 알아 봐도 좋을 것이다. 아래 코드를 참고 바란다.

#include<linux/init.h>
#include<linux/workqueue.h>
#include<linux/module.h>
#include<linux/wait.h>
#include<linux/cdev.h>
#include<linux/device.h>
#include<linux/err.h>
#include<linux/interrupt.h>
#include<linux/time.h>
#include<linux/gpio.h>
#define ECHO 536 // gpio-536 (GPIO-24) in /sys/kernel/debug/info
#define ECHO_LABEL "GPIO_24"
#define TRIG 535 // GPIO 23 // gpio-535 (GPIO-23) in /sys/kernel/debug/info
#define TRIG_LABEL "GPIO_23"
static wait_queue_head_t waitqueue; //waitqueue for wait and wakeup

dev_t dev = 0; // device driver's major/minor number

int IRQ_NO; //variabe for storing echo pin irq
_Bool echo_status; //for checking ECHO pin status, needed for identifying RISING/FALLING
uint64_t sr04_send_ts, sr04_recv_ts, duration;
/* start of IRQ Handler */

static irqreturn_t echo_irq_triggered(int irq, void *dev_id) {
	echo_status = (_Bool)gpio_get_value(ECHO);
	if(echo_status == 1) {
		sr04_send_ts = ktime_get_ns();
		_printk("ECHO INTERRUPT\n");
	} else {
		_printk("SUCCEED TO GET sr04_recv_ts%llu\n", sr04_recv_ts);
		sr04_recv_ts = ktime_get_ns();
		duration = sr04_recv_ts-sr04_send_ts;
		wake_up_interruptible(&waitqueue); // interrupt wake up
	}

	return IRQ_HANDLED;
}


/* -- start of function prototype */
struct class *sr04_class;
struct cdev sr04_cdev;

static int __init sr04_driver_init(void);
int sr04_driver_open(struct inode *inode, struct file *file) ;
int sr04_driver_release(struct inode *inode, struct file *file) ;
static void __exit sr04_driver_exit(void);

ssize_t sr04_read(struct file *file, char __user *buf, size_t len, loff_t * off);

/* -- end of function prototype -- */

struct file_operations fops = {
	.owner	= THIS_MODULE,
	.read	= sr04_read,
	.open	= sr04_driver_open,
	.release = sr04_driver_release,
};

int sr04_driver_open(struct inode *inode, struct file *file) {
	return 0;
}
int sr04_driver_release(struct inode *inode, struct file *file) {
	return 0;
}

static int __init sr04_driver_init(void) {
    int major = MAJOR(dev);
    int minor = MINOR(dev);
	if(alloc_chrdev_region(&dev, minor, 1, "sr04")<0) { /* NOTE: DEV_T ALLOC */
		_printk("Cannot allocate chrdev region, \n find comment \"NOTE: DEV_T ALLOC \"\n, \
			Quitting without driver ins...\n");
		goto chrdev_error;
	}
	_printk("Major = %d, Minor = %d", MAJOR(dev),MINOR(dev));
	cdev_init(&sr04_cdev,&fops);
	if((cdev_add(&sr04_cdev,dev,1)) < 0) { /* NOTE: ADDING CDEV */
		_printk("Cannot add cdev: find comment \"NOTE: ADDING CDEV\"\n,\
			 Quitting without driver ins...\n");\
		goto cdev_error;
	}
	if(IS_ERR(sr04_class = class_create("sr04_class"))) { /*NOTE: CREATING DEV CLASS */
		_printk("Cannot create class structure,\n \
			  find comment \"NOTE: CREATING DEV CLASS\",\
			  Quitting without driver ins..\n");
		goto class_error;
	}

	if(IS_ERR(device_create(sr04_class, NULL, dev, NULL, "sr04"))) {
		_printk("Cannot create the device,\n find comment \"NOTE: DEV CREATION\", \nQuitting without driver ins...\n");
		goto device_creation_error;
	}


	//gpio availability check
	if(!gpio_is_valid(ECHO)) {
		_printk("SR04 ECHO PIN IS NOT WORKING\n");
	}
	if(!gpio_is_valid(TRIG)) {
		_printk("SR04 TRIG PIN IS NOT WORKING\n");
	}

	if(gpio_request(TRIG,TRIG_LABEL)<0) {
		_printk("ERROR ON TRIG REQUEST");
		gpio_free(TRIG);
		return -1;
	}
	if(gpio_request(ECHO,ECHO_LABEL)<0) {
		_printk("ERROR ON ECHO REQUEST");
		gpio_free(TRIG); //if the program has executed until now, trig is available, and requested succesfully
		gpio_free(ECHO);
		return -1;
	}

	IRQ_NO = gpio_to_irq(ECHO); // GPIO pin as interrupt pin
	if(request_irq(IRQ_NO, echo_irq_triggered, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "hc-sr04", (void *) echo_irq_triggered)) { //request irq function is for measurement... see the top of the code.
		_printk("cannot register Irq...");
		free_irq(IRQ_NO, (void *) echo_irq_triggered);
	}

	gpio_direction_output(TRIG,0);
	gpio_direction_input(ECHO);
	init_waitqueue_head(&waitqueue); // waitqueue init
	_printk("SR04 Dev. Driver inserted.");
	return 0;


chrdev_error:
	unregister_chrdev_region(dev,1);
	_printk("SR04 Dev. Driver failed");
	return -1;
cdev_error:
	cdev_del(&sr04_cdev);
	goto chrdev_error;
class_error:
	class_destroy(sr04_class);
	goto cdev_error;

device_creation_error:
	device_destroy(sr04_class,dev);
	goto class_error;
}

static void __exit sr04_driver_exit(void) {
	free_irq(IRQ_NO, (void *) echo_irq_triggered);
	gpio_free(ECHO);
	gpio_free(TRIG);
	device_destroy(sr04_class,dev);
	class_destroy(sr04_class);
	cdev_del(&sr04_cdev);
	unregister_chrdev_region(dev,1);
	_printk( "SR04 Dev. Driver removed.\n" );
}


ssize_t sr04_read(struct file *file, char __user *buf, size_t len, loff_t * off) {
	gpio_set_value(TRIG,1);
	wait_event_interruptible(waitqueue,echo_status == 0); //wait for interrupt pin
	gpio_set_value(TRIG,0);
	if(duration<=0) { //if duration is invalid
		_printk("SR04 Distance measurement: failed to get ECHO.. : duration is %llu\n", duration);
		return 0;
	} else {
		char dist[16];
		memset(dist,0,sizeof(dist));
		sprintf(dist, "%llu", duration*170/10000000);
		_printk("duration : %llu\n", duration);
		int copied_bytes=copy_to_user(buf,dist,16);  //returning value as character
		if(copied_bytes>0) {
			_printk("Distance hasn't copied to user...remained bytes: %d", copied_bytes);
		}
		return sizeof(dist);
	}
	return 0;
}


module_init(sr04_driver_init);
module_exit(sr04_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Yunjin Lee <gzblues61@gmail.com>");
MODULE_DESCRIPTION("HC-SR04");
MODULE_VERSION("0.01");

마무리하며

이번 시간에는 캐릭터 디바이스와 인터럽트 기초까지 속성으로 나갈 수 있었다. 다음 시간에는 I2C에 대한 추가 강의로 다시 이어서 진도를 나가도록 하겠다.