Four-state values

Warning

End-to-end four-state simulation is not currently available with CPPTB’s Verilator backend. Verilator 5.050 and current upstream development expose an experimental --fourstate option, but do not yet preserve X/Z storage, net resolution, or DPI bval transport. CPPTB rejects unknown writes rather than silently converting them to zero.

CPPTB separates the four-state value model from simulator execution capability. LogicBits<W> can represent, compare, format, and inspect any width of 0, 1, X, and Z. Whether those values may be written to and propagated through RTL depends on the selected simulator backend.

What that split means on Verilator today:

Works now

Does not work yet

LogicBits<W> construction, comparison, formatting, and inspection — any mix of 0/1/X/Z

X/Z storage in RTL: the simulator holds two-state values

get_logic() on hierarchy objects

Net resolution of Z and conflicting drivers

deposit_logic() / force_logic() with known 0/1 values

deposit_logic() / force_logic() containing X or Z (rejected with a diagnostic, never coerced)

expect_eq failure output that preserves X/Z from the expected value

DPI bval transport; port and interface _logic APIs

The rest of this page covers the value model first, then the capability gate that decides the right column.

Constructing values

LogicBits<W> stores the SystemVerilog A and B planes used by DPI. The mapping for each bit is:

A

B

Logic state

0

0

0

1

0

1

1

1

X

0

1

Z

Use binary text for the clearest mixed-state stimulus:

using cpptb::Bits;
using cpptb::LogicBits;
using cpptb::LogicState;

const auto request = LogicBits<8>::from_string("10xz_0011");
const auto known = LogicBits<8>::from_uint(0xa5);

Underscores are ignored, while the number of binary digits must exactly match the width. Lowercase and uppercase x and z are accepted.

Known packed data can be promoted without copying through an intermediate integer:

Bits<137> payload;
payload.set_word(0, 0x1234'5678u);
const auto logic_payload = LogicBits<137>::from_bits(payload);

Code that already has explicit planes can use from_planes():

Bits<8> aval = Bits<8>::from_uint(0xa3);
Bits<8> bval = Bits<8>::from_uint(0x30);
const auto request = LogicBits<8>::from_planes(aval, bval);

from_dpi_words() and dpi_words<T>() provide the allocation-free conversion for generated transport code. Ordinary testbenches normally do not need those two functions.

Inspecting values

The value exposes both whole-value and per-bit queries:

const auto value = LogicBits<8>::from_string("10xz_0011");

test.expect("contains unknown", !value.is_known());
test.expect("contains X", value.contains_x());
test.expect("contains Z", value.contains_z());
test.expect_eq("bit 5", value.state(5), LogicState::X);

const Bits<8>& aval = value.aval();
const Bits<8>& bval = value.bval();

value_bits() and unknown_bits() are descriptive aliases for aval() and bval(). to_bits() succeeds only when every bit is known; converting a value that contains X or Z aborts with a focused LogicBits diagnostic.

expect_eq() formats LogicBits values as width-qualified binary, retaining X and Z in failure reports.

Hierarchical signal APIs

Elaborated four-state internal signals expose explicit logic operations:

const auto known = LogicBits<8>::from_uint(0xa5);

dut.core.control.deposit_logic(known);
test.expect_eq("deposited control", dut.core.control.get_logic(), known);

dut.core.control.force_logic(known);
test.expect_eq("forced control", dut.core.control.get_logic(), known);
dut.core.control.release();

These calls are immediate, like every hierarchy operation — nothing queues, unlike a port set() — and they do not advance simulation time. Add Delay, an edge trigger, or a scheduling phase only when the testbench needs RTL to react or settle.

The ordinary get(), deposit(), and force() methods are the explicit two-state path. On the current Verilator backend, get_logic() and logic writes remain useful with known 0/1 values. A deposit_logic() or force_logic() containing X or Z aborts before transport and reports the operation, full hierarchical path, and simulator limitation.

Top-level ports and interface members currently retain their normal get(), set(), drive(), and high_z() APIs. CPPTB will add get_logic(), set_logic(), and drive_logic() there only after a backend passes the same end-to-end capability and conformance requirements.

Wide ports must be declared two-state

Code generation rejects any port wider than 32 bits whose declared type is four-state, unless it is an inout:

cpptb: port 'scr_key_i' is 128 bits wide and four-state; wide transport
currently requires a two-state bit port

The rule applies to inputs and outputs alike and to interface members as well as top-level ports. A wide port declared bit [127:0] is accepted, and an inout net is accepted at any width. Internal signals are not affected: a 128-bit four-state variable below the top is readable and depositable through the hierarchy API described above, so this is a limit of the packed port transport rather than of the value model. Carrying the B plane for wide ports is the fix.

Until then a design whose wide ports are logic needs them redeclared as bit where the testbench meets them. Ibex’s icache testbench ported to cpptb (experiments/open_core_ports/ports/ibex_icache_cpptb) does exactly that for the 128-bit scrambling key and the 64-bit nonce on its wrapper module, which carry no meaning beyond being random bits.

Experimental capability gate

The future Verilator path is guarded by an explicit project option:

cpptb build --experimental-four-state

or:

[build]
experimental_four_state = true

This is a capability request, not a promise that the installed Verilator supports four-state execution. CPPTB builds and runs a cached semantic probe before compiling the DUT. The probe checks all of the following:

  1. A SystemVerilog variable retains literal X and Z values.

  2. An undriven net resolves to Z and conflicting drivers resolve to X.

  3. DPI transport from SystemVerilog to C++ retains svLogicVecVal::bval.

  4. A C++ to SystemVerilog to C++ DPI round trip retains both planes.

The current Verilator release fails those checks, so the build exits with a summary of the unavailable capabilities. A future passing probe still stops with an integration-pending diagnostic until every conformance item below has passed and the framework capability is deliberately enabled. Do not put --fourstate in build.verilator_args; CPPTB rejects that bypass because Verilator can accept the flag while still coercing X/Z values. --rebuild refreshes the cached probe after changing or upgrading the simulator.

Normal two-state builds never run this probe and retain their existing transport and performance path.

Enablement criteria

Verilator support can be enabled only after all four semantic checks pass and the CPPTB conformance suite verifies:

  • scalar, wide, array, interface-member, and hierarchy reads and writes;

  • X/Z propagation through representative combinational and sequential RTL;

  • inout high-impedance and contention resolution;

  • force, release, deposit, and readback behavior; and

  • exact C++ and pure-SystemVerilog peers with the normal performance guard.

Until then, use known values with Verilator and run X/Z-sensitive tests on a standards-compliant four-state simulator through the portable backend work. The hierarchy guide covers generated access, while interfaces and inouts covers direction and drive intent.

The upstream status is documented in Verilator’s --fourstate option, its unsupported-option diagnostic, and the DPI conversion helpers that currently discard or clear bval.