3. Verilog基础回顾:模块化设计、组合逻辑与时序逻辑、阻塞与非阻塞赋值

好,咱们进入第三章。这一章我打算带大家快速过一遍Verilog的核心基础。别觉得基础就简单,高频交易里很多诡异的bug,追根溯源都是这些基础没打牢。我自己带团队时,新人写代码最容易出问题的,恰恰就是阻塞和非阻塞赋值这块。

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

模块化设计,说白了就是“分而治之”。你想想看,一个高频交易信号处理系统,可能包含行情解析、订单生成、风控逻辑、时钟管理……如果全塞在一个模块里,那代码量得上万行,后期维护简直是噩梦。

我个人习惯,每个模块只干一件事。比如:

  • 行情解析模块:只负责把网络数据包转成内部总线格式
  • 信号计算模块:只做数学运算,比如移动平均、方差计算
  • 订单生成模块:根据计算结果,生成买卖指令

每个模块都有清晰的接口定义。我在项目中遇到过,有人把行情解析和信号计算写在一个模块里,结果行情格式一改,整个模块都得重写。拆开之后,改一个模块就行,其他模块纹丝不动。

模块化设计三原则:

  • 高内聚:模块内部功能紧密相关
  • 低耦合:模块之间只通过端口通信
  • 接口清晰:每个端口都有明确的时序和功能定义

来看一个简单的模块定义示例:

module signal_calculator (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [31:0] price_in,      // 输入价格
    input  wire        data_valid,    // 数据有效标志
    output reg  [31:0] avg_price,     // 平均价格输出
    output reg         result_ready   // 结果就绪标志
);

// 内部逻辑写在这里

endmodule

嗯,这里要注意:端口方向一定要写对。input对应wire,output可以是reg或wire。我见过有人把output写成input,综合出来一堆奇怪的错误。

3.2 组合逻辑:信号直来直去

组合逻辑,就是输出只取决于当前输入,跟历史状态没关系。说白了,就是一堆逻辑门连在一起,信号从输入到输出,一路直通。

在Verilog里,组合逻辑通常用 assign 语句或者 always @(*) 块来实现。

用assign实现:

wire [31:0] sum;
assign sum = a + b;  // 组合逻辑,a和b一变,sum立刻变

用always @(*)实现:

reg [31:0] result;
always @(*) begin
    if (sel)
        result = a + b;
    else
        result = a - b;
end

这里有个坑,我必须要说。在 always @(*) 块里,如果你漏掉了某个分支,综合器会给你生成一个锁存器(latch)。锁存器这东西,在高频交易里是灾难——时序不可控,容易产生毛刺。

避坑指南: 我曾经在一个行情解析模块里,因为if-else少写了一个else分支,综合出来一个latch。结果行情数据一波动,那个latch就锁住了错误的值,导致后续所有信号计算全部跑偏。排查了整整两天才找到原因。

所以,写组合逻辑时,要么用 case 写全所有分支,要么if-else配对完整。实在不行,在default或else里给个默认值。

3.3 时序逻辑:记住过去的状态

时序逻辑就不一样了。它需要时钟边沿来驱动,输出不仅取决于当前输入,还取决于之前的状态。说白了,就是带“记忆”的电路。

在Verilog里,时序逻辑用 always @(posedge clk)always @(negedge clk) 来实现。

reg [31:0] counter;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        counter <= 32'd0;
    else if (enable)
        counter <= counter + 1'b1;
end

你看,这个计数器在每个时钟上升沿才更新。就算输入enable在时钟中间变了,计数器也不会立刻反应——它要等到下一个时钟沿。

在高频交易里,时序逻辑是核心。所有的信号处理、状态机、流水线,都靠时钟来同步。我个人习惯,所有时序逻辑都用 同步复位,也就是复位信号也跟时钟同步。异步复位虽然省资源,但在高频场景下容易出时序违例。

小技巧: 写时序逻辑时,敏感列表里只放时钟和复位。别把其他信号加进去,否则综合器会认为你是组合逻辑,或者生成奇怪的电路。

3.4 阻塞赋值 vs 非阻塞赋值:最容易翻车的地方

好,重点来了。阻塞赋值(=)和非阻塞赋值(<=),是Verilog里最容易搞混的概念。我面试新人时,十个人里有八个答不清楚。

阻塞赋值(=): 执行完当前语句,再执行下一条。说白了,是“串行”的。

非阻塞赋值(<=): 所有赋值同时生效。说白了,是“并行”的。

看个例子就明白了:

// 阻塞赋值示例
always @(posedge clk) begin
    a = b;
    c = a;  // c得到的是a更新后的值,也就是b
end

// 非阻塞赋值示例
always @(posedge clk) begin
    a <= b;
    c <= a;  // c得到的是a更新前的值,不是b
end

为什么会这样?因为非阻塞赋值是在时钟沿到来时,先把所有右边的值“拍快照”,然后统一更新左边的变量。所以 c <= a 用的是a的旧值。

黄金法则:

  • 写时序逻辑(always @(posedge clk)):用非阻塞赋值 <=
  • 写组合逻辑(always @(*) 或 assign):用阻塞赋值 =

这条法则,我建议你刻在桌子上。

我在项目中遇到过,有人把时序逻辑里的赋值写成了阻塞赋值。结果仿真时看起来没问题,但上板之后,信号总是差一个时钟周期。因为阻塞赋值让信号提前更新了,破坏了流水线的时序关系。

还有一个常见错误:在同一个always块里混用阻塞和非阻塞赋值。综合器会报warning,但很多人不当回事。实际上,这会导致仿真和综合结果不一致——仿真时可能对,但综合出来的电路完全不是你想的那样。

避坑指南: 我曾经调试一个订单生成模块,仿真跑了上千个case都正确,但一上实盘就偶尔丢订单。最后发现,是一个always块里同时用了 =<=,导致综合出来的电路多了一级不必要的流水线。订单在极端情况下被“吞”掉了。

所以,一个always块里,要么全用 =,要么全用 <=。别混着来。

3.5 知识体系总览

下面这张图,是我自己整理的本章知识结构。你可以把它当成一个“速查地图”,写代码时忘了就回来看一眼。

Verilog基础回顾:知识体系 模块化设计 组合逻辑 时序逻辑 高内聚、低耦合 接口清晰、端口定义 模块拆分原则 assign 语句 always @(*) 避免latch(补全分支) always @(posedge clk) 同步复位 vs 异步复位 敏感列表只放clk和rst 核心对比:阻塞赋值(=) vs 非阻塞赋值(<=) 组合逻辑用 = 时序逻辑用 <= 串行执行 并行更新(拍快照)

这张图把三大块的关系理清楚了。模块化设计是顶层架构,组合逻辑和时序逻辑是底层实现。而阻塞与非阻塞赋值,是连接这两者的“语法开关”——用对了,电路就听话;用错了,bug就来了。

好了,这一章的内容就这些。记住我说的黄金法则,写代码时多留个心眼。下一章我们会把这些基础用到实际的高频交易信号处理中,到时候你会看到,这些看似简单的概念,在纳秒级的竞争中是如何决定成败的。


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