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

5305

积分

0

好友

682

主题
发表于 1 小时前 | 查看: 3| 回复: 0

背景

在 Verilator 的常规集成方式里,C++ 侧驱动 DUT(Design Under Test)通常是这样写的:

top.eval();
top.clk = 1; top.eval();
top.clk = 0; top.eval();
// 手动判定 "task 到底跑完没有"

一旦 SV 侧的 task 里带上 while 循环和 @(posedge clk),情况立刻变得复杂:C++ 侧必须自己跟踪状态、手动推进时钟、轮询标志位,而且 task 一多,代码会以 O(n) 的速度膨胀,维护成本急剧上升。

本文介绍的方案想解决的核心问题是:让上层使用者完全不需要懂 Verilator。对他而言,调用一个仿真 task 就像调用一个普通函数:传参、等待、拿返回值。阻塞的接口、简单的返回值、屏蔽所有仿真细节,这是本设计的第一原则。


设计原则:让使用者“看不见” Verilator

在任何协同仿真系统里,都有两类人:

角色 关注点 需要懂 Verilator 吗?
仿真框架开发者 Vtop、VerilatedContext、--timing、协程调度 必须懂
仿真接口使用者 调什么函数、拿什么结果 完全不需要懂

本方案把这两类人的职责硬性切分:

  • 框架开发者负责把 Vtop 藏在 Worker 线程里,处理所有时序细节;
  • 接口使用者只需要写 int r = engine.task1().get();,就像调一个普通函数。

Verilator 仿真框架分层架构:使用者通过阻塞接口获取结果

为什么要做成阻塞? 因为异步语义对不懂 Verilator 的使用者来说毫无意义。他们不关心 co_await、不关心时钟周期、不关心 VlCoroutine 是怎么调度的——他们只想要 result。阻塞接口是最简单、最不容易出错的选择:调用 -> 返回。没有回调、没有状态机、没有额外的心智负担。


整体架构

主线程与 Worker 线程任务提交与协程执行架构图

主线程只做三件事:提交请求、拿到 future、get() 阻塞。它完全不接触 Vtop,也完全不关心仿真时间是怎么推进的。

Worker 线程独占 Vtop 和 VerilatedContext,所有时钟推进、协程调度都在这里发生。这是整个设计的安全边界:Verilator 的 --timing 模式大量依赖线程本地状态,把 Vtop 的构造、eval、final 全部锁在一个线程内,从根上避免了跨线程访问未定义行为。


关键角色

1. SV 侧:带 while 和 @ 的 task

task task1();
    i1 = 0;
    while (i1 < 5) begin
        @(posedge clk);
        i1++;
        count1++;
    end
    result1 = count1;
endtask

task 本身没有返回值——这是 SV 语言层面的限制。我们用模块内的变量 result1 承载结果,C++ 侧完成后读取它。

2. .vlt 控制文件:暴露 task 给 C++

`verilator_config
public -module "top" -task "task1"
public -module "top" -task "task2"

Verilator 会把它们编译成 Vtop_top 的成员方法,返回 VlCoroutine:

class Vtop_top final {
    // ...
    VlCoroutine task1();
    VlCoroutine task2();
};

注意:task 挂在 Vtop_top 上,而不是 Vtop 上。访问路径是 top->top->task1()——第一个 top 是 Vtop 实例,第二个 top 是 Vtop 里指向 Vtop_top 的指针成员。这一点非常容易被忽略,也是上手时最常见的坑。

3. 宏:一个 task 一行代码

#define DEFINE_SIM_TASK(NAME, RESULT)                                    \
    std::future<int> NAME() {                                            \
        return submit_sv_task<&Vtop_top::NAME,                           \
                              &Vtop_top::__PVT__##RESULT>();             \
    }

这里用的是 C++17 起支持的非类型模板参数:成员函数指针(&Vtop_top::task1)和成员变量指针(&Vtop_top::__PVT__result1)都可以作为编译期常量传入模板。

加新 task 时,C++ 侧只加一行:

DEFINE_SIM_TASK(task3, result3)

没有分支、没有重复代码、没有额外的状态机。


编译期到运行期的映射

SV task 到 C++ 协程的编译期映射链路

从 SV 源码到 C++ 接口,经过 .vlt 处理、Verilator 代码生成、宏展开四步。使用者只需要看到最后的 std::future<int>——前面的一切都是编译期魔法。


核心机制

submit_sv_task 的两段式

template <auto TaskFn, auto ResultMember>
std::future<int> submit_sv_task()
{
    auto promise = std::make_shared<std::promise<int>>();
    auto fut = promise->get_future();

    {
        std::lock_guard lk(queue_mtx_);
        tasks_.emplace([this, promise] {
            active_coros_.push_back(
                run_task_coro<TaskFn, ResultMember>(promise));
        });
    }
    queue_cv_.notify_one();
    return fut;
}

这段代码分两段执行:

  • 主线程侧:只创建一个 promise/future,把闭包入队,立刻返回 future<int>——不阻塞、不等结果。
  • Worker 侧:队列里的闭包被取出时,才在 Worker 线程里启动协程。

std::packaged_task 也可以实现类似功能,但它更适用于“任务本身直接算出结果”的场景。这里需要“启动协程 -> 挂起 -> 时钟推进 -> 恢复 -> 写结果”这样一条更长的链路,用 promise 手动控制更直观。

协程体只有 co_await

template <auto TaskFn, auto ResultMember>
VlCoroutine run_task_coro(std::shared_ptr<std::promise<int>> promise)
{
    active_count_.fetch_add(1);

    co_await (top_->top->*TaskFn)();

    int result = static_cast<int>(top_->top->*ResultMember);
    promise->set_value(result);

    active_count_.fetch_sub(1);
    co_return;
}

这个协程体只做三件事:启动 SV task 并 co_await、完成后读结果、set_value 通知主线程。它完全不推进时钟——时钟由 Worker 主循环统一处理:

while (!stop_.load()) {
    // 1) 消费提交队列
    drain_tasks();

    // 2) 有活跃协程就推进时钟
    if (active_count_.load() > 0) {
        top_->clk = 1; top_->eval(); context_->timeInc(1);
        top_->clk = 0; top_->eval(); context_->timeInc(1);
    }
}

时钟推进与协程调度解耦,这是整个设计最重要的简化点。协程只关心“我要等什么”,Worker 循环只关心“仿真时间怎么走”,两者互不干扰。


执行时序

异步协同仿真时序图:主线程阻塞与 Worker 时钟推进

这张时序图清楚地显示了主线程的“阻塞”与Worker 线程的“忙碌”之间的关系:主线程在 f1.get() 处完全停止,等待 Worker 线程一路把 task1 跑完;Worker 一旦 set_value,主线程立刻被唤醒继续。对使用者而言,这就是一个普通的同步调用——只是耗时略长。


数据流

异步任务执行与协程调度流程图

数据从主线程 -> 队列 -> Worker -> Vtop -> promise -> future -> 主线程走了一整圈。唯一跨线程的通道是 promise/future——这是 C++ 标准库提供的、最安全的跨线程通信原语之一,不需要手写锁、条件变量或原子标记。


为什么这么设计

1. 主线程接口看起来完全同步

int r1 = engine.task1().get();   // 就是阻塞调用

调用者不需要理解协程、也不需要感知线程,接口语义简单到极致。他写的代码和调用 std::sqrt() 没有本质区别——输入、等待、输出。

2. 使用者完全不需要懂 Verilator

这一点再强调一次:这是本设计的核心目标。

  • 他不知道 --timing 是什么;
  • 他不知道 VlCoroutine 是什么;
  • 他不知道 @(posedge clk) 对应 C++ 里的什么;
  • 他甚至不需要知道仿真跑在另一个线程。

他只需要知道:“我调 task1(),它返回一个 future,我 get() 就拿到 int。”

3. 仿真状态绑定到 Worker 线程

Verilator 的 --timing 模式大量依赖线程本地状态(协程调度器、VlTriggerScheduler)。把 Vtop 和 VerilatedContext 的构造、eval、final全部放在 Worker 线程内,避免跨线程访问导致未定义行为。

Vtop 线程访问权限规则图

粗实线代表合法路径,细虚线代表违规路径。主线程唯一被允许的通信方式是右侧的 future。

4. task 数量与代码量解耦

task 数 传统写法 本方案
2 2 段时钟循环 + 状态机 DEFINE_SIM_TASK × 2
10 10 段重复代码 DEFINE_SIM_TASK × 10
新增 手抄一段 加一行宏

使用者新增 task 的成本几乎为零——不需要读一行 Verilator 文档。

5. 与 std::future 生态天然兼容

std::future<int> f = engine.task1();
// 可以传给任何接收 std::future<int> 的函数

std::future 是 C++ 标准库的一等公民。这意味着本框架天然可以和现有异步代码互操作:你可以把它塞进任何等待 future 的地方,也可以在未来把它换成 std::shared_future、std::promise、甚至 C++20 的 std::coroutine handle。


线程与状态归属

Worker 线程与主线程状态归属图

每个资源都有明确的归属:Vtop、VerilatedContext、active_coros_ 只属于 Worker 线程;主线程只持有 future。这种清晰的划分让代码的线程安全性一眼可辨。


构建与运行

verilate(sim
    SOURCES top.vlt top.sv
    TOP_MODULE top
    VERILATOR_ARGS --timing -Wall -Wno-fatal
)
target_sources(sim PRIVATE main.cpp)
target_link_libraries(sim PRIVATE Threads::Threads)
mkdir build && cd build
cmake ..
make -j
./sim

输出:

[main] main thread id = 140234567890176

===== Round 1 / 5 =====
[main] futures submitted, doing other work ...
[3] task1: i1=1 count1=1
...
[11] task1 done, result1=5
[13] task2: i2=1 count2=1
...
[17] task2 done, result2=3
[main] task1 returned: 5
[main] task2 returned: 3

使用者看到的输出里,只有 [main] task1 returned: 5 这一行是他关心的;其余的 [3] task1: i1=... 都是 Verilator 在仿真内部打印的调试信息。


小结

传统方式 本方案
C++ 手动驱动时钟 Worker 线程统一推进
轮询 task 完成标志 std::promise 一次性通知
每个 task 手写包装 DEFINE_SIM_TASK 宏
主线程参与仿真 主线程只阻塞取结果
使用者需懂 Verilator 使用者只需知道 future.get()
无返回值语义 std::future<int> 天然异步

这套结构把 Verilator 的时序语义、C++ 的异步抽象、使用者的心智模型三者分层:

  • 协程负责任务生命周期;
  • Worker 线程负责仿真时间;
  • future 负责跨线程结果传递;
  • 使用者只需要面对一个阻塞的、返回 int 的函数。

三者各司其职,新增 SV task 的成本降到一行宏,而使用者永远不需要跨过 Verilator 的学习曲线。这就是本方案最核心的价值。

如果你对这类 Verilator 协同仿真、C++ 异步编程的落地实践有兴趣,也欢迎到 云栈社区 一起交流讨论。




上一篇:团队两个下属:一个加班按时交付但技术一般,一个技术好却总因追求完美延期,绩效A该给谁?
下一篇:竞品调研必备:我常用的3个SEO流量分析浏览器插件
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-10-11 12:30 , Processed in 0.087990 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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