feat: implement IRQ topology and bindings for NMI and APB groups

This commit is contained in:
maksyuki committed 2026-07-24 21:26:28 +08:00
1 parent 87fdfe49d8
commit 659a6acfdf
9 files changed
+502 -28

No files matched your search

+2
View File
@@ -148,6 +148,8 @@ bind nmi_gpio soc_gpio_user_handoff_sva #(
.gpio_do_i (gpio.do_o)
);
`endif
`include "soc_irq_sva.svh"
`endif
`endif
+12
View File
@@ -23,6 +23,18 @@ interface names, and corresponding active APB address-map region. A target that
declares `requires_ip: "MDD"` is generated only for `IP=MDD`; its presence is
checked against the same condition in the canonical memory map.
`irq_vector_width`, `irq_groups`, and `interrupts` define the complete core
interrupt topology. `nmi` and `apb` group entries declare the wrapper output
width and the associated `retrosoc.sv` signal. Every group bit must be present
exactly once, each core-vector bit must have one source, and sources are limited
to scalar signal references or one-bit constants. The generator emits the two
wrapper bindings, the core-vector wiring, and simulation-only assertions. Core
IRQ bits 0 through 16 retain their current compatibility mapping; additions
must use currently unallocated bits and preserve the existing entries. Bits
without an interrupt entry are driven low. Firmware does not expose a generic
external interrupt API until the SoC includes a claim/complete interrupt
controller.
`user_extensions.json` separately defines the supported scalar user-core and
user-IP slots. `generate_user_extensions.py` creates isolated default routing
and one explicit instance per selected slot for `user_core_top.sv` and
+291 -2
View File
@@ -17,6 +17,27 @@ SV_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$")
SV_REFERENCE_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*(?:\[[0-9]+\])?$")
SV_CONSTANT_RE = re.compile(r"(?:1'[bB][01]|'[01])$")
FABRIC_LINK_NAMES = ("core", "dma", "native", "apb")
IRQ_GROUP_NAMES = ("nmi", "apb")
IRQ_VECTOR_WIDTH = 32
COMPATIBILITY_IRQ_BINDINGS = (
("clint_software", "nmi", 0, 0, "u_clint_if.sfr_irq_o"),
("clint_timer", "nmi", 1, 1, "u_clint_if.tmr_irq_o"),
("uart0", "nmi", 2, 2, "uart.irq_o"),
("timer0", "nmi", 3, 3, "s_tim0_irq"),
("timer1", "nmi", 4, 4, "s_tim1_irq"),
("psram", "nmi", 5, 5, "psram.irq_o"),
("spisd", "nmi", 6, 6, "spisd.irq_o"),
("i2c0", "nmi", 7, 7, "i2c0.irq_o"),
("i2s", "nmi", 8, 8, "i2s.irq_o"),
("xpi", "nmi", 9, 9, "xpi.irq_o"),
("uart1", "apb", 0, 10, "uart.irq_o"),
("pwm", "apb", 1, 11, "pwm.irq_o"),
("ps2", "apb", 2, 12, "ps2.irq_o"),
("rtc", "apb", 3, 13, "u_rtc_if.irq_o"),
("watchdog_reset", "apb", 4, 14, "u_wdg_if.rst_o"),
("advanced_timer", "apb", 5, 15, "u_tmr_if.irq_o"),
("reserved", "apb", 6, 16, "1'b0"),
)
@dataclass(frozen=True)
@@ -58,6 +79,23 @@ class GpioFunction:
alt1: GpioMode
@dataclass(frozen=True)
class IrqGroup:
name: str
width: int
top_signal: str
@dataclass(frozen=True)
class Interrupt:
name: str
description: str
group: str
group_bit: int
core_bit: int
signal: str
def atomic_write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists() and path.read_text(encoding="utf-8") == content:
@@ -84,6 +122,13 @@ def require_string(value: Any, field: str) -> str:
return value
def require_comment_text(value: Any, field: str) -> str:
text = require_string(value, field)
if "\n" in text or "\r" in text:
raise ValueError(f"{field} must be a single-line string")
return text
def require_identifier(value: Any, field: str) -> str:
identifier = require_string(value, field)
if SV_IDENTIFIER_RE.fullmatch(identifier) is None:
@@ -362,21 +407,142 @@ def parse_gpio_functions(value: Any, gpio_pins: int) -> list[GpioFunction]:
return sorted(parsed, key=lambda item: item.pin)
def parse_irq_groups(value: Any) -> list[IrqGroup]:
if not isinstance(value, list) or len(value) != len(IRQ_GROUP_NAMES):
raise ValueError("irq_groups must define every supported interrupt group exactly once")
groups: list[IrqGroup] = []
names: set[str] = set()
top_signals: set[str] = set()
for index, entry in enumerate(value):
group = require_object(entry, f"irq_groups[{index}]")
name = require_identifier(group.get("name"), f"irq_groups[{index}].name")
width = group.get("width")
if not isinstance(width, int) or width <= 0:
raise ValueError(f"irq_groups[{index}].width must be a positive integer")
top_signal = require_identifier(
group.get("top_signal"), f"irq_groups[{index}].top_signal"
)
if name not in IRQ_GROUP_NAMES:
raise ValueError(f"irq_groups[{index}].name is not a supported interrupt group")
if name in names:
raise ValueError(f"interrupt group {name} is duplicated")
if top_signal in top_signals:
raise ValueError(f"interrupt group top signal {top_signal} is duplicated")
names.add(name)
top_signals.add(top_signal)
groups.append(IrqGroup(name, width, top_signal))
if names != set(IRQ_GROUP_NAMES):
missing = ", ".join(sorted(set(IRQ_GROUP_NAMES) - names))
raise ValueError(f"irq_groups does not cover required groups: {missing}")
return sorted(groups, key=lambda item: IRQ_GROUP_NAMES.index(item.name))
def parse_interrupts(
value: Any, groups: list[IrqGroup], vector_width: int
) -> list[Interrupt]:
if not isinstance(value, list) or not value:
raise ValueError("interrupts must be a non-empty list")
group_widths = {group.name: group.width for group in groups}
interrupts: list[Interrupt] = []
names: set[str] = set()
group_bits: dict[str, set[int]] = {group.name: set() for group in groups}
core_bits: set[int] = set()
for index, entry in enumerate(value):
interrupt = require_object(entry, f"interrupts[{index}]")
name = require_identifier(interrupt.get("name"), f"interrupts[{index}].name")
description = require_comment_text(
interrupt.get("description"), f"interrupts[{index}].description"
)
group = require_identifier(interrupt.get("group"), f"interrupts[{index}].group")
group_bit = interrupt.get("group_bit")
core_bit = interrupt.get("core_bit")
signal = require_value(interrupt.get("signal"), f"interrupts[{index}].signal")
if group not in group_widths:
raise ValueError(f"interrupts[{index}].group is not a supported interrupt group")
if not isinstance(group_bit, int) or not 0 <= group_bit < group_widths[group]:
raise ValueError(f"interrupts[{index}].group_bit is out of range")
if not isinstance(core_bit, int) or not 0 <= core_bit < vector_width:
raise ValueError(f"interrupts[{index}].core_bit is out of range")
if name in names:
raise ValueError(f"interrupt name {name} is duplicated")
if group_bit in group_bits[group]:
raise ValueError(f"interrupt group {group} bit {group_bit} is duplicated")
if core_bit in core_bits:
raise ValueError(f"core interrupt bit {core_bit} is duplicated")
names.add(name)
group_bits[group].add(group_bit)
core_bits.add(core_bit)
interrupts.append(Interrupt(name, description, group, group_bit, core_bit, signal))
for group in groups:
expected = set(range(group.width))
if group_bits[group.name] != expected:
missing = sorted(expected - group_bits[group.name])
extra = sorted(group_bits[group.name] - expected)
details: list[str] = []
if missing:
details.append(f"missing {', '.join(str(bit) for bit in missing)}")
if extra:
details.append(f"extra {', '.join(str(bit) for bit in extra)}")
raise ValueError(
f"interrupt group {group.name} does not cover every group bit "
f"({'; '.join(details)})"
)
return sorted(interrupts, key=lambda item: item.core_bit)
def validate_compatibility_irq_bindings(interrupts: list[Interrupt]) -> None:
by_core_bit = {interrupt.core_bit: interrupt for interrupt in interrupts}
for name, group, group_bit, core_bit, signal in COMPATIBILITY_IRQ_BINDINGS:
interrupt = by_core_bit.get(core_bit)
actual = None
if interrupt is not None:
actual = (
interrupt.name,
interrupt.group,
interrupt.group_bit,
interrupt.core_bit,
interrupt.signal,
)
expected = (name, group, group_bit, core_bit, signal)
if actual != expected:
raise ValueError(
f"core interrupt bit {core_bit} must retain its compatibility binding"
)
def read_topology(
topology_path: Path, memory_map_path: Path, ip: str
) -> tuple[list[NativeTarget], list[ApbTarget], list[FabricLink], list[GpioFunction]]:
) -> tuple[
list[NativeTarget],
list[ApbTarget],
list[FabricLink],
list[GpioFunction],
int,
list[IrqGroup],
list[Interrupt],
]:
document = require_object(json.loads(topology_path.read_text(encoding="utf-8")), "topology")
if document.get("schema_version") != 1:
raise ValueError("schema_version must be 1")
gpio_pins = document.get("gpio_pins")
if not isinstance(gpio_pins, int) or gpio_pins <= 0:
raise ValueError("gpio_pins must be a positive integer")
irq_vector_width = document.get("irq_vector_width")
if irq_vector_width != IRQ_VECTOR_WIDTH:
raise ValueError(f"irq_vector_width must be {IRQ_VECTOR_WIDTH}")
memory_regions = read_memory_regions(memory_map_path)
irq_groups = parse_irq_groups(document.get("irq_groups"))
interrupts = parse_interrupts(document.get("interrupts"), irq_groups, irq_vector_width)
validate_compatibility_irq_bindings(interrupts)
return (
parse_native_targets(document.get("native_targets"), memory_regions),
parse_apb_targets(document.get("apb_targets"), memory_regions, ip),
parse_fabric_links(document.get("fabric_links")),
parse_gpio_functions(document.get("gpio_alt_functions"), gpio_pins),
irq_vector_width,
irq_groups,
interrupts,
)
@@ -600,8 +766,117 @@ def render_gpio_bindings(functions: list[GpioFunction]) -> str:
return "\n".join(lines) + "\n"
def irq_width_macro(group: IrqGroup) -> str:
return f"SOC_IRQ_{group.name.upper()}_WIDTH"
def render_irq_config(vector_width: int, groups: list[IrqGroup]) -> str:
lines = [
"// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit.",
"`ifndef RETROSOC_SOC_IRQ_CONFIG_SVH",
"`define RETROSOC_SOC_IRQ_CONFIG_SVH",
"",
f"`define SOC_IRQ_VECTOR_WIDTH {vector_width}",
]
lines.extend(f"`define {irq_width_macro(group)} {group.width}" for group in groups)
lines.extend(["", "`endif"])
return "\n".join(lines) + "\n"
def render_irq_group_bindings(group: IrqGroup, interrupts: list[Interrupt]) -> str:
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
for interrupt in interrupts:
if interrupt.group != group.name:
continue
lines.extend(
[
f" // Core IRQ {interrupt.core_bit}: {interrupt.name} ({interrupt.description}).",
f" assign irq_o[{interrupt.group_bit}] = {interrupt.signal};",
]
)
return "\n".join(lines) + "\n"
def render_irq_wiring(
vector_width: int, groups: list[IrqGroup], interrupts: list[Interrupt]
) -> str:
group_signals = {group.name: group.top_signal for group in groups}
lines = [
"// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit.",
" always_comb begin",
" s_irq = '0;",
]
for interrupt in interrupts:
lines.extend(
[
f" // {interrupt.name}: {interrupt.description}",
f" s_irq[{interrupt.core_bit}] = {group_signals[interrupt.group]}[{interrupt.group_bit}];",
]
)
lines.extend([" end", ""])
return "\n".join(lines)
def render_irq_sva(vector_width: int, groups: list[IrqGroup], interrupts: list[Interrupt]) -> str:
group_signals = {group.name: group.top_signal for group in groups}
lines = [
"// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit.",
'`include "soc_irq_config.svh"',
"",
"module soc_irq_topology_sva (",
" input logic clk_i,",
" input logic rst_n_i,",
" input logic [`SOC_IRQ_VECTOR_WIDTH-1:0] irq_i,",
]
lines.extend(
f" input logic [`{irq_width_macro(group)}-1:0] {group.name}_irq_i,"
for group in groups
)
lines[-1] = lines[-1].removesuffix(",")
lines.extend([");", ""])
for interrupt in interrupts:
lines.extend(
[
f" // Core IRQ {interrupt.core_bit} remains bound to {interrupt.name}.",
" assert property (@(posedge clk_i) disable iff (!rst_n_i)",
f" irq_i[{interrupt.core_bit}] == {interrupt.group}_irq_i[{interrupt.group_bit}]);",
]
)
used_core_bits = {interrupt.core_bit for interrupt in interrupts}
for core_bit in range(vector_width):
if core_bit not in used_core_bits:
lines.extend(
[
f" // Unallocated core IRQ {core_bit} must remain inactive.",
" assert property (@(posedge clk_i) disable iff (!rst_n_i)",
f" irq_i[{core_bit}] == 1'b0);",
]
)
lines.extend(["", "endmodule", "", "bind retrosoc soc_irq_topology_sva u_soc_irq_topology_sva ("])
lines.extend(
[
" .clk_i(clk_i),",
" .rst_n_i(rst_n_i),",
" .irq_i(s_irq),",
]
)
for index, group in enumerate(groups):
separator = "," if index != len(groups) - 1 else ""
lines.append(f" .{group.name}_irq_i({group_signals[group.name]}){separator}")
lines.append(");")
return "\n".join(lines) + "\n"
def generate(topology_path: Path, memory_map_path: Path, output_dir: Path, ip: str) -> None:
targets, apb_targets, fabric_links, gpio_functions = read_topology(
(
targets,
apb_targets,
fabric_links,
gpio_functions,
irq_vector_width,
irq_groups,
interrupts,
) = read_topology(
topology_path, memory_map_path, ip
)
memory_regions = read_memory_regions(memory_map_path)
@@ -632,6 +907,20 @@ def generate(topology_path: Path, memory_map_path: Path, output_dir: Path, ip: s
render_fabric_connection(fabric_links, "apb", "nmi"),
)
atomic_write(rtl_dir / "soc_gpio_alt_bindings.svh", render_gpio_bindings(gpio_functions))
atomic_write(rtl_dir / "soc_irq_config.svh", render_irq_config(irq_vector_width, irq_groups))
for group in irq_groups:
atomic_write(
rtl_dir / f"soc_{group.name}_irq_bindings.svh",
render_irq_group_bindings(group, interrupts),
)
atomic_write(
rtl_dir / "soc_irq_wiring.svh",
render_irq_wiring(irq_vector_width, irq_groups, interrupts),
)
atomic_write(
rtl_dir / "soc_irq_sva.svh",
render_irq_sva(irq_vector_width, irq_groups, interrupts),
)
atomic_write(output_dir / "soc_topology.fl", f"+incdir+{rtl_dir}\n")
+24
View File
@@ -1,6 +1,30 @@
{
"schema_version": 1,
"gpio_pins": 32,
"irq_vector_width": 32,
"irq_groups": [
{"name": "nmi", "width": 10, "top_signal": "s_nmi_irq"},
{"name": "apb", "width": 7, "top_signal": "s_apb_irq"}
],
"interrupts": [
{"name": "clint_software", "description": "CLINT machine software interrupt", "group": "nmi", "group_bit": 0, "core_bit": 0, "signal": "u_clint_if.sfr_irq_o"},
{"name": "clint_timer", "description": "CLINT machine timer interrupt", "group": "nmi", "group_bit": 1, "core_bit": 1, "signal": "u_clint_if.tmr_irq_o"},
{"name": "uart0", "description": "Native UART0 interrupt", "group": "nmi", "group_bit": 2, "core_bit": 2, "signal": "uart.irq_o"},
{"name": "timer0", "description": "Native timer0 interrupt", "group": "nmi", "group_bit": 3, "core_bit": 3, "signal": "s_tim0_irq"},
{"name": "timer1", "description": "Native timer1 interrupt", "group": "nmi", "group_bit": 4, "core_bit": 4, "signal": "s_tim1_irq"},
{"name": "psram", "description": "PSRAM controller interrupt", "group": "nmi", "group_bit": 5, "core_bit": 5, "signal": "psram.irq_o"},
{"name": "spisd", "description": "SPI SD controller interrupt", "group": "nmi", "group_bit": 6, "core_bit": 6, "signal": "spisd.irq_o"},
{"name": "i2c0", "description": "Native I2C0 interrupt", "group": "nmi", "group_bit": 7, "core_bit": 7, "signal": "i2c0.irq_o"},
{"name": "i2s", "description": "Native I2S interrupt", "group": "nmi", "group_bit": 8, "core_bit": 8, "signal": "i2s.irq_o"},
{"name": "xpi", "description": "XPI controller interrupt", "group": "nmi", "group_bit": 9, "core_bit": 9, "signal": "xpi.irq_o"},
{"name": "uart1", "description": "APB UART1 interrupt", "group": "apb", "group_bit": 0, "core_bit": 10, "signal": "uart.irq_o"},
{"name": "pwm", "description": "APB PWM interrupt", "group": "apb", "group_bit": 1, "core_bit": 11, "signal": "pwm.irq_o"},
{"name": "ps2", "description": "APB PS2 interrupt", "group": "apb", "group_bit": 2, "core_bit": 12, "signal": "ps2.irq_o"},
{"name": "rtc", "description": "APB RTC interrupt", "group": "apb", "group_bit": 3, "core_bit": 13, "signal": "u_rtc_if.irq_o"},
{"name": "watchdog_reset", "description": "APB watchdog reset request", "group": "apb", "group_bit": 4, "core_bit": 14, "signal": "u_wdg_if.rst_o"},
{"name": "advanced_timer", "description": "APB advanced timer interrupt", "group": "apb", "group_bit": 5, "core_bit": 15, "signal": "u_tmr_if.irq_o"},
{"name": "reserved", "description": "Reserved interrupt slot", "group": "apb", "group_bit": 6, "core_bit": 16, "signal": "1'b0"}
],
"native_targets": [
{"slot": 0, "name": "uart0", "interface": "u_uart_nmi_if", "regions": ["NMI_UART0"]},
{"slot": 1, "name": "gpio", "interface": "u_gpio_nmi_if", "regions": ["NMI_GPIO"]},
+4 -9
View File
@@ -13,6 +13,7 @@
`include "pwm_define.svh"
`include "ps2_define.svh"
`include "mdd_config.svh"
`include "soc_irq_config.svh"
module ip_apb_wrapper (
// verilog_format: off
@@ -29,7 +30,7 @@ module ip_apb_wrapper (
input logic [`USER_IPSEL_WIDTH-1:0] ip_sel_i,
user_gpio_if.user_ip user_gpio,
`endif
output logic [ 6:0] irq_o
output logic [`SOC_IRQ_APB_WIDTH-1:0] irq_o
// verilog_format: on
);
@@ -60,14 +61,8 @@ module ip_apb_wrapper (
apb4_tmr u_apb4_tmr (u_tmr_apb_if, u_tmr_if);
// verilog_format: on
// handle irq signals
assign irq_o[0] = uart.irq_o;
assign irq_o[1] = pwm.irq_o;
assign irq_o[2] = ps2.irq_o;
assign irq_o[3] = u_rtc_if.irq_o;
assign irq_o[4] = u_wdg_if.rst_o;
assign irq_o[5] = u_tmr_if.irq_o;
assign irq_o[6] = 1'b0;
// Generated IRQ ownership and core-vector bit assignments are topology checked.
`include "soc_apb_irq_bindings.svh"
nmi2apb u_nmi2apb (
.clk_i (clk_i),
+4 -11
View File
@@ -9,6 +9,7 @@
// See the Mulan PSL v2 for more details.
`include "mmap_define.svh"
`include "soc_irq_config.svh"
module ip_nmi_wrapper (
// verilog_format: off
@@ -38,7 +39,7 @@ module ip_nmi_wrapper (
input logic [31:0] fault_addr_i,
input logic [3:0] fault_wstrb_i,
input logic fault_reserved_i,
output logic [9:0] irq_o
output logic [`SOC_IRQ_NMI_WIDTH-1:0] irq_o
// verilog_format: on
);
@@ -61,16 +62,8 @@ simp_clint_if u_clint_if ();
// Uses ClusterIP common nmi_if and register.sv dffr through generated bindings.
`include "soc_nmi_routes.svh"
assign irq_o[0] = u_clint_if.sfr_irq_o;
assign irq_o[1] = u_clint_if.tmr_irq_o;
assign irq_o[2] = uart.irq_o;
assign irq_o[3] = s_tim0_irq;
assign irq_o[4] = s_tim1_irq;
assign irq_o[5] = psram.irq_o;
assign irq_o[6] = spisd.irq_o;
assign irq_o[7] = i2c0.irq_o;
assign irq_o[8] = i2s.irq_o;
assign irq_o[9] = xpi.irq_o;
// Generated IRQ ownership and core-vector bit assignments are topology checked.
`include "soc_nmi_irq_bindings.svh"
nmi_gpio u_nmi_gpio (
.clk_i (clk_i),
+6 -6
View File
@@ -10,6 +10,7 @@
`include "mmap_define.svh"
`include "mdd_config.svh"
`include "soc_irq_config.svh"
module retrosoc (
// verilog_format: off
@@ -54,9 +55,9 @@ module retrosoc (
// verilog_format: on
logic s_tmr_capch;
logic [31:0] s_irq;
logic [ 9:0] s_nmi_irq;
logic [ 6:0] s_apb_irq;
logic [`SOC_IRQ_VECTOR_WIDTH-1:0] s_irq;
logic [ `SOC_IRQ_NMI_WIDTH-1:0] s_nmi_irq;
logic [ `SOC_IRQ_APB_WIDTH-1:0] s_apb_irq;
logic s_bus_fault_valid;
logic [ 31:0] s_bus_fault_addr;
logic [ 3:0] s_bus_fault_wstrb;
@@ -78,9 +79,8 @@ module retrosoc (
assign u_sysctrl_if.core_sel_i = '0;
`endif
assign s_irq[9:0] = s_nmi_irq;
assign s_irq[16:10] = s_apb_irq;
assign s_irq[31:17] = 15'd0;
// Generated IRQ vector wiring defaults all unallocated core IRQ bits low.
`include "soc_irq_wiring.svh"
// Generated GPIO alternate-function wiring is checked against soc_topology.json.
`include "soc_gpio_alt_bindings.svh"
+47
View File
@@ -0,0 +1,47 @@
`timescale 1ns / 1ps
`include "soc_irq_config.svh"
module soc_irq_topology_tb;
logic [`SOC_IRQ_VECTOR_WIDTH-1:0] s_irq;
logic [ `SOC_IRQ_NMI_WIDTH-1:0] s_nmi_irq;
logic [ `SOC_IRQ_APB_WIDTH-1:0] s_apb_irq;
`include "soc_irq_wiring.svh"
task automatic expect_irq(input logic [`SOC_IRQ_VECTOR_WIDTH-1:0] expected);
begin
#1;
if (s_irq !== expected) begin
$fatal(1, "unexpected IRQ vector: got %h, expected %h", s_irq, expected);
end
end
endtask
initial begin
s_nmi_irq = '0;
s_apb_irq = '0;
expect_irq('0);
for (int bit_index = 0; bit_index < `SOC_IRQ_NMI_WIDTH; bit_index++) begin
s_nmi_irq = '0;
s_nmi_irq[bit_index] = 1'b1;
s_apb_irq = '0;
expect_irq(32'd1 << bit_index);
end
for (int bit_index = 0; bit_index < `SOC_IRQ_APB_WIDTH; bit_index++) begin
s_nmi_irq = '0;
s_apb_irq = '0;
s_apb_irq[bit_index] = 1'b1;
expect_irq(32'd1 << (10 + bit_index));
end
if (s_irq[31:17] !== '0) begin
$fatal(1, "unallocated core IRQ bits are not low: %h", s_irq[31:17]);
end
$display("SoC topology IRQ routing test passed");
$finish;
end
endmodule
+112
View File
@@ -70,6 +70,11 @@ def test_topology_generates_complete_native_apb_and_gpio_bindings(tmp_path: Path
apb_response = (tmp_path / "rtl/soc_apb_response_mux.svh").read_text(encoding="utf-8")
fabric = (tmp_path / "rtl/soc_fabric_interfaces.svh").read_text(encoding="utf-8")
bus_fabric = (tmp_path / "rtl/soc_bus_fabric.svh").read_text(encoding="utf-8")
irq_config = (tmp_path / "rtl/soc_irq_config.svh").read_text(encoding="utf-8")
nmi_irq = (tmp_path / "rtl/soc_nmi_irq_bindings.svh").read_text(encoding="utf-8")
apb_irq = (tmp_path / "rtl/soc_apb_irq_bindings.svh").read_text(encoding="utf-8")
irq_wiring = (tmp_path / "rtl/soc_irq_wiring.svh").read_text(encoding="utf-8")
irq_sva = (tmp_path / "rtl/soc_irq_sva.svh").read_text(encoding="utf-8")
filelist = (tmp_path / "soc_topology.fl").read_text(encoding="utf-8")
assert interfaces.count("nmi_if u_") == 18
@@ -87,9 +92,52 @@ def test_topology_generates_complete_native_apb_and_gpio_bindings(tmp_path: Path
assert fabric.count("nmi_if u_") == 4
assert ".core_nmi(u_core_nmi_if)" in bus_fabric
assert ".apb_nmi(u_apb_nmi_if)" in bus_fabric
assert "`define SOC_IRQ_VECTOR_WIDTH 32" in irq_config
assert "`define SOC_IRQ_NMI_WIDTH 10" in irq_config
assert "`define SOC_IRQ_APB_WIDTH 7" in irq_config
assert "assign irq_o[0] = u_clint_if.sfr_irq_o;" in nmi_irq
assert "assign irq_o[5] = u_tmr_if.irq_o;" in apb_irq
assert "s_irq[16] = s_apb_irq[6];" in irq_wiring
assert "irq_i[31] == 1'b0" in irq_sva
assert "bind retrosoc soc_irq_topology_sva" in irq_sva
assert filelist.startswith("+incdir+")
def test_topology_preserves_default_irq_compatibility_mapping() -> None:
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
assert document["irq_vector_width"] == 32
mappings = [
(
interrupt["name"],
interrupt["group"],
interrupt["group_bit"],
interrupt["core_bit"],
interrupt["signal"],
)
for interrupt in document["interrupts"]
]
assert mappings[:17] == [
("clint_software", "nmi", 0, 0, "u_clint_if.sfr_irq_o"),
("clint_timer", "nmi", 1, 1, "u_clint_if.tmr_irq_o"),
("uart0", "nmi", 2, 2, "uart.irq_o"),
("timer0", "nmi", 3, 3, "s_tim0_irq"),
("timer1", "nmi", 4, 4, "s_tim1_irq"),
("psram", "nmi", 5, 5, "psram.irq_o"),
("spisd", "nmi", 6, 6, "spisd.irq_o"),
("i2c0", "nmi", 7, 7, "i2c0.irq_o"),
("i2s", "nmi", 8, 8, "i2s.irq_o"),
("xpi", "nmi", 9, 9, "xpi.irq_o"),
("uart1", "apb", 0, 10, "uart.irq_o"),
("pwm", "apb", 1, 11, "pwm.irq_o"),
("ps2", "apb", 2, 12, "ps2.irq_o"),
("rtc", "apb", 3, 13, "u_rtc_if.irq_o"),
("watchdog_reset", "apb", 4, 14, "u_wdg_if.rst_o"),
("advanced_timer", "apb", 5, 15, "u_tmr_if.irq_o"),
("reserved", "apb", 6, 16, "1'b0"),
]
assert all(mapping[3] >= 17 for mapping in mappings[17:])
def test_topology_adds_user_apb_only_for_the_mdd_ip(tmp_path: Path) -> None:
none_output = tmp_path / "none"
mdd_output = tmp_path / "mdd"
@@ -180,6 +228,70 @@ def test_topology_rejects_invalid_gpio_coverage_and_expression(tmp_path: Path) -
assert result.returncode != 0
assert "scalar SystemVerilog reference or constant" in result.stderr
def test_topology_rejects_invalid_irq_groups_and_bindings(tmp_path: Path) -> None:
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["irq_groups"][1]["name"] = "nmi"
result = validate(write_invalid_topology(tmp_path, document))
assert result.returncode != 0
assert "interrupt group nmi is duplicated" in result.stderr
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["interrupts"][1]["group_bit"] = 0
result = validate(write_invalid_topology(tmp_path, document))
assert result.returncode != 0
assert "interrupt group nmi bit 0 is duplicated" in result.stderr
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["interrupts"][1]["core_bit"] = 0
result = validate(write_invalid_topology(tmp_path, document))
assert result.returncode != 0
assert "core interrupt bit 0 is duplicated" in result.stderr
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["interrupts"].pop()
result = validate(write_invalid_topology(tmp_path, document))
assert result.returncode != 0
assert "interrupt group apb does not cover every group bit" in result.stderr
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["interrupts"][0]["signal"] = "left | right"
result = validate(write_invalid_topology(tmp_path, document))
assert result.returncode != 0
assert "scalar SystemVerilog reference or constant" in result.stderr
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["interrupts"][0]["signal"] = "uart.irq_o"
result = validate(write_invalid_topology(tmp_path, document))
assert result.returncode != 0
assert "core interrupt bit 0 must retain its compatibility binding" in result.stderr
def test_generated_irq_wiring_preserves_the_expected_core_vector(tmp_path: Path) -> None:
iverilog = shutil.which("iverilog")
vvp = shutil.which("vvp")
if iverilog is None or vvp is None:
return
topology_output = tmp_path / "topology"
generate(topology_output)
simulation = tmp_path / "soc_irq_topology_tb"
subprocess.run(
[
iverilog,
"-g2012",
"-I",
str(topology_output / "rtl"),
"-s",
"soc_irq_topology_tb",
"-o",
str(simulation),
str(ROOT / "tests/rtl/soc_irq_topology_tb.sv"),
],
check=True,
)
result = subprocess.run([vvp, str(simulation)], text=True, capture_output=True, check=True)
assert "SoC topology IRQ routing test passed" in result.stdout
def test_generated_nmi_routes_select_and_return_the_expected_target(tmp_path: Path) -> None:
iverilog = shutil.which("iverilog")