第三章:Testbench编写技巧——如何为HLS设计编写高效的C级Testbench

说实话,很多做HLS的朋友都栽在同一个坑里:C仿真跑得飞快,RTL仿真就崩了。为什么?说白了,就是C级Testbench没写好。我刚开始带项目时也吃过这个亏,后来慢慢摸索出一套方法,今天跟你聊聊。

核心观点:C级Testbench不是随便写几个main函数就完事了。它要能模拟真实的硬件行为,同时保持C语言的验证效率。

3.1 为什么C级Testbench这么重要?

你想想看,HLS设计从C到RTL,中间经过了调度、绑定、控制逻辑生成。如果C级验证不充分,RTL仿真时发现问题,回头改C代码再重新综合,这一来一回可能就浪费好几天。

我个人习惯,在写HLS代码之前,先把Testbench框架搭好。这样写代码的时候就能边写边测,心里有底。

3.2 高效Testbench的三大要素

我总结了一下,一个高效的C级Testbench需要具备这三个特点:

  1. 自检能力——不用人工盯着波形看,程序自己判断对错
  2. 覆盖率意识——不是跑一遍就完,要覆盖边界和异常
  3. 可复用性——换个参数、换个数据量,改一行代码就能跑

小技巧:我在项目中习惯把Testbench分成三个文件:stimulus.h(激励生成)、checker.h(结果比对)、main.cpp(主控流程)。这样后期维护起来特别方便。

3.3 实战:一个矩阵乘法的Testbench

咱们拿矩阵乘法来举例。这是HLS里最常见的场景,但很多人写的Testbench其实有问题。

先看一个反面教材:

// 这样写不行!
int main() {
    int A[4][4], B[4][4], C[4][4];
    // 手动填数据...
    matmul_hw(A, B, C);
    // 手动看结果...
    return 0;
}

这种Testbench有什么问题?第一,数据是写死的;第二,结果要人眼去对。跑一次两次还行,跑一百次你试试?

正确的写法应该是这样:

#include <stdlib.h>
#include <math.h>

// 黄金模型:用纯软件实现参考结果
void matmul_golden(int A[N][N], int B[N][N], int C_golden[N][N]) {
    for(int i = 0; i < N; i++)
        for(int j = 0; j < N; j++) {
            C_golden[i][j] = 0;
            for(int k = 0; k < N; k++)
                C_golden[i][j] += A[i][k] * B[k][j];
        }
}

// 随机激励生成
void gen_stimulus(int A[N][N], int B[N][N]) {
    for(int i = 0; i < N; i++)
        for(int j = 0; j < N; j++) {
            A[i][j] = rand() % 256;  // 8bit数据范围
            B[i][j] = rand() % 256;
        }
}

// 自动比对
int check_result(int C_hw[N][N], int C_golden[N][N]) {
    int errors = 0;
    for(int i = 0; i < N; i++)
        for(int j = 0; j < N; j++) {
            if(C_hw[i][j] != C_golden[i][j]) {
                errors++;
                printf("Error at [%d][%d]: hw=%d, golden=%d\n",
                       i, j, C_hw[i][j], C_golden[i][j]);
            }
        }
    return errors;
}

int main() {
    int A[N][N], B[N][N];
    int C_hw[N][N], C_golden[N][N];
    
    // 跑100次随机测试
    int total_errors = 0;
    for(int t = 0; t < 100; t++) {
        gen_stimulus(A, B);
        matmul_golden(A, B, C_golden);
        matmul_hw(A, B, C_hw);  // 调用HLS设计
        total_errors += check_result(C_hw, C_golden);
    }
    
    if(total_errors == 0)
        printf("All tests passed!\n");
    else
        printf("Found %d errors\n", total_errors);
    
    return total_errors;
}

注意:我曾经遇到过一个坑——C语言的int是32位,但硬件里可能只用了8位或16位。比对结果时一定要考虑数据位宽,否则明明硬件算对了,你这边却报错。建议在黄金模型里也模拟一下截位行为。

3.4 处理HLS特有的接口类型

HLS设计里经常用到ap_int、hls::stream这些类型。Testbench里怎么处理?

对于ap_int:

// 直接用,但注意位宽
ap_uint<8> data_in;
ap_int<16> data_out;

// 赋值和C语言一样
data_in = 0xAB;
// 但打印要用c_str()
printf("data_in = %s\n", data_in.to_string(10).c_str());

对于hls::stream:

// 模拟FIFO行为
hls::stream<int> input_stream("input");
hls::stream<int> output_stream("output");

// 写数据
for(int i = 0; i < 100; i++)
    input_stream.write(i);

// 调用DUT
my_hls_design(input_stream, output_stream);

// 读结果
while(!output_stream.empty()) {
    int val = output_stream.read();
    printf("Got: %d\n", val);
}

我的经验:用hls::stream时,一定要检查empty()。有一次我忘了检查,结果读到了垃圾数据,排查了半天才发现是stream空了还在读。嗯,这种低级错误犯过一次就不会再犯了。

3.5 性能验证:不只是功能正确

功能对了就完事了吗?不是的。HLS设计还要看能不能满足时序和吞吐量要求。

我一般会在Testbench里加性能统计:

#include <chrono>

int main() {
    // ... 初始化数据 ...
    
    auto start = std::chrono::high_resolution_clock::now();
    
    // 跑多次取平均
    const int ITER = 1000;
    for(int i = 0; i < ITER; i++) {
        matmul_hw(A, B, C_hw);
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    
    double avg_time = duration.count() / (double)ITER;
    printf("Average execution time: %.2f us\n", avg_time);
    printf("Throughput: %.2f M ops/s\n", 1000.0 / avg_time);
    
    return 0;
}

3.6 知识体系总览

下面这张图是我自己整理的C级Testbench编写要点,你可以照着这个框架来搭建自己的验证环境:

C级Testbench核心框架 激励生成 (Stimulus Generation) 随机数据 · 边界值 · 特殊序列 · 真实场景数据 DUT调用 (HLS Design Under Test) ap_int处理 · hls::stream读写 · 接口适配 结果比对 (Result Checking) 黄金模型 · 自动比对 · 误差容忍 · 错误报告 性能统计 执行时间 · 吞吐量 · 延迟 覆盖率分析 代码覆盖率 · 分支覆盖率 · 边界覆盖 自动化回归测试 (Automated Regression)

3.7 避坑指南

最后,分享几个我踩过的坑:

  • 数据位宽不匹配——C语言默认int是32位,但硬件可能只有8位。比对前一定要做截位处理。
  • 流接口的时序——hls::stream在C仿真里是立即读写的,但RTL里需要握手。建议在Testbench里加一个简单的延迟模型。
  • 浮点精度——如果设计里用了float,C仿真和RTL仿真的结果可能有微小差异。设置一个合理的误差容忍范围。
  • 随机种子固定——调试时固定随机种子,确保每次跑的数据一样。等验证通过了再放开随机性。

总结一下:好的C级Testbench,应该做到「一次编写,到处运行;自动比对,无需盯屏」。花两天时间把Testbench写好,后面能省下两周的调试时间。这笔账,你算算值不值?

可可精品资料库