Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   at startup    walk a configuration script: write N registers, in order
   then forever  on each data-ready event, read a burst of registers

Both 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.

A left-to-right path: a sensor drives a data-ready pin into a two-flop synchroniser, which feeds an event-and-policy block, which starts the command sequencer, which runs a three-byte frame and produces a result with a tag. Below, a missed counter hangs off the event block for the drop policy and a stale counter hangs off the result for the queue policy.sensorconverts on its owntimebase2-flopsynchroniserthe only clock-domaincrossingevent + policyDROP or QUEUE, decidedherecommand sequencerconfig script, thenburst readresult + tagwhich event this readservedn_missedthe sample was neverreadn_staleread, but attributedwronglydrdyone per assertstart read3-byte frameDROPQUEUE12
Figure 1 — how a sensor event becomes a burst read, and where each failure is counted. The policy decision lives in exactly one place; the two failure counters hang off different points, because a dropped sample and a mis-attributed sample are different defects with different fixes.

2. The Policy Question

An event arrives while a read is running. Two answers:

PolicyWhat happensWhat it costs
DROPdiscard the new eventthe sample it announced is never read
QUEUEremember it, service it when freethe 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 soft

3. The Only Clock-Domain Crossing Here

Exactly one signal crosses: drdy, a single-bit asynchronous input. It gets two flops and an edge detector.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   single-bit level from an unrelated clock  →  2-flop synchroniser  →  edge detect

That 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 cycles
Five rows over fourteen cycles. Reset is low until cycle four. The data-ready pin is high throughout. The synchroniser output rises at cycle six, producing a single edge pulse there. A state row shows reset, then the configuration script, then streaming from cycle twelve. The only edge falls inside the configuration window.reset assertedreset assertedconfiguration scriptconfiguration scriptstreamingstrea…the only rising edge, inside CFGthe only rising edge,inside CFGstreaming begins, nothing pendingstreaming begins, nothingpendingrst_ndrdysync outedgestaterstrstrstrstCFGCFGCFGCFGCFGCFGCFGCFGWAITWAITt0t1t2t3t4t5t6t7t8t9t10t11t12t13
Figure 2 — reset releasing with the data-ready pin already asserted. The synchroniser chain fills from zero, manufacturing one rising edge at cycle 6 — inside the configuration window, where events are ignored. There is never another, because a level does not assert twice.

5. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 normally

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
spi_sensor_evt.sv — the event-driven reader — a configuration script, a synchronised event, and one policy decision in one place
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_sensor_evt.v — the same design in Verilog-2001
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_sensor_evt.vhd — the same design in VHDL
-- 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.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_sensor_evt_tb.sv — a free-running sensor, two policies, and a rate sweep that shows each policy's own failure
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_sensor_evt_tb.v — the same bench in Verilog-2001
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_sensor_evt_tb.vhd — the same bench in VHDL
-- 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 prediction

9. What Assertions Are Worth Writing

Three properties here are genuinely temporal and cheap to state:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 frames

10. 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"

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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

MisconceptionWhat is actually true
Queueing a missed event is strictly better than dropping itIt converts a lost sample into a mis-timestamped one
The right policy is a hardware decisionIt is an application decision, and it turns on a datasheet line
A data-ready pin is a data-ready pinA pulse and a level are different interfaces with different failure modes
A held assertion means no eventIt means an event still waiting
An edge detector always sees an assertionNot one that predates the synchroniser coming out of reset
cfg_done proves the script ranIt proves the design thinks so; the pins prove it happened
A tri-HDL port that all passes is three implementations of one designOnly 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