3. 硬件描述语言基础(Verilog):模块化设计、组合逻辑与时序逻辑、阻塞与非阻塞赋值、Testbench编写
各位同学,咱们今天聊聊Verilog。说实话,很多新手觉得Verilog就是写代码,跟C语言差不多。嗯,这个想法很危险。我当年刚入行时也这么想,结果写出来的东西综合出来一堆莫名其妙的Latch,被老工程师骂得狗血淋头。
Verilog不是软件语言,它是用来描述硬件的。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。脑子里没有电路图,代码写得再花哨也没用。
3.1 模块化设计:搭积木的艺术
模块化设计,说白了就是把大系统拆成小模块。每个模块干一件事,干好一件事。我在做行情加速卡时,会把整个系统拆成:UDP解析模块、行情解码模块、排序模块、分发模块。每个模块独立设计、独立验证,最后再拼起来。
一个标准的Verilog模块长这样:
module udp_parser (
input wire clk,
input wire rst_n,
input wire [7:0] data_in,
input wire data_valid,
output reg [31:0] payload,
output reg payload_valid
);
// 内部逻辑写在这里
endmodule
注意看,模块的端口列表就是它的对外接口。我习惯把所有输入输出都写清楚,这样别人一看就知道这个模块干什么用。端口类型就三种:input、output、inout。别乱用inout,那玩意儿容易出问题。
3.2 组合逻辑与时序逻辑:两个世界
这是Verilog里最重要的概念,没有之一。你想想看,数字电路就两种:组合逻辑和时序逻辑。
组合逻辑:输出只取决于当前输入。没有记忆功能,信号来了就反应。用assign或者always @(*)实现。
// 组合逻辑:一个简单的加法器
assign sum = a + b;
// 或者用always块
always @(*) begin
if (sel)
out = a;
else
out = b;
end
时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。说白了就是有记忆,靠时钟沿触发。用always @(posedge clk)实现。
// 时序逻辑:一个D触发器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
我曾经犯过一个低级错误:在组合逻辑里用了posedge clk,结果综合出来一堆奇怪的触发器。记住:组合逻辑用@(*),时序逻辑用@(posedge clk),这是铁律。
3.3 阻塞与非阻塞赋值:一个老生常谈的话题
这个知识点,面试必问,项目必用。但说实话,很多人用了好几年都没搞明白。
简单说:
- 阻塞赋值
=:顺序执行,前面的赋值完才执行后面的。用于组合逻辑。 - 非阻塞赋值
<=:并行执行,所有赋值同时发生。用于时序逻辑。
看个例子就明白了:
// 阻塞赋值:组合逻辑
always @(*) begin
a = b + c;
d = a + e; // 这里用的是更新后的a
end
// 非阻塞赋值:时序逻辑
always @(posedge clk) begin
a <= b + c;
d <= a + e; // 这里用的是更新前的a(旧值)
end
为什么会这样?因为硬件里,组合逻辑的信号是立即传播的,而触发器的输出是时钟沿后才更新的。非阻塞赋值模拟的就是触发器的行为。
我见过太多新手在时序逻辑里用阻塞赋值,结果仿真和实际行为对不上。记住一个口诀:组合用=,时序用<=。别问为什么,先记住,等你写了几万行代码自然就懂了。
- 同一个always块里,不要混用
=和<= - 时序逻辑的always块里,全部用
<= - 组合逻辑的always块里,全部用
=
3.4 Testbench编写:验证才是硬道理
写代码只占30%的时间,剩下70%都在调试和验证。Testbench就是你的调试工具。
一个基本的Testbench结构:
module tb_udp_parser();
// 1. 声明信号
reg clk;
reg rst_n;
reg [7:0] data_in;
reg data_valid;
wire [31:0] payload;
wire payload_valid;
// 2. 实例化待测模块
udp_parser u_dut (
.clk (clk),
.rst_n (rst_n),
.data_in (data_in),
.data_valid (data_valid),
.payload (payload),
.payload_valid (payload_valid)
);
// 3. 生成时钟
always #5 clk = ~clk; // 100MHz时钟
// 4. 测试激励
initial begin
// 初始化
clk = 0;
rst_n = 0;
data_in = 0;
data_valid = 0;
// 复位
#20 rst_n = 1;
// 发送数据
#10 data_in = 8'h55;
data_valid = 1;
#10 data_valid = 0;
// 等待结果
#100;
$finish;
end
// 5. 监控输出
always @(posedge clk) begin
if (payload_valid)
$display("Payload = %h", payload);
end
endmodule
写Testbench有几个要点:
- 时钟生成:用
always块生成时钟,别用initial里的循环 - 复位时序:先复位再释放,给模块一个确定的初始状态
- 激励生成:用
initial块按时间顺序发送激励 - 结果检查:用
$display或$monitor打印关键信号
3.5 知识体系总览
下面这张图,是我自己总结的Verilog基础体系。你照着这个框架去学,不会走偏。
这张图把Verilog基础分成了四大块:模块化设计、组合逻辑、时序逻辑、Testbench。每一块都有对应的核心知识点。你学的时候,可以对照这张图,看看自己哪个部分还没掌握。
最后说一句:Verilog不难,难的是用硬件的思维去思考。多写、多仿真、多看看综合后的电路图,慢慢就找到感觉了。
- 模块化设计:每个模块只做一件事,端口清晰
- 组合逻辑:用
assign或always @(*),用阻塞赋值= - 时序逻辑:用
always @(posedge clk),用非阻塞赋值<= - Testbench:时钟生成、激励发送、结果检查三步走
- 避免latch:组合逻辑里所有分支写全
好了,这一章就到这里。记住,写Verilog的时候,脑子里要有电路图。
公众号:蓝海资料掘金营,微信deep3321