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 知识体系总览
为了让你更直观地理解今天的内容,我画了一张图:
3.6 写在最后
今天的内容就到这里。模块与端口是骨架,数据类型是血肉,组合逻辑是功能,赋值方式是灵魂。这四个概念搞清楚了,后面的时序逻辑、状态机、FIFO设计才能顺利推进。
记住,写Verilog不是写软件。你脑子里想的应该是电路,而不是代码。每次写完一段代码,问自己一句:这个综合出来是什么样子的?
好了,去练练手吧。下一节我们继续深入时序逻辑,到时候我会分享一个我在交易系统里遇到的时序收敛的实战案例。
公众号:蓝海资料掘金营,微信deep3321