|
CocoTB Framework · Verification Infrastructure for RTL Testing GitHub · Documentation Index · MIT License |
field_config.py¶
Field configuration classes for the GAXI validation framework — real class-based field definitions instead of ad-hoc dictionaries, with validation, encoding support, and Rich table formatting.
Overview¶
Dictionary-based field configs work until they don't: a typo'd key, a missing bits entry, a format string nobody validates, and the failure shows up three calls away from the mistake. field_config.py replaces that with FieldDefinition (one field) and FieldConfig (an ordered set of fields for a packet), with validation at construction time instead of at 3 a.m. in a failing test. It handles both individual field definitions and complete packet layouts.
Classes¶
FieldDefinition¶
Definition of a single field within a packet, as a dataclass.
Constructor¶
@dataclass
class FieldDefinition:
name: str
bits: int
default: int = 0
format: str = "hex"
display_width: int = 0
active_bits: Optional[Tuple[int, int]] = None
description: Optional[str] = None
encoding: Optional[Dict[int, str]] = None
Parameters:
- name: Field name
- bits: Width of the field in bits
- default: Default value for the field (default: 0)
- format: Display format - 'hex', 'bin', or 'dec' (default: 'hex')
- display_width: Width for display formatting (auto-calculated if 0)
- active_bits: Tuple of (msb, lsb) defining active bit range (defaults to full width)
- description: Human-readable description (defaults to formatted name)
- encoding: Optional dictionary mapping values to state names
Methods¶
to_dict() -> Dict[str, Any]¶
Convert to dictionary format for backward compatibility.
field_def = FieldDefinition("addr", 32, format="hex", description="Address field")
field_dict = field_def.to_dict()
# Returns: {'bits': 32, 'default': 0, 'format': 'hex', 'display_width': 8, ...}
from_dict(name: str, field_dict: Dict[str, Any]) -> 'FieldDefinition'¶
Create a FieldDefinition from a dictionary representation.
field_dict = {'bits': 16, 'format': 'bin', 'default': 0xFF}
field_def = FieldDefinition.from_dict("data", field_dict)
Examples¶
# Basic field definition
addr_field = FieldDefinition(
name="addr",
bits=32,
format="hex",
description="Memory address"
)
# Field with active bits (partial field)
status_field = FieldDefinition(
name="status",
bits=8,
active_bits=(7, 4), # Only bits 7:4 are used
format="bin",
encoding={
0x0: "IDLE",
0x1: "ACTIVE",
0x2: "ERROR",
0x3: "DONE"
}
)
# Binary field with custom width
mask_field = FieldDefinition(
name="mask",
bits=16,
format="bin",
display_width=16, # Show all 16 bits
default=0xFFFF
)
FieldConfig¶
An ordered configuration of all fields in a packet, with helpers for building, querying, and modifying the layout.
Constructor¶
Creates an empty field configuration. Fields are added using the add_field() method. The lsb_first flag selects the legacy bit-ordering mode (see "Bit Ordering Semantics" below).
Bit Ordering Semantics¶
Bit positions are assigned by add_field() order, and the two modes differ in
which end of the layout a new field lands on:
- Default mode (
lsb_first=False): each newly added field is stacked on top of the existing layout, so the LAST field added occupies the highest bits and the first field added ends up in the lowest bits. - Legacy mode (
lsb_first=True): each newly added field is placed below the existing layout, so the FIRST field added occupies the highest bits.
Concrete two-field example — add_field("a", 4 bits) then add_field("b", 8 bits), total 12 bits:
| Mode | a bit range |
b bit range |
|---|---|---|
Default (lsb_first=False) |
[3:0] |
[11:4] |
Legacy (lsb_first=True) |
[11:8] |
[7:0] |
Read this section twice before you build a config — getting the packing order backwards is the classic way to spend an afternoon wondering why your address field contains your data. These semantics are pinned by tests/unit/test_field_config.py and will not change silently.
Methods¶
add_field(field_def: FieldDefinition) -> 'FieldConfig'¶
Add a field to the configuration. Bit positions follow the mode-dependent rules described in "Bit Ordering Semantics" above (default: last-added field gets the highest bits).
Parameters:
- field_def: Field definition to add
Returns: Self for method chaining
config = FieldConfig()
config.add_field(FieldDefinition("addr", 32, format="hex"))
config.add_field(FieldDefinition("data", 32, format="hex"))
# Default mode: data (added last) -> bits [63:32], addr -> bits [31:0]
add_field_dict(name: str, field_dict: Dict[str, Any]) -> 'FieldConfig'¶
Add a field from dictionary representation (for backward compatibility).
remove_field(name: str) -> 'FieldConfig'¶
Remove a field from the configuration.
get_field(name: str) -> FieldDefinition¶
Get a field definition by name.
Raises: KeyError if field doesn't exist
has_field(name: str) -> bool¶
Check if a field exists in the configuration.
field_names() -> List[str]¶
Get ordered list of field names in bit-position order (highest bits first). In the default mode this is the reverse of the order fields were added; use get_logical_order() for the add order.
fields() -> List[FieldDefinition]¶
Get ordered list of field definitions.
items()¶
Get name/definition pairs in order, similar to dict.items().
for name, field_def in config.items():
print(f"{name} = {field_def.bits} bits, format={field_def.format}")
get_total_bits() -> int¶
Calculate the total number of bits across all fields.
create_packet()¶
Create a packet object compatible with the field configuration.
debug_str(indent=0) -> str¶
Return a formatted string representation using Rich tables.
Dictionary-like Interface¶
FieldConfig supports dictionary-style operations, so old code that treated configs as dicts mostly keeps working:
# Length
num_fields = len(config)
# Iteration
for field_name in config:
print(field_name)
# Membership testing
if "addr" in config:
print("Address field exists")
# Direct access
addr_field = config["addr"]
Validation and Creation Methods¶
validate_and_create(field_dict: Dict[str, Dict[str, Any]], raise_errors: bool = False) -> 'FieldConfig'¶
Validate a dictionary-based field configuration and convert to FieldConfig object.
Parameters:
- field_dict: Dictionary mapping field names to field properties
- raise_errors: If True, raise exceptions for validation errors; if False, attempt to correct or warn
Returns: New validated FieldConfig instance
Notes:
- The caller's dictionary is never mutated; corrections are applied to an internal copy.
- An active_bits range with msb < lsb is clamped and swapped (with a warning) rather than the field being dropped.
# Define fields as dictionary
field_dict = {
'addr': {'bits': 32, 'format': 'hex'},
'data': {'bits': 32, 'format': 'hex'},
'ctrl': {'bits': 8, 'format': 'bin', 'encoding': {0: 'READ', 1: 'WRITE'}}
}
# Validate and create
config = FieldConfig.validate_and_create(field_dict)
from_dict(field_dict: Dict[str, Dict[str, Any]]) -> 'FieldConfig'¶
Create a FieldConfig from a dictionary representation.
Pre-built Configuration Methods¶
create_data_only(data_width: int = 32) -> 'FieldConfig'¶
Create a simple data-only field configuration.
create_standard(addr_width: int = 32, data_width: int = 32) -> 'FieldConfig'¶
Create a standard address/data field configuration.
create_multi_data(addr_width: int = 4, ctrl_width: int = 4, data_width: int = 8, num_data: int = 2) -> 'FieldConfig'¶
Create a multi-data field configuration with addr, ctrl, and multiple data fields.
# Creates: addr(4), ctrl(4), data0(8), data1(8)
config = FieldConfig.create_multi_data(addr_width=4, ctrl_width=4, data_width=8, num_data=2)
Field Manipulation Methods¶
update_field_width(field_name: str, new_bits: int, update_active_bits: bool = True) -> 'FieldConfig'¶
Update a field's bit width and optionally its active bits.
set_encoding(field_name: str, encoding: Dict[int, str]) -> 'FieldConfig'¶
Set an encoding dictionary for a field.
add_encoding_value(field_name: str, value: int, state_name: str) -> 'FieldConfig'¶
Add a single encoding value to a field.
Usage Patterns¶
Basic Field Configuration¶
# Create configuration
config = FieldConfig()
# Add address field
config.add_field(FieldDefinition(
name="addr",
bits=32,
format="hex",
description="Memory address"
))
# Add data field
config.add_field(FieldDefinition(
name="data",
bits=32,
format="hex",
description="Data value"
))
# Add control field with encoding
config.add_field(FieldDefinition(
name="cmd",
bits=4,
format="hex",
encoding={
0x0: "NOP",
0x1: "READ",
0x2: "WRITE",
0x3: "BURST"
},
description="Command type"
))
print(f"Total packet size: {config.get_total_bits()} bits")
Configuration with Active Bits¶
# Field where only certain bits are used
config.add_field(FieldDefinition(
name="status",
bits=32,
active_bits=(15, 8), # Only bits 15:8 are active
format="hex",
description="Status register"
))
# This is useful for registers where not all bits are used
Active bits are the answer for registers where the designer only wired up a slice of the word — you keep the full-width field for layout, and the framework only cares about the bits that actually exist.
Validation and Error Correction¶
# Dictionary with potential issues
problematic_config = {
'addr': {'bits': 32, 'format': 'hex'},
'data': {'bits': -5, 'format': 'invalid'}, # Invalid bits and format
'ctrl': {} # Missing required 'bits' field
}
# Validate and auto-correct
try:
config = FieldConfig.validate_and_create(problematic_config, raise_errors=False)
print("Configuration created with corrections")
except ValueError as e:
print(f"Validation failed: {e}")
Dynamic Field Configuration¶
class ConfigBuilder:
def __init__(self):
self.config = FieldConfig()
def add_address_field(self, width=32):
self.config.add_field(FieldDefinition("addr", width, format="hex"))
return self
def add_data_fields(self, count=1, width=32):
for i in range(count):
self.config.add_field(FieldDefinition(f"data{i}", width, format="hex"))
return self
def add_control_field(self, width=8, commands=None):
encoding = commands or {0: "NOP", 1: "READ", 2: "WRITE"}
self.config.add_field(FieldDefinition("ctrl", width, format="bin", encoding=encoding))
return self
def build(self):
return self.config
# Usage
config = (ConfigBuilder()
.add_address_field(32)
.add_data_fields(count=2, width=64)
.add_control_field(width=4)
.build())
Integration with Packets¶
# Create configuration
config = FieldConfig.create_standard(addr_width=32, data_width=32)
# Create packet using configuration
packet = config.create_packet()
# Set field values
packet.addr = 0x12345678
packet.data = 0xDEADBEEF
# Access field configuration
addr_field = config.get_field("addr")
print(f"Address field: {addr_field.bits} bits, format: {addr_field.format}")
Rich Table Display¶
# Display configuration in formatted table
print(config.debug_str())
# Output:
# ┏━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
# ┃ Field Name ┃ Bits ┃ Format ┃ Active Bits ┃ Default ┃ Encoding ┃ Description ┃
# ┡━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
# │ addr │ 32 │ hex │ (31:0) │ 0x0 │ │ Address │
# │ data │ 32 │ hex │ (31:0) │ 0x0 │ │ Data value │
# └────────────┴──────┴────────┴─────────────┴─────────┴──────────┴────────────────────┘
# Total bits: 64
Converting Legacy Configurations¶
# Legacy dictionary format
legacy_config = {
'addr': {
'bits': 32,
'format': 'hex',
'default': 0,
'description': 'Address field'
},
'data': {
'bits': 32,
'format': 'hex',
'default': 0,
'description': 'Data field'
}
}
# Convert to new format
modern_config = FieldConfig.from_dict(legacy_config)
# Or with validation
validated_config = FieldConfig.validate_and_create(legacy_config)
Error Handling¶
FieldConfig fails loudly and early, which is what you want from a config layer:
Field Definition Validation¶
- Bit Width: Must be positive integer
- Active Bits: Must be within field bounds
- Format: Must be 'hex', 'bin', or 'dec'
- Encoding: Keys must be integers, values must be strings
Configuration Validation¶
- Duplicate Fields: Prevents adding fields with same name
- Missing Fields: Graceful handling of missing field lookups
- Type Checking: Ensures proper types for all parameters
Error Messages¶
try:
config.add_field(FieldDefinition("test", bits=-5))
except ValueError as e:
# Error: Invalid active_bits (4:-1) for field 'test' with width -5
pass
try:
config.get_field("nonexistent")
except KeyError as e:
# Error: Field 'nonexistent' not found in configuration
pass
Best Practices¶
- Use Method Chaining: The fluent interface makes config construction readable — use it.
- Validate Early: Run
validate_and_create()on any config that comes from outside your code. - Document Fields: Write real descriptions. "Address field" beats "addr" when someone else reads the dump.
- Use Encoding: Give control fields state names.
0x2: "WRITE"in a log line saves a decode step every time. - Check Existence: Use
has_field()before accessing optional fields. - Display Configs:
debug_str()is the fastest way to eyeball a config in review or in a log.
Integration with Other Components¶
FieldConfig is the piece the rest of the framework reads from:
- Packet: Gets its structure and validation rules from FieldConfig
- PacketFactory: Uses FieldConfig for packet creation
- DataStrategies: Uses field widths and layouts for cached collection/driving
- FlexRandomizer: Can size generated values from field bit widths
- Memory Model: Uses field definitions for transaction processing