在5.4.79内核中,从设备树中获取 获取中断号到方法: 1, 宏定义 IRQ_EINT(号码) 2,设备树文件中 arch/arm/boot/dts/exynos4412-origen.dts 硬件连接: key ---- gpx1_2--- EINT10 设备树文件:arch/arm/boot/dts/exynos4412-pinctrl.dtsi gpx1: gpx1 { gpio-controller; #gpio-cells = <2>; interrupt-controller; interrupt-parent = <&gic>; interrupts = <GIC_SPI 24 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 25 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 28 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 29 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 30 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH>; #interrupt-cells = <2>; }; 在编程过程中,需要定义自己的节点--描述当前设备用的中断号 arch/arm/boot/dts/exynos4412-origen.dts +51 key_int_node{ compatible = "test_key"; interrupt-parent = <&gpx1>; interrupts = <2 4>; }; 编译设备树文件: make dtbs -j6 ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- 更新dtbs文件: 复制 exynos4412-origen.dtb 到NAS
上图中gpx1设备树代码是3.14内核中的,在5.4版本中 代码略有不同,具体如下:
linux@ubuntu:~/Level10/day6/linux-5.4.79/arch/arm/boot/dts$ vim exynos4412-pinctrl.dtsi
设备树中可以查到该设备节点
[root@farsight ]# cd proc/device-tree/key_int_node/ [root@farsight key_int_node]# ls compatible interrupt-parent interrupts name
a,获取到中断号码: int get_irqno_from_node(void) { // 获取到设备树中的节点 struct device_node *np = of_find_node_by_path("/key_int_node"); if(np){ printk("find node ok\n"); }else{ printk("find node failed\n"); } // 通过节点去获取到中断号码 int irqno = irq_of_parse_and_map(np, 0); printk("irqno = %d\n", irqno); return irqno; } b,申请中断 int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char * name, void * dev) 参数1: 设备对应的中断号 参数2: 中断的处理函数 typedef irqreturn_t (*irq_handler_t)(int, void *); 参数3:触发方式 #define IRQF_TRIGGER_NONE 0x00000000 //内部控制器触发中断的时候的标志 #define IRQF_TRIGGER_RISING 0x00000001 //上升沿 #define IRQF_TRIGGER_FALLING 0x00000002 //下降沿 #define IRQF_TRIGGER_HIGH 0x00000004 // 高点平 #define IRQF_TRIGGER_LOW 0x00000008 //低电平触发 参数4:中断的描述,自定义,主要是给用户查看的 /proc/interrupts 参数5:传递给参数2中函数指针的值 返回值: 正确为0,错误非0 参数2的赋值:参数1:irqno 和参数2:devid 都是由上面的request_irq函数传递的分别是该函数的参数1和参数5. irqreturn_t key_irq_handler(int irqno, void *devid) { return IRQ_HANDLED; } 释放中断: void free_irq(unsigned int irq, void *dev_id) 参数1: 设备对应的中断号 参数2:与request_irq中第5个参数保持一致
效果展示
[root@farsight ]# insmod *.ko [ 17.570120] key_drv: loading out-of-tree module taints kernel. [ 17.575433] find node ok [ 17.577120] irqno = 107
[root@farsight ]# cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 36: 0 0 0 0 GIC-0 89 Level mct_comp_irq 37: 20031 3406 1122 6958 GIC-0 28 Level MCT 38: 0 0 0 0 COMBINER 18 Edge arm-pmu 39: 0 0 0 0 COMBINER 26 Edge arm-pmu ... 106: 19237 0 0 0 exynos4210_wkup_irq_chip 6 Level eth0 107: 0 0 0 0 exynos4210_wkup_irq_chip 2 Edge IRQ_EINT10 IPI0: 0 1 1 1 CPU wakeup interrupts ...
按下按键K3 [root@farsight ]# [ 2979.408843] --------------key_irq_handler------------------ [ 2983.202843] --------------key_irq_handler------------------ [root@farsight ]#
// 1,设定一个全局的设备对象 key_dev = kzalloc(sizeof(struct key_desc), GFP_KERNEL); // 2,申请主设备号 key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops); // 3,创建设备节点文件 key_dev->cls = class_create(THIS_MODULE, "key_cls"); key_dev->dev = device_create(key_dev->cls, NULL, MKDEV(key_dev->dev_major,0), NULL, "key0");
a,硬件如何获取数据 key: 按下和抬起: 1/0 读取key对应的gpio的状态,可以判断按下还是抬起 读取key对应gpio的寄存器--数据寄存器 //读取数据寄存器 int value = readl(key_dev->reg_base + 4) & (1<<2); b,驱动如何传递给用户 在中断处理中填充数据: key_dev->event.code = KEY_ENTER; key_dev->event.value = 0; 在xxx_read中将数据传递给用户 ret = copy_to_user(buf, &key_dev->event, count); c,用户如何拿到--编写应用程序 while(1) { read(fd, &event, sizeof(struct key_event)); if(event.code == KEY_ENTER) { if(event.value) { printf("APP__ key enter pressed\n"); }else{ printf("APP__ key enter up\n"); } } }
文件io模型: 1,非阻塞 2,阻塞 3,多路复用--select/poll 4, 异步信号通知faync 阻塞: 当进程在读取外部设备的资源(数据),资源没有准备好,进程就会休眠 linux应用中,大部分的函数接口都是阻塞 scanf(); read(); write(); accept(); 驱动中需要调用 1,将当前进程加入到等待队列头中 add_wait_queue(wait_queue_head_t * q, wait_queue_t * wait) 2,将当前进程状态设置成TASK_INTERRUPTIBLE set_current_state(TASK_INTERRUPTIBLE) 3,让出调度--休眠 schedule(void) 更加智能的接口,等同于上面的三个接口: wait_event_interruptible(wq, condition) 驱动如何去写代码 1,等待队列头 wait_queue_head_t init_waitqueue_head(wait_queue_head_t *q); 2,在需要等待(没有数据)的时候,进行休眠 wait_event_interruptible(wait_queue_head_t wq, condition) // 内部会构建一个等待队列项/节点wait_queue_t 参数1: 等待队列头 参数2: 条件,如果是为假,就会等待,如果为真,就不会等待 可以用一标志位,来表示是否有数据 3,在一个合适的时候(有数据),会将进程唤醒 wake_up_interruptible(wait_queue_head_t *q) 用法: wake_up_interruptible(&key_dev->wq_head); //同时设置标志位 key_dev->key_state = 1;
用的会比较少,因为比较耗资源 open("/dev/key0", O_RDWR|O_NONBLOCK); ------------------------------------ 驱动中需要去区分,当前模式是阻塞还是非阻塞 //如果当前是非阻塞模式,并且没有数据,立马返回一个出错码 if(filp->f_flags & O_NONBLOCK && !key_dev->key_state) return -EAGAIN;
poll的应用: 0, 存在的意义:单进程单线程下,解决同时监听多个设备的难题。(当然解决此问题也可以使用多线程或多进程来处理) 1, 需要打开多个文件(多个设备) 2, 利用poll来实现监控fd的读,写,出错 #include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); 参数1: 表示多个文件描述符集合 struct pollfd描述的是文件描述符到信息 struct pollfd { int fd; //文件描述符 short events; //希望监控fd的什么事件:读,写,出错 POLLIN 读, POLLOUT 写, POLLERR出错 short revents; //结果描述,表示当前的fd是否有读,写,出错 //用于判断,是内核自动赋值 POLLIN 读, POLLOUT 写, POLLERR出错 }; 参数2:被监控到fd的个数 参数3: 监控的时间: 正: 表示监控多少ms 负数: 无限的时间去监控 0: 等待0ms,类似于非阻赛 返回值: 负数:出错 大于0,表示fd中有数据 等于0: 时间到
unsigned int key_drv_poll(struct file *filp, struct poll_table_struct *pts) { // 返回一个mask值 unsigned int mask; // 调用poll_wait,将当前到等待队列注册系统中 poll_wait(filp, &key_dev->wq_head, pts); // 1,当没有数据到时候返回一个0 if(!key_dev->key_state) mask = 0; // 2,有数据返回一个POLLIN if(key_dev->key_state) mask |= POLLIN; return mask; } const struct file_operations key_fops = { .poll = key_drv_poll, };
a,应用--处理信号,主要是读写数据 void catch_signale(int signo) { if(signo == SIGIO) { printf("we got sigal SIGIO"); // 读取数据 read(fd, &event, sizeof(struct key_event)); if(event.code == KEY_ENTER) { if(event.value) { printf("APP__ key enter pressed\n"); }else { printf("APP__ key enter up\n"); } } } } // 1,设置信号处理方法 signal(SIGIO,catch_signale); // 2,将当前进程设置成SIGIO的属主进程 fcntl(fd, F_SETOWN, getpid()); // 3,将io模式设置成异步模式 int flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | FASYNC ); fcntl函数 根据文件描述词来操作文件的特性 文件控制函数 fcntl -- file control 头文件:
#include <unistd.h> #include <fcntl.h>
函数原型:
int fcntl(int fd, int cmd); int fcntl(int fd, int cmd, long arg); int fcntl(int fd, int cmd, struct flock *lock);`
描述:
fcntl()针对(文件)描述符提供控制.参数fd是被参数cmd操作(如下面的描述)的描述符.
针对cmd的值,fcntl能够接受第三个参数(arg)
fcntl函数有5种功能:
1.复制一个现有的描述符(cmd=F_DUPFD). 2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD). 3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL). ------本次实验使用的参数 4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).------本次实验使用的参数 5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
cmd 选项:
F_GETFL 取得fd的文件状态标志,如同下面的描述一样(arg被忽略) F_SETFL 设置给arg描述符状态标志,可以更改的几个标志是:O_APPEND, O_NONBLOCK,O_SYNC和O_ASYNC。 F_GETOWN 取得当前正在接收SIGIO或者SIGURG信号的进程id或进程组id,进程组id返回成负值(arg被忽略)
命令字(cmd)F_GETFL和F_SETFL的标志如下面的描述:
O_NONBLOCK 非阻塞I/O;如果read(2)调用没有可读取的数据,或者如果write(2)操作将阻塞,read或write调用返回-1和EAGAIN错误 O_APPEND 强制每次写(write)操作都添加在文件大的末尾,相当于open(2)的O_APPEND标志 O_DIRECT 最小化或去掉reading和writing的缓存影响.系统将企图避免缓存你的读或写的数据. 如果不能够避免缓存,那么它将最小化已经被缓存了的数 据造成的影响.如果这个标志用的不够好,将大大的降低性能 FASYNC 当I/O可用的时候,允许SIGIO信号发送到进程组,例如:当有数据可以读的时候
注意: 在修改文件描述符标志或文件状态标志时必须谨慎,先要取得现在的标志值,然后按照希望修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位。
fcntl的返回值: 与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列三个命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。第一个返回新的文件描述符,第二个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。
b,驱动--发送信号 1,需要和进程进行关联--记录信号该发送给谁 实现一个fasync的接口 int key_drv_fasync(int fd, struct file *filp, int on) { //只需要调用一个函数记录信号该发送给谁 return fasync_helper(fd, filp, on, &key_dev->faysnc); } 2,在某个特定的时候去发送信号,在有数据的时候 //发送信号 kill_fasync(&key_dev->faysnc, SIGIO, POLLIN);
struct fasync_struct { spinlock_t fa_lock; int magic; int fa_fd; struct fasync_struct *fa_next; /* singly linked list */ struct file *fa_file; struct rcu_head fa_rcu; }; /** *fasync_helper - 将一个fasync_struct对象注册进内核 *@fd:文件描述符,由fasync传入 *@filp:file指针,由fasync传入 *@sig:信号类型,通常使用的就是SIGIO *@dev_fasync:事前准备的fasync_struct对象指针的指针 */ int fasync_helper(int fd, struct file * filp, int sig, struct fasync_struct ** dev_fasync); /** *kill_fasync - 释放一个信号 *@dev_fasync:事前使用fasync_helper注册进内核的fasync_struct对象指针的指针 *@filp:file指针,由fasync传入 *@sig:信号类型,通常使用的就是SIGIO *@flag:标志,通常,如果资源可读用POLLIN,如果资源可写用POLLOUT */ void kill_fasync(struct fasync_struct **dev_fasync, int sig, int flag);
示例代码
应用代码
//---------------key_test.c---------------------- #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <poll.h> #include <signal.h> #define KEY_ENTER 28 #define EAGAIN 35 int fd; struct key_value_dsc{ int code; //表示按键的类型:home, esc, Q, W, E, R, T... int date; //表示按下还是抬起 1/0 }; struct key_value_dsc key_value; void catch_signal(int signo) { int ret; if(signo == SIGIO) { printf("__APP__:catch_signal\n"); //可以直接去读数据, 一定可以读到数据 ret = read(fd, &key_value, sizeof(struct key_value_dsc)); if(ret < 0){ perror("read:"); exit(1); } if(key_value.code == KEY_ENTER) { if(key_value.date == 1) { printf("__APP__key enter pressed\n",key_value.date); } else if(key_value.date == 0) { printf("__APP__key enter up\n",key_value.date); } } } } int main(int argc, char **argv) { int ret, flag; char in_buf[128] = {0}; fd = open("/dev/INTRRUPT KEY3",O_RDWR); if(fd < 0){ perror("open:"); exit(1); } //1.设置信号处理方法 signal(SIGIO, catch_signal); //2.将当前进程设置成SIGIO的属主进程 fcntl(fd,F_SETOWN,getpid()); //3. 将IO模式设置成异步模式 flag = fcntl(fd,F_GETFL); fcntl(fd,F_SETFL,flag | FASYNC); while(1) { printf("__APP__: wating\n"); sleep(1); } close(fd); return 0; }
驱动代码
//--------key_drv.c-------------- #include <linux/init.h> #include <linux/module.h> #include <linux/of.h> #include <linux/of_irq.h> #include <linux/interrupt.h> #include <linux/slab.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/uaccess.h> #include <linux/input-event-codes.h> #include <linux/wait.h> #include <linux/poll.h> #include <asm/io.h> #define GPX1CON_ADDR 0x11000c20 #define GPX1_SIZE 8 //设计一个全局设备对象--描述按键信息 struct key_irq_str{ int irqno; unsigned int dev_major; unsigned int dev_minor; struct class* pkey_class; struct device* pkey_device; void* reg_virt_base; wait_queue_head_t wq; int key_state; //表示是否有数据 struct fasync_struct* fasync; }; struct key_irq_str *pkey_irq_str; //设计一个描述按键的数据的对象 struct key_value_dsc{ int code; //表示按键的类型:home, esc, Q, W, E, R, T... int date; //表示按下还是抬起 1/0 }; struct key_value_dsc key3_value_dsc = { .code = 0, .date = 0, }; int key_drv_fasync(int fd, struct file * filp, int on) { //只需要调用一个函数,记录信号该发送给谁 return fasync_helper(fd, filp, on, &pkey_irq_str->fasync); } irqreturn_t key_irq_handler(int irqno, void *devid) { unsigned int GPX1DAT; printk("--------------%s------------------\n",__FUNCTION__); GPX1DAT = readl(pkey_irq_str->reg_virt_base + 4); if( GPX1DAT &= 0x1<<2 ){ printk("__KERN__:Enter up\n"); key3_value_dsc.code = KEY_ENTER; key3_value_dsc.date = 0; } else{ printk("__KERN__:Enter down\n"); key3_value_dsc.code = KEY_ENTER; key3_value_dsc.date = 1; } //表示有数据,需要唤醒整个等待队列 wake_up_interruptible(&(pkey_irq_str->wq)); //同时设置标志位 pkey_irq_str->key_state = 1; //发送信号 kill_fasync(&(pkey_irq_str->fasync), SIGIO, POLLIN); return IRQ_HANDLED; } int key_drv_open (struct inode* inode, struct file *filp){ printk("__KERN__:open\n"); return 0; } ssize_t key_drv_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos){ int ret; //兼容阻塞和非阻塞模式。如果当前是非阻塞模式,并且没有数据,立马返回一个出错码 if(filp->f_flags & O_NONBLOCK && !pkey_irq_str->key_state) { return EAGAIN; } wait_event_interruptible(pkey_irq_str->wq, pkey_irq_str->key_state); //表示有数据 ret = copy_to_user(buf, &key3_value_dsc, count); if(ret == 0) { //printk("__KERN__:read\n"); } else { printk("__KERN__:Err to read\n"); } key3_value_dsc.code = 0; key3_value_dsc.date = 0; pkey_irq_str->key_state = 0; return count; } ssize_t key_drv_write (struct file *filp, const char __user *buf, size_t count, loff_t *fpos){ return 0; } int key_drv_release (struct inode *inod, struct file *filp){ printk("__KERN__:release\n"); return 0; } const struct file_operations key_drv_fops = { .open = key_drv_open, .read = key_drv_read, .write = key_drv_write, .release = key_drv_release, .fasync = key_drv_fasync, }; int get_irqno_from_node(void) { //获取到设备树中的节点 struct device_node *np = of_find_node_by_path("/key_int_node"); if(np){ printk("find node ok\n"); } else{ printk("find node failed\n"); } //通过节点去获取到中断号码 pkey_irq_str->irqno = irq_of_parse_and_map(np,0); printk("irqno = %d\n",pkey_irq_str->irqno); return pkey_irq_str->irqno; } static int __init key_drv_init(void) { int ret; //1.设定一个全局的设备对象 pkey_irq_str = kzalloc(sizeof(struct key_irq_str), 1); //2.申请主设备号,设置次设备号 pkey_irq_str->dev_major = register_chrdev(0, "key_drv_test", &key_drv_fops); pkey_irq_str->dev_minor = 0; //3.创建设备节点 pkey_irq_str->pkey_class = class_create(THIS_MODULE, "key_interrupt_dev"); pkey_irq_str->pkey_device = device_create(pkey_irq_str->pkey_class,NULL,MKDEV(pkey_irq_str->dev_major, pkey_irq_str->dev_minor),NULL,"INTRRUPT KEY%d",3); //4.硬件初始化--地址映射或中断申请 ret = get_irqno_from_node(); ret = request_irq(pkey_irq_str->irqno, key_irq_handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, "IRQ_EINT10", NULL); if(ret != 0) { printk("request_irq ERROR!\n"); return ret; } pkey_irq_str->reg_virt_base = ioremap(GPX1CON_ADDR, GPX1_SIZE); //5. 初始化等待队列头 init_waitqueue_head(&(pkey_irq_str->wq)); pkey_irq_str->key_state = 0; return 0; } static void __exit key_drv_exit(void) { iounmap(pkey_irq_str->reg_virt_base); free_irq(pkey_irq_str->irqno,NULL); device_destroy(pkey_irq_str->pkey_class, MKDEV(pkey_irq_str->dev_major, pkey_irq_str->dev_minor)); class_destroy(pkey_irq_str->pkey_class); unregister_chrdev(pkey_irq_str->dev_major, "key_drv_test"); kfree(pkey_irq_str); } module_init(key_drv_init); module_exit(key_drv_exit); MODULE_LICENSE("GPL");
Makefie代码
ROOTFS_DIR = /home/linux/Level11 APP_NAME = key_test CROSS_COMPILE = arm-none-linux-gnueabi- CC = $(CROSS_COMPILE)gcc ifeq ($(KERNELRELEASE), ) KERNEL_DIR = /home/linux/Level10/day6/linux-5.4.79 CUR_DIR = $(shell pwd) all : make -C $(KERNEL_DIR) M=$(CUR_DIR) modules -j6 ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- $(CC) $(APP_NAME).c -o $(APP_NAME) clean : make -C $(KERNEL_DIR) M=$(CUR_DIR) clean rm $(APP_NAME) ./1/$(APP_NAME) clear install: cp -raf *.ko $(APP_NAME) ./1/ else obj-m += key_drv.o endif
0,存在的意义:有的时候中断处理程序耗时较长,会影响用户体验甚至影响到其他进程的正常执行,此时可以将中断一分为二,关键部分优先执行, 剩下的部分切分出去,不再占用中断。关键部分执行完毕后,调用剩余部分,让其在内核线程中继续执行,而不是继续占用原线程。 延申阅读:https://developer.aliyun.com/article/379819 1,softirq: 处理比较快,但是内核级别的机制,需要修改整个内核源码,不推荐也不常用 2,tasklet: 内部实现实际调用了softirq 3, workqueue: 工作队列 1,tasklet: struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); // 下半部的实现逻辑 unsigned long data; // 传递给func }; a, 初始化,不必直接对其初始化,使用下面的tasklet_init函数即可。 struct tasklet_struct mytasklet; tasklet_init(struct tasklet_struct * t, void(* func)(unsigned long), unsigned long data) 参数1:结构体地址, 参数2:下半部实现函数 参数3:传递给 下半部实现函数 的参数 例子: void key_tasklet_half_irq(unsigned long data) { // 表示有数据,需要去唤醒整个进程/等待队列 wake_up_interruptible(&key_dev->wq_head); //同时设置标志位 key_dev->key_state = 1; //发送信号 kill_fasync(&key_dev->faysnc, SIGIO, POLLIN); } tasklet_init(&key_dev->mytasklet, key_tasklet_half_irq, 45); b,在上半部中放入到内核线程中--启动 // 启动下半步 tasklet_schedule(&key_dev->mytasklet); c,模块卸载的时候: tasklet_kill(&key_dev->mytasklet);