Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,17 @@ jobs:
dockerfile: .devcontainer/Dockerfile
tag-prefix: ""
secrets: inherit

svlint:
needs: select_ci_image
if: always() # dont skip if build_ci_image is skipped
runs-on: ubuntu-latest
container:
image: ${{ needs.select_ci_image.outputs.image-tag }}

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: lint uart
run: cd uart && make svlint
84 changes: 84 additions & 0 deletions .svlint.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@

# Style ruleset begin

option.textwidth = 100
option.indent = 2
option.exclude_paths = [
"test/utils.svh",
] # this file contains a complex macro that svlint chokes on

# textrules.style_textwidth = true
textrules.style_semicolon = true
syntaxrules.tab_character = true
syntaxrules.style_indent = true
syntaxrules.multiline_if_begin = false
syntaxrules.multiline_for_begin = true
syntaxrules.style_trailingwhitespace = true
textrules.style_directives = true
syntaxrules.style_operator_arithmetic = true
syntaxrules.style_operator_boolean = false
syntaxrules.style_operator_integer = true
syntaxrules.style_operator_unary = true
syntaxrules.style_operator_arithmetic_leading_space = true
syntaxrules.style_operator_boolean_leading_space = true
syntaxrules.style_operator_integer_leading_space = true

syntaxrules.style_keyword_0or1space = true
syntaxrules.style_keyword_0space = true
syntaxrules.style_keyword_1or2space = true
syntaxrules.style_keyword_1space = true
syntaxrules.style_keyword_construct = false
syntaxrules.style_keyword_datatype = false # overly restrictive.
syntaxrules.style_keyword_end = true
syntaxrules.style_keyword_maybelabel = true
syntaxrules.style_keyword_new = true
syntaxrules.style_keyword_newline = true
syntaxrules.style_commaleading = true
syntaxrules.eventlist_or = true

# Style ruleset end

# from designintents.toml
syntaxrules.blocking_assignment_in_always_ff = true
syntaxrules.blocking_assignment_in_always_latch = true
syntaxrules.non_blocking_assignment_in_always_comb = true
syntaxrules.case_default = true
syntaxrules.enum_with_type = true
syntaxrules.function_with_automatic = true
syntaxrules.keyword_forbidden_priority = true
syntaxrules.keyword_forbidden_unique = true
syntaxrules.keyword_forbidden_unique0 = true
# TODO syntaxrules.operator_case_equality = true
syntaxrules.procedural_continuous_assignment = true
syntaxrules.action_block_with_side_effect = true
# TODO syntaxrules.default_nettype_none = true
syntaxrules.function_same_as_system_function = true
# TODO syntaxrules.keyword_forbidden_always = true
# TODO syntaxrules.keyword_forbidden_wire_reg = true
syntaxrules.module_nonansi_forbidden = true
syntaxrules.generate_case_with_label = true
syntaxrules.generate_for_with_label = true
syntaxrules.generate_if_with_label = true
syntaxrules.localparam_type_twostate = true
syntaxrules.parameter_type_twostate = true
syntaxrules.localparam_explicit_type = true
# TODO syntaxrules.parameter_explicit_type = true
syntaxrules.parameter_default_value = true
syntaxrules.parameter_in_generate = true
syntaxrules.parameter_in_package = true
syntaxrules.genvar_declaration_in_loop = true
syntaxrules.genvar_declaration_out_loop = false
syntaxrules.keyword_forbidden_generate = true
syntaxrules.keyword_required_generate = false
# TODO syntaxrules.explicit_case_default = true
# TODO syntaxrules.explicit_if_else = true
syntaxrules.loop_statement_in_always_comb = true
syntaxrules.loop_statement_in_always_ff = true
syntaxrules.loop_statement_in_always_latch = true
syntaxrules.sequential_block_in_always_comb = true
syntaxrules.sequential_block_in_always_ff = true
syntaxrules.sequential_block_in_always_latch = true
syntaxrules.inout_with_tri = true
# TODO syntaxrules.input_with_var = true
# TODO syntaxrules.output_with_var = true
syntaxrules.interface_port_with_modport = true
7 changes: 7 additions & 0 deletions uart/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
dv/__pycache__
sim_build
sw/__pycache__
results.xml

*.vcd
*.fst
35 changes: 35 additions & 0 deletions uart/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
SIM = verilator
TOPLEVEL_LANG = verilog
VERILOG_SOURCES = \
$(CURDIR)/rtl/uart_rx.sv \
$(CURDIR)/rtl/uart_tx.sv \
$(CURDIR)/rtl/uart.sv

SVLINT_SOURCES = $(wildcard $(CURDIR)/rtl/*.sv)

TOPLEVEL = uart
MODULE = test_uart_pyuvm

WAVES = 1
EXTRA_ARGS += --timing --trace --trace-structs -Wno-PINMISSING -Wno-WIDTHEXPAND
PLUSARGS += --trace --trace-file dv/dump.vcd

PYTHONPATH := $(CURDIR)/dv:$(CURDIR)/sw:$(PYTHONPATH)
export PYTHONPATH

include $(shell cocotb-config --makefiles)/Makefile.sim

# Post-simulation VCD to FST conversion
post_waves: sim
@if [ -f dv/dump.vcd ]; then \
vcd2fst dv/dump.vcd dv/dump.fst; \
echo "Generated dv/dump.fst ($$(du -h dv/dump.fst | cut -f1))"; \
fi

# ===========================
# Linting
# ===========================
svlint:
bash -o pipefail -c 'svlint $(if $(CI),--github-actions) $(SVLINT_SOURCES) $(if $(CI),| sed "s/::error/::warning/g")'

.PHONY: svlint post_waves
26 changes: 26 additions & 0 deletions uart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# How to test uart

## Requirements:

Python 3.9+
cocotb — pip install cocotb
cocotbext-uart — pip install cocotbext-uart
Icarus Verilog — brew install icarus-verilog on Mac

In the uart/ folder:

## to run all tests:

make SIM=icarus

## to run a single test:

make SIM=icarus COCOTB_TEST_FILTER=test_halt

## What each test does:

test_halt — sends HALT, verifies the DUT asserts hold_core
test_wr32 — sends HALT then writes a word, verifies correct address and data appear on the bus
test_load_and_run — full CLI flow: halt, load 20 program words, run. Mirrors cli option 1 in uart_cli.py. Takes ~30 seconds due to real baud-rate timing.
test_rdreg — reads back a register value through the debug interface
test_bad_checksum — verifies the DUT rejects a corrupted packet with STATUS_CHK
217 changes: 217 additions & 0 deletions uart/dv/test_uart_pyuvm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import cocotb
from cocotb.triggers import RisingEdge, FallingEdge, Timer
from cocotb.clock import Clock
import pyuvm
from pyuvm import *
import random


# -----------------------------------------------------------------------------
# 1. Sequence Item
# -----------------------------------------------------------------------------
class UartItem(uvm_sequence_item):
def __init__(self, name="UartItem", data=0):
super().__init__(name)
self.data = data

def __str__(self):
return f"UartItem(data=0x{self.data:02X})"


# -----------------------------------------------------------------------------
# 2. Bus Functional Model (BFM)
# -----------------------------------------------------------------------------
class UartBFM:
def __init__(self, dut):
self.dut = dut
self.rx_listeners = []

async def reset(self):
self.dut.rst.value = 1
self.dut.i_valid_s.value = 0
self.dut.i_data_s.value = 0
self.dut.i_ready_m.value = 1
self.dut.i_rxd.value = 1
await Timer(100, units="ns")
await RisingEdge(self.dut.clk)
self.dut.rst.value = 0
await RisingEdge(self.dut.clk)

async def send_tx_byte(self, byte_val):
while not self.dut.o_ready_s.value:
await RisingEdge(self.dut.clk)

self.dut.i_data_s.value = byte_val
self.dut.i_valid_s.value = 1
await RisingEdge(self.dut.clk)
self.dut.i_valid_s.value = 0

while self.dut.o_ready_s.value:
await RisingEdge(self.dut.clk)

async def monitor_rx(self):
while True:
await RisingEdge(self.dut.clk)
if self.dut.o_valid_m.value and self.dut.i_ready_m.value:
received_byte = int(self.dut.o_data_m.value)
for listener in self.rx_listeners:
listener(received_byte)

async def loopback_wire(self):
"""Connect o_txd directly to i_rxd on falling clock edges for stability."""
while True:
await FallingEdge(self.dut.clk)
self.dut.i_rxd.value = self.dut.o_txd.value


# -----------------------------------------------------------------------------
# 3. Driver
# -----------------------------------------------------------------------------
class UartDriver(uvm_driver):
def build_phase(self):
self.ap = uvm_analysis_port("ap", self)
self.bfm = ConfigDB().get(self, "", "BFM")

async def run_phase(self):
while True:
item = await self.seq_item_port.get_next_item()
self.logger.info(f"[TX DRIVER] Transmitting byte: 0x{item.data:02X}")
await self.bfm.send_tx_byte(item.data)
self.ap.write(item) # Publish EXPECTED item via TLM Analysis Port
self.seq_item_port.item_done()


# -----------------------------------------------------------------------------
# 4. Monitor
# -----------------------------------------------------------------------------
class UartMonitor(uvm_monitor):
def build_phase(self):
self.ap = uvm_analysis_port("ap", self)
self.bfm = ConfigDB().get(self, "", "BFM")

def connect_phase(self):
self.bfm.rx_listeners.append(self.on_rx_byte)

def on_rx_byte(self, byte_val):
self.logger.info(f"[RX MONITOR] Sampled byte: 0x{byte_val:02X}")
item = UartItem("rx_item", data=byte_val)
self.ap.write(item) # Publish ACTUAL item via TLM Analysis Port


# -----------------------------------------------------------------------------
# 5. Scoreboard (Canonical Dual TLM FIFO)
# -----------------------------------------------------------------------------
class UartScoreboard(uvm_scoreboard):
def build_phase(self):
self.expected_fifo = uvm_tlm_analysis_fifo("expected_fifo", self)
self.actual_fifo = uvm_tlm_analysis_fifo("actual_fifo", self)

self.expected_export = self.expected_fifo.analysis_export
self.actual_export = self.actual_fifo.analysis_export

self.passed_count = 0
self.failed_count = 0

def check_phase(self):
while self.expected_fifo.can_get() and self.actual_fifo.can_get():
_, exp_item = self.expected_fifo.try_get()
_, act_item = self.actual_fifo.try_get()

if exp_item.data == act_item.data:
self.logger.info(f"[SCOREBOARD PASS] Expected=0x{exp_item.data:02X}, Received=0x{act_item.data:02X}")
self.passed_count += 1
else:
self.logger.error(f"[SCOREBOARD FAIL] Expected=0x{exp_item.data:02X}, Got=0x{act_item.data:02X}")
self.failed_count += 1

if self.expected_fifo.can_get():
self.logger.error(f"[SCOREBOARD FAIL] Unmatched expected bytes remaining in FIFO!")
self.failed_count += 1

if self.actual_fifo.can_get():
self.logger.error(f"[SCOREBOARD FAIL] Unmatched actual bytes remaining in FIFO!")
self.failed_count += 1

if self.failed_count == 0:
self.logger.info(f"[SCOREBOARD SUMMARY] All {self.passed_count} items matched successfully!")


# -----------------------------------------------------------------------------
# 6. Agent
# -----------------------------------------------------------------------------
class UartAgent(uvm_agent):
def build_phase(self):
self.driver = UartDriver("driver", self)
self.sequencer = uvm_sequencer("sequencer", self)
self.monitor = UartMonitor("monitor", self)

def connect_phase(self):
self.driver.seq_item_port.connect(self.sequencer.seq_item_export)


# -----------------------------------------------------------------------------
# 7. Environment
# -----------------------------------------------------------------------------
class UartEnv(uvm_env):
def build_phase(self):
self.agent = UartAgent("agent", self)
self.scoreboard = UartScoreboard("scoreboard", self)

def connect_phase(self):
# Canonical UVM Connections:
# 1. Driver ap -> Scoreboard expected_export
self.agent.driver.ap.connect(self.scoreboard.expected_export)
# 2. Monitor ap -> Scoreboard actual_export
self.agent.monitor.ap.connect(self.scoreboard.actual_export)


# -----------------------------------------------------------------------------
# 8. Sequence
# -----------------------------------------------------------------------------
class UartRandomSequence(uvm_sequence):
def __init__(self, name="UartRandomSequence", num_items=10):
super().__init__(name)
self.num_items = num_items

async def body(self):
for _ in range(self.num_items):
val = random.randint(0, 255)
item = UartItem("item", data=val)
await self.start_item(item)
await self.finish_item(item)


# -----------------------------------------------------------------------------
# 9. Test
# -----------------------------------------------------------------------------
@pyuvm.test()
class UartLoopbackTest(uvm_test):
def build_phase(self):
self.env = UartEnv("env", self)
self.bfm = UartBFM(cocotb.top)
ConfigDB().set(self, "*", "BFM", self.bfm)

async def run_phase(self):
self.raise_objection()

# Start clock
cocotb.start_soon(Clock(cocotb.top.clk, 20, units="ns").start())

# Start BFM background monitors (loopback wiring & RX monitor)
cocotb.start_soon(self.bfm.loopback_wire())
cocotb.start_soon(self.bfm.monitor_rx())

# Reset DUT
await self.bfm.reset()

# Run Sequence
seq = UartRandomSequence("random_seq", num_items=5)
await seq.start(self.agent_sequencer())

# Wait for last byte to transmit & receive over serial link
await Timer(600, units="us")

self.drop_objection()

def agent_sequencer(self):
return self.env.agent.sequencer
Loading
Loading