韦东山stm32hal库--定时器喂狗模型按键消抖原理+实操详细步骤

一.定时器按键消抖的原理:

按键消抖的原因:

image-20241128221138526

当我们按下按键的后, 端口从高电平变成低电平, 理想的情况是, 按下, 只发生一次中断, 中断程序只记录一个数据.

但是我们使用的是金属弹片, 实际的情况就是如上图所示, 可能会发生多次中断,难道我们要记录3/4次数据吗?

答:按键按下的时候, 会有机械振动, 这个震荡也会引起单片机io口变化, 从而实现多次触发按键中断,实际我们只需要一次按键中断, 通过科学家的研究, 按下按键到按键平稳, 这个时长是10ms

处理方法(1)

所以我们一般情况下, 是当检测到按键电平变化的时候, 延时10ms后, 然后才真正判断, 按键是否真正按下, 从而把这个按键震荡周期躲了过去

分析第一种方法

​ 第一种延时躲避按键震荡周期的方法, 通过强行让单片机死循环10ms, 来进行实现的。有一个弊端就是延时的这10ms,我们是做不了事情的, 单片机只能傻傻的停留在那里, 如果我们多按下几次, 那系统不就卡爆了。一般按键的事件, 都不是特别紧急的,开发者开发系统的时候, 留给用户的按键, 都是保证绝对安全的, 所以按键的优先级不能太高。程序的运行应该留给更重要并且能够保证系统安全的功能。

这个时候再来看, 我们按下按键程序竟然卡死了10ms, 这个不是很严重的事情吗? 所以我们要节省这10ms, 必须用定时器来做,意思就是系统该干什么就干什么, 我买了一个秒表,按下按键后开始计时, 到点了(震荡周期过了)提醒系统处理按键中断就行了。

处理方法(2)定时器优化

现实因素:

我们按下按键, 不可避免的会产生, 按键机械振动

实际需求:

当按下按键, 按键平稳的时候, 我们才认定是按下了按键, 我们就进入中断去处理。

分析因素和需求之间的困难

因素:按键振动的不可避免

需求: 按键平稳才处理按键事件

困难: 常用方法解决的弊端(ctrl 加鼠标左键,快速跳转)

确定真正软件层面的需求

了解了这中间的矛盾, 我们仔细分析按键的整个过程

image-20241128095639305

我们需要的是按键平稳的时候, 再处理按键, 所以如果我们精准的找到最后一次按键按下的时机,那么不就是可以了吗?

所以现在我们的需求就变成了,如何精准的找到,按键彻底按下的时机,也就是按键按下到按键彻底闭合, 这之间虽然会产生按键振动,也就是上图所示的①②③④, 这些电平都会被我们的单片机捕捉到,识别到io口电平变化,但这只是蜻蜓点水,不是真正的按下, 我们要的是稳稳地幸福。

解决方案

(1)逃避法(ctrl 加鼠标左键,快速跳转)
(2)按键中断+定时器实时扫描法(喂狗模型)

通过了解方案(1), 我们不能进行逃避, 所以要精准的识别到最后一次按键,然后触发事件就行了.

那如何判断, 最后一次按键,就是最后一次呢?

观察最后一次按键抖动的特点:

最后一次按键按下后, 就是彻底的闭合了,因为按键抖动的时长最长是10ms,所以按键按下,然后电平保持稳定10ms,就可以认定是此次按键事件触发。

但是按键按下的信息都一样, 都是电平变化, 不管电平变化间隔的时间长短,都会触发if(电平变化){代表按键按下}

所以我们就需要, 在每一次按键按下后, 都开始计时10ms,

按键识别分成两种情况:

(1)这个按键是抖动

虽然是抖动, 我们一视同仁, 也开始用定时器计时从零计时10ms,然后还没来得及计时到 10ms

下次抖动就触发了, 然后就把这个计时抛弃了, 再次刷新定时器从头开始计时,判断下次的抖动是否是最后一次按键

(2)最后一次按键来临

通过过滤上面的抖动按键(每次新的按键来临,并且这个按键定时器计时不超过10ms),终于等到你(最后一次稳稳地幸福),那么我们还是开始计时, 此时定时器就计时到了10ms,然后我们就去处理按键事件就行了。

二.代码执行方案

测试真的存在按键抖动问题

1.首先解压打开0602_key_isr_oled.7z,然后改名为 0603_key_timer

0602工程

密码:5bpw

image-20241128143947105

2.打开工程

image-20241128144016652

3.找到中断函数

image-20241128144158455

4.当发生按键中断的时候, 按键被调用

image-20241128144310580

5.我们会看到中断调用了回调函数, 我们接着f12进入

image-20241128144343053

6.这里就是我们之前自己写的中断回调函数,之前我们是点亮led,现在我们开始记录按键次数, 看看是否存在按键抖动现象

int g_key_cnt = 0;
void HAL_GPIO_EXTI_Callback()
{
    g_key_cnt++;
    
}    
image-20241128144609622

7.我们现在开始测试, 那现在我们就按下按键, 查看这个g_key的值, 怎么显示呢? 我们就用OLED函数吧, 我们用之前的OLED函数

image-20241128144913684

8.复制初始化函数, 拿到main函数里面

image-20241128145006417 image-20241128145114198

9.先显示 一串字符串, 表明这个是我们的按键次数, 复制示例代码里面的函数

image-20241128145258402

image-20241128145344355

10.然后在while循环里面, 重复的进行, 显示按键次数

image-20241128145500410

11.编译运行, 发现没有oled函数头文件, 所以我们把路径, 加入到环境变量里面(只指定目录即可)

image-20241128150800051

12.然后我们烧录, 按下按键, 观察现象(我们只是按下松开一次, 数值就增加了好多次)

定时器解决按键抖动问题

1.我们进入启动文件, 来定位到我们的滴答定时器计数中断,然后f12进入

image-20241128151126074

2.每一毫秒运行一次

image-20241128151501397

3.f12接着进入, 我们再看看, 他真的是只增加一个计数值而已

image-20241128151547431

4.那么如何获得计数值呢?

image-20241128151650190

5.当我们按下或者松开按键的时候, 我们回调函数被调用

image-20241128151809055

6.每当有按键按下(即使这个按键是按键抖动), 我们都要去刷新修改定时器的计数值, 从而实现上文所说的(精确的检测到最后一次按键按下)

image-20241128152436131

7.当最后一次按键按下的时候, 然后等待10ms, 定时器超时, 我们就要进行按键事件的处理

image-20241128152746338

8.什么是timer, 所以我们就需要在main.c里面声明一个结构体.

我们想一下, 我们计时需要哪些变量, 我们把他们放在一个结构体里面就行了

① 超时时间: 在我们SysTick里面, 我们每次过1ms, 都会增加一个计数值,

uint32_t timeout; //当前uwTick + 某一个数值

② 想做的通用一点的话, 来个参数

void *arg;

③ 调用什么函数

void (*func)(void *);

9.我们在main.c里面定义一个结构体

struct soft_timer{
    uint32_t timeout;
    void * args;
    void (*func)(void *);
};
image-20241128161125704

10.我们再根据这个结构体,来定义一个按键的结构体:

第一个超时时间,我们对0取反,就相当于一个巨大的数

第二个我们定义成引脚吧, 先设置成NULL

第三个,就是我们按键事件触发,调用的函数, 我们还没有写, 先欠着

struct soft_timer key_timer = {~0, NULL, key_timeout_func};
image-20241128161250984

11.之前我们发生按键操作的时候, 我们是修改led, 现在是来修改这个结构体里面的超时时间, 也就是我们之前说的, 按键来了(不管这个按键是抖动还是最后一次按键), 我们都进行刷新定时器计数(也就是结构体里面的超时时间)

我们首先传入此时的滴答计数器的数值 , 再传入10ms这个参数, 把超时时间设置成 10ms后,

后面我们定时器里面就每毫秒查询这个 超时时间和滴答定时器的数值, 如果到达,则代表按键平稳是最后一次按键, 如果没达到, 就会被下次按键刷新

image-20241128163408522
image-20241128163440611
mod_timer(&key_timer, 10);

12.我们按键中断函数里面, 已经设置好了超时时间, 那么检测是否超时的任务,就交给定时器中断了, 我们进入此定时器中断函数,进行设置检测是否超时函数

extern void check_timer(void);//声明一下这是外部函数
check_timer();	//每毫秒都调用检测是否超时, 从而实现按键抖动过滤

image-20241128165028123

13.下面我们来完善这些代码

mod_timer(&key_timer, 10);//修改超时时间

void mod_timer(struct soft_timer *pTimer, uint32_t timeout)
{
 pTimer->timeout = HAL_GetTick() + timeout;
}    

超时时间等于 10ms, 我们传入的是当前的按键结构体, 和设置的超时时长(10ms)

我们结构体的超时时间, 需要和定时器的滴答计数器对比,

所以我们的超时时间 = 当前Tick时间 + 超时时长

image-20241128170306372

也就是 pTimer->timeout = HAL_GetTick() + timeout;

image-20241128170112347

14.接下来我们写check_timer();

image-20241128170732504
void check_timer(void)
{
    if(key_timer.timeout <= HAL_GetTick())
    {
        key_timer.func(key_timer.args);
    }
}    

我们定时器, 一直在检测, 滴答定时器是否达到超时时间, 在按下按键的时候, 刚开始会产生抖动, 也就是我们还没达到超时时间, 就又触发按键中断, 那么按键中断里面的

mod_timer(&key_timer, 10); 函数,就会刷新超时时间, 从而使定时器无法达到超时时间

也就进入不了 if(key_timer.timeout <= HAL_GetTick())

只有最后一次按键触发,电平稳定,也就是达到超时时间, 才能够触发我们的按键处理事件

也就是 我们调用了结构体里面的按键函数事件 key_timer.func(key_timer.args);

15.定时器通过调用 check_timer() 函数检测uwTick是否达到超时时间, 如果达到, 就代表着是最后一次按键事件, 我们就调用结构体里的key_timer.func(key_timer.args);

我们在这里处理的就是 , 对按键计数值加一

一次按键中断, 累加一次, 并不会重复触发中断,从而实现了消抖

当然, 需要注意的一点是, 我们一次按键事件触发后, 在key处理函数中, 要记得清除超时时间, 因为定时器是一直工作的, 要么按键事件处理完后,我们就把key_timer.timeout设置成一个很大的值.定时器就不会再超时触发按键事件了, 直到下次按键再次按下, 重新设置超时时间.

image-20241128200123621

void key_timeout_func(void *args);

void key_timeout_func(void *args)
{
    g_key_cnt++;
    key_timer.timeout = ~0;
}    

16.烧录运行, 发现按键按下一次, 触发一次

测试成功的工程

1732801940901 (1)
<think>好的,我现在需要帮助用户解决关于STM32旋转编码器按键功能的实现细节和调试方法的问题。首先,用户提到了使用HALstm32f1xx_hal,具体函数是RotaryEncoder_Get_Key。我需要先确认这个函数是否是HAL中的标准函数,或者是否是用户自定义的。 首先,我应该回忆HAL中是否有直接处理旋转编码器的标准函数。通常,旋转编码器涉及GPIO和定时器的配置,尤其是编码器模式。HAL中的定时器配置可能有编码器接口的支持,例如使用TIMx的编码器模式。但是按键功能可能是指旋转编码器上的按压开关,这通常是一个独立的GPIO输入。 用户提到的RotaryEncoder_Get_Key可能是一个自定义函数,用于读取按键状态。我需要检查HAL的文档,确认是否存在这样的函数。如果不存在,那可能是用户自己实现的,需要指导如何编写这样的函数。 接下来,我需要考虑旋转编码器的工作原理。旋转编码器通常有两个信号线(A和B相)用于检测旋转方向和步数,以及一个按键信号线(SW)作为按钮。旋转部分可以通过定时器的编码器模式来处理,而按键部分则是一个普通的GPIO输入,可能需要去动处理。 在HAL中,配置定时器为编码器模式是关键步骤。例如,使用TIM2或TIM3,配置通道1和通道2为编码器模式,这样定时器会自动根据A、B相的边沿变化来计数,从而确定旋转方向和步数。这部分需要使用HAL_TIM_Encoder_Init函数,并启动编码器接口。 对于按键部分,需要配置对应的GPIO引脚为输入模式,并启用内部上拉或下拉电阻。读取按键状态时,需要处理机械动,可以通过软件延时或硬件滤波来实现。HAL提供了GPIO读取函数HAL_GPIO_ReadPin,可以在这里使用。 调试方法可能包括使用逻辑分析仪或示波器检查A、B相和按键信号的波形,确保没有噪声或动问题。另外,使用ST-Link和STM32CubeIDE的调试功能,设置断点,观察变量的变化,比如旋转计数值和按键状态的变化。 用户可能需要示例代码来展示如何初始化和读取旋转编码器及按键。例如,配置定时器为编码器模式,初始化GPIO输入,然后在主循环中读取计数值的变化和按键状态。同时,可能需要中断或DMA来处理按键输入,确保实时响应。 此外,还需要考虑旋转编码器的类型,是增量式还是绝对式,这里通常是增量式的,所以需要处理方向判断和步进计数。按键部分可能需要检测按下、释放、长按等不同状态,这需要状态机的设计。 总结步骤:配置定时器编码器模式,配置按键GPIO,实现按键读取函数(包括去动),在主循环或中断中处理旋转和按键事件,调试时检查信号和代码逻辑。</think>### STM32旋转编码器按键功能实现与调试 #### 一、旋转编码器基础原理 旋转编码器包含旋转检测(A/B相脉冲)和按键(SW引脚)两部分功能。A/B相通过正交编码产生脉冲信号,按键为独立机械开关[^1]。旋转方向通过两相信号相位差判断,按键需处理机械动。 #### 二、HAL实现步骤 1. **硬件配置** - **旋转检测**:使用定时器编码器模式(如TIMx) ```c TIM_Encoder_InitTypeDef sConfig = {0}; sConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 正交编码模式 sConfig.IC1Polarity = TIM_ICPOLARITY_RISING; sConfig.IC2Polarity = TIM_ICPOLARITY_RISING; HAL_TIM_Encoder_Init(&htim3, &sConfig); HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL); ``` - **按键检测**:配置GPIO输入模式(带内部上拉) ```c GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); ``` 2. **按键状态读取** ```c // 自定义按键检测函数(含) RotaryEncoder_KeyState RotaryEncoder_Get_Key(void) { static uint8_t debounce_cnt = 0; static GPIO_PinState last_state = GPIO_PIN_SET; GPIO_PinState current = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); if (current != last_state) { debounce_cnt = 10; // 10ms last_state = current; } else if (debounce_cnt > 0) { debounce_cnt--; } return (debounce_cnt == 0) ? current : KEY_NO_PRESS; } ``` 3. **旋转方向检测** ```c int16_t Get_EncoderDiff(void) { static int16_t last_cnt = 0; int16_t current_cnt = TIM3->CNT; int16_t diff = (current_cnt - last_cnt) / 4; // 4步/周期 last_cnt = current_cnt; return diff; } ``` #### 三、调试方法 1. **信号完整性检查** - 使用逻辑分析仪捕获A/B相波形,验证相位差是否符合正交编码特性(90度相位差) - 检查按键信号是否存在动(正常按压时长>20ms) 2. **寄存器级调试** ```c // 查看TIM3计数器值 volatile uint32_t encoder_cnt = TIM3->CNT; // 检查GPIO输入寄存器 volatile uint32_t key_state = GPIOA->IDR & GPIO_PIN_0; ``` 3. **HAL调试技巧** ```c // 启用定时器溢出中断 HAL_TIM_Base_Start_IT(&htim3); // 在中断回调函数中记录事件 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { encoder_overflow_count++; } } ``` 4. **CubeMX配置验证** - 确认TIMx的Encoder Mode已启用 - 检查GPIO模式设置为输入模式并启用上拉电阻 - 验证时钟树配置满足编码器最大转速需求 #### 四、典型问题排查表 | 现象 | 可能原因 | 解决方法 | |---------------------|-------------------------|----------------------------| | 旋转方向检测反向 | A/B相序接反 | 交换A/B相接线或修改解码逻辑 | | 按键响应不灵敏 | 时间过长/过短 | 调整debounce_cnt初始值 | | 高速旋转时计数丢失 | 定时器溢出频率不足 | 降低编码器分辨率或提高时钟 | ####
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值