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,建议先把这个方案吃透。

总结一下:计数器延时消抖,核心就是「延时等待,稳定再采」。资源少,逻辑简单,适合大多数按键场景。

可可精品资料库