2. C/RTL协同仿真基础:C仿真与RTL仿真的衔接,仿真环境的搭建

好,咱们直接进入正题。

C/RTL协同仿真,说白了就是让C模型和RTL模型一起跑。你写HLS代码时,C仿真跑得飞快,但RTL仿真慢得像蜗牛。怎么让它们俩配合起来?这就是本章要解决的问题。

2.1 为什么需要协同仿真?

我刚开始做HLS项目时,犯过一个低级错误。C仿真全通过,高高兴兴去综合。结果RTL仿真一跑,数据对不上。查了三天,发现是C模型里一个变量类型用错了。

你想想看,C仿真和RTL仿真各有各的脾气:

  • C仿真:速度快,调试方便,但精度有限
  • RTL仿真:精度高,能反映硬件行为,但慢得要命

协同仿真就是取长补短。C模型做快速验证,RTL模型做精确验证。两者配合,效率翻倍。

核心思想:C/RTL协同仿真不是替代,而是互补。C模型负责功能正确性,RTL模型负责时序和接口正确性。

2.2 仿真环境的整体架构

先看一张图,了解整体结构。

C/RTL协同仿真环境架构 C仿真环境 Testbench (C++) DUT (C模型) 参考模型 数据流 接口适配层 数据格式转换 时序对齐 握手协议 RTL仿真环境 Testbench (SystemVerilog) DUT (RTL) 波形输出 结果比较模块 C输出 vs RTL输出 → 自动比对 图例: C仿真 接口适配 RTL仿真 结果比较 数据流

这张图我画了好几次才满意。你看,核心就是三个模块:C仿真、接口适配、RTL仿真。接口适配层是关键,它负责把C的数据格式转成RTL能吃的格式。

2.3 仿真环境的搭建步骤

2.3.1 准备C测试平台

先写一个C语言的testbench。我个人习惯用C++,因为STL库用起来顺手。

// testbench.cpp
#include <iostream>
#include <vector>
#include "dut.h"  // HLS生成的C模型头文件

int main() {
    // 准备测试数据
    std::vector<int> input_data = {1, 2, 3, 4, 5};
    std::vector<int> expected_output = {2, 4, 6, 8, 10};
    
    // 调用DUT
    int result;
    for (int i = 0; i < input_data.size(); i++) {
        dut(input_data[i], &result);
        if (result != expected_output[i]) {
            std::cout << "Error at index " << i << std::endl;
            return 1;
        }
    }
    std::cout << "All tests passed!" << std::endl;
    return 0;
}

小技巧:C测试平台最好包含边界条件测试。比如输入最大值、最小值、0值。我在项目中吃过亏,边界条件没测全,RTL仿真时才发现问题。

2.3.2 生成RTL仿真文件

用Vivado HLS或Vitis HLS生成RTL代码。这一步很简单,点一下按钮就行。但要注意:

  • 确保选择了正确的目标器件
  • 检查接口协议是否匹配
  • 确认时钟和复位信号正确

2.3.3 编写RTL testbench

RTL testbench要模拟C testbench的行为。我一般用SystemVerilog写,因为支持面向对象,复用性好。

// testbench.sv
module testbench;
    reg clk, rst_n;
    reg [31:0] input_data;
    wire [31:0] output_data;
    reg valid_in;
    wire ready_in;
    wire valid_out;
    reg ready_out;
    
    // 实例化DUT
    dut_top u_dut (
        .clk(clk),
        .rst_n(rst_n),
        .input_data(input_data),
        .output_data(output_data),
        .valid_in(valid_in),
        .ready_in(ready_in),
        .valid_out(valid_out),
        .ready_out(ready_out)
    );
    
    // 时钟生成
    always #5 clk = ~clk;
    
    // 测试序列
    initial begin
        // 初始化
        clk = 0;
        rst_n = 0;
        #20 rst_n = 1;
        
        // 发送数据
        input_data = 32'd1;
        valid_in = 1;
        ready_out = 1;
        #10;
        
        // 检查结果
        if (output_data !== 32'd2) begin
            $display("Error: expected 2, got %d", output_data);
            $finish;
        end
        
        $display("All tests passed!");
        $finish;
    end
endmodule

注意:RTL testbench的时序必须和C模型一致。我曾经因为握手信号没对齐,浪费了两天时间调试。建议先用波形图对比一下。

2.4 协同仿真的关键点

2.4.1 数据格式对齐

C语言里int是32位,但RTL里可能是任意位宽。怎么对齐?

  • C端:用ap_int<N>或ap_uint<N>类型
  • RTL端:用logic [N-1:0]类型
  • 确保N一致,否则数据会截断

2.4.2 接口协议匹配

HLS生成的接口协议有很多种:

协议类型 C端表示 RTL端表示 适用场景
ap_ctrl_hs 函数调用 start/done/ready 控制密集型
ap_fifo 流式接口 FIFO读写 数据流处理
ap_memory 数组参数 RAM接口 大数据块处理
axis hls::stream AXI4-Stream 高速数据传输

选错协议,仿真肯定跑不通。我建议一开始就用axis协议,通用性强,后续移植也方便。

2.4.3 仿真脚本自动化

手动跑仿真太累了。写个Makefile或Tcl脚本,一键搞定。

# Makefile
C_SIM:
    g++ -o c_sim testbench.cpp dut.cpp
    ./c_sim

RTL_SIM:
    xvlog testbench.sv
    xelab testbench
    xsim testbench

CO_SIM:
    # 先跑C仿真,再跑RTL仿真,最后比较结果
    make C_SIM
    make RTL_SIM
    python compare_results.py

我的习惯:每次修改代码后,先跑C仿真,再跑RTL仿真。如果C仿真没过,绝不跑RTL。这样能节省大量时间。

2.5 常见问题与避坑指南

做协同仿真时,我踩过不少坑。分享几个典型的:

  • 数据位宽不匹配:C里用int,RTL里用32位,但HLS综合后可能变成64位。解决办法:在C代码里显式指定位宽。
  • 时序没对齐:C仿真里数据一拍就出结果,RTL里可能要等好几个时钟周期。解决办法:在RTL testbench里加等待逻辑。
  • 复位状态不一致:C模型没有复位概念,但RTL有。解决办法:在C testbench里模拟复位行为。

避坑指南:我曾经遇到一个项目,C仿真和RTL仿真结果总差一点点。查了三天,发现是C模型里用了float,但RTL里用了fixed-point。精度不一样,结果自然不同。从那以后,我坚持在C模型里就用定点数。

2.6 小结

C/RTL协同仿真,说白了就是让两个世界的人说同一种语言。C模型负责快,RTL模型负责准。接口适配层就是翻译官。

搭建环境时,记住三点:数据格式对齐、接口协议匹配、仿真脚本自动化。做到这三点,协同仿真基本不会出大问题。

嗯,今天就到这里。下一章我们讲约束文件怎么写,那个更有意思。

可可精品资料库