SPI · Module 19
FPGA to Sensor — Register Access and Event-Driven Reads
A sensor owns the schedule. An event arriving mid-read has exactly two answers with opposite costs — a lost sample or a mis-timestamped one — and which is right turns on one line of the datasheet.
Chapter 19.1 had the FPGA own the schedule: it decided when to convert and the device followed. This chapter inverts that. The sensor converts on its own timebase and asserts a pin when a result exists, and the FPGA's job is to notice.
An event can arrive while a read is in progress. There are exactly two things to do about it, they fail in opposite ways, and the right choice depends on one line of the datasheet.
1. Two Things The FPGA Must Do
at startup walk a configuration script: write N registers, in order
then forever on each data-ready event, read a burst of registersBoth are SPI frames and both are trivial in isolation. The chapter is about the seam between them and about the event that arrives at the wrong moment.
2. The Policy Question
An event arrives while a read is running. Two answers:
| Policy | What happens | What it costs |
|---|---|---|
| DROP | discard the new event | the sample it announced is never read |
| QUEUE | remember it, service it when free | the queued read fetches whatever is in the register then — a mis-timestamped sample |
Neither is better. They trade one failure for a different one, and which failure is acceptable is an application question:
a control loop usually prefers a dropped sample to one attributed to
the wrong instant -- a wrong timestamp is a phase error
in the loop, and phase errors destabilise
a data logger usually prefers the opposite -- a gap in the record is
worse than a record whose timestamps are slightly soft3. The Only Clock-Domain Crossing Here
Exactly one signal crosses: drdy, a single-bit asynchronous input. It gets two flops and an edge detector.
single-bit level from an unrelated clock → 2-flop synchroniser → edge detectThat is the correct mechanism for this crossing and there is nothing else to cross — SCLK is generated here, so the read path is entirely in the system domain, for the reason Chapter 19.1 set out.
4. A Pulse And A Level Are Different Interfaces
This is the trap that strands designs at bring-up, and it is worth separating from the policy question because the two interact.
a PULSE is a transient notification. Miss it and it is gone.
→ the DROP-versus-QUEUE question is exactly what to do about that.
a LEVEL is held until the data is read. It does not expire.
→ there is nothing to drop and nothing to queue.So the policy question belongs to the pulse interface alone. Recognising a level only when the engine is free is the whole policy, and applying a drop policy to a level destroys the notification — because a level does not assert again.
An already-asserted level produces exactly one edge, in the wrong window
14 cycles5. The Measurement
Identical output from all three languages. The sensor converts every conv_iv cycles on its own timebase; a burst read takes 102 cycles.
configuration: 3 frames on the pins -> 2001 2140 2208, cfg_done=1, reads before cfg=0
read duration = 102 cycles (lead 2 + 3 bytes x 16 edges x half 2 + lag 2 + gap 2)
policy conv_iv events reads missed queued stale what it costs
DROP 240 12 12 0 0 0 nothing -- no contention
DROP 80 12 6 6 0 0 samples LOST, timestamps intact
DROP 50 12 4 8 0 0 samples LOST, timestamps intact
QUEUE 240 12 12 0 0 0 nothing -- no contention
QUEUE 80 12 10 2 9 2 samples KEPT, timestamps wrong
QUEUE 50 12 7 5 6 5 samples KEPT, timestamps wrong
mode drdy at reset events reads missed outcome
EDGE 1 1 0 1 STRANDED -- the one edge it saw was spent during configuration
LEVEL 1 1 1 0 serviced normallyThe configuration frames are verified on the pins, not from the design's cfg_done flag. A flag a design raises about itself is not evidence that the frames happened — and a script that silently does nothing leaves a sensor that never converts, whose symptom is an absent data-ready pin and whose investigation is a continuity check.
With no contention the two policies are indistinguishable. 12 events, 12 reads, nothing missed, nothing mis-timestamped, under both. That row is what makes the contention rows attributable to the policy rather than to the design — a comparison that only ever runs under load cannot establish that the two agree where they should.
Under contention each fails differently. At a 50-cycle interval DROP lost 8 of 12 and mis-timestamped none; QUEUE lost 5 and mis-timestamped 5. The bench requires DROP's stale count to be exactly zero and QUEUE's to be non-zero, so the trade is measured rather than asserted.
6. What The Port To Three Languages Found
This chapter's HDL contains one design and three implementations of it, and the ports found four defects — all of them the same root cause, none of them visible in the language they were written in.
7. Building It — Three HDLs
// spi_sensor_evt.sv
//
// Chapter 19.2 -- a sensor decides when data is ready, and the transfer is triggered by an event that
// is asynchronous to everything including a transfer already in progress.
//
// CHAPTER 19.1 INVERTED. There, the FPGA owned the schedule and the device followed it. Here the device
// owns the schedule: it converts on its own timebase and asserts a data-ready pin when a result exists.
// The FPGA's job is to notice, and to read a burst of registers before the next result replaces it.
//
// THE DESIGN QUESTION THIS MODULE EXISTS TO ANSWER. A data-ready event can arrive WHILE a read is in
// progress. There are exactly two policies, and they are not better and worse -- they trade one failure
// for a different one:
//
// DROP discard the new event. The sample it announced is never read.
// -> a LOST sample. The remaining stream is correctly timestamped.
//
// QUEUE remember it and service it when the read finishes.
// -> no sample is lost, and the read that services it fetches whatever is in the
// sensor's data register AT THAT MOMENT, which may be a LATER conversion.
// -> a MIS-TIMESTAMPED sample.
//
// Which is worse is an APPLICATION question and not a hardware one. A control loop usually prefers a
// dropped sample to a sample attributed to the wrong instant; a logger usually prefers the opposite.
// The hardware's obligation is to implement one of them deliberately and to make the other one's cost
// visible -- which is why this module counts `n_missed` and `n_stale` separately and the bench drives
// both policies across every arrival phase.
//
// AND THE ANSWER DEPENDS ON A LINE IN THE DATASHEET. If the sensor's data register is DOUBLE-BUFFERED --
// the result is held until read, and a new conversion goes to a shadow -- then QUEUE loses nothing and
// is strictly better. If it is not, QUEUE silently converts a lost sample into a wrongly-timestamped
// one. The register block below cannot tell the difference, so the choice has to be configured, not
// inferred, and the bench measures both.
//
// THE SECOND TRAP, AND IT IS A BRING-UP CLASSIC. A data-ready pin may be a PULSE or a LEVEL held until
// the data is read. An edge-detecting design meets a level-held pin that is ALREADY ASSERTED when the
// FPGA comes out of reset -- because the sensor converted while the FPGA was booting -- sees no edge,
// and waits forever. Nothing is broken, nothing is reported, and the link is dead. `drdy_level_mode`
// exists so both readings can be built, and the bench starts a run with the pin already high.
//
// WHAT CROSSES A CLOCK DOMAIN HERE. Exactly one thing: `drdy`, a single-bit asynchronous input. It gets
// two flops and an edge detector. That is the correct mechanism for a single-bit level, and it is the
// ONLY crossing in this design -- SCLK is generated here, so the read path is entirely in the system
// domain, for the reason Chapter 19.1 set out.
`timescale 1ns/1ps
module spi_sensor_evt #(
parameter int CFG_N = 3, // configuration writes to perform before streaming
parameter int CNT_W = 16
) (
input wire clk,
input wire rst_n,
// ---- the asynchronous event from the sensor ----
input wire drdy,
input wire drdy_level_mode, // 0: treat drdy as a PULSE (edge); 1: as a LEVEL
input wire policy_queue, // 0: DROP a mid-read event; 1: QUEUE one
// ---- timing, in system-clock cycles ----
input wire [7:0] half,
input wire [7:0] lead,
input wire [7:0] lag,
input wire [7:0] gap,
// ---- pins ----
output reg sclk,
output reg cs_n,
output reg mosi,
input wire miso,
// ---- results ----
output reg cfg_done,
output reg [15:0] rd_data,
output reg [CNT_W-1:0] rd_tag, // which event this read was servicing
output reg rd_valid,
// ---- health counters ----
output reg [CNT_W-1:0] n_events, // data-ready events observed
output reg [CNT_W-1:0] n_reads, // burst reads started
output reg [CNT_W-1:0] n_missed, // events discarded by the DROP policy
output reg [CNT_W-1:0] n_queued // events deferred by the QUEUE policy
);
localparam [2:0] S_CFG = 3'd0, // walking the configuration script
S_WAIT = 3'd1, // streaming, waiting for a data-ready event
S_LEAD = 3'd2,
S_SHIFT = 3'd3,
S_LAG = 3'd4,
S_GAP = 3'd5;
localparam [7:0] CMD_READ = 8'h0B; // the sensor's burst-read opcode
// ---- the configuration script ----
//
// A tiny ROM rather than a hand-unrolled sequence, because the whole point of a script is that
// adding a register is a data change. A real design's script is longer and often holds a delay
// after certain writes; the shape is the same.
function [15:0] cfg_word(input [CNT_W-1:0] k);
begin
case (k)
{{(CNT_W-1){1'b0}}, 1'b0}: cfg_word = 16'h2001; // reg 0x20 <= 0x01 (enable)
{{(CNT_W-1){1'b0}}, 1'b1}: cfg_word = 16'h2140; // reg 0x21 <= 0x40 (output rate)
default: cfg_word = 16'h2208; // reg 0x22 <= 0x08 (data-ready on)
endcase
end
endfunction
// ---- the asynchronous event, synchronised ----
//
// TWO FLOPS then an edge detector. `drdy` is asynchronous to `clk` by construction -- the sensor
// has its own oscillator -- so this is the one place in the design where a crossing exists.
//
// Simulation cannot establish that two flops are ENOUGH: metastability is not representable in
// zero-delay RTL and a single flop would behave identically here. What simulation does establish is
// the thing below it -- that the edge detector sees each assertion exactly once, and that the LEVEL
// reading does not need an edge at all.
reg drdy_s1, drdy_s2, drdy_s3;
wire drdy_rise = drdy_s2 & ~drdy_s3;
wire drdy_high = drdy_s2;
// A LEVEL MUST PRODUCE EXACTLY ONE EVENT PER ASSERTION, which takes a one-shot.
//
// The first version made `evt_now` the level itself. A pin that stays asserted then produced an
// event on EVERY cycle the engine was free, so `n_events` counted cycles rather than events and the
// reported event rate was meaningless -- while the read behaviour looked perfectly correct. A
// counter that is wrong in a way the datapath hides is worse than one that is missing: it gets
// believed.
//
// `lvl_armed` re-arms only when the pin has actually gone low, which is what the sensor does once it
// sees the data being read.
reg lvl_armed;
wire evt_now = drdy_level_mode ? (drdy_high & lvl_armed) : drdy_rise;
reg [2:0] st;
reg [CNT_W-1:0] dwell;
reg [7:0] edges;
// THE WHOLE FRAME IN ONE SHIFT REGISTER, up to three bytes, MSB first.
//
// The first version of this engine tracked a byte index and indexed into a byte with an expression
// derived from the edge count. It was wrong in a way that compiled, and it was unreadable -- which
// is worse, because a transmit path nobody can check by eye is a transmit path nobody checks. A
// single shift register makes the invariant obvious: MOSI is always the top bit, and the top bit
// advances once per trailing edge.
reg [23:0] tx_buf, rx_buf;
reg [1:0] nbytes; // bytes in the current frame
reg is_read;
reg [CNT_W-1:0] cfg_i;
reg pending; // a queued event waiting to be serviced
reg [CNT_W-1:0] pend_tag;
reg [CNT_W-1:0] cur_tag;
// A part-select of a function CALL is not legal, so the current script word lands on a wire first.
wire [15:0] cfg_cur = cfg_word(cfg_i);
wire busy = (st != S_WAIT) && (st != S_CFG);
// The event is only recognised outside the configuration phase, and this is the single condition that
// both the policy logic and the level one-shot key off -- so the two can never disagree about whether
// an event happened.
//
// AND IN LEVEL MODE IT IS ONLY RECOGNISED WHEN THE ENGINE IS FREE, which is a semantic difference
// rather than an optimisation.
//
// A PULSE is a transient notification: if the design is busy when it arrives, it is gone, and the
// DROP-versus-QUEUE question is exactly what to do about that. A LEVEL does not expire -- the pin
// stays asserted because the data is still waiting -- so there is nothing to drop and nothing to
// queue. Looking at it only when free is the whole policy.
//
// Getting this wrong stranded the design twice while it was being built. Recognising the level while
// busy sent it down the policy path, which discarded it and consumed the one-shot; because a level
// does not pulse again, it never re-armed and the link was dead for the rest of time. The policy
// question belongs to the pulse interface and applying it to a level destroys the notification.
wire evt_take = evt_now && (st != S_CFG) && (drdy_level_mode ? !busy : 1'b1);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
drdy_s1 <= 1'b0; drdy_s2 <= 1'b0; drdy_s3 <= 1'b0;
lvl_armed <= 1'b1;
st <= S_CFG;
dwell <= {CNT_W{1'b0}};
edges <= 8'd0;
tx_buf <= 24'd0;
rx_buf <= 24'd0;
nbytes <= 2'd2;
is_read <= 1'b0;
cfg_i <= {CNT_W{1'b0}};
pending <= 1'b0;
pend_tag <= {CNT_W{1'b0}};
cur_tag <= {CNT_W{1'b0}};
sclk <= 1'b0;
cs_n <= 1'b1;
mosi <= 1'b0;
cfg_done <= 1'b0;
rd_data <= 16'd0;
rd_tag <= {CNT_W{1'b0}};
rd_valid <= 1'b0;
n_events <= {CNT_W{1'b0}};
n_reads <= {CNT_W{1'b0}};
n_missed <= {CNT_W{1'b0}};
n_queued <= {CNT_W{1'b0}};
end else begin
rd_valid <= 1'b0;
drdy_s1 <= drdy;
drdy_s2 <= drdy_s1;
drdy_s3 <= drdy_s2;
// THE ONE-SHOT IS CONSUMED ONLY WHEN THE EVENT IS ACTUALLY RECOGNISED, not merely when the
// pin is high. Clearing it on `evt_now` alone burned the assertion during the configuration
// phase -- where events are deliberately ignored -- and because the pin then stayed high it
// never re-armed. The design was stranded for the rest of time, having consumed the only
// notification it would ever get. A one-shot that can be spent by a path that discards the
// event is not a one-shot, it is a leak.
if (!drdy_high) lvl_armed <= 1'b1;
else if (evt_take) lvl_armed <= 1'b0;
// ---- the event, and the policy ----
//
// THE POLICY IS APPLIED HERE AND NOWHERE ELSE, so a reader can see the whole decision in
// one place. An event is counted the moment it is observed, whatever happens to it next:
// a design that only counts events it managed to service cannot report its own loss rate.
if (evt_take) begin
if (!busy) begin
n_events <= n_events + 1'b1;
cur_tag <= n_events;
n_reads <= n_reads + 1'b1;
is_read <= 1'b1;
nbytes <= 2'd3; // one command byte plus two data bytes
tx_buf <= {CMD_READ, 16'h0000};
rx_buf <= 24'd0;
st <= S_LEAD;
dwell <= {CNT_W{1'b0}};
cs_n <= 1'b0;
end else if (policy_queue) begin
// QUEUE. One slot: a second mid-read event overwrites the first, which is itself a
// loss -- and a deeper queue only moves the problem, because the sensor's data
// register is not deeper.
n_events <= n_events + 1'b1;
if (!pending) begin
pending <= 1'b1;
pend_tag <= n_events;
n_queued <= n_queued + 1'b1;
end else begin
n_missed <= n_missed + 1'b1;
end
end else begin
// DROP. The sample this event announced will never be read.
n_events <= n_events + 1'b1;
n_missed <= n_missed + 1'b1;
end
end
case (st)
// ---- the configuration script ----
S_CFG: begin
if (cfg_i >= CFG_N[CNT_W-1:0]) begin
cfg_done <= 1'b1;
st <= S_WAIT;
end else begin
is_read <= 1'b0;
nbytes <= 2'd2; // register address plus value
tx_buf <= {cfg_cur, 8'h00};
rx_buf <= 24'd0;
st <= S_LEAD;
dwell <= {CNT_W{1'b0}};
cs_n <= 1'b0;
end
end
S_WAIT: begin
// A queued event is serviced here, one cycle after the engine became free, so the
// deselected gap is always honoured between frames.
if (pending) begin
pending <= 1'b0;
cur_tag <= pend_tag;
n_reads <= n_reads + 1'b1;
is_read <= 1'b1;
nbytes <= 2'd3;
tx_buf <= {CMD_READ, 16'h0000};
rx_buf <= 24'd0;
st <= S_LEAD;
dwell <= {CNT_W{1'b0}};
cs_n <= 1'b0;
end
end
S_LEAD: begin
// The first bit is on the pin through the lead, which is what a mode-0 slave needs
// in order to sample it at the very first leading edge.
mosi <= tx_buf[23];
if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, lead}) begin
st <= S_SHIFT;
dwell <= {CNT_W{1'b0}};
edges <= 8'd0;
end else dwell <= dwell + 1'b1;
end
S_SHIFT: begin
if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, half}) begin
dwell <= {CNT_W{1'b0}};
if (!sclk) begin
// MODE 0: capture at the leading edge; the device has held this bit since
// the previous trailing edge.
rx_buf <= {rx_buf[22:0], miso};
sclk <= 1'b1;
end else begin
// Advance the transmit frame on the trailing edge, so MOSI is stable for a
// full half period before the slave samples it.
sclk <= 1'b0;
tx_buf <= {tx_buf[22:0], 1'b0};
mosi <= tx_buf[22];
end
if (edges + 8'd1 >= {6'b0, nbytes} * 8'd16) begin
st <= S_LAG;
end else begin
edges <= edges + 8'd1;
end
end else dwell <= dwell + 1'b1;
end
S_LAG: begin
if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, lag}) begin
st <= S_GAP;
dwell <= {CNT_W{1'b0}};
cs_n <= 1'b1;
if (is_read) begin
// The last two bytes of the frame are the data; the first was the command,
// during which the slave drove nothing meaningful.
rd_data <= rx_buf[15:0];
rd_tag <= cur_tag;
rd_valid <= 1'b1;
end
end else dwell <= dwell + 1'b1;
end
S_GAP: begin
if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, gap}) begin
dwell <= {CNT_W{1'b0}};
if (!cfg_done) begin
cfg_i <= cfg_i + 1'b1;
st <= S_CFG;
end else begin
st <= S_WAIT;
end
end else dwell <= dwell + 1'b1;
end
default: ;
endcase
end
end
endmodule// spi_sensor_evt.v
//
// Chapter 19.2 -- a sensor decides when data is ready, and the transfer is triggered by an event that
// is asynchronous to everything including a transfer already in progress.
//
// CHAPTER 19.1 INVERTED. There, the FPGA owned the schedule and the device followed it. Here the device
// owns the schedule: it converts on its own timebase and asserts a data-ready pin when a result exists.
// The FPGA's job is to notice, and to read a burst of registers before the next result replaces it.
//
// THE DESIGN QUESTION THIS MODULE EXISTS TO ANSWER. A data-ready event can arrive WHILE a read is in
// progress. There are exactly two policies, and they are not better and worse -- they trade one failure
// for a different one:
//
// DROP discard the new event. The sample it announced is never read.
// -> a LOST sample. The remaining stream is correctly timestamped.
//
// QUEUE remember it and service it when the read finishes.
// -> no sample is lost, and the read that services it fetches whatever is in the
// sensor's data register AT THAT MOMENT, which may be a LATER conversion.
// -> a MIS-TIMESTAMPED sample.
//
// Which is worse is an APPLICATION question and not a hardware one. A control loop usually prefers a
// dropped sample to a sample attributed to the wrong instant; a logger usually prefers the opposite.
// The hardware's obligation is to implement one of them deliberately and to make the other one's cost
// visible -- which is why this module counts `n_missed` and `n_stale` separately and the bench drives
// both policies across every arrival phase.
//
// AND THE ANSWER DEPENDS ON A LINE IN THE DATASHEET. If the sensor's data register is DOUBLE-BUFFERED --
// the result is held until read, and a new conversion goes to a shadow -- then QUEUE loses nothing and
// is strictly better. If it is not, QUEUE silently converts a lost sample into a wrongly-timestamped
// one. The register block below cannot tell the difference, so the choice has to be configured, not
// inferred, and the bench measures both.
//
// THE SECOND TRAP, AND IT IS A BRING-UP CLASSIC. A data-ready pin may be a PULSE or a LEVEL held until
// the data is read. An edge-detecting design meets a level-held pin that is ALREADY ASSERTED when the
// FPGA comes out of reset -- because the sensor converted while the FPGA was booting -- sees no edge,
// and waits forever. Nothing is broken, nothing is reported, and the link is dead. `drdy_level_mode`
// exists so both readings can be built, and the bench starts a run with the pin already high.
//
// WHAT CROSSES A CLOCK DOMAIN HERE. Exactly one thing: `drdy`, a single-bit asynchronous input. It gets
// two flops and an edge detector. That is the correct mechanism for a single-bit level, and it is the
// ONLY crossing in this design -- SCLK is generated here, so the read path is entirely in the system
// domain, for the reason Chapter 19.1 set out.
`timescale 1ns/1ps
module spi_sensor_evt #(
parameter CFG_N = 3, // configuration writes to perform before streaming
parameter CNT_W = 16
) (
input wire clk,
input wire rst_n,
// ---- the asynchronous event from the sensor ----
input wire drdy,
input wire drdy_level_mode, // 0: treat drdy as a PULSE (edge); 1: as a LEVEL
input wire policy_queue, // 0: DROP a mid-read event; 1: QUEUE one
// ---- timing, in system-clock cycles ----
input wire [7:0] half,
input wire [7:0] lead,
input wire [7:0] lag,
input wire [7:0] gap,
// ---- pins ----
output reg sclk,
output reg cs_n,
output reg mosi,
input wire miso,
// ---- results ----
output reg cfg_done,
output reg [15:0] rd_data,
output reg [CNT_W-1:0] rd_tag, // which event this read was servicing
output reg rd_valid,
// ---- health counters ----
output reg [CNT_W-1:0] n_events, // data-ready events observed
output reg [CNT_W-1:0] n_reads, // burst reads started
output reg [CNT_W-1:0] n_missed, // events discarded by the DROP policy
output reg [CNT_W-1:0] n_queued // events deferred by the QUEUE policy
);
localparam [2:0] S_CFG = 3'd0, // walking the configuration script
S_WAIT = 3'd1, // streaming, waiting for a data-ready event
S_LEAD = 3'd2,
S_SHIFT = 3'd3,
S_LAG = 3'd4,
S_GAP = 3'd5;
localparam [7:0] CMD_READ = 8'h0B; // the sensor's burst-read opcode
// ---- the configuration script ----
//
// A tiny ROM rather than a hand-unrolled sequence, because the whole point of a script is that
// adding a register is a data change. A real design's script is longer and often holds a delay
// after certain writes; the shape is the same.
function [15:0] cfg_word;
input [CNT_W-1:0] k;
begin
case (k)
{{(CNT_W-1){1'b0}}, 1'b0}: cfg_word = 16'h2001; // reg 0x20 <= 0x01 (enable)
{{(CNT_W-1){1'b0}}, 1'b1}: cfg_word = 16'h2140; // reg 0x21 <= 0x40 (output rate)
default: cfg_word = 16'h2208; // reg 0x22 <= 0x08 (data-ready on)
endcase
end
endfunction
// ---- the asynchronous event, synchronised ----
//
// TWO FLOPS then an edge detector. `drdy` is asynchronous to `clk` by construction -- the sensor
// has its own oscillator -- so this is the one place in the design where a crossing exists.
//
// Simulation cannot establish that two flops are ENOUGH: metastability is not representable in
// zero-delay RTL and a single flop would behave identically here. What simulation does establish is
// the thing below it -- that the edge detector sees each assertion exactly once, and that the LEVEL
// reading does not need an edge at all.
reg drdy_s1, drdy_s2, drdy_s3;
wire drdy_rise = drdy_s2 & ~drdy_s3;
wire drdy_high = drdy_s2;
// A LEVEL MUST PRODUCE EXACTLY ONE EVENT PER ASSERTION, which takes a one-shot.
//
// The first version made `evt_now` the level itself. A pin that stays asserted then produced an
// event on EVERY cycle the engine was free, so `n_events` counted cycles rather than events and the
// reported event rate was meaningless -- while the read behaviour looked perfectly correct. A
// counter that is wrong in a way the datapath hides is worse than one that is missing: it gets
// believed.
//
// `lvl_armed` re-arms only when the pin has actually gone low, which is what the sensor does once it
// sees the data being read.
reg lvl_armed;
wire evt_now = drdy_level_mode ? (drdy_high & lvl_armed) : drdy_rise;
reg [2:0] st;
reg [CNT_W-1:0] dwell;
reg [7:0] edges;
// THE WHOLE FRAME IN ONE SHIFT REGISTER, up to three bytes, MSB first.
//
// The first version of this engine tracked a byte index and indexed into a byte with an expression
// derived from the edge count. It was wrong in a way that compiled, and it was unreadable -- which
// is worse, because a transmit path nobody can check by eye is a transmit path nobody checks. A
// single shift register makes the invariant obvious: MOSI is always the top bit, and the top bit
// advances once per trailing edge.
reg [23:0] tx_buf, rx_buf;
reg [1:0] nbytes; // bytes in the current frame
reg is_read;
reg [CNT_W-1:0] cfg_i;
reg pending; // a queued event waiting to be serviced
reg [CNT_W-1:0] pend_tag;
reg [CNT_W-1:0] cur_tag;
// A part-select of a function CALL is not legal, so the current script word lands on a wire first.
wire [15:0] cfg_cur = cfg_word(cfg_i);
wire busy = (st != S_WAIT) && (st != S_CFG);
// The event is only recognised outside the configuration phase, and this is the single condition that
// both the policy logic and the level one-shot key off -- so the two can never disagree about whether
// an event happened.
//
// AND IN LEVEL MODE IT IS ONLY RECOGNISED WHEN THE ENGINE IS FREE, which is a semantic difference
// rather than an optimisation.
//
// A PULSE is a transient notification: if the design is busy when it arrives, it is gone, and the
// DROP-versus-QUEUE question is exactly what to do about that. A LEVEL does not expire -- the pin
// stays asserted because the data is still waiting -- so there is nothing to drop and nothing to
// queue. Looking at it only when free is the whole policy.
//
// Getting this wrong stranded the design twice while it was being built. Recognising the level while
// busy sent it down the policy path, which discarded it and consumed the one-shot; because a level
// does not pulse again, it never re-armed and the link was dead for the rest of time. The policy
// question belongs to the pulse interface and applying it to a level destroys the notification.
wire evt_take = evt_now && (st != S_CFG) && (drdy_level_mode ? !busy : 1'b1);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
drdy_s1 <= 1'b0; drdy_s2 <= 1'b0; drdy_s3 <= 1'b0;
lvl_armed <= 1'b1;
st <= S_CFG;
dwell <= {CNT_W{1'b0}};
edges <= 8'd0;
tx_buf <= 24'd0;
rx_buf <= 24'd0;
nbytes <= 2'd2;
is_read <= 1'b0;
cfg_i <= {CNT_W{1'b0}};
pending <= 1'b0;
pend_tag <= {CNT_W{1'b0}};
cur_tag <= {CNT_W{1'b0}};
sclk <= 1'b0;
cs_n <= 1'b1;
mosi <= 1'b0;
cfg_done <= 1'b0;
rd_data <= 16'd0;
rd_tag <= {CNT_W{1'b0}};
rd_valid <= 1'b0;
n_events <= {CNT_W{1'b0}};
n_reads <= {CNT_W{1'b0}};
n_missed <= {CNT_W{1'b0}};
n_queued <= {CNT_W{1'b0}};
end else begin
rd_valid <= 1'b0;
drdy_s1 <= drdy;
drdy_s2 <= drdy_s1;
drdy_s3 <= drdy_s2;
// THE ONE-SHOT IS CONSUMED ONLY WHEN THE EVENT IS ACTUALLY RECOGNISED, not merely when the
// pin is high. Clearing it on `evt_now` alone burned the assertion during the configuration
// phase -- where events are deliberately ignored -- and because the pin then stayed high it
// never re-armed. The design was stranded for the rest of time, having consumed the only
// notification it would ever get. A one-shot that can be spent by a path that discards the
// event is not a one-shot, it is a leak.
if (!drdy_high) lvl_armed <= 1'b1;
else if (evt_take) lvl_armed <= 1'b0;
// ---- the event, and the policy ----
//
// THE POLICY IS APPLIED HERE AND NOWHERE ELSE, so a reader can see the whole decision in
// one place. An event is counted the moment it is observed, whatever happens to it next:
// a design that only counts events it managed to service cannot report its own loss rate.
if (evt_take) begin
if (!busy) begin
n_events <= n_events + 1'b1;
cur_tag <= n_events;
n_reads <= n_reads + 1'b1;
is_read <= 1'b1;
nbytes <= 2'd3; // one command byte plus two data bytes
tx_buf <= {CMD_READ, 16'h0000};
rx_buf <= 24'd0;
st <= S_LEAD;
dwell <= {CNT_W{1'b0}};
cs_n <= 1'b0;
end else if (policy_queue) begin
// QUEUE. One slot: a second mid-read event overwrites the first, which is itself a
// loss -- and a deeper queue only moves the problem, because the sensor's data
// register is not deeper.
n_events <= n_events + 1'b1;
if (!pending) begin
pending <= 1'b1;
pend_tag <= n_events;
n_queued <= n_queued + 1'b1;
end else begin
n_missed <= n_missed + 1'b1;
end
end else begin
// DROP. The sample this event announced will never be read.
n_events <= n_events + 1'b1;
n_missed <= n_missed + 1'b1;
end
end
case (st)
// ---- the configuration script ----
S_CFG: begin
if (cfg_i >= CFG_N[CNT_W-1:0]) begin
cfg_done <= 1'b1;
st <= S_WAIT;
end else begin
is_read <= 1'b0;
nbytes <= 2'd2; // register address plus value
tx_buf <= {cfg_cur, 8'h00};
rx_buf <= 24'd0;
st <= S_LEAD;
dwell <= {CNT_W{1'b0}};
cs_n <= 1'b0;
end
end
S_WAIT: begin
// A queued event is serviced here, one cycle after the engine became free, so the
// deselected gap is always honoured between frames.
if (pending) begin
pending <= 1'b0;
cur_tag <= pend_tag;
n_reads <= n_reads + 1'b1;
is_read <= 1'b1;
nbytes <= 2'd3;
tx_buf <= {CMD_READ, 16'h0000};
rx_buf <= 24'd0;
st <= S_LEAD;
dwell <= {CNT_W{1'b0}};
cs_n <= 1'b0;
end
end
S_LEAD: begin
// The first bit is on the pin through the lead, which is what a mode-0 slave needs
// in order to sample it at the very first leading edge.
mosi <= tx_buf[23];
if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, lead}) begin
st <= S_SHIFT;
dwell <= {CNT_W{1'b0}};
edges <= 8'd0;
end else dwell <= dwell + 1'b1;
end
S_SHIFT: begin
if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, half}) begin
dwell <= {CNT_W{1'b0}};
if (!sclk) begin
// MODE 0: capture at the leading edge; the device has held this bit since
// the previous trailing edge.
rx_buf <= {rx_buf[22:0], miso};
sclk <= 1'b1;
end else begin
// Advance the transmit frame on the trailing edge, so MOSI is stable for a
// full half period before the slave samples it.
sclk <= 1'b0;
tx_buf <= {tx_buf[22:0], 1'b0};
mosi <= tx_buf[22];
end
if (edges + 8'd1 >= {6'b0, nbytes} * 8'd16) begin
st <= S_LAG;
end else begin
edges <= edges + 8'd1;
end
end else dwell <= dwell + 1'b1;
end
S_LAG: begin
if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, lag}) begin
st <= S_GAP;
dwell <= {CNT_W{1'b0}};
cs_n <= 1'b1;
if (is_read) begin
// The last two bytes of the frame are the data; the first was the command,
// during which the slave drove nothing meaningful.
rd_data <= rx_buf[15:0];
rd_tag <= cur_tag;
rd_valid <= 1'b1;
end
end else dwell <= dwell + 1'b1;
end
S_GAP: begin
if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, gap}) begin
dwell <= {CNT_W{1'b0}};
if (!cfg_done) begin
cfg_i <= cfg_i + 1'b1;
st <= S_CFG;
end else begin
st <= S_WAIT;
end
end else dwell <= dwell + 1'b1;
end
default: ;
endcase
end
end
endmodule-- spi_sensor_evt.vhd
--
-- Chapter 19.2 -- a sensor decides when data is ready, and the transfer is triggered by an event that
-- is asynchronous to everything including a transfer already in progress.
--
-- CHAPTER 19.1 INVERTED. There, the FPGA owned the schedule and the device followed it. Here the device
-- owns the schedule: it converts on its own timebase and asserts a data-ready pin when a result exists.
-- The FPGA's job is to notice, and to read a burst of registers before the next result replaces it.
--
-- THE DESIGN QUESTION THIS MODULE EXISTS TO ANSWER. A data-ready event can arrive WHILE a read is in
-- progress. There are exactly two policies, and they are not better and worse -- they trade one failure
-- for a different one:
--
-- DROP discard the new event. The sample it announced is never read.
-- -> a LOST sample. The remaining stream is correctly timestamped.
--
-- QUEUE remember it and service it when the read finishes.
-- -> no sample is lost, and the read that services it fetches whatever is in the
-- sensor's data register AT THAT MOMENT, which may be a LATER conversion.
-- -> a MIS-TIMESTAMPED sample.
--
-- Which is worse is an APPLICATION question and not a hardware one. A control loop usually prefers a
-- dropped sample to a sample attributed to the wrong instant; a logger usually prefers the opposite.
-- The hardware's obligation is to implement one of them deliberately and to make the other one's cost
-- visible -- which is why this module counts `n_missed` and `n_stale` separately and the bench drives
-- both policies across every arrival phase.
--
-- AND THE ANSWER DEPENDS ON A LINE IN THE DATASHEET. If the sensor's data register is DOUBLE-BUFFERED --
-- the result is held until read, and a new conversion goes to a shadow -- then QUEUE loses nothing and
-- is strictly better. If it is not, QUEUE silently converts a lost sample into a wrongly-timestamped
-- one. The register block below cannot tell the difference, so the choice has to be configured, not
-- inferred, and the bench measures both.
--
-- THE SECOND TRAP, AND IT IS A BRING-UP CLASSIC. A data-ready pin may be a PULSE or a LEVEL held until
-- the data is read. An edge-detecting design meets a level-held pin that is ALREADY ASSERTED when the
-- FPGA comes out of reset -- because the sensor converted while the FPGA was booting -- sees no edge,
-- and waits forever. Nothing is broken, nothing is reported, and the link is dead. `drdy_level_mode`
-- exists so both readings can be built, and the bench starts a run with the pin already high.
--
-- WHAT CROSSES A CLOCK DOMAIN HERE. Exactly one thing: `drdy`, a single-bit asynchronous input. It gets
-- two flops and an edge detector. That is the correct mechanism for a single-bit level, and it is the
-- ONLY crossing in this design -- SCLK is generated here, so the read path is entirely in the system
-- domain, for the reason Chapter 19.1 set out.
--
-- WHAT THE VHDL VERSION ADDS. The policy is an ENUMERATION rather than a bit, so `POLICY_DROP` and
-- `POLICY_QUEUE` are named where they are declared and a third value is not constructible -- which
-- matters for a design whose entire subject is a choice between two behaviours. The data-ready reading
-- is an enumeration for the same reason: `DRDY_PULSE` and `DRDY_LEVEL` are semantically different
-- interfaces, not two settings of one.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). Generics are `CFG_N_C` and `CNT_W`. The timing PORTS are
-- `half`, `lead`, `lag`, `gap`; their integer copies inside the process are `n_half`, `n_lead`, `n_lag`,
-- `n_gap` -- deliberately not `HALF` or `Lead`, because a variable differing from a port only in case IS
-- that port. Nothing here is distinguished from anything else by case alone.
--
-- RESERVED-WORD REVIEW. No identifier collides with a VHDL keyword; in particular nothing is named
-- `label`, `range`, `next`, `access`, `body`, `bus`, `register`, `guarded` or `open`.
--
-- RANGE-DIRECTION REVIEW. Every vector is declared `downto`, every subprogram formal is constrained, and
-- the frame shift registers are constrained subtypes -- so no slice inherits an ascending range from a
-- concatenation or a bit-string literal.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package spi_sensor_pkg is
type evt_state_t is (S_CFG, S_WAIT, S_LEAD, S_SHIFT, S_LAG, S_GAP);
-- The two policies, named. They are not better and worse; they trade a lost sample for a
-- mis-timestamped one, and which is worse is an application question.
type evt_policy_t is (POLICY_DROP, POLICY_QUEUE);
-- The two data-ready interfaces. A PULSE is a transient notification; a LEVEL does not expire.
-- They are different interfaces rather than two settings of one, which is why the policy question
-- applies to the first and is meaningless for the second.
type drdy_kind_t is (DRDY_PULSE, DRDY_LEVEL);
end package spi_sensor_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_sensor_pkg.all;
entity spi_sensor_evt is
generic (
CFG_N_C : positive := 3;
CNT_W : positive := 16
);
port (
clk : in std_logic;
rst_n : in std_logic;
drdy : in std_logic;
drdy_kind : in drdy_kind_t;
policy : in evt_policy_t;
half : in unsigned(7 downto 0);
lead : in unsigned(7 downto 0);
lag : in unsigned(7 downto 0);
gap : in unsigned(7 downto 0);
sclk : out std_logic;
cs_n : out std_logic;
mosi : out std_logic;
miso : in std_logic;
cfg_done : out std_logic;
rd_data : out std_logic_vector(15 downto 0);
rd_tag : out natural;
rd_valid : out std_logic;
n_events : out natural;
n_reads : out natural;
n_missed : out natural;
n_queued : out natural
);
end entity spi_sensor_evt;
architecture rtl of spi_sensor_evt is
constant CMD_READ_C : std_logic_vector(7 downto 0) := x"0B";
subtype frame_t is std_logic_vector(23 downto 0);
signal sk_r, cs_r, mo_r, cd_r, rv_r : std_logic := '0';
-- THE SYNCHRONISER IS A SIGNAL SHIFT REGISTER, NOT THREE PROCESS VARIABLES, and this is the third
-- place in this chapter where the distinction decides whether the design works.
--
-- Written as variables in forward order -- `d1 := drdy; d2 := d1; d3 := d2;` -- all three take the
-- SAME value in one invocation, because a variable assignment is visible immediately. The chain
-- collapses to a single stage, `d2` and `d3` are always equal, and the rising-edge detector built from them
-- can never fire. The design then observed zero events while the sensor converted happily, and the
-- only symptom was a counter reading 0.
--
-- A signal assignment has the non-blocking semantics the SystemVerilog and Verilog versions rely on,
-- so the depth is the number in the range and a reviewer can read it in one place.
signal drdy_sr : std_logic_vector(2 downto 0) := (others => '0');
signal rd_r : std_logic_vector(15 downto 0) := (others => '0');
signal tag_r : natural := 0;
signal ne_r, nr_r, nm_r, nq_r : natural := 0;
-- The configuration script. A tiny table rather than an unrolled sequence, because the point of a
-- script is that adding a register is a data change.
type cfg_t is array (0 to 2) of std_logic_vector(15 downto 0);
constant CFG_C : cfg_t := (x"2001", x"2140", x"2208");
begin
sclk <= sk_r;
cs_n <= cs_r;
mosi <= mo_r;
cfg_done <= cd_r;
rd_data <= rd_r;
rd_tag <= tag_r;
rd_valid <= rv_r;
n_events <= ne_r;
n_reads <= nr_r;
n_missed <= nm_r;
n_queued <= nq_r;
process (clk, rst_n) is
variable st : evt_state_t;
variable st_now : evt_state_t;
variable dwell : natural;
variable edges : natural;
variable tx_buf : frame_t;
variable rx_buf : frame_t;
variable nbytes : natural;
variable is_read : boolean;
variable cfg_i : natural;
variable pending : boolean;
variable pend_tag : natural;
variable cur_tag : natural;
variable lvl_armed : boolean;
variable n_half, n_lead, n_lag, n_gap : natural;
variable drdy_rise, drdy_high, busy, evt_now, evt_take : boolean;
begin
if rst_n = '0' then
st := S_CFG; dwell := 0; edges := 0;
tx_buf := (others => '0'); rx_buf := (others => '0');
nbytes := 2; is_read := false; cfg_i := 0;
pending := false; pend_tag := 0; cur_tag := 0;
drdy_sr <= (others => '0'); lvl_armed := true;
sk_r <= '0'; cs_r <= '1'; mo_r <= '0'; cd_r <= '0'; rv_r <= '0';
rd_r <= (others => '0'); tag_r <= 0;
ne_r <= 0; nr_r <= 0; nm_r <= 0; nq_r <= 0;
elsif rising_edge(clk) then
rv_r <= '0';
-- THE CASE SELECTOR IS A SNAPSHOT, and this is the fourth place in this chapter where a
-- process variable behaves differently from a non-blocking reg.
--
-- `st` is a variable, so when the event branch below sets it the `case` that follows would
-- execute the NEW state's arm in the same cycle and advance the machine a step early. The
-- SystemVerilog and Verilog versions cannot do that: `st` is a reg, the case reads its
-- pre-edge value, and the event branch's assignment lands after. Snapshotting reproduces
-- that exactly.
--
-- The symptom was one event out of twelve accounted as taken-directly rather than queued --
-- identical reads, identical losses, identical mis-timestamps, and one internal counter off
-- by one. Nothing about the design's behaviour was wrong; the language's timing was.
st_now := st;
n_half := to_integer(half);
n_lead := to_integer(lead);
n_lag := to_integer(lag);
n_gap := to_integer(gap);
-- TWO FLOPS then an edge detector. `drdy` is asynchronous by construction -- the sensor has
-- its own oscillator -- so this is the one crossing in the design. Simulation cannot show
-- that two flops are ENOUGH, because metastability is not representable in zero-delay RTL;
-- what it does show is that the detector sees each assertion exactly once.
drdy_rise := (drdy_sr(1) = '1') and (drdy_sr(2) = '0');
drdy_high := (drdy_sr(1) = '1');
busy := (st /= S_WAIT) and (st /= S_CFG);
if drdy_kind = DRDY_LEVEL then
evt_now := drdy_high and lvl_armed;
else
evt_now := drdy_rise;
end if;
-- In LEVEL mode the event is only recognised when the engine is FREE, and that is a semantic
-- difference rather than an optimisation. A pulse is transient, so the drop-versus-queue
-- question is exactly what to do when one arrives while busy. A level does not expire -- the
-- pin stays asserted because the data is still waiting -- so there is nothing to drop and
-- nothing to queue, and looking at it only when free IS the whole policy. Applying a drop
-- policy to a level destroys the notification and strands the design permanently.
if drdy_kind = DRDY_LEVEL then
evt_take := evt_now and (st /= S_CFG) and (not busy);
else
evt_take := evt_now and (st /= S_CFG);
end if;
drdy_sr <= drdy_sr(1 downto 0) & drdy;
-- The one-shot is consumed only when the event is actually RECOGNISED. Clearing it whenever
-- the pin was merely high spent the assertion during the configuration phase, where events
-- are ignored, and because a level does not pulse again it never re-armed.
if not drdy_high then lvl_armed := true;
elsif evt_take then lvl_armed := false;
end if;
-- ---- the event, and the policy, in one place ----
if evt_take then
if not busy then
ne_r <= ne_r + 1;
cur_tag := ne_r;
nr_r <= nr_r + 1;
is_read := true;
nbytes := 3; -- one command byte plus two data bytes
tx_buf := CMD_READ_C & x"0000";
rx_buf := (others => '0');
st := S_LEAD;
dwell := 0;
cs_r <= '0';
elsif policy = POLICY_QUEUE then
-- One slot. A second mid-read event overwrites the first, which is itself a loss --
-- and a deeper queue only moves the problem, because the sensor's data register is
-- not deeper.
ne_r <= ne_r + 1;
if not pending then
pending := true;
pend_tag := ne_r;
nq_r <= nq_r + 1;
else
nm_r <= nm_r + 1;
end if;
else
ne_r <= ne_r + 1;
nm_r <= nm_r + 1;
end if;
end if;
case st_now is
when S_CFG =>
if cfg_i >= CFG_N_C then
cd_r <= '1';
st := S_WAIT;
else
is_read := false;
nbytes := 2; -- register address plus value
tx_buf := CFG_C(cfg_i) & x"00";
rx_buf := (others => '0');
st := S_LEAD;
dwell := 0;
cs_r <= '0';
end if;
when S_WAIT =>
-- A queued event is serviced here, one cycle after the engine became free, so the
-- deselected gap is always honoured between frames.
if pending then
pending := false;
cur_tag := pend_tag;
nr_r <= nr_r + 1;
is_read := true;
nbytes := 3;
tx_buf := CMD_READ_C & x"0000";
rx_buf := (others => '0');
st := S_LEAD;
dwell := 0;
cs_r <= '0';
end if;
when S_LEAD =>
-- The first bit is on the pin through the lead, which is what a mode-0 slave needs
-- in order to sample it at the very first leading edge.
mo_r <= tx_buf(23);
if dwell + 1 >= n_lead then
st := S_SHIFT; dwell := 0; edges := 0;
else dwell := dwell + 1;
end if;
when S_SHIFT =>
if dwell + 1 >= n_half then
dwell := 0;
if sk_r = '0' then
-- MODE 0: capture at the leading edge.
rx_buf := rx_buf(22 downto 0) & miso;
sk_r <= '1';
else
-- Advance the transmit frame on the trailing edge, so MOSI is stable for a
-- full half period before the slave samples it.
-- THE READ COMES BEFORE THE SHIFT, and the order is not cosmetic.
--
-- `tx_buf` is a process VARIABLE, so the shift takes effect immediately and
-- anything reading it afterwards sees the shifted value. The SystemVerilog
-- and Verilog versions use a reg with a non-blocking assignment, where the
-- shift lands after the edge and a read in the same cycle gets the OLD
-- value. Written in the other order the VHDL presented the bit AFTER next,
-- so every word on the wire came out shifted left by one -- while the
-- design's own captured data, which does not read the variable in the same
-- cycle, agreed with the other two languages and hid the fault completely.
sk_r <= '0';
mo_r <= tx_buf(22);
tx_buf := tx_buf(22 downto 0) & '0';
end if;
if edges + 1 >= nbytes * 16 then
st := S_LAG;
else
edges := edges + 1;
end if;
else dwell := dwell + 1;
end if;
when S_LAG =>
if dwell + 1 >= n_lag then
st := S_GAP; dwell := 0; cs_r <= '1';
if is_read then
-- The last two bytes are the data; the first was the command, during which
-- the slave drove nothing meaningful.
rd_r <= rx_buf(15 downto 0);
tag_r <= cur_tag;
rv_r <= '1';
end if;
else dwell := dwell + 1;
end if;
when S_GAP =>
if dwell + 1 >= n_gap then
dwell := 0;
if cd_r = '0' then
cfg_i := cfg_i + 1;
st := S_CFG;
else
st := S_WAIT;
end if;
else dwell := dwell + 1;
end if;
end case;
end if;
end process;
end architecture rtl;The Bench
The sensor converts on its own timebase and never waits for the master, which is what makes the contention real. Its data register is not double-buffered, because that is the case where the policy choice has a cost.
// spi_sensor_evt_tb.sv
//
// A FREE-RUNNING SENSOR, TWO POLICIES, AND A RATE SWEEP THAT SHOWS EACH POLICY'S OWN FAILURE.
//
// The sensor is modelled here and converts on its OWN timebase: every `conv_iv` system cycles it
// replaces its data register and asserts the data-ready pin. It never waits for the master, which is
// what makes the contention real. Its data register is NOT double-buffered -- the newest conversion
// overwrites the previous one whether or not it was read -- because that is the case where the policy
// choice has a cost, and the case a datasheet has to be read to rule out.
//
// THE FOUR RESULTS.
//
// 1. THE CONFIGURATION SCRIPT RUNS FIRST, AND NO EVENT IS SERVICED BEFORE IT FINISHES. Three write
// frames, then `cfg_done`. The bench asserts the write frames appeared on the pins with the right
// bytes, because a script that silently does nothing produces a sensor that silently never
// converts -- and the symptom is an absent data-ready pin, which is investigated as a wiring fault.
//
// 2. WITH NO CONTENTION BOTH POLICIES ARE IDENTICAL. When the conversion interval exceeds the read
// duration, every event is serviced, nothing is missed, and no sample is mis-timestamped -- under
// EITHER policy. A policy comparison that only ever runs under contention cannot tell you that the
// policies agree where they should.
//
// 3. UNDER CONTENTION EACH POLICY FAILS DIFFERENTLY, AND THE BENCH MEASURES BOTH FAILURES.
// DROP loses samples and mis-timestamps none. QUEUE loses far fewer and mis-timestamps instead,
// because a queued read fetches whatever is in the sensor's register when it finally runs. The
// bench requires DROP's stale count to be ZERO and QUEUE's to be NON-ZERO, so the trade is
// measured rather than described.
//
// 4. A LEVEL-HELD DATA-READY PIN ALREADY ASSERTED AT RESET IS INVISIBLE TO AN EDGE DETECTOR. The last
// experiment releases reset with the pin already high. The edge-mode design services NOTHING and
// reports no error of any kind; the level-mode design proceeds normally. That is a bring-up failure
// with no symptom other than silence.
`timescale 1ns/1ps
module spi_sensor_evt_tb;
localparam int CFG_N = 3;
localparam int CNT_W = 16;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b1;
// ONE DRIVER EACH. The sensor model drives `sv_drdy`; the bench's "already asserted at reset"
// experiment drives `drdy_preset`. The first version had the model and the initial block both
// assigning the same reg, which is two drivers -- the level experiment then saw whichever process
// ran last and serviced nothing in either mode.
reg sv_drdy = 1'b0;
reg drdy_preset = 1'b0;
wire drdy = sv_drdy | drdy_preset;
reg drdy_level_mode = 1'b0;
reg policy_queue = 1'b0;
// half = 2, not 1. Chapter 19.1 measured why: this sensor model presents each bit one system cycle
// after the trailing edge, so a mode-0 master's leading edge has `half - 1` cycles of setup and a
// half period of 1 leaves none. Running at 1 shifts every captured word right by one bit -- a data
// fault, not an event-policy fault, and running this chapter's experiment there would measure the
// wrong thing. The boundary belongs to 19.1; this chapter stays above it.
reg [7:0] half = 8'd2, lead = 8'd2, lag = 8'd2, gap = 8'd2;
wire sclk, cs_n, mosi;
wire miso;
wire cfg_done, rd_valid;
wire [15:0] rd_data;
wire [CNT_W-1:0] rd_tag, n_events, n_reads, n_missed, n_queued;
spi_sensor_evt #(.CFG_N(CFG_N), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.drdy(drdy), .drdy_level_mode(drdy_level_mode), .policy_queue(policy_queue),
.half(half), .lead(lead), .lag(lag), .gap(gap),
.sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso),
.cfg_done(cfg_done), .rd_data(rd_data), .rd_tag(rd_tag), .rd_valid(rd_valid),
.n_events(n_events), .n_reads(n_reads), .n_missed(n_missed), .n_queued(n_queued)
);
integer errors = 0;
// ------------------------------------------------------------------
// THE SENSOR MODEL
//
// One process. Converts every `conv_iv` cycles on its own timebase, replaces its data register, and
// asserts the data-ready pin -- as a two-cycle PULSE or as a LEVEL held until a read begins,
// selected by `sensor_level`. The data register is NOT double-buffered.
// ------------------------------------------------------------------
integer conv_iv;
reg sensor_level;
reg sensor_run;
reg [15:0] sv_value; // the sensor's data register
integer conv_cnt;
integer conv_tmr;
integer pulse_tmr;
reg [23:0] sv_sh; // what the sensor presents on MISO this frame
reg cs_d, sclk_d;
// A distinct, predictable value per conversion, so a sample attributed to the wrong conversion is
// detectable rather than merely suspicious.
function [15:0] sv_of(input integer k);
begin sv_of = {8'h50 + k[7:0], 8'hC3 ^ k[7:0]}; end
endfunction
// The value that was current when each event was announced. The design tags every read with the
// event it is servicing, so a mis-timestamped sample is an exact comparison rather than a guess.
reg [15:0] val_at_event [0:255];
integer n_stale, n_checked;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sv_value <= 16'd0;
conv_cnt <= 0;
conv_tmr <= 0;
pulse_tmr <= 0;
sv_sh <= 24'd0;
cs_d <= 1'b1;
sclk_d <= 1'b0;
sv_drdy <= 1'b0;
end else begin
cs_d <= cs_n;
sclk_d <= sclk;
// ---- the conversion timebase, independent of the master ----
if (sensor_run) begin
if (conv_tmr + 1 >= conv_iv) begin
conv_tmr <= 0;
sv_value <= sv_of(conv_cnt);
val_at_event[conv_cnt % 256] = sv_of(conv_cnt);
conv_cnt <= conv_cnt + 1;
sv_drdy <= 1'b1;
pulse_tmr <= 2;
end else begin
conv_tmr <= conv_tmr + 1;
end
end
// ---- the data-ready pin's shape ----
if (sensor_level) begin
// A LEVEL held until a read begins. This is the shape that strands an edge detector when
// the pin is already asserted before reset releases.
if (cs_d && !cs_n) sv_drdy <= 1'b0;
end else begin
if (pulse_tmr > 1) pulse_tmr <= pulse_tmr - 1;
else if (pulse_tmr == 1) begin pulse_tmr <= 0; sv_drdy <= 1'b0; end
end
// ---- the read response ----
// Loaded at the select with a leading zero byte (the command phase, during which the sensor
// drives nothing meaningful) followed by the CURRENT contents of the data register. A queued
// read therefore gets whatever is there when it finally runs, which is the whole point.
if (cs_d && !cs_n) begin
sv_sh <= {8'h00, sv_value};
end else if (sclk_d && !sclk) begin
sv_sh <= {sv_sh[22:0], 1'b0};
end
end
end
assign miso = sv_sh[23];
// ---- per-read checking: was this sample attributed to the right conversion? ----
integer x_reports;
reg dbg = 1'b0;
always @(posedge clk) if (rst_n && rd_valid) begin
n_checked = n_checked + 1;
if (rd_data !== val_at_event[rd_tag % 256]) begin
n_stale = n_stale + 1;
if (dbg) $display(" DBG read tag=%0d data=%04h expected=%04h", rd_tag, rd_data, val_at_event[rd_tag % 256]);
end
if ((^rd_data === 1'bx) || (^rd_tag === 1'bx) || (^n_events === 1'bx)
|| (^n_missed === 1'bx) || (^n_queued === 1'bx))
x_reports = x_reports + 1;
end
// ---- the configuration script, observed on the pins ----
//
// Independent of the design's own `cfg_done`: a flag the design raises about itself is not evidence
// that the frames happened.
integer cfg_frames;
reg [23:0] mon_sh;
integer mon_bits;
reg [15:0] cfg_seen [0:7];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cfg_frames <= 0; mon_sh <= 24'd0; mon_bits <= 0;
end else begin
if (cs_d && !cs_n) begin mon_sh <= 24'd0; mon_bits <= 0; end
else if (!cs_n && !sclk_d && sclk) begin
mon_sh <= {mon_sh[22:0], mosi};
mon_bits <= mon_bits + 1;
end else if (!cs_d && cs_n) begin
if (mon_bits == 16 && cfg_frames < 8) begin
cfg_seen[cfg_frames] = mon_sh[15:0];
cfg_frames <= cfg_frames + 1;
end
end
end
end
// ------------------------------------------------------------------
task automatic reset_all;
begin
sensor_run = 1'b0;
@(negedge clk); rst_n = 1'b0;
n_stale = 0; n_checked = 0;
repeat (6) @(negedge clk);
rst_n = 1'b1;
repeat (2) @(negedge clk);
end
endtask
// Read duration in system cycles: lead + 3 bytes x 16 edges x half + lag + gap.
//
// A Verilog-2001 function must take at least one argument, so the half period is passed rather than
// read from the enclosing scope. That restriction is worth honouring in the SystemVerilog source
// too: the Verilog conversion is mechanical, and a zero-argument function would not survive it.
function integer read_cycles(input [7:0] h);
begin read_cycles = lead + 3*16*h + lag + gap; end
endfunction
integer k, p, guard;
integer r_ev, r_rd, r_ms, r_qu, r_st;
integer IVS [0:2];
integer res_ev [0:5], res_rd[0:5], res_ms[0:5], res_qu[0:5], res_st[0:5];
integer mutations, idx;
// Run until the sensor has produced `n` conversions, bounded.
task automatic run_convs(input integer n);
begin
guard = 0;
while ((conv_cnt < n) && (guard < 400000)) begin @(posedge clk); guard = guard + 1; end
if (guard >= 400000) begin
$display(" FAIL: timeout waiting for %0d conversions (saw %0d)", n, conv_cnt);
errors = errors + 1;
end
sensor_run = 1'b0;
repeat (read_cycles(half) + 20) @(posedge clk);
r_ev = n_events; r_rd = n_reads; r_ms = n_missed; r_qu = n_queued; r_st = n_stale;
end
endtask
initial begin
x_reports = 0; mutations = 0; n_stale = 0; n_checked = 0;
conv_iv = 240; sensor_level = 1'b0; sensor_run = 1'b0; dbg = 1'b0;
IVS[0] = 240; IVS[1] = 80; IVS[2] = 50;
// ============ 1. the configuration script, observed on the pins ============
reset_all;
guard = 0;
while (!cfg_done && guard < 40000) begin @(posedge clk); guard = guard + 1; end
if (!cfg_done) begin
$display(" FAIL: the configuration script never completed");
errors = errors + 1;
end
if (cfg_frames != CFG_N) begin
$display(" FAIL: %0d configuration frames appeared on the pins where %0d were expected",
cfg_frames, CFG_N);
errors = errors + 1;
end
if (n_reads != 0) begin
$display(" FAIL: %0d read(s) were started before configuration finished", n_reads);
errors = errors + 1;
end
$display(" configuration: %0d frames on the pins -> %04h %04h %04h, cfg_done=%b, reads before cfg=%0d",
cfg_frames, cfg_seen[0], cfg_seen[1], cfg_seen[2], cfg_done, n_reads);
if (!(cfg_seen[0] === 16'h2001 && cfg_seen[1] === 16'h2140 && cfg_seen[2] === 16'h2208)) begin
$display(" FAIL: the configuration bytes on the pins are not the script's");
errors = errors + 1;
end
// ============ 2 and 3. the rate sweep, both policies ============
$display("");
$display(" read duration = %0d cycles (lead %0d + 3 bytes x 16 edges x half %0d + lag %0d + gap %0d)",
read_cycles(half), lead, half, lag, gap);
$display("");
$display(" policy conv_iv events reads missed queued stale what it costs");
for (p = 0; p < 2; p = p + 1) begin
for (k = 0; k < 3; k = k + 1) begin
idx = p*3 + k;
policy_queue = p[0];
conv_iv = IVS[k];
drdy_level_mode = 1'b0;
sensor_level = 1'b0;
reset_all;
guard = 0;
while (!cfg_done && guard < 40000) begin @(posedge clk); guard = guard + 1; end
sensor_run = 1'b1;
run_convs(12);
res_ev[idx] = r_ev; res_rd[idx] = r_rd; res_ms[idx] = r_ms;
res_qu[idx] = r_qu; res_st[idx] = r_st;
// `%-6s` IS NOT PORTABLE. Icarus pads it in one language mode and not the other, so the
// SystemVerilog and Verilog transcripts differed by whitespace alone -- which is exactly
// the kind of difference that makes a cross-language comparison useless. Every string
// column here is pre-padded to a fixed width and every free-text column sits at the END
// of the row.
$display(" %0s %7d %6d %5d %6d %6d %5d %0s",
(p == 0) ? "DROP " : "QUEUE", conv_iv, r_ev, r_rd, r_ms, r_qu, r_st,
(conv_iv > read_cycles(half)) ? "nothing -- no contention" :
(p == 0) ? "samples LOST, timestamps intact" :
"samples KEPT, timestamps wrong");
end
end
// no contention: both policies clean
if (!(res_ms[0] == 0 && res_st[0] == 0 && res_ms[3] == 0 && res_st[3] == 0)) begin
$display(" FAIL: a conversion interval longer than a read still lost or mis-timestamped samples (DROP %0d/%0d, QUEUE %0d/%0d)",
res_ms[0], res_st[0], res_ms[3], res_st[3]);
errors = errors + 1;
end
if (!(res_rd[0] == res_ev[0] && res_rd[3] == res_ev[3])) begin
$display(" FAIL: without contention not every event was serviced (DROP %0d/%0d, QUEUE %0d/%0d)",
res_rd[0], res_ev[0], res_rd[3], res_ev[3]);
errors = errors + 1;
end
// contention: DROP loses and never mis-timestamps; QUEUE mis-timestamps and loses less
if (res_ms[2] == 0) begin
$display(" FAIL: the DROP policy under heavy contention lost nothing");
errors = errors + 1;
end
if (res_st[2] != 0) begin
$display(" FAIL: the DROP policy mis-timestamped %0d sample(s); dropping cannot do that",
res_st[2]);
errors = errors + 1;
end
if (res_st[5] == 0) begin
$display(" FAIL: the QUEUE policy under heavy contention mis-timestamped nothing, so the trade this chapter is about was not exercised");
errors = errors + 1;
end
if (!(res_ms[5] < res_ms[2])) begin
$display(" FAIL: the QUEUE policy did not lose fewer samples than DROP (%0d against %0d)",
res_ms[5], res_ms[2]);
errors = errors + 1;
end
// ============ 4. a level-held pin already asserted at reset ============
$display("");
$display(" mode drdy at reset events reads missed outcome");
for (p = 0; p < 2; p = p + 1) begin
policy_queue = 1'b0;
drdy_level_mode = p[0];
sensor_level = 1'b1;
conv_iv = 240;
sensor_run = 1'b0;
@(negedge clk); rst_n = 1'b0;
n_stale = 0; n_checked = 0;
// ONE WRITER PER OBJECT. The first version also assigned `val_at_event[0]` and `sv_value`
// from here, which the sensor process already drives. A SystemVerilog reg tolerates two
// procedural writers and simply takes the last one; a VHDL signal resolves them, and the
// expected-value array came back as neither value. This experiment checks no data, so the
// second writer was never needed -- and removing it is what makes the three languages agree.
drdy_preset = 1'b1; // the sensor converted while the FPGA was booting
repeat (6) @(negedge clk);
rst_n = 1'b1;
guard = 0;
while (!cfg_done && guard < 40000) begin @(posedge clk); guard = guard + 1; end
repeat (read_cycles(half) * 3) @(posedge clk);
$display(" %0s %13b %6d %5d %6d %0s",
(p == 0) ? "EDGE " : "LEVEL", 1'b1, n_events, n_reads, n_missed,
(n_reads == 0) ? "STRANDED -- the one edge it saw was spent during configuration"
: "serviced normally");
if (p == 0 && n_reads != 0) begin
$display(" FAIL: the edge-mode design serviced a pin that never produced an edge");
errors = errors + 1;
end
if (p == 1 && n_reads == 0) begin
$display(" FAIL: the level-mode design did not service an already-asserted pin");
errors = errors + 1;
end
// A level assertion must be ONE event, not one per cycle. Without the design's one-shot this
// read 515 -- a meaningless number that the read behaviour hid completely.
if (p == 1 && n_events != 1) begin
$display(" FAIL: a single held assertion produced %0d events where 1 was expected",
n_events);
errors = errors + 1;
end
end
drdy_preset = 1'b0; sensor_level = 1'b0;
// ============ conclusions ============
$display("");
$display(" 1. the configuration script ran to completion before any event was serviced, and the three write frames were verified ON THE PINS rather than from the design's own cfg_done flag -- %04h, %04h, %04h. A flag a design raises about itself is not evidence that the frames happened, and a script that silently does nothing leaves a sensor that never converts, whose symptom is an absent data-ready pin and whose investigation is a wiring check",
cfg_seen[0], cfg_seen[1], cfg_seen[2]);
$display(" 2. with a conversion interval of %0d cycles against a read duration of %0d, the two policies are INDISTINGUISHABLE -- every event serviced, nothing missed, nothing mis-timestamped, under both. A policy comparison that only runs under contention never establishes that the policies agree where they should, and that agreement is what makes the contention result attributable to the policy rather than to the design",
IVS[0], read_cycles(half));
$display(" 3. under contention each policy fails DIFFERENTLY. At a %0d-cycle interval DROP lost %0d of %0d events and mis-timestamped %0d; QUEUE lost %0d and mis-timestamped %0d. Dropping keeps the remaining stream correctly attributed and throws samples away; queueing keeps the samples and attributes some of them to the wrong instant, because a queued read fetches whatever is in the sensor's register when it finally runs. Which is worse is an APPLICATION question -- a control loop usually prefers the dropped sample, a logger usually prefers the kept one -- and the hardware's obligation is to implement one deliberately and make the other's cost visible",
IVS[2], res_ms[2], res_ev[2], res_st[2], res_ms[5], res_st[5]);
$display(" 4. and a LEVEL-held data-ready pin that was already asserted when reset released left the EDGE-mode design permanently stranded: 1 event, 1 missed, 0 reads, and then nothing for the rest of time. The single event is itself the interesting part -- the synchroniser chain comes out of reset holding zero, so an already-high pin manufactures exactly ONE spurious rising edge, which landed inside the configuration window where events are deliberately ignored. The design therefore spent the only notification it would ever receive before it was ready to act on it. The LEVEL-mode design serviced the same pin normally, because a level does not expire. No counter distinguishes the two outcomes and no assertion fires: the failure has no symptom other than silence, and the sensor having converted while the FPGA was still booting is the NORMAL case rather than a corner one");
// ============ BENCH INTEGRITY ============
// Two deliberately wrong expectations, compared by the same operators as the real checks; and
// the stale checker must have actually run.
if (cfg_seen[0] !== 16'hDEAD) mutations = mutations + 1;
if (res_st[5] != 0) mutations = mutations + 1;
if (mutations != 2) begin
$display(" FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
errors = errors + 1;
end
if (n_checked == 0) begin
$display(" FAIL: the mis-timestamp checker never executed");
errors = errors + 1;
end
if (x_reports != 0) begin
$display(" FAIL: %0d reported fields contained an X", x_reports);
errors = errors + 1;
end
if (errors == 0) begin
$display("");
$display(" and the bench proved itself: two deliberately wrong expectations mismatched, the mis-timestamp checker ran on every completed read, every reported field carried a known value, and the sensor model converts on its own timebase without ever waiting for the master -- so the contention it creates is real");
$display("PASS: when the DEVICE owns the schedule, the design question is what to do about an event that arrives while a read is in progress, and there are exactly two answers with opposite costs. With a conversion interval longer than a read the two policies are indistinguishable -- which is what makes the contention result attributable. Under contention DROP lost %0d of %0d events and mis-timestamped 0, while QUEUE lost %0d and mis-timestamped %0d: dropping throws samples away and keeps the rest correctly attributed, queueing keeps the samples and attributes some to the wrong instant, because a queued read fetches whatever is in the sensor's register when it finally runs. Which is worse is an application question, and it turns on one line of the datasheet -- a DOUBLE-BUFFERED data register makes queueing strictly better and a single register makes it a trade. And the policy question belongs to the PULSE interface alone: a level-held pin does not expire, so there is nothing to drop and nothing to queue, and recognising it only when the engine is free IS the whole policy. Applying a drop policy to a level destroys the notification -- which is how the edge-mode design ended up permanently stranded by a pin that was already asserted when reset released, having spent on the configuration phase the single spurious edge its synchroniser manufactured coming out of reset. 1 event, 1 missed, 0 reads, no counter distinguishing it from a dead sensor",
res_ms[2], res_ev[2], res_ms[5], res_st[5]);
end else begin
$display("FAIL: %0d error(s)", errors);
end
$finish;
end
endmodule// spi_sensor_evt_tb.v
//
// A FREE-RUNNING SENSOR, TWO POLICIES, AND A RATE SWEEP THAT SHOWS EACH POLICY'S OWN FAILURE.
//
// The sensor is modelled here and converts on its OWN timebase: every `conv_iv` system cycles it
// replaces its data register and asserts the data-ready pin. It never waits for the master, which is
// what makes the contention real. Its data register is NOT double-buffered -- the newest conversion
// overwrites the previous one whether or not it was read -- because that is the case where the policy
// choice has a cost, and the case a datasheet has to be read to rule out.
//
// THE FOUR RESULTS.
//
// 1. THE CONFIGURATION SCRIPT RUNS FIRST, AND NO EVENT IS SERVICED BEFORE IT FINISHES. Three write
// frames, then `cfg_done`. The bench asserts the write frames appeared on the pins with the right
// bytes, because a script that silently does nothing produces a sensor that silently never
// converts -- and the symptom is an absent data-ready pin, which is investigated as a wiring fault.
//
// 2. WITH NO CONTENTION BOTH POLICIES ARE IDENTICAL. When the conversion interval exceeds the read
// duration, every event is serviced, nothing is missed, and no sample is mis-timestamped -- under
// EITHER policy. A policy comparison that only ever runs under contention cannot tell you that the
// policies agree where they should.
//
// 3. UNDER CONTENTION EACH POLICY FAILS DIFFERENTLY, AND THE BENCH MEASURES BOTH FAILURES.
// DROP loses samples and mis-timestamps none. QUEUE loses far fewer and mis-timestamps instead,
// because a queued read fetches whatever is in the sensor's register when it finally runs. The
// bench requires DROP's stale count to be ZERO and QUEUE's to be NON-ZERO, so the trade is
// measured rather than described.
//
// 4. A LEVEL-HELD DATA-READY PIN ALREADY ASSERTED AT RESET IS INVISIBLE TO AN EDGE DETECTOR. The last
// experiment releases reset with the pin already high. The edge-mode design services NOTHING and
// reports no error of any kind; the level-mode design proceeds normally. That is a bring-up failure
// with no symptom other than silence.
`timescale 1ns/1ps
module spi_sensor_evt_tb;
localparam CFG_N = 3;
localparam CNT_W = 16;
reg clk;
always #5 clk = ~clk;
reg rst_n;
// ONE DRIVER EACH. The sensor model drives `sv_drdy`; the bench's "already asserted at reset"
// experiment drives `drdy_preset`. The first version had the model and the initial block both
// assigning the same reg, which is two drivers -- the level experiment then saw whichever process
// ran last and serviced nothing in either mode.
reg sv_drdy;
reg drdy_preset;
wire drdy = sv_drdy | drdy_preset;
reg drdy_level_mode;
reg policy_queue;
// half = 2, not 1. Chapter 19.1 measured why: this sensor model presents each bit one system cycle
// after the trailing edge, so a mode-0 master's leading edge has `half - 1` cycles of setup and a
// half period of 1 leaves none. Running at 1 shifts every captured word right by one bit -- a data
// fault, not an event-policy fault, and running this chapter's experiment there would measure the
// wrong thing. The boundary belongs to 19.1; this chapter stays above it.
reg [7:0] half, lead, lag, gap;
wire sclk, cs_n, mosi;
wire miso;
wire cfg_done, rd_valid;
wire [15:0] rd_data;
wire [CNT_W-1:0] rd_tag, n_events, n_reads, n_missed, n_queued;
spi_sensor_evt #(.CFG_N(CFG_N), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.drdy(drdy), .drdy_level_mode(drdy_level_mode), .policy_queue(policy_queue),
.half(half), .lead(lead), .lag(lag), .gap(gap),
.sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso),
.cfg_done(cfg_done), .rd_data(rd_data), .rd_tag(rd_tag), .rd_valid(rd_valid),
.n_events(n_events), .n_reads(n_reads), .n_missed(n_missed), .n_queued(n_queued)
);
integer errors;
// ------------------------------------------------------------------
// THE SENSOR MODEL
//
// One process. Converts every `conv_iv` cycles on its own timebase, replaces its data register, and
// asserts the data-ready pin -- as a two-cycle PULSE or as a LEVEL held until a read begins,
// selected by `sensor_level`. The data register is NOT double-buffered.
// ------------------------------------------------------------------
integer conv_iv;
reg sensor_level;
reg sensor_run;
reg [15:0] sv_value; // the sensor's data register
integer conv_cnt;
integer conv_tmr;
integer pulse_tmr;
reg [23:0] sv_sh; // what the sensor presents on MISO this frame
reg cs_d, sclk_d;
// A distinct, predictable value per conversion, so a sample attributed to the wrong conversion is
// detectable rather than merely suspicious.
function [15:0] sv_of;
input integer k;
begin sv_of = {8'h50 + k[7:0], 8'hC3 ^ k[7:0]}; end
endfunction
// The value that was current when each event was announced. The design tags every read with the
// event it is servicing, so a mis-timestamped sample is an exact comparison rather than a guess.
reg [15:0] val_at_event [0:255];
integer n_stale, n_checked;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sv_value <= 16'd0;
conv_cnt <= 0;
conv_tmr <= 0;
pulse_tmr <= 0;
sv_sh <= 24'd0;
cs_d <= 1'b1;
sclk_d <= 1'b0;
sv_drdy <= 1'b0;
end else begin
cs_d <= cs_n;
sclk_d <= sclk;
// ---- the conversion timebase, independent of the master ----
if (sensor_run) begin
if (conv_tmr + 1 >= conv_iv) begin
conv_tmr <= 0;
sv_value <= sv_of(conv_cnt);
val_at_event[conv_cnt % 256] = sv_of(conv_cnt);
conv_cnt <= conv_cnt + 1;
sv_drdy <= 1'b1;
pulse_tmr <= 2;
end else begin
conv_tmr <= conv_tmr + 1;
end
end
// ---- the data-ready pin's shape ----
if (sensor_level) begin
// A LEVEL held until a read begins. This is the shape that strands an edge detector when
// the pin is already asserted before reset releases.
if (cs_d && !cs_n) sv_drdy <= 1'b0;
end else begin
if (pulse_tmr > 1) pulse_tmr <= pulse_tmr - 1;
else if (pulse_tmr == 1) begin pulse_tmr <= 0; sv_drdy <= 1'b0; end
end
// ---- the read response ----
// Loaded at the select with a leading zero byte (the command phase, during which the sensor
// drives nothing meaningful) followed by the CURRENT contents of the data register. A queued
// read therefore gets whatever is there when it finally runs, which is the whole point.
if (cs_d && !cs_n) begin
sv_sh <= {8'h00, sv_value};
end else if (sclk_d && !sclk) begin
sv_sh <= {sv_sh[22:0], 1'b0};
end
end
end
assign miso = sv_sh[23];
// ---- per-read checking: was this sample attributed to the right conversion? ----
integer x_reports;
reg dbg;
always @(posedge clk) if (rst_n && rd_valid) begin
n_checked = n_checked + 1;
if (rd_data !== val_at_event[rd_tag % 256]) begin
n_stale = n_stale + 1;
if (dbg) $display(" DBG read tag=%0d data=%04h expected=%04h", rd_tag, rd_data, val_at_event[rd_tag % 256]);
end
if ((^rd_data === 1'bx) || (^rd_tag === 1'bx) || (^n_events === 1'bx)
|| (^n_missed === 1'bx) || (^n_queued === 1'bx))
x_reports = x_reports + 1;
end
// ---- the configuration script, observed on the pins ----
//
// Independent of the design's own `cfg_done`: a flag the design raises about itself is not evidence
// that the frames happened.
integer cfg_frames;
reg [23:0] mon_sh;
integer mon_bits;
reg [15:0] cfg_seen [0:7];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cfg_frames <= 0; mon_sh <= 24'd0; mon_bits <= 0;
end else begin
if (cs_d && !cs_n) begin mon_sh <= 24'd0; mon_bits <= 0; end
else if (!cs_n && !sclk_d && sclk) begin
mon_sh <= {mon_sh[22:0], mosi};
mon_bits <= mon_bits + 1;
end else if (!cs_d && cs_n) begin
if (mon_bits == 16 && cfg_frames < 8) begin
cfg_seen[cfg_frames] = mon_sh[15:0];
cfg_frames <= cfg_frames + 1;
end
end
end
end
// ------------------------------------------------------------------
task reset_all;
begin
sensor_run = 1'b0;
@(negedge clk); rst_n = 1'b0;
n_stale = 0; n_checked = 0;
repeat (6) @(negedge clk);
rst_n = 1'b1;
repeat (2) @(negedge clk);
end
endtask
// Read duration in system cycles: lead + 3 bytes x 16 edges x half + lag + gap.
//
// A Verilog-2001 function must take at least one argument, so the half period is passed rather than
// read from the enclosing scope. That restriction is worth honouring in the SystemVerilog source
// too: the Verilog conversion is mechanical, and a zero-argument function would not survive it.
function integer read_cycles;
input [7:0] h;
begin read_cycles = lead + 3*16*h + lag + gap; end
endfunction
integer k, p, guard;
integer r_ev, r_rd, r_ms, r_qu, r_st;
integer IVS [0:2];
integer res_ev [0:5], res_rd[0:5], res_ms[0:5], res_qu[0:5], res_st[0:5];
integer mutations, idx;
// Run until the sensor has produced `n` conversions, bounded.
task run_convs;
input integer n;
begin
guard = 0;
while ((conv_cnt < n) && (guard < 400000)) begin @(posedge clk); guard = guard + 1; end
if (guard >= 400000) begin
$display(" FAIL: timeout waiting for %0d conversions (saw %0d)", n, conv_cnt);
errors = errors + 1;
end
sensor_run = 1'b0;
repeat (read_cycles(half) + 20) @(posedge clk);
r_ev = n_events; r_rd = n_reads; r_ms = n_missed; r_qu = n_queued; r_st = n_stale;
end
endtask
initial begin
x_reports = 0; mutations = 0; n_stale = 0; n_checked = 0;
conv_iv = 240; sensor_level = 1'b0; sensor_run = 1'b0; dbg = 1'b0;
IVS[0] = 240; IVS[1] = 80; IVS[2] = 50;
// ============ 1. the configuration script, observed on the pins ============
reset_all;
guard = 0;
while (!cfg_done && guard < 40000) begin @(posedge clk); guard = guard + 1; end
if (!cfg_done) begin
$display(" FAIL: the configuration script never completed");
errors = errors + 1;
end
if (cfg_frames != CFG_N) begin
$display(" FAIL: %0d configuration frames appeared on the pins where %0d were expected",
cfg_frames, CFG_N);
errors = errors + 1;
end
if (n_reads != 0) begin
$display(" FAIL: %0d read(s) were started before configuration finished", n_reads);
errors = errors + 1;
end
$display(" configuration: %0d frames on the pins -> %04h %04h %04h, cfg_done=%b, reads before cfg=%0d",
cfg_frames, cfg_seen[0], cfg_seen[1], cfg_seen[2], cfg_done, n_reads);
if (!(cfg_seen[0] === 16'h2001 && cfg_seen[1] === 16'h2140 && cfg_seen[2] === 16'h2208)) begin
$display(" FAIL: the configuration bytes on the pins are not the script's");
errors = errors + 1;
end
// ============ 2 and 3. the rate sweep, both policies ============
$display("");
$display(" read duration = %0d cycles (lead %0d + 3 bytes x 16 edges x half %0d + lag %0d + gap %0d)",
read_cycles(half), lead, half, lag, gap);
$display("");
$display(" policy conv_iv events reads missed queued stale what it costs");
for (p = 0; p < 2; p = p + 1) begin
for (k = 0; k < 3; k = k + 1) begin
idx = p*3 + k;
policy_queue = p[0];
conv_iv = IVS[k];
drdy_level_mode = 1'b0;
sensor_level = 1'b0;
reset_all;
guard = 0;
while (!cfg_done && guard < 40000) begin @(posedge clk); guard = guard + 1; end
sensor_run = 1'b1;
run_convs(12);
res_ev[idx] = r_ev; res_rd[idx] = r_rd; res_ms[idx] = r_ms;
res_qu[idx] = r_qu; res_st[idx] = r_st;
// `%0s` IS NOT PORTABLE. Icarus pads it in one language mode and not the other, so the
// SystemVerilog and Verilog transcripts differed by whitespace alone -- which is exactly
// the kind of difference that makes a cross-language comparison useless. Every string
// column here is pre-padded to a fixed width and every free-text column sits at the END
// of the row.
$display(" %0s %7d %6d %5d %6d %6d %5d %0s",
(p == 0) ? "DROP " : "QUEUE", conv_iv, r_ev, r_rd, r_ms, r_qu, r_st,
(conv_iv > read_cycles(half)) ? "nothing -- no contention" :
(p == 0) ? "samples LOST, timestamps intact" :
"samples KEPT, timestamps wrong");
end
end
// no contention: both policies clean
if (!(res_ms[0] == 0 && res_st[0] == 0 && res_ms[3] == 0 && res_st[3] == 0)) begin
$display(" FAIL: a conversion interval longer than a read still lost or mis-timestamped samples (DROP %0d/%0d, QUEUE %0d/%0d)",
res_ms[0], res_st[0], res_ms[3], res_st[3]);
errors = errors + 1;
end
if (!(res_rd[0] == res_ev[0] && res_rd[3] == res_ev[3])) begin
$display(" FAIL: without contention not every event was serviced (DROP %0d/%0d, QUEUE %0d/%0d)",
res_rd[0], res_ev[0], res_rd[3], res_ev[3]);
errors = errors + 1;
end
// contention: DROP loses and never mis-timestamps; QUEUE mis-timestamps and loses less
if (res_ms[2] == 0) begin
$display(" FAIL: the DROP policy under heavy contention lost nothing");
errors = errors + 1;
end
if (res_st[2] != 0) begin
$display(" FAIL: the DROP policy mis-timestamped %0d sample(s); dropping cannot do that",
res_st[2]);
errors = errors + 1;
end
if (res_st[5] == 0) begin
$display(" FAIL: the QUEUE policy under heavy contention mis-timestamped nothing, so the trade this chapter is about was not exercised");
errors = errors + 1;
end
if (!(res_ms[5] < res_ms[2])) begin
$display(" FAIL: the QUEUE policy did not lose fewer samples than DROP (%0d against %0d)",
res_ms[5], res_ms[2]);
errors = errors + 1;
end
// ============ 4. a level-held pin already asserted at reset ============
$display("");
$display(" mode drdy at reset events reads missed outcome");
for (p = 0; p < 2; p = p + 1) begin
policy_queue = 1'b0;
drdy_level_mode = p[0];
sensor_level = 1'b1;
conv_iv = 240;
sensor_run = 1'b0;
@(negedge clk); rst_n = 1'b0;
n_stale = 0; n_checked = 0;
// ONE WRITER PER OBJECT. The first version also assigned `val_at_event[0]` and `sv_value`
// from here, which the sensor process already drives. A SystemVerilog reg tolerates two
// procedural writers and simply takes the last one; a VHDL signal resolves them, and the
// expected-value array came back as neither value. This experiment checks no data, so the
// second writer was never needed -- and removing it is what makes the three languages agree.
drdy_preset = 1'b1; // the sensor converted while the FPGA was booting
repeat (6) @(negedge clk);
rst_n = 1'b1;
guard = 0;
while (!cfg_done && guard < 40000) begin @(posedge clk); guard = guard + 1; end
repeat (read_cycles(half) * 3) @(posedge clk);
$display(" %0s %13b %6d %5d %6d %0s",
(p == 0) ? "EDGE " : "LEVEL", 1'b1, n_events, n_reads, n_missed,
(n_reads == 0) ? "STRANDED -- the one edge it saw was spent during configuration"
: "serviced normally");
if (p == 0 && n_reads != 0) begin
$display(" FAIL: the edge-mode design serviced a pin that never produced an edge");
errors = errors + 1;
end
if (p == 1 && n_reads == 0) begin
$display(" FAIL: the level-mode design did not service an already-asserted pin");
errors = errors + 1;
end
// A level assertion must be ONE event, not one per cycle. Without the design's one-shot this
// read 515 -- a meaningless number that the read behaviour hid completely.
if (p == 1 && n_events != 1) begin
$display(" FAIL: a single held assertion produced %0d events where 1 was expected",
n_events);
errors = errors + 1;
end
end
drdy_preset = 1'b0; sensor_level = 1'b0;
// ============ conclusions ============
$display("");
$display(" 1. the configuration script ran to completion before any event was serviced, and the three write frames were verified ON THE PINS rather than from the design's own cfg_done flag -- %04h, %04h, %04h. A flag a design raises about itself is not evidence that the frames happened, and a script that silently does nothing leaves a sensor that never converts, whose symptom is an absent data-ready pin and whose investigation is a wiring check",
cfg_seen[0], cfg_seen[1], cfg_seen[2]);
$display(" 2. with a conversion interval of %0d cycles against a read duration of %0d, the two policies are INDISTINGUISHABLE -- every event serviced, nothing missed, nothing mis-timestamped, under both. A policy comparison that only runs under contention never establishes that the policies agree where they should, and that agreement is what makes the contention result attributable to the policy rather than to the design",
IVS[0], read_cycles(half));
$display(" 3. under contention each policy fails DIFFERENTLY. At a %0d-cycle interval DROP lost %0d of %0d events and mis-timestamped %0d; QUEUE lost %0d and mis-timestamped %0d. Dropping keeps the remaining stream correctly attributed and throws samples away; queueing keeps the samples and attributes some of them to the wrong instant, because a queued read fetches whatever is in the sensor's register when it finally runs. Which is worse is an APPLICATION question -- a control loop usually prefers the dropped sample, a logger usually prefers the kept one -- and the hardware's obligation is to implement one deliberately and make the other's cost visible",
IVS[2], res_ms[2], res_ev[2], res_st[2], res_ms[5], res_st[5]);
$display(" 4. and a LEVEL-held data-ready pin that was already asserted when reset released left the EDGE-mode design permanently stranded: 1 event, 1 missed, 0 reads, and then nothing for the rest of time. The single event is itself the interesting part -- the synchroniser chain comes out of reset holding zero, so an already-high pin manufactures exactly ONE spurious rising edge, which landed inside the configuration window where events are deliberately ignored. The design therefore spent the only notification it would ever receive before it was ready to act on it. The LEVEL-mode design serviced the same pin normally, because a level does not expire. No counter distinguishes the two outcomes and no assertion fires: the failure has no symptom other than silence, and the sensor having converted while the FPGA was still booting is the NORMAL case rather than a corner one");
// ============ BENCH INTEGRITY ============
// Two deliberately wrong expectations, compared by the same operators as the real checks; and
// the stale checker must have actually run.
if (cfg_seen[0] !== 16'hDEAD) mutations = mutations + 1;
if (res_st[5] != 0) mutations = mutations + 1;
if (mutations != 2) begin
$display(" FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
errors = errors + 1;
end
if (n_checked == 0) begin
$display(" FAIL: the mis-timestamp checker never executed");
errors = errors + 1;
end
if (x_reports != 0) begin
$display(" FAIL: %0d reported fields contained an X", x_reports);
errors = errors + 1;
end
if (errors == 0) begin
$display("");
$display(" and the bench proved itself: two deliberately wrong expectations mismatched, the mis-timestamp checker ran on every completed read, every reported field carried a known value, and the sensor model converts on its own timebase without ever waiting for the master -- so the contention it creates is real");
$display("PASS: when the DEVICE owns the schedule, the design question is what to do about an event that arrives while a read is in progress, and there are exactly two answers with opposite costs. With a conversion interval longer than a read the two policies are indistinguishable -- which is what makes the contention result attributable. Under contention DROP lost %0d of %0d events and mis-timestamped 0, while QUEUE lost %0d and mis-timestamped %0d: dropping throws samples away and keeps the rest correctly attributed, queueing keeps the samples and attributes some to the wrong instant, because a queued read fetches whatever is in the sensor's register when it finally runs. Which is worse is an application question, and it turns on one line of the datasheet -- a DOUBLE-BUFFERED data register makes queueing strictly better and a single register makes it a trade. And the policy question belongs to the PULSE interface alone: a level-held pin does not expire, so there is nothing to drop and nothing to queue, and recognising it only when the engine is free IS the whole policy. Applying a drop policy to a level destroys the notification -- which is how the edge-mode design ended up permanently stranded by a pin that was already asserted when reset released, having spent on the configuration phase the single spurious edge its synchroniser manufactured coming out of reset. 1 event, 1 missed, 0 reads, no counter distinguishing it from a dead sensor",
res_ms[2], res_ev[2], res_ms[5], res_st[5]);
end else begin
$display("FAIL: %0d error(s)", errors);
end
$finish;
end
initial begin
half = 8'd2;
lead = 8'd2;
lag = 8'd2;
gap = 8'd2;
clk = 1'b0;
rst_n = 1'b1;
sv_drdy = 1'b0;
drdy_preset = 1'b0;
drdy_level_mode = 1'b0;
policy_queue = 1'b0;
errors = 0;
dbg = 1'b0;
end
endmodule-- spi_sensor_evt_tb.vhd
--
-- A FREE-RUNNING SENSOR, TWO POLICIES, AND A RATE SWEEP THAT SHOWS EACH POLICY'S OWN FAILURE.
--
-- The sensor is modelled here and converts on its OWN timebase: every `conv_iv` system cycles it replaces
-- its data register and asserts the data-ready pin. It never waits for the master, which is what makes
-- the contention real. Its data register is NOT double-buffered -- the newest conversion overwrites the
-- previous one whether or not it was read -- because that is the case where the policy choice has a cost,
-- and the case a datasheet has to be read to rule out.
--
-- The same four results as the other two languages, with the same numbers.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). `CFG_N_C`, `CNT_W`, `HALF_C`, `LEAD_C`, `LAG_C`, `GAP_C`
-- carry suffixes; the signals driving the design are `s_half`, `s_lead` and so on, prefixed rather than
-- case-varied. Nothing collides with a reserved word.
--
-- RANGE DIRECTION: every vector is `downto`, every subprogram formal is constrained.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.spi_sensor_pkg.all;
entity spi_sensor_evt_tb is
end entity spi_sensor_evt_tb;
architecture tb of spi_sensor_evt_tb is
constant CFG_N_C : positive := 3;
constant CNT_W : positive := 16;
subtype word_t is std_logic_vector(15 downto 0);
subtype frame_t is std_logic_vector(23 downto 0);
signal clk : std_logic := '0';
signal rst_n : std_logic := '1';
signal run : boolean := true;
-- half = 2, not 1. Chapter 19.1 measured why: this sensor model presents each bit one system cycle
-- after the trailing edge, so a mode-0 master's leading edge has `half - 1` cycles of setup and a half
-- period of 1 leaves none. Running at 1 shifts every captured word right by one bit -- a data fault,
-- not an event-policy fault. The boundary belongs to 19.1; this chapter stays above it.
signal s_half : unsigned(7 downto 0) := to_unsigned(2, 8);
signal s_lead : unsigned(7 downto 0) := to_unsigned(2, 8);
signal s_lag : unsigned(7 downto 0) := to_unsigned(2, 8);
signal s_gap : unsigned(7 downto 0) := to_unsigned(2, 8);
signal s_kind : drdy_kind_t := DRDY_PULSE;
signal s_policy : evt_policy_t := POLICY_DROP;
-- ONE DRIVER EACH. The sensor model drives `sv_drdy`; the "already asserted at reset" experiment
-- drives `drdy_preset`. Two processes assigning one signal is two drivers, and on a resolved type
-- that produces 'X' rather than an error.
signal sv_drdy : std_logic := '0';
signal drdy_preset : std_logic := '0';
signal drdy : std_logic;
signal sclk, cs_n, mosi, miso : std_logic;
signal cfg_done, rd_valid : std_logic;
signal rd_data : word_t;
signal rd_tag : natural;
signal n_events, n_reads, n_missed, n_queued : natural;
-- sensor state, driven by ONE process
signal conv_iv : natural := 240;
signal sensor_level : boolean := false;
signal sensor_run : boolean := false;
signal sv_value : word_t := (others => '0');
signal conv_cnt : natural := 0;
signal sv_sh : frame_t := (others => '0');
-- checking state, driven by ONE process
signal n_stale, n_checked, x_reports : natural := 0;
signal cfg_frames : natural := 0;
-- DELAYED COPIES AS SIGNALS, NOT PROCESS VARIABLES, and this is the one place where a mechanical
-- VHDL port of a SystemVerilog bench is silently wrong.
--
-- A SystemVerilog `reg` assigned non-blockingly takes its new value AFTER the edge, so a process
-- reading it at edge N sees the value from edge N-1 and an edge detector built from it lags by one
-- cycle. A VHDL process VARIABLE assigned at the end of the same process invocation is already
-- updated, so the detector lags by nothing. Both are correct code; they detect the edge one cycle
-- apart -- and that one cycle shifted this monitor's capture window by a single leading edge, so
-- every configuration word came back shifted left by one bit while the numbers elsewhere agreed.
--
-- Signal assignment reproduces the non-blocking semantics exactly, so all three languages sample the
-- same instants. The sensor model's delayed copies are signals for the same reason.
signal sen_cs_d, sen_sclk_d : std_logic := '1';
signal mon_cs_d, mon_sclk_d : std_logic := '1';
type nat_arr is array (natural range <>) of natural;
type word_arr is array (natural range <>) of word_t;
signal cfg_seen : word_arr(0 to 7) := (others => (others => '0'));
function i2s (v : integer; w : natural) return string is
constant S : string := integer'image(v);
constant P : string(1 to 40) := (others => ' ');
begin
if S'length >= w then return S; end if;
return P(1 to w - S'length) & S;
end function i2s;
function hex4 (v : word_t) return string is
constant D : string := "0123456789abcdef";
variable u : natural := to_integer(unsigned(v));
variable r : string(1 to 4);
begin
r(1) := D((u / 4096) mod 16 + 1);
r(2) := D((u / 256) mod 16 + 1);
r(3) := D((u / 16) mod 16 + 1);
r(4) := D(u mod 16 + 1);
return r;
end function hex4;
-- A distinct, predictable value per conversion, so a sample attributed to the wrong conversion is
-- detectable rather than merely suspicious. Built through a constrained variable so the
-- concatenation's index range cannot leak out.
function sv_of (k : natural) return word_t is
variable hi, lo : std_logic_vector(7 downto 0);
variable r : word_t;
begin
hi := std_logic_vector(to_unsigned((16#50# + k) mod 256, 8));
lo := std_logic_vector(to_unsigned(k mod 256, 8)) xor x"C3";
r := hi & lo;
return r;
end function sv_of;
-- The value that was current when each event was announced.
signal val_at_event : word_arr(0 to 255) := (others => (others => '0'));
begin
drdy <= sv_drdy or drdy_preset;
clk_gen : process is
begin
while run loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process clk_gen;
dut : entity work.spi_sensor_evt
generic map (CFG_N_C => CFG_N_C, CNT_W => CNT_W)
port map (
clk => clk, rst_n => rst_n,
drdy => drdy, drdy_kind => s_kind, policy => s_policy,
half => s_half, lead => s_lead, lag => s_lag, gap => s_gap,
sclk => sclk, cs_n => cs_n, mosi => mosi, miso => miso,
cfg_done => cfg_done, rd_data => rd_data, rd_tag => rd_tag, rd_valid => rd_valid,
n_events => n_events, n_reads => n_reads, n_missed => n_missed, n_queued => n_queued
);
-- ONE PROCESS DRIVES THE WHOLE SENSOR MODEL.
sensor : process (clk, rst_n) is
variable conv_tmr : natural := 0;
variable pulse_tmr : natural := 0;
begin
if rst_n = '0' then
sv_value <= (others => '0');
conv_cnt <= 0;
sv_sh <= (others => '0');
sv_drdy <= '0';
conv_tmr := 0; pulse_tmr := 0;
sen_cs_d <= '1'; sen_sclk_d <= '0';
elsif rising_edge(clk) then
-- the conversion timebase, independent of the master
if sensor_run then
if conv_tmr + 1 >= conv_iv then
conv_tmr := 0;
sv_value <= sv_of(conv_cnt);
val_at_event(conv_cnt mod 256) <= sv_of(conv_cnt);
conv_cnt <= conv_cnt + 1;
sv_drdy <= '1';
pulse_tmr := 2;
else
conv_tmr := conv_tmr + 1;
end if;
end if;
-- the data-ready pin's shape
if sensor_level then
-- A LEVEL held until a read begins: the shape that strands an edge detector when the pin
-- is already asserted before reset releases.
if sen_cs_d = '1' and cs_n = '0' then sv_drdy <= '0'; end if;
else
if pulse_tmr > 1 then pulse_tmr := pulse_tmr - 1;
elsif pulse_tmr = 1 then pulse_tmr := 0; sv_drdy <= '0';
end if;
end if;
-- the read response: a leading zero byte for the command phase, then the CURRENT contents of
-- the data register. A queued read therefore gets whatever is there when it finally runs.
if sen_cs_d = '1' and cs_n = '0' then
sv_sh <= x"00" & sv_value;
elsif sen_sclk_d = '1' and sclk = '0' then
sv_sh <= sv_sh(22 downto 0) & '0';
end if;
sen_cs_d <= cs_n;
sen_sclk_d <= sclk;
end if;
end process sensor;
miso <= sv_sh(23);
-- Per-read checking plus the pin-level configuration monitor, in ONE process.
chk : process (clk, rst_n) is
variable mon_sh : frame_t := (others => '0');
variable mon_bits : natural := 0;
variable ln : line;
begin
if rst_n = '0' then
n_stale <= 0;
n_checked <= 0;
x_reports <= 0;
cfg_frames <= 0;
mon_sh := (others => '0'); mon_bits := 0;
mon_cs_d <= '1'; mon_sclk_d <= '0';
elsif rising_edge(clk) then
if rd_valid = '1' then
n_checked <= n_checked + 1;
if rd_data /= val_at_event(rd_tag mod 256) then
n_stale <= n_stale + 1;
end if;
for i in 0 to 15 loop
if rd_data(i) /= '0' and rd_data(i) /= '1' then
x_reports <= x_reports + 1;
end if;
end loop;
end if;
-- The configuration frames are observed ON THE PINS, independently of the design's own
-- cfg_done: a flag a design raises about itself is not evidence that the frames happened.
if mon_cs_d = '1' and cs_n = '0' then
mon_sh := (others => '0'); mon_bits := 0;
elsif cs_n = '0' and mon_sclk_d = '0' and sclk = '1' then
mon_sh := mon_sh(22 downto 0) & mosi;
mon_bits := mon_bits + 1;
elsif mon_cs_d = '0' and cs_n = '1' then
if mon_bits = 16 and cfg_frames < 8 then
cfg_seen(cfg_frames) <= mon_sh(15 downto 0);
cfg_frames <= cfg_frames + 1;
end if;
end if;
mon_cs_d <= cs_n;
mon_sclk_d <= sclk;
end if;
end process chk;
stim : process is
variable e, mutations : natural := 0;
variable r_ev, r_rd, r_ms, r_qu, r_st : natural := 0;
constant IVS_C : nat_arr(0 to 2) := (240, 80, 50);
variable res_ev, res_rd, res_ms, res_qu, res_st : nat_arr(0 to 5);
variable guard, idx, rdc : natural := 0;
variable ln : line;
function read_cycles (h : natural) return natural is
begin
return 2 + 3 * 16 * h + 2 + 2;
end function read_cycles;
procedure reset_all is
begin
sensor_run <= false;
wait until falling_edge(clk);
rst_n <= '0';
for i in 1 to 6 loop wait until falling_edge(clk); end loop;
rst_n <= '1';
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
end procedure reset_all;
procedure wait_cfg is
begin
guard := 0;
while cfg_done = '0' and guard < 40000 loop
wait until rising_edge(clk); guard := guard + 1;
end loop;
end procedure wait_cfg;
-- Run until the sensor has produced `n` conversions, bounded.
procedure run_convs (n : natural) is
begin
guard := 0;
while conv_cnt < n and guard < 400000 loop
wait until rising_edge(clk); guard := guard + 1;
end loop;
if guard >= 400000 then
write(ln, string'(" FAIL: timeout waiting for ") & i2s(n, 1)
& string'(" conversions"));
writeline(output, ln); e := e + 1;
end if;
sensor_run <= false;
for i in 1 to read_cycles(to_integer(s_half)) + 20 loop
wait until rising_edge(clk);
end loop;
r_ev := n_events; r_rd := n_reads; r_ms := n_missed;
r_qu := n_queued; r_st := n_stale;
end procedure run_convs;
begin
rdc := read_cycles(2);
-- ============ 1. the configuration script, observed on the pins ============
reset_all;
wait_cfg;
if cfg_done = '0' then
write(ln, string'(" FAIL: the configuration script never completed"));
writeline(output, ln); e := e + 1;
end if;
if cfg_frames /= CFG_N_C then
write(ln, string'(" FAIL: ") & i2s(cfg_frames, 1)
& string'(" configuration frames appeared on the pins where ")
& i2s(CFG_N_C, 1) & string'(" were expected"));
writeline(output, ln); e := e + 1;
end if;
if n_reads /= 0 then
write(ln, string'(" FAIL: reads were started before configuration finished"));
writeline(output, ln); e := e + 1;
end if;
write(ln, string'(" configuration: ") & i2s(cfg_frames, 1) & string'(" frames on the pins -> ")
& hex4(cfg_seen(0)) & string'(" ") & hex4(cfg_seen(1)) & string'(" ")
& hex4(cfg_seen(2)) & string'(", cfg_done=1, reads before cfg=")
& i2s(n_reads, 1));
writeline(output, ln);
if not (cfg_seen(0) = x"2001" and cfg_seen(1) = x"2140" and cfg_seen(2) = x"2208") then
write(ln, string'(" FAIL: the configuration bytes on the pins are not the script's"));
writeline(output, ln); e := e + 1;
end if;
-- ============ 2 and 3. the rate sweep, both policies ============
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" read duration = ") & i2s(rdc, 1)
& string'(" cycles (lead 2 + 3 bytes x 16 edges x half 2 + lag 2 + gap 2)"));
writeline(output, ln);
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" policy conv_iv events reads missed queued stale what it costs"));
writeline(output, ln);
for p in 0 to 1 loop
for k in 0 to 2 loop
idx := p * 3 + k;
if p = 0 then s_policy <= POLICY_DROP; else s_policy <= POLICY_QUEUE; end if;
conv_iv <= IVS_C(k);
s_kind <= DRDY_PULSE;
sensor_level <= false;
reset_all;
wait_cfg;
sensor_run <= true;
run_convs(12);
res_ev(idx) := r_ev; res_rd(idx) := r_rd; res_ms(idx) := r_ms;
res_qu(idx) := r_qu; res_st(idx) := r_st;
-- Every string column is pre-padded to a fixed width and the free text sits at the END
-- of the row: `%-Ns`-style left justification is not portable across these simulators.
write(ln, string'(" "));
if p = 0 then write(ln, string'("DROP ")); else write(ln, string'("QUEUE")); end if;
write(ln, string'(" ") & i2s(IVS_C(k), 7) & string'(" ") & i2s(r_ev, 6)
& string'(" ") & i2s(r_rd, 5) & string'(" ") & i2s(r_ms, 6)
& string'(" ") & i2s(r_qu, 6) & string'(" ") & i2s(r_st, 5)
& string'(" "));
if IVS_C(k) > rdc then
write(ln, string'("nothing -- no contention"));
elsif p = 0 then
write(ln, string'("samples LOST, timestamps intact"));
else
write(ln, string'("samples KEPT, timestamps wrong"));
end if;
writeline(output, ln);
end loop;
end loop;
if not (res_ms(0) = 0 and res_st(0) = 0 and res_ms(3) = 0 and res_st(3) = 0) then
write(ln, string'(" FAIL: a conversion interval longer than a read still lost or mis-timestamped samples"));
writeline(output, ln); e := e + 1;
end if;
if not (res_rd(0) = res_ev(0) and res_rd(3) = res_ev(3)) then
write(ln, string'(" FAIL: without contention not every event was serviced"));
writeline(output, ln); e := e + 1;
end if;
if res_ms(2) = 0 then
write(ln, string'(" FAIL: the DROP policy under heavy contention lost nothing"));
writeline(output, ln); e := e + 1;
end if;
if res_st(2) /= 0 then
write(ln, string'(" FAIL: the DROP policy mis-timestamped a sample; dropping cannot do that"));
writeline(output, ln); e := e + 1;
end if;
if res_st(5) = 0 then
write(ln, string'(" FAIL: the QUEUE policy under heavy contention mis-timestamped nothing, so the trade this chapter is about was not exercised"));
writeline(output, ln); e := e + 1;
end if;
if not (res_ms(5) < res_ms(2)) then
write(ln, string'(" FAIL: the QUEUE policy did not lose fewer samples than DROP"));
writeline(output, ln); e := e + 1;
end if;
-- ============ 4. a level-held pin already asserted at reset ============
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" mode drdy at reset events reads missed outcome"));
writeline(output, ln);
for p in 0 to 1 loop
s_policy <= POLICY_DROP;
if p = 0 then s_kind <= DRDY_PULSE; else s_kind <= DRDY_LEVEL; end if;
sensor_level <= true;
conv_iv <= 240;
sensor_run <= false;
wait until falling_edge(clk);
rst_n <= '0';
-- ONE WRITER PER OBJECT: `val_at_event` is driven by the sensor process alone. Assigning it
-- from here as well gave the signal two drivers, and a resolved array came back as neither
-- value -- so the very first read compared correct data against nothing and was reported
-- stale. This experiment checks no data, so the second writer was never needed.
drdy_preset <= '1'; -- the sensor converted while the FPGA was booting
for i in 1 to 6 loop wait until falling_edge(clk); end loop;
rst_n <= '1';
wait_cfg;
for i in 1 to rdc * 3 loop wait until rising_edge(clk); end loop;
write(ln, string'(" "));
if p = 0 then write(ln, string'("EDGE ")); else write(ln, string'("LEVEL")); end if;
write(ln, string'(" 1 ") & i2s(n_events, 6) & string'(" ")
& i2s(n_reads, 5) & string'(" ") & i2s(n_missed, 6) & string'(" "));
if n_reads = 0 then
write(ln, string'("STRANDED -- the one edge it saw was spent during configuration"));
else
write(ln, string'("serviced normally"));
end if;
writeline(output, ln);
if p = 0 and n_reads /= 0 then
write(ln, string'(" FAIL: the edge-mode design serviced a pin that never produced an edge"));
writeline(output, ln); e := e + 1;
end if;
if p = 1 and n_reads = 0 then
write(ln, string'(" FAIL: the level-mode design did not service an already-asserted pin"));
writeline(output, ln); e := e + 1;
end if;
-- A level assertion must be ONE event, not one per cycle. Without the design's one-shot this
-- read 515 -- a meaningless number that the read behaviour hid completely.
if p = 1 and n_events /= 1 then
write(ln, string'(" FAIL: a single held assertion produced ") & i2s(n_events, 1)
& string'(" events where 1 was expected"));
writeline(output, ln); e := e + 1;
end if;
end loop;
drdy_preset <= '0';
sensor_level <= false;
-- ============ conclusions ============
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" 1. the configuration script ran to completion before any event was serviced, and the three write frames were verified ON THE PINS rather than from the design's own cfg_done flag -- ")
& hex4(cfg_seen(0)) & string'(", ") & hex4(cfg_seen(1)) & string'(", ")
& hex4(cfg_seen(2))
& string'(". A flag a design raises about itself is not evidence that the frames happened, and a script that silently does nothing leaves a sensor that never converts, whose symptom is an absent data-ready pin and whose investigation is a wiring check"));
writeline(output, ln);
write(ln, string'(" 2. with a conversion interval of ") & i2s(IVS_C(0), 1)
& string'(" cycles against a read duration of ") & i2s(rdc, 1)
& string'(", the two policies are INDISTINGUISHABLE -- every event serviced, nothing missed, nothing mis-timestamped, under both. A policy comparison that only runs under contention never establishes that the policies agree where they should, and that agreement is what makes the contention result attributable to the policy rather than to the design"));
writeline(output, ln);
write(ln, string'(" 3. under contention each policy fails DIFFERENTLY. At a ") & i2s(IVS_C(2), 1)
& string'("-cycle interval DROP lost ") & i2s(res_ms(2), 1) & string'(" of ")
& i2s(res_ev(2), 1) & string'(" events and mis-timestamped ") & i2s(res_st(2), 1)
& string'("; QUEUE lost ") & i2s(res_ms(5), 1) & string'(" and mis-timestamped ")
& i2s(res_st(5), 1)
& string'(". Dropping keeps the remaining stream correctly attributed and throws samples away; queueing keeps the samples and attributes some of them to the wrong instant, because a queued read fetches whatever is in the sensor's register when it finally runs. Which is worse is an APPLICATION question -- a control loop usually prefers the dropped sample, a logger usually prefers the kept one -- and the hardware's obligation is to implement one deliberately and make the other's cost visible"));
writeline(output, ln);
write(ln, string'(" 4. and a LEVEL-held data-ready pin that was already asserted when reset released left the EDGE-mode design permanently stranded: 1 event, 1 missed, 0 reads, and then nothing for the rest of time. The single event is itself the interesting part -- the synchroniser chain comes out of reset holding zero, so an already-high pin manufactures exactly ONE spurious rising edge, which landed inside the configuration window where events are deliberately ignored. The design therefore spent the only notification it would ever receive before it was ready to act on it. The LEVEL-mode design serviced the same pin normally, because a level does not expire. No counter distinguishes the two outcomes and no assertion fires: the failure has no symptom other than silence, and the sensor having converted while the FPGA was still booting is the NORMAL case rather than a corner one"));
writeline(output, ln);
-- ============ BENCH INTEGRITY ============
if cfg_seen(0) /= x"DEAD" then mutations := mutations + 1; end if;
if res_st(5) /= 0 then mutations := mutations + 1; end if;
if mutations /= 2 then
write(ln, string'(" FAIL: a deliberately wrong expectation did not mismatch (")
& i2s(mutations, 1) & string'(" of 2)"));
writeline(output, ln); e := e + 1;
end if;
if n_checked = 0 then
write(ln, string'(" FAIL: the mis-timestamp checker never executed"));
writeline(output, ln); e := e + 1;
end if;
if x_reports /= 0 then
write(ln, string'(" FAIL: reported fields contained a metavalue"));
writeline(output, ln); e := e + 1;
end if;
if e = 0 then
write(ln, string'(""));
writeline(output, ln);
write(ln, string'(" and the bench proved itself: two deliberately wrong expectations mismatched, the mis-timestamp checker ran on every completed read, every reported field carried a known value, and the sensor model converts on its own timebase without ever waiting for the master -- so the contention it creates is real"));
writeline(output, ln);
write(ln, string'("PASS: when the DEVICE owns the schedule, the design question is what to do about an event that arrives while a read is in progress, and there are exactly two answers with opposite costs. With a conversion interval longer than a read the two policies are indistinguishable -- which is what makes the contention result attributable. Under contention DROP lost ")
& i2s(res_ms(2), 1) & string'(" of ") & i2s(res_ev(2), 1)
& string'(" events and mis-timestamped 0, while QUEUE lost ") & i2s(res_ms(5), 1)
& string'(" and mis-timestamped ") & i2s(res_st(5), 1)
& string'(": dropping throws samples away and keeps the rest correctly attributed, queueing keeps the samples and attributes some to the wrong instant, because a queued read fetches whatever is in the sensor's register when it finally runs. Which is worse is an application question, and it turns on one line of the datasheet -- a DOUBLE-BUFFERED data register makes queueing strictly better and a single register makes it a trade. And the policy question belongs to the PULSE interface alone: a level-held pin does not expire, so there is nothing to drop and nothing to queue, and recognising it only when the engine is free IS the whole policy. Applying a drop policy to a level destroys the notification -- which is how the edge-mode design ended up permanently stranded by a pin that was already asserted when reset released, having spent on the configuration phase the single spurious edge its synchroniser manufactured coming out of reset. 1 event, 1 missed, 0 reads, no counter distinguishing it from a dead sensor"));
writeline(output, ln);
else
write(ln, string'("FAIL: ") & i2s(e, 1) & string'(" error(s)"));
writeline(output, ln);
end if;
run <= false;
wait;
end process stim;
end architecture tb;8. What A UVM Environment Adds Here
The valuable UVM content for this chapter is not a class hierarchy — it is the recognition that the event is stimulus and therefore belongs to a sequence, while the policy's consequence is a property and belongs to a checker.
a virtual sequence drives the SENSOR side: conversions at a chosen rate,
with the data-ready pin shaped as a pulse or a level
a monitor on the pins reconstructs frames -- config writes and burst reads
a predictor holds the sensor's register model and knows which conversion
each read SHOULD have returned
a scoreboard compares the design's tagged result against that prediction9. What Assertions Are Worth Writing
Three properties here are genuinely temporal and cheap to state:
no read may start before cfg_done
a queued event must be serviced before a third event is accepted
cs_n must be deasserted for at least `gap` cycles between frames10. FPGA Implementation
The data-ready pin is an asynchronous input and should be treated like one: a normal input buffer, two flops in the destination domain, and no attempt to route it as a clock even though it looks like a periodic pulse. A tool that infers a clock from it will constrain a path nobody wants constrained.
The configuration ROM is three words. Put it in logic, not in a block RAM — a block RAM for three entries costs a whole primitive and adds a read-latency state to the sequencer for nothing.
And the one thing worth a constraint file entry: the drdy input has no setup or hold requirement relative to the system clock, because it is synchronised. Mark it as a false path or give it an asynchronous group, or static timing analysis will report a violation on a path that is deliberately unconstrained — and an engineer will spend a day trying to fix it.
11. Why an ASIC Engineer Cares
The policy decision is a specification item, not an implementation detail. It changes which samples reach software and how they are timestamped, so it belongs in the register map as a configurable bit with a documented default — and the default should be the one that is safe for the application the part is sold into.
The double-buffering question runs the other way for a part you are building: if the sensor side is yours, double-buffering the data register costs one register file's worth of flops and removes the entire trade. That is usually the right call, and it is much cheaper to decide before the register map is frozen than to explain afterwards.
And the reset-sequencing case in section 4 is a silicon-level integration concern. A part whose data-ready pin is a level must document that it may already be asserted when the host releases reset, and a host that only edge-detects will hang. Neither side is wrong in isolation; the combination is.
12. Failure Signature — "The Sensor Never Responds"
Symptom after power-up the FPGA reads nothing; the data-ready pin is
high and stays high
Checked continuity, pull-ups, the sensor's supply, the I2C-versus-SPI
strap, the configuration bytes on a scope -- all correct
Concluded a faulty sensor; the part was replaced
Result unchanged
Actual the sensor had converted while the FPGA was loading its
bitstream, so the level-held data-ready pin was already
asserted when reset released. The synchroniser manufactured
one rising edge as it filled, that edge landed inside the
configuration window where events are ignored, and there was
never another
Found by an engineer who noticed the pin was HIGH -- which for a
level-held interface means "data waiting", not "no event"Every check was correct and the conclusion followed from the evidence. The detail that inverts it is that a high data-ready pin is not the absence of an event — it is the event, still waiting. An edge-detecting design reads a held assertion as nothing at all, and the one observation that separates a dead sensor from a stranded host is whether the pin is high.
13. Common Misconceptions
| Misconception | What is actually true |
|---|---|
| Queueing a missed event is strictly better than dropping it | It converts a lost sample into a mis-timestamped one |
| The right policy is a hardware decision | It is an application decision, and it turns on a datasheet line |
| A data-ready pin is a data-ready pin | A pulse and a level are different interfaces with different failure modes |
| A held assertion means no event | It means an event still waiting |
| An edge detector always sees an assertion | Not one that predates the synchroniser coming out of reset |
cfg_done proves the script ran | It proves the design thinks so; the pins prove it happened |
| A tri-HDL port that all passes is three implementations of one design | Only if the languages' timing semantics were reconciled |
14. Reason It Through
15. Understanding Check
16. Summary
When the device owns the schedule, the design question is what to do about an event that arrives while a read is in progress, and there are exactly two answers with opposite costs. With a conversion interval longer than a read the two policies are indistinguishable — 12 events, 12 reads, nothing lost and nothing mis-attributed under both — and that agreement is what makes the contention result attributable to the policy rather than to the design.
Under contention DROP lost 8 of 12 events and mis-timestamped none, while QUEUE lost 5 and mis-timestamped 5. Dropping throws samples away and keeps the rest correctly attributed; queueing keeps the samples and attributes some of them to the wrong instant, because a queued read fetches whatever is in the sensor's register when it finally runs. Which is worse is an application question, and it turns on one datasheet line: a double-buffered data register makes queueing strictly better, and a single register makes it a trade.
The policy question belongs to the pulse interface alone. A level does not expire, so there is nothing to drop and nothing to queue, and recognising it only when the engine is free is the whole policy — while applying a drop policy to a level destroys the notification. That is how an edge-detecting design ended up permanently stranded by a pin already asserted when reset released, having spent on its configuration phase the single spurious edge its synchroniser manufactured coming out of reset: 1 event, 1 missed, 0 reads, and no counter distinguishing it from a dead sensor.
And the three-language port earned its place by finding four defects, all of them the same root cause — a VHDL process variable is not a non-blocking reg — and two of them producing correct-looking results. A tri-HDL port is evidence only if the three implementations are the same design.
17. What Comes Next
Both chapters so far have had hardware decide everything. Chapter 19.3 puts software in the loop: the master sits behind a register bus, a processor writes configuration and reads status, and the two sides disagree about who owns a field and when. The register that software writes while a transfer is running is the race that ships, and the fix is a contract rather than a circuit.
Continue learning
Related tutorials
- Related topic
FPGA to ADC — Streaming Under a Sample Deadline
Throughput and a deadline are different requirements. The measured busy interval matches t_conv + lead + 2·NB·half + lag + gap exactly — and the fixed terms cap what any clock rate can buy.
- Related topic
MOSI Data Flow and CS Framing
What the master drives on MOSI through every region of a transfer, what the two chip-select edges bracket, and why an asynchronous CS must cross into the slave's clock domain before any edge is derived from it.
- Related topic
SoC to SPI Peripheral Through a Register Bus
Software becomes part of the design. A configuration shadow, two sticky status bits and a toggle handshake — the three races that ship, measured at four unrelated clock ratios.
- Related topic
Why SPI Exists
The engineering problem that produces an interface like SPI: moving control and data between chips without spending a wide parallel port on every peripheral. Where the pin budget goes, why sending a clock alongside the data changes what the receiver must do, and what SPI gives up to stay that simple.
