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
5 changes: 5 additions & 0 deletions hdl-src/regblock_udps.rdl
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,8 @@ property fracwidth {
type = longint unsigned;
component = field;
};

property broadcast {
type = ref[];
component = reg | regfile;
};
49 changes: 43 additions & 6 deletions src/peakrdl_regblock/addr_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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};")
Expand All @@ -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:
Expand Down
184 changes: 184 additions & 0 deletions src/peakrdl_regblock/broadcast/__init__.py
Original file line number Diff line number Diff line change
@@ -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]
4 changes: 4 additions & 0 deletions src/peakrdl_regblock/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,6 +35,7 @@ class RegblockExporter:
readback: Readback
write_buffering: WriteBuffering
read_buffering: ReadBuffering
broadcast_logic: BroadcastLogic
dereferencer: Dereferencer
ds: 'DesignState'

Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions src/peakrdl_regblock/field_logic/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
10 changes: 10 additions & 0 deletions src/peakrdl_regblock/hwif/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions src/peakrdl_regblock/readback/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/peakrdl_regblock/udps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -14,4 +15,5 @@
IntWidth,
FracWidth,
IsSigned,
Broadcast
]
Loading