PL功能设计
本篇以一个简单的纯逻辑工程作为案例,穿插介绍一些FPGA逻辑开发的设计思路。先构思一个基础功能:以开发板上的按钮为控制输入,让开发板上的两颗LED灯在每次按下时交替闪烁。
下面对这需求进行拆解:
- 工程以外部的高低电平切换作为信号输入,对应的FPGA引脚约束至开发板外部按钮;
- 逻辑工程内部对输入的数字信号进行处理,识别信号翻转;
- 两颗LED的交替闪烁对应输出信号的0-1翻转,位宽为2的寄存器从0开始持续累加即可实现比特位交替为1;
- 逻辑工程需在识别到的输入信号翻转时,对控制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