Fix max_decode_depth to control decoder hierarchy and port generation (#18)
* Initial plan * Fix max_decode_depth to properly control decoder hierarchy and port generation Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> * Fix test that relied on old depth behavior Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> * Update documentation for max_decode_depth parameter Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> * fix format * Add variable_depth RDL file and smoke tests for max_decode_depth parameter Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> * Add variable depth tests for APB3 and AXI4-Lite CPUIFs Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> * fix * fix * bump --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
This commit is contained in:
@@ -116,7 +116,9 @@ class Exporter(ExporterSubcommandPlugin):
|
||||
type=int,
|
||||
default=1,
|
||||
help="""Maximum depth for address decoder to descend into nested
|
||||
addressable components. Default is 1.
|
||||
addressable components. Value of 0 decodes all levels (infinite depth).
|
||||
Value of 1 decodes only top-level children. Value of 2 decodes top-level
|
||||
and one level deeper, etc. Default is 1.
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
@@ -25,11 +25,7 @@ class BaseCpuif:
|
||||
|
||||
@property
|
||||
def addressable_children(self) -> list[AddressableNode]:
|
||||
return [
|
||||
child
|
||||
for child in self.exp.ds.top_node.children(unroll=self.unroll)
|
||||
if isinstance(child, AddressableNode)
|
||||
]
|
||||
return self.exp.ds.get_addressable_children_at_depth(unroll=self.unroll)
|
||||
|
||||
@property
|
||||
def addr_width(self) -> int:
|
||||
|
||||
@@ -27,6 +27,17 @@ class FaninGenerator(BusDecoderListener):
|
||||
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
|
||||
action = super().enter_AddressableComponent(node)
|
||||
|
||||
should_generate = action == WalkerAction.SkipDescendants
|
||||
if not should_generate and self._ds.max_decode_depth == 0:
|
||||
for child in node.children():
|
||||
if isinstance(child, AddressableNode):
|
||||
break
|
||||
else:
|
||||
should_generate = True
|
||||
|
||||
if not should_generate:
|
||||
return action
|
||||
|
||||
if node.array_dimensions:
|
||||
for i, dim in enumerate(node.array_dimensions):
|
||||
fb = ForLoopBody(
|
||||
|
||||
@@ -23,6 +23,17 @@ class FanoutGenerator(BusDecoderListener):
|
||||
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
|
||||
action = super().enter_AddressableComponent(node)
|
||||
|
||||
should_generate = action == WalkerAction.SkipDescendants
|
||||
if not should_generate and self._ds.max_decode_depth == 0:
|
||||
for child in node.children():
|
||||
if isinstance(child, AddressableNode):
|
||||
break
|
||||
else:
|
||||
should_generate = True
|
||||
|
||||
if not should_generate:
|
||||
return action
|
||||
|
||||
if node.array_dimensions:
|
||||
for i, dim in enumerate(node.array_dimensions):
|
||||
fb = ForLoopBody(
|
||||
|
||||
@@ -85,6 +85,20 @@ class DecodeLogicGenerator(BusDecoderListener):
|
||||
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
|
||||
action = super().enter_AddressableComponent(node)
|
||||
|
||||
should_decode = action == WalkerAction.SkipDescendants
|
||||
|
||||
if not should_decode and self._ds.max_decode_depth == 0:
|
||||
# When decoding all levels, treat leaf registers as decode boundary
|
||||
for child in node.children():
|
||||
if isinstance(child, AddressableNode):
|
||||
break
|
||||
else:
|
||||
should_decode = True
|
||||
|
||||
# Only generate select logic if we're at the decode boundary
|
||||
if not should_decode:
|
||||
return action
|
||||
|
||||
conditions: list[str] = []
|
||||
conditions.extend(self.cpuif_addr_predicate(node))
|
||||
conditions.extend(self.cpuif_prot_predicate(node))
|
||||
@@ -146,6 +160,8 @@ class DecodeLogicGenerator(BusDecoderListener):
|
||||
def __str__(self) -> str:
|
||||
body = self._decode_stack[-1]
|
||||
if isinstance(body, IfBody):
|
||||
if len(body) == 0:
|
||||
return f"{self._flavor.cpuif_select}.cpuif_err = 1'b1;"
|
||||
with body.cm(...) as b:
|
||||
b += f"{self._flavor.cpuif_select}.cpuif_err = 1'b1;"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import TypedDict
|
||||
|
||||
from systemrdl.node import AddrmapNode
|
||||
from systemrdl.node import AddressableNode, AddrmapNode
|
||||
from systemrdl.rdltypes.user_enum import UserEnum
|
||||
|
||||
from .design_scanner import DesignScanner
|
||||
@@ -72,3 +72,56 @@ class DesignState:
|
||||
if user_addr_width < self.addr_width:
|
||||
msg.fatal(f"User-specified address width shall be greater than or equal to {self.addr_width}.")
|
||||
self.addr_width = user_addr_width
|
||||
|
||||
def get_addressable_children_at_depth(self, unroll: bool = False) -> list[AddressableNode]:
|
||||
"""
|
||||
Get addressable children at the decode boundary based on max_decode_depth.
|
||||
|
||||
max_decode_depth semantics:
|
||||
- 0: decode all levels (return leaf registers)
|
||||
- 1: decode only top level (return children at depth 1)
|
||||
- 2: decode top + 1 level (return children at depth 2)
|
||||
- N: decode down to depth N (return children at depth N)
|
||||
|
||||
Args:
|
||||
unroll: Whether to unroll arrayed nodes
|
||||
|
||||
Returns:
|
||||
List of addressable nodes at the decode boundary
|
||||
"""
|
||||
from systemrdl.node import RegNode
|
||||
|
||||
def collect_nodes(node: AddressableNode, current_depth: int) -> list[AddressableNode]:
|
||||
"""Recursively collect nodes at the decode boundary."""
|
||||
result: list[AddressableNode] = []
|
||||
|
||||
# For depth 0, collect all leaf registers
|
||||
if self.max_decode_depth == 0:
|
||||
# If this is a register, it's a leaf
|
||||
if isinstance(node, RegNode):
|
||||
result.append(node)
|
||||
else:
|
||||
# Recurse into children
|
||||
for child in node.children(unroll=unroll):
|
||||
if isinstance(child, AddressableNode):
|
||||
result.extend(collect_nodes(child, current_depth + 1))
|
||||
else:
|
||||
# For depth N, collect children at depth N
|
||||
if current_depth == self.max_decode_depth:
|
||||
# We're at the decode boundary - return this node
|
||||
result.append(node)
|
||||
elif current_depth < self.max_decode_depth:
|
||||
# We haven't reached the boundary yet - recurse
|
||||
for child in node.children(unroll=unroll):
|
||||
if isinstance(child, AddressableNode):
|
||||
result.extend(collect_nodes(child, current_depth + 1))
|
||||
|
||||
return result
|
||||
|
||||
# Start collecting from top node's children
|
||||
nodes: list[AddressableNode] = []
|
||||
for child in self.top_node.children(unroll=unroll):
|
||||
if isinstance(child, AddressableNode):
|
||||
nodes.extend(collect_nodes(child, 1))
|
||||
|
||||
return nodes
|
||||
|
||||
@@ -89,7 +89,9 @@ class BusDecoderExporter:
|
||||
interface. By default, arrayed nodes are kept as arrays.
|
||||
max_decode_depth: int
|
||||
Maximum depth for address decoder to descend into nested addressable
|
||||
components. By default, the decoder descends 1 level deep.
|
||||
components. A value of 0 decodes all levels (infinite depth). A value
|
||||
of 1 decodes only top-level children. A value of 2 decodes top-level
|
||||
and one level deeper, etc. By default, the decoder descends 1 level deep.
|
||||
"""
|
||||
# If it is the root node, skip to top addrmap
|
||||
if isinstance(node, RootNode):
|
||||
|
||||
@@ -15,7 +15,12 @@ class BusDecoderListener(RDLListener):
|
||||
def should_skip_node(self, node: AddressableNode) -> bool:
|
||||
"""Check if this node should be skipped (not decoded)."""
|
||||
# Check if current depth exceeds max depth
|
||||
if self._depth > self._ds.max_decode_depth:
|
||||
# max_decode_depth semantics:
|
||||
# - 0 means decode all levels (infinite)
|
||||
# - 1 means decode only top level (depth 0)
|
||||
# - 2 means decode top + 1 level (depth 0 and 1)
|
||||
# - N means decode down to depth N-1
|
||||
if self._ds.max_decode_depth > 0 and self._depth >= self._ds.max_decode_depth:
|
||||
return True
|
||||
|
||||
# Check if this node only contains external addressable children
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections import deque
|
||||
from systemrdl.node import AddressableNode
|
||||
from systemrdl.walker import WalkerAction
|
||||
|
||||
from .body import Body, StructBody
|
||||
from .body import StructBody
|
||||
from .design_state import DesignState
|
||||
from .identifier_filter import kw_filter as kwf
|
||||
from .listener import BusDecoderListener
|
||||
@@ -16,30 +16,38 @@ class StructGenerator(BusDecoderListener):
|
||||
) -> None:
|
||||
super().__init__(ds)
|
||||
|
||||
self._stack: deque[Body] = deque()
|
||||
self._stack.append(StructBody("cpuif_sel_t", True, False))
|
||||
self._stack: list[StructBody] = [StructBody("cpuif_sel_t", True, False)]
|
||||
self._struct_defs: list[StructBody] = []
|
||||
self._created_struct_stack: deque[bool] = deque() # Track if we created a struct for each node
|
||||
|
||||
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
|
||||
action = super().enter_AddressableComponent(node)
|
||||
|
||||
self._skip = False
|
||||
if action == WalkerAction.SkipDescendants:
|
||||
self._skip = True
|
||||
skip = action == WalkerAction.SkipDescendants
|
||||
|
||||
if node.children():
|
||||
# Only create nested struct if we're not skipping and node has addressable children
|
||||
has_addressable_children = any(isinstance(child, AddressableNode) for child in node.children())
|
||||
if has_addressable_children and not skip:
|
||||
# Push new body onto stack
|
||||
body = StructBody(f"cpuif_sel_{node.inst_name}_t", True, False)
|
||||
self._stack.append(body)
|
||||
self._created_struct_stack.append(True)
|
||||
else:
|
||||
self._created_struct_stack.append(False)
|
||||
|
||||
return action
|
||||
|
||||
def exit_AddressableComponent(self, node: AddressableNode) -> None:
|
||||
type = "logic"
|
||||
|
||||
if node.children():
|
||||
# Pop the created_struct flag
|
||||
created_struct = self._created_struct_stack.pop()
|
||||
|
||||
# Only pop struct body if we created one
|
||||
if created_struct:
|
||||
body = self._stack.pop()
|
||||
if body and isinstance(body, StructBody) and not self._skip:
|
||||
self._stack.appendleft(body)
|
||||
if body:
|
||||
self._struct_defs.append(body)
|
||||
type = body.name
|
||||
|
||||
name = kwf(node.inst_name)
|
||||
@@ -53,5 +61,8 @@ class StructGenerator(BusDecoderListener):
|
||||
super().exit_AddressableComponent(node)
|
||||
|
||||
def __str__(self) -> str:
|
||||
self._stack[-1] += "logic cpuif_err;"
|
||||
return "\n".join(map(str, self._stack))
|
||||
if "logic cpuif_err;" not in self._stack[-1].lines:
|
||||
self._stack[-1] += "logic cpuif_err;"
|
||||
bodies = [str(body) for body in self._struct_defs]
|
||||
bodies.append(str(self._stack[-1]))
|
||||
return "\n".join(bodies)
|
||||
|
||||
Reference in New Issue
Block a user