Linux驱动实践:一起来梳理中断的前世今生(附代码)
作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++、嵌入式、Linux。
关注下方公众号,回复【书籍】,获取 Linux、嵌入式领域经典书籍;回复【PDF】,获取所有原创文章( PDF 格式)。
目录
别人的经验,我们的阶梯!
大家好,我是道哥,今天我为大伙儿解说的技术知识点是:【Linux 中断的注册和处理】。
在前两篇文章中,描述的是在应用层如何调用驱动函数来控制GPIO
,以及在驱动中如何发送发送信号给应用层。
假如存在这样一个需求:应用程序需要监控某个硬件GPIO
口的电平状态,当发生变化时,应用程序就做出相应的动作。
利用之前已经介绍的知识,是可以完成这个需求的。
比如:在驱动程序中不停的读取GPIO
口的状态,一旦发生变化,就把新的电平状态通过信号发送到应用层。
这样的方式称作:轮询。
轮询方式的缺点显而易见:轮询的时间间隔应该是多少毫秒(or 微秒),才比较合适呢?
轮询太慢:可能会丢失信号;轮询太快:消耗 CPU 资源!
因此,在实际的产品中,用中断触发的方式才是更切合实际的选择!
本文所有的描述和测试,都是在 x86 平台上完成的;
Linux 中断的知识点梳理
中断的分类
Linux
的版本在持续更新,对中断的处理方式也在不停的发生变化。
下面几张图,是以前在学习时画的思维导图。
这几张图比较清晰地描述了在Linux
操作系统中,关于中断的一些基本概念。
这张图的结构还是比较清晰的,基本上概括了Linux
系统中的中断分类。
另外,在很多关于中断的书籍中,大部分都是从基础的 PIC
(可编程中断控制器)开始讲解的。
如果您想非常具体、专业、深入的了解关于中断的相关内容,有一篇文章《Interrupt in Linux.pdf》讲得非常好(文章的后面部分我也没有看懂)。
在文末有下载链接,感兴趣的小伙伴可以学习一下。
中断号和中断向量
这张图只要记住中断号与中断向量的关系就可以了:
中断号与中断控制器(PIC/APIC)相关;
中断向量与 CPU 相关,用来查找中断处理函数的入口地址;
中断服务例程 ISR
中断服务程序,就是针对每一个中断如何进行处理。
如果您了解Linux
中断的相关内容,一定会看到这样的描述:中断处理分为上半部分和下半部分。
上半部分不能消耗太多的时间,主要处理与硬件相关的重要工作;其他不重要的工作,都放在下半部分去做。
从上面这张图中可以看出,用来完成下半部分工作有好几种机制可以选择,每一种方式都是针对不同的需求场景。
在每一种下半部分机制中,Linux
都设计了非常方便的接口函数。
作为开发者的我们来说,使用这些下半部分的机制很简单,只需要几个函数调用即可。
例如:如果使用工作队列来实现下半部分的工作,只需要2
步动作:
1. 定义处理函数
static struct work_struct mywork;
static void mywork_handler(struct work_struct *work)
{
printk("This is myword_handler...\n");
}
2. 在中断处理函数中,注册注册函数
INIT_WORK(&mywork, mywork_handler);
schedule_work(&mywork);
下面几张图,是针对每一种“下半部分”处理机制的一些特点,注意:有些机制在新版本中已经废弃不用了,了解即可。
中断处理的注册和注销 API
所谓的中断注册,就是告诉操作系统:我对哪个中断感兴趣。
当这些中断发生的时候,请通知我。通知的方式就是:调用一个预先注册好的回调函数。
驱动程序可以通过函数 request_irq(),向操作系统注册,并且激活指定的中断线:
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *devname,
void *dev_id);
参数说明:
irq: 申请的硬件中断号;
handler: 中断处理函数。一旦中断发生,这个函数就被调用;
flags: 中断的属性,例如:IRQF_DISABLED,IRQF_TIMER,IRQF_SHARED;
devname: 中断驱动程序的名称,在 /proc/interrupts 文件中看到对应的内容;
dev_id: 中断程序的唯一标识,比如:在共享中断中,可以用来区分不同的中断处理程序;
驱动程序通过函数 free_irq(),向操作系统注销一个中断处理函数:
void free_irq(unsigned int irq, void *dev_id);
参数说明:
irq: 硬件中断号;
dev_id: 中断程序的唯一标识;
实操:捕获键盘中断
示例代码
有了上面的知识铺垫,下面就来实操一下,实现的功能是:
捕获键盘的中断,在中断处理函数中,打印出按键的扫描码,如果是 ESC 键被按下,就打印出指定的信息。
与往常一样,操作的目录位于: tmp/linux-4.15/drivers 目录下。
$ mkdir my_driver_interrupt
$ touch driver_interrupt.c
文件内容:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
// 中断号
static int irq;
// 驱动程序名称
static char * devname;
// 用来接收加载驱动模块时传入的参数
module_param(irq, int, 0644);
module_param(devname, charp, 0644);
// 定义驱动程序的 ID,在中断处理函数中用来判断是否需要处理
#define MY_DEV_ID 1211
// 驱动程序数据结构
struct myirq
{
int devid;
};
// 保存驱动程序的所有信息
struct myirq mydev ={ MY_DEV_ID };
// 键盘相关的 IO 端口
#define KBD_DATA_REG 0x60
#define KBD_STATUS_REG 0x64
#define KBD_SCANCODE_MASK 0x7f
#define KBD_STATUS_MASK 0x80
// 中断处理函数
static irqreturn_t myirq_handler(int irq, void * dev)
{
struct myirq mydev;
unsigned char key_code;
mydev = *(struct myirq*)dev;
// 检查设备 id,只有当相等的时候才需要处理
if (MY_DEV_ID == mydev.devid)
{
// 读取键盘扫描码
key_code = inb(KBD_DATA_REG);
/* 这里如果放开,每次按键都会打印出很多信息
printk("key_code: %x %s\n",
key_code & KBD_SCANCODE_MASK,
key_code & KBD_STATUS_MASK ? "released" : "pressed");
*/
// 判断:是否为 ESC 键
if (key_code == 0x01)
{
printk("EXC key is pressed! \n");
}
}
return IRQ_HANDLED;
}
// 驱动模块初始化函数
static int __init myirq_init(void)
{
printk("myirq_init is called. \n");
// 注册中断处理函数
if(request_irq(irq, myirq_handler, IRQF_SHARED, devname, &mydev)!=0)
{
printk("register irq[%d] handler failed. \n", irq);
return -1;
}
printk("register irq[%d] handler success. \n", irq);
return 0;
}
// 驱动模块退出函数
static void __exit myirq_exit(void)
{
printk("myirq_exit is called. \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 文件中,具体定义如下:
#define module_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
端口,例如:
// 读写一个字节
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
// 读写一个字
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
编译、验证
编译驱动模块:
$ make
输出文件:driver_interrupt.ko
因为我们捕获的是键盘中断(中断号:1),先看一下在加载驱动模块之前的中断驱动程序 head /proc/interrupts
:
可以把 demsg 的输出也清理一下:dmesg -c
执行下面指令来加载驱动模块(传递2
个参数):
insmod driver_interrupt.ko irq=1 devname=myirq
再次执行一下指令 head /proc/interrupts
查看驱动程序:
在中断号 1 的右侧,是不是看到了我们的驱动程序:my_irq?
再来看一下 dmesg 的输出信息:
成功注册了中断号1的处理函数!
此时,按几次键盘左上角的 ESC 键,然后再查看 dmesg 的输出信息:
以上,就是最简单的中断注册和相应的中断处理函数!
在实际的项目中,如果要把中断信息通知到应用层,可以通过上一篇文章介绍的发送信号来实现,或者通过其他的回调机制也可以。
下一篇文章,我们在这个示例代码上进行扩展,看一下:中断处理中每一个“下半部分”机制应该如何编程。
------ End ------
文中的测试代码和相关文档,已经放在网盘了。
在公众号【IOT物联网小镇】后台回复关键字:1212,即可获取下载地址。
强烈建议您看一下网盘里的这篇文档:《Interrupt in Linux.pdf》,一定有很大收获!
谢谢!
推荐阅读
【2】C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻
星标公众号,第一时间看文章!
Linux驱动实践:一起来梳理中断的前世今生(附代码)的更多相关文章
- Linux驱动实践:中断处理函数如何【发送信号】给应用层?
作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++.嵌入式.Linux. 关注下方公众号,回复[书籍],获取 Linux.嵌入式领域经典书籍:回复[PDF],获取所有原创文章( PDF 格式). ...
- Linux驱动技术(六) _内核中断
在硬件上,中断源可以通过中断控制器向CPU提交中断,进而引发中断处理程序的执行,不过这种硬件中断体系每一种CPU都不一样,而Linux作为操作系统,需要同时支持这些中断体系,如此一来,Linux中就提 ...
- Linux驱动实践:你知道【字符设备驱动程序】的两种写法吗?
作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++.嵌入式.Linux. 关注下方公众号,回复[书籍],获取 Linux.嵌入式领域经典书籍:回复[PDF],获取所有原创文章( PDF 格式). ...
- Linux驱动实践:带你一步一步编译内核驱动程序
作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++.嵌入式.Linux. 关注下方公众号,回复[书籍],获取 Linux.嵌入式领域经典书籍:回复[PDF],获取所有原创文章( PDF 格式). ...
- Linux驱动实践:如何编写【 GPIO 】设备的驱动程序?
作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++.嵌入式.Linux. 关注下方公众号,回复[书籍],获取 Linux.嵌入式领域经典书籍:回复[PDF],获取所有原创文章( PDF 格式). ...
- Linux驱动设计—— 中断与时钟
中断和时钟技术可以提升驱动程序的效率 中断 中断在Linux中的实现 通常情况下,一个驱动程序只需要申请中断,并添加中断处理函数就可以了,中断的到达和中断函数的调用都是内核实现框架完成的.所以程序员只 ...
- Linux开源模块迁移概述暨交叉编译跨平台移植总结--从《嵌入式Linux驱动模板简洁和工程实践》
本文摘录<嵌入式Linux驱动模板简洁和工程实践>一本书"开发和调试技术". Linux强大的是,有那么多的开源项目可以使用.通常非常需要可以通过寻找相关的源模块被定义 ...
- zynq linux驱动之PL-PS中断【转】
转自:https://blog.csdn.net/h244259402/article/details/83993524 PC:Windows 10 虚拟机:ubuntu 16.04 vivado:2 ...
- Linux驱动之按键驱动编写(中断方式)
在Linux驱动之按键驱动编写(查询方式)已经写了一个查询方式的按键驱动,但是查询方式太占用CPU,接下来利用中断方式编写一个驱动程序,使得CPU占有率降低,在按键空闲时调用read系统调用的进程可以 ...
随机推荐
- [luogu6466]分散层叠算法
做法1 对于每一个询问,直接暴力在每一个序列中二分查询 时间复杂度为$o(nk)-o(k\log n)$ 做法2 将所有序列合并后排序,并对每一个元素预处理出每个序列中第一个大于等于其的元素(位置), ...
- [hdu7099]Just Another Data Structure Problem
不难发现,问题即求满足以下条件的$(i,j)$对数: 1.$1\le i<j\le n$且$a_{i}=a_{j}$ 2.$\min_{i\le k\le j}y_{k}\ge l$且$\max ...
- [cf1495E]Qingshan and Daniel
选择其中卡片总数较少的一类,当相同时选择$t_{1}$所对应的一类(以下记作$A$类) 如果$t_{1}$不是$A$类,就先对$t_{1}$操作一次(即令$a_{1}$减少1) 下面,问题即不断删去$ ...
- [cf674E]Bear and Destroying Subtrees
令$f_{i,j}$表示以$i$为根的子树中,深度小于等于$j$的概率,那么$ans_{i}=\sum_{j=1}^{dep}(f_{i,j}-f_{i,j-1})j$ 大约来估计一下$f_{i,j} ...
- 【Tool】IntelliJ IDEA Ultimate2019.1 中文版 安装
IntelliJ IDEA Ultimate2019.1 2019-07-26 09:26:24 by冲冲 1. 下载 https://mp.weixin.qq.com/s/SdFQqGzMy-g ...
- Anaconda建立新的环境,出现CondaHTTPError: HTTP 000 CONNECTION FAILED for url ...... 解决过程
2020.3.7准备scrapy,使用anaconda创建一个新的环境,执行"conda create -n scrapyEnv python=3.6",结果出现了"Co ...
- 【机器学习与R语言】12- 如何评估模型的性能?
目录 1.评估分类方法的性能 1.1 混淆矩阵 1.2 其他评价指标 1)Kappa统计量 2)灵敏度与特异性 3)精确度与回溯精确度 4)F度量 1.3 性能权衡可视化(ROC曲线) 2.评估未来的 ...
- windows下typora图片自动上传到图床(附带腾讯云教程)
目录 1. 背景 2. 在Typora中使用 3. 安装 3.1 安装教程环境说明 3.2 安装步骤 3.3 配置图床 3.3.1 插件配置 3.3.2 腾讯云参数获取 3.3.2.1 新建子用户,获 ...
- kubernetes部署haproxy、keepalived为kube-apiserver做集群
也可以用nginx.keepalived做负载均衡,看大家的需求. # yum -y install haproxy keepalived haproxy的配置文件(三台一样): cat > / ...
- 13 — springboot集成mybatis-plus — 更新完毕
1.mybatis-plus需要掌握的知识 1).mybatis-plus是什么? 不写了,老衲一般都是直接进官网 mybatis-plus官网地址:https://baomidou.com/guid ...