从近似0基础开始FPGA开发 -- part.6 逻辑设计与状态机

PL功能设计

本篇以一个简单的纯逻辑工程作为案例,穿插介绍一些FPGA逻辑开发的设计思路。先构思一个基础功能:以开发板上的按钮为控制输入,让开发板上的两颗LED灯在每次按下时交替闪烁

下面对这需求进行拆解:

  1. 工程以外部的高低电平切换作为信号输入,对应的FPGA引脚约束至开发板外部按钮;
  2. 逻辑工程内部对输入的数字信号进行处理,识别信号翻转;
  3. 两颗LED的交替闪烁对应输出信号的0-1翻转,位宽为2的寄存器从0开始持续累加即可实现比特位交替为1;
  4. 逻辑工程需在识别到的输入信号翻转时,对控制LED引脚的寄存器进行累加操作

完成以上拆解后,其实逻辑的数字部分功能就已经很清晰了。

但是,在做FPGA设计时,如果只考虑数字部分那工作只算做了一半,因为代码里的所有信号,最终都会变为芯片的一根根翻转的信号线,如果不考虑模拟部分的电气属性,往往会埋下很多的隐患。

按键消抖

开发板上使用的机械按键是按键的一种,特点是:接触电阻小,按键按下或弹起时有滴答的清脆声。但由于其构造和原理,在按键闭合及断开的瞬间均伴随有一连串的抖动。

按键的机械特性,决定着按键的抖动时间,产生的抖动次数以及间隔时间均是不可预期的,一般抖动时间在5ms~10ms。通常检测到输入信号状态为低电平时表示按键按下,当 FPGA 的晶振频率为 100MHz,即系统时钟 10ns时,以 10ns 的周期去采样信号的状态,由于采样时间远小于按键抖动时间,FPGA 在检测信号状态时会认为按键多次被按下

这就需要通过消除抖动,以确保按键被按下时只检测到一次低电平,意味着每次在按键闭合或松开期间,跳过这段抖动时间,再检测按键的状态。实际工程中,有很多消抖方案,按键消抖可分为硬件消抖和软件消抖。硬件消抖主要使用 RS 触发器或电容等方法在硬件电路上实现消抖,一般在按键较少时使用。软件消抖可以通过简单的延时实现按键的消抖动,也可以通过状态机的方式完成。 本文重点介绍利用 FPGA 内部来设计消抖,即采取软件消抖。

延时设计

一般机械按键在被按下和释放时,产生的抖动不会超过 20ms,因此可以通过简单的延时来检测,当检测到输入信号为低电平时,开始计时 20ms,计时结束后检测此时按键信号状态,当信号状态为高电平时,表示按键抖动过程,等待下一次低电平到来后再次开始计时检测按键信号状态;当信号为低电平时,表示按键为按下状态,产生一个脉冲,表示按键按下状态。当检测到输入信号为高电平时,开始计时 20ms,计时结束后检测此时按键信号状态,当信号状态为低电平时,表示按键抖动过程,等待下一次高电平到来后再次开始计时检测按键信号状态;当信号为高电平时,产生一个脉冲,表示按键释放状态。

在进行逻辑设计时,代码最好按照功能进行划分,这在复杂的工程下利用解耦定位问题。

这里给出一段按键消抖代码参考

复制代码
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// -------------------------------------------------------------------
// 模块名:debounce
// 功能:按钮输入消抖,含亚稳态同步
// 时钟:100MHz (周期10ns)
// 消抖时间:20ms (计数器计数值 = 20_000_000)
// -------------------------------------------------------------------

module debounce (
    input  wire clk,      // 100MHz 系统时钟
    input  wire rst_n,    // 低电平有效异步复位
    input  wire btn_in,   // 原始按钮输入(高电平有效)
    output reg  btn_out   // 消抖后的稳定输出
);

    // -----------------------------------------------------------------
    // 参数定义
    // -----------------------------------------------------------------
    parameter DEBOUNCE_MS = 20;              // 消抖时间(毫秒)
    localparam CLK_FREQ = 100_000_000;       // 时钟频率 100MHz
    localparam MAX_CNT = CLK_FREQ / 1000 * DEBOUNCE_MS - 1; // 20ms对应计数值

    // 计数器位宽:2^22 = 4,194,304 > 20,000,000,因此22位足够
    reg [21:0] cnt;

    // 两级同步寄存器,用于亚稳态处理
    reg btn_sync1, btn_sync2;

    // 同步后的按钮信号(已消除亚稳态)
    wire btn_sync = btn_sync2;

    // -----------------------------------------------------------------
    // 亚稳态处理:两级寄存器同步
    // -----------------------------------------------------------------
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            btn_sync1 <= 1'b0;
            btn_sync2 <= 1'b0;
        end else begin
            btn_sync1 <= btn_in;
            btn_sync2 <= btn_sync1;
        end
    end

    // -----------------------------------------------------------------
    // 消抖逻辑:当同步输入与当前输出不一致时启动计数器
    // 若输入持续稳定达到消抖时间,则更新输出;否则计数器清零重计
    // -----------------------------------------------------------------
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt     <= 22'd0;
            btn_out <= 1'b0;           // 复位默认输出低电平
        end else begin
            // 如果同步输入与当前输出状态不同,说明可能发生按键变化
            if (btn_sync != btn_out) begin
                // 计数器未满则继续累加
                if (cnt < MAX_CNT) begin
                    cnt <= cnt + 1'b1;
                end else begin
                    // 计数满,确认电平稳定,更新输出
                    btn_out <= btn_sync;
                    cnt     <= 22'd0;  // 复位计数器,为下一次变化做准备
                end
            end else begin
                // 输入与输出一致,说明状态已稳定,计数器清零
                cnt <= 22'd0;
            end
        end
    end

endmodule

在上述代码中可以看到,对与外部输入的信号,并不会直接进行处理,而是先采用模块的时钟进行两次打排操作。

信号亚稳态的处理

外部按钮信号相对于 FPGA 内部 100MHz 时钟是异步 的。它随时可能跳变,极大概率会刚好 落在时钟上升沿的建立时间(Setup Time) 和**保持时间(Hold Time)**窗口内。

当这种"时序违例"发生时,第一级触发器(btn_sync1)的输出端不会 立刻变成稳定的 0 或 1,而是进入一种不确定的中间状态------亚稳态(Metastability)

  • 第一拍(FF1) :承担"牺牲者"角色。它必定会进入亚稳态,但其输出端的电压会在一个时钟周期内 (通常指 Tmet 解析时间)以极大概率趋向于稳定的逻辑电平。

  • 第二拍(FF2)关键所在。由于第一拍的亚稳态最终会在第二个时钟沿到来之前稳定下来(99.9%以上的情况),第二拍在下一个时钟沿采到的就是一个确定的 0 或 1。

如果不打(直接使用 btn_in)外部信号直接接入内部组合逻辑(如计数器比较器)。一旦亚稳态出现,后续所有组合逻辑的输出都会是"X"(不定态),这会瞬间污染整个内部状态机,导致功能错乱甚至系统"死锁",这种情况绝对禁止

如果只打一拍(单级同步),虽然能让信号"同步",但无法隔离亚稳态。如果第一级进入亚稳态且在下一个时钟沿还未解析完成(虽然概率低,但在高频 100MHz 下并非不可能),那么这个"不定态"会直接透传到第二级的输入,进而传播到后端逻辑。

在 100MHz 及以上频率的工程实践中,单级同步是不安全的,必须使用两级。

时序设计*

借用这延时模块,可以简单看一下逻辑开发的根本------时序设计。

通过延时20ms的时序控制,规避掉按键的抖动区间,而Verilog代码只是描述出这段时序

状态机设计*

在这里也展开一下状态机的概念。例如上述这种简单的延时处理,依靠 if else语句的判断即可实现,但当系统功能十分复杂的时候,状态机的必要性就体现出来了。

传统的计数器消抖可以显式地建模为有限状态机(FSM),状态更清晰,便于理解和维护。下面给出一个基于两状态 的消抖 FSM 设计,与之前的计数器逻辑功能完全一致。在开始写Verilog代码前,一定要先画好状态机的跳转图,所谓谋定而后动,状态机的跳转图是后续做代码开发的重要依据。

以当前想构建一个消抖状态机为例,先对该状态机进行状态定义划分:

状态 含义
IDLE 空闲状态,当前输出 btn_out 已稳定,与同步后的输入 btn_sync 一致
COUNT 计数状态,检测到输入电平与输出不同,正在累计稳定时长

这里指定义了两个状态,不妨衍生一下,再加一些例如 ERR 态进行异常处理也未尝不可。

在定义好了所需的状态后,下一步需要设计明确各状态之间的跳转条件

在定义好所有的状态以及跳转条件后会发现,Verilog只是把设计好的这些描述出来罢了。 FPGA的底层开发逻辑无非归于 时序 和 **状态机。**所有的功能都可以依靠二者组合实现。

附上参考代码:

复制代码
// -------------------------------------------------------------------
// 模块名:debounce_fsm
// 功能:按钮消抖,采用显式状态机,亚稳态同步同上
// -------------------------------------------------------------------

module debounce_fsm (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       btn_in,
    output reg        btn_out
);

    // -------- 参数定义 ----------
    parameter DEBOUNCE_MS = 20;
    localparam CLK_FREQ = 100_000_000;
    localparam MAX_CNT = CLK_FREQ / 1000 * DEBOUNCE_MS - 1;

    // -------- 状态编码 ----------
    localparam IDLE  = 1'b0;
    localparam COUNT = 1'b1;
    reg state;

    // -------- 信号声明 ----------
    reg [21:0] cnt;
    reg btn_sync1, btn_sync2;
    wire btn_sync = btn_sync2;

    // -------- 亚稳态同步 ----------
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            btn_sync1 <= 1'b0;
            btn_sync2 <= 1'b0;
        end else begin
            btn_sync1 <= btn_in;
            btn_sync2 <= btn_sync1;
        end
    end

    // -------- 状态机与计数器 ----------
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state   <= IDLE;
            cnt     <= 22'd0;
            btn_out <= 1'b0;
        end else begin
            case (state)
                IDLE: begin
                    if (btn_sync != btn_out) begin
                        state <= COUNT;
                        cnt   <= 22'd0;
                    end
                end

                COUNT: begin
                    // 如果输入恢复到与输出一致(抖动),回到IDLE
                    if (btn_sync == btn_out) begin
                        state <= IDLE;
                        cnt   <= 22'd0;
                    end else begin
                        // 继续计数
                        if (cnt < MAX_CNT) begin
                            cnt <= cnt + 1'b1;
                        end else begin
                            // 计数满,确认电平稳定,更新输出
                            btn_out <= btn_sync;
                            state   <= IDLE;
                            cnt     <= 22'd0;
                        end
                    end
                end

                default: state <= IDLE;
            endcase
        end
    end

endmodule

这里给一个通用的三段式状态机模板,可以根据设计需求套用:verilog三段式状态机伸手就拿凑合用版_axi三段式状态-CSDN博客

LED点亮

在按键消抖模块设计完成后,再补一个简单的计数器模块控制LED

复制代码
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// -------------------------------------------------------------------
// 模块名:led_driver
// 功能:按键计数,驱动2个LED左右交替点亮
// 输入:clk (100MHz), rst_n, btn_out (消抖后稳定信号)
// 输出:led_out[1:0] - 2位LED控制信号
//         led_out[0] 控制右LED,led_out[1] 控制左LED
//         每次按键,左右LED交替点亮
// -------------------------------------------------------------------

module led_driver (
    input  wire       clk,      // 100MHz 系统时钟
    input  wire       rst_n,    // 低电平有效异步复位
    input  wire       btn_out,  // 消抖后的按钮有效信号(高电平表示按下)
    output reg  [1:0] led_out   // LED输出,2位
);

    // 内部计数器(32位,防止溢出,实际只取最低位)
    reg [31:0] cnt;

    // 边沿检测寄存器
    reg btn_prev;

    // -----------------------------------------------------------------
    // 边沿检测与计数
    // -----------------------------------------------------------------
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt      <= 32'd0;
            btn_prev <= 1'b0;
            led_out  <= 2'b01;          // 默认右LED亮(可根据需求调整)
        end else begin
            // 寄存上一拍btn_out,用于检测上升沿
            btn_prev <= btn_out;

            // 检测上升沿:当前为高,上一拍为低(表明按键有效)
            if (btn_out == 1'b1 && btn_prev == 1'b0) begin
                cnt <= cnt + 1'b1;      // 计数器加1
            end

            // 根据计数器最低位决定LED输出
            // cnt[0] = 0 → 右LED亮 (led_out = 2'b01)
            // cnt[0] = 1 → 左LED亮 (led_out = 2'b10)
            if (cnt[0] == 1'b0)
                led_out <= 2'b01;
            else
                led_out <= 2'b10;
        end
    end

endmodule

顶层调用模块

复制代码
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company: 
// Engineer: 
// 
// Create Date: 2026/08/28 10:01:09
// Design Name: 
// Module Name: led_shift
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////


// -------------------------------------------------------------------
// 顶层模块:top
// 功能:实例化按钮消抖模块和 LED 驱动模块,形成完整按键控制 LED 交替点亮系统
// 输入:clk (100MHz), rst_n, btn_in (原始按钮)
// 输出:led_out[1:0] (2位LED控制信号)
// -------------------------------------------------------------------

module top (
    input  wire       clk,      // 100MHz 系统时钟
    input  wire       rst_n,    // 低有效异步复位
    input  wire       btn_in,   // 原始按钮输入(高电平有效)
    output wire [1:0] led_out   // LED输出
);

    // 内部连线:消抖后的稳定按钮信号
    wire btn_debounced;

    // -----------------------------------------------------------------
    // 实例化消抖模块 (默认消抖时间 20ms)
    // -----------------------------------------------------------------
    debounce u_debounce (
        .clk    (clk),
        .rst_n  (rst_n),
        .btn_in (btn_in),
        .btn_out(btn_debounced)
    );

    // -----------------------------------------------------------------
    // 实例化 LED 驱动模块
    // -----------------------------------------------------------------
    led_driver u_led_driver (
        .clk    (clk),
        .rst_n  (rst_n),
        .btn_out(btn_debounced),
        .led_out(led_out)
    );

endmodule

后续工作即 添加约束、编译比特、上板验证

继Uart、GPIO之后,下周暂定看看SPI、IIC这些通用接口吧。周末愉快。

Fin

相关推荐
LCG元2 小时前
【实战】Vivado FPGA 从零实现 UART 串口收发:波特率生成、状态机设计与仿真调试
fpga开发
郭郭的柳柳在学FPGA2 小时前
紫光DDR3工程仿真遇到的坑
fpga开发
国科安芯6 小时前
ASC8T245S:把 8 位并行总线稳稳“跨过“电压域的双电源收发器
网络·分布式·单片机·嵌入式硬件·fpga开发·架构
HUSTCH20217 小时前
DW IP详解
fpga开发
明德扬7 小时前
Xilinx下载文件里的BIT、BIN、MCS有什么区别?先分清JTAG配置和Flash启动
fpga开发·fpga
IT信息时代9 小时前
FPGA技术未来竞争发展出路
fpga开发
昆山森启智能科技有限公司12 小时前
FPGA控制器
fpga开发
昆山森启智能科技有限公司12 小时前
基于深度学习GPU(ORIN NX)+FPGA双光谱视频处理模块
人工智能·深度学习·fpga开发
LCMICRO-1331084774613 小时前
国产长芯微LD151完全P2P替代ADP151,是一款超低噪声、低压差线性稳压器
单片机·嵌入式硬件·fpga开发·硬件工程·电子工程师·线性稳压器·adp151