This commit is contained in:
Arnav Sacheti
2025-10-13 18:39:19 -07:00
parent b4f9eaff71
commit 35015d7051
79 changed files with 2401 additions and 5601 deletions

View File

@@ -0,0 +1,3 @@
from .exporter import BusDecoderExporter
__all__ = ["BusDecoderExporter"]

View File

@@ -0,0 +1,120 @@
from typing import TYPE_CHECKING
import functools
from peakrdl.plugins.exporter import ExporterSubcommandPlugin
from peakrdl.config import schema
from peakrdl.plugins.entry_points import get_entry_points
from .exporter import BusDecoderExporter
from .cpuif import BaseCpuif, apb3, apb4
from .udps import ALL_UDPS
if TYPE_CHECKING:
import argparse
from systemrdl.node import AddrmapNode
class Exporter(ExporterSubcommandPlugin):
short_desc = "Generate a SystemVerilog control/status register (CSR) block"
udp_definitions = ALL_UDPS
cfg_schema = {
"cpuifs": {"*": schema.PythonObjectImport()},
}
@functools.lru_cache()
def get_cpuifs(self) -> dict[str, type[BaseCpuif]]:
# All built-in CPUIFs
cpuifs: dict[str, type[BaseCpuif]] = {
# "passthrough": passthrough.PassthroughCpuif,
"apb3": apb3.APB3Cpuif,
"apb3-flat": apb3.APB3CpuifFlat,
"apb4": apb4.APB4Cpuif,
"apb4-flat": apb4.APB4CpuifFlat,
# "axi4-lite": axi4lite.AXI4Lite_Cpuif,
# "axi4-lite-flat": axi4lite.AXI4Lite_Cpuif_flattened,
}
# Load any cpuifs specified via entry points
for ep, _ in get_entry_points("peakrdl_busdecoder.cpuif"):
name = ep.name
cpuif = ep.load()
if name in cpuifs:
raise RuntimeError(
f"A plugin for 'peakrdl-busdecoder' tried to load cpuif '{name}' but it already exists"
)
if not issubclass(cpuif, BaseCpuif):
raise RuntimeError(
f"A plugin for 'peakrdl-busdecoder' tried to load cpuif '{name}' but it not a BaseCpuif class"
)
cpuifs[name] = cpuif
# Load any CPUIFs via config import
for name, cpuif in self.cfg["cpuifs"].items():
if name in cpuifs:
raise RuntimeError(
f"A plugin for 'peakrdl-busdecoder' tried to load cpuif '{name}' but it already exists"
)
if not issubclass(cpuif, BaseCpuif):
raise RuntimeError(
f"A plugin for 'peakrdl-busdecoder' tried to load cpuif '{name}' but it not a BaseCpuif class"
)
cpuifs[name] = cpuif
return cpuifs
def add_exporter_arguments(self, arg_group: "argparse._ActionsContainer") -> None:
cpuifs = self.get_cpuifs()
arg_group.add_argument(
"--cpuif",
choices=cpuifs.keys(),
default="apb3",
help="Select the CPU interface protocol to use [apb3]",
)
arg_group.add_argument(
"--module-name",
metavar="NAME",
default=None,
help="Override the SystemVerilog module name",
)
arg_group.add_argument(
"--package-name",
metavar="NAME",
default=None,
help="Override the SystemVerilog package name",
)
arg_group.add_argument(
"--addr-width",
type=int,
default=None,
help="""Override the CPU interface's address width. By default,
address width is sized to the contents of the busdecoder.
""",
)
arg_group.add_argument(
"--unroll",
action="store_true",
help="""Unroll arrayed addressable nodes into separate instances in
the CPU interface. By default, arrayed nodes are kept as arrays.
""",
)
def do_export(self, top_node: "AddrmapNode", options: "argparse.Namespace") -> None:
cpuifs = self.get_cpuifs()
x = BusDecoderExporter()
x.export(
top_node,
options.output,
cpuif_cls=cpuifs[options.cpuif],
module_name=options.module_name,
package_name=options.package_name,
address_width=options.addr_width,
unroll=options.unroll,
)

View File

@@ -0,0 +1,162 @@
from typing import TYPE_CHECKING
from systemrdl.node import FieldNode, RegNode
from systemrdl.walker import WalkerAction
from .utils import get_indexed_path
from .forloop_generator import RDLForLoopGenerator
from .sv_int import SVInt
if TYPE_CHECKING:
from .exporter import BusDecoderExporter
from systemrdl.node import AddrmapNode, AddressableNode
class AddressDecode:
def __init__(self, exp: "BusDecoderExporter"):
self.exp = exp
@property
def top_node(self) -> "AddrmapNode":
return self.exp.ds.top_node
def get_implementation(self) -> str:
gen = DecodeLogicGenerator(self)
s = gen.get_content(self.top_node)
assert s is not None
return s
def get_access_strobe(
self, node: RegNode | FieldNode, reduce_substrobes: bool = True
) -> str:
"""
Returns the Verilog string that represents the register/field's access strobe.
"""
if isinstance(node, FieldNode):
field = node
path = get_indexed_path(self.top_node, node.parent)
regwidth = node.parent.get_property("regwidth")
accesswidth = node.parent.get_property("accesswidth")
if regwidth > accesswidth:
# Is wide register.
# Determine the substrobe(s) relevant to this field
sidx_hi = field.msb // accesswidth
sidx_lo = field.lsb // accesswidth
if sidx_hi == sidx_lo:
suffix = f"[{sidx_lo}]"
else:
suffix = f"[{sidx_hi}:{sidx_lo}]"
path += suffix
if sidx_hi != sidx_lo and reduce_substrobes:
return "|decoded_reg_strb." + path
else:
path = get_indexed_path(self.top_node, node)
return "decoded_reg_strb." + path
def get_external_block_access_strobe(self, node: "AddressableNode") -> str:
assert node.external
assert not isinstance(node, RegNode)
path = get_indexed_path(self.top_node, node)
return "decoded_reg_strb." + path
class DecodeLogicGenerator(RDLForLoopGenerator):
def __init__(self, addr_decode: AddressDecode) -> None:
self.addr_decode = addr_decode
super().__init__()
# List of address strides for each dimension
self._array_stride_stack: list[int] = []
def enter_AddressableComponent(
self, node: "AddressableNode"
) -> WalkerAction | None:
super().enter_AddressableComponent(node)
if node.array_dimensions:
assert node.array_stride is not None
# Collect strides for each array dimension
current_stride = node.array_stride
strides: list[int] = []
for dim in reversed(node.array_dimensions):
strides.append(current_stride)
current_stride *= dim
strides.reverse()
self._array_stride_stack.extend(strides)
if node.external and not isinstance(node, RegNode):
# Is an external block
addr_str = self._get_address_str(node)
strb = self.addr_decode.get_external_block_access_strobe(node)
rhs = f"cpuif_req_masked & (cpuif_addr >= {addr_str}) & (cpuif_addr <= {addr_str} + {SVInt(node.size - 1, self.addr_decode.exp.ds.addr_width)})"
self.add_content(f"{strb} = {rhs};")
self.add_content(f"is_external |= {rhs};")
return WalkerAction.SkipDescendants
return WalkerAction.Continue
def _get_address_str(self, node: "AddressableNode", subword_offset: int = 0) -> str:
expr_width = self.addr_decode.exp.ds.addr_width
a = str(
SVInt(
node.raw_absolute_address
- self.addr_decode.top_node.raw_absolute_address
+ subword_offset,
expr_width,
)
)
for i, stride in enumerate(self._array_stride_stack):
a += f" + ({expr_width})'(i{i}) * {SVInt(stride, expr_width)}"
return a
def enter_Reg(self, node: RegNode) -> None:
regwidth = node.get_property("regwidth")
accesswidth = node.get_property("accesswidth")
if regwidth == accesswidth:
rhs = f"cpuif_req_masked & (cpuif_addr == {self._get_address_str(node)})"
s = f"{self.addr_decode.get_access_strobe(node)} = {rhs};"
self.add_content(s)
if node.external:
readable = node.has_sw_readable
writable = node.has_sw_writable
if readable and writable:
self.add_content(f"is_external |= {rhs};")
elif readable and not writable:
self.add_content(f"is_external |= {rhs} & !cpuif_req_is_wr;")
elif not readable and writable:
self.add_content(f"is_external |= {rhs} & cpuif_req_is_wr;")
else:
raise RuntimeError
else:
# Register is wide. Create a substrobe for each subword
n_subwords = regwidth // accesswidth
subword_stride = accesswidth // 8
for i in range(n_subwords):
rhs = f"cpuif_req_masked & (cpuif_addr == {self._get_address_str(node, subword_offset=(i * subword_stride))})"
s = f"{self.addr_decode.get_access_strobe(node)}[{i}] = {rhs};"
self.add_content(s)
if node.external:
readable = node.has_sw_readable
writable = node.has_sw_writable
if readable and writable:
self.add_content(f"is_external |= {rhs};")
elif readable and not writable:
self.add_content(f"is_external |= {rhs} & !cpuif_req_is_wr;")
elif not readable and writable:
self.add_content(f"is_external |= {rhs} & cpuif_req_is_wr;")
else:
raise RuntimeError
def exit_AddressableComponent(self, node: "AddressableNode") -> None:
super().exit_AddressableComponent(node)
if not node.array_dimensions:
return
for _ in node.array_dimensions:
self._array_stride_stack.pop()

View File

@@ -0,0 +1,3 @@
from .base_cpuif import BaseCpuif
__all__ = ["BaseCpuif"]

View File

@@ -0,0 +1,4 @@
from .apb3_cpuif import APB3Cpuif
from .apb3_cpuif_flat import APB3CpuifFlat
__all__ = ["APB3Cpuif", "APB3CpuifFlat"]

View File

@@ -0,0 +1,79 @@
from systemrdl.node import AddressableNode
from ..base_cpuif import BaseCpuif
class APB3Cpuif(BaseCpuif):
template_path = "apb3_tmpl.sv"
is_interface = True
def _port_declaration(self, child: AddressableNode) -> str:
base = f"apb3_intf.master m_apb_{child.inst_name}"
if not child.is_array:
return base
if child.current_idx is not None:
return f"{base}_{'_'.join(map(str, child.current_idx))} [N_{child.inst_name.upper()}S]"
return f"{base} [N_{child.inst_name.upper()}S]"
@property
def port_declaration(self) -> str:
slave_ports: list[str] = ["apb3_intf.slave s_apb"]
master_ports: list[str] = list(
map(self._port_declaration, self.addressable_children)
)
return ",\n".join(slave_ports + master_ports)
def signal(
self,
signal: str,
node: AddressableNode | None = None,
idx: str | int | None = None,
) -> str:
if node is None:
# Node is none, so this is a slave signal
return f"s_apb.{signal}"
# Master signal
base = f"m_apb_{node.inst_name}"
if not node.is_array:
return f"{base}.{signal}"
if node.current_idx is not None:
# This is a specific instance of an array
return f"{base}_{'_'.join(map(str, node.current_idx))}.{signal}"
if idx is not None:
return f"{base}[{idx}].{signal}"
raise ValueError("Must provide an index for arrayed interface signals")
def get_address_predicate(self, node: AddressableNode) -> str:
"""
Returns a SystemVerilog expression that evaluates to true when the
address on the bus matches the address range of the given node.
"""
addr_mask = (1 << self.addr_width) - 1
addr = node.absolute_address & addr_mask
size = node.size
if size == 0:
raise ValueError("Node size must be greater than 0")
if (addr % size) != 0:
raise ValueError("Node address must be aligned to its size")
# Calculate the address range of the node
addr_start = addr
addr_end = addr + size - 1
if addr_end > addr_mask:
raise ValueError("Node address range exceeds address width")
if addr_start == addr_end:
return f"({self.signal('PADDR')} == 'h{addr_start:X})"
return f"({self.signal('PADDR')} >= 'h{addr_start:X} && {self.signal('PADDR')} <= 'h{addr_end:X})"
def get_address_decode_condition(self, node: AddressableNode) -> str:
"""
Returns a SystemVerilog expression that evaluates to true when the
address on the bus matches the address range of the given node.
"""
addr_pred = self.get_address_predicate(node)
return addr_pred

View File

@@ -0,0 +1,62 @@
from systemrdl.node import AddressableNode
from ..base_cpuif import BaseCpuif
class APB3CpuifFlat(BaseCpuif):
template_path = "apb3_tmpl.sv"
is_interface = False
def _port_declaration(self, child: AddressableNode) -> list[str]:
return [
f"input logic {self.signal('PCLK', child)}",
f"input logic {self.signal('PRESETn', child)}",
f"input logic {self.signal('PSELx', child)}",
f"input logic {self.signal('PENABLE', child)}",
f"input logic {self.signal('PWRITE', child)}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"output logic {self.signal('PREADY', child)}",
f"output logic {self.signal('PSLVERR', child)}",
]
@property
def port_declaration(self) -> str:
slave_ports: list[str] = [
f"input logic {self.signal('PCLK')}",
f"input logic {self.signal('PRESETn')}",
f"input logic {self.signal('PSELx')}",
f"input logic {self.signal('PENABLE')}",
f"input logic {self.signal('PWRITE')}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR')}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA')}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA')}",
f"output logic {self.signal('PREADY')}",
f"output logic {self.signal('PSLVERR')}",
]
master_ports: list[str] = []
for child in self.addressable_children:
master_ports.extend(self._port_declaration(child))
return ",\n".join(slave_ports + master_ports)
def signal(
self,
signal: str,
node: AddressableNode | None = None,
idx: str | int | None = None,
) -> str:
if node is None:
# Node is none, so this is a slave signal
return f"s_apb_{signal}"
# Master signal
base = f"m_apb_{node.inst_name}"
if not node.is_array:
return f"{base}_{signal}"
if node.current_idx is not None:
# This is a specific instance of an array
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}"
if idx is not None:
return f"{base}_{signal}[{idx}]"
return f"{base}_{signal}[N_{node.inst_name.upper()}S]"

View File

@@ -0,0 +1,115 @@
{%- if cpuif.is_interface -%}
`ifndef SYNTHESIS
initial begin
assert_bad_addr_width: assert($bits({{cpuif.signal("PADDR")}}) >= {{ds.package_name}}::{{ds.module_name|upper}}_MIN_ADDR_WIDTH)
else $error("Interface address width of %0d is too small. Shall be at least %0d bits", $bits({{cpuif.signal("PADDR")}}), {{ds.package_name}}::{{ds.module_name|upper}}_MIN_ADDR_WIDTH);
assert_bad_data_width: assert($bits({{cpuif.signal("PWDATA")}}) == {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH)
else $error("Interface data width of %0d is incorrect. Shall be %0d bits", $bits({{cpuif.signal("PWDATA")}}), {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH);
end
`endif
{% endif -%}
//======================================================
// APB Fanout
//======================================================
{%- for child in cpuif.addressable_children -%}
{%- if child is array -%}
for (genvar g_{{child.inst_name|lower}}_idx = 0; g_{{child.inst_name|lower}}_idx < N_{{child.inst_name|upper}}S; g_{{child.inst_name|lower}}_idx++) begin : g_passthrough_{{child.inst_name|lower}}
assign {{self.signal("PCLK", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PCLK")}};
assign {{self.signal("PRESETn", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PRESETn")}};
assign {{self.signal("PENABLE", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PENABLE")}};
assign {{self.signal("PWRITE", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PWRITE")}};
assign {{self.signal("PADDR", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PADDR")}} [{{child.addr_width - 1}}:0]; // FIXME: Check slicing
assign {{self.signal("PWDATA", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PWDATA")}};
end
{%- else -%}
assign {{self.signal("PCLK", child)}} = {{self.signal("PCLK")}};
assign {{self.signal("PRESETn", child)}} = {{self.signal("PRESETn")}};
assign {{self.signal("PENABLE", child)}} = {{self.signal("PENABLE")}};
assign {{self.signal("PWRITE", child)}} = {{self.signal("PWRITE")}};
assign {{self.signal("PADDR", child)}} = {{self.signal("PADDR")}} [{{child.addr_width - 1}}:0]; // FIXME: Check slicing
assign {{self.signal("PWDATA", child)}} = {{self.signal("PWDATA")}};
{%- endif -%}
{%- endfor -%}
//======================================================
// Address Decode Logic
//======================================================
always_comb begin
// Default all PSELx signals to 0
{%- for child in cpuif.addressable_children -%}
{%- if child is array -%}
for (int {{child.inst_name|lower}}_idx = 0; {{child.inst_name|lower}}_idx < N_{{child.inst_name|upper}}S; {{child.inst_name|lower}}_idx++) begin
{{self.signal("PSELx", child, f"{child.inst_name.lower()}_idx")}} = 1'b0;
end
{%- else -%}
{{self.signal("PSELx", child)}} = 1'b0;
{%- endif -%}
{%- endfor -%}
if ({{self.signal("PSELx")}}) begin
{%- for child in cpuif.addressable_children -%}
{%- if loop.first -%}
if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- else -%}
end else if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- endif -%}
// Address matched for {{child.inst_name}}
{%- if child is array -%}
for (genvar {{child.inst_name|lower}}_idx = 0; {{child.inst_name|lower}}_idx < N_{{child.inst_name|upper}}S; {{child.inst_name|lower}}_idx++) begin
{{self.signal("PSELx", child, f"{child.inst_name.lower()}_idx")}} = 1'b1;
end
{%- else -%}
{{self.signal("PSELx", child)}} = 1'b1;
{%- endif -%}
{%- if loop.last -%}
end else begin
// No address matched
{%- endif -%}
{%- endfor -%}
end
end else begin
// PSELx is low, nothing to do
end
end
//======================================================
// Read Data Mux
//======================================================
always_comb begin
// Default read data to 0
{{self.signal("PRDATA")}} = '0;
{{self.signal("PREADY")}} = 1'b1;
{{self.signal("PSLVERR")}} = 1'b0;
if ({{self.signal("PSELx")}} && !{{self.signal("PWRITE")}} && {{self.signal("PENABLE")}}) begin
{%- for child in cpuif.addressable_children -%}
{%- if loop.first -%}
if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- else -%}
end else if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- endif -%}
// Address matched for {{child.inst_name}}
{%- if child is array -%}
for (genvar {{child.inst_name|lower}}_idx = 0; {{child.inst_name|lower}}_idx < N_{{child.inst_name|upper}}S; {{child.inst_name|lower}}_idx++) begin
{{self.signal("PRDATA")}} = {{self.signal("PRDATA", child, f"{child.inst_name.lower()}_idx")}};
{{self.signal("PREADY")}} = {{self.signal("PREADY", child, f"{child.inst_name.lower()}_idx")}};
{{self.signal("PSLVERR")}} = {{self.signal("PSLVERR", child, f"{child.inst_name.lower()}_idx")}};
end
{%- else -%}
{{self.signal("PRDATA")}} = {{self.signal("PRDATA", child)}};
{{self.signal("PREADY")}} = {{self.signal("PREADY", child)}};
{{self.signal("PSLVERR")}} = {{self.signal("PSLVERR", child)}};
{%- endif -%}
{%- if loop.last -%}
end else begin
// No address matched
{{self.signal("PRDATA")}} = {'hdeadbeef}[{{ds.data_width - 1}}:0]; // Indicate error on no match
{{self.signal("PSLVERR")}} = 1'b1; // Indicate error on no match
end
{%- endif -%}
{%- endfor -%}
end else begin
// Not a read transfer, nothing to do
end
end

View File

@@ -0,0 +1,4 @@
from .apb4_cpuif import APB4Cpuif
from .apb4_cpuif_flat import APB4CpuifFlat
__all__ = ["APB4Cpuif", "APB4CpuifFlat"]

View File

@@ -0,0 +1,81 @@
from systemrdl.node import AddressableNode
from ..base_cpuif import BaseCpuif
class APB4Cpuif(BaseCpuif):
template_path = "apb4_tmpl.sv"
is_interface = True
def _port_declaration(self, child: AddressableNode) -> str:
base = f"apb4_intf.master m_apb_{child.inst_name}"
if not child.is_array:
return base
if child.current_idx is not None:
return f"{base}_{'_'.join(map(str, child.current_idx))} [N_{child.inst_name.upper()}S]"
return f"{base} [N_{child.inst_name.upper()}S]"
@property
def port_declaration(self) -> str:
"""Returns the port declaration for the APB4 interface."""
slave_ports: list[str] = ["apb4_intf.slave s_apb"]
master_ports: list[str] = list(
map(self._port_declaration, self.addressable_children)
)
return ",\n".join(slave_ports + master_ports)
def signal(
self,
signal: str,
node: AddressableNode | None = None,
idx: str | int | None = None,
) -> str:
"""Returns the signal name for the given signal and node."""
if node is None:
# Node is none, so this is a slave signal
return f"s_apb.{signal}"
# Master signal
base = f"m_apb_{node.inst_name}"
if not node.is_array:
return f"{base}.{signal}"
if node.current_idx is not None:
# This is a specific instance of an array
return f"{base}_{'_'.join(map(str, node.current_idx))}.{signal}"
if idx is not None:
return f"{base}[{idx}].{signal}"
raise ValueError("Must provide an index for arrayed interface signals")
def get_address_predicate(self, node: AddressableNode) -> str:
"""
Returns a SystemVerilog expression that evaluates to true when the
address on the bus matches the address range of the given node.
"""
addr_mask = (1 << self.addr_width) - 1
addr = node.absolute_address & addr_mask
size = node.size
if size == 0:
raise ValueError("Node size must be greater than 0")
if (addr % size) != 0:
raise ValueError("Node address must be aligned to its size")
# Calculate the address range of the node
addr_start = addr
addr_end = addr + size - 1
if addr_end > addr_mask:
raise ValueError("Node address range exceeds address width")
if addr_start == addr_end:
return f"({self.signal('PADDR')} == 'h{addr_start:X})"
return f"({self.signal('PADDR')} >= 'h{addr_start:X} && {self.signal('PADDR')} <= 'h{addr_end:X})"
def get_address_decode_condition(self, node: AddressableNode) -> str:
"""
Returns a SystemVerilog expression that evaluates to true when the
address on the bus matches the address range of the given node.
"""
addr_pred = self.get_address_predicate(node)
return addr_pred

View File

@@ -0,0 +1,66 @@
from systemrdl.node import AddressableNode
from ..base_cpuif import BaseCpuif
class APB4CpuifFlat(BaseCpuif):
template_path = "apb4_tmpl.sv"
is_interface = False
def _port_declaration(self, child: AddressableNode) -> list[str]:
return [
f"input logic {self.signal('PCLK', child)}",
f"input logic {self.signal('PRESETn', child)}",
f"input logic {self.signal('PSELx', child)}",
f"input logic {self.signal('PENABLE', child)}",
f"input logic {self.signal('PWRITE', child)}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}",
f"input logic [2:0] {self.signal('PPROT', child)}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}",
f"input logic [{self.data_width // 8 - 1}:0] {self.signal('PSTRB', child)}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"output logic {self.signal('PREADY', child)}",
f"output logic {self.signal('PSLVERR', child)}",
]
@property
def port_declaration(self) -> str:
slave_ports: list[str] = [
f"input logic {self.signal('PCLK')}",
f"input logic {self.signal('PRESETn')}",
f"input logic {self.signal('PSELx')}",
f"input logic {self.signal('PENABLE')}",
f"input logic {self.signal('PWRITE')}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR')}",
f"input logic [2:0] {self.signal('PPROT')}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA')}",
f"input logic [{self.data_width // 8 - 1}:0] {self.signal('PSTRB')}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA')}",
f"output logic {self.signal('PREADY')}",
f"output logic {self.signal('PSLVERR')}",
]
master_ports: list[str] = []
for child in self.addressable_children:
master_ports.extend(self._port_declaration(child))
return ",\n".join(slave_ports + master_ports)
def signal(
self,
signal: str,
node: AddressableNode | None = None,
idx: str | int | None = None,
) -> str:
if node is None:
# Node is none, so this is a slave signal
return f"s_apb_{signal}"
# Master signal
base = f"m_apb_{node.inst_name}"
if not node.is_array:
return f"{base}_{signal}"
if node.current_idx is not None:
# This is a specific instance of an array
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}"
if idx is not None:
return f"{base}_{signal}[{idx}]"
return f"{base}_{signal}[N_{node.inst_name.upper()}S]"

View File

@@ -0,0 +1,119 @@
{%- if cpuif.is_interface -%}
`ifndef SYNTHESIS
initial begin
assert_bad_addr_width: assert($bits({{cpuif.signal("PADDR")}}) >= {{ds.package_name}}::{{ds.module_name|upper}}_MIN_ADDR_WIDTH)
else $error("Interface address width of %0d is too small. Shall be at least %0d bits", $bits({{cpuif.signal("PADDR")}}), {{ds.package_name}}::{{ds.module_name|upper}}_MIN_ADDR_WIDTH);
assert_bad_data_width: assert($bits({{cpuif.signal("PWDATA")}}) == {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH)
else $error("Interface data width of %0d is incorrect. Shall be %0d bits", $bits({{cpuif.signal("PWDATA")}}), {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH);
end
`endif
{% endif -%}
//======================================================
// APB Fanout
//======================================================
{%- for child in cpuif.addressable_children -%}
{%- if child is array -%}
for (genvar g_{{child.inst_name|lower}}_idx = 0; g_{{child.inst_name|lower}}_idx < N_{{child.inst_name|upper}}S; g_{{child.inst_name|lower}}_idx++) begin : g_passthrough_{{child.inst_name|lower}}
assign {{self.signal("PCLK", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PCLK")}};
assign {{self.signal("PRESETn", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PRESETn")}};
assign {{self.signal("PENABLE", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PENABLE")}};
assign {{self.signal("PWRITE", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PWRITE")}};
assign {{self.signal("PADDR", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PADDR")}} [{{child.addr_width - 1}}:0]; // FIXME: Check slicing
assign {{self.signal("PPROT", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PPROT")}};
assign {{self.signal("PWDATA", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PWDATA")}};
assign {{self.signal("PSTRB", child, f"g_{child.inst_name.lower()}_idx")}} = {{self.signal("PSTRB")}};
end
{%- else -%}
assign {{self.signal("PCLK", child)}} = {{self.signal("PCLK")}};
assign {{self.signal("PRESETn", child)}} = {{self.signal("PRESETn")}};
assign {{self.signal("PENABLE", child)}} = {{self.signal("PENABLE")}};
assign {{self.signal("PWRITE", child)}} = {{self.signal("PWRITE")}};
assign {{self.signal("PADDR", child)}} = {{self.signal("PADDR")}} [{{child.addr_width - 1}}:0]; // FIXME: Check slicing
assign {{self.signal("PPROT", child)}} = {{self.signal("PPROT")}};
assign {{self.signal("PWDATA", child)}} = {{self.signal("PWDATA")}};
assign {{self.signal("PSTRB", child)}} = {{self.signal("PSTRB")}};
{%- endif -%}
{%- endfor -%}
//======================================================
// Address Decode Logic
//======================================================
always_comb begin
// Default all PSELx signals to 0
{%- for child in cpuif.addressable_children -%}
{%- if child is array -%}
for (int {{child.inst_name|lower}}_idx = 0; {{child.inst_name|lower}}_idx < N_{{child.inst_name|upper}}S; {{child.inst_name|lower}}_idx++) begin
{{self.signal("PSELx", child, f"{child.inst_name.lower()}_idx")}} = 1'b0;
end
{%- else -%}
{{self.signal("PSELx", child)}} = 1'b0;
{%- endif -%}
{%- endfor -%}
if ({{self.signal("PSELx")}}) begin
{%- for child in cpuif.addressable_children -%}
{%- if loop.first -%}
if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- else -%}
end else if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- endif -%}
// Address matched for {{child.inst_name}}
{%- if child is array -%}
for (genvar {{child.inst_name|lower}}_idx = 0; {{child.inst_name|lower}}_idx < N_{{child.inst_name|upper}}S; {{child.inst_name|lower}}_idx++) begin
{{self.signal("PSELx", child, f"{child.inst_name.lower()}_idx")}} = 1'b1;
end
{%- else -%}
{{self.signal("PSELx", child)}} = 1'b1;
{%- endif -%}
{%- if loop.last -%}
end else begin
// No address matched
{%- endif -%}
{%- endfor -%}
end
end else begin
// PSELx is low, nothing to do
end
end
//======================================================
// Read Data Mux
//======================================================
always_comb begin
// Default read data to 0
{{self.signal("PRDATA")}} = '0;
{{self.signal("PREADY")}} = 1'b1;
{{self.signal("PSLVERR")}} = 1'b0;
if ({{self.signal("PSELx")}} && !{{self.signal("PWRITE")}} && {{self.signal("PENABLE")}}) begin
{%- for child in cpuif.addressable_children -%}
{%- if loop.first -%}
if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- else -%}
end else if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- endif -%}
// Address matched for {{child.inst_name}}
{%- if child is array -%}
for (genvar {{child.inst_name|lower}}_idx = 0; {{child.inst_name|lower}}_idx < N_{{child.inst_name|upper}}S; {{child.inst_name|lower}}_idx++) begin
{{self.signal("PRDATA")}} = {{self.signal("PRDATA", child, f"{child.inst_name.lower()}_idx")}};
{{self.signal("PREADY")}} = {{self.signal("PREADY", child, f"{child.inst_name.lower()}_idx")}};
{{self.signal("PSLVERR")}} = {{self.signal("PSLVERR", child, f"{child.inst_name.lower()}_idx")}};
end
{%- else -%}
{{self.signal("PRDATA")}} = {{self.signal("PRDATA", child)}};
{{self.signal("PREADY")}} = {{self.signal("PREADY", child)}};
{{self.signal("PSLVERR")}} = {{self.signal("PSLVERR", child)}};
{%- endif -%}
{%- if loop.last -%}
end else begin
// No address matched
{{self.signal("PRDATA")}} = {'hdeadbeef}[{{ds.data_width - 1}}:0]; // Indicate error on no match
{{self.signal("PSLVERR")}} = 1'b1; // Indicate error on no match
end
{%- endif -%}
{%- endfor -%}
end else begin
// Not a read transfer, nothing to do
end
end

View File

@@ -0,0 +1,67 @@
from ..base_cpuif import CpuifBase
class AXI4Lite_Cpuif(CpuifBase):
template_path = "axi4lite_tmpl.sv"
is_interface = True
@property
def port_declaration(self) -> str:
return "axi4lite_intf.slave s_axil"
def signal(self, name: str) -> str:
return "s_axil." + name.upper()
@property
def busdecoder_latency(self) -> int:
return max(self.exp.ds.min_read_latency, self.exp.ds.min_write_latency)
@property
def max_outstanding(self) -> int:
"""
Best pipelined performance is when the max outstanding transactions
is the design's latency + 2.
Anything beyond that does not have any effect, aside from adding unnecessary
logic and additional buffer-bloat latency.
"""
return self.busdecoder_latency + 2
@property
def resp_buffer_size(self) -> int:
"""
Response buffer size must be greater or equal to max outstanding
transactions to prevent response overrun.
"""
return self.max_outstanding
class AXI4Lite_Cpuif_flattened(AXI4Lite_Cpuif):
is_interface = False
@property
def port_declaration(self) -> str:
lines = [
"output logic " + self.signal("awready"),
"input wire " + self.signal("awvalid"),
f"input wire [{self.addr_width - 1}:0] " + self.signal("awaddr"),
"input wire [2:0] " + self.signal("awprot"),
"output logic " + self.signal("wready"),
"input wire " + self.signal("wvalid"),
f"input wire [{self.data_width - 1}:0] " + self.signal("wdata"),
f"input wire [{self.data_width_bytes - 1}:0]" + self.signal("wstrb"),
"input wire " + self.signal("bready"),
"output logic " + self.signal("bvalid"),
"output logic [1:0] " + self.signal("bresp"),
"output logic " + self.signal("arready"),
"input wire " + self.signal("arvalid"),
f"input wire [{self.addr_width - 1}:0] " + self.signal("araddr"),
"input wire [2:0] " + self.signal("arprot"),
"input wire " + self.signal("rready"),
"output logic " + self.signal("rvalid"),
f"output logic [{self.data_width - 1}:0] " + self.signal("rdata"),
"output logic [1:0] " + self.signal("rresp"),
]
return ",\n".join(lines)
def signal(self, name: str) -> str:
return "s_axil_" + name

View File

@@ -0,0 +1,254 @@
{%- if cpuif.is_interface -%}
`ifndef SYNTHESIS
initial begin
assert_bad_addr_width: assert($bits({{cpuif.signal("araddr")}}) >= {{ds.package_name}}::{{ds.module_name.upper()}}_MIN_ADDR_WIDTH)
else $error("Interface address width of %0d is too small. Shall be at least %0d bits", $bits({{cpuif.signal("araddr")}}), {{ds.package_name}}::{{ds.module_name.upper()}}_MIN_ADDR_WIDTH);
assert_bad_data_width: assert($bits({{cpuif.signal("wdata")}}) == {{ds.package_name}}::{{ds.module_name.upper()}}_DATA_WIDTH)
else $error("Interface data width of %0d is incorrect. Shall be %0d bits", $bits({{cpuif.signal("wdata")}}), {{ds.package_name}}::{{ds.module_name.upper()}}_DATA_WIDTH);
end
`endif
{% endif -%}
// Max Outstanding Transactions: {{cpuif.max_outstanding}}
logic [{{clog2(cpuif.max_outstanding+1)-1}}:0] axil_n_in_flight;
logic axil_prev_was_rd;
logic axil_arvalid;
logic [{{cpuif.addr_width-1}}:0] axil_araddr;
logic axil_ar_accept;
logic axil_awvalid;
logic [{{cpuif.addr_width-1}}:0] axil_awaddr;
logic axil_wvalid;
logic [{{cpuif.data_width-1}}:0] axil_wdata;
logic [{{cpuif.data_width_bytes-1}}:0] axil_wstrb;
logic axil_aw_accept;
logic axil_resp_acked;
// Transaction request acceptance
always_ff {{get_always_ff_event(cpuif.reset)}} begin
if({{get_resetsignal(cpuif.reset)}}) begin
axil_prev_was_rd <= '0;
axil_arvalid <= '0;
axil_araddr <= '0;
axil_awvalid <= '0;
axil_awaddr <= '0;
axil_wvalid <= '0;
axil_wdata <= '0;
axil_wstrb <= '0;
axil_n_in_flight <= '0;
end else begin
// AR* acceptance register
if(axil_ar_accept) begin
axil_prev_was_rd <= '1;
axil_arvalid <= '0;
end
if({{cpuif.signal("arvalid")}} && {{cpuif.signal("arready")}}) begin
axil_arvalid <= '1;
axil_araddr <= {{cpuif.signal("araddr")}};
end
// AW* & W* acceptance registers
if(axil_aw_accept) begin
axil_prev_was_rd <= '0;
axil_awvalid <= '0;
axil_wvalid <= '0;
end
if({{cpuif.signal("awvalid")}} && {{cpuif.signal("awready")}}) begin
axil_awvalid <= '1;
axil_awaddr <= {{cpuif.signal("awaddr")}};
end
if({{cpuif.signal("wvalid")}} && {{cpuif.signal("wready")}}) begin
axil_wvalid <= '1;
axil_wdata <= {{cpuif.signal("wdata")}};
axil_wstrb <= {{cpuif.signal("wstrb")}};
end
// Keep track of in-flight transactions
if((axil_ar_accept || axil_aw_accept) && !axil_resp_acked) begin
axil_n_in_flight <= axil_n_in_flight + 1'b1;
end else if(!(axil_ar_accept || axil_aw_accept) && axil_resp_acked) begin
axil_n_in_flight <= axil_n_in_flight - 1'b1;
end
end
end
always_comb begin
{{cpuif.signal("arready")}} = (!axil_arvalid || axil_ar_accept);
{{cpuif.signal("awready")}} = (!axil_awvalid || axil_aw_accept);
{{cpuif.signal("wready")}} = (!axil_wvalid || axil_aw_accept);
end
// Request dispatch
always_comb begin
cpuif_wr_data = axil_wdata;
for(int i=0; i<{{cpuif.data_width_bytes}}; i++) begin
cpuif_wr_biten[i*8 +: 8] = {8{axil_wstrb[i]}};
end
cpuif_req = '0;
cpuif_req_is_wr = '0;
cpuif_addr = '0;
axil_ar_accept = '0;
axil_aw_accept = '0;
if(axil_n_in_flight < {{clog2(cpuif.max_outstanding+1)}}'d{{cpuif.max_outstanding}}) begin
// Can safely issue more transactions without overwhelming response buffer
if(axil_arvalid && !axil_prev_was_rd) begin
cpuif_req = '1;
cpuif_req_is_wr = '0;
{%- if cpuif.data_width_bytes == 1 %}
cpuif_addr = axil_araddr;
{%- else %}
cpuif_addr = {axil_araddr[{{cpuif.addr_width-1}}:{{clog2(cpuif.data_width_bytes)}}], {{clog2(cpuif.data_width_bytes)}}'b0};
{%- endif %}
if(!cpuif_req_stall_rd) axil_ar_accept = '1;
end else if(axil_awvalid && axil_wvalid) begin
cpuif_req = '1;
cpuif_req_is_wr = '1;
{%- if cpuif.data_width_bytes == 1 %}
cpuif_addr = axil_awaddr;
{%- else %}
cpuif_addr = {axil_awaddr[{{cpuif.addr_width-1}}:{{clog2(cpuif.data_width_bytes)}}], {{clog2(cpuif.data_width_bytes)}}'b0};
{%- endif %}
if(!cpuif_req_stall_wr) axil_aw_accept = '1;
end else if(axil_arvalid) begin
cpuif_req = '1;
cpuif_req_is_wr = '0;
{%- if cpuif.data_width_bytes == 1 %}
cpuif_addr = axil_araddr;
{%- else %}
cpuif_addr = {axil_araddr[{{cpuif.addr_width-1}}:{{clog2(cpuif.data_width_bytes)}}], {{clog2(cpuif.data_width_bytes)}}'b0};
{%- endif %}
if(!cpuif_req_stall_rd) axil_ar_accept = '1;
end
end
end
// AXI4-Lite Response Logic
{%- if cpuif.resp_buffer_size == 1 %}
always_ff {{get_always_ff_event(cpuif.reset)}} begin
if({{get_resetsignal(cpuif.reset)}}) begin
{{cpuif.signal("rvalid")}} <= '0;
{{cpuif.signal("rresp")}} <= '0;
{{cpuif.signal("rdata")}} <= '0;
{{cpuif.signal("bvalid")}} <= '0;
{{cpuif.signal("bresp")}} <= '0;
end else begin
if({{cpuif.signal("rvalid")}} && {{cpuif.signal("rready")}}) begin
{{cpuif.signal("rvalid")}} <= '0;
end
if({{cpuif.signal("bvalid")}} && {{cpuif.signal("bready")}}) begin
{{cpuif.signal("bvalid")}} <= '0;
end
if(cpuif_rd_ack) begin
{{cpuif.signal("rvalid")}} <= '1;
{{cpuif.signal("rdata")}} <= cpuif_rd_data;
if(cpuif_rd_err) {{cpuif.signal("rresp")}} <= 2'b10; // SLVERR
else {{cpuif.signal("rresp")}} <= 2'b00; // OKAY
end
if(cpuif_wr_ack) begin
{{cpuif.signal("bvalid")}} <= '1;
if(cpuif_wr_err) {{cpuif.signal("bresp")}} <= 2'b10; // SLVERR
else {{cpuif.signal("bresp")}} <= 2'b00; // OKAY
end
end
end
always_comb begin
axil_resp_acked = '0;
if({{cpuif.signal("rvalid")}} && {{cpuif.signal("rready")}}) axil_resp_acked = '1;
if({{cpuif.signal("bvalid")}} && {{cpuif.signal("bready")}}) axil_resp_acked = '1;
end
{%- else %}
struct {
logic is_wr;
logic err;
logic [{{cpuif.data_width-1}}:0] rdata;
} axil_resp_buffer[{{roundup_pow2(cpuif.resp_buffer_size)}}];
{%- if not is_pow2(cpuif.resp_buffer_size) %}
// axil_resp_buffer is intentionally padded to the next power of two despite
// only requiring {{cpuif.resp_buffer_size}} entries.
// This is to avoid quirks in some tools that cannot handle indexing into a non-power-of-2 array.
// Unused entries are expected to be optimized away
{% endif %}
logic [{{clog2(cpuif.resp_buffer_size)}}:0] axil_resp_wptr;
logic [{{clog2(cpuif.resp_buffer_size)}}:0] axil_resp_rptr;
always_ff {{get_always_ff_event(cpuif.reset)}} begin
if({{get_resetsignal(cpuif.reset)}}) begin
for(int i=0; i<{{cpuif.resp_buffer_size}}; i++) begin
axil_resp_buffer[i].is_wr <= '0;
axil_resp_buffer[i].err <= '0;
axil_resp_buffer[i].rdata <= '0;
end
axil_resp_wptr <= '0;
axil_resp_rptr <= '0;
end else begin
// Store responses in buffer until AXI response channel accepts them
if(cpuif_rd_ack || cpuif_wr_ack) begin
if(cpuif_rd_ack) begin
axil_resp_buffer[axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].is_wr <= '0;
axil_resp_buffer[axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].err <= cpuif_rd_err;
axil_resp_buffer[axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].rdata <= cpuif_rd_data;
end else if(cpuif_wr_ack) begin
axil_resp_buffer[axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].is_wr <= '1;
axil_resp_buffer[axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].err <= cpuif_wr_err;
end
{%- if is_pow2(cpuif.resp_buffer_size) %}
axil_resp_wptr <= axil_resp_wptr + 1'b1;
{%- else %}
if(axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] == {{cpuif.resp_buffer_size-1}}) begin
axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] <= '0;
axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)}}] <= ~axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)}}];
end else begin
axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] <= axil_resp_wptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] + 1'b1;
end
{%- endif %}
end
// Advance read pointer when acknowledged
if(axil_resp_acked) begin
{%- if is_pow2(cpuif.resp_buffer_size) %}
axil_resp_rptr <= axil_resp_rptr + 1'b1;
{%- else %}
if(axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] == {{cpuif.resp_buffer_size-1}}) begin
axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] <= '0;
axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)}}] <= ~axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)}}];
end else begin
axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] <= axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0] + 1'b1;
end
{%- endif %}
end
end
end
always_comb begin
axil_resp_acked = '0;
{{cpuif.signal("bvalid")}} = '0;
{{cpuif.signal("rvalid")}} = '0;
if(axil_resp_rptr != axil_resp_wptr) begin
if(axil_resp_buffer[axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].is_wr) begin
{{cpuif.signal("bvalid")}} = '1;
if({{cpuif.signal("bready")}}) axil_resp_acked = '1;
end else begin
{{cpuif.signal("rvalid")}} = '1;
if({{cpuif.signal("rready")}}) axil_resp_acked = '1;
end
end
{{cpuif.signal("rdata")}} = axil_resp_buffer[axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].rdata;
if(axil_resp_buffer[axil_resp_rptr[{{clog2(cpuif.resp_buffer_size)-1}}:0]].err) begin
{{cpuif.signal("bresp")}} = 2'b10;
{{cpuif.signal("rresp")}} = 2'b10;
end else begin
{{cpuif.signal("bresp")}} = 2'b00;
{{cpuif.signal("rresp")}} = 2'b00;
end
end
{%- endif %}

View File

@@ -0,0 +1,92 @@
from typing import TYPE_CHECKING
import inspect
import os
import jinja2 as jj
from systemrdl.node import AddressableNode
from ..utils import clog2, is_pow2, roundup_pow2
if TYPE_CHECKING:
from ..exporter import BusDecoderExporter
class BaseCpuif:
# Path is relative to the location of the class that assigns this variable
template_path = ""
def __init__(self, exp: "BusDecoderExporter") -> None:
self.exp = exp
self.reset = exp.ds.top_node.cpuif_reset
self.unroll = exp.ds.cpuif_unroll
@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)
]
@property
def addr_width(self) -> int:
return self.exp.ds.addr_width
@property
def data_width(self) -> int:
return self.exp.ds.cpuif_data_width
@property
def data_width_bytes(self) -> int:
return self.data_width // 8
@property
def port_declaration(self) -> str:
raise NotImplementedError()
@property
def parameters(self) -> list[str]:
"""
Optional list of additional parameters this CPU interface provides to
the module's definition
"""
array_parameters = [
f"parameter N_{child.inst_name.upper()}S = {child.n_elements}"
for child in self.addressable_children
if self.check_is_array(child)
]
return array_parameters
def _get_template_path_class_dir(self) -> str:
"""
Traverse up the MRO and find the first class that explicitly assigns
template_path. Returns the directory that contains the class definition.
"""
for cls in inspect.getmro(self.__class__):
if "template_path" in cls.__dict__:
class_dir = os.path.dirname(inspect.getfile(cls))
return class_dir
raise RuntimeError
def check_is_array(self, node: AddressableNode) -> bool:
return node.is_array and not self.unroll
def get_implementation(self) -> str:
class_dir = self._get_template_path_class_dir()
loader = jj.FileSystemLoader(class_dir)
jj_env = jj.Environment(
loader=loader,
undefined=jj.StrictUndefined,
)
jj_env.tests["array"] = self.check_is_array # type: ignore
jj_env.filters["clog2"] = clog2 # type: ignore
jj_env.filters["is_pow2"] = is_pow2 # type: ignore
jj_env.filters["roundup_pow2"] = roundup_pow2 # type: ignore
context = {
"cpuif": self,
"ds": self.exp.ds,
}
template = jj_env.get_template(self.template_path)
return template.render(context)

View File

@@ -0,0 +1,42 @@
from typing import TYPE_CHECKING
from systemrdl.node import AddrmapNode, FieldNode, RegNode, AddressableNode
if TYPE_CHECKING:
from .exporter import BusDecoderExporter, DesignState
from .addr_decode import AddressDecode
class Dereferencer:
"""
This class provides an interface to convert conceptual SystemRDL references
into Verilog identifiers
"""
def __init__(self, exp: "BusDecoderExporter"):
self.exp = exp
@property
def address_decode(self) -> "AddressDecode":
return self.exp.address_decode
@property
def ds(self) -> "DesignState":
return self.exp.ds
@property
def top_node(self) -> AddrmapNode:
return self.exp.ds.top_node
def get_access_strobe(
self, obj: RegNode | FieldNode, reduce_substrobes: bool = True
) -> str:
"""
Returns the Verilog string that represents the register's access strobe
"""
return self.address_decode.get_access_strobe(obj, reduce_substrobes)
def get_external_block_access_strobe(self, obj: "AddressableNode") -> str:
"""
Returns the Verilog string that represents the external block's access strobe
"""
return self.address_decode.get_external_block_access_strobe(obj)

View File

@@ -0,0 +1,184 @@
import os
from typing import TYPE_CHECKING, Any
import jinja2 as jj
from systemrdl.node import AddrmapNode, RootNode
from systemrdl.rdltypes.user_enum import UserEnum
from .addr_decode import AddressDecode
from .dereferencer import Dereferencer
from .identifier_filter import kw_filter as kwf
from .utils import clog2
from .scan_design import DesignScanner
from .validate_design import DesignValidator
from .cpuif import BaseCpuif
from .cpuif.apb4 import APB4Cpuif
from .sv_int import SVInt
if TYPE_CHECKING:
pass
class BusDecoderExporter:
cpuif: BaseCpuif
address_decode: AddressDecode
dereferencer: Dereferencer
ds: "DesignState"
def __init__(self, **kwargs: Any) -> None:
# Check for stray kwargs
if kwargs:
raise TypeError(
f"got an unexpected keyword argument '{list(kwargs.keys())[0]}'"
)
loader = jj.ChoiceLoader(
[
jj.FileSystemLoader(os.path.dirname(__file__)),
jj.PrefixLoader(
{
"base": jj.FileSystemLoader(os.path.dirname(__file__)),
},
delimiter=":",
),
]
)
self.jj_env = jj.Environment(
loader=loader,
undefined=jj.StrictUndefined,
)
def export(
self, node: RootNode | AddrmapNode, output_dir: str, **kwargs: Any
) -> None:
"""
Parameters
----------
node: AddrmapNode
Top-level SystemRDL node to export.
output_dir: str
Path to the output directory where generated SystemVerilog will be written.
Output includes two files: a module definition and package definition.
cpuif_cls: :class:`peakrdl_busdecoder.cpuif.CpuifBase`
Specify the class type that implements the CPU interface of your choice.
Defaults to AMBA APB4.
module_name: str
Override the SystemVerilog module name. By default, the module name
is the top-level node's name.
package_name: str
Override the SystemVerilog package name. By default, the package name
is the top-level node's name with a "_pkg" suffix.
address_width: int
Override the CPU interface's address width. By default, address width
is sized to the contents of the busdecoder.
unroll: bool
Unroll arrayed addressable nodes into separate instances in the CPU
interface. By default, arrayed nodes are kept as arrays.
"""
# If it is the root node, skip to top addrmap
if isinstance(node, RootNode):
top_node = node.top
else:
top_node = node
self.ds = DesignState(top_node, kwargs)
cpuif_cls: type[BaseCpuif] = kwargs.pop("cpuif_cls", None) or APB4Cpuif
# Check for stray kwargs
if kwargs:
raise TypeError(
f"got an unexpected keyword argument '{list(kwargs.keys())[0]}'"
)
# Construct exporter components
self.cpuif = cpuif_cls(self)
self.address_decode = AddressDecode(self)
self.dereferencer = Dereferencer(self)
# Validate that there are no unsupported constructs
DesignValidator(self).do_validate()
# Build Jinja template context
context = {
"cpuif": self.cpuif,
"address_decode": self.address_decode,
"ds": self.ds,
"kwf": kwf,
"SVInt": SVInt,
}
# Write out design
os.makedirs(output_dir, exist_ok=True)
package_file_path = os.path.join(output_dir, self.ds.package_name + ".sv")
template = self.jj_env.get_template("package_tmpl.sv")
stream = template.stream(context)
stream.dump(package_file_path)
module_file_path = os.path.join(output_dir, self.ds.module_name + ".sv")
template = self.jj_env.get_template("module_tmpl.sv")
stream = template.stream(context)
stream.dump(module_file_path)
if hwif_report_file:
hwif_report_file.close()
class DesignState:
"""
Dumping ground for all sorts of variables that are relevant to a particular
design.
"""
def __init__(self, top_node: AddrmapNode, kwargs: Any) -> None:
self.top_node = top_node
msg = top_node.env.msg
# ------------------------
# Extract compiler args
# ------------------------
self.reuse_hwif_typedefs: bool = kwargs.pop("reuse_hwif_typedefs", True)
self.module_name: str = kwargs.pop("module_name", None) or kwf(
self.top_node.inst_name
)
self.package_name: str = kwargs.pop("package_name", None) or (
self.module_name + "_pkg"
)
user_addr_width: int | None = kwargs.pop("address_width", None)
self.cpuif_unroll: bool = kwargs.pop("cpuif_unroll", False)
# ------------------------
# Info about the design
# ------------------------
self.cpuif_data_width = 0
# Track any referenced enums
self.user_enums: list[type[UserEnum]] = []
# Scan the design to fill in above variables
DesignScanner(self).do_scan()
if self.cpuif_data_width == 0:
# Scanner did not find any registers in the design being exported,
# so the width is not known.
# Assume 32-bits
msg.warning(
"Addrmap being exported only contains external components. Unable to infer the CPUIF bus width. Assuming 32-bits.",
self.top_node.inst.def_src_ref,
)
self.cpuif_data_width = 32
# ------------------------
# Min address width encloses the total size AND at least 1 useful address bit
self.addr_width = max(
clog2(self.top_node.size), clog2(self.cpuif_data_width // 8) + 1
)
if user_addr_width is not None:
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

View File

@@ -0,0 +1,101 @@
from typing import TYPE_CHECKING
import textwrap
from systemrdl.walker import RDLListener, RDLWalker, WalkerAction
if TYPE_CHECKING:
from systemrdl.node import AddressableNode, Node
class Body:
def __init__(self) -> None:
self.children: list[str | Body] = []
def __str__(self) -> str:
s = "\n".join((str(x) for x in self.children))
return s
class LoopBody(Body):
def __init__(self, dim: int, iterator: str, i_type: str) -> None:
super().__init__()
self.dim = dim
self.iterator = iterator
self.i_type = i_type
def __str__(self) -> str:
s = super().__str__()
return (
f"for({self.i_type} {self.iterator}=0; {self.iterator}<{self.dim}; {self.iterator}++) begin\n"
+ textwrap.indent(s, "\t")
+ "\nend"
)
class ForLoopGenerator:
i_type = "int"
loop_body_cls = LoopBody
def __init__(self) -> None:
self._loop_level = 0
self._stack: list[Body] = []
@property
def current_loop(self) -> Body:
return self._stack[-1]
def push_loop(self, dim: int) -> None:
i = f"i{self._loop_level}"
b = self.loop_body_cls(dim, i, self.i_type)
self._stack.append(b)
self._loop_level += 1
def add_content(self, s: str) -> None:
self.current_loop.children.append(s)
def pop_loop(self) -> None:
b = self._stack.pop()
if b.children:
# Loop body is not empty. Attach it to the parent
self.current_loop.children.append(b)
self._loop_level -= 1
def start(self) -> None:
assert not self._stack
b = Body()
self._stack.append(b)
def finish(self) -> str | None:
b = self._stack.pop()
assert not self._stack
if not b.children:
return None
return str(b)
class RDLForLoopGenerator(ForLoopGenerator, RDLListener):
def get_content(self, node: "Node") -> str | None:
self.start()
walker = RDLWalker()
walker.walk(node, self, skip_top=True)
return self.finish()
def enter_AddressableComponent(
self, node: "AddressableNode"
) -> WalkerAction | None:
if not node.array_dimensions:
return None
for dim in node.array_dimensions:
self.push_loop(dim)
return None
def exit_AddressableComponent(self, node: "AddressableNode") -> WalkerAction | None:
if not node.array_dimensions:
return None
for _ in node.array_dimensions:
self.pop_loop()
return None

View File

@@ -0,0 +1,52 @@
# All SystemVerilog 2017 keywords
SV_KEYWORDS = {
'accept_on', 'alias', 'always', 'always_comb', 'always_ff', 'always_latch',
'and', 'assert', 'assign', 'assume', 'automatic', 'before', 'begin', 'bind',
'bins', 'binsof', 'bit', 'break', 'buf', 'bufif0', 'bufif1', 'byte', 'case',
'casex', 'casez', 'cell', 'chandle', 'checker', 'class', 'clocking', 'cmos',
'config', 'const', 'constraint', 'context', 'continue', 'cover', 'covergroup',
'coverpoint', 'cross', 'deassign', 'default', 'defparam', 'design', 'disable',
'dist', 'do', 'edge', 'else', 'end', 'endcase', 'endchecker', 'endclass',
'endclocking', 'endconfig', 'endfunction', 'endgenerate', 'endgroup',
'endinterface', 'endmodule', 'endpackage', 'endprimitive', 'endprogram',
'endproperty', 'endspecify', 'endsequence', 'endtable', 'endtask', 'enum',
'event', 'eventually', 'expect', 'export', 'extends', 'extern', 'final',
'first_match', 'for', 'force', 'foreach', 'forever', 'fork', 'forkjoin',
'function', 'generate', 'genvar', 'global', 'highz0', 'highz1', 'if', 'iff',
'ifnone', 'ignore_bins', 'illegal_bins', 'implements', 'implies', 'import',
'incdir', 'include', 'initial', 'inout', 'input', 'inside', 'instance',
'int', 'integer', 'interconnect', 'interface', 'intersect', 'join',
'join_any', 'join_none', 'large', 'let', 'liblist', 'library', 'local',
'localparam', 'logic', 'longint', 'macromodule', 'matches', 'medium',
'modport', 'module', 'nand', 'negedge', 'nettype', 'new', 'nexttime', 'nmos',
'nor', 'noshowcancelled', 'not', 'notif0', 'notif1', 'null', 'or', 'output',
'package', 'packed', 'parameter', 'pmos', 'posedge', 'primitive', 'priority',
'program', 'property', 'protected', 'pull0', 'pull1', 'pulldown', 'pullup',
'pulsestyle_ondetect', 'pulsestyle_onevent', 'pure', 'rand', 'randc',
'randcase', 'randsequence', 'rcmos', 'real', 'realtime', 'ref', 'reg',
'reject_on', 'release', 'repeat', 'restrict', 'return', 'rnmos', 'rpmos',
'rtran', 'rtranif0', 'rtranif1', 's_always', 's_eventually', 's_nexttime',
's_until', 's_until_with', 'scalared', 'sequence', 'shortint', 'shortreal',
'showcancelled', 'signed', 'small', 'soft', 'solve', 'specify', 'specparam',
'static', 'string', 'strong', 'strong0', 'strong1', 'struct', 'super',
'supply0', 'supply1', 'sync_accept_on', 'sync_reject_on', 'table', 'tagged',
'task', 'this', 'throughout', 'time', 'timeprecision', 'timeunit', 'tran',
'tranif0', 'tranif1', 'tri', 'tri0', 'tri1', 'triand', 'trior', 'trireg',
'type', 'typedef', 'union', 'unique', 'unique0', 'unsigned', 'until',
'until_with', 'untyped', 'use', 'uwire', 'var', 'vectored', 'virtual', 'void',
'wait', 'wait_order', 'wand', 'weak', 'weak0', 'weak1', 'while', 'wildcard',
'wire', 'with', 'within', 'wor', 'xnor', 'xor'
}
def kw_filter(s: str) -> str:
"""
Make all user identifiers 'safe' and ensure they do not collide with
SystemVerilog keywords.
If an SV keyword is encountered, add an underscore suffix
"""
if s in SV_KEYWORDS:
s += "_"
return s

View File

@@ -0,0 +1,103 @@
//==========================================================
// Module: {{ds.module_name}}
// Description: CPU Interface Bus Decoder
// Author: PeakRDL-busdecoder
// License: GLPLv3
// Date: {{current_date}}
// Version: {{version}}
// Links:
// - https://github.com/SystemRDL/PeakRDL-busdecoder
//==========================================================
module {{ds.module_name}}
{%- if cpuif.parameters %} #(
{{-cpuif.parameters|join(",\n")|indent(8)}}
) {%- endif %} (
{{-cpuif.port_declaration|indent(8)}}
);
//--------------------------------------------------------------------------
// CPU Bus interface logic
//--------------------------------------------------------------------------
logic cpuif_req;
logic [{{cpuif.addr_width-1}}:0] cpuif_wr_addr;
logic [{{cpuif.addr_width-1}}:0] cpuif_rd_addr;
logic cpuif_wr_ack;
logic cpuif_wr_err;
logic [{{cpuif.data_width-1}}:0] cpuif_wr_data;
logic [{{cpuif.data_width//8-1}}:0] cpuif_wr_byte_en;
logic cpuif_rd_ack;
logic cpuif_rd_err;
logic [{{cpuif.data_width-1}}:0] cpuif_rd_data;
//--------------------------------------------------------------------------
// Child instance signals
//--------------------------------------------------------------------------
logic [{{cpuif.addressable_children | length}}-1:0] cpuif_wr_sel;
logic [{{cpuif.addressable_children | length}}-1:0] cpuif_rd_sel;
//--------------------------------------------------------------------------
// Slave <-> Internal CPUIF <-> Master
//--------------------------------------------------------------------------
{{-cpuif.get_implementation()|indent}}
//--------------------------------------------------------------------------
// Write Address Decoder
//--------------------------------------------------------------------------
always_comb begin
// Default all write select signals to 0
cpuif_wr_sel = '0;
if (cpuif_req && cpuif_wr_en) begin
// A write request is pending
{%- for child in cpuif.addressable_children -%}
{%- if loop.first -%}
if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- else -%}
end else if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- endif -%}
// Address matched for {{child.inst_name}}
cpuif_wr_sel[{{loop.index0}}] = 1'b1;
{%- endfor -%}
end else begin
// No address match, all select signals remain 0
cpuif_wr_err = 1'b1; // Indicate error on no match
end
end else begin
// No write request, all select signals remain 0
end
end
//--------------------------------------------------------------------------
// Read Address Decoder
//--------------------------------------------------------------------------
always_comb begin
// Default all read select signals to 0
cpuif_rd_sel = '0;
if (cpuif_req && !cpuif_wr_en) begin
// A read request is pending
{%- for child in cpuif.addressable_children -%}
{%- if loop.first -%}
if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- else -%}
end else if ({{cpuif.get_address_decode_condition(child)}}) begin
{%- endif -%}
// Address matched for {{child.inst_name}}
cpuif_rd_sel[{{loop.index0}}] = 1'b1;
{%- endfor -%}
end else begin
// No address match, all select signals remain 0
cpuif_rd_err = 1'b1; // Indicate error on no match
end
end else begin
// No read request, all select signals remain 0
end
end
endmodule
{# (eof newline anchor) #}

View File

@@ -0,0 +1,10 @@
// Generated by PeakRDL-busdecoder - A free and open-source SystemVerilog generator
// https://github.com/SystemRDL/PeakRDL-busdecoder
package {{ds.package_name}};
localparam {{ds.module_name.upper()}}_DATA_WIDTH = {{ds.cpuif_data_width}};
localparam {{ds.module_name.upper()}}_MIN_ADDR_WIDTH = {{ds.addr_width}};
localparam {{ds.module_name.upper()}}_SIZE = {{SVInt(ds.top_node.size)}};
endpackage
{# (eof newline anchor) #}

View File

@@ -0,0 +1,47 @@
from typing import TYPE_CHECKING
from systemrdl.walker import RDLListener, RDLWalker, WalkerAction
from systemrdl.node import RegNode
if TYPE_CHECKING:
from systemrdl.node import Node, AddressableNode, AddrmapNode
from .exporter import DesignState
class DesignScanner(RDLListener):
"""
Scans through the register model and validates that any unsupported features
are not present.
Also collects any information that is required prior to the start of the export process.
"""
def __init__(self, ds: "DesignState") -> None:
self.ds = ds
self.msg = self.top_node.env.msg
@property
def top_node(self) -> "AddrmapNode":
return self.ds.top_node
def do_scan(self) -> None:
RDLWalker().walk(self.top_node, self)
if self.msg.had_error:
self.msg.fatal("Unable to export due to previous errors")
def enter_Component(self, node: "Node") -> WalkerAction | None:
if node.external and (node != self.top_node):
# Do not inspect external components. None of my business
return WalkerAction.SkipDescendants
# Collect any signals that are referenced by a property
for prop_name in node.list_properties():
_ = node.get_property(prop_name)
return WalkerAction.Continue
def enter_AddressableComponent(self, node: "AddressableNode") -> None:
if node.external and node != self.top_node:
self.ds.has_external_addressable = True
if not isinstance(node, RegNode):
self.ds.has_external_block = True

View File

@@ -0,0 +1,15 @@
class SVInt:
def __init__(self, value: int, width: int | None = None) -> None:
self.value = value
self.width = width
def __str__(self) -> str:
if self.width is not None:
# Explicit width
return f"{self.width}'h{self.value:x}"
elif self.value.bit_length() > 32:
# SV standard only enforces that unsized literals shall be at least 32-bits
# To support larger literals, they need to be sized explicitly
return f"{self.value.bit_length()}'h{self.value:x}"
else:
return f"'h{self.value:x}"

View File

@@ -0,0 +1,18 @@
from systemrdl.udp import UDPDefinition
from .rw_buffering import BufferWrites, WBufferTrigger
from .rw_buffering import BufferReads, RBufferTrigger
from .extended_swacc import ReadSwacc, WriteSwacc
from .fixedpoint import IntWidth, FracWidth
from .signed import IsSigned
ALL_UDPS: list[type[UDPDefinition]] = [
BufferWrites,
WBufferTrigger,
BufferReads,
RBufferTrigger,
ReadSwacc,
WriteSwacc,
IntWidth,
FracWidth,
IsSigned,
]

View File

@@ -0,0 +1,23 @@
from typing import TYPE_CHECKING, Any
from systemrdl.udp import UDPDefinition
from systemrdl.component import Field
if TYPE_CHECKING:
from systemrdl.node import Node
class ReadSwacc(UDPDefinition):
name = "rd_swacc"
valid_components = {Field}
valid_type = bool
def get_unassigned_default(self, node: 'Node') -> Any:
return False
class WriteSwacc(UDPDefinition):
name = "wr_swacc"
valid_components = {Field}
valid_type = bool
def get_unassigned_default(self, node: 'Node') -> Any:
return False

View File

@@ -0,0 +1,73 @@
from typing import Any
from systemrdl.component import Field
from systemrdl.node import Node, FieldNode
from systemrdl.udp import UDPDefinition
class _FixedpointWidth(UDPDefinition):
valid_components = {Field}
valid_type = int
def validate(self, node: "Node", value: Any) -> None:
assert isinstance(node, FieldNode)
intwidth = node.get_property("intwidth")
fracwidth = node.get_property("fracwidth")
assert intwidth is not None
assert fracwidth is not None
prop_ref = node.inst.property_src_ref.get(self.name)
# incompatible with "counter" fields
if node.get_property("counter"):
self.msg.error(
"Fixed-point representations are not supported for counter fields.",
prop_ref
)
# incompatible with "encode" fields
if node.get_property("encode") is not None:
self.msg.error(
"Fixed-point representations are not supported for fields encoded as an enum.",
prop_ref
)
# ensure node width = fracwidth + intwidth
if intwidth + fracwidth != node.width:
self.msg.error(
f"Number of integer bits ({intwidth}) plus number of fractional bits ({fracwidth})"
f" must be equal to the width of the component ({node.width}).",
prop_ref
)
class IntWidth(_FixedpointWidth):
name = "intwidth"
def get_unassigned_default(self, node: "Node") -> Any:
"""
If 'fracwidth' is defined, 'intwidth' is inferred from the node width.
"""
assert isinstance(node, FieldNode)
fracwidth = node.get_property("fracwidth", default=None)
if fracwidth is not None:
return node.width - fracwidth
else:
# not a fixed-point number
return None
class FracWidth(_FixedpointWidth):
name = "fracwidth"
def get_unassigned_default(self, node: "Node") -> Any:
"""
If 'intwidth' is defined, 'fracwidth' is inferred from the node width.
"""
assert isinstance(node, FieldNode)
intwidth = node.get_property("intwidth", default=None)
if intwidth is not None:
return node.width - intwidth
else:
# not a fixed-point number
return None

View File

@@ -0,0 +1,130 @@
from typing import Any
from systemrdl.udp import UDPDefinition
from systemrdl.component import Reg
from systemrdl.rdltypes.references import RefType, PropertyReference
from systemrdl.rdltypes import NoValue
from systemrdl.node import Node, RegNode, VectorNode, SignalNode, FieldNode
class xBufferTrigger(UDPDefinition):
valid_components = {Reg}
valid_type = RefType
def validate(self, node: Node, value: Any) -> None:
# TODO: Reference shall not cross an internal/external boundary
if value is NoValue:
self.msg.error(
"Double-buffer trigger property is missing a value assignment",
self.get_src_ref(node)
)
elif isinstance(value, VectorNode):
# Trigger can reference a vector, but only if it is a single-bit
if value.width != 1:
self.msg.error(
"%s '%s' references %s '%s' but its width is not 1"
% (
type(node.inst).__name__.lower(), node.inst_name,
type(value.inst).__name__.lower(), value.inst_name
),
self.get_src_ref(node)
)
if isinstance(value, SignalNode):
if not value.get_property('activehigh') and not value.get_property('activelow'):
self.msg.error(
"Trigger was asigned a signal, but it does not specify whether it is activehigh/activelow",
self.get_src_ref(node)
)
elif isinstance(value, PropertyReference) and value.width is not None:
# Trigger can reference a property, but only if it is a single-bit
if value.width != 1:
self.msg.error(
"%s '%s' references property '%s->%s' but its width is not 1"
% (
type(node.inst).__name__.lower(), node.inst_name,
value.node.inst_name, value.name,
),
self.get_src_ref(node)
)
elif isinstance(value, RegNode):
# Trigger can reference a register, which implies access of the
# 'correct half' of the register is the trigger.
# For buffered writes, it is the upper-half.
# For buffered reads, it is the lower-half.
pass
else:
# All other reference types are invalid
self.msg.error(
"Reference to a %s component is incompatible with the '%s' property."
% (type(node.inst).__name__.lower(), self.name),
self.get_src_ref(node)
)
#-------------------------------------------------------------------------------
class BufferWrites(UDPDefinition):
name = "buffer_writes"
valid_components = {Reg}
valid_type = bool
def validate(self, node: 'Node', value: Any) -> None:
assert isinstance(node, RegNode)
if value:
if not node.has_sw_writable:
self.msg.error(
"'buffer_writes' is set to true, but this register does not contain any writable fields.",
self.get_src_ref(node)
)
def get_unassigned_default(self, node: 'Node') -> Any:
return False
class WBufferTrigger(xBufferTrigger):
name = "wbuffer_trigger"
def get_unassigned_default(self, node: 'Node') -> Any:
# If buffering is enabled, trigger is the register itself
if node.get_property('buffer_writes'):
return node
return None
def validate(self, node: Node, value: Any) -> None:
super().validate(node, value)
if isinstance(value, FieldNode):
if value.parent == node:
self.msg.error(
"Trigger for a write-buffered register cannot be a field "
"within the same register since the buffering makes it impossible to trigger."
)
class BufferReads(UDPDefinition):
name = "buffer_reads"
valid_components = {Reg}
valid_type = bool
def validate(self, node: 'Node', value: Any) -> None:
assert isinstance(node, RegNode)
if value:
if not node.has_sw_readable:
self.msg.error(
"'buffer_reads' is set to true, but this register does not contain any readable fields.",
self.get_src_ref(node)
)
def get_unassigned_default(self, node: 'Node') -> Any:
return False
class RBufferTrigger(xBufferTrigger):
name = "rbuffer_trigger"
def get_unassigned_default(self, node: 'Node') -> Any:
# If buffering is enabled, trigger is the register itself
if node.get_property('buffer_reads'):
return node
return None

View File

@@ -0,0 +1,33 @@
from typing import Any
from systemrdl.component import Field
from systemrdl.node import Node
from systemrdl.udp import UDPDefinition
class IsSigned(UDPDefinition):
name = "is_signed"
valid_components = {Field}
valid_type = bool
default_assignment = True
def validate(self, node: "Node", value: Any) -> None:
# "counter" fields can not be signed
if value and node.get_property("counter"):
self.msg.error(
"The property is_signed=true is not supported for counter fields.",
node.inst.property_src_ref["is_signed"]
)
# incompatible with "encode" fields
if value and node.get_property("encode") is not None:
self.msg.error(
"The property is_signed=true is not supported for fields encoded as an enum.",
node.inst.property_src_ref["is_signed"]
)
def get_unassigned_default(self, node: "Node") -> Any:
"""
Unsigned by default if not specified.
"""
return False

View File

@@ -0,0 +1,121 @@
import re
from typing import Match, overload
from systemrdl.rdltypes.references import PropertyReference
from systemrdl.node import Node, AddrmapNode
from .identifier_filter import kw_filter as kwf
from .sv_int import SVInt
def get_indexed_path(top_node: Node, target_node: Node) -> str:
"""
TODO: Add words about indexing and why i'm doing this. Copy from logbook
"""
path = target_node.get_rel_path(top_node, empty_array_suffix="[!]")
# replace unknown indexes with incrementing iterators i0, i1, ...
class ReplaceUnknown:
def __init__(self) -> None:
self.i = 0
def __call__(self, match: Match[str]) -> str:
s = f"i{self.i}"
self.i += 1
return s
path = re.sub(r"!", ReplaceUnknown(), path)
# Sanitize any SV keywords
def kw_filter_repl(m: Match[str]) -> str:
return kwf(m.group(0))
path = re.sub(r"\w+", kw_filter_repl, path)
return path
def clog2(n: int) -> int:
return (n - 1).bit_length()
def is_pow2(x: int) -> bool:
return (x > 0) and ((x & (x - 1)) == 0)
def roundup_pow2(x: int) -> int:
return 1 << (x - 1).bit_length()
def ref_is_internal(top_node: AddrmapNode, ref: Node | PropertyReference) -> bool:
"""
Determine whether the reference is internal to the top node.
For the sake of this exporter, root signals are treated as internal.
"""
# current_node: Optional[Node]
if isinstance(ref, Node):
current_node = ref
elif isinstance(ref, PropertyReference):
current_node = ref.node
else:
raise RuntimeError
while current_node is not None:
if current_node == top_node:
# reached top node without finding any external components
# is internal!
return True
if current_node.external:
# not internal!
return False
current_node = current_node.parent
# A root signal was referenced, which dodged the top addrmap
# This is considered internal for this exporter
return True
@overload
def do_slice(value: SVInt, high: int, low: int) -> SVInt: ...
@overload
def do_slice(value: str, high: int, low: int) -> str: ...
def do_slice(value: SVInt | str, high: int, low: int) -> SVInt | str:
if isinstance(value, str):
# If string, assume this is an identifier. Append bit-slice
if high == low:
return f"{value}[{low}]"
else:
return f"{value}[{high}:{low}]"
else:
# it is an SVInt literal. Slice it down
mask = (1 << (high + 1)) - 1
v = (value.value & mask) >> low
if value.width is not None:
w = high - low + 1
else:
w = None
return SVInt(v, w)
@overload
def do_bitswap(value: SVInt) -> SVInt: ...
@overload
def do_bitswap(value: str) -> str: ...
def do_bitswap(value: SVInt | str) -> SVInt | str:
if isinstance(value, str):
# If string, assume this is an identifier. Wrap in a streaming operator
return "{<<{" + value + "}}"
else:
# it is an SVInt literal. bitswap it
assert value.width is not None # width must be known!
v = value.value
vswap = 0
for _ in range(value.width):
vswap = (vswap << 1) + (v & 1)
v >>= 1
return SVInt(vswap, value.width)

View File

@@ -0,0 +1,208 @@
from typing import TYPE_CHECKING
from systemrdl.walker import RDLListener, RDLWalker, WalkerAction
from systemrdl.rdltypes import PropertyReference
from systemrdl.node import Node, RegNode, FieldNode, SignalNode, AddressableNode
from systemrdl.node import RegfileNode, AddrmapNode
from .utils import roundup_pow2, is_pow2
from .utils import ref_is_internal
if TYPE_CHECKING:
from .exporter import BusDecoderExporter
class DesignValidator(RDLListener):
"""
Performs additional rule-checks on the design that check for limitations
imposed by this exporter.
"""
def __init__(self, exp: "BusDecoderExporter") -> None:
self.exp = exp
self.ds = exp.ds
self.msg = self.top_node.env.msg
self._contains_external_block_stack: list[bool] = []
self.contains_external_block = False
@property
def top_node(self) -> "AddrmapNode":
return self.exp.ds.top_node
def do_validate(self) -> None:
RDLWalker().walk(self.top_node, self)
if self.msg.had_error:
self.msg.fatal("Unable to export due to previous errors")
def enter_Component(self, node: "Node") -> WalkerAction | None:
if node.external and (node != self.top_node):
# Do not inspect external components. None of my business
return WalkerAction.SkipDescendants
# Check if any property references reach across the internal/external boundary
for prop_name in node.list_properties():
value = node.get_property(prop_name)
if isinstance(value, (PropertyReference, Node)):
if not ref_is_internal(self.top_node, value):
if isinstance(value, PropertyReference):
src_ref = value.src_ref
else:
src_ref = node.inst.property_src_ref.get(
prop_name, node.inst.inst_src_ref
)
self.msg.error(
"Property is assigned a reference that points to a component not internal to the busdecoder being exported.",
src_ref,
)
return None
def enter_Signal(self, node: "SignalNode") -> None:
# If encountering a CPUIF reset that is nested within the register model,
# warn that it will be ignored.
# Only cpuif resets in the top-level node or above will be honored
if node.get_property("cpuif_reset") and (node.parent != self.top_node):
self.msg.warning(
"Only cpuif_reset signals that are instantiated in the top-level "
"addrmap or above will be honored. Any cpuif_reset signals nested "
"within children of the addrmap being exported will be ignored.",
node.inst.inst_src_ref,
)
def enter_AddressableComponent(self, node: "AddressableNode") -> None:
# All registers must be aligned to the internal data bus width
alignment = self.exp.cpuif.data_width_bytes
if (node.raw_address_offset % alignment) != 0:
self.msg.error(
"Unaligned registers are not supported. Address offset of "
f"instance '{node.inst_name}' must be a multiple of {alignment}",
node.inst.inst_src_ref,
)
if node.is_array and (node.array_stride % alignment) != 0: # type: ignore # is_array implies stride is not none
self.msg.error(
"Unaligned registers are not supported. Address stride of "
f"instance array '{node.inst_name}' must be a multiple of {alignment}",
node.inst.inst_src_ref,
)
if not isinstance(node, RegNode):
# Entering a block-like node
if node == self.top_node:
# Ignore top addrmap's external property when entering
self._contains_external_block_stack.append(False)
else:
self._contains_external_block_stack.append(node.external)
def enter_Regfile(self, node: RegfileNode) -> None:
self._check_sharedextbus(node)
def enter_Addrmap(self, node: AddrmapNode) -> None:
self._check_sharedextbus(node)
def _check_sharedextbus(self, node: RegfileNode | AddrmapNode) -> None:
if node.get_property("sharedextbus"):
self.msg.error(
"This exporter does not support enabling the 'sharedextbus' property yet.",
node.inst.property_src_ref.get("sharedextbus", node.inst.inst_src_ref),
)
def enter_Reg(self, node: "RegNode") -> None:
# accesswidth of wide registers must be consistent within the register block
accesswidth = node.get_property("accesswidth")
regwidth = node.get_property("regwidth")
if accesswidth < regwidth:
# register is 'wide'
if accesswidth != self.exp.cpuif.data_width:
self.msg.error(
f"Multi-word registers that have an accesswidth ({accesswidth}) "
"that are inconsistent with this busdecoder's CPU bus width "
f"({self.exp.cpuif.data_width}) are not supported.",
node.inst.inst_src_ref,
)
def enter_Field(self, node: "FieldNode") -> None:
parent_accesswidth = node.parent.get_property("accesswidth")
parent_regwidth = node.parent.get_property("regwidth")
if (parent_accesswidth < parent_regwidth) and (
node.lsb // parent_accesswidth
) != (node.msb // parent_accesswidth):
# field spans multiple sub-words
if node.is_sw_writable and not node.parent.get_property("buffer_writes"):
# ... and is writable without the protection of double-buffering
# Enforce 10.6.1-f
self.msg.error(
f"Software-writable field '{node.inst_name}' shall not span"
" multiple software-accessible subwords. Consider enabling"
" write double-buffering.\n"
"For more details, see: https://peakrdl-busdecoder.readthedocs.io/en/latest/udps/write_buffering.html",
node.inst.inst_src_ref,
)
if node.get_property("onread") is not None and not node.parent.get_property(
"buffer_reads"
):
# ... is modified by an onread action without the atomicity of read buffering
# Enforce 10.6.1-f
self.msg.error(
f"The field '{node.inst_name}' spans multiple software-accessible"
" subwords and is modified on-read, making it impossible to"
" access its value correctly. Consider enabling read"
" double-buffering. \n"
"For more details, see: https://peakrdl-busdecoder.readthedocs.io/en/latest/udps/read_buffering.html",
node.inst.inst_src_ref,
)
# Check for unsynthesizable reset
reset = node.get_property("reset")
if not (reset is None or isinstance(reset, int)):
# Has reset that is not a constant value
resetsignal = node.get_property("resetsignal")
if resetsignal:
is_async_reset = resetsignal.get_property("async")
else:
is_async_reset = self.ds.default_reset_async
if is_async_reset:
self.msg.error(
"A field that uses an asynchronous reset cannot use a dynamic reset value. This is not synthesizable.",
node.inst.inst_src_ref,
)
def exit_AddressableComponent(self, node: AddressableNode) -> None:
if not isinstance(node, RegNode):
# Exiting block-like node
contains_external_block = self._contains_external_block_stack.pop()
if self._contains_external_block_stack:
# Still in the design. Update stack
self._contains_external_block_stack[-1] |= contains_external_block
else:
# Exiting top addrmap. Resolve final answer
self.contains_external_block = contains_external_block
if contains_external_block:
# Check that addressing follows strict alignment rules to allow
# for simplified address bit-pruning
if node.external:
err_suffix = "is external"
else:
err_suffix = "contains an external addrmap/regfile/mem"
req_align = roundup_pow2(node.size)
if (node.raw_address_offset % req_align) != 0:
self.msg.error(
f"Address offset +0x{node.raw_address_offset:x} of instance '{node.inst_name}' is not a power of 2 multiple of its size 0x{node.size:x}. "
f"This is required by the busdecoder exporter if a component {err_suffix}.",
node.inst.inst_src_ref,
)
if node.is_array:
assert node.array_stride is not None
if not is_pow2(node.array_stride):
self.msg.error(
f"Address stride of instance array '{node.inst_name}' is not a power of 2"
f"This is required by the busdecoder exporter if a component {err_suffix}.",
node.inst.inst_src_ref,
)