캐릭터 디바이스는, 말 그대로 문자 기반으로 시스템과 상호작용한다는 뜻이다. 동일한 디바이스 드라이버가 관리하는 디바이스들은 같은 메이저 번호를 지니고, 각각의 디바이스들은 서로 다른 마이너 번호를 가진다.
사용자와 버퍼링 없이 순차적으로 소통한다는 특성은 시리얼 포트 등에 적합하다. 이러한 시리얼 포트를 통해 터미널, 키보드/마우스, 라인 프린터 등의 상호작용을 할 수가 있다.
간단한 인터럽트가 포함된 캐릭터 디바이스를 실제로 구현하며 알아보도톡 하자.
캐릭터 디바이스는 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에 대한 추가 강의로 다시 이어서 진도를 나가도록 하겠다.