自上篇文章STM32 非阻塞HAL_UART_Receive IT解析与实际应用,具体总结了HAL库下套娃函数中如何看清库函数的脉络,更细致的认识调用的过程,以解决潜在的问题。又又又遇到了新的问题(GPIO按键中断),感觉网络上和各种资料都没解释清楚的情况下。我又去扒了扒HAL库函数源码,提供不一样解决按键抖动的问题(个人认为除了那些通过配置具体寄存器的外;是从HAL库提供函数的基础上解决的)有别于那种在Callback返回函数中添加延迟和判断具体是否按下按键的方法(个人实际操作感觉这种方法效果也不好)。而是在HAL_GPIO库函数上的HAL_GPIO_EXTI_IRQHandler处理中断函数解决按键抖动带来的问题,是更根上解决问题的方法。
前言
Yume-知乎文章www.zhihu.com在这里,还是以我一贯的风格——不重复造轮子。具体如何配置STM32CubeMX初始化、生成工程目录之类的问题。包括关于STM32的Cortex-M3处理器的中断细节都不会在本文章中赘述,想了解的朋友可以自行去翻看“Cortex-M3 权威指南”、“Cortex-M3 技术手册”等相关技术文档,都有中文的翻译版本。本文的目的只在如何具体分析问题,解决问题。
1 STM32CubeMX中GPIO配置(基于正点原子的Stm32F1-Nano板)
像如何配置RCC、配置相关的时钟树、配置中断NVIC、配置中断输入等基础配置,生成工程文件的问题。这里就省略了,有需要的朋友可以参考其他网络上资料。重点就看看关于GPIO配置,便于后面分析讲解代码。
其中,PC0、PC1、PC2为LED灯,当按键触发中断时反转。PC8、PC9、PD2为共地按键(低电平有效),所以设置GPIO为Pull-up。按键的GPIO mode有两个为上升沿触发(Rising edge)和一个下降沿触发(Falling edge),这里其实上升沿或下降沿都没啥特别大的关系。
2 HAL库的GPIO中断响应过程
在初始化相关中断响应函数后,HAL库是如何进行中断响应的。其实和上篇文章
Yume:STM32 非阻塞HAL_UART_Receive_IT解析与实际应用zhuanlan.zhihu.com有很多相似的地方,可以借助上篇文章来理解。
同样的,GPIO的入口函数是什么呢?这里先引用“Cortex-M3权威指南”的一张图来简单说明Cortex-M3处理器的中断问题
可以看出Cortex-M3处理器有16个外部I/O中断,分别对应[0:15]端口上。也就是说每个中断端口号(0-15)可以在任一Port(A-G)上,然后通过选择器去决定中断端口采用哪个Port。当然这部分在STM32CubeMX上很容易就能配置并初始化好。自然就会有对应的Handler函数。那具体是啥,我们看看生成的工程文件中“Src”文件夹里的“stm32f1xx_it.c”中断文件中有这么一段注释
大致意思就是:STM32F1xx外部中断处理(Handlers)在“stratup file”中提及到了。有了这信息,再去看看在工程文件的根目录中“startup_stm32f103xb.s”文件(基于Nano版上的处理器)虽然里面是汇编语言,但通过查找“IRQHandler”可以看到
这7个就是外部I/O中断的入口函数(可以看出[0:4]端口是独立的入口,而[5:9]、[10-15]分别共用一个入口)。再看看“stm32f1xx_it.c”文件也能看到对应的函数定义
可以看到进入对应的I/O中断入口函数后就会传输GPIO_Pin口(用户配置的中断端口[0:15])到“HAL_GPIO_EXTI_IRQHandler()”中。那我们再看看这个IRQHandle函数
描述也很清晰地表明了这个函数功能是EXTI中断回应。那具体发生了什么呢?在函数里调用了“__HAL_GPIO_EXTI_GET_IT()”和“__HAL_GPIO_EXTI_CLEAR_IT()”后就调用“HAL_GPIO_EXTI_Callback”用户处理函数,有了解上篇文章
Yume:STM32 非阻塞HAL_UART_Receive_IT解析与实际应用
,应该已经清楚怎么回事了,这里就不赘述了。但关键问题是前面两个调用又是什么?
可以看到,这两并不是函数,而是一个宏。是干嘛的呢?大概可以看出是检查到底是哪个I/O端口触发中断的。所以这里就可以理解为啥[5:9]和[10:15]可以共用入口函数也不会出问题了。原因就在这里可以检查。
那再细致点的去看这两个宏定义,不难发现第一个是获取中断的标志位和确认中断是否产生,由谁产生。第二个则是清除标志位。通过查看“Cortex-M3 技术文档”的相关寄存器,也能更好地理解上面说的过程。
那现在应该很清楚发生了什么事了。发生中断响应时,通过入口函数“EXITx_IRQHandler”进入到“HAL_GPIO_EXTI_IRQHandler()”确认中断端口后,重置对应中断的标志位,进入到“HAL_GPIO_EXTI_Callback”用户处理函数中去。那我们只需要在用户处理函数中用switch语句选择不同I/O中断端口GPIO_Pin([0:15])对应不同中断处理任务就行了。例:
3 按键抖动问题分析与解决方案
那如果单纯在用户函数中类似上例中这么写,肯定会发现很严重的问题。发现按下去触发中断时可能会产生两次任务(预期是按下去就触发一次任务)。因为按键时会有抖动的,导致进入了两次中断,相信这不用我来解释为何按键抖动会引发这类的问题。
解决方案大家应该也清楚,要么通过计算添加合适大小电容消除抖动产生的影响,那另一种方案,也是最常见的方案就是添加去抖延迟。在网上也有很多类似的教程,甚至也是针对STM32的。会发现,他们都是在用户处理函数“HAL_GPIO_EXTI_Callback”中添加延迟,然后读取确认按键。那问题来了,通过上面一步一步分析HAL库的I/O中断处理过程,就知道在用户处理函数之前的“HAL_GPIO_EXTI_IRQHandler()”确认中断端口中就已经将中断标志位消除了(在用户处理函数之前),意味着抖动仍然能触发中断。然后再通过阅读相关文档,发现STM32中断是依靠向量表机制,也就是说只要触发了中断,一般情况下总是要去响应和清除相应的中断标志位。所以我认为在用户处理函数这么做可能可以解决问题,但以我个人经验,效果并不是很好,原因就是解决问题的方法不太对。
个人认为更正确的做法是在清除标志位之前延迟等待抖动消失,防止因抖动在此将中断标志位置为有效。即需要修改HAL库(Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_gpio.c)中的“HAL_GPIO_EXTI_IRQHandler”函数。如图,在“__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);”之前添加延迟“HAL_Delay((uint32_t)20);”
然后再用户处理函数中返回前添加延迟。就能比较好的解决按键抖动带来可能重复进入中断的问题。(虽然在我摧残下,还是有可能出现问题,但感觉基本上是能达到预期的,毕竟鼠标都可能因为微动问题出现双击呢)
想说的话
网络上有人批判HAL库效率、为非EE专业设计的,将所有东西都抽象了等一系列问题。但我想说的是,对于大多数普通用户或相关工作者。处理效率固然重要,但开发效率、移植效率也是需要实际考察的。会想起刚接触C51用汇编操作寄存器能时,我也会有点批判高级程序语言的效率各种不如汇编,不如汇编一步一步清晰。但问题是现在微处理器计算性能、寄存器数量也是远远超过以前。面对复杂的项目时,需要操作32位甚至64位总线宽度时,你还能处理各种复杂的关系么。
虽然我有时也觉得怎么一个中断都能套娃般弄得如此复杂,明明感觉有更简便的方法。但也意识到,之所以会弄这么复杂,一方面是为了提高开发者的开发效率,方便移植,另一方面也减少因为手误配置错寄存器,导致不可预知的后果。随着项目复杂度的上升,人为出错的概率也会上升,而用库相比去配置寄存器,出错的概率我认为是更低的。更别说在抽象后高层级的去思考能更好的完成某件任务的逻辑,而不是苦与寄存器相互如何作用的问题发愁。
总的来说,如果认为HAL库牺牲效率,甚至出现感觉不符合预期的Bug出现时无法像配置寄存器类似的方法排错。那我感觉可能就只是片面的去看HAL库函数就是黑盒,实际上这些函数都是能去追溯到具体的寄存器,在追溯的过程中慢慢的可能也能找到Bug的原因,在针对实际场景做修改也不是不可以的。
也许很多地方我表达的不专业,还请多多包涵。
Yume:STM32 非阻塞HAL_UART_Receive_IT解析与实际应用zhuanlan.zhihu.com