2. Verilog基础回顾:模块结构、数据类型、连续赋值与过程赋值、阻塞与非阻塞赋值

各位同学,咱们今天聊点实在的。Verilog基础,听起来像是入门课,对吧?但说实话,我见过太多工作三五年的工程师,还在阻塞赋值上翻车。我自己也踩过这个坑——有一次调试一个高速接口,仿真波形怎么看怎么不对,折腾了两天,最后发现就是一个非阻塞赋值用成了阻塞的。嗯,从那以后,我对这部分内容就格外上心。

咱们今天不讲那些花里胡哨的,就聚焦四个核心点:模块结构、数据类型、连续赋值与过程赋值、阻塞与非阻塞赋值。你把这四个点吃透了,写测试激励和调RTL的时候,底气会足很多。

核心观点:Verilog不是C语言,它是硬件描述语言。你写的每一行代码,最终都会映射成电路。脑子里要有电路,手上才能写出好代码。

Verilog基础回顾 模块结构 端口定义、实例化 数据类型 wire / reg / integer 连续赋值 assign 语句 过程赋值 always 块内赋值 阻塞 vs 非阻塞 = 与 <= 的区别

2.1 模块结构——你的第一个"黑盒子"

模块(module)是Verilog的基本单元。说白了,它就是你把一堆逻辑封装起来,对外只留几个接口。就像你买了个芯片,你只关心它的引脚,不关心里面有多少晶体管。

一个标准的模块长这样:

module counter (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 复位,低有效
    input  wire       en,       // 使能
    output reg  [7:0] count     // 计数值
);

    // 功能描述
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 8'd0;
        else if (en)
            count <= count + 1'b1;
    end

endmodule

我个人习惯把端口声明和内部信号分开写。端口用 input/output 明确标出方向,内部信号用 wire 或 reg 单独声明。这样代码一读就懂,不用猜。

小技巧:端口列表里每个信号都加上注释,说明是时钟、复位还是数据。我见过一个项目,端口没注释,后来接手的人花了三天才理清楚每个信号是干嘛的。别让这种悲剧发生在你身上。

2.2 数据类型——wire 和 reg 到底怎么选?

这个问题,我几乎每次面试都会问。很多人背了答案,但一写代码就乱。

记住一句话:wire 是线,reg 是变量。但这不是说 reg 就一定会被综合成寄存器。

  • wire:用于组合逻辑,由连续赋值(assign)驱动。你不能在 always 块里给 wire 赋值。
  • reg:用于过程赋值(always 块内)。它可以是组合逻辑,也可以是时序逻辑,取决于你怎么写。

举个例子:

// wire 用法
wire sum;
assign sum = a ^ b;   // 组合逻辑,用 assign

// reg 用法——组合逻辑
reg sum_reg;
always @(*) begin
    sum_reg = a ^ b;   // 组合逻辑,用阻塞赋值
end

// reg 用法——时序逻辑
reg count;
always @(posedge clk) begin
    count <= count + 1;  // 时序逻辑,用非阻塞赋值
end

你看,同样是 reg,既可以做组合逻辑,也可以做时序逻辑。关键看你怎么赋值。

注意:千万别以为 reg 就一定是寄存器。我在项目中遇到过有人把组合逻辑的 reg 误当成寄存器来用,结果综合出来的面积大了三倍。嗯,那个项目后来重做了。

除了 wire 和 reg,还有 integer、real 这些类型。但说实话,在 RTL 设计里,integer 偶尔用在 for 循环里,real 基本不用。你先把 wire 和 reg 搞明白,就够用了。

2.3 连续赋值 vs 过程赋值——两种思维方式

连续赋值用 assign,过程赋值用 always 块。这是两种完全不同的思维方式。

连续赋值:描述的是组合逻辑,输入一变,输出立刻变。就像一根导线,你给它高电平,它马上就高。

assign y = (sel) ? a : b;   // 多路选择器

过程赋值:在 always 块里赋值,可以描述组合逻辑,也可以描述时序逻辑。

// 组合逻辑
always @(*) begin
    if (sel)
        y = a;
    else
        y = b;
end

// 时序逻辑
always @(posedge clk) begin
    if (rst_n)
        q <= 0;
    else
        q <= d;
end

我个人习惯:简单的组合逻辑用 assign,复杂的用 always 块。比如一个简单的与门,用 assign 一行搞定。但如果是带优先级的译码器,用 always 块加 case 语句,可读性更好。

关键区别:连续赋值是"线"的思维,过程赋值是"块"的思维。连续赋值不能有记忆,过程赋值可以(通过时钟沿触发)。

2.4 阻塞与非阻塞赋值——最容易翻车的地方

好,重点来了。这部分我多说几句,因为真的太容易出错了。

阻塞赋值(=):顺序执行,前面的赋值完成,后面的才开始。像C语言一样。

非阻塞赋值(<=):并行执行,所有赋值同时发生。这是硬件特有的。

记住这个黄金法则:

黄金法则:时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。永远不要混用。

为什么?我给你看个反面教材:

// 错误示例——时序逻辑用了阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;   // 这里 a 已经是新值了,但硬件上不是这样的!
end

这段代码在仿真里,a 和 c 会在同一个时钟沿更新。但实际硬件上,c 拿到的是 a 的旧值。仿真和硬件不一致,这就是 bug 的来源。

正确的写法:

// 正确示例——时序逻辑用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;   // c 拿到的是 a 的旧值,和硬件一致
end

我曾经在一个项目里,写了一个移位寄存器,用了阻塞赋值。仿真波形完美,但上板测试就是不对。后来用逻辑分析仪抓信号,发现数据总是差一拍。查了两天,才发现是赋值方式用错了。从那以后,我写 always 块之前,都会先问自己一句:这是组合逻辑还是时序逻辑?

避坑指南:如果你在 always 块里同时给多个变量赋值,而且这些变量之间有依赖关系,一定要用非阻塞赋值。否则,仿真和综合结果可能不一致。

最后,给你一个简单的判断方法:

场景 赋值方式 示例
组合逻辑(always @(*)) 阻塞赋值(=) y = a & b;
时序逻辑(always @(posedge clk)) 非阻塞赋值(<=) q <= d;
锁存器(不推荐) 阻塞赋值(=) if (en) q = d;

嗯,今天就先聊这么多。这四个基础点,你回去好好消化一下。写代码的时候多想想:这个信号是 wire 还是 reg?这个 always 块是组合还是时序?赋值用 = 还是 <=?想清楚了再下笔,能省很多调试的时间。