diff --git a/Makefile b/Makefile index 6ba7ce3a..b2754dc8 100644 --- a/Makefile +++ b/Makefile @@ -534,6 +534,13 @@ include physical/librelane/mini/Makefile endif include physical/ecc/Makefile +.PHONY: spi-p1-lint spi-p1-rtl spi-p1-formal spi-p1-synth spi-p1-netsim spi-p1-sta spi-p1-report +spi-p1-lint spi-p1-rtl spi-p1-formal spi-p1-synth spi-p1-netsim spi-p1-sta spi-p1-report: manifest + $(PYTHON) $(ROOT_PATH)/scripts/spi_p1.py $(patsubst spi-p1-%,%,$@) \ + --variant-root $(VARIANT_ROOT) --pdk $(PDK) --simulator $(SIMU) --jobs $(JOBS) \ + $(if $(SPI_P1_COMMON_ROOT),--common-variant-root $(SPI_P1_COMMON_ROOT),) \ + $(if $(filter spi-p1-formal,$@),$(if $(SPI_P1_FORMAL_REUSE_ROOT),--formal-reuse-root $(SPI_P1_FORMAL_REUSE_ROOT),),) + .PHONY: crypto-p0-constants crypto-p0-rtl crypto-p0-baseline crypto-p0-report crypto-p0-constants: manifest $(PYTHON) $(ROOT_PATH)/scripts/crypto_p0.py constants --variant-root $(VARIANT_ROOT) diff --git a/crt/include/retrosoc/hal/spi_regs.h b/crt/include/retrosoc/hal/spi_regs.h new file mode 100644 index 00000000..8f02fe58 --- /dev/null +++ b/crt/include/retrosoc/hal/spi_regs.h @@ -0,0 +1,110 @@ +#ifndef RETROSOC_HAL_SPI_REGS_H +#define RETROSOC_HAL_SPI_REGS_H + +#include + +/* SPI APB ABI 1.0. Constants do not imply a mapped product instance. */ +#define RS_SPI_ABI_IP_ID UINT32_C(0x000) +#define RS_SPI_ABI_IP_VERSION UINT32_C(0x004) +#define RS_SPI_ABI_CAPABILITY UINT32_C(0x008) +#define RS_SPI_ABI_FIFO_CAPABILITY UINT32_C(0x00c) +#define RS_SPI_ABI_CLOCK_HZ UINT32_C(0x010) +#define RS_SPI_ABI_ROUTABLE_MASK UINT32_C(0x014) +#define RS_SPI_ABI_COMMAND UINT32_C(0x018) +#define RS_SPI_ABI_STATUS UINT32_C(0x01c) +#define RS_SPI_ABI_SESSION_CFG UINT32_C(0x020) +#define RS_SPI_ABI_DIV UINT32_C(0x024) +#define RS_SPI_ABI_CS_SETUP UINT32_C(0x028) +#define RS_SPI_ABI_CS_HOLD UINT32_C(0x02c) +#define RS_SPI_ABI_CS_INACTIVE UINT32_C(0x030) +#define RS_SPI_ABI_SEG_CONFIG UINT32_C(0x034) +#define RS_SPI_ABI_FRAME_COUNT UINT32_C(0x038) +#define RS_SPI_ABI_DUMMY UINT32_C(0x03c) +#define RS_SPI_ABI_TX_BIND UINT32_C(0x040) +#define RS_SPI_ABI_RX_BIND UINT32_C(0x044) +#define RS_SPI_ABI_WATERMARKS UINT32_C(0x048) +#define RS_SPI_ABI_CPU_TXDATA UINT32_C(0x04c) +#define RS_SPI_ABI_CPU_RXDATA UINT32_C(0x050) +#define RS_SPI_ABI_DMA_TXDATA UINT32_C(0x054) +#define RS_SPI_ABI_DMA_RXDATA UINT32_C(0x058) +#define RS_SPI_ABI_FIFO_LEVELS UINT32_C(0x05c) +#define RS_SPI_ABI_IRQ_STATE UINT32_C(0x060) +#define RS_SPI_ABI_IRQ_ENABLE UINT32_C(0x064) +#define RS_SPI_ABI_IRQ_STATUS UINT32_C(0x068) +#define RS_SPI_ABI_FAULT_CODE UINT32_C(0x06c) +#define RS_SPI_ABI_FAULT_FRAME UINT32_C(0x070) +#define RS_SPI_ABI_FRAMES_DONE UINT32_C(0x074) +#define RS_SPI_ABI_TRANSPORT_WORDS UINT32_C(0x078) +#define RS_SPI_ABI_TX_WORDS_ACCEPTED UINT32_C(0x07c) +#define RS_SPI_ABI_RX_WORDS_PRODUCED UINT32_C(0x080) +#define RS_SPI_ABI_RX_WORDS_REMOVED UINT32_C(0x084) +#define RS_SPI_ABI_TX_STALL_CYCLES UINT32_C(0x088) +#define RS_SPI_ABI_RX_STALL_CYCLES UINT32_C(0x08c) +#define RS_SPI_ABI_SESSION_PINS UINT32_C(0x090) +#define RS_SPI_ABI_DMA_STATUS UINT32_C(0x094) +#define RS_SPI_ABI_PINS_READY UINT32_C(0x098) +#define RS_SPI_ABI_PAYLOAD_BYTES UINT32_C(0x09c) +#define RS_SPI_ABI_ID_VALUE UINT32_C(0x53504930) +#define RS_SPI_ABI_VERSION_VALUE UINT32_C(0x00010000) +#define RS_SPI_ABI_CAPABILITY_VALUE UINT32_C(0x00003f7f) +#define RS_SPI_ABI_FIFO_VALUE UINT32_C(0x00200808) +#define RS_SPI_ABI_ROUTES_VALUE UINT32_C(0xd8000000) +#define RS_SPI_ABI_TIMING_RESET UINT32_C(0x00000001) +#define RS_SPI_ABI_DUMMY_RESET UINT32_C(0x0000ffff) +#define RS_SPI_ABI_WATERMARK_RESET UINT32_C(0x00000100) +#define RS_SPI_ABI_ENABLE 0U +#define RS_SPI_ABI_DISABLE 1U +#define RS_SPI_ABI_ARM 2U +#define RS_SPI_ABI_START 3U +#define RS_SPI_ABI_END 4U +#define RS_SPI_ABI_ABORT 5U +#define RS_SPI_ABI_RECOVER 6U +#define RS_SPI_ABI_RELEASE 7U +#define RS_SPI_ABI_RESET 8U +#define RS_SPI_ABI_CPOL 0U +#define RS_SPI_ABI_CPHA 1U +#define RS_SPI_ABI_LSB_FIRST 2U +#define RS_SPI_ABI_MISO_ENABLE 3U +#define RS_SPI_ABI_DISPLAY_DC 4U +#define RS_SPI_ABI_FRAME16 0U +#define RS_SPI_ABI_DIRECTION 1U +#define RS_SPI_ABI_HOLD_CS 3U +#define RS_SPI_ABI_BIND_ENABLE 31U + +#define RS_SPI_ABI_STATE_DISABLED 0U +#define RS_SPI_ABI_STATE_IDLE 1U +#define RS_SPI_ABI_STATE_ARMED 2U +#define RS_SPI_ABI_STATE_SETUP 3U +#define RS_SPI_ABI_STATE_SHIFT 4U +#define RS_SPI_ABI_STATE_HOLD 5U +#define RS_SPI_ABI_STATE_HELD 6U +#define RS_SPI_ABI_STATE_GAP 7U +#define RS_SPI_ABI_STATE_CLOSING 8U +#define RS_SPI_ABI_TX_ONLY 0U +#define RS_SPI_ABI_FULL_DUPLEX 1U +#define RS_SPI_ABI_RX_ONLY 2U +#define RS_SPI_ABI_FAULT_NONE 0U +#define RS_SPI_ABI_FAULT_ROUTE 1U +#define RS_SPI_ABI_FAULT_READINESS 2U +#define RS_SPI_ABI_FAULT_DMA 3U +#define RS_SPI_ABI_FAULT_CREDIT 4U +#define RS_SPI_ABI_FAULT_QUOTA 5U +#define RS_SPI_ABI_IRQ_SEGMENT_DONE 0U +#define RS_SPI_ABI_IRQ_DONE 1U +#define RS_SPI_ABI_IRQ_ABORTED 2U +#define RS_SPI_ABI_IRQ_FAULT 3U +#define RS_SPI_ABI_IRQ_TX_STALL 4U +#define RS_SPI_ABI_IRQ_RX_STALL 5U +#define RS_SPI_ABI_IRQ_TX_LEVEL 8U +#define RS_SPI_ABI_IRQ_RX_LEVEL 9U +#define RS_SPI_ABI_STATUS_CS_ACTIVE 4U +#define RS_SPI_ABI_STATUS_SESSION_OWNED 5U +#define RS_SPI_ABI_STATUS_SEGMENT_PENDING 6U +#define RS_SPI_ABI_STATUS_SEGMENT_CLEAN 7U +#define RS_SPI_ABI_STATUS_TX_WAIT 8U +#define RS_SPI_ABI_STATUS_RX_WAIT 9U +#define RS_SPI_ABI_STATUS_BUS_IDLE 10U +#define RS_SPI_ABI_STATUS_TRANSPORT_IDLE 11U +#define RS_SPI_ABI_STATUS_GPIO_READY 12U + +#endif diff --git a/physical/smoke/sta/opensta/spi_block.sdc b/physical/smoke/sta/opensta/spi_block.sdc new file mode 100644 index 00000000..4162f7ab --- /dev/null +++ b/physical/smoke/sta/opensta/spi_block.sdc @@ -0,0 +1,3 @@ +# SPI-P1 internal PCLK observation. External slave/board timing is NOT qualified. +create_clock -name pclk -period 41.666666667 [get_ports clk_i] +# No blanket false path on reset or raw MISO. Report uncharacterized boundaries. diff --git a/physical/smoke/sta/opensta/spi_block.tcl b/physical/smoke/sta/opensta/spi_block.tcl new file mode 100644 index 00000000..f49f2531 --- /dev/null +++ b/physical/smoke/sta/opensta/spi_block.tcl @@ -0,0 +1,13 @@ +foreach name {SPI_NETLIST SPI_LIBERTY SPI_REPORT_DIR} { + if {![info exists ::env($name)]} { error "missing $name" } +} +read_liberty $::env(SPI_LIBERTY) +read_verilog $::env(SPI_NETLIST) +link_design apb4_spi_block_top +read_sdc [file join [file dirname [info script]] spi_block.sdc] +check_setup -verbose > $::env(SPI_REPORT_DIR)/constraints.rpt +report_checks -path_delay min_max -group_path_count 20 > $::env(SPI_REPORT_DIR)/paths.rpt +report_wns -max > $::env(SPI_REPORT_DIR)/setup_wns.rpt +report_tns -max > $::env(SPI_REPORT_DIR)/setup_tns.rpt +report_wns -min > $::env(SPI_REPORT_DIR)/hold_wns.rpt +report_tns -min > $::env(SPI_REPORT_DIR)/hold_tns.rpt diff --git a/physical/smoke/syn/yosys/apb4_spi_block_top.sv b/physical/smoke/syn/yosys/apb4_spi_block_top.sv new file mode 100644 index 00000000..c1ec7b2e --- /dev/null +++ b/physical/smoke/syn/yosys/apb4_spi_block_top.sv @@ -0,0 +1,80 @@ +// Flat-pin standalone verification/synthesis boundary; not a Tiny instance. +module apb4_spi_block_top ( + input logic clk_i, + input logic rst_n_i, + input logic [31:0] clock_hz_i, + input logic [ 1:0] origin_i, + input logic [ 4:0] request_i, + input logic [ 2:0] channel_i, + input logic [ 1:0] credit_valid_i, + input logic [ 1:0] credit_retire_i, + input logic [ 1:0] dma_start_pending_i, + input logic [ 3:0] dma_status_i, + input logic dma_error_i, + input logic [31:0] user_select_i, + input logic [31:0] handoff_i, + input logic [31:0] alt_enable_i, + input logic [31:0] alt_select_i, + input logic [31:0] output_enable_i, + input logic [31:0] open_drain_i, + input logic gpio_ready_i, + input logic miso_i, + output logic sck_o, + output logic mosi_o, + output logic cs_n_o, + output logic output_enable_o, + output logic irq_o, + output logic [ 1:0] credit_commit_o, + input logic [31:0] paddr_i, + pwdata_i, + input logic [ 3:0] pstrb_i, + input logic psel_i, + penable_i, + pwrite_i, + output logic [31:0] prdata_o, + output logic pready_o, + pslverr_o +); + apb4_if s_apb4 ( + .pclk (clk_i), + .presetn(rst_n_i) + ); + assign s_apb4.paddr = paddr_i; + assign s_apb4.pwdata = pwdata_i; + assign s_apb4.pstrb = pstrb_i; + assign s_apb4.psel = psel_i; + assign s_apb4.penable = penable_i; + assign s_apb4.pwrite = pwrite_i; + assign s_apb4.pprot = 3'd0; + assign prdata_o = s_apb4.prdata; + assign pready_o = s_apb4.pready; + assign pslverr_o = s_apb4.pslverr; + apb4_spi u_spi ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .clock_hz_i (clock_hz_i), + .origin_i (origin_i), + .request_i (request_i), + .channel_i (channel_i), + .credit_valid_i (credit_valid_i), + .credit_retire_i (credit_retire_i), + .dma_start_pending_i(dma_start_pending_i), + .dma_status_i (dma_status_i), + .dma_error_i (dma_error_i), + .user_select_i (user_select_i), + .handoff_i (handoff_i), + .alt_enable_i (alt_enable_i), + .alt_select_i (alt_select_i), + .output_enable_i (output_enable_i), + .open_drain_i (open_drain_i), + .gpio_ready_i (gpio_ready_i), + .miso_i (miso_i), + .sck_o (sck_o), + .mosi_o (mosi_o), + .cs_n_o (cs_n_o), + .output_enable_o (output_enable_o), + .irq_o (irq_o), + .credit_commit_o (credit_commit_o), + .apb4 (s_apb4) + ); +endmodule diff --git a/rtl/ip/serial/apb4_spi.sv b/rtl/ip/serial/apb4_spi.sv new file mode 100644 index 00000000..5560fe90 --- /dev/null +++ b/rtl/ip/serial/apb4_spi.sv @@ -0,0 +1,93 @@ +// Standalone SPI-P1 shell. No product route or integrated DMA capability is implied. +module apb4_spi ( + input logic clk_i, + rst_n_i, + input logic [31:0] clock_hz_i, + input logic [ 1:0] origin_i, + input logic [ 4:0] request_i, + input logic [ 2:0] channel_i, + input logic [ 1:0] credit_valid_i, + credit_retire_i, + input logic [ 3:0] dma_status_i, + input logic [ 1:0] dma_start_pending_i, + input logic dma_error_i, + input logic [31:0] user_select_i, + handoff_i, + alt_enable_i, + alt_select_i, + input logic [31:0] output_enable_i, + open_drain_i, + input logic gpio_ready_i, + miso_i, + output logic sck_o, + mosi_o, + cs_n_o, + output_enable_o, + irq_o, + output logic [ 1:0] credit_commit_o, + apb4_if.slave apb4 +); + spi_ctrl_if u_ctrl (); + spi_pkg::configuration_t s_cfg; + logic [31:0] s_claim, s_ready; + logic s_handback, s_credit_fault; + logic s_fifo_pending; + logic [1:0] s_credit_pending; + logic [3:0] s_dma_stat; + logic s_unused; + assign s_cfg = u_ctrl.cfg; + // Only personality bits feed the GPIO guard; other fields feed the core/reg blocks. + assign s_unused = ^s_cfg; + assign s_dma_stat = dma_status_i | {s_credit_pending, 2'd0}; + spi_gpio_guard u_gpio_guard ( + .session_i (s_cfg.session), + .user_select_i (user_select_i), + .handoff_i (handoff_i), + .alt_enable_i (alt_enable_i), + .alt_select_i (alt_select_i), + .output_enable_i(output_enable_i), + .open_drain_i (open_drain_i), + .gpio_ready_i (gpio_ready_i), + .claim_o (s_claim), + .ready_o (s_ready), + .handback_o (s_handback) + ); + spi_reg u_reg ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .clock_hz_i (clock_hz_i), + .origin_i (origin_i), + .request_i (request_i), + .channel_i (channel_i), + .credit_valid_i (credit_valid_i), + .credit_retire_i (credit_retire_i), + .dma_status_i (s_dma_stat), + .dma_start_pending_i(dma_start_pending_i), + .credit_commit_o (credit_commit_o), + .credit_pending_o (s_credit_pending), + .fifo_pending_o (s_fifo_pending), + .credit_fault_o (s_credit_fault), + .irq_o (irq_o), + .apb4 (apb4), + .ctrl (u_ctrl) + ); + spi_master_core u_master_core ( + .apb_pending_i (s_fifo_pending), + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .claim_i (s_claim), + .ready_pins_i (s_ready), + .gpio_ready_i (gpio_ready_i), + .handback_i (s_handback), + .dma_status_i (s_dma_stat), + .dma_start_pending_i(dma_start_pending_i), + .dma_error_i (dma_error_i), + .credit_fault_i (s_credit_fault), + .miso_i (miso_i), + .sck_o (sck_o), + .mosi_o (mosi_o), + .cs_n_o (cs_n_o), + .output_enable_o (output_enable_o), + .ctrl (u_ctrl) + ); +endmodule diff --git a/rtl/ip/serial/spi_ctrl_if.sv b/rtl/ip/serial/spi_ctrl_if.sv new file mode 100644 index 00000000..815a486c --- /dev/null +++ b/rtl/ip/serial/spi_ctrl_if.sv @@ -0,0 +1,20 @@ +interface spi_ctrl_if; + spi_pkg::configuration_t cfg; + spi_pkg::status_t stat; + logic [8:0] cmd; + logic cmd_err, session_writable, segment_writable; + logic tx_push, rx_pop, tx_ready, rx_ready; + logic [31:0] tx_data, rx_data; + logic [5:0] events; + logic clear_events, clear_bindings; + modport registers( + output cfg, cmd, tx_push, rx_pop, tx_data, + input stat, cmd_err, session_writable, segment_writable, + tx_ready, rx_ready, rx_data, events, clear_events, clear_bindings + ); + modport core( + input cfg, cmd, tx_push, rx_pop, tx_data, + output stat, cmd_err, session_writable, segment_writable, + tx_ready, rx_ready, rx_data, events, clear_events, clear_bindings + ); +endinterface diff --git a/rtl/ip/serial/spi_define.svh b/rtl/ip/serial/spi_define.svh new file mode 100644 index 00000000..06d6e747 --- /dev/null +++ b/rtl/ip/serial/spi_define.svh @@ -0,0 +1,104 @@ +`ifndef RETROSOC_SPI_DEFINE_SVH +`define RETROSOC_SPI_DEFINE_SVH +`define APB4_SPI__IP_ID 12'h000 +`define APB4_SPI__IP_VERSION 12'h004 +`define APB4_SPI__CAPABILITY 12'h008 +`define APB4_SPI__FIFO_CAPABILITY 12'h00c +`define APB4_SPI__CLOCK_HZ 12'h010 +`define APB4_SPI__ROUTABLE_MASK 12'h014 +`define APB4_SPI__COMMAND 12'h018 +`define APB4_SPI__STATUS 12'h01c +`define APB4_SPI__SESSION_CFG 12'h020 +`define APB4_SPI__DIV 12'h024 +`define APB4_SPI__CS_SETUP 12'h028 +`define APB4_SPI__CS_HOLD 12'h02c +`define APB4_SPI__CS_INACTIVE 12'h030 +`define APB4_SPI__SEG_CONFIG 12'h034 +`define APB4_SPI__FRAME_COUNT 12'h038 +`define APB4_SPI__DUMMY 12'h03c +`define APB4_SPI__TX_BIND 12'h040 +`define APB4_SPI__RX_BIND 12'h044 +`define APB4_SPI__WATERMARKS 12'h048 +`define APB4_SPI__CPU_TXDATA 12'h04c +`define APB4_SPI__CPU_RXDATA 12'h050 +`define APB4_SPI__DMA_TXDATA 12'h054 +`define APB4_SPI__DMA_RXDATA 12'h058 +`define APB4_SPI__FIFO_LEVELS 12'h05c +`define APB4_SPI__IRQ_STATE 12'h060 +`define APB4_SPI__IRQ_ENABLE 12'h064 +`define APB4_SPI__IRQ_STATUS 12'h068 +`define APB4_SPI__FAULT_CODE 12'h06c +`define APB4_SPI__FAULT_FRAME 12'h070 +`define APB4_SPI__FRAMES_DONE 12'h074 +`define APB4_SPI__TRANSPORT_WORDS 12'h078 +`define APB4_SPI__TX_WORDS_ACCEPTED 12'h07c +`define APB4_SPI__RX_WORDS_PRODUCED 12'h080 +`define APB4_SPI__RX_WORDS_REMOVED 12'h084 +`define APB4_SPI__TX_STALL_CYCLES 12'h088 +`define APB4_SPI__RX_STALL_CYCLES 12'h08c +`define APB4_SPI__SESSION_PINS 12'h090 +`define APB4_SPI__DMA_STATUS 12'h094 +`define APB4_SPI__PINS_READY 12'h098 +`define APB4_SPI__PAYLOAD_BYTES 12'h09c +`define APB4_SPI__ID_VALUE 32'h53504930 +`define APB4_SPI__VERSION_VALUE 32'h00010000 +`define APB4_SPI__CAPABILITY_VALUE 32'h00003f7f +`define APB4_SPI__FIFO_VALUE 32'h00200808 +`define APB4_SPI__ROUTES_VALUE 32'hd8000000 +`define APB4_SPI__TIMING_RESET 32'h00000001 +`define APB4_SPI__DUMMY_RESET 32'h0000ffff +`define APB4_SPI__WATERMARK_RESET 32'h00000100 +`define APB4_SPI__ENABLE 0 +`define APB4_SPI__DISABLE 1 +`define APB4_SPI__ARM 2 +`define APB4_SPI__START 3 +`define APB4_SPI__END 4 +`define APB4_SPI__ABORT 5 +`define APB4_SPI__RECOVER 6 +`define APB4_SPI__RELEASE 7 +`define APB4_SPI__RESET 8 +`define APB4_SPI__CPOL 0 +`define APB4_SPI__CPHA 1 +`define APB4_SPI__LSB_FIRST 2 +`define APB4_SPI__MISO_ENABLE 3 +`define APB4_SPI__DISPLAY_DC 4 +`define APB4_SPI__FRAME16 0 +`define APB4_SPI__DIRECTION 1 +`define APB4_SPI__HOLD_CS 3 +`define APB4_SPI__BIND_ENABLE 31 +`define APB4_SPI__STATE_DISABLED 0 +`define APB4_SPI__STATE_IDLE 1 +`define APB4_SPI__STATE_ARMED 2 +`define APB4_SPI__STATE_SETUP 3 +`define APB4_SPI__STATE_SHIFT 4 +`define APB4_SPI__STATE_HOLD 5 +`define APB4_SPI__STATE_HELD 6 +`define APB4_SPI__STATE_GAP 7 +`define APB4_SPI__STATE_CLOSING 8 +`define APB4_SPI__TX_ONLY 0 +`define APB4_SPI__FULL_DUPLEX 1 +`define APB4_SPI__RX_ONLY 2 +`define APB4_SPI__FAULT_NONE 0 +`define APB4_SPI__FAULT_ROUTE 1 +`define APB4_SPI__FAULT_READINESS 2 +`define APB4_SPI__FAULT_DMA 3 +`define APB4_SPI__FAULT_CREDIT 4 +`define APB4_SPI__FAULT_QUOTA 5 +`define APB4_SPI__IRQ_SEGMENT_DONE 0 +`define APB4_SPI__IRQ_DONE 1 +`define APB4_SPI__IRQ_ABORTED 2 +`define APB4_SPI__IRQ_FAULT 3 +`define APB4_SPI__IRQ_TX_STALL 4 +`define APB4_SPI__IRQ_RX_STALL 5 +`define APB4_SPI__IRQ_TX_LEVEL 8 +`define APB4_SPI__IRQ_RX_LEVEL 9 +`define APB4_SPI__STATUS_CS_ACTIVE 4 +`define APB4_SPI__STATUS_SESSION_OWNED 5 +`define APB4_SPI__STATUS_SEGMENT_PENDING 6 +`define APB4_SPI__STATUS_SEGMENT_CLEAN 7 +`define APB4_SPI__STATUS_TX_WAIT 8 +`define APB4_SPI__STATUS_RX_WAIT 9 +`define APB4_SPI__STATUS_BUS_IDLE 10 +`define APB4_SPI__STATUS_TRANSPORT_IDLE 11 +`define APB4_SPI__STATUS_GPIO_READY 12 +`endif diff --git a/rtl/ip/serial/spi_gpio_guard.sv b/rtl/ip/serial/spi_gpio_guard.sv new file mode 100644 index 00000000..510b9cde --- /dev/null +++ b/rtl/ip/serial/spi_gpio_guard.sv @@ -0,0 +1,32 @@ +// Private read-only GPIO qualification; never drives or changes another owner. +module spi_gpio_guard ( + input logic [ 4:0] session_i, + input logic [31:0] user_select_i, + handoff_i, + alt_enable_i, + alt_select_i, + input logic [31:0] output_enable_i, + open_drain_i, + input logic gpio_ready_i, + output logic [31:0] claim_o, + ready_o, + output logic handback_o +); + logic [31:0] s_native, s_route; + // Session timing and unrelated ALT selectors are outside this guard's scope. + logic s_unused; + assign s_unused = ^{session_i[2:0], alt_select_i[29], alt_select_i[26:0]}; + assign claim_o = (session_i[3] || session_i[4]) ? 32'hd800_0000 : 32'h9800_0000; + assign s_native = ~(user_select_i | handoff_i | open_drain_i); + always_comb begin + s_route = '0; + s_route[27] = alt_enable_i[27] && !alt_select_i[27]; + s_route[28] = alt_enable_i[28] && alt_select_i[28]; + s_route[31] = alt_enable_i[31] && alt_select_i[31]; + s_route[30] = session_i[4] ? (!alt_enable_i[30] && output_enable_i[30]) : + (alt_enable_i[30] && alt_select_i[30]); + end + assign ready_o = gpio_ready_i ? (claim_o & s_native & s_route) : 32'd0; + assign handback_o = gpio_ready_i && + ((claim_o & (user_select_i | handoff_i | alt_enable_i | output_enable_i)) == 32'd0); +endmodule diff --git a/rtl/ip/serial/spi_master_core.sv b/rtl/ip/serial/spi_master_core.sv new file mode 100644 index 00000000..48810508 --- /dev/null +++ b/rtl/ip/serial/spi_master_core.sv @@ -0,0 +1,363 @@ +`include "spi_define.svh" + +module spi_master_core ( + input logic apb_pending_i, + input logic clk_i, + rst_n_i, + input logic [31:0] claim_i, + ready_pins_i, + input logic gpio_ready_i, + handback_i, + input logic [ 3:0] dma_status_i, + input logic [ 1:0] dma_start_pending_i, + input logic dma_error_i, + credit_fault_i, + miso_i, + output logic sck_o, + mosi_o, + cs_n_o, + output_enable_o, + spi_ctrl_if.core ctrl +); + typedef struct packed { + spi_pkg::wire_state_e state; + logic owned, pending, complete, cs_active, gap_event, close_done; + logic guard_faulted; + logic enabled; + logic [1:0] close_phase; + logic [15:0] timer; + logic [31:0] pins, frames, words, payload, frame_count, fault_frame; + logic [31:0] tx_stalls, rx_stalls; + logic [3:0] segment; + logic [7:0] fault; + } core_t; + core_t s_core_d, s_core_q; + spi_pkg::configuration_t s_cfg; + spi_pkg::status_t s_stat; + logic s_transport_idle, s_clean, s_guard_fault, s_normal_fault, s_close; + logic s_cmd_accept, s_clear, s_phase_active, s_phase_done, s_start; + logic s_tx_ready, s_rx_ready, s_tx_frame_ready, s_rx_frame_ready, s_empty; + logic s_tx_wait, s_rx_wait, s_last, s_data_live; + logic [3:0] s_tx_level, s_rx_level; + logic [15:0] s_tx_frame, s_rx_frame; + logic [31:0] s_tx_words, s_rx_words, s_rx_removed; + logic [63:0] s_payload, s_transport; + logic [7:0] s_fault_code; + // Watermarks belong exclusively to the register block's IRQ-level logic. + logic s_unused; + assign s_cfg = ctrl.cfg; + assign ctrl.stat = s_stat; + assign s_unused = ^s_cfg.watermarks; + + assign s_transport_idle = (dma_status_i == 4'd0) && + (dma_start_pending_i == 2'd0) && !apb_pending_i; + assign s_clean = !s_core_q.pending && s_empty && s_transport_idle; + assign s_guard_fault = s_core_q.owned && s_core_q.enabled && + !s_core_q.guard_faulted && + (!gpio_ready_i || ((ready_pins_i & s_core_q.pins) != s_core_q.pins)); + assign s_normal_fault = s_core_q.owned && (s_core_q.state != spi_pkg::Disabled) && + (s_core_q.state != spi_pkg::Closing) && + (dma_error_i || credit_fault_i); + assign s_fault_code = !gpio_ready_i ? 8'd2 : s_guard_fault ? 8'd1 : dma_error_i ? 8'd3 : 8'd4; + assign s_close = s_guard_fault || s_normal_fault || (s_cmd_accept && ctrl.cmd[`APB4_SPI__ABORT]); + assign s_payload = {32'd0, s_cfg.frames} << (s_cfg.segment[0] ? 1 : 0); + assign s_transport = (s_payload + 64'd3) & 64'hffff_ffff_ffff_fffc; + assign s_data_live = s_core_q.pending && (s_core_q.state != spi_pkg::Closing) && !s_close; + assign ctrl.session_writable = (s_core_q.state == spi_pkg::Disabled) && + !s_core_q.owned && s_transport_idle; + assign ctrl.segment_writable = s_transport_idle && !s_guard_fault && !s_normal_fault && + ((!s_core_q.owned && (s_core_q.state == spi_pkg::Disabled)) || + (s_clean && ((s_core_q.state == spi_pkg::Idle) || (s_core_q.state == spi_pkg::Held)))); + always_comb begin + ctrl.cmd_err = 1'b0; + unique case (ctrl.cmd) + 9'd0: ctrl.cmd_err = 1'b0; + 9'b000000001: + ctrl.cmd_err = (s_core_q.state != spi_pkg::Disabled) || + s_core_q.owned || !s_clean || !gpio_ready_i || (ready_pins_i != claim_i); + 9'b000000010: ctrl.cmd_err = (s_core_q.state != spi_pkg::Idle) || !s_clean; + 9'b000000100: + ctrl.cmd_err = !ctrl.segment_writable || !s_core_q.owned || + (s_cfg.frames == 32'd0) || (s_transport > 64'hffff_fffc) || + (s_cfg.segment[2:1] == 2'd3) || + ((s_cfg.segment[2:1] != 2'd0) && !s_cfg.session[3]) || + ((s_cfg.segment[2:1] == 2'd0) && s_cfg.rx_binding[31]) || + ((s_cfg.segment[2:1] == 2'd2) && s_cfg.tx_binding[31]); + 9'b000001000: ctrl.cmd_err = (s_core_q.state != spi_pkg::Armed); + 9'b000010000: ctrl.cmd_err = (s_core_q.state != spi_pkg::Held) || !s_clean; + 9'b000100000: ctrl.cmd_err = !s_core_q.owned; + 9'b001000000: + ctrl.cmd_err = (s_core_q.state != spi_pkg::Closing) || + !s_core_q.close_done || !s_transport_idle; + 9'b010000000: + ctrl.cmd_err = (s_core_q.state != spi_pkg::Disabled) || + !s_core_q.owned || !s_clean || !handback_i || + s_cfg.tx_binding[31] || s_cfg.rx_binding[31]; + 9'b100000000: ctrl.cmd_err = !ctrl.session_writable; + default: ctrl.cmd_err = 1'b1; + endcase + if ((ctrl.cmd != 9'd0) && (s_guard_fault || s_normal_fault)) ctrl.cmd_err = 1'b1; + end + assign s_cmd_accept = (ctrl.cmd != 9'd0) && !ctrl.cmd_err; + assign s_clear = s_cmd_accept && (ctrl.cmd[`APB4_SPI__ARM] || + ctrl.cmd[`APB4_SPI__RECOVER] || ctrl.cmd[`APB4_SPI__RESET]); + assign ctrl.clear_events = s_cmd_accept && + (ctrl.cmd[`APB4_SPI__RECOVER] || ctrl.cmd[`APB4_SPI__RESET]); + assign ctrl.clear_bindings = s_cmd_accept && (ctrl.cmd[`APB4_SPI__DISABLE] || + ctrl.cmd[`APB4_SPI__RECOVER] || ctrl.cmd[`APB4_SPI__RESET]); + assign ctrl.tx_ready = s_data_live && s_tx_ready && (s_core_q.segment[2:1] != 2'd2); + assign ctrl.rx_ready = s_data_live && s_rx_ready && (s_core_q.segment[2:1] != 2'd0); + assign s_start = (s_core_q.state == spi_pkg::Shift) && !s_phase_active && + (s_core_q.frames < s_core_q.frame_count) && s_tx_frame_ready && s_rx_frame_ready && !s_close; + assign s_last = s_core_q.frames == (s_core_q.frame_count - 32'd1); + assign s_tx_wait = (s_core_q.state == spi_pkg::Shift) && !s_phase_active && !s_tx_frame_ready; + assign s_rx_wait = (s_core_q.state == spi_pkg::Shift) && !s_phase_active && !s_rx_frame_ready; + assign cs_n_o = !(s_core_q.cs_active && !s_guard_fault); + assign output_enable_o = s_core_q.owned && s_core_q.enabled && + gpio_ready_i && ((ready_pins_i & s_core_q.pins) == s_core_q.pins); + + always_comb begin + s_core_d = s_core_q; + ctrl.events = '0; + if (s_core_q.pending && s_core_q.complete && s_empty && s_transport_idle && + ((s_core_q.segment[2:1] == 2'd2) || (s_tx_words == s_core_q.words)) && + ((s_core_q.segment[2:1] == 2'd0) || + ((s_rx_words == s_core_q.words) && (s_rx_removed == s_core_q.words)))) + s_core_d.pending = 1'b0; + if (s_tx_wait && !s_close) begin + ctrl.events[4] = 1'b1; + if (s_core_q.tx_stalls != 32'hffff_ffff) s_core_d.tx_stalls = s_core_q.tx_stalls + 32'd1; + end + if (s_rx_wait && !s_close) begin + ctrl.events[5] = 1'b1; + if (s_core_q.rx_stalls != 32'hffff_ffff) s_core_d.rx_stalls = s_core_q.rx_stalls + 32'd1; + end + unique case (s_core_q.state) + spi_pkg::Disabled, spi_pkg::Idle, spi_pkg::Armed, spi_pkg::Held: ; + spi_pkg::Setup: begin + if (s_core_q.timer == 16'd0) s_core_d.state = spi_pkg::Shift; + else s_core_d.timer = s_core_q.timer - 16'd1; + end + spi_pkg::Shift: begin + if (s_phase_done) begin + s_core_d.frames = s_core_q.frames + 32'd1; + if (s_last) begin + s_core_d.state = spi_pkg::Hold; + s_core_d.timer = s_cfg.hold_cycles - 16'd1; + end + end + end + spi_pkg::Hold: begin + if (s_core_q.timer != 16'd0) s_core_d.timer = s_core_q.timer - 16'd1; + else begin + ctrl.events[0] = 1'b1; + s_core_d.complete = 1'b1; + if (s_core_q.segment[3]) s_core_d.state = spi_pkg::Held; + else begin + s_core_d.state = spi_pkg::Gap; + s_core_d.cs_active = 1'b0; + s_core_d.gap_event = 1'b1; + s_core_d.timer = s_cfg.inactive_cycles - 16'd1; + end + end + end + spi_pkg::Gap: begin + if (s_core_q.timer != 16'd0) s_core_d.timer = s_core_q.timer - 16'd1; + else begin + s_core_d.state = spi_pkg::Idle; + ctrl.events[1] = s_core_q.gap_event; + s_core_d.gap_event = 1'b0; + end + end + spi_pkg::Closing: begin + if (!s_core_q.close_done) begin + if (s_core_q.close_phase == 2'd0) begin + if (!s_phase_active) begin + s_core_d.close_phase = 2'd1; + s_core_d.timer = s_cfg.hold_cycles - 16'd1; + end + end else if (s_core_q.timer != 16'd0) s_core_d.timer = s_core_q.timer - 16'd1; + else if (s_core_q.close_phase == 2'd1) begin + s_core_d.cs_active = 1'b0; + s_core_d.close_phase = 2'd2; + s_core_d.timer = s_cfg.inactive_cycles - 16'd1; + end else begin + s_core_d.close_done = 1'b1; + ctrl.events[2] = (s_core_q.fault == 8'd0); + end + end + end + default: begin + s_core_d.state = spi_pkg::Closing; + s_core_d.close_done = 1'b1; + s_core_d.cs_active = 1'b0; + s_core_d.fault = 8'd5; + ctrl.events[3] = 1'b1; + end + endcase + if (s_cmd_accept) begin + unique case (ctrl.cmd) + 9'b000000001: begin + s_core_d.owned = 1'b1; + s_core_d.enabled = 1'b1; + s_core_d.pins = claim_i; + s_core_d.state = spi_pkg::Gap; + s_core_d.gap_event = 1'b0; + s_core_d.timer = s_cfg.inactive_cycles - 16'd1; + end + 9'b000000010: begin + s_core_d.state = spi_pkg::Disabled; + s_core_d.enabled = 1'b0; + end + 9'b000000100: begin + s_core_d.state = spi_pkg::Armed; + s_core_d.segment = s_cfg.segment; + s_core_d.frame_count = s_cfg.frames; + s_core_d.words = s_transport[33:2]; + s_core_d.payload = s_payload[31:0]; + s_core_d.frames = '0; + s_core_d.tx_stalls = '0; + s_core_d.rx_stalls = '0; + s_core_d.complete = 1'b0; + s_core_d.pending = 1'b1; + end + 9'b000001000: begin + s_core_d.state = spi_pkg::Setup; + s_core_d.cs_active = 1'b1; + s_core_d.timer = s_cfg.setup_cycles - 16'd1; + end + 9'b000010000: begin + s_core_d.state = spi_pkg::Gap; + s_core_d.cs_active = 1'b0; + s_core_d.gap_event = 1'b1; + s_core_d.timer = s_cfg.inactive_cycles - 16'd1; + end + 9'b001000000: begin + s_core_d = '0; + s_core_d.owned = s_core_q.owned; + s_core_d.pins = s_core_q.pins; + s_core_d.state = spi_pkg::Disabled; + end + 9'b010000000: begin + s_core_d.owned = 1'b0; + s_core_d.pins = '0; + end + 9'b100000000: s_core_d = '0; + default: ; + endcase + end + if (s_close && ((s_core_q.state != spi_pkg::Closing) || s_guard_fault)) begin + s_core_d.state = spi_pkg::Closing; + s_core_d.complete = 1'b0; + s_core_d.close_phase = '0; + s_core_d.close_done = s_guard_fault; + ctrl.events[1:0] = '0; + if (s_guard_fault) begin + s_core_d.cs_active = 1'b0; + s_core_d.guard_faulted = 1'b1; + s_core_d.enabled = 1'b0; + end + if (s_guard_fault || s_normal_fault) begin + ctrl.events[3] = 1'b1; + if (s_core_q.fault == 8'd0) begin + s_core_d.fault = s_fault_code; + s_core_d.fault_frame = s_core_q.frames; + end + end + end + end + always_comb begin + s_stat = '0; + s_stat.status[3:0] = s_core_q.state; + s_stat.status[4] = s_core_q.cs_active; + s_stat.status[5] = s_core_q.owned; + s_stat.status[6] = s_core_q.pending; + s_stat.status[7] = s_clean; + s_stat.status[8] = s_tx_wait; + s_stat.status[9] = s_rx_wait; + s_stat.status[10] = !s_core_q.cs_active && + ((s_core_q.state == spi_pkg::Idle) || (s_core_q.state == spi_pkg::Disabled)); + s_stat.status[11] = s_transport_idle; + s_stat.status[12] = gpio_ready_i && (ready_pins_i == claim_i); + s_stat.levels = {20'd0, s_rx_level, 4'd0, s_tx_level}; + s_stat.fault = {24'd0, s_core_q.fault}; + s_stat.fault_frame = s_core_q.fault_frame; + s_stat.frames = s_core_q.frames; + s_stat.words = s_core_q.words; + s_stat.tx_words = s_tx_words; + s_stat.rx_words = s_rx_words; + s_stat.rx_removed = s_rx_removed; + s_stat.tx_stalls = s_core_q.tx_stalls; + s_stat.rx_stalls = s_core_q.rx_stalls; + s_stat.pins = s_core_q.pins; + s_stat.ready_pins = ready_pins_i & s_core_q.pins; + s_stat.payload = s_core_q.payload; + end + dffr #( + .DATA_WIDTH($bits(core_t)) + ) u_core ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_core_d), + .dat_o (s_core_q) + ); + spi_phase_ctrl u_phase_ctrl ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .start_i (s_start), + .abort_i (s_close || (s_core_q.state == spi_pkg::Closing)), + .clamp_i (!output_enable_o), + .cpol_i (s_cfg.session[0]), + .cpha_i (s_cfg.session[1]), + .lsb_first_i(s_cfg.session[2]), + .frame16_i (s_core_q.segment[0]), + .divider_i (s_cfg.divider), + .tx_data_i ((s_core_q.segment[2:1] == 2'd2) ? s_cfg.dummy : s_tx_frame), + .miso_i (miso_i && s_cfg.session[3]), + .active_o (s_phase_active), + .done_o (s_phase_done), + .sck_o (sck_o), + .mosi_o (mosi_o), + .rx_data_o (s_rx_frame) + ); + spi_word_packer u_word_packer ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .clear_i (s_clear), + .frame16_i (s_core_q.segment[0]), + .direction_i (s_core_q.segment[2:1]), + .quota_i (s_core_q.words), + .tx_data_i (ctrl.tx_data), + .tx_push_i (ctrl.tx_push), + .rx_pop_i (ctrl.rx_pop), + .frame_start_i (s_start), + .frame_done_i (s_phase_done && !s_close), + .last_frame_i (s_last), + .rx_frame_i (s_rx_frame), + .tx_frame_o (s_tx_frame), + .rx_data_o (ctrl.rx_data), + .tx_words_o (s_tx_words), + .rx_words_o (s_rx_words), + .rx_removed_o (s_rx_removed), + .tx_ready_o (s_tx_ready), + .rx_ready_o (s_rx_ready), + .tx_frame_ready_o(s_tx_frame_ready), + .rx_frame_ready_o(s_rx_frame_ready), + .empty_o (s_empty), + .tx_level_o (s_tx_level), + .rx_level_o (s_rx_level) + ); +`ifdef FORMAL + always_ff @(posedge clk_i) begin + if (rst_n_i) begin + assert (s_core_q.state <= spi_pkg::Closing); + assert (s_tx_level <= 4'd8 && s_rx_level <= 4'd8); + assert (s_tx_words <= s_core_q.words); + assert (s_rx_words <= s_core_q.words); + assert (s_rx_removed <= s_rx_words); + assert (!ctrl.tx_push || (ctrl.tx_ready && !s_close)); + assert (!ctrl.rx_pop || (ctrl.rx_ready && !s_close)); + end + end +`endif +endmodule diff --git a/rtl/ip/serial/spi_phase_ctrl.sv b/rtl/ip/serial/spi_phase_ctrl.sv new file mode 100644 index 00000000..462b4309 --- /dev/null +++ b/rtl/ip/serial/spi_phase_ctrl.sv @@ -0,0 +1,97 @@ +// One finite frame, exclusively clocked by PCLK. Abort never samples more bits. +module spi_phase_ctrl ( + input logic clk_i, + rst_n_i, + start_i, + abort_i, + clamp_i, + input logic cpol_i, + cpha_i, + lsb_first_i, + frame16_i, + input logic [15:0] divider_i, + tx_data_i, + input logic miso_i, + output logic active_o, + done_o, + sck_o, + mosi_o, + output logic [15:0] rx_data_o +); + typedef struct packed { + logic active, sck, mosi; + logic [15:0] timer, tx_data, rx_data; + logic [5:0] edge_count; + } phase_t; + phase_t s_phase_d, s_phase_q; + logic [3:0] s_bit; + logic [3:0] s_next_bit; + logic [4:0] s_last_bit; + logic s_trailing; + assign s_last_bit = frame16_i ? 5'd15 : 5'd7; + assign s_trailing = s_phase_q.edge_count[0]; + assign s_bit = lsb_first_i ? s_phase_q.edge_count[4:1] : + 4'(s_last_bit - s_phase_q.edge_count[5:1]); + assign s_next_bit = lsb_first_i ? s_bit[3:0] + 4'd1 : s_bit[3:0] - 4'd1; + assign active_o = s_phase_q.active; + assign sck_o = clamp_i ? cpol_i : s_phase_q.sck; + assign mosi_o = s_phase_q.mosi; + always_comb begin + s_phase_d = s_phase_q; + done_o = 1'b0; + rx_data_o = s_phase_q.rx_data; + if (clamp_i) begin + s_phase_d.active = 1'b0; + s_phase_d.sck = cpol_i; + end else if (!s_phase_q.active) begin + s_phase_d.sck = cpol_i; + if (start_i && !abort_i) begin + s_phase_d.active = 1'b1; + s_phase_d.edge_count = '0; + s_phase_d.timer = divider_i - 16'd1; + s_phase_d.tx_data = tx_data_i; + s_phase_d.rx_data = '0; + s_phase_d.mosi = cpha_i ? 1'b0 : tx_data_i[lsb_first_i?4'd0 : s_last_bit[3:0]]; + end + end else if (abort_i && (s_phase_q.sck == cpol_i)) begin + s_phase_d.active = 1'b0; + end else if (s_phase_q.timer != 16'd0) begin + s_phase_d.timer = s_phase_q.timer - 16'd1; + end else if (abort_i) begin + s_phase_d.sck = cpol_i; + s_phase_d.active = 1'b0; + end else begin + s_phase_d.timer = divider_i - 16'd1; + s_phase_d.sck = !s_phase_q.sck; + s_phase_d.edge_count = s_phase_q.edge_count + 6'd1; + if (s_trailing == cpha_i) begin + s_phase_d.rx_data[s_bit[3:0]] = miso_i; + rx_data_o[s_bit[3:0]] = miso_i; + end + if (cpha_i && !s_trailing) s_phase_d.mosi = s_phase_q.tx_data[s_bit[3:0]]; + if (!cpha_i && s_trailing && (s_phase_q.edge_count != {s_last_bit, 1'b1})) + s_phase_d.mosi = s_phase_q.tx_data[s_next_bit[3:0]]; + if (s_phase_q.edge_count == {s_last_bit, 1'b1}) begin + done_o = 1'b1; + s_phase_d.active = 1'b0; + end + end + end + dffr #( + .DATA_WIDTH($bits(phase_t)) + ) u_phase ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_phase_d), + .dat_o (s_phase_q) + ); +`ifdef FORMAL + // Reachable timer/edge invariants strengthen induction without constraining the environment. + always_ff @(posedge clk_i) begin + if (rst_n_i && s_phase_q.active) begin + assert (s_phase_q.timer < divider_i); + assert (s_phase_q.edge_count <= {s_last_bit, 1'b1}); + end + end +`endif +endmodule diff --git a/rtl/ip/serial/spi_pkg.sv b/rtl/ip/serial/spi_pkg.sv new file mode 100644 index 00000000..2620105a --- /dev/null +++ b/rtl/ip/serial/spi_pkg.sv @@ -0,0 +1,27 @@ +package spi_pkg; + typedef enum logic [3:0] { + Disabled = 4'd0, + Idle = 4'd1, + Armed = 4'd2, + Setup = 4'd3, + Shift = 4'd4, + Hold = 4'd5, + Held = 4'd6, + Gap = 4'd7, + Closing = 4'd8 + } wire_state_e; + typedef struct packed { + logic [4:0] session; + logic [15:0] divider, setup_cycles, hold_cycles, inactive_cycles; + logic [3:0] segment; + logic [31:0] frames; + logic [15:0] dummy; + logic [31:0] tx_binding, rx_binding; + logic [11:0] watermarks; + } configuration_t; + typedef struct packed { + logic [31:0] status, levels, fault, fault_frame, frames, words; + logic [31:0] tx_words, rx_words, rx_removed, tx_stalls, rx_stalls; + logic [31:0] pins, ready_pins, payload; + } status_t; +endpackage diff --git a/rtl/ip/serial/spi_reg.sv b/rtl/ip/serial/spi_reg.sv new file mode 100644 index 00000000..5660c317 --- /dev/null +++ b/rtl/ip/serial/spi_reg.sv @@ -0,0 +1,299 @@ +`include "spi_define.svh" + +// The private qualifier is captured with this APB access, never taken from PPROT. +module spi_reg ( + input logic clk_i, + rst_n_i, + input logic [31:0] clock_hz_i, + input logic [ 1:0] origin_i, + input logic [ 4:0] request_i, + input logic [ 2:0] channel_i, + input logic [ 1:0] credit_valid_i, + credit_retire_i, + input logic [ 3:0] dma_status_i, + input logic [ 1:0] dma_start_pending_i, + output logic [ 1:0] credit_commit_o, + output logic [ 1:0] credit_pending_o, + output logic fifo_pending_o, + output logic credit_fault_o, + irq_o, + apb4_if.slave apb4, + spi_ctrl_if.registers ctrl +); + localparam spi_pkg::configuration_t ResetCfg = { + 5'd0, 16'd1, 16'd1, 16'd1, 16'd1, 4'd0, 32'd0, 16'hffff, 32'd0, 32'd0, 12'h100 + }; + spi_pkg::configuration_t s_cfg_d, s_cfg_q; + spi_pkg::status_t s_stat; + logic s_req, s_req_accept, s_access_err, s_ready_d, s_ready_q; + logic s_resp_err_d, s_resp_err_q; + logic [31:0] s_rdata_d, s_rdata_q, s_read_data; + logic [5:0] s_irq_d, s_irq_q, s_irq_clear; + logic [9:0] s_irq_en_d, s_irq_en_q, s_irq_stat; + logic [1:0] s_credit_used_d, s_credit_used_q; + logic s_data_access_d, s_data_access_q; + logic s_cpu, s_tx_sel, s_rx_sel, s_dma_tx, s_dma_rx, s_tx_qualified, s_rx_qualified; + logic s_session_write, s_segment_write; + logic [11:0] s_offset; + assign s_stat = ctrl.stat; + assign ctrl.cfg = s_cfg_q; + assign s_req = apb4.psel && apb4.penable; + assign s_req_accept = s_req && !s_ready_q; + assign s_offset = apb4.paddr[11:0]; + assign s_cpu = origin_i == 2'd0; + assign s_dma_tx = s_offset == `APB4_SPI__DMA_TXDATA; + assign s_dma_rx = s_offset == `APB4_SPI__DMA_RXDATA; + assign s_tx_sel = s_dma_tx || (s_offset == `APB4_SPI__CPU_TXDATA); + assign s_rx_sel = s_dma_rx || (s_offset == `APB4_SPI__CPU_RXDATA); + assign s_tx_qualified = (origin_i == 2'd1) && (request_i == 5'd16) && + s_cfg_q.tx_binding[31] && (channel_i == s_cfg_q.tx_binding[2:0]) && + credit_valid_i[0] && !s_credit_used_q[0]; + assign s_rx_qualified = (origin_i == 2'd1) && (request_i == 5'd17) && + s_cfg_q.rx_binding[31] && (channel_i == s_cfg_q.rx_binding[2:0]) && + credit_valid_i[1] && !s_credit_used_q[1]; + assign credit_pending_o = credit_valid_i | s_credit_used_q; + assign fifo_pending_o = s_ready_q && s_data_access_q; + assign s_data_access_d = s_req_accept ? (s_tx_sel || s_rx_sel) : s_data_access_q; + assign credit_fault_o = s_req_accept && (origin_i == 2'd1) && s_stat.status[5] && + ((s_dma_tx && (!s_tx_qualified || !apb4.pwrite || (apb4.pstrb != 4'hf) || + (s_stat.tx_words >= s_stat.words) || (s_stat.levels[3:0] == 4'd8))) || + (s_dma_rx && (!s_rx_qualified || apb4.pwrite || + (s_stat.rx_removed >= s_stat.rx_words) || (s_stat.rx_removed >= s_stat.words)))); + assign s_session_write = ctrl.session_writable; + assign s_segment_write = ctrl.segment_writable && (dma_start_pending_i == 2'd0); + assign ctrl.cmd = (s_req_accept && s_cpu && apb4.pwrite && + (s_offset == `APB4_SPI__COMMAND) && (apb4.paddr[1:0] == 2'd0) && + (apb4.pstrb == 4'hf) && (apb4.pwdata[31:9] == 23'd0)) ? apb4.pwdata[8:0] : 9'd0; + assign s_irq_stat = s_irq_en_q & + { (s_stat.levels[11:8] >= s_cfg_q.watermarks[11:8]), + (s_stat.levels[3:0] <= s_cfg_q.watermarks[3:0]), 2'd0, s_irq_q }; + assign irq_o = (s_irq_stat != 10'd0); + assign apb4.pready = s_ready_q; + assign apb4.prdata = s_rdata_q; + assign apb4.pslverr = s_resp_err_q; + + always_comb begin + s_cfg_d = s_cfg_q; + s_read_data = '0; + s_irq_en_d = s_irq_en_q; + s_irq_clear = '0; + ctrl.tx_data = apb4.pwdata; + ctrl.tx_push = 1'b0; + ctrl.rx_pop = 1'b0; + credit_commit_o = '0; + s_credit_used_d = s_credit_used_q & ~credit_retire_i; + s_access_err = (apb4.paddr[1:0] != 2'd0) || + (apb4.pwrite && (apb4.pstrb != 4'hf)) || + (!s_cpu && !(s_dma_tx || s_dma_rx)); + unique case (s_offset) + `APB4_SPI__IP_ID: s_read_data = `APB4_SPI__ID_VALUE; + `APB4_SPI__IP_VERSION: s_read_data = `APB4_SPI__VERSION_VALUE; + `APB4_SPI__CAPABILITY: s_read_data = `APB4_SPI__CAPABILITY_VALUE; + `APB4_SPI__FIFO_CAPABILITY: s_read_data = `APB4_SPI__FIFO_VALUE; + `APB4_SPI__CLOCK_HZ: s_read_data = clock_hz_i; + `APB4_SPI__ROUTABLE_MASK: s_read_data = `APB4_SPI__ROUTES_VALUE; + `APB4_SPI__STATUS: s_read_data = s_stat.status; + `APB4_SPI__FIFO_LEVELS: s_read_data = s_stat.levels; + `APB4_SPI__FAULT_CODE: s_read_data = s_stat.fault; + `APB4_SPI__FAULT_FRAME: s_read_data = s_stat.fault_frame; + `APB4_SPI__FRAMES_DONE: s_read_data = s_stat.frames; + `APB4_SPI__TRANSPORT_WORDS: s_read_data = s_stat.words; + `APB4_SPI__TX_WORDS_ACCEPTED: s_read_data = s_stat.tx_words; + `APB4_SPI__RX_WORDS_PRODUCED: s_read_data = s_stat.rx_words; + `APB4_SPI__RX_WORDS_REMOVED: s_read_data = s_stat.rx_removed; + `APB4_SPI__TX_STALL_CYCLES: s_read_data = s_stat.tx_stalls; + `APB4_SPI__RX_STALL_CYCLES: s_read_data = s_stat.rx_stalls; + `APB4_SPI__SESSION_PINS: s_read_data = s_stat.pins; + `APB4_SPI__DMA_STATUS: s_read_data = {28'd0, dma_status_i}; + `APB4_SPI__PINS_READY: s_read_data = s_stat.ready_pins; + `APB4_SPI__PAYLOAD_BYTES: s_read_data = s_stat.payload; + `APB4_SPI__IRQ_STATUS: s_read_data = {22'd0, s_irq_stat}; + `APB4_SPI__SESSION_CFG: s_read_data = {27'd0, s_cfg_q.session}; + `APB4_SPI__DIV: s_read_data = {16'd0, s_cfg_q.divider}; + `APB4_SPI__CS_SETUP: s_read_data = {16'd0, s_cfg_q.setup_cycles}; + `APB4_SPI__CS_HOLD: s_read_data = {16'd0, s_cfg_q.hold_cycles}; + `APB4_SPI__CS_INACTIVE: s_read_data = {16'd0, s_cfg_q.inactive_cycles}; + `APB4_SPI__SEG_CONFIG: s_read_data = {28'd0, s_cfg_q.segment}; + `APB4_SPI__FRAME_COUNT: s_read_data = s_cfg_q.frames; + `APB4_SPI__DUMMY: s_read_data = {16'd0, s_cfg_q.dummy}; + `APB4_SPI__TX_BIND: s_read_data = s_cfg_q.tx_binding; + `APB4_SPI__RX_BIND: s_read_data = s_cfg_q.rx_binding; + `APB4_SPI__WATERMARKS: s_read_data = {20'd0, s_cfg_q.watermarks}; + `APB4_SPI__IRQ_STATE: s_read_data = {26'd0, s_irq_q}; + `APB4_SPI__IRQ_ENABLE: s_read_data = {22'd0, s_irq_en_q}; + `APB4_SPI__COMMAND: begin + if (!apb4.pwrite || (apb4.pwdata[31:9] != 23'd0)) s_access_err = 1'b1; + if (ctrl.cmd_err) s_access_err = 1'b1; + end + `APB4_SPI__CPU_TXDATA, `APB4_SPI__DMA_TXDATA: begin + if (!apb4.pwrite || !ctrl.tx_ready || + (s_dma_tx ? !s_tx_qualified : (!s_cpu || s_cfg_q.tx_binding[31]))) + s_access_err = 1'b1; + end + `APB4_SPI__CPU_RXDATA, `APB4_SPI__DMA_RXDATA: begin + s_read_data = ctrl.rx_data; + if (apb4.pwrite || !ctrl.rx_ready || + (s_dma_rx ? !s_rx_qualified : (!s_cpu || s_cfg_q.rx_binding[31]))) + s_access_err = 1'b1; + end + default: s_access_err = 1'b1; + endcase + if (s_req_accept && apb4.pwrite) begin + unique case (s_offset) + `APB4_SPI__SESSION_CFG: begin + if (!s_session_write || (apb4.pwdata[31:5] != 27'd0) || (apb4.pwdata[4:3] == 2'b11)) + s_access_err = 1'b1; + s_cfg_d.session = apb4.pwdata[4:0]; + end + `APB4_SPI__DIV, `APB4_SPI__CS_SETUP, `APB4_SPI__CS_HOLD, `APB4_SPI__CS_INACTIVE: begin + if (!s_session_write || (apb4.pwdata[31:16] != 16'd0) || (apb4.pwdata[15:0] == 16'd0)) + s_access_err = 1'b1; + unique case (s_offset) + `APB4_SPI__DIV: s_cfg_d.divider = apb4.pwdata[15:0]; + `APB4_SPI__CS_SETUP: s_cfg_d.setup_cycles = apb4.pwdata[15:0]; + `APB4_SPI__CS_HOLD: s_cfg_d.hold_cycles = apb4.pwdata[15:0]; + default: s_cfg_d.inactive_cycles = apb4.pwdata[15:0]; + endcase + end + `APB4_SPI__SEG_CONFIG: begin + if (!s_segment_write || (apb4.pwdata[31:4] != 28'd0) || (apb4.pwdata[2:1] == 2'd3)) + s_access_err = 1'b1; + s_cfg_d.segment = apb4.pwdata[3:0]; + end + `APB4_SPI__FRAME_COUNT: begin + if (!s_segment_write || (apb4.pwdata == 32'd0)) s_access_err = 1'b1; + s_cfg_d.frames = apb4.pwdata; + end + `APB4_SPI__DUMMY: begin + if (!s_segment_write || (apb4.pwdata[31:16] != 16'd0)) s_access_err = 1'b1; + s_cfg_d.dummy = apb4.pwdata[15:0]; + end + `APB4_SPI__TX_BIND, `APB4_SPI__RX_BIND: begin + if (!s_segment_write || ((apb4.pwdata & 32'h7fff_fff8) != 32'd0)) s_access_err = 1'b1; + if (s_offset == `APB4_SPI__TX_BIND) s_cfg_d.tx_binding = apb4.pwdata; + else s_cfg_d.rx_binding = apb4.pwdata; + if (s_cfg_d.tx_binding[31] && s_cfg_d.rx_binding[31] && + (s_cfg_d.tx_binding[2:0] == s_cfg_d.rx_binding[2:0])) + s_access_err = 1'b1; + end + `APB4_SPI__WATERMARKS: begin + if (((apb4.pwdata & 32'hffff_f0f0) != 32'd0) || + (apb4.pwdata[3:0] > 4'd8) || (apb4.pwdata[11:8] == 4'd0) || + (apb4.pwdata[11:8] > 4'd8)) + s_access_err = 1'b1; + s_cfg_d.watermarks = apb4.pwdata[11:0]; + end + `APB4_SPI__IRQ_STATE: begin + if (apb4.pwdata[31:6] != 26'd0) s_access_err = 1'b1; + s_irq_clear = apb4.pwdata[5:0]; + end + `APB4_SPI__IRQ_ENABLE: begin + if ((apb4.pwdata & 32'hffff_fcc0) != 32'd0) s_access_err = 1'b1; + s_irq_en_d = apb4.pwdata[9:0]; + end + `APB4_SPI__COMMAND, `APB4_SPI__CPU_TXDATA, `APB4_SPI__DMA_TXDATA: ; + default: s_access_err = 1'b1; + endcase + end + if (s_access_err) begin + s_cfg_d = s_cfg_q; + s_irq_en_d = s_irq_en_q; + s_irq_clear = '0; + end + if (s_req_accept && !s_access_err) begin + ctrl.tx_push = s_tx_sel && apb4.pwrite; + ctrl.rx_pop = s_rx_sel && !apb4.pwrite; + credit_commit_o = {ctrl.rx_pop && s_dma_rx, ctrl.tx_push && s_dma_tx}; + s_credit_used_d = s_credit_used_d | credit_commit_o; + end + s_irq_d = ctrl.clear_events ? 6'd0 : ((s_irq_q & ~s_irq_clear) | ctrl.events); + if (ctrl.clear_bindings) begin + s_cfg_d.tx_binding = '0; + s_cfg_d.rx_binding = '0; + end + if (s_req_accept && !s_access_err && ctrl.cmd[`APB4_SPI__RESET]) begin + s_cfg_d = ResetCfg; + s_irq_en_d = '0; + end + s_ready_d = s_req; + s_rdata_d = s_req_accept ? s_read_data : s_rdata_q; + s_resp_err_d = s_req_accept ? s_access_err : s_resp_err_q; + end + dffrc #( + .DATA_WIDTH($bits(spi_pkg::configuration_t)), + .RESET_VAL (ResetCfg) + ) u_configuration ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_cfg_d), + .dat_o (s_cfg_q) + ); + dffr #( + .DATA_WIDTH(1) + ) u_response_ready ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_ready_d), + .dat_o (s_ready_q) + ); + dffr #( + .DATA_WIDTH(32) + ) u_response_data ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_rdata_d), + .dat_o (s_rdata_q) + ); + dffr #( + .DATA_WIDTH(1) + ) u_response_error ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_resp_err_d), + .dat_o (s_resp_err_q) + ); + dffr #( + .DATA_WIDTH(6) + ) u_irq_state ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_irq_d), + .dat_o (s_irq_q) + ); + dffr #( + .DATA_WIDTH(10) + ) u_irq_enable ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_irq_en_d), + .dat_o (s_irq_en_q) + ); + dffr #( + .DATA_WIDTH(2) + ) u_credit_used ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_credit_used_d), + .dat_o (s_credit_used_q) + ); + dffr #( + .DATA_WIDTH(1) + ) u_data_access ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_data_access_d), + .dat_o (s_data_access_q) + ); +`ifdef FORMAL + always_ff @(posedge clk_i) begin + if (rst_n_i) begin + assert ((credit_commit_o & s_credit_used_q) == 2'd0); + if (origin_i != 2'd1) assert (credit_commit_o == 2'd0); + assert (!ctrl.tx_push || ctrl.tx_ready); + assert (!ctrl.rx_pop || ctrl.rx_ready); + if (s_dma_tx && !s_tx_qualified) assert (!ctrl.tx_push); + if (s_dma_rx && !s_rx_qualified) assert (!ctrl.rx_pop); + end + end +`endif +endmodule diff --git a/rtl/ip/serial/spi_word_packer.sv b/rtl/ip/serial/spi_word_packer.sv new file mode 100644 index 00000000..862a7d6c --- /dev/null +++ b/rtl/ip/serial/spi_word_packer.sv @@ -0,0 +1,112 @@ +// Common FIFO inputs are pre-qualified: full plus pop does NOT accept a push. +module spi_word_packer ( + input logic clk_i, + rst_n_i, + clear_i, + frame16_i, + input logic [ 1:0] direction_i, + input logic [31:0] quota_i, + tx_data_i, + input logic tx_push_i, + rx_pop_i, + frame_start_i, + frame_done_i, + last_frame_i, + input logic [15:0] rx_frame_i, + output logic [15:0] tx_frame_o, + output logic [31:0] rx_data_o, + tx_words_o, + rx_words_o, + rx_removed_o, + output logic tx_ready_o, + rx_ready_o, + tx_frame_ready_o, + rx_frame_ready_o, + empty_o, + output logic [ 3:0] tx_level_o, + rx_level_o +); + typedef struct packed { + logic [31:0] tx_word, rx_word, accepted, produced, removed; + logic [1:0] tx_lane, rx_lane; + logic tx_valid; + } packing_t; + packing_t s_pack_d, s_pack_q; + logic s_tx_full, s_tx_empty, s_rx_full, s_rx_empty; + logic s_tx_pop, s_rx_push; + logic [31:0] s_tx_head, s_tx_word, s_rx_word; + logic [1:0] s_last_lane; + logic [4:0] s_tx_shift, s_rx_shift; + assign s_last_lane = frame16_i ? 2'd1 : 2'd3; + assign s_tx_shift = frame16_i ? {s_pack_q.tx_lane[0], 4'd0} : {s_pack_q.tx_lane, 3'd0}; + assign s_rx_shift = frame16_i ? {s_pack_q.rx_lane[0], 4'd0} : {s_pack_q.rx_lane, 3'd0}; + assign s_tx_word = s_pack_q.tx_valid ? s_pack_q.tx_word : s_tx_head; + assign tx_frame_o = 16'(s_tx_word >> s_tx_shift); + assign s_rx_word = s_pack_q.rx_word | + ((frame16_i ? {16'd0, rx_frame_i} : {24'd0, rx_frame_i[7:0]}) << s_rx_shift); + assign tx_ready_o = !s_tx_full && (s_pack_q.accepted < quota_i); + assign rx_ready_o = !s_rx_empty && (s_pack_q.removed < quota_i); + assign tx_frame_ready_o = (direction_i == 2'd2) || s_pack_q.tx_valid || !s_tx_empty; + assign rx_frame_ready_o = (direction_i == 2'd0) || (s_pack_q.rx_lane != 2'd0) || !s_rx_full; + assign s_tx_pop = frame_start_i && (direction_i != 2'd2) && !s_pack_q.tx_valid; + assign s_rx_push = frame_done_i && (direction_i != 2'd0) && + ((s_pack_q.rx_lane == s_last_lane) || last_frame_i); + assign empty_o = s_tx_empty && s_rx_empty && !s_pack_q.tx_valid && (s_pack_q.rx_lane == 2'd0); + assign tx_words_o = s_pack_q.accepted; + assign rx_words_o = s_pack_q.produced; + assign rx_removed_o = s_pack_q.removed; + always_comb begin + s_pack_d = s_pack_q; + if (tx_push_i && tx_ready_o) s_pack_d.accepted = s_pack_q.accepted + 32'd1; + if (rx_pop_i && rx_ready_o) s_pack_d.removed = s_pack_q.removed + 32'd1; + if (frame_start_i && (direction_i != 2'd2)) begin + s_pack_d.tx_word = s_tx_word; + s_pack_d.tx_valid = !last_frame_i && (s_pack_q.tx_lane != s_last_lane); + s_pack_d.tx_lane = s_pack_d.tx_valid ? s_pack_q.tx_lane + 2'd1 : 2'd0; + end + if (frame_done_i && (direction_i != 2'd0)) begin + s_pack_d.rx_word = s_rx_push ? 32'd0 : s_rx_word; + s_pack_d.rx_lane = s_rx_push ? 2'd0 : s_pack_q.rx_lane + 2'd1; + if (s_rx_push) s_pack_d.produced = s_pack_q.produced + 32'd1; + end + if (clear_i) s_pack_d = '0; + end + dffr #( + .DATA_WIDTH($bits(packing_t)) + ) u_packing ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_pack_d), + .dat_o (s_pack_q) + ); + fifo #( + .DATA_WIDTH (32), + .BUFFER_DEPTH(8) + ) u_tx_fifo ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .flush_i(clear_i), + .push_i (tx_push_i && tx_ready_o), + .full_o (s_tx_full), + .dat_i (tx_data_i), + .pop_i (s_tx_pop && !s_tx_empty), + .empty_o(s_tx_empty), + .dat_o (s_tx_head), + .cnt_o (tx_level_o) + ); + fifo #( + .DATA_WIDTH (32), + .BUFFER_DEPTH(8) + ) u_rx_fifo ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .flush_i(clear_i), + .push_i (s_rx_push && !s_rx_full), + .full_o (s_rx_full), + .dat_i (s_rx_word), + .pop_i (rx_pop_i && rx_ready_o), + .empty_o(s_rx_empty), + .dat_o (rx_data_o), + .cnt_o (rx_level_o) + ); +endmodule diff --git a/rtl/rtl_readiness.json b/rtl/rtl_readiness.json index d2973028..ef899fca 100644 --- a/rtl/rtl_readiness.json +++ b/rtl/rtl_readiness.json @@ -1,6 +1,25 @@ { "schema_version": 1, "targets": [ + { + "name": "spi-p1", + "paths": ["rtl/ip/serial/apb4_spi.sv", "rtl/ip/serial/spi_reg.sv", + "rtl/ip/serial/spi_master_core.sv", "rtl/ip/serial/spi_phase_ctrl.sv", + "rtl/ip/serial/spi_word_packer.sv", "rtl/ip/serial/spi_gpio_guard.sv"], + "status": "prototype", + "baseline_revision": "e62ccdad87e1fd412eff70cc2527ad3090f16a55", + "configuration_digest": null, + "configuration_profiles": ["configs/ci/ics55-tiny.mk", "configs/ci/ihp130-tiny.mk"], + "required_evidence": [], + "synthesis_intent": { + "registers": "Common registers; exactly-once registered APB response and finite segment state", + "reset": "active-low system reset invalidates traffic; RECOVER drains before clearing local validity", + "clock_enable": "PCLK only; SCK is a registered output, never an internal clock", + "memory": "two 8x32 Common FIFOs; pre-edge eligibility; no payload reset or SRAM macro", + "pipeline": "APB response, FIFO packing, per-frame phase engine; standalone source qualification only" + }, + "waivers": [] + }, { "name": "tiny-r2-p5", "paths": ["rtl/tiny/top"], diff --git a/rtl/rtl_style_audit.json b/rtl/rtl_style_audit.json index 41edcd49..35c9a17d 100644 --- a/rtl/rtl_style_audit.json +++ b/rtl/rtl_style_audit.json @@ -265,6 +265,15 @@ "user_gpio_if.sv" ], "rtl/ip/serial": [ + "apb4_spi.sv", + "spi_ctrl_if.sv", + "spi_define.svh", + "spi_gpio_guard.sv", + "spi_master_core.sv", + "spi_phase_ctrl.sv", + "spi_pkg.sv", + "spi_reg.sv", + "spi_word_packer.sv", "apb4_i2c.sv", "apb4_i2c_define.svh", "apb4_i2s.sv", diff --git a/scripts/spi_p1.py b/scripts/spi_p1.py new file mode 100644 index 00000000..798e19b8 --- /dev/null +++ b/scripts/spi_p1.py @@ -0,0 +1,496 @@ +#!/usr/bin/env python3 +"""Standalone SPI-P1 flows; no product integration or qualification promotion.""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +from pathlib import Path +import shutil +import subprocess +import sys +import tarfile + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +from scripts.check_simulation import DEFAULT_FAILURE # noqa: E402 +from scripts.setup_helpers import atomic_write # noqa: E402 +from scripts.yosys_period import period_ps # noqa: E402 + +COMMON = ROOT / "rtl/managed/clusterip/common/rtl" +IP = ROOT / "rtl/ip/serial" +TOP = "apb4_spi_block_top" +WRAPPER = ROOT / f"physical/smoke/syn/yosys/{TOP}.sv" +SOURCES = [COMMON / "interface/apb4_if.sv", COMMON / "utils/register.sv", + ROOT / "rtl/ip/util/soc_common_fifo.sv", + *[IP / f"{name}.sv" for name in ( + "spi_pkg", "spi_ctrl_if", "spi_gpio_guard", "spi_phase_ctrl", + "spi_word_packer", "spi_master_core", "spi_reg", "apb4_spi")], WRAPPER] +INCLUDES = [COMMON, COMMON / "utils", IP, ROOT / "tests/rtl"] +CASES = ("spi_pin_checker_tb", "spi_core_tb", "spi_apb_tb", "spi_lifecycle_tb", "spi_packer_tb") +TOOL_INPUT_SCHEMA = 2 + + +def digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def dump(path: Path, data: dict) -> None: + atomic_write(path, json.dumps(data, indent=2, sort_keys=True) + "\n") + + +def inputs() -> dict[str, str]: + files = set(SOURCES) | {COMMON / "utils/fifo.sv", COMMON / "config.svh", IP / "spi_define.svh", + ROOT / "Makefile", ROOT / "scripts/spi_p1.py", ROOT / "scripts/run_flow.py", + ROOT / "scripts/check_simulation.py", ROOT / "dependencies/dependencies.lock.json", + ROOT / "docs/ip/spi.md", ROOT / "configs/ci/ics55-tiny.mk", + ROOT / "rtl/tiny/integration/clock_reset_domains.json", + ROOT / "scripts/bitwuzla_smt2.py", ROOT / "scripts/yosys_period.py", + ROOT / "configs/ci/ihp130-tiny.mk", ROOT / "crt/include/retrosoc/hal/spi_regs.h"} + for pattern in ("tests/rtl/spi_*", "tests/test_spi*", "physical/smoke/sta/opensta/spi_block.*", + "physical/smoke/syn/yosys/script/*"): + files.update(ROOT.glob(pattern)) + return {str(p.relative_to(ROOT)): digest(p) for p in sorted(files) if p.is_file()} + + +def yosys_json(path: Path) -> dict: + """Remove only Yosys tee timestamp prefixes, preserving the raw artifact.""" + return json.loads(re.sub(r'^\[\d+\.\d+\]\s?', '', path.read_text(), flags=re.M)) + + +def formal_model_counts(content: str, mode: str) -> dict: + counts = {kind: len(re.findall(rf'^; yosys-smt2-{kind}\b', content, re.M)) + for kind in ('assert', 'assume', 'cover')} + required = 'cover' if mode == 'cover' else 'assert' + if counts[required] == 0: + raise ValueError(f'formal model has no active {required} properties') + return counts + + +def cover_witness_steps(directory: Path, minimum: int = 193) -> int: + lengths = [len(json.loads(p.read_text())['steps']) for p in directory.glob('trace*.yw')] + if not lengths or max(lengths) < minimum: + raise ValueError('formal cover lacks a witness for the complete 192-cycle window') + return max(lengths) + + +def require_pass(content: str, result: dict, marker: str) -> None: + if result.get("status") != "passed" or result.get("exit_code") != 0: + raise ValueError("command did not pass") + if content.count(marker) != 1 or DEFAULT_FAILURE.search(content): + raise ValueError("missing/duplicate success or forbidden error marker") + + +def checked_record(path: Path, source: dict) -> dict: + data = json.loads(path.read_text()) + if data.get('tool_input_schema') != TOOL_INPUT_SCHEMA: + raise ValueError(f'incomplete historical tool inventory: {path}') + if data.get("status") != "passed" or data.get("source") != source: + raise ValueError(f"incomplete or stale SPI evidence: {path}") + pdk = data['manifest']['configuration']['PDK'] + if data.get('external_inputs') != external_inputs(pdk): + raise ValueError(f'changed or incomplete SPI tool/input inventory: {path}') + for name, value in data["artifacts"].items(): + if digest(Path(name)) != value: + raise ValueError(f"changed SPI artifact: {name}") + for name, value in data.get('external_inputs', {}).items(): + if digest(Path(name)) != value: + raise ValueError(f'changed SPI external input: {name}') + return data + + +def execute(directory: Path, name: str, command: list[str], timeout: int = 300, + environment: dict | None = None) -> tuple[dict, str]: + log, result = directory / f"{name}.log", directory / f"{name}.json" + args = [sys.executable, str(ROOT / "scripts/run_flow.py"), "--tool", name, + "--log", str(log), "--result", str(result), "--cwd", str(ROOT)] + for key, value in (environment or {}).items(): + args += ["--env", f"{key}={value}"] + args += ["--", "timeout", "--foreground", "--kill-after=5s", f"{timeout}s", *command] + completed = subprocess.run(args, cwd=ROOT, check=False) + data = json.loads(result.read_text()) + if completed.returncode != 0 or data.get("exit_code") != 0 or data.get('status') != 'passed': + raise RuntimeError(f"{name} failed; see {log}") + if re.search(r'(?im)^Error:', log.read_text(errors='replace')): + raise RuntimeError(f'{name} reported a tool error; see {log}') + return data, log.read_text(errors="replace") + + +def verilator_config(directory: Path) -> str: + path = directory / "common.vlt" + atomic_write(path, "`verilator_config\n" + f'lint_off -rule DECLFILENAME -file "{COMMON}/utils/register.sv"\n' + f'lint_off -rule UNUSEDSIGNAL -file "{COMMON}/interface/apb4_if.sv"\n') + return str(path) + + +def simulate(directory: Path, simulator: str, top: str, jobs: int, + netlist: Path | None = None, models: list[Path] | None = None) -> None: + extra = [ROOT / "tests/rtl/spi_pin_bfm.sv", ROOT / f"tests/rtl/{top}.sv"] + defines = [f"-I{p}" for p in INCLUDES] + model = directory / top + if netlist is not None: + adapter = netlist_adapter(netlist, directory) + command = ["iverilog", "-g2012", "-gno-specify", "-s", top, "-o", str(model), + '-DSPI_DUT=spi_netlist_adapter', *defines, + *map(str, (models or []) + [netlist, adapter] + extra)] + elif simulator == "VERILATOR": + command = ["verilator", "--binary", "--timing", "--timescale", "1ns/1ps", + verilator_config(directory), "--assert", "-j", str(jobs), + "--top-module", top, "--Mdir", str(directory / f"obj-{top}"), + "-o", str(model), *defines, *map(str, SOURCES + extra)] + else: + converted = directory / f"{top}.v" + conversion = subprocess.run(["sv2v", *defines, *map(str, SOURCES + extra)], + capture_output=True, text=True, timeout=180, check=False) + atomic_write(directory / f"{top}-sv2v.log", conversion.stderr) + if conversion.returncode: + raise RuntimeError(conversion.stderr) + atomic_write(converted, '`timescale 1ns/1ps\n' + conversion.stdout) + command = ["iverilog", "-g2012", "-s", top, "-o", str(model), str(converted)] + execute(directory, f"{top}-compile", command, environment={"CCACHE_DISABLE": "1"}) + launch = [str(model)] if simulator == "VERILATOR" and netlist is None else ["vvp", str(model)] + result, log = execute(directory, top, launch, timeout=600) + require_pass(log, result, f"SPI_P1_PASS {top}") + + +def synth(directory: Path, pdk: str) -> None: + period = period_ps(json.loads((ROOT / 'rtl/tiny/integration/clock_reset_domains.json').read_text()), + 'peripheral') + for name in ("out", "tmp", "rpt"): + (directory / name).mkdir() + filelist = directory / "source.fl" + atomic_write(filelist, "\n".join([*[f"+incdir+{p}" for p in INCLUDES], + *map(str, SOURCES)]) + "\n") + execute(directory, "yosys", ["yosys", "-t", "--perffile", str(directory / "yosys-perf.json"), + "-c", str(ROOT / "physical/smoke/syn/yosys/script/synth.tcl")], 10800, { + "PDK": pdk, "SOC": "TINY", "SYNTH_RECIPE": "balanced", "HAVE_SRAM_MACRO": "NO", + "YOSYS_TARGET_PERIOD_PS": str(period), "SRAM_SIZE_KIB": '128', + "SV_FLIST": str(filelist), "TOP_DESIGN": TOP, + "PROJ_NAME": TOP, "WORK": str(directory / "tmp"), "BUILD": str(directory / "out"), + "REPORTS": str(directory / "rpt"), "NETLIST": str(directory / "out/netlist.v"), + "CONFIG": str(directory / "out/config"), "YOSYS_KEEP_HIER_INST": "", + "YOSYS_REPORT_INSTS": "", "YOSYS_FLATTEN_HIER": "1"}) + mapped = json.loads((directory / 'out/netlist.v.json').read_text())['modules'][TOP] + cells = [cell['type'] for cell in mapped['cells'].values()] + if any(not (c.startswith('sg13g2_') if pdk == 'IHP130' else c.endswith('H7R')) for c in cells): + raise ValueError('foreign, unresolved or macro cell in SPI block netlist') + generic = (directory / f'rpt/{TOP}_generic.rpt').read_text() + if '$dlatch' in generic.lower() or '$_dlatch' in generic.lower(): + raise ValueError('SPI inferred latch') + check = (directory / f'rpt/{TOP}_synth.rpt').read_text() + if 'Found and reported 0 problems.' not in check: + raise ValueError('SPI netlist structural check did not pass') + memory = yosys_json(directory / f'rpt/{TOP}_pre_memory.json')['modules'] + fifo_bits = {name: item.get('num_memory_bits', 0) for name, item in memory.items() + if 'u_tx_fifo' in name or 'u_rx_fifo' in name} + if sorted(fifo_bits.values()) != [256, 256]: + raise ValueError(f'SPI FIFO storage mapping differs: {fifo_bits}') + area = yosys_json(directory / f'rpt/{TOP}_area.json')['design']['area'] + dump(directory / 'mapping.json', {'top': TOP, 'pdk': pdk, 'period_ps': period, + 'cells': len(cells), 'area': area, 'fifo_bits': fifo_bits, 'sram_macros': 0}) + + +def netlist_adapter(netlist: Path, directory: Path) -> Path: + ports = json.loads(Path(str(netlist) + '.json').read_text())['modules'][TOP]['ports'] + groups = {} + connections = [] + for name, value in ports.items(): + match = re.fullmatch(r'(.*?)(?:__v(\d+)|_(\d+)_|\[(\d+)\])', name) + if match: + base = match[1] + index = int(next(v for v in match.groups()[1:] if v is not None)) + group = groups.setdefault(base, {'direction': value['direction'], 'indices': []}) + if group['direction'] != value['direction'] or len(value['bits']) != 1: + raise ValueError('invalid split-port direction/width') + group['indices'].append(index) + target = f'{base}[{index}]' + else: + base = name + groups[base] = {'direction': value['direction'], 'indices': list(range(len(value['bits'])))} + target = name + escaped = f'\\{name} ' if not re.fullmatch(r'[A-Za-z_]\w*', name) else name + connections.append(f'.{escaped}({target})') + declarations = [] + for name, value in groups.items(): + indices = sorted(value['indices']) + if indices != list(range(len(indices))): + raise ValueError('noncontiguous split port') + width = '' if len(indices) == 1 else f'[{len(indices)-1}:0] ' + declarations.append(f'{value["direction"]} wire {width}{name}') + path = directory / 'adapter.v' + atomic_write(path, 'module spi_netlist_adapter(\n' + ',\n'.join(declarations) + ');\n' + + f'{TOP} u_mapped(\n' + ',\n'.join(connections) + ');\nendmodule\n') + return path + + +def technology(pdk: str) -> tuple[Path, list[Path]]: + if pdk == "ICS55": + return (ROOT / ".cache/retrosoc/pdk/ics55/ics55_h7cr_ss.lib", + [ROOT / ".cache/retrosoc/pdk/ics55/ics55_h7cr_functional.v"]) + base = ROOT / "physical/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_stdcell" + return base / "lib/sg13g2_stdcell_slow_1p08V_125C.lib", [ + base / "verilog/sg13g2_udp.v", base / "verilog/sg13g2_stdcell.v"] + + +def tool_path(name: str) -> Path: + executable = shutil.which(name) + if executable is None: + raise ValueError(f'missing SPI tool: {name}') + return Path(executable).resolve() + + +def python_tool_directory(executable: Path, required: tuple[str, ...]) -> Path: + """Resolve the installed search order used by SBY/yosys-smtbmc.""" + candidates = [executable.parent / 'share/python3', + executable.parent.parent / 'share/yosys/python3'] + # A script-directory module would take precedence over the installed tree. + if any((executable.parent / name).exists() for name in required): + raise ValueError(f'shadowed Python tool module beside {executable}') + selected = next((p for p in candidates if (p / required[0]).is_file()), None) + if selected is None or any(not (selected / name).is_file() for name in required): + raise ValueError(f'incomplete Python tool installation: {executable}') + return selected + + +def proof_tool_components(executables: dict[str, Path]) -> dict[str, str]: + """Hash the actual helper programs and installed tool data, excluding caches.""" + yosys = executables['yosys'] + for name in ('yosys-smtbmc', 'yosys-abc'): + if executables[name] != (yosys.parent / name).resolve(): + raise ValueError(f'mixed Yosys installation: {name}') + sby_modules = python_tool_directory(executables['sby'], ( + 'sby_core.py', 'sby_engine_smtbmc.py', 'sby_cmdline.py', 'sby_mode_bmc.py', + 'sby_mode_cover.py', 'sby_mode_prove.py', 'sby_design.py', 'sby_jobserver.py', 'sby_status.py')) + yosys_modules = python_tool_directory(executables['yosys-smtbmc'], ('smtio.py', 'ywio.py')) + paths = set(executables.values()) + for directory in (yosys.parent, yosys.parent.parent / 'share/yosys', sby_modules, yosys_modules): + paths.update(p for p in directory.rglob('*') if p.is_file() + and '__pycache__' not in p.parts and p.suffix not in ('.pyc', '.pyo')) + return {str(p): digest(p) for p in sorted(paths)} + + +def proof_environment() -> dict[str, str]: + # Eliminate user PYTHONPATH/site-module overrides; the scripts append their + # fingerprinted installation paths. Keep the active pinned venv interpreter. + return {'PYTHONPATH': '', 'PYTHONNOUSERSITE': '1', 'PYTHONDONTWRITEBYTECODE': '1'} + + +def external_inputs(pdk: str) -> dict[str, str]: + slow, models = technology(pdk) + typical = Path(str(slow).replace('_ss.lib', '_tt.lib').replace( + '_slow_1p08V_125C.lib', '_typ_1p20V_25C.lib')) + paths = [slow, typical, *models] + if pdk == 'ICS55': + paths += [ROOT / '.cache/retrosoc/sources/ics55_ecos_pll/lib/PLL_TOP_typ.lib', + ROOT / 'physical/pdk/icsprout55-pdk/IP/IO/ICsprout_55LLULP1233_IO_251013/liberty/ICSIOA_N55_3P3_tt_1p2_3p3_25c.lib'] + else: + paths += [ROOT / 'physical/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_io/lib/sg13g2_io_typ_1p2V_3p3V_25C.lib'] + executables = {name: tool_path(name) for name in ( + 'verilator', 'iverilog', 'vvp', 'sv2v', 'yosys', 'yosys-smtbmc', + 'yosys-abc', 'sta', 'sby', 'bitwuzla', 'python3')} + paths.append(Path(sys.executable).resolve()) + venv_config = Path(sys.prefix) / 'pyvenv.cfg' + if venv_config.is_file(): + paths.append(venv_config) + return {**{str(p): digest(p) for p in paths}, **proof_tool_components(executables)} + + +def reuse_formal_task(previous: Path, directory: Path, label: str, mode: str, + external: dict) -> dict[str, str]: + """Reuse an intact successful subtask, never the failed enclosing campaign.""" + original = previous / 'record.json' + record = json.loads(original.read_text()) + if record.get('tool_input_schema') != TOOL_INPUT_SCHEMA: + raise ValueError('historical proof lacks complete tool-component fingerprints') + if record.get('phase') != 'SPI-P1' or record.get('stage') != 'formal': + raise ValueError('not SPI-P1 formal evidence') + if record.get('external_inputs') != external: + raise ValueError('formal tools or external inputs changed') + # Driver/Make/test changes may affect orchestration, but never the model, + # solver adapter, dependency lock or any other proof-producing source. + orchestration = {'Makefile', 'scripts/spi_p1.py', 'tests/test_spi_p1.py'} + old_source = {k: v for k, v in record['source'].items() if k not in orchestration} + new_source = {k: v for k, v in inputs().items() if k not in orchestration} + if old_source != new_source: + raise ValueError('formal source inputs changed') + artifacts = record['artifacts'] + required = [previous / f'{label}{suffix}' for suffix in ('.v', '.sby', '.json', '.log')] + required += [previous / label / name for name in + ('status', 'config.sby', 'src/design.v', 'model/design_smt2.smt2')] + if any(str(p) not in artifacts for p in required): + raise ValueError('formal producer artifact missing from original inventory') + for name, value in {**artifacts, **external}.items(): + if digest(Path(name)) != value: + raise ValueError(f'changed formal producer input/artifact: {name}') + result = json.loads((previous / f'{label}.json').read_text()) + if result.get('status') != 'passed' or result.get('exit_code') != 0: + raise ValueError('formal subtask command failed') + if (previous / label / 'status').read_text().split()[0] != 'PASS': + raise ValueError('formal subtask did not pass') + model = directory / f'{label}.v' + if any(digest(p) != digest(model) for p in + (previous / f'{label}.v', previous / label / 'src/design.v')): + raise ValueError('formal model differs from actual consumed model') + + def normalized_config(path: Path, design: Path) -> str: + content = path.read_text() + line = f'design.v {design}\n' + if content.count(line) != 1: + raise ValueError('unexpected formal model mapping') + return content.replace(line, f'design.v SHA256:{digest(design)}\n') + + candidate = normalized_config(directory / f'{label}.sby', model) + for config in (previous / f'{label}.sby', previous / label / 'config.sby'): + if normalized_config(config, previous / f'{label}.v') != candidate: + raise ValueError('formal configuration changed') + counts = formal_model_counts((previous / label / 'model/design_smt2.smt2').read_text(), mode) + dump(directory / f'{label}-properties.json', counts) + dump(directory / f'{label}-reuse.json', { + 'status': 'passed', 'original_record': str(original), 'original_record_sha256': digest(original), + 'producer_source': record['source'], 'producer_manifest': record['manifest'], + 'producer_result': result, 'model_sha256': digest(model), + 'validation': 'identical consumed model, configuration, inputs and intact artifacts', + 'phase_accepted': False}) + return {**artifacts, str(original): digest(original)} + + +def formal(directory: Path, external: dict, retained: dict, + previous: Path | None = None) -> None: + sources = SOURCES + [ROOT / "tests/rtl/spi_formal.sv", ROOT / "tests/rtl/spi_formal_props.sv"] + solver = shutil.which('bitwuzla') + if solver is None: + raise ValueError('missing locked Bitwuzla solver') + bindir = directory / 'bin' + bindir.mkdir() + wrapper = bindir / 'bitwuzla' + wrapper.symlink_to(ROOT / 'scripts/bitwuzla_smt2.py') + for label, mode, depth in [('prove', 'prove', 32), ('register', 'prove', 32), + ('bmc', 'bmc', 192), ('cover', 'cover', 320)]: + defines = [] if mode == 'prove' else ['-DSPI_FORMAL_BOUNDED'] + if label == 'register': + defines = ['-DSPI_FORMAL_REGISTER'] + converted = subprocess.run(['sv2v', '-E', 'Assert', '-DFORMAL', '-DSV_ASSRT_DISABLE', *defines, + *[f'-I{p}' for p in INCLUDES], *map(str, sources)], + capture_output=True, text=True, timeout=180, check=True) + design = directory / f'{label}.v' + for keyword in (('assert', 'cover') if mode == 'cover' else ('assert',)): + if not re.search(rf'\b{keyword}\s*\(', converted.stdout): + raise ValueError(f'formal conversion removed {keyword} properties') + atomic_write(design, converted.stdout) + atomic_write(directory / f'{label}-sv2v.log', converted.stderr) + config = directory / f'{label}.sby' + task_timeout = 1200 if mode == 'bmc' else 600 + # The mandatory depth-192 cover witnesses satisfiable assumptions for + # the whole BMC window; avoid a redundant presat query at every step. + engine = 'smtbmc --nopresat bitwuzla' if mode == 'bmc' else 'smtbmc --presat --nounroll bitwuzla' + waveform = 'vcd off\n' if mode == 'cover' else '' + atomic_write(config, f'[options]\nmode {mode}\ndepth {depth}\ntimeout {task_timeout}\n{waveform}' + f'\n[engines]\n{engine}\n\n[script]\n' + 'read_verilog -formal -sv design.v\nprep -top spi_formal\nasync2sync\ndffunmap\n' + f'\n[files]\ndesign.v {design}\n') + if previous is not None and mode != 'cover': + retained.update(reuse_formal_task(previous, directory, label, mode, external)) + continue + execute(directory, label, [str(tool_path('sby')), '--yosys', str(tool_path('yosys')), + '--smtbmc', str(tool_path('yosys-smtbmc')), '--abc', str(tool_path('yosys-abc')), + '-d', str(directory / label), str(config)], task_timeout + 60, + {**proof_environment(), 'PATH': f'{bindir}:{os.environ["PATH"]}', + 'RETROSOC_BITWUZLA': solver}) + if (directory / label / 'status').read_text().split()[0] != 'PASS': + raise ValueError(f'formal {mode} did not pass') + counts = formal_model_counts((directory / label / 'model/design_smt2.smt2').read_text(), mode) + dump(directory / f'{label}-properties.json', counts) + if mode == 'cover': + steps = cover_witness_steps(directory / mode / 'engine_0') + dump(directory / 'nonvacuity.json', {'bmc_depth': 192, 'witness_steps': steps}) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("stage", choices=("lint", "rtl", "formal", "synth", "netsim", "sta", "report")) + parser.add_argument("--variant-root", type=Path, required=True) + parser.add_argument("--pdk", choices=("ICS55", "IHP130"), required=True) + parser.add_argument("--simulator", choices=("VERILATOR", "IVERILOG"), default="VERILATOR") + parser.add_argument("--jobs", type=int, default=4) + parser.add_argument('--common-variant-root', type=Path) + parser.add_argument('--formal-reuse-root', type=Path, + help='original formal stage; reuse only identical successful subtasks') + args = parser.parse_args() + variant = args.variant_root.resolve() + variant.relative_to(ROOT / "build") + manifest = json.loads((variant / "meta/manifest.json").read_text()) + if manifest["configuration"]["SOC"] != "TINY" or manifest["configuration"]["PDK"] != args.pdk: + raise ValueError("profile/PDK mismatch") + phase = variant / "meta/spi-p1" + suffix = args.stage + (f"-{args.simulator.lower()}" if args.stage == "rtl" else "") + directory = phase / suffix + source = inputs() + retained = {} + previous = args.formal_reuse_root.resolve() if args.formal_reuse_root else None + if previous is not None: + previous.relative_to(ROOT / 'build') + if args.stage != 'formal': + raise ValueError('formal reuse is valid only for the formal stage') + if directory.exists(): + raise ValueError(f"existing attempt: {directory}; use a fresh timestamp") + directory.mkdir(parents=True) + record = {"phase": "SPI-P1", "stage": args.stage, "status": "running", + 'tool_input_schema': TOOL_INPUT_SCHEMA, + "source": source, "manifest": manifest, "phase_accepted": False, + 'external_inputs': external_inputs(args.pdk)} + dump(directory / "record.json", record) + with tarfile.open(directory / "sources.tar.gz", "w:gz") as archive: + for name in source: + archive.add(ROOT / name, arcname=name) + try: + if args.stage == "lint": + execute(directory, "lint", ["verilator", "--lint-only", "--Wall", + verilator_config(directory), "--top-module", TOP, + *[f"-I{p}" for p in INCLUDES], *map(str, SOURCES)]) + elif args.stage == "rtl": + record['cases'] = list(CASES) + for top in CASES: + simulate(directory, args.simulator, top, args.jobs) + elif args.stage == "synth": + synth(directory, args.pdk) + elif args.stage in ("netsim", "sta"): + checked_record(phase / "synth/record.json", source) + netlist = phase / "synth/out/netlist.v" + liberty, models = technology(args.pdk) + if args.stage == "netsim": + simulate(directory, "IVERILOG", "spi_netlist_tb", args.jobs, netlist, models) + else: + execute(directory, "sta", ["sta", "-exit", str(ROOT / + "physical/smoke/sta/opensta/spi_block.tcl")], environment={ + "SPI_NETLIST": str(netlist), "SPI_LIBERTY": str(liberty), + "SPI_REPORT_DIR": str(directory)}) + elif args.stage == "formal": + formal(directory, record['external_inputs'], retained, previous) + else: + common = (args.common_variant_root.resolve() / 'meta/spi-p1' + if args.common_variant_root else phase) + record["stages"] = {name: checked_record( + (common if name in ('lint', 'rtl-verilator', 'rtl-iverilog', 'formal') else phase) / + name / 'record.json', source) for name in + ('lint', 'rtl-verilator', 'rtl-iverilog', 'formal', 'synth', 'netsim', 'sta')} + if source != inputs(): + raise ValueError("source changed during execution") + if record['external_inputs'] != external_inputs(args.pdk): + raise ValueError('external SPI inputs changed during execution') + record["status"] = "passed" + except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as error: + record.update(status="failed", error=str(error)) + print(error, file=sys.stderr) + return 1 + finally: + record["artifacts"] = {**retained, **{str(p): digest(p) for p in directory.rglob("*") + if p.is_file() and p.name != "record.json"}} + dump(directory / "record.json", record) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/rtl/spi_apb_tb.sv b/tests/rtl/spi_apb_tb.sv new file mode 100644 index 00000000..c030be74 --- /dev/null +++ b/tests/rtl/spi_apb_tb.sv @@ -0,0 +1,6 @@ +`timescale 1ns / 1ps +module spi_apb_tb; + localparam integer TestKind = 1; + localparam TestName = "spi_apb_tb"; + `include "spi_test_body.svh" +endmodule diff --git a/tests/rtl/spi_core_tb.sv b/tests/rtl/spi_core_tb.sv new file mode 100644 index 00000000..3b98fb85 --- /dev/null +++ b/tests/rtl/spi_core_tb.sv @@ -0,0 +1,6 @@ +`timescale 1ns / 1ps +module spi_core_tb; + localparam integer TestKind = 0; + localparam TestName = "spi_core_tb"; + `include "spi_test_body.svh" +endmodule diff --git a/tests/rtl/spi_formal.sv b/tests/rtl/spi_formal.sv new file mode 100644 index 00000000..8ae8f18c --- /dev/null +++ b/tests/rtl/spi_formal.sv @@ -0,0 +1,223 @@ +module spi_formal ( + input wire clk_i +); + reg rst_n = 0; + always @(posedge clk_i) rst_n <= 1; + (* anyseq *) reg miso; +`ifndef SPI_FORMAL_BOUNDED +`ifndef SPI_FORMAL_REGISTER + (* anyconst *) reg cpol, cpha, lsb, frame16; + (* anyconst *) reg [15:0] divider; + (* anyseq *) reg request_start, request_abort; + (* anyseq *) reg [15:0] tx_data; + wire active, done, sck, mosi; + wire [15:0] rx_data; + reg closing = 0; + always @(posedge clk_i) begin + assume (divider >= 1 && divider <= 4); + if (!rst_n || !active) closing <= 0; + else if (request_abort) closing <= 1; + end + spi_phase_ctrl u_phase ( + .clk_i (clk_i), + .rst_n_i (rst_n), + .start_i (request_start && !active), + .abort_i (request_abort || closing), + .clamp_i (!rst_n), + .cpol_i (cpol), + .cpha_i (cpha), + .lsb_first_i(lsb), + .frame16_i (frame16), + .divider_i (divider), + .tx_data_i (tx_data), + .miso_i (miso), + .active_o (active), + .done_o (done), + .sck_o (sck), + .mosi_o (mosi), + .rx_data_o (rx_data) + ); + spi_formal_props u_properties ( + .clk_i (clk_i), + .rst_n_i (rst_n), + .active_i(active), + .done_i (done), + .abort_i (request_abort || closing), + .sck_i (sck), + .cpol_i (cpol) + ); +`endif +`endif +`ifdef SPI_FORMAL_BOUNDED + // Component contract: register legality/stability is proved separately. + // All command values and legal FIFO service interleavings remain symbolic. + spi_ctrl_if u_session (); + reg [7:0] model_cycles = 0; + always @(posedge clk_i) begin + if (!rst_n) model_cycles <= 0; + else if (model_cycles != 192) model_cycles <= model_cycles + 1; + if (rst_n) cover (model_cycles == 192); + end + reg [4:0] session_cfg = 0; + reg [3:0] segment_cfg = 0; + reg [2:0] frame_count = 0; + reg [2:0] period_value = 1; + reg [1:0] setup_value = 1, hold_value = 1, inactive_value = 1; + (* anyseq *)reg update_configuration; + (* anyseq *)reg [4:0] next_session; + (* anyseq *)reg [3:0] next_segment; + (* anyseq *)reg [2:0] next_count; + (* anyseq *) reg [1:0] next_period, next_setup, next_hold, next_inactive; + (* anyseq *) reg [8:0] command_value; + (* anyseq *) reg [1:0] fifo_access, pending_start; + (* anyseq *) reg [3:0] transport; + (* anyseq *) reg route_ok, gpio_ok, handback, dma_fault, credit_fault_value; + (* anyseq *)reg apb_pending; + (* anyseq *)reg [31:0] payload; + spi_pkg::configuration_t model_cfg; + spi_pkg::status_t model_status; + wire [31:0] claim_mask; + wire core_sck, core_mosi, core_cs, core_oe; + assign claim_mask = (session_cfg[3] || session_cfg[4]) ? 32'hd8000000 : 32'h98000000; + always @* begin + model_cfg = '0; + model_cfg.session = session_cfg; + model_cfg.segment = segment_cfg; + model_cfg.frames = {29'd0, frame_count}; + model_cfg.divider = {13'd0, period_value}; + model_cfg.setup_cycles = {14'd0, setup_value}; + model_cfg.hold_cycles = {14'd0, hold_value}; + model_cfg.inactive_cycles = {14'd0, inactive_value}; + model_cfg.dummy = 16'hffff; + model_cfg.watermarks = 12'h100; + end + assign u_session.cfg = model_cfg; + assign model_status = u_session.stat; + assign u_session.cmd = command_value; + assign u_session.tx_data = payload; + assign u_session.tx_push = (fifo_access == 1) && u_session.tx_ready && command_value == 0; + assign u_session.rx_pop = (fifo_access == 2) && u_session.rx_ready && command_value == 0; + always @(posedge clk_i) begin + assume (next_count <= 5); + assume (next_session[4:3] != 2'b11); + assume (next_segment[2:1] != 2'd3); + assume (next_setup != 0 && next_hold != 0 && next_inactive != 0); + if (!rst_n) begin + session_cfg <= 0; + segment_cfg <= 0; + frame_count <= 0; + period_value <= 1; + setup_value <= 1; + hold_value <= 1; + inactive_value <= 1; + end else if (update_configuration && command_value == 0) begin + if (u_session.session_writable) begin + session_cfg <= next_session; + period_value <= {1'b0, next_period} + 3'd1; + setup_value <= next_setup; + hold_value <= next_hold; + inactive_value <= next_inactive; + end + if (u_session.segment_writable) begin + segment_cfg <= next_segment; + if (next_count != 0) frame_count <= next_count; + end + end + if (rst_n) begin + assert (model_status.status[3:0] <= 8); + if (!gpio_ok || !route_ok) assert (!core_oe); + cover (core_oe && !core_cs && core_sck != $past(core_sck)); + cover (u_session.events[0]); + cover (model_status.status[3:0] == 8); + end + end + spi_master_core u_session_core ( + .apb_pending_i (apb_pending), + .clk_i (clk_i), + .rst_n_i (rst_n), + .claim_i (claim_mask), + .ready_pins_i (route_ok ? claim_mask : 32'd0), + .gpio_ready_i (gpio_ok), + .handback_i (handback), + .dma_status_i (transport), + .dma_start_pending_i(pending_start), + .dma_error_i (dma_fault), + .credit_fault_i (credit_fault_value), + .miso_i (miso), + .sck_o (core_sck), + .mosi_o (core_mosi), + .cs_n_o (core_cs), + .output_enable_o (core_oe), + .ctrl (u_session) + ); +`endif +`ifdef SPI_FORMAL_REGISTER + apb4_if u_apb ( + .pclk (clk_i), + .presetn(rst_n) + ); + spi_ctrl_if u_ctrl (); + (* anyseq *) reg [31:0] address_data, write_data, rx_data_value; + (* anyseq *) reg selected, access_phase, writing, command_error; + (* anyseq *)reg [1:0] ready_value; + (* anyseq *)reg [5:0] event_value; + (* anyseq *) reg [1:0] origin_value, credit_value, retire_value; + (* anyseq *)reg [4:0] request_value; + (* anyseq *)reg [2:0] channel_value; + wire irq, credit_fault; + wire fifo_pending; + wire [1:0] committed; + reg register_past_valid = 0; + assign u_apb.paddr = address_data; + assign u_apb.pwdata = write_data; + assign u_apb.pstrb = 4'hf; + assign u_apb.pprot = 0; + assign u_apb.psel = selected; + assign u_apb.penable = access_phase; + assign u_apb.pwrite = writing; + assign u_ctrl.stat = '0; + assign u_ctrl.cmd_err = command_error; + assign u_ctrl.session_writable = 1; + assign u_ctrl.segment_writable = 1; + assign u_ctrl.tx_ready = ready_value[0]; + assign u_ctrl.rx_ready = ready_value[1]; + assign u_ctrl.rx_data = rx_data_value; + assign u_ctrl.events = event_value; + assign u_ctrl.clear_events = 0; + assign u_ctrl.clear_bindings = 0; + spi_reg u_reg ( + .clk_i (clk_i), + .rst_n_i (rst_n), + .clock_hz_i (32'd24000000), + .origin_i (origin_value), + .request_i (request_value), + .channel_i (channel_value), + .credit_valid_i (credit_value), + .credit_retire_i (retire_value), + .dma_status_i (4'd0), + .dma_start_pending_i(2'd0), + .credit_commit_o (committed), + .credit_pending_o (), // Observed through the wrapper in integration fixtures. + .fifo_pending_o (fifo_pending), + .credit_fault_o (credit_fault), + .irq_o (irq), + .apb4 (u_apb), + .ctrl (u_ctrl) + ); + always @(posedge clk_i) begin + register_past_valid <= rst_n; + if (rst_n && register_past_valid) begin + if ($past(selected && access_phase)) assert (u_apb.pready); + if ($past(u_ctrl.tx_push || u_ctrl.rx_pop)) assert (fifo_pending); + if ($past(u_apb.pready)) begin + assert (u_apb.prdata == $past(u_apb.prdata)); + assert (u_apb.pslverr == $past(u_apb.pslverr)); + end + if (origin_value != 1) assert (committed == 0); + if (u_apb.pready) begin + assert (!u_ctrl.tx_push && !u_ctrl.rx_pop && u_ctrl.cmd == 0); + end + end + end +`endif +endmodule diff --git a/tests/rtl/spi_formal_props.sv b/tests/rtl/spi_formal_props.sv new file mode 100644 index 00000000..27ca2e8f --- /dev/null +++ b/tests/rtl/spi_formal_props.sv @@ -0,0 +1,24 @@ +module spi_formal_props ( + input wire clk_i, + rst_n_i, + active_i, + done_i, + abort_i, + sck_i, + cpol_i +); + reg past_valid = 0; + reg [4:0] closing_cycles = 0; + always @(posedge clk_i) begin + past_valid <= rst_n_i; + if (!rst_n_i || !active_i || !abort_i) closing_cycles <= 0; + else closing_cycles <= closing_cycles + 1; + if (rst_n_i && past_valid) begin + assert (!done_i || (active_i && !abort_i)); + assert (closing_cycles <= 10); + if (!active_i && !$past(active_i)) assert (sck_i == cpol_i); + cover (done_i); + cover ($past(active_i && abort_i) && !active_i); + end + end +endmodule diff --git a/tests/rtl/spi_lifecycle_tb.sv b/tests/rtl/spi_lifecycle_tb.sv new file mode 100644 index 00000000..fa29406b --- /dev/null +++ b/tests/rtl/spi_lifecycle_tb.sv @@ -0,0 +1,6 @@ +`timescale 1ns / 1ps +module spi_lifecycle_tb; + localparam integer TestKind = 2; + localparam TestName = "spi_lifecycle_tb"; + `include "spi_test_body.svh" +endmodule diff --git a/tests/rtl/spi_netlist_tb.sv b/tests/rtl/spi_netlist_tb.sv new file mode 100644 index 00000000..c2440686 --- /dev/null +++ b/tests/rtl/spi_netlist_tb.sv @@ -0,0 +1,6 @@ +`timescale 1ns / 1ps +module spi_netlist_tb; + localparam integer TestKind = 3; + localparam TestName = "spi_netlist_tb"; + `include "spi_test_body.svh" +endmodule diff --git a/tests/rtl/spi_packer_tb.sv b/tests/rtl/spi_packer_tb.sv new file mode 100644 index 00000000..686cc2ce --- /dev/null +++ b/tests/rtl/spi_packer_tb.sv @@ -0,0 +1,92 @@ +`timescale 1ns / 1ps +module spi_packer_tb; + logic clk_i = 0, rst_n_i = 0, clear_i = 0, frame16_i = 0; + logic [1:0] direction_i = 0; + logic [31:0] quota_i = 10, tx_data_i = 0; + logic tx_push_i = 0, rx_pop_i = 0, request_start = 0, frame_done_i = 0, last_frame_i = 0; + logic [15:0] rx_frame_i = 0, tx_frame_o; + logic [31:0] rx_data_o, tx_words_o, rx_words_o, rx_removed_o; + logic tx_ready_o, rx_ready_o, tx_frame_ready_o, rx_frame_ready_o, empty_o; + logic [3:0] tx_level_o, rx_level_o; + always #20.833 clk_i = ~clk_i; + spi_word_packer u_packer ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .clear_i (clear_i), + .frame16_i (frame16_i), + .direction_i (direction_i), + .quota_i (quota_i), + .tx_data_i (tx_data_i), + .tx_push_i (tx_push_i), + .rx_pop_i (rx_pop_i), + .frame_start_i (request_start && tx_frame_ready_o && rx_frame_ready_o), + .frame_done_i (frame_done_i), + .last_frame_i (last_frame_i), + .rx_frame_i (rx_frame_i), + .tx_frame_o (tx_frame_o), + .rx_data_o (rx_data_o), + .tx_words_o (tx_words_o), + .rx_words_o (rx_words_o), + .rx_removed_o (rx_removed_o), + .tx_ready_o (tx_ready_o), + .rx_ready_o (rx_ready_o), + .tx_frame_ready_o(tx_frame_ready_o), + .rx_frame_ready_o(rx_frame_ready_o), + .empty_o (empty_o), + .tx_level_o (tx_level_o), + .rx_level_o (rx_level_o) + ); + initial begin + repeat (3) @(negedge clk_i); + rst_n_i = 1; + for (integer index = 0; index < 8; index++) begin + tx_push_i = 1; + tx_data_i = 32'(index); + @(negedge clk_i); + end + if (tx_level_o != 8 || tx_ready_o) $fatal(1, "SPI full FIFO eligibility"); + request_start = 1; + tx_push_i = 1; + @(negedge clk_i); + request_start = 0; + tx_push_i = 0; + if (tx_level_o != 7 || tx_words_o != 8) $fatal(1, "SPI full lookahead admitted extra word"); + clear_i = 1; + @(negedge clk_i); + clear_i = 0; + request_start = 1; + tx_push_i = 1; + if (tx_frame_ready_o) $fatal(1, "SPI empty fallthrough"); + @(negedge clk_i); + request_start = 0; + tx_push_i = 0; + if (tx_level_o != 1 || tx_words_o != 1) $fatal(1, "SPI empty simultaneous push/pop"); + clear_i = 1; + @(negedge clk_i); + clear_i = 0; + direction_i = 2; + frame16_i = 1; + for (integer frame = 0; frame < 16; frame++) begin + request_start = 1; + @(negedge clk_i); + request_start = 0; + frame_done_i = 1; + rx_frame_i = 16'(frame); + @(negedge clk_i); + frame_done_i = 0; + end + if (rx_level_o != 8 || rx_frame_ready_o) $fatal(1, "SPI RX reservation at full"); + request_start = 1; + rx_pop_i = 1; + @(negedge clk_i); + request_start = 0; + rx_pop_i = 0; + if (rx_level_o != 7 || rx_words_o != 8 || rx_removed_o != 1) $fatal(1, "SPI RX full pop"); + $display("SPI_P1_PASS spi_packer_tb"); + $finish; + end + initial begin + #100000; + $fatal(1, "SIM_TEST_TIMEOUT SPI packer"); + end +endmodule diff --git a/tests/rtl/spi_pin_bfm.sv b/tests/rtl/spi_pin_bfm.sv new file mode 100644 index 00000000..2460165b --- /dev/null +++ b/tests/rtl/spi_pin_bfm.sv @@ -0,0 +1,119 @@ +// Pin-only oracle: no DUT state/shifter references. Timing is counted in PCLKs. +// Fault bits: 0 half-period, 1 CS setup, 2 first-bit setup, 3 incomplete frame, +// 4 CS hold, 5 CS inactive, 6 edge after abort, 7 pause polarity, 8 payload. +// Normal fixtures must fail on any fault; the checker fixture tests rejection. +module spi_pin_bfm ( + input logic [15:0] divider_i, + setup_i, + hold_i, + inactive_i, + input logic segment_i, + pause_i, + input logic [15:0] dummy_i, + input logic clk_i, + begin_i, + enabled_i, + abort_i, + input logic cpol_i, + cpha_i, + lsb_i, + frame16_i, + input logic [ 1:0] direction_i, + input logic cs_n_i, + sck_i, + mosi_i, + output logic miso_o, + output integer frames_o, + edges_o, + output logic [ 8:0] faults_o +); + logic previous_sck = 0; + logic [15:0] received = 0; + integer samples = 0; + integer index_bit; + integer width_bits; + logic [31:0] expected_word; + logic [15:0] expected_frame, response; + integer cycle = 0, last_edge = 0, segment_start = 0, released = 0; + integer frame_edges = 0; + logic previous_cs = 1, previous_mosi = 0; + logic have_edge = 0, have_release = 0, aborted = 0; + function automatic logic [15:0] reply(input integer frame); + return 16'ha5c3 ^ (16'(frame) * 16'h1237); + endfunction + always @(negedge clk_i) begin + width_bits = frame16_i ? 16 : 8; + if (begin_i) begin + cycle = 0; + last_edge = 0; + segment_start = 0; + released = 0; + frame_edges = 0; + have_edge = 0; + have_release = 0; + aborted = 0; + faults_o = 0; + previous_cs = cs_n_i; + previous_mosi = mosi_i; + frames_o = 0; + edges_o = 0; + samples = 0; + received = 0; + previous_sck = cpol_i; + response = reply(0); + miso_o = response[lsb_i?0 : width_bits-1]; + end else if (enabled_i) begin + cycle++; + if (segment_i || (previous_cs && !cs_n_i)) segment_start = cycle; + if (abort_i) aborted = 1; + if (previous_cs && !cs_n_i && have_release && cycle - released < integer'(inactive_i)) + faults_o[5] = 1; + if (!previous_cs && cs_n_i) begin + if (sck_i != cpol_i || (!aborted && frame_edges != 0)) faults_o[3] = 1; + if (have_edge && cycle - last_edge < integer'(hold_i)) faults_o[4] = 1; + have_release = 1; + released = cycle; + end + if (pause_i && (!cs_n_i && sck_i != cpol_i)) faults_o[7] = 1; + if (!cs_n_i && sck_i != previous_sck) begin + if (have_edge) begin + // Only the idle CPOL interval BETWEEN complete frames may stretch. + if (cycle - last_edge < integer'(divider_i) || + ((frame_edges != 0) && cycle - last_edge != integer'(divider_i))) + faults_o[0] = 1; + end + if (aborted && (previous_sck == cpol_i || sck_i != cpol_i)) faults_o[6] = 1; + if (!have_edge && cycle - segment_start < integer'(setup_i)) faults_o[1] = 1; + if (frame_edges == 0 && !cpha_i && mosi_i != previous_mosi) faults_o[2] = 1; + last_edge = cycle; + have_edge = 1; + frame_edges = (frame_edges + 1) % (2 * width_bits); + edges_o++; + if (!aborted && ((sck_i == cpol_i) == cpha_i)) begin + index_bit = lsb_i ? samples : width_bits - 1 - samples; + received[index_bit] = mosi_i; + samples++; + if (samples == width_bits) begin + expected_word = 32'h80ff017e ^ (32'(frames_o / (32 / width_bits)) * 32'h01020408); + expected_frame = 16'(expected_word >> ((frames_o % (32 / width_bits)) * width_bits)); + if (direction_i == 2) expected_frame = dummy_i; + if ((received & (frame16_i ? 16'hffff : 16'h00ff)) !== + (expected_frame & (frame16_i ? 16'hffff : 16'h00ff))) + faults_o[8] = 1; + frames_o++; + samples = 0; + received = 0; + end + end else if (!aborted) begin + response = reply(frames_o); + miso_o = response[lsb_i?samples : width_bits-1-samples]; + end + end else if (!cs_n_i && have_edge && frame_edges != 0 && + (!aborted || sck_i != cpol_i) && cycle - last_edge > integer'(divider_i)) + faults_o[0] = 1; + previous_sck = sck_i; + previous_cs = cs_n_i; + previous_mosi = mosi_i; + end + end +endmodule diff --git a/tests/rtl/spi_pin_checker_tb.sv b/tests/rtl/spi_pin_checker_tb.sv new file mode 100644 index 00000000..31345e96 --- /dev/null +++ b/tests/rtl/spi_pin_checker_tb.sv @@ -0,0 +1,162 @@ +`timescale 1ns / 1ps +// Synthetic pins test the oracle itself, without a DUT or implementation state. +module spi_pin_checker_tb; + logic clk_i = 0, begin_i = 0, enabled_i = 1, abort_i = 0, pause_i = 0, segment_i = 0; + logic cpol_i = 0, cpha_i = 0, lsb_i = 0, frame16_i = 0; + logic cs_n_i = 1, sck_i = 0, mosi_i = 1, miso_o; + logic [8:0] faults_o; + integer frames_o, edges_o; + always #20.833 clk_i = !clk_i; + spi_pin_bfm u_checker ( + .clk_i (clk_i), + .begin_i (begin_i), + .enabled_i (enabled_i), + .abort_i (abort_i), + .pause_i (pause_i), + .segment_i (segment_i), + .divider_i (16'd2), + .setup_i (16'd2), + .hold_i (16'd2), + .inactive_i (16'd2), + .cpol_i (cpol_i), + .cpha_i (cpha_i), + .lsb_i (lsb_i), + .frame16_i (frame16_i), + .direction_i(2'd2), + .dummy_i (16'hffff), + .cs_n_i (cs_n_i), + .sck_i (sck_i), + .mosi_i (mosi_i), + .miso_o (miso_o), + .frames_o (frames_o), + .edges_o (edges_o), + .faults_o (faults_o) + ); + task automatic tick; + @(negedge clk_i); + #1; + endtask + task automatic drive(input logic cs, sck, mosi); + @(posedge clk_i); + #1; + cs_n_i = cs; + sck_i = sck; + mosi_i = mosi; + tick(); + endtask + task automatic reset_checker; + @(posedge clk_i); + #1; + begin_i = 1; + abort_i = 0; + pause_i = 0; + segment_i = 0; + cs_n_i = 1; + sck_i = cpol_i; + mosi_i = 1; + tick(); + @(posedge clk_i); + #1; + begin_i = 0; + segment_i = 1; + cs_n_i = 0; + tick(); + segment_i = 0; + endtask + task automatic frame_edges(input integer number, input logic data_bit); + for (integer edge_number = 0; edge_number < number; edge_number++) begin + drive(0, sck_i, data_bit); + drive(0, !sck_i, data_bit); + end + endtask + task automatic check(input logic [8:0] expected, input integer case_id); + if (faults_o !== expected) + $fatal(1, "SPI checker selftest case=%0d got=%h expected=%h", case_id, faults_o, expected); + $display("SPI_CHECKER_CASE id=%0d faults=%h", case_id, faults_o); + endtask + initial begin + for (integer mode = 0; mode < 4; mode++) begin + cpol_i = 1'(mode / 2); + cpha_i = 1'(mode % 2); + for (integer width_case = 0; width_case < 2; width_case++) begin + frame16_i = 1'(width_case); + for (integer order = 0; order < 2; order++) begin + lsb_i = 1'(order); + reset_checker(); + frame_edges(frame16_i ? 32 : 16, 1); + drive(0, cpol_i, 1); + drive(1, cpol_i, 1); + check(0, 100 + mode * 4 + width_case * 2 + order); + end + end + end + cpol_i = 0; + cpha_i = 0; + frame16_i = 0; + lsb_i = 0; + reset_checker(); + frame_edges(1, 1); + drive(0, 0, 1); + check(9'h001, 0); + reset_checker(); + frame_edges(1, 1); + repeat (3) drive(0, 1, 1); + check(9'h001, 1); + reset_checker(); + drive(0, 1, 1); + check(9'h002, 2); + reset_checker(); + drive(0, 0, 0); + drive(0, 1, 1); + check(9'h004, 3); + reset_checker(); + frame_edges(15, 1); + drive(0, 1, 1); + drive(1, 1, 1); + check(9'h008, 4); + reset_checker(); + frame_edges(16, 1); + drive(1, 0, 1); + check(9'h010, 5); + reset_checker(); + frame_edges(16, 1); + drive(0, 0, 1); + drive(1, 0, 1); + drive(0, 0, 1); + check(9'h020, 6); + reset_checker(); + frame_edges(2, 1); + abort_i = 1; + drive(0, 0, 1); + drive(0, 1, 1); + check(9'h040, 7); + reset_checker(); + frame_edges(1, 1); + pause_i = 1; + drive(0, 1, 1); + check(9'h080, 8); + reset_checker(); + frame_edges(16, 0); + check(9'h100, 9); + // Normal abort permits the remaining trailing edge, but no new leading edge. + reset_checker(); + frame_edges(1, 1); + abort_i = 1; + drive(0, 1, 1); + drive(0, 0, 1); + drive(0, 0, 1); + drive(1, 0, 1); + check(0, 10); + reset_checker(); + frame_edges(1, 1); + abort_i = 1; + drive(0, 0, 1); + check(9'h001, 11); + $display("SPI_P1_PASS spi_pin_checker_tb"); + $finish; + end + initial begin + #1000000; + $fatal(1, "SIM_TEST_TIMEOUT SPI pin checker"); + end +endmodule diff --git a/tests/rtl/spi_test_body.svh b/tests/rtl/spi_test_body.svh new file mode 100644 index 00000000..4f104300 --- /dev/null +++ b/tests/rtl/spi_test_body.svh @@ -0,0 +1,836 @@ +logic clk_i = 0; +always #20.833 clk_i = !clk_i; // SAFE24, rounded to the fixture's 1 ps precision. +logic rst_n_i = '0; +logic [31:0] clock_hz_i = '0; +logic [ 1:0] origin_i = '0; +logic [ 4:0] request_i = '0; +logic [ 2:0] channel_i = '0; +logic [ 1:0] credit_valid_i = '0; +logic [ 1:0] credit_retire_i = '0; +logic [ 1:0] dma_start_pending_i = '0; +logic [ 3:0] dma_status_i = '0; +logic dma_error_i = '0; +logic [31:0] user_select_i = '0; +logic [31:0] handoff_i = '0; +logic [31:0] alt_enable_i = '0; +logic [31:0] alt_select_i = '0; +logic [31:0] output_enable_i = '0; +logic [31:0] open_drain_i = '0; +logic gpio_ready_i = '0; +logic miso_i; +logic sck_o; +logic mosi_o; +logic cs_n_o; +logic output_enable_o; +logic irq_o; +logic [ 1:0] credit_commit_o; +logic [31:0] paddr_i = 0, pwdata_i = 0, prdata_o; +logic [3:0] pstrb_i = 0; +logic psel_i = 0, penable_i = 0, pwrite_i = 0, pready_o, pslverr_o; +logic model_begin = 0, model_enable = 0, model_abort = 0; +logic model_segment = 0, model_pause = 0; +logic [15:0] model_divider = 1, model_setup = 1, model_hold = 1, model_inactive = 1; +logic [8:0] model_faults; +logic cpol = 0, cpha = 0, lsb = 0, frame16 = 0; +logic [1:0] direction = 0; +integer observed_frames, observed_edges; +logic [31:0] value, expected, word_value; +logic [31:0] random_state; +logic [15:0] dummy_value = 16'hffff; +realtime access_accept_time; +`ifndef SPI_DUT +`define SPI_DUT apb4_spi_block_top +`endif +`SPI_DUT u_dut ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .clock_hz_i (clock_hz_i), + .origin_i (origin_i), + .request_i (request_i), + .channel_i (channel_i), + .credit_valid_i (credit_valid_i), + .credit_retire_i (credit_retire_i), + .dma_start_pending_i(dma_start_pending_i), + .dma_status_i (dma_status_i), + .dma_error_i (dma_error_i), + .user_select_i (user_select_i), + .handoff_i (handoff_i), + .alt_enable_i (alt_enable_i), + .alt_select_i (alt_select_i), + .output_enable_i (output_enable_i), + .open_drain_i (open_drain_i), + .gpio_ready_i (gpio_ready_i), + .miso_i (miso_i), + .sck_o (sck_o), + .mosi_o (mosi_o), + .cs_n_o (cs_n_o), + .output_enable_o (output_enable_o), + .irq_o (irq_o), + .credit_commit_o (credit_commit_o), + .paddr_i (paddr_i), + .pwdata_i (pwdata_i), + .pstrb_i (pstrb_i), + .psel_i (psel_i), + .penable_i (penable_i), + .pwrite_i (pwrite_i), + .prdata_o (prdata_o), + .pready_o (pready_o), + .pslverr_o (pslverr_o) +); +spi_pin_bfm u_model ( + .divider_i (model_divider), + .setup_i (model_setup), + .hold_i (model_hold), + .inactive_i (model_inactive), + .segment_i (model_segment), + .pause_i (model_pause), + .faults_o (model_faults), + .dummy_i (dummy_value), + .clk_i (clk_i), + .begin_i (model_begin), + .enabled_i (model_enable), + .abort_i (model_abort), + .cpol_i (cpol), + .cpha_i (cpha), + .lsb_i (lsb), + .frame16_i (frame16), + .direction_i(direction), + .cs_n_i (cs_n_o), + .sck_i (sck_o), + .mosi_i (mosi_o), + .miso_o (miso_i), + .frames_o (observed_frames), + .edges_o (observed_edges) +); +// Registered-APB first access edge is the side-effect edge. Observe only +// public bus signals; response completion remains checked by access(). +always @(posedge clk_i) begin + model_segment <= 0; + if (!rst_n_i || !model_enable) model_abort <= 0; + else begin + if (psel_i && penable_i && !pready_o && pwrite_i && paddr_i == 32'h18) begin + if (pwdata_i == 8) model_segment <= 1; + if (pwdata_i == 32) model_abort <= 1; + end + if (!model_begin && model_faults != 0) + $fatal(1, "SPI pin checker faults=%h edges=%0d", model_faults, observed_edges); + end +end +task automatic access (input logic write_access, input logic [11:0] offset, input logic [31:0] data, + input logic [3:0] strobe, input logic error_expected, + output logic [31:0] result_data); + @(negedge clk_i); + psel_i = 1; + penable_i = 0; + pwrite_i = write_access; + paddr_i = {20'd0, offset}; + pwdata_i = data; + pstrb_i = strobe; + @(negedge clk_i); + penable_i = 1; + for (integer timeout_cycles = 0; !pready_o; timeout_cycles++) begin + if (timeout_cycles == 3) $fatal(1, "SPI APB response exceeded local bound"); + @(negedge clk_i); + end + if (pslverr_o !== error_expected) + $fatal( + 1, "SPI APB offset=%h data=%h error=%b expected=%b", offset, data, pslverr_o, error_expected + ); + result_data = prdata_o; + @(posedge clk_i); + access_accept_time = $realtime; + @(negedge clk_i); + psel_i = 0; + penable_i = 0; + @(negedge clk_i); +endtask +task automatic wr(input logic [11:0] offset, input logic [31:0] data, input logic err); + logic [31:0] unused_data; + access (1, offset, data, 4'hf, err, unused_data); + if (!err) begin + case (offset) + 12'h024: model_divider = data[15:0]; + 12'h028: model_setup = data[15:0]; + 12'h02c: model_hold = data[15:0]; + 12'h030: model_inactive = data[15:0]; + default: ; + endcase + end +endtask +task automatic rd(input logic [11:0] offset, output logic [31:0] data); + access (0, offset, 0, 0, 0, data); +endtask +task automatic reset_dut; + model_enable = 0; + model_abort = 0; + model_pause = 0; + model_divider = 1; + model_setup = 1; + model_hold = 1; + model_inactive = 1; + rst_n_i = 0; + origin_i = 0; + request_i = 0; + channel_i = 0; + credit_valid_i = 0; + credit_retire_i = 0; + dma_status_i = 0; + dma_start_pending_i = 0; + dma_error_i = 0; + user_select_i = 0; + handoff_i = 0; + open_drain_i = 0; + alt_enable_i = 32'hd8000000; + alt_select_i = 32'hd0000000; + output_enable_i = 0; + gpio_ready_i = 1; + clock_hz_i = 24000000; + repeat (3) @(negedge clk_i); + rst_n_i = 1; + repeat (3) @(negedge clk_i); +endtask +task automatic wait_state(input integer state_value); + integer cycles; + logic reached; + cycles = 0; + reached = 0; + while (!reached && cycles < 3000000) begin + rd(12'h01c, value); + reached = value[3:0] == 4'(state_value); + cycles++; + end + if (!reached) $fatal(1, "SPI state timeout wanted=%0d", state_value); +endtask +task automatic begin_model; + @(posedge clk_i); + model_begin = 1; + model_enable = 1; + @(posedge clk_i); + model_begin = 0; +endtask +task automatic segment_test(input integer count, input integer divider); + integer words; + $display("SPI_P1_CASE time=%0t mode=%0d%0d width16=%0d order=%0d dir=%0d count=%0d div=%0d", + $time, cpol, cpha, frame16, lsb, direction, count, divider); + reset_dut(); + wr(12'h020, {27'd0, 1'b0, (direction != 0), lsb, cpha, cpol}, 0); + wr(12'h024, 32'(divider), 0); + wr(12'h034, {29'd0, direction, frame16}, 0); + wr(12'h038, 32'(count), 0); + wr(12'h03c, {16'd0, dummy_value}, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + words = (count * (frame16 ? 2 : 1) + 3) / 4; + if (direction != 2) + for (integer index = 0; index < words; index++) + wr(12'h04c, 32'h80ff017e ^ (32'(index) * 32'h01020408), 0); + begin_model(); + wr(12'h018, 8, 0); + wait_state(1); + if (observed_frames != count || observed_edges != 2 * count * (frame16 ? 16 : 8)) + $fatal(1, "SPI edge/frame mismatch: %0d/%0d", observed_frames, observed_edges); + if (direction != 0) begin + rd(12'h01c, value); + if (!value[6] || value[7]) $fatal(1, "RX data lost pending ownership"); + wr(12'h018, 4, 1); + for (integer index = 0; index < words; index++) begin + expected = 0; + for (integer lane = 0; lane < (frame16 ? 2 : 4); lane++) begin + if (index * (frame16 ? 2 : 4) + lane < count) + expected |= (32'(16'ha5c3^(16'(index*(frame16?2:4)+lane)*16'h1237)) & + (frame16?32'hffff:32'hff)) << (lane*(frame16?16:8)); + end + rd(12'h050, word_value); + if (word_value !== expected) + $fatal(1, "SPI RX packing got=%h expected=%h", word_value, expected); + end + end + rd(12'h01c, value); + if (value[6] || !value[7]) $fatal(1, "SPI cleanup failed"); + rd(12'h074, value); + if (value != 32'(count)) $fatal(1, "SPI wrong completed frame count"); + rd(12'h060, value); + if (value[1:0] != 2'b11) $fatal(1, "SPI missing wire completion events"); + model_enable = 0; +endtask +task automatic wait_pressure(input integer status_bit); + integer attempts, saved_edges; + logic reached; + logic [31:0] status_value, before_stalls, after_stalls; + attempts = 0; + reached = 0; + while (!reached && attempts < 20000) begin + rd(12'h01c, status_value); + reached = status_value[status_bit]; + attempts++; + end + if (!reached) $fatal(1, "SPI pressure state not reached bit=%0d", status_bit); + model_pause = 1; + saved_edges = observed_edges; + rd(status_bit == 8 ? 12'h088 : 12'h08c, before_stalls); + repeat (31) @(negedge clk_i); + rd(status_bit == 8 ? 12'h088 : 12'h08c, after_stalls); + if (cs_n_o || sck_o != cpol || observed_edges != saved_edges || + saved_edges % (frame16 ? 32 : 16) != 0 || after_stalls - before_stalls < 31) + $fatal(1, "SPI pressure did not pause at a complete frame boundary"); + $display("SPI_PRESSURE_WAIT bit=%0d edges=%0d cycles=%0d", status_bit, saved_edges, + after_stalls - before_stalls); +endtask + +task automatic checked_abort; + integer bound_cycles, cycles, saved_frames; + realtime accepted; + logic [31:0] events_value, frames_value; + saved_frames = observed_frames; + wr(12'h064, 4, 0); + wr(12'h060, 63, 0); + wr(12'h018, 32, 0); + accepted = access_accept_time; + bound_cycles = 2 * integer'(model_divider) + integer'(model_hold) + integer'(model_inactive) + 2; + cycles = 0; + while (!irq_o && cycles < bound_cycles) begin + @(negedge clk_i); + cycles++; + end + if (!irq_o || $realtime - accepted > real'(bound_cycles) * 41.666 + 0.01 || + !cs_n_o || sck_o != cpol) + $fatal(1, "SPI pressure/phase abort deadline"); + rd(12'h060, events_value); + rd(12'h074, frames_value); + if (events_value[3:0] != 4 || observed_frames != saved_frames || + frames_value != 32'(saved_frames)) + $fatal(1, "SPI aborted transfer changed successful frame/event counts"); + wr(12'h018, 64, 0); + rd(12'h01c, events_value); + if (!events_value[5] || events_value[6] || output_enable_o) + $fatal(1, "SPI pressure recovery lost reservation or retained data"); + model_pause = 0; + model_enable = 0; +endtask + +task automatic pressure_test(input integer abort_kind); + integer count, words, polls; + logic [31:0] levels, received_word, expected_word, status_value; + count = frame16 ? 19 : 37; + words = 10; + reset_dut(); + wr(12'h020, {27'd0, 1'b0, (direction != 0), lsb, cpha, cpol}, 0); + wr(12'h024, 1 + 2 * 32'(cpol) + 4 * 32'(cpha), 0); + wr(12'h034, {29'd0, direction, frame16}, 0); + wr(12'h038, 32'(count), 0); + wr(12'h03c, {16'd0, dummy_value}, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + if (direction != 2) wr(12'h04c, 32'h80ff017e, 0); + begin_model(); + wr(12'h018, 8, 0); + if (direction != 2) begin + wait_pressure(8); + if (abort_kind != 1) begin + model_pause = 0; + for (integer index = 1; index < words; index++) begin + rd(12'h05c, levels); + polls = 0; + while (levels[3:0] == 8 && polls < 20000) begin + rd(12'h05c, levels); + polls++; + end + if (levels[3:0] == 8) $fatal(1, "SPI TX service did not resume"); + wr(12'h04c, 32'h80ff017e ^ (32'(index) * 32'h01020408), 0); + end + end + end + if (abort_kind == 1) checked_abort(); + else begin + if (direction != 0) begin + wait_pressure(9); + rd(12'h05c, levels); + if (levels[11:8] != 8) $fatal(1, "SPI RX pause did not reach a full FIFO"); + end + if (abort_kind == 2) checked_abort(); + else begin + model_pause = 0; + if (direction != 0) begin + for (integer index = 0; index < words; index++) begin + rd(12'h05c, levels); + polls = 0; + while (levels[11:8] == 0 && polls < 20000) begin + rd(12'h05c, levels); + polls++; + end + if (levels[11:8] == 0) $fatal(1, "SPI RX service did not resume"); + rd(12'h050, received_word); + expected_word = 0; + for (integer lane = 0; lane < (frame16 ? 2 : 4); lane++) begin + if (index * (frame16 ? 2 : 4) + lane < count) + expected_word |= (32'(16'ha5c3 ^ (16'(index * (frame16 ? 2 : 4) + lane) * + 16'h1237)) & (frame16 ? 32'hffff : 32'hff)) << + (lane * (frame16 ? 16 : 8)); + end + if (received_word !== expected_word) $fatal(1, "SPI pressure RX data/tail mismatch"); + repeat (23 + index % 3) @(negedge clk_i); + end + end + wait_state(1); + rd(12'h01c, status_value); + if (status_value[6] || !status_value[7] || observed_frames != count || + observed_edges != count * (frame16 ? 32 : 16)) + $fatal(1, "SPI pressure cleanup"); + rd(12'h074, status_value); + if (status_value != 32'(count)) $fatal(1, "SPI pressure frame accounting"); + rd(12'h07c, status_value); + if (status_value != (direction == 2 ? 0 : 10)) $fatal(1, "SPI pressure TX accounting"); + rd(12'h080, status_value); + if (status_value != (direction == 0 ? 0 : 10)) $fatal(1, "SPI pressure RX production"); + rd(12'h084, status_value); + if (status_value != (direction == 0 ? 0 : 10)) $fatal(1, "SPI pressure RX removal"); + rd(12'h060, status_value); + if (status_value[3:0] != 3) $fatal(1, "SPI pressure terminal events"); + model_enable = 0; + end + end + $display( + "SPI_PRESSURE_CASE mode=%0d%0d width16=%0d order=%0d dir=%0d abort=%0d frames=%0d words=10", + cpol, cpha, frame16, lsb, direction, abort_kind, count); +endtask + +task automatic phase_abort_test(input integer delay_cycles); + reset_dut(); + direction = 0; + wr(12'h020, {29'd0, lsb, cpha, cpol}, 0); + wr(12'h024, 7, 0); + wr(12'h034, {31'd0, frame16}, 0); + wr(12'h038, 5, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + wr(12'h04c, 32'h80ff017e, 0); + begin_model(); + wr(12'h018, 8, 0); + while (sck_o == cpol) @(negedge clk_i); + repeat (delay_cycles) @(negedge clk_i); + checked_abort(); + $display("SPI_PHASE_ABORT mode=%0d%0d width16=%0d delay=%0d", cpol, cpha, frame16, delay_cycles); +endtask + +initial begin + if (TestKind == 0 || TestKind == 3) begin + for (integer mode = 0; mode < 4; mode++) + for (integer width_case = 0; width_case < 2; width_case++) + for (integer order = 0; order < 2; order++) + for (integer dir = 0; dir < 3; dir++) begin + cpol = 1'(mode / 2); + cpha = 1'(mode % 2); + frame16 = 1'(width_case); + lsb = 1'(order); + direction = 2'(dir); + segment_test(5, 1 + mode); + end + cpol = 0; + cpha = 0; + frame16 = 0; + lsb = 0; + direction = 1; + if (TestKind == 0) begin + segment_test(1, 65535); + // Fixed seed; the oracle still decodes pins independently of the DUT. + random_state = 32'h53504931; + for (integer sample_index = 0; sample_index < 256; sample_index++) begin + random_state = (random_state >> 1) ^ ((random_state[0] != 0) ? 32'h80200003 : 32'd0); + cpol = random_state[0]; + cpha = random_state[1]; + lsb = random_state[2]; + frame16 = random_state[3]; + direction = 2'(random_state[5:4] % 3); + dummy_value = random_state[31:16]; + segment_test(1 + integer'(random_state[9:6]), 1 + integer'(random_state[11:10])); + end + end + // Complete-core stress also runs against each PDK's mapped netlist. + for (integer mode = 0; mode < 4; mode++) + for (integer width_case = 0; width_case < 2; width_case++) + for (integer order = 0; order < 2; order++) + for (integer dir = 0; dir < 3; dir++) begin + cpol = 1'(mode / 2); + cpha = 1'(mode % 2); + frame16 = 1'(width_case); + lsb = 1'(order); + direction = 2'(dir); + pressure_test(0); + end + for (integer mode = 0; mode < 4; mode++) + for (integer width_case = 0; width_case < 2; width_case++) begin + cpol = 1'(mode / 2); + cpha = 1'(mode % 2); + frame16 = 1'(width_case); + lsb = 0; + direction = 0; + pressure_test(1); + direction = 2; + pressure_test(2); + for (integer delay_cycles = 0; delay_cycles < 14; delay_cycles++) + phase_abort_test(delay_cycles); + end + end else if (TestKind == 1) begin + reset_dut(); + rd(12'h000, value); + if (value != 32'h53504930) $fatal(1, "SPI ID"); + rd(12'h008, value); + if (value != 32'h3f7f) $fatal(1, "SPI capability"); + wr(12'h064, 256, 0); + if (!irq_o) $fatal(1, "SPI low-watermark level missing"); + wr(12'h060, 63, 0); + if (!irq_o) $fatal(1, "SPI W1C incorrectly cleared watermark level"); + wr(12'h064, 0, 0); + wr(12'h000, 0, 1); + wr(12'h024, 0, 1); + wr(12'h020, 24, 1); + wr(12'h038, 0, 1); + wr(12'h03c, 32'h10000, 1); + wr(12'h018, 3, 1); + wr(12'h018, 8, 1); + access (1, 12'h020, 0, 4'h1, 1, value); + access (0, 12'h001, 0, 0, 1, value); + access (0, 12'h100, 0, 0, 1, value); + access (0, 12'h018, 0, 0, 1, value); + origin_i = 2; + access (0, 12'h000, 0, 0, 1, value); + origin_i = 0; + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 1); // Reset FRAME_COUNT is still unconfigured. + for (integer width_case = 0; width_case < 2; width_case++) begin + wr(12'h034, 32'(width_case), 0); + wr(12'h038, 32'hffffffff, 0); + wr(12'h018, 4, 1); + wr(12'h038, width_case == 0 ? 32'hfffffffd : 32'h7fffffff, 0); + wr(12'h018, 4, 1); + end + wr(12'h034, 0, 0); + wr(12'h020, 0, 1); + wr(12'h038, 32'hffffffff, 0); + wr(12'h018, 4, 1); + wr(12'h038, 40, 0); + wr(12'h018, 4, 0); + for (integer index = 0; index < 8; index++) wr(12'h04c, 32'(index), 0); + wr(12'h04c, 32'hdeadbeef, 1); + rd(12'h07c, value); + if (value != 8) $fatal(1, "SPI FIFO quota changed on error"); + wr(12'h054, 0, 1); + access (0, 12'h050, 0, 0, 1, value); + wr(12'h018, 32, 0); + wait_state(8); + repeat (8) @(negedge clk_i); + wr(12'h018, 64, 0); + rd(12'h01c, value); + if (!value[5] || value[3:0] != 0) $fatal(1, "SPI recovery ownership"); + wr(12'h018, 128, 1); + alt_enable_i = 0; + output_enable_i = 0; + wr(12'h018, 128, 0); + wr(12'h018, 256, 0); + reset_dut(); + wr(12'h038, 1, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + wr(12'h018, 8, 0); + repeat (4) @(negedge clk_i); + wr(12'h060, 16, 0); + rd(12'h060, value); + if (!value[4]) $fatal(1, "SPI event did not dominate W1C"); + wr(12'h018, 32, 0); + repeat (8) @(negedge clk_i); + wr(12'h018, 64, 0); + // A source-qualified credit is consumed only once, and CPU cannot steal it. + reset_dut(); + wr(12'h040, 32'h80000003, 0); + wr(12'h038, 8, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + credit_valid_i = 1; + wr(12'h054, 32'h80ff017e, 1); + wr(12'h04c, 32'h80ff017e, 1); + origin_i = 1; + request_i = 16; + channel_i = 3; + wr(12'h054, 32'h80ff017e, 0); + credit_retire_i = 1; + @(negedge clk_i); + credit_retire_i = 0; + wr(12'h054, 32'h81fd0576, 0); + wr(12'h054, 32'hdeadbeef, 1); + origin_i = 0; + credit_valid_i = 0; + credit_retire_i = 1; + repeat (8) @(negedge clk_i); + credit_retire_i = 0; + rd(12'h07c, value); + if (value != 2) $fatal(1, "SPI duplicate credit mutated FIFO"); + rd(12'h06c, value); + if (value != 4) $fatal(1, "SPI credit fault not latched"); + wr(12'h018, 64, 0); + // A consumed credit remains transport-busy until its terminal retirement. + reset_dut(); + wr(12'h040, 32'h80000003, 0); + wr(12'h038, 1, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + origin_i = 1; + request_i = 16; + channel_i = 3; + credit_valid_i = 1; + wr(12'h054, 32'h80ff017e, 0); + origin_i = 0; + credit_valid_i = 0; + wr(12'h018, 8, 0); + wait_state(1); + rd(12'h01c, value); + if (!value[6] || value[7] || value[11]) $fatal(1, "SPI retired credit too early"); + wr(12'h018, 4, 1); + wr(12'h040, 0, 1); + credit_retire_i = 1; + @(negedge clk_i); + credit_retire_i = 0; + repeat (2) @(negedge clk_i); + rd(12'h01c, value); + if (value[6] || !value[7] || !value[11]) + $fatal(1, "SPI credit retirement did not clean segment"); + for (integer width_case = 0; width_case < 2; width_case++) begin + reset_dut(); + wr(12'h034, 32'(width_case), 0); + wr(12'h038, width_case == 0 ? 32'hfffffffc : 32'h7ffffffe, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + rd(12'h09c, value); + if (value != 32'hfffffffc) $fatal(1, "SPI maximum payload arithmetic"); + rd(12'h078, value); + if (value != 32'h3fffffff) $fatal(1, "SPI maximum quota arithmetic"); + wr(12'h018, 32, 0); + repeat (8) @(negedge clk_i); + wr(12'h018, 64, 0); + end + end else begin + for (integer target = 0; target < 8; target++) begin + integer bound_cycles; + realtime abort_accepted; + reset_dut(); + wr(12'h024, 4, 0); + if (target == 3) wr(12'h028, 32, 0); + if (target == 5) wr(12'h02c, 32, 0); + if (target == 7) wr(12'h030, 32, 0); + wr(12'h064, 4, 0); + wr(12'h034, target == 6 ? 8 : 0, 0); + wr(12'h038, 1, 0); + wr(12'h018, 1, 0); + if (target != 7) wait_state(1); + if (target == 0) wr(12'h018, 2, 0); + if (target >= 2 && target <= 6) wr(12'h018, 4, 0); + if (target >= 3 && target <= 6) begin + wr(12'h04c, 32'h80ff017e, 0); + wr(12'h018, 8, 0); + end + wait_state(target); + wr(12'h060, 63, 0); + wr(12'h018, 32, 0); + abort_accepted = access_accept_time; + bound_cycles = 8 + (target == 5 ? 32 : 1) + (target == 7 ? 32 : 1) + 2; + for (integer cycles = 0; cycles < bound_cycles && !irq_o; cycles++) @(negedge clk_i); + if (!irq_o || $realtime - abort_accepted > real'(bound_cycles) * 41.666 + 0.01) + $fatal(1, "SPI ABORT deadline state=%0d", target); + if (!cs_n_o || sck_o) $fatal(1, "SPI ABORT wire state=%0d", target); + if (target == 0 && output_enable_o) $fatal(1, "SPI ABORT re-enabled disabled outputs"); + wr(12'h018, 64, 0); + end + for (integer abort_delay = 0; abort_delay < 40; abort_delay++) begin + reset_dut(); + wr(12'h038, 5, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + wr(12'h04c, 32'habcdef01, 0); + wr(12'h018, 8, 0); + repeat (abort_delay) @(negedge clk_i); + wr(12'h018, 32, 0); + repeat (8) @(negedge clk_i); + if (!cs_n_o || sck_o) $fatal(1, "SPI abort wire bound"); + rd(12'h060, value); + if (value[1:0] != 0 || !value[2]) $fatal(1, "SPI aborted segment reported success"); + dma_status_i = 1; + wr(12'h018, 64, 1); + dma_status_i = 0; + wr(12'h018, 64, 0); + end + // Maximum CS delays are exercised on pins, not only read back from CSRs. + reset_dut(); + wr(12'h038, 1, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + wr(12'h018, 32, 0); + gpio_ready_i = 0; + repeat (3) @(negedge clk_i); + rd(12'h06c, value); + if (value != 2) $fatal(1, "SPI closing did not monitor GPIO readiness"); + if (output_enable_o || !cs_n_o) $fatal(1, "SPI closing guard clamp"); + wr(12'h018, 64, 0); + rd(12'h01c, value); + if (value[3:0] != 0 || !value[5]) $fatal(1, "SPI persistent closing guard recovery"); + // Maximum CS delays are exercised on pins, not only read back from CSRs. + begin + realtime asserted_at, first_edge, last_edge, released_at; + reset_dut(); + cpol = 0; + cpha = 0; + lsb = 0; + frame16 = 0; + direction = 0; + for (integer offset = 40; offset <= 48; offset += 4) begin + wr(12'(offset), 0, 1); + wr(12'(offset), 65535, 0); + end + wr(12'h038, 1, 0); + wr(12'h018, 1, 0); + wait_state(1); + rd(12'h060, value); + if (value[1:0] != 0) $fatal(1, "SPI initial gap emitted DONE"); + wr(12'h018, 4, 0); + wr(12'h04c, 32'h80ff017e, 0); + begin_model(); + fork + begin + @(negedge cs_n_o); + asserted_at = $realtime; + @(posedge sck_o); + first_edge = $realtime; + if (first_edge - asserted_at < 65535.0 * 41.666 - 0.01) + $fatal(1, "SPI short maximum setup"); + repeat (7) @(posedge sck_o); + @(negedge sck_o); + last_edge = $realtime; + @(posedge cs_n_o); + released_at = $realtime; + if (released_at - last_edge < 65535.0 * 41.666 - 0.01) + $fatal(1, "SPI short maximum hold"); + end + begin + wr(12'h018, 8, 0); + wait_state(1); + end + join + if ($realtime - released_at < 65535.0 * 41.666 - 0.01) + $fatal(1, "SPI short maximum inactive gap"); + model_enable = 0; + end + // Held-CS transitions preserve mode/timing and wait for segment cleanup. + reset_dut(); + wr(12'h038, 3, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + wr(12'h04c, 32'h80ff017e, 0); + wr(12'h018, 8, 0); + wait_state(4); + rst_n_i = 0; + #1; + if (output_enable_o || !cs_n_o) $fatal(1, "SPI asynchronous reset did not clamp pads"); + repeat (2) @(negedge clk_i); + rst_n_i = 1; + rd(12'h01c, value); + if (value[6:0] != 0) $fatal(1, "SPI reset retained a traffic epoch"); + rd(12'h05c, value); + if (value != 0) $fatal(1, "SPI reset retained FIFO validity"); + // Held-CS transitions preserve mode/timing and wait for segment cleanup. + reset_dut(); + cpol = 0; + cpha = 0; + lsb = 0; + frame16 = 0; + direction = 0; + wr(12'h034, 8, 0); + wr(12'h038, 1, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + wr(12'h04c, 32'h80ff017e, 0); + begin_model(); + wr(12'h018, 8, 0); + wait_state(6); + model_enable = 0; + if (cs_n_o || observed_frames != 1 || observed_edges != 16) $fatal(1, "SPI held command"); + wr(12'h024, 2, 1); + wr(12'h034, 9, 0); + wr(12'h038, 1, 0); + frame16 = 1; + wr(12'h018, 4, 0); + wr(12'h04c, 32'h80ff017e, 0); + begin_model(); + wr(12'h018, 8, 0); + wait_state(6); + model_enable = 0; + if (cs_n_o || observed_frames != 1 || observed_edges != 32) + $fatal(1, "SPI held frame-width switch"); + wr(12'h018, 16, 0); + wait_state(1); + wr(12'h018, 2, 0); + rd(12'h01c, value); + if (!value[5] || output_enable_o) $fatal(1, "SPI DISABLE reservation"); + reset_dut(); + wr(12'h038, 1, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + handoff_i = 32'h08000000; + repeat (3) @(negedge clk_i); + if (output_enable_o || !cs_n_o) $fatal(1, "SPI guard clamp"); + rd(12'h06c, value); + if (value != 1) $fatal(1, "SPI first fault"); + wr(12'h018, 64, 0); + repeat (3) @(negedge clk_i); + rd(12'h01c, value); + if (value[3:0] != 0 || !value[5]) $fatal(1, "SPI repeated guard fault"); + // Display D/C is ordinary GPIO output; an invalid electrical mode closes ownership. + reset_dut(); + alt_enable_i[30] = 0; + output_enable_i[30] = 1; + wr(12'h020, 16, 0); + wr(12'h018, 1, 0); + wait_state(1); + rd(12'h090, value); + if (value != 32'hd8000000) $fatal(1, "SPI display claim mask"); + open_drain_i[30] = 1; + repeat (3) @(negedge clk_i); + if (output_enable_o) $fatal(1, "SPI display electrical guard"); + wr(12'h018, 64, 0); + end + if (TestKind == 3) begin + reset_dut(); + wr(12'h024, 0, 1); + wr(12'h038, 1, 0); + wr(12'h018, 1, 0); + wait_state(1); + wr(12'h018, 4, 0); + wr(12'h018, 8, 0); + wait_state(4); + wr(12'h018, 32, 0); + repeat (8) @(negedge clk_i); + if (!cs_n_o || sck_o) $fatal(1, "SPI netlist abort bound"); + wr(12'h018, 64, 0); + alt_enable_i = 0; + output_enable_i = 0; + wr(12'h018, 128, 0); + rd(12'h01c, value); + if (value[5]) $fatal(1, "SPI netlist release"); + end + $display("SPI_P1_PASS %s", TestName); + $finish; +end +initial begin + #100000000; + $fatal(1, "SIM_TEST_TIMEOUT SPI fixture"); +end diff --git a/tests/test_spi_p1.py b/tests/test_spi_p1.py new file mode 100644 index 00000000..d9dc4d59 --- /dev/null +++ b/tests/test_spi_p1.py @@ -0,0 +1,233 @@ +"""SPI-P1 evidence must fail closed on command, marker and input drift.""" +import json +from pathlib import Path +import shutil + +import pytest + +from scripts import spi_p1 as spi + + +@pytest.mark.parametrize('suffix', ['FAILED', 'FATAL', 'assertion failed', '%Error', + 'SIM_TEST_FAIL', 'SIM_TEST_TIMEOUT']) +def test_spi_rejects_error_after_success(suffix): + with pytest.raises(ValueError): + spi.require_pass(f'SPI_P1_PASS case\n{suffix}', + {'status': 'passed', 'exit_code': 0}, 'SPI_P1_PASS case') + + +@pytest.mark.parametrize('result', [{'status': 'failed', 'exit_code': 0}, + {'status': 'passed', 'exit_code': 1}]) +def test_spi_rejects_failed_command(result): + with pytest.raises(ValueError): + spi.require_pass('SPI_P1_PASS case', result, 'SPI_P1_PASS case') + + +def test_spi_requires_exactly_one_terminal(): + result = {'status': 'passed', 'exit_code': 0} + spi.require_pass('SPI_P1_PASS case', result, 'SPI_P1_PASS case') + for text in ('', 'SPI_P1_PASS case\nSPI_P1_PASS case'): + with pytest.raises(ValueError): + spi.require_pass(text, result, 'SPI_P1_PASS case') + + +def test_spi_retained_evidence_requires_same_source_and_artifact(tmp_path, monkeypatch): + tool = tmp_path / 'tool' + tool.write_text('original executable') + external = {str(tool): spi.digest(tool)} + monkeypatch.setattr(spi, 'external_inputs', lambda pdk: external) + log = tmp_path / 'log' + log.write_text('original') + record = tmp_path / 'record.json' + record.write_text(json.dumps({'status': 'passed', 'source': {'rtl': 'one'}, + 'tool_input_schema': spi.TOOL_INPUT_SCHEMA, + 'manifest': {'configuration': {'PDK': 'ICS55'}}, + 'external_inputs': external, + 'artifacts': {str(log): spi.digest(log)}})) + assert spi.checked_record(record, {'rtl': 'one'})['status'] == 'passed' + with pytest.raises(ValueError): + spi.checked_record(record, {'rtl': 'two'}) + log.write_text('changed') + with pytest.raises(ValueError): + spi.checked_record(record, {'rtl': 'one'}) + + +def test_spi_input_inventory_includes_headers_and_oracle(): + sources = spi.inputs() + for name in ('rtl/ip/serial/spi_define.svh', 'tests/rtl/spi_pin_bfm.sv', + 'tests/rtl/spi_test_body.svh', 'dependencies/dependencies.lock.json', + 'crt/include/retrosoc/hal/spi_regs.h'): + assert name in sources and (spi.ROOT / Path(name)).is_file() + + +def test_yosys_timestamp_prefix_is_not_json_corruption(tmp_path): + report = tmp_path / 'stat.json' + report.write_text('[00001.123456] {\n[00001.123457] "modules": {}\n[00001.123458] }\n') + assert spi.yosys_json(report) == {'modules': {}} + report.write_text('[00001.123456] {"modules": broken}') + with pytest.raises(ValueError): + spi.yosys_json(report) + + +@pytest.mark.parametrize('mode', ['prove', 'bmc', 'cover']) +def test_empty_formal_model_cannot_pass(mode): + with pytest.raises(ValueError): + spi.formal_model_counts('; no properties\n', mode) + keyword = 'cover' if mode == 'cover' else 'assert' + assert spi.formal_model_counts(f'; yosys-smt2-{keyword} 0 property\n', mode)[keyword] == 1 + + +def test_short_cover_witness_cannot_qualify_the_full_window(tmp_path): + witness=tmp_path/'trace0.yw' + witness.write_text(json.dumps({'steps': [{}]*10})) + with pytest.raises(ValueError): + spi.cover_witness_steps(tmp_path) + witness.write_text(json.dumps({'steps': [{}]*193})) + assert spi.cover_witness_steps(tmp_path)==193 + + +@pytest.mark.parametrize('drift', ['none', 'model', 'consumed', 'config', 'tool', + 'artifact', 'command', 'status', 'vacuous', 'source', 'inventory', + 'old_schema', 'missing_tool']) +def test_formal_reuse_requires_exact_successful_producer(tmp_path, monkeypatch, drift): + previous, current = tmp_path / 'old', tmp_path / 'new' + previous.mkdir() + current.mkdir() + (previous / 'bmc/src').mkdir(parents=True) + (previous / 'bmc/model').mkdir() + for root in (previous, current): + (root / 'bmc.v').write_text('module spi_formal; endmodule\n') + (root / 'bmc.sby').write_text(f'[options]\nmode bmc\ndepth 192\n[files]\ndesign.v {root}/bmc.v\n') + shutil.copyfile(previous / 'bmc.v', previous / 'bmc/src/design.v') + shutil.copyfile(previous / 'bmc.sby', previous / 'bmc/config.sby') + (previous / 'bmc/status').write_text('PASS 0 192\n') + (previous / 'bmc/model/design_smt2.smt2').write_text('; yosys-smt2-assert 0 property\n') + (previous / 'bmc.json').write_text(json.dumps({'status': 'passed', 'exit_code': 0})) + (previous / 'bmc.log').write_text('original producer log') + tool = tmp_path / 'tool' + tool.write_text('tool') + external = {str(tool): spi.digest(tool)} + source = {'rtl.sv': 'rtl', 'scripts/spi_p1.py': 'old-driver'} + monkeypatch.setattr(spi, 'inputs', lambda: {**source, 'scripts/spi_p1.py': 'new-driver'}) + if drift == 'command': + (previous / 'bmc.json').write_text(json.dumps({'status': 'failed', 'exit_code': 1})) + if drift == 'status': + (previous / 'bmc/status').write_text('FAIL\n') + if drift == 'vacuous': + (previous / 'bmc/model/design_smt2.smt2').write_text('; no properties\n') + if drift == 'consumed': + (previous / 'bmc/src/design.v').write_text('different consumed model') + record = {'phase': 'SPI-P1', 'stage': 'formal', 'status': 'failed', + 'tool_input_schema': spi.TOOL_INPUT_SCHEMA, + 'source': dict(source), 'manifest': {'producer': 'original'}, + 'external_inputs': external, + 'artifacts': {str(p): spi.digest(p) for p in previous.rglob('*') if p.is_file()}} + if drift == 'old_schema': + record.pop('tool_input_schema') + if drift == 'missing_tool': + record['external_inputs'] = {} + if drift == 'source': + record['source']['rtl.sv'] = 'different' + if drift == 'inventory': + record['artifacts'].pop(str(previous / 'bmc/src/design.v')) + (previous / 'record.json').write_text(json.dumps(record)) + if drift == 'tool': + tool.write_text('changed tool') + if drift == 'model': + (current / 'bmc.v').write_text('changed model') + if drift == 'config': + config = current / 'bmc.sby' + config.write_text(config.read_text().replace('depth 192', 'depth 1')) + if drift == 'artifact': + (previous / 'bmc.log').write_text('changed log') + if drift == 'none': + retained = spi.reuse_formal_task(previous, current, 'bmc', 'bmc', external) + assert str(previous / 'record.json') in retained + reuse = json.loads((current / 'bmc-reuse.json').read_text()) + assert reuse['producer_source']['scripts/spi_p1.py'] == 'old-driver' + assert reuse['phase_accepted'] is False + else: + with pytest.raises(ValueError): + spi.reuse_formal_task(previous, current, 'bmc', 'bmc', external) + + +@pytest.fixture +def proof_installation(tmp_path): + paths = {} + for tool in ('yosys', 'yosys-smtbmc', 'yosys-abc', 'sby'): + root = tmp_path / ('sby' if tool == 'sby' else 'yosys') + executable = root / 'bin' / tool + executable.parent.mkdir(parents=True, exist_ok=True) + executable.write_text(f'original {tool}') + paths[tool] = executable + for owner, modules in [('sby', ('sby_core', 'sby_engine_smtbmc', 'sby_cmdline', 'sby_mode_bmc', + 'sby_mode_cover', 'sby_mode_prove', 'sby_design', 'sby_jobserver', + 'sby_status')), ('yosys', ('smtio', 'ywio'))]: + directory = tmp_path / owner / 'share/yosys/python3' + directory.mkdir(parents=True) + for name in modules: + (directory / f'{name}.py').write_text(f'# original {name}') + return paths + + +@pytest.mark.parametrize('component', ['yosys-smtbmc', 'yosys-abc', 'sby_engine_smtbmc.py', + 'sby_core.py', 'smtio.py', 'ywio.py']) +def test_helper_drift_changes_fingerprint(proof_installation, component): + before = spi.proof_tool_components(proof_installation) + path = next(Path(name) for name in before if Path(name).name == component) + path.write_text('changed component without changing launcher') + after = spi.proof_tool_components(proof_installation) + assert before != after and before[str(path)] != after[str(path)] + + +@pytest.mark.parametrize('component', ['sby_engine_smtbmc.py', 'smtio.py', 'ywio.py']) +def test_missing_helper_fails_closed(proof_installation, component): + before = spi.proof_tool_components(proof_installation) + next(Path(name) for name in before if Path(name).name == component).unlink() + with pytest.raises(ValueError, match='incomplete Python tool installation'): + spi.proof_tool_components(proof_installation) + + +def test_module_search_shadow_is_rejected(proof_installation): + (proof_installation['sby'].parent / 'sby_core.py').write_text('# shadow') + with pytest.raises(ValueError, match='shadowed'): + spi.proof_tool_components(proof_installation) + + +def test_mixed_yosys_helper_is_rejected(proof_installation, tmp_path): + alternate = tmp_path / 'other-abc' + alternate.write_text('other') + proof_installation['yosys-abc'] = alternate + with pytest.raises(ValueError, match='mixed Yosys'): + spi.proof_tool_components(proof_installation) + + +def test_new_component_changes_inventory_but_python_cache_does_not(proof_installation): + before = spi.proof_tool_components(proof_installation) + root = proof_installation['sby'].parent.parent / 'share/yosys/python3' + cache = root / '__pycache__' + cache.mkdir() + (cache / 'sby_core.pyc').write_bytes(b'cache') + assert spi.proof_tool_components(proof_installation) == before + (root / 'sby_new_helper.py').write_text('# new consumed module') + assert spi.proof_tool_components(proof_installation) != before + + +def test_incomplete_record_inventory_cannot_pass(tmp_path, monkeypatch): + monkeypatch.setattr(spi, 'external_inputs', lambda pdk: {'required-component': 'hash'}) + path = tmp_path / 'record.json' + path.write_text(json.dumps({'status': 'passed', 'source': {}, 'artifacts': {}, + 'tool_input_schema': spi.TOOL_INPUT_SCHEMA, + 'manifest': {'configuration': {'PDK': 'ICS55'}}, + 'external_inputs': {}})) + with pytest.raises(ValueError, match='incomplete SPI tool/input inventory'): + spi.checked_record(path, {}) + + +@pytest.mark.parametrize('simulator', ['VERILATOR', 'IVERILOG']) +@pytest.mark.parametrize('top', spi.CASES) +def test_spi_standalone_protocol(tmp_path, simulator, top): + required = ['verilator'] if simulator == 'VERILATOR' else ['iverilog','vvp','sv2v'] + for tool in required: + assert shutil.which(tool), f'required SPI verification tool missing: {tool}' + spi.simulate(tmp_path, simulator, top, 4) diff --git a/tests/test_spi_register_parity.py b/tests/test_spi_register_parity.py new file mode 100644 index 00000000..2111ea55 --- /dev/null +++ b/tests/test_spi_register_parity.py @@ -0,0 +1,57 @@ +"""Independent frozen SPI APB 1.0 constants and handwritten mirror checks.""" +from pathlib import Path +import re + +ROOT = Path(__file__).resolve().parents[1] +REGISTERS = """IP_ID IP_VERSION CAPABILITY FIFO_CAPABILITY CLOCK_HZ ROUTABLE_MASK +COMMAND STATUS SESSION_CFG DIV CS_SETUP CS_HOLD CS_INACTIVE SEG_CONFIG FRAME_COUNT +DUMMY TX_BIND RX_BIND WATERMARKS CPU_TXDATA CPU_RXDATA DMA_TXDATA DMA_RXDATA +FIFO_LEVELS IRQ_STATE IRQ_ENABLE IRQ_STATUS FAULT_CODE FAULT_FRAME FRAMES_DONE +TRANSPORT_WORDS TX_WORDS_ACCEPTED RX_WORDS_PRODUCED RX_WORDS_REMOVED TX_STALL_CYCLES +RX_STALL_CYCLES SESSION_PINS DMA_STATUS PINS_READY PAYLOAD_BYTES""".split() + + +def test_frozen_spi_register_mirrors(): + sv = (ROOT / "rtl/ip/serial/spi_define.svh").read_text() + header = (ROOT / "crt/include/retrosoc/hal/spi_regs.h").read_text() + rtl = {} + for name, width, hexadecimal, decimal in re.findall( + r"^`define\s+APB4_SPI__(\w+)\s+(?:(\d+)'h([0-9a-f]+)|(\d+))\s*$", sv, re.M): + rtl[name] = int(hexadecimal, 16) if width else int(decimal) + c = {name: int(hexa, 16) if hexa else int(decimal) + for name, hexa, decimal in re.findall( + r"^#define\s+RS_SPI_ABI_(\w+)\s+(?:UINT32_C\((0x[0-9a-f]+)\)|(\d+)U)\s*$", header, re.M)} + assert rtl == c + assert {name: rtl[name] for name in REGISTERS} == { + name: index * 4 for index, name in enumerate(REGISTERS)} + assert rtl['ID_VALUE'] == 0x53504930 + assert rtl['VERSION_VALUE'] == 0x10000 + assert rtl['CAPABILITY_VALUE'] == 0x3f7f + assert rtl['FIFO_VALUE'] == 0x200808 + assert rtl['ROUTES_VALUE'] == 0xd8000000 + for bit, name in enumerate('ENABLE DISABLE ARM START END ABORT RECOVER RELEASE RESET'.split()): + assert rtl[name] == bit + assert rtl['TIMING_RESET'] == 1 and rtl['DUMMY_RESET'] == 0xffff + assert rtl['WATERMARK_RESET'] == 0x100 + for bit, name in enumerate('CPOL CPHA LSB_FIRST MISO_ENABLE DISPLAY_DC'.split()): + assert rtl[name] == bit + for number, name in enumerate('DISABLED IDLE ARMED SETUP SHIFT HOLD HELD GAP CLOSING'.split()): + assert rtl['STATE_' + name] == number + for number, name in enumerate('NONE ROUTE READINESS DMA CREDIT QUOTA'.split()): + assert rtl['FAULT_' + name] == number + for bit, name in enumerate('SEGMENT_DONE DONE ABORTED FAULT TX_STALL RX_STALL'.split()): + assert rtl['IRQ_' + name] == bit + for bit, name in enumerate(('CS_ACTIVE SESSION_OWNED SEGMENT_PENDING SEGMENT_CLEAN ' + 'TX_WAIT RX_WAIT BUS_IDLE TRANSPORT_IDLE GPIO_READY').split(), 4): + assert rtl['STATUS_' + name] == bit + assert [rtl[name] for name in ('TX_ONLY', 'FULL_DUPLEX', 'RX_ONLY')] == [0, 1, 2] + assert [rtl[name] for name in ('FRAME16', 'DIRECTION', 'HOLD_CS', 'BIND_ENABLE')] == [0, 1, 3, 31] + assert rtl['IRQ_TX_LEVEL'] == 8 and rtl['IRQ_RX_LEVEL'] == 9 + + +def test_spi_is_not_advertised_by_product_integration(): + for product in ('tiny', 'mini'): + for filelist in (ROOT / f'rtl/{product}/filelist').glob('*.fl'): + assert '/serial/apb4_spi.sv' not in filelist.read_text() + assert 'spi' not in [row['name'] for row in __import__('json').loads( + (ROOT / 'rtl/tiny/integration/soc_topology.json').read_text())['apb_targets']]