Vivado 2019.2 环境下 LoongArch 单周期 CPU 调试实录:从波形图里揪出那6个隐藏的Bug
Vivado 2019.2环境下LoongArch单周期CPU调试实战波形图里的六个致命陷阱调试一个单周期CPU就像在黑暗中寻找六只不同颜色的蚂蚁——它们可能藏在任何角落而波形图就是你唯一的手电筒。这次在Vivado 2019.2环境下对LoongArch架构CPU的调试经历让我深刻体会到硬件调试与软件debug的截然不同。下面我将分享如何通过波形分析逐一揪出那些让CPU行为失常的六个关键错误。1. 调试环境搭建与初步验证在开始真正的bug狩猎之前必须确保实验环境完全正确配置。Vivado 2019.2虽然已经不是最新版本但对于这个LoongArch CPU实验来说却是最稳定的选择。首先需要确认的是仿真环境的完整性。我使用的是实验提供的压缩包exp6.zip这避免了交叉编译工具链配置可能带来的额外问题。在Vivado中创建工程后关键的准备工作包括IP核版本检查确保所有IP核与Vivado 2019.2兼容避免因版本不匹配导致的异常行为仿真参数设置将仿真运行时间设置为足够长通常至少100微秒以便观察完整指令流波形配置添加所有关键信号到波形窗口特别是pc、指令码、寄存器读写信号等提示在开始调试前建议先完整运行一次仿真保存黄金参考波形作为后续比较基准第一次全仿真运行后我立即发现了异常——程序计数器(pc)的行为不符合预期。本该顺序执行的指令流出现了奇怪的跳转这提示我们可能存在控制流方面的错误。2. 第一个Bug消失的调试信息在检查波形时我首先注意到debug_wb_pc信号始终为零这明显不正常。该信号应该反映每条指令执行时的程序计数器值是调试的重要依据。通过信号追踪发现问题出在mycpu_top.v文件中的调试信息生成部分// 原始错误代码 assign debug_wb_pc pc; // 理论上正确但实际波形显示为零经过仔细检查发现这个信号被意外连接到了一个未使用的端口上导致优化后被综合掉了。修正方法是显式声明这些调试信号的保持属性// 修正后的代码 (* keep true *) assign debug_wb_pc pc; assign debug_wb_rf_we {4{rf_we}}; assign debug_wb_rf_wnum dest; assign debug_wb_rf_wdata final_result;这个错误教会我一个重要教训在FPGA设计中未被使用的信号可能被优化掉特别是调试信号需要特殊处理。3. 第二个BugALU操作数的幽灵值当检查算术逻辑单元(ALU)的操作时波形显示一些运算结果明显异常。特别是移位操作输出与预期不符。深入分析发现两个关键问题操作数来源错误alu_src1连接到了错误的信号线未声明变量final_result被使用但未定义修正后的ALU实例化如下// 修正后的ALU连接 alu u_alu( .alu_op (alu_op), .alu_src1 (src1_is_pc ? pc : rj_value), // 修正操作数1选择 .alu_src2 (src2_is_imm ? imm : rkd_value), .alu_result (alu_result) ); // 显式声明final_result wire [31:0] final_result res_from_mem ? mem_result : alu_result;这个bug导致所有依赖ALU的指令都无法正常工作特别是移位和算术运算。通过波形图可以清晰看到错误的操作数被送入ALU模块。4. 第三个BugBL指令的寄存器写回失效在测试跳转指令时发现bl指令执行后预期的返回地址没有正确保存到寄存器中。波形显示gr_we寄存器写使能信号在bl指令执行时为低电平。问题出在寄存器写使能的生成逻辑上// 原始错误代码 assign gr_we ~inst_st_w ~inst_beq ~inst_bne ~inst_b; // bl被错误排除bl指令需要写回链接寄存器但被错误地包含在不写回的条件中。修正后的逻辑应该明确包含bl指令// 修正后的代码 assign gr_we ~inst_st_w ~inst_beq ~inst_bne ~inst_b | inst_bl;这个错误导致函数调用无法正确返回是较难发现的逻辑设计错误。通过对比波形中bl指令执行时的gr_we信号与预期行为最终锁定了问题。5. 第四个Bug立即数扩展的陷阱在处理立即数指令时发现一些立即数的符号扩展不正确。特别是ui5类型的立即数处理存在问题。原始代码如下assign imm src2_is_4 ? 32h4 : need_si20 ? {i20[19:0], 12b0} : need_ui5 ? rk : // 错误应该使用零扩展的立即数 {{20{i12[11]}}, i12[11:0]};问题在于ui5立即数错误地使用了rk寄存器值而非指令中的立即数字段。修正后assign imm src2_is_4 ? 32h4 : need_si20 ? {i20[19:0], 12b0} : need_ui5 ? {27b0, inst[14:10]} : // 正确零扩展ui5 {{20{i12[11]}}, i12[11:0]};这个错误导致所有使用ui5立即数的移位指令都使用了错误的移位量。通过观察波形中移位指令的实际移位量与指令编码的对比发现了这一不一致性。6. 第五和第六Bug移位操作的暗礁最后两个bug都涉及移位操作但表现不同。第五个bug是逻辑移位结果错误第六个是算术右移的符号扩展问题。SLL/SRL问题 原始SLL实现没有限制移位位数可能导致不可预测行为// 原始错误代码 assign sll_result alu_src1 alu_src2; // 未限制移位位数修正后限制移位位数为5位因为ui5最大值为31// 修正后的代码 assign sll_result alu_src1 alu_src2[4:0]; // 限制移位位数SRA问题 算术右移的符号扩展处理不正确// 原始不完整实现 assign sr64_result {{32{op_sra alu_src1[31]}}, alu_src1[31:0]} alu_src2[4:0];修正后确保符号位正确扩展// 修正后的代码 assign sr64_result $signed({{32{alu_src1[31]}}, alu_src1}) alu_src2[4:0];这两个移位相关的bug导致所有移位指令在特定情况下会产生错误结果。通过编写专门的测试用例在波形中观察不同移位量和不同符号输入时的输出最终定位了这些问题。7. 调试经验与技巧总结经过这六个bug的调试过程我总结出一些有价值的硬件调试技巧波形分析黄金法则从外到内先观察顶层信号如pc、指令码再深入模块内部比较法将异常波形与预期行为做逐周期对比最小化测试为每个可疑问题编写最小测试用例信号完整性检查关键信号是否被意外优化掉Vivado仿真调试实用技巧使用Force功能临时修改信号值快速验证假设合理设置波形分组提高分析效率利用标记(Marker)功能标注关键时间点保存不同版本的波形文件以便比较LoongArch CPU设计特别注意事项立即数扩展方式多样SI12、SI16、SI20、UI5等需仔细检查每种情况跳转指令的偏移量计算要特别注意符号扩展和字节对齐寄存器写回时机要严格符合单周期设计的要求边界条件测试尤为重要如移位量为0或31时这次调试经历让我对CPU内部运作机制有了更深入的理解。硬件调试就像解谜每个bug都是一个独特的谜题而波形图提供了最直接的线索。当你终于找到那个让整个系统行为异常的微小设计错误时那种成就感是无与伦比的。