Linux驱动实践:一起来梳理【中断】的前世今生(附代码)

系统 Linux
假如存在这样一个需求:应用程序需要监控某个硬件GPIO口的电平状态,当发生变化时,应用程序就做出相应的动作。

[[440009]]

别人的经验,我们的阶梯!

大家好,我是道哥,今天我为大伙儿解说的技术知识点是:【Linux 中断的注册和处理】。

在前两篇文章中,描述的是在应用层如何调用驱动函数来控制GPIO,以及在驱动中如何发送发送信号给应用层。

假如存在这样一个需求:应用程序需要监控某个硬件GPIO口的电平状态,当发生变化时,应用程序就做出相应的动作。

利用之前已经介绍的知识,是可以完成这个需求的。

比如:在驱动程序中不停的读取GPIO口的状态,一旦发生变化,就把新的电平状态通过信号发送到应用层。

这样的方式称作:轮询。

轮询方式的缺点显而易见:轮询的时间间隔应该是多少毫秒(or 微秒),才比较合适呢?

轮询太慢:可能会丢失信号;轮询太快:消耗 CPU 资源!

因此,在实际的产品中,用中断触发的方式才是更切合实际的选择!

本文所有的描述和测试,都是在 x86 平台上完成的;

Linux 中断的知识点梳理

中断的分类

Linux 的版本在持续更新,对中断的处理方式也在不停的发生变化。

下面几张图,是以前在学习时画的思维导图。

这几张图比较清晰地描述了在Linux操作系统中,关于中断的一些基本概念。

这张图的结构还是比较清晰的,基本上概括了Linux系统中的中断分类。

另外,在很多关于中断的书籍中,大部分都是从基础的 PIC(可编程中断控制器)开始讲解的。

如果您想非常具体、专业、深入的了解关于中断的相关内容,有一篇文章《Interrupt in Linux.pdf》讲得非常好(文章的后面部分我也没有看懂)。

在文末有下载链接,感兴趣的小伙伴可以学习一下。

中断号和中断向量

这张图只要记住中断号与中断向量的关系就可以了:

  • 中断号与中断控制器(PIC/APIC)相关;
  • 中断向量与 CPU 相关,用来查找中断处理函数的入口地址;

中断服务例程 ISR

中断服务程序,就是针对每一个中断如何进行处理。

如果您了解Linux中断的相关内容,一定会看到这样的描述:中断处理分为上半部分和下半部分。

上半部分不能消耗太多的时间,主要处理与硬件相关的重要工作;其他不重要的工作,都放在下半部分去做。

从上面这张图中可以看出,用来完成下半部分工作有好几种机制可以选择,每一种方式都是针对不同的需求场景。

在每一种下半部分机制中,Linux都设计了非常方便的接口函数。

作为开发者的我们来说,使用这些下半部分的机制很简单,只需要几个函数调用即可。

例如:如果使用工作队列来实现下半部分的工作,只需要2步动作:

1. 定义处理函数

  1. static struct work_struct mywork; 
  2.  
  3. static void mywork_handler(struct work_struct *work
  4.     printk("This is myword_handler...\n"); 

2. 在中断处理函数中,注册注册函数

  1. NIT_WORK(&mywork, mywork_handler); 
  2.  
  3. schedule_work(&mywork); 

下面几张图,是针对每一种“下半部分”处理机制的一些特点,注意:有些机制在新版本中已经废弃不用了,了解即可。

中断处理的注册和注销 API

所谓的中断注册,就是告诉操作系统:我对哪个中断感兴趣。

当这些中断发生的时候,请通知我。通知的方式就是:调用一个预先注册好的回调函数。

驱动程序可以通过函数 request_irq(),向操作系统注册,并且激活指定的中断线:

  1. int request_irq(unsigned int irq,  
  2.                 irq_handler_t handler, 
  3.                 unsigned long flags,  
  4.                 const char *devname,  
  5.                 void *dev_id); 

参数说明:

irq: 申请的硬件中断号;

handler: 中断处理函数。一旦中断发生,这个函数就被调用;

flags: 中断的属性,例如:IRQF_DISABLED,IRQF_TIMER,IRQF_SHARED;

devname: 中断驱动程序的名称,在 /proc/interrupts 文件中看到对应的内容;

dev_id: 中断程序的唯一标识,比如:在共享中断中,可以用来区分不同的中断处理程序;

驱动程序通过函数 free_irq(),向操作系统注销一个中断处理函数:

  1. void free_irq(unsigned int irq, void *dev_id); 

参数说明:

irq: 硬件中断号;

dev_id: 中断程序的唯一标识;

实操:捕获键盘中断

示例代码

有了上面的知识铺垫,下面就来实操一下,实现的功能是:

捕获键盘的中断,在中断处理函数中,打印出按键的扫描码,如果是 ESC 键被按下,就打印出指定的信息。

与往常一样,操作的目录位于:tmp/linux-4.15/drivers 目录下。

  1. $ mkdir my_driver_interrupt 
  2.  
  3. $ touch driver_interrupt.c 

文件内容:

  1. #include <linux/kernel.h> 
  2. #include <linux/module.h> 
  3. #include <linux/interrupt.h> 
  4.  
  5. // 中断号 
  6. static int irq;  
  7.  
  8. // 驱动程序名称  
  9. static char * devname;           
  10.              
  11. // 用来接收加载驱动模块时传入的参数 
  12. module_param(irq, int, 0644); 
  13. module_param(devname, charp, 0644); 
  14.  
  15. // 定义驱动程序的 ID,在中断处理函数中用来判断是否需要处理             
  16. #define MY_DEV_ID           1211 
  17.  
  18. // 驱动程序数据结构 
  19. struct myirq 
  20.     int devid; 
  21. }; 
  22.  
  23. // 保存驱动程序的所有信息 
  24. struct myirq mydev  ={ MY_DEV_ID }; 
  25.  
  26. // 键盘相关的 IO 端口 
  27. #define KBD_DATA_REG        0x60   
  28. #define KBD_STATUS_REG      0x64 
  29. #define KBD_SCANCODE_MASK   0x7f 
  30. #define KBD_STATUS_MASK     0x80 
  31.          
  32. // 中断处理函数 
  33. static irqreturn_t myirq_handler(int irq, void * dev) 
  34.     struct myirq mydev; 
  35.     unsigned char key_code; 
  36.     mydev = *(struct myirq*)dev;     
  37.      
  38.     // 检查设备 id,只有当相等的时候才需要处理 
  39.     if (MY_DEV_ID == mydev.devid) 
  40.     { 
  41.         // 读取键盘扫描码 
  42.         key_code = inb(KBD_DATA_REG); 
  43.  
  44.         /* 这里如果放开,每次按键都会打印出很多信息 
  45.         printk("key_code: %x %s\n"
  46.                 key_code & KBD_SCANCODE_MASK, 
  47.                 key_code & KBD_STATUS_MASK ? "released" : "pressed"); 
  48.         */ 
  49.      
  50.         // 判断:是否为 ESC 键 
  51.         if (key_code == 0x01) 
  52.         { 
  53.             printk("EXC key is pressed! \n"); 
  54.         } 
  55.     }    
  56.  
  57.     return IRQ_HANDLED; 
  58.   
  59. // 驱动模块初始化函数 
  60. static int __init myirq_init(void) 
  61.     printk("myirq_init is called. \n"); 
  62.  
  63.     // 注册中断处理函数 
  64.     if(request_irq(irq, myirq_handler, IRQF_SHARED, devname, &mydev)!=0) 
  65.     { 
  66.         printk("register irq[%d] handler failed. \n", irq); 
  67.         return -1; 
  68.     } 
  69.  
  70.     printk("register irq[%d] handler success. \n", irq); 
  71.     return 0; 
  72.   
  73. // 驱动模块退出函数 
  74. static void __exit myirq_exit(void) 
  75.     printk("myirq_exit is called. \n"); 
  76.  
  77.     // 注销中断处理函数 
  78.     free_irq(irq, &mydev); 
  79.   
  80. MODULE_LICENSE("GPL"); 
  81. module_init(myirq_init); 
  82. module_exit(myirq_exit); 

上面的代码,有两个小的知识点。

向驱动程序传参

示例代码中,在调用 request_irq 时,需要指定中断号和驱动程序的名称。

这两个参数是在加载驱动模块的时候,从命令行传入的。

在驱动程序中,通过下面两行代码即可实现参数的接收:

  1. module_param(irq, int, 0644); 
  2.  
  3. module_param(devname, charp, 0644); 

module_param 是一个宏定义,定义在 include/linux/moduleparam.h 文件中,具体定义如下:

  1. #define module_param(name, type, perm) 
  2.  
  3. module_param_named(namename, type, perm); 

name: 存储参数的变量名;

type: 变量的类型;

perm: 访问参数的权限,表示此参数在sysfs文件系统中所对应的文件节点的属性;

IO地址:IO端口和IO内存

这是读取 IO 外设的两种不同方式。

IO 端口有两种编址方式:统一编址和独立编址。

统一编制

把主存单元所在的地址空间,划出一部分出来,专门用来把IO外设寄存器的地址映射到这部分划出来的地址空间中。

统一编址的好处是:读取IO外设的时候,就好像读取普通的内存地址空间中的数据一样。

独立编址

IO 外设的地址空间,与主存单元的地址空间是两个独立的地址空间,此时,IO地址一般称作: IO端口。

我们在读写IO外设的时候,从这些 “IO端口” 中读写就可以了。不同的外设,被分配了不同的 IO 端口号。

CPU 提供了一些列函数来读写 IO 端口,例如:

  1. // 读写一个字节 
  2. unsigned inb(unsigned port); 
  3. void outb(unsigned char byte, unsigned port); 
  4.  
  5. // 读写一个字 
  6. unsigned inw(unsigned port); 
  7. void outw(unsigned short word, unsigned port); 

编译、验证

编译驱动模块:

  1. $ make 
  2.  
  3. 输出文件:driver_interrupt.ko 

因为我们捕获的是键盘中断(中断号:1),先看一下在加载驱动模块之前的中断驱动程序 head /proc/interrupts:

可以把 demsg 的输出也清理一下:dmesg -c

执行下面指令来加载驱动模块(传递2个参数):

  1. insmod driver_interrupt.ko irq=1 devname=myirq 

再次执行一下指令 head /proc/interrupts 查看驱动程序:

在中断号 1 的右侧,是不是看到了我们的驱动程序:my_irq?

再来看一下 dmesg 的输出信息:

成功注册了中断号1的处理函数!

此时,按几次键盘左上角的 ESC 键,然后再查看 dmesg 的输出信息:

以上,就是最简单的中断注册和相应的中断处理函数!

在实际的项目中,如果要把中断信息通知到应用层,可以通过上一篇文章介绍的发送信号来实现,或者通过其他的回调机制也可以。

下一篇文章,我们在这个示例代码上进行扩展,看一下:中断处理中每一个“下半部分”机制应该如何编程。

本文转载自微信公众号「IOT物联网小镇」,可以通过以下二维码关注。转载本文请联系IOT物联网小镇公众号。

 

责任编辑:武晓燕 来源: IOT物联网小镇
相关推荐

2012-04-14 20:47:45

Android

2012-06-25 09:37:24

Web

2021-04-26 11:18:15

FedoraLinuxBug

2011-09-15 17:03:44

2012-11-08 17:33:53

智慧云

2020-06-11 18:35:23

C++编程语言

2009-10-29 16:32:34

Oracle表空间

2021-06-02 15:30:12

Synchronize并发多线程

2021-06-09 08:15:50

volatileJava开发

2010-05-21 17:32:07

IIS服务器

2012-09-10 13:42:55

PHP项目管理

2011-09-07 22:59:07

联想一体机

2022-05-20 12:14:50

ZuulSpringClou

2010-05-10 15:31:35

Unix文件

2009-07-14 16:35:57

Swing组件大全

2012-07-10 09:14:51

Web

2009-09-09 09:23:37

服务器稳定性

2017-11-02 14:39:54

点赞
收藏

51CTO技术栈公众号