3. 硬件描述语言基础(Verilog):模块化设计、组合逻辑与时序逻辑、always块与assign语句、仿真与测试平台
说实话,Verilog 这门语言,刚接触时觉得它跟 C 语言长得挺像。但写了两周你就会发现——完全不是一回事。C 是顺序执行的,Verilog 是并发的。这个思维转变,是新手最容易卡住的地方。
我个人习惯把 Verilog 看作「描述电路的语言」,而不是「写程序的工具」。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。脑子里要有这个画面,代码才不会跑偏。
3.1 模块化设计:把大电路拆成小积木
一个复杂的硬件系统,没人能一口气写完。模块化设计就是把它拆成多个小模块,每个模块负责一个功能。就像搭积木,每个积木简单,组合起来就强大。
一个 Verilog 模块的基本结构长这样:
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
每个模块都有三要素:端口定义、内部逻辑、实例化。端口就是模块的「手脚」,用来跟外界通信。内部逻辑是模块的「大脑」,决定它做什么。
我在项目中遇到过一个问题:一个同事把所有功能写在一个大模块里,足足 2000 行。后来要改一个功能,找了半天才找到位置,还改出了 bug。从那以后,我坚持每个模块不超过 300 行。超过就拆。
wire 和 reg 明确声明。别偷懒用默认类型,后期维护时你会感谢自己的。
3.2 组合逻辑与时序逻辑:两个世界的分界线
这是 Verilog 里最核心的概念,没有之一。搞混了,你的电路就会「跑飞」。
组合逻辑:输出只取决于当前输入。没有记忆功能。比如一个加法器,输入 1+1,输出就是 2。输入变了,输出立刻变。
时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。它有记忆功能。比如计数器,它记得自己数到哪了。
你想想看,为什么要有这两种逻辑?因为现实世界需要「记忆」。CPU 要记住指令,状态机要记住当前状态,这些都靠时序逻辑。
- 组合逻辑用
assign或always @(*) - 时序逻辑用
always @(posedge clk) - 千万别混用!
我曾经犯过一个低级错误:在时序逻辑的 always 块里用了阻塞赋值 =,结果仿真波形完全不对。查了两天才发现是赋值方式的问题。嗯,这里要注意——时序逻辑用非阻塞赋值 <=,组合逻辑用阻塞赋值 =。这是铁律。
3.3 always块与assign语句:两种描述方式
说白了,Verilog 就两种描述电路的方式:assign 和 always。
assign 语句:用来描述组合逻辑。它连续赋值,只要右边变了,左边立刻更新。
assign sum = a + b;
assign sel = (addr > 8'hFF) ? 1'b1 : 1'b0;
always 块:既可以描述组合逻辑,也可以描述时序逻辑。关键看敏感列表怎么写。
// 组合逻辑
always @(*) begin
if (sel)
out = a;
else
out = b;
end
// 时序逻辑
always @(posedge clk) begin
if (rst_n)
q <= 0;
else
q <= d;
end
我个人习惯:简单的组合逻辑用 assign,复杂的用 always @(*)。时序逻辑一律用 always @(posedge clk)。这样代码读起来一目了然。
always @(*) 里,如果某个分支没写全,综合时会生成锁存器(latch)。这玩意通常不是你想要的。我曾经因为一个 if 少了 else,导致芯片功耗超标。所以,组合逻辑的 always 块,一定要把所有条件写全。
3.4 仿真与测试平台:不仿真的代码都是耍流氓
写完了代码,怎么知道它对不对?靠仿真。仿真就是给电路输入各种激励,看它输出是否符合预期。
测试平台(testbench)是仿真的核心。它没有端口,内部实例化被测试模块,然后产生激励信号。
module tb_counter;
reg clk;
reg rst_n;
reg en;
wire [7:0] count;
// 实例化被测试模块
counter u_counter (
.clk (clk),
.rst_n(rst_n),
.en (en),
.count(count)
);
// 产生时钟
initial begin
clk = 0;
forever #5 clk = ~clk; // 周期 10ns
end
// 产生激励
initial begin
rst_n = 0;
en = 0;
#20;
rst_n = 1;
#10;
en = 1;
#100;
en = 0;
#50;
$finish;
end
// 监控输出
initial begin
$monitor("time=%0t, count=%d", $time, count);
end
endmodule
你看,测试平台里用了 initial 块来产生激励。这是仿真特有的结构,不能综合成硬件。它只在仿真时存在。
我建议你养成一个习惯:写模块之前,先写测试计划。列出要测哪些功能、边界条件、异常情况。然后根据测试计划写 testbench。这样不会漏测。
$monitor 和 $display 打印关键信号。波形图虽然直观,但有时候看打印信息更快定位问题。我调试时经常两者结合着用。
3.5 本章知识体系
下面这张图总结了本章的核心逻辑。你看一眼,就能把知识点串起来。
这张图把本章内容串起来了。模块化是骨架,组合逻辑和时序逻辑是血肉,仿真测试是检验手段。三者缺一不可。
最后说一句:Verilog 不难,难的是建立「硬件思维」。多写、多仿、多查波形,慢慢就上手了。别怕犯错,我到现在偶尔还会翻车呢。
可可精品资料库