第4章:Verilog基础回顾——模块化设计、always块、assign语句、寄存器与组合逻辑、Testbench编写基础
各位同学,欢迎来到DFT测试环境搭建的第一站。说实话,很多刚入行的朋友一上来就盯着Scan Chain、ATPG这些高大上的概念,结果写Testbench时连always和assign都分不清,代码跑起来全是红叉叉。
我当年带过一个新人,上来就问我:“老师,为什么我的寄存器没锁住数据?”我一看代码,好家伙,他把组合逻辑和时序逻辑全写在一个always块里了。嗯,这种坑咱们今天就得填上。
这一章,咱们把Verilog里最核心的几个基础概念过一遍。别嫌简单,DFT工程师写测试代码时,天天跟这些东西打交道。
4.1 模块化设计——把大问题拆成小积木
Verilog的设计哲学,说白了就是“搭积木”。一个复杂的芯片,你不可能在一个文件里写完所有逻辑。你得把它拆成一个个模块(module),每个模块干一件事。
举个例子,一个简单的DFT测试模块,可能包含:
- 顶层模块:负责连接所有子模块
- 扫描链控制模块:管理shift和capture模式
- 测试时钟模块:产生测试用的时钟信号
- 响应分析模块:比对输出结果
每个模块都有固定的结构:
module module_name (
input wire clk,
input wire rst_n,
input wire data_in,
output reg data_out
);
// 内部逻辑写在这里
endmodule
我个人习惯,每个模块只做一件事。比如一个模块只负责产生时钟,另一个只负责数据比对。这样调试的时候,你一眼就能看出问题出在哪。
4.2 assign语句——组合逻辑的直通车
assign语句,说白了就是“连线”。它描述的是组合逻辑,没有时钟,没有存储。输入一变,输出立刻跟着变。
比如,我们要做一个简单的与门:
assign result = a & b;
再比如,一个多路选择器:
assign out = sel ? data1 : data0;
我在项目中遇到过一个问题:有人用assign给寄存器赋值,结果综合出来一堆锁存器。为什么?因为assign只能描述组合逻辑,你没法用它描述“在时钟上升沿锁存数据”这种时序行为。
4.3 always块——时序逻辑与组合逻辑的舞台
always块是Verilog里最灵活、也最容易出错的语法。它既可以描述时序逻辑,也可以描述组合逻辑。关键看敏感列表怎么写。
时序逻辑(寄存器):
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
注意这里用的是“<=”(非阻塞赋值)。为什么?因为时序逻辑需要模拟寄存器的行为:所有赋值在时钟沿同时发生,不会互相干扰。
组合逻辑:
always @(*) begin
if (sel)
out = data1;
else
out = data0;
end
这里用的是“=”(阻塞赋值)。敏感列表写成“@(*)”表示“所有输入信号变化都触发”。
你想想看,如果把组合逻辑写成“<=”,会怎么样?仿真结果会延迟一个时钟周期,综合出来的电路跟你想要的完全不一样。我刚开始学的时候,就因为这个bug调了整整两天。
- 描述寄存器(时序逻辑):用 always @(posedge clk),用 <= 赋值
- 描述组合逻辑:用 always @(*),用 = 赋值
- 千万别混着用!一个always块里要么全是组合,要么全是时序
4.4 寄存器与组合逻辑——数字电路的两大基石
数字电路就两种东西:寄存器和组合逻辑。寄存器存数据,组合逻辑算数据。
寄存器(Flip-Flop):
- 在时钟沿采样输入,保持输出
- 用于状态存储、流水线、计数器
- 在DFT中,扫描链就是把所有寄存器串起来
组合逻辑:
- 输出只取决于当前输入
- 用于算术运算、数据选择、译码
- 没有存储能力,信号传播有延迟
我画了一张图,帮你理清它们的关系:
在DFT测试中,我们最关心的就是这些寄存器能不能正常工作。扫描链测试,就是把所有寄存器连成一条链,然后灌数据进去,看能不能原样吐出来。
4.5 Testbench编写基础——给设计当“考官”
写Testbench,说白了就是给你的设计模块写一个“考官”。这个考官负责:
- 产生输入激励(时钟、复位、数据)
- 监控输出响应
- 判断结果对不对
一个最简单的Testbench长这样:
module tb_counter();
// 1. 声明信号
reg clk;
reg rst_n;
wire [3:0] count;
// 2. 实例化待测模块
counter u_counter (
.clk (clk),
.rst_n (rst_n),
.count (count)
);
// 3. 产生时钟
always #5 clk = ~clk; // 周期10ns
// 4. 产生复位
initial begin
clk = 0;
rst_n = 0;
#20;
rst_n = 1;
end
// 5. 监控输出
initial begin
#100;
$display("count = %d", count);
#200;
$finish;
end
endmodule
这里有几个要点:
- reg和wire:Testbench里,驱动输入用reg,观察输出用wire
- initial块:用来产生一次性的激励,比如复位
- always块:用来产生周期性的信号,比如时钟
- $display和$monitor:打印调试信息
嗯,这里要注意一点:Testbench本身是不可综合的。它只在仿真器里跑,目的是验证你的设计功能对不对。所以你可以随便用initial、$display这些语法,不用考虑硬件能不能实现。
我曾经犯过一个低级错误:在Testbench里用always块驱动复位信号,结果复位一直在跳变,仿真根本没法跑。后来才反应过来,复位应该用initial块,只拉低一次就够了。
小结
这一章咱们把Verilog的四大基础过了一遍:
- 模块化设计:把大问题拆成小模块,每个模块只干一件事
- assign语句:描述组合逻辑,输入一变输出立刻变
- always块:既能描述时序逻辑(寄存器),也能描述组合逻辑,关键看敏感列表和赋值方式
- 寄存器与组合逻辑:数字电路的两大基石,一个存数据,一个算数据
- Testbench编写:给设计当考官,产生激励、监控输出、判断结果
这些基础打牢了,后面学Scan Chain、ATPG、Boundary Scan的时候,你才能把精力放在DFT本身,而不是被Verilog语法绊住脚。
可可精品资料库