Verification components

The optional cpptb_vc layer: bus components, analysis plumbing, scoreboards, transaction recording, the sparse memory, and the register abstraction. Everything lives in namespace cpptb::vc (the cpptb_vc name is only the include directory), reached through the umbrella header:

#include "cpptb_vc/cpptb_vc.hpp"
using namespace cpptb::vc;
// CMake: target_link_libraries(my_tb PRIVATE cpptb::cpptb cpptb::vc)

Verification components and Memory and register models are the guides; this page maps the surface and pins down which calls take simulation time. Most component types are deliberately non-copyable (several also non-movable) — declare them where they live.

Bus masters and monitors

MemoryStatus

enum class MemoryStatus : uint8_t { Okay, SlaveError, DecodeError, Timeout };
enum class MemoryOperation : uint8_t { Read, Write };
enum class AccessPath : uint8_t { Frontdoor, Backdoor };

The shared vocabulary: every master returns MemoryReadResponse<Data> / MemoryWriteResponse carrying a status and wait-cycle count, and every observed access is a MemoryTransaction<Address, Data, ByteEnable>. A timeout comes back as MemoryStatus::Timeout — a value to check, never an automatic failure.

ApbBus

ApbMaster

ApbBus bus{dut.pclk, dut.psel, dut.penable, dut.pwrite, dut.paddr,
           dut.pwdata, dut.prdata, dut.pready, dut.pslverr};  // + pstrb for APB4

ApbMaster master{bus, ApbConfig{...}};        // non-copyable

co_await master.write(address, data);          // Task<MemoryWriteResponse>
co_await master.write(address, data, byte_enable);   // needs a PSTRB signal
co_await master.read(address);                 // Task<MemoryReadResponse<Data>>
master.idle();                                 // immediate

Class-template-argument deduction covers APB3 (nine signals) and APB4 (ten, with strobe). Concurrent callers serialize on an internal lock; a partial write on a strobe-less bus throws.

ApbMonitor

ApbProtocolChecker

ApbMonitor monitor{test, bus};
test.spawn(monitor.run_forever());             // or run(n_transactions)
auto connection = monitor.observed().connect(subscriber);

ApbProtocolChecker checker{test, bus};
test.spawn(checker.run_forever());             // violations() to read the count

Monitors are passive — they never drive — and publish TransactionObservation<MemoryTransaction<...>> through an owned analysis port.

ReadyValidDriver

ReadyValidMonitor

ReadyValidSink

co_await driver.send(value);        // Task<uint32_t>: cycles stalled
co_await sink.receive();            // Task<value_type>
test.spawn(monitor.run(observed_port, n));   // takes an external AnalysisPort&

The stream trio for ready/valid handshakes.

Analysis plumbing

AnalysisPort

template <typename T> class AnalysisPort {    // move-only
    [[nodiscard]] Connection connect(Subscriber& subscriber);
    void write(const T& value) const;          // synchronous, zero-time
};

A subscriber implements exactly one thing: void write(const T&) — no base class, no registration. write() dispatches to every live subscriber in connection order without suspending; a subscriber’s write must not wait. connect() returns a [[nodiscard]] RAII connection — dropping the return value silently unsubscribes. Subscribers are borrowed and must outlive their connections.

AnalysisBuffer

AnalysisBuffer<T> buffer{capacity, AnalysisOverflowPolicy::DropOldest};
port.connect(buffer);               // buffer is a subscriber
T item = co_await buffer.get();     // and a coroutine source

The bridge from the synchronous analysis domain into coroutines, with an explicit overflow policy (DropNewest, DropOldest, or Error, which throws into the publisher).

PutPort

GetPort

PutPort<T> input{queue};    co_await input.put(value);   input.put_nowait(value);
GetPort<T> output{queue};   co_await output.get();       output.get_nowait();

Non-owning single-consumer endpoints over a coro::Queue<T> (or any backend with the exact signatures).

Scoreboards

InOrderScoreboard

KeyedScoreboard

InOrderScoreboard<T> scoreboard{test, "label"};        // non-copyable, non-movable
predicted.connect(scoreboard.expected());
monitor.observed().connect(scoreboard.actual());       // unwraps Completed observations
scoreboard.finalize();                                 // reports unpaired counts

KeyedScoreboard<T, KeyOf> keyed{test, "label", key_of};  // out-of-order, FIFO per key

Comparison happens as soon as both sides have an entry, as a nonfatal expect_eq under the scoreboard’s label. The expected()/actual() inputs accept both bare transactions and monitor observations.

ReferenceModelAdapter

auto model = make_reference_model<Input>(function);
observed.connect(model);            // publishes function(input) on
model.predicted.connect(...);       // its own AnalysisPort<Output>

Transaction recording

TransactionRecorder

TransactionRecorder recorder;                       // non-copyable, non-movable
auto& stream = recorder.stream<MyTransaction>("apb0");   // stable reference
auto connection = recorder.connect(sink);           // [[nodiscard]]
stream.write(observation);                          // immediate

Type-erased recording of described transaction types (CPPTB_VC_DESCRIBE_TRANSACTION(Type, "name", fields...); MemoryTransaction is described out of the box). Sinks implement write(const TransactionRecordView&); InMemoryTransactionSink collects RecordedTransaction values, JsonLinesTransactionSink streams one JSON object per line and must be finalize()d. Every call is immediate.

TransactionMonitor

template <typename T> class TransactionMonitor {    // base for protocol monitors
    AnalysisPort<TransactionObservation<T>>& observed();
  protected:
    Task<void> sample(Clock clock);                  // one sample point per cycle
    void publish(SimTime begin_time, T value,
                 TransactionDisposition disposition = Completed);
};

What a custom protocol monitor derives from; ApbMonitor is the worked example.

Sparse memory

SparseMemory

SparseMemory memory;                                // every method immediate
memory.add_region({.name = "ram", .base = 0x8000'0000, .size = 0x10000});

auto read  = memory.read_word<uint32_t>(address);   // MemoryReadResponse
auto write = memory.write_word(address, value, byte_enable);
MemoryStatus status = memory.read_into(address, span);   // zero-alloc hot path

memory.load(address, bytes);   memory.fill(address, size, value);
memory.load_file(address, path);   memory.dump_file(address, size, path);
std::vector<uint8_t> bytes = memory.inspect(address, size);

The expected-memory model: region-configured (permissions, byte order, fill), sparse (untouched fill-value bytes allocate nothing), and entirely zero-time. Out-of-region access returns DecodeError, permission violations SlaveError; the backdoor calls (load/fill/inspect) throw on a range outside one region. A MemoryAccessCallback can observe or rewrite accesses.

MemoryPredictor

auto predictor = make_memory_predictor<Transaction>(test, memory);
monitor.observed().connect(predictor);   // checks reads against the model,
                                         // applies writes; mismatches() counts

Register abstraction

The generated register model (from SystemRDL, IP-XACT, or RgGen contracts) is constructed over any bus master and optionally a generated backdoor:

peripheral_regs::RegModel<decltype(master)> regs{test, master, base_address};
auto backdoor = peripheral_regs::make_backdoor<decltype(master)>(dut);

The complete surface — register, field, and memory handles, staging and mirroring, the standard sequences, passive prediction, access coverage, and the extension points — has its own page: Register models. The semantics live in Registers & memory.

What takes simulation time

co_await: master read/write, monitor and checker run*, stream send/receive, PutPort::put/GetPort::get/AnalysisBuffer::get, every register-handle frontdoor operation (read/write/update/ mirror and the model-wide update_all/mirror_all), and the three standard sequences.

Immediate, zero-time: AnalysisPort::write/connect, every scoreboard/predictor/coverage write, all *_nowait forms, all peek/poke, model state (stage/predict*/reset), all metadata accessors, the entire SparseMemory, and the entire transaction-recording API.

See also