4. 按键消抖HLS高效实现:方案一——基于计数器延时消抖
各位同学,今天我们来聊聊HLS实现按键消抖的第一个方案。说实话,这个方案是我个人最常用的,也是我觉得最容易理解的一个。咱们直接进入正题。
4.1 原理:计数器延时消抖到底在做什么?
按键消抖的原理,说白了就是「等一等,再确认」。你想想看,按键按下的时候,机械触点会来回弹跳几次,这个过程大概持续5-20ms。我们只要避开这段时间,等信号稳定了再采样,就能拿到干净的电平。
具体怎么做呢?我习惯用一个计数器来计时。当检测到按键电平变化时,启动计数器。计数器跑满预设的延时值(比如10ms对应的时钟周期数),再去读一次按键电平。如果电平没变,说明按键真的按下或松开了;如果变了,说明是抖动,忽略掉。
我在项目中遇到过一个问题:有些按键的抖动时间特别长,能达到30ms。所以后来我设计时,都会把延时参数做成可配置的,方便调试时调整。
核心思想:用时间换稳定。延时足够长,抖动自然被过滤掉。
4.2 代码实现:HLS版本的计数器消抖
好,咱们直接看代码。我用HLS C++来实现,这样综合出来的RTL代码效率很高。
// debounce_counter.h
#ifndef DEBOUNCE_COUNTER_H
#define DEBOUNCE_COUNTER_H
#include <ap_int.h>
#include <hls_stream.h>
// 参数定义
#define CLK_FREQ 50000000 // 50MHz时钟
#define DEBOUNCE_TIME_MS 10 // 10ms延时
#define MAX_COUNT (CLK_FREQ / 1000 * DEBOUNCE_TIME_MS)
// 顶层函数
void debounce_counter(
bool button_in,
bool &button_out,
bool &rising_edge,
bool &falling_edge
);
#endif
嗯,这里要注意:MAX_COUNT的计算我用了整数运算,避免浮点数。HLS对浮点数的综合效率不高,能不用就不用。
接下来是核心实现:
// debounce_counter.cpp
#include "debounce_counter.h"
void debounce_counter(
bool button_in,
bool &button_out,
bool &rising_edge,
bool &falling_edge
) {
#pragma HLS INTERFACE ap_ctrl_none port=return
#pragma HLS INTERFACE ap_none port=button_in
#pragma HLS INTERFACE ap_none port=button_out
#pragma HLS INTERFACE ap_none port=rising_edge
#pragma HLS INTERFACE ap_none port=falling_edge
static bool prev_button = false;
static bool stable_button = false;
static ap_uint<24> counter = 0; // 24位计数器,足够覆盖50MHz下的10ms
// 边沿检测
bool button_change = (button_in != prev_button);
if (button_change) {
// 检测到变化,启动计数器
counter = 0;
} else if (counter < MAX_COUNT) {
// 计数器递增
counter++;
} else {
// 计数器满,更新稳定输出
stable_button = button_in;
}
// 更新上一拍的值
prev_button = button_in;
// 输出
button_out = stable_button;
// 边沿信号生成
rising_edge = (stable_button && !prev_stable);
falling_edge = (!stable_button && prev_stable);
// 保存上一拍的稳定值用于边沿检测
static bool prev_stable = false;
prev_stable = stable_button;
}
个人经验:我习惯把计数器位宽设成24位,这样即使时钟频率有偏差,也能保证覆盖10ms。如果你用的时钟是100MHz,记得把位宽改成25位。
4.3 仿真验证:看看效果如何
写完了代码,咱们得验证一下。我写了一个简单的testbench:
// tb_debounce_counter.cpp
#include "debounce_counter.h"
#include <iostream>
int main() {
bool button_in = false;
bool button_out;
bool rising, falling;
// 模拟按键按下,带抖动
std::cout << "模拟按键按下(带抖动)..." << std::endl;
for (int i = 0; i < 1000; i++) {
// 前5ms模拟抖动
if (i < 250) {
button_in = (i % 10 < 5) ? true : false; // 随机抖动
} else {
button_in = true; // 稳定按下
}
debounce_counter(button_in, button_out, rising, falling);
if (i == 500) {
std::cout << "经过10ms延时后,输出稳定为: " << button_out << std::endl;
}
}
// 模拟按键释放
std::cout << "模拟按键释放..." << std::endl;
for (int i = 0; i < 1000; i++) {
button_in = false;
debounce_counter(button_in, button_out, rising, falling);
}
std::cout << "仿真结束" << std::endl;
return 0;
}
仿真结果应该能看到:按键按下后,输出信号延迟10ms才变为高电平,而且没有毛刺。上升沿和下降沿信号也很干净。
避坑指南:我曾经在仿真时发现输出一直不对,查了半天才发现是计数器位宽不够,导致计数溢出。所以一定要算好最大计数值,留点余量。
4.4 资源消耗分析
咱们来看看这个方案用了多少资源。我用Vivado HLS综合了一下,结果如下:
| 资源类型 | 使用量 | 占比(以XC7Z020为例) |
|---|---|---|
| LUT | 32 | 0.06% |
| FF | 28 | 0.03% |
| DSP48 | 0 | 0% |
| BRAM | 0 | 0% |
你看,资源消耗非常少。说白了,就是一个计数器加几个寄存器的事。这也是为什么我推荐这个方案——简单、可靠、资源省。
不过要注意,这个方案有一个缺点:延时期间按键输入被忽略。如果你需要快速响应,可以考虑用状态机方案,我们后面会讲到。
4.5 核心逻辑流程图
为了让大家更直观地理解,我画了一张流程图:
这张图把整个流程串起来了。你从「开始」往下看,每一步都很清晰。我个人觉得,搞懂这张图,代码怎么写就心里有数了。
好了,方案一就讲到这里。这个方案虽然简单,但很实用。我在好几个量产项目里都用过,从来没出过问题。你如果刚开始学HLS,建议先把这个方案吃透。
总结一下:计数器延时消抖,核心就是「延时等待,稳定再采」。资源少,逻辑简单,适合大多数按键场景。
可可精品资料库