1. FPGA调试基础:常见错误分类与准备工作

做FPGA设计这么多年,我最大的体会就是:调试比设计更花时间。很多刚入行的朋友,写完代码就往板子上烧,结果灯不亮、波形不对,就开始抓瞎。其实,调试是有章可循的。

今天这一讲,我们先打好基础。把错误分分类,把准备工作做足。磨刀不误砍柴工嘛。

1.1 常见错误分类:先知道敌人长什么样

FPGA开发中的错误,我习惯分成三类。你想想看,就像看病一样,先得知道是感冒、骨折还是中毒,才能对症下药。

1.1.1 语法错误

这类错误最「友好」。为什么?因为编译器会直接告诉你哪里错了。

比如你写Verilog时漏了个分号,或者端口声明写错了。综合工具会报错,甚至告诉你行号。我刚开始学的时候,一天能遇到十几次语法错误。嗯,这很正常。

典型表现: 综合/仿真报错,无法生成比特流。
// 错误示例:端口声明漏了方向
module test(
    clk,      // 忘了写 input
    rst_n,
    data_out
);
    input clk;      // 这里才补上,但顺序乱了
    ...
endmodule
我的习惯: 写代码时每写完一个模块,就立即做一次语法检查。别攒到一块儿,否则报错信息能把你淹死。

1.1.2 逻辑错误

这类错误最隐蔽。语法没错,综合能过,但功能不对。说白了,就是你脑子里的算法和写出来的代码对不上。

我在项目中遇到过最典型的一个:一个计数器,本应计数到100就清零,结果我写成了 if(cnt == 100)。你猜怎么着?它永远计不到100,因为从0开始,第100个时钟沿时cnt已经是101了。这种边界错误,仿真时一眼就能看出来,但板级调试时能让你找三天。

典型表现: 仿真波形不对,或者板级功能与预期不符。
// 错误示例:计数器边界错误
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        cnt <= 0;
    else if(cnt == 100)  // 应该用 cnt == 99
        cnt <= 0;
    else
        cnt <= cnt + 1;
end
避坑指南: 我曾经因为一个位宽不匹配的问题,在板子上调了整整两天。一个16位的信号赋值给了8位的寄存器,高位被截断,数据一直不对。从那以后,我写代码时一定会检查每个信号的位宽。

1.1.3 时序错误

这类错误最「要命」。功能仿真全对,上板就跑飞。为什么?因为时序没满足。

说白了,就是你的电路跑不到你想要的频率。比如你设了100MHz的时钟约束,但实际路径延迟太大,建立时间不满足。芯片内部就会出现亚稳态,数据随机出错。

典型表现: 板级功能时好时坏,温度一变就出问题,或者某些条件下稳定、某些条件下崩溃。
错误类型 发现时机 调试难度 典型原因
语法错误 综合/仿真阶段 ★☆☆☆☆ 拼写、端口、语法规范
逻辑错误 仿真/板级调试 ★★★☆☆ 算法实现、状态机、边界条件
时序错误 板级调试 ★★★★★ 路径延迟、时钟域、组合逻辑过深

1.2 调试前的准备工作:别急着上板

我见过太多人,代码写完就烧录,然后对着示波器发呆。其实,调试前的准备工作做好了,能省下一半的时间。

1.2.1 仿真环境搭建

仿真不是可选项,是必选项。我个人习惯是:写代码之前先写testbench。听起来反直觉,对吧?但这样做的好处是,你会逼着自己把接口时序想清楚。

一个完整的仿真环境至少包括:

  • 时钟和复位生成:模拟真实的时钟抖动和复位时序
  • 激励输入:覆盖正常、边界、异常三种情况
  • 自动比对:用脚本或断言检查输出是否正确
  • 波形dump:保存关键信号,方便事后分析
我的建议: 别只用默认的仿真时间。我习惯把仿真跑长一点,比如10000个时钟周期。很多bug在短时间仿真里是看不出来的。

1.2.2 约束文件准备

很多人觉得约束文件是综合时才用的,仿真阶段无所谓。错!约束文件定义了你的设计目标。没有约束,综合工具就不知道你的时钟频率是多少,它会用最保守的方式布线,结果就是性能上不去。

我一般会先写一个简单的时序约束:

# 时钟约束
create_clock -name sys_clk -period 10.000 [get_ports clk]

# 输入延迟约束
set_input_delay -clock sys_clk -max 5.000 [get_ports data_in]
set_input_delay -clock sys_clk -min 2.000 [get_ports data_in]

# 输出延迟约束
set_output_delay -clock sys_clk -max 6.000 [get_ports data_out]
set_output_delay -clock sys_clk -min 1.000 [get_ports data_out]
注意: 约束不是越紧越好。我曾经把约束设得太严,结果综合工具为了满足时序,把面积撑大了两倍。后来发现,实际需求根本没那么高。约束要实事求是。

1.3 版本管理的重要性:血的教训

这一节,我想重点说说版本管理。为什么?因为我吃过亏。

几年前,我做一个通信项目。调试到一半,发现一个bug。我改了几行代码,重新综合,上板测试。结果bug没修好,原来的功能也坏了。我想回退,但发现——我忘了备份原始代码

你想想看,几十个模块,几百个文件,我根本记不清改了哪里。最后只能对着git log一行一行地看。那两天,我几乎没合眼。

从那以后,我给自己定了几条铁律:

  1. 每次修改前,先提交一次。哪怕只是改了一个注释,也要commit。
  2. 用分支管理不同版本。开发分支、稳定分支、实验分支,各司其职。
  3. commit信息要写清楚。别写「fix bug」这种废话,要写「修复FIFO空标志在复位后误判的问题」。
  4. 每天下班前push一次。硬盘坏了、电脑丢了,代码还在服务器上。
版本管理工具推荐: Git是目前的主流。如果你还在用复制文件夹的方式做版本管理,我建议你立刻改过来。真的,不骗你。

嗯,这一章的内容就到这里。总结一下:

  • 错误分三类:语法、逻辑、时序。调试难度依次递增。
  • 调试前做好仿真环境和约束文件,能省一半时间。
  • 版本管理不是锦上添花,是保命用的。

下一章,我们会深入讲仿真技巧。我会分享一些我常用的仿真调试方法,包括如何高效地定位bug、如何写可复用的testbench。到时候见。