找回密码
立即注册
搜索
发回帖 发新帖

4804

积分

0

好友

612

主题
发表于 2 小时前 | 查看: 7| 回复: 0

核心思路:用 DPI 快速把 RTL 变成函数,让 RTL 版本和纯 C++ 版本接口一致、结果对齐。

一、为什么是 DPI:绕过信号级的捷径

1.1 传统封装的痛点

想让软件工程师调用 RTL,最直接的做法是在 C++ 里手动推时钟:

Result add(uint32_t a, uint32_t b, uint32_t cin){
    dut_->a = a; dut_->b = b; dut_->cin = cin;
    dut_->start = 1;
    tick();                    // 时钟 1
    dut_->start = 0;
    while (!dut_->done) tick(); // 时钟 2..N
    return { dut_->sum, dut_->cout };
}
  • tick() 需要手工写,每个模块都要重复一遍
  • while (!dut_->done) 依赖 RTL 内部握手信号
  • 一旦 RTL 的周期数改变,封装层要跟着改
  • 调用者被绑死在 Verilator 的 API 上

1.2 DPI 的核心优势

DPI 让 Verilog 和 C++ 直接对话,不需要在 C++ 里“模拟”时钟:

`ifdef USE_DPI
    import "DPI-C" function longint add_c(input int a, input int b, input int cin);
    assign {cout, sum} = add_c(a, b, cin);   // ⚡ 一个函数调用搞定
`endif
extern "C"long long add_c(int a, int b, int cin){
    return (long long)((uint64_t)(uint32_t)a + (uint32_t)b + (cin & 1));
}
  • Verilog 侧只多了一行 import 和一行 assign
  • C++ 侧就是一个普通的纯函数,无时钟概念
  • 从“推 8 个时钟”变成“调 1 次函数”
  • 封装层不再和 RTL 的周期数耦合

1.3 两种封装的对比

DPI封装与传统C++封装流程对比

  • 传统封装把时序知识写进了 C++
  • DPI 封装把时序留在 Verilog,C++ 只做计算
  • 结果:C++ 侧代码量减少,可维护性提升

二、DPI 的三种用法

2.1 用法一:纯组合替换(最快)

import "DPI-C" function longint add_c(input int a, input int b, input int cin);
assign {cout, sum} = add_c(a, b, cin);
  • 适用于无内部状态的纯算法模块
  • 周期语义完全不变,组合逻辑本来就没周期
  • 每次 eval() 都会调用一次 DPI 函数
  • 外部逻辑看到的接口和原来一致

2.2 用法二:保留时序外壳(周期精确)

always @(posedge clk) begin
    if (busy) begin
        cnt <= cnt + 1;
        if (cnt == 30) {cout_r, sum_r} <= add_c(a, b, cin);  // 第 31 拍才锁存
        if (cnt == 31) busy <= 0;
    end
end
  • 适用于外部依赖 done 时序的模块
  • 端口、周期数、握手时序全部保持不变
  • DPI 只负责“算”,不负责“什么时候算”
  • 外部逻辑完全无感

2.3 用法三:完全切换实现(结果对齐)

`ifdef USE_DPI
    import "DPI-C" function longint add_c(...);
    assign {cout, sum} = add_c(a, b, cin);
    assign done = 1'b1;
`else
    // 原时序进位 FSM
`endif
  • 适用于外部只关心最终结果、不关心周期数
  • 周期语义变了,多周期变为单周期
  • 编译期通过 -DUSE_DPI 切换
  • 是“快速封装 + 结果对齐”的最简形态

2.4 三种用法决策树

DPI替换策略三分支决策流程图

  • 用法一最激进,适合算法模块
  • 用法二最保守,适合有握手协议的场景
  • 用法三居中,适合快速验证
  • 三者可以共存于同一份 RTL,用宏切换

三、双版本对齐:工程的核心

3.1 为什么要双版本

单有 RTL 版本,软件团队跑得慢;单有 C++ 版本,跑得快但和 RTL 脱节。两个都要,且接口一致。

统一接口与RTL和参考版两种实现的关系图

  • 软件团队面向统一接口编程
  • 日常回归用参考版,适合亿级向量
  • 每日构建用 RTL 版,适合百万级
  • 两者共享同一份测试代码

3.2 接口设计原则

原则 ✅ 正确 ❌ 错误
事务级 add(a, b, cin) start()、wait_done()、get_sum()
结构体返回 Result{sum, cout} void add(..., uint32_t* sum, uint32_t* cout)
稳定类型 uint32_t sc_uint<32> / WIDTH 宏
隐藏时序 内部 tick() 外部感知 clk / busy
错误处理 异常 / 错误码 返回 x 传播
  • 用事务级概念而非信号级概念
  • 返回结构体而非多个输出引用
  • 类型固定,不随 RTL 参数变化
  • 调用者感知不到时钟和握手

3.3 DPI 让接口天然一致

关键洞察:DPI 函数和 C++ 参考模型都是同一个 add() 签名。

// RTL 侧:DPI 函数
extern "C"long long add_c(int a, int b, int cin);

// 参考侧:纯 C++ 函数
inline long long add_native(int a, int b, int cin){
    return (long long)((uint64_t)(uint32_t)a + (uint32_t)b + (cin & 1));
}
  • 两个函数体几乎一样
  • 因为 DPI 的本质就是“让 C++ 代码在 RTL 里跑”
  • 这是对齐的根基:计算逻辑同源
  • 差异只在“被谁调用”

3.4 对齐验证流程

输入向量经RTL版与参考版处理后结果比对流程图

  • 输入向量同时喂给两个版本
  • 逐组比对 sum / cout
  • 不一致立即打印现场并 abort
  • 先小位宽穷举,再大位宽随机

四、完整工程示例

4.1 目录结构

project/
├── CMakeLists.txt
├── rtl/
│   └── adder.v              # RTL + DPI 条件编译
├── dpi/
│   └── adder_dpi.cpp        # DPI 的 C++ 实现
├── include/
│   └── adder.h              # 统一接口(软件团队看这个)
├── src/
│   ├── seq_adder_rtl.cpp    # RTL 版封装
│   └── seq_adder_ref.cpp    # 参考版实现
└── tb/
    └── testbench.cpp        # 对拍测试
  • rtl/ 由硬件团队维护
  • dpi/ 是 RTL 的计算核心
  • include/ 是给软件团队的唯一依赖
  • src/ 是两种实现
  • tb/ 是对拍验证

4.2 统一接口 include/adder.h

#pragma once
#include <cstdint>

struct AddResult {
    uint32_t sum;
    uint32_t cout;
};

class SeqAdder {
public:
    virtual ~SeqAdder() = default;
virtual AddResult add(uint32_t a, uint32_t b, uint32_t cin = 0)= 0;
};

// 工厂函数:返回不同实现
SeqAdder* make_rtl_adder();   // Verilator + DPI
SeqAdder* make_ref_adder();   // 纯 C++
  • 抽象基类定义接口,两种实现派生
  • 工厂函数让调用者选择实现
  • 头文件只依赖 <cstdint>,无 Verilator 细节
  • 软件团队看到的就是这个文件

4.3 RTL 侧:DPI 条件编译 rtl/adder.v

module adder (
    input         clk,
    input         rst,
    input         start,
    input  [31:0] a, b,
    input         cin,
    output [31:0] sum,
    output        cout,
    output        done
);

`ifdef USE_DPI
    import "DPI-C" function longint add_c(input int a, input int b, input int cin);
    wire [32:0] add_result = add_c(a, b, cin);
    assign sum  = add_result[31:0];
    assign cout = add_result[32];
    assign done = 1'b1;
`else
    reg [5:0] i;
    reg       carry;
    reg [31:0] sum_r;
    reg       busy;

    assign sum  = sum_r;
    assign cout = carry;
    assign done = ~busy;

    always @(posedge clk or posedge rst) begin
        if (rst) begin
            i <= 0; carry <= 0; sum_r <= 0; busy <= 0;
        end else if (start && !busy) begin
            i <= 0; carry <= cin; busy <= 1;
        end else if (busy) begin
            sum_r[i] <= a[i] ^ b[i] ^ carry;
            carry    <= (a[i]&b[i]) | (a[i]&carry) | (b[i]&carry);
            i <= i + 1;
            if (i == 31) busy <= 0;
        end
    end
`endif
endmodule
  • 端口列表在两种实现下完全一致
  • USE_DPI 时用 longint 打包 33 位结果
  • 非 DPI 时保留原来的时序进位 FSM
  • 用 -DUSE_DPI 编译期切换

4.4 DPI 实现 dpi/adder_dpi.cpp

#include <cstdint>
extern "C"long long add_c(int a, int b, int cin){
    uint64_t r = (uint64_t)(uint32_t)a + (uint32_t)b + (cin & 1);
    return (long long)r;
}
  • 无状态,纯函数
  • 用 64 位中间值容纳第 32 位进位
  • extern "C" 防止 C++ 名称修饰
  • 返回值低 32 位是 sum,第 32 位是 cout

4.5 RTL 版封装 src/seq_adder_rtl.cpp

#include "adder.h"
#include "Vadder.h"
#include "verilated.h"

class SeqAdderRTL : public SeqAdder {
    Vadder* dut_;
void tick(){ dut_->clk = 0; dut_->eval(); dut_->clk = 1; dut_->eval(); }
public:
    SeqAdderRTL() { dut_ = new Vadder; dut_->rst = 1; tick(); dut_->rst = 0; }
    ~SeqAdderRTL() override { dut_->final(); delete dut_; }

    AddResult add(uint32_t a, uint32_t b, uint32_t cin) override{
        dut_->a = a; dut_->b = b; dut_->cin = cin; dut_->start = 1;
        tick(); dut_->start = 0;
        int guard = 0;
        while (!dut_->done && guard++ < 64) tick();
        return { (uint32_t)dut_->sum, (uint32_t)dut_->cout };
    }
};

SeqAdder* make_rtl_adder(){ return new SeqAdderRTL; }
  • 构造时复位 DUT
  • 析构时清理 Verilator 资源
  • add() 内部完成握手,对外只返回结果
  • guard 防止 DUT 异常时死循环

4.6 参考版实现 src/seq_adder_ref.cpp

#include "adder.h"
#include <cstdint>

class SeqAdderRef : public SeqAdder {
public:
    AddResult add(uint32_t a, uint32_t b, uint32_t cin) override{
        uint64_t r = (uint64_t)a + (uint64_t)b + (cin & 1);
        return { (uint32_t)r, (uint32_t)(r >> 32) };
    }
};

SeqAdder* make_ref_adder(){ return new SeqAdderRef; }
  • 和 DPI 函数体几乎一样
  • 一行加法搞定
  • 无时钟、无握手、无 Verilator
  • 执行速度比 RTL 版快几个数量级

4.7 对拍测试 tb/testbench.cpp

#include "adder.h"
#include <cstdio>
#include <cstdint>

int main(){
    SeqAdder* rtl = make_rtl_adder();
    SeqAdder* ref = make_ref_adder();

    int errors = 0, total = 0;
    const uint32_t vals[] = {
        0, 1, 2, 0xFF, 0x100, 0x7FFFFFFF,
        0x80000000, 0xFFFFFFFF, 0x12345678, 0xDEADBEEF,
    };
    const int N = sizeof(vals) / sizeof(vals[0]);

    for (int ia = 0; ia < N; ia++)
        for (int ib = 0; ib < N; ib++)
            for (int cin = 0; cin < 2; cin++) {
                uint32_t a = vals[ia], b = vals[ib];
                auto r_rtl = rtl->add(a, b, cin);
                auto r_ref = ref->add(a, b, cin);
                total++;
                if (r_rtl.sum != r_ref.sum || r_rtl.cout != r_ref.cout) {
                    printf("MISMATCH: a=0x%08X b=0x%08X cin=%u | "
                           "rtl=(%u,%u) ref=(%u,%u)\n",
                           a, b, cin, r_rtl.sum, r_rtl.cout, r_ref.sum, r_ref.cout);
                    errors++;
                }
            }

    printf("\n%d tests, %d errors\n", total, errors);
    delete rtl; delete ref;
    return errors ? 1 : 0;
}
  • 同一份输入喂给两个实现
  • 覆盖边界值:0、1、最大正整数、全 1 等
  • 逐组比对,不一致打印现场
  • 退出码反映是否有错,方便 CI 集成

4.8 CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

if(POLICY CMP0144)
    cmake_policy(SET CMP0144 NEW)
endif()

project(dpi_adder LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()

find_package(verilator HINTS $ENV{VERILATOR_ROOT} ${VERILATOR_ROOT} REQUIRED)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)

include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)

# ---- 参考版(纯 C++,无 Verilator)----
add_library(seq_adder_ref STATIC src/seq_adder_ref.cpp)
target_link_libraries(seq_adder_ref PUBLIC Threads::Threads)

# ---- RTL 版(Verilator + DPI)----
add_library(seq_adder_rtl STATIC src/seq_adder_rtl.cpp dpi/adder_dpi.cpp)

verilate(seq_adder_rtl
    SOURCES       rtl/adder.v
    TOP_MODULE    adder
    VERILATOR_ARGS --no-timing -DUSE_DPI --Wno-fatal
)

target_link_libraries(seq_adder_rtl PUBLIC Threads::Threads)
target_compile_options(seq_adder_rtl PRIVATE -O3 -march=native)

# ---- 对拍测试 ----
add_executable(compare tb/testbench.cpp)
target_link_libraries(compare PRIVATE seq_adder_rtl seq_adder_ref)

enable_testing()
add_test(NAME compare COMMAND compare)
  • 两个库独立编译,互不污染
  • verilate() 是 Verilator 官方 CMake 函数
  • -DUSE_DPI 只在 verilator 进程生效
  • Threads::Threads 是 Verilator 运行时的依赖

4.9 运行

cmake -B build -S .
cmake --build build -j
./build/compare

输出:

100 tests, 0 errors
  • RTL 版本和 C++ 版本结果完全对齐
  • 退出码为 0 表示全部通过
  • 可以集成到 CI 里作为回归测试

五、DPI 的边界与坑

5.1 能用 / 不能用

DPI替换决策与适用场景流程图

  • 可综合逻辑才有对应的 C++ 实现
  • 纯组合 → 用法一
  • 有时序且外部依赖 → 用法二
  • 有时序但外部不依赖 → 用法三

5.2 常见坑

坑 现象 修法
用 static 保存状态 多实例互相踩、复位清不掉 状态放 Verilog reg,通过参数传
output 参数当返回值 编译报 "Missing argument" 显式传变量,或用 longint 打包
位宽不匹配 WIDTHEXPAND 警告 用 longint / 显式扩展
多实例共享 DPI 结果错乱 DPI 必须是无状态纯函数
# 延迟 Verilator 不支持 用 always @(posedge clk) + 计数器
改 clk 推进时钟 无效,DPI 拿不到控制权 时钟推进必须在 testbench
  • static 是最常见的坑,会导致多实例共享状态
  • output 参数必须显式传变量,不能靠返回值
  • Verilator 是周期仿真器,不支持 # 延迟
  • DPI 函数拿不到时钟控制权,clk 推进只能在 testbench

5.3 无状态原则

DPI 函数必须是纯函数:输出只依赖输入,无 static,无全局变量。

  • Verilator 为每个实例独立管理状态
  • DPI 不参与状态管理,只做“计算”
  • 状态放在 Verilog reg 里,通过参数进出 DPI
  • 这是 DPI 能和 Verilator 和谐共处的根本原因

六、软硬件对齐的完整图景

6.1 三方视角

硬件、验证与软件团队在DPI流程中的协作图

  • 硬件团队维护 RTL 和 DPI 函数
  • 验证团队跑双版本对拍
  • 软件团队只依赖统一头文件
  • 对拍通过后软件团队才能安全使用

6.2 三种交付形态

形态 内容 用途
📚 源码 rtl/ + dpi/ + include/ 硬件团队维护
📦 静态库 libseq_adder_rtl.a + libseq_adder_ref.a 链接到测试
🎁 头文件 adder.h 软件团队唯一依赖
  • 源码交给硬件团队
  • 静态库用于构建测试和集成
  • 头文件是软件团队唯一需要看的

6.3 核心价值

DPI 让“RTL 版本”和“C++ 版本”共享同一个接口,因为 DPI 的本质就是把 C++ 代码注入 RTL。

  • 接口一致:两边都是 Result add(a, b, cin)
  • 结果对齐:对拍验证,100% 一致
  • 速度可控:软件用参考版,适合亿级向量;验证用 RTL 版,适合百万级
  • 周期可选:三种 DPI 用法,按需选择

七、总结

7.1 三步走

写DPI、做封装、对拍验证三步骤流程图

  • 第一步:在 RTL 里 import DPI,C++ 侧实现计算
  • 第二步:用统一的 C++ 接口包住两种实现
  • 第三步:穷举 + 随机对拍,确保结果一致

7.2 关键洞察

洞察 说明
🎯 DPI 是捷径 不用手工推时钟,直接调函数
🔑 接口统一是核心 RTL 版和 C++ 版共享 add() 签名
✅ 对拍是保障 穷举 + 随机,逐一对齐
⚙️ 周期语义可选 三种用法:纯组合 / 保外壳 / 完全切换
🚫 DPI 必须无状态 状态放 Verilog,计算放 C++
  • DPI 省掉了手工写握手代码的工作量
  • 接口统一是软硬件协同的基础
  • 对拍是保证等价性的工程手段
  • 周期语义按需选择,不必一刀切
  • 无状态是 DPI 能正常工作的前提

7.3 最后一句

仿真器的速度差异是事实,但真正的价值不是“用 C++ 加速仿真”,而是“用 DPI 把 RTL 变成软件工程师可以调用的函数,同时和纯 C++ 版本严格对齐”。

  • 软件团队在流片前就跑通代码
  • 验证团队用同份测试跑两种实现
  • 硬件团队的 RTL 被更早、更多地验证
  • 这一切的支点,就是 DPI 那一行 import "DPI-C"



上一篇:Redis 在 Agent 运行时的四件事:短期记忆、会话缓存、限流与分布式锁
下一篇:18个CPU核怎么连起来?Snapdragon X2 Elite片上互连架构拆解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-4 23:19 , Processed in 0.104519 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表