APB register file

This example uses the optional cpptb_vc package rather than defining a local bus helper. The registered test owns one active master and three passive consumers: a monitor, protocol checker, and functional coverage subscriber. The monitor also feeds an in-order transaction scoreboard.

The example registers a second memory_model_apb_test that replaces the hand-authored expected queue with SparseMemory and MemoryPredictor. It covers writable regions, a read-only ID image, and error translation without putting APB timing into the model.

Construct the bus

Generated signals are assembled into a typed ApbBus. No configuration file or DUT-specific component subclass is required:

#include "cpptb/cpptb.hpp"
#include "cpptb_vc/cpptb_vc.hpp"

using namespace cpptb::vc;

auto make_apb_bus(Dut dut) {
    return ApbBus{dut.clk,           dut.apb_select,    dut.apb_enable,
                  dut.apb_write,     dut.apb_address,   dut.apb_write_data,
                  dut.apb_read_data, dut.apb_ready,     dut.apb_error};
}

An APB4 design can pass PSTRB as a tenth signal. The same master and monitor types then retain the strobe in every generic memory transaction.

Write a reusable sequence

The sequence is templated on the protocol-neutral MemoryMappedMaster concept. It can be reused with a future AXI-Lite, Wishbone, or custom adapter:

template <MemoryMappedMaster BusMaster>
Task<void> register_sequence(
    BusMaster& apb, TestContext& test,
    AnalysisPort<typename BusMaster::transaction_type>& expected) {
    uint32_t state = 0x1020'3040u;

    for (uint32_t index = 0; index < kRegisterTransactions; ++index) {
        const uint32_t address = (index % 4u) * 4u;
        const uint32_t value = next_word(state);

        const auto write = co_await apb.write(address, value);
        test.expect_eq("APB write status", write.status,
                       MemoryStatus::Okay);

        const auto read = co_await apb.read(address);
        test.expect_eq("APB register readback", read.data, value);
        test.expect_eq("APB read status", read.status,
                       MemoryStatus::Okay);
    }
}

Every protocol operation remains an explicit co_await. A response retains its status and wait-cycle count; the user decides how to check it.

Compose the test

Task<void> component_apb_test(Dut dut, TestContext& test) {
    dut.clk.set_now(0);
    test.start_clock(dut.clk, 10_ns);
    co_await reset_dut(dut);

    const auto bus = make_apb_bus(dut);
    Master master{bus, ApbConfig{.sample_delay = 1_ps}};
    ApbMonitor monitor{test, bus, 1_ps};
    ApbProtocolChecker checker{test, bus, 1_ps};
    AnalysisPort<Transaction> expected;
    InOrderScoreboard<Transaction> scoreboard{test, "APB transaction"};

    auto expected_connection = expected.connect(scoreboard.expected());
    auto actual_connection = monitor.observed().connect(scoreboard.actual());
    auto coverage_connection =
        monitor.observed().connect(coverage_subscriber);
    test.spawn_detached(checker.run_forever());

    co_await Join{register_sequence(master, test, expected),
                  monitor.run(kRegisterTransactions * 2u)};

    scoreboard.finalize();
    test.expect_eq("APB protocol violations", checker.violations(),
                   uint64_t{0});
}

CPPTB_REGISTER_TEST(component_apb_test);

The root test explicitly starts the clock and resets the DUT. The master owns only APB transfer timing, while the passive components are started by authored test code.

C++ and pure SV

cpptb_vc (C++ DPI)
const auto write = co_await master.write(address, value);
test.expect_eq("APB write status", write.status, MemoryStatus::Okay);

const auto read = co_await master.read(address);
test.expect_eq("APB read data", read.data, value);
Pure SystemVerilog
@(negedge clk);
paddr = address;
pwdata = value;
pstrb = 4'hf;
pwrite = 1'b1;
psel = 1'b1;
penable = 1'b0;
@(posedge clk);
@(negedge clk);
penable = 1'b1;
do @(posedge clk); while (!pready);
expect_eq("APB write status", pslverr, 1'b0);
@(negedge clk);
psel = 1'b0;
penable = 1'b0;

Run the example implementations with:

make cpp-dpi-apb-regfile-run
make cpp-dpi-apb-regfile-sv-run

Run the 100,000-iteration exact component benchmark with:

make feature-test FEATURE=apb_component
make feature-benchmark FEATURE=apb_component

Run the equivalent sparse-memory predictor pair with:

make feature-test FEATURE=memory_model
make feature-benchmark FEATURE=memory_model

The authored register contract is in examples/apb_regfile/registers.rdl and can be exported through PeakRDL as described in the four-framework register workflow.

See Verification components for the package boundary and complete component contracts.