本篇文章会用简单易懂的方式讲解STM32中断,不涉及到太深奥的内容,适合初学STM32还没用过中断功能,或用过中断功能但看起中断代码还是云里雾里的朋友阅读。
文章涉及到的参数及代码都基于STM32F103系列单片机。
什么是“中断”
专业术语:
中断(Interrupt)是一种硬件或软件机制,用于响应外部或内部事件,并在系统运行中临时中断当前执行的程序,以处理紧急或高优先级任务。中断是嵌入式系统中非常重要的一部分,它使得系统能够实时、高效地响应各种事件。
简单来说,中断就是CPU在正常运行程序时突然被打断,转而去做一个更加紧急的(优先级更高)的事件。中断会涉及到以下几点概念:
- 中断源:引发中断的事件来源。
- 中断优先级:用于决定在多个中断源同时发生时,哪个中断优先被处理。
- 中断屏蔽:一种机制,用于暂时禁止某些中断的触发。
- 中断服务程序:处理中断的程序,当中断发生时,CPU会执行这段程序。
这里做个比喻:你正在上着课,这时你就相当于“CPU”,你在执行的程序就是“上课”。突然一个很重要的电话打给你,你必须马上停止上课然后去接电话,这时这个“电话”就是“中断源”,你“接电话”就是在执行“中断服务程序”。接完电话之后你回到教室上课。以上的过程就叫做“中断”。其实这里还涉及到“中断优先级”和“中断屏蔽”的概念。假如你在上课前就把手机关机了,就算这时有人给你打电话你也不知道,相当于屏蔽了电话对你的影响,这个就是“中断屏蔽”。如果你没有把手机关机,电话响起的同时你刚好有点胃痛,而且你的课本刚好掉地上了,这时你认为先吃药最重要,你会先吃完药再处理后面的事情,吃药的优先级大于接电话和捡课本,这就涉及到“中断优先级”的概念。
中断还涉及到中断分类、中断向量表等概念,这里就不一一展开,不影响下面对STM32中断的理解。
STM32的中断
异常/中断类型
STM32的中断功能是非常强大的。除了中断外,我们在数据手册或指导手册中还会看到“异常”的字眼,一般这些手册都会声明:“如无特别说明,异常就是中断,中断就是异常”。本篇博客也如是。
通过上一个小节对中断的学习,我们对中断都有了一个初步的了解。我们在上课时,可能会被胃痛等内部事件,或者电话(电话响起)、书本(书本掉落)等外部事件打断,同样的,STM32在运行程序时,也会被类似内部和外部的事件打断。
STM32F103单片机在内核上搭在了一个异常响应系统,支持系统异常(也有的文章会把系统异常叫做“内部中断”)8个,如果算上Reset
和HardFault
就是10个;支持外部中断60个。这些异常和中断声明在HAL库stm32f103xe.h
文件的IRQn_Type
枚举里,或者声明在标准库stm32f10x.h
文件的IRQn_Type
枚举里,下面列出HAL库里对这些异常和中断的声明(标准库里的也是类似的):
/*!< Interrupt Number Definition */
typedef enum
{
/****** Cortex-M3 Processor Exceptions Numbers ***************************************************/
NonMaskableInt_IRQn = -14, /*!< 2 Non Maskable Interrupt */
HardFault_IRQn = -13, /*!< 3 Cortex-M3 Hard Fault Interrupt */
MemoryManagement_IRQn = -12, /*!< 4 Cortex-M3 Memory Management Interrupt */
BusFault_IRQn = -11, /*!< 5 Cortex-M3 Bus Fault Interrupt */
UsageFault_IRQn = -10, /*!< 6 Cortex-M3 Usage Fault Interrupt */
SVCall_IRQn = -5, /*!< 11 Cortex-M3 SV Call Interrupt */
DebugMonitor_IRQn = -4, /*!< 12 Cortex-M3 Debug Monitor Interrupt */
PendSV_IRQn = -2, /*!< 14 Cortex-M3 Pend SV Interrupt */
SysTick_IRQn = -1, /*!< 15 Cortex-M3 System Tick Interrupt */
/****** STM32 specific Interrupt Numbers *********************************************************/
WWDG_IRQn = 0, /*!< Window WatchDog Interrupt */
PVD_IRQn = 1, /*!< PVD through EXTI Line detection Interrupt */
TAMPER_IRQn = 2, /*!< Tamper Interrupt */
RTC_IRQn = 3, /*!< RTC global Interrupt */
FLASH_IRQn = 4, /*!< FLASH global Interrupt */
RCC_IRQn = 5, /*!< RCC global Interrupt */
EXTI0_IRQn = 6, /*!< EXTI Line0 Interrupt */
EXTI1_IRQn = 7, /*!< EXTI Line1 Interrupt */
EXTI2_IRQn = 8, /*!< EXTI Line2 Interrupt */
EXTI3_IRQn = 9, /*!< EXTI Line3 Interrupt */
EXTI4_IRQn = 10, /*!< EXTI Line4 Interrupt */
DMA1_Channel1_IRQn = 11, /*!< DMA1 Channel 1 global Interrupt */
DMA1_Channel2_IRQn = 12, /*!< DMA1 Channel 2 global Interrupt */
DMA1_Channel3_IRQn = 13, /*!< DMA1 Channel 3 global Interrupt */
DMA1_Channel4_IRQn = 14, /*!< DMA1 Channel 4 global Interrupt */
DMA1_Channel5_IRQn = 15, /*!< DMA1 Channel 5 global Interrupt */
DMA1_Channel6_IRQn = 16, /*!< DMA1 Channel 6 global Interrupt */
DMA1_Channel7_IRQn = 17, /*!< DMA1 Channel 7 global Interrupt */
ADC1_2_IRQn = 18, /*!< ADC1 and ADC2 global Interrupt */
USB_HP_CAN1_TX_IRQn = 19, /*!< USB Device High Priority or CAN1 TX Interrupts */
USB_LP_CAN1_RX0_IRQn = 20, /*!< USB Device Low Priority or CAN1 RX0 Interrupts */
CAN1_RX1_IRQn = 21, /*!< CAN1 RX1 Interrupt */
CAN1_SCE_IRQn = 22, /*!< CAN1 SCE Interrupt */
EXTI9_5_IRQn = 23, /*!< External Line[9:5] Interrupts */
TIM1_BRK_IRQn = 24, /*!< TIM1 Break Interrupt */
TIM1_UP_IRQn = 25, /*!< TIM1 Update Interrupt */
TIM1_TRG_COM_IRQn = 26, /*!< TIM1 Trigger and Commutation Interrupt */
TIM1_CC_IRQn = 27, /*!< TIM1 Capture Compare Interrupt */
TIM2_IRQn = 28, /*!< TIM2 global Interrupt */
TIM3_IRQn = 29, /*!< TIM3 global Interrupt */
TIM4_IRQn = 30, /*!< TIM4 global Interrupt */
I2C1_EV_IRQn = 31, /*!< I2C1 Event Interrupt */
I2C1_ER_IRQn = 32, /*!< I2C1 Error Interrupt */
I2C2_EV_IRQn = 33, /*!< I2C2 Event Interrupt */
I2C2_ER_IRQn = 34, /*!< I2C2 Error Interrupt */
SPI1_IRQn = 35, /*!< SPI1 global Interrupt */
SPI2_IRQn = 36, /*!< SPI2 global Interrupt */
USART1_IRQn = 37, /*!< USART1 global Interrupt */
USART2_IRQn = 38, /*!< USART2 global Interrupt */
USART3_IRQn = 39, /*!< USART3 global Interrupt */
EXTI15_10_IRQn = 40, /*!< External Line[15:10] Interrupts */
RTC_Alarm_IRQn = 41, /*!< RTC Alarm through EXTI Line Interrupt */
USBWakeUp_IRQn = 42, /*!< USB Device WakeUp from suspend through EXTI Line Interrupt */
TIM8_BRK_IRQn = 43, /*!< TIM8 Break Interrupt */
TIM8_UP_IRQn = 44, /*!< TIM8 Update Interrupt */
TIM8_TRG_COM_IRQn = 45, /*!< TIM8 Trigger and Commutation Interrupt */
TIM8_CC_IRQn = 46, /*!< TIM8 Capture Compare Interrupt */
ADC3_IRQn = 47, /*!< ADC3 global Interrupt */
FSMC_IRQn = 48, /*!< FSMC global Interrupt */
SDIO_IRQn = 49, /*!< SDIO global Interrupt */
TIM5_IRQn = 50, /*!< TIM5 global Interrupt */
SPI3_IRQn = 51, /*!< SPI3 global Interrupt */
UART4_IRQn = 52, /*!< UART4 global Interrupt */
UART5_IRQn = 53, /*!< UART5 global Interrupt */
TIM6_IRQn = 54, /*!< TIM6 global Interrupt */
TIM7_IRQn = 55, /*!< TIM7 global Interrupt */
DMA2_Channel1_IRQn = 56, /*!< DMA2 Channel 1 global Interrupt */
DMA2_Channel2_IRQn = 57, /*!< DMA2 Channel 2 global Interrupt */
DMA2_Channel3_IRQn = 58, /*!< DMA2 Channel 3 global Interrupt */
DMA2_Channel4_5_IRQn = 59, /*!< DMA2 Channel 4 and Channel 5 global Interrupt */
} IRQn_Type;
从以上可以看出,STM32F103涉及到的中断总共有70个,那么STM32是如何管理这么多中断,保证中断符合逻辑的运行呢?这就需要提到NVIC。
NVIC
NVIC:嵌套向量中断控制器。NVIC控制着整个芯片中断相关的功能,它跟内核紧密耦合,是内核里面的一个外设。但是各个芯片厂商在设计芯片的时候会对 Cortex-M3 内核里面的 NVIC 进行裁剪,把不需要的部分去掉,所以说 STM32的 NVIC 是 Cortex-M3 的 NVIC 的一个子集。
下面看看NVIC在代码文件core_cm3.h
中的结构体定义:
typedef struct
{
__IO uint32_t ISER[8]; // 中断使能寄存器
uint32_t RESERVED0[24];
__IO uint32_t ICER[8]; // 中断清除寄存器
uint32_t RSERVED1[24];
__IO uint32_t ISPR[8]; // 中断使能悬起寄存器
uint32_t RESERVED2[24];
__IO uint32_t ICPR[8]; // 中断清除悬起寄存器
uint32_t RESERVED3[24];
__IO uint32_t IABR[8]; // 中断有效位寄存器
uint32_t RESERVED4[56];
__IO uint8_t IP[240]; // 中断优先级寄存器
uint32_t RESERVED5[644];
__O uint32_t STIR; // 软件触发中断寄存器
} NVIC_Type;
常用的是ISER、ICER和IP。ISER用来使能中断,ICER用来失能中断,IP用来设置中断优先级。
以上涉及到的代码和知识相对底层,而且我们平时编程时很多也不会直接用到,下一小节会讲两个我们比较有可能用到的NVIC函数:HAL库 HAL_NVIC_SetPriorityGrouping
和标准库 NVIC_PriorityGroupConfig
这两个函数用于设置优先级分组。
优先级分组
在 NVIC 有一个专门的寄存器:中断优先级寄存器 NVIC_IPRx,用来配置外部中断的优先级,IPR宽度为 8bit,原则上每个外部中断可配置的优先级为 0~255,数值越小,优先级越高。但是绝大多数 CM3 芯片都会精简设计,以致实际上支持的优先级数减少,在 F103 中,只使用了高 4bit。
用于表达优先级的这 4bit,又被分组成抢占优先级和子优先级。如果有多个中断同时响应,抢占优先级高的就会抢占抢占优先级低的优先得到执行,如果抢占优先级相同,就比较子优先级。如果抢占优先级和子优先级都相同的话,就比较他们的硬件中断编号,编号越小,优先级越高。
在STM32F103中,优先级共可以分成五组:
这五个分组在HAL库的stm32f1xx_hal_cortex.h
文件中或标准库的misc.h
文件中有对应的宏定义(两者基本一致):
HAL库:
#define NVIC_PRIORITYGROUP_0 0x00000007U /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PRIORITYGROUP_1 0x00000006U /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PRIORITYGROUP_2 0x00000005U /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PRIORITYGROUP_3 0x00000004U /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PRIORITYGROUP_4 0x00000003U /*!< 4 bits for pre-emption priority
0 bits for subpriority */
标准库:
#define NVIC_PriorityGroup_0 ((uint32_t)0x700) /*!< 0 bits for pre-emption priority
4 bits for subpriority */
#define NVIC_PriorityGroup_1 ((uint32_t)0x600) /*!< 1 bits for pre-emption priority
3 bits for subpriority */
#define NVIC_PriorityGroup_2 ((uint32_t)0x500) /*!< 2 bits for pre-emption priority
2 bits for subpriority */
#define NVIC_PriorityGroup_3 ((uint32_t)0x400) /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PriorityGroup_4 ((uint32_t)0x300) /*!< 4 bits for pre-emption priority
0 bits for subpriority */
HAL库的HAL_NVIC_SetPriorityGrouping
:
在用STM32CubeMX生成的代码里,main函数中的HAL_Init();
函数里调用了HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
,这是将中断优先级设置为NVIC_PRIORITYGROUP_4
模式,即没有子优先级,只有抢占式优先级有效且对应16个优先级。
标准库的NVIC_PriorityGroupConfig
:
我们在用标准库编写STM32代码时,要注意NVIC_PriorityGroupConfig()
在整个代码文件里只需要调用一次即可,该函数的用法和HAL库的类似,例如:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
中断编程
分三步:
- 配置中断源、中断优先级;
- 使能中断;
- 编写中断服务函数。
以下示例以按键外部中断EXIT为例。
HAL库
基于HAL库的中断编程主要涉及到两部分代码,一部分是用户自定义C文件下的编程和中断服务函数管理文件stm32f1xx_it.c
的编程。
自定义文件:
void EXTI_Key_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
KEY1_INT_GPIO_CLK_ENABLE();
KEY2_INT_GPIO_CLK_ENABLE();
GPIO_InitStructure.Pin = KEY1_INT_GPIO_PIN;
GPIO_InitStructure.Mode = GPIO_MODE_IT_RISING;
GPIO_InitStructure.Pull = GPIO_NOPULL;
HAL_GPIO_Init(KEY1_INT_GPIO_PORT, &GPIO_InitStructure);
/* 配置 EXTI 中断源 到key1 引脚、配置中断优先级*/
HAL_NVIC_SetPriority(KEY1_INT_EXTI_IRQ, 0, 0);
/* 使能中断 */
HAL_NVIC_EnableIRQ(KEY1_INT_EXTI_IRQ);
GPIO_InitStructure.Pin = KEY2_INT_GPIO_PIN;
HAL_GPIO_Init(KEY2_INT_GPIO_PORT, &GPIO_InitStructure);
/* 配置 EXTI 中断源 到key2 引脚、配置中断优先级*/
HAL_NVIC_SetPriority(KEY2_INT_EXTI_IRQ, 0, 0);
/* 使能中断 */
HAL_NVIC_EnableIRQ(KEY2_INT_EXTI_IRQ);
}
stm32f1xx_it.c
文件:
void EXTI0_IRQHandler(void)
{
//确保是否产生了EXTI Line中断
if(__HAL_GPIO_EXTI_GET_IT(KEY1_INT_GPIO_PIN) != RESET)
{
// LED1 取反
LED1_TOGGLE;
//清除中断标志位
__HAL_GPIO_EXTI_CLEAR_IT(KEY1_INT_GPIO_PIN);
}
}
void EXTI15_10_IRQHandler(void)
{
//确保是否产生了EXTI Line中断
if(__HAL_GPIO_EXTI_GET_IT(KEY2_INT_GPIO_PIN) != RESET)
{
// LED2 取反
LED2_TOGGLE;
//清除中断标志位
__HAL_GPIO_EXTI_CLEAR_IT(KEY2_INT_GPIO_PIN);
}
}
以上,通过HAL_NVIC_SetPriority()
函数来配置中断源和中断优先级,通过HAL_NVIC_EnableIRQ()
函数使能中断。而EXTI0_IRQHandler()
和EXTI15_10_IRQHandler()
就是对应的中断服务函数。一开始写中断代码时大家可能会有这样一个疑问:为什么一定要用类似EXTI0_IRQHandler()
的名字来命名中断服务函数,中断服务函数的函数名可以自定义吗?实际上,我们用的中断服务函数名是要和启动文件startup_stm32f103xe.s
里列出的函数名相对应的,如果不严格按照启动文件里的定义来,会导致中断服务函数无法正常执行。
startup_stm32f103xe.s
启动文件列出的中断服务函数名如下:
WWDG_IRQHandler
PVD_IRQHandler
TAMPER_IRQHandler
RTC_IRQHandler
FLASH_IRQHandler
RCC_IRQHandler
EXTI0_IRQHandler
EXTI1_IRQHandler
EXTI2_IRQHandler
EXTI3_IRQHandler
EXTI4_IRQHandler
DMA1_Channel1_IRQHandler
DMA1_Channel2_IRQHandler
DMA1_Channel3_IRQHandler
DMA1_Channel4_IRQHandler
DMA1_Channel5_IRQHandler
DMA1_Channel6_IRQHandler
DMA1_Channel7_IRQHandler
ADC1_2_IRQHandler
USB_HP_CAN1_TX_IRQHandler
USB_LP_CAN1_RX0_IRQHandler
CAN1_RX1_IRQHandler
CAN1_SCE_IRQHandler
EXTI9_5_IRQHandler
TIM1_BRK_IRQHandler
TIM1_UP_IRQHandler
TIM1_TRG_COM_IRQHandler
TIM1_CC_IRQHandler
TIM2_IRQHandler
TIM3_IRQHandler
TIM4_IRQHandler
I2C1_EV_IRQHandler
I2C1_ER_IRQHandler
I2C2_EV_IRQHandler
I2C2_ER_IRQHandler
SPI1_IRQHandler
SPI2_IRQHandler
USART1_IRQHandler
USART2_IRQHandler
USART3_IRQHandler
EXTI15_10_IRQHandler
RTC_Alarm_IRQHandler
USBWakeUp_IRQHandler
TIM8_BRK_IRQHandler
TIM8_UP_IRQHandler
TIM8_TRG_COM_IRQHandler
TIM8_CC_IRQHandler
ADC3_IRQHandler
FSMC_IRQHandler
SDIO_IRQHandler
TIM5_IRQHandler
SPI3_IRQHandler
UART4_IRQHandler
UART5_IRQHandler
TIM6_IRQHandler
TIM7_IRQHandler
DMA2_Channel1_IRQHandler
DMA2_Channel2_IRQHandler
DMA2_Channel3_IRQHandler
DMA2_Channel4_5_IRQHandler
标准库
基于标准库的中断编程主要涉及到两部分代码,一部分是用户自定义C文件下的编程和中断服务函数管理文件stm32f10x_it.c
的编程。
自定义文件:
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 配置中断源:按键1 */
NVIC_InitStructure.NVIC_IRQChannel = KEY1_INT_EXTI_IRQ;
/* 配置抢占优先级 */
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
/* 配置子优先级 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
/* 使能中断通道 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 配置中断源:按键2,其他使用上面相关配置 */
NVIC_InitStructure.NVIC_IRQChannel = KEY2_INT_EXTI_IRQ;
NVIC_Init(&NVIC_InitStructure);
}
void EXTI_Key_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
/*开启按键GPIO口的时钟*/
RCC_APB2PeriphClockCmd(KEY1_INT_GPIO_CLK,ENABLE);
RCC_APB2PeriphClockCmd(KEY2_INT_GPIO_CLK,ENABLE);
/* 配置 NVIC 中断*/
NVIC_Configuration();
/*--------------------------KEY1配置-----------------------------*/
/* 选择按键用到的GPIO */
GPIO_InitStructure.GPIO_Pin = KEY1_INT_GPIO_PIN;
/* 配置为浮空输入 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(KEY1_INT_GPIO_PORT, &GPIO_InitStructure);
/* 选择EXTI的信号源 */
GPIO_EXTILineConfig(KEY1_INT_EXTI_PORTSOURCE, KEY1_INT_EXTI_PINSOURCE);
EXTI_InitStructure.EXTI_Line = KEY1_INT_EXTI_LINE;
/* EXTI为中断模式 */
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
/* 上升沿中断 */
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
/* 使能中断 */
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
/*--------------------------KEY2配置-----------------------------*/
/* 选择按键用到的GPIO */
GPIO_InitStructure.GPIO_Pin = KEY2_INT_GPIO_PIN;
/* 配置为浮空输入 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(KEY2_INT_GPIO_PORT, &GPIO_InitStructure);
/* 选择EXTI的信号源 */
GPIO_EXTILineConfig(KEY2_INT_EXTI_PORTSOURCE, KEY2_INT_EXTI_PINSOURCE);
EXTI_InitStructure.EXTI_Line = KEY2_INT_EXTI_LINE;
/* EXTI为中断模式 */
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
/* 下降沿中断 */
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
/* 使能中断 */
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
}
stm32f10x_it.c
文件:
void EXTI0_IRQHandler(void)
{
//确保是否产生了EXTI Line中断
if(EXTI_GetITStatus(KEY1_INT_EXTI_LINE) != RESET)
{
// LED1 取反
LED1_TOGGLE;
//清除中断标志位
EXTI_ClearITPendingBit(KEY1_INT_EXTI_LINE);
}
}
void EXTI15_10_IRQHandler(void)
{
//确保是否产生了EXTI Line中断
if(EXTI_GetITStatus(KEY2_INT_EXTI_LINE) != RESET)
{
// LED2 取反
LED2_TOGGLE;
//清除中断标志位
EXTI_ClearITPendingBit(KEY2_INT_EXTI_LINE);
}
}
对比HAL库和标准库的代码可以发现,标准库在配置中断和使能中断时对应的步骤会多一些,但是中断服务程序的编写基本是一样的,只是部分代码方法名称不同。
写在最后
以上代码有一些函数没看懂是什么意思没关系,本篇文章主要是介绍STM32中断编写的大概流程,涉及到具体某个模块的中断程序编写需要阅读对应模型的指导手册或资料,之后有机会我也会发文章给大家介绍。