From 3ee1a4eba7f33d20b61edbcce97086c8a7f12f8b Mon Sep 17 00:00:00 2001 From: Sebastien Baillou Date: Wed, 19 Nov 2025 00:27:00 +0100 Subject: [PATCH] feat: broadcast read/wwrite feature to registers and regfiles feat: broadcast_write to arrays feat: broadcast_write to regfiles refactor: simplified broadcast scanner refactor: simplified broadcasters for target refactor: simplified broadcasters for target using inst instead of path refactor: improved broadcasters for target for arrays test: add test_broadcast_regfiles fix: remove field_storage for broadcaster refactor: simplify broadcast address decoding logic fix: remove decoding strobe for broadcasters fix: interaction between CPU error response and broadcast refactor: minor clean-up or broadcast UDP validation refactor: rename broadcast_write UDP to broadcast, since it supports read refactor: move a section of the template back to where it belongs lint: pylint fixes lint: mypy fixes refactor: remove unnecessary expand_target function --- hdl-src/regblock_udps.rdl | 5 + src/peakrdl_regblock/addr_decode.py | 49 ++++- src/peakrdl_regblock/broadcast/__init__.py | 184 ++++++++++++++++++ src/peakrdl_regblock/exporter.py | 4 + .../field_logic/generators.py | 15 ++ src/peakrdl_regblock/hwif/generators.py | 10 + src/peakrdl_regblock/readback/generators.py | 4 + src/peakrdl_regblock/udps/__init__.py | 2 + src/peakrdl_regblock/udps/broadcast.py | 84 ++++++++ tests/test_broadcast_arrays/__init__.py | 1 + tests/test_broadcast_arrays/regblock.rdl | 19 ++ tests/test_broadcast_arrays/tb_template.sv | 51 +++++ tests/test_broadcast_arrays/testcase.py | 6 + tests/test_broadcast_basic/__init__.py | 0 tests/test_broadcast_basic/regblock.rdl | 21 ++ tests/test_broadcast_basic/tb_template.sv | 45 +++++ tests/test_broadcast_basic/testcase.py | 6 + tests/test_broadcast_regfiles/__init__.py | 1 + tests/test_broadcast_regfiles/regblock.rdl | 38 ++++ tests/test_broadcast_regfiles/tb_template.sv | 75 +++++++ tests/test_broadcast_regfiles/testcase.py | 6 + tests/test_cpuif_err_rsp/regblock.rdl | 34 ++++ tests/test_cpuif_err_rsp/tb_template.sv | 61 ++++++ 23 files changed, 715 insertions(+), 6 deletions(-) create mode 100644 src/peakrdl_regblock/broadcast/__init__.py create mode 100644 src/peakrdl_regblock/udps/broadcast.py create mode 100644 tests/test_broadcast_arrays/__init__.py create mode 100644 tests/test_broadcast_arrays/regblock.rdl create mode 100644 tests/test_broadcast_arrays/tb_template.sv create mode 100644 tests/test_broadcast_arrays/testcase.py create mode 100644 tests/test_broadcast_basic/__init__.py create mode 100644 tests/test_broadcast_basic/regblock.rdl create mode 100644 tests/test_broadcast_basic/tb_template.sv create mode 100644 tests/test_broadcast_basic/testcase.py create mode 100644 tests/test_broadcast_regfiles/__init__.py create mode 100644 tests/test_broadcast_regfiles/regblock.rdl create mode 100644 tests/test_broadcast_regfiles/tb_template.sv create mode 100644 tests/test_broadcast_regfiles/testcase.py diff --git a/hdl-src/regblock_udps.rdl b/hdl-src/regblock_udps.rdl index f6ded7bd..6f395a93 100644 --- a/hdl-src/regblock_udps.rdl +++ b/hdl-src/regblock_udps.rdl @@ -52,3 +52,8 @@ property fracwidth { type = longint unsigned; component = field; }; + +property broadcast { + type = ref[]; + component = reg | regfile; +}; diff --git a/src/peakrdl_regblock/addr_decode.py b/src/peakrdl_regblock/addr_decode.py index 7617a447..7597cc70 100644 --- a/src/peakrdl_regblock/addr_decode.py +++ b/src/peakrdl_regblock/addr_decode.py @@ -201,20 +201,53 @@ def _add_reg_decoding_flags(self, subword_index: Union[int, None] = None, subword_stride: Union[int, None] = None) -> None: if subword_index is None or subword_stride is None: - addr_decoding_str = f"cpuif_req_masked & (cpuif_addr == {self._get_address_str(node)})" + addr_decoding_str = f"(cpuif_addr == {self._get_address_str(node)})" else: - addr_decoding_str = f"cpuif_req_masked & (cpuif_addr == {self._get_address_str(node, subword_offset=subword_index*subword_stride)})" - rhs_valid_addr = addr_decoding_str + addr_decoding_str = f"(cpuif_addr == {self._get_address_str(node, subword_offset=subword_index*subword_stride)})" + + # Check if this register is a broadcast target + # If so, OR in the broadcaster's address decode + broadcasters = self.addr_decode.exp.broadcast_logic.get_broadcasters_for_target(node) + + if broadcasters: + # For both reads and writes: OR in the broadcaster's address decode directly + # This eliminates the need for separate broadcast_wr_* wires + broadcaster_addr_terms = [] + + for broadcaster_node in broadcasters: + expr_width = self.addr_decode.exp.ds.addr_width + broadcaster_addr = broadcaster_node.raw_absolute_address - self.addr_decode.top_node.raw_absolute_address + broadcaster_addr_str = str(SVInt(broadcaster_addr, expr_width)) + + # Add broadcaster's own array offsets if it's in an array + if broadcaster_node.is_array: + # Calculate which loop indices apply to the broadcaster + # We need to figure out the broadcaster's position in the loop hierarchy + # For now, assume broadcasters are not in arrays (simplification) + # TODO: Handle array broadcasters properly + pass + + broadcaster_addr_terms.append(f"(cpuif_addr == {broadcaster_addr_str})") + + # OR all broadcaster addresses into the main address decode + if broadcaster_addr_terms: + broadcaster_addrs = " | ".join(broadcaster_addr_terms) + # Modify addr_decoding_str to include broadcaster addresses + # The original rhs already has the direct address check, we just OR in the broadcaster addresses + addr_decoding_str = f"({addr_decoding_str} | {broadcaster_addrs})" + + rhs_valid_addr = f"cpuif_req_masked & {addr_decoding_str}" readable = node.has_sw_readable writable = node.has_sw_writable if readable and writable: - rhs = addr_decoding_str + rhs = f"cpuif_req_masked & {addr_decoding_str}" elif readable and not writable: - rhs = f"{addr_decoding_str} & !cpuif_req_is_wr" + rhs = f"cpuif_req_masked & {addr_decoding_str} & !cpuif_req_is_wr" elif not readable and writable: - rhs = f"{addr_decoding_str} & cpuif_req_is_wr" + rhs = f"cpuif_req_masked & {addr_decoding_str} & cpuif_req_is_wr" else: raise RuntimeError + # Add decoding flags if subword_index is None: self.add_content(f"{self.addr_decode.get_access_strobe(node)} = {rhs};") @@ -233,6 +266,10 @@ def enter_Reg(self, node: RegNode) -> None: regwidth = node.get_property('regwidth') accesswidth = node.get_property('accesswidth') + if self.addr_decode.exp.broadcast_logic.is_in_broadcast_scope(node): + # Register is within a broadcaster scope. Do not add decoding flags + return + if regwidth == accesswidth: self._add_reg_decoding_flags(node) else: diff --git a/src/peakrdl_regblock/broadcast/__init__.py b/src/peakrdl_regblock/broadcast/__init__.py new file mode 100644 index 00000000..af88b8de --- /dev/null +++ b/src/peakrdl_regblock/broadcast/__init__.py @@ -0,0 +1,184 @@ +from typing import TYPE_CHECKING, Dict, List, Tuple, Any, Optional +from collections import defaultdict + +from systemrdl.node import RegNode, RegfileNode, AddressableNode, Node +from systemrdl.walker import RDLListener, RDLWalker + +if TYPE_CHECKING: + from ..exporter import RegblockExporter + + +class BroadcastLogic: + """ + Manages broadcast logic for registers and regfiles. + + A broadcaster register/regfile does not have hardware storage. + Instead, writes to the broadcaster address generate write strobes + for all target registers. + """ + + def __init__(self, exp: 'RegblockExporter') -> None: + self.exp = exp + + # Maps broadcaster node to list of target nodes + # List of tuples: (broadcaster_node, [target_nodes]) + self.broadcast_map: List[Tuple[AddressableNode, List[RegNode]]] = [] + + # List of all broadcaster nodes (no hardware implementation) + self.broadcasters: List[AddressableNode] = [] + + # List of all target nodes (receive broadcast writes) + self.targets: List[RegNode] = [] + + # Scan the design to build the broadcast map + self._scan_design() + + def _scan_design(self) -> None: + """Scan the design to identify broadcasters and their targets""" + listener = BroadcastScanner(self) + RDLWalker(skip_not_present=False).walk(self.exp.ds.top_node, listener) + + def is_broadcaster(self, node: AddressableNode) -> bool: + """Check if a node is a broadcaster""" + return node in self.broadcasters + + def is_in_broadcast_scope(self, node: AddressableNode) -> bool: + """ + Check if a node is within a broadcast scope. + This includes: + 1. The node itself is a broadcaster + 2. The node is a child of a broadcaster (e.g. reg in a broadcast regfile) + """ + curr: Optional[Node] = node + while curr is not None: + if isinstance(curr, AddressableNode) and self.is_broadcaster(curr): + return True + curr = curr.parent + return False + + def is_target(self, node: RegNode) -> bool: + """Check if a node is a broadcast target""" + return node in self.targets + + def get_broadcasters_for_target(self, target: RegNode) -> List[AddressableNode]: + """ + Get all broadcaster nodes that write to this target. + """ + broadcasters: List[AddressableNode] = [] + + # Calculate the total number of instances this target represents + # (including its own array dimensions and any parent array dimensions) + expected_count = 1 + curr: Optional[Node] = target + while curr is not None: + if isinstance(curr, AddressableNode) and curr.is_array: + expected_count *= curr.n_elements + curr = curr.parent + + for broadcaster_node, targets in self.broadcast_map: + # 1. Exact match check + if target in targets: + broadcasters.append(broadcaster_node) + continue + + # 2. Array iteration match check + # If target is a canonical node inside an array loop, it won't be in the map. + # But its unrolled instances (sharing the same underlying Component 'inst') might be. + + # Count how many targets share the same underlying component instance + match_count = 0 + for t in targets: + if t.inst == target.inst: + # If current_idx is None, this node represents the entire array (all elements) + if t.current_idx is None: + match_count += t.n_elements + else: + # Otherwise it represents a single element + match_count += 1 + + if match_count == expected_count and expected_count > 0: + broadcasters.append(broadcaster_node) + + return broadcasters + + +class BroadcastScanner(RDLListener): + """Listener that scans the design to build the broadcast mapping""" + + def __init__(self, broadcast_logic: BroadcastLogic) -> None: + self.bl = broadcast_logic + # Stack of active broadcaster scopes: List[Tuple[BroadcasterNode, List[TargetNode]]] + self.scope_stack: List[Tuple[AddressableNode, List[RegfileNode]]] = [] + + def enter_Component(self, node: Node) -> None: + + # 1. Check if we are inside an active broadcaster scope (Structural Broadcast) + if self.scope_stack and isinstance(node, RegNode): + broadcaster_scope, target_scopes = self.scope_stack[-1] + + # Calculate relative path from the broadcaster scope + b_path = broadcaster_scope.get_path() + node_path = node.get_path() + + if node_path.startswith(b_path + '.'): + rel_path = node_path[len(b_path)+1:] + + # Map to each target scope + for target_scope in target_scopes: + # Find corresponding node in target + target_reg = target_scope.find_by_path(rel_path) + + if target_reg and isinstance(target_reg, RegNode): + self._add_broadcast_map(node, target_reg) + + # 2. Check if this node defines a new broadcast (is a Broadcaster) + if isinstance(node, (RegNode, RegfileNode)): + broadcast_targets = node.get_property('broadcast') + + if broadcast_targets is not None: + self.bl.broadcasters.append(node) + + if isinstance(node, RegfileNode): + # Structural Broadcast: Push to stack to handle children + # Filter targets to only RegfileNodes (validation ensures they are compatible) + rf_targets = [t for t in broadcast_targets if isinstance(t, RegfileNode)] + if rf_targets: + self.scope_stack.append((node, rf_targets)) + + elif isinstance(node, RegNode): + # Direct Register Broadcast: Map immediately + for target in broadcast_targets: + if isinstance(target, RegNode): + self._add_broadcast_map(node, target) + + def exit_Component(self, node: Node) -> None: + # Pop scope if we are exiting the current broadcaster scope + if self.scope_stack and self.scope_stack[-1][0] == node: + self.scope_stack.pop() + + def _add_broadcast_map(self, broadcaster: AddressableNode, target: RegNode) -> None: + """Helper to add a broadcaster -> target mapping""" + + # Find existing entry for this broadcaster + found = False + for b_node, t_list in self.bl.broadcast_map: + if b_node == broadcaster: + if target not in t_list: + t_list.append(target) + found = True + break + + if not found: + self.bl.broadcast_map.append((broadcaster, [target])) + + # Also track in global targets list + if target not in self.bl.targets: + self.bl.targets.append(target) + + def _expand_array(self, node: AddressableNode) -> List[AddressableNode]: + """ + For broadcast targets, we expect explicit array indexing in the RDL. + Just return the node as-is since it's already the specific element. + """ + # Simply return the node - RDL references should already be explicit + return [node] diff --git a/src/peakrdl_regblock/exporter.py b/src/peakrdl_regblock/exporter.py index ba4d2d6f..821b7796 100644 --- a/src/peakrdl_regblock/exporter.py +++ b/src/peakrdl_regblock/exporter.py @@ -18,6 +18,7 @@ from .hwif import Hwif from .write_buffering import WriteBuffering from .read_buffering import ReadBuffering +from .broadcast import BroadcastLogic from .external_acks import ExternalWriteAckGenerator, ExternalReadAckGenerator from .parity import ParityErrorReduceGenerator from .sv_int import SVInt @@ -34,6 +35,7 @@ class RegblockExporter: readback: Readback write_buffering: WriteBuffering read_buffering: ReadBuffering + broadcast_logic: BroadcastLogic dereferencer: Dereferencer ds: 'DesignState' @@ -157,6 +159,7 @@ def export(self, node: Union[RootNode, AddrmapNode], output_dir:str, **kwargs: A self.field_logic = FieldLogic(self) self.write_buffering = WriteBuffering(self) self.read_buffering = ReadBuffering(self) + self.broadcast_logic = BroadcastLogic(self) self.dereferencer = Dereferencer(self) ext_write_acks = ExternalWriteAckGenerator(self) ext_read_acks = ExternalReadAckGenerator(self) @@ -180,6 +183,7 @@ def export(self, node: Union[RootNode, AddrmapNode], output_dir:str, **kwargs: A "get_module_port_list": self.get_module_port_list, "write_buffering": self.write_buffering, "read_buffering": self.read_buffering, + "broadcast_logic": self.broadcast_logic, "get_resetsignal": self.dereferencer.get_resetsignal, "default_resetsignal_name": self.dereferencer.default_resetsignal_name, "address_decode": self.address_decode, diff --git a/src/peakrdl_regblock/field_logic/generators.py b/src/peakrdl_regblock/field_logic/generators.py index 281ecb23..6f217f74 100644 --- a/src/peakrdl_regblock/field_logic/generators.py +++ b/src/peakrdl_regblock/field_logic/generators.py @@ -34,6 +34,11 @@ def enter_Field(self, node: 'FieldNode') -> None: if not node.implements_storage: return + # Skip fields in broadcaster registers + parent_reg = node.parent + if self.field_logic.exp.broadcast_logic.is_in_broadcast_scope(parent_reg): + return + # collect any extra combo signals that this field requires extra_combo_signals = OrderedDict() # type: OrderedDict[str, SVLogic] for conditional in self.field_logic.get_conditionals(node): @@ -87,6 +92,11 @@ def enter_AddressableComponent(self, node: 'AddressableNode') -> Optional[Walker return WalkerAction.Continue def enter_Field(self, node: 'FieldNode') -> None: + # Skip fields in broadcaster registers + parent_reg = node.parent + if self.field_logic.exp.broadcast_logic.is_in_broadcast_scope(parent_reg): + return + self.push_struct(kwf(node.inst_name)) if node.implements_storage: @@ -146,6 +156,11 @@ def enter_Reg(self, node: 'RegNode') -> Optional[WalkerAction]: def enter_Field(self, node: 'FieldNode') -> None: + # Skip fields in broadcaster registers - they don't have storage or outputs + parent_reg = node.parent + if self.exp.broadcast_logic.is_in_broadcast_scope(parent_reg): + return + if node.implements_storage: self.generate_field_storage(node) diff --git a/src/peakrdl_regblock/hwif/generators.py b/src/peakrdl_regblock/hwif/generators.py index e53f0fe3..a6731a0b 100644 --- a/src/peakrdl_regblock/hwif/generators.py +++ b/src/peakrdl_regblock/hwif/generators.py @@ -273,6 +273,11 @@ def add_external_reg_wr_data(self, name: str, node: 'RegNode', width: int, n_sub self.add_member(name, width) def enter_Field(self, node: 'FieldNode') -> None: + # Skip fields in broadcaster registers - they have no outputs + parent_reg = node.parent + if self.hwif.exp.broadcast_logic.is_in_broadcast_scope(parent_reg): + return + type_name = self.get_typdef_name(node) self.push_struct(type_name, kwf(node.inst_name)) @@ -298,6 +303,11 @@ def enter_Field(self, node: 'FieldNode') -> None: self.add_member('decrthreshold') def exit_Field(self, node: 'FieldNode') -> None: + # Skip fields in broadcaster registers + parent_reg = node.parent + if self.hwif.exp.broadcast_logic.is_in_broadcast_scope(parent_reg): + return + self.pop_struct() def exit_Reg(self, node: 'RegNode') -> None: diff --git a/src/peakrdl_regblock/readback/generators.py b/src/peakrdl_regblock/readback/generators.py index 87f29698..ce896e00 100644 --- a/src/peakrdl_regblock/readback/generators.py +++ b/src/peakrdl_regblock/readback/generators.py @@ -94,6 +94,10 @@ def enter_AddressableComponent(self, node: 'AddressableNode') -> WalkerAction: return WalkerAction.Continue def enter_Reg(self, node: RegNode) -> WalkerAction: + # Skip broadcaster registers - they have no storage to read back + if self.exp.broadcast_logic.is_in_broadcast_scope(node): + return WalkerAction.SkipDescendants + if not node.has_sw_readable: return WalkerAction.SkipDescendants diff --git a/src/peakrdl_regblock/udps/__init__.py b/src/peakrdl_regblock/udps/__init__.py index 2eb9c208..dc9f9eef 100644 --- a/src/peakrdl_regblock/udps/__init__.py +++ b/src/peakrdl_regblock/udps/__init__.py @@ -3,6 +3,7 @@ from .extended_swacc import ReadSwacc, WriteSwacc from .fixedpoint import IntWidth, FracWidth from .signed import IsSigned +from .broadcast import Broadcast ALL_UDPS = [ BufferWrites, @@ -14,4 +15,5 @@ IntWidth, FracWidth, IsSigned, + Broadcast ] diff --git a/src/peakrdl_regblock/udps/broadcast.py b/src/peakrdl_regblock/udps/broadcast.py new file mode 100644 index 00000000..5ba62fcf --- /dev/null +++ b/src/peakrdl_regblock/udps/broadcast.py @@ -0,0 +1,84 @@ +from typing import Any + +from systemrdl.udp import UDPDefinition +from systemrdl.component import Reg, Regfile +from systemrdl.rdltypes.references import RefType +from systemrdl.rdltypes.array import ArrayedType +from systemrdl.rdltypes import NoValue +from systemrdl.node import Node, RegNode, RegfileNode + + +class Broadcast(UDPDefinition): + name = "broadcast" + valid_components = {Reg, Regfile} + valid_type = ArrayedType(RefType) + + def validate(self, node: Node, value: Any) -> None: + if value is NoValue: + self.msg.error( + "The 'broadcast' property requires a target assignment", + self.get_src_ref(node) + ) + return + + # Convert single reference to list for uniform handling + if not isinstance(value, list): + targets = [value] + else: + targets = value + + if not targets: + self.msg.error( + "The 'broadcast_write' property must reference at least one target", + self.get_src_ref(node) + ) + return + + # Validate each target + for target in targets: + self._validate_target(node, target) + + def _validate_target(self, broadcaster: Node, target: Node) -> None: + """Validate a single broadcast target""" + + # Check that target is a Reg or Regfile + if not isinstance(target, (RegNode, RegfileNode)): + self.msg.error( + f"Broadcast target must be a register or regfile, not '{type(target.inst).__name__.lower()}'", + self.get_src_ref(broadcaster) + ) + return + + # Check type compatibility + # TODO: How to check identity more thoroughly? + broadcaster_type = type(broadcaster.inst) + target_type = type(target.inst) + + if broadcaster_type != target_type: + self.msg.error( + f"Incompatible broadcast types. " + f"Broadcaster '{broadcaster_type.__name__.lower()}' cannot target '{target_type.__name__.lower()}'. " + f"Supported: reg->reg, regfile->regfile (same structure)", + self.get_src_ref(broadcaster) + ) + return + + # Check internal/external boundary + # A broadcaster and its targets must both be internal, or both be external + if broadcaster.external != target.external: + self.msg.error( + f"Broadcaster and target must not cross internal/external boundary. " + f"Broadcaster is {'external' if broadcaster.external else 'internal'}, " + f"target is {'external' if target.external else 'internal'}", + self.get_src_ref(broadcaster) + ) + + # Check for circular references (target is also a broadcaster) + if target.get_property('broadcast') is not None: + self.msg.error( + f"Broadcast target '{target.inst_name}' cannot also be a broadcaster", + self.get_src_ref(broadcaster) + ) + + def get_unassigned_default(self, node: Node) -> Any: + return None diff --git a/tests/test_broadcast_arrays/__init__.py b/tests/test_broadcast_arrays/__init__.py new file mode 100644 index 00000000..6c3bdbe7 --- /dev/null +++ b/tests/test_broadcast_arrays/__init__.py @@ -0,0 +1 @@ +# Test package for broadcast to arrays diff --git a/tests/test_broadcast_arrays/regblock.rdl b/tests/test_broadcast_arrays/regblock.rdl new file mode 100644 index 00000000..0ceb9d9b --- /dev/null +++ b/tests/test_broadcast_arrays/regblock.rdl @@ -0,0 +1,19 @@ +addrmap top { + default regwidth = 32; + default sw = rw; + default hw = r; + + reg my_reg_t { + field { + sw=rw; hw=r; + reset=0; + } f[8]; + }; + + // Array of target registers (4 elements) + my_reg_t reg_array[4] @ 0x00 += 0x4; + + // Broadcaster that writes to entire array (must list all elements explicitly) + my_reg_t broadcast_all @ 0x20; + broadcast_all -> broadcast = '{ reg_array[0], reg_array[1], reg_array[2], reg_array[3] }; +}; diff --git a/tests/test_broadcast_arrays/tb_template.sv b/tests/test_broadcast_arrays/tb_template.sv new file mode 100644 index 00000000..3ed0b562 --- /dev/null +++ b/tests/test_broadcast_arrays/tb_template.sv @@ -0,0 +1,51 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // Test 1: Broadcast to entire array + cpuif.write('h20, 32'hAA_BB_CC_DD); + @cb; + + // Verify all 4 array elements were updated + assert(cb.hwif_out.reg_array[0].f.value == 8'hDD) + else $error("reg_array[0].f expected 0xDD, got 0x%0x", cb.hwif_out.reg_array[0].f.value); + assert(cb.hwif_out.reg_array[1].f.value == 8'hDD) + else $error("reg_array[1].f expected 0xDD, got 0x%0x", cb.hwif_out.reg_array[1].f.value); + assert(cb.hwif_out.reg_array[2].f.value == 8'hDD) + else $error("reg_array[2].f expected 0xDD, got 0x%0x", cb.hwif_out.reg_array[2].f.value); + assert(cb.hwif_out.reg_array[3].f.value == 8'hDD) + else $error("reg_array[3].f expected 0xDD, got 0x%0x", cb.hwif_out.reg_array[3].f.value); + + // Verify readback from all elements + cpuif.assert_read('h00, 32'h00_00_00_DD); + cpuif.assert_read('h04, 32'h00_00_00_DD); + cpuif.assert_read('h08, 32'h00_00_00_DD); + cpuif.assert_read('h0C, 32'h00_00_00_DD); + + // Test 2: Write different values to individual array elements + cpuif.write('h00, 32'h00_00_00_11); + cpuif.write('h04, 32'h00_00_00_22); + cpuif.write('h08, 32'h00_00_00_33); + cpuif.write('h0C, 32'h00_00_00_44); + @cb; + + cpuif.assert_read('h00, 32'h00_00_00_11); + cpuif.assert_read('h04, 32'h00_00_00_22); + cpuif.assert_read('h08, 32'h00_00_00_33); + cpuif.assert_read('h0C, 32'h00_00_00_44); + + // Test 3: Another full array broadcast + cpuif.write('h20, 32'hFF_EE_DD_CC); + @cb; + + // All elements should have new value + cpuif.assert_read('h00, 32'h00_00_00_CC); + cpuif.assert_read('h04, 32'h00_00_00_CC); + cpuif.assert_read('h08, 32'h00_00_00_CC); + cpuif.assert_read('h0C, 32'h00_00_00_CC); + +{% endblock %} diff --git a/tests/test_broadcast_arrays/testcase.py b/tests/test_broadcast_arrays/testcase.py new file mode 100644 index 00000000..bfe26a50 --- /dev/null +++ b/tests/test_broadcast_arrays/testcase.py @@ -0,0 +1,6 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + + def test_dut(self): + self.run_test() diff --git a/tests/test_broadcast_basic/__init__.py b/tests/test_broadcast_basic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_broadcast_basic/regblock.rdl b/tests/test_broadcast_basic/regblock.rdl new file mode 100644 index 00000000..1d4324ed --- /dev/null +++ b/tests/test_broadcast_basic/regblock.rdl @@ -0,0 +1,21 @@ +addrmap top { + default regwidth = 32; + default sw = rw; + default hw = r; + + reg my_reg_t { + field { + sw=rw; hw=r; + reset=0; + } f[8]; + }; + + // Target registers + my_reg_t rega @ 0x0; + my_reg_t regb @ 0x4; + my_reg_t regc @ 0x8; + + // Broadcaster register + my_reg_t reg_all @ 0x10; + reg_all -> broadcast = '{ rega, regb, regc }; +}; diff --git a/tests/test_broadcast_basic/tb_template.sv b/tests/test_broadcast_basic/tb_template.sv new file mode 100644 index 00000000..75822fbb --- /dev/null +++ b/tests/test_broadcast_basic/tb_template.sv @@ -0,0 +1,45 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // Test 1: Write to broadcaster should update all targets + cpuif.write('h10, 32'hAA_BB_CC_DD); + @cb; + + // Verify all target registers were updated + assert(cb.hwif_out.rega.f.value == 8'hDD) + else $error("rega.f expected 0xDD, got 0x%0x", cb.hwif_out.rega.f.value); + assert(cb.hwif_out.regb.f.value == 8'hDD) + else $error("regb.f expected 0xDD, got 0x%0x", cb.hwif_out.regb.f.value); + assert(cb.hwif_out.regc.f.value == 8'hDD) + else $error("regc.f expected 0xDD, got 0x%0x", cb.hwif_out.regc.f.value); + + // Verify values can be read back from targets + cpuif.assert_read('h0, 32'h00_00_00_DD); + cpuif.assert_read('h4, 32'h00_00_00_DD); + cpuif.assert_read('h8, 32'h00_00_00_DD); + + // Test 2: Individual writes to targets still work + cpuif.write('h0, 32'h00_00_00_11); + cpuif.write('h4, 32'h00_00_00_22); + cpuif.write('h8, 32'h00_00_00_33); + @cb; + + cpuif.assert_read('h0, 32'h00_00_00_11); + cpuif.assert_read('h4, 32'h00_00_00_22); + cpuif.assert_read('h8, 32'h00_00_00_33); + + // Test 3: Broadcast write with different value + cpuif.write('h10, 32'h12_34_56_78); + @cb; + + // All targets should have new broadcast value + cpuif.assert_read('h0, 32'h00_00_00_78); + cpuif.assert_read('h4, 32'h00_00_00_78); + cpuif.assert_read('h8, 32'h00_00_00_78); + +{% endblock %} diff --git a/tests/test_broadcast_basic/testcase.py b/tests/test_broadcast_basic/testcase.py new file mode 100644 index 00000000..44068d29 --- /dev/null +++ b/tests/test_broadcast_basic/testcase.py @@ -0,0 +1,6 @@ +from ..lib.sim_testcase import SimTestCase + + +class Test(SimTestCase): + def test_dut(self): + self.run_test() diff --git a/tests/test_broadcast_regfiles/__init__.py b/tests/test_broadcast_regfiles/__init__.py new file mode 100644 index 00000000..39b9d23c --- /dev/null +++ b/tests/test_broadcast_regfiles/__init__.py @@ -0,0 +1 @@ +# Test package for broadcast to regfiles diff --git a/tests/test_broadcast_regfiles/regblock.rdl b/tests/test_broadcast_regfiles/regblock.rdl new file mode 100644 index 00000000..f2b6f1d7 --- /dev/null +++ b/tests/test_broadcast_regfiles/regblock.rdl @@ -0,0 +1,38 @@ +addrmap top { + default regwidth = 32; + default sw = rw; + default hw = r; + + reg my_reg_t { + field { + sw=rw; hw=r; + reset=0; + } f[8]; + }; + + // Define a regfile containing multiple registers + regfile my_regfile_t { + my_reg_t reg_a @ 0x00; + my_reg_t reg_b @ 0x04; + my_reg_t reg_c @ 0x08; + }; + + // Instantiate multiple regfiles + my_regfile_t regfile_a @ 0x00; + my_regfile_t regfile_b @ 0x10; + my_regfile_t regfile_c @ 0x20; + + // Instantiate a regfile array + // Each element is 0xC size, stride must be >= size + my_regfile_t regfile_array[2] @ 0x100 += 0x10; + + // Broadcaster that writes to all regfile instances + // This should automatically expand to all registers within each regfile + my_regfile_t broadcast_all @ 0x40; + broadcast_all -> broadcast = '{ regfile_a, regfile_b, regfile_c }; + + // Broadcaster that writes to the regfile array + // Must explicitly list array elements due to SystemRDL syntax limitation + my_regfile_t broadcast_to_array @ 0x80; + broadcast_to_array -> broadcast = '{ regfile_array[0], regfile_array[1] }; +}; diff --git a/tests/test_broadcast_regfiles/tb_template.sv b/tests/test_broadcast_regfiles/tb_template.sv new file mode 100644 index 00000000..bbfe9c15 --- /dev/null +++ b/tests/test_broadcast_regfiles/tb_template.sv @@ -0,0 +1,75 @@ +{% extends "lib/tb_base.sv" %} + +{% block seq %} + {% sv_line_anchor %} + ##1; + cb.rst <= '0; + ##1; + + // Test 1: Broadcast to reg_a in all regfiles + // broadcast_all is at 0x40. reg_a is at offset 0x0 -> 0x40 + cpuif.write('h40, 32'h12_34_56_78); + @cb; + + // Verify regfile_a.reg_a updated + assert(cb.hwif_out.regfile_a.reg_a.f.value == 8'h78) + else $error("regfile_a.reg_a.f expected 0x78, got 0x%0x", cb.hwif_out.regfile_a.reg_a.f.value); + // Verify regfile_b.reg_a updated + assert(cb.hwif_out.regfile_b.reg_a.f.value == 8'h78) + else $error("regfile_b.reg_a.f expected 0x78, got 0x%0x", cb.hwif_out.regfile_b.reg_a.f.value); + // Verify regfile_c.reg_a updated + assert(cb.hwif_out.regfile_c.reg_a.f.value == 8'h78) + else $error("regfile_c.reg_a.f expected 0x78, got 0x%0x", cb.hwif_out.regfile_c.reg_a.f.value); + + // Verify other registers NOT updated (should be default 0) + assert(cb.hwif_out.regfile_a.reg_b.f.value == 8'h00) + else $error("regfile_a.reg_b.f expected 0x00, got 0x%0x", cb.hwif_out.regfile_a.reg_b.f.value); + + // Test 2: Broadcast to reg_b in all regfiles + // broadcast_all.reg_b is at 0x44 + cpuif.write('h44, 32'h87_65_43_21); + @cb; + + // Verify regfile_a.reg_b updated + assert(cb.hwif_out.regfile_a.reg_b.f.value == 8'h21) + else $error("regfile_a.reg_b.f expected 0x21, got 0x%0x", cb.hwif_out.regfile_a.reg_b.f.value); + // Verify regfile_b.reg_b updated + assert(cb.hwif_out.regfile_b.reg_b.f.value == 8'h21) + else $error("regfile_b.reg_b.f expected 0x21, got 0x%0x", cb.hwif_out.regfile_b.reg_b.f.value); + + // Verify reg_a still holds previous value + assert(cb.hwif_out.regfile_a.reg_a.f.value == 8'h78) + else $error("regfile_a.reg_a.f expected 0x78, got 0x%0x", cb.hwif_out.regfile_a.reg_a.f.value); + + // Test 3: Individual writes still work + cpuif.write('h08, 32'hAA_BB_CC_DD); // regfile_a.reg_c + @cb; + assert(cb.hwif_out.regfile_a.reg_c.f.value == 8'hDD) + else $error("regfile_a.reg_c.f expected 0xDD, got 0x%0x", cb.hwif_out.regfile_a.reg_c.f.value); + // Should not affect others + assert(cb.hwif_out.regfile_b.reg_c.f.value == 8'h00) + else $error("regfile_b.reg_c.f expected 0x00, got 0x%0x", cb.hwif_out.regfile_b.reg_c.f.value); + + // Test 4: Broadcast to regfile array + // broadcast_to_array is at 0x80. reg_a is at 0x80 + cpuif.write('h80, 32'hCA_FE_BA_BE); + @cb; + + // Verify regfile_array[0].reg_a (Base 0x100) + assert(cb.hwif_out.regfile_array[0].reg_a.f.value == 8'hBE) + else $error("regfile_array[0].reg_a.f expected 0xBE, got 0x%0x", cb.hwif_out.regfile_array[0].reg_a.f.value); + // Verify regfile_array[1].reg_a (Base 0x110) + assert(cb.hwif_out.regfile_array[1].reg_a.f.value == 8'hBE) + else $error("regfile_array[1].reg_a.f expected 0xBE, got 0x%0x", cb.hwif_out.regfile_array[1].reg_a.f.value); + + // Verify reg_b NOT updated + assert(cb.hwif_out.regfile_array[0].reg_b.f.value == 8'h00) + else $error("regfile_array[0].reg_b.f expected 0x00, got 0x%0x", cb.hwif_out.regfile_array[0].reg_b.f.value); + + // Readback verification + // regfile_array[0].reg_a + cpuif.assert_read('h100, 32'h00_00_00_BE); + // regfile_array[1].reg_a + cpuif.assert_read('h110, 32'h00_00_00_BE); + +{% endblock %} diff --git a/tests/test_broadcast_regfiles/testcase.py b/tests/test_broadcast_regfiles/testcase.py new file mode 100644 index 00000000..bfe26a50 --- /dev/null +++ b/tests/test_broadcast_regfiles/testcase.py @@ -0,0 +1,6 @@ +from ..lib.sim_testcase import SimTestCase + +class Test(SimTestCase): + + def test_dut(self): + self.run_test() diff --git a/tests/test_cpuif_err_rsp/regblock.rdl b/tests/test_cpuif_err_rsp/regblock.rdl index 6d387f2a..67193751 100644 --- a/tests/test_cpuif_err_rsp/regblock.rdl +++ b/tests/test_cpuif_err_rsp/regblock.rdl @@ -73,4 +73,38 @@ addrmap top { } f[31:0] = 100; } r_wo; } external_rf @ 0x40; + + // Broadcast regfile test + // Define a regfile type with different access types + regfile broadcast_target_t { + reg { + field { + sw=rw; hw=r; + reset=0; + } f[31:0]; + } r_rw @ 0x00; + + reg { + field { + sw=r; hw=na; + reset=0; + } f[31:0]; + } r_ro @ 0x04; + + reg { + field { + sw=w; hw=r; + reset=0; + } f[31:0]; + } r_wo @ 0x08; + }; + + // Target regfiles + broadcast_target_t bc_target_a @ 0x50; + broadcast_target_t bc_target_b @ 0x60; + broadcast_target_t bc_target_c @ 0x70; + + // Broadcaster regfile + broadcast_target_t bc_all @ 0x80; + bc_all -> broadcast = '{ bc_target_a, bc_target_b, bc_target_c }; }; diff --git a/tests/test_cpuif_err_rsp/tb_template.sv b/tests/test_cpuif_err_rsp/tb_template.sv index 50e56203..bb37715e 100644 --- a/tests/test_cpuif_err_rsp/tb_template.sv +++ b/tests/test_cpuif_err_rsp/tb_template.sv @@ -178,4 +178,65 @@ logic [7:0] addr; cpuif.assert_read(addr, 'h0, .expects_err(expected_rd_err)); cpuif.write(addr, 'hD0, .expects_err(expected_wr_err)); cpuif.assert_read(addr, 'hD0, .expects_err(expected_rd_err)); + + // Broadcast regfile tests + // First, initialize target regfiles with different values + // bc_target_a.r_rw + cpuif.write('h50, 'hA1, .expects_err('h0)); + @cb; + cpuif.assert_read('h50, 'hA1, .expects_err('h0)); + + // bc_target_b.r_rw + cpuif.write('h60, 'hB2, .expects_err('h0)); + @cb; + cpuif.assert_read('h60, 'hB2, .expects_err('h0)); + + // bc_target_c.r_rw + cpuif.write('h70, 'hC3, .expects_err('h0)); + @cb; + cpuif.assert_read('h70, 'hC3, .expects_err('h0)); + + // Test broadcast write to r_rw (should succeed) + addr = 'h80; // bc_all.r_rw + expected_rd_err = 'h0; + expected_wr_err = 'h0; + cpuif.write(addr, 'hFF, .expects_err(expected_wr_err)); + @cb; + // Verify all targets were written + cpuif.assert_read('h50, 'hFF, .expects_err('h0)); // bc_target_a.r_rw + cpuif.assert_read('h60, 'hFF, .expects_err('h0)); // bc_target_b.r_rw + cpuif.assert_read('h70, 'hFF, .expects_err('h0)); // bc_target_c.r_rw + // Verify broadcast read returns OR of all targets + cpuif.assert_read(addr, 'hFF, .expects_err(expected_rd_err)); + + // Test broadcast write to r_ro (should error if err_if_bad_rw) + addr = 'h84; // bc_all.r_ro + expected_rd_err = 'h0; + expected_wr_err = bad_rw_expected_wr_err; + cpuif.write(addr, 'hEE, .expects_err(expected_wr_err)); + @cb; + // Verify broadcast read returns OR of all targets (should be 0) + cpuif.assert_read(addr, 'h0, .expects_err(expected_rd_err)); + + // Test broadcast write to r_wo (should succeed) + addr = 'h88; // bc_all.r_wo + expected_rd_err = bad_rw_expected_rd_err; + expected_wr_err = 'h0; + cpuif.write(addr, 'hDD, .expects_err(expected_wr_err)); + @cb; + // Verify all targets were written by checking hwif_out + assert(cb.hwif_out.bc_target_a.r_wo.f.value == 'hDD); + assert(cb.hwif_out.bc_target_b.r_wo.f.value == 'hDD); + assert(cb.hwif_out.bc_target_c.r_wo.f.value == 'hDD); + // Verify broadcast read (should error if err_if_bad_rw) + cpuif.assert_read(addr, 'h0, .expects_err(expected_rd_err)); + + // Test that writing to one target doesn't affect others + cpuif.write('h50, 'hA5, .expects_err('h0)); // bc_target_a.r_rw + @cb; + cpuif.assert_read('h50, 'hA5, .expects_err('h0)); + cpuif.assert_read('h60, 'hFF, .expects_err('h0)); // bc_target_b.r_rw unchanged + cpuif.assert_read('h70, 'hFF, .expects_err('h0)); // bc_target_c.r_rw unchanged + // Broadcast read should return OR of all targets + cpuif.assert_read('h80, 'hFF, .expects_err('h0)); // 'hA5 | 'hFF | 'hFF = 'hFF {% endblock %}