别人的经验,我们的阶梯!大家好,我是刀哥,今天给大家讲解的技术知识点是:【Linux注册与处理中断】。前面两篇文章介绍了在应用层如何调用驱动函数控制GPIO,以及在驱动中如何向应用层发送信号。如果有这样的需求:应用程序需要监控某个硬件GPIO端口的电平状态,当有变化时,应用程序会采取相应的动作。这个需求可以用之前介绍过的知识来完成。例如:在驱动程序中不断读取GPIO端口的状态,一旦发生变化,就通过信号将新的电平状态发送给应用层。这种方法称为:轮询。轮询方式的缺点很明显:轮询间隔应该是多少毫秒(或微秒),才比较合适?轮询太慢:信号可能丢失;轮询太快:CPU资源被消耗!因此,在实际产品中,还是采用中断触发的方式比较实用!本文所有的描述和测试都是在x86平台上完成的;Linux中断的知识点整理了中断的分类。处理中断的方式在不断变化。下面几张图是我之前学习的时候画的思维导图。这些图片清楚地描述了Linux操作系统中有关中断的一些基本概念。这张图结构比较清晰,基本概括了Linux系统中的中断分类。另外,在很多关于中断的书籍中,大多都是从基础的PIC(ProgrammableInterruptController)开始的。如果你想对中断相关的内容有一个非常具体、专业、深入的理解,有一篇文章《Interrupt in Linux.pdf》讲的非常好(我没看懂文章后半部分)。文末有下载链接,感兴趣的朋友可以学习一下。中断号和中断向量在这张图中,只要记住中断号和中断向量的关系即可:中断号与中断控制器(PIC/APIC)有关;中断向量与CPU有关,用于查找中断处理函数的入口地址;中断服务程序ISR中断服务程序是如何处理每个中断的。如果你了解Linux中断的相关内容,你一定会看到这样的描述:中断处理分为前半部分和后半部分。上半年不能消耗太多时间,主要处理与硬件相关的重要工作;其他不重要的工作在下半年完成。从上图可以看出,有几种机制可以选择完成后半部分的工作,每种方式针对的是不同的需求场景。在每一个下半部机制中,Linux都设计了非常方便的接口函数。对于我们开发人员来说,使用这些下半部分的机制很简单,只需要调用几个函数。例如:如果使用工作队列来实现后半部分的工作,只需要2步:1.定义处理函数staticstructwork_structmywork;staticvoidmywork_handler(structwork_struct*work){printk("Thisismyword_handler...\n");}2.在中断处理函数中,注册注册函数NIT_WORK(&mywork,mywork_handler);schedule_work(&mywork);下图是各“下半部”处理机制的一些特点。注:新版本更新了部分机制。丢弃它,了解它。中断处理的注册和注销API所谓的中断注册就是告诉操作系统:我对哪个中断感兴趣,请在这些中断发生时通知我。通知的方式是调用预先注册的回调函数。驱动程序可以通过函数request_irq()向操作系统注册,并激活指定的中断线:intrequest_irq(unsignedintirq,irq_handler_thandler,unsignedlongflags,constchar*devname,void*dev_id);参数说明:irq:请求的硬件中断号;handler:中断处理函数。一旦中断发生,这个函数就会被调用;flags:中断属性,例如:IRQF_DISABLED、IRQF_TIMER、IRQF_SHARED;devname:中断驱动的名称,见/proc/interrupts文件中对应的内容;dev_id:中断程序的唯一标识,例如:在共享中断中,可以用来区分不同的中断处理程序;驱动程序通过函数free_irq()向操作系统注册一个中断处理程序:voidfree_irq(unsignedintirq,void*dev_id);参数说明:irq:硬件中断号;dev_id:中断程序的唯一标识;实操:捕获键盘中断示例代码有了上面的知识铺垫,下面是实操,实现的功能是:捕获键盘中断,在中断处理函数中,打印出按键的扫描码,如果ESC键被按下,打印出指定的信息。和往常一样,运行目录位于:tmp/linux-4.15/drivers目录。$mkdirmy_driver_interrupt$touchdriver_interrupt.cfilecontent:#include#include#include//interruptnumberstaticintirq;//drivernamestaticchar*devname;//Usedtoreceivetheparameterspassedinwhenloadingthedrivermodulemodule_param(irq,int,0644);module_param(devname,charp,0644);//DefinetheIDofthedriver,usedtodeterminewhetheritisneededintheinterruptprocessingfunction处理#defineMY_DEV_ID1211//驱动程序数据结构structmyirq{intdevid;};//保存驱动程序的所有信息structmyirqmydev={MY_DEV_ID};//键盘相关的IO端口#defineKBD_DATA_REG0x60#defineKBD_STATUS_REG0x64#defineKBD_SCANCODE_MASK0x7f#defineKBD_STATUS_MASK0x80//中断处理函数staticirqreturn_tmyirq_handler(intirq,void*dev){structmyirqmydev;unsignedcharkey_code;mydev=*(structmyirq*)dev;//Checkthedeviceid,onlywhenitisequal,itneedstobeprocessedif(MY_DEV_ID==mydev.devid){//ReadthekeyboardScancodekey_code=inb(KBD_DATA_REG);/*Ifyouletgohere,alotofinformationwillbeprintedouteverytimeyoupressthekeyprintk("key_code:%x%s\n",key_code&KBD_SCANCODE_MASK,key_code&KBD_STATUS_MASK?"released":"pressed");*///judgment:whetheritisanESCkeyif(key_code==0x01){printk("EXCkeyispressed!\n");}}returnIRQ_HANDLED;}//驱动模块初始化函数staticint__initmyirq_init(void){printk("myirq_initiscalled.\n");//注册中断处理函数if(request_irq(irq,myirq_handler,IRQF_SHARED,devname,&mydev)!=0){printk("registerirq[%d]handlerfailed.\n",irq);return-1;}printk("registerirq[%d]handlersuccess.\n",irq);return0;}//驱动模块退出函数staticvoid__exitmyirq_exit(void){printk("myirq_exitiscalled.\n");//注销中断处理程序free_irq(irq,&mydev);}MODULE_LICENSE("GPL");module_init(myirq_init);module_exit(myirq_exit);上面的代码,有两个小知识点在给驱动传递参数的示例代码中,调用request_irq时,需要指定中断号和驱动的名称。这两个参数是加载驱动模块时从命令行传入的。在驱动程序中,可以通过下面两行代码实现参数的接收:module_param(irq,int,0644);module_param(devname,charp,0644);module_param是一个宏定义,定义在include/linux/moduleparam.h文件中,具体定义如下:#definemodule_param(name,type,perm)module_param_named(name,name,type,perm);name:存放参数的变量名;type:类型变量的;perm:访问该参数的权限,表示该参数对应的文件节点在sysfs文件系统中的属性;IO地址:IO口和IO内存这是读取IO外设的两种不同方式。IO口的编址方式有两种:统一编址和独立编址。在统一编译时,会留出一部分主存单元所在的地址空间,专门用来将IO外设寄存器的地址映射到这部分所列的地址空间。统一寻址的好处是:读取IO外设时,就像读取普通内存地址空间中的数据一样。独立寻址IO外设的地址空间和主存单元的地址空间是两个独立的地址空间。这时的IO地址一般称为:IO口。我们在读写IO外设时,可以从这些“IO口”进行读写。不同的外设分配不同的IO端口号。CPU提供了一系列读写IO口的函数,例如://读写一个字节unsignedinb(unsignedport);voidoutb(unsignedcharbyte,unsignedport);//读写一个字unsignedinw(unsignedport);voidoutw(unsignedshortword,unsignedport);编译验证编译驱动模块:$make输出文件:driver_interrupt.ko因为我们捕获键盘中断(中断号:1),加载驱动模块前先看中断驱动头/proc/interrupts:you可以把demsg的输出清理一下:dmesg-c执行如下命令加载驱动模块(传2个参数):insmoddriver_interrupt.koirq=1devname=myirq再次执行命令head/proc/interrupts查看驱动:在中断号1的右侧,您看到我们的驱动程序了吗:my_irq?再看dmesg的输出信息:1号中断的处理函数已经注册成功!这时候按几次键盘左上角的ESC键,然后查看dmesg输出的信息:以上是最简单的中断注册和对应的中断处理函数!在实际项目中,如果要将中断信息通知给应用层,可以通过发送上一篇介绍的信号来实现,也可以通过其他回调机制实现。在下一篇文章中,我们将扩展这个示例代码,看看中断处理中的每个“下半部分”机制应该如何编程。本文转载自微信公众号“IOT物联网小镇”,可通过以下二维码关注。转载本文请联系物联小镇公众号。