SPI — Serial Peripheral Interface Verification

Fatma Vural
8 min readMay 30, 2024

UVM (Universal Verification Methodology) is a comprehensive methodology used for the verification of digital designs. It is standardized by IEEE and was developed specifically to standardize verification processes of complex systems and integrated circuits. • Testbench Components: UVM provides a set of base classes that can be extended to create testbench components, such as drivers, monitors, scoreboards, and agents.

UVM Components

UVM provides several components to create the verification environment:

  • Test: A scenario or set of tests used to accomplish a specific verification goal.
  • Environment: Contains all components (agen, monitor, scoreboard, etc.) required to run a test.
  • Agent: A combination of driver, monitor, and sequence elements used to authenticate an interface.
  • Driver: Receives test scenarios and converts them into DUT (Device Under Test) signals.
  • Monitor: Monitors the signals coming from the DUT and collects these signals for analysis.
  • Sequencer: Creates test scenarios (sequence) and sends them to the driver.
  • Scoreboard: Verifies by comparing expected and actual results.
  • Transaction: It is an abstract data structure that represents the exchange of data.

UVM-TestBench

Transactions Class is where input and output data are defined and where these data are transferred as transactions between each class in the “Environment”. A transaction class represents the data moved during the initiation, execution, and termination of a transaction. Transaction encapsulates input and output fields inside it with the keyword ‘rand’ or ‘randc‘ to give the generator the ability to randomize inputs given to the driver to drive design.

  • ‘rand’: Ensures that the variable is randomized independently in each randomization process.
  • ’randc’: Provides cyclic randomization, that is, the same value is not repeated until all possible values are exhausted.

Thanks to these keywords, it gives the generator the ability to randomize the inputs to be given to the driver. The driver drives the design using these randomized inputs.

//transaction.sv
class transaction;
rand bit MOSI;
bit MISO;
rand bit sS_n.rst_n;

bit[9:0] rx data;
bit rx valid;

rand bit [7:0] tx data:
rand bit tx valid:

function print info(string name);
$display("=====================%0s=====================") ;
$display("**********transaction information************");
$display("MOSI | MISO | SS_n | rst_n | rx_data | rx_valid | tx_data | tx_valid | \n%0d | %0d | %0d |%0d |%0d |%0d |%0d |%0d | @%0t", MOSI,MISO,SS_n,rst_n,rx_data,rx_valid,tx_data,tx_valid,&time);
&display ("********************************************");
endfunction
endclass

Generator class is an important component used in the verification process. Its purpose is to generate random test data (stimulus) to test how the design (DUT — Device Under Test) behaves under various situations and conditions. This class is used to create various test scenarios and apply these scenarios to the design through the driver.

class generator;

rand int rst_cyc_num;
rand int trn_num_seq1;
rand int trn_num_seq2;
rand int trn_num_seq3;
rand int trn_num_seq4;
rand int trn_num_seq5;
rand int trn_num_seq6;
rand int trn_num_seq7;
rand int trn_num_seq8;

// Posta kutusu
mailbox #(transaction) gen2drv;

// Constructor
function new(mailbox #(transaction) mbox);
this.gen2drv = mbox;
endfunction

// Reset task
task reset_seq();
repeat (rst_cyc_num) begin
transaction trn = new();
if (!(trn.randomize() with {rst_n == 0;})) begin
$fatal("Randomization failed");
end
gen2drv.put(trn);
end
endtask

// run_seq1 task
task run_seq1();
repeat (trn_num_seq1) begin
transaction trn = new();
if (!(trn.randomize() with {rst_n == 1; SS_n == 1;})) begin
$fatal("Randomization failed");
end
gen2drv.put(trn);
end
endtask

// run_seq2 task
task run_seq2();
repeat (trn_num_seq2) begin
transaction trn = new();
if (!(trn.randomize() with {rst_n == 1; SS_n == 0; MOSI == 0;})) begin
$fatal("Randomization failed");
end
gen2drv.put(trn);
end
endtask

// run_seq3 task
task run_seq3();
repeat (trn_num_seq3) begin
transaction trn = new();
if (!(trn.randomize() with {rst_n == 1; SS_n == 0;})) begin
$fatal("Randomization failed");
end
gen2drv.put(trn);
end
endtask

// run_seq4 task
task run_seq4();
repeat (trn_num_seq4) begin
transaction trn = new();
if (!(trn.randomize() with {rst_n == 1; SS_n == 0; MOSI == 1;})) begin
$fatal("Randomization failed");
end
gen2drv.put(trn);
end
endtask

// run_seq5 task
task run_seq5();
repeat (trn_num_seq5) begin
transaction trn = new();
if (!(trn.randomize() with {rst_n == 1; SS_n == 0;})) begin
$fatal("Randomization failed");
end
gen2drv.put(trn);
end
endtask

// run_seq6 task
task run_seq6();
repeat (trn_num_seq6) begin
transaction trn = new();
if (!(trn.randomize() with {rst_n == 1; SS_n == 1; MOSI == 1; tx_valid == 1;})) begin
$fatal("Randomization failed");
end
gen2drv.put(trn);
end
endtask

// run_seq7 task
task run_seq7();
repeat (trn_num_seq7) begin
transaction trn = new();
if (!(trn.randomize() with {rst_n == 1; SS_n == 0; MOSI == 1; tx_valid == 1; tx_data == 20;})) begin
$fatal("Randomization failed");
end
gen2drv.put(trn);
end
endtask

// run_seq8 task
task run_seq8();
repeat (trn_num_seq8) begin
transaction trn = new();
if (!(trn.randomize() with {rst_n == 1; SS_n == 0; tx_valid == 1; tx_data == 20;})) begin
$fatal("Randomization failed");
end
gen2drv.put(trn);
end
endtask

endclass

Driver class drives the generated stimulus from the generator (transactions) to the design interface. It acts as a bit-level transaction driver and assigns the output from the design interface after one clock cycle.

// Interface defination
virtual interface SPI_Slave_intf vif;

// Mailbox generator forwarded to driver mailbox genzdrv
// Process definition
transaction trn;

class driver;
// access the SPI_Slave_intf interface
virtual SPI_Slave_intf vif;
// Access to mailbox genzdrv
mailbox genzdrv;

// Constructor method
function new(virtual SPI_Slave_intf vif, mailbox genzdrv);
this.vif = vif;
this.genzdrv = genzdrv;
endfunction

// Main working method
task run();
forever begin
// Get action from mailbox
genzdrv.get(trn);
// Process the operation within the driver
trn.print_into_an_side_driver();
//Wait on SPI clock edge
@(posedge vif.clk);
// Reset control
vif.rst <= trn.rst;
// TX data transfer
vif.cb.tx_data <= trn.tx_data;
//TX valid data
vif.cb.tx_valid <= trn.tx_valid;
//MOSI line
vif.cb.MOSI <= trn.MOSI;
// SS line
vif.cb.SS_n <= trn.SS_n;
end
endtask
endclass

Monitor class is used to monitor the input and output of the design interface. It encapsulates these into a transaction and sends the collected transactions to two mailboxes: one for the scoreboard and another for the coverage collector.

class monitor:
# Interface definition
virtual SPI_Slave_intf vif:
# Mailbox generator to driver
mailbox mon2scb
mailbox mon2cov
# Transaction definition
transaction trn:

function new(virtual SPI_Slave_intf vif, mailbox mon2scb, mailbox mon2cov):
this.vif = vif
this.mon2scb = mon2scb
this.mon2cov = mon2cov
endfunction

task run():
forever begin
trn = new()

// Operate on the rising edge of the SPI clock signal
@(posedge vif.clk)

trn.rst = vif.rst
trn.tx_data = vif.tx_data
trn.tx_valid = vif.tx_valid
trn.MOSI = vif.MOSI
trn.ss_n = vif.ss_n
trn.MISO = vif.MISO
trn.rx_data = vif.rx_data
trn.rx_valid = vif.rx_valid

// Put transactions into mailboxes
mon2scb.put(trn)
mon2cov.put(trn)

// Print info (inside Monitor*)
$display("Print info (inside Monitor*)")
end
endtask
endclass

Scoreboard class checks if the output from the design is as expected. It takes transactions from the monitor and verifies the correctness of the DUT’s output.

class Scoreboard;
// Variables
int counter;
int counter2;
int counter3;
bit [31:0] RX_OUT [11:0];
bit [31:0] RX_OUT2 [11:0];
bit flag;
bit flag2;
bit flag3;
bit flag4;
fifo fifo;
fifo fifo2;

// Constructor
function new(string name = "scoreboard");
super.new(name);
// Initialize variables
counter = 0;
counter2 = 0;
counter3 = 0;
flag = 0;
flag2 = 0;
flag3 = 0;
flag4 = 0;
endfunction

// Scoreboard task
task run_phase(uvm_phase phase);
forever begin
transaction trn;
// Get Data
trn = fifo.get();

// Satatus Check
if (trn.rst_n && trn.SS_n && (trn.MOSI || flag) && flag2) begin
flag = 1;
fifo.push_front(trn.MOSI);
counter++;
if (counter == 12) begin
for (int i = 10; i >= 0; i--) begin
RX_OUT[i] = fifo.pop_back();
$display("RX_OUT[%0d] = %0d", i, RX_OUT[i]);
end
if (trn.rx_data != RX_OUT[9:0] || trn.rx_valid != 1) begin
`uvm_error(get_type_name(), $sformatf("Output isn't as expected: rx_data=%0d RX_OUT=%0d", trn.rx_data, RX_OUT[9:0]))
end else begin
`uvm_info(get_type_name(), "Output is correct.", UVM_LOW)
end
end
end else if (trn.rst_n && trn.SS_n && !flag) begin
flag = 1;
counter = 0;
fifo.delete();
end
// Other states are added here...
end
endtask
endclass

Environment class encapsulates various components needed for the testbench. It serves as a container for the testbench architecture and manages the stimulus given to the DUT.

class env;
// Component instances
gen gen;
driver drv;
monitor mon;
scoreboard scb;
coverage cov;

// Mailbox definitions
mailbox gen2drv;
mailbox mon2scb;
mailbox mon2cov;

// Virtual interface definition
virtual SPI_Slave_intf vif;

// Constructor
function new(virtual SPI_Slave_intf vif);
this.vif = vif;

// Initialize mailboxes
gen2drv = new();
mon2scb = new();
mon2cov = new();

// Component creations
gen = new(gen2drv);
drv = new(vif, gen2drv);
mon = new(vif, mon2scb, mon2cov);
scb = new(mon2scb);
cov = new(vif, mon2cov);
endfunction

// Sequence generation task
task seq_gen();
gen.reset_seq();
// Write sequences
repeat (5) begin
gen.run_seq1();
gen.run_seq2();
gen.run_seq3();
end
// Read Address sequence
gen.run_seq6();
gen.run_seq4();
gen.run_seq5();
// Write address sequence
gen.run_seq6();
gen.run_seq7();
gen.run_seq8();
gen.run_seq5();
gen.run_seq6();
gen.run_seq9();
gen.reset_seq();
endtask

// Main task
task main();
fork
drv.run();
mon.run();
scb.run();
cov.sample_task();
join_none
endtask
endclass

Test Module and Testbench Top Module

  • Test Module: Creates the environment class, configures the number of generated stimuli, and runs environment tasks.
`include "env.sv"

module test(SPI_Slave_intf intf);
// Test environment instance
env ENV;

initial begin
// Test environment created
ENV = new(intf);

// Transaction numbers are determined
ENV.gen.trn_num_seq1 = 3;
ENV.gen.trn_num_seq2 = 3;
ENV.gen.trn_num_seq3 = 20;
ENV.gen.trn_num_seq4 = 3;
ENV.gen.trn_num_seq5 = 20;
ENV.gen.trn_num_seq6 = 300;

// The number of reset cycles is determined
ENV.gen.rst_cyc_num = 5;

// Running sequence generation task
ENV.seq_gen();

// Main task is running
ENV.main();
end
endmodule

Testbench Top Module: Connects the test module and design through the interface, provides a clock signal (100MHz), and generates a reset signal for the interface.

// Clock generation
always #5 clk = ~clk;

// Interface instance
SPI_Slave_intf intf(clk);

// Instantiate design (SPI Slave)
SPI_Slave DUT (.intf(intf));

// Instantiate testbench
testbench_tb tb (.intf(intf));

// Assertions
// Assertion to confirm that when SS_n is applied, DUT will go to IDLE state
assert property @(posedge intf.clk)
disable iff (intf.rst_n || $rose(intf.rst_n))
(intf.SS_n |-> (DUT.state == 0))
else $error("Output isn't true when slave select is high. Output must be in the idle state.");

// Assertion to confirm that when SS_n is deactivated and DUT is in IDLE state, it will go to CHECK mode
assert property @(posedge intf.clk)
disable iff (intf.rst_n || $rose(intf.rst_n))
((!intf.SS_n && (DUT.CS == 1)) |-> (DUT.state == 1))
else $error("Output isn't true when slave select is low. Output must be in the check mode state.");

// Assertion to confirm that when SS_n is deactivated, DUT is in CHECK mode, and DUT in CHECK mode state will go to WRITE state
assert property @(posedge intf.clk)
disable iff (intf.rst_n || $rose(intf.rst_n))
((!intf.SS_n && intf.MOSI && (DUT.CS == 0)) |-> (DUT.state == 2))
else $error("Output isn't true when slave select is low and MOSI first time is low. Output must be in the write state.");

// Initial block
initial begin
$dumpfile("SPI.vcd");
repeat (160) @(posedge clk);
$finish;
end

Simulation and Coverage

Simulation results indicate that the design behaves correctly under the given stimuli, with no errors reported from the scoreboard.

References

https://github.com/Mohamed-Younis/SPI-UVM-Testbench/blob/master/README.md

https://github.com/nandland/spi-slave/blob/master/Verilog/source/SPI_Slave.v

https://www.linkedin.com/in/hend-mohamed-0a306b235/ — Hend Mohamed

--

--

Fatma Vural

I’m a analog person in digital world also part - time engineer.. ✨