在 Linux 下的驱动实验中, 中断是频繁使用的功能, Linux 内核提供了完善的中断框架, 我们只需要使用内核提供的函数, 便可以方便的使用中断功能。
Linux 中断介绍
中断是指 CPU 在执行程序的过程中, 出现了某些突发事件急待处理, CPU 必须暂停当前程序的执行,转去处理突发事件, 处理完毕后又返回原程序被中断的位置继续执行。 由于中断的存在极大的提高了 CPU的运行效率, 但是设备的中断会打断内核进程中的正常调度和运行, 系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。
举例来说, 我现在正在厨房做饭, 突然电话响了, 然后我关火去接电话, 接完电话在回去开火继续做饭, 这个过程就是中断的一个过程。 在这个看似简单的过程中, 却涉及到了中断的几个过程, 我们一起来看一下:
电话铃声响了: 中断请求
我要去接电话: 中断相应
我关掉火: 保护现场
我接电话的过程: 中断处理
接完电话回到厨房开火: 恢复现场
继续做饭: 中断返回
如果我不接电话: 中断屏蔽
为保证系统实时性, 中断服务程序必须足够简短, 但实际应用中某些时候发生中断时必须处理大量的事物, 这时候如果都在中断服务程序中完成, 则会严重降低中断的实时性, 基于这个原因, linux 系统提出了一个概念: 把中断服务程序分为两部分: 顶半部-底半部 。
顶半部(中断上文) : 完成尽可能少的比较急的功能, 它往往只是简单的读取寄存器的中断状态, 并清除中断标志后就进行“中断标记”(也就是把底半部处理程序挂到设备的底半部执行队列中) 的工作。 顶半部的特点就是响应速度快。
底半部(中断下文) : 处理中断的剩余大部分任务, 可以被新的中断打断。
中断相关函数
linux 中断有专门的中断子系统, 其实现原理很复杂, 但是驱动开发者不需要知道其实现的具体细节,只需要知道如何应用该子系统提供的 API 函数来编写中断相关驱动代码即可。
1 获取中断号相关函数
编写驱动的时候需要用到中断号, 每一个中断都有中断号, 我们用到中断号, 中断信息已经写到了设备树里面, 因此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号, 函数原型如下表所示:
函数 | unsigned int irq_of_parse_and_map(struct device_node *dev,int index) |
dev | 设备节点 |
index | 索引号, interrupts 属性可能包含多条中断信息, 通过 index 指定要获取的信息。 |
返回值 | 中断号 |
功能 | 通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号 |
如果使用 GPIO 的话, 可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号, 函数原型如下表所示
函数 | int gpio_to_irq(unsigned int gpio) |
gpio | 要获取的 GPIO 编号 |
返回值 | GPIO 对应的中断号 |
功能 | 获取 GPIO 对应的中断号 |
2 申请中断函数
同 GPIO 一样, 在 Linux 内核里面, 如果我们要使用某个中断也是需要申请的, 申请中断我们使用的函数是 request_irq。 函数原型如下表所示:
函数 | int request_irq( unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev) |
irq | 要申请中断的中断号 |
handler | 中断处理函数, 当中断发生以后就会执行此中断处理函数。 |
flags | 中断标志 |
name | 中断名字, 设置以后可以在开发板/proc/interrupts 文件中看到对应的中断名字 |
dev | 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断, 一般情况下将 dev 设 置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。 |
返回值 | 中断申请成功返回 0, 其他负值则中断申请失败, 如果返回-EBUSY 的话表示中断已经被申请 了。 |
中断标识可以在文件 include/linux/interrupt.h 里面查看所有的中断标志, 这里我们介绍几个常用的中断标志, 如下图所示:
标志 | 功能 |
IRQF_SHARED | 多个设备共享一个中断线, 共享的所有中断都必须指 定此标志。 如果使用共享中断的话, request_irq 函数的 dev 参数就是唯一区分他们的标志。 |
IRQF_ONESHOT | 单次中断, 中断执行一次就结束。 |
IRQF_TRIGGER_NONE | 无触发。 |
IRQF_TRIGGER_RISING | 上升沿触发。 |
IRQF_TRIGGER_FALLING | 下降沿触发。 |
IRQF_TRIGGER_HIGH | 高电平触发。 |
IRQF_TRIGGER_LOW | 低电平触发。 |
3 、 free_irq 函数
中断使用完成以后就要通过 free_irq 函数释放掉相应的中断。 如果中断不是共享的, 那么 free_irq 会删除中断处理函数并且禁止中断。 free_irq 函数原型如下所示
函数 | void free_irq(unsigned int irq,void *dev) |
irq | 要释放的中断 |
dev | 如果中断设置为共享(IRQF_SHARED)的话, 此参数用来区分具体的中断。 共享中断只有在释 放最后中断处理函数的时候才会被禁止掉。 |
返回值 | 无 |
功能 | 释放掉相应的中断 |
4、 中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数, 中断处理函数函数如下表所示:
函数 | irqreturn_t (*irq_handler_t) (int, void *) |
第一个参数 | 要中断处理函数要相应的中断号 |
第二个参数 | 是一个指向 void 的指针, 也就是个通用指针, 需要与 request_irq 函数的 dev 参数保持 一致。 用于区分共享中断的不同设备, dev 也可以指向设备数据结构。 |
返回值 | 中断处理函数的返回值为 irqreturn_t 类型 |
irqreturn_t 类型定义如下所示:
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
可以看出 irqreturn_t 是个枚举类型, 一共有三种返回值。 一般中断服务函数返回值使用如下形式。
return IRQ_RETVAL(IRQ_HANDLED)
5、 中断使能和禁止函数
常用的中断使用和禁止函数如下所示:
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
enable_irq 和 disable_irq 用于使能和禁止指定的中断, irq 就是要禁止的中断号。 disable_irq 函数要等到当前正在执行的中断处理函数执行完才返回, 因此使用者需要保证不会产生新的中断, 并且确保所有已经开始执行的中断处理程序已经全部退出。 在这种情况下, 可以使用另外一个中断禁止函数:
void disable_irq_nosync(unsigned int irq)
disable_irq_nosync 函数调用以后立即返回, 不会等待当前中断处理程序执行完毕。
中断上文和中断下文
中断的存在可以极大的提高 CPU 的运行效率, 但是中断会打断内核进程中的正常调度和运行, 所以为保证系统实时性, 中断服务程序必须足够简短, 但实际应用中某些时候发生中断时必须处理大量的事物,这时候如果都在中断服务程序中完成, 则会严重降低中断的实时性, 基于这个原因, linux 系统提出了一个概念: 把中断服务程序分为两部分: 中断上文和中断下文。
有些资料中也将顶半部和底半部称为上半部和下半部, 都是一个意思。 Linux 内核将中断分为顶半部和底半部的主要目的就是实现中断处理函数的快进快出, 那些对时间敏感、 执行速度快的操作可以放到中断处理函数中, 也就是顶半部。 剩下的所有工作都可以放到底半部去执行, 至于哪些代码要在顶半部完成,哪些代码要在底半部完成, 并没有严格的要求, 要根据实际情况来判断, 下面有一些参考点:
① 如果要处理的内容不希望被其他中断打断, 那么可以放到上半部。
② 如果要处理的任务对时间敏感, 可以放到上半部。
③ 如果要处理的任务与硬件有关, 可以放到上半部
④ 除了上述三点以外的其他任务, 优先考虑放到下半部。
中断上文: 完成尽可能少却比较急的任务, 中断上文的特点就是响应速度快。 中断下文: 处理中断剩余的大量比较耗时间的任务, 而且可以被新的中断打断。
举例来说, 我现在正在厨房做饭, 突然电话响了, 然后我关火去接电话, 快递员打电话让我下楼去拿快递, 接完电话叫我女朋友去下楼拿快递, 然后我在回去开火继续做饭, 这个过程就是中断上下文。
分析例子: 快递员打电话让我下去拿快递, 这个事情很紧急, 所以要快速处理, 这个就是要在中断上文中完成。 但是下楼拿快递这个过程非常耗时间, 所以叫女朋友去拿快递, 这个就是中断下文。 下楼拿快递很耗时间, 如果我不叫女朋友去帮我拿而是自己拿, 等我拿完饭回来我锅里的菜是不是就凉了呀, 同理,如果你在中断里面做很耗时间的时间, 系统就会崩溃。 如果女朋友在去拿快递的过程中, 突然口渴了, 要去超市买水, 所以, 中断下半部分是可以被中断打断的。
总之, 中断上文越快越好, 中断下文可以做比较耗时间的事情, 但是不能死循环。 Linux 中断可以嵌套吗? 以前是可以, 现在不可以。
设备树中的中断节点
如果一个设备需要用到中断功能, 开发人员就需要在设备树中配置好中断属性信息, 因为设备树是用来描述硬件信息的, 然后 Linux 内核通过设备树配置的中断属性来配置中断功能。 对于中断控制器而言, 设备树绑定信息参考文档 Documentation/devicetree/bindings/arm/gic.txt。 打开 imx6ull.dtsi 文件, 其中的 intc节点就是 i.MX6ULL 的中断控制器节点, 节点内容如下所示:
intc:interrupt-controller @00a01000
{
compatible = "arm,cortex-a7-gic";
#interrupt - cells = < 3>;
interrupt - controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
compatible 属性值为“arm,cortex-a7-gic” 在 Linux 内核源码中搜索“arm,cortex-a7- gic” 即可找到 GIC中断控制器驱动文件。
#interrupt-cells 和#address-cells、 #size-cells 一样。 表示此中断控制器下设备的 cells 大小, 对于设备而言, 会使用 interrupts 属性描述中断信息, #interrupt-cells 描述了 interrupts 属性的 cells 大小, 也就是一条信息有几个 cells。 每个 cells 都是 32 位整型值, 对于 ARM 处理的 GIC 来说, 一共有 3 个 cells, 这三个 cells 的含义如下:
第一个 cells: 中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断。
第二个 cells: 中断号, 对于 SPI 中断来说中断号的范围为 0~987, 对于 PPI 中断来说中断号的范围为 0~15。
第三个 cells: 标志, bit[3:0]表示中断触发类型, 为 1 的时候表示上升沿触发, 为 2 的时候表示下降沿触发, 为 4 的时候表示高电平触发, 为 8 的时候表示低电平触发。 bit[15:8]为 PPI 中断的 CPU 掩码。
interrupt-controller 节点为空, 表示当前节点是中断控制器。
对于 gpio 来说, gpio 节点也可以作为中断控制器, 比如 imx6ull.dtsi 文件中的 gpio5 节点内容如下所示:
1 gpio5 : gpio @020ac000{
2 compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
3 reg = <0x020ac000 0x4000>;
4 interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
5 gpio-controller;
6 #gpio-cells = <2>;
7 interrupt-controller;
8 #interrupt-cells = <2>;
9 };
第 4 行, interrupts 描述中断源信息, 对于 gpio5 来说一共有两条信息, 中断类型都是 SPI, 触发电平都是 IRQ_TYPE_LEVEL_HIGH。 不同之处在于中断源, 一个是 74, 一个是 75, 可以打开《IMX6ULL 参考手册》 的“Chapter 3 Interrupts and DMA Events” 章节, 找到表 3-1, 有如图所示的内容:
从 上 图 可 以 看 出 , GPIO5 一 共 用 了 2 个 中 断 号 , 一 个 是 74 , 一 个 是 75 。 其 中 74 对 应GPIO5_IO00~GPIO5_IO15 这低 16 个 IO, 75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 位 IO。
第 7 行, interrupt-controller 表明了 gpio5 节点也是个中断控制器, 用于控制 gpio5 所有 IO 的中断。
第 8 行, 将#interrupt-cells 修改为 2。
中断实际上是非常复杂的, 但是作为开发人员, 我们只需要关心怎么在设备树中指定中断, 怎么在代码中获得中断就可以。 其他的事情, 比如设备树中的中断控制器, 这些都是由原厂的 BSP 工程师帮我们写好了, 我们不需要来修改他。 除非将来你有机会去原厂工作, 否则我们不会从头开始写一个设备树文件的,分工是非常明确的。 我们需要关注的点是怎么在设备树里面描述一个外设的中断节点, 我们来看一个例子。这个例子是用按键给大家举例的, 比如说你现在要接一个外设按键, 通过中断来获取中断的键值, 首先在设备树里面写节点。
key
{
#address - cells = < 1>;
#size - cells = < 1>;
compatible = "key"; //和驱动进行匹配的
pinctrl - names = "default";
pinctrl - 0 = <&pinctrl_key>; //把引脚的复用关系设置为 GPIO
key - gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
interrupt - parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* 双边沿触发 */
status = "okay";
}
在这个例子中, 我们先使用 pinctrl 和 gpio 子系统把这个引脚设置为了 gpio 功能, 因为我们在使用中断的时候需要把引脚设置成输入。 然后使用 interrupt-parent 和 interrupts 属性来描述中断。 interrupt-parent 的属性值是 gpio1, 也就是他的要使用 gpio1 这个中断控制器, 为什么是 gpio1 呢, 因为我们的引脚使用的是gpio1 里面的 io18, 所以我们使用的是 gpio1 这个中断控制器。 interrupts 属性设置的是中断源, 为什么里面是俩个 cells 呢, 因为我们在 gpio1 这个中断控制器里面#interrupt-cells 的值为 2, 如下图所示:
例子中的第一个 cells 的 18 表示 GPIO1 组的 18 号 IO。 IRQ_TYPE_EDGE_BOTH 表示上升沿和下降沿同时有效。
IRQ_TYPE_EDGE_BOTH 定义在文件 include/linux/irq.h 中, 定义如下:
所以我们在设备树里面配置中断的时候只需要俩个步骤即可, 第一个步骤是把管脚设置为 gpio 功能。第二个步骤是使用 interrupt-parent 和 interrupts 属性来描述中断。