前言
首先铺垫一个场景,研发需求是用户通过按下按键来和屏幕上的UI画面进行交互,这里我就以按下按键后按键值自增为例。我们定义了一个KeyCount计数值来统计按下按键的次数,如果我们在主进程之中检测按键连接的GPIO口的电平变化,毫无疑问按键计数值的自增会有延迟,因为While循环的执行需要机器时间。这时自然而然联想到中断检测IO口,但新的问题产生了,因为按键的机械触点在按下后会产生简谐振动,电平的变化会有一段毛刺。放任不管的话我们按下一次按键,可能计数值跳变了三次。从硬件角度来解决可以考虑并联一个滤波电容,而从软件角度来讲,我们需要定时器延时让这段电平毛刺结束,这里我来给大家介绍软件定时器。
虽然叫软件但是也还是需要依赖SysTick系统滴答计时器(一个Tick用时1ms的那位),因为这样比while循环里面定义变量自减精准太多了。精确程度排名:SysTick系统滴答计时器>TIM计时器>>纯软件计时器,因为SysTick系统滴答计时器的时钟源直接来自ARM内核。
肯定有读者老爷要说了番茄你又在Ⅹ上雕花了,你直接用HAL_Delay不行么?
(参考拙作stm32外部中断-CSDN博客里的实验,ok我承认这是回旋镖来的最快的一集)
还真不行,按键的机械触点振动时间可能在5~10ms之间,单纯的delay死等可能会严重影响使用体验,尤其是涉及到和屏幕复杂UI交互的场景(比如游戏手柄和主机之间的通信)。咱们这儿可以利用编程控制超时时间的绝对值(当然是借鉴了RTOS里面vTaskDealyUntil的思路不是我自创)。
废话不多说,下面开启正文。
需求梳理
我们的实验目的是让单片机准确地把按下按键电平跳变的次数打印在屏幕之上,且不依赖阻塞延时死等解决。所以我们就需要构造一个“虚拟外设”,叫做软件定时器。在当前场景下,它的内部应该至少包含着一个变量用来存储溢出时间,以及一个用来和GPIO输入中断进行交互的接口函数。(这个接口函数里面至少要做到两件事,第一是按键值的自增(业务逻辑),第二是将延时时间恢复至默认状态。)在C语言环境下一个很容易联想到用struct构造结构体来包含这个数值变量和接口函数,具体的结构体成员我们需要在下文继续分析。
本质上来讲这个功能的实现是依靠System Tick中断嵌套GPIO输入中断,System Tick中断每1ms发生一次的时候对定时时间的溢出发起质询(注意这个质询下文有用),如果定时时间的溢出发生了(即当前实际Tick值超出了软件定时器的溢出时间),那么软件定时器执行交互接口函数。这个定时时间的来源是GPIO输入中断,一旦GPIO口检测到了上升沿/下降沿,那么中断回调函数里面会触发修改定时时间的函数,先获取当前的Tick数值同时加上想要延时的时间。注意GPIO输入中断优先级此时应该高于System Tick中断,按键的频次再怎么高也不会超过1ms。
下面开始介绍详细步骤。
第一步自然是设置GPIO为输入中断模式,咱们按下按键后IO口检测到电平变化,我这里上升/下降边沿都检测,所以按下按键之后计数值会+2。
至于中断优先级设置,事实上可以保持默认。
第二步是进入中断的时候软件延时10ms等待机械触点可靠接触。
我们需要构造一个函数修改超时时间,因此需要传入两个参数,一个是我们的软件定时器,一个是超时时间的数值。现在我们来参考HAL库函数操作硬件定时器TIMx的办法,首先是设备头文件里通过宏定义规定了TIMx们的硬件地址:
其次是通过构造结构体,定义句柄htimx来操作时钟的参数:
TIM_HandleTypeDef htim7;
htim7.Instance = TIM7;
htim7.Init.Prescaler = 3999;
htim7.Init.CounterMode = TIM_COUNTERMODE_UP;
htim7.Init.Period = 499;
htim7.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
其中TIM_HandleTypeDef定义(节选):
typedef struct
#endif /* USE_HAL_TIM_REGISTER_CALLBACKS */
{
TIM_TypeDef *Instance; /*!< Register base address */
TIM_Base_InitTypeDef Init; /*!< TIM Time Base required parameters */
HAL_TIM_ActiveChannel Channel; /*!< Active channel */
DMA_HandleTypeDef *hdma[7]; /*!< DMA Handlers array
This array is accessed by a @ref DMA_Handle_index */
HAL_LockTypeDef Lock; /*!< Locking object */
__IO HAL_TIM_StateTypeDef State; /*!< TIM operation state */
__IO HAL_TIM_ChannelStateTypeDef ChannelState[4]; /*!< TIM channel operation state */
__IO HAL_TIM_ChannelStateTypeDef ChannelNState[4]; /*!< TIM complementary channel operation state */
__IO HAL_TIM_DMABurstStateTypeDef DMABurstState; /*!< DMA burst operation state */
} TIM_HandleTypeDef;
OK现在我们自己来构造软件计时器,首先它不需要硬件地址也就不用宏定义一个了;其次是操作句柄,我们也通过struct关键字定义结构体来实现。现在结构体成员的需求已经分析明确了,它需要的结构体成员有一个整数变量代表超时时间,一个超时之后的交互接口(函数),一个传递给接口的参数。
第三步是在按键的中断回调函数之中,修改软件定时器的超时时间为10个Tick(10ms),也需要封装一个函数实现就叫mod_timer。mod_timer就需要上一步创建的结构体发力了,结构体中的成员“超时时间”需要发生自增,先用HAL库的函数获取当前的系统Tick数,然后加上这10个Tick的超时时间。
第四步是软件定时器超时之后令按键计数值自增,同时恢复超时时间为默认值(与第二步配合,在那个交互接口之中实现)。
编程思路
查阅stm32的启动文件(汇编语言)中的中断向量表得知系统滴答计时器的中断处理函数是 SysTick_Handler;
跳转定义得知它在it.c中被定义。
函数内部每一毫秒调用一次HAL_IncTick进行Tick数自加,这个Tick数可以通过HAL_GetTick函数进行获取。同时上文说的定时器质询也需要在这里发生,检测软件定时器的定时时间是否被实际的系统Tick数超越。
跳转到HAL_IncTick的定义,同时也找到了获取系统当前Tick数的函数HAL_GetTick。
ok,现在可以说是成竹在胸了,开始动手写代码。
定义全局变量按键次数;
int g_key_cnt = 0;
声明一个接口函数;
void key_timeout_func(void *args);
构建软件定时器的结构体,如上文所诉包含超时时间和一个函数指针,指向上面的接口函数;
typedef struct software_timer {
uint32_t timeout;
void * args;
void (*func)(void *);
}soft_timer;
超时时间要尽量大,不然会被系统GetTick轻松超越,我这里设置为了~0,在32位平台中代表0xFFFFFFFF;
soft_timer key_timer = {~0, NULL, key_timeout_func};
接口函数定义为静态的吧,不想暴露出去,里面的业务是按键值的自增以及让软件定时器的超时时间恢复默认值;
static void key_timeout_func(void *args)
{
g_key_cnt++;
key_timer.timeout = ~0;
}
接下来封装给GPIO输入中断使用的接口函数,软件定时器的超时时间修改为当系统Tick数加上若干个延时Tick;
void mod_timer(soft_timer *pTimer, uint32_t delayTICK)
{
pTimer->timeout = HAL_GetTick() + delayTICK;
}
最后就是封装在SysTick_Handler里面结束Tick自增后的质询函数,检查当前的系统Tick数有没有超过软件定时器的超时时间;
void check_timer(void)
{
if (key_timer.timeout <= HAL_GetTick())
{
key_timer.func(key_timer.args);
}
}
修改GPIO输入中断的中断回调函数;
/* USER CODE BEGIN 1 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_14)
{
mod_timer(&key_timer, 10);
}
}
/* USER CODE END 1 *
最后就是在main函数里面写屏幕显示的逻辑。
附录:整体代码
SoftTimer.c
#include "SoftTimer.h"
int g_key_cnt = 0;
void key_timeout_func(void *args);
soft_timer key_timer = {~0, NULL, key_timeout_func};
static void key_timeout_func(void *args)
{
g_key_cnt++;
key_timer.timeout = ~0;
}
void mod_timer(soft_timer *pTimer, uint32_t delayTICK)
{
pTimer->timeout = HAL_GetTick() + delayTICK;
}
void check_timer(void)
{
if (key_timer.timeout <= HAL_GetTick())
{
key_timer.func(key_timer.args);
}
}
SoftTimer.h
#ifndef __SoftTimer_H
#define __SoftTimer_H
#include <stdint.h>
#include <stdio.h>
typedef struct software_timer {
uint32_t timeout;
void * args;
void (*func)(void *);
}soft_timer;
extern int g_key_cnt;
extern uint32_t HAL_GetTick(void);
extern soft_timer key_timer;
void mod_timer(soft_timer *pTimer, uint32_t timeout);
void check_timer(void);
#endif
stm32f1xx_it.c
HAL库编程就是在CubeMX自动生成的基础上面修改三个地方
①添加头文件
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "SoftTimer.h"
/* USER CODE END Includes */
②修改SysTick_Handler函数
/**
* @brief This function handles System tick timer.
*/
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
check_timer();
/* USER CODE END SysTick_IRQn 1 */
}
③GPIO输入中断回调函数
/* USER CODE BEGIN 1 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_14)
{
mod_timer(&key_timer, 10);
}
}
/* USER CODE END 1 */
main.c
①包含头文件
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "SoftTimer.h"
#include "driver_oled.h"
/* USER CODE END Includes */
②屏幕初始化
/* USER CODE BEGIN 2 */
OLED_Init();
OLED_Clear();
OLED_PrintString(0, 4, "Key ISR cnt = ");
/* USER CODE END 2 */
③while循环显示全局变量
/* USER CODE BEGIN WHILE */
while (1)
{
OLED_PrintSignedVal(14, 4, g_key_cnt);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */