Skip to content
RTL Design Sherpa 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

FieldConfig(lsb_first: bool = False)

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).

config.add_field_dict("ctrl", {
    'bits': 8,
    'format': 'bin',
    'encoding': {0: 'READ', 1: 'WRITE'}
})
remove_field(name: str) -> 'FieldConfig'

Remove a field from the configuration.

config.remove_field("unused_field")
get_field(name: str) -> FieldDefinition

Get a field definition by name.

Raises: KeyError if field doesn't exist

addr_field = config.get_field("addr")
print(f"Address field has {addr_field.bits} bits")
has_field(name: str) -> bool

Check if a field exists in the configuration.

if config.has_field("optional_field"):
    field = config.get_field("optional_field")
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.

names = config.field_names()
# Bit-position order (highest bits first)
fields() -> List[FieldDefinition]

Get ordered list of field definitions.

for field_def in config.fields():
    print(f"{field_def.name}: {field_def.bits} bits")
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.

total = config.get_total_bits()
print(f"Total packet width: {total} bits")
create_packet()

Create a packet object compatible with the field configuration.

packet = config.create_packet()
packet.addr = 0x1000
packet.data = 0xDEADBEEF
debug_str(indent=0) -> str

Return a formatted string representation using Rich tables.

print(config.debug_str())
# Displays formatted table with all field information

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.

config = FieldConfig.from_dict(field_dict)

Pre-built Configuration Methods

create_data_only(data_width: int = 32) -> 'FieldConfig'

Create a simple data-only field configuration.

config = FieldConfig.create_data_only(64)  # 64-bit data field
create_standard(addr_width: int = 32, data_width: int = 32) -> 'FieldConfig'

Create a standard address/data field configuration.

config = FieldConfig.create_standard(addr_width=32, data_width=64)
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.

config.update_field_width("addr", 64)  # Expand address to 64 bits
set_encoding(field_name: str, encoding: Dict[int, str]) -> 'FieldConfig'

Set an encoding dictionary for a field.

config.set_encoding("status", {
    0: "IDLE",
    1: "ACTIVE", 
    2: "ERROR"
})
add_encoding_value(field_name: str, value: int, state_name: str) -> 'FieldConfig'

Add a single encoding value to a field.

config.add_encoding_value("status", 3, "DONE")

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

  1. Use Method Chaining: The fluent interface makes config construction readable — use it.
  2. Validate Early: Run validate_and_create() on any config that comes from outside your code.
  3. Document Fields: Write real descriptions. "Address field" beats "addr" when someone else reads the dump.
  4. Use Encoding: Give control fields state names. 0x2: "WRITE" in a log line saves a decode step every time.
  5. Check Existence: Use has_field() before accessing optional fields.
  6. 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