第1章:Verilog基础回顾——模块结构、always块、assign语句、reg与wire类型、阻塞与非阻塞赋值
各位同学,欢迎来到《FPGA高速收发器按键消抖应用实战》的第一课。
我知道,很多人一上来就想直接写收发器代码。但别急,我做了十几年FPGA设计,踩过的坑比你们走过的路还多。按键消抖这个看似简单的功能,恰恰是检验Verilog基本功的试金石。今天,我们就从最基础的东西开始,把这些概念彻底理清楚。
核心观点:按键消抖的本质是「用硬件思维解决时序问题」。如果你不理解reg和wire的区别,不理解阻塞和非阻塞赋值的差异,那写出来的消抖代码大概率会出问题。
1.1 模块结构——FPGA设计的“骨架”
每个Verilog模块,就像一块乐高积木。你得先知道积木的接口长什么样,才能拼出复杂的系统。
我个人习惯,写模块时先画端口图。比如一个按键消抖模块,它的输入是时钟、复位和按键信号,输出是消抖后的稳定信号。嗯,就这么简单。
module debounce (
input wire clk, // 系统时钟,50MHz
input wire rst_n, // 异步复位,低有效
input wire key_in, // 按键输入
output reg key_out // 消抖后输出
);
// 内部逻辑写在这里
endmodule
你看,模块名、端口列表、端口方向、数据类型,一个都不能少。我见过新手把input写成in,结果编译报错半天找不出原因。这种低级错误,我当年也犯过。
小技巧:端口方向用input、output、inout三种。inout很少用,初学者先别碰。
1.2 always块——时序逻辑的“心脏”
always块,说白了就是一段「一直重复执行」的代码。但它不是软件里的while循环,而是硬件里的触发器网络。
为什么会这样?因为FPGA里的寄存器,每个时钟沿都会采样输入,更新输出。always块就是描述这个行为的。
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
key_out <= 1'b1; // 复位时输出高电平
else
key_out <= key_in; // 每个时钟沿采样按键输入
end
注意看敏感列表:posedge clk和negedge rst_n。前者是时钟上升沿触发,后者是复位下降沿触发。我曾经在项目中漏写了复位信号,结果仿真时寄存器状态全乱套了。嗯,这种教训一次就够。
警告:组合逻辑的always块用@(*),时序逻辑的always块用@(posedge clk)。别搞混了,否则综合出来的电路可能不是你想要的。
1.3 assign语句——组合逻辑的“直通车”
assign语句,就是连续赋值。它没有时钟,没有复位,输入一变,输出立刻变。你想想看,这像什么?像一根导线。
assign key_out = key_in; // 直接连线,没有寄存器
在按键消抖中,assign通常用来做简单的逻辑运算。比如把多个按键信号合并:
assign any_key_pressed = ~(key1 & key2 & key3); // 任意按键按下,输出高电平
我个人习惯,能用assign的地方绝不用always块。因为assign更直观,综合出来的电路也更简洁。但注意,assign只能驱动wire类型,不能驱动reg。
1.4 reg与wire类型——数据存储的“两种形态”
很多初学者搞不清reg和wire的区别。我打个比方:
- wire:就像一根电线,信号从一端传到另一端,没有记忆能力。
- reg:就像一个小盒子,能把数据存起来,下次再用。
在按键消抖中,我们需要计数器来计时。计数器必须用reg,因为它要记住当前计数值:
reg [19:0] cnt; // 20位计数器,可以计数到1,048,575
而连接两个模块的信号,通常用wire:
wire debounced_key; // 消抖后的按键信号,用于连接下一级模块
重要:在always块中被赋值的变量,必须是reg类型。在assign语句中被赋值的变量,必须是wire类型。这是Verilog的语法铁律,没得商量。
1.5 阻塞与非阻塞赋值——FPGA设计的“分水岭”
这是本章的重头戏。我敢说,90%的FPGA初学者都在这里栽过跟头。
阻塞赋值用=,非阻塞赋值用<=。区别在哪?
- 阻塞赋值(=):顺序执行,上一条赋值完成,下一条才能开始。像流水线上的工人,一个干完下一个才能干。
- 非阻塞赋值(<=):同时执行,所有赋值在同一个时刻完成。像发令枪响,所有运动员同时起跑。
在时序逻辑中,必须用非阻塞赋值。为什么?我举个例子:
// 错误写法:阻塞赋值
always @(posedge clk) begin
a = b;
c = a; // 这里a已经是新值了
end
// 正确写法:非阻塞赋值
always @(posedge clk) begin
a <= b;
c <= a; // 这里a还是旧值
end
你看,阻塞赋值时,c得到的是a的新值。而非阻塞赋值时,c得到的是a的旧值。这在硬件上对应着不同的电路结构。我曾经在项目中用阻塞赋值写了一个移位寄存器,结果仿真和实测完全对不上。查了两天才发现是赋值方式用错了。你说冤不冤?
黄金法则:时序逻辑用<=,组合逻辑用=。永远不要混用。如果你在同一个always块里既用阻塞又用非阻塞,那综合工具会给你一个「惊喜」——大概率是仿真和综合结果不一致。
1.6 知识体系总览
下面这张图,是我梳理的本章知识结构。你可以把它当作一张地图,随时回来查阅。
1.7 本章小结
好了,这一章的内容就这些。我们来快速回顾一下:
- 模块结构:端口定义要准确,input/output不能搞反。
- always块:时序逻辑用时钟沿触发,组合逻辑用
@(*)。 - assign语句:连续赋值,驱动wire类型。
- reg与wire:reg存数据,wire传信号。
- 阻塞与非阻塞:时序逻辑用
<=,组合逻辑用=。这是铁律。
这些基础概念,是后续学习按键消抖的基石。下一章,我们会把这些知识串起来,写一个真正的按键消抖模块。到时候你会发现,原来这些基础概念这么有用。
记住:FPGA设计不是写软件,而是画电路。每一行代码,都要能对应到实际的硬件电路上。这是硬件工程师和软件工程师最大的区别。