Files
retroSoC/tests/rtl/spi_formal.sv
T
maksyuki a64526c6a9 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.
2026-10-19 13:47:03 +08:00

224 lines
8.1 KiB
Systemverilog

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