feat: enhance SoC topology and user extensions with APB targets and validation scripts

- Added APB target definitions in soc_topology.json for various peripherals.
- Introduced user_extensions.json to define user core and IP targets.
- Updated generate_filelist.py to include SVA paths.
- Refactored ip_apb_wrapper.sv to utilize generated APB interfaces and bridges.
- Simplified user_core_top.sv and user_ip_top.sv by including generated bindings.
- Implemented check_clock_reset_domains.py to validate clock/reset domains and CDC crossings.
- Updated regress.py to include SVA support in simulation commands.
- Added tests for clock/reset domain validation and user extensions generation.
- Enhanced test coverage for topology generation and APB target ownership validation.
This commit is contained in:
maksyuki committed 2026-07-23 16:42:25 +08:00
1 parent d886cd992c
commit 7d90d2ffb4
22 files changed
+1118 -561

No files matched your search

+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
run: python3 scripts/dependency_lock.py --lock config/dependencies.lock.json
- name: Validate SoC integration topology
run: make check-soc-topology
run: make check-soc-topology check-user-extensions check-clock-reset-domains
- name: Lint Python
run: ruff check .
+5 -1
View File
@@ -167,7 +167,8 @@ endif
clean-all purge-cache manifest check-warnings metrics check-metrics package \
regress-pr regress-nightly sim-asm format format-check sw-format sw-format-check mk-format \
mk-format-check rtl-format rtl-format-check sw-policy-check sw-host-test \
pin-map check-pin-map soc-topology check-soc-topology
pin-map check-pin-map soc-topology check-soc-topology user-extensions check-user-extensions \
check-clock-reset-domains
.NOTPARALLEL: setup
help:
@@ -188,6 +189,9 @@ help:
' check-pin-map validate the canonical SoC pin map' \
' soc-topology generate the selected internal SoC integration artifacts' \
' check-soc-topology validate the canonical internal SoC integration map' \
' user-extensions generate the selected scalar user-extension bindings' \
' check-user-extensions validate the canonical user-extension map' \
' check-clock-reset-domains validate the root clock/reset and CDC inventory' \
' check-warnings | metrics analyze flow logs and reports' \
' check-metrics apply the committed metrics policy' \
' format format self-owned C, Makefile, and RTL sources' \
+12 -146
View File
@@ -15,18 +15,7 @@ module nmi2apb (
input logic clk_i,
input logic rst_n_i,
nmi_if.slave nmi,
`ifdef IP_MDD
apb4_pure_if.master user_ip,
`endif
apb4_pure_if.master archinfo,
apb4_pure_if.master rng,
apb4_pure_if.master uart,
apb4_pure_if.master pwm,
apb4_pure_if.master ps2,
apb4_pure_if.master rtc,
apb4_pure_if.master wdg,
apb4_pure_if.master crc,
apb4_pure_if.master tmr
`include "soc_apb_ports.svh"
// verilog_format: on
);
@@ -35,109 +24,25 @@ module nmi2apb (
localparam FSM_ENAB = 2'd2;
logic [31:0] s_rd_data;
logic [31:0] s_rd_data_base;
logic s_xfer_valid, s_xfer_ready;
logic s_xfer_ready_base;
logic s_mem_valid_re;
logic [1:0] s_fsm_d, s_fsm_q;
// Registered slave-select one-hot for response mux
`ifdef IP_MDD
localparam NSLV = 10;
`else
localparam NSLV = 9;
`endif
logic [NSLV-1:0] s_psel_comb, s_psel_d, s_psel_q;
`include "soc_apb_declarations.svh"
// Generated APB request routing retains the scalar interface protocol.
`include "soc_apb_request_routes.svh"
// verilog_format: off
assign archinfo.paddr = nmi.addr;
assign rng.paddr = nmi.addr;
assign uart.paddr = nmi.addr;
assign pwm.paddr = nmi.addr;
assign ps2.paddr = nmi.addr;
assign rtc.paddr = nmi.addr;
assign wdg.paddr = nmi.addr;
assign crc.paddr = nmi.addr;
assign tmr.paddr = nmi.addr;
assign archinfo.pprot = '0;
assign rng.pprot = '0;
assign uart.pprot = '0;
assign pwm.pprot = '0;
assign ps2.pprot = '0;
assign rtc.pprot = '0;
assign wdg.pprot = '0;
assign crc.pprot = '0;
assign tmr.pprot = '0;
assign archinfo.psel = s_xfer_valid && `SOC_ADDR_IS_APB_ARCHINFO(nmi.addr);
assign rng.psel = s_xfer_valid && `SOC_ADDR_IS_APB_RNG(nmi.addr);
assign uart.psel = s_xfer_valid && `SOC_ADDR_IS_APB_UART1(nmi.addr);
assign pwm.psel = s_xfer_valid && `SOC_ADDR_IS_APB_PWM(nmi.addr);
assign ps2.psel = s_xfer_valid && `SOC_ADDR_IS_APB_PS2(nmi.addr);
assign rtc.psel = s_xfer_valid && `SOC_ADDR_IS_APB_RTC(nmi.addr);
assign wdg.psel = s_xfer_valid && `SOC_ADDR_IS_APB_WDG(nmi.addr);
assign crc.psel = s_xfer_valid && `SOC_ADDR_IS_APB_CRC(nmi.addr);
assign tmr.psel = s_xfer_valid && `SOC_ADDR_IS_APB_TMR(nmi.addr);
assign archinfo.penable = s_fsm_q == FSM_ENAB;
assign rng.penable = s_fsm_q == FSM_ENAB;
assign uart.penable = s_fsm_q == FSM_ENAB;
assign pwm.penable = s_fsm_q == FSM_ENAB;
assign ps2.penable = s_fsm_q == FSM_ENAB;
assign rtc.penable = s_fsm_q == FSM_ENAB;
assign wdg.penable = s_fsm_q == FSM_ENAB;
assign crc.penable = s_fsm_q == FSM_ENAB;
assign tmr.penable = s_fsm_q == FSM_ENAB;
assign archinfo.pwrite = |nmi.wstrb;
assign rng.pwrite = |nmi.wstrb;
assign uart.pwrite = |nmi.wstrb;
assign pwm.pwrite = |nmi.wstrb;
assign ps2.pwrite = |nmi.wstrb;
assign rtc.pwrite = |nmi.wstrb;
assign wdg.pwrite = |nmi.wstrb;
assign crc.pwrite = |nmi.wstrb;
assign tmr.pwrite = |nmi.wstrb;
assign archinfo.pwdata = nmi.wdata;
assign rng.pwdata = nmi.wdata;
assign uart.pwdata = nmi.wdata;
assign pwm.pwdata = nmi.wdata;
assign ps2.pwdata = nmi.wdata;
assign rtc.pwdata = nmi.wdata;
assign wdg.pwdata = nmi.wdata;
assign crc.pwdata = nmi.wdata;
assign tmr.pwdata = nmi.wdata;
assign archinfo.pstrb = nmi.wstrb;
assign rng.pstrb = nmi.wstrb;
assign uart.pstrb = nmi.wstrb;
assign pwm.pstrb = nmi.wstrb;
assign ps2.pstrb = nmi.wstrb;
assign rtc.pstrb = nmi.wstrb;
assign wdg.pstrb = nmi.wstrb;
assign crc.pstrb = nmi.wstrb;
assign tmr.pstrb = nmi.wstrb;
`ifdef IP_MDD
assign user_ip.paddr = nmi.addr;
assign user_ip.pprot = '0;
assign user_ip.psel = s_xfer_valid && `SOC_ADDR_IS_APB_USER_IP(nmi.addr);
assign user_ip.penable = s_fsm_q == FSM_ENAB;
assign user_ip.pwrite = |nmi.wstrb;
assign user_ip.pwdata = nmi.wdata;
assign user_ip.pstrb = nmi.wstrb;
`endif
// verilog_format: on
edge_det_sync_re #(1) u_mem_valid_edge_det_sync_re (
clk_i,
rst_n_i,
nmi.valid,
s_mem_valid_re
);
// verilog_format: on
assign s_xfer_valid = ((s_fsm_q == FSM_IDLE) && s_mem_valid_re) ||
(s_fsm_q == FSM_SETP) || (s_fsm_q == FSM_ENAB);
@@ -158,26 +63,13 @@ module nmi2apb (
s_fsm_q
);
assign nmi.ready = nmi.valid && (s_fsm_q == FSM_ENAB) && s_xfer_ready;
assign nmi.rdata = {32{nmi.ready}} & s_rd_data;
assign nmi.ready = nmi.valid && (s_fsm_q == FSM_ENAB) && s_xfer_ready;
assign nmi.rdata = {32{nmi.ready}} & s_rd_data;
// Capture slave-select one-hot at FSM_SETP for cleaner response mux
// verilog_format: off
assign s_psel_comb[0] = `SOC_ADDR_IS_APB_ARCHINFO(nmi.addr);
assign s_psel_comb[1] = `SOC_ADDR_IS_APB_RNG(nmi.addr);
assign s_psel_comb[2] = `SOC_ADDR_IS_APB_UART1(nmi.addr);
assign s_psel_comb[3] = `SOC_ADDR_IS_APB_PWM(nmi.addr);
assign s_psel_comb[4] = `SOC_ADDR_IS_APB_PS2(nmi.addr);
assign s_psel_comb[5] = `SOC_ADDR_IS_APB_RTC(nmi.addr);
assign s_psel_comb[6] = `SOC_ADDR_IS_APB_WDG(nmi.addr);
assign s_psel_comb[7] = `SOC_ADDR_IS_APB_CRC(nmi.addr);
assign s_psel_comb[8] = `SOC_ADDR_IS_APB_TMR(nmi.addr);
`ifdef IP_MDD
assign s_psel_comb[9] = `SOC_ADDR_IS_APB_USER_IP(nmi.addr);
`endif
// Capture slave-select one-hot at FSM_SETP for cleaner response mux.
`include "soc_apb_select_routes.svh"
assign s_psel_d = (s_fsm_q == FSM_IDLE && s_mem_valid_re) ? s_psel_comb : s_psel_q;
// verilog_format: on
dffr #(NSLV) u_psel_dffr (
clk_i,
rst_n_i,
@@ -185,33 +77,7 @@ module nmi2apb (
s_psel_q
);
// Response mux using registered slave-select (cleaner timing)
assign s_rd_data_base = ({32{s_psel_q[0]}} & archinfo.prdata) |
({32{s_psel_q[1]}} & rng.prdata) |
({32{s_psel_q[2]}} & uart.prdata) |
({32{s_psel_q[3]}} & pwm.prdata) |
({32{s_psel_q[4]}} & ps2.prdata) |
({32{s_psel_q[5]}} & rtc.prdata) |
({32{s_psel_q[6]}} & wdg.prdata) |
({32{s_psel_q[7]}} & crc.prdata) |
({32{s_psel_q[8]}} & tmr.prdata);
assign s_xfer_ready_base = (s_psel_q[0] & archinfo.pready) |
(s_psel_q[1] & rng.pready) |
(s_psel_q[2] & uart.pready) |
(s_psel_q[3] & pwm.pready) |
(s_psel_q[4] & ps2.pready) |
(s_psel_q[5] & rtc.pready) |
(s_psel_q[6] & wdg.pready) |
(s_psel_q[7] & crc.pready) |
(s_psel_q[8] & tmr.pready);
`ifdef IP_MDD
assign s_rd_data = s_rd_data_base | ({32{s_psel_q[9]}} & user_ip.prdata);
assign s_xfer_ready = s_xfer_ready_base | (s_psel_q[9] & user_ip.pready);
`else
assign s_rd_data = s_rd_data_base;
assign s_xfer_ready = s_xfer_ready_base;
`endif
// Generated response mux uses the registered one-hot target selection.
`include "soc_apb_response_mux.svh"
endmodule
+54 -29
View File
@@ -1,27 +1,34 @@
GENERATED_FL_DIR := $(FLOW_FILELIST_DIR)
MEMORY_MAP_SOURCE := $(RTL_PATH)/address_map/memory_map.json
MEMORY_MAP_SCRIPT := $(RTL_PATH)/address_map/generate_memory_map.py
MEMORY_MAP_DIR := $(VARIANT_ROOT)/generated/memory_map
MEMORY_MAP_STAMP := $(MEMORY_MAP_DIR)/.stamp
MEMORY_MAP_RTL_DIR := $(MEMORY_MAP_DIR)/rtl
MEMORY_MAP_C_DIR := $(MEMORY_MAP_DIR)/include
MEMORY_MAP_LINKER_DIR := $(MEMORY_MAP_DIR)/linker
MEMORY_MAP_FILELIST := $(MEMORY_MAP_DIR)/memory_map.fl
PIN_MAP_SOURCE := $(RTL_PATH)/pin_map/pin_map.json
PIN_MAP_SCRIPT := $(RTL_PATH)/pin_map/generate_pin_map.py
PIN_MAP_DIR := $(VARIANT_ROOT)/generated/pin_map
PIN_MAP_STAMP := $(PIN_MAP_DIR)/.stamp
PIN_MAP_FILELIST := $(PIN_MAP_DIR)/pin_map.fl
SOC_TOPOLOGY_SOURCE := $(RTL_PATH)/integration/soc_topology.json
SOC_TOPOLOGY_SCRIPT := $(RTL_PATH)/integration/generate_soc_topology.py
SOC_TOPOLOGY_DIR := $(VARIANT_ROOT)/generated/soc_topology
SOC_TOPOLOGY_STAMP := $(SOC_TOPOLOGY_DIR)/.stamp
SOC_TOPOLOGY_FILELIST := $(SOC_TOPOLOGY_DIR)/soc_topology.fl
MPW_STAMP := $(CACHE_ROOT)/stamps/mpw-$(LOCK_DIGEST).prepared
MPW_OUTPUT_DIR := $(VARIANT_ROOT)/generated/mpw/$(SIM_TOOL_NAME)
MPW_VARIANT_STAMP := $(MPW_OUTPUT_DIR)/.stamp
FILELIST_STAMP := $(GENERATED_FL_DIR)/.stamp
FILELIST_TEMPLATES := $(sort $(wildcard $(ROOT_PATH)/rtl/filelist/pdk_*.fl) \
GENERATED_FL_DIR := $(FLOW_FILELIST_DIR)
MEMORY_MAP_SOURCE := $(RTL_PATH)/address_map/memory_map.json
MEMORY_MAP_SCRIPT := $(RTL_PATH)/address_map/generate_memory_map.py
MEMORY_MAP_DIR := $(VARIANT_ROOT)/generated/memory_map
MEMORY_MAP_STAMP := $(MEMORY_MAP_DIR)/.stamp
MEMORY_MAP_RTL_DIR := $(MEMORY_MAP_DIR)/rtl
MEMORY_MAP_C_DIR := $(MEMORY_MAP_DIR)/include
MEMORY_MAP_LINKER_DIR := $(MEMORY_MAP_DIR)/linker
MEMORY_MAP_FILELIST := $(MEMORY_MAP_DIR)/memory_map.fl
PIN_MAP_SOURCE := $(RTL_PATH)/pin_map/pin_map.json
PIN_MAP_SCRIPT := $(RTL_PATH)/pin_map/generate_pin_map.py
PIN_MAP_DIR := $(VARIANT_ROOT)/generated/pin_map
PIN_MAP_STAMP := $(PIN_MAP_DIR)/.stamp
PIN_MAP_FILELIST := $(PIN_MAP_DIR)/pin_map.fl
SOC_TOPOLOGY_SOURCE := $(RTL_PATH)/integration/soc_topology.json
SOC_TOPOLOGY_SCRIPT := $(RTL_PATH)/integration/generate_soc_topology.py
SOC_TOPOLOGY_DIR := $(VARIANT_ROOT)/generated/soc_topology
SOC_TOPOLOGY_STAMP := $(SOC_TOPOLOGY_DIR)/.stamp
SOC_TOPOLOGY_FILELIST := $(SOC_TOPOLOGY_DIR)/soc_topology.fl
USER_EXTENSIONS_SOURCE := $(RTL_PATH)/integration/user_extensions.json
USER_EXTENSIONS_SCRIPT := $(RTL_PATH)/integration/generate_user_extensions.py
USER_EXTENSIONS_DIR := $(VARIANT_ROOT)/generated/user_extensions
USER_EXTENSIONS_STAMP := $(USER_EXTENSIONS_DIR)/.stamp
USER_EXTENSIONS_FILELIST := $(USER_EXTENSIONS_DIR)/user_extensions.fl
CLOCK_RESET_DOMAINS_SOURCE := $(RTL_PATH)/integration/clock_reset_domains.json
CLOCK_RESET_DOMAINS_SCRIPT := $(ROOT_PATH)/scripts/check_clock_reset_domains.py
MPW_STAMP := $(CACHE_ROOT)/stamps/mpw-$(LOCK_DIGEST).prepared
MPW_OUTPUT_DIR := $(VARIANT_ROOT)/generated/mpw/$(SIM_TOOL_NAME)
MPW_VARIANT_STAMP := $(MPW_OUTPUT_DIR)/.stamp
FILELIST_STAMP := $(GENERATED_FL_DIR)/.stamp
FILELIST_TEMPLATES := $(sort $(wildcard $(ROOT_PATH)/rtl/filelist/pdk_*.fl) \
$(wildcard $(RTL_PATH)/filelist/*.fl) $(RTL_PATH)/lint.msg)
$(MPW_STAMP):
@@ -64,17 +71,33 @@ check-pin-map:
$(SOC_TOPOLOGY_STAMP): $(SOC_TOPOLOGY_SOURCE) $(SOC_TOPOLOGY_SCRIPT) $(MEMORY_MAP_SOURCE)
python3 $(SOC_TOPOLOGY_SCRIPT) --map $(SOC_TOPOLOGY_SOURCE) \
--memory-map $(MEMORY_MAP_SOURCE) --output-dir $(SOC_TOPOLOGY_DIR)
--memory-map $(MEMORY_MAP_SOURCE) --ip $(IP) --output-dir $(SOC_TOPOLOGY_DIR)
@touch $@
soc-topology: $(SOC_TOPOLOGY_STAMP)
check-soc-topology:
python3 $(SOC_TOPOLOGY_SCRIPT) --map $(SOC_TOPOLOGY_SOURCE) \
--memory-map $(MEMORY_MAP_SOURCE) --check
--memory-map $(MEMORY_MAP_SOURCE) --ip NONE --check
python3 $(SOC_TOPOLOGY_SCRIPT) --map $(SOC_TOPOLOGY_SOURCE) \
--memory-map $(MEMORY_MAP_SOURCE) --ip MDD --check
$(USER_EXTENSIONS_STAMP): $(USER_EXTENSIONS_SOURCE) $(USER_EXTENSIONS_SCRIPT)
python3 $(USER_EXTENSIONS_SCRIPT) --map $(USER_EXTENSIONS_SOURCE) \
--output-dir $(USER_EXTENSIONS_DIR)
@touch $@
user-extensions: $(USER_EXTENSIONS_STAMP)
check-user-extensions:
python3 $(USER_EXTENSIONS_SCRIPT) --map $(USER_EXTENSIONS_SOURCE) --check
check-clock-reset-domains:
python3 $(CLOCK_RESET_DOMAINS_SCRIPT) --map $(CLOCK_RESET_DOMAINS_SOURCE) --root $(ROOT_PATH)
$(FILELIST_STAMP): $(FILELIST_TEMPLATES) $(RTL_PATH)/script/generate_filelist.py \
$(RTL_PATH)/script/filelist.py $(MEMORY_MAP_STAMP) $(PIN_MAP_STAMP) $(SOC_TOPOLOGY_STAMP)
$(RTL_PATH)/script/filelist.py $(MEMORY_MAP_STAMP) $(PIN_MAP_STAMP) $(SOC_TOPOLOGY_STAMP) \
$(USER_EXTENSIONS_STAMP)
python3 $(RTL_PATH)/script/generate_filelist.py \
--output-dir $(GENERATED_FL_DIR) \
$(foreach define,$(DEF_LIST),--define $(define))
@@ -101,7 +124,8 @@ NET_FLIST += -f $(GENERATED_FL_DIR)/sys_def.fl
NET_FLIST += -f $(GENERATED_FL_DIR)/commonip.fl
NET_FLIST += -f $(GENERATED_FL_DIR)/inc.fl
RTL_FLIST := -f $(MEMORY_MAP_FILELIST) -f $(PIN_MAP_FILELIST) -f $(SOC_TOPOLOGY_FILELIST) $(NET_FLIST)
RTL_FLIST := -f $(MEMORY_MAP_FILELIST) -f $(PIN_MAP_FILELIST) -f $(SOC_TOPOLOGY_FILELIST) \
-f $(USER_EXTENSIONS_FILELIST) $(NET_FLIST)
RTL_FLIST += -f $(GENERATED_FL_DIR)/clusterip.fl
RTL_FLIST += -f $(GENERATED_FL_DIR)/ip.fl
RTL_FLIST += -f $(GENERATED_FL_DIR)/tech.fl
@@ -130,4 +154,5 @@ else ifeq ($(SIMU), IVERILOG)
endif
.PHONY: gen_mpw_code generate_filelist memory-map check-memory-map pin-map check-pin-map \
soc-topology check-soc-topology firmware
soc-topology check-soc-topology user-extensions check-user-extensions \
check-clock-reset-domains firmware
+153
View File
@@ -0,0 +1,153 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// retroSoC is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
// http://license.coscl.org.cn/MulanPSL2
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
`ifndef SV_ASSRT_DISABLE
`ifndef SYNTHESIS
module soc_bus_sva (
input logic clk_i,
input logic rst_n_i,
input logic fault_sel_i,
input logic fault_valid_i,
input logic arb_locked_i,
input logic arb_dma_owner_i,
input logic core_valid_i,
input logic dma_valid_i
);
// A decode miss must be reported and terminate the transaction locally.
assert property (@(posedge clk_i) disable iff (!rst_n_i) fault_sel_i |-> fault_valid_i);
// Retain the documented DMA-over-core arbitration policy while idle.
assert property (@(posedge clk_i) disable iff (!rst_n_i)
!arb_locked_i && dma_valid_i |=> arb_locked_i && arb_dma_owner_i);
// A sole core request must acquire ownership rather than being dropped.
assert property (@(posedge clk_i) disable iff (!rst_n_i)
!arb_locked_i && core_valid_i && !dma_valid_i |=> arb_locked_i && !arb_dma_owner_i);
endmodule
bind bus soc_bus_sva u_soc_bus_sva (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.fault_sel_i (s_fault_sel),
.fault_valid_i (fault_valid_o),
.arb_locked_i (s_mstr_lock_q),
.arb_dma_owner_i(s_mstr_id_q),
.core_valid_i (core_nmi.valid),
.dma_valid_i (dma_nmi.valid)
);
module soc_nmi2apb_sva #(
parameter int NSLV = 1
) (
input logic clk_i,
input logic rst_n_i,
input logic [NSLV-1:0] psel_comb_i,
input logic [NSLV-1:0] psel_q_i,
input logic nmi_ready_i,
input logic xfer_ready_i
);
// Address regions are disjoint and only the registered select drives a response.
assert property (@(posedge clk_i) disable iff (!rst_n_i) $onehot0(psel_comb_i));
assert property (@(posedge clk_i) disable iff (!rst_n_i) $onehot0(psel_q_i));
assert property (@(posedge clk_i) disable iff (!rst_n_i) nmi_ready_i |-> xfer_ready_i);
endmodule
bind nmi2apb soc_nmi2apb_sva #(
.NSLV(NSLV)
) u_soc_nmi2apb_sva (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.psel_comb_i (s_psel_comb),
.psel_q_i (s_psel_q),
.nmi_ready_i (nmi.ready),
.xfer_ready_i(s_xfer_ready)
);
module soc_pll_rcu_sva (
input logic clk_i,
input logic rst_n_i,
input logic [2:0] state_i,
input logic pll_capable_i,
input logic pll_apply_i,
input logic select_ext_clk_i
);
localparam logic [2:0] PLL_SAFE = 3'd1;
localparam logic [2:0] PLL_APPLY = 3'd2;
localparam logic [2:0] PLL_WAIT_LOCK = 3'd3;
localparam logic [2:0] PLL_RESPOND = 3'd5;
// The output remains on the external clock until the PLL switch stage completes.
assert property (@(posedge clk_i) disable iff (!rst_n_i)
state_i == PLL_SAFE |-> select_ext_clk_i);
assert property (@(posedge clk_i) disable iff (!rst_n_i)
pll_apply_i |-> state_i inside {PLL_APPLY, PLL_WAIT_LOCK});
assert property (@(posedge clk_i) disable iff (!rst_n_i)
state_i == PLL_SAFE && !pll_capable_i |=> state_i == PLL_RESPOND);
endmodule
bind pll_rcu_controller soc_pll_rcu_sva u_soc_pll_rcu_sva (
.clk_i (ext_clk_i),
.rst_n_i (ext_rst_n_i),
.state_i (s_state_q),
.pll_capable_i (pll_capable_i),
.pll_apply_i (pll_apply_o),
.select_ext_clk_i(select_ext_clk_o)
);
`ifdef IP_MDD
module soc_gpio_user_handoff_sva #(
parameter int DATA_WIDTH = 1
) (
input logic clk_i,
input logic rst_n_i,
input logic [DATA_WIDTH-1:0] user_sel_i,
input logic [DATA_WIDTH-1:0] user_handoff_i,
input logic [DATA_WIDTH-1:0] user_oe_i,
input logic [DATA_WIDTH-1:0] user_do_i,
input logic [DATA_WIDTH-1:0] native_oe_i,
input logic [DATA_WIDTH-1:0] native_do_i,
input logic [DATA_WIDTH-1:0] gpio_oe_i,
input logic [DATA_WIDTH-1:0] gpio_do_i
);
// A changing owner first drives output-enable low for a full handoff cycle.
assert property (@(posedge clk_i) disable iff (!rst_n_i)
gpio_oe_i == ((~user_handoff_i & user_sel_i & user_oe_i) |
(~user_handoff_i & ~user_sel_i & native_oe_i)));
assert property (@(posedge clk_i) disable iff (!rst_n_i)
gpio_do_i == ((user_sel_i & user_do_i) | (~user_sel_i & native_do_i)));
endmodule
bind nmi_gpio soc_gpio_user_handoff_sva #(
.DATA_WIDTH(`NMI_GPIO_NUM)
) u_soc_gpio_user_handoff_sva (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.user_sel_i (s_gpio_user_sel_q),
.user_handoff_i(s_gpio_user_handoff_q),
.user_oe_i (user_gpio.oe_o),
.user_do_i (user_gpio.do_o),
.native_oe_i (s_gpio_native_oe),
.native_do_i (s_gpio_native_out),
.gpio_oe_i (gpio.oe_o),
.gpio_do_i (gpio.do_o)
);
`endif
`endif
`endif
+1
View File
@@ -15,3 +15,4 @@
/top/user_ip_design_template.sv
/top/user_ip_top.sv
/top/user_ip_wrapper.sv
/sva/soc_protocol_sva.sv
+26 -3
View File
@@ -3,9 +3,11 @@
`soc_topology.json` is the source of truth for internal Mini SoC integration
that is not part of the software address-map ABI or the package pad map.
The topology generator validates the native NMI target ownership against
The topology generator validates native NMI and APB target ownership against
`../address_map/memory_map.json` and emits generated SystemVerilog include
files for `ip_nmi_wrapper.sv` and `retrosoc.sv`.
files for `ip_nmi_wrapper.sv`, `ip_apb_wrapper.sv`, `nmi2apb.sv`, and
`retrosoc.sv`. The output retains scalar interface instances and ports, so it
does not depend on interface-array support in FPGA or netlist simulation tools.
The `native_targets` list uses stable response slots. Every active native
memory-map region must appear exactly once. Disabled targets keep their slot
@@ -16,12 +18,33 @@ Each `gpio_alt_functions` entry defines both alternate modes for one GPIO pin.
peripheral output path. Values are restricted to scalar signal references and
one-bit constants so malformed expressions are rejected before RTL generation.
The `apb_targets` list defines the stable APB response slot, timed/pure APB
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.
`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
`user_ip_top.sv`. Adding an extension therefore requires a reviewed JSON entry
rather than manual edits to the SoC muxes. The module port contract remains the
existing user design template contract.
`clock_reset_domains.json` is the checked inventory of root clock domains,
reset synchronizers, and RCU PLL control crossings. It records the primitive,
source file, and instance name so an RTL refactor cannot silently remove the
declared reset or CDC structure. It complements, but does not replace, a
signoff CDC/RDC analysis tool.
Use these commands to validate or generate the selected build artifacts:
```sh
make check-soc-topology
make soc-topology
make check-user-extensions
make user-extensions
make check-clock-reset-domains
```
Generated files are written under `build/<variant>/generated/soc_topology/` and
must not be committed.
`build/<variant>/generated/user_extensions/`; they must not be committed.
@@ -0,0 +1,50 @@
{
"schema_version": 1,
"domains": [
{
"name": "external",
"clock": "s_ext_clk_buf",
"reset": "s_ext_rst_n_sync",
"reset_source": "ext_rst_n_i",
"reset_primitive": "rst_sync",
"path": "rtl/mini/top/rcu.sv",
"instance": "u_ext_rst_sync"
},
{
"name": "system",
"clock": "sys_clk_o",
"reset": "s_sys_rst_n_sync",
"reset_source": "ext_rst_n_i",
"reset_primitive": "rst_sync",
"path": "rtl/mini/top/rcu.sv",
"instance": "u_sys_rst_sync"
},
{
"name": "audio",
"clock": "s_aud_clk_buf",
"reset": "s_aud_rst_n_sync",
"reset_source": "ext_rst_n_i",
"reset_primitive": "rst_sync",
"path": "rtl/mini/top/rcu.sv",
"instance": "u_aud_rst_sync"
}
],
"crossings": [
{
"name": "pll_request",
"source": "system",
"destination": "external",
"primitive": "cdc_2phase",
"path": "rtl/mini/top/rcu.sv",
"instance": "u_pll_request_cdc"
},
{
"name": "pll_response",
"source": "external",
"destination": "system",
"primitive": "cdc_2phase",
"path": "rtl/mini/top/rcu.sv",
"instance": "u_pll_response_cdc"
}
]
}
+219 -14
View File
@@ -14,9 +14,7 @@ from typing import Any
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_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])$")
@@ -29,6 +27,16 @@ class NativeTarget:
disabled: bool
@dataclass(frozen=True)
class ApbTarget:
slot: int
name: str
timed_interface: str
pure_interface: str
region: str
requires_ip: str | None
@dataclass(frozen=True)
class GpioMode:
inputs: tuple[str, ...]
@@ -114,7 +122,17 @@ def parse_regions(value: Any, field: str) -> tuple[str, ...]:
return regions
def parse_native_targets(value: Any, memory_regions: dict[str, dict[str, Any]]) -> list[NativeTarget]:
def parse_requires_ip(value: Any, field: str) -> str | None:
if value is None:
return None
if value != "MDD":
raise ValueError(f"{field} must be MDD when present")
return value
def parse_native_targets(
value: Any, memory_regions: dict[str, dict[str, Any]]
) -> list[NativeTarget]:
if not isinstance(value, list) or not value:
raise ValueError("native_targets must be a non-empty list")
targets: list[NativeTarget] = []
@@ -180,7 +198,92 @@ def parse_native_targets(value: Any, memory_regions: dict[str, dict[str, Any]])
details.append(f"missing {', '.join(missing)}")
if extra:
details.append(f"extra {', '.join(extra)}")
raise ValueError(f"native target regions do not cover the memory map ({'; '.join(details)})")
raise ValueError(
f"native target regions do not cover the memory map ({'; '.join(details)})"
)
return ordered
def parse_apb_targets(
value: Any, memory_regions: dict[str, dict[str, Any]], ip: str
) -> list[ApbTarget]:
if not isinstance(value, list) or not value:
raise ValueError("apb_targets must be a non-empty list")
targets: list[ApbTarget] = []
names: set[str] = set()
timed_interfaces: set[str] = set()
pure_interfaces: set[str] = set()
regions: set[str] = set()
slots: set[int] = set()
for index, entry in enumerate(value):
target = require_object(entry, f"apb_targets[{index}]")
slot = target.get("slot")
if not isinstance(slot, int) or slot < 0:
raise ValueError(f"apb_targets[{index}].slot must be a non-negative integer")
name = require_identifier(target.get("name"), f"apb_targets[{index}].name")
timed_interface = require_identifier(
target.get("timed_interface"), f"apb_targets[{index}].timed_interface"
)
pure_interface = require_identifier(
target.get("pure_interface"), f"apb_targets[{index}].pure_interface"
)
region_name = require_identifier(target.get("region"), f"apb_targets[{index}].region")
requires_ip = parse_requires_ip(
target.get("requires_ip"), f"apb_targets[{index}].requires_ip"
)
region = memory_regions.get(region_name)
if region is None:
raise ValueError(f"apb_targets[{index}] references unknown region {region_name}")
if region.get("route") != "apb" or region.get("kind") != "active":
raise ValueError(
f"apb_targets[{index}] region {region_name} is not an active APB region"
)
if requires_ip != region.get("requires_ip"):
raise ValueError(
f"apb_targets[{index}] requires_ip does not match region {region_name}"
)
if name in names:
raise ValueError(f"APB target name {name} is duplicated")
if timed_interface in timed_interfaces:
raise ValueError(f"APB timed interface {timed_interface} is duplicated")
if pure_interface in pure_interfaces:
raise ValueError(f"APB pure interface {pure_interface} is duplicated")
if region_name in regions:
raise ValueError(f"APB region {region_name} has multiple targets")
if slot in slots:
raise ValueError(f"APB target slot {slot} is duplicated")
names.add(name)
timed_interfaces.add(timed_interface)
pure_interfaces.add(pure_interface)
regions.add(region_name)
slots.add(slot)
targets.append(
ApbTarget(slot, name, timed_interface, pure_interface, region_name, requires_ip)
)
enabled = [
target for target in targets if target.requires_ip is None or target.requires_ip == ip
]
ordered = sorted(enabled, key=lambda item: item.slot)
if [target.slot for target in ordered] != list(range(len(ordered))):
raise ValueError("enabled APB target slots must be contiguous from zero")
active_regions = {
symbol
for symbol, region in memory_regions.items()
if region.get("route") == "apb"
and region.get("kind") == "active"
and (region.get("requires_ip") is None or region.get("requires_ip") == ip)
}
claimed_regions = {target.region for target in ordered}
if claimed_regions != active_regions:
missing = sorted(active_regions - claimed_regions)
extra = sorted(claimed_regions - active_regions)
details: list[str] = []
if missing:
details.append(f"missing {', '.join(missing)}")
if extra:
details.append(f"extra {', '.join(extra)}")
raise ValueError(f"APB target regions do not cover the memory map ({'; '.join(details)})")
return ordered
@@ -228,8 +331,8 @@ def parse_gpio_functions(value: Any, gpio_pins: int) -> list[GpioFunction]:
def read_topology(
topology_path: Path, memory_map_path: Path
) -> tuple[list[NativeTarget], list[GpioFunction]]:
topology_path: Path, memory_map_path: Path, ip: str
) -> tuple[list[NativeTarget], list[ApbTarget], list[GpioFunction]]:
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")
@@ -239,6 +342,7 @@ def read_topology(
memory_regions = read_memory_regions(memory_map_path)
return (
parse_native_targets(document.get("native_targets"), memory_regions),
parse_apb_targets(document.get("apb_targets"), memory_regions, ip),
parse_gpio_functions(document.get("gpio_alt_functions"), gpio_pins),
)
@@ -271,9 +375,7 @@ def render_nmi_routes(
)
for target in targets:
if not target.disabled:
lines.append(
f" logic [{len(target.regions) - 1}:0] s_{target.name}_region_sel;"
)
lines.append(f" logic [{len(target.regions) - 1}:0] s_{target.name}_region_sel;")
lines.append("")
for target in targets:
if target.disabled:
@@ -322,6 +424,100 @@ def render_nmi_routes(
return "\n".join(lines) + "\n"
def render_apb_interfaces(targets: list[ApbTarget]) -> str:
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
for target in targets:
lines.extend(
[
f" apb4_if {target.timed_interface} (clk_i, rst_n_i);",
f" apb4_pure_if {target.pure_interface} ();",
]
)
return "\n".join(lines) + "\n"
def render_apb_bridges(targets: list[ApbTarget]) -> str:
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
for target in targets:
lines.extend(
[
f" apb4_if_bridge u_{target.name}_apb_bridge (",
f" .apb_pure({target.pure_interface}),",
f" .timed ({target.timed_interface})",
" );",
"",
]
)
return "\n".join(lines) + "\n"
def render_apb_ports(targets: list[ApbTarget]) -> str:
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
for index, target in enumerate(targets):
separator = "," if index != len(targets) - 1 else ""
lines.append(f" apb4_pure_if.master {target.name}{separator}")
return "\n".join(lines) + "\n"
def render_apb_connections(targets: list[ApbTarget]) -> str:
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
for index, target in enumerate(targets):
separator = "," if index != len(targets) - 1 else ""
lines.append(f" .{target.name}({target.pure_interface}){separator}")
return "\n".join(lines) + "\n"
def render_apb_declarations(targets: list[ApbTarget]) -> str:
count = len(targets)
return "\n".join(
[
"// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit.",
f" localparam int NSLV = {count};",
" logic [NSLV-1:0] s_psel_comb, s_psel_d, s_psel_q;",
"",
]
)
def render_apb_request_routes(targets: list[ApbTarget]) -> str:
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
for target in targets:
lines.extend(
[
f" assign {target.name}.paddr = nmi.addr;",
f" assign {target.name}.pprot = '0;",
f" assign {target.name}.psel = s_xfer_valid && `SOC_ADDR_IS_{target.region}(nmi.addr);",
f" assign {target.name}.penable = s_fsm_q == FSM_ENAB;",
f" assign {target.name}.pwrite = |nmi.wstrb;",
f" assign {target.name}.pwdata = nmi.wdata;",
f" assign {target.name}.pstrb = nmi.wstrb;",
"",
]
)
return "\n".join(lines) + "\n"
def render_apb_select_routes(targets: list[ApbTarget]) -> str:
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
lines.extend(
f" assign s_psel_comb[{target.slot}] = `SOC_ADDR_IS_{target.region}(nmi.addr);"
for target in targets
)
return "\n".join(lines) + "\n"
def render_apb_response_mux(targets: list[ApbTarget]) -> str:
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
for signal, expression in (("s_rd_data", "prdata"), ("s_xfer_ready", "pready")):
terms = [
f"({{{'32' if signal == 's_rd_data' else '1'}{{s_psel_q[{target.slot}]}}}} & "
f"{target.name}.{expression})"
for target in targets
]
lines.append(f" assign {signal} = {' | '.join(terms)};")
return "\n".join(lines) + "\n"
def render_gpio_bindings(functions: list[GpioFunction]) -> str:
lines = ["// Generated by rtl/mini/integration/generate_soc_topology.py; do not edit."]
for function in functions:
@@ -341,12 +537,20 @@ def render_gpio_bindings(functions: list[GpioFunction]) -> str:
return "\n".join(lines) + "\n"
def generate(topology_path: Path, memory_map_path: Path, output_dir: Path) -> None:
targets, gpio_functions = read_topology(topology_path, memory_map_path)
def generate(topology_path: Path, memory_map_path: Path, output_dir: Path, ip: str) -> None:
targets, apb_targets, gpio_functions = read_topology(topology_path, memory_map_path, ip)
memory_regions = read_memory_regions(memory_map_path)
rtl_dir = output_dir / "rtl"
atomic_write(rtl_dir / "soc_nmi_interfaces.svh", render_nmi_interfaces(targets))
atomic_write(rtl_dir / "soc_nmi_routes.svh", render_nmi_routes(targets, memory_regions))
atomic_write(rtl_dir / "soc_apb_interfaces.svh", render_apb_interfaces(apb_targets))
atomic_write(rtl_dir / "soc_apb_bridges.svh", render_apb_bridges(apb_targets))
atomic_write(rtl_dir / "soc_apb_ports.svh", render_apb_ports(apb_targets))
atomic_write(rtl_dir / "soc_apb_connections.svh", render_apb_connections(apb_targets))
atomic_write(rtl_dir / "soc_apb_declarations.svh", render_apb_declarations(apb_targets))
atomic_write(rtl_dir / "soc_apb_request_routes.svh", render_apb_request_routes(apb_targets))
atomic_write(rtl_dir / "soc_apb_select_routes.svh", render_apb_select_routes(apb_targets))
atomic_write(rtl_dir / "soc_apb_response_mux.svh", render_apb_response_mux(apb_targets))
atomic_write(rtl_dir / "soc_gpio_alt_bindings.svh", render_gpio_bindings(gpio_functions))
atomic_write(output_dir / "soc_topology.fl", f"+incdir+{rtl_dir}\n")
@@ -356,15 +560,16 @@ def main() -> int:
parser.add_argument("--map", required=True, type=Path)
parser.add_argument("--memory-map", required=True, type=Path)
parser.add_argument("--output-dir", type=Path)
parser.add_argument("--ip", choices=("NONE", "MDD"), default="NONE")
parser.add_argument("--check", action="store_true", help="validate only; write nothing")
arguments = parser.parse_args()
try:
if arguments.check:
read_topology(arguments.map, arguments.memory_map)
read_topology(arguments.map, arguments.memory_map, arguments.ip)
else:
if arguments.output_dir is None:
parser.error("--output-dir is required unless --check is used")
generate(arguments.map, arguments.memory_map, arguments.output_dir)
generate(arguments.map, arguments.memory_map, arguments.output_dir, arguments.ip)
except (OSError, ValueError, json.JSONDecodeError) as error:
parser.error(str(error))
return 0
@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""Generate scalar user core and user IP integration bindings."""
from __future__ import annotations
import argparse
import json
import os
import re
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any
IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$")
APB_REQUEST_SIGNALS = ("paddr", "pprot", "psel", "penable", "pwrite", "pwdata", "pstrb")
@dataclass(frozen=True)
class ExtensionTarget:
slot: int
module: str
instance: str
@dataclass(frozen=True)
class ExtensionMap:
core_selector_width: int
ip_selector_width: int
gpio_width: int
core_targets: tuple[ExtensionTarget, ...]
ip_targets: tuple[ExtensionTarget, ...]
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:
return
descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
try:
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as output:
output.write(content)
os.replace(temporary_name, path)
except BaseException:
Path(temporary_name).unlink(missing_ok=True)
raise
def require_object(value: Any, field: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise ValueError(f"{field} must be an object")
return value
def require_identifier(value: Any, field: str) -> str:
if not isinstance(value, str) or IDENTIFIER_RE.fullmatch(value) is None:
raise ValueError(f"{field} must be a SystemVerilog identifier")
return value
def require_positive_integer(value: Any, field: str) -> int:
if not isinstance(value, int) or value <= 0:
raise ValueError(f"{field} must be a positive integer")
return value
def parse_targets(value: Any, field: str, selector_width: int) -> tuple[ExtensionTarget, ...]:
if not isinstance(value, list) or not value:
raise ValueError(f"{field} must be a non-empty list")
targets: list[ExtensionTarget] = []
slots: set[int] = set()
instances: set[str] = set()
for index, entry in enumerate(value):
target = require_object(entry, f"{field}[{index}]")
slot = target.get("slot")
if not isinstance(slot, int) or not 0 < slot < (1 << selector_width):
raise ValueError(f"{field}[{index}].slot must be within the selector range")
module = require_identifier(target.get("module"), f"{field}[{index}].module")
instance = require_identifier(target.get("instance"), f"{field}[{index}].instance")
if slot in slots:
raise ValueError(f"{field} slot {slot} is duplicated")
if instance in instances:
raise ValueError(f"{field} instance {instance} is duplicated")
slots.add(slot)
instances.add(instance)
targets.append(ExtensionTarget(slot, module, instance))
ordered = tuple(sorted(targets, key=lambda item: item.slot))
if [target.slot for target in ordered] != list(range(1, len(ordered) + 1)):
raise ValueError(f"{field} slots must be contiguous from one")
return ordered
def read_extensions(path: Path) -> ExtensionMap:
document = require_object(json.loads(path.read_text(encoding="utf-8")), "user extensions")
if document.get("schema_version") != 1:
raise ValueError("schema_version must be 1")
core_selector_width = require_positive_integer(
document.get("core_selector_width"), "core_selector_width"
)
ip_selector_width = require_positive_integer(
document.get("ip_selector_width"), "ip_selector_width"
)
gpio_width = require_positive_integer(document.get("gpio_width"), "gpio_width")
return ExtensionMap(
core_selector_width,
ip_selector_width,
gpio_width,
parse_targets(document.get("core_targets"), "core_targets", core_selector_width),
parse_targets(document.get("ip_targets"), "ip_targets", ip_selector_width),
)
def render_core_bindings(extensions: ExtensionMap) -> str:
lines = ["// Generated by rtl/mini/integration/generate_user_extensions.py; do not edit."]
for target in extensions.core_targets:
lines.extend(
[
f" nmi_if u_user_{target.slot}_nmi_if ();",
f" logic [31:0] s_user_{target.slot}_irq;",
]
)
lines.extend(
[
"",
" always_comb begin",
" nmi.valid = '0;",
" nmi.addr = '0;",
" nmi.wdata = '0;",
" nmi.wstrb = '0;",
]
)
for target in extensions.core_targets:
lines.extend(
[
f" u_user_{target.slot}_nmi_if.rdata = '0;",
f" u_user_{target.slot}_nmi_if.ready = '0;",
f" s_user_{target.slot}_irq = '0;",
]
)
lines.append(" unique case (sel_i)")
for target in extensions.core_targets:
lines.extend(
[
f" {extensions.core_selector_width}'d{target.slot}: begin",
f" nmi.valid = u_user_{target.slot}_nmi_if.valid;",
f" nmi.addr = u_user_{target.slot}_nmi_if.addr;",
f" nmi.wdata = u_user_{target.slot}_nmi_if.wdata;",
f" nmi.wstrb = u_user_{target.slot}_nmi_if.wstrb;",
f" u_user_{target.slot}_nmi_if.rdata = nmi.rdata;",
f" u_user_{target.slot}_nmi_if.ready = nmi.ready;",
f" s_user_{target.slot}_irq = irq_i;",
" end",
]
)
lines.extend([" default: ;", " endcase", " end", ""])
for target in extensions.core_targets:
lines.extend(
[
f" {target.module} #({target.slot}) {target.instance} (",
" .clk_i (clk_i),",
" .rst_n_i(rst_n_i),",
f" .irq_i (s_user_{target.slot}_irq),",
f" .nmi (u_user_{target.slot}_nmi_if)",
" );",
"",
]
)
return "\n".join(lines)
def render_ip_bindings(extensions: ExtensionMap) -> str:
lines = ["// Generated by rtl/mini/integration/generate_user_extensions.py; do not edit."]
for target in extensions.ip_targets:
lines.extend(
[
f" user_gpio_if #(`USER_GPIO_NUM) u_user_{target.slot}_gpio_if ();",
f" apb4_if u_user_{target.slot}_apb_if (clk_i, rst_n_i);",
]
)
lines.extend(
[
"",
" always_comb begin",
" gpio.do_o = '0;",
" gpio.oe_o = '0;",
" apb.pready = '0;",
" apb.prdata = '0;",
" apb.pslverr = '0;",
]
)
for target in extensions.ip_targets:
lines.extend(
f" u_user_{target.slot}_apb_if.{signal} = '0;" for signal in APB_REQUEST_SIGNALS
)
lines.append(" unique case (sel_i)")
for target in extensions.ip_targets:
lines.extend(
[
f" {extensions.ip_selector_width}'d{target.slot}: begin",
f" gpio.do_o = u_user_{target.slot}_gpio_if.do_o;",
f" gpio.oe_o = u_user_{target.slot}_gpio_if.oe_o;",
f" apb.pready = u_user_{target.slot}_apb_if.pready;",
f" apb.prdata = u_user_{target.slot}_apb_if.prdata;",
f" apb.pslverr = u_user_{target.slot}_apb_if.pslverr;",
]
)
lines.extend(
f" u_user_{target.slot}_apb_if.{signal} = apb.{signal};"
for signal in APB_REQUEST_SIGNALS
)
lines.append(" end")
lines.extend([" default: ;", " endcase", " end", ""])
for target in extensions.ip_targets:
lines.append(f" assign u_user_{target.slot}_gpio_if.di_i = gpio.di_i;")
lines.append("")
for target in extensions.ip_targets:
lines.extend(
[
f" {target.module} #({target.slot}) {target.instance} (",
" .clk_i (clk_i),",
" .rst_n_i(rst_n_i),",
f" .gpio (u_user_{target.slot}_gpio_if),",
f" .apb (u_user_{target.slot}_apb_if)",
" );",
"",
]
)
return "\n".join(lines)
def generate(source: Path, output_dir: Path) -> None:
extensions = read_extensions(source)
rtl_dir = output_dir / "rtl"
atomic_write(rtl_dir / "user_core_bindings.svh", render_core_bindings(extensions))
atomic_write(rtl_dir / "user_ip_bindings.svh", render_ip_bindings(extensions))
atomic_write(output_dir / "user_extensions.fl", f"+incdir+{rtl_dir}\n")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--map", required=True, type=Path)
parser.add_argument("--output-dir", type=Path)
parser.add_argument("--check", action="store_true", help="validate only; write nothing")
arguments = parser.parse_args()
try:
if arguments.check:
read_extensions(arguments.map)
else:
if arguments.output_dir is None:
parser.error("--output-dir is required unless --check is used")
generate(arguments.map, arguments.output_dir)
except (OSError, ValueError, json.JSONDecodeError) as error:
parser.error(str(error))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+12
View File
@@ -21,6 +21,18 @@
{"slot": 16, "name": "opipsram", "interface": "u_opipsram_nmi_if", "disabled": true},
{"slot": 17, "name": "i2c1", "interface": "u_i2c1_nmi_if", "regions": ["NMI_I2C1"]}
],
"apb_targets": [
{"slot": 0, "name": "archinfo", "timed_interface": "u_archinfo_apb_if", "pure_interface": "u_archinfo_apb_pure_if", "region": "APB_ARCHINFO"},
{"slot": 1, "name": "rng", "timed_interface": "u_rng_apb_if", "pure_interface": "u_rng_apb_pure_if", "region": "APB_RNG"},
{"slot": 2, "name": "uart", "timed_interface": "u_uart1_apb_if", "pure_interface": "u_uart1_apb_pure_if", "region": "APB_UART1"},
{"slot": 3, "name": "pwm", "timed_interface": "u_pwm_apb_if", "pure_interface": "u_pwm_apb_pure_if", "region": "APB_PWM"},
{"slot": 4, "name": "ps2", "timed_interface": "u_ps2_apb_if", "pure_interface": "u_ps2_apb_pure_if", "region": "APB_PS2"},
{"slot": 5, "name": "rtc", "timed_interface": "u_rtc_apb_if", "pure_interface": "u_rtc_apb_pure_if", "region": "APB_RTC"},
{"slot": 6, "name": "wdg", "timed_interface": "u_wdg_apb_if", "pure_interface": "u_wdg_apb_pure_if", "region": "APB_WDG"},
{"slot": 7, "name": "crc", "timed_interface": "u_crc_apb_if", "pure_interface": "u_crc_apb_pure_if", "region": "APB_CRC"},
{"slot": 8, "name": "tmr", "timed_interface": "u_tmr_apb_if", "pure_interface": "u_tmr_apb_pure_if", "region": "APB_TMR"},
{"slot": 9, "name": "user_ip", "timed_interface": "u_user_ip_apb_if", "pure_interface": "u_user_ip_apb_pure_if", "region": "APB_USER_IP", "requires_ip": "MDD"}
],
"gpio_alt_functions": [
{"pin": 0, "alt0": {"inputs": ["u_uart1_if.rx_i"], "do": "1'b0", "oe": "1'b0"}, "alt1": {"inputs": ["u_ps2_if.ps2_clk_i"], "do": "1'b0", "oe": "1'b0"}},
{"pin": 1, "alt0": {"inputs": [], "do": "u_uart1_if.tx_o", "oe": "1'b1"}, "alt1": {"inputs": ["u_ps2_if.ps2_dat_i"], "do": "1'b0", "oe": "1'b0"}},
+18
View File
@@ -0,0 +1,18 @@
{
"schema_version": 1,
"core_selector_width": 5,
"ip_selector_width": 8,
"gpio_width": 32,
"core_targets": [
{"slot": 1, "module": "user_core_design_username1", "instance": "u_user_core_design_username1"},
{"slot": 2, "module": "user_core_design_username2", "instance": "u_user_core_design_username2"},
{"slot": 3, "module": "user_core_design_username3", "instance": "u_user_core_design_username3"},
{"slot": 4, "module": "user_core_design_username4", "instance": "u_user_core_design_username4"},
{"slot": 5, "module": "user_core_design_username5", "instance": "u_user_core_design_username5"},
{"slot": 6, "module": "user_core_design_username6", "instance": "u_user_core_design_username6"}
],
"ip_targets": [
{"slot": 1, "module": "user_ip_design_username1", "instance": "u_user_ip_design_username1"},
{"slot": 2, "module": "user_ip_design_username2", "instance": "u_user_ip_design_username2"}
]
}
+2 -3
View File
@@ -26,6 +26,7 @@ PATH_PREFIXES = {
"picorv32": RTL_DIR / "managed" / "picorv32" / "rtl",
"tb": MINI_DIR / "dv" / "tb",
"model": MINI_DIR / "dv" / "model",
"sva": MINI_DIR / "dv" / "sva",
"top": MINI_DIR / "top",
}
@@ -101,9 +102,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR, help="output directory"
)
parser.add_argument(
"--define", action="append", default=[], help="filelist +define+ token"
)
parser.add_argument("--define", action="append", default=[], help="filelist +define+ token")
return parser.parse_args()
+12 -88
View File
@@ -33,85 +33,20 @@ module ip_apb_wrapper (
// verilog_format: on
);
// verilog_format: off
apb4_if u_archinfo_apb_if (clk_i, rst_n_i);
apb4_if u_rng_apb_if (clk_i, rst_n_i);
apb4_if u_uart1_apb_if (clk_i, rst_n_i);
apb4_if u_pwm_apb_if (clk_i, rst_n_i);
apb4_if u_ps2_apb_if (clk_i, rst_n_i);
apb4_if u_rtc_apb_if (clk_i, rst_n_i);
apb4_if u_wdg_apb_if (clk_i, rst_n_i);
apb4_if u_crc_apb_if (clk_i, rst_n_i);
apb4_if u_tmr_apb_if (clk_i, rst_n_i);
// Generated timed and pure scalar APB interfaces preserve FPGA compatibility.
`include "soc_apb_interfaces.svh"
// NOTE: for FPGA-compatible
apb4_pure_if u_archinfo_apb_pure_if ();
apb4_pure_if u_rng_apb_pure_if ();
apb4_pure_if u_uart1_apb_pure_if ();
apb4_pure_if u_pwm_apb_pure_if ();
apb4_pure_if u_ps2_apb_pure_if ();
apb4_pure_if u_rtc_apb_pure_if ();
apb4_pure_if u_wdg_apb_pure_if ();
apb4_pure_if u_crc_apb_pure_if ();
apb4_pure_if u_tmr_apb_pure_if ();
// low freq clock perip
rtc_if u_rtc_if (clk_aud_i, rst_aud_n_i);
// Low-frequency peripherals retain their dedicated interfaces.
rtc_if u_rtc_if (
clk_aud_i,
rst_aud_n_i
);
wdg_if u_wdg_if (clk_aud_i);
tmr_if u_tmr_if (clk_aud_i);
assign u_tmr_if.capch_i = tmr_capch_i;
// verilog_format: on
// verilog_format: off
`ifdef IP_MDD
apb4_if u_user_ip_apb_if (clk_i, rst_n_i);
apb4_pure_if u_user_ip_apb_pure_if ();
`endif
// verilog_format: on
apb4_if_bridge u_archinfo_apb_bridge (
.apb_pure(u_archinfo_apb_pure_if),
.timed (u_archinfo_apb_if)
);
apb4_if_bridge u_rng_apb_bridge (
.apb_pure(u_rng_apb_pure_if),
.timed (u_rng_apb_if)
);
apb4_if_bridge u_uart1_apb_bridge (
.apb_pure(u_uart1_apb_pure_if),
.timed (u_uart1_apb_if)
);
apb4_if_bridge u_pwm_apb_bridge (
.apb_pure(u_pwm_apb_pure_if),
.timed (u_pwm_apb_if)
);
apb4_if_bridge u_ps2_apb_bridge (
.apb_pure(u_ps2_apb_pure_if),
.timed (u_ps2_apb_if)
);
apb4_if_bridge u_rtc_apb_bridge (
.apb_pure(u_rtc_apb_pure_if),
.timed (u_rtc_apb_if)
);
apb4_if_bridge u_wdg_apb_bridge (
.apb_pure(u_wdg_apb_pure_if),
.timed (u_wdg_apb_if)
);
apb4_if_bridge u_crc_apb_bridge (
.apb_pure(u_crc_apb_pure_if),
.timed (u_crc_apb_if)
);
apb4_if_bridge u_tmr_apb_bridge (
.apb_pure(u_tmr_apb_pure_if),
.timed (u_tmr_apb_if)
);
`ifdef IP_MDD
apb4_if_bridge u_user_ip_apb_bridge (
.apb_pure(u_user_ip_apb_pure_if),
.timed (u_user_ip_apb_if)
);
`endif
`include "soc_apb_bridges.svh"
// verilog_format: off
apb4_archinfo u_apb4_archinfo (u_archinfo_apb_if);
@@ -135,21 +70,10 @@ module ip_apb_wrapper (
assign irq_o[6] = 1'b0;
nmi2apb u_nmi2apb (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.nmi (nmi),
`ifdef IP_MDD
.user_ip (u_user_ip_apb_pure_if),
`endif
.archinfo(u_archinfo_apb_pure_if),
.rng (u_rng_apb_pure_if),
.uart (u_uart1_apb_pure_if),
.pwm (u_pwm_apb_pure_if),
.ps2 (u_ps2_apb_pure_if),
.rtc (u_rtc_apb_pure_if),
.wdg (u_wdg_apb_pure_if),
.crc (u_crc_apb_pure_if),
.tmr (u_tmr_apb_pure_if)
.clk_i (clk_i),
.rst_n_i(rst_n_i),
.nmi (nmi),
`include "soc_apb_connections.svh"
);
`ifdef IP_MDD
+2 -163
View File
@@ -20,168 +20,7 @@ module user_core_top (
// verilog_format: on
);
// ====== GENERATED BY SCRIPT ======
// verilog_format: off
nmi_if u_user_1_nmi_if ();
nmi_if u_user_2_nmi_if ();
nmi_if u_user_3_nmi_if ();
nmi_if u_user_4_nmi_if ();
nmi_if u_user_5_nmi_if ();
nmi_if u_user_6_nmi_if ();
logic [31:0] s_user_1_irq;
logic [31:0] s_user_2_irq;
logic [31:0] s_user_3_irq;
logic [31:0] s_user_4_irq;
logic [31:0] s_user_5_irq;
logic [31:0] s_user_6_irq;
// ...
// verilog_format: on
always_comb begin
nmi.valid = '0;
nmi.addr = '0;
nmi.wdata = '0;
nmi.wstrb = '0;
u_user_1_nmi_if.rdata = '0;
u_user_1_nmi_if.ready = '0;
s_user_1_irq = '0;
u_user_2_nmi_if.rdata = '0;
u_user_2_nmi_if.ready = '0;
s_user_2_irq = '0;
u_user_3_nmi_if.rdata = '0;
u_user_3_nmi_if.ready = '0;
s_user_3_irq = '0;
u_user_4_nmi_if.rdata = '0;
u_user_4_nmi_if.ready = '0;
s_user_4_irq = '0;
u_user_5_nmi_if.rdata = '0;
u_user_5_nmi_if.ready = '0;
s_user_5_irq = '0;
u_user_6_nmi_if.rdata = '0;
u_user_6_nmi_if.ready = '0;
s_user_6_irq = '0;
unique case (sel_i)
5'd1: begin
nmi.valid = u_user_1_nmi_if.valid;
nmi.addr = u_user_1_nmi_if.addr;
nmi.wdata = u_user_1_nmi_if.wdata;
nmi.wstrb = u_user_1_nmi_if.wstrb;
u_user_1_nmi_if.rdata = nmi.rdata;
u_user_1_nmi_if.ready = nmi.ready;
s_user_1_irq = irq_i;
end
5'd2: begin
nmi.valid = u_user_2_nmi_if.valid;
nmi.addr = u_user_2_nmi_if.addr;
nmi.wdata = u_user_2_nmi_if.wdata;
nmi.wstrb = u_user_2_nmi_if.wstrb;
u_user_2_nmi_if.rdata = nmi.rdata;
u_user_2_nmi_if.ready = nmi.ready;
s_user_2_irq = irq_i;
end
5'd3: begin
nmi.valid = u_user_3_nmi_if.valid;
nmi.addr = u_user_3_nmi_if.addr;
nmi.wdata = u_user_3_nmi_if.wdata;
nmi.wstrb = u_user_3_nmi_if.wstrb;
u_user_3_nmi_if.rdata = nmi.rdata;
u_user_3_nmi_if.ready = nmi.ready;
s_user_3_irq = irq_i;
end
5'd4: begin
nmi.valid = u_user_4_nmi_if.valid;
nmi.addr = u_user_4_nmi_if.addr;
nmi.wdata = u_user_4_nmi_if.wdata;
nmi.wstrb = u_user_4_nmi_if.wstrb;
u_user_4_nmi_if.rdata = nmi.rdata;
u_user_4_nmi_if.ready = nmi.ready;
s_user_4_irq = irq_i;
end
5'd5: begin
nmi.valid = u_user_5_nmi_if.valid;
nmi.addr = u_user_5_nmi_if.addr;
nmi.wdata = u_user_5_nmi_if.wdata;
nmi.wstrb = u_user_5_nmi_if.wstrb;
u_user_5_nmi_if.rdata = nmi.rdata;
u_user_5_nmi_if.ready = nmi.ready;
s_user_5_irq = irq_i;
end
5'd6: begin
nmi.valid = u_user_6_nmi_if.valid;
nmi.addr = u_user_6_nmi_if.addr;
nmi.wdata = u_user_6_nmi_if.wdata;
nmi.wstrb = u_user_6_nmi_if.wstrb;
u_user_6_nmi_if.rdata = nmi.rdata;
u_user_6_nmi_if.ready = nmi.ready;
s_user_6_irq = irq_i;
end
default: begin
nmi.valid = '0;
nmi.addr = '0;
nmi.wdata = '0;
nmi.wstrb = '0;
u_user_1_nmi_if.rdata = '0;
u_user_1_nmi_if.ready = '0;
s_user_1_irq = '0;
u_user_2_nmi_if.rdata = '0;
u_user_2_nmi_if.ready = '0;
s_user_2_irq = '0;
u_user_3_nmi_if.rdata = '0;
u_user_3_nmi_if.ready = '0;
s_user_3_irq = '0;
u_user_4_nmi_if.rdata = '0;
u_user_4_nmi_if.ready = '0;
s_user_4_irq = '0;
u_user_5_nmi_if.rdata = '0;
u_user_5_nmi_if.ready = '0;
s_user_5_irq = '0;
u_user_6_nmi_if.rdata = '0;
u_user_6_nmi_if.ready = '0;
s_user_6_irq = '0;
end
endcase
end
user_core_design_username1 #(1) u_user_core_design_username1 (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
.irq_i (s_user_1_irq),
.nmi (u_user_1_nmi_if)
);
user_core_design_username2 #(2) u_user_core_design_username2 (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
.irq_i (s_user_2_irq),
.nmi (u_user_2_nmi_if)
);
user_core_design_username3 #(3) u_user_core_design_username3 (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
.irq_i (s_user_3_irq),
.nmi (u_user_3_nmi_if)
);
user_core_design_username4 #(4) u_user_core_design_username4 (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
.irq_i (s_user_4_irq),
.nmi (u_user_4_nmi_if)
);
user_core_design_username5 #(5) u_user_core_design_username5 (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
.irq_i (s_user_5_irq),
.nmi (u_user_5_nmi_if)
);
user_core_design_username6 #(6) u_user_core_design_username6 (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
.irq_i (s_user_6_irq),
.nmi (u_user_6_nmi_if)
);
// Generated bindings preserve scalar user-core interfaces and isolation.
`include "user_core_bindings.svh"
endmodule
+2 -106
View File
@@ -21,111 +21,7 @@ module user_ip_top (
// verilog_format: on
);
// ====== GENERATED BY SCRIPT ======
// verilog_format: off
user_gpio_if #(`USER_GPIO_NUM) u_user_1_gpio_if();
user_gpio_if #(`USER_GPIO_NUM) u_user_2_gpio_if();
apb4_if u_user_1_apb_if(clk_i, rst_n_i);
apb4_if u_user_2_apb_if(clk_i, rst_n_i);
// ...
// verilog_format: on
always_comb begin
// gpio
gpio.do_o = '0;
gpio.oe_o = '0;
// apb
apb.pready = '0;
apb.prdata = '0;
apb.pslverr = '0;
u_user_1_apb_if.paddr = '0;
u_user_1_apb_if.pprot = '0;
u_user_1_apb_if.psel = '0;
u_user_1_apb_if.penable = '0;
u_user_1_apb_if.pwrite = '0;
u_user_1_apb_if.pwdata = '0;
u_user_1_apb_if.pstrb = '0;
u_user_2_apb_if.paddr = '0;
u_user_2_apb_if.pprot = '0;
u_user_2_apb_if.psel = '0;
u_user_2_apb_if.penable = '0;
u_user_2_apb_if.pwrite = '0;
u_user_2_apb_if.pwdata = '0;
u_user_2_apb_if.pstrb = '0;
unique case (sel_i)
8'd1: begin
// gpio
gpio.do_o = u_user_1_gpio_if.do_o;
gpio.oe_o = u_user_1_gpio_if.oe_o;
// apb
apb.pready = u_user_1_apb_if.pready;
apb.prdata = u_user_1_apb_if.prdata;
apb.pslverr = u_user_1_apb_if.pslverr;
u_user_1_apb_if.paddr = apb.paddr;
u_user_1_apb_if.pprot = apb.pprot;
u_user_1_apb_if.psel = apb.psel;
u_user_1_apb_if.penable = apb.penable;
u_user_1_apb_if.pwrite = apb.pwrite;
u_user_1_apb_if.pwdata = apb.pwdata;
u_user_1_apb_if.pstrb = apb.pstrb;
end
8'd2: begin
// gpio
gpio.do_o = u_user_2_gpio_if.do_o;
gpio.oe_o = u_user_2_gpio_if.oe_o;
// apb
apb.pready = u_user_2_apb_if.pready;
apb.prdata = u_user_2_apb_if.prdata;
apb.pslverr = u_user_2_apb_if.pslverr;
u_user_2_apb_if.paddr = apb.paddr;
u_user_2_apb_if.pprot = apb.pprot;
u_user_2_apb_if.psel = apb.psel;
u_user_2_apb_if.penable = apb.penable;
u_user_2_apb_if.pwrite = apb.pwrite;
u_user_2_apb_if.pwdata = apb.pwdata;
u_user_2_apb_if.pstrb = apb.pstrb;
end
default: begin
// gpio
gpio.do_o = '0;
gpio.oe_o = '0;
// apb
apb.pready = '0;
apb.prdata = '0;
apb.pslverr = '0;
u_user_1_apb_if.paddr = '0;
u_user_1_apb_if.pprot = '0;
u_user_1_apb_if.psel = '0;
u_user_1_apb_if.penable = '0;
u_user_1_apb_if.pwrite = '0;
u_user_1_apb_if.pwdata = '0;
u_user_1_apb_if.pstrb = '0;
u_user_2_apb_if.paddr = '0;
u_user_2_apb_if.pprot = '0;
u_user_2_apb_if.psel = '0;
u_user_2_apb_if.penable = '0;
u_user_2_apb_if.pwrite = '0;
u_user_2_apb_if.pwdata = '0;
u_user_2_apb_if.pstrb = '0;
end
endcase
end
assign u_user_1_gpio_if.di_i = gpio.di_i;
assign u_user_2_gpio_if.di_i = gpio.di_i;
user_ip_design_username1 #(1) u_user_ip_design_username1 (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
.gpio (u_user_1_gpio_if),
.apb (u_user_1_apb_if)
);
user_ip_design_username2 #(2) u_user_ip_design_username2 (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
.gpio (u_user_2_gpio_if),
.apb (u_user_2_apb_if)
);
// Generated bindings preserve scalar user-IP interfaces and isolation.
`include "user_ip_bindings.svh"
endmodule
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Validate the declared Mini SoC root clock/reset domains and CDC crossings."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
from typing import Any
RESET_PRIMITIVES = {"rst_sync"}
CDC_PRIMITIVES = {"async_fifo", "cdc_2phase", "cdc_sync"}
IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*$")
def require_object(value: Any, field: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise ValueError(f"{field} must be an object")
return value
def require_string(value: Any, field: str) -> str:
if not isinstance(value, str) or not value:
raise ValueError(f"{field} must be a non-empty string")
return value
def require_identifier(value: Any, field: str) -> str:
identifier = require_string(value, field)
if IDENTIFIER_RE.fullmatch(identifier) is None:
raise ValueError(f"{field} must be a SystemVerilog identifier")
return identifier
def require_instance(path: Path, primitive: str, instance: str, field: str) -> None:
if not path.is_file():
raise ValueError(f"{field}.path does not exist: {path}")
source = path.read_text(encoding="utf-8")
expression = re.compile(rf"\b{re.escape(primitive)}\b[\s\S]*?\b{re.escape(instance)}\b")
if expression.search(source) is None:
raise ValueError(f"{field} does not declare {primitive} instance {instance}")
def validate(document_path: Path, root: Path) -> None:
document = require_object(json.loads(document_path.read_text(encoding="utf-8")), "domain map")
if document.get("schema_version") != 1:
raise ValueError("schema_version must be 1")
raw_domains = document.get("domains")
if not isinstance(raw_domains, list) or not raw_domains:
raise ValueError("domains must be a non-empty list")
names: set[str] = set()
for index, value in enumerate(raw_domains):
field = f"domains[{index}]"
domain = require_object(value, field)
name = require_identifier(domain.get("name"), f"{field}.name")
if name in names:
raise ValueError(f"domain {name} is duplicated")
names.add(name)
require_identifier(domain.get("clock"), f"{field}.clock")
require_identifier(domain.get("reset"), f"{field}.reset")
require_identifier(domain.get("reset_source"), f"{field}.reset_source")
primitive = require_string(domain.get("reset_primitive"), f"{field}.reset_primitive")
if primitive not in RESET_PRIMITIVES:
raise ValueError(f"{field}.reset_primitive is not supported")
path = root / require_string(domain.get("path"), f"{field}.path")
instance = require_identifier(domain.get("instance"), f"{field}.instance")
require_instance(path, primitive, instance, field)
raw_crossings = document.get("crossings")
if not isinstance(raw_crossings, list):
raise ValueError("crossings must be a list")
crossing_names: set[str] = set()
for index, value in enumerate(raw_crossings):
field = f"crossings[{index}]"
crossing = require_object(value, field)
name = require_identifier(crossing.get("name"), f"{field}.name")
if name in crossing_names:
raise ValueError(f"crossing {name} is duplicated")
crossing_names.add(name)
source = require_identifier(crossing.get("source"), f"{field}.source")
destination = require_identifier(crossing.get("destination"), f"{field}.destination")
if source not in names or destination not in names:
raise ValueError(f"{field} references an unknown domain")
if source == destination:
raise ValueError(f"{field} must cross between distinct domains")
primitive = require_string(crossing.get("primitive"), f"{field}.primitive")
if primitive not in CDC_PRIMITIVES:
raise ValueError(f"{field}.primitive is not an approved CDC primitive")
path = root / require_string(crossing.get("path"), f"{field}.path")
instance = require_identifier(crossing.get("instance"), f"{field}.instance")
require_instance(path, primitive, instance, field)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--map", required=True, type=Path)
parser.add_argument("--root", required=True, type=Path)
arguments = parser.parse_args()
try:
validate(arguments.map, arguments.root)
except (OSError, ValueError, json.JSONDecodeError) as error:
parser.error(str(error))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+7 -4
View File
@@ -21,13 +21,13 @@ PR_COMMANDS = (
("configs/ci/hazard3-rv32im-ihp130-ip-mdd-shell.mk", ("firmware",)),
(
"configs/ci/hazard3-rv32im-ihp130-ip-mdd.mk",
("SIMU=VERILATOR", "firmware", "sim"),
("SIMU=VERILATOR", "HAVE_SVA=YES", "firmware", "sim"),
),
(
"configs/ci/mdd-rv32im-ihp130.mk",
("SIMU=VERILATOR", "firmware", "sim"),
("SIMU=VERILATOR", "HAVE_SVA=YES", "firmware", "sim"),
),
("configs/ci/hazard3-rv32im-ihp130.mk", ("SIMU=VERILATOR", "sim")),
("configs/ci/hazard3-rv32im-ihp130.mk", ("SIMU=VERILATOR", "HAVE_SVA=YES", "sim")),
(
"configs/ci/hazard3-rv32im-ihp130.mk",
("SIMU=IVERILOG", "RTL_SIM_TIMEOUT=5200000", "sim-asm"),
@@ -46,7 +46,10 @@ PR_COMMANDS = (
("configs/ci/hazard3-rv32im-ihp130.mk", ("STA=OPENSTA", "sta")),
)
NIGHTLY_COMMANDS = PR_COMMANDS + (
("configs/nightly/picorv32-rv32im-ihp130.mk", ("SIMU=VERILATOR", "firmware", "sim")),
(
"configs/nightly/picorv32-rv32im-ihp130.mk",
("SIMU=VERILATOR", "HAVE_SVA=YES", "firmware", "sim"),
),
(
"configs/nightly/picorv32-rv32im-ihp130.mk",
("SIMU=IVERILOG", "RTL_SIM_TIMEOUT=5200000", "sim-asm"),
+46
View File
@@ -0,0 +1,46 @@
"""Tests for the root clock/reset and CDC inventory checker."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOMAIN_MAP = ROOT / "rtl/mini/integration/clock_reset_domains.json"
CHECKER = ROOT / "scripts/check_clock_reset_domains.py"
def check(domain_map: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(CHECKER), "--map", str(domain_map), "--root", str(ROOT)],
text=True,
capture_output=True,
)
def write_invalid_map(tmp_path: Path, document: dict[str, object]) -> Path:
path = tmp_path / "clock_reset_domains.json"
path.write_text(json.dumps(document), encoding="utf-8")
return path
def test_clock_reset_domain_inventory_matches_the_rcu() -> None:
result = check(DOMAIN_MAP)
assert result.returncode == 0, result.stderr
def test_clock_reset_domain_inventory_rejects_unknown_domain_and_instance(tmp_path: Path) -> None:
document = json.loads(DOMAIN_MAP.read_text(encoding="utf-8"))
document["crossings"][0]["destination"] = "unknown"
result = check(write_invalid_map(tmp_path, document))
assert result.returncode != 0
assert "references an unknown domain" in result.stderr
document = json.loads(DOMAIN_MAP.read_text(encoding="utf-8"))
document["domains"][0]["instance"] = "u_missing_rst_sync"
result = check(write_invalid_map(tmp_path, document))
assert result.returncode != 0
assert "does not declare rst_sync instance" in result.stderr
+1
View File
@@ -306,6 +306,7 @@ def test_verilator_simulations_use_uniform_timeout() -> None:
for _, values in regression_commands:
if "SIMU=VERILATOR" in values:
assert not any(value.startswith("SOC_SIM_TIME=") for value in values)
assert "HAVE_SVA=YES" in values
def test_run_flow_writes_structured_result(tmp_path: Path) -> None:
+53 -3
View File
@@ -16,7 +16,7 @@ MEMORY_MAP = ROOT / "rtl/mini/address_map/memory_map.json"
MEMORY_MAP_GENERATOR = ROOT / "rtl/mini/address_map/generate_memory_map.py"
def generate(output_dir: Path) -> None:
def generate(output_dir: Path, ip: str = "NONE") -> None:
subprocess.run(
[
sys.executable,
@@ -27,12 +27,14 @@ def generate(output_dir: Path) -> None:
str(MEMORY_MAP),
"--output-dir",
str(output_dir),
"--ip",
ip,
],
check=True,
)
def validate(topology: Path) -> subprocess.CompletedProcess[str]:
def validate(topology: Path, ip: str = "NONE") -> subprocess.CompletedProcess[str]:
return subprocess.run(
[
sys.executable,
@@ -41,6 +43,8 @@ def validate(topology: Path) -> subprocess.CompletedProcess[str]:
str(topology),
"--memory-map",
str(MEMORY_MAP),
"--ip",
ip,
"--check",
],
text=True,
@@ -54,12 +58,16 @@ def write_invalid_topology(tmp_path: Path, document: dict[str, object]) -> Path:
return path
def test_topology_generates_complete_native_and_gpio_bindings(tmp_path: Path) -> None:
def test_topology_generates_complete_native_apb_and_gpio_bindings(tmp_path: Path) -> None:
generate(tmp_path)
interfaces = (tmp_path / "rtl/soc_nmi_interfaces.svh").read_text(encoding="utf-8")
routes = (tmp_path / "rtl/soc_nmi_routes.svh").read_text(encoding="utf-8")
gpio = (tmp_path / "rtl/soc_gpio_alt_bindings.svh").read_text(encoding="utf-8")
apb_interfaces = (tmp_path / "rtl/soc_apb_interfaces.svh").read_text(encoding="utf-8")
apb_declarations = (tmp_path / "rtl/soc_apb_declarations.svh").read_text(encoding="utf-8")
apb_routes = (tmp_path / "rtl/soc_apb_request_routes.svh").read_text(encoding="utf-8")
apb_response = (tmp_path / "rtl/soc_apb_response_mux.svh").read_text(encoding="utf-8")
filelist = (tmp_path / "soc_topology.fl").read_text(encoding="utf-8")
assert interfaces.count("nmi_if u_") == 18
@@ -69,9 +77,31 @@ def test_topology_generates_complete_native_and_gpio_bindings(tmp_path: Path) ->
assert gpio.count("// GPIO") == 64
assert "assign u_uart1_if.rx_i = u_gpio_if.di_i[0];" in gpio
assert "assign u_gpio_if.alt1_do_i[22] = u_psram_if.nss_o[0];" in gpio
assert apb_interfaces.count("apb4_if u_") == 9
assert "apb4_pure_if u_archinfo_apb_pure_if ();" in apb_interfaces
assert "assign tmr.paddr = nmi.addr;" in apb_routes
assert "({32{s_psel_q[8]}} & tmr.prdata)" in apb_response
assert "localparam int NSLV = 9;" in apb_declarations
assert filelist.startswith("+incdir+")
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"
generate(none_output)
generate(mdd_output, ip="MDD")
none_interfaces = (none_output / "rtl/soc_apb_interfaces.svh").read_text(encoding="utf-8")
mdd_interfaces = (mdd_output / "rtl/soc_apb_interfaces.svh").read_text(encoding="utf-8")
mdd_declarations = (mdd_output / "rtl/soc_apb_declarations.svh").read_text(encoding="utf-8")
mdd_response = (mdd_output / "rtl/soc_apb_response_mux.svh").read_text(encoding="utf-8")
assert "u_user_ip_apb_if" not in none_interfaces
assert "apb4_if u_user_ip_apb_if (clk_i, rst_n_i);" in mdd_interfaces
assert "localparam int NSLV = 10;" in mdd_declarations
assert "s_psel_q[9]" in mdd_response
def test_topology_rejects_unknown_or_non_native_regions(tmp_path: Path) -> None:
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["native_targets"][0]["regions"] = ["UNKNOWN"]
@@ -100,6 +130,26 @@ def test_topology_rejects_duplicate_region_and_disabled_owner(tmp_path: Path) ->
assert "disabled but declares regions" in result.stderr
def test_topology_rejects_invalid_apb_target_ownership(tmp_path: Path) -> None:
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["apb_targets"][0]["region"] = "SRAM"
result = validate(write_invalid_topology(tmp_path, document))
assert result.returncode != 0
assert "not an active APB region" in result.stderr
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["apb_targets"][8]["slot"] = 0
result = validate(write_invalid_topology(tmp_path, document))
assert result.returncode != 0
assert "APB target slot 0 is duplicated" in result.stderr
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["apb_targets"][9]["requires_ip"] = None
result = validate(write_invalid_topology(tmp_path, document), ip="MDD")
assert result.returncode != 0
assert "requires_ip does not match" in result.stderr
def test_topology_rejects_invalid_gpio_coverage_and_expression(tmp_path: Path) -> None:
document = json.loads(TOPOLOGY.read_text(encoding="utf-8"))
document["gpio_alt_functions"][1]["pin"] = 0
+73
View File
@@ -0,0 +1,73 @@
"""Tests for the generated scalar user-extension bindings."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
EXTENSIONS = ROOT / "rtl/mini/integration/user_extensions.json"
GENERATOR = ROOT / "rtl/mini/integration/generate_user_extensions.py"
def generate(output_dir: Path) -> None:
subprocess.run(
[
sys.executable,
str(GENERATOR),
"--map",
str(EXTENSIONS),
"--output-dir",
str(output_dir),
],
check=True,
)
def validate(extensions: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(GENERATOR), "--map", str(extensions), "--check"],
text=True,
capture_output=True,
)
def write_invalid_extensions(tmp_path: Path, document: dict[str, object]) -> Path:
path = tmp_path / "invalid_extensions.json"
path.write_text(json.dumps(document), encoding="utf-8")
return path
def test_extensions_generate_isolated_scalar_bindings(tmp_path: Path) -> None:
generate(tmp_path)
core = (tmp_path / "rtl/user_core_bindings.svh").read_text(encoding="utf-8")
ip = (tmp_path / "rtl/user_ip_bindings.svh").read_text(encoding="utf-8")
filelist = (tmp_path / "user_extensions.fl").read_text(encoding="utf-8")
assert core.count("nmi_if u_user_") == 6
assert "nmi.valid = '0;" in core
assert "5'd6: begin" in core
assert "user_core_design_username1 #(" in core
assert ip.count("user_gpio_if #(`USER_GPIO_NUM)") == 2
assert "gpio.do_o = '0;" in ip
assert "8'd2: begin" in ip
assert "u_user_2_apb_if.psel = apb.psel;" in ip
assert filelist.startswith("+incdir+")
def test_extensions_reject_noncontiguous_slots_and_invalid_modules(tmp_path: Path) -> None:
document = json.loads(EXTENSIONS.read_text(encoding="utf-8"))
document["core_targets"][2]["slot"] = 7
result = validate(write_invalid_extensions(tmp_path, document))
assert result.returncode != 0
assert "slots must be contiguous from one" in result.stderr
document = json.loads(EXTENSIONS.read_text(encoding="utf-8"))
document["ip_targets"][0]["module"] = "user-ip"
result = validate(write_invalid_extensions(tmp_path, document))
assert result.returncode != 0
assert "must be a SystemVerilog identifier" in result.stderr