SPI · Module 17
A Reusable UVM SPI Agent
Three arbitration policies deliver identical items and identical functional coverage, and completely different traffic: round robin switches between sequences 23 times and strict priority once. A coverage report cannot tell you which one ran, so it cannot tell you whether two sequences ever overlapped.
Module 16 built four components and packaged them. This chapter assembles the whole environment, and then measures the one piece of it whose behaviour surprises people most.
Three arbitration policies. Identical items, identical coverage, and traffic so different that a concurrency bug is reachable under one and unreachable under another.
1. The Environment
Every block in that picture has already been built and measured. What is new here is the box that has not been examined yet.
2. What A Sequencer Promises
A sequencer stands between N sequences and one driver, and it makes exactly one promise:
within a sequence, items are delivered in the order the sequence produced themIt promises nothing about the order across sequences. That is not a gap in the specification — it is the whole point. The interleaving is chosen by an arbitration policy, and changing the policy changes which concurrency bugs a test can reach without changing a single item.
3. Three Policies, One Set Of Items
ARB_RR round robin. Each sequence with an item pending gets the next
grant in turn.
ARB_PRIO strict priority. Sequence 0 wins whenever it has an item pending.
ARB_WEIGHTED sequence 0 wins three grants in four.4. The Measurement
Twelve items from each sequence, both queues full before the first grant — because an interleaving experiment in which one sequence arrives late measures the arrival times rather than the policy.
policy from s0 from s1 order errors switches s1 longest wait
round robin 12 12 0 23 1
strict priority 12 12 0 1 12
weighted 3:1 12 12 0 7 3Zero order errors under all three. That is the promise, and it holds because the policy decides whose turn it is while the per-sequence queue decides which item.
All three delivered every item. So every functional coverage number is identical across the three runs — which is the point: a coverage report cannot tell you which policy ran, and therefore cannot tell you whether the two sequences ever overlapped.
And the traffic is completely different. Round robin switched between the streams 23 times; strict priority switched once. Under priority, sequence 1's items all arrive after sequence 0 has finished, so the two never overlap and any design bug that needs them to overlap is unreachable.
"we ran both sequences" and "we ran both sequences together"
are different claims5. Building It — Three HDLs
The sequencer is the part of a UVM environment that can be built and measured in a plain simulator, and this is that build. The class form of the whole agent is in section 7.
// spi_sequencer.sv
//
// Chapter 17.5 -- the agent's sequencer, which is the part of a UVM environment that can be built
// and measured in a plain simulator, and the part whose behaviour surprises people most.
//
// WHAT A SEQUENCER IS FOR.
//
// A sequence produces items in an order that means something -- configure, then a burst, then a
// read-back. A driver consumes one item at a time. A sequencer stands between N sequences and one
// driver, and it makes exactly ONE promise:
//
// within a sequence, items are delivered in the order the sequence produced them
//
// It promises nothing at all about the order ACROSS sequences, and that is not a gap in the
// specification. It is the whole point: the interleaving is chosen by an arbitration policy, and
// changing the policy changes which concurrency bugs a test can reach without changing a single
// item.
//
// THREE POLICIES, because the differences between them are invisible in a coverage report.
//
// ARB_RR round robin. Each sequence with an item pending gets the next grant in turn.
// ARB_PRIO strict priority. Sequence 0 wins whenever it has an item pending.
// ARB_WEIGHTED sequence 0 wins three grants in four.
//
// All three deliver the same items. All three produce identical functional coverage. They differ
// only in INTERLEAVING -- and a design bug that needs two sequences to overlap is reachable under
// one policy and unreachable under another, which is why "we ran both sequences" is not the same
// statement as "we ran both sequences together".
//
// AND STARVATION IS A FIRST-CLASS OUTPUT HERE.
//
// Strict priority starves the lower sequence for as long as the higher one has work. That is
// correct behaviour and it is also the most common way a test does less than its author thinks:
// the low-priority sequence's items all arrive after the high-priority sequence has finished, so
// the two never overlap and every interleaving bug is unreachable. A sequencer that does not
// COUNT this has no way to tell the author.
`timescale 1ns/1ps
module spi_sequencer #(
parameter int NSEQ = 2,
parameter int DEPTH = 16,
parameter int DW = 32,
parameter int CNT_W = 16
) (
input wire clk,
input wire rst_n,
input wire [1:0] arb, // 0 = round robin, 1 = strict priority, 2 = weighted
// Sequence 0 and sequence 1 push items. Each has its own queue, which is what keeps the
// one promise: a queue preserves order within a sequence whatever the arbiter does.
input wire s0_push,
input wire [DW-1:0] s0_item,
input wire s1_push,
input wire [DW-1:0] s1_item,
output wire s0_full,
output wire s1_full,
// The driver side: one item at a time.
input wire drv_ready,
output reg drv_valid,
output reg [DW-1:0] drv_item,
output reg drv_from, // which sequence the item came from
output reg [CNT_W-1:0] n_from_s0,
output reg [CNT_W-1:0] n_from_s1,
// THE LONGEST RUN of consecutive grants sequence 1 waited through while it had an item
// pending -- not the total.
//
// The total is the wrong measure and the difference is instructive. With both queues full,
// EVERY grant to sequence 0 is a grant sequence 1 did not get, so the total is the length of
// sequence 0 under every policy including round robin. The number that distinguishes the
// policies is how many in a ROW: one under round robin, three under a 3:1 weighting, and all
// of sequence 0 under strict priority. A starvation metric that cannot tell round robin from
// strict priority is measuring queue occupancy, not starvation.
output reg [CNT_W-1:0] s1_max_wait,
output reg [CNT_W-1:0] s0_max_wait
);
localparam [1:0] ARB_RR = 2'd0, ARB_PRIO = 2'd1, ARB_WEIGHTED = 2'd2;
reg [DW-1:0] q0 [0:DEPTH-1];
reg [DW-1:0] q1 [0:DEPTH-1];
reg [CNT_W-1:0] h0, t0, d0;
reg [CNT_W-1:0] h1, t1, d1;
assign s0_full = (d0 == DEPTH[CNT_W-1:0]);
assign s1_full = (d1 == DEPTH[CNT_W-1:0]);
wire have0 = (d0 != {CNT_W{1'b0}});
wire have1 = (d1 != {CNT_W{1'b0}});
reg last_grant; // for round robin
reg [1:0] w_phase; // for the weighted policy: three of four grants to sequence 0
// The current run of consecutive grants each sequence has been passed over for.
reg [CNT_W-1:0] s0_run, s1_run;
// The arbitration decision, as one expression per policy. Nothing about the QUEUES appears
// here, which is the separation that matters: the policy decides WHOSE turn it is and the
// queues decide WHICH item, so a policy change can never reorder a sequence.
wire pick0 =
(arb == ARB_PRIO) ? have0 :
(arb == ARB_WEIGHTED) ? (have0 && (w_phase != 2'd3 || !have1)) :
/* ARB_RR */ (have0 && (!have1 || last_grant == 1'b1));
wire grant = have0 | have1;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
h0 <= {CNT_W{1'b0}}; t0 <= {CNT_W{1'b0}}; d0 <= {CNT_W{1'b0}};
h1 <= {CNT_W{1'b0}}; t1 <= {CNT_W{1'b0}}; d1 <= {CNT_W{1'b0}};
drv_valid <= 1'b0;
drv_item <= {DW{1'b0}};
drv_from <= 1'b0;
n_from_s0 <= {CNT_W{1'b0}};
n_from_s1 <= {CNT_W{1'b0}};
s1_max_wait <= {CNT_W{1'b0}};
s0_max_wait <= {CNT_W{1'b0}};
s1_run <= {CNT_W{1'b0}};
s0_run <= {CNT_W{1'b0}};
last_grant <= 1'b1;
w_phase <= 2'd0;
end else begin
drv_valid <= 1'b0;
if (s0_push && !s0_full) begin
q0[t0] <= s0_item;
t0 <= (t0 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : t0 + 1'b1;
d0 <= d0 + 1'b1;
end
if (s1_push && !s1_full) begin
q1[t1] <= s1_item;
t1 <= (t1 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : t1 + 1'b1;
d1 <= d1 + 1'b1;
end
if (drv_ready && grant) begin
if (pick0) begin
drv_item <= q0[h0];
drv_from <= 1'b0;
drv_valid <= 1'b1;
h0 <= (h0 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : h0 + 1'b1;
d0 <= (s0_push && !s0_full) ? d0 : d0 - 1'b1;
n_from_s0 <= n_from_s0 + 1'b1;
last_grant <= 1'b0;
// STARVATION IS COUNTED AT THE GRANT, not inferred afterwards. Sequence 1 had
// an item pending and did not get this grant, so its run grows; sequence 0
// just got one, so its run ends.
if (have1) begin
s1_run <= s1_run + 1'b1;
if (s1_run + 1'b1 > s1_max_wait) s1_max_wait <= s1_run + 1'b1;
end
s0_run <= {CNT_W{1'b0}};
end else begin
drv_item <= q1[h1];
drv_from <= 1'b1;
drv_valid <= 1'b1;
h1 <= (h1 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : h1 + 1'b1;
d1 <= (s1_push && !s1_full) ? d1 : d1 - 1'b1;
n_from_s1 <= n_from_s1 + 1'b1;
last_grant <= 1'b1;
if (have0) begin
s0_run <= s0_run + 1'b1;
if (s0_run + 1'b1 > s0_max_wait) s0_max_wait <= s0_run + 1'b1;
end
s1_run <= {CNT_W{1'b0}};
end
w_phase <= (w_phase == 2'd3) ? 2'd0 : w_phase + 2'd1;
end
end
end
endmodule// spi_sequencer.v
//
// Chapter 17.5 -- the agent's sequencer, which is the part of a UVM environment that can be built
// and measured in a plain simulator, and the part whose behaviour surprises people most.
//
// WHAT A SEQUENCER IS FOR.
//
// A sequence produces items in an order that means something -- configure, then a burst, then a
// read-back. A driver consumes one item at a time. A sequencer stands between N sequences and one
// driver, and it makes exactly ONE promise:
//
// within a sequence, items are delivered in the order the sequence produced them
//
// It promises nothing at all about the order ACROSS sequences, and that is not a gap in the
// specification. It is the whole point: the interleaving is chosen by an arbitration policy, and
// changing the policy changes which concurrency bugs a test can reach without changing a single
// item.
//
// THREE POLICIES, because the differences between them are invisible in a coverage report.
//
// ARB_RR round robin. Each sequence with an item pending gets the next grant in turn.
// ARB_PRIO strict priority. Sequence 0 wins whenever it has an item pending.
// ARB_WEIGHTED sequence 0 wins three grants in four.
//
// All three deliver the same items. All three produce identical functional coverage. They differ
// only in INTERLEAVING -- and a design bug that needs two sequences to overlap is reachable under
// one policy and unreachable under another, which is why "we ran both sequences" is not the same
// statement as "we ran both sequences together".
//
// AND STARVATION IS A FIRST-CLASS OUTPUT HERE.
//
// Strict priority starves the lower sequence for as long as the higher one has work. That is
// correct behaviour and it is also the most common way a test does less than its author thinks:
// the low-priority sequence's items all arrive after the high-priority sequence has finished, so
// the two never overlap and every interleaving bug is unreachable. A sequencer that does not
// COUNT this has no way to tell the author.
`timescale 1ns/1ps
module spi_sequencer #(
parameter NSEQ = 2,
parameter DEPTH = 16,
parameter DW = 32,
parameter CNT_W = 16
) (
input wire clk,
input wire rst_n,
input wire [1:0] arb, // 0 = round robin, 1 = strict priority, 2 = weighted
// Sequence 0 and sequence 1 push items. Each has its own queue, which is what keeps the
// one promise: a queue preserves order within a sequence whatever the arbiter does.
input wire s0_push,
input wire [DW-1:0] s0_item,
input wire s1_push,
input wire [DW-1:0] s1_item,
output wire s0_full,
output wire s1_full,
// The driver side: one item at a time.
input wire drv_ready,
output reg drv_valid,
output reg [DW-1:0] drv_item,
output reg drv_from, // which sequence the item came from
output reg [CNT_W-1:0] n_from_s0,
output reg [CNT_W-1:0] n_from_s1,
// THE LONGEST RUN of consecutive grants sequence 1 waited through while it had an item
// pending -- not the total.
//
// The total is the wrong measure and the difference is instructive. With both queues full,
// EVERY grant to sequence 0 is a grant sequence 1 did not get, so the total is the length of
// sequence 0 under every policy including round robin. The number that distinguishes the
// policies is how many in a ROW: one under round robin, three under a 3:1 weighting, and all
// of sequence 0 under strict priority. A starvation metric that cannot tell round robin from
// strict priority is measuring queue occupancy, not starvation.
output reg [CNT_W-1:0] s1_max_wait,
output reg [CNT_W-1:0] s0_max_wait
);
localparam [1:0] ARB_RR = 2'd0, ARB_PRIO = 2'd1, ARB_WEIGHTED = 2'd2;
reg [DW-1:0] q0 [0:DEPTH-1];
reg [DW-1:0] q1 [0:DEPTH-1];
reg [CNT_W-1:0] h0, t0, d0;
reg [CNT_W-1:0] h1, t1, d1;
assign s0_full = (d0 == DEPTH[CNT_W-1:0]);
assign s1_full = (d1 == DEPTH[CNT_W-1:0]);
wire have0 = (d0 != {CNT_W{1'b0}});
wire have1 = (d1 != {CNT_W{1'b0}});
reg last_grant; // for round robin
reg [1:0] w_phase; // for the weighted policy: three of four grants to sequence 0
// The current run of consecutive grants each sequence has been passed over for.
reg [CNT_W-1:0] s0_run, s1_run;
// The arbitration decision, as one expression per policy. Nothing about the QUEUES appears
// here, which is the separation that matters: the policy decides WHOSE turn it is and the
// queues decide WHICH item, so a policy change can never reorder a sequence.
wire pick0 =
(arb == ARB_PRIO) ? have0 :
(arb == ARB_WEIGHTED) ? (have0 && (w_phase != 2'd3 || !have1)) :
/* ARB_RR */ (have0 && (!have1 || last_grant == 1'b1));
wire grant = have0 | have1;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
h0 <= {CNT_W{1'b0}}; t0 <= {CNT_W{1'b0}}; d0 <= {CNT_W{1'b0}};
h1 <= {CNT_W{1'b0}}; t1 <= {CNT_W{1'b0}}; d1 <= {CNT_W{1'b0}};
drv_valid <= 1'b0;
drv_item <= {DW{1'b0}};
drv_from <= 1'b0;
n_from_s0 <= {CNT_W{1'b0}};
n_from_s1 <= {CNT_W{1'b0}};
s1_max_wait <= {CNT_W{1'b0}};
s0_max_wait <= {CNT_W{1'b0}};
s1_run <= {CNT_W{1'b0}};
s0_run <= {CNT_W{1'b0}};
last_grant <= 1'b1;
w_phase <= 2'd0;
end else begin
drv_valid <= 1'b0;
if (s0_push && !s0_full) begin
q0[t0] <= s0_item;
t0 <= (t0 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : t0 + 1'b1;
d0 <= d0 + 1'b1;
end
if (s1_push && !s1_full) begin
q1[t1] <= s1_item;
t1 <= (t1 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : t1 + 1'b1;
d1 <= d1 + 1'b1;
end
if (drv_ready && grant) begin
if (pick0) begin
drv_item <= q0[h0];
drv_from <= 1'b0;
drv_valid <= 1'b1;
h0 <= (h0 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : h0 + 1'b1;
d0 <= (s0_push && !s0_full) ? d0 : d0 - 1'b1;
n_from_s0 <= n_from_s0 + 1'b1;
last_grant <= 1'b0;
// STARVATION IS COUNTED AT THE GRANT, not inferred afterwards. Sequence 1 had
// an item pending and did not get this grant, so its run grows; sequence 0
// just got one, so its run ends.
if (have1) begin
s1_run <= s1_run + 1'b1;
if (s1_run + 1'b1 > s1_max_wait) s1_max_wait <= s1_run + 1'b1;
end
s0_run <= {CNT_W{1'b0}};
end else begin
drv_item <= q1[h1];
drv_from <= 1'b1;
drv_valid <= 1'b1;
h1 <= (h1 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : h1 + 1'b1;
d1 <= (s1_push && !s1_full) ? d1 : d1 - 1'b1;
n_from_s1 <= n_from_s1 + 1'b1;
last_grant <= 1'b1;
if (have0) begin
s0_run <= s0_run + 1'b1;
if (s0_run + 1'b1 > s0_max_wait) s0_max_wait <= s0_run + 1'b1;
end
s1_run <= {CNT_W{1'b0}};
end
w_phase <= (w_phase == 2'd3) ? 2'd0 : w_phase + 2'd1;
end
end
end
endmodule-- spi_sequencer.vhd
--
-- Chapter 17.5 -- the agent's sequencer, which is the part of a UVM environment that can be built
-- and measured in a plain simulator, and the part whose behaviour surprises people most.
--
-- WHAT A SEQUENCER IS FOR.
--
-- A sequence produces items in an order that means something -- configure, then a burst, then a
-- read-back. A driver consumes one item at a time. A sequencer stands between N sequences and one
-- driver, and it makes exactly ONE promise:
--
-- within a sequence, items are delivered in the order the sequence produced them
--
-- It promises nothing at all about the order ACROSS sequences, and that is not a gap in the
-- specification. It is the whole point: the interleaving is chosen by an arbitration policy, and
-- changing the policy changes which concurrency bugs a test can reach without changing a single
-- item.
--
-- THREE POLICIES, because the differences between them are invisible in a coverage report.
--
-- ARB_RR round robin. Each sequence with an item pending gets the next grant in turn.
-- ARB_PRIO strict priority. Sequence 0 wins whenever it has an item pending.
-- ARB_WEIGHTED sequence 0 wins three grants in four.
--
-- All three deliver the same items. All three produce identical functional coverage. They differ
-- only in INTERLEAVING -- and a design bug that needs two sequences to overlap is reachable under
-- one policy and unreachable under another, which is why "we ran both sequences" is not the same
-- statement as "we ran both sequences together".
--
-- AND STARVATION IS A FIRST-CLASS OUTPUT HERE.
--
-- Strict priority starves the lower sequence for as long as the higher one has work. That is
-- correct behaviour and it is also the most common way a test does less than its author thinks:
-- the low-priority sequence's items all arrive after the high-priority sequence has finished, so
-- the two never overlap and every interleaving bug is unreachable. A sequencer that does not
-- COUNT this has no way to tell the author.
--
-- WHAT VHDL ADDS HERE: the arbitration policy is an ENUMERATION, so a policy added to the type
-- without a branch in the decision is a case-statement error at analysis time rather than a run in
-- which the new policy silently behaves like the default. That is the same argument Chapter 16.4
-- made about its fault codes and Chapter 17.4 about its constraint sets, and it is the third time
-- it has been worth the type declaration.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package spi_seqr_pkg is
-- A policy added here without a branch in the arbiter below is an analysis error.
type arb_t is (ARB_RR, ARB_PRIO, ARB_WEIGHTED);
end package spi_seqr_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_seqr_pkg.all;
entity spi_sequencer is
generic (
DEPTH : positive := 16;
DW : positive := 32
);
port (
clk : in std_logic;
rst_n : in std_logic;
arb : in arb_t;
-- Sequence 0 and sequence 1 push items. Each has its own queue, which is what keeps the
-- one promise: a queue preserves order within a sequence whatever the arbiter does.
s0_push : in std_logic;
s0_item : in std_logic_vector(DW - 1 downto 0);
s1_push : in std_logic;
s1_item : in std_logic_vector(DW - 1 downto 0);
s0_full : out std_logic;
s1_full : out std_logic;
-- The driver side: one item at a time.
drv_ready : in std_logic;
drv_valid : out std_logic;
drv_item : out std_logic_vector(DW - 1 downto 0);
drv_from : out natural;
n_from_s0 : out natural;
n_from_s1 : out natural;
-- THE LONGEST RUN of consecutive grants each sequence waited through while it had an item
-- pending -- not the total. With both queues full, every grant to sequence 0 is a grant
-- sequence 1 did not get, so the TOTAL is the length of sequence 0 under every policy
-- including round robin. The number that distinguishes the policies is how many in a ROW.
s1_max_wait : out natural;
s0_max_wait : out natural
);
end entity spi_sequencer;
architecture rtl of spi_sequencer is
type q_t is array (0 to DEPTH - 1) of std_logic_vector(DW - 1 downto 0);
signal dv_r : std_logic := '0';
signal di_r : std_logic_vector(DW - 1 downto 0) := (others => '0');
signal df_r : natural := 0;
signal n0_r, n1_r : natural := 0;
signal w0_r, w1_r : natural := 0;
signal f0_r, f1_r : std_logic := '0';
begin
drv_valid <= dv_r;
drv_item <= di_r;
drv_from <= df_r;
n_from_s0 <= n0_r;
n_from_s1 <= n1_r;
s0_max_wait <= w0_r;
s1_max_wait <= w1_r;
s0_full <= f0_r;
s1_full <= f1_r;
process (clk, rst_n) is
variable q0, q1 : q_t := (others => (others => '0'));
variable h0, t0, d0 : natural := 0;
variable h1, t1, d1 : natural := 0;
variable have0, have1 : boolean;
variable pick0 : boolean;
variable last_grant : natural := 1;
variable w_phase : natural := 0;
variable r0, r1 : natural := 0;
variable pushed0, pushed1 : boolean;
begin
if rst_n = '0' then
h0 := 0; t0 := 0; d0 := 0;
h1 := 0; t1 := 0; d1 := 0;
last_grant := 1;
w_phase := 0;
r0 := 0; r1 := 0;
dv_r <= '0';
di_r <= (others => '0');
df_r <= 0;
n0_r <= 0; n1_r <= 0;
w0_r <= 0; w1_r <= 0;
elsif rising_edge(clk) then
dv_r <= '0';
pushed0 := (s0_push = '1') and (d0 < DEPTH);
pushed1 := (s1_push = '1') and (d1 < DEPTH);
if pushed0 then
q0(t0) := s0_item;
if t0 = DEPTH - 1 then t0 := 0; else t0 := t0 + 1; end if;
d0 := d0 + 1;
end if;
if pushed1 then
q1(t1) := s1_item;
if t1 = DEPTH - 1 then t1 := 0; else t1 := t1 + 1; end if;
d1 := d1 + 1;
end if;
have0 := d0 > 0;
have1 := d1 > 0;
-- The arbitration decision, one branch per policy. Nothing about the QUEUES appears
-- here, which is the separation that matters: the policy decides WHOSE turn it is and
-- the queues decide WHICH item, so a policy change can never reorder a sequence.
case arb is
when ARB_PRIO => pick0 := have0;
when ARB_WEIGHTED => pick0 := have0 and ((w_phase /= 3) or not have1);
when ARB_RR => pick0 := have0 and ((not have1) or (last_grant = 1));
end case;
if drv_ready = '1' and (have0 or have1) then
if pick0 then
di_r <= q0(h0);
df_r <= 0;
dv_r <= '1';
if h0 = DEPTH - 1 then h0 := 0; else h0 := h0 + 1; end if;
d0 := d0 - 1;
n0_r <= n0_r + 1;
last_grant := 0;
-- STARVATION IS COUNTED AT THE GRANT, not inferred afterwards. Sequence 1 had
-- an item pending and did not get this grant, so its run grows; sequence 0
-- just got one, so its run ends.
if have1 then
r1 := r1 + 1;
if r1 > w1_r then w1_r <= r1; end if;
end if;
r0 := 0;
else
di_r <= q1(h1);
df_r <= 1;
dv_r <= '1';
if h1 = DEPTH - 1 then h1 := 0; else h1 := h1 + 1; end if;
d1 := d1 - 1;
n1_r <= n1_r + 1;
last_grant := 1;
if have0 then
r0 := r0 + 1;
if r0 > w0_r then w0_r <= r0; end if;
end if;
r1 := 0;
end if;
if w_phase = 3 then w_phase := 0; else w_phase := w_phase + 1; end if;
end if;
if d0 >= DEPTH then f0_r <= '1'; else f0_r <= '0'; end if;
if d1 >= DEPTH then f1_r <= '1'; else f1_r <= '0'; end if;
end if;
end process;
end architecture rtl;The Bench
// spi_sequencer_tb.sv
//
// TWO SEQUENCES, ONE DRIVER, THREE ARBITRATION POLICIES, AND FOUR MEASUREMENTS.
//
// Each sequence pushes a numbered stream -- sequence 0 sends 0x0000, 0x0001, ... and sequence 1
// sends 0x1000, 0x1001, ... -- so the delivered stream can be checked for two completely
// different properties at once:
//
// ORDER within each sequence, the numbers must arrive ascending with no gaps.
// OVERLAP across the sequences, how the two streams interleave.
//
// 1. ORDER IS PRESERVED UNDER EVERY POLICY. Zero order violations in all three, which is the
// sequencer's one promise. A policy that could reorder a sequence would make every directed
// sequence in the suite meaningless, and the property is cheap to check and almost never is.
//
// 2. ALL THREE POLICIES DELIVER THE SAME ITEMS. Identical counts from both sequences. So every
// functional coverage number is identical across the three runs -- which is the measurement
// that matters, because it means a coverage report cannot tell you which policy ran.
//
// 3. THE INTERLEAVING IS COMPLETELY DIFFERENT. Measured as the number of times the delivered
// stream switches from one sequence to the other. Round robin switches on almost every item;
// strict priority switches once. Same items, same coverage, and a concurrency bug that needs
// the two streams to overlap is reachable under one and unreachable under the other.
//
// 4. STARVATION IS COUNTED, NOT INFERRED. Under strict priority, sequence 1 waits through every
// one of sequence 0's grants. The number is reported by the sequencer itself, because a test
// whose second sequence only ran after the first one finished has done less than its author
// believes and nothing else in the report says so.
`timescale 1ns/1ps
module spi_sequencer_tb;
localparam int DW = 32;
localparam int CNT_W = 16;
localparam int NITEM = 12; // items pushed by each sequence
localparam [1:0] ARB_RR = 2'd0, ARB_PRIO = 2'd1, ARB_WEIGHTED = 2'd2;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b1;
reg [1:0] arb = ARB_RR;
reg s0_push = 1'b0, s1_push = 1'b0;
reg [DW-1:0] s0_item = {DW{1'b0}}, s1_item = {DW{1'b0}};
wire s0_full, s1_full;
reg drv_ready = 1'b1;
wire drv_valid, drv_from;
wire [DW-1:0] drv_item;
wire [CNT_W-1:0] n_from_s0, n_from_s1, s0_max_wait, s1_max_wait;
spi_sequencer #(.NSEQ(2), .DEPTH(16), .DW(DW), .CNT_W(CNT_W)) u_s (
.clk(clk), .rst_n(rst_n), .arb(arb),
.s0_push(s0_push), .s0_item(s0_item), .s1_push(s1_push), .s1_item(s1_item),
.s0_full(s0_full), .s1_full(s1_full),
.drv_ready(drv_ready), .drv_valid(drv_valid), .drv_item(drv_item), .drv_from(drv_from),
.n_from_s0(n_from_s0), .n_from_s1(n_from_s1),
.s1_max_wait(s1_max_wait), .s0_max_wait(s0_max_wait)
);
integer errors = 0;
initial begin
#400_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
// ------------------------------------------------------------------
// The observer of the delivered stream. It checks ORDER and counts SWITCHES, which are the
// two independent properties of an interleaving.
// ------------------------------------------------------------------
integer exp0, exp1, order_bad, switches, delivered;
reg last_from;
reg seen_any;
always @(posedge clk) if (rst_n && drv_valid) begin
delivered = delivered + 1;
if (drv_from == 1'b0) begin
if (drv_item !== exp0[DW-1:0]) order_bad = order_bad + 1;
exp0 = exp0 + 1;
end else begin
if (drv_item !== exp1[DW-1:0]) order_bad = order_bad + 1;
exp1 = exp1 + 1;
end
if (seen_any && drv_from !== last_from) switches = switches + 1;
last_from = drv_from;
seen_any = 1'b1;
end
task automatic reset_all;
begin
@(negedge clk);
rst_n = 1'b1;
@(negedge clk);
rst_n = 1'b0;
repeat (4) @(negedge clk);
rst_n = 1'b1;
repeat (2) @(negedge clk);
exp0 = 0; exp1 = 32'h1000; order_bad = 0; switches = 0; delivered = 0;
seen_any = 1'b0; last_from = 1'b0;
end
endtask
// Pushes both sequences' items as fast as the queues accept them, then lets the driver drain.
// Both sequences are made ready BEFORE any grant, because an interleaving experiment in which
// one sequence arrives late measures the arrival times rather than the policy.
task automatic run_policy(input [1:0] a);
integer i;
begin
arb = a;
reset_all();
drv_ready = 1'b0; // hold the driver off while both queues fill
for (i = 0; i < NITEM; i = i + 1) begin
@(negedge clk);
s0_push = 1'b1; s0_item = i[DW-1:0];
s1_push = 1'b1; s1_item = 32'h1000 + i[DW-1:0];
@(negedge clk);
s0_push = 1'b0; s1_push = 1'b0;
end
drv_ready = 1'b1;
// Drain, with a bound: a sequencer that stops granting must not hang the bench.
for (i = 0; i < 200 && delivered < 2*NITEM; i = i + 1) @(negedge clk);
repeat (4) @(negedge clk);
end
endtask
integer rr_s0, rr_s1, rr_sw, rr_bad, rr_starv;
integer pr_s0, pr_s1, pr_sw, pr_bad, pr_starv;
integer wt_s0, wt_s1, wt_sw, wt_bad, wt_starv;
integer pr_first_s1_at;
initial begin
// ============================================================
// Round robin.
// ============================================================
run_policy(ARB_RR);
rr_s0 = n_from_s0; rr_s1 = n_from_s1; rr_sw = switches; rr_bad = order_bad;
rr_starv = s1_max_wait;
// ============================================================
// Strict priority.
// ============================================================
run_policy(ARB_PRIO);
pr_s0 = n_from_s0; pr_s1 = n_from_s1; pr_sw = switches; pr_bad = order_bad;
pr_starv = s1_max_wait;
// ============================================================
// Weighted.
// ============================================================
run_policy(ARB_WEIGHTED);
wt_s0 = n_from_s0; wt_s1 = n_from_s1; wt_sw = switches; wt_bad = order_bad;
wt_starv = s1_max_wait;
$display(" %0d items pushed by each sequence, both queues full before the first grant",
NITEM);
$display(" policy from s0 from s1 order errors switches s1 longest wait");
$display(" round robin %7d %7d %12d %8d %10d", rr_s0, rr_s1, rr_bad, rr_sw, rr_starv);
$display(" strict priority %7d %7d %12d %8d %10d", pr_s0, pr_s1, pr_bad, pr_sw, pr_starv);
$display(" weighted 3:1 %7d %7d %12d %8d %10d", wt_s0, wt_s1, wt_bad, wt_sw, wt_starv);
// ---- 1. order ----
if (rr_bad != 0 || pr_bad != 0 || wt_bad != 0) begin
$display(" FAIL: a policy reordered a sequence (%0d, %0d, %0d order errors)",
rr_bad, pr_bad, wt_bad);
errors = errors + 1;
end
$display(" 1. zero order errors under all three policies. That is the sequencer's ONE promise -- items within a sequence arrive in the order the sequence produced them -- and it is what makes a directed sequence mean anything. The policy decides whose turn it is; the per-sequence queue decides which item, so no policy change can reorder a sequence");
// ---- 2. same items ----
if (rr_s0 != NITEM || rr_s1 != NITEM || pr_s0 != NITEM || pr_s1 != NITEM
|| wt_s0 != NITEM || wt_s1 != NITEM) begin
$display(" FAIL: the policies did not all deliver every item");
errors = errors + 1;
end
$display(" 2. all three policies delivered every one of the %0d items from both sequences. So every functional coverage number is IDENTICAL across the three runs -- which is the point: a coverage report cannot tell you which policy ran, and therefore cannot tell you whether the two sequences ever overlapped",
NITEM);
// ---- 3. different interleaving ----
if (pr_sw >= rr_sw) begin
$display(" FAIL: strict priority switched between sequences at least as often as round robin (%0d vs %0d), so the interleaving experiment measured nothing",
pr_sw, rr_sw);
errors = errors + 1;
end
if (pr_sw != 1) begin
$display(" FAIL: strict priority switched %0d times; with both queues full it should switch exactly once, when sequence 0 runs out",
pr_sw);
errors = errors + 1;
end
$display(" 3. round robin switched between the two streams %0d times; strict priority switched %0d. Same items, same coverage, and completely different traffic: under priority, sequence 1's items ALL arrive after sequence 0 has finished, so the two never overlap and any design bug that needs them to overlap is unreachable. `we ran both sequences` and `we ran both sequences together` are different claims",
rr_sw, pr_sw);
// ---- 4. starvation counted ----
if (rr_starv != 1) begin
$display(" FAIL: under round robin sequence 1's longest wait should be a single grant, not %0d",
rr_starv);
errors = errors + 1;
end
if (pr_starv != NITEM) begin
$display(" FAIL: under strict priority sequence 1 should have waited through all %0d of sequence 0's grants in a row, not %0d",
NITEM, pr_starv);
errors = errors + 1;
end
if (wt_starv <= rr_starv || wt_starv >= pr_starv) begin
$display(" FAIL: the weighted policy's longest wait (%0d) should sit between round robin's (%0d) and priority's (%0d)",
wt_starv, rr_starv, pr_starv);
errors = errors + 1;
end
$display(" 4. sequence 1's LONGEST consecutive wait was %0d grant under round robin, %0d under weighted 3:1, and %0d under strict priority -- all of sequence 0. Note that the TOTAL number of grants it was passed over for is %0d under every policy, because with both queues full every grant to sequence 0 is one sequence 1 did not get. A starvation metric that counts the total cannot tell round robin from strict priority; it is measuring queue occupancy",
rr_starv, wt_starv, pr_starv, NITEM);
if (errors == 0)
$display("PASS: a sequencer makes exactly one promise -- that items within a sequence are delivered in the order the sequence produced them -- and it deliberately promises nothing about the order across sequences, because that is what an arbitration policy is for. Measured with %0d items from each of two sequences and both queues full before the first grant: zero order errors under all three policies, so the promise holds; every policy delivered all %0d items from both sequences, so every functional coverage number is IDENTICAL across the three runs and a coverage report cannot tell you which policy ran. What differs is the INTERLEAVING -- round robin switched between the streams %0d times and strict priority switched %0d, because under priority sequence 1's items all arrive after sequence 0 has finished. The two never overlap, and any design bug that needs them to overlap is unreachable, which is why `we ran both sequences` and `we ran both sequences together` are different claims. And starvation is COUNTED rather than inferred, as the LONGEST consecutive wait rather than the total: %0d grant under round robin, %0d under weighted, and all %0d of sequence 0's under priority. The total would have been %0d under every policy -- with both queues full, every grant to sequence 0 is one sequence 1 did not get -- so a starvation metric built on the total cannot tell round robin from strict priority and is really measuring queue occupancy. The sequencer reports the number itself, because nothing else in a suite's output distinguishes a test that overlapped its sequences from one that queued them",
NITEM, NITEM, rr_sw, pr_sw, rr_starv, wt_starv, pr_starv, NITEM);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_sequencer_tb.v
//
// TWO SEQUENCES, ONE DRIVER, THREE ARBITRATION POLICIES, AND FOUR MEASUREMENTS.
//
// Each sequence pushes a numbered stream -- sequence 0 sends 0x0000, 0x0001, ... and sequence 1
// sends 0x1000, 0x1001, ... -- so the delivered stream can be checked for two completely
// different properties at once:
//
// ORDER within each sequence, the numbers must arrive ascending with no gaps.
// OVERLAP across the sequences, how the two streams interleave.
//
// 1. ORDER IS PRESERVED UNDER EVERY POLICY. Zero order violations in all three, which is the
// sequencer's one promise. A policy that could reorder a sequence would make every directed
// sequence in the suite meaningless, and the property is cheap to check and almost never is.
//
// 2. ALL THREE POLICIES DELIVER THE SAME ITEMS. Identical counts from both sequences. So every
// functional coverage number is identical across the three runs -- which is the measurement
// that matters, because it means a coverage report cannot tell you which policy ran.
//
// 3. THE INTERLEAVING IS COMPLETELY DIFFERENT. Measured as the number of times the delivered
// stream switches from one sequence to the other. Round robin switches on almost every item;
// strict priority switches once. Same items, same coverage, and a concurrency bug that needs
// the two streams to overlap is reachable under one and unreachable under the other.
//
// 4. STARVATION IS COUNTED, NOT INFERRED. Under strict priority, sequence 1 waits through every
// one of sequence 0's grants. The number is reported by the sequencer itself, because a test
// whose second sequence only ran after the first one finished has done less than its author
// believes and nothing else in the report says so.
`timescale 1ns/1ps
module spi_sequencer_tb;
localparam DW = 32;
localparam CNT_W = 16;
localparam NITEM = 12; // items pushed by each sequence
localparam [1:0] ARB_RR = 2'd0, ARB_PRIO = 2'd1, ARB_WEIGHTED = 2'd2;
reg clk;
always #5 clk = ~clk;
reg rst_n;
reg [1:0] arb;
reg s0_push, s1_push;
reg [DW-1:0] s0_item, s1_item;
wire s0_full, s1_full;
reg drv_ready;
wire drv_valid, drv_from;
wire [DW-1:0] drv_item;
wire [CNT_W-1:0] n_from_s0, n_from_s1, s0_max_wait, s1_max_wait;
spi_sequencer #(.NSEQ(2), .DEPTH(16), .DW(DW), .CNT_W(CNT_W)) u_s (
.clk(clk), .rst_n(rst_n), .arb(arb),
.s0_push(s0_push), .s0_item(s0_item), .s1_push(s1_push), .s1_item(s1_item),
.s0_full(s0_full), .s1_full(s1_full),
.drv_ready(drv_ready), .drv_valid(drv_valid), .drv_item(drv_item), .drv_from(drv_from),
.n_from_s0(n_from_s0), .n_from_s1(n_from_s1),
.s1_max_wait(s1_max_wait), .s0_max_wait(s0_max_wait)
);
integer errors;
initial begin
#400_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
// ------------------------------------------------------------------
// The observer of the delivered stream. It checks ORDER and counts SWITCHES, which are the
// two independent properties of an interleaving.
// ------------------------------------------------------------------
integer exp0, exp1, order_bad, switches, delivered;
reg last_from;
reg seen_any;
always @(posedge clk) if (rst_n && drv_valid) begin
delivered = delivered + 1;
if (drv_from == 1'b0) begin
if (drv_item !== exp0[DW-1:0]) order_bad = order_bad + 1;
exp0 = exp0 + 1;
end else begin
if (drv_item !== exp1[DW-1:0]) order_bad = order_bad + 1;
exp1 = exp1 + 1;
end
if (seen_any && drv_from !== last_from) switches = switches + 1;
last_from = drv_from;
seen_any = 1'b1;
end
task reset_all;
begin
@(negedge clk);
rst_n = 1'b1;
@(negedge clk);
rst_n = 1'b0;
repeat (4) @(negedge clk);
rst_n = 1'b1;
repeat (2) @(negedge clk);
exp0 = 0; exp1 = 32'h1000; order_bad = 0; switches = 0; delivered = 0;
seen_any = 1'b0; last_from = 1'b0;
end
endtask
// Pushes both sequences' items as fast as the queues accept them, then lets the driver drain.
// Both sequences are made ready BEFORE any grant, because an interleaving experiment in which
// one sequence arrives late measures the arrival times rather than the policy.
task run_policy;
input [1:0] a;
integer i;
begin
arb = a;
reset_all();
drv_ready = 1'b0; // hold the driver off while both queues fill
for (i = 0; i < NITEM; i = i + 1) begin
@(negedge clk);
s0_push = 1'b1; s0_item = i[DW-1:0];
s1_push = 1'b1; s1_item = 32'h1000 + i[DW-1:0];
@(negedge clk);
s0_push = 1'b0; s1_push = 1'b0;
end
drv_ready = 1'b1;
// Drain, with a bound: a sequencer that stops granting must not hang the bench.
for (i = 0; i < 200 && delivered < 2*NITEM; i = i + 1) @(negedge clk);
repeat (4) @(negedge clk);
end
endtask
integer rr_s0, rr_s1, rr_sw, rr_bad, rr_starv;
integer pr_s0, pr_s1, pr_sw, pr_bad, pr_starv;
integer wt_s0, wt_s1, wt_sw, wt_bad, wt_starv;
integer pr_first_s1_at;
initial begin
// ============================================================
// Round robin.
// ============================================================
run_policy(ARB_RR);
rr_s0 = n_from_s0; rr_s1 = n_from_s1; rr_sw = switches; rr_bad = order_bad;
rr_starv = s1_max_wait;
// ============================================================
// Strict priority.
// ============================================================
run_policy(ARB_PRIO);
pr_s0 = n_from_s0; pr_s1 = n_from_s1; pr_sw = switches; pr_bad = order_bad;
pr_starv = s1_max_wait;
// ============================================================
// Weighted.
// ============================================================
run_policy(ARB_WEIGHTED);
wt_s0 = n_from_s0; wt_s1 = n_from_s1; wt_sw = switches; wt_bad = order_bad;
wt_starv = s1_max_wait;
$display(" %0d items pushed by each sequence, both queues full before the first grant",
NITEM);
$display(" policy from s0 from s1 order errors switches s1 longest wait");
$display(" round robin %7d %7d %12d %8d %10d", rr_s0, rr_s1, rr_bad, rr_sw, rr_starv);
$display(" strict priority %7d %7d %12d %8d %10d", pr_s0, pr_s1, pr_bad, pr_sw, pr_starv);
$display(" weighted 3:1 %7d %7d %12d %8d %10d", wt_s0, wt_s1, wt_bad, wt_sw, wt_starv);
// ---- 1. order ----
if (rr_bad != 0 || pr_bad != 0 || wt_bad != 0) begin
$display(" FAIL: a policy reordered a sequence (%0d, %0d, %0d order errors)",
rr_bad, pr_bad, wt_bad);
errors = errors + 1;
end
$display(" 1. zero order errors under all three policies. That is the sequencer's ONE promise -- items within a sequence arrive in the order the sequence produced them -- and it is what makes a directed sequence mean anything. The policy decides whose turn it is; the per-sequence queue decides which item, so no policy change can reorder a sequence");
// ---- 2. same items ----
if (rr_s0 != NITEM || rr_s1 != NITEM || pr_s0 != NITEM || pr_s1 != NITEM
|| wt_s0 != NITEM || wt_s1 != NITEM) begin
$display(" FAIL: the policies did not all deliver every item");
errors = errors + 1;
end
$display(" 2. all three policies delivered every one of the %0d items from both sequences. So every functional coverage number is IDENTICAL across the three runs -- which is the point: a coverage report cannot tell you which policy ran, and therefore cannot tell you whether the two sequences ever overlapped",
NITEM);
// ---- 3. different interleaving ----
if (pr_sw >= rr_sw) begin
$display(" FAIL: strict priority switched between sequences at least as often as round robin (%0d vs %0d), so the interleaving experiment measured nothing",
pr_sw, rr_sw);
errors = errors + 1;
end
if (pr_sw != 1) begin
$display(" FAIL: strict priority switched %0d times; with both queues full it should switch exactly once, when sequence 0 runs out",
pr_sw);
errors = errors + 1;
end
$display(" 3. round robin switched between the two streams %0d times; strict priority switched %0d. Same items, same coverage, and completely different traffic: under priority, sequence 1's items ALL arrive after sequence 0 has finished, so the two never overlap and any design bug that needs them to overlap is unreachable. `we ran both sequences` and `we ran both sequences together` are different claims",
rr_sw, pr_sw);
// ---- 4. starvation counted ----
if (rr_starv != 1) begin
$display(" FAIL: under round robin sequence 1's longest wait should be a single grant, not %0d",
rr_starv);
errors = errors + 1;
end
if (pr_starv != NITEM) begin
$display(" FAIL: under strict priority sequence 1 should have waited through all %0d of sequence 0's grants in a row, not %0d",
NITEM, pr_starv);
errors = errors + 1;
end
if (wt_starv <= rr_starv || wt_starv >= pr_starv) begin
$display(" FAIL: the weighted policy's longest wait (%0d) should sit between round robin's (%0d) and priority's (%0d)",
wt_starv, rr_starv, pr_starv);
errors = errors + 1;
end
$display(" 4. sequence 1's LONGEST consecutive wait was %0d grant under round robin, %0d under weighted 3:1, and %0d under strict priority -- all of sequence 0. Note that the TOTAL number of grants it was passed over for is %0d under every policy, because with both queues full every grant to sequence 0 is one sequence 1 did not get. A starvation metric that counts the total cannot tell round robin from strict priority; it is measuring queue occupancy",
rr_starv, wt_starv, pr_starv, NITEM);
if (errors == 0)
$display("PASS: a sequencer makes exactly one promise -- that items within a sequence are delivered in the order the sequence produced them -- and it deliberately promises nothing about the order across sequences, because that is what an arbitration policy is for. Measured with %0d items from each of two sequences and both queues full before the first grant: zero order errors under all three policies, so the promise holds; every policy delivered all %0d items from both sequences, so every functional coverage number is IDENTICAL across the three runs and a coverage report cannot tell you which policy ran. What differs is the INTERLEAVING -- round robin switched between the streams %0d times and strict priority switched %0d, because under priority sequence 1's items all arrive after sequence 0 has finished. The two never overlap, and any design bug that needs them to overlap is unreachable, which is why `we ran both sequences` and `we ran both sequences together` are different claims. And starvation is COUNTED rather than inferred, as the LONGEST consecutive wait rather than the total: %0d grant under round robin, %0d under weighted, and all %0d of sequence 0's under priority. The total would have been %0d under every policy -- with both queues full, every grant to sequence 0 is one sequence 1 did not get -- so a starvation metric built on the total cannot tell round robin from strict priority and is really measuring queue occupancy. The sequencer reports the number itself, because nothing else in a suite's output distinguishes a test that overlapped its sequences from one that queued them",
NITEM, NITEM, rr_sw, pr_sw, rr_starv, wt_starv, pr_starv, NITEM);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
s0_push = 1'b0;
s1_push = 1'b0;
s0_item = {DW{1'b0}};
s1_item = {DW{1'b0}};
clk = 1'b0;
rst_n = 1'b1;
arb = ARB_RR;
drv_ready = 1'b1;
errors = 0;
end
endmodule-- spi_sequencer_tb.vhd
--
-- TWO SEQUENCES, ONE DRIVER, THREE ARBITRATION POLICIES, AND FOUR MEASUREMENTS.
--
-- Each sequence pushes a numbered stream -- sequence 0 sends 0x0000, 0x0001, ... and sequence 1
-- sends 0x1000, 0x1001, ... -- so the delivered stream can be checked for two completely
-- different properties at once:
--
-- ORDER within each sequence, the numbers must arrive ascending with no gaps.
-- OVERLAP across the sequences, how the two streams interleave.
--
-- 1. ORDER IS PRESERVED UNDER EVERY POLICY. Zero order violations in all three, which is the
-- sequencer's one promise. A policy that could reorder a sequence would make every directed
-- sequence in the suite meaningless, and the property is cheap to check and almost never is.
--
-- 2. ALL THREE POLICIES DELIVER THE SAME ITEMS. Identical counts from both sequences. So every
-- functional coverage number is identical across the three runs -- which is the measurement
-- that matters, because it means a coverage report cannot tell you which policy ran.
--
-- 3. THE INTERLEAVING IS COMPLETELY DIFFERENT. Measured as the number of times the delivered
-- stream switches from one sequence to the other. Round robin switches on almost every item;
-- strict priority switches once. Same items, same coverage, and a concurrency bug that needs
-- the two streams to overlap is reachable under one and unreachable under the other.
--
-- 4. STARVATION IS COUNTED, NOT INFERRED. Under strict priority, sequence 1 waits through every
-- one of sequence 0's grants. The number is reported by the sequencer itself, because a test
-- whose second sequence only ran after the first one finished has done less than its author
-- believes and nothing else in the report says so.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_seqr_pkg.all;
entity spi_sequencer_tb is
end entity spi_sequencer_tb;
architecture tb of spi_sequencer_tb is
constant DW : positive := 32;
constant NITEM : natural := 12; -- items pushed by each sequence
constant HALF_T : time := 5 ns;
signal clk : std_logic := '0';
signal rst_n : std_logic := '1';
signal done_sim : boolean := false;
signal arb : arb_t := ARB_RR;
signal s0_push : std_logic := '0';
signal s1_push : std_logic := '0';
signal s0_item : std_logic_vector(DW - 1 downto 0) := (others => '0');
signal s1_item : std_logic_vector(DW - 1 downto 0) := (others => '0');
signal s0_full, s1_full : std_logic;
signal drv_ready : std_logic := '1';
signal drv_valid : std_logic;
signal drv_item : std_logic_vector(DW - 1 downto 0);
signal drv_from : natural;
signal n_from_s0, n_from_s1, s0_max_wait, s1_max_wait : natural;
-- The observer of the delivered stream. It checks ORDER and counts SWITCHES, which are the
-- two independent properties of an interleaving.
signal obs_clr : boolean := false;
signal order_bad : natural := 0;
signal switches : natural := 0;
signal delivered : natural := 0;
signal errors : integer := 0;
begin
clk_gen : process is
begin
while not done_sim loop
wait for HALF_T;
clk <= not clk;
end loop;
wait;
end process clk_gen;
u_s : entity work.spi_sequencer
generic map (DEPTH => 16, DW => DW)
port map (clk => clk, rst_n => rst_n, arb => arb,
s0_push => s0_push, s0_item => s0_item,
s1_push => s1_push, s1_item => s1_item,
s0_full => s0_full, s1_full => s1_full,
drv_ready => drv_ready, drv_valid => drv_valid,
drv_item => drv_item, drv_from => drv_from,
n_from_s0 => n_from_s0, n_from_s1 => n_from_s1,
s1_max_wait => s1_max_wait, s0_max_wait => s0_max_wait);
observe : process (clk) is
variable exp0, exp1 : natural := 0;
variable last_from : natural := 0;
variable seen_any : boolean := false;
begin
if rising_edge(clk) then
if obs_clr then
exp0 := 0; exp1 := 16#1000#;
last_from := 0; seen_any := false;
order_bad <= 0; switches <= 0; delivered <= 0;
elsif rst_n = '1' and drv_valid = '1' then
delivered <= delivered + 1;
if drv_from = 0 then
if to_integer(unsigned(drv_item)) /= exp0 then
order_bad <= order_bad + 1;
end if;
exp0 := exp0 + 1;
else
if to_integer(unsigned(drv_item)) /= exp1 then
order_bad <= order_bad + 1;
end if;
exp1 := exp1 + 1;
end if;
if seen_any and drv_from /= last_from then
switches <= switches + 1;
end if;
last_from := drv_from;
seen_any := true;
end if;
end if;
end process observe;
main : process is
procedure idle_n (n : natural) is
begin
for i in 1 to n loop wait until falling_edge(clk); end loop;
end procedure idle_n;
procedure reset_all is
begin
wait until falling_edge(clk);
rst_n <= '1';
wait until falling_edge(clk);
rst_n <= '0';
idle_n(4);
obs_clr <= true;
wait until falling_edge(clk);
obs_clr <= false;
rst_n <= '1';
idle_n(2);
end procedure reset_all;
-- Pushes both sequences' items as fast as the queues accept them, then lets the driver
-- drain. Both sequences are made ready BEFORE any grant, because an interleaving
-- experiment in which one sequence arrives late measures the arrival times rather than
-- the policy.
procedure run_policy (a : arb_t) is
begin
arb <= a;
reset_all;
drv_ready <= '0'; -- hold the driver off while both queues fill
for i in 0 to NITEM - 1 loop
wait until falling_edge(clk);
s0_push <= '1'; s0_item <= std_logic_vector(to_unsigned(i, DW));
s1_push <= '1'; s1_item <= std_logic_vector(to_unsigned(16#1000# + i, DW));
wait until falling_edge(clk);
s0_push <= '0'; s1_push <= '0';
end loop;
drv_ready <= '1';
-- Drain, with a bound: a sequencer that stops granting must not hang the bench.
for i in 1 to 200 loop
exit when delivered >= 2 * NITEM;
wait until falling_edge(clk);
end loop;
idle_n(4);
end procedure run_policy;
variable rr_s0, rr_s1, rr_sw, rr_bad, rr_wait : natural;
variable pr_s0, pr_s1, pr_sw, pr_bad, pr_wait : natural;
variable wt_s0, wt_s1, wt_sw, wt_bad, wt_wait : natural;
begin
run_policy(ARB_RR);
rr_s0 := n_from_s0; rr_s1 := n_from_s1; rr_sw := switches;
rr_bad := order_bad; rr_wait := s1_max_wait;
run_policy(ARB_PRIO);
pr_s0 := n_from_s0; pr_s1 := n_from_s1; pr_sw := switches;
pr_bad := order_bad; pr_wait := s1_max_wait;
run_policy(ARB_WEIGHTED);
wt_s0 := n_from_s0; wt_s1 := n_from_s1; wt_sw := switches;
wt_bad := order_bad; wt_wait := s1_max_wait;
report " " & integer'image(NITEM) &
" items pushed by each sequence, both queues full before the first grant";
report " policy from s0 from s1 order errors switches s1 longest wait";
report " round robin " & integer'image(rr_s0) & " " & integer'image(rr_s1) &
" " & integer'image(rr_bad) & " " & integer'image(rr_sw) & " " &
integer'image(rr_wait);
report " strict priority " & integer'image(pr_s0) & " " & integer'image(pr_s1) &
" " & integer'image(pr_bad) & " " & integer'image(pr_sw) & " " &
integer'image(pr_wait);
report " weighted 3:1 " & integer'image(wt_s0) & " " & integer'image(wt_s1) &
" " & integer'image(wt_bad) & " " & integer'image(wt_sw) & " " &
integer'image(wt_wait);
if rr_bad /= 0 or pr_bad /= 0 or wt_bad /= 0 then
report " FAIL: a policy reordered a sequence";
errors <= errors + 1; wait for 1 ns;
end if;
report " 1. zero order errors under all three policies. That is the sequencer's ONE promise -- items within a sequence arrive in the order the sequence produced them -- and it is what makes a directed sequence mean anything. The policy decides whose turn it is; the per-sequence queue decides which item, so no policy change can reorder a sequence";
if rr_s0 /= NITEM or rr_s1 /= NITEM or pr_s0 /= NITEM or pr_s1 /= NITEM
or wt_s0 /= NITEM or wt_s1 /= NITEM then
report " FAIL: the policies did not all deliver every item";
errors <= errors + 1; wait for 1 ns;
end if;
report " 2. all three policies delivered every one of the " & integer'image(NITEM) &
" items from both sequences. So every functional coverage number is IDENTICAL across the three runs -- which is the point: a coverage report cannot tell you which policy ran, and therefore cannot tell you whether the two sequences ever overlapped";
if pr_sw >= rr_sw then
report " FAIL: strict priority switched between sequences at least as often as round robin, so the interleaving experiment measured nothing";
errors <= errors + 1; wait for 1 ns;
end if;
if pr_sw /= 1 then
report " FAIL: strict priority switched " & integer'image(pr_sw) &
" times; with both queues full it should switch exactly once, when sequence 0 runs out";
errors <= errors + 1; wait for 1 ns;
end if;
report " 3. round robin switched between the two streams " & integer'image(rr_sw) &
" times; strict priority switched " & integer'image(pr_sw) &
". Same items, same coverage, and completely different traffic: under priority, sequence 1's items ALL arrive after sequence 0 has finished, so the two never overlap and any design bug that needs them to overlap is unreachable. `we ran both sequences` and `we ran both sequences together` are different claims";
if rr_wait /= 1 then
report " FAIL: under round robin sequence 1's longest wait should be a single grant, not " &
integer'image(rr_wait);
errors <= errors + 1; wait for 1 ns;
end if;
if pr_wait /= NITEM then
report " FAIL: under strict priority sequence 1 should have waited through all " &
integer'image(NITEM) & " of sequence 0's grants in a row, not " &
integer'image(pr_wait);
errors <= errors + 1; wait for 1 ns;
end if;
if wt_wait <= rr_wait or wt_wait >= pr_wait then
report " FAIL: the weighted policy's longest wait should sit between round robin's and priority's";
errors <= errors + 1; wait for 1 ns;
end if;
report " 4. sequence 1's LONGEST consecutive wait was " & integer'image(rr_wait) &
" grant under round robin, " & integer'image(wt_wait) & " under weighted 3:1, and " &
integer'image(pr_wait) &
" under strict priority -- all of sequence 0. Note that the TOTAL number of grants it was passed over for is " &
integer'image(NITEM) &
" under every policy, because with both queues full every grant to sequence 0 is one sequence 1 did not get. A starvation metric that counts the total cannot tell round robin from strict priority; it is measuring queue occupancy";
wait for 1 ns;
if errors = 0 then
report "PASS: a sequencer makes exactly one promise -- that items within a sequence are delivered in the order the sequence produced them -- and it deliberately promises nothing about the order across sequences, because that is what an arbitration policy is for. Measured with " &
integer'image(NITEM) &
" items from each of two sequences and both queues full before the first grant: zero order errors under all three policies, so the promise holds; every policy delivered all " &
integer'image(NITEM) &
" items from both sequences, so every functional coverage number is IDENTICAL across the three runs and a coverage report cannot tell you which policy ran. What differs is the INTERLEAVING -- round robin switched between the streams " &
integer'image(rr_sw) & " times and strict priority switched " &
integer'image(pr_sw) &
", because under priority sequence 1's items all arrive after sequence 0 has finished. The two never overlap, and any design bug that needs them to overlap is unreachable, which is why `we ran both sequences` and `we ran both sequences together` are different claims. And starvation is COUNTED rather than inferred, as the LONGEST consecutive wait rather than the total: " &
integer'image(rr_wait) & " grant under round robin, " & integer'image(wt_wait) &
" under weighted, and all " & integer'image(pr_wait) &
" of sequence 0's under priority. The total would have been " &
integer'image(NITEM) &
" under every policy -- with both queues full, every grant to sequence 0 is one sequence 1 did not get -- so a starvation metric built on the total cannot tell round robin from strict priority and is really measuring queue occupancy"
severity note;
else
report "FAIL: " & integer'image(errors) & " error(s)" severity error;
end if;
done_sim <= true;
wait for 100 ns;
std.env.stop;
end process main;
end architecture tb;6. Where The Pieces Go
AGENT driver + sequencer + monitor + the configuration they share,
and the knowledge of how all of it connects. Active or passive,
and Chapter 16.7 measured why that must be structural.
ENV one agent per interface, plus the scoreboard and the coverage
model. Both subscribe to a MONITOR's analysis port -- never to
a sequencer's, because a transaction that was generated is not
a transaction that reached the pins.
TEST selects the sequences, the arbitration policy and the
configuration. Everything above it is reusable; the test is
where the choices live.The connection that decides whether the environment is honest is the one from the passive agent's monitor to the scoreboard. A suite whose observations come from the active agent's monitor has the driver and the observer sharing a component instance, and the far end of the bus is unobserved.
7. The Whole Agent, As Reviewed Code
Icarus cannot run UVM — Chapter 16.3's toolchain note sets out what it does and does not implement. This is the class form of everything the module has built.
// ---------------------------------------------------------------------------
// THE SEQUENCER. Almost always the default, and the two lines worth writing are
// the arbitration mode and the reason for it -- because section 4 measured that
// the choice changes the traffic without changing the coverage.
// ---------------------------------------------------------------------------
class spi_sequencer extends uvm_sequencer #(spi_item);
`uvm_component_utils(spi_sequencer)
function new(string n, uvm_component p); super.new(n, p); endfunction
endclass
// ---------------------------------------------------------------------------
// THE AGENT. `is_active` decides whether the driver and sequencer are CONSTRUCTED,
// not whether they are used -- Chapter 16.7's measurement, in its UVM spelling.
// ---------------------------------------------------------------------------
class spi_agent extends uvm_agent;
`uvm_component_utils(spi_agent)
spi_cfg cfg;
spi_monitor monitor; // both roles
spi_master_driver driver; // active only
spi_sequencer sequencer; // active only
function new(string n, uvm_component p); super.new(n, p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(spi_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "no configuration object for the SPI agent")
monitor = spi_monitor::type_id::create("monitor", this);
if (get_is_active() == UVM_ACTIVE) begin
sequencer = spi_sequencer::type_id::create("sequencer", this);
driver = spi_master_driver::type_id::create("driver", this);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (get_is_active() == UVM_ACTIVE)
driver.seq_item_port.connect(sequencer.seq_item_export);
endfunction
endclass
// ---------------------------------------------------------------------------
// THE ENVIRONMENT. One active agent, one PASSIVE agent at the far end of the bus,
// and both analysis consumers fed from the passive one's monitor.
// ---------------------------------------------------------------------------
class spi_env extends uvm_env;
`uvm_component_utils(spi_env)
spi_agent master_agent;
spi_agent observer_agent;
spi_ref_model model;
spi_scoreboard sb;
spi_cov cov;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(uvm_active_passive_enum)::set(this, "master_agent", "is_active", UVM_ACTIVE);
uvm_config_db#(uvm_active_passive_enum)::set(this, "observer_agent", "is_active", UVM_PASSIVE);
master_agent = spi_agent::type_id::create("master_agent", this);
observer_agent = spi_agent::type_id::create("observer_agent", this);
model = spi_ref_model::type_id::create("model", this);
sb = spi_scoreboard::type_id::create("sb", this);
cov = spi_cov::type_id::create("cov", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// The PREDICTION comes from the sequencer's traffic and the OBSERVATION from a monitor --
// never both from the same place, which is Chapter 16.6's whole argument.
master_agent.sequencer.ap.connect(model.analysis_export);
model.pred_ap.connect(sb.pred_fifo.analysis_export);
// Observations and coverage from the PASSIVE agent, at the far end of the bus.
observer_agent.monitor.ap.connect(sb.obs_fifo.analysis_export);
observer_agent.monitor.ap.connect(cov.analysis_export);
endfunction
endclass
// ---------------------------------------------------------------------------
// A VIRTUAL SEQUENCE running two sequences CONCURRENTLY on one sequencer, which is
// the arrangement section 4 measured. The arbitration mode is set here because it
// is a property of the TEST rather than of the sequencer.
// ---------------------------------------------------------------------------
class spi_overlap_vseq extends uvm_sequence;
`uvm_object_utils(spi_overlap_vseq)
spi_sequencer seqr;
task body();
spi_width_walk_seq a = spi_width_walk_seq::type_id::create("a");
spi_boundary_seq b = spi_boundary_seq::type_id::create("b");
// ROUND ROBIN, chosen deliberately: under strict priority the second sequence's items would
// all arrive after the first had finished, the two would never overlap, and every bug that
// needs them to overlap would be unreachable -- with identical coverage either way.
seqr.set_arbitration(UVM_SEQ_ARB_RANDOM);
fork
a.start(seqr);
b.start(seqr);
join
endtask
endclass
// ---------------------------------------------------------------------------
// THE TEST. Everything above is reusable; this is where the choices live.
// ---------------------------------------------------------------------------
class spi_overlap_test extends uvm_test;
`uvm_component_utils(spi_overlap_test)
spi_env env;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = spi_env::type_id::create("env", this);
endfunction
task run_phase(uvm_phase phase);
spi_overlap_vseq vseq = spi_overlap_vseq::type_id::create("vseq");
phase.raise_objection(this);
vseq.seqr = env.master_agent.sequencer;
vseq.start(null);
// Drain, so that the last transaction reaches the monitor and the scoreboard before
// `check_phase` asks whether anything is outstanding -- Chapter 16.6's emptiness check needs
// the pipeline to have emptied, and an objection dropped too early makes that check fire on a
// transaction that was merely still in flight.
phase.phase_done.set_drain_time(this, 200ns);
phase.drop_objection(this);
endtask
endclass8. Why a Verification Engineer Cares
Because "we ran both sequences" is the claim most often made and least often true in the form that matters.
The practical consequence is a rule about what a test must record. An arbitration policy is not recoverable from a coverage report — all three policies here produced identical coverage — so the policy and the reason for it belong in the test as an explicit line. A test that does not set it has whatever the default is, which for two sequences started together is round robin, which is the good case — until somebody sets a priority to express importance and removes the overlap without noticing.
The second is about fairness metrics generally: a sum cannot measure a run. The total wait was identical under all three policies here and the longest consecutive wait separated them completely. The same mistake shows up in latency reports, arbiter reviews and bus-utilisation numbers.
9. Why an FPGA or ASIC Engineer Cares
Because the arbitration policy in your testbench decides which of your design's concurrency bugs is reachable, and you are the one who gets the bug report.
A slave whose registers are written by one sequence and read by another is exercised completely differently under round robin and under strict priority — and the strict case, which reads as the more controlled test, is the one in which the two never overlap. If your design has a read-during-write hazard, a priority-arbitrated test cannot find it, and the coverage report will look the same as the one that could.
And the same argument applies to your own arbiters. A round of grants that looks fair in a total is the shape in which a low-priority requester waits through a burst of thirty — and a latency report built on averages, like the starvation metric this chapter had to correct, will say nothing about it.
10. Failure Signature — Two Sequences That Never Met
Symptom a concurrency bug -- a read during a write, a reconfiguration
during a transfer -- escapes to the lab. The suite has both
sequences and runs them in the same test. Coverage is closed.
What happened the test set a sequence priority, so the sequencer granted
every item of the higher-priority sequence first. The second
sequence's items all arrived after the first had finished.
The two never overlapped.
What would have a longest-consecutive-wait metric per sequence, or reading the
caught it arbitration mode the test set. Coverage cannot help: all
policies deliver identical items and identical bins.
The tell the test names a priority. A priority is a statement about
IMPORTANCE and the sequencer reads it as a statement about
ORDER -- and strict ordering is the same thing as no overlap.11. Common Misconceptions
"A sequencer decides the order of items." It decides the order across sequences. Within a sequence the order is the sequence's, preserved by a per-sequence queue, and no policy can change it.
"Running both sequences in one test means they overlapped." Under strict priority the second sequence's items all arrive after the first has finished. Same test, same items, same coverage, no overlap.
"Coverage will show whether the sequences interleaved." It will not. All three policies here delivered identical items and produced identical bins. Interleaving is not a coverage property.
"Setting a priority makes a sequence more important." It makes the sequencer grant it first, exhaustively. The effect on the test is the removal of overlap, which is usually the opposite of what was intended.
"Starvation is the number of grants a sequence missed." That total is identical under round robin and strict priority when both queues are full — it measures queue occupancy. Starvation is the longest consecutive run of missed grants.
"The agent is a convenience wrapper." It is the object that holds the knowledge of how the components connect, which is what makes a second instance on a second bus one line. And its active/passive form is a structural guarantee rather than a mode — Chapter 16.7 measured what happens when it is not.
12. Reason It Through
All three policies produced identical coverage. What does that tell you about what coverage measures?
That it measures which transactions occurred, not how they were arranged in time. Interleaving, concurrency and overlap are properties of the ordering across streams, and no bin in a functional coverage model is indexed by them — so a report can be closed while an entire class of concurrency bug is unreachable.
Both queues are filled before the first grant. Why does the experiment need that?
Because otherwise the interleaving is decided by when the items arrived rather than by the policy. With one sequence arriving late, even round robin produces a sequential-looking stream, and the measurement would be about the bench's push timing.
Why is the total number of missed grants a bad starvation metric here?
With both queues full, every grant to sequence 0 is a grant sequence 1 did not get — so the total is the length of sequence 0 under every policy, round robin included. It is measuring queue occupancy. The longest consecutive run separates the policies: 1, 3, and 12.
A test sets a sequence priority to express that one sequence matters more. What has it actually done?
Told the sequencer to grant that sequence exhaustively before the other, which removes the overlap between them. If the design has a hazard that needs the two to interleave, the test can no longer reach it — and nothing in the coverage report changes.
Where should a scoreboard's observations come from in a two-agent environment, and why?
From the passive agent's monitor, at the far end of the bus. Taking them from the active agent's monitor means the driver and the observer share a component instance and the other end of the bus is never watched — which is Chapter 16.5's peeking problem arriving through the environment's wiring rather than through a configuration field.
13. Understanding Check
14. Summary
An agent is the object that knows how a driver, a sequencer, a monitor and their shared configuration connect, so that a second instance on a second bus costs one line — and the piece of it worth measuring is the sequencer. It makes exactly one promise, that items within a sequence arrive in the order the sequence produced them, and it deliberately promises nothing across sequences because that is what an arbitration policy decides. Measured with twelve items from each of two sequences and both queues full before the first grant: zero order errors under all three policies, so the promise holds; every policy delivered all twelve items from both sequences, so every functional coverage number is identical across the three runs and a coverage report cannot tell you which policy ran. What differs is the interleaving — round robin switched between the streams 23 times and strict priority once — so under priority sequence 1's items all arrive after sequence 0 has finished, the two never overlap, and any bug needing them to overlap is unreachable. And starvation is counted as the longest consecutive wait rather than the total: 1 grant under round robin, 3 under weighted, and all 12 under priority. The total would have been 12 under every policy, so a metric built on it measures queue occupancy rather than fairness.
15. What Comes Next
The environment is complete and reusable. Chapter 17.6 points it at the traffic that breaks designs — including three cases that are perfectly legal and must produce no report at all.
Continue learning
Related tutorials
- Related topic
Active and Passive Agents
Three agents on one bus. A structurally passive agent reports every transaction and is high impedance on every cycle measured; a flag-gated one is indistinguishable from it until a single gate is missing, at which point it corrupts the bus while still declared passive.
- Related topic
Full-Duplex Exchange
Every SPI transfer moves a bit in both directions on every edge, whether the software wanted it to or not. Where dummy bytes come from, why bytes received during a command phase exist but mean nothing, and why read and write are interpretations rather than modes.
- Related topic
Extracting Protocol Rules and the Verification Plan
Eight pin-observable SPI rules, each with a checker and an exercised counter, because a checker alone cannot tell never-broken from never-reached. Legal traffic violates nothing and exercises all eight; eight injected faults produce a diagonal violation matrix; and one plan row is proved to have no checker at all.
- Related topic
Driver Architecture
A driver owns every timing number in the protocol, so it is the one component that must be checked against something not written to agree with it. Thirty-two legal transactions violate nothing and exercise all eight rules; seven injected faults fire exactly the rules predicted.
