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 知识体系总览
下面这张图,是我自己整理的本章知识结构。你可以把它当成一个“速查地图”,写代码时忘了就回来看一眼。
这张图把三大块的关系理清楚了。模块化设计是顶层架构,组合逻辑和时序逻辑是底层实现。而阻塞与非阻塞赋值,是连接这两者的“语法开关”——用对了,电路就听话;用错了,bug就来了。
好了,这一章的内容就这些。记住我说的黄金法则,写代码时多留个心眼。下一章我们会把这些基础用到实际的高频交易信号处理中,到时候你会看到,这些看似简单的概念,在纳秒级的竞争中是如何决定成败的。
公众号:蓝海资料掘金营,微信deep3321