mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
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:
29 files changed
+3654
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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']]
|
||||
Reference in new issue
Block a user