检查中断函数的目的是避免被低级错误浪费时间,比如,一开始用PA0 引脚输入中断信号,中断函数写的是EXTI0_IRQHandler
,后来引脚换成PA2 了,但是中断函数忘记改。
原理
引脚一般都是用宏或者常量定义的,比如:
//宏 PA0
#define INPUT_PIN_SOURCE GPIO_PinSource0
#deine INPUT_PORT_SOURCE GPIO_PortSourceGPIOA
// 或者常量
constexpr auto INPUT_PIN_SOURCE = GPIO_PinSource0;
constexpr auto INPUT_PORT_SOURCE = GPIO_PortSourceGPIOA;
拿着定义好的常量去调用EXTI 的函数,再定义好对应的中断处理函数:
void main() {
GPIO_EXTILineConfig(INPUT_PORT_SOURCE, INPUT_PIN_SOURCE);
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
// ...
}
void EXTI0_IRQHandler(void) {
// ...
}
像一开始说的,这里至少有两个容易错的地方,一个是INPUT_PIN_SOURCE
可能会和EXTI_Line0
不匹配,用了PA2 结果还是选择了Line0,之类的,拿一个函数包起来加一个参数检查就能解决,比较简单;另一个就是中断函数的名字也可以不匹配,而且就算写错了编译也能通过,只能在运行时感觉不对劲之后才能发现这里错了。
检查中断函数的原理也很简单,如果想用line0,就检查EXTI0_IRQHandler
有没有定义,方法就是在函数内原地引用一下这个函数的名字:
void main() {
// ...
extern void EXTI0_IRQHandler(void);
EXTI0_IRQHandler;
// ...
}
这行代码实际没干任何事,开启链接时优化-flto
就会被优化掉,但如果EXTI0_IRQHandler
没定义,会链接出错,提示:
Error: L6218E: Undefined symbol EXTI0_IRQHandler() (referred from main.o).
于是你就第一时间知道你写岔了。
宏实现
这种原理只能通过宏实现,先写一个宏函数:
#define init_pin_interrupt(pin_source, exti_line) \
static_assert(pin_source == exti_line, "mismatch between pin and exti line."); \
_MATCH_EXTI_PIN_INTERRUPT_HANDLER_CHECKER(exti_line); \
// ...
// 用法
init_pin_interrupt(INPUT_PIN_SOURCE, 0)
输入这两个参数,表示希望用exti 的line0,对应的引脚常量是INPUT_PIN_SOURCE
。在宏内部,先检查INPUT_PIN_SOURCE
是否等于exti_line
,保证两边是匹配的,然后可以把EXTI 的一部分初始化代码写在这个宏里面。
检查中断函数的功能调用了另一个宏函数来实现,原理是根据exti_line
的值,匹配具体负责引用中断函数的宏:
#define _MATCH_EXTI_PIN_INTERRUPT_HANDLER_CHECKER(exti_line) _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_##exti_line()
根据exti_line
的值,如果值是0,就会进一步调用_EXTI_PIN_INTERRUPT_HANDLER_CHECKER_0
这个宏,其他的同理:
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_0() \
extern void EXTI0_IRQHandler(void); \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wunused-value\"") \
EXTI0_IRQHandler; \
_Pragma("clang diagnostic pop")
实际的检查代码放在这个宏里,就是把引用中断函数的两行代码就地插入,如果EXTI0_IRQHandler
没定义,就引起一个链接错误。上面说过,EXTI0_IRQHandler;
这行代码实际不会做任何事,所以clang 编译器会给一个警告,说表达式的值没有被使用,宏里面额外加的三行代码就是用来临时关闭编译器的这个警告,只影响这一行代码,其他地方有没用的表达式的话编译器还是会照常警告。
因为EXTI 的通道5 至9、10 至15 是放在一起的,代码上可以简化一下,比如,对这两组只定义一个宏来检查:
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_9_5() \
extern void EXTI9_5_IRQHandler(void); \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wunused-value\"") \
EXTI9_5_IRQHandler; \
_Pragma("clang diagnostic pop")
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_15_10() \
extern void EXTI15_10_IRQHandler(void); \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wunused-value\"") \
EXTI15_10_IRQHandler; \
_Pragma("clang diagnostic pop")
然后再做一层映射:
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_5() _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_9_5()
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_6() _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_9_5()
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_7() _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_9_5()
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_8() _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_9_5()
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_9() _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_9_5()
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_10() _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_15_10()
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_11() _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_15_10()
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_12() _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_15_10()
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_13() _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_15_10()
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_14() _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_15_10()
#define _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_15() _EXTI_PIN_INTERRUPT_HANDLER_CHECKER_15_10()
实际测试过了,原理是没问题的。
兼容性
- 只适用于clang 编译器,确切的说是keil 的AC6 编译器,AC5 等其他的没测试;
- 只适用stm32f103 单片机,因为依赖官方的固件库,原理上其他单片机也能用;