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

各位同学,咱们今天聊点实在的。Verilog 这东西,说白了就是硬件描述语言,不是软件编程。我见过太多人把 Verilog 当 C 语言写,结果仿真跑得欢,综合出来一堆问题。嗯,咱们今天就把这些基础概念掰扯清楚。

3.1 模块结构——Verilog 的基本单元

模块(module)是 Verilog 的骨架。每个设计,不管多复杂,最终都是由一个个模块拼起来的。我个人习惯把模块比作一个黑盒子——有输入、有输出、内部有逻辑。

module counter (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [3:0] count
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 4'd0;
        else
            count <= count + 1'b1;
    end
endmodule

你看,模块的端口声明、内部逻辑,一目了然。我在项目中遇到过有人把端口方向写反了,仿真没问题,但综合后死活连不上。所以,写模块时端口方向一定要仔细核对。

小技巧:模块名建议用下划线分隔,比如 uart_tx,别用驼峰命名。这是行业惯例,方便脚本处理。

3.2 数据类型——wire 和 reg 的区别

很多新手搞不清 wire 和 reg。其实很简单:

  • wire:连线类型,用于组合逻辑。说白了就是物理连线的抽象,不能存储值。
  • reg:寄存器类型,用于时序逻辑。它可以保存值,直到被重新赋值。

但注意,这不是绝对的。在 always 块里赋值的变量,必须声明为 reg;在 assign 语句里赋值的,必须用 wire。我刚开始学的时候也犯过混,后来记住一句话:always 块里用 reg,assign 用 wire,基本不会错。

类型 默认值 使用场景 能否存储
wire 高阻态 z assign、模块端口连接 否
reg 不定态 x always 块内赋值 是
注意:在 SystemVerilog 中,logic 类型可以替代 wire 和 reg,但老项目里还是 wire/reg 居多。我建议新手先掌握传统写法,再学 SV 的简化。

3.3 连续赋值与过程赋值

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

连续赋值(assign)

assign 语句是组合逻辑。只要右边信号变化,左边立即更新。你想想看,这就像一根导线,输入变了输出马上变。

assign sum = a + b;
assign sel = (addr > 8'h7F) ? 1'b1 : 1'b0;

我曾经在项目中用 assign 写了一个复杂的多级组合逻辑,结果时序跑不过。后来拆成两级流水线才解决。所以,assign 虽然方便,但别滥用,组合逻辑太深会出问题。

过程赋值(always)

always 块可以描述组合逻辑,也可以描述时序逻辑。区别在于敏感列表:

  • 组合逻辑:敏感列表用 @(*) 或列出所有输入信号
  • 时序逻辑:敏感列表用 @(posedge clk) 或 @(negedge clk)
// 组合逻辑
always @(*) begin
    if (sel)
        out = a;
    else
        out = b;
end

// 时序逻辑
always @(posedge clk) begin
    if (rst_n)
        q <= d;
    else
        q <= 1'b0;
end
核心区别:连续赋值是 持续驱动,过程赋值是 事件触发。assign 没有时钟概念,always 靠敏感列表触发。

3.4 阻塞赋值与非阻塞赋值——面试必考题

这个知识点,我面试过的人十个有八个答不全。阻塞赋值用 =,非阻塞赋值用 <=。区别在哪?

  • 阻塞赋值(=):顺序执行,上一条没执行完,下一条等着。适合组合逻辑。
  • 非阻塞赋值(<=):并行执行,所有赋值同时计算,然后同时更新。适合时序逻辑。

为什么会这样?因为硬件是并行的。你想想看,一个寄存器在时钟沿同时采样多个输入,如果用了阻塞赋值,就会出现竞争——谁先赋值谁后赋值,结果不确定。

我曾经在项目中遇到一个 bug:一个 always 块里同时用阻塞和非阻塞赋值,结果仿真和综合结果不一致。查了两天才发现是赋值方式混用了。从那以后,我给自己定了个规矩:

我的经验:
  • 组合逻辑 always 块:用阻塞赋值 =
  • 时序逻辑 always 块:用非阻塞赋值 <=
  • 同一个 always 块里,不要混用两种赋值方式
// 正确示例:时序逻辑用非阻塞
always @(posedge clk) begin
    q1 <= d;
    q2 <= q1;
end

// 错误示例:时序逻辑用阻塞
always @(posedge clk) begin
    q1 = d;
    q2 = q1;  // 这里 q2 得到的是 d,不是 q1 的旧值
end

你看,上面那个错误示例,用阻塞赋值时 q2 直接得到了 d 的值,而不是 q1 的旧值。这在移位寄存器里就完全错了。

3.5 知识体系总览

为了让大家更直观地理解本章内容,我画了一张图。这张图把 Verilog 基础的核心概念串起来了:

Verilog 基础回顾知识体系 模块结构 (module) 数据类型 赋值方式 端口声明 内部逻辑 input/output/inout wire reg 组合/时序区分 连续赋值 过程赋值 assign 语句 always 块 阻塞赋值 (=) vs 非阻塞赋值 (<=) 顺序执行,组合逻辑用 并行执行,时序逻辑用

这张图把模块结构、数据类型、赋值方式串在了一起。你从上往下看,左边是模块的骨架,中间是数据类型的选择,右边是赋值方式的区分。最下面就是阻塞和非阻塞这个核心考点。

好了,今天的内容就到这里。记住一句话:写 Verilog 的时候,时刻问自己——这是组合逻辑还是时序逻辑?该用 assign 还是 always?该用 = 还是 <=? 想清楚了再下笔,能省掉后面很多 debug 的时间。

本章核心总结:
  • 模块是基本单元,端口方向要写对
  • wire 用于 assign,reg 用于 always
  • 连续赋值是组合逻辑,过程赋值可组合可时序
  • 时序逻辑用非阻塞赋值,组合逻辑用阻塞赋值
  • 同一个 always 块里不要混用两种赋值

可可精品资料库