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

各位同学,咱们今天聊聊Verilog最基础的东西。说实话,很多老工程师干了十年,回头一看,最容易出bug的地方反而是这些基础概念。我见过太多人把wire和reg搞混,最后仿真跑出来一堆红叉叉。

好,咱们直接开干。

3.1 模块结构——Verilog的骨架

Verilog里,模块(module)是最基本的设计单元。你可以把它想象成一个黑盒子:有输入、有输出,里面装着逻辑。

一个典型的模块长这样:

module counter (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 复位,低有效
    output reg  [3:0] count     // 4位计数器输出
);

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

endmodule

结构上就三部分:

  • 端口声明:告诉外界这个模块长什么样
  • 内部信号与逻辑:干活的代码
  • endmodule:收工

我个人习惯,端口列表里只写名字,类型和位宽放到下面单独声明。这样看起来清爽,也方便后期改位宽。

小技巧:模块名用下划线分隔单词,比如 uart_rxspi_master。别用驼峰,也别全大写。这是行业不成文的规矩。

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

这是新手最容易懵的地方。我当年刚学的时候,也纠结过:什么时候用wire?什么时候用reg?

其实很简单:

  • wire:连线。它不存储值,只是把两个点连起来。用在assign语句、模块端口连接。
  • reg:寄存器。它存储值,用在always块里被赋值。

但注意!reg不一定会综合成寄存器。如果你在组合逻辑的always块里用reg,它综合出来只是一堆门电路,没有存储功能。嗯,这里要小心。

举个例子:

// 组合逻辑:reg综合成纯组合电路
reg [3:0] sum;
always @(*) begin
    sum = a + b;
end

// 时序逻辑:reg综合成触发器
reg [3:0] count;
always @(posedge clk) begin
    count <= count + 1'b1;
end

我在项目中遇到过有人把组合逻辑的reg误当成寄存器用,结果仿真对了,上板子就乱跳。查了两天才发现是综合出来没有锁存功能。

避坑指南:我曾经在代码里把always块的敏感列表写漏了,比如只写了 always @(a) 没写 b,结果综合出来一堆latch。后来我养成了习惯:组合逻辑一律用 always @(*),省心。

3.3 连续赋值——assign 的用法

连续赋值,说白了就是用 assign 把右边的表达式实时赋给左边的wire。它没有时钟概念,输入一变,输出立刻变。

wire [3:0] sum;
assign sum = a + b;

这里 sum 必须声明为wire。为什么?因为assign是持续驱动的,它不存储值,只是把a+b的结果“连”到sum上。

连续赋值适合做:

  • 简单的组合逻辑(加法、比较、选择)
  • 数据通路中的连线
  • 三态门控制

你想想看,如果我要做一个多路选择器,用assign写起来多清爽:

wire [7:0] data_out;
assign data_out = (sel == 2'b00) ? data0 :
                  (sel == 2'b01) ? data1 :
                  (sel == 2'b10) ? data2 : data3;

一行搞定,不用always,不用case。简洁就是美。

3.4 过程赋值——always 块里的门道

过程赋值,就是在 always 块里给reg赋值。这里有两个大坑:阻塞赋值(=)和非阻塞赋值(<=)。

我直接说结论:

  • 组合逻辑:用阻塞赋值 =
  • 时序逻辑:用非阻塞赋值 <=

为什么?

阻塞赋值是“先算完再赋值”,后面的语句要等前面的执行完。非阻塞赋值是“先算好右边,再统一赋值”,所有赋值同时发生。

看个例子你就明白了:

// 错误示范:时序逻辑用了阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;  // 这里c拿到的是a的新值,不是旧值
end

// 正确做法:时序逻辑用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;  // 这里c拿到的是a的旧值,符合寄存器行为
end

我曾经在项目里看到有人把阻塞和非阻塞混用,结果仿真和综合结果不一致。那哥们查了三天,最后发现是赋值方式用错了。所以,记住这个铁律:时序逻辑永远用 <=,组合逻辑永远用 =

核心总结
- wire 配 assign,reg 配 always
- 组合逻辑用 =,时序逻辑用 <=
- always @(*) 写组合逻辑,always @(posedge clk) 写时序逻辑
- 敏感列表写全,别漏信号

3.5 知识体系总览

为了让你对本章内容有个整体印象,我画了张图。你看看,其实Verilog基础就这些东西,环环相扣。

Verilog基础回顾 模块结构 数据类型 连续赋值 过程赋值 端口声明 / 内部逻辑 assign 语句 阻塞 vs 非阻塞 wire配assign,reg配always,时序用<=,组合用=

这张图把本章的核心逻辑串起来了。你记住:模块是外壳,wire和reg是血肉,assign和always是灵魂。搞懂这四样,Verilog就算入门了。

好,今天就到这儿。下一节咱们聊运算符和表达式,那又是另一片天地。


专注资料整理