|
CocoTB Framework · Verification Infrastructure for RTL Testing GitHub · Documentation Index · MIT License |
WaveDrom Automatic Signal Binding Guide¶
Version: 2.0 (SignalResolver Integration) Last Updated: 2025-10-04 Status: Production Ready
Overview¶
WaveDrom signal binding runs through SignalResolver, the same pattern-matching layer the BFMs use. Hand it a prefix and it finds the signals on your DUT; if your naming is unusual, signal_map gives you exact control. When discovery fails, the error message tells you which patterns it tried and which candidate signals it actually found on the DUT — you spend your time fixing the name, not hunting through hierarchy.
What You Get¶
- Automatic discovery — each logical signal is matched against a list of naming patterns until one sticks
- Manual override —
signal_mapbinds exactly what you say, no guessing - A printed mapping table — you can see what got bound before the test runs
- Errors that help — failures list the valid-like signals present on the DUT and show the override syntax
- Protocol coverage — GAXI, APB, AXIS today, more as presets land
Quick Start¶
Wavedrom User Examples
The protocol-specific wavedrom user examples (GAXIWaveDromTemplate, APBWaveDromTemplate, etc.) are located in the RTLDesignSherpa repository under tbclasses/wavedrom_user/.
GAXI WaveDrom (Simplest Case)¶
If your signals share a prefix, the whole setup is one constructor:
# Import from the RTLDesignSherpa main repo (tbclasses/wavedrom_user/gaxi.py)
from CocoTBFramework.tbclasses.wavedrom_user.gaxi import GAXIWaveDromTemplate
@cocotb.test()
async def my_test(dut):
# ONE LINE setup - automatic signal discovery!
gaxi_wave = GAXIWaveDromTemplate(
dut=dut,
signal_prefix="wr_", # Finds: wr_valid, wr_ready, wr_data
data_width=32
)
await gaxi_wave.start_sampling()
# Run your test...
# ...
results = await gaxi_wave.stop_sampling()
gaxi_wave.get_status()
Discovery, binding, constraint setup, and the WaveJSON generator are all wired up behind that call.
APB WaveDrom¶
Same shape, APB signals:
# Import from the RTLDesignSherpa main repo (tbclasses/wavedrom_user/apb.py)
from CocoTBFramework.tbclasses.wavedrom_user.apb import APBWaveDromTemplate
@cocotb.test()
async def my_apb_test(dut):
# ONE LINE setup - automatic APB signal discovery!
apb_wave = APBWaveDromTemplate(
dut=dut,
signal_prefix="apb_", # Finds: apb_psel, apb_penable, etc.
data_width=32,
addr_width=32
)
await apb_wave.start_sampling()
# Run APB transactions...
# ...
results = await apb_wave.stop_sampling()
How Automatic Discovery Works¶
Pattern Matching¶
SignalResolver doesn't take one guess and give up — it walks a list of patterns per logical signal and uses the first that exists on the DUT:
GAXI Example (prefix='wr_'):¶
validsignal: trieswr_valid,wr_gaxi_valid,wr_m2s_valid, etc.readysignal: trieswr_ready,wr_gaxi_ready,wr_s2m_ready, etc.datasignal: trieswr_data,wr_pkt,wr_packet, etc.
APB Example (prefix='apb_'):¶
pselsignal: triesapb_psel,apb_PSEL,s_apb_psel, etc.penablesignal: triesapb_penable,apb_PENABLE, etc.paddrsignal: triesapb_paddr,apb_PADDR,apb_addr, etc.
The Mapping Table¶
Discovery prints what it bound. Read this table once when you bring up a new bench — a wrong match here explains every strange waveform downstream:
| Logical Signal | Matched Signal | Cocotb Signal | Status |
|---|---|---|---|
| valid | wr_valid | valid | Found |
| ready | wr_ready | ready | Found |
| data_sig | wr_data | data | Found (Optional) |
Signal mapping for GAXI WaveDrom (gaxi_wavedrom), automatic discovery
Template Classes¶
GAXIWaveDromTemplate¶
class GAXIWaveDromTemplate:
def __init__(self,
dut,
signal_prefix: str = "gaxi_",
data_width: int = 32,
addr_width: int = 0,
ctrl_width: int = 0,
num_data_fields: int = 1,
preset: str = "comprehensive",
bus_name: str = '',
pkt_prefix: str = '',
signal_map: Optional[Dict[str, str]] = None,
clock_signal = None)
Parameters:
- signal_prefix: prefix prepended to discovered names ('wr_', 'cmd_', 's_', ...)
- data_width: data field width
- preset: which constraint set to load:
- 'comprehensive': all handshake patterns (default)
- 'basic_handshake': plain valid/ready
- 'performance': throughput analysis
- 'debug': debug patterns
- signal_map: manual override (see below)
- clock_signal: auto-detected when None (tries axi_aclk, i_clk, clk)
APBWaveDromTemplate¶
class APBWaveDromTemplate:
def __init__(self,
dut,
signal_prefix: str = "apb_",
data_width: int = 32,
addr_width: int = 32,
preset: str = "comprehensive",
bus_name: str = '',
signal_map: Optional[Dict[str, str]] = None,
clock_signal = None)
Parameters:
- signal_prefix: prefix prepended to discovered names ('apb_', 's_apb_', '')
- preset: which constraint set to load:
- 'comprehensive': full APB protocol analysis (default)
- 'basic_rw': read/write transactions only
- 'timing': timing and wait state analysis
- 'debug': debug patterns
- 'error': error-focused
- signal_map: manual override
- clock_signal: auto-detected (tries pclk, apb_pclk, i_clk, clk)
Manual Signal Override¶
When You Need It¶
Reach for signal_map when you have:
- Non-standard signal naming
- A prefix the pattern lists don't cover
- Legacy RTL you can't rename
- Or when you just want one signal pinned without touching anything else
GAXI Manual Override¶
gaxi_wave = GAXIWaveDromTemplate(
dut,
signal_prefix="", # Empty - using manual map
data_width=32,
signal_map={
'valid': 'weird_valid_name',
'ready': 'custom_ready_sig',
'data': 'pkt_data_field'
}
)
APB Manual Override¶
apb_wave = APBWaveDromTemplate(
dut,
signal_prefix="",
data_width=32,
addr_width=32,
signal_map={
'psel': 'chip_select',
'penable': 'enable_sig',
'pwrite': 'wr_en',
'pready': 'slave_ready',
'paddr': 'address_bus',
'pwdata': 'write_data',
'prdata': 'read_data'
}
)
Multi-Field Protocols¶
Multiple Data Fields (GAXI)¶
For packet-style interfaces, describe the fields once and discovery looks for one signal per field:
from CocoTBFramework.components.shared.field_config import FieldConfig, FieldDefinition
# Create field configuration
field_config = FieldConfig()
field_config.add_field(FieldDefinition('addr', bits=32))
field_config.add_field(FieldDefinition('data', bits=64))
field_config.add_field(FieldDefinition('ctrl', bits=8))
gaxi_wave = GAXIWaveDromTemplate(
dut,
signal_prefix="wr_",
data_width=0, # Not used when field_config provided
field_config=field_config # Signal discovery will find: wr_addr, wr_data, wr_ctrl
)
Error Handling¶
When a Signal Isn't Found¶
Setup fails loudly, and the message is long on purpose: it shows the patterns tried, the valid-like signals that do exist on the DUT, and the signal_map line that would fix it.
🚨 CRITICAL: No valid signal found for GAXI WaveDrom!
Component: GAXI WaveDrom
Protocol: gaxi_wavedrom
Mode: single-signal (multi_sig=False)
Bus name: '' (empty means no bus prefix)
This component REQUIRES a valid signal for proper operation.
💡 TROUBLESHOOTING:
1. Check signal naming - expected patterns:
- valid (current prefix + 'valid')
- wr_valid (for write-side)
- m2s_valid (master-to-slave)
2. Available valid-like signals found on DUT:
DATA_VALID, cmd_valid, wr_v
3. Use manual signal_map to specify correct signal:
signal_map={'valid': 'cmd_valid'}
4. Check signal_prefix parameter - currently: 'wr_'
If your signals have a different prefix, update signal_prefix
Architecture Overview¶
Component Stack¶
graph TB
subgraph Stack["Architecture Stack"]
Templates["GAXIWaveDromTemplate / APB...<br/>(User-facing templates)"]
Solver["TemporalConstraintSolver<br/>- auto_bind_signals()"]
Binder["WavedromSignalBinder<br/>(Wavedrom-specific layer)"]
Resolver["SignalResolver<br/>- PROTOCOL_SIGNAL_CONFIGS<br/>(Pattern matching engine)"]
end
Templates --> Solver
Solver --> Binder
Binder --> Resolver
Signal Flow¶
- Template init — you create
GAXIWaveDromTemplate(dut, signal_prefix='wr_') - Auto-bind — the template calls
wave_solver.auto_bind_signals('gaxi', signal_prefix='wr_') - Wavedrom binder — builds a
WavedromSignalBinderwith protocol='gaxi_wavedrom' - Signal resolver — pulls its patterns from
PROTOCOL_SIGNAL_CONFIGS['gaxi_wavedrom'] - Pattern match — tries each combination in turn:
wr_valid,wr_gaxi_valid, ... - Binding — matched signals are bound to the solver via
add_signal_binding() - Display — the mapping table is printed
Configuration Files¶
Adding New Protocols¶
Teaching discovery a new protocol takes three edits:
1. Add patterns to signal_mapping_helper.py:
PROTOCOL_SIGNAL_CONFIGS = {
# ... existing configs ...
'myprotocol_wavedrom': {
'signal_map': {
'req': ['{prefix}{bus_name}req', '{prefix}{bus_name}request'],
'ack': ['{prefix}{bus_name}ack', '{prefix}{bus_name}acknowledge'],
},
'optional_signal_map': {
'multi_sig_false': ['{prefix}{bus_name}data'],
'multi_sig_true': ['{prefix}{bus_name}{field_name}']
}
}
}
2. Add protocol type handling in signal_mapping_helper.py:
# In _resolve_optional_signals() method:
elif self.protocol_type in ['myprotocol_master', 'myprotocol_slave', 'myprotocol_wavedrom']:
signal_obj = self._find_signal_match('data_sig', patterns, required=False)
self.resolved_signals['data_sig'] = signal_obj
3. Create template class (similar to GAXIWaveDromTemplate)
Testing¶
Test Your Wavedrom Setup¶
@cocotb.test()
async def test_wavedrom(dut):
# Setup wavedrom
wave = GAXIWaveDromTemplate(dut, signal_prefix="wr_", data_width=32)
await wave.start_sampling()
# Generate transactions
for i in range(5):
dut.wr_valid.value = 1
dut.wr_data.value = 0xA000 + i
await RisingEdge(dut.clk)
dut.wr_valid.value = 0
await RisingEdge(dut.clk)
# Get results
results = await wave.stop_sampling()
# Verify
assert len(results['solutions']) > 0, "Should find patterns"
assert 'wr_handshake' in results['satisfied_constraints']
wave.get_status() # Print summary
View Generated WaveJSON¶
One WaveJSON file per matched scenario lands next to the sim output:
wr_handshake_001.jsonwr_back2back_001.jsonwr_stall_001.json- etc.
View at: https://wavedrom.com/editor.html
Advanced Usage¶
Custom Constraints with Auto-Binding¶
You can drive the solver directly, write your own constraints, and still get discovery for the signals:
# Create solver with auto-binding
wave_solver = TemporalConstraintSolver(dut=dut, log=dut._log)
wave_solver.add_clock_group('default', dut.clk)
# Auto-bind signals
# Note: providing signal_map bypasses automatic discovery entirely,
# so list ALL required signals when using it
num_signals = wave_solver.auto_bind_signals(
protocol_type='gaxi',
signal_prefix='',
signal_map={
'valid': 'custom_valid',
'ready': 'wr_ready',
'data': 'wr_data'
}
)
# Add custom constraints
custom_constraint = TemporalConstraint(
name="my_pattern",
events=[...],
...
)
wave_solver.add_constraint(custom_constraint)
# Run
await wave_solver.start_sampling()
# ...
await wave_solver.stop_sampling()
Note the comment in the code — it trips people up: passing signal_map bypasses discovery entirely, so the map has to list every required signal, not just the one with the odd name.
Debugging Signal Resolution¶
If discovery is being mysterious, ask it to narrate:
wave_solver.auto_bind_signals(
protocol_type='gaxi',
signal_prefix='wr_',
super_debug=True # Verbose signal resolution logging
)
Migration Guide¶
From Manual Binding¶
Manual binding still works — auto-binding is just shorter:
Old approach:
# Manual binding (still supported, but verbose)
wave_solver.add_signal_binding('wr_valid', 'wr_valid')
wave_solver.add_signal_binding('wr_ready', 'wr_ready')
wave_solver.add_signal_binding('wr_data', 'wr_data')
New approach:
From Old Template¶
Old:
# Old GAXIWaveDrom class with manual setup
wave = GAXIWaveDrom(dut, ...)
# ... complex setup code ...
New:
# New GAXIWaveDromTemplate - one line!
wave = GAXIWaveDromTemplate(dut, signal_prefix='wr_', data_width=32)
Troubleshooting¶
Signal Not Found¶
Symptom: "No valid signal found"
Fixes:
1. Look at the actual hierarchy — print(dir(dut)) is your friend here
2. Check that signal_prefix matches your naming
3. Pin the signal manually:
Clock Not Auto-Detected¶
Symptom: "Could not auto-detect clock signal"
Fix: hand it the clock explicitly.
wave = GAXIWaveDromTemplate(
dut,
signal_prefix='wr_',
clock_signal=dut.my_custom_clk # Explicit clock
)
No Patterns Found¶
Symptom: sampling runs to completion, but nothing matches
Fixes:
1. The constraint windows may not fit your traffic — try preset='debug'
2. Confirm the signals are actually toggling with wave.get_status()
3. Widen the window — max_cycles=50 or more for slow patterns
Best Practices¶
Do¶
- Use discovery with
signal_prefix— it's the path everything else is tested against - Let the clock auto-detect when your naming is standard
- Pick the preset that matches what you're actually looking for
- Call
wave.get_status()after sampling; it tells you what happened, not what you hoped happened
Don't¶
- Reach for
signal_mapwhen a prefix would do the job - Hardcode signal handles into tests
- Skim past the error messages — they usually name the fix
See Also¶
- SignalResolver Documentation:
src/CocoTBFramework/components/shared/signal_mapping_helper.py(this repo) - Constraint Solver:
src/CocoTBFramework/components/wavedrom/constraint_solver.py(this repo) - GAXI Constraints: Located in the RTLDesignSherpa repo under
tbclasses/wavedrom_user/gaxi.py - APB Constraints: Located in the RTLDesignSherpa repo under
tbclasses/wavedrom_user/apb.py
Version History: - v2.0 (2025-10-04): SignalResolver integration, auto-binding - v1.0 (2025-09-15): Initial manual binding version
Maintained By: RTL Design Sherpa Project