返回

AXI-Stream Testbench 设计经验:从踩坑到正确架构

在验证 AXI-Stream Skid Buffer (Register Slice) 模块时,遇到了一个典型的 Testbench 设计问题:**激励发送的数据与 DUT 输出的数据对不上**。经过反复调试,最终通过重构 Testbench 架构解决了问题。

背景

在验证 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 比较混在一起:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// ❌ 有问题的写法
task send_data(...);
    @(posedge i_clk);
    i_s_valid = 1;
    i_s_data  = ...;
    ref_queue.push_back(...);  // 在 task 里 push
    
    // 等待握手
    do @(posedge i_clk);
    while (!(i_s_valid && o_s_ready));
endtask

always @(posedge i_clk) begin
    #1;  // 延迟1ns
    if (o_m_valid && i_m_ready)
        exp = ref_queue.pop_front();  // 在 always 里 pop
end

问题根源:时序竞争

  1. task 和 always 的执行顺序不确定
    • SystemVerilog 中,同一时刻触发的多个进程执行顺序取决于仿真器调度
    • #1 延迟不够可靠
  2. push 和 pop 可能在同一时钟沿竞争
    • 如果 DUT 是组合透传(0延迟),输入和输出握手可能在同一拍发生
    • task 里的 push 和 always 里的 pop 可能同时执行
  3. 复位期间的意外行为
    • 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)

只负责驱动信号,不操作队列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
task send_burst(input int count, input bit random_gap);
    for (int i = 0; i < count; i++) begin
        @(posedge i_clk);
        i_s_valid <= 1;                    // 非阻塞赋值
        i_s_data  <= {$urandom, $urandom};
        i_s_keep  <= 8'hFF;
        i_s_last  <= (i == count - 1);
        
        // 等待握手成功
        @(posedge i_clk);
        while (!(i_s_valid && o_s_ready)) @(posedge i_clk);
        
        // 可选的随机间隙
        if (random_gap && ($urandom % 3 == 0)) begin
            i_s_valid <= 0;
            repeat($urandom_range(1, 3)) @(posedge i_clk);
        end
    end
    @(posedge i_clk);
    i_s_valid <= 0;
endtask

2. Input Monitor (Always 块)

检测输入握手,push 到队列:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
always @(posedge i_clk) begin
    if (!i_rst_p && i_s_valid && o_s_ready) begin
        pkt_t pkt;
        pkt.data = i_s_data;
        pkt.keep = i_s_keep;
        pkt.last = i_s_last;
        ref_queue.push_back(pkt);
        tx_cnt++;
        $display("[%t] TX[%0d]: data=%h", $time, tx_cnt, i_s_data);
    end
end

3. Output Checker (Always 块 + 延迟)

检测输出握手,pop 并比较:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
always @(posedge i_clk) begin
    #2;  // 关键:延迟2ns,确保 Monitor 的 push 已完成
    if (!i_rst_p && o_m_valid && i_m_ready) begin
        rx_cnt++;
        if (ref_queue.size() > 0) begin
            automatic pkt_t exp = ref_queue.pop_front();
            if (o_m_data !== exp.data) begin
                $error("[%t] DATA MISMATCH!", $time);
                error_cnt++;
            end else begin
                match_cnt++;
            end
        end else begin
            $error("[%t] UNEXPECTED DATA! Queue empty.", $time);
            error_cnt++;
        end
    end
end

关键设计点

1. Checker 加延迟 #2

1
2
3
4
always @(posedge i_clk) begin
    #2;  // 确保 Monitor 的 push 已完成
    ...
end
  • Monitor 和 Checker 都在 @(posedge i_clk) 触发
  • Monitor 立即执行 push(delta cycle 0)
  • Checker 延迟 2ns 后执行 pop
  • 保证 push 先于 pop

2. 使用非阻塞赋值 <=

1
2
i_s_valid <= 1;
i_s_data  <= ...;
  • 模拟 RTL 寄存器输出的行为
  • 信号在下一个 delta cycle 更新
  • 波形更清晰,更接近真实硬件

3. 复位门控

1
if (!i_rst_p && i_s_valid && o_s_ready) ...
  • 复位期间(i_rst_p=1)不操作队列
  • 防止复位期间的意外行为

4. 使用 automatic 关键字

1
automatic pkt_t exp = ref_queue.pop_front();
  • always 块内的局部变量默认是 static
  • automatic 确保每次触发都是独立的临时变量
  • 避免多次触发之间的变量污染

5. 结构化数据类型

1
2
3
4
5
6
7
typedef struct {
    logic [DATA_W-1:0] data;
    logic [KEEP_W-1:0] keep;
    logic              last;
} pkt_t;

pkt_t ref_queue[$];
  • 比拼接位向量更清晰
  • 字段访问更直观:exp.data vs exp[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 延迟更保险)。


总结

踩过的坑

  1. 在 task 里直接操作共享队列 → 时序竞争
  2. relied on #1 延迟 → 不够可靠
  3. 没有复位门控 → 复位期间意外行为
  4. while 循环条件检查时机错误 → 死锁

正确做法

  1. 职责分离:Driver 只驱动,Monitor 只 push,Checker 只 pop+比较
  2. 用 always 块做 Monitor/Checker:时序一致,便于调度
  3. Checker 加延迟:确保 push 先于 pop
  4. 复位门控:避免复位期间的意外行为
  5. 非阻塞赋值:模拟真实硬件时序

与 UVM 的关系

这种架构是 UVM 验证方法学的简化版:

本文架构UVM 等价物
Driver taskuvm_driver
Input Monitoruvm_monitor + analysis_port
ref_queueuvm_tlm_fifo
Output Checkeruvm_scoreboard

掌握这种思想后,迁移到 UVM 会更容易理解其设计理念。


参考资料

  • AXI-Stream 协议规范 (AMBA 4)
  • SystemVerilog LRM: 变量存储类型 (static vs automatic)
  • Verification Academy: Scoreboard Architecture

记录于 2026-01-06

🄯 2025 - 2026 DeerStar的博客· 0Days
共书写了50.5k字·共 14篇文章
本站总访问量 · 访客数

DeerStar的一些笔记
使用 Hugo 构建
主题 Stack ModIce Year 设计
🄯 Licensed Under CC BY-NC-SA 4.0