深入剖析Cortex-M中断

描述

在嵌入式系统开发中,中断是十分重要的知识点,在大部分单片机构建的应用产品中,基本都是以前后台方式(大循环加中断)的方式来实现功能,在主循环中处理应用,并在中断中处理外部的触发信号,以及对响应时间有要求的应用,如用于时间相关处理的定时器中断,对按键响应的外部中断,用于通讯的收发和异常处理的串口中断,SPI中断, 网络中断等。另外,对于大部分RTOS来说,如Cortex-M系统中的systick中断和PendSV中断,又是实现基于队列和任务调度算法的RTOS的核心。

1 异常类型

在单片机开发中,对于中断的表示方法也因为内核的不同有很大的差异,如51使用中断号来表示指定中断,而ARM Cortex-M内核中则使用中断向量表的方式配合内核中的NVIC控制器来实现中断的处理,不过考虑到目前的主流单片机方案,因此以典型的Cortex-M3内核说明单片机中的中断控制机制, 另外Cortex-M系列中的其它内核中的中断流程也基本一致。

Cortex-M3内核支持256个中断,其中 16个内核中断和240个外部中断,并具有256级可编程中断设置,其中015为系统异常,主要处理系统执行中产生的复位,错误,主动触发的SVC,异常等,编号16255则是由芯片设计厂商自定义设计,用于满足芯片功能需求的中断(芯片厂商可以自由定制,不超过最大编号且不重复即可)。STM32并没有使用Cortex-M3的全部内容,而是使用了一部分。 STM32有84个中断,包括16个内核中断和68个可屏蔽中断,具有16级可编程中断优先级 。STM32F103 在内核水平上搭载了一个异常响应系统, 支持为数众多的系统异常和外部中断。其中系统异常有 8 个(如果把 Reset 和 HardFault 也算上的话就是 10 个), 外部中断有 60个 。除了个别异常的优先级被定死外,其它异常的优先级都是可编程的。

下面以STM32F1举例,有关具体的系统异常和外部中断可在标准库文件 stm32f10x.h 这个头文件查询到,在 IRQn_Type 这个结构体里面包含了Cortex-M的异常声明。系统异常清单见下表。

表1 系统异常清单

单片机

表2 STM32F103 外部中断清单

单片机

反映在软件实现就是在startup_xxx.s启动文件中定义的中断向量表,具体结构如下:

单片机

其中External Interrupts就是由厂商定义的中断类型,另外中断号为0的位置为空,设计上就用来存储堆栈指针。

在芯片在上电的过程中就是执行复位机制根据SCB->VTOR查询向量表,找到Reset_Handler入口,并加载__initial_sp到堆栈指针R13中,后续就可以正常的工作了。

在上述结构中,系统中断是在内核定义时确定的,外部中断在芯片设计时被确定,将中断编号和指定外设的中断触发信号绑定,就构建了完整的中断向量表。

2 中断向量表

中断向量表每一位为一个32bit的地址。每一个地址对应一个中断函数的地址(第一位除外)。除了第一位以外,所有地址的目标都为寻址寄存器(PC)。当相应中断触发时,ARM Cortex-M硬件会自动把中断向量表中相应的中断函数地址装载入寻址寄存器(PC)然后开始执行中断函数。如上所述,前16位为ARM保留的系统中断,建议读者熟记。之后的中断为芯片自定义的外部中断,可以在使用时查询手册或者厂商提供的驱动程序。表中每个向量大小都是 4 字节,除了第 0 个向量外,其余向量都是函数地址,这个表集中保存了系统全部的中断处理函数(xxxIRQHandler)地址。

对于内嵌 Flash 的 MCU 来说,初始中断向量表一般会被要求固定链接到 Flash 起始地址处,因为系统启动总是从 Flash 起始地址获取第 0(初始栈)、1个向量(初始PC,复位函数ResetHandler)来开始应用程序代码的执行。对于一些包含 BootROM 或者没有内部 Flash 的 MCU,初始中断向量表也许可以放到 Flash 中的其他地址处,这要取决于具体芯片设计。

当应用程序执行起来后,如果发生了中断,系统会根据发出请求的外设中断号来中断向量表里找到对应的外设中断响应函数并去执行。Cortex-M 内核(除了CM0)模块 SCB 里有个专门的 VTOR 寄存器用来控制中断向量表首地址(注意,地址需要 128 字节对齐),程序运行起来后用户可以配置 SCB->VTOR 寄存器来重设中断向量表地址。

2.1 中断向量表定义

中断向量表可以通过汇编语言定义也可以通过C语言定义。以下列出两种方式的示例程序。

  • C语言定义

这里我们定义了一个数组,数组的每一项对应相应的中断函数。如上所述,数组的第一项为初始栈指针,第二项为入口函数地址。余下的所有中断都指向了一个通用中断函数。开发者可以根据需求替代相应的中断函数。

上文还提到中断向量表需要放置于闪存起始地址处。这里__attribute__((section(".vectors")))为gcc的特定语法(如果开发者使用IAR或者其他编译器,语法有所不同),目的是告诉编译器在链接所有对象文件(objects)时把_vector[]数组放在链接脚本(linker script)中的.vectors段落(section)。在链接脚本中,.vector段落被定义于闪存起始处。

#ifndef ARMV7M_PERIPHERAL_INTERRUPTS
#  error ARMV7M_PERIPHERAL_INTERRUPTS must be defined to the number of I/O interrupts to be supported
#endif

extern void exception_common(void);
unsigned _vectors[] __attribute__((section(".vectors"))) =
{
  /* Initial stack */
  IDLE_STACK,
  /* Reset exception handler */
  (unsigned)&__start,
  /* Vectors 2 - n point directly at the generic handler */
  [2 ... (15 + ARMV7M_PERIPHERAL_INTERRUPTS)] = (unsigned)&exception_common
};
  • Assembly语言定义

这里以STM32Cube生成的startup_stm32f103xe.s为示例。同样的,汇编程序中也定义了默认中断函数。所有中断也都指向了默认的中断函数Default_Handler(默认为无限循环)。

/**
 * @brief  This is the code that gets called when the processor receives an
 *         unexpected interrupt.  This simply enters an infinite loop, preserving
 *         the system state for examination by a debugger.
 *
 * @param  None
 * @retval : None
*/
    .section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
  b Infinite_Loop
  .size Default_Handler, .-Default_Handler

之后是中断向量表。这里为了缩减示例代码长度,略去了中间的中断函数定义。注意在初始处 .section .isr_vector,"a",%progbits语句指定了g_pfnVectors在链接是需要被放置在isr_vector段落,也就是闪存起始地址处。

/******************************************************************************
*
* The minimal vector table for a Cortex M3.  Note that the proper constructs
* must be placed on this to ensure that it ends up at physical address
* 0x0000.0000.
*
******************************************************************************/
  .section .isr_vector,"a",%progbits
  .type g_pfnVectors, %object
  .size g_pfnVectors, .-g_pfnVectors


g_pfnVectors:

  .word _estack
  .word Reset_Handler
  .word NMI_Handler
  .word HardFault_Handler
  .word MemManage_Handler
  .word BusFault_Handler
  .word UsageFault_Handler
  .word 0
  .word 0
  .word 0
  .word 0
  .word SVC_Handler
  .word DebugMon_Handler
  .word 0
  .word PendSV_Handler
  .word SysTick_Handler
  .word WWDG_IRQHandler
  .word PVD_IRQHandler
……

在中断向量表的定义之后,程序还将所有函数定义为.weak。也就是说如果开发者在其他地方重新定义了同样名称的中断函数,那么默认的中断函数实现会被自动覆盖。weak是GNU GCC编译器定义的关键词,如果采用其他编译器会有对应的关键词。

/*******************************************************************************
*
* Provide weak aliases for each Exception handler to the Default_Handler.
* As they are weak aliases, any function with the same name will override
* this definition.
*
*******************************************************************************/

  .weak NMI_Handler
  .thumb_set NMI_Handler,Default_Handler

  .weak HardFault_Handler
  .thumb_set HardFault_Handler,Default_Handler

  .weak MemManage_Handler
  .thumb_set MemManage_Handler,Default_Handler

  .weak BusFault_Handler
  .thumb_set BusFault_Handler,Default_Handler

  .weak UsageFault_Handler
  .thumb_set UsageFault_Handler,Default_Handler
……

2.2 中断向量表偏移寄存器

ARM Cortex-M默认的中断向量表地址位于闪存起始地址处。但是ARM Cortex-M3/4系列提供了一个中断向量表偏移寄存器(Vector Table Offset Reigster)。系统中中断向量表的位置是0x00000000加上偏移寄存器的值。上电复位后这个寄存器值为0,所以中断向量表默认位于0x00000000闪存起始处。这个寄存器的目的是为了让开发者可以重新设置中断向量表的位置。

中断向量表偏移寄存器第29位(bit 29)定义了中断向量表的位置。0表示位于闪存程序(code)中,1表示位于内存中(SRAM)。

中断向量表寄存器低7位(bit 6~0)为系统预留位。

偏移地址寄存器值有对齐要求。这个要求和系统中断数量或者说中断向量表长度相关。偏移地址寄存器值至少是128 (32words = 128bytes)的整数倍,这也意味着中断偏移寄存器中地址的低7位始终会是0。如果系统中断数量大于16个,则总中断数为ARM预留的16个中断加上n个系统中断。如果(n+2)不为2的指数,则向上找到最近的2的指数m。每个地址为4bytes所以对齐要求为m*4。例如系统有21个中断,加上ARM预留的16个中断位,则中断向量表有效长度为37words。最近的2的指数值为64words = 256bytes。所以偏移寄存器的值必须为256的整数倍。

3 NVIC 简介

在讲如何配置中断优先级之前,我们需要先了解下 NVIC。NVIC 是嵌套向量中断控制器,控制着整个芯片中断相关的功能,它跟内核紧密耦合,是内核里面的一个外设。但是各个芯片厂商在设计芯片的时候会对 Cortex-M内核里面的 NVIC 进行裁剪,把不需要的部分去掉,所以说 STM32 的 NVIC 是 Cortex-M的 NVIC 的一个子集。

3.1 NVIC 寄存器简介

在固件库中, NVIC 的结构体定义可谓是颇有远虑,给每个寄存器都预留了很多位,恐怕为的是日后扩展功能。不过 STM32F103 可用不了这么多,只是用了部分而已,具体使用了多少可参考《Cortex-M3内核编程手册》的NVIC 寄存器映射。

单片机

[ps] NVIC 结构体定义,来自固件库头文件: core_cm3.h。

在配置中断的时候我们一般只用 ISER、 ICER 和 IP 这三个寄存器, ISER 用来使能中断, ICER 用来失能中断,IP 用来设置中断优先级。

3.2 NVIC 中断配置固件库

固件库文件 core_cm3.h 的最后,还提供了 NVIC 的一些函数,这些函数遵循 CMSIS 规则,只要是 Cortex-M3 的处理器都可以使用,具体如下:

表3符合 CMSIS 标准的 NVIC 库函数

NVIC库函数 描述
void NVIC_EnableIRQ(IRQn_Type IRQn) 使能中断
void NVIC_DisableIRQ(IRQn_Type IRQn) 失能中断
void NVIC_SetPendingIRQ(IRQn_Type IRQn) 设置中断悬起位
void NVIC_ClearPendingIRQ(IRQn_Type IRQn) 清除中断悬起位
uint32_t NVIC_GetPendingIRQ(IRQn_Type IRQn) 获取悬起中断编号
void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority) 设置中断优先级
uint32_t NVIC_GetPriority(IRQn_Type IRQn) 获取中断优先级
void NVIC_SystemReset(void) 系统复位

这些库函数我们在编程的时候用的都比较少,甚至基本都不用。

4 优先级的定义

4.1 优先级定义

在 NVIC 有一个专门的寄存器:中断优先级寄存器 NVIC_IPRx,用来配置外部中断的优先级, IPR 宽度为 8bit,原则上每个外部中断可配置的优先级为 0~255,数值越小,优先级越高。但是绝大多数 CM3 芯片都会精简设计,以致实际上支持的优先级数减少,在F103 中,只使用了高 4bit,如下所示:

表4 STM32F103 使用 4bit 表达优先级

单片机

用于表达优先级的这 4bit,又被分组成抢占优先级和子优先级。如果有多个中断同时响应,抢占优先级高的就会 抢占 抢占优先级低的优先得到执行,如果抢占优先级相同,就比较子优先级。如果抢占优先级和子优先级都相同的话,就比较他们的硬件中断编号,编号越小,优先级越高。

4.2 优先级分组

优先级的分组由内核外设 SCB 的应用程序中断及复位控制寄存器 AIRCR 的PRIGROUP[10:8]位决定,STM32F103 分为了 5 组,具体如下:主优先级=抢占优先级。

表5优先级分组

单片机

设置优先级分组可调用库函数 NVIC_PriorityGroupConfig()实现,有关 NVIC 中断相关的库函数都在库文件 misc.c 和 misc.h 中。

/**
  * 配置中断优先级分组:抢占优先级和子优先级
 * 形参如下:
 * @arg NVIC_PriorityGroup_0: 0bit for 抢占优先级
 *                            4 bits for 子优先级
 * @arg NVIC_PriorityGroup_1: 1 bit for 抢占优先级
 *                            3 bits for 子优先级
 * @arg NVIC_PriorityGroup_2: 2 bit for 抢占优先级
 *                            2 bits for 子优先级
 * @arg NVIC_PriorityGroup_3: 3 bit for 抢占优先级
 *                            1 bits for 子优先级
 * @arg NVIC_PriorityGroup_4: 4 bit for 抢占优先级
 *                            0 bits for 子优先级
 * @注意 如果优先级分组为 0,则抢占优先级就不存在,优先级就全部由子优先级控制
 */
 void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
 {
     // 设置优先级分组
     SCB- >AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;
 }

表6 优先级分组真值表

单片机

5 中断编程

在配置每个中断的时候一般有 3 个编程要点:

1、使能外设某个中断,这个具体由每个外设的相关中断使能位控制。比如串口有发送完成中断,接收完成中断,这两个中断都由串口控制寄存器的相关中断使能位控制。

2、初始化 NVIC_InitTypeDef 结构体,配置中断优先级分组,设置抢占优先级和子优先级,使能中断请求。 NVIC_InitTypeDef 结构体在固件库头文件 misc.h 中定义。

typedef struct {
    uint8_t NVIC_IRQChannel; // 中断源
    uint8_t NVIC_IRQChannelPreemptionPriority; // 抢占优先级
    uint8_t NVIC_IRQChannelSubPriority; // 子优先级
    FunctionalState NVIC_IRQChannelCmd; // 中断使能或者失能
} NVIC_InitTypeDef;

有关 NVIC 初始化结构体的成员我们一一解释下:

1) NVIC_IROChannel:用来设置中断源,不同的中断中断源不一样,且不可写错,即使写错了程序也不会报错,只会导致不响应中断。具体的成员配置可参考 stm32f10x.h 头文件里面的 IRQn_Type 结构体定义,这个结构体包含了所有的中断源。

typedef enum IRQn {
    //Cortex-M3 处理器异常编号
    NonMaskableInt_IRQn = -14,
    MemoryManagement_IRQn = -12,
    BusFault_IRQn = -11,
    UsageFault_IRQn = -10,
    SVCall_IRQn = -5,
    DebugMonitor_IRQn = -4,
    PendSV_IRQn = -2,
   SysTick_IRQn = -1,
   //STM32 外部中断编号
   WWDG_IRQn = 0,
   PVD_IRQn = 1,
   TAMP_STAMP_IRQn = 2,

   //限于篇幅,中间部分代码省略,具体的可查看库文件 stm32f10x.h

   DMA2_Channel2_IRQn = 57,
   DMA2_Channel3_IRQn = 58,
   DMA2_Channel4_5_IRQn = 59
} IRQn_Type;

2) NVIC_IRQChannelPreemptionPriority:抢占优先级,具体的值要根据优先级分组来确定,具体参考表6优先级分组真值表 。

3) NVIC_IRQChannelSubPriority:子优先级,具体的值要根据优先级分组来确定,具体参考表6优先级分组真值表 。

4) NVIC_IRQChannelCmd:中断使能( ENABLE)或者失能( DISABLE)。操作的是 NVIC_ISER 和 NVIC_ICER 这两个寄存器。

3、编写中断服务函数

在启动文件 startup_stm32f10x_hd.s 中我们预先为每个中断都写了一个中断服务函数,只是这些中断函数都是为空,为的只是初始化中断向量表。实际的中断服务函数都需要我们重新编写,为了方便管理我们把中断服务函数统一写在 stm32f10x_it.c 这个库文件中。关于中断服务函数的函数名必须跟启动文件里面预先设置的一样,如果写错,系统就在中断向量表中找不到中断服务函数的入口,直接跳转到启动文件里面预先写好的空函数,并且在里面无限循环,实现不了中断。

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分