Skip to content

Commit 3ee1a4e

Browse files
author
Sebastien Baillou
committed
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
1 parent 4201ce9 commit 3ee1a4e

File tree

23 files changed

+715
-6
lines changed

23 files changed

+715
-6
lines changed

hdl-src/regblock_udps.rdl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,8 @@ property fracwidth {
5252
type = longint unsigned;
5353
component = field;
5454
};
55+
56+
property broadcast {
57+
type = ref[];
58+
component = reg | regfile;
59+
};

src/peakrdl_regblock/addr_decode.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,20 +201,53 @@ def _add_reg_decoding_flags(self,
201201
subword_index: Union[int, None] = None,
202202
subword_stride: Union[int, None] = None) -> None:
203203
if subword_index is None or subword_stride is None:
204-
addr_decoding_str = f"cpuif_req_masked & (cpuif_addr == {self._get_address_str(node)})"
204+
addr_decoding_str = f"(cpuif_addr == {self._get_address_str(node)})"
205205
else:
206-
addr_decoding_str = f"cpuif_req_masked & (cpuif_addr == {self._get_address_str(node, subword_offset=subword_index*subword_stride)})"
207-
rhs_valid_addr = addr_decoding_str
206+
addr_decoding_str = f"(cpuif_addr == {self._get_address_str(node, subword_offset=subword_index*subword_stride)})"
207+
208+
# Check if this register is a broadcast target
209+
# If so, OR in the broadcaster's address decode
210+
broadcasters = self.addr_decode.exp.broadcast_logic.get_broadcasters_for_target(node)
211+
212+
if broadcasters:
213+
# For both reads and writes: OR in the broadcaster's address decode directly
214+
# This eliminates the need for separate broadcast_wr_* wires
215+
broadcaster_addr_terms = []
216+
217+
for broadcaster_node in broadcasters:
218+
expr_width = self.addr_decode.exp.ds.addr_width
219+
broadcaster_addr = broadcaster_node.raw_absolute_address - self.addr_decode.top_node.raw_absolute_address
220+
broadcaster_addr_str = str(SVInt(broadcaster_addr, expr_width))
221+
222+
# Add broadcaster's own array offsets if it's in an array
223+
if broadcaster_node.is_array:
224+
# Calculate which loop indices apply to the broadcaster
225+
# We need to figure out the broadcaster's position in the loop hierarchy
226+
# For now, assume broadcasters are not in arrays (simplification)
227+
# TODO: Handle array broadcasters properly
228+
pass
229+
230+
broadcaster_addr_terms.append(f"(cpuif_addr == {broadcaster_addr_str})")
231+
232+
# OR all broadcaster addresses into the main address decode
233+
if broadcaster_addr_terms:
234+
broadcaster_addrs = " | ".join(broadcaster_addr_terms)
235+
# Modify addr_decoding_str to include broadcaster addresses
236+
# The original rhs already has the direct address check, we just OR in the broadcaster addresses
237+
addr_decoding_str = f"({addr_decoding_str} | {broadcaster_addrs})"
238+
239+
rhs_valid_addr = f"cpuif_req_masked & {addr_decoding_str}"
208240
readable = node.has_sw_readable
209241
writable = node.has_sw_writable
210242
if readable and writable:
211-
rhs = addr_decoding_str
243+
rhs = f"cpuif_req_masked & {addr_decoding_str}"
212244
elif readable and not writable:
213-
rhs = f"{addr_decoding_str} & !cpuif_req_is_wr"
245+
rhs = f"cpuif_req_masked & {addr_decoding_str} & !cpuif_req_is_wr"
214246
elif not readable and writable:
215-
rhs = f"{addr_decoding_str} & cpuif_req_is_wr"
247+
rhs = f"cpuif_req_masked & {addr_decoding_str} & cpuif_req_is_wr"
216248
else:
217249
raise RuntimeError
250+
218251
# Add decoding flags
219252
if subword_index is None:
220253
self.add_content(f"{self.addr_decode.get_access_strobe(node)} = {rhs};")
@@ -233,6 +266,10 @@ def enter_Reg(self, node: RegNode) -> None:
233266
regwidth = node.get_property('regwidth')
234267
accesswidth = node.get_property('accesswidth')
235268

269+
if self.addr_decode.exp.broadcast_logic.is_in_broadcast_scope(node):
270+
# Register is within a broadcaster scope. Do not add decoding flags
271+
return
272+
236273
if regwidth == accesswidth:
237274
self._add_reg_decoding_flags(node)
238275
else:
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
from typing import TYPE_CHECKING, Dict, List, Tuple, Any, Optional
2+
from collections import defaultdict
3+
4+
from systemrdl.node import RegNode, RegfileNode, AddressableNode, Node
5+
from systemrdl.walker import RDLListener, RDLWalker
6+
7+
if TYPE_CHECKING:
8+
from ..exporter import RegblockExporter
9+
10+
11+
class BroadcastLogic:
12+
"""
13+
Manages broadcast logic for registers and regfiles.
14+
15+
A broadcaster register/regfile does not have hardware storage.
16+
Instead, writes to the broadcaster address generate write strobes
17+
for all target registers.
18+
"""
19+
20+
def __init__(self, exp: 'RegblockExporter') -> None:
21+
self.exp = exp
22+
23+
# Maps broadcaster node to list of target nodes
24+
# List of tuples: (broadcaster_node, [target_nodes])
25+
self.broadcast_map: List[Tuple[AddressableNode, List[RegNode]]] = []
26+
27+
# List of all broadcaster nodes (no hardware implementation)
28+
self.broadcasters: List[AddressableNode] = []
29+
30+
# List of all target nodes (receive broadcast writes)
31+
self.targets: List[RegNode] = []
32+
33+
# Scan the design to build the broadcast map
34+
self._scan_design()
35+
36+
def _scan_design(self) -> None:
37+
"""Scan the design to identify broadcasters and their targets"""
38+
listener = BroadcastScanner(self)
39+
RDLWalker(skip_not_present=False).walk(self.exp.ds.top_node, listener)
40+
41+
def is_broadcaster(self, node: AddressableNode) -> bool:
42+
"""Check if a node is a broadcaster"""
43+
return node in self.broadcasters
44+
45+
def is_in_broadcast_scope(self, node: AddressableNode) -> bool:
46+
"""
47+
Check if a node is within a broadcast scope.
48+
This includes:
49+
1. The node itself is a broadcaster
50+
2. The node is a child of a broadcaster (e.g. reg in a broadcast regfile)
51+
"""
52+
curr: Optional[Node] = node
53+
while curr is not None:
54+
if isinstance(curr, AddressableNode) and self.is_broadcaster(curr):
55+
return True
56+
curr = curr.parent
57+
return False
58+
59+
def is_target(self, node: RegNode) -> bool:
60+
"""Check if a node is a broadcast target"""
61+
return node in self.targets
62+
63+
def get_broadcasters_for_target(self, target: RegNode) -> List[AddressableNode]:
64+
"""
65+
Get all broadcaster nodes that write to this target.
66+
"""
67+
broadcasters: List[AddressableNode] = []
68+
69+
# Calculate the total number of instances this target represents
70+
# (including its own array dimensions and any parent array dimensions)
71+
expected_count = 1
72+
curr: Optional[Node] = target
73+
while curr is not None:
74+
if isinstance(curr, AddressableNode) and curr.is_array:
75+
expected_count *= curr.n_elements
76+
curr = curr.parent
77+
78+
for broadcaster_node, targets in self.broadcast_map:
79+
# 1. Exact match check
80+
if target in targets:
81+
broadcasters.append(broadcaster_node)
82+
continue
83+
84+
# 2. Array iteration match check
85+
# If target is a canonical node inside an array loop, it won't be in the map.
86+
# But its unrolled instances (sharing the same underlying Component 'inst') might be.
87+
88+
# Count how many targets share the same underlying component instance
89+
match_count = 0
90+
for t in targets:
91+
if t.inst == target.inst:
92+
# If current_idx is None, this node represents the entire array (all elements)
93+
if t.current_idx is None:
94+
match_count += t.n_elements
95+
else:
96+
# Otherwise it represents a single element
97+
match_count += 1
98+
99+
if match_count == expected_count and expected_count > 0:
100+
broadcasters.append(broadcaster_node)
101+
102+
return broadcasters
103+
104+
105+
class BroadcastScanner(RDLListener):
106+
"""Listener that scans the design to build the broadcast mapping"""
107+
108+
def __init__(self, broadcast_logic: BroadcastLogic) -> None:
109+
self.bl = broadcast_logic
110+
# Stack of active broadcaster scopes: List[Tuple[BroadcasterNode, List[TargetNode]]]
111+
self.scope_stack: List[Tuple[AddressableNode, List[RegfileNode]]] = []
112+
113+
def enter_Component(self, node: Node) -> None:
114+
115+
# 1. Check if we are inside an active broadcaster scope (Structural Broadcast)
116+
if self.scope_stack and isinstance(node, RegNode):
117+
broadcaster_scope, target_scopes = self.scope_stack[-1]
118+
119+
# Calculate relative path from the broadcaster scope
120+
b_path = broadcaster_scope.get_path()
121+
node_path = node.get_path()
122+
123+
if node_path.startswith(b_path + '.'):
124+
rel_path = node_path[len(b_path)+1:]
125+
126+
# Map to each target scope
127+
for target_scope in target_scopes:
128+
# Find corresponding node in target
129+
target_reg = target_scope.find_by_path(rel_path)
130+
131+
if target_reg and isinstance(target_reg, RegNode):
132+
self._add_broadcast_map(node, target_reg)
133+
134+
# 2. Check if this node defines a new broadcast (is a Broadcaster)
135+
if isinstance(node, (RegNode, RegfileNode)):
136+
broadcast_targets = node.get_property('broadcast')
137+
138+
if broadcast_targets is not None:
139+
self.bl.broadcasters.append(node)
140+
141+
if isinstance(node, RegfileNode):
142+
# Structural Broadcast: Push to stack to handle children
143+
# Filter targets to only RegfileNodes (validation ensures they are compatible)
144+
rf_targets = [t for t in broadcast_targets if isinstance(t, RegfileNode)]
145+
if rf_targets:
146+
self.scope_stack.append((node, rf_targets))
147+
148+
elif isinstance(node, RegNode):
149+
# Direct Register Broadcast: Map immediately
150+
for target in broadcast_targets:
151+
if isinstance(target, RegNode):
152+
self._add_broadcast_map(node, target)
153+
154+
def exit_Component(self, node: Node) -> None:
155+
# Pop scope if we are exiting the current broadcaster scope
156+
if self.scope_stack and self.scope_stack[-1][0] == node:
157+
self.scope_stack.pop()
158+
159+
def _add_broadcast_map(self, broadcaster: AddressableNode, target: RegNode) -> None:
160+
"""Helper to add a broadcaster -> target mapping"""
161+
162+
# Find existing entry for this broadcaster
163+
found = False
164+
for b_node, t_list in self.bl.broadcast_map:
165+
if b_node == broadcaster:
166+
if target not in t_list:
167+
t_list.append(target)
168+
found = True
169+
break
170+
171+
if not found:
172+
self.bl.broadcast_map.append((broadcaster, [target]))
173+
174+
# Also track in global targets list
175+
if target not in self.bl.targets:
176+
self.bl.targets.append(target)
177+
178+
def _expand_array(self, node: AddressableNode) -> List[AddressableNode]:
179+
"""
180+
For broadcast targets, we expect explicit array indexing in the RDL.
181+
Just return the node as-is since it's already the specific element.
182+
"""
183+
# Simply return the node - RDL references should already be explicit
184+
return [node]

src/peakrdl_regblock/exporter.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .hwif import Hwif
1919
from .write_buffering import WriteBuffering
2020
from .read_buffering import ReadBuffering
21+
from .broadcast import BroadcastLogic
2122
from .external_acks import ExternalWriteAckGenerator, ExternalReadAckGenerator
2223
from .parity import ParityErrorReduceGenerator
2324
from .sv_int import SVInt
@@ -34,6 +35,7 @@ class RegblockExporter:
3435
readback: Readback
3536
write_buffering: WriteBuffering
3637
read_buffering: ReadBuffering
38+
broadcast_logic: BroadcastLogic
3739
dereferencer: Dereferencer
3840
ds: 'DesignState'
3941

@@ -157,6 +159,7 @@ def export(self, node: Union[RootNode, AddrmapNode], output_dir:str, **kwargs: A
157159
self.field_logic = FieldLogic(self)
158160
self.write_buffering = WriteBuffering(self)
159161
self.read_buffering = ReadBuffering(self)
162+
self.broadcast_logic = BroadcastLogic(self)
160163
self.dereferencer = Dereferencer(self)
161164
ext_write_acks = ExternalWriteAckGenerator(self)
162165
ext_read_acks = ExternalReadAckGenerator(self)
@@ -180,6 +183,7 @@ def export(self, node: Union[RootNode, AddrmapNode], output_dir:str, **kwargs: A
180183
"get_module_port_list": self.get_module_port_list,
181184
"write_buffering": self.write_buffering,
182185
"read_buffering": self.read_buffering,
186+
"broadcast_logic": self.broadcast_logic,
183187
"get_resetsignal": self.dereferencer.get_resetsignal,
184188
"default_resetsignal_name": self.dereferencer.default_resetsignal_name,
185189
"address_decode": self.address_decode,

src/peakrdl_regblock/field_logic/generators.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ def enter_Field(self, node: 'FieldNode') -> None:
3434
if not node.implements_storage:
3535
return
3636

37+
# Skip fields in broadcaster registers
38+
parent_reg = node.parent
39+
if self.field_logic.exp.broadcast_logic.is_in_broadcast_scope(parent_reg):
40+
return
41+
3742
# collect any extra combo signals that this field requires
3843
extra_combo_signals = OrderedDict() # type: OrderedDict[str, SVLogic]
3944
for conditional in self.field_logic.get_conditionals(node):
@@ -87,6 +92,11 @@ def enter_AddressableComponent(self, node: 'AddressableNode') -> Optional[Walker
8792
return WalkerAction.Continue
8893

8994
def enter_Field(self, node: 'FieldNode') -> None:
95+
# Skip fields in broadcaster registers
96+
parent_reg = node.parent
97+
if self.field_logic.exp.broadcast_logic.is_in_broadcast_scope(parent_reg):
98+
return
99+
90100
self.push_struct(kwf(node.inst_name))
91101

92102
if node.implements_storage:
@@ -146,6 +156,11 @@ def enter_Reg(self, node: 'RegNode') -> Optional[WalkerAction]:
146156

147157

148158
def enter_Field(self, node: 'FieldNode') -> None:
159+
# Skip fields in broadcaster registers - they don't have storage or outputs
160+
parent_reg = node.parent
161+
if self.exp.broadcast_logic.is_in_broadcast_scope(parent_reg):
162+
return
163+
149164
if node.implements_storage:
150165
self.generate_field_storage(node)
151166

src/peakrdl_regblock/hwif/generators.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ def add_external_reg_wr_data(self, name: str, node: 'RegNode', width: int, n_sub
273273
self.add_member(name, width)
274274

275275
def enter_Field(self, node: 'FieldNode') -> None:
276+
# Skip fields in broadcaster registers - they have no outputs
277+
parent_reg = node.parent
278+
if self.hwif.exp.broadcast_logic.is_in_broadcast_scope(parent_reg):
279+
return
280+
276281
type_name = self.get_typdef_name(node)
277282
self.push_struct(type_name, kwf(node.inst_name))
278283

@@ -298,6 +303,11 @@ def enter_Field(self, node: 'FieldNode') -> None:
298303
self.add_member('decrthreshold')
299304

300305
def exit_Field(self, node: 'FieldNode') -> None:
306+
# Skip fields in broadcaster registers
307+
parent_reg = node.parent
308+
if self.hwif.exp.broadcast_logic.is_in_broadcast_scope(parent_reg):
309+
return
310+
301311
self.pop_struct()
302312

303313
def exit_Reg(self, node: 'RegNode') -> None:

src/peakrdl_regblock/readback/generators.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ def enter_AddressableComponent(self, node: 'AddressableNode') -> WalkerAction:
9494
return WalkerAction.Continue
9595

9696
def enter_Reg(self, node: RegNode) -> WalkerAction:
97+
# Skip broadcaster registers - they have no storage to read back
98+
if self.exp.broadcast_logic.is_in_broadcast_scope(node):
99+
return WalkerAction.SkipDescendants
100+
97101
if not node.has_sw_readable:
98102
return WalkerAction.SkipDescendants
99103

src/peakrdl_regblock/udps/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .extended_swacc import ReadSwacc, WriteSwacc
44
from .fixedpoint import IntWidth, FracWidth
55
from .signed import IsSigned
6+
from .broadcast import Broadcast
67

78
ALL_UDPS = [
89
BufferWrites,
@@ -14,4 +15,5 @@
1415
IntWidth,
1516
FracWidth,
1617
IsSigned,
18+
Broadcast
1719
]

0 commit comments

Comments
 (0)