3. Verilog基础回顾(上):模块与端口、数据类型(reg/wire)、组合逻辑(assign/always)、阻塞与非阻塞赋值

各位同学,欢迎来到实战课的第一站。

我知道,很多人一听到「Verilog基础回顾」就觉得枯燥。但说实话,这部分恰恰是高频交易系统里最容易出bug的地方。我见过太多人,时序分析通不过、仿真结果对不上,最后发现都是基础概念没吃透。

今天咱们不讲虚的,直接上干货。我会结合我在FPGA交易系统项目中的实际踩坑经历,带你把这几个核心概念彻底理清。

3.1 模块与端口:你的第一个硬件单元

Verilog里,模块(module) 就是最小的硬件单元。你可以把它想象成一个黑盒子——有输入、有输出、内部有逻辑。

核心要点: 一个模块就是一个硬件电路。不是函数,不是类,是实实在在的电路。

看一个最简单的例子:

module adder (
    input  [7:0] a,      // 8位输入
    input  [7:0] b,      // 8位输入
    output [8:0] sum     // 9位输出,防止溢出
);
    assign sum = a + b;
endmodule

这里有几个关键点:

  • 端口方向:input、output、inout。我建议你尽量少用inout,除非你真的需要双向总线。在交易系统里,我们几乎只用input和output。
  • 端口类型:默认是wire类型。如果你需要寄存器输出,要显式声明为reg。
  • 位宽声明:[7:0]表示8位,高位在左,低位在右。这是业界惯例,别搞反了。

我的习惯: 每个模块的端口我都会写清楚位宽和方向,哪怕只有一个bit。这样代码可读性高,也方便后续维护。

3.2 数据类型:reg vs wire,别再傻傻分不清

这是新手最容易混淆的地方。我当年刚入行时,也在这上面栽过跟头。

特性 wire reg
本质 连线,物理连接 寄存器,存储单元
赋值方式 只能被assign连续赋值 只能在always块中被赋值
默认值 高阻态z 不定态x
综合结果 组合逻辑 可能是组合逻辑或时序逻辑

嗯,这里要注意:reg不一定会综合成寄存器。如果你在always块里写的是组合逻辑(比如用always @(*)),那reg综合出来还是组合逻辑。只有当时钟沿触发的always块,reg才会变成真正的寄存器。

我曾经踩过的坑: 有一次做交易系统的行情解析模块,我把一个中间变量声明为reg,但用的是组合逻辑赋值。结果综合后面积比预期大了30%。后来才发现,工具把reg当成了寄存器来优化,白白浪费了资源。

我的建议很简单:

  • 如果你只是做连线、组合逻辑,用wire
  • 如果你需要存储状态、做时序逻辑,用reg
  • 如果你不确定,先问自己:这个信号需要记住上一个时钟周期的值吗?需要就reg,不需要就wire。

3.3 组合逻辑:assign vs always,两种写法

组合逻辑就是输出只取决于当前输入,没有记忆功能。在Verilog里,有两种写法:

3.3.1 assign语句

简单、直接,适合做数据通路:

// 用assign实现一个4选1多路选择器
module mux4 (
    input  [1:0] sel,
    input  [3:0] a, b, c, d,
    output [3:0] out
);
    assign out = (sel == 2'b00) ? a :
                 (sel == 2'b01) ? b :
                 (sel == 2'b10) ? c : d;
endmodule

3.3.2 always @(*) 语句

更灵活,适合复杂逻辑:

// 用always实现同样的4选1
module mux4_always (
    input  [1:0] sel,
    input  [3:0] a, b, c, d,
    output reg [3:0] out
);
    always @(*) begin
        case (sel)
            2'b00: out = a;
            2'b01: out = b;
            2'b10: out = c;
            2'b11: out = d;
            default: out = 4'b0;
        endcase
    end
endmodule

关键区别: assign的左边必须是wire,always的左边必须是reg。但综合出来的电路是一样的。

我个人习惯是:简单的逻辑用assign,复杂的(比如有case、if-else嵌套)用always。你想想看,用assign写一个复杂的case语句,那代码得多难看?

3.4 阻塞与非阻塞赋值:时序逻辑的灵魂

这是Verilog里最重要的概念,没有之一。在高频交易系统里,时序错了,钱就没了。

3.4.1 阻塞赋值 =

说白了就是「立即生效」。先算完,再往下走。

// 阻塞赋值示例
always @(posedge clk) begin
    a = b;  // 先执行
    c = a;  // 此时a已经更新,c拿到的是b的值
end

3.4.2 非阻塞赋值 <=

「并行生效」。所有赋值同时计算,在时钟沿统一更新。

// 非阻塞赋值示例
always @(posedge clk) begin
    a <= b;  // 计算a的新值,但不立即更新
    c <= a;  // 计算c的新值,用的是a的旧值
end
特性 阻塞赋值 = 非阻塞赋值 <=
执行顺序 顺序执行 并行执行
更新时机 立即更新 时钟沿统一更新
适用场景 组合逻辑 时序逻辑
综合结果 组合逻辑或锁存器 寄存器

铁律: 在同一个always块里,不要混用阻塞和非阻塞赋值。我见过有人这么写,仿真能过,但综合出来的电路完全不是那么回事。

为什么会这样?因为仿真器和综合器对这两种赋值的处理方式不同。仿真器是按顺序执行的,而综合器是并行的。混用会导致仿真和综合结果不一致——这是硬件设计的大忌。

3.4.3 实战中的选择

我的经验法则很简单:

  • 时序逻辑(带时钟沿的always):一律用非阻塞赋值 <=
  • 组合逻辑(always @(*) 或 assign):一律用阻塞赋值 =

记住这个规则,你就能避开90%的时序问题。剩下的10%,等你真正遇到的时候,再来找我。

3.5 知识体系总览

为了让你更直观地理解今天的内容,我画了一张图:

Verilog基础回顾(上)知识体系 模块与端口 数据类型 组合逻辑 赋值方式 端口方向 input / output / inout 位宽声明 [7:0] 高位在左 wire 连线,assign赋值 reg 寄存器,always赋值 assign 连续赋值,简单逻辑 always @(*) 过程赋值,复杂逻辑 阻塞 = 立即生效 非阻塞 <= 并行生效 核心原则 时序逻辑用非阻塞赋值,组合逻辑用阻塞赋值 wire用于连线,reg用于存储

3.6 写在最后

今天的内容就到这里。模块与端口是骨架,数据类型是血肉,组合逻辑是功能,赋值方式是灵魂。这四个概念搞清楚了,后面的时序逻辑、状态机、FIFO设计才能顺利推进。

记住,写Verilog不是写软件。你脑子里想的应该是电路,而不是代码。每次写完一段代码,问自己一句:这个综合出来是什么样子的?

好了,去练练手吧。下一节我们继续深入时序逻辑,到时候我会分享一个我在交易系统里遇到的时序收敛的实战案例。


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