리눅스 캐릭터 디바이스란?
캐릭터 디바이스는, 말 그대로 문자 기반으로 시스템과 상호작용한다는 뜻이다. 동일한 디바이스 드라이버가 관리하는 디바이스들은 같은 메이저 번호를 지니고, 각각의 디바이스들은 서로 다른 마이너 번호를 가진다.
주요 특성
- 스트림 기반 순차 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에 대한 추가 강의로 다시 이어서 진도를 나가도록 하겠다.