一、Linux 中断简介
1. Linux 中断 API 函数
① 中断号
每个中断都有一个中断号,通过中断号可以区分出不同的中断。在 Linux 内核中使用一个 int 变量表示中断号。
② request_irq 函数
在 Linux 中想要使用某个中断是需要申请的,request_irq
函数就是用来申请中断的,并且 request_irq
函数会激活(使能)中断,但 request_irq
函数会导致睡眠,所以不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq
函数。
/** @description : 申请内核中断,并使能中断函数* @param - irq : 要申请中断的中断号* @param - handler : 中断处理函数,当中断发生以后就会执行此中断处理函数* @param - flags : 中断标志* @param - name : 中断名字* @param - dev : 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断。
一般情况下将dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数* @return : 0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经被申请了*/
int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev)
中断标志 | 描述 |
IRQF_SHARED | 多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话, request_irq 函数的 dev 参数就是唯一区分他们的标志。 |
IRQF_ONESHOT | 单次中断,中断执行一次就结束。 |
IRQF_TRIGGER_NONE | 无触发。 |
IRQF_TRIGGER_RISING | 上升沿触发。 |
IRQF_TRIGGER_FALLING | 下降沿触发。 |
IRQF_TRIGGER_HIGH | 高电平触发。 |
IRQF_TRIGGER_LOW | 低电平触发。 |
③ free_irq 函数
使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。
/** @description : 释放中断* @param - irq : 要释放的中断* @param - dev : 如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉* @return : 无*/
void free_irq(unsigned int irq, void *dev);
④ 中断处理函数
使用 request_irq 函数申请中断的时候,需要设置中断处理函数:
irqreturn_t (*irq_handler_t) (int, void *);/*第一个参数是要中断处理函数相应的中断号。第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构。*//* irqreturn_t 结构体 */
enum irqreturn
{IRQ_NONE = (0 << 0),IRQ_HANDLED = (1 << 0),IRQ_WAKE_THREAD = (1 << 1),
};typedef enum irqreturn irqreturn_t;/* 其实一般中断服务函数返回值使用:*/
return IRQ_RETVAL(IRQ_HANDLED)
⑤ 中断使能与禁止函数
void enable_irq(unsigned int irq); // 使能指定中断
void disable_irq(unsigned int irq); // 禁止指定中断// 其实他们的参数 irq 都是要使能/禁止的中断号
// disable_irq 函数有个缺点是,使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出
但我们不能确保的情况下,使用这个中断禁止函数(推荐使用):
// 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。
void disable_irq_nosync(unsigned int irq);
当我们需要关闭当前处理器整个中断系统的时候,使用以下函数:
local_irq_enable(); // 使能当前处理器的中断系统
local_irq_disable(); // 禁止当前处理器的中断系统
这里也有一个缺点是,如果在中断禁止的时候使能中断,这时候可能会任务崩溃,所以使用(推荐使用):
local_irq_save(flags); // 禁止中断,并且将中断状态保存在 flags 中
local_irq_restore(flags); // 恢复中断,将中断到 flags 状态
2. 上半部与下半部
上半部: 中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部: 如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
Linux 内核将中断分为上半部和下半部的目的就是为了实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行。
关于哪些工作放上半部,哪些放下半部,可以参考:
① 如果要处理的内容不希望被其他中断打扰,放在上半部;
② 如果要处理的任务对时间敏感,放到上半部;
③ 如果要处理的任务与硬件有关,放到上半部;
④ 除了以上三点,其他任务都可以放到下半部。
上半部其实就是编写中断处理函数,下半部 Linux 提供了许多机制:
① 软中断
软中断常用于处理需要及时响应的事件,优先级较高的任务。它可以根据优先级和中断处理队列的情况来确定哪个软中断被优先处理。
Linux 内核使用 softirq_action
结构体表示软中断,并且在 kernel/softirq.c 文件中一共定义了 10 个软中断:
static struct softirq_action softirq_vec[NR_SOFTIRQS];
// NR_SOFTIRQS 是枚举类型,定义如下:
enum
{HI_SOFTIRQ=0, /* 高优先级软中断 */TIMER_SOFTIRQ, /* 定时器软中断 */NET_TX_SOFTIRQ, /* 网络数据发送软中断 */NET_RX_SOFTIRQ, /* 网络数据接收软中断 */BLOCK_SOFTIRQ,IRQ_POLL_SOFTIRQ,TASKLET_SOFTIRQ, /* tasklet 软中断 */SCHED_SOFTIRQ, /* 调度软中断 */HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */RCU_SOFTIRQ, /* RCU 软中断 */NR_SOFTIRQS
};
10 个软中断,所以 NR_SOFTIRQS
元素有 10 个。
softirq_action
结构体中的 action
成员变量就是软中断的服务函数,数组 softirq_vec
是个全局数组。
如果要使用软中断,必须先使用 open_softirq
函数注册对应的软中断处理函数:
/** @description : 注册软中断处理函数* @param - nr : 要开启的软中断,选择 NR_SOFTIRQS 其中一个元素* @param - action : 软中断对应的处理函数* @return : 没有返回值*/
void open_softirq(int nr, void (*action)(struct softirq_action *));
但是软中断必须必须在编译的时候静态注册(在编译时期将组件与系统进行绑定的配置方式)。内核使用 softirq_init
函数进行初始化软中断:
void __init softirq_init(void)
{int cpu;for_each_possible_cpu(cpu) {per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head;per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head;}open_softirq(TASKLET_SOFTIRQ, tasklet_action);open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
从 softirq_init
函数可以看出,当使用软中断的时候,这个函数会自动打开:
HI_SOFTIRQ, /* 高优先级软中断 */
TASKLET_SOFTIRQ, /* tasklet 软中断 */
② tasklet
tasklet
适用于低优先级的、需要延迟处理的事件。如果事件需要尽快得到处理并具有不同的优先级,那么软中断更适合;如果事件可以在稍后的时间点进行处理,并且没有特定的优先级要求,那么 tasklet 更适合。但我们已经在下半部分了,所以 tasklet 更适合使用。
tasklet_struct
结构体如下:
struct tasklet_struct
{struct tasklet_struct *next; /* 下一个 tasklet */unsigned long state; /* tasklet 状态 */atomic_t count; /* 计数器,记录对 tasklet 的引用数 */void (*func)(unsigned long); /* tasklet 执行的函数 */ // 这里相当于中断处理函数unsigned long data; /* 函数 func 的参数 */
};
如果要使用 tasklet
,必须先定义一个 tasklet_struct
变量,然后使用 tasklet_init
函数对其进行初始化:
/** @description :tasklet初始化函数* @param - t : 要初始化的 tasklet* @param - func : tasklet 的处理函数* @param - data : 要传递给 func 函数的参数* @return : 没有返回值*/
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long),unsigned long data);
当然也可以使用宏 DECLARE_TASKLET
一次性来完成 tasklet 的定义和初始化。
/** @description : 定义和初始化tasklet* @param - name : 要定义的tasklet名字* @param - func : tasklet的处理函数* @param - data : 传递给 func 函数的参数*/
DECLARE_TASKLET(name, func, data);
除此之外,在上半部分的中断处理函数需要调用 tasklet_schedule 函数,这就可以让 tasklet 在合适的时间运行:
// 这里的形参指针 t:要调度的tasklet
void tasklet_schedule(struct tasklet_struct *t);
tasklet参考示例:
/* 定义 taselet */
struct tasklet_struct testtasklet;/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{....../* 调度 tasklet */tasklet_schedule(&testtasklet);......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{....../* 初始化 tasklet */tasklet_init(&testtasklet, testtasklet_func, data);/* 注册中断处理函数 */request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);......
}
流程图如下:
③ 工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。 所以如果你要推后的工作可以睡眠的话,那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
在 Linux 内核中,使用 work_struct
结构体表示一个工作,这些工作组成工作队列,工作队列用 workqueue_struct
结构体表示,有了工作队列之后,Linux 内核使用 worker
结构体表示工作者线程,就是管理工作队列的结构体。
在实际的驱动开发中,我们只需要定义工作(work_struct
)即可,关于工作队列和工作者线程基本不用管,因为这两者都是由内核自动管理的。
创建工作其实就直接定义一个 work_struct 结构体变量:
#define INIT_WORK(_work, _func)
/*_work:要初始化的工作_func:工作对应的要处理函数*/// 也可以用使用 DECLARE_WORK 宏一次性完成工作的创建和初始化
#define DECLARE_WORK(n, f)
/*n:要初始化的工作(work_struct)f:工作对应的要处理函数*/
和 tasklet 一样,工作也是需要调度才能工作,它的调度函数为 schedule_work
:
/** @description : 调度工作(wrok_struct)的函数* @param - work : 要调度的工作* @return : 0 成功,其他值 失败*/
bool schedule_work(struct work_struct *work);
工作使用示例:
/* 定义工作(work) */
struct work_struct testwork;/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{/* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{....../* 调度 work */schedule_work(&testwork);......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{....../* 初始化 work */INIT_WORK(&testwork, testwork_func_t);/* 注册中断处理函数 */request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);......
}
这里的 工作
的使用流程其实跟 tasklet
一模一样。但他们区别还是蛮大的,例如 工作 是在进程上下文中执行,tasklet 是在软中断上下文执行等等。
3. 设备树中断信息节点
① GIC 中断控制器
STM32MP1 有三个与中断有关的控制器: GIC、EXTI 和 NVIC 。因为 NVIC 是 Cortex-M4 内核的中断控制器,暂时不考虑。
GIC 有 4 个版本:V1~V4,V1 被淘汰,V3 和 V4 是 64 位芯片使用,这次使用的是 GIC V2。
GIC 中断控制器是用来管理中断的优先级、中断分发、中断控制等。当 GIC 接收到外部中断信号以后就会报给 ARM 内核,但是 ARM 内核只提供了四个信号给 GIC 来汇报中断情况: VFIQ、 VIRQ、 FIQ 和 IRQ,他们之间的关系如下:
GIC 接受很多的外部中断,然后对其进行处理,最终通过4个信号报给 ARM 内核:
VFIQ:虚拟快速 FIQ。
VIRQ:虚拟快速 IRQ。
FIQ:快速中断 IRQ。
IRQ:外部中断 IRQ。
虚拟 FIQ 是专门虚拟化环境设计的中断机制,与传统的 FIQ 相互独立,VIRQ 也是有虚拟化环境机制。
FIQ 必须尽快处理,处理结束后离开这个 FIQ。IRQ 可以被 FIQ 中断,但 IRQ 不能中断 FIQ,因此 FIQ 响应更快。GIC V2的逻辑图如下:
左边就是中断源,中间是 GIC 控制器,右边是中断控制器向处理器内核发送的中断信息。
重点看中间部分,GIC 将中断源分为三类:
① SPI(Shared Peripheral Interrupt),共享中断,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于共享中断 。比如 GPIO 中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
② PPI(Private Peripheral Interrupt),私有中断,GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③ SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器 GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。
② 中断 ID
因为有很多中断,为了区分他们必须给他们分配唯一一个 ID 号。这个 ID 号就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 包含了 PPI、 SPI 和 SGI,这 1020 个 ID 分配如下:
D0~ID15:这 16 个 ID 分配给 SGI(软件中断)。
ID16~ID31:这 16 个 ID 分配给 PPI(私有中断)。
D32~ID1019:这 988 个 ID 分配给 SPI(共享中断)。
STM32MP157 总共分配了 265 个中断 ID,加上 SGI 和 PPI,就有 288 个中断ID。从 ID 32 开始的 SPI 中断:
③ EXTI
EXTI 称为 外部中断和事件控制器, EXTI 通过可配置的事件输入和直接事件输入来管理唤醒。它可以针对电源控制提供唤醒请求、针对 CPU 事件输入生成事件。 EXTI 唤醒请求可让系统从停止模式唤醒,以及让 CPU 从 CSTOP 和 CSTANDBY 模式唤醒。此外, EXTI 还可以在运行模式下生成中断请求和事件请求。在实际使用中 EXTI 主要是为 STM32 的 GPIO 中断服务的。
EXTI 的异步输入事件可以分为 2 组:
① 可配置事件(来自能够生成脉冲的 I/O 或外设的信号),这类事件具有以下特性:
– 可选择的有效触发边沿。
– 中断挂起状态寄存器位。
– 单独的中断和事件生成屏蔽。
– 支持软件触发。
② 直接事件(来自其他外设的中断和唤醒源,需要在外设中清除),这类事件具有以下特性:
– 固定上升沿有效触发。
– EXTI 中无中断挂起状态寄存器位(中断挂起状态由生成事件的外设提供)。
– 单独的中断和事件生成屏蔽。
– 不支持软件触发。
对于 GPIO 中断来说,就是可配置事件,EXIT 和 GIC 关系如下:
从上图中可以看出中断方式:
① 外设直接产生中断到 GIC,然后 GIC 通知 CPU 内核。
② GPIO 或外设产生中断到 EXTI,EXTI 将信号提交给 GIC,最终再将中断信号提交给 CPU。
③ GPIO 或外设产生中断到 EXTI,EXTI 直接将中断信号提交给 CPU。
Linux 系统会用到这三种中断方式,一个外设最多可以有两种中断方式。GPIO 中断是我们最常用的。STM32 每一组 GPIO 最多 16 个 IO,比如 PA0~PA15,因此每组 GPIO 就有 16 个中断,这 16 个 GPIO 事件输入对应 EXTI0~15,其中 PA0、PB0,只要是 PX0 的都是对应的是 EXTI0(其实跟学习STM32裸机的时候一样):
如果要在 Linux 系统中使用中断,那么就需要在设备树中设置好中断信息,Linux 内核通过读取设备树中的中断属性信息来配置中断。
④ GIC 控制节点
首先进入 /linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts 目录下,打开 stm32mp151.dtsi 文件。
122 intc: interrupt-controller@a0021000 {123 compatible = "arm,cortex-a7-gic"; // compatible属性为"arm,cortex-a7-gic",那么内核就会去找""里的内容,即可找到GIC中断驱动文件124 #interrupt-cells = <3>; // #interrupt-cells 和#address-cells、 #size-cells 一样。125 interrupt-controller; // 表示当前节点为中断控制器,类似于gpio-controller;126 reg = <0xa0021000 0x1000>,127 <0xa0022000 0x2000>;128 };/*详细了解 #interrupt-cells = <3>;表示此中断控制器下设备的 cells 大小,对于设备而言,会使用 interrupts 属性描述中断信息。#interrupt-cells 描述了 interrupts 属性的 cells 大小,也就是一条信息有几个 cells。每个cells都是32位整型值。这三个cells含义如下:第一个 cells:中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断。第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 32~287(256 个),对于 PPI 中断来说中断号的范围为 16~31,但是该 cell 描述的中断号是从 0 开始。第三个 cells:标志, bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触发。 bit[15:8]为 PPI 中断的 CPU 掩码。*/
首先来看一下 SPI6 如何在设备树节点中描述中断信息的,找到 SPI6 对应的中断号:
第一列的 Num 是 86 号,但是注意,这里并没有算上前面的 PPI + SGI = 32,所以这里应该是 32 + 86 = 118,就跟第二列的 ID 号所对应。
打开stm32mp151.dtsi,找到 SPI6 节点内容:
1712 spi6: spi@5c001000 {
1713 #address-cells = <1>;
1714 #size-cells = <0>;
1715 compatible = "st,stm32h7-spi";
1716 reg = <0x5c001000 0x400>;
1717 interrupts = <GIC_SPI 86 IRQ_TYPE_LEVEL_HIGH>;
1718 clocks = <&scmi0_clk CK_SCMI0_SPI6>;
1719 resets = <&scmi0_reset RST_SCMI0_SPI6>;
1720 dmas = <&mdma1 34 0x0 0x40008 0x0 0x0 0x0>,
1721 <&mdma1 35 0x0 0x40002 0x0 0x0 0x0>;
1722 dma-names = "rx", "tx";
1723 power-domains = <&pd_core>;
1724 status = "disabled";/*interrupts = <GIC_SPI 86 IRQ_TYPE_LEVEL_HIGH>;第一个表示中断类型,为 GIC_SPI,也就是共享中断。第二个表示中断号为86。第三个表示中断出发类型,高电平触发*/
⑤ EXTI 控制节点
打开 stm32mp151.dtsi,其中的 exti 节点就是 EXTI 中断控制器的节点:
exti: interrupt-controller@5000d000 {compatible = "st,stm32mp1-exti", "syscon";interrupt-controller; // 表示exti节点是中断控制器#interrupt-cells = <2>; // 第一个cell表示中断号,第二个cell表示中断标志位reg = <0x5000d000 0x400>;hwlocks = <&hsem 1 1>; // 硬件锁,指向hsem节点,数字 "1 1" 是传递给硬件锁节点的参数
};
在 GPIO 中其实也用到了 EXIT,所以 GPIO 节点里面也有 EXTI 相关内容:
pinctrl: pin-controller@50002000 {
1815 #address-cells = <1>;
1816 #size-cells = <1>;
1817 compatible = "st,stm32mp157-pinctrl";
1818 ranges = <0 0x50002000 0xa400>;
1819 interrupt-parent = <&exti>; // 指定pinctrl所有子节点的中断父节点为exti,这样就可以将GPIO和EXTI联系起来
1820 st,syscfg = <&exti 0x60 0xff>;
1821 hwlocks = <&hsem 0 1>;
1822 pins-are-numbered;
1823
1824 gpioa: gpio@50002000 {
1825 gpio-controller;
1826 #gpio-cells = <2>;
1827 interrupt-controller; // 表示gpioa节点也是中断控制器
1828 #interrupt-cells = <2>; // 这里的第一个cell表示某个IO所处组的编号(类似PA0),第二个cell表示中断触发方式,每个#interrupt-cells在EXTI、GPIO和GIC含义都不一样
1829 reg = <0x0 0x400>;
1830 clocks = <&rcc GPIOA>;
1831 st,bank-name = "GPIOA";// 比如现在要设置PA1引脚为下降沿触发,interrupts=<1 IRQ_TYPE_EDGE_FALLING>
1832 status = "disabled";
1833 };...
/* 由于GPIOA-GPIOK是连续的,GPIOZ对应的寄存器地址不是连续的,所以单独使用pinctrl_z来描述GPIOZ */
pinctrl_z: pin-controller-z@54004000 {
1947 #address-cells = <1>;
1948 #size-cells = <1>;
1949 compatible = "st,stm32mp157-z-pinctrl";
1950 ranges = <0 0x54004000 0x400>;
1951 pins-are-numbered;
1952 interrupt-parent = <&exti>;
1953 st,syscfg = <&exti 0x60 0xff>;
1954 hwlocks = <&hsem 0 1>;
1955
1956 gpioz: gpio@54004000 {
1957 gpio-controller;
1958 #gpio-cells = <2>;
1959 interrupt-controller;
1960 #interrupt-cells = <2>;
1961 reg = <0 0x400>;
1962 clocks = <&scmi0_clk CK_SCMI0_GPIOZ>;
1963 st,bank-name = "GPIOZ";
1964 st,bank-ioport = <11>;
1965 status = "disabled";
1966 };
1967 };
1968 };
来看一个具体应用:
hdmi-transmitter@39 {compatible = "sil,sii9022"; // sii9022是ST开发板上的HDMI芯片reg = <0x39>;iovcc-supply = <&v3v3_hdmi>;cvcc12-supply = <&v1v2_hdmi>;reset-gpios = <&gpioa 10 GPIO_ACTIVE_LOW>; interrupts = <1 IRQ_TYPE_EDGE_FALLING>; // 这个芯片是连接到PG1,下降沿触发中断。interrupt-parent = <&gpiog>; // 指定中断节点的父节点为 gpiog#sound-dai-cells = <0>;status = "okay";
};// 其实在实际开发过程中,只需要通过interrupts和interrupt-parent就可以指定引脚和触发方式。
stm32mp157f-ev1-a7-examples.dts 文件,再来看一个应用:
16 test_keys {17 compatible = "gpio-keys";18 #address-cells = <1>;19 #size-cells = <0>;20 autorepeat;21 status = "okay";22 /* gpio needs vdd core in retention for wakeup */23 power-domains = <&pd_core_ret>;24 25 button@1 {26 label = "PA13";27 linux,code = <BTN_1>;28 interrupts-extended = <&gpioa 13 IRQ_TYPE_EDGE_FALLING>; // 新出现的interrupts-extended29 status = "okay";30 wakeup-source;31 };32 };/*上述代码来描述一个按键,此按键采用中断方式并且使用到PA13引脚。直接通过 interrupts-extended 一个属性描述了所有中断信息,如果要用普通方式来描述的话:interrupt-parent = <&gpioa>;interrupts = <13 IRQ_TYPE_EDGE_FALLING>;这种 interrupts-extended 更加简介。*/
⑥ 获取中断号
编写驱动的时候就需要中断号,用到的中断号,这个信息都已经写到了设备树里面。
一个是 interrupt 属性提取对应设备号:
/** @description : 从interrupt属性提取到对应的设备号* @param - dev : 设备节点* @param - index : 索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息* @return : 中断号*/
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
一个是从 gpio 属性里提取设备号:
/** @description : 从 GPIO 属性提取到对应的设备号* @param - gpio : 要获取的GPIO编号* @return : GPIO 对应的中断号*/
int gpio_to_irq(unsigned int gpio);
二、实验程序编写
这次使用的是案件来触发中断。
首先修改按键中的设备树,打开 /linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts 目录下的 stm32mp157d-atk.dts 文件,修改 key 节点内容:
52 key {53 compatible = "alientek,key";54 status = "okay";55 key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;56 interrupts-extended = <&gpiog 3 IRQ_TYPE_EDGE_BOTH>; // IRQ_TYPE_EDGE_BOTH表示上升沿和下降沿同时有效,相当于按下KEY0和释放的时候同时有效57 // 也可以这样写58 // interrupt-parent = <&gpiog>;59 // interrupts = <3 IRQ_TYPE_EDGE_BOTH>;60 };
之后编译设备树:
cd
cd linux/atk-mpl/linux/my_linux/linux-5.4.31/
make dtbs
将编译好的设备树复制:
cd arch/arm/boot/dts/
sudo cp stm32mp157d-atk.dtb /home/alientek/linux/tftpboot/ -f
在 ~/linux/atk-mpl/Drivers 目录下创建 13_irq,并在里面创建 Vscode 工作区,新建一个 keyirq.c 文件:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>#define KEY_CNT 1 /* 设备号个数 */
#define KEY_NAME "key" /* 名字 *//* 定义按键状态 */
enum key_status {KEY_PRESS = 0, /* 按键按下 */ KEY_RELEASE, /* 按键松开 */ KEY_KEEP, /* 按键状态保持 */
};/* key设备结构体 */
struct key_dev{dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */struct device_node *nd; /* 设备节点 */int key_gpio; /* key所使用的GPIO编号 */struct timer_list timer; /* 按键值 */int irq_num; /* 中断号 */spinlock_t spinlock; /* 自旋锁 */
};static struct key_dev key; /* 按键设备 */
static int status = KEY_KEEP; /* 按键状态 *//* 中断进入定时器,定时时间是把按键抖动给延时掉 */
static irqreturn_t key_interrupt(int irq, void *dev_id) // 中断处理函数
{/* 按键防抖处理,开启定时器延时15ms */mod_timer(&key.timer, jiffies + msecs_to_jiffies(15)); // 为什么需要周期性的定时器,是因为每当检测到按下一次就需要定时器延时return IRQ_HANDLED; // IRQ_HANDLED是一个预定义的常量,表示中断已经得到处理,并且处理程序成功执行了必要的操作
}/** @description : 初始化按键IO,open函数打开驱动的时候* 初始化按键所使用的GPIO引脚。* @param : 无* @return : 无*/
static int key_parse_dt(void)
{int ret;const char *str;/* 设置LED所使用的GPIO *//* 1、获取设备节点:key */key.nd = of_find_node_by_path("/key");if(key.nd == NULL) {printk("key node not find!\r\n");return -EINVAL;}/* 2.读取status属性 */ret = of_property_read_string(key.nd, "status", &str);if(ret < 0) return -EINVAL;if (strcmp(str, "okay"))return -EINVAL;/* 3、获取compatible属性值并进行匹配 */ret = of_property_read_string(key.nd, "compatible", &str);if(ret < 0) {printk("key: Failed to get compatible property\n");return -EINVAL;}if (strcmp(str, "alientek,key")) {printk("key: Compatible match failed\n");return -EINVAL;}/* 4、 获取设备树中的gpio属性,得到KEY0所使用的KYE编号 */key.key_gpio = of_get_named_gpio(key.nd, "key-gpio", 0);if(key.key_gpio < 0) {printk("can't get key-gpio");return -EINVAL;}/* 5 、获取GPIO对应的中断号 */key.irq_num = irq_of_parse_and_map(key.nd, 0);if(!key.irq_num){return -EINVAL;}printk("key-gpio num = %d\r\n", key.key_gpio);return 0;
}/* 主要进行GPIO和中断的初始化 */
static int key_gpio_init(void)
{int ret;unsigned long irq_flags;/* 使用GPIO就要申请GPIO使用权 */ret = gpio_request(key.key_gpio, "KEY0");if (ret) {printk(KERN_ERR "key: Failed to request key-gpio\n");return ret;} /* 将GPIO设置为输入模式 */gpio_direction_input(key.key_gpio);/* 获取设备树中指定的中断触发类型 */irq_flags = irq_get_trigger_type(key.irq_num); // 获得定义的中断触发类型if (IRQF_TRIGGER_NONE == irq_flags)irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;/* 申请中断(使用中断必须申请中断) */ret = request_irq(key.irq_num, key_interrupt, irq_flags, "Key0_IRQ", NULL); // request_irq会默认使能中断,所以不需要enable_irq使能中断if (ret) {gpio_free(key.key_gpio);return ret;}// 建议申请成功后先用disbale_irq函数禁止中断,等所有工作完成之后再来使能中断return 0;
}/* 定时器处理函数 */
static void key_timer_function(struct timer_list *arg)
{static int last_val = 1; // 保存按键上一次读取到的值unsigned long flags;int current_val; // 存放当前按键读取的值/* 自旋锁上锁 */spin_lock_irqsave(&key.spinlock, flags);/* 读取按键值并判断按键当前状态 */current_val = gpio_get_value(key.key_gpio);if (0 == current_val && last_val) /* 按下 */ // 读取的值为0,上次的值为1,则是按下status = KEY_PRESS;else if (1 == current_val && !last_val)status = KEY_RELEASE; /* 松开 */ elsestatus = KEY_KEEP; /* 状态保持 */ last_val = current_val;/* 自旋锁解锁 */spin_unlock_irqrestore(&key.spinlock, flags);
}/** @description : 打开设备* @param - inode : 传递给驱动的inode* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量* 一般在open的时候将private_data指向设备结构体。* @return : 0 成功;其他 失败*/
static int key_open(struct inode *inode, struct file *filp)
{return 0;
}/** @description : 从设备读取数据 * @param – filp : 要打开的设备文件(文件描述符)* @param – buf : 返回给用户空间的数据缓冲区* @param – cnt : 要读取的数据长度* @param – offt : 相对于文件首地址的偏移* @return : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t key_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{unsigned long flags;int ret;/* 自旋锁上锁 */spin_lock_irqsave(&key.spinlock, flags);/* 将按键状态信息发送给应用程序 */ret = copy_to_user(buf, &status, sizeof(int)); // 当前的status保存了按键当前的状态/* 状态重置 */status = KEY_KEEP;/* 自旋锁解锁 */spin_unlock_irqrestore(&key.spinlock, flags);return ret;
}/** @description : 向设备写数据 * @param - filp : 设备文件,表示打开的文件描述符* @param - buf : 要写给设备写入的数据* @param - cnt : 要写入的数据长度* @param - offt : 相对于文件首地址的偏移* @return : 写入的字节数,如果为负值,表示写入失败*/
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{return 0;
}/** @description : 关闭/释放设备* @param - filp : 要关闭的设备文件(文件描述符)* @return : 0 成功;其他 失败*/
static int key_release(struct inode *inode, struct file *filp)
{return 0;
}/* 设备操作函数 */
static struct file_operations key_fops = {.owner = THIS_MODULE,.open = key_open,.read = key_read,.write = key_write,.release = key_release,
};/** @description : 驱动入口函数* @param : 无* @return : 无*/
static int __init mykey_init(void)
{int ret;/* 初始化自旋锁 */spin_lock_init(&key.spinlock);/* 设备树解析 */ret = key_parse_dt();if(ret)return ret;/* GPIO 中断初始化 */ret = key_gpio_init();if(ret)return ret;/* 注册字符设备驱动 *//* 1、创建设备号 */ret = alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME); /* 申请设备号 */if(ret < 0) {pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", KEY_NAME, ret);goto free_gpio;}/* 2、初始化cdev */key.cdev.owner = THIS_MODULE;cdev_init(&key.cdev, &key_fops);/* 3、添加一个cdev */ret = cdev_add(&key.cdev, key.devid, KEY_CNT);if(ret < 0)goto del_unregister;/* 4、创建类 */key.class = class_create(THIS_MODULE, KEY_NAME);if (IS_ERR(key.class)) {goto del_cdev;}/* 5、创建设备 */key.device = device_create(key.class, NULL, key.devid, NULL, KEY_NAME);if (IS_ERR(key.device)) {goto destroy_class;}/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */timer_setup(&key.timer, key_timer_function, 0);return 0;destroy_class:class_destroy(key.class);
del_cdev:cdev_del(&key.cdev);
del_unregister:unregister_chrdev_region(key.devid, KEY_CNT);
free_gpio:free_irq(key.irq_num, NULL);gpio_free(key.key_gpio);return -EIO;
}/** @description : 驱动出口函数* @param : 无* @return : 无*/
static void __exit mykey_exit(void)
{/* 注销字符设备驱动 */cdev_del(&key.cdev);/* 删除cdev */unregister_chrdev_region(key.devid, KEY_CNT); /* 注销设备号 */del_timer_sync(&key.timer); /* 删除timer */device_destroy(key.class, key.devid);/*注销设备 */class_destroy(key.class); /* 注销类 */free_irq(key.irq_num, NULL); /* 释放中断 */gpio_free(key.key_gpio); /* 释放IO */
}module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
新建一个 keyirqApp 测试文件, 通过不断的读取/dev/key 设备文件来获取按键值来判断当前按键的状态,从按键驱动上传到应用程序的数据可以有 3 个值,分别为 0、 1、 2; 0 表示按键按下时的这个状态, 1 表示按键松开时对应的状态,而 2 表示按键一直被按住或者松开。编写测试 APP:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>/** @description : main主程序* @param – argc : argv数组元素个数* @param – argv : 具体参数* @return : 0 成功;其他 失败*/
int main(int argc, char *argv[])
{int fd, ret;int key_val;/* 判断传参个数是否正确 */if(2 != argc) {printf("Usage:\n""\t./keyApp /dev/key\n");return -1;}/* 打开设备 */fd = open(argv[1], O_RDONLY);if(0 > fd) {printf("ERROR: %s file open failed!\n", argv[1]);return -1;}/* 循环读取按键数据 */while(1) {read(fd, &key_val, sizeof(int));if (0 == key_val)printf("Key Press\n");else if (1 == key_val)printf("Key Release\n");}/* 关闭设备 */close(fd);return 0;
}
编写 Makefile 文件:
KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)obj-m := keyirq.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modulesclean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
编译 keyirq.c 和 keyirqApp.c 文件:
make -j32
arm-none-linux-gnueabihf-gcc keyirqApp.c -o keyirqApp
将编译好的 keyirqApp 和 keyirq.ko 文件复制:
sudo cp keyirqApp keyirq.ko /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/
开启开发板,输入以下命令:
cd lib/modules/modules/5.4.31/
depmod # 第一次加载驱动需要运行此命令
modprobe keyirq.ko # 加载驱动
可以查看 /proc/interrputs 文件来检查对应的中断是否注册上了:
cd
cat /proc/interrupts
从上图可以看出,keyirq.c 驱动文件里面的 KYE0 中断已经存在,触发方式为跳边沿(Edge)。
接下来测试中断:
cd lib/modules/5.4.31/
./keyirqApp /dev/key
按键值成功获取,并且不会有抖动的误判发生,说明消抖工作正常。
卸载驱动:
rmmod keyirq.ko
总结
概念:
首先,我们学习了 Linux 中断号,并且了解了中断是如何开启的。每当使用到了中断,必须去申请中断(request_irq),在驱动出口再释放中断(free_irq),如果使用了 request_irq
函数,那么就不用使用使能中断 (enable_irq)。建议在申请成功后先用 disbale_irq
函数禁止中断,等所有工作完成之后再来使能中断。
其次,学习了上半部和下半部,上半部其实就是对哪些时间敏感、执行速度快的操作放在中断处理函数中,也就是上半部,其他的就放在下半部。下半部里面我们学习了三个东西:
① 软中断:它是处理需要及时响应的事件,一般这个了解即可。
② tasklet:是利用软中断来实现,这个是适用于低优先级,需要延迟的事件。这个需要掌握概念和使用方法,定义->处理函数->中断处理函数里写调度->驱动入口函数里写初始化和注册中断处理函数。
③ 工作队列:工作队列在进程上下文执行,如果你的工作可以睡眠,那么选择工作队列。掌握概念及使用方法,使用方法和 tasklet 极为相似。
最后,学习了设备树的中断信息节点,这里面有 GIC 中断控制(重点了解SPI(共享中断))、中断ID(需要查手册)和 EXTI。后面又了解到了 GIC 控制节点和 EXTI 控制节点,这两者 compatible 里面的元素和 #interrupt-cells 信息稍许不一样外,其他类似。并且在设备树里加入中断信息的方式有两种:一种是 interrupts-extended、另一种是 interrupt-parent 和 interrupts,前者是后者的结合体。
程序:
① 在初始化阶段分开了设备树信息设置和 GPIO 初始化设置;
② 在中断处理函数中增减定时器消除按键抖动;
③ 定时器处理函数(也就是回调函数)中去判断按键的值,并打印出按键的值。