C3: 调试技巧

一、阶段目标

C3 阶段的核心目标是掌握系统性的硬件调试方法论,能够高效地定位和修复 RTL 设计中的 bug。完成后你将:

  1. 建立"分层定位"的调试思维——从宏观到微观逐步收窄范围
  2. 熟练使用波形 (GTKWave) 分析时序行为
  3. 掌握 DiffTest 报错信息的快速解读
  4. 理解常见 bug 模式及其排查路径
  5. 能够独立定位和修复处理器设计中的各类错误

二、调试方法论:分层定位

2.1 核心原则

不要猜测,要观察。不要随机修改代码,要系统排查。

调试流程遵循"从高层到底层"的原则:

第1层: DiffTest 报错 → 哪条指令出错?哪个寄存器不一致?
         │
         ▼
第2层: 反汇编确认 → 这条指令应该做什么?
         │
         ▼
第3层: 信号追踪 → IDU 输出的控制信号正确吗?
         │
         ▼
第4层: 波形验证 → 数据通路上哪一级的数据出了问题?
         │
         ▼
第5层: 根因修复 → Verilog 代码具体哪一行写错了?

2.2 调试工具层级

层级 工具 适用场景 效率
高层 DiffTest 发现"哪条指令出错" ★★★★★
中层 ITRACE/IRINGBUF 查看出错前后的指令流 ★★★★
中层 MTRACE 内存访问地址/数据错误 ★★★
底层 波形 (GTKWave) 分析信号时序关系 ★★
底层 printf/DPI-C 打印 观察特定信号值 ★★

效率越高的工具越先使用——DiffTest 能在几秒内定位到出错指令,而波形分析可能需要几十分钟。


三、DiffTest 报错解读

3.1 典型报错信息

difftest error at nextpc = 0x800000a4, reg a0 is diff: ref = 0x00000005, dut = 0x00000003

解读

  • nextpc = 0x800000a4 → 出错指令的下一条地址,即出错指令在 0x800000a0
  • reg a0 is diff → a0 寄存器值不一致
  • ref = 5, dut = 3 → 参考模型算出 5,你的硬件算出 3

3.2 从报错到定位

# Step 1: 找到出错的指令
riscv64-linux-gnu-objdump -d build/xxx.elf | grep "800000a0"
# 输出: 800000a0: 00b50533  add a0, a0, a1

# Step 2: 分析预期行为
# add a0, a0, a1 → a0 = a0 + a1
# ref 说结果是 5 → 说明 a0=2, a1=3 (2+3=5)
# dut 说结果是 3 → 可能把 a1 的值直接放到了 a0?或者 add 做成了 mov?

# Step 3: 检查 IDU 和 EXU
# 看 IDU 是否正确输出: exu_opt=ADD, exu_src_sel=REG
# 看 EXU 的两个输入 src1 和 src2 是否正确

3.3 常见报错模式与含义

报错模式 可能原因
PC diff 分支条件判断错误 / 跳转地址计算错
rd 差一个固定值 立即数符号扩展错误
rd 值完全无关 exu_opt 或 exu_src_sel 错误
rd 值和 rs2 相同 R 型被当成了 mov (可能 IDU 没设 exu_opt)
Load 值错位 地址不对齐 / 字节序处理错误
CSR diff ecall/mret 处理逻辑有误

四、反汇编辅助调试

4.1 生成反汇编文件

每次编译 AM 程序时会自动生成:

# 位于:
build/xxx-riscv32e-npc.txt   # objdump -d 的输出

4.2 快速定位指令

# 在反汇编文件中搜索出错地址
grep "800000a0" build/add-riscv32e-npc.txt

# 或者查看某个函数
grep -A 20 "<add>:" build/add-riscv32e-npc.txt

4.3 手动解码指令

如果你看到一个 32-bit 机器码想知道它是什么指令:

机器码: 0x00b50533

二进制: 0000000 01011 01010 000 01010 0110011
        ─────── ───── ───── ─── ───── ───────
        funct7  rs2   rs1   f3  rd    opcode

opcode = 0110011 → R 型
funct3 = 000 → ADD/SUB
funct7 = 0000000 → ADD
rs1 = 01010 = 10 = a0
rs2 = 01011 = 11 = a1
rd  = 01010 = 10 = a0

结论: add a0, a0, a1

五、波形调试技巧

5.1 准备波形

# 限制仿真周期,避免波形文件过大
make run WAVE=1 MAXCYCLE=10000 DIFFTEST=1

5.2 GTKWave 使用要点

常用操作

  • 添加信号:在左侧信号列表中双击或拖拽到波形区域
  • 放大/缩小:Ctrl+滚轮,或使用工具栏的 + / - 按钮
  • 搜索时刻:Edit → Find → Search for value/time
  • 标记:在时间轴上点击放置光标,便于来回跳转
  • 分组:将相关信号放入同一组(如 IDU 输出、EXU 输入输出)

推荐观察的信号组

Group 1: 总控
  clock, reset, pc, instr

Group 2: IDU 输出
  exu_opt, exu_src_sel, lsu_opt
  rs1id, rs2id, rdid, rdwen
  imm, brch, jal, jalr

Group 3: 数据通路
  rs1, rs2 (寄存器读出值)
  exu_res (ALU 结果)
  lsu_res (内存读出值)
  rd (写回数据)

Group 4: 流控 (流水线阶段)
  ifu_valid, idu_ready
  idu_valid, exu_ready
  exu_valid, lsu_ready
  lsu_valid, wbu_ready

5.3 波形分析步骤

  1. 定位出错时刻:搜索 pc == 0x800000a0
  2. 确认指令正确取回:检查 instr 信号值是否等于 objdump 显示的机器码
  3. 检查 IDU 输出exu_optexu_src_sel 是否正确
  4. 检查 EXU 输入src1src2 的值是否等于预期的寄存器值
  5. 检查 EXU 输出exu_res 是否等于预期结果
  6. 检查写回rdwen 是否为 1,写回地址和数据是否正确

六、常见 Bug 模式详解

6.1 符号扩展错误

症状:涉及负数的运算结果不对

示例addi a0, zero, -1 应得 0xFFFFFFFF,却得 0x00000FFF

原因:I 型立即数提取时没有符号扩展

// 错误
o_imm = {20'b0, i_instr[31:20]};           // 零扩展!
// 正确
o_imm = {{20{i_instr[31]}}, i_instr[31:20]}; // 符号扩展

6.2 移位量截断错误

症状:移位结果不对(移了过多位)

原因:RISC-V 规定移位量只取 rs2/imm 的低 5 位

// 错误
o_alu_res = i_src1 << i_src2;        // 用完整的 32 位做移位!
// 正确
o_alu_res = i_src1 << i_src2[4:0];   // 只取低 5 位

6.3 算术右移 vs 逻辑右移

症状:负数右移后高位为 0 而非全 1

原因:Verilog 的 >> 总是逻辑右移

// 错误
o_alu_res = i_src1 >> i_src2[4:0];   // 逻辑右移,高位补 0
// 正确 (符号扩展后逻辑右移)
wire [63:0] tmp = {{32{i_src1[31]}}, i_src1} >> i_src2[5:0];
o_alu_res = tmp[31:0];

6.4 Store 误写回寄存器

症状:Store 指令执行后某个寄存器值被覆盖

原因:IDU 中 Store 指令的 rdwen 未设为 0

// S 型指令不写寄存器
`TYPE_S: begin
  o_rdwen = 1'b0;   // ← 关键! Store 不写 rd
  // ...
end

6.5 分支偏移地址错误

症状:分支跳转到奇怪的地址

原因:B 型立即数拼接顺序错误

// B 型立即数正确拼接:
// {sign, [7], [30:25], [11:8], 0}
o_imm = {{20{i_instr[31]}}, i_instr[7], i_instr[30:25], i_instr[11:8], 1'b0};
//       ─────符号扩展──── ─bit7─ ────bit30:25──── ────bit11:8──── ─末位0─

6.6 JAL/JALR 返回地址错误

症状:函数返回后跑飞

原因rd = PC + 4 的 PC 值取错(取了下一条PC而非当前PC)

// 正确: 保存的是当前指令的 PC + 4
case (`TYPE_J): exu_src_sel = EXU_SEL_PC4;  // result = current_pc + 4

七、调试心态与方法论

7.1 "Bug 总是确定性的"

硬件设计中的 bug 不像多线程程序那样不可复现。给定相同的输入,RTL 仿真必定产生相同的输出。所以:

  • 不要说"我重新编译一下看看会不会好"
  • bug 不会自己消失——除非你理解并修复了它

7.2 "二分法定位"

如果不确定 bug 在数据通路的哪一级:

  1. 先看 EXU 输出是否正确
  2. 正确 → bug 在 LSU/WBU
  3. 不正确 → 再看 EXU 输入是否正确
  4. 正确 → bug 在 EXU/ALU 本身
  5. 不正确 → 再看 IDU 输出
  6. 如此二分,快速收窄

7.3 "最小复现"

如果一个复杂程序出错:

  1. 用 DiffTest 找到出错的指令
  2. 写一个只包含该指令(及最小前置指令)的小程序
  3. 在小程序上调试——信号少、波形短、容易分析

7.4 "回归测试"

修复一个 bug 后:

make run DIFFTEST=1   # 跑全部 cpu-tests

确保修复没有引入新 bug(回归)。


八、调试工具速查

8.1 命令行速查

# 编译 + 运行 + DiffTest
make run DIFFTEST=1

# 编译 + 运行 + 波形 + 限周期
make run WAVE=1 MAXCYCLE=5000

# 编译 + 运行 + DiffTest + 波形
make run DIFFTEST=1 WAVE=1

# 编译 + 运行 + 内存追踪
make run MTRACE=1

# 打开波形文件
make wave

# 反汇编 ELF 文件
riscv64-linux-gnu-objdump -d build/xxx.elf | less

# 查看 ELF 段信息
riscv64-linux-gnu-readelf -S build/xxx.elf

8.2 GTKWave 快捷键

快捷键 功能
Ctrl++ 放大
Ctrl+- 缩小
Ctrl+F 搜索信号值
Home 跳转到起始
End 跳转到末尾
左/右箭头 在时间轴上移动

九、PA 讲义相关思考题

Q: 为什么说"机器永远是对的"?

回答:PA 讲义的核心哲学。如果程序运行结果不对:

  • 不是编译器的 bug(99.99% 不是)
  • 不是 Verilator 的 bug(99.99% 不是)
  • 不是电脑的问题
  • 是你的代码的问题

接受这一点,才能停止无谓的怀疑,专注于排查自己的实现。


Q: RTFM (Read The Fantastic Manual) 在调试中的作用?

回答:当一条指令行为与预期不符时,唯一权威的参考是 RISC-V ISA 手册。不要凭记忆实现指令——每条指令都应该对照手册中的精确描述:

  • 立即数的位域排列
  • 符号扩展的精确规则
  • 溢出行为(RISC-V 大多数运算忽略溢出)
  • 除零行为(RISC-V 规定除零结果为全 1,不触发异常)

Q: 什么时候应该放弃调试,重新设计?

回答:如果一个 bug:

  1. 修了一处,另一处又坏了(说明架构设计有根本问题)
  2. 无法隔离到单个模块(信号依赖关系太复杂)
  3. 涉及时序错误(组合逻辑环路、setup violation)

这时应该退一步思考是否设计本身需要重构,而不是打补丁。


十、学习建议

  1. 养成写完就跑 DiffTest 的习惯 — 不要攒一堆修改再测试
  2. 波形文件要小 — 用 MAXCYCLE 限制,否则 GTKWave 打开都很慢
  3. 建立信号 checklist — 每次看波形按固定顺序检查信号
  4. 记录 debug 过程 — 写下"现象→假设→验证→结论",避免反复踩同一个坑
  5. 学会用 $display — 在 RTL 中临时加调试打印,确认某个内部信号的值
// 临时调试代码 (调试完成后删除)
always @(posedge clock) begin
  if (rst_n_sync && ifu_valid)
    $display("PC=%h INSTR=%h EXU_OPT=%d", pc, instr, exu_opt);
end

参考资料:

  • PA 讲义中关于调试的建议
  • GTKWave User's Guide
  • RISC-V ISA Specification (用于确认指令行为)
  • 项目源码: npc/vsrc/, npc/csrc/difftest.cpp
posted @ 2026-06-16 10:45  mo686  阅读(17)  评论(0)    收藏  举报