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:
Copilot
2025-10-28 23:38:54 -07:00
committed by GitHub
parent 858a7870ad
commit d7481e71ba
22 changed files with 1518 additions and 26 deletions

View File

@@ -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.
""",
)

View File

@@ -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:

View File

@@ -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(

View File

@@ -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(

View File

@@ -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;"

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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)