corosim is a C++20 coroutine-based co-simulation engine for Verilator. It maps co_await to delta-cycle and timed-event scheduling, letting you write concurrent hardware testbenches without manual state machines or eval loops.
- Coroutine concurrency —
co_await posedge(),co_await delay(),co_await any() - Non-blocking signals —
sig.next(val)with delta-cycle scheduling - Trigger-based processes —
sim.always()for repeating triggers,sim.instance()for one-shot coroutines - Phase-aligned I/O —
sim.sample()/sim.drive()for BFM-style transactions - Software signals — cross-process sync without DUT binding
# macOS (with Homebrew)
brew install verilator
# Ubuntu/Debian
sudo apt-get install verilator g++ makegit clone https://github.com/dozecat/corosim.git
cd corosim/examples/async_fifo/sim/tb
make # Compile and run simulationgtkwave waveform.vcdcorosim/
├── src/ # Library source
│ ├── corosim.hpp # Umbrella header
│ ├── api/ # Public facades: Simulator<TOP>, Module, Dut
│ ├── core/ # Kernel, types, detail (engine core)
│ ├── signal/ # SignalVal, Signal<T>, wide, registry
│ ├── trigger/ # Trigger specs + posedge/negedge/change/delay/any
│ ├── coroutine/ # Task, Coroutine, WaitGroup, CoroutineManager
│ └── scheduler/ # Delta-cycle + timed event engine
└── examples/
├── async_fifo/ # Dual-clock async FIFO (basic)
└── axis_async_fifo/ # AXI4-Stream async FIFO (advanced, with BFM)
Most testbenches interact through Simulator<TOP>, which owns a Kernel and provides signal binding, coroutine registration, and clock generation:
#include "corosim.hpp"
using namespace corosim;
Vasync_fifo top;
Simulator sim(top); // wrap the Verilator top
auto& clk = sim.sig(top.clk); // bind a DUT signal
auto& rst = sim.sig<uint8_t>(); // software-only signal (no DUT)
sim.clock(clk, 10); // full period 10; toggles every 5, starts low
sim.run(500, [&](sim_time t) { tfp.dump(t); });For pointer-to-member binding, use Dut<TOP>:
Dut<Vasync_fifo> dut(top, sim.signals());
auto& clk = dut.sig(&Vasync_fifo::clk);
auto& rst = dut.sig(&Vasync_fifo::rst);auto& sig = sim.sig(top.sig_field); // bind a Verilator field
auto& flag = sim.sig<bool>(); // software-only (no binding)
sig.next(val); // non-blocking assignment (applied at delta end)
sig.read(); // read current value
auto v = sig; // implicit read
// Signal type is deduced from the Verilator field type:
auto& rst = sim.sig(top.rst); // 1-bit → CData (Signal<uint8_t>)
auto& data = sim.sig(top.data); // 8-bit → CData (Signal<uint8_t>)
auto& wide = sim.sig(top.wide); // 16-bit → SData (Signal<uint16_t>)For wide vectors (VlWide<N>), sim.sig(top.wide_vec) yields Signal<VlWide<N>> — see signal/wide.hpp.
| Trigger | Type constraint | sim.always() |
co_await |
|---|---|---|---|
posedge(sig) |
sizeof(T) <= 1 |
✓ | ✓ |
negedge(sig) |
sizeof(T) <= 1 |
✓ | ✓ |
change(sig) |
any Signal<T> |
✓ | ✓ |
delay(n) |
— | ✓ | ✓ |
posedge/negedge require 1-byte signal types; change works with any signal width.
// Auto-repeat on trigger
sim.always(posedge(clk), [&] { cnt.next(cnt.read() + 1); });
sim.always(delay(5), [&] { clk.next(!clk.read()); }); // first fire at t=5
// One-shot coroutine (lambda)
sim.instance([&]() -> Task {
co_await delay(30);
go.next(true);
});
// One-shot coroutine (standalone function)
sim.instance(reset, &rst, &wr_clk);Task is only the return type of a top-level process. It is not awaitable:
// Not supported
co_await child_task();Use sim.instance() for coroutine processes and keep sim.always() callbacks non-suspending:
sim.always(posedge(clk), [&] {
// must return void; do not use co_await here
});sim.clock(sig, period) takes the full period. The optional third argument selects the
initial level, and the default is low.
Coroutine functions support co_await for synchronization:
Task reset(Signal<uint8_t>* rst, Signal<uint8_t>* clk) {
rst->next(1);
for (int i = 0; i < 5; i++) co_await posedge(*clk);
rst->next(0);
}
co_await delay(30); // wait 30 time units
co_await posedge(clk); // wait for rising edge
// Multi-condition wait — returns winning trigger index
int w = co_await any(posedge(clk), delay(200));
// w == 0: posedge(clk) fired first
// w == 1: delay(200) fired firstFor phase-aligned I/O, sample runs before DUT eval and drive runs after:
sim.sample(posedge(clk), [&] { bfm.sample_inputs(); });
sim.drive(posedge(clk), [&] { bfm.drive_outputs(); });Edge visibility (DUT-driven signals). The kernel observes signal transitions after each eval, so
co_await posedge(dut_output)works for DUT-driven outputs.sampleruns before eval and can only see TB-written edges;driveruns after eval, sotriggered()on a DUT output is true there — a behavior change vs. earlier versions where DUT-driven signals never triggered.
COROSIM_CHECK(cond); // abort with source location if false
COROSIM_ASSERT(cond, "message"); // same, with a message
COROSIM_FAIL("message"); // unconditional abort
set_log_level(LogLevel::INFO); // TRACE/DEBUG/INFO/WARN/ERROR_/FATAL
COROSIM_INFO("sim started"); // level-gated log to stderrDual-clock asynchronous FIFO testbench demonstrating basic triggers, software signals, coroutine processes, and VCD dump.
cd examples/async_fifo/sim/tb
makeAXI4-Stream async FIFO testbench using BFM with sample/drive phase hooks. Frame-fifo mode only (aux FIFO stores per-frame side-channel signals; bad frames are detected and discarded).
cd examples/axis_async_fifo/sim/tb
make- C++20 (coroutines)
- Verilator 5.x
- Make
MIT License — see LICENSE.
corosim 是一个基于 C++20 协程的 Verilator 协同仿真引擎。它将 co_await 映射为 delta 周期和定时事件调度,用于编写并发硬件测试台。
- 协程并发 —
co_await posedge()、co_await delay()、co_await any() - 非阻塞信号 —
sig.next(val),delta 周期调度 - 触发进程 —
sim.always()重复触发,sim.instance()一次性协程 - 相位 I/O —
sim.sample()/sim.drive()用于 BFM 总线事务 - 软件信号 — 无需 DUT 绑定的跨进程同步
# macOS (使用 Homebrew)
brew install verilator
# Ubuntu/Debian
sudo apt-get install verilator g++ makegit clone https://github.com/dozecat/corosim.git
cd corosim/examples/async_fifo/sim/tb
make # 编译并运行仿真gtkwave waveform.vcdcorosim/
├── src/ # 库源码
│ ├── corosim.hpp # 总头文件
│ ├── api/ # 公共门面:Simulator<TOP>、Module、Dut
│ ├── core/ # 引擎核心:Kernel、types、detail
│ ├── signal/ # SignalVal、Signal<T>、wide、registry
│ ├── trigger/ # 触发规格 + posedge/negedge/change/delay/any 等待者
│ ├── coroutine/ # Task、Coroutine、WaitGroup、CoroutineManager
│ └── scheduler/ # delta 周期 + 定时事件调度
└── examples/
├── async_fifo/ # 双时钟异步 FIFO(基础)
└── axis_async_fifo/ # AXI4-Stream 异步 FIFO(高级,带 BFM)
大部分测试台通过 Simulator<TOP> 与引擎交互:
#include "corosim.hpp"
using namespace corosim;
Vasync_fifo top;
Simulator sim(top);
auto& clk = sim.sig(top.clk); // 绑定 DUT 信号
auto& rst = sim.sig<uint8_t>(); // 纯软件信号
sim.clock(clk, 10); // 完整周期 10;每 5 时间单位翻转,初始为低
sim.run(500, [&](sim_time t) { tfp.dump(t); });使用 Dut<TOP> 进行成员指针绑定:
Dut<Vasync_fifo> dut(top, sim.signals());
auto& clk = dut.sig(&Vasync_fifo::clk);
auto& rst = dut.sig(&Vasync_fifo::rst);auto& sig = sim.sig(top.sig_field); // 绑定 Verilator 字段
auto& flag = sim.sig<bool>(); // 纯软件信号(无绑定)
sig.next(val); // 非阻塞赋值(delta 结束时提交)
sig.read(); // 读当前值
auto v = sig; // 隐式读取
// 信号类型由 Verilator 字段类型推导:
auto& rst = sim.sig(top.rst); // 1-bit → CData (Signal<uint8_t>)
auto& data = sim.sig(top.data); // 8-bit → CData (Signal<uint8_t>)
auto& wide = sim.sig(top.wide); // 16-bit → SData (Signal<uint16_t>)宽向量(VlWide<N>)用 sim.sig(top.wide_vec) 得到 Signal<VlWide<N>>。
| 触发器 | 类型限制 | sim.always() |
co_await |
|---|---|---|---|
posedge(sig) |
sizeof(T) <= 1 |
✓ | ✓ |
negedge(sig) |
sizeof(T) <= 1 |
✓ | ✓ |
change(sig) |
任意 Signal<T> |
✓ | ✓ |
delay(n) |
— | ✓ | ✓ |
posedge/negedge 仅支持 1 字节信号;change 支持任意宽度。
// 触发重复执行
sim.always(posedge(clk), [&] { cnt.next(cnt.read() + 1); });
sim.always(delay(5), [&] { clk.next(!clk.read()); }); // t=5 首次执行
// 一次性协程 (lambda)
sim.instance([&]() -> Task {
co_await delay(30);
go.next(true);
});
// 一次性协程 (独立函数)
sim.instance(reset, &rst, &wr_clk);Task 只表示顶层进程的返回类型,不是可等待对象:
// 不支持
co_await child_task();协程进程统一使用 sim.instance(),sim.always() 回调必须是无挂起的 void 回调:
sim.always(posedge(clk), [&] {
// 必须返回 void,回调内不能使用 co_await
});sim.clock(sig, period) 的 period 表示完整周期;第三个可选参数控制初始电平,默认从低电平开始。
协程函数中使用 co_await 同步:
Task reset(Signal<uint8_t>* rst, Signal<uint8_t>* clk) {
rst->next(1);
for (int i = 0; i < 5; i++) co_await posedge(*clk);
rst->next(0);
}
co_await delay(30);
co_await posedge(clk);
// 多条件等待 — 返回先触发的索引
int w = co_await any(posedge(clk), delay(200));
// w == 0: posedge(clk) 先到
// w == 1: delay(200) 先到sample 在 DUT eval 前执行,drive 在 eval 后执行:
sim.sample(posedge(clk), [&] { bfm.sample_inputs(); });
sim.drive(posedge(clk), [&] { bfm.drive_outputs(); });边沿可见性(DUT 驱动信号)。 引擎在每次 eval 后统一观测信号跳变, 因此
co_await posedge(DUT输出)对 DUT 驱动输出同样生效。sample在 eval 前,只能看到 TB 写入产生的边沿;drive在 eval 后, 此时triggered(DUT输出)为 true——这是相对旧版的行为变化(旧版 DUT 驱动信号永不触发)。
COROSIM_CHECK(cond); // 条件不成立则带源位置中止
COROSIM_ASSERT(cond, "message"); // 同上,可带消息
COROSIM_FAIL("message"); // 无条件中止
set_log_level(LogLevel::INFO); // TRACE/DEBUG/INFO/WARN/ERROR_/FATAL
COROSIM_INFO("sim started"); // 按级别输出到 stderr双时钟异步 FIFO 测试,演示基本触发、软件信号、协程进程和 VCD 波形。
cd examples/async_fifo/sim/tb
makeAXI4-Stream 异步 FIFO 测试,使用 BFM + sample/drive 相位钩子。仅 frame-fifo 模式(aux FIFO 存储帧级 side-channel 信号,检测并丢弃坏帧)。
cd examples/axis_async_fifo/sim/tb
make- C++20(协程支持)
- Verilator 5.x
- Make
MIT License — 见 LICENSE。