| name | ctu-can-fd-tb-architecture |
| description | Architecture map of the CTU CAN FD VHDL testbench in test/main_tb — TB hierarchy, the 10 VIP agents and their communication protocol, feature-test conventions, reference-test flow, and functional coverage mechanism. Load this before writing/modifying a feature test, touching an agent under test/main_tb/agents, or answering questions about how the TB drives/checks the DUT. Does not cover test/main_tb/iso-16845-compliance-tests. Triggers on: feature test, ftest, feature_test_agent, can_agent, memory_bus_agent, reset_agent, clock_agent, timestamp_agent, interrupt_agent, test_probe_agent, test_controller_agent, reference_test_agent, functional_coverage_agent, tb_top, ctu_can_fd_vip, com_channel, check_m. |
CTU CAN FD Testbench Architecture
Scope: /WORK/ctu_can_fd/test/main_tb/ only, excluding iso-16845-compliance-tests/.
~216 VHDL files. This is the VUnit/NVC-based verification environment for the CTU CAN
FD RTL (see ctu-can-fd-rtl-architecture skill for the DUT itself).
Top-level hierarchy
Three interchangeable top entities, all declaring the same tb_top_ctu_can_fd
interface, each instantiating the DUT and ctu_can_fd_vip.vhd side by side (VIP does
NOT wrap the DUT — they're siblings connected only via top-level signals: clk_sys,
rst_n, memory bus, can_tx/can_rx, int, timestamp, test_probe, scan_mode):
tb_top_simple_ctu_can_fd.vhd — no VUnit dependency. A plain process pulses
test_start, waits for test_done, calls std.env.finish.
tb_top_vunit_ctu_can_fd.vhd — pulls in vunit_lib; uses the standard
runner_cfg generic, test_runner_setup/cleanup, test_runner_watchdog. VUnit is
deliberately confined to this file only, so the VIP itself stays VUnit-independent
("keep the VIP usable when integrating RTL+VIP into other TBs").
tb_top_gates_ctu_can_fd.vhd — VUnit-based like above, but the DUT is a
separately-compiled post-synthesis gate-level netlist (fixed configuration, no
generics, test_probe exposed as individual bit-blasted signals instead of a record).
The one place VUnit-vs-not leaks into shared code is logging: pkg/tb_report_pkg.vhd
(plain report) and pkg/tb_report_pkg_vunit.vhd (wraps VUnit's info/warning/error)
declare the same package name/interface — only one is ever compiled into a given
build, selected by which is included in that build's file list.
contexts/*.vhd (VHDL-2008 context declarations, used because free Quartus doesn't
support context clauses for synthesis so these are TB-only sugar): ieee_context
(std_logic_1164/numeric_std/math_real/textio), rtl_context (DUT-facing RTL packages),
tb_agents_context (one use per agent package), tb_common_context (all shared
pkg/*.vhd). All four are opened identically at the top of every tb_top and
ctu_can_fd_vip.vhd.
Shared pkg/*.vhd (test/main_tb/pkg/)
tb_report_pkg[.vhd|_vunit.vhd] — logging/checking facade: debug_m/info_m/ warning_m/error_m/check_m/check_false_m. Any failed check or error_m funnels
through a shared protected-type singleton ctu_vip_test_result.set_result(false)
(declared in tb_shared_vars_pkg) — this is the ONE place pass/fail state lives.
tb_communication_pkg.vhd — the inter-agent RPC protocol (see next section).
tb_prot_types_pkg.vhd — the protected types backing shared mutable state:
t_com_channel_data (message payload mailbox), t_ctu_test_result (pass/fail latch),
t_prot_boolean, t_prot_force_values (DUT-internal-signal force/release via
external names).
tb_random_pkg.vhd (largest, ~1580 lines) — pseudo-random stimulus library:
rand_int_{s,v}, rand_logic_{s,v}, rand_logic_vect_{s,v} (plain/bit-toggle-aware/
constrained variants), Gaussian/exponential distributions. _v suffix = returns a
variable, _s suffix = returns/drives a signal.
tb_pli_conversion_pkg.vhd — string/time ↔ std_logic_vector packing helpers for
the PLI boundary with the external ISO-16845 compliance C library.
tb_reg_map_defs_pkg.vhd + can_fd_tb_register_map.vhd (autogenerated,
mirrors the RTL register map) — used by feature tests for generic register
consistency/reset-value checks instead of hardcoding expected values per test.
Inter-agent communication protocol
Every agent is a passive, always-running VHDL process, driven by a custom
message-passing mechanism (tb_communication_pkg.vhd) that deliberately imitates
VUnit's com library without depending on it:
- A single-bit rendezvous signal (
t_com_channel, '1'/'Z') plus one shared global
protected-variable "mailbox" (com_channel_data, one scratch slot per primitive type:
logic/vector/time/int/bool/string) transport both the command and its arguments.
- Caller side:
set_param(...) for each argument in call order, then
send(channel, dest_agent_id, msg_code) — blocks until the agent replies.
- Agent side:
receive_start(channel, dest) (polls for messages addressed to its own
numeric ID — many agents share one channel), reads params back with get_param in
the same order the caller set them (no field tagging — protocol correctness depends
on call order matching read order), case cmd is ... end case, then
receive_finish(channel, reply_code).
- Agent IDs:
C_RESET_AGENT_ID=0, C_CLOCK_AGENT_ID=1, C_MEM_BUS_AGENT_ID=2, C_CAN_AGENT_ID=3, C_FEATURE_TEST_AGENT_ID=4, C_INTERRUPT_AGENT_ID=5, C_TIMESTAMP_AGENT_ID=6, C_TEST_PROBE_AGENT_ID=7 (C_NUM_AGENTS=10).
- Most agents additionally run a second, purely signal-driven process (FIFO drainer,
clock generator, bus-cycle executor, monitor FSM) that reacts to state set by the
command-handling process — decoupling "configuration/commands" (blocking rendezvous)
from "continuous behavior" (ordinary processes/signals).
The 10 VIP agents (test/main_tb/agents/)
Low-level pin drivers/monitors:
clock_agent — drives clock_out. API: clk_gen_agent_start/stop,
clk_agent_set_period/get_period, clk_agent_set_jitter, clk_agent_set_duty,
clk_agent_wait_cycle (blocks the whole com channel while waiting).
reset_agent — drives reset. API: rst_agent_assert/deassert,
rst_agent_polarity_set/get.
memory_bus_agent — DUT register-bus master (scs/swr/srd/sbe/write_data/ address out, read_data in), supports multiple slaves via slave_index. API:
mem_bus_agent_write/read (generic width: 8/16/32-bit or 32*N burst, LSB-first
cells), raw _blocking/_non_blocking single-word variants, mem_bus_agent_start/ stop, _x_mode_start/stop (X-driving outside setup/hold window to model realistic
timing), set_x_mode_setup/hold, set_output_delay, wait_done,
set_slave_index, enable/disable_transaction_reporting.
can_agent — bit-level CAN bus driver (can_rx out) + monitor (can_tx in),
with an optional tx→rx feedback mux so the DUT can see its own transmitted
error/overload frames. Both driver and monitor operate on ring-buffer FIFOs of
timed bit entries. Monitor is an explicit FSM (mon_disabled → mon_waiting_for_trigger → mon_running → mon_passed|mon_failed) with configurable trigger source
(trig_immediately, trig_can_rx/tx_rising/falling_edge, trig_time_elapsed,
trig_driver_start/stop) and an input-delay compensation. API:
can_agent_driver_start/stop/flush, _push_item/_push_value,
_drive_single_item/_drive_all_items, _set_wait_for_monitor;
can_agent_monitor_start/stop/flush, _push_item/_push_value, _set_trigger,
_set_sample_rate, _set_input_delay, _single_item/_all_items,
_check_result (asserts mismatch counter == 0); can_agent_configure_tx_to_rx_feedback.
interrupt_agent — point-in-time IRQ check only (no edge/event monitor). API:
interrupt_agent_polarity_set/get, _check_asserted, _check_not_asserted.
timestamp_agent — drives the DUT's free-running 64-bit timestamp input. API:
timestamp_agent_start/stop, _set_step, _set_prescaler, _timestamp_preset,
_get_timestamp.
test_probe_agent — observes the DUT's (and "Test node"'s) internal debug
test_probe bus (rx_trigger_nbs/rx_trigger_wbs/tx_trigger — bit-timing markers)
and drives scan_mode. API: test_probe_agent_wait_sample(channel, node, skip_stuff_bits) (node 0=DUT, 1=Test node), _wait_sync (waits for next Sync
segment / start-of-bit), _configure_scan. Includes a delta-cycle glitch filter
before accepting an edge as real.
Orchestration:
test_controller_agent — the root sequencer, one instance per simulation run.
On test_start, configures the shared agents (clock period/duty, timestamp
step/prescaler + randomized preset, reset assert/deassert timing, memory-bus X-mode
timing), then branches on generic test_type ("feature"|"reference"|"compliance")
to pulse the matching *_start signal and block on *_done/*_result. The
"compliance" path additionally runs a PLI (byte-command) listener process that lets
an external ISO-16845 C library remotely drive the other agents.
feature_test_agent — active only for test_type="feature". Instantiates a
second full DUT ("Test node"), wires an ANDed wired-OR CAN bus between DUT and Test
node with per-node transceiver-delay and force/flip/override hooks on the bus level
or either node's can_rx. On feature_start: clears both nodes' TXT buffers,
applies identical bit-timing/SSP config to both, sets retransmit limit to 0 on both,
turns both on, waits for both to reach Error Active, then calls
exec_feature_test(test_name, channel) — control passes to the individual feature
test. Result comes from ctu_vip_test_result.
feature_test_agent_pkg.vhd is the HAL every feature test is written against
(~90 procedures/functions; types re-derived manually from the CAN spec rather than
reused from RTL, specifically to catch spec-vs-RTL mismatches). Key groups:
- Node targeting:
t_feature_node = DUT_NODE | TEST_NODE, passed to nearly every call.
- Register access:
ctu_write/ctu_read (+ byte variants), ctu_write_tst_mem/
ctu_read_tst_mem.
- Config:
ctu_set_bit_time_cfg/_get_, ctu_set_ssp, ctu_set_retr_limit,
ctu_set_mask_filter/_ran_filter, ctu_set_mode/_get_mode
(t_ctu_mode record), ctu_set_int_ena/_int_mask, ctu_set_fault_thresholds,
ctu_set_txt_buf_prio.
- Control:
ctu_turn (enable/disable node), ctu_soft_reset, ctu_give_cmd,
ctu_give_txt_cmd (TXT buffer set-ready/empty/abort).
- Frame I/O:
generate_can_frame (randomize a t_ctu_frame), ctu_put_tx_frame/
ctu_send_frame (load + kick a TXT buffer), ctu_read_frame,
compare_can_frames, print_can_frame[_simple], ctu_set_tx_frame_test
(FSTC/FCRC/SDLC frame corruption injection).
- Waiting/sync (poll status regs or protocol state):
ctu_wait_frame_sent,
_bus_idle, _err_frame, _n_bits, _frame_start, _err_active,
_sample_point, _sync_seg, _ff/_not_ff (wait for/until a given protocol
frame-field, t_ctu_frame_field).
- Status/diagnostics:
ctu_get_status (t_ctu_status), _int_status/
_clr_int_status, _fault_state, _err_ctrs, _alc (arbitration-lost capture),
_traff_ctrs, _trv_delay, _err_capt (t_ctu_err_capt: err_pos/err_type),
_hw_config (t_ctu_hw_cfg — synthesis-time capabilities, used to skip tests on
unsupported configs), ctu_get_txt_buf_state/_rx_buf_state.
- Bus-level poking:
force_bus_level, flip_bus_level, release_bus_level,
check_bus_level, force_can_rx, release_can_rx, check_can_tx/get_can_tx/
get_can_rx, set_transceiver_delay, set_timestamp.
feature_test_list_pkg.vhd is a hand-maintained dispatch table: one
use ctu_can_fd_tb.<name>_ftest.all; per feature test, plus exec_feature_test
implemented as an if/elsif (test_name = "<name>") chain calling
<name>_ftest_exec(channel). A deliberate replacement for a prior Python
code-generation step (VHDL has no reflection).
reference_test_agent — active only for test_type="reference". Unlike feature
tests, there's no Test node — it drives raw bit sequences straight from
pre-recorded golden data (reference_test_data_sets/, 10 packages of 1000
(t_ctu_frame, bit-sequence) entries each) directly through the low-level
can_agent, then reads back the DUT's decoded frame and diff-checks it against the
golden frame via compare_can_frames. Bit timing is hardcoded to 2 Mbit/500 Kbit,
80% sample point regardless of any bit-timing generics — reference tests always run
at this fixed rate. test_name selects which data set ("data_set_1".."_10"); a
reference_iterations generic (1-1000) and a random starting offset control how much
of the 1000-entry set gets replayed per run. Because there's no second live
transmitter, reference tests can only verify single-DUT receive-and-decode
correctness, not arbitration/collision scenarios.
functional_coverage_agent — passive, independent of pass/fail, gated by a
func_cov_en generic (no start/stop handshake with test_controller_agent; free-runs
for the whole simulation). Reaches into DUT internals via VHDL-2008 external names
(alias x is << signal .path... : type >>) and expresses every coverage point as a
PSL cover {...} directive embedded in a VHDL comment (not OSVVM, not a custom
covergroup package) — including temporal/ordered-event coverage via PSL SERE syntax
(e.g. cover {write_raw_intent='1'; read_increment='1'}; for ordering, or
cover {(cond)[*4]}; for burst-length coverage). One collector entity per DUT
subsystem: func_cov_can_core (largest — arbitration, all 5 CAN error types, fault
confinement, FSM states, per-field shift-register store/load), func_cov_prescaler
(+ _nbt/_dbt variants), func_cov_bus_sampling, func_cov_rx_buffer,
func_cov_tx_arbitrator (+ _per_buf, one per TXT buffer), func_cov_txt_buffer_even/
_odd (one instance per TXT buffer index).
Feature-test file convention (test/main_tb/feature_tests/, ~155 files)
Flat directory, organized purely by filename prefix (err_capt_* = error-code
capture, mode_* = MODE register bits, int_* = interrupt sources, status_* =
STATUS register, rx_buf_*/rx_err_log* = RX buffer, tx_*/txt_buffer_* = TX
path, alc_* = arbitration-lost capture, ssp_* = secondary sample point,
pc_fsm_transitions_* = protocol-control FSM, retr_limit_*, btr*/dbt_*/
prescaler_* = bit timing, command_* = COMMAND register, frame_test_*/
frame_filters_* = frame corruption / acceptance filters, etc. Numeric suffixes like
_2, _3 denote additional independent test files for the same feature, not
versions.)
Every file follows the exact same template:
Library ctu_can_fd_tb;
context ctu_can_fd_tb.ieee_context;
context ctu_can_fd_tb.rtl_context;
context ctu_can_fd_tb.tb_common_context;
use ctu_can_fd_tb.feature_test_agent_pkg.all;
package <name>_ftest is
procedure <name>_ftest_exec(signal chn : inout t_com_channel);
end package;
package body <name>_ftest is
procedure <name>_ftest_exec(signal chn : inout t_com_channel) is
begin
-- test steps
end procedure;
end package body;
Rules, consistent across the whole suite:
- Package name = filename minus
_ftest.vhd, plus _ftest.
- Exactly one exported procedure,
<name>_ftest_exec, taking exactly one parameter
(signal chn : inout t_com_channel) — scenario variation happens via internal
constants/randomization, not generics/parameters.
- A mandatory
@TestInfoStart / @Purpose / @Verifies / @Test sequence / @TestInfoEnd
doc header, followed by a dated Revision History block. Numbered @N. steps in
"Test sequence" are echoed as comment banners + info_m("Step N") in the body.
- Pass/fail is never exception-based — everything goes through
check_m(cond, msg) /
check_false_m(cond, msg) / error_m(msg), which set the shared
ctu_vip_test_result singleton on failure.
- Common skeleton: configure mode/registers for the scenario → generate/inject
stimulus (frame send via
ctu_send_frame, or bus-level force via
force_bus_level/release_bus_level) → poll for a synchronization point
(ctu_wait_*) → read back status/register/frame → check_m against expectation.
Often wrapped in loops over TXT buffers or config combinations for coverage.
Wiring in a new feature test requires four manual edits: (1) add the *_ftest.vhd
file, (2) add its use clause to feature_test_list_pkg.vhd, (3) add the matching
elsif (test_name = "<name>") dispatch branch there, (4) add a - name: <name> entry
to the test list YAML (test/tlf_feature_and_reference.yml, optionally with per-test
generics: overrides, e.g. to change the top-level iterations generic that wraps
the whole test body in a repeat loop for extra randomized coverage).
Where to look for more detail
- RTL side of the DUT this TB drives: see the
ctu-can-fd-rtl-architecture skill.
- Build/target wiring (which files compile into which VUnit target):
test/slf_tb_common.yml, test/slf_tb_dependencies_{simple,vunit}.yml,
test/slf_tb_top_{simple,vunit,gates}.yml, sim/ts_sim_config.yml.
- ISO 16845 compliance test bridge: out of scope for this skill — see
test/main_tb/iso-16845-compliance-tests/ directly if needed.