Skip to content
RTL Design Sherpa CocoTB Framework · Verification Infrastructure for RTL Testing
GitHub · Documentation Index · MIT License

apb_packet.py

Two classes live here. APBPacket is the object that actually travels through the BFMs — direction, address, data, strobes, protection, error, plus timing stamps. APBTransaction is a constrained-random generator that stamps packets out for you when directed tests stop finding bugs.

Overview

  • APBPacket: the framework's base Packet class with the APB field set baked in
  • APBTransaction: randomized stimulus generator that produces APBPacket objects

Key Features

  • Field definitions via FieldConfig — widths and formats are declared, not hoped for
  • Constrained randomization through FlexRandomizer, with a sane default profile
  • Direction-aware comparison and formatting — a read and a write to the same address are not the same transaction
  • Defaults that match a standard 32-bit APB bus, overridable per packet
  • Full integration with the base packet machinery — timing, counting, equality, formatting

Constants and Mappings

One decode table, used everywhere the direction gets printed:

# PWRITE direction mapping
PWRITE_MAP = ['READ', 'WRITE']

Core Classes

APBPacket

The base Packet with APB fields. If you've used any other packet class in this framework you already know the API — comparison, formatting, and timing stamps are inherited; only the field set is APB-specific.

Constructor

APBPacket(field_config=None, skip_compare_fields=None, data_width=32, addr_width=32, strb_width=4, **kwargs)

Parameters: - field_config: Field configuration (default: auto-generated APB config) - skip_compare_fields: Fields to skip during equality comparison - data_width: Width of data fields in bits (default: 32) - addr_width: Width of address field in bits (default: 32) - strb_width: Width of strobe field in bits (default: 4) - **kwargs: Initial field values (e.g., paddr=0x123, pwrite=1)

# Create APB packet with default configuration
packet = APBPacket(
    pwrite=1,           # Write transaction
    paddr=0x1000,       # Address
    pwdata=0xDEADBEEF,  # Write data
    pstrb=0xF           # All strobes active
)

# Create with custom configuration
custom_packet = APBPacket(
    data_width=64,
    addr_width=16,
    strb_width=8,
    pwrite=0,           # Read transaction
    paddr=0x200
)

Properties

  • direction: 'READ' or 'WRITE', decoded from PWRITE
  • data_width: Data field width in bits
  • addr_width: Address field width in bits
  • strb_width: Strobe field width in bits
  • count: Transaction counter — handy when correlating log lines

APB Field Configuration

The default layout — what you get unless you pass your own field_config:

Field Bits Format Description
pwrite 1 dec Write Enable (0=Read, 1=Write)
paddr 32 hex Address
pwdata 32 hex Write Data
prdata 32 hex Read Data
pstrb 4 bin Write Strobes
pprot 3 hex Protection Control
pslverr 1 dec Slave Error

Methods

create_apb4_field_config(addr_width, data_width, strb_width) [Static]

Build a FieldConfig for a non-standard bus width, then pass it to the constructor. Most tests never need this — the defaults already cover 32-bit data, 32-bit address, 4 strobes.

Parameters: - addr_width: Address field width in bits - data_width: Data field width in bits - strb_width: Strobe field width in bits

Returns: FieldConfig object with APB fields

# Create custom field configuration
config = APBPacket.create_apb4_field_config(
    addr_width=16,
    data_width=64,
    strb_width=8
)

# Use in packet creation
packet = APBPacket(field_config=config)
__str__() -> str

The verbose dump — every field, plus timing:

print(packet)
# Output:
# APB Packet:
#   Direction:  WRITE
#   Address:    0x00001000
#   Write Data: 0xDEADBEEF
#   Strobes:    1111
#   Protection: 0x0
#   Slave Err:  0
#   Start Time: 1000 ns
#   End Time:   2000 ns
#   Duration:   1000 ns
#   Count:      1
formatted(compact=False) -> str

The one you'll actually call in loops. compact=True gives you one line per transaction, which is the difference between a readable log and a 40 MB one.

Parameters: - compact: If True, return compact one-line format

# Detailed format
print(packet.formatted())

# Compact format
print(packet.formatted(compact=True))
# Output: APBPacket(time=1000, dir=WRITE, addr=0x00001000, wdata=0xDEADBEEF, strb=1111, prot=0x0)
__eq__(other) -> bool

Equality is direction-aware: reads compare PRDATA, writes compare PWDATA, and a read never equals a write no matter how similar the addresses look. Anything listed in skip_compare_fields is ignored.

packet1 = APBPacket(pwrite=1, paddr=0x100, pwdata=0x123)
packet2 = APBPacket(pwrite=1, paddr=0x100, pwdata=0x123)
assert packet1 == packet2  # True

# Direction-aware data comparison
read_packet = APBPacket(pwrite=0, paddr=0x100, prdata=0x456)
write_packet = APBPacket(pwrite=1, paddr=0x100, pwdata=0x123)
assert read_packet != write_packet  # Different directions and data

APBTransaction

A Randomized subclass that generates APBPacket objects from a constraint set. Reach for it when hand-written packets stop turning up new failures.

Constructor

APBTransaction(data_width=32, addr_width=32, strb_width=4, randomizer=None, **kwargs)

Parameters: - data_width: Data width in bits (default: 32) - addr_width: Address width in bits (default: 32) - strb_width: Strobe width in bits (default: 4) - randomizer: FlexRandomizer instance or constraint dictionary - **kwargs: Initial field values

# Create with default randomization
transaction = APBTransaction()

# Create with custom randomization
custom_randomizer = FlexRandomizer({
    'pwrite': ([(0, 0), (1, 1)], [1, 2]),  # 2:1 write bias
    'paddr': ([(0x1000, 0x1FFF)], [1]),    # Limited address range
    'pstrb': ([(0xF, 0xF)], [1])           # Always full strobes
})

transaction = APBTransaction(
    randomizer=custom_randomizer,
    data_width=64
)

Default Randomization

Out of the box you get a balanced mix — 50/50 reads and writes, addresses weighted toward the low end of the map, mostly full strobes, mostly normal protection:

# Default randomization constraints
{
    'pwrite': ([(0, 0), (1, 1)], [1, 1]),           # Equal read/write
    'paddr': ([(0, addr_min), (addr_max_lo, addr_max_hi)], [4, 1]),  # Address distribution
    'pstrb': ([(15, 15), (0, 14)], [4, 1]),         # Mostly full strobes
    'pprot': ([(0, 0), (1, 7)], [4, 1])             # Mostly normal protection
}

Methods

next() -> APBPacket

Generate and return the next randomized APBPacket.

Returns: APBPacket with randomized field values

# Generate random transactions
for i in range(10):
    packet = transaction.next()
    print(f"Transaction {i}: {packet.formatted(compact=True)}")
set_constrained_random() -> 'APBTransaction'

Randomize the transaction's fields in place. Note the return value is self, for chaining — the generated packet is read back from .packet, not from the return.

Returns: Self for method chaining

# Randomize in place; the generated packet is available via .packet
transaction.set_constrained_random()
packet = transaction.packet
formatted(compact=False) -> str

Format the transaction's current packet, compact or verbose.

print(transaction.formatted())
print(transaction.formatted(compact=True))

Usage Patterns

Basic Packet Creation

Everything is a keyword argument — say what you want and let the rest default:

from CocoTBFramework.components.apb.apb_packet import APBPacket

# Create write packet
write_packet = APBPacket(
    pwrite=1,
    paddr=0x2000,
    pwdata=0x12345678,
    pstrb=0xF,
    pprot=0
)

# Create read packet
read_packet = APBPacket(
    pwrite=0,
    paddr=0x2000,
    pprot=0
)

print(f"Write: {write_packet.formatted(compact=True)}")
print(f"Read: {read_packet.formatted(compact=True)}")

Custom Field Configuration

64-bit data bus? Eight strobes? Build the config once and reuse it:

# Create 64-bit APB configuration
config = APBPacket.create_apb4_field_config(
    addr_width=32,
    data_width=64,
    strb_width=8
)

# Create packet with custom configuration
packet = APBPacket(
    field_config=config,
    pwrite=1,
    paddr=0x8000,
    pwdata=0x123456789ABCDEF0,
    pstrb=0xFF  # All 8 strobes active
)

Transaction Generation

Let the defaults drive when you just need traffic:

from CocoTBFramework.components.apb.apb_packet import APBTransaction

# Create transaction generator
transaction = APBTransaction(
    data_width=32,
    addr_width=16
)

# Generate test sequence
packets = []
for i in range(100):
    packet = transaction.next()
    packets.append(packet)

# Analyze generated patterns
write_count = sum(1 for p in packets if p.direction == 'WRITE')
read_count = len(packets) - write_count
print(f"Generated {write_count} writes, {read_count} reads")

Constrained Randomization

When you're hunting something specific, weight the bins toward it — here, low addresses and a spread of strobe patterns:

# Create address-constrained randomizer
address_randomizer = FlexRandomizer({
    'pwrite': ([(0, 0), (1, 1)], [1, 1]),
    'paddr': ([(0x1000, 0x1FFF), (0x8000, 0x8FFF)], [3, 1]),  # Prefer low addresses
    'pstrb': ([(0xF, 0xF), (0x3, 0x3), (0xC, 0xC)], [6, 1, 1]),  # Various strobe patterns
    'pprot': ([(0, 0)], [1])  # Always normal protection
})

# Create constrained transaction generator
constrained_transaction = APBTransaction(randomizer=address_randomizer)

# Generate constrained sequence
for i in range(20):
    packet = constrained_transaction.next()
    print(f"Addr: 0x{packet.paddr:04X}, Strb: 0b{packet.pstrb:04b}, Dir: {packet.direction}")

Register Access Patterns

A helper-shaped pattern you'll write in every APB project: write a known value to each register, then read them all back.

def create_register_access_sequence(base_addr, num_regs):
    """Create sequence for register testing"""
    packets = []

    # Write pattern to all registers
    for i in range(num_regs):
        addr = base_addr + (i * 4)
        data = 0xA0000000 + i

        write_packet = APBPacket(
            pwrite=1,
            paddr=addr,
            pwdata=data,
            pstrb=0xF,
            count=len(packets)
        )
        packets.append(write_packet)

    # Read back all registers
    for i in range(num_regs):
        addr = base_addr + (i * 4)

        read_packet = APBPacket(
            pwrite=0,
            paddr=addr,
            count=len(packets)
        )
        packets.append(read_packet)

    return packets

# Generate register test sequence
reg_packets = create_register_access_sequence(0x1000, 16)

Error Testing Patterns

Packets destined to fail, on purpose — note PSLVERR starts at 0 in the packet and the slave is what sets it:

def create_error_test_packets():
    """Create packets for error testing"""
    packets = []

    # Valid transaction
    valid_packet = APBPacket(
        pwrite=1,
        paddr=0x100,
        pwdata=0x12345678,
        pstrb=0xF,
        pslverr=0
    )
    packets.append(valid_packet)

    # Transaction that should cause slave error
    error_packet = APBPacket(
        pwrite=1,
        paddr=0xDEAD,  # Invalid address
        pwdata=0xBAADF00D,
        pstrb=0xF,
        pslverr=0  # Will be set by slave
    )
    packets.append(error_packet)

    return packets

Strobe Pattern Testing

One address, one data word, every strobe combination you care about — byte-lane bugs have nowhere to hide:

def create_strobe_test_packets(base_addr):
    """Create packets testing different strobe patterns"""
    packets = []
    test_data = 0x12345678

    # Test individual byte strobes
    for byte_pos in range(4):
        strobe = 1 << byte_pos
        packet = APBPacket(
            pwrite=1,
            paddr=base_addr,
            pwdata=test_data,
            pstrb=strobe,
            count=len(packets)
        )
        packets.append(packet)

    # Test combinations
    strobe_patterns = [0x3, 0xC, 0x5, 0xA, 0x7, 0xE, 0xF]
    for pattern in strobe_patterns:
        packet = APBPacket(
            pwrite=1,
            paddr=base_addr + 4,
            pwdata=test_data,
            pstrb=pattern,
            count=len(packets)
        )
        packets.append(packet)

    return packets

Performance Testing

All writes, a tight address range, full strobes: maximum traffic, minimum randomness.

def create_performance_test_sequence(num_transactions):
    """Create high-performance test sequence"""
    # Use transaction generator for variety
    transaction = APBTransaction()

    # Override randomizer for performance testing
    perf_randomizer = FlexRandomizer({
        'pwrite': ([(1, 1)], [1]),  # All writes for performance
        'paddr': ([(0x1000, 0x2000)], [1]),  # Sequential addresses
        'pstrb': ([(0xF, 0xF)], [1]),  # Always full strobes
        'pprot': ([(0, 0)], [1])  # Normal protection
    })

    transaction.randomizer = perf_randomizer

    # Generate performance sequence
    packets = []
    for i in range(num_transactions):
        packet = transaction.next()
        packet.count = i
        packets.append(packet)

    return packets

Packet Validation

A small checker like this earns its keep in scoreboards and callbacks. The packet will happily hold a misaligned address — deciding whether that's legal is your test's job.

def validate_apb_packet(packet):
    """Validate APB packet for protocol compliance"""
    errors = []

    # Check required fields
    if packet.pwrite not in [0, 1]:
        errors.append("Invalid PWRITE value")

    # Address alignment check (assuming 32-bit data)
    if packet.paddr % 4 != 0:
        errors.append("Address not word-aligned")

    # Strobe validation for writes
    if packet.direction == 'WRITE':
        if packet.pstrb == 0:
            errors.append("No strobes active for write")

        # Check strobe-data consistency
        if packet.pstrb > 0xF:
            errors.append("Invalid strobe pattern")

    # Protection field validation
    if packet.pprot > 7:
        errors.append("Invalid protection value")

    return errors

# Validate packet
packet = APBPacket(pwrite=1, paddr=0x103, pwdata=0x123, pstrb=0xF)
validation_errors = validate_apb_packet(packet)
if validation_errors:
    print(f"Validation errors: {validation_errors}")

Integration with Test Framework

Testbench Integration

Packets go in via master.send(); the driver's queue does the rest.

import cocotb
from cocotb.triggers import RisingEdge
from CocoTBFramework.components.apb.apb_components import APBMaster
from CocoTBFramework.components.apb.apb_packet import APBPacket

@cocotb.test()
async def packet_driven_test(dut):
    # Create master
    master = APBMaster(dut, "Master", "apb_", dut.clk)

    # Create test packets
    packets = [
        APBPacket(pwrite=1, paddr=0x100, pwdata=0x11111111),
        APBPacket(pwrite=1, paddr=0x104, pwdata=0x22222222),
        APBPacket(pwrite=0, paddr=0x100),
        APBPacket(pwrite=0, paddr=0x104)
    ]

    # Send packets
    for packet in packets:
        await master.send(packet)
        print(f"Sent: {packet.formatted(compact=True)}")

    # Wait for completion
    while master.transfer_busy:
        await RisingEdge(dut.clk)

Scoreboard Integration

The simplest scoreboard that still catches real bugs: queue expected packets, compare on arrival, and let APBPacket.__eq__ do the field-level work.

class APBScoreboard:
    def __init__(self):
        self.expected_packets = []
        self.actual_packets = []

    def add_expected(self, packet):
        self.expected_packets.append(packet)

    def add_actual(self, packet):
        self.actual_packets.append(packet)
        self.check_transaction()

    def check_transaction(self):
        if len(self.actual_packets) <= len(self.expected_packets):
            expected = self.expected_packets[len(self.actual_packets) - 1]
            actual = self.actual_packets[-1]

            if expected == actual:
                print("✓ Transaction match")
            else:
                print(f"✗ Transaction mismatch:")
                print(f"  Expected: {expected.formatted(compact=True)}")
                print(f"  Actual:   {actual.formatted(compact=True)}")

Best Practices

1. Use Appropriate Field Widths

Match the packet to the DUT, not to the defaults:

# Match your DUT configuration
packet = APBPacket(
    data_width=32,     # Match DUT data width
    addr_width=16,     # Match DUT address width
    strb_width=4       # data_width / 8
)

2. Validate Packets Before Use

Cheap asserts, run early, save afternoons:

# Always validate critical fields
assert packet.direction in ['READ', 'WRITE']
assert packet.paddr % 4 == 0  # Word alignment
if packet.direction == 'WRITE':
    assert packet.pstrb > 0  # At least one strobe

3. Use Transactions for Random Testing

Directed packets for the bugs you can imagine; APBTransaction for the ones you can't.

# Use transaction generators for stimulus
transaction = APBTransaction(randomizer=custom_randomizer)
for _ in range(1000):
    packet = transaction.next()
    # Send to DUT

4. Handle Timing Information

Stamp start_time/end_time around the transfer if you want latency numbers later:

# Set timing for performance analysis
import cocotb
packet.start_time = cocotb.utils.get_sim_time('ns')
# ... send packet ...
packet.end_time = cocotb.utils.get_sim_time('ns')
duration = packet.end_time - packet.start_time

5. Use Compact Format for Logging

Your future self, scrolling a log at midnight, will thank you.

# Use compact format in loops
for packet in packet_sequence:
    print(f"Processing: {packet.formatted(compact=True)}")

That's the packet layer. apb_components.py shows what consumes these objects; apb_sequence.py shows how to generate them in bulk.