第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 知识体系总览

下面这张图,是我梳理的本章知识结构。你可以把它当作一张地图,随时回来查阅。

Verilog基础回顾:知识体系 Verilog基础 模块结构 always块(时序/组合) assign语句(连续赋值) reg与wire类型 阻塞赋值(=) vs 非阻塞赋值(<=) ★ 时序逻辑用 <=,组合逻辑用 = 按键消抖应用 • 计数器(reg) • 状态机(always) • 信号连线(wire) • 组合逻辑(assign) • 时序控制(<=) 常见陷阱 • 漏写复位信号 • 混用赋值方式 • reg/wire类型错误 • 敏感列表不完整 • 组合逻辑锁存器

1.7 本章小结

好了,这一章的内容就这些。我们来快速回顾一下:

  • 模块结构:端口定义要准确,input/output不能搞反。
  • always块:时序逻辑用时钟沿触发,组合逻辑用@(*)。
  • assign语句:连续赋值,驱动wire类型。
  • reg与wire:reg存数据,wire传信号。
  • 阻塞与非阻塞:时序逻辑用<=,组合逻辑用=。这是铁律。

这些基础概念,是后续学习按键消抖的基石。下一章,我们会把这些知识串起来,写一个真正的按键消抖模块。到时候你会发现,原来这些基础概念这么有用。

记住:FPGA设计不是写软件,而是画电路。每一行代码,都要能对应到实际的硬件电路上。这是硬件工程师和软件工程师最大的区别。

可可精品资料库