feat(spi): add standalone SPI-P1 core and verification

Implement SPI-P1 from docs/ip/spi.md with a PCLK master, registered APB
responses, packed 8x32 TX/RX FIFOs, finite quotas and retained-session
abort/recovery. Add the handwritten SVH/C register mirror and private
source, credit and GPIO qualification interfaces.

Add independent pin timing and data checks, malformed-waveform negatives,
FIFO stall/resume and phase-safe abort cases. Provide isolated lint,
simulation, compositional formal, synthesis, netlist and observational STA
flows with source-bound artifacts and tool-component fingerprints.

Validation on the reviewed worktree: 2085 pytest tests passed and one
optional pypdf test skipped; both RTL simulators, fresh depth-192 BMC and
cover, and ICS55/IHP130 block synthesis and netlist simulation passed.
Formatting, style/readiness, Ruff and regression dry-runs passed.

SPI remains outside product filelists with DMA capability disabled.
SPI-P2/P3 integration, full-product regression closure and physical/IO
qualification remain pending; no phase acceptance or policy change.
This commit is contained in:
maksyuki committed 2026-10-19 13:47:03 +08:00
1 parent e62ccdad87
commit a64526c6a9
29 files changed
+3654

No files matched your search

+7
View File
@@ -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)
+110
View File
@@ -0,0 +1,110 @@
#ifndef RETROSOC_HAL_SPI_REGS_H
#define RETROSOC_HAL_SPI_REGS_H
#include <stdint.h>
/* 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
+3
View File
@@ -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.
+13
View File
@@ -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
@@ -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
+93
View File
@@ -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
+20
View File
@@ -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
+104
View File
@@ -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
+32
View File
@@ -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
+363
View File
@@ -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
+97
View File
@@ -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
+27
View File
@@ -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
+299
View File
@@ -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
+112
View File
@@ -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
+19
View File
@@ -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"],
+9
View File
@@ -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",
+496
View File
@@ -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())
+6
View File
@@ -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
+6
View File
@@ -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
+223
View File
@@ -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
+24
View File
@@ -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
+6
View File
@@ -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
+6
View File
@@ -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
+92
View File
@@ -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
+119
View File
@@ -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
+162
View File
@@ -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
+836
View File
@@ -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
+233
View File
@@ -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)
+57
View File
@@ -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']]