瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。
【视频观看链接】
【北京迅为】嵌入式学习之Linux驱动(第五期
【粉丝交流群】824412014 (加群获取文档+例程)
【购买链接】https://item.taobao.com/item.htm?spm=a1z10.5-c-s.w4002-22452452613.11.2fec74a6elWNeA&id=669939423234
在前面的课程中,我们深入学习了高级字符设备的进阶知识,包括IO模型、定时器原理、llseek设备定位和通过ioctl传递参数等。通过这些课程,我们对高级字符设备有了深入的理解,并掌握了一些实用的技术和编程方法。从今天开始,我们就进入中断课程的学习了。中断是操作系统中至关重要的机制,它能够显著提高系统的响应性能和并发处理能力。
中断是指在CPU正常运行期间,由外部或内部事件引起的一种机制。当中断发生时,CPU会停止当前正在执行的程序,并转而执行触发该中断的中断处理程序。处理完中断处理程序后,CPU会返回到中断发生的地方,继续执行被中断的程序。中断机制允许CPU在实时响应外部或内部事件的同时,保持对其他任务的处理能力。
可以想象这样一幅画面,你正在烹饪一顿美味的晚餐,准备了各种食材,点燃了炉灶,开始了幸福的烹饪过程,突然,你的手机响起,有人打来了一个紧急电话,打破了你正常的烹饪流程,这时候你需要立刻停止手中的工作,迅速接起电话,与对方进行交流,在接完电话之后,再回到厨房继续之前的烹饪流程。这就是一个在实际生活中的中断案例,中断的概念流程图如下(39-1)所示:
图 39-1
在上面的场景中,作为唯一具有处理能力的主体,我们一次只能专注于一个任务,可以等待水烧开、看电视等等。然而,当我们专心致志地完成一项任务时,常常会有紧迫或不紧迫的其他事情突然出现,需要我们关注和处理。有些情况甚至要求我们立即停下手头的工作来应对。只有在处理完这些中断事件之后,我们才能回到先前的任务。
中断机制赋予了我们处理意外情况的能力,而且如果我们能充分利用这个机制,就能够同时完成多个任务。回到烧水的例子,无论我们是否在厨房,煤气灶都会将水烧开。我们只需要在水烧开后及时关掉煤气。为了避免在厨房等待的时间,而水烧开时产生的声音就是中断信号,提醒我们炉子上的水已经烧开。这样,我们就可以在等待的时间里做其他事情,比如看电视。当水壶烧开发出声音之后,它会打断当前的任务,提醒水已经烧开,这时只需要前往厨房关掉煤气即可。
中断机制使我们能够有条不紊地同时处理多个任务,从而提高了并发处理能力。类似地,计算机系统中也使用中断机制来应对各种外部事件。例如,在键盘输入时,会发送一个中断信号给CPU,以便及时响应用户的操作。这样,CPU就不必一直轮询键盘的状态,而可以专注于其他任务。中断机制还可以用于处理硬盘读写完成、网络数据包接收等事件,提高了系统的资源利用率和并发处理能力。
中断的执行需要快速响应,但并不是所有中断都能迅速完成。此外,Linux中的中断不支持嵌套,意味着在正式处理中断之前会屏蔽其他中断,直到中断处理完成后再重新允许接收中断,如果中断处理时间过长,将会引发问题。
这里仍旧以烹饪的过程中接电话进行举例:当你正在烹饪一顿美味的晚餐时,所有的食材都准备好了,炉灶上的火焰跳跃着,你正享受着烹饪的乐趣。突然,你的手机响起,发出紧急电话的铃声,打破了你正常的烹饪流程,接电话的时间很短并不会对烹饪产生很大的影响,而接电话的时候可能就有问题了,水烧开之后可能会煮干、错过了最好的添加调味料的时间等等。
而为了让系统可以更好地处理中断事件,提高实时性和响应能力,将中断服务程序划分为上下文两部分:
中断上文是中断服务程序的第一部分,它主要处理一些紧急且需要快速响应的任务。中断上文的特点是执行时间较短,旨在尽快完成对中断的处理。这些任务可能包括保存寄存器状态、更新计数器等,以便在中断处理完成后能够正确地返回到中断前的执行位置。
中断下文是中断服务程序的第二部分,它主要处理一些相对耗时的任务。由于中断上文需要尽快完成,因此中断下文负责处理那些不能立即完成的、需要更多时间的任务。这些任务可能包括复杂的计算、访问外部设备或进行长时间的数据处理等。
一个完整的中断子系统框架可以分为四个层次,由上到下分别为用户层、通用层、硬件相关层和硬件层,每个层相关的介绍如下(图39-2)所示:
用户层:用户层是中断的使用者,主要包括各类设备驱动。这些驱动程序通过中断相关的接口进行中断的申请和注册。当外设触发中断时,用户层驱动程序会进行相应的回调处理,执行特定的操作。
通用层:通用层也可称为框架层,它是硬件无关的层次。通用层的代码在所有硬件平台上都是通用的,不依赖于具体的硬件架构或中断控制器。通用层提供了统一的接口和功能,用于管理和处理中断,使得驱动程序能够在不同的硬件平台上复用。
硬件相关层:硬件相关层包含两部分代码。一部分是与特定处理器架构相关的代码,比如ARM64处理器的中断处理相关代码。这些代码负责处理特定架构的中断机制,包括中断向量表、中断处理程序等。另一部分是中断控制器的驱动代码,用于与中断控制器进行通信和配置。这些代码与具体的中断控制器硬件相关。
硬件层:硬件层位于最底层,与具体的硬件连接相关。它包括外设与SoC(系统片上芯片)的物理连接部分。中断信号从外设传递到中断控制器,由中断控制器统一管理和路由到处理器。硬件层的设计和实现决定了中断信号的传递方式和硬件的中断处理能力。
图 39-2
本小节的重点会聚集在硬件层各部分的详细讲解以及用户层编写驱动程序所用到的接口函数。
中断控制器GIC(Generic Interrupt Controller)是中断子系统框架硬件层中的一个关键组件,用于管理和控制中断。它接收来自各种中断源的中断请求,并根据预先配置的中断优先级、屏蔽和路由规则,将中断请求分发给适当的处理器核心或中断服务例程。
GIC是由ARM公司提出设计规范,当前有四个版本,GIC V1-v4。设计规范中最常用的,有3个版本V2.0、V3.1、V4.1,GICv3版本设计主要运行在Armv8-A, Armv9-A等架构上。ARM公司并给出一个实际的控制器设计参考,比如GIC-400(支持GIC v2架构)、gic500(支持GIC v3架构)、GIC-600(支持GIC v3和GIC v4架构)。最终芯片厂商可以自己实现GIC或者直接购买ARM提供的设计。
每个GIC版本及相应特性如下表(表 39-3)所示:
版本 |
关键特性 |
常用核心 |
---|---|---|
GICv1 |
-支持最多八个处理器核心(PE) - 支持最多1020个中断ID |
ARM Cortex-A5 MPCore ARM Cortex-A9 MPCore ARM Cortex-R7 MPCore |
GICv2 |
- GICv1的所有关键特性 -支持虚拟化 |
ARM Cortex-A7 MPCore ARM Cortex-A15 MPCore ARM Cortex-A53 MPCore ARM Cortex-A57 MPCore |
GICv3 |
- GICv2的所有关键特性 -支持超过8个处理器核心 -支持基于消息的中断 -支持超过1020个中断ID - CPU接口寄存器的系统寄存器访问 -增强的安全模型,分离安全和非安全的Group 1中断 |
ARM Cortex-A53MPCore ARM Cortex-A57MPCore ARM Cortex-A72 MPCore |
GICv4 |
- GICv3的所有关键特性 -虚拟中断的直接注入 |
ARM Cortex-A53 MPCore ARMCortex-A57MPCore ARM Cortex-A72 MPCore |
表 39-3
在RK3568上使用的GIC版本为GICv3,相应的中断控制器模型如下(图 39-4)所示:
图 39-4
GIC中断控制器可以分为Distributor接口、Redistributor接口和CPU接口,下面是每个部分的说明:
Distributor中断仲裁器:
包含影响所有处理器核心中断的全局设置。包含以下编程接口:
●启用和禁用SPI。
●设置每个SPI的优先级级别。
●每个SPI的路由信息。
●将每个SPI设置为电平触发或边沿触发。
●生成基于消息的SPI。
●控制SPI的活动和挂起状态。
●用于确定在每个安全状态中使用的程序员模型的控制(亲和性路由或遗留模型)。
Redistributor重新分配器:
对于每个连接的处理器核心(PE),都有一个重新分配器(Redistributor)。重新分配器提供以下编程接口:
●启用和禁用SGI(软件生成的中断)和PPI(处理器专用中断)。
●设置SGI和PPI的优先级级别。
●将每个PPI设置为电平触发或边沿触发。
●将每个SGI和PPI分配给一个中断组。
●控制SGI和PPI的状态。
●对支持关联LPI(低功耗中断)的中断属性和挂起状态的内存中的数据结构进行基址控制。
●支持与连接的处理器核心的电源管理。
CPU接口:
每个重新分配器都连接到一个CPU接口。CPU接口提供以下编程接口:
●通用控制和配置,用于启用中断处理。
●确认中断。
●执行中断的优先级降低和停用。
●为处理器核心设置中断优先级屏蔽。
●定义处理器核心的抢占策略。
●确定处理器核心最高优先级的挂起中断。
GIC-V3支持四种类型的中断,分别是SGI、PPI、SPI和LPI,每个中断类型的介绍如下:
SGI(Software Generated Interrupt,软件生成中断):SGI 是通过向 GIC 中的 SGI 寄存器写入来生成的中断。它通常用于处理器之间的通信,允许一个 PE 发送中断给一个或多个指定的 PE,中断号ID0 - ID15用于SGI。
PPI(Private Peripheral Interrupt,私有外设中断):针对特定 PE 的外设中断。不与其他 PE 共享,中断号ID16 - ID31用于PPI。
SPI(Shared Peripheral Interrupt,共享外设中断):全局外设中断,可以路由到指定的处理器核心(PE)或一组 PE,它允许多个 PE 接收同一个中断。中断号ID32 - ID1019用于SPI,
LPI(Locality-specific Peripheral Interrupt,特定局部外设中断):LPI 是 GICv3 中引入的一种中断类型,与其他类型的中断有几个不同之处。LPI 总是基于消息的中断,其配置存储在内存表中,而不是寄存器中。
INTID范围 |
中断类型 |
备注 |
0 - 15 |
SGI(软件生成中断) |
每个核心分别存储 |
16 - 31 |
PPI(私有外设中断) |
每个核心分别存储 |
32 - 1019 |
SPI(共享外设中断) |
|
1020 - 1023 |
特殊中断号 |
用于表示特殊情况 |
1024 - 8191 |
保留 |
|
8192及更大 |
LPI(特定局部外设中断) |
上限由实现定义 |
中断处理的状态机如下图(图 39-6)所示:
图 39-6
Inactive(非活动状态):中断源当前未被触发。
Pending(等待状态):中断源已被触发,但尚未被处理器核心确认。
Active(活动状态):中断源已被触发,并且已被处理器核心确认。
Active and Pending(活动且等待状态):已确认一个中断实例,同时另一个中断实例正在等待处理。
每个外设中断可以是以下两种类型之一:
边沿触发(Edge-triggered):
这是一种在检测到中断信号上升沿时触发的中断,然后无论信号状态如何,都保持触发状态,直到满足本规范定义的条件来清除中断。
电平触发(Level-sensitive):
这是一种在中断信号电平处于活动状态时触发的中断,并且在电平不处于活动状态时取消触发。
在linux 内核中,我们使用IRQ number和HW interrupt ID两个ID来标识一个来自外设的中断:
IRQ number:CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断。
HW interrupt ID:对于GIC中断控制器而言,它收集了多个外设的interrupt request line并向上传递,因此,GIC中断控制器需要对外设中断进行编码。GIC中断控制器用HW interrupt ID来标识外设的中断。如果只有一个GIC中断控制器,那IRQ number和HW interrupt ID是可以一一对应的,如下图(图 39-7)所示:
图 39-7
但如果是在GIC中断控制器级联的情况下,仅仅用HW interrupt ID就不能唯一标识一个外设中断,还需要知道该HW interrupt ID所属的GIC中断控制器(HW interrupt ID在不同的Interrupt controller上是会重复编码的)。
图 39-8
这样,CPU和中断控制器在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和CPU视角是一样的,我们只希望得到一个IRQ number,而不关系具体是那个GIC中断控制器上的那个HW interrupt ID。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel中的中断子系统需要提供一个将HW interrupt ID映射到IRQ number上来的机制,也就是irq domain。
request_irq 函数是在 Linux 内核中用于注册中断处理程序的函数。它用于请求一个中断号(IRQ number)并将一个中断处理程序与该中断关联起来。下面是对 request_irq 函数的详细介绍:
函数原型:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
头文件:
#include
函数作用:
request_irq 函数的主要功能是请求一个中断号,并将一个中断处理程序与该中断号关联起来。当中断事件发生时,与该中断号关联的中断处理程序会被调用执行。
参数含义:
irq:要请求的中断号(IRQ number)。
handler:指向中断处理程序的函数指针。
flags:标志位,用于指定中断处理程序的行为和属性,如中断触发方式、中断共享等。
name:中断的名称,用于标识该中断。
dev:指向设备或数据结构的指针,可以在中断处理程序中使用。
返回值:
成功:0 或正数,表示中断请求成功。
失败:负数,表示中断请求失败,返回的负数值表示错误代码。
irq参数用来指定要请求的中断号,中断号需要通过gpio_to_irq 函数映射 GPIO 引脚来获得(gpio_to_irq 函数接下来会进行介绍)。
irq_handler_t handler参数是一个函数指针,指向了中断处理程序的函数。中断处理程序是在中断事件发生时调用的函数,用于处理中断事件(关于中断处理程序会在下个小节进行详细的讲解)。
unsigned long flags:中断处理程序的标志位
这个参数用于指定中断处理程序的行为和属性,如中断触发方式、中断共享等。可以使用不同的标志位进行位运算来组合多个属性。常用的标志位包括:
IRQF_TRIGGER_NONE:无触发方式,表示中断不会被触发。
IRQF_TRIGGER_RISING:上升沿触发方式,表示中断在信号上升沿时触发。
IRQF_TRIGGER_FALLING:下降沿触发方式,表示中断在信号下降沿时触发。
IRQF_TRIGGER_HIGH:高电平触发方式,表示中断在信号为高电平时触发。
IRQF_TRIGGER_LOW:低电平触发方式,表示中断在信号为低电平时触发。
IRQF_SHARED:中断共享方式,表示中断可以被多个设备共享使用。
gpio_to_irq 函数用于将 GPIO 引脚的编号(GPIO pin number)转换为对应的中断请求号(interrupt request number)。
函数原型:
unsigned int gpio_to_irq(unsigned int gpio);
头文件:
#include
函数功能:
gpio_to_irq 是一个用于将 GPIO 引脚映射到对应中断号的函数。它的作用是根据给定的 GPIO 引脚号,获取与之关联的中断号。
参数说明:
gpio:要映射的 GPIO 引脚号。
返回值:
成功:返回值为该 GPIO 引脚所对应的中断号。
失败:返回值为负数,表示映射失败或无效的 GPIO 引脚号。
free_irq 函数用于释放之前通过 request_irq 函数注册的中断处理程序。它的作用是取消对中断的注册并释放相关的系统资源。下面是关于该函数的详细解释:
函数原型:
void free_irq(unsigned int irq, void *dev_id);
头文件:
#include
函数功能:
free_irq 函数用于释放之前通过 request_irq 函数注册的中断处理程序。它会取消对中断的注册并释放相关的系统资源,包括中断号、中断处理程序和设备标识等。
参数说明:
irq:要释放的中断号。
dev_id:设备标识,用于区分不同的中断请求。它通常是在 request_irq 函数中传递的设备特定数据指针。
返回值:
free_irq 函数没有返回值。
中断处理程序是在中断事件发生时自动调用的函数。它负责处理与中断相关的操作,例如读取数据、清除中断标志、更新状态等。
irqreturn_t handler(int irq, void *dev_id) 是一个典型的中断服务函数的函数原型。下面对该函数原型及其参数进行详细解释:
函数原型:
irqreturn_t handler(int irq, void *dev_id);
函数功能:
handler 函数是一个中断服务函数,用于处理特定中断事件。它在中断事件发生时被操作系统或硬件调用,执行必要的操作来响应和处理中断请求。
参数说明:
irq:表示中断号或中断源的标识符。它指示引发中断的硬件设备或中断控制器。
dev_id:是一个 void 类型的指针,用于传递设备特定的数据或标识符。它通常用于在中断处理程序中区分不同的设备或资源。
返回值:
irqreturn_t 是一个特定类型的枚举值,用于表示中断服务函数的返回状态。它可以有以下几种取值:
IRQ_NONE:表示中断服务函数未处理该中断,中断控制器可以继续处理其他中断请求。
IRQ_HANDLED:表示中断服务函数已成功处理该中断,中断控制器无需进一步处理。
IRQ_WAKE_THREAD:表示中断服务函数已处理该中断,并且请求唤醒一个内核线程来继续执行进一步的处理。这在一些需要长时间处理的中断情况下使用。
在处理程序中,通常需要注意以下几个方面:
(1)处理程序应该尽可能地快速执行,以避免中断丢失或过多占用 CPU 时间。
(2)如果中断源是共享的,处理程序需要处理多个设备共享同一个中断的情况。
(3)处理程序可能需要与其他部分的代码进行同步,例如访问共享数据结构或使用同步机制来保护临界区域。
(4)处理程序可能需要与其他线程或进程进行通信,例如唤醒等待的线程或发送信号给其他进程。
本实验对应的网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\30_interrupt\03_中断驱动例程。
本实验将实现注册显示屏触摸中断,每按当触摸LCD显示屏就会触发中断服务函数,在中断服务函数中会打印申请的GPIO号和This is irq_handler。
iTOP-RK3568有 5 组 GPIO bank:GPIO0~GPIO4,每组又以 A0~A7, B0~B7, C0~C7, D0~D7 作为编号区分,常用以下公式计算引脚:
GPIO pin脚计算公式:pin = bank * 32 + number //bank为组号,number为小组编号
GPIO 小组编号计算公式:number = group * 8 + X
LCD触摸屏对应的中断引脚标号为TP_INT_L_GPIO3_A5,对应的计算过程如下所示:
bank = 3; //GPIO3_A5=> 3, bank ∈ [0,4]
group = 0; //GPIO3_A5 => 0, group ∈ {(A=0), (B=1), (C=2), (D=3)}
X = 5; //GPIO3_A5 => 5, X ∈ [0,7]
number = group * 8 + X = 0 * 8 + 5 =5
pin = bank*32 + number= 3 * 32 + 5 = 101;
得到中断引脚的引脚标号后,下面开始编写对应的驱动程序,编写完成的interrupt.c如下所示:
#include
#include
#include
#include
#define GPIO_PIN 101
// 中断处理函数
static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
printk(KERN_INFO "Interrupt occurred on GPIO %d\n", GPIO_PIN);
printk(KERN_INFO "This is irq_handler\n");
return IRQ_HANDLED;
}
static int __init interrupt_init(void)
{
int irq_num;
printk(KERN_INFO "Initializing GPIO Interrupt Driver\n");
// 将GPIO引脚映射到中断号
irq_num = gpio_to_irq(GPIO_PIN);
printk(KERN_INFO "GPIO %d mapped to IRQ %d\n", GPIO_PIN, irq_num);
// 请求中断
if (request_irq(irq_num, gpio_irq_handler, IRQF_TRIGGER_RISING, "irq_test", NULL) != 0){
printk(KERN_ERR "Failed to request IRQ %d\n", irq_num);
// 请求中断失败,释放GPIO引脚
gpio_free(GPIO_PIN);
return -ENODEV;
}
return 0;
}
static void __exit interrupt_exit(void)
{
int irq_num = gpio_to_irq(GPIO_PIN);
// 释放中断
free_irq(irq_num, NULL);
printk(KERN_INFO "GPIO Interrupt Driver exited successfully\n");
}
module_init(interrupt_init);
module_exit(interrupt_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");
在上一小节中的timer_mod.c代码同一目录下创建 Makefile 文件,Makefile 文件内容如下所示:
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m +=interrupt.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
对于Makefile的内容注释已在上图添加,保存退出之后,来到存放interrupt.c和Makefile文件目录下,如下图(图 39-9)所示:
图 39-9
然后使用命令“make”进行驱动的编译,编译完成如下图(图 39-10)所示:
图 39-10
编译完生成 interrupt.ko目标文件,如下图(图 39-11)所示:
至此驱动模块就编译成功了。
开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图 39-12)所示:
insmod interrupt.ko
图 39-12
可以看到驱动加载之后,打印了“Initializing GPIO Interrupt Driver”表示程序加载成功了,在后面又打印了gpio映射后的中断请求号为113,然后触摸LCD屏,触发中断服务程序,打印如下图(图 39-13)所示:
图 39-13
成功打印了GPIO的引脚编号以及“This is irq_handler”,证明编写的驱动程序没有问题,最后使用以下命令卸载相应的驱动,如下图(图 39-14)所示:
rmmod interrupt.ko
【RK3568 购买链接】
https://item.taobao.com/item.htm?spm=a1z10.5-c-s.w4002-2245
2452613.11.2fec74a6elWNeA&id=669939423234
【公众号】迅为电子
【粉丝交流群8】:824412014(进群获取驱动最新资料)