STM32的“中断”机制很复杂,看了PM(Cortex-m4)和RM,对它只了解了一个大概。首先,与“中断”相关的术语就有 exception, interrupt, event 三个。Cortex-m4核中包含一个NVIC控制器,用于处理 exception。而 interrupt 是属于 exception 之一种,其它 exception 类型包括 SysTick等。interrupt 又叫作IRQ。
STM32之中、Cortex-m4核之外的“中断”,即为 interrupt/IRQ。STM32通过 IRQ Channel 向 NVIC 请求处理 IRQ,而 NVIC 处理包括 IRQ 在内的各种 exception,例如:优先级...等等。对于 IRQ,NVIC 将调用其“中断处理程序” ISR。
有些 STM32 外围接口直接通过 IRQ Channel 与 NVIC 接口,而 GPIO 外部中断则要通过另一个控制器--EXTI--与NVIC接口。GPIO与 EXTI 之间的接口称为 EXTI line;而 EXTI 与 NVIC之间则为 IRQ Channel。GPIO pin与EXTI line之间是n:1的关系,而EXTI line与 IRQ Channel之间也是n:1的关系。基本上,PXn 对应 EXTI line n,这里X=A, B, ... H,n=0, 1, 2 ... 15。例如,PX2(PA2, PB2 ...)都对应于 EXTI line 2。
EXTI line与 IRQ Channel之间的对应关系则稍微复杂,16个 EXTI line 占用7个 IRQ:
- EXTI line 0 - 4 分别对应一个IRQ,因此,共有5个 IRQ
- EXTI line 5 - 9 共用一个IRQ
- EXTI line 10 - 15 共用一个IRQ
此外,EXTI line 上除了支持 interrupt 之外,还支持 event。event 被触发之后,并不传递给 NVIC 去处理(像 IRQ 那样),而是发送一个脉冲给电源管理模块,似乎是用来实现唤醒功能的。
GPIO、EXTI 与 NVIC 之间的关系,用下图简单表示:
因此,对于编程而言,需要对GPIO、EXTI、NVIC 3个模块分别进行配置和操作。所幸,Cube HAL 以及 CubeMX 工具大大地降低了开发的复杂度。
Nucleo 开发板上有一个用户按钮B1和一个用户LED LD2,可以用它们来实现一个简单的 GPIO 外部中断 Demo。Nucleo 原理图显示,B1 进行了 RC de-bouncing,因此可以作为外部中断源。未经 de-bouncing 的按钮,是不应该触发中断的。B1接在 PC13 口,已经设计了上拉电阻:
使用CubeMX,将B1口模式设置为 GPIO_EXIT13。可见,PC13 使用了 EXTI line 13。另外,由于使用了上拉电阻,选择中断为下降沿触发:
这样,GPIO和 EXTI 就配置好了。别忘了还需要配置 NVIC。这里只需要简单地启用它对应的 IRQ即可,其余保持默认:
简要分析一下 CubeMX 生成的代码。首先,中断向量表定义在启动代码 startup_stm32f303xe.s 中,在这个文件中可以看到所有 exception 处理程序(函数名),包括 EXTI ISR:
g_pfnVectors:
.word _estack
.word Reset_Handler
.word NMI_Handler
...
.word SysTick_Handler
...
.word EXTI0_IRQHandler
.word EXTI1_IRQHandler
.word EXTI2_TSC_IRQHandler
.word EXTI3_IRQHandler
.word EXTI4_IRQHandler
...
.word EXTI9_5_IRQHandler
....
.word EXTI15_10_IRQHandler
....
其中,EXTI15_10_IRQHandler 就是按钮B1的中断处理程序。这个函数的实现在 stm32f3xx_it.c 中,它实际上仅仅调用了 Cube 库的 HAL_GPIO_EXTI_IRQHandler() 函数,将端口号作为参数传递进去:
/** * @brief This function handles EXTI line[15:10] interrupts. */ void EXTI15_10_IRQHandler(void) { /* USER CODE BEGIN EXTI15_10_IRQn 0 */ /* USER CODE END EXTI15_10_IRQn 0 */ HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13); /* USER CODE BEGIN EXTI15_10_IRQn 1 */ /* USER CODE END EXTI15_10_IRQn 1 */ }
检查 HAL_GPIO_EXTI_IRQHandler() 函数的实现,发现它位于 GPIO HAL 模块内,它又调用了一个回调函数 HAL_GPIO_EXTI_Callback(),而该回调函数的默认实现声明为 __weak 属性,我们可以覆盖:
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin) { /* EXTI line interrupt detected */ if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin); HAL_GPIO_EXTI_Callback(GPIO_Pin); } } __weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { ...
因此,我们在 stm32f3xx_it.c 增加 HAL_GPIO_EXTI_Callback() 的实现,每当B1按下,开/关LD2:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin); }
另外,在gpio.c 中的 MX_GPIO_Init() 函数中,看到了 NVIC 的配置,但并没有看到与 EXTI 有关的配置。其实,EXTI 配置已由 HAL_GPIO_Init() 函数处理,不劳我们费心。也就是说,对 GPIO 的外部中断的处理,要使用 GPIO 和 NVIC 2个Cube 模块:
... HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct); ... /* EXTI interrupt init*/ HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0); HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);