GPIO外部中断

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外部中断

因此,对于编程而言,需要对GPIO、EXTI、NVIC 3个模块分别进行配置和操作。所幸,Cube HAL 以及 CubeMX 工具大大地降低了开发的复杂度。

Nucleo 开发板上有一个用户按钮B1和一个用户LED LD2,可以用它们来实现一个简单的 GPIO 外部中断 Demo。Nucleo 原理图显示,B1 进行了 RC de-bouncing,因此可以作为外部中断源。未经 de-bouncing 的按钮,是不应该触发中断的。B1接在 PC13 口,已经设计了上拉电阻

GPIO外部中断

使用CubeMX,将B1口模式设置为 GPIO_EXIT13。可见,PC13 使用了 EXTI line 13。另外,由于使用了上拉电阻,选择中断为下降沿触发:

GPIO外部中断GPIO外部中断

这样,GPIO和 EXTI 就配置好了。别忘了还需要配置 NVIC。这里只需要简单地启用它对应的 IRQ即可,其余保持默认:

GPIO外部中断

简要分析一下 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	<strong>EXTI15_10_IRQHandler</strong>
	....

其中,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 */
  <strong>HAL_GPIO_EXTI_IRQHandler</strong>(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);
    <strong>HAL_GPIO_EXTI_Callback</strong>(GPIO_Pin);
  }
}

<strong>__weak </strong>void<strong> HAL_GPIO_EXTI_Callback</strong>(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);

相关推荐