第二章 硬件描述语言基础(Verilog):模块化设计、组合逻辑与时序逻辑、Testbench编写
各位同学,欢迎来到第二章。说实话,Verilog 这门语言,你把它当成软件来学,那就走偏了。它本质上是描述硬件的工具。我见过太多人,C 语言写得飞起,一写 Verilog 就翻车,为什么?因为脑子里还是「顺序执行」那套思维。
这一章,我们不讲那些枯燥的语法大全。我带你从实际工程的角度,把模块化设计、组合逻辑、时序逻辑,还有 Testbench 怎么写,一次性理清楚。这些都是我这些年做 FPGA 加速交易系统时,每天都要打交道的东西。
2.1 模块化设计:把大系统拆成小积木
做量化交易系统,FPGA 里的逻辑动不动就是几万行代码。你不可能在一个文件里写完所有东西。模块化设计,说白了就是「分而治之」。
每个模块只干一件事,干好一件事。接口清晰,内部实现随便改,只要接口不变,整个系统就不受影响。
2.1.1 模块的基本结构
一个 Verilog 模块,就像一块芯片。有输入引脚,有输出引脚,内部是逻辑功能。看个最简单的例子:
module adder (
input wire [7:0] a,
input wire [7:0] b,
output wire [7:0] sum
);
assign sum = a + b;
endmodule
嗯,这里要注意:wire 类型用于组合逻辑,reg 类型用于时序逻辑。这个区分很重要,我后面会细讲。
2.1.2 模块实例化:把积木拼起来
模块写好了,怎么用?实例化。就像你画电路图,把一个个芯片焊到板子上。
module top (
input wire [7:0] data_in1,
input wire [7:0] data_in2,
output wire [7:0] result
);
wire [7:0] sum_out;
// 实例化 adder 模块
adder u_adder (
.a(data_in1),
.b(data_in2),
.sum(sum_out)
);
assign result = sum_out;
endmodule
我个人习惯,实例化时用 u_ 前缀,一眼就能看出这是个模块实例,不是信号。这个小习惯,在大型项目中能省不少 debug 时间。
data_valid 比 dv 好一万倍。你想想看,三个月后你自己回来看代码,看到 dv 还得猜半天。
2.2 组合逻辑:信号一来,立刻反应
组合逻辑,就是没有记忆功能的电路。输入变了,输出立刻变,不依赖时钟。说白了,就是一堆逻辑门连在一起。
2.2.1 用 assign 实现组合逻辑
assign 是最直接的组合逻辑描述方式。它相当于硬件连线。
module and_gate (
input wire a,
input wire b,
output wire y
);
assign y = a & b;
endmodule
我在项目中遇到过一个问题:有人用 assign 写了很长的表达式,比如 assign y = (a & b) | (c & d) | (e & f) ...。综合工具会把它展开成一大堆逻辑门,时序很容易出问题。我的建议是,复杂的组合逻辑,拆成多级,中间加寄存器。
2.2.2 用 always @(*) 实现组合逻辑
另一种写法是用 always 块。注意,组合逻辑要用 @(*),表示敏感列表自动推导。
module mux2to1 (
input wire [3:0] a,
input wire [3:0] b,
input wire sel,
output reg [3:0] y
);
always @(*) begin
if (sel)
y = a;
else
y = b;
end
endmodule
这里 y 声明为 reg 类型,但别被名字骗了。在 always @(*) 块里,reg 只是语法要求,它仍然是组合逻辑。
always @(*) 块里漏写了 else 分支,结果综合出了锁存器(latch)。锁存器在 FPGA 里是大忌,时序难分析,还容易出毛刺。记住:组合逻辑的 always 块,所有条件分支必须写全,或者给变量赋默认值。
2.3 时序逻辑:靠时钟驱动,记住状态
时序逻辑,就是带记忆的电路。它靠时钟沿触发,把当前状态存下来,下一个时钟沿再更新。
为什么需要时序逻辑?你想想看,如果整个系统全是组合逻辑,信号一传播就是几纳秒,稍微长一点路径,时序就收敛不了。用寄存器把路径打断,把计算分到多个时钟周期里,系统才能跑在高频率。
2.3.1 最简单的时序逻辑:D 触发器
module dff (
input wire clk,
input wire rst_n,
input wire [7:0] d,
output reg [7:0] q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 8'd0;
else
q <= d;
end
endmodule
注意几个关键点:
posedge clk:上升沿触发。FPGA 里绝大多数时序逻辑都用上升沿。negedge rst_n:异步复位,低电平有效。复位信号一来,立刻清零。<=是非阻塞赋值。时序逻辑里必须用这个,和组合逻辑的=区分开。
= 是「立刻生效」,非阻塞赋值 <= 是「下一个时钟沿生效」。这个搞混了,仿真结果和实际硬件会完全不一样。我见过有人 debug 了一整天,最后发现就是赋值符号用错了。
2.3.2 时序逻辑的典型应用:计数器
计数器在交易系统里太常见了。比如,你要在收到行情数据后,等待固定周期再发出订单,就需要计数器。
module counter (
input wire clk,
input wire rst_n,
input wire en,
output reg [7:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'd0;
else if (en)
count <= count + 1'b1;
end
endmodule
嗯,这里要注意:计数器溢出后会自动回绕到 0。如果你需要特定范围的计数,要加比较逻辑。
2.4 Testbench 编写:仿真才是硬道理
代码写完了,直接上板子?别急。我见过太多人,代码一写就烧录,结果板子冒烟了都不知道哪里错。正确的流程是:先仿真,再综合,最后上板。
Testbench 就是用来仿真的。它本身不是硬件,是软件,用来给我们的模块施加激励,检查输出。
2.4.1 一个完整的 Testbench 模板
`timescale 1ns / 1ps
module tb_adder;
// 信号声明
reg [7:0] tb_a;
reg [7:0] tb_b;
wire [7:0] tb_sum;
// 实例化被测试模块
adder u_adder (
.a(tb_a),
.b(tb_b),
.sum(tb_sum)
);
// 产生激励
initial begin
tb_a = 8'd0;
tb_b = 8'd0;
#10;
tb_a = 8'd10;
tb_b = 8'd20;
#10;
tb_a = 8'd100;
tb_b = 8'd200;
#10;
tb_a = 8'd255;
tb_b = 8'd1;
#10;
$finish;
end
// 监控输出
initial begin
$monitor("Time=%0t, a=%d, b=%d, sum=%d", $time, tb_a, tb_b, tb_sum);
end
endmodule
这里有几个关键点:
`timescale:定义时间单位。1ns/1ps 表示单位是 1ns,精度是 1ps。initial块:只执行一次,用来产生激励。#10:延时 10 个时间单位,也就是 10ns。$monitor:每次信号变化时打印信息,方便 debug。
2.4.2 带时钟的 Testbench
对于时序逻辑,我们需要产生时钟信号。
`timescale 1ns / 1ps
module tb_counter;
reg tb_clk;
reg tb_rst_n;
reg tb_en;
wire [7:0] tb_count;
counter u_counter (
.clk(tb_clk),
.rst_n(tb_rst_n),
.en(tb_en),
.count(tb_count)
);
// 产生时钟:周期 10ns,频率 100MHz
initial begin
tb_clk = 0;
forever #5 tb_clk = ~tb_clk;
end
// 产生激励
initial begin
tb_rst_n = 0;
tb_en = 0;
#20;
tb_rst_n = 1;
#10;
tb_en = 1;
#100;
tb_en = 0;
#50;
$finish;
end
// 波形导出
initial begin
$dumpfile("counter.vcd");
$dumpvars(0, tb_counter);
end
endmodule
$dumpfile 和 $dumpvars,把波形导出来。看波形比看打印信息直观得多。你一眼就能看出信号什么时候跳变,有没有毛刺,时序对不对。
2.5 本章知识体系总览
下面这张图,是我自己总结的 Verilog 基础学习路径。你照着这个顺序学,不会走偏。
从模块化设计开始,掌握组合逻辑和时序逻辑这两个核心,最后用 Testbench 验证你的设计是否正确。每一步都踩实了,后面写复杂的交易系统才不会慌。
好了,这一章的内容就到这里。代码示例我都跑过仿真,你可以直接复制到 Vivado 或 Modelsim 里试试。有什么问题,欢迎交流。
公众号:蓝海资料掘金营,微信deep3321