二、Verilog基础与组合逻辑:模块化设计、assign语句、always块、阻塞与非阻塞赋值、位宽与数据类型

各位同学,咱们今天聊点实在的。Verilog 这东西,说白了就是硬件描述语言,不是软件编程。我见过太多新手把 Verilog 当 C 语言写,结果综合出来的电路完全不是那么回事。嗯,咱们今天就把这些基础但关键的点,一个一个掰扯清楚。

2.1 模块化设计:把大问题拆成小积木

我个人习惯,拿到一个通信协议解析任务,第一件事不是写代码,而是画框图。把整个系统拆成几个功能模块,比如:帧同步模块、CRC校验模块、数据提取模块。每个模块独立设计、独立仿真,最后再拼起来。

模块化设计的好处,说白了就是好维护、好复用。你想想看,如果所有代码都写在一个大模块里,出错了找 bug 得找到猴年马月去。

一个典型的模块声明长这样:

module frame_sync (
    input   wire        clk,        // 系统时钟
    input   wire        rst_n,      // 异步复位,低有效
    input   wire        data_in,    // 串行数据输入
    output  reg         sync_done   // 同步完成标志
);

// 模块内部逻辑

endmodule

这里要注意,端口方向要写清楚。input、output、inout,别搞混了。我在项目中遇到过有人把 input 写成 output,结果仿真怎么都不对,查了两天才发现是端口方向反了。

2.2 assign语句:组合逻辑的直通车

assign 语句,说白了就是连续赋值。它描述的是组合逻辑,没有时钟,没有寄存器。只要右边表达式变了,左边立刻跟着变。

举个例子,我们要实现一个 8 位数据的奇偶校验:

module parity_check (
    input   wire [7:0]  data,
    output  wire        even_parity,
    output  wire        odd_parity
);

assign even_parity = ^data;  // 异或归约,结果为1表示奇数个1
assign odd_parity  = ~even_parity;

endmodule

你看,两行 assign 就搞定了。简洁、高效、一目了然。

我的小技巧: assign 语句的右边可以是复杂的表达式,但别写得太长。如果表达式超过一行,建议拆成多个 assign 或者用 always 块。代码可读性很重要,毕竟你写的代码可能半年后自己都看不懂。

2.3 always块:时序逻辑与组合逻辑的舞台

always 块是 Verilog 的核心。它既可以描述时序逻辑(带时钟),也可以描述组合逻辑(不带时钟)。

描述时序逻辑时,敏感列表里要有时钟沿:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        counter <= 8'd0;
    else if (enable)
        counter <= counter + 1'b1;
end

描述组合逻辑时,敏感列表里要包含所有输入信号:

always @(*) begin
    if (sel == 1'b0)
        mux_out = a;
    else
        mux_out = b;
end

这里有个坑,我必须要说。组合逻辑的 always 块里,如果敏感列表不完整,综合出来的电路会多出锁存器。我曾经因为这个原因,导致一个简单的地址译码器多出了十几个锁存器,面积大了不少。

避坑指南: 组合逻辑 always 块中,所有被赋值的变量,必须在所有分支中都被赋值。否则会综合出锁存器。用 always @(*) 可以自动包含所有敏感信号,推荐使用。

2.4 阻塞赋值与非阻塞赋值:天壤之别

这个问题,我每次讲课都要强调。阻塞赋值 = 和非阻塞赋值 <=,用错了,电路行为就完全变了。

简单来说:

  • 阻塞赋值(=):顺序执行,前面的赋值会立即影响后面的语句。用于组合逻辑。
  • 非阻塞赋值(<=):并行执行,所有赋值在 always 块结束时同时更新。用于时序逻辑。

看个对比的例子:

// 阻塞赋值 - 组合逻辑
always @(*) begin
    temp = a & b;
    c = temp | d;
end

// 非阻塞赋值 - 时序逻辑
always @(posedge clk) begin
    temp <= a & b;
    c <= temp | d;  // 这里用的是上一个时钟周期的 temp
end

在时序逻辑里用阻塞赋值,会导致仿真结果和实际电路不一致。我刚开始做 FPGA 时犯过这个错,仿真波形看着完美,上板子就是不对。后来发现是赋值方式用错了。

黄金法则: 时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。别混用,别偷懒。

2.5 位宽与数据类型:细节决定成败

位宽这东西,看似简单,但出问题往往就在这。通信协议里,数据包的长度、CRC 的宽度、状态机的编码,都跟位宽有关。

先看数据类型:

类型 说明 默认值
wire 线网类型,用于 assign 赋值、模块端口连接 高阻态 z
reg 寄存器类型,用于 always 块中赋值 不定态 x
integer 32位有符号整数,常用于仿真 0

位宽声明时,要特别注意:

wire [7:0] data_bus;    // 8位宽,data_bus[7]是最高位
wire [0:7] reverse_bus; // 也是8位宽,但 reverse_bus[0]是最高位

// 位宽不匹配的赋值
reg [3:0] small;
reg [7:0] large;

assign large = small;  // 高位自动补0,没问题
assign small = large;  // 高位被截断,可能丢失数据

我在做以太网 MAC 层解析时,遇到过一个问题。CRC 校验结果是 32 位,但我定义了一个 31 位的寄存器来存,结果校验永远不对。查了半天才发现是位宽少了一位。

我的习惯: 定义信号时,位宽用 [高:低] 的形式,比如 [7:0]。这样 bit 序号和实际意义对应,不容易搞混。另外,能用 localparam 定义常量就别用硬编码,方便后期修改。

2.6 本章知识体系

下面这张图,是我自己总结的本章知识结构。你可以把它当成一个检查清单,看看自己掌握了哪些。

Verilog基础与组合逻辑 模块化设计 端口声明、模块实例化 顶层模块连接 assign语句 连续赋值、组合逻辑 表达式与运算符 always块 时序逻辑 vs 组合逻辑 敏感列表、锁存器 阻塞与非阻塞赋值 = 用于组合逻辑 <= 用于时序逻辑 位宽与数据类型 wire、reg、integer 位宽匹配与截断 核心原则:组合逻辑用assign或always@(*),时序逻辑用always@(posedge clk) 赋值方式选对,位宽算准,模块拆好

嗯,这张图把今天讲的内容串起来了。你写代码的时候,可以对照着看看,有没有哪个环节没注意到。

最后说一句,Verilog 基础这东西,光看不行,得动手。找个简单的通信协议,比如 UART,自己从头写一遍,把模块化、assign、always、阻塞非阻塞、位宽这些都用上。写完了仿真,仿真过了上板子。这样走一遍,比看十遍书都管用。


公众号:蓝海资料掘金营,微信deep3321