一起来学习 LiteOS 中断模块的源代码
摘要:本文带领大家一起剖析了 LiteOS 中断模块的源代码。
本文我们来一起学习下 LiteOS 中断模块的源代码,文中所涉及的源代码,均可以在 LiteOS 开源站点https://gitee.com/LiteOS/LiteOS 获取。中断源代码、开发文档,示例程序代码如下:
LiteOS 内核中断源代码
包括中断模块的私有头文件 kernel\base\include\los_hwi_pri.h、头文件 kernel\include\los_hwi.h、C 源代码文件 kernel\base\los_hwi.c。
中断控制器实现代码
开源 LiteOS 支持的中断控制器有通用中断控制器 GIC(General Interrupt Controller)、嵌套向量中断控制器 NVIC(Nested Vectored Interrupt Controller),本文以 STM32F769IDISCOVERY 为例,分析一下适用于 Cortex-M 核的 NVIC。各中断控制器的源代码包含头文件、源文件,代码路径如下:https://gitee.com/LiteOS/LiteOS/blob/master/targets/bsp/hw/include/、https://gitee.com/LiteOS/LiteOS/blob/master/targets/bsp/hw/arm/interrupt/gic/、https://gitee.com/LiteOS/LiteOS/blob/master/targets/bsp/hw/arm/interrupt/nvic/。
开关中断汇编实现代码
开关中断的函数 UINT32 ArchIntLock(VOID)、UINT32 ArchIntUnlock(VOID)、ArchIntRestore(UINT32 intSave)是基于汇编语言实现的,根据不同的 CPU 架构,分布在下述文件里: arch\arm\cortex_a_r\include\arch\interrupt.h、arch\arm64\include\arch\interrupt.h、arch\arm\cortex_m\src\dispatch.S。
开发指南中断文档
在线文档https://gitee.com/LiteOS/LiteOS/blob/master/doc/LiteOS_Kernel_Developer_Guide.md#%E4%B8%AD%E6%96%AD。
我们先看看中断的相关概念,详细的介绍,请参考 LiteOS 开发指南中断文档。
1、中断概念介绍
中断是指出现需要时,CPU 暂停执行当前程序,转而执行新程序的过程。当外设需要 CPU 时,将通过产生中断信号使 CPU 立即中断当前任务来响应中断请求。在剖析中断源代码之前,下面介绍些中断相关的硬件、中断相关的概念。
1.1 中断相关的硬件介绍
与中断相关的硬件可以划分为三类:设备、中断控制器、CPU 本身。
设备
发起中断的源,当设备需要请求 CPU 时,产生一个中断信号,该信号连接至中断控制器。
中断控制器
中断控制器是 CPU 众多外设中的一个,它一方面接收其它外设中断引脚的输入。另一方面,它会发出中断信号给 CPU。可以通过对中断控制器编程来打开和关闭中断源、设置中断源的优先级和触发方式。
CPU
CPU 会响应中断源的请求,中断当前正在执行的任务,转而执行中断处理程序。
1.2 中断相关的概念
中断号
每个中断请求信号都会有特定的标志,使得计算机能够判断是哪个设备提出的中断请求,这个标志就是中断号。
中断优先级
为使系统能够及时响应并处理所有中断,系统根据中断时间的重要性和紧迫程度,将中断源分为若干个级别,称作中断优先级。
中断处理程序
当外设产生中断请求后,CPU 暂停当前的任务,转而响应中断申请,即执行中断处理程序。产生中断的每个设备都有相应的中断处理程序。
中断向量
中断服务程序的入口地址 。
中断向量表
存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量表中按照中断号顺序存储。
中断共享
当外设较少时,可以实现一个外设对应一个中断号,但为了支持更多的硬件设备,可以让多个设备共享一个中断号,共享同一个中断号的中断处理程序形成一个链表。当外部设备产生中断申请时,系统会遍历中断号对应的中断处理程序链表,直到找到对应设备的中断处理程序。在遍历执行过程中,各中断处理程序可以通过检测设备 ID,判断是否是这个中断处理程序对应的设备产生的中断。
我们再看看 LiteOS 内核中断源代码。
2、LiteOS 内核中断源代码
2.1 中断相关的结构体
在文件 kernel\base\include\los_hwi_pri.h 中定义了 2 个结构体,HwiHandleInfo 和 HwiControllerOps。HwiHandleInfo 结构体记录中断处理程序的相关信息,包括中断处理程序、中断共享模式或中断处理程序参数、中断处理程序执行的次数等。开启共享中断时,还包含指向下一个中断处理程序结构体的链表指针,如⑴所示。中断控制器操作项结构体 HwiControllerOps 包括中断操作相关的函数,如触发中断、清除中断、使能中断、失能中断、设置中断优先级、获取当前中断号、获取中断版本、根据中断号获取中断处理程序信息、处理调用中断程序。对于 SMP 多核,还包括设置中断 CPU 亲和性,发送核间中断等函数,如⑵所示。
kernel\include\los_hwi.h 定义的结构体 HWI_IRQ_PARAM_S,用于处理中断处理程序的参数,包含中断号、设备 Id、中断名称等。在创建、删除中断时,需要提供这个参数。
2.2 中断初始化 OsHwiInit()
在系统启动时,在 kernel\init\los_init.c 中调用 OsHwiInit()进行中断初始化。这个函数定义在 kernel\base\los_hwi.c,然后进一步调用定义在 targets\bsp\hw\arm\interrupt\nvic\nvic.c 文件中 HalIrqInit()函数完成中断向量初始化,并调用 OsHwiControllerReg()注册中断控制器操作项。在下文分析 NVIC 时再分析该函数 HalIrqInit()。
2.3 创建中断 UINT32 LOS_HwiCreate()
开发者可以调用函数 UINT32 LOS_HwiCreate()创建中断,注册中断处理程序。我们先看看这个函数的参数,HWI_HANDLE_T hwiNum 是硬件中断号,HWI_PRIOR_T hwiPrio 中断的优先级,HWI_MODE_T hwiMode 中断模式,可以用来标记是否共享中断。HWI_PROC_FUNC hwiHandler 是需要注册的中断处理程序,中断被触发后会调用这个函数。HWI_IRQ_PARAM_S *irqParam 是中断处理程序的参数。
一起剖析下这个函数的源代码,⑴处代码获取对应中断号 hwiNum 的中断处理程序信息,其中 g_hwiOps 是中断初始化时注册的中断控制器操作项,后文分析 NVIC 时会分析该中断控制器操作项。⑵、⑶处根据是否开启了共享中断 LOSCFG_NO_SHARED_IRQ 来分别配置创建中断。⑷处在创建中断成功,且支持配置中断优先级时,调用 g_hwiOps->setIrqPriority(hwiNum, hwiPrio)函数设置中断的优先级。
2.3.1 不支持共享中断时创建中断 UINT32 OsHwiCreateNoShared()
我们先看下没有开启共享中断支持时,如何创建中断。⑴处代码表示如果创建的中断的模式 hwiMode 等于共享模式 IRQF_SHARED,则返回错误 OS_ERRNO_HWI_SHARED_ERROR。⑵处代码判断中断处理程序 hwiForm->hook 是否为空,为空时则把 hwiHandler 赋值赋值给它;hwiForm->hook 不为空则执行⑹,说明中断已经创建过,返回异常 OS_ERRNO_HWI_ALREADY_CREATED;⑷处判断 irqParam 是否为空,如果不为空,则为这个中断处理程序的参数申请内存,设置数组,并赋值给 hwiForm->registerInfo。⑸处表示如果为中断处理程序的参数申请内存失败,则返回异常。
可以结合示意图理解创建非共享中断,对应的中断处理信息表 HwiHandleInfo *hwiForm 结构体示意图如下。创建中断时,除了校验,主要是设置中断处理程序 hwiForm->hook 及其参数 hwiForm->registerInfo。
2.3.2 支持共享时创建中断 UINT32 OsHwiCreateShared()
我们先看下开启共享中断支持时,如何创建创建中断。支持共享中断时,可以创建共享中断,也可以创建非共享的中断,如果⑴处的 modeResult == 1 表示创建的是共享中断,否则是非共享中断。没有开启共享中断支持时,使用 HwiHandleInfo *hwiForm 单节点维护中断处理程序信息,开启共享中断支持时,需要使用 HwiHandleInfo *hwiForm 单链表来维护中断处理程序信息。
首先做些基础的参数校验,⑵处表示,如果创建的是共享中断,但是参数 irqParam 或参数的设备 Id 为空,返回错误 OS_ERRNO_HWI_SHARED_ERROR。也就是说,共享模式 hwiMode 和中断处理程序的参数 irqParam 是必须的,因为需要指定特定的设备来共享同一个中断号。⑶处判断表示,如果 head->next 不为空,说明此中断号已经有其他设备使用了,该中断号需要被不同的设备共享,此时如果 hwiMode 或 head->shareMode 不是共享模式,则返回错误。⑷处 while 循环代码处理此中断号已经有其他设备使用的情况,依次循环中断处理信息表的单链表,判断是否已经存在同一个设备已经注册的情况,如果存在则返回错误 OS_ERRNO_HWI_ALREADY_CREATED。循环完毕之后,hwiForm 为链表中的最后一个设备节点。如果创建的不是共享中断,循环条件不满足,此处代码就不会执行。
做完参数校验后,⑸处为一个 HwiHandleInfo 节点申请内存,申请的这个节点是中断的设备节点,管理具体设备的中断处理程序信息,参数列表中的节点 HwiHandleInfo *head 是中断头节点,只标记是不是共享中断,可以参考下文的示意图,来加深理解。如果申请失败则返回错误 OS_ERRNO_HWI_NO_MEMORY;申请成功继续执行后面的语句,把 hwiForm->respCount 赋值为 0,表示中断处理程序还没有执行过。⑹处代码判断参数 irqParam,不为空时申请内存空间保存参数,否则参数赋值为 0。⑺处把新增设备的中断处理程序赋值给 hwiFormNode->hook,然后把新增的节点挂载链表的尾部。⑻处表示更新头节点的共享模式。
配置完毕中断后,对应的中断处理信息表 HwiHandleInfo *hwiForm 示意图如下:
可以结合示意图来理解在开启共享中断支持时,如何创建中断。HwiHandleInfo *hwiForm 结构体单链表示意图如下。创建非共享中断时,只需要两个节点,左侧头结点中的 head->shareMode 等于 0,表示非共享中断,右侧一个设备节点,表示指定设备的中断处理程序,包含中断处理程序 hwiForm->hook 及其参数 hwiForm->registerInfo,hwiForm->next 为空。创建共享中断时,至少两个节点,左侧头结点中的 head->shareMode 等于 1,表示共享中断,右侧一到多个设备节点,表示不同设备的中断处理程序,包含中断处理程序 hwiForm->hook 及其参数 hwiForm->registerInfo,hwiForm->next 依次指向下一个设备的节点,最后一个节点的 hwiForm->next 为空。
2.4 删除中断 UINT32 LOS_HwiDelete()
中断删除操作是创建操作的反向操作,也比较好理解。开发者可以调用函数 UINT32 LOS_HwiDelete(HWI_HANDLE_T hwiNum, HWI_IRQ_PARAM_S *irqParam)删除中断。我们先看看这个函数的参数,HWI_HANDLE_T hwiNum 是硬件中断号,HWI_IRQ_PARAM_S *irqParam 是中断处理程序的参数,如果开启了共享中断需要该参数来删除指定设备的中断信息。
一起剖析下这个函数的源代码,⑴处代码获取对应中断号 hwiNum 的中断处理程序信息,其中 g_hwiOps 是中断初始化时注册的中断控制器操作项。⑵、⑶处根据是否开启了共享中断 LOSCFG_NO_SHARED_IRQ 来分别调用相应的函数删除中断。
没有开启共享中断支持时,删除中断调用 OsHwiDelNoShared(hwiForm)函数,中断处理程序 hwiForm->hook,释放内存等,代码简单,读者自行阅读。我们主要来剖析下支持共享中断时,是如何调用 UINT32 OsHwiDelShared(HwiHandleInfo *head, const HWI_IRQ_PARAM_S *irqParam)删除中断的。
2.4.1 支持共享时删除中断 UINT32 OsHwiDelShared()
我们来分析下开启共享中断支持时,删除中断的源代码流程是什么样的。⑴处表示,如果删除的是共享中断,但是参数 irqParam 或参数的设备 Id 为空,返回错误 OS_ERRNO_HWI_SHARED_ERROR。⑵处表示删除的是非共享中断,如果 wiForm->registerInfo 不为空,则释放参数占用的内存,然后释放设备节点占用的内存,并把 head->next 置空。
⑶处代码处理如何删除共享中断。⑷处 while 循环对中断程序信息链表上的设备节点进行遍历。⑸处表示如果没有匹配到要删除的设备,则继续遍历下一个设备节点。⑹处表示匹配到要删除的设备中断处理程序节点,则释放参数的内存、释放设备节点内存,并执行⑺,标记匹配到设备节点,然后跳出循环。⑻处,如果循环遍历完毕,还没有匹配到设备节点,则返回错误。⑼处表示如果删除设备节点后,只有一个头结点,则把共享模式改为非共享。
2.5 中断控制器操作项函数
其他操作代码结构非常类似,以 UINT32 LOS_HwiEnable(HWI_HANDLE_T hwiNum)为例剖析一下,执行⑴处代码,调用 NVIC 中定义的中断控制器操作项 g_hwiOps->enableIrq 对应的函数 HalIrqUnmask(),然后调用 arch\arm\cortex_m\cmsis\core_cm7.h 中的 NVIC_EnableIRQ()函数,CMSIS 中的代码以后专门分析,此处不再深入剖析。
中断控制器操作项中,其他的各个函数调用对应关系如下:
其中,第 7 行获取中断号的函数在 arch\arm\cortex_m\cmsis\cmsis_armcc.h 文件中定义的 uint32_t __get_IPSR(void)。
第 9-11 行,只适用于 SMP 多核,在 NVIC 中不支持。
2.6 中断的其他操作
我们再来看看其他常用的函数操作。
2.6.1 IntActive()判断是否有处理中的中断
其他模块中经常使用 OS_INT_ACTIVE 或 OS_INT_INACTIVE 来判断是否在处理中断,下文源代码⑴处的数组 g_intCount[ArchCurrCpuid()]表示各个 CPU 核中正在处理的中断的数目。返回值大于 1,则表示正在处理中断。
2.6.2 中断处理函数 VOID OsIntHandle()
在 LiteOS 归一化内核中,VOID OsIntEntry(VOID)中断程序处理入口函数适用于 arm(cortex-a/r)/arm64 平台,VOID OsIntHandle(UINT32 hwiNum, HwiHandleInfo *handleForm)适用于 arm(cortex-m), xtensa, riscv 等平台,我们主要来剖析下后者。
⑴处获取全局变量指针 &g_intCount[ArchCurrCpuid()],然后把它表示的中断数量加 1,在中断执行完毕后,在⑸处再把活跃的中断数量减 1。
⑵、⑷处代码在开启中断嵌套、中断抢占 LOSCFG_ARCH_INTERRUPT_PREEMPTION 时,在执行中断处理程序时,需要开、关中断,具体的代码就是调用
LOS_IntUnLock()、LOS_IntLock(),读者可以访问 LiteOS 开源站点自行查看。⑶处代码调用 InterruptHandle(hwiForm)来执行中断处理程序,继续往下看来剖析这个函数。
我们来具体看下这个函数 VOID InterruptHandle(HwiHandleInfo *hwiForm),⑴处把中断处理程序的执行次数加 1。⑵处代码处理共享中断的情况,会依次循环中断号对应的中断程序信息链表,一个中断号对应的中断发生时,所有注册在这个中断后下的中断处理程序都会执行。
⑶处判断中断处理程序的参数是否为空,不为空时执行⑷处代码,获取中断处理程序 HWI_PROC_FUNC2 func,它需要 2 个参数,分别为中断号,设备 Id。然后执行⑸获取中断处理程序的参数,接着执行⑹处代码调用中断处理程序。如果中断处理程序的参数为空,则获取不需要参数的中断处理程序 HWI_PROC_FUNC0 func,然后执行。
我们下来看看 LiteOS 中封装的 NVIC 代码。
3、NVIC 嵌套向量中断控制器代码
NVIC 代码包含一个头文件 targets\bsp\hw\include\nvic.h 和 C 源代码文件 targets\bsp\hw\arm\interrupt\nvic\nvic.c。
3.1 nvic.h 头文件
NVIC 头文件 nvic.h 中,主要定义了一些宏,如中断寄存器的地址、各个系统中断号等,声明了如下三个函数,其中函数 IrqEntryV7M(VOID)在 targets\bsp\hw\arm\interrupt\nvic\nvic.c 文件中定义,用于处理中断的程序,也叫做中断向量;函数 VOID Reset_Handler(VOID)在汇编启动文件 targets\STM32F769IDISCOVERY\los_startup_gcc.S 中定义的,用于复位、启动时的处理;函数 VOID osPendSV(VOID)定义在 arch\arm\cortex_m\src\dispatch.S,处理 PendSV 异常。
3.2 nvic.c 源代码文件
NVIC 源代码文件 nvic.c 中,定义中断向量函数、中断初始化函数。我们一起学习下源代码。
⑴、⑵处代码为系统支持的中断定义了 2 个数组,对于每一个中断号 hwiNum,对应的数组元素 g_hwiForm[hwiNum]表示每一个中断对应的中断处理程序的相关信息,g_hwiVec[hwiNum]表示中断发生时需要执行的程序,该程序也叫中断向量,这个数组有时候也叫做中断向量表。⑵处代码只定义了 16 个系统中断号对应的中断处理程序,其他在调用中断初始化函数 VOID HalIrqInit(VOID)时指定。其中 1 号中断对应复位处理程序 Reset_Handler,14 号中断对应 osPendSV 处理程序,15 号中断是 tick 中断,还有些系统保留的中断号。在 LiteOS 内核里对中断处理进行接管,当中断发生,执行的中断处理函数在⑶处定义,这个函数会进一步调用用户定义的中断处理函数。
逐行分析下 VOID IrqEntryV7M(VOID)函数,⑷处通过读取 ipsr 寄存器获取中断号,__get_IPSR()定义在文件 arch\arm\cortex_m\cmsis\cmsis_armcc.h。然后,根据中断号从中断处理程序信息表中获取 &g_hwiForm[hwiIndex],作为参数传递给函数 VOID OsIntHandle(UINT32 hwiNum, HwiHandleInfo *hwiForm)进一步处理,该函数上文已经分析过。
继续分析代码,一起看下中断初始化做了些什么,中断控制器操作项有哪些。⑴处代码定义的函数 VOID HalIrqPending(UINT32 hwiNum)会请求触发指定中断号的中断处理程序,先对中断号进行校验,确保中断号合法,减去系统中断数量 OS_SYS_VECTOR_CNT,然后调用⑵处代码执行定义在 arch\arm\cortex_m\cmsis\core_cm7.h 文件内的函数 NVIC_SetPendingIRQ((IRQn_Type)hwiNum),请求触发中断。接着,这个函数 VOID HalIrqPending(UINT32 hwiNum)在执行⑶处代码时赋值给 NVIC 的中断控制器操作项 g_nvicOps 的结构体成员.triggerIrq。除了触发请求中断,还有使能中断、失能中断、设置中断优先级,获取当前中断号,获取中断版本,获取中断处理信息表等函数。这些 HalXXXX 函数,格式差不多,区别在调用不同的 NVIC_XXX()函数,不再一一分析。
我们再看看中断初始化函数,⑷处会把系统支持的系统中断以后的中断号对应的中断处理程序都初始化为(HWI_PROC_FUNC)IrqEntryV7M 这个中断接管函数。如果是 Cortex-M0 核,执行⑸处代码,使能时钟,重新映射 SRAM 内存,对于其他核,执行⑹处代码把中断向量表赋值给 SCB->VTOR。对于 Cortex-M3 及以上的 CPU 核,还需要执行⑺设置优先级组。最后,调用定义在 kernel\base\los_hwi.c 里的函数 VOID OsHwiControllerReg(const HwiControllerOps *ops)注册中断控制器操作项,这样 LiteOS 的中断处理程序就可以调用 NVIC 里定义的中断相关的操作。
4、开关中断
最后,分享下开关中断的相关知识,开、关中断分别指的是:
开中断
执行完毕特定的短暂的程序,打开中断,可以响应中断了。
关中断
为了保护执行的程序不被打断,关闭相应外部的中断。
对应的开关中断的函数定义在 kernel\include\los_hwi.h:
ArchIntLock(),ArchIntUnlock()这些基于汇编语言实现的,读者们自行阅读查看。我们看下开关断函数的使用场景。⑴处的 UINT32 LOS_IntLock(VOID)会关闭所有的中断,与之对应,⑵处的函数 UINT32 LOS_IntUnLock(VOID)会使能所有的中断。⑶处的函数 VOID LOS_IntRestore(UINT32 intSave)可以用来恢复 UINT32 LOS_IntLock(VOID)函数关闭的中断,UINT32 LOS_IntLock(VOID)的返回值作为 VOID LOS_IntRestore(UINT32 intSave)的参数进行恢复中断。
小结
本文带领大家一起剖析了 LiteOS 中断模块的源代码,结合讲解,参考官方示例程序代码,自己写写程序,实际编译运行一下,加深理解。
感谢阅读,如有任何问题、建议,都可以留言给我们: https://gitee.com/LiteOS/LiteOS/issues 。为了更容易找到 LiteOS 代码仓,建议访问 https://gitee.com/LiteOS/LiteOS ,关注 Watch、点赞 Star、并 Fork 到自己账户下,如下图,谢谢。
本文分享自华为云社区《LiteOS 内核源码分析系列三 中断 Hwi》,原文作者:zhushy。
版权声明: 本文为 InfoQ 作者【华为云开发者社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/642f580d6dcb0ea7d87a191b9】。文章转载请联系作者。
评论