From 772e682503357723de53e873fd2bb51f31eea361 Mon Sep 17 00:00:00 2001 From: Max Becker Date: Tue, 15 Sep 2026 11:19:29 +0200 Subject: [PATCH] fpga: Update SPI simulation testbenches Extend the SPI slave model to propagate simulation failures to the test execution framework. Extract the timing monitor from the control-port SPI testbench so it can be shared by the GPIO SPI simulations while keeping timing checks centralized and consistent. --- .../ctrlport_spi_adrv_tb.sv | 197 +++--------- fpga/usrp3/sim/control/spi_slave_bfm_pkg.sv | 282 +++++++++++++++++- 2 files changed, 320 insertions(+), 159 deletions(-) diff --git a/fpga/usrp3/lib/sim/control/ctrlport_spi_adrv_tb/ctrlport_spi_adrv_tb.sv b/fpga/usrp3/lib/sim/control/ctrlport_spi_adrv_tb/ctrlport_spi_adrv_tb.sv index 5c1513225..07ee318b6 100644 --- a/fpga/usrp3/lib/sim/control/ctrlport_spi_adrv_tb/ctrlport_spi_adrv_tb.sv +++ b/fpga/usrp3/lib/sim/control/ctrlport_spi_adrv_tb/ctrlport_spi_adrv_tb.sv @@ -42,6 +42,7 @@ module ctrlport_spi_adrv_tb #( import ctrlport_pkg::*; import ctrlport_spi_adrv_pkg::*; import ctrlport_bfm_pkg::*; + import spi_slave_bfm_pkg::*; //--------------------------------------------------------------------------- @@ -83,11 +84,10 @@ module ctrlport_spi_adrv_tb #( // Expected SPI half-period in simulation time. All SPI signal edges are // registered on posedge clk, so every half-period is exactly this long. - realtime half_per_time = (HALF_PER + 1) * CLK_PERIOD_NS * 1.0ns; + time half_per_time; - // Number of CS_N assertions observed by the timing monitor. Checked at the - // end of the test to confirm the monitor actually ran for every transaction. - int cs_n_monitor_count = 0; // Incremented by the timing monitor + // Number of issued transactions. Checked against the timing monitor's + // observed transaction count at the end of the test. int expected_cs_n_count = 0; // Incremented once per issued transaction @@ -123,7 +123,16 @@ module ctrlport_spi_adrv_tb #( // //--------------------------------------------------------------------------- - logic spi_sclk, spi_cs_n, spi_mosi, spi_miso; + // SPI interface + logic spi_sclk, spi_cs_n, spi_mosi, spi_miso; + spi_if spi_monitor_if (); + + assign spi_monitor_if.cs_n = spi_cs_n; + assign spi_monitor_if.sck = spi_sclk; + assign spi_monitor_if.mosi = spi_mosi; + assign spi_monitor_if.miso = spi_miso; + + spi_timing_monitor_bfm spi_timing_mon; logic [7:0] miso_byte_ctr = '0; logic [7:0] miso_shift = '0; @@ -1811,6 +1820,12 @@ module ctrlport_spi_adrv_tb #( $sformatf("REG_HALF_PER after write: got 0x%h, expected 0x%h", rd_val[HALF_PER_W-1:0], NEW_HALF_PER)); half_per_time = (NEW_HALF_PER + 1) * CLK_PERIOD_NS * 1.0ns; + spi_timing_mon.set_timing( + .half_per_time (half_per_time), + .cs_hold_time (CS_HOLD * half_per_time), + .cs_to_first_rise_time (half_per_time), + .cs_guard_time (CS_GUARD * half_per_time) + ); //------------------------------------------------------------------------- // Write 3 bytes at the new HALF_PER setting @@ -1827,156 +1842,17 @@ module ctrlport_spi_adrv_tb #( $sformatf("REG_HALF_PER after restore: got 0x%h, expected 0x%h", rd_val[HALF_PER_W-1:0], HALF_PER)); half_per_time = (HALF_PER + 1) * CLK_PERIOD_NS * 1.0ns; + spi_timing_mon.set_timing( + .half_per_time (half_per_time), + .cs_hold_time (CS_HOLD * half_per_time), + .cs_to_first_rise_time (half_per_time), + .cs_guard_time (CS_GUARD * half_per_time) + ); test.end_test(); endtask : test_sclk_freq - //--------------------------------------------------------------------------- - // SPI Timing Monitor - //--------------------------------------------------------------------------- - // - // Runs in parallel with the main test. On every transaction (CS_N - // assertion), checks: - // - // 1. CS_N assert -> first SCLK rise is exactly 1 half-period - // 2. SCLK high period (posedge -> negedge) is exactly 1 half-period - // 3. SCLK low period (negedge -> posedge): - // - intra-byte (fall N where N % 8 != 0): exactly 1 half-period - // - inter-byte (fall N where N % 8 == 0): >= 1 half-period - // 4. Last SCLK fall -> CS_N deassert: - // - CS_HOLD=1 : exactly 1 half-period - // - CS_HOLD=0 : 0 (same registered clock edge) - // 5. CS_N deassert -> next CS_N assert : >= CS_GUARD half-periods - // - // CS_HOLD and CS_GUARD are read hierarchically from dut.axis_spi_i. - //--------------------------------------------------------------------------- - - initial begin : spi_timing_monitor - realtime t_cs_assert, t_rise, t_fall, t_cs_deassert, t_cs_deassert_prev; - int rise_cnt; // rising edge count within the current transaction - static bit first_txn = 1'b1; // suppress guard check on first transaction - - @(negedge rst); // wait for reset to deassert - - forever begin - - //----------------------------------------------------------------------- - // Wait for CS_N assertion (start of transaction) - //----------------------------------------------------------------------- - @(negedge spi_cs_n); - t_cs_assert = $realtime; - cs_n_monitor_count++; - - //----------------------------------------------------------------------- - // Check 5: CS_N guard: previous transaction's CS_N deassert must be >= - // CS_GUARD half-periods before this assertion. Checked here (at the - // start of each transaction) so the guard @(negedge spi_cs_n) doesn't - // consume a transaction count. Skipped on the first transaction. - //----------------------------------------------------------------------- - if (!first_txn) begin - `ASSERT_ERROR(t_cs_assert - t_cs_deassert_prev >= - dut.axis_spi_i.CS_GUARD * half_per_time, - $sformatf("CS_N guard too short: minimum %0t, got %0t", - dut.axis_spi_i.CS_GUARD * half_per_time, - t_cs_assert - t_cs_deassert_prev)); - end - first_txn = 1'b0; - - //----------------------------------------------------------------------- - // Check 1: CS_N assert -> first SCLK rising edge = exactly 1 half-period - //----------------------------------------------------------------------- - @(posedge spi_sclk); - t_rise = $realtime; - rise_cnt = 1; - `ASSERT_ERROR(t_rise - t_cs_assert == half_per_time, - $sformatf("CS_N assert to first SCLK rise: expected %0t, got %0t", - half_per_time, t_rise - t_cs_assert)); - - //----------------------------------------------------------------------- - // Checks 2, 3, 4: Walk all SCLK edges until CS_N deasserts. - // - // State at loop top: we are at a posedge sclk; rise_cnt is current. - // - // Step A: Wait for negedge sclk OR posedge cs_n (CS_HOLD=0 causes - // both to arrive on the same clock edge). - // Step B: If cs_n is still low, wait for posedge sclk OR posedge - // cs_n (CS_HOLD=1 deasserts cs_n here after ST_HOLD). - //----------------------------------------------------------------------- - forever begin - - // Step A: wait for SCLK fall (or simultaneous CS_N deassert) - fork : wait_fall - begin : fall_th @(negedge spi_sclk); end - begin : cs0_th @(posedge spi_cs_n); end - join_any - disable wait_fall; - t_fall = $realtime; - - // Check 2: SCLK high period must be exactly 1 half-period - `ASSERT_ERROR(t_fall - t_rise == half_per_time, - $sformatf("SCLK high period: expected %0t, got %0t", - half_per_time, t_fall - t_rise)); - - if (spi_cs_n) begin - // CS_HOLD=0: CS_N deasserted on the same clock edge as last SCLK - // fall, so time difference is 0. - t_cs_deassert = t_fall; - `ASSERT_ERROR(dut.axis_spi_i.CS_HOLD == 0, - "CS_N deasserted coincident with last SCLK fall but CS_HOLD != 0"); - break; - end - - // Step B: wait for SCLK rise or CS_N deassert (CS_HOLD=1) - fork : wait_rise - begin : rise_th @(posedge spi_sclk); end - begin : cs1_th @(posedge spi_cs_n); end - join_any - disable wait_rise; - t_rise = $realtime; - rise_cnt++; - - // Check 3: SCLK low period. - // - // rise_cnt just incremented, so the preceding fall was fall number - // (rise_cnt - 1). A fall at a multiple of 8 is an inter-byte boundary - // where the FSM waits in ST_IDLE for the next byte; duration is - // unbounded but must be >= 1 half-period. All other falls are - // intra-byte and must be exactly 1 half-period. - if ((rise_cnt - 1) % 8 == 0) begin - `ASSERT_ERROR(t_rise - t_fall >= half_per_time, - $sformatf("SCLK low period (inter-byte) too short: minimum %0t, got %0t", - half_per_time, t_rise - t_fall)); - end else begin - `ASSERT_ERROR(t_rise - t_fall == half_per_time, - $sformatf("SCLK low period (intra-byte, fall %0d): expected %0t, got %0t", - rise_cnt - 1, half_per_time, t_rise - t_fall)); - end - - if (spi_cs_n) begin - // CS_HOLD=1: CS_N deasserted exactly 1 half-period after last - // SCLK fall (after ST_HOLD). - t_cs_deassert = t_rise; - `ASSERT_ERROR(dut.axis_spi_i.CS_HOLD == 1, - "CS_N deasserted 1 half-period after SCLK fall but CS_HOLD != 1"); - `ASSERT_ERROR(t_cs_deassert - t_fall == half_per_time, - $sformatf("CS_HOLD=1 hold time: expected %0t, got %0t", - half_per_time, t_cs_deassert - t_fall)); - break; - end - // cs_n still low and we got posedge sclk; continue to next bit - - end // inner forever (SCLK edge walk) - - // Save deassert time for next iteration's guard check - t_cs_deassert_prev = t_cs_deassert; - - end // outer forever (transaction loop) - - end : spi_timing_monitor - - - //--------------------------------------------------------------------------- // Main test //--------------------------------------------------------------------------- @@ -2005,6 +1881,17 @@ module ctrlport_spi_adrv_tb #( clk_gen.reset(); @(negedge rst); + // Create and start passive SPI timing checks after reset is released. + half_per_time = (HALF_PER + 1) * CLK_PERIOD_NS * 1.0ns; + spi_timing_mon = new( + .iface (spi_monitor_if), + .half_per_time (half_per_time), + .cs_hold_time (CS_HOLD * half_per_time), + .cs_to_first_rise_time (half_per_time), + .cs_guard_time (CS_GUARD * half_per_time) + ); + spi_timing_mon.run(); + //------------------------------------------------------------------------- // Tests //------------------------------------------------------------------------- @@ -2025,9 +1912,15 @@ module ctrlport_spi_adrv_tb #( // Confirm the timing monitor ran exactly once per SPI transaction. // expected_cs_n_count is incremented once per issued transaction, so it // tracks the true number of transactions without manual bookkeeping. - `ASSERT_ERROR(cs_n_monitor_count == expected_cs_n_count, + `ASSERT_ERROR(spi_timing_mon.get_transaction_count() == expected_cs_n_count, $sformatf("Timing monitor CS_N count mismatch: expected %0d, got %0d", - expected_cs_n_count, cs_n_monitor_count)); + expected_cs_n_count, spi_timing_mon.get_transaction_count())); + `ASSERT_ERROR(spi_timing_mon.get_error_count() == 0, + $sformatf("SPI timing monitor reported %0d errors", + spi_timing_mon.get_error_count())); + + // Stop the monitor before the next parameterized testbench instance runs. + disable fork; // Don't call $finish; other instances of this testbench may follow. test.end_tb(0); diff --git a/fpga/usrp3/sim/control/spi_slave_bfm_pkg.sv b/fpga/usrp3/sim/control/spi_slave_bfm_pkg.sv index c05acc34b..b7f37bd64 100644 --- a/fpga/usrp3/sim/control/spi_slave_bfm_pkg.sv +++ b/fpga/usrp3/sim/control/spi_slave_bfm_pkg.sv @@ -1,5 +1,5 @@ // -// Copyright 2025 Ettus Research, a National Instruments Brand +// Copyright 2026 Ettus Research, a National Instruments Brand // // SPDX-License-Identifier: LGPL-3.0-or-later // @@ -16,6 +16,8 @@ package spi_slave_bfm_pkg; + timeunit 1ns / 1ps; + class spi_slave_bfm #(int TRANSACTION_LENGTH = 32); local mailbox #(logic [TRANSACTION_LENGTH-1:0]) mosi_mailbox; @@ -24,6 +26,9 @@ package spi_slave_bfm_pkg; // link to interface local virtual spi_if iface; + + local int unsigned error_count; + // ------------------------------------------------------------------------ // Constructor // ------------------------------------------------------------------------ @@ -33,6 +38,12 @@ package spi_slave_bfm_pkg; this.iface = iface; mosi_mailbox = new; miso_mailbox = new; + error_count = 0; + endfunction + + // Number of errors reported by the BFM. + function int unsigned get_error_count(); + return error_count; endfunction // ------------------------------------------------------------------------ @@ -81,6 +92,11 @@ package spi_slave_bfm_pkg; join_none endtask + local function void report_error(string msg); + error_count++; + $error("%s @ t=%0t", msg, $time); + endfunction + // check constantly the status of the CS and SCK signal task cs_sck_check_body(); automatic int clock_edge_counter = 0; @@ -99,7 +115,8 @@ package spi_slave_bfm_pkg; // check if the transaction length is correct // 2 clock edges per transaction bit assert(clock_edge_counter <= 2*TRANSACTION_LENGTH) else - $error("Too many SCLK cycles received. Clock cycles: %0d", clock_edge_counter); + report_error($sformatf( + "Too many SCLK cycles received. Clock cycles: %0d", clock_edge_counter)); end end // ignore special case when cs turn to high impedance @@ -107,7 +124,8 @@ package spi_slave_bfm_pkg; // if cs goes high check if the transaction length is correct // 2 clock edges per transaction bit assert(clock_edge_counter == 2*TRANSACTION_LENGTH) else - $error("Unexpected SPI CS interrupt. Clock cycles: %0d", clock_edge_counter); + report_error($sformatf( + "Unexpected SPI CS interrupt. Clock cycles: %0d", clock_edge_counter)); end end endtask @@ -135,12 +153,13 @@ package spi_slave_bfm_pkg; end // compare received data with expected data assert(received_data == expected_data) else - $error("Unexpected data received on SPI interface. Expected: %h Received: %h", - expected_data, received_data); + report_error($sformatf( + "Unexpected data received on SPI interface. Expected: %h Received: %h", + expected_data, received_data)); end else begin // ignore special case when cs turn to high impedance if (iface.cs_n !== 'z) begin - $error("No data available in SPI BFM MOSI mailbox."); + report_error("No data available in SPI BFM MOSI mailbox."); end end end @@ -174,7 +193,7 @@ package spi_slave_bfm_pkg; end else begin // ignore special case when cs turn to high impedance if (iface.cs_n !== 'z) begin - $error("No data available in SPI BFM MISO mailbox."); + report_error("No data available in SPI BFM MISO mailbox."); end end end @@ -182,4 +201,253 @@ package spi_slave_bfm_pkg; endclass + // ------------------------------------------------------------------------ + // SPI timing monitor BFM + // ------------------------------------------------------------------------ + // Passive monitor for CPOL=0, CPHA=0 SPI timing checks. + // + // On each transaction (CS_N low pulse), the monitor checks: + // 1) CS_N assert to first SCLK rise (minimum) + // 2) SCLK high period (exact) + // 3) SCLK low period: + // - intra-byte: exact + // - inter-byte: minimum (optional) + // 4) CS hold time after final SCLK fall (minimum) + // 5) CS guard time between transactions (minimum) + // + // The monitor is reusable across tests within a testbench by updating timing with + // set_timing() when expected timings change. + class spi_timing_monitor_bfm; + + local virtual spi_if iface; + + local time half_per_time; + local time cs_hold_time; + local time cs_to_first_rise_time; + local time cs_guard_time; + + local int unsigned bits_per_byte; + local bit check_inter_byte_gaps; + + local int unsigned transaction_count; + local int unsigned error_count; + + // ---------------------------------------------------------------------- + // Constructor + // ---------------------------------------------------------------------- + // Arguments: + // iface: SPI interface to monitor + // half_per_time: expected SCLK half period (must be non-zero) + // cs_hold_time: minimum time from last SCLK fall to CS_N deassert + // (0 disables CS hold checks) + // cs_to_first_rise_time: minimum time from CS_N assert to first SCLK rise + // (0 disables first-rise check) + // cs_guard_time: minimum time between transactions + // bits_per_byte: bit grouping for inter-byte low-gap checks + // check_inter_byte_gaps: enable minimum-gap checks at byte boundaries + function new( + virtual spi_if iface, + time half_per_time, + time cs_hold_time = 0, + time cs_to_first_rise_time = 0, + time cs_guard_time = 0, + int unsigned bits_per_byte = 8, + bit check_inter_byte_gaps = 1'b1 + ); + assert (half_per_time != 0) + else $fatal(1, "half_per_time must be non-zero"); + + this.iface = iface; + this.half_per_time = half_per_time; + this.cs_hold_time = cs_hold_time; + this.cs_to_first_rise_time = cs_to_first_rise_time; + this.cs_guard_time = cs_guard_time; + this.bits_per_byte = bits_per_byte; + this.check_inter_byte_gaps = check_inter_byte_gaps; + this.transaction_count = 0; + this.error_count = 0; + endfunction + + // Update timing expectations at runtime. + // Use this after changing SPI rate configuration in the DUT. + function void set_timing( + time half_per_time, + time cs_hold_time, + time cs_to_first_rise_time, + time cs_guard_time + ); + assert (half_per_time != 0) + else $fatal(1, "half_per_time must be non-zero"); + + this.half_per_time = half_per_time; + this.cs_hold_time = cs_hold_time; + this.cs_to_first_rise_time = cs_to_first_rise_time; + this.cs_guard_time = cs_guard_time; + endfunction + + // Number of observed CS_N assertions (transactions). + function int unsigned get_transaction_count(); + return transaction_count; + endfunction + + // Number of timing violations reported by the monitor. + function int unsigned get_error_count(); + return error_count; + endfunction + + // Start monitoring. Intended to run in a forked process. + task run(); + fork + monitor_body(); + join_none; + endtask + + local function void report_error(string msg); + error_count++; + $error("%s @ t=%0t", msg, $time); + endfunction + + // Main timing monitor loop. + // This runs forever and validates each SPI transaction independently. + local task monitor_body(); + time t_cs_assert; + time t_rise; + time t_fall; + time t_cs_deassert; + time t_cs_deassert_prev; + int rise_cnt; + bit first_txn; + + first_txn = 1'b1; + + forever begin + + //----------------------------------------------------------------------- + // Wait for CS_N assertion (start of transaction) + //----------------------------------------------------------------------- + @(negedge iface.cs_n); + t_cs_assert = $time; + transaction_count++; + + //----------------------------------------------------------------------- + // Check 5: CS_N guard: previous transaction's CS_N deassert must be >= + // CS_GUARD_TIME before this assertion. Checked here (at the + // start of each transaction) so the guard @(negedge iface.cs_n) doesn't + // consume a transaction count. Skipped on the first transaction. + //----------------------------------------------------------------------- + if (!first_txn) begin + if (t_cs_assert - t_cs_deassert_prev < cs_guard_time) begin + report_error($sformatf( + "CS_N guard too short: minimum %0t, got %0t", + cs_guard_time, + t_cs_assert - t_cs_deassert_prev)); + end + end + first_txn = 1'b0; + + //----------------------------------------------------------------------- + // Check 1: CS_N assert -> first SCLK rising edge >= cs_to_first_rise_time + //----------------------------------------------------------------------- + @(posedge iface.sck); + t_rise = $time; + rise_cnt = 1; + if ((cs_to_first_rise_time != 0) && (t_rise - t_cs_assert < cs_to_first_rise_time)) begin + report_error($sformatf( + "CS_N assert to first SCLK rise too short: minimum %0t, got %0t", + cs_to_first_rise_time, t_rise - t_cs_assert)); + end + + + //----------------------------------------------------------------------- + // Checks 2, 3, 4: Walk all SCLK edges until CS_N deasserts. + // + // State at loop top: we are at a posedge sclk; rise_cnt is current. + // + // Step A: Wait for negedge sclk OR posedge cs_n. + // Step B: If cs_n is still low, wait for posedge sclk OR posedge + // cs_n. + //----------------------------------------------------------------------- + forever begin + + // Step A: wait for SCLK fall (or simultaneous CS_N deassert) + fork : wait_fall + begin : fall_th @(negedge iface.sck); end + begin : cs0_th @(posedge iface.cs_n); end + join_any + disable wait_fall; + t_fall = $time; + + // Check 2: SCLK high period must be exactly half_per_time + // This indirectly also ensures that CS is not deasserted too early + if (t_fall - t_rise != half_per_time) begin + report_error($sformatf( + "SCLK high period: expected %0t, got %0t", + half_per_time, t_fall - t_rise)); + end + + if (iface.cs_n) begin + // CS_N deasserted on the same clock edge as last SCLK + t_cs_deassert = t_fall; + if ((cs_hold_time != 0)) begin + report_error($sformatf( + "CS hold time at SCLK fall too short: minimum %0t, got 0", + cs_hold_time)); + end + break; + end + + // Step B: wait for SCLK rise or CS_N deassert (CS_HOLD=1) + fork : wait_rise + begin : rise_th @(posedge iface.sck); end + begin : cs1_th @(posedge iface.cs_n); end + join_any + disable wait_rise; + + if (iface.cs_n) begin + // CS_N deasserted after the last SCLK fall; enforce minimum hold. + t_cs_deassert = $time; + if ((cs_hold_time != 0) && (t_cs_deassert - t_fall < cs_hold_time)) begin + report_error($sformatf( + "CS hold time after last SCLK fall too short: minimum %0t, got %0t", + cs_hold_time, t_cs_deassert - t_fall)); + end + break; + end + + t_rise = $time; + rise_cnt++; + + // Check 3: SCLK low period. + // + // rise_cnt just incremented, so the preceding fall was fall number + // (rise_cnt - 1). A fall at a multiple of bits_per_byte is an + // inter-byte boundary + // where the FSM waits in ST_IDLE for the next byte; duration is + // unbounded but must be >= 1 half-period. All other falls are + // intra-byte and must be exactly 1 half-period. + if (check_inter_byte_gaps && ((rise_cnt - 1) % bits_per_byte == 0)) begin + if (t_rise - t_fall < half_per_time) begin + report_error($sformatf( + "SCLK low period (inter-byte) too short: minimum %0t, got %0t", + half_per_time, t_rise - t_fall)); + end + end else begin + if (t_rise - t_fall != half_per_time) begin + report_error($sformatf( + "SCLK low period: expected %0t, got %0t", + half_per_time, t_rise - t_fall)); + end + end + + // CS_N is still low and we got posedge sclk; continue to next bit + + end // inner forever (SCLK edge walk) + + // Save deassert time for next iteration's guard check + t_cs_deassert_prev = t_cs_deassert; + end // outer forever (transaction loop) + endtask + + endclass + endpackage