|
CocoTB Framework · Verification Infrastructure for RTL Testing GitHub · Documentation Index · MIT License |
← Back to Components Index | Main Index
UART BFM Components¶
Package: src/CocoTBFramework/components/uart/
Last Updated: 2025-11-09
Overview¶
The UART BFM (Bus Functional Model) package provides CocoTB-based verification components for UART protocol testing. These components implement the standard 8N1 UART protocol (8 data bits, no parity, 1 stop bit) with configurable baud rates.
Package Contents¶
| Component | Purpose | Direction |
|---|---|---|
| UARTMaster | UART transmitter | TX (sends data) |
| UARTMonitor | UART receiver monitor | RX (captures data) |
| UARTSlave | UART responder | RX/TX (echo, respond) |
UARTMaster¶
Purpose¶
Transmits UART data for stimulating DUT UART receivers. Used to drive commands and test data into UART-based designs.
Features¶
- Configurable baud rate (via
clks_per_bit) - Automatic start/stop bit generation
- String and byte transmission
- Non-blocking async transmission
- Transaction logging
Usage¶
from CocoTBFramework.components.uart import UARTMaster
class MyTestbench(TBBase):
def __init__(self, dut):
super().__init__(dut)
# Initialize UART master
self.uart_tx = UARTMaster(
entity=dut,
title="UART_TX",
signal_name="i_uart_rx", # DUT input signal name
clock=dut.aclk,
clks_per_bit=868, # 100 MHz / 115200 baud
direction='TX',
log=self.log
)
async def send_command(self, cmd):
"""Send UART command string"""
await self.uart_tx.send_string(cmd)
async def send_byte(self, byte_val):
"""Send single byte"""
await self.uart_tx.send_byte(byte_val)
API¶
Constructor¶
UARTMaster(
entity, # CocoTB DUT entity
title, # String for logging (e.g., "UART_TX")
signal_name, # DUT signal name (e.g., "i_uart_rx")
clock, # Clock signal handle
clks_per_bit, # Clocks per UART bit
direction='TX', # Direction string (for logging)
log=None # Logger instance (optional)
)
Methods¶
async send_byte(data: int)
- Transmits single byte over UART
- data: 8-bit value (0-255)
- Automatically adds start/stop bits
- Non-blocking (awaitable)
async send_string(text: str)
- Transmits ASCII string over UART
- text: String to transmit
- Sends each character sequentially
- Non-blocking (awaitable)
Timing¶
Per-Byte Transmission Time:
T_byte = clks_per_bit * 10 clock cycles
= (1 start) + (8 data) + (1 stop) bits
Example (115200 baud, 100 MHz clock):
clks_per_bit = 868
T_byte = 868 * 10 = 8680 clocks = 86.8 µs
Example¶
@cocotb.test()
async def test_uart_commands(dut):
tb = UARTBridgeTB(dut)
await tb.setup_clocks_and_reset()
# Send write command
await tb.uart_tx.send_string("W 1000 DEADBEEF\n")
# Wait for response
await tb.wait_clocks('clk', 100000)
# Check UART response via monitor
# (see UARTMonitor section)
UARTMonitor¶
Purpose¶
Monitors UART output from DUT transmitters. Captures transmitted data for verification and analysis.
Features¶
- Configurable baud rate (via
clks_per_bit) - Automatic start/stop bit detection
- Queue-based transaction capture (
_recvQ) - Transaction logging
- Data validation
Usage¶
from CocoTBFramework.components.uart import UARTMonitor
class MyTestbench(TBBase):
def __init__(self, dut):
super().__init__(dut)
# Initialize UART monitor
self.uart_rx_monitor = UARTMonitor(
entity=dut,
title="UART_RX_MON",
signal_name="o_uart_tx", # DUT output signal name
clock=dut.aclk,
clks_per_bit=868, # 100 MHz / 115200 baud
direction='RX',
log=self.log
)
async def check_response(self, expected_str):
"""Verify UART response"""
# Wait for data
await self.wait_clocks('clk', 10000)
# Check queue
if len(self.uart_rx_monitor._recvQ) >= len(expected_str):
received = ""
for _ in range(len(expected_str)):
pkt = self.uart_rx_monitor._recvQ.popleft()
received += chr(pkt.data)
assert received == expected_str, f"Expected '{expected_str}', got '{received}'"
return True
return False
API¶
Constructor¶
UARTMonitor(
entity, # CocoTB DUT entity
title, # String for logging (e.g., "UART_RX_MON")
signal_name, # DUT signal name (e.g., "o_uart_tx")
clock, # Clock signal handle
clks_per_bit, # Clocks per UART bit
direction='RX', # Direction string (for logging)
log=None # Logger instance (optional)
)
Attributes¶
_recvQ - collections.deque
- Queue of received UART packets
- Access with .popleft() to retrieve oldest packet
- Each packet is a UARTPacket object
UARTPacket Structure:
Methods¶
Monitor runs automatically in background. Access received data via _recvQ.
Common Patterns:
# Check if data available
if len(self.uart_rx_monitor._recvQ) > 0:
pkt = self.uart_rx_monitor._recvQ.popleft()
byte_val = pkt.data
timestamp = pkt.timestamp
# Clear queue
self.uart_rx_monitor._recvQ.clear()
# Collect string response
response = ""
while len(self.uart_rx_monitor._recvQ) > 0:
pkt = self.uart_rx_monitor._recvQ.popleft()
response += chr(pkt.data)
Example¶
@cocotb.test()
async def test_uart_echo(dut):
tb = UARTTestbench(dut)
await tb.setup_clocks_and_reset()
# Clear any stale data
tb.uart_rx_monitor._recvQ.clear()
# Send command
await tb.uart_tx.send_string("HELLO\n")
# Wait for response
await tb.wait_clocks('clk', 50000)
# Collect response
response = ""
while len(tb.uart_rx_monitor._recvQ) > 0:
pkt = tb.uart_rx_monitor._recvQ.popleft()
response += chr(pkt.data)
tb.log.debug(f"Received: 0x{pkt.data:02X} ({chr(pkt.data)})")
# Verify
assert response == "HELLO\n", f"Echo failed: {response}"
UARTSlave¶
Purpose¶
Simulates UART slave device that can respond to received commands. Useful for testing UART masters.
Features¶
- Receives commands via UART
- Generates responses
- Configurable response patterns
- Automatic echo mode
- Custom response callbacks
Usage¶
from CocoTBFramework.components.uart import UARTSlave
class MyTestbench(TBBase):
def __init__(self, dut):
super().__init__(dut)
# Initialize UART slave
self.uart_slave = UARTSlave(
entity=dut,
title="UART_SLAVE",
rx_signal_name="o_uart_tx", # Receive from DUT TX
tx_signal_name="i_uart_rx", # Transmit to DUT RX
clock=dut.aclk,
clks_per_bit=868,
log=self.log,
echo_mode=True # Echo received bytes
)
async def setup_custom_response(self):
"""Configure custom response callback"""
async def custom_handler(byte_val):
if byte_val == ord('?'):
return "READY\n"
elif byte_val == ord('V'):
return "VERSION 1.0\n"
return None # No response
self.uart_slave.set_response_handler(custom_handler)
API¶
Constructor¶
UARTSlave(
entity, # CocoTB DUT entity
title, # String for logging
rx_signal_name, # DUT TX signal name (slave receives from)
tx_signal_name, # DUT RX signal name (slave transmits to)
clock, # Clock signal handle
clks_per_bit, # Clocks per UART bit
log=None, # Logger instance
echo_mode=False # Echo received bytes back
)
Methods¶
set_response_handler(callback)
- Set custom response callback
- callback: async function(byte) -> str or None
- Called for each received byte
- Return string to transmit response, None for no response
enable_echo_mode()
- Enable automatic echo of received bytes
disable_echo_mode()
- Disable automatic echo
Example¶
@cocotb.test()
async def test_uart_slave(dut):
tb = UARTTestbench(dut)
await tb.setup_clocks_and_reset()
# Configure slave responses
async def command_handler(byte_val):
if byte_val == ord('R'):
return "READ_OK\n"
elif byte_val == ord('W'):
return "WRITE_OK\n"
return None
tb.uart_slave.set_response_handler(command_handler)
# Master sends command
# Slave automatically responds based on handler
Protocol Details¶
UART 8N1 Protocol¶
Frame Format:
{ signal: [
{ name: "UART Frame", wave: "x0.2345678.1x", data: ["Start","D0","D1","D2","D3","D4","D5","D6","D7","Stop"] }
],
head: { text: "UART 8N1 Frame: Start (0) + 8 Data (LSB first) + Stop (1) = 10 bits" }
}
| Bit | Name | Description |
|---|---|---|
| 1 | Start | Always 0 |
| 2-9 | D0-D7 | Data (LSB first) |
| 10 | Stop | Always 1 |
Total: 10 bits per byte
Bit Timing:
Bit Duration = clks_per_bit clock cycles
Baud Rate = Clock_Frequency / clks_per_bit
Common Baud Rates:
- 9600: clks_per_bit = 10417 (100 MHz clock)
- 115200: clks_per_bit = 868 (100 MHz clock)
- 230400: clks_per_bit = 434 (100 MHz clock)
Timing Constraints¶
Minimum Requirements: - Clock frequency >> baud rate (at least 16x recommended) - Stable clock during transmission - Proper CDC for async UART inputs
Typical Timing: | Baud Rate | Bit Time | Byte Time | |-----------|----------|-----------| | 9600 | 104.2 µs | 1.042 ms | | 115200 | 8.68 µs | 86.8 µs | | 230400 | 4.34 µs | 43.4 µs |
Integration Examples¶
Complete UART Bridge Testbench¶
# TBBase is located in the RTLDesignSherpa main repo (tbclasses/shared/tbbase.py)
from CocoTBFramework.tbclasses.shared.tbbase import TBBase
from CocoTBFramework.components.uart import UARTMaster, UARTMonitor
class UARTBridgeTB(TBBase):
"""Testbench for UART to AXI4-Lite bridge"""
def __init__(self, dut):
super().__init__(dut)
# UART master (sends commands to bridge)
self.uart_tx = UARTMaster(
entity=dut,
title="UART_TX",
signal_name="i_uart_rx",
clock=dut.aclk,
clks_per_bit=868,
direction='TX',
log=self.log
)
# UART monitor (captures responses from bridge)
self.uart_rx_monitor = UARTMonitor(
entity=dut,
title="UART_RX_MON",
signal_name="o_uart_tx",
clock=dut.aclk,
clks_per_bit=868,
direction='RX',
log=self.log
)
async def send_write_command(self, addr, data):
"""Send UART write command"""
cmd = f"W {addr:X} {data:X}\n"
self.uart_rx_monitor._recvQ.clear()
await self.uart_tx.send_string(cmd)
# Wait for response
await self.wait_clocks('clk', 200000)
# Check for "OK\n"
if len(self.uart_rx_monitor._recvQ) >= 3:
response = ""
for _ in range(3):
pkt = self.uart_rx_monitor._recvQ.popleft()
response += chr(pkt.data)
return response == "OK\n"
return False
async def send_read_command(self, addr):
"""Send UART read command"""
cmd = f"R {addr:X}\n"
self.uart_rx_monitor._recvQ.clear()
await self.uart_tx.send_string(cmd)
# Wait for response
await self.wait_clocks('clk', 200000)
# Parse "0x<hex>\n"
if len(self.uart_rx_monitor._recvQ) >= 11:
response = ""
for _ in range(11):
pkt = self.uart_rx_monitor._recvQ.popleft()
response += chr(pkt.data)
if response.startswith("0x") and response.endswith("\n"):
data_hex = response[2:-1]
return int(data_hex, 16)
return None
Testing UART Components¶
Unit Tests¶
Located in: tests/ directory
Test Coverage: - Byte transmission accuracy - Start/stop bit generation - Baud rate timing - String transmission - Monitor capture accuracy - Queue management
Running Tests¶
Design Notes¶
Clock Domain Crossing¶
UART inputs are asynchronous and require CDC:
- Use 2-FF synchronizer for RX input
- Implemented in UART RX modules (e.g., uart_rx.sv)
- BFM assumes single clock domain (testbench synchronous)
Baud Rate Calculation¶
def calculate_clks_per_bit(clock_mhz, baud_rate):
"""Calculate clks_per_bit parameter"""
clock_hz = clock_mhz * 1_000_000
return int(clock_hz / baud_rate)
# Examples
clks_per_bit_100mhz_115200 = calculate_clks_per_bit(100, 115200) # 868
clks_per_bit_50mhz_115200 = calculate_clks_per_bit(50, 115200) # 434
Performance Considerations¶
Testbench Performance: - UART is slow - expect long test times - 115200 baud ≈ 11.5 KB/s max throughput - Use higher baud rates for faster tests (if DUT supports) - Consider parallel testing for throughput-intensive tests
Simulation Optimization:
# For faster testing, use higher baud rate
FAST_CLKS_PER_BIT = 100 # ~1 Mbaud at 100 MHz
# Or skip UART BFM for bulk data
# Use direct AXI4-Lite transaction injection
Known Issues¶
None currently documented.
Future Enhancements¶
- Parity Support - 8E1, 8O1 modes
- Flow Control - RTS/CTS hardware handshaking
- Break Detection - Extended low period detection
- Framing Error Detection - Invalid stop bit detection
- Configurable Stop Bits - 1, 1.5, 2 stop bits
References¶
Internal:
- Converters Project - Usage example
- CocoTB Framework Overview
- TBBase (located in the RTLDesignSherpa repo under tbclasses/shared/tbbase.py)
External: - UART Wikipedia - CocoTB Documentation
Version: 1.0 Last Review: 2025-11-09 Maintained By: RTL Design Sherpa Project