本文基于实际 Vivado 工程代码,讲解
KEY_TOP、按键消抖模块key、复位产生模块rst_gen和clk_wiz_0的配合方式,并修复原代码中的语法错误与逻辑问题。
摘要
在一个典型的 FPGA 按键控制 LED 工程中,通常需要完成以下几件事:
- 将外部差分时钟输入到
clk_wiz_0; - 由
clk_wiz_0产生系统工作时钟; - 使用
rst_gen产生上电复位; - 使用
key模块完成按键同步和消抖; - 将按键状态输出到 LED。
本文重点分析以下代码:
KEY_TOPkeyrst_genclk_wiz_0例化模板
并给出可以综合的完整修复版本。
关键词
Vivado、Verilog、KEY_TOP、clk_wiz_0、rst_gen、key、按键消抖、FPGA、同步、复位
一、工程整体结构
顶层模块 KEY_TOP 的连接关系如下:
i_clk_p/i_clk_n
|
v
clk_wiz_0
|
v
w_clk
|
+------------------+
| |
v v
rst_gen key
| |
v v
w_rst o_led
|
v
key.i_rst
功能划分:
| 模块 | 功能 |
|---|---|
clk_wiz_0 |
将 100 MHz 差分输入时钟转换为 10 MHz 系统时钟 |
rst_gen |
上电后产生持续若干周期的复位信号 |
key |
对按键进行同步、消抖和状态翻转 |
KEY_TOP |
连接时钟、复位、按键和 LED |
二、原始代码的主要问题
1. localparam 缺少分号
原代码:
localparam P_FILTER_TARGET=P_FLITERT_TIM/(1000000000/P_CLK_FRQ)
缺少结尾的分号:
;
应改为:
localparam integer P_FILTER_TARGET =
P_FILTER_TIME_NS / (1_000_000_000 / P_CLK_FREQ);
同时 P_FLITERT_TIM 属于拼写错误,建议改成更清晰的:
P_FILTER_TIME_NS
2. <== 是非法语法
原代码:
ri_key[0]<==i_key;
Verilog 中没有 <== 运算符,应该写成:
ri_key[0] <= i_key;
3. else 缺少 begin/end
原代码:
if(i_rst)
ri_key<=3'b111;
else
ri_key[0]<==i_key;
ri_key[1]<=ri_key[0];
ri_key[2]<=ri_key[1];
即使把 <== 改成 <=,也只有第一条赋值属于 else:
else
ri_key[0] <= i_key;
ri_key[1] <= ri_key[0]; // 会变成无条件执行
ri_key[2] <= ri_key[1];
正确写法:
if (i_rst) begin
ri_key <= 3'b111;
end else begin
ri_key[0] <= i_key;
ri_key[1] <= ri_key[0];
ri_key[2] <= ri_key[1];
end
4. 隐式端口连接不推荐
原代码:
key#(
...
)key_u0(
.i_clk,
.i_rst,
.i_key,
.o_key_status
);
.i_clk 这种写法属于 SystemVerilog 风格。
如果 Vivado 将文件当作 Verilog 处理,可能报错。
建议明确写端口连接:
key #(
...
) u_key (
.i_clk (w_clk),
.i_rst (w_rst),
.i_key (i_key),
.o_key_status(o_led)
);
5. 消抖计数器逻辑不完整
原代码:
else if(ri_key[2]==1 && ri_key[1]==0 || r_cnt>0)
r_cnt <= r_cnt + 1'b1;
else
r_cnt <= r_cnt;
问题在于:
r_cnt > 0后计数器会一直递增;- 按键释放后计数器不会清零;
- 完成一次翻转后不会停止;
- 多次按键时滤波窗口不稳定;
- 计数器绕回后可能产生误触发。
正确思路是:
按键同步
-> 等待连续稳定低电平
-> 计数达到滤波时间
-> 翻转一次状态
-> 等待按键释放
-> 准备下一次按键
三、修复后的完整代码
以下代码已经修正语法、端口连接和消抖逻辑。
`timescale 1ns / 1ps
module KEY_TOP (
input wire i_clk_p,
input wire i_clk_n,
input wire i_key,
output wire o_led
);
wire w_clk;
wire w_rst;
/*
* Clock Wizard:
* 输入差分时钟 100 MHz
* 输出时钟 clk_out1 = 10 MHz
*/
clk_wiz_0 u_clk_wiz (
.clk_out1 (w_clk),
.reset (1'b0),
.locked (),
.clk_in1_p (i_clk_p),
.clk_in1_n (i_clk_n)
);
/*
* 上电复位:
* w_rst 保持 100 个 w_clk 周期的高电平,然后释放。
*/
rst_gen #(
.P_CLK_CYCLE(100)
) u_rst_gen (
.i_clk(w_clk),
.o_rst(w_rst)
);
/*
* 按键消抖:
* 10 MHz 时钟,滤波时间 10 ms。
*/
key #(
.P_CLK_FREQ (10_000_000),
.P_FILTER_TIME_NS (10_000_000)
) u_key (
.i_clk (w_clk),
.i_rst (w_rst),
.i_key (i_key),
.o_key_status(o_led)
);
endmodule
module key #(
parameter integer P_CLK_FREQ = 10_000_000,
parameter integer P_FILTER_TIME_NS = 10_000_000
)(
input wire i_clk,
input wire i_rst,
input wire i_key,
output wire o_key_status
);
/*
* P_FILTER_TIME_NS 单位是 ns,
* P_CLK_FREQ 单位是 Hz。
*
* 10 MHz 时钟周期 = 100 ns
* 10 ms 滤波时间 = 10_000_000 ns
* 需要计数 = 10_000_000 / 100 = 100_000
*/
localparam integer P_FILTER_TARGET =
P_FILTER_TIME_NS / (1_000_000_000 / P_CLK_FREQ);
reg [2:0] ri_key;
reg ro_key_status;
reg [31:0] r_cnt;
reg r_done;
wire [2:0] w_key_next;
wire w_filter_target;
assign w_key_next = {ri_key[1:0], i_key};
assign w_filter_target = (r_cnt == P_FILTER_TARGET - 1);
assign o_key_status = ro_key_status;
always @(posedge i_clk or posedge i_rst) begin
if (i_rst) begin
ri_key <= 3'b111;
ro_key_status <= 1'b0;
r_cnt <= 32'd0;
r_done <= 1'b0;
end
else begin
ri_key <= w_key_next;
/*
* 连续三个采样值都为 0,
* 认为按键已经稳定按下。
*/
if (w_key_next != 3'b000) begin
r_cnt <= 32'd0;
r_done <= 1'b0;
end
else if (!r_done) begin
if (w_filter_target) begin
ro_key_status <= ~ro_key_status;
r_done <= 1'b1;
end
else begin
r_cnt <= r_cnt + 32'd1;
end
end
else begin
r_cnt <= r_cnt;
r_done <= r_done;
end
end
end
endmodule
module rst_gen #(
parameter integer P_CLK_CYCLE = 100
)(
input wire i_clk,
output reg o_rst
);
reg [31:0] r_cnt;
initial begin
r_cnt = 32'd0;
o_rst = 1'b1;
end
always @(posedge i_clk) begin
if (r_cnt < (P_CLK_CYCLE - 1)) begin
r_cnt <= r_cnt + 32'd1;
o_rst <= 1'b1;
end
else begin
o_rst <= 1'b0;
end
end
endmodule
四、关键代码解析
1. 时钟频率与滤波时间
key #(
.P_CLK_FREQ (10_000_000),
.P_FILTER_TIME_NS (10_000_000)
) u_key (...);
含义:
P_CLK_FREQ = 10 MHz
P_FILTER_TIME_NS = 10 ms
计算:
时钟周期 = 1 / 10 MHz = 100 ns
滤波计数 = 10 ms / 100 ns = 100000
所以:
P_FILTER_TARGET = 100000
2. 三级同步
assign w_key_next = {ri_key[1:0], i_key};
每次时钟上升沿:
ri_key <= w_key_next;
这样按键信号被同步到 w_clk 时钟域,并形成三级采样。
3. 消抖计数
if (w_key_next != 3'b000) begin
r_cnt <= 32'd0;
r_done <= 1'b0;
end
只要按键不是连续稳定低电平,计数器就清零。
当连续稳定低电平达到:
w_filter_target = (r_cnt == P_FILTER_TARGET - 1)
则:
ro_key_status <= ~ro_key_status;
r_done <= 1'b1;
翻转一次状态,并标记 r_done。
r_done 的作用是:
防止按键一直按住时反复翻转
只有按键释放后:
w_key_next != 3'b000
才会清除 r_done,允许下一次按键。
五、Vivado 工程需要做什么
1. 添加源文件
在 Vivado 中添加:
KEY_TOP 和 key 所在文件
rst_gen.v
操作:
Add Sources
-> Add or create design sources
-> Add Files
2. 添加 IP 核
Add Sources
-> Add Existing IP
-> 选择:
key.srcs/sources_1/ip/clk_wiz_0/clk_wiz_0.xci
然后在:
Sources
-> IP Sources
-> clk_wiz_0
右键:
Generate Output Products
六、约束文件 XDC
还需要在 XDC 文件中约束:
i_clk_pi_clk_ni_keyo_led
例如:
# 差分输入时钟
set_property PACKAGE_PIN <你的时钟P引脚> [get_ports i_clk_p]
set_property PACKAGE_PIN <你的时钟N引脚> [get_ports i_clk_n]
set_property IOSTANDARD LVDS [get_ports {i_clk_p i_clk_n}]
# 按键
set_property PACKAGE_PIN <你的按键引脚> [get_ports i_key]
set_property IOSTANDARD LVCMOS33 [get_ports i_key]
# LED
set_property PACKAGE_PIN <你的LED引脚> [get_ports o_led]
set_property IOSTANDARD LVCMOS33 [get_ports o_led]
具体引脚编号和电平标准必须根据你的开发板原理图填写。
七、验证方法
1. 确认 Clock Wizard 配置
打开 clk_wiz_0:
Sources
-> IP Sources
-> clk_wiz_0
-> Re-customize IP
确认:
Input Clock = 100 MHz
Output Clock = 10 MHz
如果输入频率不是 100 MHz,key 模块的滤波时间会不准确。
2. 综合和实现
依次执行:
Generate Output Products
Run Synthesis
Run Implementation
Generate Bitstream
3. 上板测试
预期现象:
上电后:
o_led = 0
按下按键并保持超过 10 ms:
o_led 翻转一次
再次按下:
o_led 再次翻转
八、总结
原始代码主要问题包括:
localparam缺少分号;<==非法语法;else缺少begin/end;- 隐式端口连接不规范;
- 参数名拼写错误;
- 消抖计数器不会释放后清零;
- 缺少一次有效翻转后的锁定标志。
修复后:
- 语法可以正常综合;
- 按键经过三级同步;
- 滤波时间可配置;
- 按键稳定后只翻转一次;
- 按键释放后可以重新触发;
rst_gen和clk_wiz_0配合清晰。
这套代码可以作为 Vivado 按键消抖、复位产生和时钟管理的基础模板继续扩展。
