背景
在验证 AXI-Stream Skid Buffer (Register Slice) 模块时,遇到了一个典型的 Testbench 设计问题:激励发送的数据与 DUT 输出的数据对不上。经过反复调试,最终通过重构 Testbench 架构解决了问题。
本文总结这次调试过程中的经验教训。
问题现象
仿真日志显示数据错位:
# i_s_data: 8eb74a7c12f837e7 ← 发送的第1个数据
# i_s_data: 3030ba1c4682679c ← 发送的第2个数据
# ** Error: DATA MISMATCH!
# Exp: Data=3030ba1c4682679c ← 期望值是第2个
# Got: Data=8eb74a7c12f837e7 ← DUT输出的是第1个
症状:Checker 期望的值总是比 DUT 输出"超前一个",说明参考队列少了第一个元素。
原始设计的问题
原来的 Testbench 把激励驱动、push 队列、pop 比较混在一起:
| |
问题根源:时序竞争
- task 和 always 的执行顺序不确定
- SystemVerilog 中,同一时刻触发的多个进程执行顺序取决于仿真器调度
#1延迟不够可靠
- push 和 pop 可能在同一时钟沿竞争
- 如果 DUT 是组合透传(0延迟),输入和输出握手可能在同一拍发生
- task 里的 push 和 always 里的 pop 可能同时执行
- 复位期间的意外行为
- Checker 的 always 块在复位期间也会运行
- 如果 DUT 输出意外有效,可能提前 pop 空队列
正确的架构:职责分离
重构后采用三模块分离架构:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Driver │ ───► │ DUT │ ───► │ Checker │
│ (send_burst)│ │ (待测模块) │ │ (always) │
└─────────────┘ └─────────────┘ └─────────────┘
│ ↑
↓ │
┌─────────────┐ ┌───────────────┐
│ Monitor │ ───────────────────────► │ ref_queue[$] │
│ (always) │ push └───────────────┘
└─────────────┘
三个独立模块
| 模块 | 职责 | 实现方式 |
|---|---|---|
| Driver | 只负责驱动 valid/data 信号 | task |
| Input Monitor | 检测输入握手 → push 队列 | always @(posedge clk) |
| Output Checker | 检测输出握手 → pop 并比较 | always @(posedge clk) #2 |
正确的代码实现
1. Driver (Task)
只负责驱动信号,不操作队列:
| |
2. Input Monitor (Always 块)
检测输入握手,push 到队列:
| |
3. Output Checker (Always 块 + 延迟)
检测输出握手,pop 并比较:
| |
关键设计点
1. Checker 加延迟 #2
| |
- Monitor 和 Checker 都在
@(posedge i_clk)触发 - Monitor 立即执行 push(delta cycle 0)
- Checker 延迟 2ns 后执行 pop
- 保证 push 先于 pop
2. 使用非阻塞赋值 <=
| |
- 模拟 RTL 寄存器输出的行为
- 信号在下一个 delta cycle 更新
- 波形更清晰,更接近真实硬件
3. 复位门控
| |
- 复位期间(
i_rst_p=1)不操作队列 - 防止复位期间的意外行为
4. 使用 automatic 关键字
| |
- always 块内的局部变量默认是
static - 加
automatic确保每次触发都是独立的临时变量 - 避免多次触发之间的变量污染
5. 结构化数据类型
| |
- 比拼接位向量更清晰
- 字段访问更直观:
exp.datavsexp[63:0]
时序分析
以 Skid Buffer(1拍延迟)为例:
时钟沿 输入侧 输出侧 队列操作
────────────────────────────────────────────────────────────────────
T1后 valid=1, data=D0 - -
T2 输入握手成功 - Monitor push D0
T2+2ns - - -
T3 valid=1, data=D1 输出 D0 Monitor push D1
T3+2ns - Checker检测到D0 Checker pop D0, 比较OK
T4 输入握手成功 输出 D1 Monitor push D2
T4+2ns - Checker检测到D1 Checker pop D1, 比较OK
...
关键点:push 总是比 pop 早至少 1 个时钟周期(加上 2ns 延迟更保险)。
总结
踩过的坑
- 在 task 里直接操作共享队列 → 时序竞争
- relied on
#1延迟 → 不够可靠 - 没有复位门控 → 复位期间意外行为
- while 循环条件检查时机错误 → 死锁
正确做法
- 职责分离:Driver 只驱动,Monitor 只 push,Checker 只 pop+比较
- 用 always 块做 Monitor/Checker:时序一致,便于调度
- Checker 加延迟:确保 push 先于 pop
- 复位门控:避免复位期间的意外行为
- 非阻塞赋值:模拟真实硬件时序
与 UVM 的关系
这种架构是 UVM 验证方法学的简化版:
| 本文架构 | UVM 等价物 |
|---|---|
| Driver task | uvm_driver |
| Input Monitor | uvm_monitor + analysis_port |
| ref_queue | uvm_tlm_fifo |
| Output Checker | uvm_scoreboard |
掌握这种思想后,迁移到 UVM 会更容易理解其设计理念。
参考资料
- AXI-Stream 协议规范 (AMBA 4)
- SystemVerilog LRM: 变量存储类型 (static vs automatic)
- Verification Academy: Scoreboard Architecture
记录于 2026-01-06