4. 中断子系统–按键中断实验

本章我们以按键为例讲解在驱动程序中如何使用中断, 这里主要介绍在驱动中如何使用中断。 对于中断的概念及GIC中断控制器相关内容,则借鉴其他基于cortex-A7内核的芯片进行简单讲解, 因为中断相关的内容,大多都于ARM内核紧密联系在一起,这部分内容比较通用。

本章配套源码和设备树插件位于“~/linux_driver/button_interrupt”目录下。

4.1. ARM内核通用中断管理

由于MP157为AMP(非对称多处理)架构,包含了Cortex-M及Cortex-A两款ARM内核,所以拥有两套中断系统。 相比于Cortex-M核使用的NVIC,Cortex-A核中的中断控制系统更复杂,它的中断管理器使用的是GIC V2, GIC V2的实现方式与我们熟知的Cortex-M核中使用的NVIC差别较大。 本章简单讲解Cortex-A核的GIC基本结构以及实现方法,更详细的介绍可以参考 《ARM® Generic Interrupt Controller》 。 至于Cortex-M核的中断实现方法,可以学习野火STM32微控制器系列的教学内容。

关于中断背景知识内容,我们简单讲两点:

  • 掌握GIC V2的实现原理

  • 了解中断的种类及部分代码细节

4.2. GIC简介

GIC是Generic Interrupt Controller的缩写,直译为通用中断控制器。它由ARM公司提出设计规范,并给出一个实际的控制器设计参考,比如GIC-400。最终芯片厂商可以自己实现GIC或者直接购买ARM提供的设计。目前的设计规范中最常用的,有3个版本V2.0、V3.1、V4.1,MP157使用的是GIC V2.0设计标准设计的GIC。

ARM提出的GIC V2.0标准下的功能框图如下所示。

GIC V2.0标准的功能框图

ST为MP157根据GIC V2.0标准设计的GIC功能框图如下。

MP157 GIC的功能框图

GIC V2.0最多支持8个处理器(processor0~ processor7,MP157按照这个规范设计了只支持双核的GIC控制器)。不同处理器的GIC功能是相同的,我们只看其中一个即可。 GIC主要分为分发器(Distributor)和CPU接口(CPU interface/Virtual CPU interface)。下面重点讲解着两部分。

4.2.1. 分发器

4.2.1.1. 分发器简介

分发器用于管理CPU所有中断源,确定每个中断的优先级,管理中断的屏蔽和中断抢占。最终将优先级最高的中断转发到一个或者多个CPU接口。CPU的中断源分为三类,讲解如下:

  • SPI(Shared peripheral interrupts),SPI是共享外设中断,共享外设也就是我们在常用的串口中断、DMA中断等等,对于多个CPU而言,它们操作的都是同一个外设,从某些角度来说它们的中断就是共享的。在上图中SPI中断编号为(32~1019),这是GIC V2.0规范中支持的最大SPI的ID数量,那么实际芯片支持的共享外设中断数量由芯片设计厂商决定,MP157的GIC支持的共享外设中断数量为256个(中断编号为32~287)。那么实际使用的SPI中断请求,如下所示。

图片摘自MP157的参考手册中的第21章《Interrupt list》下的《21.2 GIC Interrupts》小节。

中断源
  • SGI,SGI是软件中断,PPI是CPU私有中断。SGI中断,共有16个中断,中断编号为0~15,SGI一般用于CPU之间通信。

  • PPI,PPI有16个中断,中断编号为16~31,SGI与PPI中断都属于CPU私有中断,每个CPU都有各自的SGI和PPI,这些中断被存储在GIC分发器中。CPU之间通过中断号和CPU编号唯一确定一个SGI或PPI中断。

分发器提供了一些编程接口或者说是寄存器,我们可以通过分发器的编程接口实现如下操作,稍后将介绍这些操作对应的寄存器。

  • 全局的开启或关闭CPU的中断。

  • 控制任意一个中断请求的开启和关闭。

  • 设置每个中断请求的中断优先级。

  • 指定中断发生时将中断请求发送到那些CPU。

  • 设置每个”外部中断”的触发方式(边缘触发或者电平触发)。

4.2.1.2. 分发器相关寄存器介绍

上一小节提到GIC分发器提供了一些编程接口,”编程接口”可以认为是寄存器,这里将简单介绍这些寄存器,因为我们程序中很少会去修改它。 更详细的内容请参考《ARM® Generic Interrupt Controller》4.3 Distributor register descriptions。

GIC分发器映射表如下所示。

表 分发器编程接口

偏移地址

寄存器名

类型

默认值

描述

0x000

GICD_CTLR

RW

0x00000000

分发器控制寄存器

0x004

GICD_TYPER

RO

待定

中断类型控制寄存器

0x008

GICD_IIDR

RO

待定

分发器版本信息寄存器

0x080

GICD_IGROUPRn

RW

待定

中断分组寄存器

0x100-0x17C

GICD_ISENABLERn

RW

待定

中断使能寄存器

0x180-0x1FC

GICD_ICENABLERn

RW

待定

中断屏蔽寄存器

0x200-0x27C

GICD_ISPENDRn

RW

0x00000000

设置中断挂起寄存器

0x280-0x2FC

GICD_ICPENDRn

RW

0x00000000

清除中断挂起寄存器

0x300-0x37C

GICD_ISACTIVERn

RW

0x00000000

设置中断活动状态寄存器

0x380-0x3FC

GICD_ICACTIVERn

RW

0x00000000

清除中断活动状态寄存器

0x400-0x7F8

GICD_IPRIORITYRn

RW

0x00000000

中断优先级设置寄存器

0x800-0x81C

GICD_ITARGETSRn

RO

待定

中断处理目标CPU寄存器

0xC00-0xCFC

GICD_ICFGRn

RW

待定

中断类型(配置)寄存器

0xE00-0xEFC

GICD_NSACRn

RW

0x00000000

非安全访问配置寄存器

0xF00

GICD_SGIR

WO

软件中断产生寄存器

0xF10-0xF1C

GICD_CPENDSGIRn

RW

0x00000000

软件中断挂起寄存器

0xF20-0xF2C

GICD_SPENDSGIRn

RW

0x00000000

软件中断取消挂起寄存器。

首先我们简单介绍上表,在表中只给出了寄存器相对于GIC分发器基地址的偏移地址,GIC基地址保存在另外的寄存器中, 使用到是我们将详细介绍。”默认值”选项中有”待定”手册原文是”IMPLEMENTATION DEFINED”, 原因是这张表格摘自《ARM® Generic Interrupt Controller》,它不针对具体的芯片,这些寄存器的默认值由芯片厂商决定。

表格项”地址偏移”部分值是一个范围比如”中断使能寄存器”地址偏移为”0x100-0x17C”, 原因是”中断使能寄存器”有很多,地址偏移范围是”0x100-0x17C”。

部分寄存器简单介绍如下:

中断使能寄存器GICD_ISENABLERn

中断使能寄存器与中断屏蔽寄存器(GICD_ICENABLERn)是一一对应的,GIC分发器将中断的使能与屏蔽分开设置。

中断使能寄存器如下所示。

中断使能寄存器

仅从《ARM® Generic Interrupt Controller》可知共有1020个(0~1019)中断号即1020个中断,很显然要分别控制每一个中断, 中断使能寄存器(GICD_ISENABLER)肯定不止一个。从表54‑1可知中断使能寄存器偏移地址为0x100-0x17C, 中断使能寄存器从GICD_ISENABLER0到GICD_ISENABLERn依次排布在这段地址空间。

在程序中我们是通过中断号区分不同的中断,假如我们已知中断号为m,那么我们怎么开启或关闭中断m呢?计算过程如下:

  1. 计算要设置那个中断使能寄存器(假设为n),n = m / 32。例如中断号m = 34则中断使能寄存器为GICD_ISENABLER1。寄存偏移地址为(0x100 + (4*n)) = 0x120。

  2. 计算要设置哪一位,接步骤①,假设设置位为q,则q = m %32,m = 34,则开启中断号为34的中断就需要设置GICD_ISENABLER1[2]。

中断使能寄存器支持按位读、写,读出的数据是终端店额当前状态,为0则中断禁用,为1则中断启用。对中断使能寄存器写1则开启中断,写0无效。

中断优先级设置寄存器GICD_IPRIORITYRn

与中断使能寄存器一样GICD_IPRIORITYRn是一组寄存器,根据表 54‑1可知组寄存器位于0x400-0x7F8偏移地址处。中断优先级设置寄存器如下所示。

中断优先级设置寄存器

从上图可以看出每个中断标号占用8位,数值越小中断优先级越高。下面介绍如何根据中断编号找到对应的中断优先级设置寄存器。 假设中断编号为m,中断优先级寄存器为n,中断优先级设置位偏移为offset,则n = m /4。寄存器偏移地址为(0x400 + (4*n))。 在寄存器中的偏移为 offset= m%4。以 m = 65为例,n = 65/4 =16,所以中断优先级设置寄存器为GICD_IPRIORITYR16,offset(n) = 65%4 = 1, 中断号65对应的寄存器为GICD_IPRIORITYR16[8~15].

中断处理目标CPU寄存器GICD_ITARGETSRn

根据之前讲解GIC支持多大8个CPU,在多核处理器中,中断可以通过该寄存器设置处理该中断的从CPU。 例如中断A发生了,通过该寄存器可以将中断A发送到CPU0或发送到CPU1。中断处理目标寄存器如下图所示。

中断处理目标CPU寄存器

每个中断对应8位,位0~7分别代表CPU0~CPU7如下所示,一个中断也可以同时发送到多个CPU, 例如中断A对应的寄存器设置为0x03,则中断A发生后将会发送到CPU0和CPU1。

中断处理目标CPU寄存器

同样,中断处理目标CPU寄存器GICD_ITARGETSRn是一组寄存器,知道中断号经过简单计算之后就可以找到对应的寄存器,这里设中断编号为m, 中断处理目标CPU寄存器为n,中断处理目标CPU寄存器位偏移为offset,则n = m /4。在寄存器中的偏移为 offse t= m%4。 以 m = 65为例,n = 65/4 =16,所以中断处理目标CPU寄存器为GICD_ITARGETSR16,offset = 65%4 = 1,中断处理目标CPU寄存器为GICD_ITARGETSR16[8~15]。

4.3. CPU接口

4.3.1. CPU接口简介

CPU接口为链接到GIC的处理器提供接口,与分发器类似它也提供了一些编程接口,我们可以通过CPU接口实现以下功能:

  • 开启或关闭向处理器发送中断请求.。

  • 确认中断(acknowledging an interrupt)。

  • 指示中断处理的完成。

  • 为处理器设置中断优先级掩码。

  • 定义处理器的抢占策略

  • 确定挂起的中断请求中优先级最高的中断请求。

简单来说,CPU接口可以开启或关闭发往CPU的中断请求,CPU中断开启后只有优先级高于 “中断优先级掩码”的中断请求才能被发送到CPU。 在任何时候CPU都可以从其GICC_Hppir(CPU接口寄存器)读取当前活动的最高优先级。

4.3.2. CPU接口寄存器介绍

同GIC分发器,GIC的CPU接口模块同样提供了一些编程接口,”编程接口”在这里就是一些寄存器,GPU接口寄存器有很多, 我们只介绍几个常用的寄存器,其他寄存使用到时再详细介绍,CPU接口寄存器列表如下表所示。

表 CPU接口寄存器

地址偏移

寄存器名字

类型

复位值

寄存器描述

0x0000

GICC_CTLR

RW

0x00000000

CPU接口控制寄存器

0x0004

GICC_PMR

RW

0x00000000

中断优先掩码寄存器

0x0008

GICC_BPR

RW

0x0000000

中断优先级分组寄存器

0x000C

GICC_IAR

RO

0x000003FF

中断确认寄存器

0x0010

GICC_EOIR

WO

中断结束寄存器

0x0014

GICC_RPR

RO

0x000000FF

运行优先级寄存器

0x0018

GICC_HPPIR

RO

0x000003FF

最高优先级挂起中断寄存器

0x001C

GICC_ABPR

RW

0x0000000

GICC_BPR别名寄存器

0x0020

GICC_AIAR

RO

0x000003FF

GICC_IAR别名寄存器

0x0024

GICC_AEOIR

WO

GICC_EOIR别名寄存器

0x0028

GICC_AHPPIR

RO

0x000003FF

GICC_HPPIR别名寄存器

0x00D0-0x00DC

GICC_APRn

RW

0x00000000

活动的优先级寄存器

0x00E0-0x00EC

GICC_NSAPRnc

RW

0x00000000

不安全的活动优先级寄存器

0x00FC

GICC_IIDR

RO

待定

CPU接口识别寄存器

0x1000

GICC_DIR

WO

禁用中断寄存器

结合上表常用的CPU接口寄存器介绍如下:

CPU接口控制寄存器GICC_CTLR

中断优先掩码寄存器GICC_PMR

在上一小节我们讲解了GIC分发器的中断优先级设置寄存器GICD_IPRIORITYRn,每个中断占8位。这里的中断优先级掩码寄存器GICC_PMR用8位代表一个中断阈值。高于这个优先级的中断才能被送到CPU。GICC_PMR寄存器如下所示。

中断优先掩码寄存器

从上图可以看出GICC_PMR寄存器后8位(0~7)用于设置一个优先级,它的格式与GICD_IPRIORITYR寄存器相同。设置生效后高于此优先级的中断才能发送到CPU。需要注意的是8位寄存器只有高4位有效。与STM32一样,这四位还将分为”抢占优先级”和”子优先级”。讲解优先级分组时再详细介绍。

中断优先级分组寄存器GICC_BPR

中断优先级分组寄存器用于将8位的优先级分成两部分,一部分表示抢占优先级另外一部分表示自优先级,这和STM32的中断优先级分组相同。GICC_BPR寄存器如下所示。

中断优先级分组寄存器

中断优先级分组寄存器的后三位用于设置中断优先级分组,如下表所示。

表 中断优先级分组

GICC_BPR [2:0]

中断优先级值PRI_N[7:4]

级数

二进制点

抢占级位

子优先级位

主优先级

子优先级

0b 001

0b xxxx

[7:4]

16

0

0b 010

0b xxxx

[7:4]

16

0

0b 011

0b xxxx

[7:4]

16

0

0b 100

0b xxx.y

[7:5]

[4]

8

2

0b 101

0b xx.yy

[7:6]

[5:4]

4

4

0b 110

0b x.yyy

[7]

[6:4]

2

8

0b 111

0b .yyyy

None

[7:4]

None

16

每个中断拥有8为中断优先级设置位,但是只有高4位有效,所以表 54‑3中GICC_BPR [2:0] 设置为1到3是相同的,即只有16级抢占优先级没有子优先级。

中断确认寄存器GICC_IAR

中断确认寄存器GICC_IAR保存当前挂起的最高优先级中断,寄存器描述如下图所示。

中断确认寄存器

GICC_IAR寄存器共有两个字段,CPUID[10:12]保存请求中断的CPU ID。对于多核的CPU来说,在处理中断的时候会使用到该位保存的信息。interrupt ID[0:9]用于记录当前挂起的最高优先级中断,读取该寄存器, 如果结果是1023则表示当前没有可用的中断,常见的几种情况如下所示:

  1. 在GIC分发器中禁止了向CPU发送中断请求。

  2. 在GIC的CPU接口中禁止了向CPU发送中断请求。

  3. CPU接口上没有挂起的中断或者挂起的中断优先级小于等于GICC_PMR寄存器设定的优先级值。

4.4. CP15协处理器

在上一小节的GIC寄存器讲解部分我们只给出了”偏移地址”,GIC的基地址保存在CP15协处理器中。 我们修改系统控制寄存器以及设置中断向量表地址都会用到CP15协处理器。

4.4.1. CP15协处理器简介

CP15寄存器是一组寄存器编号为C0~c15。每一个寄存器根据寄存器操作码(opc1和opc2)和CRm又可分为多个寄存器,

以C0为例,如下图所示。

CP15寄存器C0

从上图可以看出根据opc1、CRm、opc2不同CRn(c0)寄存器分为了多个寄存器,我们修改c0寄存器时将会用到opc1、CRm、opc2, 它们的含义如下表所示。(表格摘自Cortex-A7 TechnicalReferenceManua,Table 4-1,更准确的解释请参考官方原文)。

表 协处理器寄存器说明

选项

描述

原文

CRn

协处理器寄存器编号

Primary register number within the system control coprocessor

Op1

寄存器操作码1

Opcode_1 value for the register

CRm

寄存器从编号

Operational register number within CRn

Op2

寄存器操作码2

Opcode_2 value for the register

4.4.2. CP15协处理器寄存器的访问

对于A7内核,我们通常会在芯片的启动文件中用到CP15协处理寄存器,第一处是系统复位中断服务函数开始处,这里通过CP15修改系统控制寄存器,第二处是获取GIC控制器的基地址。稍后我们将介绍着两处代码,首先我们先学习如何读、写CP15协处理器寄存器。

CP15寄存器只能使用MRC/MCR寄存器进行读、写。

  1. 将CP15寄存器(c0~c15)的值读取到通用寄存器(r0~r12)。

mrc {cond} p15, <Opcode_1>, <Rd>, <CRn>, <CRm>, <Opcode_2>

  1. 将通用寄存器(r0~r12)的值写回到CP15寄存器(c0~c15)

mcr {cond} p15, <Opcode_1>, <Rd>, <CRn>, <CRm>, <Opcode_2>

CP15寄存器读、写指令说明如下:

  • cond:指令执行的条件码,忽略则表示无条件执行命令。

  • Opcode_1:寄存器操作码1 ,对应Op1选项。

  • Rd:通用寄存器,当为mrc时,用于保存从CP15寄存器读取得到的数据。当为mcr时,用于保存将要写入CP15寄存器的数据。

  • CRn:要读、写的CP15寄存器(c0~c15),对应的CRn选项。

  • CRm:寄存器从编号,对应CRm选项。

  • Opcode_2:寄存器操作码2,对应的Op2选项。

4.4.3. CP15读、写实例说明

这里我们参考NXP同为A7核处理器的裸机启动代码讲解,代码里有两处使用到了CP15寄存器,包括本小节要使用的GIC基地址。说明如下:

4.4.3.1. 复位中断服务函数中修改系统控制寄存器

通常情况下系统刚刚启动时为防止cache、中断、mmu对初始化造成不必要的影响,需要在复位中断服务函数中暂时关闭这些功能, 如下所示。

裸机复位中断处理代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Reset_Handler:
    cpsid   i               /* Mask interrupts */

/* Reset SCTlr Settings */
mrc  p15, 0, r0, c1, c0, 0     /* Read CP15 System Control register   */
bic  r0,  r0, #(0x1 << 12)     /* Clear I bit 12 to disable I Cache   */
bic  r0,  r0, #(0x1 <<  2)     /* Clear C bit  2 to disable D Cache    */
bic  r0,  r0, #0x2    /* Clear A bit  1 to disable strict alignment  */
bic  r0, r0, #(0x1 << 11) /*Clear Z bit 11 to disable branchprediction */
bic  r0,  r0, #0x1     /* Clear M bit  0 to disable MMU  */
mcr  p15, 0, r0,c1,c0,0 /* Write value back to CP15 System Controlregister */
...

结合以上代码,我们只看”mrc p15, 0, r0, c1, c0, 0”,不难看出,这里读取的CP15标号为c1的寄存器, 该寄存器介绍如下图所示。

CP15标号为c1的寄存器

结合”mrc {cond} p15, <Opcode_1>, <Rd>, <CRn>, <CRm>, <Opcode_2> “指令不难看出这里就是读取的SCTLR(系统控制寄存器)。

  • 第11行: “mcr p15, 0, r0,c1,c0,0”将修改过的r0寄存器值写入到系统控制寄存器。

4.4.3.2. 在IRQ中断服务函数中获取GIC控制器基地址

GIC基地址获取相关代码如下所示

获取GIC基地址
1
2
3
4
/*******************第三部分******************************/
 MRC     P15, 4, r1, C15, C0, 0   /* Get GIC base address  */
 ADD     r1, r1, #0x2000          /* r1: GICC base address  */
 LDR     r0, [r1, #0xC]           /* r0: IAR */

对比”mrc {cond} p15, <Opcode_1>, <Rd>, <CRn>, <CRm>, <Opcode_2> “指令格式可知,CRn、CRm、Opcode_1、Opcode_2分别为c15、c0、4、0。 C15寄存器介绍如下图所示。

C15寄存器

结合上图 可知”MRC P15, 4, r1, C15, C0, 0”读取的是CBAR寄存器。GIC基地址保存在CBAR寄存器中。

4.5. ARM异常类型(中断向量表)

在前面我们知道了CP15协处理器协调系统的启动过程,那么在芯片启动中,还需要对中断进行管理。下面我们来了解一些具体的中断内容。

查找一个芯片有哪些类型的中断,最简单的方法是查看启动文件,这点和STM32单片机一样。当然也可以学习Cortex-A7内核的手册。最简单的方法还是直接百度ARM体系结构学习。

这里我们简单参考一款A7内核的芯片的启动文件。 启动代码的中断向量表部分代码如下所示,其他部分省略。

启动代码
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
__vector_table
        ARM
        LDR     PC, Reset_Word           ; Reset
        LDR     PC, Undefined_Word       ; Undefined instructions
        LDR     PC, SVC_Word             ; Supervisor Call
        LDR     PC, PrefAbort_Word       ; Prefetch abort
        LDR     PC, DataAbort_Word       ; Data abort
        DCD     0                        ; RESERVED
        LDR     PC, IRQ_Word             ; IRQ interrupt
        LDR     PC, FIQ_Word             ; FIQ interrupt

        DATA

Reset_Word      DCD   __iar_program_start
Undefined_Word  DCD   Undefined_Handler
SVC_Word        DCD   SVC_Handler
PrefAbort_Word  DCD   PrefAbort_Handler
DataAbort_Word  DCD   DataAbort_Handler
IRQ_Word        DCD   IRQ_Handler
FIQ_Word        DCD   FIQ_Handler

...

__iar_program_start
        CPSID   I                         ; Mask interrupts

; Reset SCTLR Settings
MRC     P15, 0, R0, C1, C0, 0     ; Read CP15 System Control register
BIC     R0,  R0, #(0x1 << 12)     ; Clear I bit 12 to disable I Cache
BIC     R0,  R0, #(0x1 <<  2)     ; Clear C bit  2 to disable D Cache
BIC  R0,  R0, #0x2          ; Clear A bit  1 to disable strict alignment
BIC   R0, R0, #(0x1 << 11) ; Clear Z bit 11 to disable branch prediction
BIC     R0,  R0, #0x1             ; Clear M bit  0 to disable MMU
; Write value back to CP15 System Control register
MCR     P15, 0, R0, C1, C0, 0

        ; Set up stack for IRQ, System/User and Supervisor Modes
        ; Enter IRQ mode
        CPS     #0x12
        LDR     SP, =SFE(ISTACK)     ; Set up IRQ handler stack

        ; Enter System mode
        CPS     #0x1F
        LDR     SP, =SFE(CSTACK)     ; Set up System/User Mode stack

        ; Enter Supervisor mode
        CPS     #0x13
        LDR     SP, =SFE(CSTACK)     ; Set up Supervisor Mode stack

        LDR     R0, =SystemInit
        BLX     R0
        CPSIE   I                    ; Unmask interrupts

        ; Application runs in Supervisor mode
        LDR     R0, =__cmain
        BX      R0

        PUBWEAK Undefined_Handler
        PUBWEAK SVC_Handler
        PUBWEAK PrefAbort_Handler
        PUBWEAK DataAbort_Handler
        PUBWEAK IRQ_Handler
        PUBWEAK FIQ_Handler
        SECTION .text:CODE:REORDER:NOROOT(2)

        EXTERN  SystemIrqHandler

        ARM

Undefined_Handler
        B .     ; Undefined instruction at address LR-Off \
                        (Off=4 in ARM mode and Off=2 in THUMB mode)

SVC_Handler
        B .     ; Supervisor call from Address LR

PrefAbort_Handler
        B .     ; Prefetch instruction abort at address LR-4

DataAbort_Handler
        B .     ; Load data abort at instruction address LR-8

IRQ_Handler
        PUSH    {LR}             ; Save return address+4
        PUSH    {R0-R3, R12}     ; Push caller save registers

        MRS     R0, SPSR         ; Save SPRS to allow interrupt reentry
        PUSH    {R0}

        MRC     P15, 4, R1, C15, C0, 0  ; Get GIC base address
        ADD     R1, R1, #0x2000         ; R1: GICC base address
        LDR     R0, [R1, #0xC]          ; R0: IAR

        PUSH    {R0, R1}

    CPS     #0x13   ; Change to Supervisor mode to allow interrupt reentry

        PUSH    {LR}              ; Save Supervisor LR
        LDR     R2, =SystemIrqHandler
        BLX     R2                ; Call SystemIrqHandler with param IAR
        POP     {LR}

        CPS     #0x12             ; Back to IRQ mode

        POP     {R0, R1}

    STR     R0, [R1, #0x10]   ; Now IRQ handler finished: write to EOIR

        POP     {R0}
        MSR     SPSR_CXSF, R0

        POP     {R0-R3, R12}
        POP     {LR}
        SUBS    PC, LR, #4

FIQ_Handler
        B .     ; Unexpected FIQ

        END

我们不具体讲解汇编启动文件实现,仅仅从中提取我们需要的信息。

  • 第1-10行: 这是我们要找的中断向量表,从这部分可知这个”表”共有8项,除去一个保留项(RESIVED)剩余7个有效项。各项介绍如下:

    • Res(reset)复位中断,即系统上电或者硬件复位,根据之前讲解系统复位后默认运行在SVC(特权模式)模式,我们裸机默认工作在该模式。

    • Undefined_Word(未定义指令异常中断),如果CPU检测到无法识别的指令时会进入未定义指令异常中断。这种情况下系统已经无法继续运行,只能通过硬件复位或者看门狗复位系统。

    • Supervisor Call(系统调用中断),这种中断用于带linux操作系统的情况,Linux内核(即驱动程序)运行在SVC(特权模式),而Linux应用程序运行在usr模式。应用程序中如果需要调用驱动程序,就需要首先通过系统调用中断切换到SVC(特权模式),即我们常说的从”用户(应用)空间”切换到”内核空间”。

    • Prefetch abort(指令预取失败中断),这种中断的解释就是它的名字。在CPU执行当前指令时会”预取”下一个要执行的指令。如果”取指”失败就会进入该中断。CPU无法获取指令,所以这种情况下可以认为系统”挂了”。

    • Data abort(数据访问终止中断),同样这种中断的解释就是它的名字。CPU读取数据终止,就是说系统读数据错误、读不到数据,所以这种中断后系统也是不正常的。

    • IRQ(中断)与FIQ(快速中断),IRQ与FIQ稍微复杂,简单理解理解为我们常用的外设中断(串口中断、DMA中断、外部中断等等)都将经过IRQ或FIQ传送到CPU。稍后将会详细介绍IRQ与FIQ。

  • 第14-20行: 设置中断向量表,以第15行为例,使用”DCD”伪指令将”Reset_Word”(复位中断)跳转地址设置为”__iar_program_start”即复位中断发生后将会跳转到”__iar_program_start”位置执行即第三部分。

  • 第24-68行: 程序入口,复位中断发生后程序将会跳转到这里执行,这里是程序的入口。具体代码我们暂时不关心。

  • 第70-81行: 从上到下依次为Undefined_Word(未定义指令异常中断)、Supervisor Call(系统调用中断)、Prefetch abort(指令预取失败中断)、Data abort(数据访问终止中断)的中断跳转地址,可以看到他们都会跳转到”B .”即死循环,程序将会卡死在这里。

  • 第83-114行: IRQ(中断),IRQ中断发生后程序将会跳转到这里执行,这里是后面小节讲解的重点,这里暂时跳过。

  • 第116-119行: FIQ(快速中断),裸机程序只使用了IRQ,所以这里将FIQ中断执行代码设置为(B .)即死循环。

这里我们重点理解前面提到的7种异常模式以及其他中断的上报处理流程,这在ARM体系结构中是非常重要的。

4.6. 共享中断实现

上一小节介绍了中断类型,我们可以发现虽然中断向量表中定义了7个中断,但是其中5个中断发生后直接进入死循环, 仅剩下复位中断和IRQ中断。我们常用的外设中断如串口中断、DMA中断等等怎么实现呢?很容易猜到是通过IRQ, 所有外设中断(共享中断)发生时都会进入IRQ中断,在IRQ中断处理函数中进一步区分具体的外设中断。

我们可以从GIC功能框图中的到印证。如下图所示。

GIC功能框图

结合上图介绍,无论是SPI中断、PPI中断、还是SGI中断,它们都链接到了CPU接口,而CPU接口接口输出到CPU的只有两个FIQ和IRQ(VFIQ和VIRQ这里没有用到,暂时忽略)。 中断标号为0~1019的任意一个中断发生后CPU都会跳转到FIQ或IRQ中断服务函数去执行。在前面的代码中默认关闭了FIQ,只使用了IRQ。

根据之前讲解,GIC控制器为我们提供了两个编程接口,分别为分发器寄存器和CPU接口寄存器。

下面将讲解一款A7内核的芯片共享中断的底层处理实现,大家能理解即可。

IRQ共享中断实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
IRQ_Handler:

    push    {lr}         /* Save return address+4     */
    push    {r0-r3, r12} /* Push caller save registers           */

    MRS     r0, spsr    /* Save SPRS to allow interrupt reentry  */
    push    {r0}

    MRC     P15, 4, r1, C15, C0, 0   /* Get GIC base address  */
    ADD     r1, r1, #0x2000          /* r1: GICC base address  */
    LDR     r0, [r1, #0xC]           /* r0: IAR */

    push    {r0, r1}

   CPS  #0x13 /* Change to Supervisor mode to allow interrupt reentry */

    push    {lr}            /* Save Supervisor lr  */
    LDR     r2, =SystemIrqHandler
    BLX     r2              /* Call SystemIrqHandler with param GCC */
    POP     {lr}

    CPS     #0x12           /* Back to IRQ mode */

    POP     {r0, r1}

    STR     r0, [r1, #0x10] /* Now IRQ handler finished: write to EOIR */

    POP     {r0}
    MSR     spsr_cxsf, r0

    POP     {r0-r3, r12}
    POP     {lr}
    SUBS    pc, lr, #4
    .size IRQ_Handler, . - IRQ_Handler

    .align 2
    .arm
    .weak FIQ_Handler
    .type FIQ_Handler, %function

结合代码讲解如下:

同函数调用类似,进入中断函数之前要将程序当前的运行状态保存保存到”栈”中。中断执行完成后能够恢复进入中断之前的状态。

  • 第3、4行: 保存当前状态,同函数调用类似,进入中断函数之前要将程序当前的运行状态保存保存到”栈”中。中断执行完成后能够恢复进入中断之前的状态。

  • 指令”push {lr}”将lr寄存器”入栈”,根据之前讲解当进行函数调用或发生中断时sp(程序计数寄存器,保存当前程序执行位置(Thumb)加4)的值会自动保存到lr寄存器中。lr的值将做为函数会中断返回的地址。

  • 指令”push {r0-r3, r12}”将r0~r3寄存器以及r12寄存器”入栈”。r0~r3和r12是通用寄存器,在函数中它们可以用于任何用途,但是在函数调用或函数返回时它们用于传入函数参数以及传出返回值等等。中断可能发生在程序的任意时刻,所以进入中断之前也要保存这些信息。

  • 第6、7行:于保存spsr(备份程序状态寄存器)。SPRS是特殊功能寄存器不能直接访问,指令”MRS r0, spsr”用于将spsr寄存器的值保存到r0寄存器,然后使用”push {r0}”指令将spsr寄存器的值保存到”栈”中。

  • 第9-11行:获取GIC基地址以及GICC_IAR寄存器的值。这部分代码使用到了CP15协处理器,在54.2 CP15协处理器章节已经介绍,这里不再赘述,简单说明各指令的作用。指令”MRC P15, 4, r1, C15, C0, 0”将GIC基地址保存到r1寄存器中。指令”ADD r1, r1,#0x2000”在GIC基地址基础上增加0x2000,得到GICC(GIC的cpu接口寄存器基地址)基地址。指令”LDR r0, [r1, #0xC]”读取GICC_IAR寄存器的值到r0寄存器。

  • 第13行:将GICC基地址和GICC_IAR寄存器值入栈。第三部分代码将GICC基地址保存在了r1寄存器,将GICC_IAR寄存器的值保存在了r0寄存器,中断执行完成后我们还要用到这些内容,所以这里将他们”入栈”保存。

  • 第15行:切换到Supervisor模式。

  • 第17-20行:跳转到”SystemIrqHandler”函数执行共享中断对应的中断服务函数。指令”push {lr}”保存当前的链接寄存器,即保存程序的返回地址。指令”LDR r2, =SystemIrqHandler”用于将函数”SystemIrqHandler”地址保存到r2寄存器中。指令”BLX r2”是有返回的跳转,程序将会跳转到”SystemIrqHandler”函数执行。去掉不必要的条件编译后如下所示。

systemIrqHandler共享中断处理函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
__attribute__((weak)) void SystemIrqHandler(uint32_t giccIar)
{
  uint32_t intNum = giccIar & 0x3FFUL;

  /* Spurious interrupt ID or Wrong interrupt number */
  if ((intNum == 1023) || (intNum >= NUMBER_OF_INT_VECTORS))
  {
    return;
  }

  irqNesting++;

  /* Now call the real irq handler for intNum */
  irqTable[intNum].irqHandler(giccIar, irqTable[intNum].userParam);

  irqNesting--;
}

结合代码,各部分讲解如下:

  • 第1行:SystemIrqHandler函数有一个入口参数”giccIar”,它是GICC_IAR寄存器的值。在前面的代码中,我们将GICC_IAR寄存器的值保存到了R0寄存器,跳转到SystemIrqHandler函数之后R0寄存器的值作为函数参数。

  • 第3行:获取中断的中断编号。中断编号保存在GICC_IAR寄存器的后10位(0~9)如下所示。

未找到图片
  • 第6-9行:判断中断标号是否有效。根据之前讲解如果中断无效,则读取得到的中断号为1023。”NUMBER_OF_INT_VECTORS”是芯片支持的最大中断号加一,大于等于这个值的中断编号也被认为无效。

那么既然获取到了中断标号,我们就知道发生了什么中断了,再根据中断标号找到对应的中断处理函数,就完成了中断的调用流程。

4.7. GIC知识小结

GIC可以说是ARM芯片对中断管理的得力助手,它有两部分实现:分发器、CPU接口。

分发器负责收集来自于外设或芯片内部的各种中断事件,并基于它们的中断特性(优先级、是否使能等等)对中断进行分发处理。

在中断分发后,就由CPU接口接管接下来的工作,在CPU接口中,中断被统一派发到IRQ或FIQ,触发IRQ或FIQ中断,在IRQ或FIQ中断服务函数中, 通过CP15协处理器获取到GIC中储存的中断分发信息,并基于这些信息,再去执行如DMA中断、串口中断等的对应中断服务函数。这样一个简单的中断触发流程就走完了。

4.8. 设备树中的中断信息以及中断基本函数介绍

在前面,我们通过GIC控制器的背景知识讲解,以及部分汇编代码对中断过程的处理,知道了中断的使用过程。那么前面说到的这一些底层细节, 在Linux系统中已经替我们封装好了大部分的工作内容,我们需要做的只是简单地在这个框架下使用。

MP157的中断来源有四类,分别是GIC、EXTI、PWR、GPIO,它们的关系在底层是并行的,各自负责一块中断功能, 比如GIC负责各项通用外设的中断比如串口、DMA,EXTI、PWR则主要负责那些能将系统从休眠模式唤醒的中断管理,至于GPIO则是GPIO电平检测的中断了。

虽然这些中断分为了四类,但是它们的实现是各有交叉的,也就是说使用GIC中断管理器,也可以管理GPIO触发的中断。

大家参考下图所示:

broken

相关内容参考:

《ST Interrupt overview》

4.8.1. 设备树中的中断相关内容

让我们先来了解一下设备树是如何描述整个中断系统信息的。

4.8.1.1. GIC中断控制器

打开 /arch/arm/boot/dts/ 目录下的 stm32mp157c.dtsi 设备树文件, 找到“interrupt-controller”节点,如下所示。

中断interrupt-controller节点
1
2
3
4
5
6
7
intc:interrupt-controller@a0021000 {
    compatible = "arm,cortex-a7-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    reg = <0xa0021000 0x1000>,
          <0xa0022000 0x2000>;
};
  • compatible:compatible属性用于平台设备驱动的匹配

  • reg:reg指定中断控制器相关寄存器的地址及大小

  • interrupt-controller:声明该设备树节点是一个中断控制器。

  • #interrupt-cells :指定使用该中断控制器的节点要用几个cells来描述一个中断,可理解为用几个参数来描述一个中断信息。 在这里的意思是在intc节点的子节点将用3个参数来描述中断。

学过前面内容的同学们想必对GIC中断控制器并不陌生,GIC架构分为了:分发器(Distributor)和 CPU接口(CPU Interface),上面设备树节点就是用来描述整个GIC控制器的。

一个GIC中断控制器的使用实例,RCC外设:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
soc {
    compatible = "simple-bus";
    #address-cells = <1>;
    #size-cells = <1>;
    interrupt-parent = <&intc>;
    ranges;

    rcc: rcc@50000000 {
        compatible = "st,stm32mp1-rcc", "syscon";
        reg = <0x50000000 0x1000>;
        #clock-cells = <1>;
        #reset-cells = <1>;
        interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
    };
};

RCC外设是soc下的子节点,soc指定了interrupt-parent为intc。 那么RCC外设也就是使用了GIC控制器中断控制器,并用interrupts描述了它使用的资源。

  • interrupts:具体的中断描述信息,在该节点使用的中断控制器intc,规定了使用三个cells来描述子控制器的信息。 三个参数表示的含义如下:

    第一个参数用于指定中断类型,在GIC的中断的类型有三种(SPI共享中断、PPI私有中断、SGI软件中断), 我们使用的外部中断均属于SPI中断类型。

    第二个参数用于设定中断编号,范围和第一个参数有关。PPI中断范围是[0-15],SPI中断范围是[0-256]。

    第三个参数指定中断触发方式,参数是一个u32类型,其中后四位[0-3]用于设置中断触发类型。 每一位代表一个触发方式,可进行组合,系统提供了相对的宏顶义我们可以直接使用,如下所示:

中断触发方式设置
1
2
3
4
5
6
#define IRQ_TYPE_NONE           0
#define IRQ_TYPE_EDGE_RISING    1
#define IRQ_TYPE_EDGE_FALLING   2
#define IRQ_TYPE_EDGE_BOTH      (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH     4
#define IRQ_TYPE_LEVEL_LOW      8

其中第三个参数的[8-15]位,在PPI中断中还用于设置“CPU屏蔽”。在多核系统中这8位用于设置PPI中断发送到那个CPU,一位代表一个CPU, 为1则将PPI中断发送到CPU0,否则屏蔽。

如下示例:

1
2
3
4
5
6
7
8
9
timer {
            compatible = "arm,armv7-timer";
            interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>,
                         <GIC_PPI 14 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>,
                         <GIC_PPI 11 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>,
                         <GIC_PPI 10 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>;
            interrupt-parent = <&intc>;
            always-on;
    };

GIC外设管理的中断非常多,使用起来相比于GPIO中断控制器也要麻烦,所以我们本节的按键实验就不使用GIC中断控制器作为按键的中断父节点,我们使用GPIO中断控制器。

4.8.1.2. EXTI中断控制器

GPIO中断控制器是在EXTI中断控制器节点上实现的,我们来看看EXTI中断控制器。

在stm32mp157c.dtsi文件中直接搜索节点标签“exti”即可找到“exti中断控制器”

exti中断控制器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
exti:interrupt-controller@5000d000 {
    compatible = "st,stm32mp1-exti", "syscon";
    interrupt-controller;
    #interrupt-cells = <2>;
    reg = <0x5000d000 0x400>;
    hwlocks = <&hsem 1>;

    /* exti_pwr is an extra interrupt controller used for
        * EXTI 55 to 60. It's mapped on pwr interrupt
        * controller.
        */
    exti_pwr: exti-pwr {
        interrupt-controller;
        #interrupt-cells = <2>;
        interrupt-parent = <&pwr>;
        st,irq-number = <6>;
    };
};

结合以上代码介绍如下:

  • interrupt-controller:声明该设备树节点是一个中断控制器,只要是中断控制器都要用该标签声明。

  • #interrupt-cells:用于规定该节点的中断控制器将使用2个参数来描述控制器的信息。

4.8.1.3. GPIO使用的中断控制器

在stm32mp157-pinctrl.dtsi文件中可找到GPIO使用的中断控制器,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    soc {
            pinctrl: pin-controller@50002000 {
                    #address-cells = <1>;
                    #size-cells = <1>;
                    compatible = "st,stm32mp157-pinctrl";
                    ranges = <0 0x50002000 0xa400>;
                    interrupt-parent = <&exti>;
                    st,syscfg = <&exti 0x60 0xff>;
                    hwlocks = <&hsem 0>;
                    pins-are-numbered;

                    gpiob: gpio@50003000 {
                            gpio-controller;
                            #gpio-cells = <2>;
                            interrupt-controller;
                            #interrupt-cells = <2>;
                            reg = <0x1000 0x400>;
                            clocks = <&rcc GPIOB>;
                            st,bank-name = "GPIOB";
                            status = "disabled";
                    };
};

soc 节点即片上外设“总节点”,翻阅源码可以发现该节点很长,我们使用的gpio外设包含soc的子节点pinctrl里面。

GPIO也可作为中断控制器,其父节点pinctrl声明了它们的中断控制器是<&exti>节点,此节点在前面有展示。

soc节点内包含的中断控制器很多,几乎用到中断的外设都是中断控制器,我们使用的是开发板上的按键, 使用的是gpiob13,所以这里以gpiob为例介绍。在stm32mp157-pinctrl.dtsi文件中直接搜索gpiob,就可找到gpiob对应的设备树节点。

在gpiob节点中描述了一些属性:

  • interrupt-controller:声明该节点是一个中断控制器

  • #interrupt-cells:声明该节点的子节点将用多少个参数来描述中断信息。

4.8.1.4. 按键设备树节点

这里我们将设备树编写成插件的形式,方便使用,如想将节点信息直接添加到主设备树中,直接拷贝设备树插件的节点内容到主设备树即可。

按键中断的设备树插件在下文中给出。

4.8.2. 中断相关函数

编写驱动之前,我们需要了解中断的使用接口函数。

4.8.2.1. request_irq中断注册函数

申请中断
1
2
static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler,
                                            unsigned long flags, const char *name, void *dev)

参数

  • irq:用于指定“内核中断号”,这个参数我们会从设备树中获取或转换得到。在内核空间中它代表一个唯一的中断编号。

  • handler:用于指定中断处理函数,中断发生后跳转到该函数去执行。

  • flags:中断触发条件,也就是我们常说的上升沿触发、下降沿触发等等 触发方式通过“|”进行组合(注意,这里的设置会覆盖设备树中的默认设置),宏定义如下所示:

中断触发方式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#define IRQF_TRIGGER_NONE       0x00000000
#define IRQF_TRIGGER_RISING     0x00000001
#define IRQF_TRIGGER_FALLING        0x00000002
#define IRQF_TRIGGER_HIGH       0x00000004
#define IRQF_TRIGGER_LOW        0x00000008
#define IRQF_TRIGGER_MASK       (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
                                IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE      0x00000010

#define IRQF_SHARED         0x00000080 ---------①
/*-----------以下宏定义省略------------*/
  • name:中断的名字,中断申请成功后会在“/proc/interrupts”目录下看到对应的文件。

  • dev: 如果使用了**IRQF_SHARED** 宏,则开启了共享中断。“共享中断”指的是多个驱动程序共用同一个中断。 开启了共享中断之后,中断发生后内核会依次调用这些驱动的“中断服务函数”。 这就需要我们在中断服务函数里判断中断是否来自本驱动,这里就可以用dev参数做中断判断。 即使不用dev参数判断中断来自哪个驱动,在申请中断时也要加上dev参数 因为在注销驱动时内核会根据dev参数决定删除哪个中断服务函数。

返回值

  • 成功:返回0

  • 失败:返回负数。

4.8.2.2. 中断注销函数free_irq

申请中断
1
void free_irq(unsigned int irq, void *dev);

参数

  • irq:从设备树中得到或者转换得到的中断编号。

  • dev:与request_irq函数中dev传入的参数一致。

返回值

4.8.2.3. 中断处理函数

在中断申请时需要指定一个中断处理函数,书写格式如下所示。

中断服务函数格式
1
irqreturn_t (*irq_handler_t)(int irq, void * dev);

参数

  • irq:用于指定“内核中断号”。

  • dev:在共享中断中,用来判断中断产生的驱动是哪个,具体介绍同上中断注册函数。 不同的是dev参数是内核“带回”的。如果使用了共享中断还得根据dev带回的硬件信息判断中断是否来自本驱动,或者不使用dev, 直接读取硬件寄存器判断中断是否来自本驱动。如果不是,应当立即跳出中断服务函数,否则正常执行中断服务函数。

返回值

  • irqreturn_t类型:枚举类型变量,如下所示。

中断服务函数返回值类型
1
2
3
4
5
6
7
enum irqreturn {
    IRQ_NONE                = (0 << 0),
    IRQ_HANDLED             = (1 << 0),
    IRQ_WAKE_THREAD         = (1 << 1),
};

typedef enum irqreturn irqreturn_t;

如果是“共享中断”并且在中断服务函数中发现中断不是来自本驱动则应当返回 IRQ_NONE , 如果没有开启共享中断或者开启了并且中断来自本驱动则返回 IRQ_HANDLED,表示中断请求已经被正常处理。 第三个参数涉及到我们后面会讲到的中断服务函数的“上半部分”和“下半部分”, 如果在中断服务函数是使用“上半部分”和“下半部分”实现,则应当返回IRQ_WAKE_THREAD。

4.8.2.4. 中断的使能和禁用函数

通过函数使能、禁用某一个中断。

中断的使能和禁用函数
1
2
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)

参数

  • irq:指定的“内核中断号”

返回值

4.8.2.5. 关闭和开启全局中断相关函数(宏定义)

关闭和开启全局中断相关函数
1
2
3
4
local_irq_enable()
local_irq_disable()
local_irq_save(flags)
local_irq_restore(flags)

由于“全局中断”的特殊性,通常情况下载关闭之前要使用local_irq_save保存当前中断状态, 开启之后使用local_irq_restore宏恢复关闭之前的状态。flags是一个unsigned long 类型的数据。

了解了以上函数的作用,我们就可以编写中断的驱动程序了, 如有遗漏的内容我们将会在代码介绍中,驱动程序介绍如下。

4.9. 按键中断程序实现

4.9.1. 设备树插件实现

在前面提到的中断控制器内容都是厂商为我们提供好的,我们要做的内容很简单, 只需要在我们编写的设备树节点中引用已经写好的中断控制器父节点以及配置中断信息即可。

这里我们编写成设备树插件的形式,方便使用,如下所示:

button按键中断的设备树插件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
/*
 * Copyright (C) STMicroelectronics 2018 - All Rights Reserved
 * Author: Alexandre Torgue <alexandre.torgue@st.com>.
 */

/dts-v1/;
/plugin/;
//#include "../stm32mp157c.dtsi"
#include <dt-bindings/pinctrl/stm32-pinfunc.h>
#include <dt-bindings/input/input.h>
#include <dt-bindings/mfd/st,stpmic1.h>
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>

 / {
     fragment@0 {
          target-path = "/";
         __overlay__ {
            button_interrupt {
                compatible = "button_interrupt";
                pinctrl-names = "default";
                pinctrl-0 = <&pinctrl_button>;
                button_gpio = <&gpiob 13 GPIO_ACTIVE_LOW>;  //默认低电平,按键按下高电平
                status = "okay";
                interrupts-extended = <&gpiob 13 IRQ_TYPE_EDGE_RISING>;
                interrupt-names = "button_interrupt";
                //interrupt-parent = <&gpiob>;
                //interrupts = <13 IRQ_TYPE_EDGE_RISING>;     // 指定中断,触发方式为上升沿触发。
            };
         };
     };

     fragment@1 {
         target = <&pinctrl>;
         __overlay__ {
        pinctrl_button: buttongrp {
                pins {
                    pinmux = <STM32_PINMUX('B', 13, GPIO)>;
                    drive-push-pull;
                };
            };
         };
     };
 };

这里主要介绍和中断相关部分的内容。

  • interrupts-extended:指定“父中断控制器节点”及使用的中断信息。我们按键所在的引脚是gpiob,故我们按键所在的中断控制父节点 为gpiob,gpiob中断控制器需要两个cells描述中断信息,则13、IRQ_TYPE_EDGE_RISING指定了使用中断的引脚编号和中断触发方式。

  • interrupt-names: 定义了该中断的名称为button_interrupt。

需要注意的是,我们编写的这个节点并不是个中断控制器,而是使用中断控制器,所以没有“interrupt-controller”标签。当然大家使用注释里的中断写法也是可以的。

  • 第10-14行:我们在设备树插件中用了几个宏定义,这里需要包含相应头文件,

  • 第20-29行,新增的button_interrupt节点,

  • 第18行,指定设备节点插入位置,这里是根节点。

  • 第22-23行,配置按键引脚。

  • 第24行,定义button使用的GPIO。

  • 第26-27行,添加中断相关信息。

  • 第37-41行,插件按键引脚的复用信息,即pinctrl。

4.9.2. 按键中断驱动程序实现

这里不再介绍有关字符设备的内容,重点放在驱动程序中如何使用中断。 完整的代码请参考本章配套例程。

虽然使用了设备树(设备树插件)但是驱动程序是一个简单的字符设备驱动,不会和设备树中的节点匹配。 无论是否匹配与我们“读设备树”无关,驱动源码大致分为驱动入口和出口函数实现、字符设备操作函数实现两部分内容, 结合源码介绍如下:

4.9.2.1. 驱动入口和出口函数实现

在驱动的入口函数中实现字符设备的注册, 在出口函数中注销字符设备,部分代码如下所示:

驱动入口和出口函数实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 /*
*驱动初始化函数
*/
static int __init button_driver_init(void)
{
    int error = -1;
    /*采用动态分配的方式,获取设备编号,次设备号为0,*/
    error = alloc_chrdev_region(&button_devno, 0, DEV_CNT, DEV_NAME);
    if (error < 0)
    {
            printk("fail to alloc button_devno\n");
            goto alloc_err;
    }
    /*关联字符设备结构体cdev与文件操作结构体file_operations*/
    button_chr_dev.owner = THIS_MODULE;
    cdev_init(&button_chr_dev, &button_chr_dev_fops);

    /*添加设备至cdev_map散列表中*/
    /*------------一下代码省略---------------*/
}

/*
*驱动注销函数
*/
static void __exit button_driver_exit(void)
{
    pr_info("button_driver_exit\n");
    /*删除设备*/
    device_destroy(class_button, button_devno);                //清除设备
    class_destroy(class_button);                                       //清除类
    cdev_del(&button_chr_dev);                                             //清除设备号
    unregister_chrdev_region(button_devno, DEV_CNT);   //取消注册字符设备
}

module_init(button_driver_init);
module_exit(button_driver_exit);

MODULE_LICENSE("GPL");

字符设备注册于注销已经使用n次了,为方便阅读这里将它的部分代码列出来了。完整的内容请参考本小节配套代码。

4.9.2.2. .open函数实现

open函数实现button的初始化工作,代码如下:

open函数实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static int button_open(struct inode *inode, struct file *filp)
{
    int error = -1;


    /*添加初始化代码*/
    // printk_green("button_open");

    /*获取按键 设备树节点*/
    button_device_node = of_find_node_by_path("/button_interrupt");
    if(NULL == button_device_node)
    {
            printk("of_find_node_by_path error!");
            return -1;
    }

    /*获取按键使用的GPIO*/
    button_GPIO_number = of_get_named_gpio(button_device_node ,"button_gpio", 0);
    if(0 == button_GPIO_number)
    {
            printk("of_get_named_gpio error");
            return -1;
    }

    /*申请GPIO  , 记得释放*/
    error = gpio_request(button_GPIO_number, "button_gpio");
    if(error < 0)
    {
            printk("gpio_request error");
            gpio_free(button_GPIO_number);
            return -1;
    }

    error = gpio_direction_input(button_GPIO_number);

    /*获取中断号*/
    interrupt_number = irq_of_parse_and_map(button_device_node, 0);
    printk("\n irq_of_parse_and_map! =  %d \n",interrupt_number);

    /*申请中断, 记得释放*/
    error = request_irq(interrupt_number,button_irq_hander,IRQF_TRIGGER_RISING,"button_interrupt",device_button);
    if(error != 0)
    {
            printk("request_irq error");
            free_irq(interrupt_number, device_button);
            return -1;
    }

    /*申请之后已经开启了,切记不要再次打开,否则运行时报错*/
    // // enable_irq(interrupt_number);

    return 0;
}
  • 第10行,获取button的设备树节点,我们之前说过,虽然驱动没有采用与设备树节点匹配的方式, 但这不影响我们获取设备树节点,只要节点路径正确即可获取其他设备树节点。

  • 第18行,获取使用的GPIO。详细说明可参考“GPIO子系统章节”。

  • 第26行,注册GPIO。

  • 第34行,设置GPIO为输入模式。

  • 第37行,使用函数irq_of_parse_and_map解析并映射(map)中断函数。函数原型如下:

  • 第41行,申请中断,这个函数在本章的开始已经介绍,需要注意的是,这里虽然没有使用共享中断, 但是仍然将dev参数设置为字符设备结构体指针。当然你也可以设置为NULL或其他值。

解析并映射中断函数
1
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)

该函数的功能是从设备树中获取某一个中断,并且将中断ID转化为linux内核虚拟IRQ number。 IRQ number用于区别中断ID。

参数

  • dev:用于指定设备节点

  • index:指定解析、映射第几个中断, 一个设备树节点可能包含多个中断,这里指定第几个,标号从0开始。

返回值

  • 成功:解析、映射得到的内核中断号

  • 失败:返回0

4.9.2.3. 中断服务函数实现

在open函数申请中断时要指定中断服务函数,一个简答的中断服务函数如下。

中断服务函数实现
1
2
3
4
5
6
7
8
atomic_t   button_status = ATOMIC_INIT(0);  //定义整型原子变量,保存按键状态 ,设置初始值为0
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
    // printk("button on \n");
    /*按键状态加一*/
    atomic_inc(&button_status);
    return IRQ_HANDLED;
}

从以上代码可以看到我们定义了一个整型原子变量用于保存按键状态,中断发送后,整型原子变量自增一。 整型原子变量大于0表示有按键按下。

4.9.2.4. .read和.release函数实现

.read函数的工作是向用户空间返回按键状态值,.release函数实现退出之前的清理工作。函数实现源码如下:

.read 和.release函数实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int button_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    int error = -1;
    int button_countervc = 0;

    /*读取按键状态值*/
    button_countervc = atomic_read(&button_status);

    /*结果拷贝到用户空间*/
    error = copy_to_user(buf, &button_countervc, sizeof(button_countervc));
    if(error < 0)
    {
            printk_red("copy_to_user error");
            return -1;
    }

    /*清零按键状态值*/
    atomic_set(&button_status,0);
    return 0;
}

/*字符设备操作函数集,.release函数实现*/
static int button_release(struct inode *inode, struct file *filp)
{
    /*释放申请的引脚,和中断*/
    gpio_free(button_GPIO_number);
    free_irq(interrupt_number, device_button);
    return 0;
}
  • 第1-20行,在button_read函数中我们读取按键状态值,然后使用copy_to_user拷贝到用户空间, 最后设置按键状态为0。

  • 第23-29行,button_release函数很简单,它只是释放.open函数中申请的中断和GPIO.

4.9.3. 测试应用程序实现

测试应用程序工作是读取按键状态然后打印状态,就这么简单,源码如下:

测试应用程序
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int error = -20;
    int button_status = 0;

    /*打开文件*/
    int fd = open("/dev/button", O_RDWR);
    if (fd < 0)
    {
        printf("open file : /dev/button error!\n");
        return -1;
    }

    printf("wait button down... \n");

    do
    {
        /*读取按键状态*/
        error = read(fd, &button_status, sizeof(button_status));
        if (error < 0)
        {
            printf("read file error! \n");
        }
        usleep(1000 * 100); //延时100毫秒
    } while (0 == button_status);
    printf("button Down !\n");

    /*关闭文件*/
    error = close(fd);
    if (error < 0)
    {
        printf("close file error! \n");
    }
    return 0;
}

测试应用程序仅仅是测试驱动是否正常,我们只需要打开、读取状态、关闭文件即可。 需要注意的是打开之后需要关闭才能再次打开,如果连续打开两次由于第一次打开申请的GPIO和中断还没有释放打开会失败。

4.9.4. 实验准备

在板卡上的部分GPIO可能会被系统占用,在使用前请根据需要修改 /boot/uEnv.txt 文件, 可注释掉某些设备树插件的加载,重启系统,释放相应的GPIO引脚。

如本节实验中,可能在鲁班猫系统中默认使能了 KEY 的设备功能, 用在了GPIO子系统。引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源。

方法参考如下:

broken

取消 KEY 设备树插件,以释放系统对应KEY资源,并添加按键中断的设备树插件,操作如下:

broken
dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-button-interrupt.dtbo

如若运行代码时出现“Device or resource busy”或者运行代码卡死等等现象, 请按上述情况检查并按上述步骤操作。

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

4.9.4.1. 通过内核工具编译设备树插件

设备树插件与设备树一样都是使用DTC工具编译,只不过设备树编译为.dtb。而设备树插件需要编译为.dtbo。 我们可以使用DTC编译命令编译生成.dtbo,但是这样比较繁琐、容易出错。

我们可以修改内核目录/arch/arm/boot/dts/overlays下的Makefile文件, 添加我们编辑好的设备树插件。并把设备树插件文件放在和Makefile文件同级目录下。 以进行设备树插件的编译。

broken

在内核的根目录下执行如下命令即可:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157_ebf_defconfig

make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs

生成的.dtbo位于内核根目录下的“/arch/arm/boot/dts/overlays”目录下。

broken

本章的按键中断设备树插件为“stm-fire-button-interrupt-overlay.dts”, 编译之后就会在/arch/arm/boot/dts/overlays目录下生成同名的stm-fire-button-interrupt.dtbo文件。得到.dtbo后,下一步就是将其加载到系统中。

4.9.4.2. 添加设备树插件文件

将上小节中编译出的设备树插件 stm-fire-button-interrupt.dtbo 添加到开发板目录 /usr/lib/linux-image-4.19.94-stm-r1/overlays/ 中重启开发板即可。

4.9.4.3. 编译驱动程序及测试程序

本节实验使用的Makefile如下所示:

Makefile(位于../linux_driver/button_interrupt/interrupt)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
KERNEL_DIR=../ebf_linux_kernel/build_image/build

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export  ARCH  CROSS_COMPILE

obj-m := interrupt.o
out =  test_app

all:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
    $(CROSS_COMPILE)gcc -o $(out) test_app.c

.PHONY:clean
clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
    rm test_app

将配套的驱动代码如:interrupt放置在与内核同级目录下,并在驱动目录中输入如下命令来编译驱动模块及测试程序:

make

4.9.5. 实验现象

将编译好的驱动、应用程序、设备树插件并拷贝到开发板,这里就不再赘述这一部分内容了,前面的章节中都有详细介绍。

在加载模块之前,先查看 /boot/uEnv.txt 文件是否加载了板子上原有的与KEY相关设备树插件。 如KEY相关设备树插件开启,则添加’#’以注销掉与KEY相关的设备树插件。 并在添加按键中断的设备树插件后重启开发板。

ls /proc/device-tree
broken broken

4.10. 中断使用进阶

linux中断我们需要知道以下两点:

  • 1、Linux中断与中断之间不能嵌套。

  • 2、中断服务函数运行时间应当尽量短,做到快进快出。

然而一些中断的产生之后需要较长的时间来处理,如由于网络传输产生的中断, 在产生网络传输中断后需要比较长的时间来处理接收或者发送数据,因为在linux中断并不能被嵌套 如果这时有其他中断产生就不能够及时的响应,为了解决这个问题,linux对中断的处理引入了“中断上半部”和 “中断下半部”的概念,在中断的上半部中只对中断做简单的处理,把需要耗时处理的部分放在中断下半部中,使得能够 对其他中断作为及时的响应,提供系统的实时性。这一概念又被称为中断分层。

  • “上半部分”是指在中断服务函数中执行的那部分代码,

  • “下半部分”是指那些原本应当在中断服务函数中执行但通过某种方式把它们放到中断服务函数外执行。

并不是所有的中断处理都需要用到“上半部分”和“下半部分”,如果像我们上面编写的按键中断程序一样并不需要用到 相对耗时的处理,对中断的处理只需放在中断“上半部分”即可。

为了学习如何使用中断分层,这里模拟一个耗时操作,加上中断分层的“下半部分”。

中断分层实现方法常用的有三种,分别为软中断、tasklet、和工作队列,下面分别介绍这三种方式。

4.10.1. 软中断和tasklet

tasklet是基于软中断实现,它们有很多相似之处,我们把它两个放到一块介绍。

4.10.1.1. 软中断

软中断由软件发送中断指令产生,Linux4.xx支持的软中断非常有限,只有10个(不同版本的内核可能不同) 在Linux内核中使用一个枚举变量列出所有可用的软中断,如下所示。

软中断中断编号
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

    NR_SOFTIRQS
};

类比硬中断,这个枚举类型列出了软中断的中断编号,我们“注册”软中断以及触发软中断都会用到软中断的中断编号。

软中断“注册”函数如下所示:

注册软中断函数
1
2
3
4
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

参数

  • nr:用于指定要“注册”的软中断中断编号

  • action:指定软中断的中断服务函数

返回值

我们再看函数实现,这里只有一个赋值语句, 重点是softirq_vec变量,在内核源码中找到这个变量如下所示:

软中断“中断向量表”
1
static struct softirq_action softirq_vec[NR_SOFTIRQS]

这是一个长度为NR_SOFTIRQS的softirq_action类型数组,长度NR_SOFTIRQS在软中断的“中断编号”枚举类型中有定义, 长度为10。这个数组是一个全局的数组,作用等同于硬中断的中断向量表。接着来看数组的类型“struct softirq_action”如下所示。

 软中断结构体
1
2
3
4
struct softirq_action
{
    void    (*action)(struct softirq_action *);
};

它只有一个参数,就是注册软中断函数的参数open_softirq。至此我们知道数组softirq_vec就是软中断的中断向量表, 所谓的注册软中断函数就是根据中断号将中断服务函数的地址写入softirq_vec数组的对应位置。

软中断注册之后还要调用“触发”函数触发软中断,进而执行软中断中断服务函数,函数如下所示:

中断interrupt-controller节点
1
void raise_softirq(unsigned int nr);

参数

  • nr:要触发的软中断

返回值

4.10.1.2. tasklet

tasklet是基于软中断实现,如果对效率没有特殊要求推荐是用tasklet实现中断分层。为什么这么说, 根据之前讲解软中断的中断服务函数是一个全局的数组,在多CPU系统中,所有CPU都可以访问, 所以在多CPU系统中需要用户自己考虑并发、可重入等问题,增加编程负担。 软中断资源非常有限一些软中断是为特定的外设准备的(不是说只能用于特定外设)例如“NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,” 从名字可以看出它们用于网络的TX和RX。像网络这种对效率要求较高的场合还是会使用软中断实现中断分层的。

相比软中断,tasklet使用起来更简单,最重要的一点是在多CPU系统中同一时间只有一个CPU运行tasklet, 所以并发、可重入问题就变得很容易处理(一个tasklet甚至不用去考虑)。而且使用时也比较简单,介绍如下。

tasklet_struct结构体

在驱动中使用tasklet_struct结构体表示一个tasklet,结构体定义如下所示:

触发软中断
1
2
3
4
5
6
7
8
struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

参数介绍如下:

  • next:指向链表的下一个tasklet_struct,这个参数我们不需要自己去配置。

  • state:保存tasklet状态,等于0表示tasklet还没有被调度,等于TASKLET_STATE_SCHED表示tasklet被调度正准备运行。 等于TASKLET_STATE_RUN表示正在运行。

  • count:引用计数器,如果为0表示tasklet可用否则表示tasklet被禁止。

  • func:指定tasklet处理函数

  • data:指定tasklet处理函数的参数。

tasklet初始化函数

函数原型如下:

tasklet初始化函数
1
2
3
4
5
6
7
8
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
{
    t->next = NULL;
    t->state = 0;
    atomic_set(&t->count, 0);
    t->func = func;
    t->data = data;
}

参数

  • t:指定要初始化的tasklet_struct结构体

  • func:指定tasklet处理函数,等同于中断中的中断服务函数

  • data:指定tasklet处理函数的参数。函数实现就是根据设置的参数填充tasklet_struct结构体结构体。

返回值

触发tasklet

和软中断一样,需要一个触发函数触发tasklet,函数定义如下所示:

tasklet触发函数
1
2
3
4
5
static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
            __tasklet_schedule(t);
}

参数

  • t:tasklet_struct结构体。

返回值

4.10.1.3. tasklet实现中断分层实验

实验在按键中断程序基础上完成,按键中断原本不需要使用中断分层,这里只是以它为例简单介绍tasklet的具体使用方法。 tasklet使用非常简单,主要包括定义tasklet结构体、初始化定义的tasklet结构体、实现tasklet中断处理函数、触发tasklet中断。

下面结合源码介绍如下。注意,源码是在“按键中断程序”基础上添加tasklet相关代码,这里只列出了tasklet相关代码。

tasklet相关代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*--------------第一部分--------------- */
struct tasklet_struct button_tasklet;  //定义全局tasklet_struct类型结构体

/*--------------第二部分-----------------*/
void button_tasklet_hander(unsigned long data)
{
    int counter = 1;
    mdelay(200);
    printk(KERN_ERR "button_tasklet_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "button_tasklet_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "button_tasklet_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "button_tasklet_hander counter = %d \n", counter++);
    mdelay(200);
    printk(KERN_ERR "button_tasklet_hander counter = %d \n", counter++);
}

/*--------------第三部分-----------------*/
static int button_open(struct inode *inode, struct file *filp)
{
    /*----------------以上代码省略----------------*/
    /*初始化button_tasklet*/
    tasklet_init(&button_tasklet,button_tasklet_hander,0);

    /*申请之后已经开启了,切记不要再次打开,否则运行时报错*/
    // // enable_irq(interrupt_number);

    return 0;
}

/*--------------第四部分-----------------*/
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
    printk(KERN_ERR "button_irq_hander----------inter");
    /*按键状态加一*/
    atomic_inc(&button_status);

    tasklet_schedule(&button_tasklet);

    printk(KERN_ERR "button_irq_hander-----------exit");
    return IRQ_RETVAL(IRQ_HANDLED);
}

结合代码各部分介绍如下:

  • 第2行:定义tasklet_struct类型结构体。

  • 第5-18行:定义tasklet的“中断服务函数”可以看到我们在tasklet的中断服务函数中使用延时 和printk语句模拟一个耗时的操作。

  • 第21-31行:在原来的代码基础上调用tasklet_init函数初始化tasklet_struct类型结构体。

  • 第40行:在中断服务函数中调用tasklet_schedule函数触发tasklet中断。 在按键中断服务函数中的开始处和结束处添加打印语句,正常情况下程序会先执行按键中断的中短发服务函数, 退出中断服务函数后再执行中断的下半部分,既tasklet的“中断服务函数”。

4.10.1.4. 下载验证

设备树插件的加载方法在前面章节中已经多次提及,此处就不再赘述,如有疑问请回头查看。

将修改后的驱动程序编译、下载到开发板,使用insmod加载驱动然后运行测试应用程序如下所示。

找不到图片02|

4.10.2. 工作队列

与软中断和tasklet不同,工作队列运行在内核线程,允许被重新调度和睡眠。 如果中断的下部分能够接受被重新调度和睡眠,推荐使用工作队列。

和tasklet类似,从使用角度讲主要包括定义工作结构体、初始化工作、触发工作。

4.10.2.1. 工作结构体

“工作队列”是一个“队列”,但是对于用户来说不必关心“队列”以及队列工作的内核线程,这些内容由内核帮我们完成, 我们只需要定义一个具体的工作、初始化工作即可,在驱动中一个工作结构体代表一个工作,工作结构体如下所示:

work_struct结构体
1
2
3
4
5
6
7
8
struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

重点关心参数“work_func_t func;”该参数用于指定“工作”的处理函数。work_func_t如下所示。

工作函数
1
void (*work_func_t)(struct work_struct *work);

4.10.2.2. 工作初始化函数

内核提初始化宏定义如下所示。

工作初始化宏定义
1
#define INIT_WORK(_work, _func)

该红顶共有两个参数,_work用于指定要初始化的工作结构体,_func用于指定工作的处理函数。

4.10.2.3. 启动工作函数

驱动工作函数执行后相应内核线程将会执行工作结构体指定的处理函数,驱动函数如下所示。

启动工作函数
1
2
3
4
static inline bool schedule_work(struct work_struct *work)
{
    return queue_work(system_wq, work);
}

启动工作函数只有一个工作结构体参数。

4.10.2.4. 工作队列实验

工作队列实验同样在按键中断程序基础上实现,这里只列出了工作队列相关代码, 完整内容请参考本小节配套驱动程序。(这里只修改驱动程序,其他内容保持不变)

工作队列相关函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*--------------第一部分-----------------*/
struct work_struct button_work;

/*--------------第二部分-----------------*/
void work_hander(struct work_struct  *work)
{
    int counter = 1;
    mdelay(200);
    printk(KERN_ERR "work_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "work_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "work_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "work_hander counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "work_hander counter = %d  \n", counter++);
}

/*--------------第三部分-----------------*/
static int button_open(struct inode *inode, struct file *filp)
{
    /*----------------以上代码省略----------------*/
    /*初始化button_work*/
    INIT_WORK(&button_work, work_hander);
    return 0;
}

/*--------------第四部分-----------------*/
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
    /*按键状态加一*/
    atomic_inc(&button_status);
    schedule_work(&button_work);
    return IRQ_HANDLED;
}
  • 第2行:定义work_struct类型结构体。

  • 第5-18行:定义工作队列中的“中断服务函数”,使用延时和printk语句模拟一个耗时的操作。

  • 第21-27行:在原来的代码基础上调用INIT_WORK宏初始化work_struct类型结构体与中断下半部分函数。

  • 第34行:在中断服务函数中调用schedule_work函数触发中断下半部。

与tasklet实现中断分层类似,使用方法几乎一样,这里不进行详细描述。