4、扫描链插入流程:RTL代码准备、综合工具设置、扫描链插入命令、验证扫描链连接
好,咱们进入第四章。扫描链插入,这可以说是DFT工程师的看家本领了。很多刚入行的朋友觉得这步就是跑个脚本完事,其实不然。我做了这么多年,踩过的坑比走过的路还多,今天就把这些经验掰开揉碎了讲给你听。
4.1 RTL代码准备:别让综合工具“看不懂”
RTL代码准备,说白了就是让你的设计能被综合工具识别出哪些是触发器,哪些是组合逻辑。工具要能准确地找到所有需要插入扫描链的寄存器。
核心要求就两条:
- 时钟和复位要清晰:所有触发器必须由同一个时钟沿(比如上升沿)触发。异步复位信号要统一,别搞什么混合复位。
- 避免“黑盒”:不要用门级网表例化在RTL里。工具看到门级网表,它没法自动推断出里面的寄存器结构。
我个人习惯:在RTL里,我会把所有触发器的时钟端口统一命名为 clk,复位端口统一命名为 rst_n(低有效)。这样综合脚本里写约束也方便,一眼就能看出来。
举个例子,一段规范的RTL代码长这样:
module my_core (
input wire clk,
input wire rst_n,
input wire [7:0] data_in,
output reg [7:0] data_out
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
data_out <= 8'b0;
else
data_out <= data_in;
end
endmodule
嗯,这里要注意:千万别在 always 块里写组合逻辑和时序逻辑混在一起。比如:
// 错误示范!千万别这么写
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_out <= 8'b0;
flag <= 1'b0;
end else begin
data_out <= data_in;
flag = some_comb; // 阻塞赋值混在里面,工具会懵
end
end
我曾经在一个项目里接手过这样的代码,综合工具死活报寄存器识别错误。查了两天才发现是阻塞赋值和非阻塞赋值混用导致的。从那以后,我要求团队所有RTL代码必须通过lint检查才能进入综合流程。
4.2 综合工具设置:告诉工具你要做什么
工具设置,就是给综合工具(比如Synopsys的DC或Cadence的Genus)下指令,告诉它:“嘿,我要做扫描链插入,你帮我准备好。”
关键设置项:
- 定义扫描时钟:指定哪个时钟是扫描时钟。通常就是功能时钟,但有时也会用独立的扫描时钟。
- 定义扫描使能:指定扫描使能信号(scan_enable)。这个信号在测试模式下拉高,让所有触发器进入移位模式。
- 定义扫描输入/输出:指定芯片的扫描输入引脚(scan_in)和扫描输出引脚(scan_out)。
在DC里,典型的设置脚本长这样:
# 设置扫描链
set_scan_configuration -chain_count 1
set_scan_configuration -clock_mixing no_mix
set_scan_configuration -style multiplexed_flip_flop
# 定义扫描端口
create_port -direction in scan_in
create_port -direction out scan_out
create_port -direction in scan_enable
# 设置扫描使能
set_dft_signal -type ScanEnable -port scan_enable -active_state 1
set_dft_signal -type ScanClock -port clk -timing {45 55}
set_dft_signal -type ScanDataIn -port scan_in
set_dft_signal -type ScanDataOut -port scan_out
避坑指南:我曾经遇到过一个问题,扫描时钟的时序设置不对。工具默认时钟占空比是50%,但我的PLL输出占空比是45%。结果插入扫描链后,仿真时移位总出错。后来我把 -timing 参数改成了 {45 55},问题就解决了。所以,时钟的占空比一定要跟实际PLL输出匹配。
4.3 扫描链插入命令:一键生成扫描链
设置都做好了,接下来就是执行插入命令。这一步其实很简单,但背后的逻辑很复杂。
核心命令:
insert_dft:这是DC里的命令,一键完成扫描链插入。connect_scan_chain:有些工具需要手动连接扫描链,这个命令就是干这个的。
在DC里,执行完 insert_dft 后,工具会做以下几件事:
- 把所有触发器替换成带扫描功能的触发器(Muxed-D flip-flop)。
- 根据你定义的扫描使能信号,把触发器的数据输入从功能路径切换到扫描路径。
- 把触发器串成一条或多条链,从
scan_in到scan_out。
完整的插入流程:
# 1. 读入设计
read_verilog my_design.v
current_design my_core
link
# 2. 设置DFT
set_scan_configuration -chain_count 4
set_dft_signal -type ScanEnable -port scan_enable
set_dft_signal -type ScanClock -port clk
set_dft_signal -type ScanDataIn -port {scan_in0 scan_in1 scan_in2 scan_in3}
set_dft_signal -type ScanDataOut -port {scan_out0 scan_out1 scan_out2 scan_out3}
# 3. 插入扫描链
insert_dft
# 4. 保存结果
write -format verilog -hierarchy -output my_design_scan.v
write_test_protocol -output my_design.spf
注意:insert_dft 执行后,一定要检查工具的报告。看看有没有寄存器没有被替换成扫描触发器。如果有,那说明你的RTL代码有问题,或者约束没设对。我见过有人直接跳过检查就往下走,结果流片回来测试才发现有寄存器扫不进去,那叫一个惨。
4.4 验证扫描链连接:别让链子断了
扫描链插完了,你以为就完事了?不,验证才是重头戏。你想想看,一条链上几百上千个触发器,只要有一个连接错了,整条链就废了。
验证方法:
- 形式验证:用Formality或LEC等工具,对比插入前后的网表,确保功能逻辑没被破坏。
- 仿真验证:写一个简单的测试向量,把一串已知数据(比如
10101010)从scan_in扫进去,再从scan_out读出来,看是否一致。 - DRC检查:工具自带的DFT DRC(Design Rule Check)会检查扫描链的完整性。
仿真验证的测试向量示例:
// 扫描链测试向量(简化版)
initial begin
// 复位
rst_n = 0;
#100 rst_n = 1;
// 进入移位模式
scan_enable = 1;
// 扫入数据:10101010
repeat (8) begin
scan_in = data_to_shift[7]; // 从高位开始
@(posedge clk);
data_to_shift = data_to_shift << 1;
end
// 切换到捕获模式
scan_enable = 0;
@(posedge clk);
// 切回移位模式,扫出数据
scan_enable = 1;
repeat (8) begin
@(posedge clk);
data_out_shift = {data_out_shift[6:0], scan_out};
end
// 比较扫入和扫出的数据
if (data_out_shift == 8'b10101010)
$display("Scan chain test PASS!");
else
$display("Scan chain test FAIL!");
end
我个人经验:验证扫描链连接,我最喜欢用“走马灯”测试。就是扫入一串 00000001,然后看它是不是在每个时钟周期往后移一位。如果某个周期 scan_out 没变化,那肯定就是那个位置断了。这个方法简单粗暴,但非常有效。
另外,别忘了检查扫描链的时序。扫描链上的触发器是串在一起的,移位时钟频率不能太高,否则信号传不过去。我一般会在综合时加一个 set_clock_transition 约束,确保扫描时钟的边沿够缓,不会产生毛刺。
好了,扫描链插入流程就讲到这里。记住,RTL准备是基础,工具设置是关键,插入命令是执行,验证是保障。四步缺一不可。下一章我们聊聊ATPG,那又是另一个有意思的话题了。