什么是中断?什么是异常?其实他们是同一个东西,只是来源不同叫法不同。有系统内部引起的异常就叫异常,而由外设或外部引脚引起的异常就叫做中断,中断也是一种异常。提到异常和中断不得不提中断向量控制器NVIC,它是一个硬件结构,它会接收很多中断源产生的请求,根据中断编号执行对应的函数,具体的结构图如下:
Cortex-M3和Cortex-M4的NVIC最多支持240个IRQ(中断请求)、一个不可屏蔽中断(NMI),一个systick(系统节拍)定时器中断以及多个系统异常。多数IRQ由定时器、I/O端口和通信接口(如UART、I2C)等外设产生。NMI通常由看门狗定时器或掉电检测器等外设产生,其余的异常则是来气处理器内核,中断还可以利用软件生成。
Cortex-M架构支持多种异常和外部中断,编号1~15为系统异常,16及以上的则为中断输入。包括中断在内的大多数的异常的优先级都是可编程的,一些系统异常具有固定的优先级。(中断编号和中断优先级不是同一个概念,注意区分;异常的枚举值和异常编号也不是一个概念)
系统异常列表:
外部中断列表:
中断编号(如中断#0)表示NVIC的中断输入,对于识别是哪一个中断有重要意义,例如:当前正在运行的异常的编号数值位于特殊寄存器-中断程序状态寄存器(IPSR)中,或者NVIC中一个名为中断控制状态寄存器中。如果使用CMSIS-Core来编程,中断编号由枚举来定义,从数值0开始(代表中断 #0),系统异常的编号为负数,具体的定义如下:
中断编号和相关的枚举是根据具体的芯片而定的,他们一般位于微控制器供应商提供的头文件中,一个名为IRQn的typedef段中。
异常及中断的管理都是通过寄存器来控制的,相关寄存器主要有NVIC和系统控制块(SCB system control block),而实际上SCB也是NVIC的一部分,只是CMSIS-Core将其定义在了单独的结构体中。NVIC和SCB位于系统控制空间(SCS system control space),地址从0xE000E000开始,大小为4KB。SCS空间中还有systick定制器,存储器保护单元MPU以及调试的寄存器。该地址区的寄存器基本上都只能由运行在特权访问等级的代码访问。唯一的例外是-软件触发中断寄存器(STIR)。中断操作一般可以使用CMSIS-Core提供的函数:函数列表如下:
复位后所有的中断都处于禁止状态,且默认优先级都为0。在使用任何一个中断之前,(1)设置中断优先级(2)使能外设中的可以触发中断的中断产生控制(3)使能NVIC的中断。
中断服务程序(ISR)需要写入启动代码中的中断向量表里面,ISR的名字要与启动代码中向量表使用的名字一样,启动代码由芯片供应商来提供。
无论是异常还是有中断都是有优先级的,优先级的大小决定了中断的执行顺序和能够嵌套(优先级数值越小,优先级越高),抢占优先级高的能够打断抢占优先级的的中断,这就是所谓的中断嵌套。有一些异常时有固定优先级的不能够被编程,例如:复位、NMI和HardFault具有固定的负数优先级,其他可编程优先级的范围为0~255(8个bit)。
可编程优先级的实际数量由芯片设计厂商直接决定,其实多数的Cortex-M3和Cortex-M4支持的有限级较少,如8(3个bit),16(4个bit),32(5个bit)等。这是因为大量的优先级会增加NVIC的复杂度,而且会增加功耗降低的速度。优先级数量的减少是通过去除优先级配置寄存器的最低位(LSB)实现的。中断优先级的个数由优先级寄存器控制,宽度为3~8bits,若设计中只有了三个bit,优先级配置寄存器如下图。(STM32使用4个bits,我们公司的BLE蓝牙芯片也是4个bits)如果是用3个bit来实现,有限级的值可以为0x00(高优先级), 0x02, 0x04, 0x60,0x80, 0xa0, 0xc0, 0xe0具体的优先级等级配置如下。
之所以移除优先级寄存器的最低位LSB而不是最高位MSB,因为这样处理的话,在不同Cortex-M设备之间移植起来更方便。从上图可以看出,3位和4位宽度的优先级寄存器有重合的优先级,因此3位上写的代码可以不需要改动就一直到4位上。
这8位的优先级寄存器配置寄存器,又被分成了两组,一组是抢占式优先级,另一组是子优先级。分组是利用系统控制块(SCB)中一个名为优先级分组的配置寄存器,具体的分组情况如下图:
配置优先级分组可以用CMSIS-Core提供的函数接口,具体的函数接口如下:
在处理器已经运行一个中断处理时能否产生另一个中断,是由该中断的抢占优先级决定的。子优先级只会用在具有相同抢占优先级的情况,具有更高优先级的异常(即优先级数值越小)会被优先处理。由于优先级的分组存在,抢占优先级最多有7个bit,因此有128个等级,若优先级分组配置为7,所有具有可编程优先级的异常则会处于相同的等级,这些异常之间就不会发生抢占。但是hardfault、NMI和复位则是例外,因为他们的优先级分别为-1,-2,-3,它们可以抢占这些异常。
若配置优先级寄存器的宽度为3,就会有2个bit的抢占优先级,6个bit的子优先级,因为配置优先级寄存器只有三位,所以只会有1个bit的子优先级,具体的优先级数值如下:
STM32使用4个bit作为优先级配置寄存器,分组用的是NVIC_PRIORITYGROUP_4,写入分组寄存器的值是3,因此STM32是没有子优先级的,4个bit全部为抢占式优先级,是可以进行中断嵌套的。
// stm32f7xx_hal_cortex.h
#define NVIC_PRIORITYGROUP_0 ((uint32_t)0x00000007) /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PRIORITYGROUP_1 ((uint32_t)0x00000006) /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PRIORITYGROUP_2 ((uint32_t)0x00000005) /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PRIORITYGROUP_3 ((uint32_t)0x00000004) /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PRIORITYGROUP_4 ((uint32_t)0x00000003) /*!< 4 bits for pre-emption priority
0 bits for subpriority */
// stm32f7xx_hal_cortex.c
/**
* @brief Sets the priority grouping field (preemption priority and subpriority)
* using the required unlock sequence.
* @param PriorityGroup: The priority grouping bits length.
* This parameter can be one of the following values:
* @arg NVIC_PRIORITYGROUP_0: 0 bits for preemption priority
* 4 bits for subpriority
* @arg NVIC_PRIORITYGROUP_1: 1 bits for preemption priority
* 3 bits for subpriority
* @arg NVIC_PRIORITYGROUP_2: 2 bits for preemption priority
* 2 bits for subpriority
* @arg NVIC_PRIORITYGROUP_3: 3 bits for preemption priority
* 1 bits for subpriority
* @arg NVIC_PRIORITYGROUP_4: 4 bits for preemption priority
* 0 bits for subpriority
* @note When the NVIC_PriorityGroup_0 is selected, IRQ preemption is no more possible.
* The pending IRQ priority will be managed only by the subpriority.
* @retval None
*/
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
/* Check the parameters */
assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup));
/* Set the PRIGROUP[10:8] bits according to the PriorityGroup parameter value */
NVIC_SetPriorityGrouping(PriorityGroup);
}
// core_cm7.h
/** \brief Set Priority Grouping
The function sets the priority grouping field using the required unlock sequence.
The parameter PriorityGroup is assigned to the field SCB->AIRCR [10:8] PRIGROUP field.
Only values from 0..7 are used.
In case of a conflict between priority grouping and available
priority bits (__NVIC_PRIO_BITS), the smallest possible priority group is set.
\param [in] PriorityGroup Priority grouping field.
*/
__STATIC_INLINE void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
uint32_t reg_value;
uint32_t PriorityGroupTmp = (PriorityGroup & (uint32_t)0x07UL); /* only values 0..7 are used */
reg_value = SCB->AIRCR; /* read old register configuration */
reg_value &= ~((uint32_t)(SCB_AIRCR_VECTKEY_Msk | SCB_AIRCR_PRIGROUP_Msk)); /* clear bits to change */
reg_value = (reg_value |
((uint32_t)0x5FAUL << SCB_AIRCR_VECTKEY_Pos) |
(PriorityGroupTmp << 8) ); /* Insert write key and priorty group */
SCB->AIRCR = reg_value;
}
// stm32f7xx_hal.c
/**
* @brief This function is used to initialize the HAL Library; it must be the first
* instruction to be executed in the main program (before to call any other
* HAL function), it performs the following:
* Configure the Flash prefetch, and instruction cache through ART accelerator.
* Configures the SysTick to generate an interrupt each 1 millisecond,
* which is clocked by the HSI (at this stage, the clock is not yet
* configured and thus the system is running from the internal HSI at 16 MHz).
* Set NVIC Group Priority to 4.
* Calls the HAL_MspInit() callback function defined in user file
* "stm32f7xx_hal_msp.c" to do the global low level hardware initialization
*
* @note SysTick is used as time base for the HAL_Delay() function, the application
* need to ensure that the SysTick time base is always set to 1 millisecond
* to have correct HAL operation.
* @retval HAL status
*/
HAL_StatusTypeDef HAL_Init(void)
{
/* Configure Flash prefetch and Instruction cache through ART accelerator */
#if (ART_ACCLERATOR_ENABLE != 0)
__HAL_FLASH_ART_ENABLE();
#endif /* ART_ACCLERATOR_ENABLE */
/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); // 分组配置
/* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */
HAL_InitTick(TICK_INT_PRIORITY);
/* Init the low level hardware */
HAL_MspInit();
/* Return function status */
return HAL_OK;
}
当Cortex-M处理器接受了某异常请求后,处理器需要确定该异常处理的起始地址(如果是中断的话就要知道中断的入口函数)。起始地址的信息都存储在向量表中,向量表默认从地址0开始,向量地址则为异常编号*4,向量一般都在芯片供应商的启动文件中,即.s文件,向量表示例如下:
向量表的起始项就是MSP主堆栈指针的初始值,这种设计是很有必要的,因为NMI等异常可能紧接着就是复位,而且此时还没有进行任何初始化操作。一般来说起始地址0x00000000处应该为负责启动的存储器,它可以为Flash或者ROM设备,而且运行期间是不能对其进行修改的。不过有些应用可能需要运行时修改或重新定义向量表,因此Cortex-M3和Cortex-M4提供了向量表重定向的功能。这个功能通过一个名为向量表偏移寄存器(VTOR)的可编程寄存器来实现,VTOR的复位值为0,默认使用存储器的起始地址定义为向量表。若使用CISIS的设备驱动库进行应用编程,可以通过SCB->VTOR访问寄存器,要将向量表重新定义到SRAM区域,可以通过以下代码来实现:
// SRAM的起始地址为0x20000000
// _DMB _DSB等屏障指令一般都是根据架构的要求 特定语句后面就要跟着这样的屏障指令
#define HW32_REG(ADDRESS) (*((volatile unsigned long*)(ADDRESS)))
#define VTOR_NEW_ADDR 0x20000000
int i; // 循环变量 指针地址
// 在设置VTOR前首先将向量表复制到SRAM中
for(i=0; i < 48; i++)
{
HW32_REG(VTOR_NEW_ADDR + (i << 2)) = HW32_REG((i << 2));
}
_DMB(); // 数据存储器屏障,确保到存储器的写操作结束
SCB -> VTOR = VTOR_NEW_ADDR; // VTOR这个寄存器里面时刻存放的都是要使用的向量表
_DSB(); // 数据同步屏障,确保接下来的所有指令都使用新配置
VTOR寄存器如下图,在使用VTOR时,需要将向量表大小扩展为下一个2的整数次方,且新向量表的及地址必须要对其这个数值。例如向量表大小为(32(中断)+ 16(系统异常)* 4(每个向量32bit,四个字节) = 192(0x0c))。192比128大因此需要2的8次方=256字节,因此向量表的地址可被设置为0x00000000, 0x00000200, 0x00000400(必须是向量表大小的整数倍)等。由于中断的最小数量为1,最小的向量表对齐为128字节,因此,VTOR的最低7bit保留,且被强制置为0。
向量表的重定向一般用于以下三种情形:
(1)具有Boot loader的设备
有些微控制器具有多个程序存储器,比如启动ROM和用户flash存储器。芯片生产商一般会将启动代码boot loader预先写到启动ROM中,这样在微控制器启动时,启动ROM中的Boot loader就会首先执行,而且在跳转到用户flash的应用程序之前,会将0x00000000地址处的向量表复制到用户flash的起始地址去,同时VTOR会被设置为指向用户flash存储器的开始处,因此会使用用户flash中的向量表。具体的实现流程如下图所示:
(2)应用程序加载到RAM
有些情况,应用程序可能会被从外部设备记载到RAM中执行,它可能会位于SD卡中,或者有的通过网络传输,这种情况下,存储在片上存储器中用于启动的程序需要初始化一些硬件、复制位于外部设备中的应用程序到RAM,然后更新VTOR后执行存储在外部的程序。具体的实现流程如下:
(3)动态修改向量表
有些情况下,ROM中可能会有一个中断的多个处理实例,可能需要在应用的不同阶段在它们之间进行切换。在这种情况下,可以将向量表从程序存储器复制到SRAM中,并且设置VTOR指向SRAM中的向量表。由于SRAM中的内容可在任意时间修改,因此可以轻易地在应用的不同阶段修改中断向量。
注:向量表最少也要提供MSP的初始值以及用于系统启动的复位向量。另外,对于一些应用,若设备在启动时有触发NMI的可能,也许还需要加入NMI和hardfault向量。
每个中断是有多个属性的,(1)每个中断都是可以被禁止(默认)或者使能的,通过中断控制寄存器;(2)每个中断都是可以被挂起(Pending状态等待执行中断函数)或者解除挂起;(3)每个中断都可以处于活跃(正在处理)或非活跃状态(活跃状态位是只读的);中断被响应并能执行中断函数的条件是:挂起状态职位同时中断使能,且中断优先级比当前的优先级高。
NVIC支持两种外设的中断请求,(1)脉冲中断请求;(2)高电平中断请求;这是不需要配置的,默认两种都能够响应。
脉冲中断请求,脉冲宽度至少要一个时钟周期;而对于高电平中断请求,在ISR中的清除Pending位之前,始终保持着高电平的状态。尽管外部中请求在I/O引脚的电平可能是低有效,NVIC收到的请求信号仍然为高有效。无论是哪一种方式的中断请求来了之后,都会在挂起状态寄存器上置位。
中断的挂起状态被存储在NVIC的可编程寄存器里面,当NVIC的中断输入被确认后,它就会触发该中断的挂起状态,即在挂起状态寄存器的相应bit位置位,即使此时中断请求取消了,挂起状态仍然为高,中断请求仍会被执行。挂起状态的意思就是中断在等待处理器处理的状态。
处理中断的时间点有分为两种情况:(1)中断刚挂起即Pending住,处理器就直接处理,然后把Pending位清掉。(2)若处理器正在处理一个更高优先级或同等优先级的中断,或中断被某个中断屏蔽器给屏蔽掉了(屏蔽中断后如果中断来了仍然会有pending位挂起),那么在其他中断处理结束前或中断屏蔽清除前,挂起请求会一直保持。
下面重点讲解几个典型的中断请求过程:
如上图7.14所示,当中断正在被处理时它会处于活跃状态,在中断的入口处,多个寄存器会被自动压入栈中,这也被称作压栈。同时ISR的起始地址会被从向量表中取出。当中断请求来的时候中断挂起状态就置1,证明中断来了,等待着被处理器执行,当处理器执行中断处理函数的时候,会把中断的挂起位清掉,中断处理的请求电平也要被拉低。
当中断处于活跃状态时,处理器无法在中断完成和异常返回前再次接受同一个中断请求,即中断请求即使来了,Pending位也不会被置位。
如上图7.15所示,当中断请求被处理器执行之前,挂起位就被清掉了,此后处理器空出来也将不会处理该中断。
如上图7.16所示,当中断请求被处理器执行之前,挂起位被清掉了,但是中断请求还在,那么挂起位仍然会被重新置起来。
如上图7.17所示,中断得到处理之后,挂起位被清除掉,但是中断请求依然还在,当中断函数执行完之后,又会再次将Pending位置起来,进入中断。
如上图7.18所示,对于脉冲形式的中断请求,在中断被执行前,即pending位被清掉前,无论来多少次中断请求,都按照一次来算。
如上图7.19所示,当处理器正在处理中断的时候(已经将Pending位清掉),有来了中断请求,这时就会又把Pending位置起,当前中断处理完成之后,则会再次进入中断。
注:即使中断被禁止了,它的Pending状态仍然是可以被置的,即在_disable_irq之后中断请求仍然会将中断Pending位置位,这种情况下,中断稍后被使能了,它让然可以被触发并得到执行。但是有的时候我们希望临界区里面来的中断不被执行,这就需要在使能中断_enable_iqr之前清除中断Pending位。一般来说,NMI的请求方式和中孤单类似。若当前没有在运行NMI处理,或者处理器被暂停或处于锁定状态,由于NMI具有最高优先级且不能被禁止,因此它几乎会立即执行。
参考书籍:
《Cortex-M3/4权威指南》
《FreeRTOS源码详解与应用开发》