前言
参考手册
STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm®-based 32-bit MCUs
STM32F10xxx/20xxx/21xxx/L1xxxx Cortex®-M3 programming manual
为什么需要外部中断
[!NOTE]
按键扫描也可以实现按键控制LED亮灭,为什么需要外部中断呢?
当我们使用按键扫描的方案时,CPU需要主动的不断轮询查看按键的状态,发现其被按下时就将LED状态反转一下。
如果把我们自己比作CPU,这就相当于我们网购之后,不断去快递点询问快递有没有到,如果没有就一直干等在那里,直到取到快递。在此期间,我们没有办法去做其他事情,时间都被浪费掉了。
好莱坞原则/控制反转
还有一个著名的好莱坞原则:当演员去试镜后,剧组会告知演员不要打电话给我们,有结果了我们会通知你。
好莱坞原则是面向对象编程中的一个设计理念,常用来指导模块之间的交互方式。其核心思想是:
“Don’t call us, we’ll call you.”
“别调用我们,我们会调用你。”
这意味着,在系统的架构设计中,下层模块不主动调用上层模块,而是通过某种机制(例如回调函数、事件驱动或依赖注入)让上层模块在合适的时候去调用下层模块。
这一原则强调 控制反转(IOC,Inversion of Control),目的是解耦模块,增强代码的可维护性和可扩展性。
异常向量表
ARM核异常向量表
类似的,在按键控制LED案例中,我们无法预知用户会在何时按下按键,因此通过CPU主动地不断轮询这一事件是不明智的。我们应该将控制权交还给用户,希望用户在按下按键时,CPU能够被动地感知到,并执行我们预先编写好的按键事件处理逻辑。
ARM核是支持中断这一机制的,当中断发生时,ARM能够停下手头上的事情、保存工作现场,转而执行对应的中断处理服务程序,完成后再跳转会中断前的地方,恢复工作现场并继续执行既定程序。
STM32F10xxx/20xxx/21xxx/L1xxxx Cortex®-M3 programming manual P156
The vector table contains the reset value of the stack pointer, and the start addresses, alsocalled exception vectors, for all exception handlers
上述是ARM核对应异常(中断是异常的一种)向量表的规范:上电后应该初始化SP栈指针,接着执行Reset
异常处理函数。我们可以对应STM32提供的标准外设库中的启动汇编程序 startup.s
来看下:
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
...
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
可以发现Reset
异常的执行做了两件事情:
- 执行
SystemInit
函数:可以在system_stm32f1xx.c
找到定义,其中做了系统时钟的初始化 - 执行
main
函数
异常分类
ARM核规范中提供了84个中断(见上表),但STM32F103只实现了70个:
STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm®-based 32-bit MCUs P1136
这些异常大致可以分为三类:
- ARM核异常:例如复位异常,用于上电后执行用户程序
- 外设中断异常:由STM32根据ARM异常规范及SOC(片上外设)实现,例如USART中断、DMA中断、定时器中断等
- 外部异常:通过服用GPIO引脚输入功能来实现,可以通过片外硬件触发(例如按键)
异常跳转
当具体的异常发生时,ARM核会根据该异常在向量表中的偏移地址找到对应的异常处理指令来执行,例如外部中断10就会找到 startup_stm32f103xe.s
中的 DCD EXTI15_10_IRQHandler
。通过DCD
指令存放了 EXTI15_10_IRQHandler
函数的入口地址(如果我们定义了该函数,则在链接时就能够找到对应的入口地址)
STM32外部中断实践
[!NOTE]
这里以STM32F103ZE为例
STM32中断体系架构
GPIO输入复用EXTI
可以发现,输入不仅会传输到输入数据寄存器IDR,也会接入到复用功能输入(AF Input);
如下,EXTI的左边是多个GPIO连接到多路选择器,右边连到中断线,这样就能通过一根中断线处理A-G 7个IO口的引脚。具体中断和其中哪个引脚短接,可以通过设置EXTI中的寄存器(多路选择寄存器)来选择具体的引脚。
外部中断控制器框图
- (falling/rising trigger selection register)边沿触发选择寄存器:可配置触发方式为上升沿、下降沿、还是两者均可
- (software interrupt event register)软件中断事件寄存器:和外部中断通过 或门相接,我们可以通过在程序中设置该寄存器从而实现软件触发中断的效果,而不是外部硬件设备(例如按键按下)触发
- (interrupt mask register)中断屏蔽(mask)寄存器:该寄存器与外部中断通过 与门相接,当该寄存器设置为1时,表明不要屏蔽该外部中断,当其发生时将其递交给 NVIC
- (pending request register)中断待处理(pending)请求寄存器:如果该外部中断经过了前述寄存器的层层考验,那么该寄存器会被设置(硬件设置);该寄存器与 NVIC对接,NVIC检测到该寄存器被设置,那么就会收到中断待处理请求。(相当于EXTI通知NVIC有外部中断了)
中断优先级层级划分
由于这一部分隶属于ARM核,因此需要查看ARM核相关的参考手册
STM32F10xxx/20xxx/21xxx/L1xxxx Cortex®-M3 programming manual
如果将这三个比特位配置为 011
,那么就不划分出子优先级,而只使用抢占优先级
中断优先级设置
[!NOTE]
其中八位的高四位([7:4])用来表示优先级的值,offset则对应中断号
代码实现(寄存器版本)
开启时钟
找到GPIO和AFIO外设挂载的总线:
增加芯片型号的定义 #define STM32F10X_HD
以开启宏定义RCC_APB2ENR_IOPFEN
#if !defined (STM32F10X_LD) && !defined (STM32F10X_LD_VL) && !defined (STM32F10X_MD) && !defined (STM32F10X_MD_VL) && !defined (STM32F10X_HD) && !defined (STM32F10X_HD_VL) && !defined (STM32F10X_XL) && !defined (STM32F10X_CL)
/* #define STM32F10X_LD */ /*!< STM32F10X_LD: STM32 Low density devices */
/* #define STM32F10X_LD_VL */ /*!< STM32F10X_LD_VL: STM32 Low density Value Line devices */
/* #define STM32F10X_MD */ /*!< STM32F10X_MD: STM32 Medium density devices */
/* #define STM32F10X_MD_VL */ /*!< STM32F10X_MD_VL: STM32 Medium density Value Line devices */
#define STM32F10X_HD /*!< STM32F10X_HD: STM32 High density devices */
/* #define STM32F10X_HD_VL */ /*!< STM32F10X_HD_VL: STM32 High density value line devices */
/* #define STM32F10X_XL */ /*!< STM32F10X_XL: STM32 XL-density devices */
/* #define STM32F10X_CL */ /*!< STM32F10X_CL: STM32 Connectivity line devices */
#endif
使能时钟:
// gpio clock
RCC->APB2ENR |= RCC_APB2ENR_IOPFEN;
// afio clock
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
GPIO配置
由于一端接3V3,因此GPIO可以配置成输入下拉,这样按下时能够产生一个上升沿:
并通过ODR将上下拉选择为下拉
// gpio config for PF10
GPIOF->CRH &= ~GPIO_CRH_MODE10; // input mode
GPIOF->CRH |= GPIO_CRH_CNF10_1; // input with pull-up/down
GPIOF->CRH &= ~GPIO_CRH_CNF10_0;
GPIOF->ODR &= ~GPIO_ODR_ODR10; // pull-down
AFIO复用输入为外部中断
接着我们需要打通GPIO输入到EXTI的通路
将外部中断线10(EXTI10)选择为PF10:
// afio config
AFIO->EXTICR[2] &= ~AFIO_EXTICR3_EXTI10;
AFIO->EXTICR[2] |= AFIO_EXTICR3_EXTI10_PF; // select PF as exti10
EXTI配置
对照其内部框图,看下哪些寄存器需要配置
解除中断屏蔽
启用上升沿触发
// exti config
EXTI->IMR |= EXTI_IMR_MR10; // unmask interrupt
EXTI->RTSR |= EXTI_RTSR_TR10; // rising edge trigger
NVIC配置
优先级层级设置
为外部中断号指定优先级
[!NOTE]
这里每8个bit对应一个中断号的优先级,因此使用了21个32bit的IPR寄存器来存储84个中断号对应的优先级
使能外部中断
[!IMPORTANT]
注意EXTI有中断屏蔽开关,NVIC也有中断使能开关;
[!NOTE]
一个bit存储一个中断对应的使能开关,使用了3个32bit的ISER寄存器来存储中断号0~67的使能开关
// nvic config
NVIC_SetPriorityGrouping(3); // only pre-priority
NVIC_SetPriority(EXTI15_10_IRQn, 3);
NVIC_EnableIRQ(EXTI15_10_IRQn); // response interrupt
定义中断处理函数
在 startup.s
中找到 EXTI10对应的中断函数名称的声明 EXTI15_10_IRQHandler
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
...
DCD EXTI15_10_IRQHandler ; EXTI Line 15..10
经过按键抖动判断后反转LED状态:
void EXTI15_10_IRQHandler(void) {
EXTI->PR |= EXTI_PR_PR10; // clear pending flag
delay_ms(10);
if (GPIOF->IDR & GPIO_IDR_IDR10) {
LED_Toggle(LED1);
}
}