C1: 工具和基础设施

一、阶段目标

C1 阶段的核心目标是搭建完善的验证和调试基础设施,使后续的处理器开发可以高效定位和修复 bug。完成后你将:

  1. 理解并实现差分测试 (DiffTest) 框架——NPC 与 NEMU 逐指令对比
  2. 掌握多层 Trace 系统 (ITRACE/MTRACE/FTRACE/ETRACE) 的设计和使用
  3. 理解 DPI-C 接口在验证中的关键作用
  4. 掌握波形调试 (VCD + GTKWave) 的方法论
  5. 理解性能计数器 (Performance Counter) 的设计

二、差分测试 (DiffTest) 框架

2.1 核心思想

              ┌─────────────────┐         ┌─────────────────┐
              │     NPC (DUT)   │         │   NEMU (REF)    │
              │  硬件RTL仿真     │         │  软件参考模型    │
              └────────┬────────┘         └────────┬────────┘
                       │                           │
                       │   同一份程序镜像           │
                       │                           │
    每执行1条指令后 ────┤                           ├──── 同步执行1条指令
                       │                           │
                       ▼                           ▼
              ┌─────────────────┐         ┌─────────────────┐
              │  DUT 寄存器状态  │  ═══?═══ │  REF 寄存器状态  │
              │  PC, x0-x31     │         │  PC, x0-x31     │
              │  CSR            │         │  CSR            │
              └─────────────────┘         └─────────────────┘
                       │                           │
                       └───────────┬───────────────┘
                                   │
                          ┌────────▼────────┐
                          │ 一致? → 继续    │
                          │ 不一致? → 报错  │
                          │   + 打印两侧状态│
                          │   + 停止仿真    │
                          └─────────────────┘

2.2 NEMU 作为 REF 的编译

NEMU 可以编译为共享库 (.so),作为参考模型:

cd nemu
make menuconfig
# 选择: Build target → Shared object (used as REF for differential testing)
make
# 输出: build/riscv32-nemu-interpreter-so

共享库导出以下 API:

void difftest_init();                                    // 初始化参考模型
void difftest_memcpy(uint32_t addr, void *buf, size_t n, bool direction);  // 内存同步
void difftest_regcpy(void *dut, bool direction);         // 寄存器同步
void difftest_exec(uint32_t n);                          // 执行 n 条指令
void difftest_raise_intr(uint32_t NO);                   // 触发中断

2.3 NPC 端 DiffTest 初始化 (difftest.cpp)

void difftest_init(char *ref_so_file, long img_size) {
  // 1. 动态加载 NEMU 共享库
  void *handle = dlopen(ref_so_file, RTLD_NOW);

  // 2. 获取 API 函数指针
  ref_difftest_memcpy = dlsym(handle, "difftest_memcpy");
  ref_difftest_regcpy = dlsym(handle, "difftest_regcpy");
  ref_difftest_exec   = dlsym(handle, "difftest_exec");
  ref_difftest_raise_intr = dlsym(handle, "difftest_raise_intr");

  // 3. 初始化参考模型
  void (*ref_difftest_init)() = dlsym(handle, "difftest_init");
  ref_difftest_init();

  // 4. 将程序镜像同步到参考模型
  ref_difftest_memcpy(INST_START, guest_to_host(INST_START), img_size, DIFFTEST_TO_REF);

  // 5. 同步初始寄存器状态
  regfile dut = pack_dut_regfile(dut_reg, INST_START, dut_csr);
  ref_difftest_regcpy(&dut, DIFFTEST_TO_REF);
}

2.4 每条指令后的对比 (difftest_check)

bool difftest_check() {
  // 如果上一条指令需要跳过检查 (如 MMIO 访问)
  if (is_skip_ref_r) {
    dut = pack_dut_regfile(dut_reg, dut_pc, dut_csr);
    ref_difftest_regcpy(&dut, DIFFTEST_TO_REF);  // 强制同步 DUT 状态到 REF
    is_skip_ref_r = false;
    return true;
  }

  // 获取 REF 寄存器状态
  regfile ref;
  ref_difftest_regcpy(&ref, DIFFTEST_TO_DUT);

  // 获取 DUT 寄存器状态 (通过 DPI-C 读取 RTL 寄存器)
  regfile dut = pack_dut_regfile(dut_reg, dut_pc, dut_csr);

  // 逐一对比
  bool ret = checkregs(&ref, &dut);
  if (!ret) print_regs(&ref, &dut);  // 不一致时打印双侧状态
  return ret;
}

2.5 寄存器对比逻辑 (reg.cpp)

bool checkregs(regfile *ref, regfile *dut) {
  // 检查 PC
  if (ref->pc != dut->pc) {
    printf("difftest error: next pc diff: ref=0x%x, dut=0x%x\n", ref->pc, dut->pc);
    return false;
  }
  // 检查 32 个通用寄存器
  for (int i = 0; i < 32; i++) {
    if (ref->x[i] != dut->x[i]) {
      printf("difftest error at pc=0x%x, reg %s diff: ref=0x%x, dut=0x%x\n",
             dut->pc, regs[i], ref->x[i], dut->x[i]);
      return false;
    }
  }
  // 检查 4 个 CSR
  for (int i = 0; i < 4; i++) {
    if (ref->csr[i] != dut->csr[i]) {
      printf("difftest error at pc=0x%x, csr %s diff: ref=0x%x, dut=0x%x\n",
             dut->pc, csrs[i], ref->csr[i], dut->csr[i]);
      return false;
    }
  }
  return true;
}

2.6 DiffTest Skip 机制

某些指令无法对比(如访问 MMIO 设备——REF 和 DUT 的设备状态可能不同):

// dpic.cpp — 当 NPC 访问 UART 时
if (waddr == 0xa0000000 && wen) {  // UART 地址
  is_skip_ref = true;  // 标记跳过
  putc(ch, stderr);
  return;
}

// difftest_step() — 处理 skip
void difftest_step() {
  if (is_skip_ref) {
    is_skip_ref_r = true;   // 下一次 check 时同步而非对比
    is_skip_ref = false;
    return;                 // 不让 REF 执行
  }
  ref_difftest_exec(1);     // 正常: REF 执行一条
}

Skip 的语义:当 DUT 执行一条不可对比的指令时:

  1. DUT 照常执行
  2. REF 跳过该指令不执行
  3. 下次 check 时将 DUT 的状态强制复制到 REF(保持两侧同步)

三、DPI-C 基础设施

3.1 从 Verilog 到 C++ 的信号传递

DPI-C 函数 方向 用途
set_reg_ptr(rf) RTL→C++ 暴露寄存器数组指针
set_csr_ptr(csr) RTL→C++ 暴露 CSR 数组指针
diff_read_pc(pc) RTL→C++ 传递当前 PC 值
diff_read_status(status) RTL→C++ 传递指令提交状态
check_rst(rst) RTL→C++ 传递复位状态
check_finish(instr) RTL→C++ 检测 ebreak 指令
pmem_read(addr, data, ren) RTL→C++ 内存读取回调
pmem_write(addr, data, mask, wen) RTL→C++ 内存写入回调

3.2 DPI-C 内存读写 (dpic.cpp)

extern "C" void pmem_read(uint32_t raddr, uint32_t *rdata, svBit ren) {
  raddr = raddr & ~0x3u;  // 4 字节对齐

  if (raddr == 0xa0002000 && ren) {        // TIMER 设备
    is_skip_ref = true;                     // DiffTest 跳过
    us = get_time();
    *rdata = (uint32_t)us;
  }
  else if (raddr == 0xa0002004 && ren) {   // TIMER 高 32 位
    is_skip_ref = true;
    *rdata = (uint32_t)(us >> 32);
  }
  else if (ren && raddr >= PMEM_START && raddr <= PMEM_END) {
    *rdata = _pmem_read(raddr, 4);          // 普通内存读取
  }
  else {
    *rdata = 0;                             // 避免 latch
  }
}

3.3 性能计数器 DPI-C

// RTL 中通过 DPI-C 报告各种事件
extern "C" void axi4_handshake(svBit valid, svBit ready, svBit last, int pfc_type) {
  if (valid && ready && last) {
    switch (pfc_type) {
      case 1: ifu_pfc_r++; break;       // IFU 完成一次读取
      case 4: lsu_pfc_w++; break;       // LSU 完成一次写入
      case 6: lsu_pfc_r++; break;       // LSU 完成一次读取
    }
  }
}

extern "C" void idu_instr_type(svBit valid, int opcode) {
  if (valid) {
    switch (opcode) {
      case 0x33: case 0x13: idu_cal_type++; break;   // 计算类指令
      case 0x23: case 0x03: idu_mem_type++; break;   // 访存类指令
      case 0x63: case 0x6f: idu_jump_type++; break;  // 跳转类指令
      case 0x73: idu_csr_type++; break;              // CSR/系统指令
    }
  }
}

四、Trace 追踪系统

4.1 ITRACE — 指令追踪

记录每条指令的 PC、机器码、反汇编文本:

// 输出格式示例
0x80000000: 00000297  auipc t0, 0
0x80000004: 01028293  addi  t0, t0, 16
0x80000008: 30029073  csrw  mtvec, t0

在 NEMU 中 (cpu-exec.c):

#ifdef CONFIG_ITRACE
  char *p = s->logbuf;
  p += snprintf(p, sizeof(s->logbuf), FMT_WORD ":", s->pc);
  // 打印机器码字节
  for (i = ilen - 1; i >= 0; i--)
    p += snprintf(p, 4, " %02x", inst[i]);
  // 反汇编
  disassemble(p, ..., s->pc, &s->isa.inst.val, ilen);
#endif

4.2 IRINGBUF — 指令环形缓冲

问题:程序崩溃时 ITRACE 可能已经输出了上百万行日志,根本无法查找。

解决方案:只保留最近 N 条指令,崩溃时一次性打印。

// 环形缓冲区数据结构
typedef struct {
  uint8_t *buffer;    // 缓冲区
  uint32_t size;      // 总大小
  uint32_t in;        // 写入位置
  uint32_t out;       // 读取位置
} RingBuffer;

// 每条指令执行后写入环形缓冲
if (RingBuffer_IsFull(rb)) {
  RingBuffer_Out(rb, &tmp, 4);    // 满了就丢弃最老的
}
RingBuffer_In(rb, &s->isa.inst.val, 4);  // 写入最新指令

// 崩溃时打印
void assert_fail_msg() {
  printf("[Last several instructions for debug.]\n");
  while (!RingBuffer_IsEmpty(rb)) {
    uint32_t dest;
    RingBuffer_Out(rb, &dest, 4);
    printf("instr: 0x%08x\n", dest);
  }
}

4.3 MTRACE — 内存访问追踪

// nemu/src/memory/paddr.c
#ifdef CONFIG_MTRACE
  if (addr >= CONFIG_MTRACE_START && addr <= CONFIG_MTRACE_START + CONFIG_MTRACE_SIZE) {
    Log("MTRACE: Read  Memory Address: 0x%x, len: %d, data: 0x%x", addr, len, ret);
  }
#endif

在 NPC 中通过 DPI-C 实现:

// 编译时加 -DMTRACE_ON
IFDEF(MTRACE_ON, printf("raddr = 0x%08x, rdata = 0x%08x\n", raddr, *rdata));

4.4 各 Trace 的适用场景

Trace 适用场景 性能影响
ITRACE 定位哪条指令出错 中 (日志写入)
IRINGBUF 崩溃后回溯 低 (内存中环形写入)
MTRACE 定位内存相关 bug (越界、错误地址) 高 (每次访存都打印)
FTRACE 定位函数调用关系 中 (需要符号表解析)
ETRACE 跟踪异常/中断触发 低 (异常较少)
DTRACE 跟踪设备 I/O 访问 低 (设备访问较少)

五、波形调试 (VCD + GTKWave)

5.1 波形生成

make run WAVE=1   # 编译时加 -DTRACE_ON,运行时打开 VCD 文件
// main.cpp
IFDEF(TRACE_ON,
  contextp->traceEverOn(true);
  top->trace(tfp, 0);
  tfp->open("build/sim.vcd");
);

// 每个仿真步骤
IFDEF(TRACE_ON, tfp->dump(contextp->time()));

5.2 波形查看

make wave   # 等效于 gtkwave build/sim.vcd &

5.3 波形调试方法论

定位 DiffTest 报错时刻

  1. DiffTest 报错打印了 PC 值(如 pc=0x80000100
  2. 在波形中找到 dut_pc == 0x80000100 的时刻
  3. 观察该时刻 IDU 输出的控制信号是否正确
  4. 检查 EXU/ALU 的输入输出
  5. 如果是访存指令,检查 LSU 的地址和数据

关键观察信号

  • clock, reset — 时钟和复位
  • pc — 当前 PC
  • instr — 当前指令
  • exu_opt — ALU 操作类型
  • exu_res — ALU 结果
  • rd, wbu_rdwen — 写回信号
  • ifu_valid, idu_ready — 流水线握手

六、错误定位完整流程

6.1 典型 Bug 定位流程图

程序执行异常 (HIT BAD TRAP / DiffTest 不一致)
           │
           ▼
┌──────────────────────┐
│ 1. 确定出错的指令 PC │
│    (DiffTest 报告)   │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│ 2. objdump 查看      │
│    该 PC 对应的指令   │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│ 3. 判断 Bug 类型     │
├──────────┬───────────┤
│          │           │
▼          ▼           ▼
译码错误  执行错误    访存错误
│          │           │
▼          ▼           ▼
检查IDU   检查ALU    检查LSU/
控制信号   输入输出   地址计算
│          │           │
▼          ▼           ▼
┌──────────────────────┐
│ 4. 打开波形确认      │
│    信号时序           │
└──────────────────────┘

6.2 常见 Bug 模式

现象 可能原因 检查方法
PC 不一致 分支条件判断错误 检查 BRU 的 zero/brch 信号
某寄存器值错 ALU 运算类型设错 检查 IDU 的 exu_opt 输出
访存数据错 Load 符号扩展/零扩展错 检查 LSU 的 lsu_opt 信号
随机崩溃 复位不充分/未初始化 增加复位周期
CSR 值不一致 ecall/mret 处理错 检查 IRU 模块

七、条件编译基础设施

7.1 NEMU 中的 IFDEF/MUXDEF

// 在 NEMU 中通过 Kconfig 控制
IFDEF(CONFIG_ITRACE, puts(s->logbuf));           // 有 ITRACE 则打印
IFDEF(CONFIG_DIFFTEST, difftest_step(...));       // 有 DIFFTEST 则对比
IFDEF(CONFIG_DEVICE, device_update());            // 有 DEVICE 则更新设备

7.2 NPC 中的条件编译宏

// npc/csrc/include/macro.h
#define IFDEF(macro, ...) MUXDEF(macro, __VA_ARGS__, )
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)

// 使用:
IFDEF(DIFFTEST_ON, difftest_step());
IFDEF(TRACE_ON, tfp->dump(contextp->time()));
IFDEF(MTRACE_ON, printf("mem access...\n"));

7.3 Makefile 控制

ifneq ($(DIFFTEST), )
CXXFLAGS += -DDIFFTEST_ON
NPC_ARGS += --diff $(NEMU_HOME)/build/riscv32-nemu-interpreter-so
endif

ifneq ($(WAVE), )
CXXFLAGS += -DTRACE_ON
endif

ifneq ($(MTRACE), )
CXXFLAGS += -DMTRACE_ON
endif

八、性能统计

8.1 仿真结束时的统计输出

void statistics() {
  printf("Total cycles: %ld\n", contextp->time() / 2);
  printf("Total insts: %ld\n", total_inst);
  printf("CPU IPC: %f\n", total_inst / (contextp->time() / 2.0));
  printf("CPU CPI: %f\n", (contextp->time() / 2.0) / total_inst);
  printf("IFU Read count: %ld\n", ifu_pfc_r);
  printf("LSU Read count: %ld\n", lsu_pfc_r);
  printf("LSU Write count: %ld\n", lsu_pfc_w);
  printf("ICACHE Hit Rate: %f\n", cache_hit_rate);
  printf("ICACHE AMAT: %f\n", cache_acc_time + (1-cache_hit_rate)*cache_miss_penalty);
}

8.2 各统计指标含义

指标 公式 含义
IPC total_inst / cycles 每周期执行指令数 (理想=1)
CPI cycles / total_inst 每条指令消耗周期数
Cache Hit Rate hits / accesses 缓存命中比例
AMAT hit_time + miss_rate × miss_penalty 平均内存访问时间

九、PA 讲义相关思考题

Q: DiffTest 为什么选择 NEMU 而不是 Spike 作为 NPC 的参考?

回答

  1. 自己实现的 NEMU 可以深度定制:添加 MMIO skip、CSR 同步等功能
  2. API 简单:只需导出 4 个函数
  3. ISA 子集一致:你的 NEMU 和 NPC 实现相同的指令子集,避免不支持指令的差异
  4. Spike 更标准但更严格:Spike 实现完整的特权规范,可能在细节上与简化实现不一致

实际项目中两者可以配合使用:先用 NEMU 做快速验证,后期用 Spike 做合规性检查。


Q: 为什么 MMIO 访问要 skip DiffTest?

回答:因为设备访问是不确定性的:

  • NPC 读定时器获得的是"NPC 仿真时的真实时间"
  • NEMU 读定时器获得的是"NEMU 执行时的真实时间"
  • 两者时间点不同,返回值必然不同

其他不确定性来源:键盘事件、随机数等。

解决方案:DUT 执行设备访问后,跳过 REF 的执行,将 DUT 的状态强制同步到 REF。


Q: dlopen/dlsym 是什么?为什么用它们?

回答

  • dlopen(path, flags) — 运行时加载共享库 (.so 文件)
  • dlsym(handle, symbol) — 从已加载的共享库中查找函数符号

为什么用:NPC 的 testbench (C++) 需要调用 NEMU 的函数,但编译时两者是独立项目。通过将 NEMU 编译为 .so 并在运行时动态加载,实现解耦。


Q: 波形文件 (VCD) 为什么不能一直开着?

回答:VCD 文件记录每个信号在每个时间步的值,数据量极大:

  • 一个 100 信号、仿真 100 万周期的设计 → VCD 可能数百 MB
  • 运行 CoreMark 等复杂程序可能产生 GB 级波形文件

最佳实践

  1. 先不开波形跑 DiffTest 找到出错的大致位置
  2. MAXCYCLE 限制仿真周期
  3. 只在出错前后的小范围开波形

Q: 如何验证你的 DiffTest 框架本身是正确的?

回答

  1. 先确保 NEMU 通过所有 cpu-tests(NEMU 自身是正确的)
  2. 让 NPC 运行一个已知正确的简单程序 (dummy),DiffTest 应该 PASS
  3. 故意引入 bug(如把 add 改成 sub),确认 DiffTest 能检测到
  4. 检查 skip 逻辑:MMIO 访问后状态应正确同步

十、学习建议

  1. DiffTest 是 NPC 开发的生命线 — 每增加一个功能就跑 DiffTest
  2. 先实现最简单的 DiffTest:只对比 PC 和通用寄存器,不对比 CSR
  3. 理解 Skip 机制:设备访问时必须 skip,否则 DiffTest 永远不一致
  4. 波形是最后手段:先用 DiffTest 定位到具体指令,再开波形看细节
  5. 性能计数器在优化阶段很有用:IPC < 0.5 说明有大量 stall

参考资料:

  • PA2.4 "基础设施(2)" 讲义
  • dlopen/dlsym man page
  • Verilator Tracing 文档
  • 项目源码: npc/csrc/difftest.cpp, npc/csrc/dpic.cpp, nemu/src/cpu/cpu-exec.c

十一、本阶段 Git 版本记录

Commit 说明 完成内容
323892b PA2.1 RTL DiffTest 框架首次引入 (NPC vs NEMU .so)
33c5971 PA2.2 w/o trace ITRACE/IRINGBUF 实现
68bd7d0 PA2.2 RTL w/o trace NPC 端 DPI-C 接口完善

DiffTest 和 Trace 系统贯穿整个项目开发,从 PA2.1 RTL 开始一直使用到最后。

查看对应版本代码

git show 323892b:npc/csrc/difftest.cpp   # DiffTest 初版
git show 33c5971:nemu/src/cpu/cpu-exec.c # ITRACE 实现
posted @ 2026-06-15 13:58  mo686  阅读(19)  评论(0)    收藏  举报