diff --git a/rtl/mini/dv/sva/soc_protocol_sva.sv b/rtl/mini/dv/sva/soc_protocol_sva.sv index ba047253..8a2c8f31 100644 --- a/rtl/mini/dv/sva/soc_protocol_sva.sv +++ b/rtl/mini/dv/sva/soc_protocol_sva.sv @@ -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 diff --git a/rtl/mini/integration/README.md b/rtl/mini/integration/README.md index 6489d2d0..f4ff170f 100644 --- a/rtl/mini/integration/README.md +++ b/rtl/mini/integration/README.md @@ -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 diff --git a/rtl/mini/integration/generate_soc_topology.py b/rtl/mini/integration/generate_soc_topology.py index 9106eace..6295fb97 100644 --- a/rtl/mini/integration/generate_soc_topology.py +++ b/rtl/mini/integration/generate_soc_topology.py @@ -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") diff --git a/rtl/mini/integration/soc_topology.json b/rtl/mini/integration/soc_topology.json index 710bf8c0..5db6e4bf 100644 --- a/rtl/mini/integration/soc_topology.json +++ b/rtl/mini/integration/soc_topology.json @@ -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"]}, diff --git a/rtl/mini/top/ip_apb_wrapper.sv b/rtl/mini/top/ip_apb_wrapper.sv index fb52006a..3e0b4b03 100644 --- a/rtl/mini/top/ip_apb_wrapper.sv +++ b/rtl/mini/top/ip_apb_wrapper.sv @@ -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,16 +61,10 @@ 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 ( +nmi2apb u_nmi2apb ( .clk_i (clk_i), .rst_n_i(rst_n_i), .nmi (nmi), diff --git a/rtl/mini/top/ip_nmi_wrapper.sv b/rtl/mini/top/ip_nmi_wrapper.sv index b80d0ce1..5ea705de 100644 --- a/rtl/mini/top/ip_nmi_wrapper.sv +++ b/rtl/mini/top/ip_nmi_wrapper.sv @@ -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,18 +62,10 @@ 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 ( +nmi_gpio u_nmi_gpio ( .clk_i (clk_i), .rst_n_i (rst_n_i), .nmi (u_gpio_nmi_if), diff --git a/rtl/mini/top/retrosoc.sv b/rtl/mini/top/retrosoc.sv index f767d581..7bc43c13 100644 --- a/rtl/mini/top/retrosoc.sv +++ b/rtl/mini/top/retrosoc.sv @@ -10,6 +10,7 @@ `include "mmap_define.svh" `include "mdd_config.svh" +`include "soc_irq_config.svh" module retrosoc ( // verilog_format: off @@ -53,14 +54,14 @@ module retrosoc ( ps2_if u_ps2_if (); // 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 s_bus_fault_valid; - logic [31:0] s_bus_fault_addr; - logic [ 3:0] s_bus_fault_wstrb; - logic s_bus_fault_reserved; + logic s_tmr_capch; + 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; + logic s_bus_fault_reserved; gpio_pad_bridge u_gpio_pad_bridge ( .inner(u_gpio_if), @@ -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" diff --git a/tests/rtl/soc_irq_topology_tb.sv b/tests/rtl/soc_irq_topology_tb.sv new file mode 100644 index 00000000..8ba16dec --- /dev/null +++ b/tests/rtl/soc_irq_topology_tb.sv @@ -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 diff --git a/tests/test_soc_topology.py b/tests/test_soc_topology.py index 27118ea5..c9fbfe4a 100644 --- a/tests/test_soc_topology.py +++ b/tests/test_soc_topology.py @@ -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")