SPI · Module 13
Busy/Done/Valid Interface, Reset, and Abort
Stopping a transfer without leaving a slave stranded: why abort is orderly and reset is instant, why the abort input belongs in the block that owns the pins, and why a partial word must be reported rather than silently absent.
Every block so far assumes transfers finish. Real systems abandon them: a timeout fires, a driver is killed, an error handler decides the slave is not responding, a watchdog resets the subsystem.
A transfer is abandoned mid-byte. Should chip select go high immediately?
On abort, no. On reset, yes. They are opposites, and the reason is worth getting exactly right — because what the master does in those moments decides whether the slave is still usable afterwards.
1. The Two Ways To Stop Are Not The Same
Abort is a request. The master is still running and still owns the pins, so it must stop the way the datasheet allows: halt the clock, wait out the chip-select hold time, release chip select, wait out the chip-select-high time. All of that takes cycles and all of it is mandatory.
An abort that yanks chip select high on the spot violates t_CSH on the way out — and a slave that samples a final edge inside the violation may latch a bit that was never meant for it. That is worse than an abandoned transfer: it is an abandoned transfer that wrote something.
Reset is not a request, it is an absence. The master is gone, and there is nothing left to sequence a graceful exit with. So reset must release chip select immediately and asynchronously, because the alternative is chip select held low by a master that no longer exists — and a slave watching a dead chip-select line stays mid-transaction indefinitely, which usually requires a power cycle to clear.
abort orderly, costs cycles the master still exists
reset instant, costs protocol the master does notThe rule is easier to remember than to derive: an abort is orderly and costs time; a reset is instant and costs correctness at the protocol level — which is acceptable precisely because reset already means "start over".
2. Where The Abort Input Belongs, And How That Was Learned
The first attempt at this chapter tried to abort without touching Chapter 13.7. The reasoning was good: that machine already knows how to leave a transaction properly, through LAG and GAP. It leaves XFER when it is told the frame is done, and it goes to LAG rather than HOLD when it is told this was the last frame. So an abort should be exactly:
tell it the frame is done, and tell it this was the last oneand the existing lag and gap would be honoured for free, with nothing in 13.7 to re-verify.
It does not work, and the reason is worth keeping: 13.7 latches hold when the frame is requested. Mid-frame the live input is not consulted, so the machine ended up in HOLD — clock stopped, chip select still low, waiting for a request that the aborting logic was busy refusing. The bus hung with the slave still selected: the exact failure the chapter is about, produced by the code meant to prevent it.
3. What The Abort Must Report
An aborted frame captured some bits but not all of them. Chapter 13.6's receive register only raises valid at the last bit, so a truncated frame raises nothing — the word is silently absent.
That is the right behaviour and the wrong amount of information. A driver sees no received word and cannot distinguish "the abort landed before any bits arrived" from "seven of eight bits arrived and were discarded". So:
abort_bits — how many bits made it out before the abort. Captured at the moment of the kill, because one cycle later the engine has been told the frame ended and the count no longer means anything.
rx_trunc — sticky, set only if bits were actually mid-flight. An abort during LEAD, HOLD, LAG or GAP loses nothing, and flagging it would train the driver to ignore the flag.
flush — the streaming shadow of Chapter 13.9 is emptied. A word queued for the abandoned transaction must not be the first thing sent in the next one, and an armed request left standing would start that next transaction by itself the moment the bus came free.
abort_ack — one cycle, when the bus is genuinely free. Including the case where there was nothing to abort: a driver waiting for the acknowledgement would otherwise hang on exactly the one case where the abort was unnecessary.
4. The Policy Machine
5. The Handshake Around It
6. One Command, One Abort
abort_req is a level, and it is generally still asserted when the abort finishes — the register interface of Chapter 13.11 clears it on the acknowledgement, which is necessarily a cycle later.
A design that re-reads the level on returning to RUN therefore services the same request a second time, and the second pass finds the bus idle. It re-latches aborted, and it overwrites abort_bits with zero while leaving rx_trunc set. The report then says a word was truncated after zero bits, which is not a thing that can happen — and a driver author spends an afternoon on it.
So the request is edge-qualified: one rising edge of abort_req, one abort. That makes the block correct regardless of how the level behaves, which matters because the level's behaviour is decided in another block.
level-triggered: abort ... complete ... level still high ... abort again
edge-qualified: abort ... complete ... level still high ... nothingThis is the second bug in this chapter that came from a level outliving the event it described, and the first was Chapter 13.9's frame_done. It is a recurring shape and worth watching for.
7. The Abort On The Wire
Read the cs_n row at cycles 10 to 13. The clock has stopped and the select is still low: that is the hold being paid, on an abandoned transfer, exactly as it would be on a completed one. And read ack against cs_n — the acknowledgement is at cycle 18, after the select has been high for the full gap, not at cycle 9 when the abort was accepted.
8. Reset Is The Only Thing That Is Allowed To Be Instant
Everything in this module resets asynchronously, and for most blocks that is a preference. For the select pins it is a requirement, and the check is unusual:
assert reset BETWEEN clock edges, and check chip select in the same instantA synchronous reset needs an edge. If reset asserts because the clock has stopped — a PLL unlocking, a power-domain shutdown, a watchdog that also gates the clock — then a synchronously-reset select pin never releases, and the slave sees a master holding it selected forever.
The testbench in §9 asserts reset at a deliberately awkward moment, two nanoseconds into a cycle, and checks cs_n one nanosecond later. No clock edge has occurred. A design that passes that check will release the bus under any reset; one that fails it will release the bus only while its clock is running, which is the one condition under which reset was least likely to be needed.
9. Building the Abort Policy — Three HDLs
The circuit
Three states, one edge detector, four status outputs. Two details beyond those above:
abort_out is one cycle. 13.7 needs no more, because it latches the decision by changing state — and a level would keep re-triggering as the machine passed back through LEAD on the next transaction, aborting it too.
req_out is gated for the whole of the abort, including the gap. Without that, the streaming controller's still-asserted request would start a fresh transaction the instant chip select came back up, and the abort would look like a one-frame hiccup rather than a stop.
// spi_abort_ctl.sv
//
// Chapter 13.10 -- stopping in the middle, and coming back from reset.
//
// Every block so far assumes transfers finish. Real systems abandon them: a
// timeout fires, a driver is killed, an error handler decides the slave is not
// responding, a watchdog resets the subsystem. What the master does in those
// moments is not a detail -- it decides whether the SLAVE is still usable
// afterwards, and a slave left mid-transaction is a slave that must be power
// cycled.
//
// THE TWO WAYS TO STOP ARE NOT THE SAME.
//
// ABORT is a request. The master is still running and still owns the pins, so
// it must stop the way the datasheet allows: halt the clock, wait out the CS
// hold time, release CS, wait out the CS-high time. All of that takes cycles
// and all of it is mandatory. An abort that yanks CS high on the spot violates
// t_CSH on the way out -- and a slave that samples a final edge inside the
// violation may latch a bit that was never meant for it.
//
// RESET is not a request, it is an absence. The master is GONE, and there is
// nothing left to sequence a graceful exit with. So reset must release CS
// IMMEDIATELY and asynchronously, because the alternative is CS held low by a
// master that no longer exists -- and a slave watching a dead CS line stays
// mid-transaction indefinitely.
//
// The rule is easier to remember than to derive: an abort is orderly and costs
// time; a reset is instant and costs correctness at the protocol level, which
// is acceptable precisely because reset already means "start over".
//
// HOW THE ABORT IS IMPLEMENTED -- AND WHERE.
//
// The first attempt at this block tried to abort WITHOUT touching Chapter
// 13.7, by synthesising a "frame done" pulse and simultaneously pulling the
// burst's `hold` low, so the chip-select machine would fall out of XFER into
// LAG and pay the hold and gap on its own. It does not work, and the reason is
// worth keeping: 13.7 LATCHES `hold` when the frame is requested. Mid-frame the
// live input is not consulted, so the machine ended up in HOLD -- clock
// stopped, chip select still LOW, waiting for a request that this block was
// busy refusing. The bus hung with the slave still selected: the exact failure
// the chapter is about, produced by the code meant to prevent it.
//
// So the abort input belongs in 13.7, where the pins are owned, and this block
// is pure POLICY:
//
// decide that an abort is happening, hold it until the bus is genuinely
// free, publish what was lost, throw away queued work, and refuse new work
// until it is over
//
// There is exactly one piece of logic that knows how to leave the bus, and it
// is not this one.
//
// WHAT THE ABORT MUST REPORT.
//
// An aborted frame captured some bits but not all of them. Chapter 13.6's
// receive register only raises valid at the LAST bit, so a truncated frame
// raises nothing -- the word is silently absent, which is the right behaviour
// and the wrong amount of information. So the partial bit count is published,
// and a truncation flag is latched. And the streaming shadow of Chapter 13.9
// is FLUSHED: a queued word that was meant for the aborted transaction must
// not be the first thing sent in the next one.
module spi_abort_ctl #(
parameter int LEN_W = 6
) (
input wire clk,
input wire rst_n,
input wire abort_req, // level, from the CPU
// As in Chapter 13.9: the flags are detected here and cleared by the
// register interface of Chapter 13.11, so each has one owner and one clear.
input wire clr_flags,
// --- from the transfer engine ---------------------------------------
input wire busy, // 13.7: a transaction is open
input wire shift_en, // 13.7: the clock is running
input wire frame_done, // 13.5: all bits captured
input wire [LEN_W-1:0] bit_idx, // 13.5: bits captured so far
input wire [LEN_W-1:0] len,
// --- from the streaming controller (13.9) ---------------------------
input wire req_in,
// --- to the chip-select controller (13.7) ---------------------------
output wire req_out,
output wire abort_out, // one cycle: leave the bus
// --- status ---------------------------------------------------------
output wire aborting, // an abort is being carried out
output reg aborted, // sticky: an abort happened
output reg [LEN_W-1:0] abort_bits, // bits that made it out
output reg rx_trunc, // sticky: a partial word was lost
output wire flush, // drop the queued word
output reg abort_ack // one cycle, when the bus is idle
);
localparam [1:0] A_RUN = 2'd0, // nothing to do
A_KILL = 2'd1, // one cycle: end the frame, end the burst
A_WAIT = 2'd2; // let 13.7 walk out through LAG and GAP
reg [1:0] astate;
// ONE COMMAND, ONE ABORT. `abort_req` is a level, and it is generally still
// asserted when the abort finishes -- the register interface of Chapter
// 13.11 clears it on the acknowledgement, which is necessarily a cycle
// later. A design that re-reads the level on returning to A_RUN therefore
// services the SAME request a second time, and the second pass finds the
// bus idle: it re-latches `aborted`, and overwrites `abort_bits` with zero
// while leaving `rx_trunc` set. The report then says a word was truncated
// after zero bits, which is not a thing that can happen, and the driver
// author spends an afternoon on it.
reg req_q;
wire abort_edge = abort_req & ~req_q;
assign aborting = (astate != A_RUN);
// A single cycle. 13.7 needs no more than that -- it latches the decision
// by changing state -- and a level would keep re-triggering as the machine
// passed back through LEAD on the next transaction.
assign abort_out = (astate == A_KILL);
// New frames are refused for the whole of the abort, including the gap.
// Without this the streaming controller's still-asserted request would
// start a fresh transaction the instant CS came back up, and the abort
// would look like a one-frame hiccup rather than a stop.
assign req_out = req_in & ~aborting & ~abort_req;
// The queued word belonged to the transaction being abandoned.
assign flush = aborting;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
astate <= A_RUN;
req_q <= 1'b0;
aborted <= 1'b0;
abort_bits <= {LEN_W{1'b0}};
rx_trunc <= 1'b0;
abort_ack <= 1'b0;
end else begin
abort_ack <= 1'b0;
req_q <= abort_req;
// Applied first, so a clear on the same cycle as a fresh abort
// does not swallow the new report.
if (clr_flags) begin
aborted <= 1'b0;
rx_trunc <= 1'b0;
end
case (astate)
A_RUN: begin
if (abort_edge && busy) begin
aborted <= 1'b1;
// Captured at the moment of the kill, because one
// cycle later the engine has been told the frame
// ended and the count no longer means anything. Zero
// unless bits were actually moving: outside XFER,
// `bit_idx` still holds the PREVIOUS frame's total and
// publishing that would be a lie in the most
// convincing possible form.
abort_bits <= shift_en ? bit_idx : {LEN_W{1'b0}};
// Truncated only if bits were actually mid-flight. An
// abort during LEAD, HOLD, LAG or GAP loses nothing,
// and flagging it would train the driver to ignore
// the flag.
if (shift_en && !frame_done &&
bit_idx != len && bit_idx != {LEN_W{1'b0}})
rx_trunc <= 1'b1;
astate <= A_KILL;
end else if (abort_edge && !busy) begin
// Nothing in flight. The abort still has to be
// acknowledged, or a driver waiting for the ack hangs
// on the one case where there was nothing to stop.
aborted <= 1'b1;
abort_bits <= {LEN_W{1'b0}};
abort_ack <= 1'b1;
end
end
A_KILL: astate <= A_WAIT;
A_WAIT: begin
// 13.7 is paying the CS hold and the CS-high time. Only
// when it reports itself idle is the bus legally free.
if (!busy) begin
astate <= A_RUN;
abort_ack <= 1'b1;
end
end
default: astate <= A_RUN;
endcase
end
end
endmodule// spi_abort_ctl_tb.sv
//
// The whole engine runs here, and the abort is injected at EVERY bit position
// of a frame -- before the first edge, on each interior bit, and on the last --
// because an abort is exactly the kind of feature that works at the convenient
// moments and breaks at one specific inconvenient one.
//
// After every single abort the same four things are checked on the PINS, not in
// the state machines:
//
// 1. no SCLK edge occurs once chip select has gone high;
// 2. chip select stayed low for at least the programmed hold after the final
// edge -- an abort is not permission to violate t_CSH;
// 3. chip select stayed high for at least the programmed gap before anything
// else started;
// 4. the engine comes back: a normal transaction immediately afterwards
// transfers every word correctly.
//
// Point 4 matters more than it looks. A master that stops cleanly but wedges is
// no better than one that stops dirtily, and an abort path is the least
// exercised logic in the design, so it is exactly where a state machine gets
// stuck in a state nothing leaves.
`timescale 1ns/1ps
module spi_abort_ctl_tb;
localparam int MAX_W = 32;
localparam int LEN_W = 6;
localparam int DIV_W = 8;
localparam int N_CS = 2;
localparam int SEL_W = 1;
localparam int CNT_W = 8;
localparam int FRAME_BITS = 8;
localparam int LEAD = 3;
localparam int LAG = 4;
localparam int GAP = 5;
logic clk = 1'b0;
logic rst_n = 1'b0;
always #5 clk = ~clk;
// --- 13.9 -------------------------------------------------------------
logic tx_push = 1'b0;
logic [MAX_W-1:0] tx_wdata = 32'h0;
logic tx_last = 1'b0;
wire tx_ready;
logic rx_pop = 1'b0;
wire [MAX_W-1:0] rx_rdata;
wire rx_ready, rx_overrun;
wire req_raw, hold_raw, load_stb, stalled;
wire [MAX_W-1:0] tx_data;
// --- 13.10, the block under test --------------------------------------
logic abort_req = 1'b0;
logic clr_flags = 1'b0;
wire req_gated, abort_out;
wire aborting, aborted, rx_trunc, flush, abort_ack;
wire [LEN_W-1:0] abort_bits;
// --- 13.7 -------------------------------------------------------------
logic [SEL_W-1:0] sel = 1'b0;
wire [N_CS-1:0] cs_n;
wire shift_en, start_stb, busy, sel_err;
wire [2:0] state_id;
// --- 13.4 / 13.5 / 13.8 -----------------------------------------------
logic [DIV_W-1:0] div = 8'd4;
logic cpol = 1'b0;
logic cpha = 1'b0;
logic [LEN_W-1:0] len = FRAME_BITS[LEN_W-1:0];
logic lsb_first = 1'b0;
wire sclk, edge_a_stb, edge_b_stb, bit_done, div_err;
wire [DIV_W-1:0] half_a, half_b;
wire preload_stb, launch_stb, capture_stb, frame_done;
wire [LEN_W-1:0] bit_idx;
wire miso, mosi;
wire [MAX_W-1:0] rx_word;
wire rx_valid_stb, len_err;
spi_clkdiv_strobe #(.DIV_W(DIV_W)) u_div (
.clk(clk), .rst_n(rst_n), .en(shift_en), .div(div), .cpol(cpol),
.sclk(sclk), .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.bit_done(bit_done), .half_a(half_a), .half_b(half_b),
.div_err(div_err)
);
spi_mode_edges #(.LEN_W(LEN_W)) u_mode (
.clk(clk), .rst_n(rst_n), .cpha(cpha), .len(len),
.active(shift_en), .start_stb(start_stb),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.preload_stb(preload_stb), .launch_stb(launch_stb),
.capture_stb(capture_stb), .bit_idx(bit_idx), .frame_done(frame_done)
);
spi_width_order #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_data (
.clk(clk), .rst_n(rst_n),
.tx_data(tx_data), .len(len), .lsb_first(lsb_first),
.load_stb(load_stb),
.preload_stb(preload_stb), .launch_stb(launch_stb),
.capture_stb(capture_stb),
.miso(miso), .mosi(mosi),
.rx_data(rx_word), .rx_valid_stb(rx_valid_stb), .len_err(len_err)
);
spi_cs_ctrl #(.N_CS(N_CS), .SEL_W(SEL_W), .CNT_W(CNT_W)) u_cs (
.clk(clk), .rst_n(rst_n),
.req(req_gated), .hold(hold_raw), .sel(sel),
.lead_cyc(LEAD[CNT_W-1:0]), .lag_cyc(LAG[CNT_W-1:0]),
.gap_cyc(GAP[CNT_W-1:0]),
.core_done(frame_done), .abort(abort_out),
.cs_n(cs_n), .shift_en(shift_en), .start_stb(start_stb),
.busy(busy), .sel_err(sel_err), .state_id(state_id)
);
spi_multibyte #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_stream (
.clk(clk), .rst_n(rst_n),
.tx_push(tx_push), .tx_wdata(tx_wdata), .tx_last(tx_last),
.tx_ready(tx_ready),
.flush(flush), .clr_flags(1'b0),
.rx_pop(rx_pop), .rx_rdata(rx_rdata), .rx_ready(rx_ready),
.rx_overrun(rx_overrun),
.core_busy(busy), .shift_en(shift_en), .start_stb(start_stb),
.rx_valid_stb(rx_valid_stb), .rx_word(rx_word),
.req(req_raw), .hold(hold_raw), .tx_data(tx_data),
.load_stb(load_stb),
.stalled(stalled)
);
spi_abort_ctl #(.LEN_W(LEN_W)) dut (
.clk(clk), .rst_n(rst_n),
.abort_req(abort_req), .clr_flags(clr_flags),
.busy(busy), .shift_en(shift_en), .frame_done(frame_done),
.bit_idx(bit_idx), .len(len),
.req_in(req_raw),
.req_out(req_gated), .abort_out(abort_out),
.aborting(aborting), .aborted(aborted), .abort_bits(abort_bits),
.rx_trunc(rx_trunc), .flush(flush), .abort_ack(abort_ack)
);
// --- the slave --------------------------------------------------------
logic [MAX_W-1:0] slv_tx_q [0:255];
logic [MAX_W-1:0] slv_rx_q [0:255];
integer slv_tx_i, slv_rx_i;
logic [MAX_W-1:0] slv_sr, slv_rx_sr;
logic slave_bit_r;
assign miso = slave_bit_r;
always_ff @(posedge clk) begin
if (load_stb) begin
slv_sr <= slv_tx_q[slv_tx_i] << (MAX_W - len);
slv_tx_i <= slv_tx_i + 1;
end else if (preload_stb || launch_stb) begin
slave_bit_r <= slv_sr[MAX_W-1];
slv_sr <= {slv_sr[MAX_W-2:0], 1'b0};
end
if (load_stb) slv_rx_sr <= {MAX_W{1'b0}};
else if (capture_stb) slv_rx_sr <= {slv_rx_sr[MAX_W-2:0], mosi};
if (rx_valid_stb) begin
slv_rx_q[slv_rx_i] <= slv_rx_sr;
slv_rx_i <= slv_rx_i + 1;
end
end
// --- the pin monitor --------------------------------------------------
wire any_cs_low = ~(&cs_n);
logic any_cs_low_q;
integer since_edge, since_rise, e_now, r_now;
integer edges_after_cs_high; // must stay zero, always
integer min_lag_seen, min_gap_seen;
integer cs_rises, cs_falls;
integer caps_seen;
integer edges_seen;
always_ff @(posedge clk) begin
if (!rst_n) begin
any_cs_low_q <= 1'b0;
since_edge <= 0;
since_rise <= 0;
end else begin
any_cs_low_q <= any_cs_low;
e_now = since_edge;
r_now = since_rise;
if (edge_a_stb || edge_b_stb) begin
since_edge <= 1;
edges_seen <= edges_seen + 1;
// The property the whole chapter turns on.
if (!any_cs_low)
edges_after_cs_high <= edges_after_cs_high + 1;
end else begin
since_edge <= e_now + 1;
end
if (!any_cs_low && any_cs_low_q) begin // CS rose
since_rise <= 1;
cs_rises <= cs_rises + 1;
if (e_now < min_lag_seen) min_lag_seen <= e_now;
end else begin
since_rise <= r_now + 1;
end
if (any_cs_low && !any_cs_low_q) begin // CS fell
cs_falls <= cs_falls + 1;
if (cs_rises > 0 && r_now < min_gap_seen) min_gap_seen <= r_now;
end
if (capture_stb) caps_seen <= caps_seen + 1;
end
end
integer errors = 0;
task automatic clear_pins;
begin
min_lag_seen = 9999; min_gap_seen = 9999;
cs_rises = 0; cs_falls = 0; caps_seen = 0; edges_seen = 0;
end
endtask
task automatic push(input [MAX_W-1:0] w, input bit last);
integer guard;
begin
guard = 8000;
while (!tx_ready && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
tx_wdata = w; tx_last = last; tx_push = 1'b1;
@(negedge clk);
tx_push = 1'b0;
end
endtask
task automatic drain_rx;
begin
while (rx_ready) begin
rx_pop = 1'b1;
@(negedge clk);
rx_pop = 1'b0;
@(negedge clk);
end
end
endtask
task automatic wait_idle;
integer guard;
begin
guard = 20000;
while (busy && guard > 0) begin
drain_rx();
@(negedge clk);
guard = guard - 1;
end
repeat (4) @(negedge clk);
drain_rx();
end
endtask
// A clean four-word transaction, fully checked. Used to prove the engine
// still works after each abort.
task automatic clean_transfer(input string tag);
integer f, base, bad;
logic [MAX_W-1:0] w [0:3];
begin
base = slv_rx_i;
w[0] = 32'h11; w[1] = 32'h22; w[2] = 32'h44; w[3] = 32'h88;
for (f = 0; f < 4; f = f + 1) begin
push(w[f], (f == 3));
drain_rx();
end
wait_idle();
bad = 0;
for (f = 0; f < 4; f = f + 1)
if (slv_rx_q[base + f] !== w[f]) bad = bad + 1;
if (slv_rx_i != base + 4) begin
$display(" FAIL: %0s -- the engine delivered %0d of 4 words after an abort",
tag, slv_rx_i - base);
errors = errors + 1;
end else if (bad != 0) begin
$display(" FAIL: %0s -- %0d of 4 words wrong after an abort",
tag, bad);
errors = errors + 1;
end
end
endtask
integer b, k, guard, seed;
integer aborts_done, trunc_seen, bits_lo, bits_hi;
initial begin
clear_pins();
edges_after_cs_high = 0;
slv_tx_i = 0; slv_rx_i = 0;
slv_sr = 32'h0; slv_rx_sr = 32'h0; slave_bit_r = 1'b0;
since_edge = 0; since_rise = 0;
edges_seen = 0; caps_seen = 0;
aborts_done = 0; trunc_seen = 0; bits_lo = 99; bits_hi = 0;
seed = 32'h00AB_0117;
for (k = 0; k < 256; k = k + 1) begin
seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
slv_tx_q[k] = (seed >> 9) & 32'hFF;
end
repeat (3) @(negedge clk);
// 1. RESET RELEASES CHIP SELECT WITHOUT WAITING FOR A CLOCK. Checked
// at a moment when no edge is anywhere near, which is the whole
// point: the release must not need one.
rst_n = 1'b1;
@(negedge clk);
push(32'hA5, 1'b0);
push(32'h5A, 1'b0);
// Wait until the clock is genuinely running and CS is low.
guard = 500;
while (!(shift_en && any_cs_low) && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
if (!any_cs_low) begin
$display(" FAIL: the engine never asserted chip select");
errors = errors + 1;
end
#2 rst_n = 1'b0; // mid-cycle, deliberately between clock edges
#1;
if (cs_n !== {N_CS{1'b1}}) begin
$display(" FAIL: an asynchronous reset did not release chip select until a clock edge");
errors = errors + 1;
end
#2 rst_n = 1'b1;
repeat (4) @(negedge clk);
$display(" reset asserted between clock edges released chip select in the same instant -- no edge required");
// Re-synchronise the slave model bookkeeping after the reset.
slv_tx_i = 0; slv_rx_i = 0;
slv_sr = 32'h0; slv_rx_sr = 32'h0; slave_bit_r = 1'b0;
repeat (2) @(negedge clk);
// 2. AN ABORT WITH NOTHING IN FLIGHT is still acknowledged.
clear_pins();
abort_req = 1'b1;
guard = 200;
while (!abort_ack && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
abort_req = 1'b0;
if (guard == 0) begin
$display(" FAIL: an abort with nothing in flight was never acknowledged");
errors = errors + 1;
end
if (rx_trunc) begin
$display(" FAIL: an abort with nothing in flight reported a truncated word");
errors = errors + 1;
end
if (cs_falls != 0) begin
$display(" FAIL: an abort with nothing in flight asserted chip select");
errors = errors + 1;
end
repeat (4) @(negedge clk);
$display(" an abort with nothing in flight: acknowledged, nothing asserted, nothing reported as lost");
clean_transfer("after an idle abort");
// 3. THE SWEEP. Abort after exactly b captured bits, for every b from
// 0 to len, and check the pins and the recovery each time.
for (b = 0; b <= FRAME_BITS; b = b + 1) begin
// Reset the sticky flags by resetting the abort block only -- a
// full reset would also lose the slave bookkeeping.
clear_pins();
// Queue two words: one to abort inside, one that must be FLUSHED
// and therefore never appear on the wire.
push(32'h3C, 1'b0);
push(32'hC3, 1'b0);
k = slv_rx_i;
// Wait for the frame to reach exactly b captured bits.
guard = 4000;
while (!(shift_en && bit_idx == b[LEN_W-1:0]) && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
if (guard == 0) begin
$display(" FAIL: the frame never reached %0d captured bits", b);
errors = errors + 1;
end
abort_req = 1'b1;
@(negedge clk);
abort_req = 1'b0;
guard = 4000;
while (!abort_ack && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
if (guard == 0) begin
$display(" FAIL: the abort at %0d bits was never acknowledged",
b);
errors = errors + 1;
end
aborts_done = aborts_done + 1;
// The pins, after the abort has fully completed.
if (busy) begin
$display(" FAIL: the engine was still busy after acknowledging the abort at %0d bits",
b);
errors = errors + 1;
end
if (cs_rises != 1) begin
$display(" FAIL: the abort at %0d bits raised chip select %0d times",
b, cs_rises);
errors = errors + 1;
end
if (min_lag_seen < LAG) begin
$display(" FAIL: the abort at %0d bits held chip select only %0d cycles past the last edge, below the %0d required",
b, min_lag_seen, LAG);
errors = errors + 1;
end
// Whether the word was truncated, and how far it got.
if (abort_bits < bits_lo) bits_lo = abort_bits;
if (abort_bits > bits_hi) bits_hi = abort_bits;
if (rx_trunc) trunc_seen = trunc_seen + 1;
if (b > 0 && b < FRAME_BITS && !rx_trunc) begin
$display(" FAIL: an abort after %0d of %0d bits was not reported as truncated",
b, FRAME_BITS);
errors = errors + 1;
end
// The flushed word must never have reached the wire.
if (slv_rx_i > k + 1) begin
$display(" FAIL: the abort at %0d bits let %0d words through -- the queued word was not flushed",
b, slv_rx_i - k);
errors = errors + 1;
end
// And the engine must still work.
drain_rx();
clean_transfer("recovery");
if (min_gap_seen < GAP) begin
$display(" FAIL: after the abort at %0d bits the next transaction began %0d cycles later, below the %0d gap",
b, min_gap_seen, GAP);
errors = errors + 1;
end
// Clear the sticky flags for the next iteration.
rst_n = 1'b0;
repeat (3) @(negedge clk);
rst_n = 1'b1;
slv_tx_i = 0; slv_rx_i = 0;
slv_sr = 32'h0; slv_rx_sr = 32'h0; slave_bit_r = 1'b0;
repeat (3) @(negedge clk);
end
if (edges_after_cs_high != 0) begin
$display(" FAIL: %0d SCLK edges occurred after chip select had gone high",
edges_after_cs_high);
errors = errors + 1;
end
if (aborts_done != FRAME_BITS + 1) begin
$display(" FAIL: %0d of %0d sweep aborts ran", aborts_done,
FRAME_BITS + 1);
errors = errors + 1;
end
if (bits_hi == bits_lo) begin
$display(" FAIL: every abort reported the same partial bit count (%0d) -- the count is not tracking anything",
bits_lo);
errors = errors + 1;
end
$display(" %0d aborts, one at each bit position from 0 to %0d: partial counts spanned %0d to %0d, %0d were reported as truncating a word, and the chip-select hold was honoured every time",
aborts_done, FRAME_BITS, bits_lo, bits_hi, trunc_seen);
$display(" no SCLK edge ever occurred after chip select went high, across every abort in the sweep");
if (errors == 0)
$display("PASS: an abort is carried out the way the datasheet allows -- the clock stops, the programmed chip-select hold is paid in full, chip select is released, and the programmed CS-high gap is paid before anything else begins -- verified at every one of %0d bit positions in a frame, with no SCLK edge ever occurring after chip select went high -- the partial bit count is published and a genuinely truncated word is flagged while an abort that interrupted nothing is not, the word queued behind the aborted one is flushed rather than becoming the first word of the next transaction, an abort with nothing in flight is still acknowledged so a waiting driver cannot hang, a reset asserted between clock edges releases chip select in the same instant without waiting for one, and after every abort a clean four-word transaction transfers correctly -- the engine stops without wedging", aborts_done);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_abort_ctl.v
//
// Chapter 13.10 -- stopping in the middle, and coming back from reset.
//
// Every block so far assumes transfers finish. Real systems abandon them: a
// timeout fires, a driver is killed, an error handler decides the slave is not
// responding, a watchdog resets the subsystem. What the master does in those
// moments is not a detail -- it decides whether the SLAVE is still usable
// afterwards, and a slave left mid-transaction is a slave that must be power
// cycled.
//
// THE TWO WAYS TO STOP ARE NOT THE SAME.
//
// ABORT is a request. The master is still running and still owns the pins, so
// it must stop the way the datasheet allows: halt the clock, wait out the CS
// hold time, release CS, wait out the CS-high time. All of that takes cycles
// and all of it is mandatory. An abort that yanks CS high on the spot violates
// t_CSH on the way out -- and a slave that samples a final edge inside the
// violation may latch a bit that was never meant for it.
//
// RESET is not a request, it is an absence. The master is GONE, and there is
// nothing left to sequence a graceful exit with. So reset must release CS
// IMMEDIATELY and asynchronously, because the alternative is CS held low by a
// master that no longer exists -- and a slave watching a dead CS line stays
// mid-transaction indefinitely.
//
// The rule is easier to remember than to derive: an abort is orderly and costs
// time; a reset is instant and costs correctness at the protocol level, which
// is acceptable precisely because reset already means "start over".
//
// HOW THE ABORT IS IMPLEMENTED -- AND WHERE.
//
// The first attempt at this block tried to abort WITHOUT touching Chapter
// 13.7, by synthesising a "frame done" pulse and simultaneously pulling the
// burst's `hold` low, so the chip-select machine would fall out of XFER into
// LAG and pay the hold and gap on its own. It does not work, and the reason is
// worth keeping: 13.7 LATCHES `hold` when the frame is requested. Mid-frame the
// live input is not consulted, so the machine ended up in HOLD -- clock
// stopped, chip select still LOW, waiting for a request that this block was
// busy refusing. The bus hung with the slave still selected: the exact failure
// the chapter is about, produced by the code meant to prevent it.
//
// So the abort input belongs in 13.7, where the pins are owned, and this block
// is pure POLICY:
//
// decide that an abort is happening, hold it until the bus is genuinely
// free, publish what was lost, throw away queued work, and refuse new work
// until it is over
//
// There is exactly one piece of logic that knows how to leave the bus, and it
// is not this one.
//
// WHAT THE ABORT MUST REPORT.
//
// An aborted frame captured some bits but not all of them. Chapter 13.6's
// receive register only raises valid at the LAST bit, so a truncated frame
// raises nothing -- the word is silently absent, which is the right behaviour
// and the wrong amount of information. So the partial bit count is published,
// and a truncation flag is latched. And the streaming shadow of Chapter 13.9
// is FLUSHED: a queued word that was meant for the aborted transaction must
// not be the first thing sent in the next one.
module spi_abort_ctl #(
parameter LEN_W = 6
) (
input wire clk,
input wire rst_n,
input wire abort_req, // level, from the CPU
// As in Chapter 13.9: the flags are detected here and cleared by the
// register interface of Chapter 13.11, so each has one owner and one clear.
input wire clr_flags,
// --- from the transfer engine ---------------------------------------
input wire busy, // 13.7: a transaction is open
input wire shift_en, // 13.7: the clock is running
input wire frame_done, // 13.5: all bits captured
input wire [LEN_W-1:0] bit_idx, // 13.5: bits captured so far
input wire [LEN_W-1:0] len,
// --- from the streaming controller (13.9) ---------------------------
input wire req_in,
// --- to the chip-select controller (13.7) ---------------------------
output wire req_out,
output wire abort_out, // one cycle: leave the bus
// --- status ---------------------------------------------------------
output wire aborting, // an abort is being carried out
output reg aborted, // sticky: an abort happened
output reg [LEN_W-1:0] abort_bits, // bits that made it out
output reg rx_trunc, // sticky: a partial word was lost
output wire flush, // drop the queued word
output reg abort_ack // one cycle, when the bus is idle
);
localparam [1:0] A_RUN = 2'd0, // nothing to do
A_KILL = 2'd1, // one cycle: end the frame, end the burst
A_WAIT = 2'd2; // let 13.7 walk out through LAG and GAP
reg [1:0] astate;
// ONE COMMAND, ONE ABORT. `abort_req` is a level, and it is generally still
// asserted when the abort finishes -- the register interface of Chapter
// 13.11 clears it on the acknowledgement, which is necessarily a cycle
// later. A design that re-reads the level on returning to A_RUN therefore
// services the SAME request a second time, and the second pass finds the
// bus idle: it re-latches `aborted`, and overwrites `abort_bits` with zero
// while leaving `rx_trunc` set. The report then says a word was truncated
// after zero bits, which is not a thing that can happen, and the driver
// author spends an afternoon on it.
reg req_q;
wire abort_edge = abort_req & ~req_q;
assign aborting = (astate != A_RUN);
// A single cycle. 13.7 needs no more than that -- it latches the decision
// by changing state -- and a level would keep re-triggering as the machine
// passed back through LEAD on the next transaction.
assign abort_out = (astate == A_KILL);
// New frames are refused for the whole of the abort, including the gap.
// Without this the streaming controller's still-asserted request would
// start a fresh transaction the instant CS came back up, and the abort
// would look like a one-frame hiccup rather than a stop.
assign req_out = req_in & ~aborting & ~abort_req;
// The queued word belonged to the transaction being abandoned.
assign flush = aborting;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
astate <= A_RUN;
req_q <= 1'b0;
aborted <= 1'b0;
abort_bits <= {LEN_W{1'b0}};
rx_trunc <= 1'b0;
abort_ack <= 1'b0;
end else begin
abort_ack <= 1'b0;
req_q <= abort_req;
// Applied first, so a clear on the same cycle as a fresh abort
// does not swallow the new report.
if (clr_flags) begin
aborted <= 1'b0;
rx_trunc <= 1'b0;
end
case (astate)
A_RUN: begin
if (abort_edge && busy) begin
aborted <= 1'b1;
// Captured at the moment of the kill, because one
// cycle later the engine has been told the frame
// ended and the count no longer means anything. Zero
// unless bits were actually moving: outside XFER,
// `bit_idx` still holds the PREVIOUS frame's total and
// publishing that would be a lie in the most
// convincing possible form.
abort_bits <= shift_en ? bit_idx : {LEN_W{1'b0}};
// Truncated only if bits were actually mid-flight. An
// abort during LEAD, HOLD, LAG or GAP loses nothing,
// and flagging it would train the driver to ignore
// the flag.
if (shift_en && !frame_done &&
bit_idx != len && bit_idx != {LEN_W{1'b0}})
rx_trunc <= 1'b1;
astate <= A_KILL;
end else if (abort_edge && !busy) begin
// Nothing in flight. The abort still has to be
// acknowledged, or a driver waiting for the ack hangs
// on the one case where there was nothing to stop.
aborted <= 1'b1;
abort_bits <= {LEN_W{1'b0}};
abort_ack <= 1'b1;
end
end
A_KILL: astate <= A_WAIT;
A_WAIT: begin
// 13.7 is paying the CS hold and the CS-high time. Only
// when it reports itself idle is the bus legally free.
if (!busy) begin
astate <= A_RUN;
abort_ack <= 1'b1;
end
end
default: astate <= A_RUN;
endcase
end
end
endmodule// spi_abort_ctl_tb.v
//
// The whole engine runs here, and the abort is injected at EVERY bit position
// of a frame -- before the first edge, on each interior bit, and on the last --
// because an abort is exactly the kind of feature that works at the convenient
// moments and breaks at one specific inconvenient one.
//
// After every single abort the same four things are checked on the PINS, not in
// the state machines:
//
// 1. no SCLK edge occurs once chip select has gone high;
// 2. chip select stayed low for at least the programmed hold after the final
// edge -- an abort is not permission to violate t_CSH;
// 3. chip select stayed high for at least the programmed gap before anything
// else started;
// 4. the engine comes back: a normal transaction immediately afterwards
// transfers every word correctly.
//
// Point 4 matters more than it looks. A master that stops cleanly but wedges is
// no better than one that stops dirtily, and an abort path is the least
// exercised logic in the design, so it is exactly where a state machine gets
// stuck in a state nothing leaves.
`timescale 1ns/1ps
module spi_abort_ctl_tb;
localparam MAX_W = 32;
localparam LEN_W = 6;
localparam DIV_W = 8;
localparam N_CS = 2;
localparam SEL_W = 1;
localparam CNT_W = 8;
localparam FRAME_BITS = 8;
localparam LEAD = 3;
localparam LAG = 4;
localparam GAP = 5;
reg clk;
reg rst_n;
always #5 clk = ~clk;
// --- 13.9 -------------------------------------------------------------
reg tx_push;
reg [MAX_W-1:0] tx_wdata;
reg tx_last;
wire tx_ready;
reg rx_pop;
wire [MAX_W-1:0] rx_rdata;
wire rx_ready, rx_overrun;
wire req_raw, hold_raw, load_stb, stalled;
wire [MAX_W-1:0] tx_data;
// --- 13.10, the block under test --------------------------------------
reg abort_req;
reg clr_flags;
wire req_gated, abort_out;
wire aborting, aborted, rx_trunc, flush, abort_ack;
wire [LEN_W-1:0] abort_bits;
// --- 13.7 -------------------------------------------------------------
reg [SEL_W-1:0] sel;
wire [N_CS-1:0] cs_n;
wire shift_en, start_stb, busy, sel_err;
wire [2:0] state_id;
// --- 13.4 / 13.5 / 13.8 -----------------------------------------------
reg [DIV_W-1:0] div;
reg cpol;
reg cpha;
reg [LEN_W-1:0] len;
reg lsb_first;
wire sclk, edge_a_stb, edge_b_stb, bit_done, div_err;
wire [DIV_W-1:0] half_a, half_b;
wire preload_stb, launch_stb, capture_stb, frame_done;
wire [LEN_W-1:0] bit_idx;
wire miso, mosi;
wire [MAX_W-1:0] rx_word;
wire rx_valid_stb, len_err;
spi_clkdiv_strobe #(.DIV_W(DIV_W)) u_div (
.clk(clk), .rst_n(rst_n), .en(shift_en), .div(div), .cpol(cpol),
.sclk(sclk), .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.bit_done(bit_done), .half_a(half_a), .half_b(half_b),
.div_err(div_err)
);
spi_mode_edges #(.LEN_W(LEN_W)) u_mode (
.clk(clk), .rst_n(rst_n), .cpha(cpha), .len(len),
.active(shift_en), .start_stb(start_stb),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.preload_stb(preload_stb), .launch_stb(launch_stb),
.capture_stb(capture_stb), .bit_idx(bit_idx), .frame_done(frame_done)
);
spi_width_order #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_data (
.clk(clk), .rst_n(rst_n),
.tx_data(tx_data), .len(len), .lsb_first(lsb_first),
.load_stb(load_stb),
.preload_stb(preload_stb), .launch_stb(launch_stb),
.capture_stb(capture_stb),
.miso(miso), .mosi(mosi),
.rx_data(rx_word), .rx_valid_stb(rx_valid_stb), .len_err(len_err)
);
spi_cs_ctrl #(.N_CS(N_CS), .SEL_W(SEL_W), .CNT_W(CNT_W)) u_cs (
.clk(clk), .rst_n(rst_n),
.req(req_gated), .hold(hold_raw), .sel(sel),
.lead_cyc(LEAD[CNT_W-1:0]), .lag_cyc(LAG[CNT_W-1:0]),
.gap_cyc(GAP[CNT_W-1:0]),
.core_done(frame_done), .abort(abort_out),
.cs_n(cs_n), .shift_en(shift_en), .start_stb(start_stb),
.busy(busy), .sel_err(sel_err), .state_id(state_id)
);
spi_multibyte #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_stream (
.clk(clk), .rst_n(rst_n),
.tx_push(tx_push), .tx_wdata(tx_wdata), .tx_last(tx_last),
.tx_ready(tx_ready),
.flush(flush), .clr_flags(1'b0),
.rx_pop(rx_pop), .rx_rdata(rx_rdata), .rx_ready(rx_ready),
.rx_overrun(rx_overrun),
.core_busy(busy), .shift_en(shift_en), .start_stb(start_stb),
.rx_valid_stb(rx_valid_stb), .rx_word(rx_word),
.req(req_raw), .hold(hold_raw), .tx_data(tx_data),
.load_stb(load_stb),
.stalled(stalled)
);
spi_abort_ctl #(.LEN_W(LEN_W)) dut (
.clk(clk), .rst_n(rst_n),
.abort_req(abort_req), .clr_flags(clr_flags),
.busy(busy), .shift_en(shift_en), .frame_done(frame_done),
.bit_idx(bit_idx), .len(len),
.req_in(req_raw),
.req_out(req_gated), .abort_out(abort_out),
.aborting(aborting), .aborted(aborted), .abort_bits(abort_bits),
.rx_trunc(rx_trunc), .flush(flush), .abort_ack(abort_ack)
);
// --- the slave --------------------------------------------------------
reg [MAX_W-1:0] slv_tx_q [0:255];
reg [MAX_W-1:0] slv_rx_q [0:255];
integer slv_tx_i, slv_rx_i;
reg [MAX_W-1:0] slv_sr, slv_rx_sr;
reg slave_bit_r;
assign miso = slave_bit_r;
always @(posedge clk) begin
if (load_stb) begin
slv_sr <= slv_tx_q[slv_tx_i] << (MAX_W - len);
slv_tx_i <= slv_tx_i + 1;
end else if (preload_stb || launch_stb) begin
slave_bit_r <= slv_sr[MAX_W-1];
slv_sr <= {slv_sr[MAX_W-2:0], 1'b0};
end
if (load_stb) slv_rx_sr <= {MAX_W{1'b0}};
else if (capture_stb) slv_rx_sr <= {slv_rx_sr[MAX_W-2:0], mosi};
if (rx_valid_stb) begin
slv_rx_q[slv_rx_i] <= slv_rx_sr;
slv_rx_i <= slv_rx_i + 1;
end
end
// --- the pin monitor --------------------------------------------------
wire any_cs_low = ~(&cs_n);
reg any_cs_low_q;
integer since_edge, since_rise, e_now, r_now;
integer edges_after_cs_high; // must stay zero, always
integer min_lag_seen, min_gap_seen;
integer cs_rises, cs_falls;
integer caps_seen;
integer edges_seen;
always @(posedge clk) begin
if (!rst_n) begin
any_cs_low_q <= 1'b0;
since_edge <= 0;
since_rise <= 0;
end else begin
any_cs_low_q <= any_cs_low;
e_now = since_edge;
r_now = since_rise;
if (edge_a_stb || edge_b_stb) begin
since_edge <= 1;
edges_seen <= edges_seen + 1;
// The property the whole chapter turns on.
if (!any_cs_low)
edges_after_cs_high <= edges_after_cs_high + 1;
end else begin
since_edge <= e_now + 1;
end
if (!any_cs_low && any_cs_low_q) begin // CS rose
since_rise <= 1;
cs_rises <= cs_rises + 1;
if (e_now < min_lag_seen) min_lag_seen <= e_now;
end else begin
since_rise <= r_now + 1;
end
if (any_cs_low && !any_cs_low_q) begin // CS fell
cs_falls <= cs_falls + 1;
if (cs_rises > 0 && r_now < min_gap_seen) min_gap_seen <= r_now;
end
if (capture_stb) caps_seen <= caps_seen + 1;
end
end
integer errors;
task clear_pins;
begin
min_lag_seen = 9999; min_gap_seen = 9999;
cs_rises = 0; cs_falls = 0; caps_seen = 0; edges_seen = 0;
end
endtask
task push;
input [MAX_W-1:0] w;
input last;
integer guard;
begin
guard = 8000;
while (!tx_ready && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
tx_wdata = w; tx_last = last; tx_push = 1'b1;
@(negedge clk);
tx_push = 1'b0;
end
endtask
task drain_rx;
begin
while (rx_ready) begin
rx_pop = 1'b1;
@(negedge clk);
rx_pop = 1'b0;
@(negedge clk);
end
end
endtask
task wait_idle;
integer guard;
begin
guard = 20000;
while (busy && guard > 0) begin
drain_rx();
@(negedge clk);
guard = guard - 1;
end
repeat (4) @(negedge clk);
drain_rx();
end
endtask
// A clean four-word transaction, fully checked. Used to prove the engine
// still works after each abort.
task clean_transfer;
input [8*40:1] tag;
integer f, base, bad;
reg [MAX_W-1:0] w [0:3];
begin
base = slv_rx_i;
w[0] = 32'h11; w[1] = 32'h22; w[2] = 32'h44; w[3] = 32'h88;
for (f = 0; f < 4; f = f + 1) begin
push(w[f], (f == 3));
drain_rx();
end
wait_idle();
bad = 0;
for (f = 0; f < 4; f = f + 1)
if (slv_rx_q[base + f] !== w[f]) bad = bad + 1;
if (slv_rx_i != base + 4) begin
$display(" FAIL: %0s -- the engine delivered %0d of 4 words after an abort",
tag, slv_rx_i - base);
errors = errors + 1;
end else if (bad != 0) begin
$display(" FAIL: %0s -- %0d of 4 words wrong after an abort",
tag, bad);
errors = errors + 1;
end
end
endtask
integer b, k, guard, seed;
integer aborts_done, trunc_seen, bits_lo, bits_hi;
initial begin
clear_pins();
edges_after_cs_high = 0;
slv_tx_i = 0; slv_rx_i = 0;
slv_sr = 32'h0; slv_rx_sr = 32'h0; slave_bit_r = 1'b0;
since_edge = 0; since_rise = 0;
edges_seen = 0; caps_seen = 0;
aborts_done = 0; trunc_seen = 0; bits_lo = 99; bits_hi = 0;
seed = 32'h00AB_0117;
for (k = 0; k < 256; k = k + 1) begin
seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
slv_tx_q[k] = (seed >> 9) & 32'hFF;
end
repeat (3) @(negedge clk);
// 1. RESET RELEASES CHIP SELECT WITHOUT WAITING FOR A CLOCK. Checked
// at a moment when no edge is anywhere near, which is the whole
// point: the release must not need one.
rst_n = 1'b1;
@(negedge clk);
push(32'hA5, 1'b0);
push(32'h5A, 1'b0);
// Wait until the clock is genuinely running and CS is low.
guard = 500;
while (!(shift_en && any_cs_low) && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
if (!any_cs_low) begin
$display(" FAIL: the engine never asserted chip select");
errors = errors + 1;
end
#2 rst_n = 1'b0; // mid-cycle, deliberately between clock edges
#1;
if (cs_n !== {N_CS{1'b1}}) begin
$display(" FAIL: an asynchronous reset did not release chip select until a clock edge");
errors = errors + 1;
end
#2 rst_n = 1'b1;
repeat (4) @(negedge clk);
$display(" reset asserted between clock edges released chip select in the same instant -- no edge required");
// Re-synchronise the slave model bookkeeping after the reset.
slv_tx_i = 0; slv_rx_i = 0;
slv_sr = 32'h0; slv_rx_sr = 32'h0; slave_bit_r = 1'b0;
repeat (2) @(negedge clk);
// 2. AN ABORT WITH NOTHING IN FLIGHT is still acknowledged.
clear_pins();
abort_req = 1'b1;
guard = 200;
while (!abort_ack && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
abort_req = 1'b0;
if (guard == 0) begin
$display(" FAIL: an abort with nothing in flight was never acknowledged");
errors = errors + 1;
end
if (rx_trunc) begin
$display(" FAIL: an abort with nothing in flight reported a truncated word");
errors = errors + 1;
end
if (cs_falls != 0) begin
$display(" FAIL: an abort with nothing in flight asserted chip select");
errors = errors + 1;
end
repeat (4) @(negedge clk);
$display(" an abort with nothing in flight: acknowledged, nothing asserted, nothing reported as lost");
clean_transfer("after an idle abort");
// 3. THE SWEEP. Abort after exactly b captured bits, for every b from
// 0 to len, and check the pins and the recovery each time.
for (b = 0; b <= FRAME_BITS; b = b + 1) begin
// Reset the sticky flags by resetting the abort block only -- a
// full reset would also lose the slave bookkeeping.
clear_pins();
// Queue two words: one to abort inside, one that must be FLUSHED
// and therefore never appear on the wire.
push(32'h3C, 1'b0);
push(32'hC3, 1'b0);
k = slv_rx_i;
// Wait for the frame to reach exactly b captured bits.
guard = 4000;
while (!(shift_en && bit_idx == b[LEN_W-1:0]) && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
if (guard == 0) begin
$display(" FAIL: the frame never reached %0d captured bits", b);
errors = errors + 1;
end
abort_req = 1'b1;
@(negedge clk);
abort_req = 1'b0;
guard = 4000;
while (!abort_ack && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
if (guard == 0) begin
$display(" FAIL: the abort at %0d bits was never acknowledged",
b);
errors = errors + 1;
end
aborts_done = aborts_done + 1;
// The pins, after the abort has fully completed.
if (busy) begin
$display(" FAIL: the engine was still busy after acknowledging the abort at %0d bits",
b);
errors = errors + 1;
end
if (cs_rises != 1) begin
$display(" FAIL: the abort at %0d bits raised chip select %0d times",
b, cs_rises);
errors = errors + 1;
end
if (min_lag_seen < LAG) begin
$display(" FAIL: the abort at %0d bits held chip select only %0d cycles past the last edge, below the %0d required",
b, min_lag_seen, LAG);
errors = errors + 1;
end
// Whether the word was truncated, and how far it got.
if (abort_bits < bits_lo) bits_lo = abort_bits;
if (abort_bits > bits_hi) bits_hi = abort_bits;
if (rx_trunc) trunc_seen = trunc_seen + 1;
if (b > 0 && b < FRAME_BITS && !rx_trunc) begin
$display(" FAIL: an abort after %0d of %0d bits was not reported as truncated",
b, FRAME_BITS);
errors = errors + 1;
end
// The flushed word must never have reached the wire.
if (slv_rx_i > k + 1) begin
$display(" FAIL: the abort at %0d bits let %0d words through -- the queued word was not flushed",
b, slv_rx_i - k);
errors = errors + 1;
end
// And the engine must still work.
drain_rx();
clean_transfer("recovery");
if (min_gap_seen < GAP) begin
$display(" FAIL: after the abort at %0d bits the next transaction began %0d cycles later, below the %0d gap",
b, min_gap_seen, GAP);
errors = errors + 1;
end
// Clear the sticky flags for the next iteration.
rst_n = 1'b0;
repeat (3) @(negedge clk);
rst_n = 1'b1;
slv_tx_i = 0; slv_rx_i = 0;
slv_sr = 32'h0; slv_rx_sr = 32'h0; slave_bit_r = 1'b0;
repeat (3) @(negedge clk);
end
if (edges_after_cs_high != 0) begin
$display(" FAIL: %0d SCLK edges occurred after chip select had gone high",
edges_after_cs_high);
errors = errors + 1;
end
if (aborts_done != FRAME_BITS + 1) begin
$display(" FAIL: %0d of %0d sweep aborts ran", aborts_done,
FRAME_BITS + 1);
errors = errors + 1;
end
if (bits_hi == bits_lo) begin
$display(" FAIL: every abort reported the same partial bit count (%0d) -- the count is not tracking anything",
bits_lo);
errors = errors + 1;
end
$display(" %0d aborts, one at each bit position from 0 to %0d: partial counts spanned %0d to %0d, %0d were reported as truncating a word, and the chip-select hold was honoured every time",
aborts_done, FRAME_BITS, bits_lo, bits_hi, trunc_seen);
$display(" no SCLK edge ever occurred after chip select went high, across every abort in the sweep");
if (errors == 0)
$display("PASS: an abort is carried out the way the datasheet allows -- the clock stops, the programmed chip-select hold is paid in full, chip select is released, and the programmed CS-high gap is paid before anything else begins -- verified at every one of %0d bit positions in a frame, with no SCLK edge ever occurring after chip select went high -- the partial bit count is published and a genuinely truncated word is flagged while an abort that interrupted nothing is not, the word queued behind the aborted one is flushed rather than becoming the first word of the next transaction, an abort with nothing in flight is still acknowledged so a waiting driver cannot hang, a reset asserted between clock edges releases chip select in the same instant without waiting for one, and after every abort a clean four-word transaction transfers correctly -- the engine stops without wedging", aborts_done);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
clk = 1'b0;
rst_n = 1'b0;
tx_push = 1'b0;
tx_wdata = 32'h0;
tx_last = 1'b0;
rx_pop = 1'b0;
abort_req = 1'b0;
clr_flags = 1'b0;
sel = 1'b0;
div = 8'd4;
cpol = 1'b0;
cpha = 1'b0;
len = FRAME_BITS[LEN_W-1:0];
lsb_first = 1'b0;
errors = 0;
end
endmodule-- spi_abort_ctl.vhd
--
-- Chapter 13.10 -- stopping in the middle, and coming back from reset.
--
-- Every block so far assumes transfers finish. Real systems abandon them: a
-- timeout fires, a driver is killed, a watchdog resets the subsystem. What the
-- master does in those moments decides whether the SLAVE is still usable
-- afterwards, and a slave left mid-transaction is a slave that must be power
-- cycled.
--
-- THE TWO WAYS TO STOP ARE NOT THE SAME.
--
-- ABORT is a request. The master is still running and still owns the pins, so
-- it must stop the way the datasheet allows: halt the clock, wait out the CS
-- hold, release CS, wait out the CS-high time. All of that takes cycles and all
-- of it is mandatory. An abort that yanks CS high on the spot violates t_CSH on
-- the way out -- and a slave that samples a final edge inside the violation may
-- latch a bit that was never meant for it.
--
-- RESET is not a request, it is an absence. The master is GONE, and there is
-- nothing left to sequence a graceful exit with. So reset must release CS
-- IMMEDIATELY and asynchronously: the alternative is CS held low by a master
-- that no longer exists, and a slave watching a dead CS line stays
-- mid-transaction indefinitely.
--
-- An abort is orderly and costs time; a reset is instant and costs correctness
-- at the protocol level, which is acceptable precisely because reset already
-- means "start over".
--
-- HOW THE ABORT IS IMPLEMENTED -- AND WHERE.
--
-- The first attempt tried to abort WITHOUT touching Chapter 13.7, by
-- synthesising a "frame done" pulse and simultaneously pulling the burst's
-- `hold` low, so the chip-select machine would fall out of XFER into LAG and
-- pay the hold and gap on its own. It does not work, and the reason is worth
-- keeping: 13.7 LATCHES `hold` when the frame is requested. Mid-frame the live
-- input is not consulted, so the machine ended up in HOLD -- clock stopped,
-- chip select still LOW, waiting for a request that this block was busy
-- refusing. The bus hung with the slave still selected: the exact failure the
-- chapter is about, produced by the code meant to prevent it.
--
-- So the abort input belongs in 13.7, where the pins are owned, and this block
-- is pure POLICY: decide that an abort is happening, hold it until the bus is
-- genuinely free, publish what was lost, throw away queued work, and refuse new
-- work until it is over. There is exactly one piece of logic that knows how to
-- leave the bus, and it is not this one.
--
-- WHAT THE ABORT MUST REPORT. An aborted frame captured some bits but not all.
-- Chapter 13.6's receive register only raises valid at the LAST bit, so a
-- truncated frame raises nothing -- the word is silently absent, which is the
-- right behaviour and the wrong amount of information. So the partial bit count
-- is published and a truncation flag is latched. And the streaming shadow of
-- Chapter 13.9 is FLUSHED: a word queued for the aborted transaction must not
-- be the first thing sent in the next one.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_abort_ctl is
generic (
LEN_W : positive := 6
);
port (
clk : in std_logic;
rst_n : in std_logic;
abort_req : in std_logic; -- level, from the CPU
-- As in Chapter 13.9: the flags are detected here and cleared by the
-- register interface of Chapter 13.11, so each has one owner and one
-- clear path.
clr_flags : in std_logic;
-- from the transfer engine
busy : in std_logic; -- 13.7: txn is open
shift_en : in std_logic; -- 13.7: clock running
frame_done : in std_logic; -- 13.5: all bits in
bit_idx : in unsigned(LEN_W - 1 downto 0); -- 13.5: bits so far
len : in unsigned(LEN_W - 1 downto 0);
-- from the streaming controller (13.9)
req_in : in std_logic;
-- to the chip-select controller (13.7)
req_out : out std_logic;
abort_out : out std_logic; -- one cycle: leave the bus
-- status
aborting : out std_logic; -- an abort is being carried out
aborted : out std_logic; -- sticky: an abort happened
abort_bits : out unsigned(LEN_W - 1 downto 0); -- bits that made it
rx_trunc : out std_logic; -- sticky: a partial word was lost
flush : out std_logic; -- drop the queued word
abort_ack : out std_logic -- one cycle, when the bus is idle
);
end entity;
architecture rtl of spi_abort_ctl is
constant A_RUN : unsigned(1 downto 0) := "00"; -- nothing to do
constant A_KILL : unsigned(1 downto 0) := "01"; -- one cycle: leave the bus
constant A_WAIT : unsigned(1 downto 0) := "10"; -- let 13.7 walk out
signal astate : unsigned(1 downto 0) := A_RUN;
-- ONE COMMAND, ONE ABORT. `abort_req` is a level, and it is generally still
-- asserted when the abort finishes -- the register interface of Chapter
-- 13.11 clears it on the acknowledgement, which is necessarily a cycle
-- later. A design that re-reads the level on returning to A_RUN therefore
-- services the SAME request a second time, and the second pass finds the
-- bus idle: it re-latches `aborted` and overwrites `abort_bits` with zero
-- while leaving `rx_trunc` set. The report then says a word was truncated
-- after zero bits, which is not a thing that can happen.
signal req_q : std_logic := '0';
signal abort_edge : std_logic;
signal abrt_i : std_logic;
signal done_r : std_logic := '0';
signal bits_r : unsigned(LEN_W - 1 downto 0) := (others => '0');
signal trunc_r : std_logic := '0';
signal ack_r : std_logic := '0';
begin
abort_edge <= abort_req and (not req_q);
abrt_i <= '0' when astate = A_RUN else '1';
aborting <= abrt_i;
-- A single cycle. 13.7 needs no more than that -- it latches the decision
-- by changing state -- and a level would keep re-triggering as the machine
-- passed back through LEAD on the next transaction.
abort_out <= '1' when astate = A_KILL else '0';
-- New frames are refused for the whole of the abort, including the gap.
-- Without this the streaming controller's still-asserted request would
-- start a fresh transaction the instant CS came back up, and the abort
-- would look like a one-frame hiccup rather than a stop.
req_out <= req_in and (not abrt_i) and (not abort_req);
-- The queued word belonged to the transaction being abandoned.
flush <= abrt_i;
aborted <= done_r;
abort_bits <= bits_r;
rx_trunc <= trunc_r;
abort_ack <= ack_r;
fsm : process (clk, rst_n)
begin
if rst_n = '0' then
astate <= A_RUN;
req_q <= '0';
done_r <= '0';
bits_r <= (others => '0');
trunc_r <= '0';
ack_r <= '0';
elsif rising_edge(clk) then
ack_r <= '0';
req_q <= abort_req;
-- Applied first, so a clear on the same cycle as a fresh abort does
-- not swallow the new report.
if clr_flags = '1' then
done_r <= '0';
trunc_r <= '0';
end if;
case to_integer(astate) is
when 0 => -- A_RUN
if abort_edge = '1' and busy = '1' then
done_r <= '1';
-- Captured at the moment of the kill, because one
-- cycle later the engine has been told the frame ended
-- and the count no longer means anything. Zero unless
-- bits were actually moving: outside XFER, `bit_idx`
-- still holds the PREVIOUS frame's total and
-- publishing that would be a lie in the most
-- convincing possible form.
if shift_en = '1' then
bits_r <= bit_idx;
else
bits_r <= (others => '0');
end if;
-- Truncated only if bits were actually mid-flight. An
-- abort during LEAD, HOLD, LAG or GAP loses nothing,
-- and flagging it would train the driver to ignore the
-- flag.
if shift_en = '1' and frame_done = '0' and
bit_idx /= len and bit_idx /= 0 then
trunc_r <= '1';
end if;
astate <= A_KILL;
elsif abort_edge = '1' and busy = '0' then
-- Nothing in flight. The abort still has to be
-- acknowledged, or a driver waiting for the ack hangs
-- on the one case where there was nothing to stop.
done_r <= '1';
bits_r <= (others => '0');
ack_r <= '1';
end if;
when 1 => -- A_KILL
astate <= A_WAIT;
when 2 => -- A_WAIT
-- 13.7 is paying the CS hold and the CS-high time. Only
-- when it reports itself idle is the bus legally free.
if busy = '0' then
astate <= A_RUN;
ack_r <= '1';
end if;
when others =>
astate <= A_RUN;
end case;
end if;
end process;
end architecture;-- spi_abort_ctl_tb.vhd
--
-- The whole engine runs here, and the abort is injected at EVERY bit position
-- of a frame -- before the first edge, on each interior bit, and on the last --
-- because an abort is exactly the kind of feature that works at the convenient
-- moments and breaks at one specific inconvenient one.
--
-- After every abort the same four things are checked on the PINS, not in the
-- state machines:
--
-- 1. no SCLK edge occurs once chip select has gone high;
-- 2. chip select stayed low for at least the programmed hold after the final
-- edge -- an abort is not permission to violate t_CSH;
-- 3. chip select stayed high for at least the programmed gap before anything
-- else started;
-- 4. the engine comes back: a normal transaction immediately afterwards
-- transfers every word correctly.
--
-- Point 4 matters more than it looks. A master that stops cleanly but wedges is
-- no better than one that stops dirtily, and an abort path is the least
-- exercised logic in the design.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_abort_ctl_tb is
end entity;
architecture sim of spi_abort_ctl_tb is
constant MAX_W : positive := 32;
constant LEN_W : positive := 6;
constant DIV_W : positive := 8;
constant N_CS : positive := 2;
constant SEL_W : positive := 1;
constant CNT_W : positive := 8;
constant FRAME_BITS : natural := 8;
constant LEAD : natural := 3;
constant LAG : natural := 4;
constant GAP : natural := 5;
type word_array is array (0 to 255) of std_logic_vector(MAX_W - 1 downto 0);
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal halt : boolean := false;
-- 13.9
signal tx_push : std_logic := '0';
signal tx_wdata : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
signal tx_last : std_logic := '0';
signal tx_ready : std_logic;
signal rx_pop : std_logic := '0';
signal rx_rdata : std_logic_vector(MAX_W - 1 downto 0);
signal rx_ready, rx_overrun : std_logic;
signal req_raw, hold_raw, load_stb, stalled : std_logic;
signal tx_data : std_logic_vector(MAX_W - 1 downto 0);
-- 13.10, the block under test
signal abort_req : std_logic := '0';
signal clr_flags : std_logic := '0';
signal req_gated, abort_out : std_logic;
signal aborting, aborted, rx_trunc, flush, abort_ack : std_logic;
signal abort_bits : unsigned(LEN_W - 1 downto 0);
-- 13.7
signal sel : unsigned(SEL_W - 1 downto 0) := (others => '0');
signal cs_n : std_logic_vector(N_CS - 1 downto 0);
signal shift_en, start_stb, busy, sel_err : std_logic;
signal state_id : unsigned(2 downto 0);
-- 13.4 / 13.5 / 13.8
signal div : unsigned(DIV_W - 1 downto 0) := to_unsigned(4, DIV_W);
signal cpol : std_logic := '0';
signal cpha : std_logic := '0';
signal len : unsigned(LEN_W - 1 downto 0)
:= to_unsigned(FRAME_BITS, LEN_W);
signal lsb_first : std_logic := '0';
signal sclk, edge_a_stb, edge_b_stb, bit_done, div_err : std_logic;
signal half_a, half_b : unsigned(DIV_W - 1 downto 0);
signal preload_stb, launch_stb, capture_stb, frame_done : std_logic;
signal bit_idx : unsigned(LEN_W - 1 downto 0);
signal miso, mosi : std_logic;
signal rx_word : std_logic_vector(MAX_W - 1 downto 0);
signal rx_valid_stb, len_err : std_logic;
-- the slave
signal slv_tx_q : word_array := (others => (others => '0'));
signal slv_rx_q : word_array := (others => (others => '0'));
signal slv_tx_i : natural := 0;
signal slv_rx_i : natural := 0;
signal slv_sr, slv_rx_sr : std_logic_vector(MAX_W - 1 downto 0)
:= (others => '0');
signal slave_bit_r : std_logic := '0';
signal slv_rst : std_logic := '0';
-- the pin monitor
signal any_cs_low, any_cs_low_q : std_logic := '0';
signal edges_after_cs_high : natural := 0;
signal min_lag_seen, min_gap_seen : natural := 9999;
signal cs_rises, cs_falls, caps_seen, edges_seen : natural := 0;
signal clear_stb : std_logic := '0';
signal errors : natural := 0;
begin
clk <= not clk after 5 ns when not halt else '0';
u_div : entity work.spi_clkdiv_strobe
generic map (DIV_W => DIV_W)
port map (clk => clk, rst_n => rst_n, en => shift_en, div => div,
cpol => cpol, sclk => sclk,
edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
bit_done => bit_done, half_a => half_a, half_b => half_b,
div_err => div_err);
u_mode : entity work.spi_mode_edges
generic map (LEN_W => LEN_W)
port map (clk => clk, rst_n => rst_n, cpha => cpha, len => len,
active => shift_en, start_stb => start_stb,
edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
preload_stb => preload_stb, launch_stb => launch_stb,
capture_stb => capture_stb, bit_idx => bit_idx,
frame_done => frame_done);
u_data : entity work.spi_width_order
generic map (MAX_W => MAX_W, LEN_W => LEN_W)
port map (clk => clk, rst_n => rst_n,
tx_data => tx_data, len => len, lsb_first => lsb_first,
load_stb => load_stb,
preload_stb => preload_stb, launch_stb => launch_stb,
capture_stb => capture_stb,
miso => miso, mosi => mosi,
rx_data => rx_word, rx_valid_stb => rx_valid_stb,
len_err => len_err);
u_cs : entity work.spi_cs_ctrl
generic map (N_CS => N_CS, SEL_W => SEL_W, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
req => req_gated, hold => hold_raw, sel => sel,
lead_cyc => to_unsigned(LEAD, CNT_W),
lag_cyc => to_unsigned(LAG, CNT_W),
gap_cyc => to_unsigned(GAP, CNT_W),
core_done => frame_done, abort => abort_out,
cs_n => cs_n, shift_en => shift_en, start_stb => start_stb,
busy => busy, sel_err => sel_err, state_id => state_id);
u_stream : entity work.spi_multibyte
generic map (MAX_W => MAX_W, LEN_W => LEN_W)
port map (clk => clk, rst_n => rst_n,
tx_push => tx_push, tx_wdata => tx_wdata, tx_last => tx_last,
tx_ready => tx_ready,
flush => flush, clr_flags => '0',
rx_pop => rx_pop, rx_rdata => rx_rdata, rx_ready => rx_ready,
rx_overrun => rx_overrun,
core_busy => busy, shift_en => shift_en,
start_stb => start_stb,
rx_valid_stb => rx_valid_stb, rx_word => rx_word,
req => req_raw, hold => hold_raw, tx_data => tx_data,
load_stb => load_stb, stalled => stalled);
dut : entity work.spi_abort_ctl
generic map (LEN_W => LEN_W)
port map (clk => clk, rst_n => rst_n,
abort_req => abort_req, clr_flags => clr_flags,
busy => busy, shift_en => shift_en, frame_done => frame_done,
bit_idx => bit_idx, len => len,
req_in => req_raw,
req_out => req_gated, abort_out => abort_out,
aborting => aborting, aborted => aborted,
abort_bits => abort_bits, rx_trunc => rx_trunc,
flush => flush, abort_ack => abort_ack);
miso <= slave_bit_r;
slave_p : process (clk)
begin
if rising_edge(clk) then
if slv_rst = '1' then
slv_tx_i <= 0;
slv_rx_i <= 0;
slv_sr <= (others => '0');
slv_rx_sr <= (others => '0');
slave_bit_r <= '0';
else
if load_stb = '1' then
slv_sr <= std_logic_vector(
shift_left(unsigned(slv_tx_q(slv_tx_i)),
MAX_W - to_integer(len)));
slv_tx_i <= slv_tx_i + 1;
slv_rx_sr <= (others => '0');
else
if preload_stb = '1' or launch_stb = '1' then
slave_bit_r <= slv_sr(MAX_W - 1);
slv_sr <= slv_sr(MAX_W - 2 downto 0) & '0';
end if;
if capture_stb = '1' then
slv_rx_sr <= slv_rx_sr(MAX_W - 2 downto 0) & mosi;
end if;
end if;
if rx_valid_stb = '1' then
slv_rx_q(slv_rx_i) <= slv_rx_sr;
slv_rx_i <= slv_rx_i + 1;
end if;
end if;
end if;
end process;
any_cs_low <= '0' when cs_n = (cs_n'range => '1') else '1';
monitor : process (clk)
variable e_now, r_now : natural;
variable since_edge, since_rise : natural := 0;
begin
if rising_edge(clk) then
if rst_n = '0' then
any_cs_low_q <= '0';
since_edge := 0;
since_rise := 0;
else
any_cs_low_q <= any_cs_low;
if clear_stb = '1' then
min_lag_seen <= 9999; min_gap_seen <= 9999;
cs_rises <= 0; cs_falls <= 0; caps_seen <= 0;
edges_seen <= 0;
end if;
e_now := since_edge;
r_now := since_rise;
if edge_a_stb = '1' or edge_b_stb = '1' then
since_edge := 1;
edges_seen <= edges_seen + 1;
-- The property the whole chapter turns on.
if any_cs_low = '0' then
edges_after_cs_high <= edges_after_cs_high + 1;
end if;
else
since_edge := e_now + 1;
end if;
if any_cs_low = '0' and any_cs_low_q = '1' then -- CS rose
since_rise := 1;
cs_rises <= cs_rises + 1;
if e_now < min_lag_seen then min_lag_seen <= e_now; end if;
else
since_rise := r_now + 1;
end if;
if any_cs_low = '1' and any_cs_low_q = '0' then -- CS fell
cs_falls <= cs_falls + 1;
if cs_rises > 0 and r_now < min_gap_seen then
min_gap_seen <= r_now;
end if;
end if;
if capture_stb = '1' then caps_seen <= caps_seen + 1; end if;
end if;
end if;
end process;
stim : process
variable errs : natural := 0;
variable guard : natural;
variable seed : unsigned(31 downto 0) := x"00AB0117";
variable r : unsigned(31 downto 0);
variable base, bad, k : natural;
variable aborts_done, trunc_seen, bits_lo, bits_hi : natural;
procedure clear_pins is
begin
clear_stb <= '1';
wait until falling_edge(clk);
clear_stb <= '0';
end procedure;
procedure push(w : std_logic_vector(MAX_W - 1 downto 0);
last : std_logic) is
begin
guard := 8000;
while tx_ready = '0' and guard > 0 loop
wait until falling_edge(clk);
guard := guard - 1;
end loop;
tx_wdata <= w; tx_last <= last; tx_push <= '1';
wait until falling_edge(clk);
tx_push <= '0';
end procedure;
procedure drain_rx is
begin
while rx_ready = '1' loop
rx_pop <= '1';
wait until falling_edge(clk);
rx_pop <= '0';
wait until falling_edge(clk);
end loop;
end procedure;
procedure wait_idle is
begin
guard := 20000;
while busy = '1' and guard > 0 loop
drain_rx;
wait until falling_edge(clk);
guard := guard - 1;
end loop;
for j in 1 to 4 loop wait until falling_edge(clk); end loop;
drain_rx;
end procedure;
-- A clean four-word transaction, fully checked. Used to prove the
-- engine still works after each abort.
procedure clean_transfer(tag : string) is
type w4 is array (0 to 3) of std_logic_vector(MAX_W - 1 downto 0);
constant W : w4 := (x"00000011", x"00000022",
x"00000044", x"00000088");
begin
base := slv_rx_i;
for f in 0 to 3 loop
if f = 3 then push(W(f), '1'); else push(W(f), '0'); end if;
drain_rx;
end loop;
wait_idle;
bad := 0;
for f in 0 to 3 loop
if slv_rx_q(base + f) /= W(f) then bad := bad + 1; end if;
end loop;
if slv_rx_i /= base + 4 then
report " FAIL: " & tag & " -- the engine delivered " &
integer'image(slv_rx_i - base) &
" of 4 words after an abort";
errs := errs + 1;
elsif bad /= 0 then
report " FAIL: " & tag & " -- " & integer'image(bad) &
" of 4 words wrong after an abort";
errs := errs + 1;
end if;
end procedure;
constant ALL_HIGH : std_logic_vector(N_CS - 1 downto 0)
:= (others => '1');
begin
for i in 0 to 255 loop
seed := resize(seed * x"0019660D", 32) + x"3C6EF35F";
slv_tx_q(i) <= std_logic_vector(
resize(shift_right(seed, 9) and x"000000FF",
MAX_W));
end loop;
aborts_done := 0; trunc_seen := 0; bits_lo := 99; bits_hi := 0;
for j in 1 to 3 loop wait until falling_edge(clk); end loop;
-- 1. RESET RELEASES CHIP SELECT WITHOUT WAITING FOR A CLOCK. Checked
-- at a moment when no edge is anywhere near, which is the point: the
-- release must not need one.
rst_n <= '1';
wait until falling_edge(clk);
push(x"000000A5", '0');
push(x"0000005A", '0');
guard := 500;
while not (shift_en = '1' and any_cs_low = '1') and guard > 0 loop
wait until falling_edge(clk);
guard := guard - 1;
end loop;
if any_cs_low /= '1' then
report " FAIL: the engine never asserted chip select";
errs := errs + 1;
end if;
wait for 2 ns; -- deliberately between clock edges
rst_n <= '0';
wait for 1 ns;
if cs_n /= ALL_HIGH then
report " FAIL: an asynchronous reset did not release chip select until a clock edge";
errs := errs + 1;
end if;
wait for 2 ns;
rst_n <= '1';
for j in 1 to 4 loop wait until falling_edge(clk); end loop;
report " reset asserted between clock edges released chip select in the same instant -- no edge required";
slv_rst <= '1';
for j in 1 to 2 loop wait until falling_edge(clk); end loop;
slv_rst <= '0';
for j in 1 to 2 loop wait until falling_edge(clk); end loop;
-- 2. AN ABORT WITH NOTHING IN FLIGHT is still acknowledged.
clear_pins;
abort_req <= '1';
guard := 200;
while abort_ack = '0' and guard > 0 loop
wait until falling_edge(clk);
guard := guard - 1;
end loop;
abort_req <= '0';
if guard = 0 then
report " FAIL: an abort with nothing in flight was never acknowledged";
errs := errs + 1;
end if;
if rx_trunc = '1' then
report " FAIL: an abort with nothing in flight reported a truncated word";
errs := errs + 1;
end if;
if cs_falls /= 0 then
report " FAIL: an abort with nothing in flight asserted chip select";
errs := errs + 1;
end if;
for j in 1 to 4 loop wait until falling_edge(clk); end loop;
report " an abort with nothing in flight: acknowledged, nothing asserted, nothing reported as lost";
clean_transfer("after an idle abort");
-- 3. THE SWEEP. Abort after exactly b captured bits, for every b from 0
-- to len, and check the pins and the recovery each time.
for b in 0 to FRAME_BITS loop
clear_pins;
-- Queue two words: one to abort inside, one that must be FLUSHED
-- and therefore never appear on the wire.
push(x"0000003C", '0');
push(x"000000C3", '0');
k := slv_rx_i;
guard := 4000;
while not (shift_en = '1' and bit_idx = b) and guard > 0 loop
wait until falling_edge(clk);
guard := guard - 1;
end loop;
if guard = 0 then
report " FAIL: the frame never reached " & integer'image(b) &
" captured bits";
errs := errs + 1;
end if;
abort_req <= '1';
wait until falling_edge(clk);
abort_req <= '0';
guard := 4000;
while abort_ack = '0' and guard > 0 loop
wait until falling_edge(clk);
guard := guard - 1;
end loop;
if guard = 0 then
report " FAIL: the abort at " & integer'image(b) &
" bits was never acknowledged";
errs := errs + 1;
end if;
aborts_done := aborts_done + 1;
if busy = '1' then
report " FAIL: still busy after acknowledging the abort at " &
integer'image(b) & " bits";
errs := errs + 1;
end if;
if cs_rises /= 1 then
report " FAIL: the abort at " & integer'image(b) &
" bits raised chip select " & integer'image(cs_rises) &
" times";
errs := errs + 1;
end if;
if min_lag_seen < LAG then
report " FAIL: the abort at " & integer'image(b) &
" bits held chip select only " &
integer'image(min_lag_seen) &
" cycles past the last edge, below the " &
integer'image(LAG) & " required";
errs := errs + 1;
end if;
if to_integer(abort_bits) < bits_lo then
bits_lo := to_integer(abort_bits);
end if;
if to_integer(abort_bits) > bits_hi then
bits_hi := to_integer(abort_bits);
end if;
if rx_trunc = '1' then trunc_seen := trunc_seen + 1; end if;
if b > 0 and b < FRAME_BITS and rx_trunc = '0' then
report " FAIL: an abort after " & integer'image(b) & " of " &
integer'image(FRAME_BITS) &
" bits was not reported as truncated";
errs := errs + 1;
end if;
if slv_rx_i > k + 1 then
report " FAIL: the abort at " & integer'image(b) &
" bits let " & integer'image(slv_rx_i - k) &
" words through -- the queued word was not flushed";
errs := errs + 1;
end if;
drain_rx;
clean_transfer("recovery");
if min_gap_seen < GAP then
report " FAIL: after the abort at " & integer'image(b) &
" bits the next transaction began " &
integer'image(min_gap_seen) &
" cycles later, below the " & integer'image(GAP) &
" gap";
errs := errs + 1;
end if;
-- Clear the sticky flags for the next iteration.
rst_n <= '0';
for j in 1 to 3 loop wait until falling_edge(clk); end loop;
rst_n <= '1';
slv_rst <= '1';
for j in 1 to 2 loop wait until falling_edge(clk); end loop;
slv_rst <= '0';
for j in 1 to 2 loop wait until falling_edge(clk); end loop;
end loop;
if edges_after_cs_high /= 0 then
report " FAIL: " & integer'image(edges_after_cs_high) &
" SCLK edges occurred after chip select had gone high";
errs := errs + 1;
end if;
if aborts_done /= FRAME_BITS + 1 then
report " FAIL: " & integer'image(aborts_done) & " of " &
integer'image(FRAME_BITS + 1) & " sweep aborts ran";
errs := errs + 1;
end if;
if bits_hi = bits_lo then
report " FAIL: every abort reported the same partial bit count -- the count is not tracking anything";
errs := errs + 1;
end if;
report " " & integer'image(aborts_done) &
" aborts, one at each bit position from 0 to " &
integer'image(FRAME_BITS) & ": partial counts spanned " &
integer'image(bits_lo) & " to " & integer'image(bits_hi) & ", " &
integer'image(trunc_seen) &
" were reported as truncating a word, and the chip-select hold was honoured every time";
report " no SCLK edge ever occurred after chip select went high, across every abort in the sweep";
errors <= errs;
if errs = 0 then
report "PASS: an abort is carried out the way the datasheet allows -- the clock stops, the programmed chip-select hold is paid in full, chip select is released, and the programmed CS-high gap is paid before anything else begins -- verified at every one of " & integer'image(aborts_done) & " bit positions in a frame, with no SCLK edge ever occurring after chip select went high -- the partial bit count is published and a genuinely truncated word is flagged while an abort that interrupted nothing is not, the word queued behind the aborted one is flushed rather than becoming the first word of the next transaction, an abort with nothing in flight is still acknowledged so a waiting driver cannot hang, a reset asserted between clock edges releases chip select in the same instant without waiting for one, and after every abort a clean four-word transaction transfers correctly -- the engine stops without wedging";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
halt <= true;
wait;
end process;
end architecture;Parity
All three implementations inject an abort at every bit position of a frame, from before the first edge to after the last, and after each one check four things on the pins: no SCLK edge once chip select has gone high, the programmed hold paid in full, the programmed CS-high gap paid before anything else starts, and a clean four-word transaction immediately afterwards. All three report partial counts spanning 0 to 8 with seven of the nine aborts flagged as truncating a word — the two that are not being the abort before any bit moved and the one after the frame completed.
10. Why a Verification Engineer Cares
// Every property here is about what the PINS did, because the entire point of an
// orderly abort is that the slave sees a legal end of transaction. A property
// about internal state would pass on a design that satisfied the state machine
// and violated the datasheet.
module spi_abort_ctl_sva #(
parameter int LEN_W = 6,
parameter int N_CS = 4,
parameter int LAG = 4,
parameter int GAP = 5
) (
input logic clk,
input logic rst_n,
input logic abort_req,
input logic busy,
input logic shift_en,
input logic [LEN_W-1:0] bit_idx,
input logic [LEN_W-1:0] len,
input logic [N_CS-1:0] cs_n,
input logic abort_out,
input logic aborting,
input logic aborted,
input logic [LEN_W-1:0] abort_bits,
input logic rx_trunc,
input logic flush,
input logic abort_ack,
input logic req_out
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
wire any_low = ~(&cs_n);
// THE property the chapter exists for: no clock edge after the select has
// been released. An abort that violates this has written something.
a_no_edge_after_release: assert property (!any_low |-> !shift_en);
// The abort must not release the select early: the clock must have been
// stopped for the programmed hold before the select rises, whether the
// transfer ended normally or was abandoned.
property p_lag_even_on_abort;
$rose(any_low) |-> $past(!shift_en, 1) && $past(!shift_en, LAG-1);
endproperty
a_lag_on_abort: assert property (p_lag_even_on_abort);
// And the gap is still owed afterwards.
property p_gap_even_on_abort;
$rose(any_low) |=> (!any_low)[*GAP-1];
endproperty
a_gap_on_abort: assert property (p_gap_even_on_abort);
// ONE command, one abort. The output pulse must be a single cycle, or 13.7
// aborts the next transaction as it passes back through LEAD.
a_abort_pulse: assert property (abort_out |=> !abort_out);
// Edge-qualified: a level that stays asserted must not produce a second
// abort. This is the property whose absence zeroes the partial bit count.
a_one_per_edge: assert property (
abort_out ##1 abort_req[*1:$] ##0 !$rose(abort_req) |-> !abort_out
);
// An abort must always be acknowledged, including when there was nothing to
// stop -- otherwise a driver waiting for the ack hangs on the easy case.
a_always_acked: assert property (
$rose(abort_req) |-> ##[1:$] abort_ack
);
// The bus must be genuinely free when the acknowledgement arrives.
a_ack_means_free: assert property (abort_ack |-> !busy || !aborting);
// A truncated word that got nowhere is a contradiction. This is exactly what
// a doubly-serviced request produces, so the assertion is worth having even
// though it looks like it cannot fail.
a_trunc_implies_bits: assert property (
$rose(rx_trunc) |-> abort_bits != '0
);
// Truncation only when bits were actually mid-flight. Flagging an abort that
// interrupted nothing trains the driver to ignore the flag.
a_no_false_trunc: assert property (
$rose(abort_out) && !shift_en |=> !$rose(rx_trunc)
);
// New work refused for the whole abort, including the gap.
a_req_gated: assert property (aborting |-> !req_out);
// And the queued word discarded, or it starts the next transaction itself.
a_flush_while_aborting: assert property (aborting |-> flush);
endmodule// The only axis that matters is WHERE in the transaction the abort arrived, and
// the interesting positions are all at the boundaries: before the first bit,
// after the last, and in each of the states that are not XFER.
covergroup cg_abort @(posedge clk);
// Bit position within the frame. Zero and `len` are the two that must NOT be
// flagged as truncating; everything between must be.
position: coverpoint bit_idx iff ($rose(abort_out) && shift_en) {
bins before_first = {0};
bins first_half = {[1:3]};
bins second_half = {[4:7]};
bins after_last = {8};
}
// Which state the abort interrupted. Each behaves differently and each must
// be seen -- LEAD and HOLD both have a select to release and no bits in
// flight, and GAP has neither.
where: coverpoint cs_state iff ($rose(abort_req)) {
bins in_idle = {0};
bins in_lead = {1};
bins in_xfer = {2};
bins in_hold = {3};
bins in_lag = {4};
bins in_gap = {5};
}
// Whether a word was queued behind the aborted one. If nothing was queued,
// the flush had nothing to do and the property is untested.
queued: coverpoint shadow_full iff ($rose(abort_out)) {
bins something_to_flush = {1};
bins nothing_queued = {0};
}
// Whether the level was still asserted when the abort completed -- the case
// that produces a second abort in a level-triggered design.
still_held: coverpoint abort_req iff (abort_ack) {
bins level_still_high = {1};
bins level_dropped = {0};
}
// Frames into the transaction. Aborting the first frame and aborting the
// fourth exercise different paths through 13.7.
frame_no: coverpoint frames_done iff ($rose(abort_out)) {
bins first = {0};
bins second = {1};
bins later = {[2:$]};
}
// And the recovery: a clean transaction must follow. Covered, because a
// suite that aborts and then stops has not tested that the engine recovers.
recovered: coverpoint clean_txn_after_abort {
bins yes = {1};
}
x_pos_queued: cross position, queued;
x_where_frame: cross where, frame_no;
endgroup11. Why an FPGA or ASIC Engineer Cares
Asynchronous reset on the select flops is the one non-negotiable reset choice in this design. Everywhere else, synchronous reset would be acceptable and would save a little routing. Here it would mean that a reset asserted while the clock is stopped — a PLL unlock, a power-domain shutdown, a watchdog that also gates the clock — never releases the select, and the slave stays selected by a master that no longer exists.
The abort adds no path to the pins. It redirects a state machine that already drives them, so there is no new logic between any flop and any output. That is why the abort input belongs in 13.7 rather than as a mux on the select outputs: a mux there would be new combinational logic on an output pin, and the pin's glitch-freedom is what makes a deselection meaningful.
The edge detector is one flop. Cheaper than the class of bug it prevents by several orders of magnitude, and worth mentioning to anyone who proposes removing it because the level "obviously" only asserts once.
The policy machine is small and entirely off the critical path. Two flops for the state, one for the edge, LEN_W for the bit count, two for the sticky flags, one for the acknowledgement — about ten flops at the defaults.
Reset assertion must be glitch-free at the source. This block relies on reset being clean, which is the reset network's responsibility and not this design's — but it is worth stating, because a reset that glitches during normal operation releases chip select mid-transaction, which is exactly the failure the asynchronous reset was added to prevent.
12. Failure Signature — A Slave That Needs A Power Cycle After Every Timeout
Symptom. A driver has a timeout on its SPI reads. When the timeout fires the driver resets the SPI controller and retries. The retry fails, and every subsequent access to that device fails, until the board is power-cycled. Other devices on the same bus continue to work.
What that establishes. Other devices working rules out the master's clock, the bus wiring and the power supply. One device permanently unresponsive after a specific software action, recoverable only by power cycling, means the device is in a state it cannot leave — and the only thing the master can do to put a device in such a state is leave it selected.
The mechanism. The driver's reset path resets the SPI controller, and the controller's select flops are synchronously reset. At the moment the driver resets the controller it also — as part of the same recovery routine — gates the SPI clock, on the reasonable theory that a hung controller should not be clocking anything. So no clock edge arrives, the synchronous reset never takes effect, and cs_n stays low.
The slave is now mid-transaction with a master that has forgotten about it. Its internal state machine is waiting for more clocks under an assertion that will never end, and it will not respond to a new transaction because from its point of view the old one has not finished. Releasing chip select would resynchronise it — and nothing in the system is going to release it.
Why it is recoverable only by power cycling. The slave's deselect logic is edge-triggered on chip select rising, and chip select is being held low by a flop with no clock. Nothing short of removing power changes either.
What the asynchronous reset changes. cs_n goes high the instant reset asserts, with no clock required. The slave sees a normal — if abrupt — end of transaction, resynchronises, and answers the retry.
Why the abort path is the better fix for the timeout itself. Reset is the correct response to a hung controller and the wrong response to a slave that is merely slow. A timeout should first abort: the transfer ends, the hold and gap are paid, the select is released legally, and the slave sees a clean end of transaction rather than a truncated one. Reset is the escalation if the abort itself does not complete — and having both means the common case does not need the violent one.
The diagnostic that identifies it in seconds. Probe cs_n after the recovery routine runs. Low means the master is holding a slave selected; high means look elsewhere. It is one probe and it distinguishes the entire class.
13. Common Misconceptions
"An abort should release chip select immediately — that is what abort means." It means abandon the transfer, not violate the slave's timing. Releasing the select on the spot skips the hold time, and a slave sampling one last edge inside the violation may latch a bit that was never meant for it.
"Reset and abort are the same operation with different names." They are opposites in the one respect that matters: abort is orderly because the master still exists, and reset is instant because it does not. A design that makes reset orderly leaves a slave selected by a dead master; one that makes abort instant violates the datasheet on every timeout.
"An abort that interrupted nothing does not need acknowledging." A driver waiting for the acknowledgement hangs on exactly the one case where the abort was unnecessary. The easy case needs the same handshake as the hard one.
"abort_req is a pulse from software, so an edge detector is redundant." It is a level by the time it reaches this block, because the register interface holds it until acknowledged. Without the edge qualifier the same request is serviced twice, and the second pass zeroes the partial bit count while leaving the truncation flag set.
"A truncated word is lost anyway, so reporting how many bits arrived is pointless." It distinguishes "the abort was harmless" from "seven of eight bits were discarded", which is the difference between retrying and investigating. And the count is what makes the truncation flag trustworthy: a flag with no count cannot be checked for self-consistency.
14. Reason It Through
Why is abort_out a single cycle rather than a level held for the duration?
Because 13.7 latches the decision by changing state, so one cycle is sufficient — and a level would still be asserted when the machine passed back through LEAD on the next transaction, aborting that one too. The abort would then look like a permanent failure rather than a single event.
What exactly goes wrong if the abort request is not edge-qualified?
The level is still asserted when the abort completes, because the register interface clears it on the acknowledgement one cycle later. The machine returns to RUN, sees the level, and services the same request again — this time with the bus idle, so it takes the "nothing to abort" path, which sets aborted and zeroes abort_bits while rx_trunc stays set. The report then claims a word was truncated after zero bits.
Why must the truncation flag not be set by an abort during LEAD or GAP?
Because nothing was lost. An abort before the first edge or after the last discards no captured bits, and flagging it teaches the driver that the flag does not mean what it says — after which a genuine truncation is ignored. A flag that fires on harmless events is worse than no flag.
Why is the reset check performed between clock edges rather than on one?
Because the whole point is that the release must not need an edge. A reset that asserts while the clock is stopped — a PLL unlock, a power-domain shutdown, a watchdog that gates the clock as part of its recovery — is precisely the case where a synchronous reset fails, and it is also the case where releasing the bus matters most.
A driver's timeout handler resets the controller and the slave never responds again. Why is aborting the better first response?
Because reset leaves the slave with a truncated transaction and — if the select flops are synchronously reset and the clock has been gated — with the select still asserted. An abort ends the transfer the way the datasheet allows: the hold is paid, the select is released, and the gap is honoured, so the slave sees a clean end of transaction and resynchronises. Reset remains the correct escalation if the abort does not complete, but making it the first response means every timeout is handled with the most violent tool available.
15. Understanding Check
16. Summary
Abort and reset are opposites. An abort is a request from a master that still exists, so it must stop the way the datasheet allows — halt the clock, pay the hold, release the select, pay the gap. A reset means the master is gone, so it must release the select immediately and asynchronously, because a slave watching a dead select line stays mid-transaction indefinitely.
The abort input belongs in the block that owns the pins. The attempt to abort from outside failed because Chapter 13.7 latches hold at frame request and does not consult the live input mid-frame: the machine went to HOLD with the select still low, waiting for a request the aborting logic was refusing. The generalisation is that a block which latches a control input cannot be steered by that input afterwards.
The abort publishes four things: the partial bit count, because a truncated frame raises no valid pulse; a sticky truncation flag, set only when bits were actually mid-flight, because flagging a harmless abort trains the driver to ignore it; a flush, so the queued word does not start the next transaction by itself; and an acknowledgement even when there was nothing to abort, so a waiting driver cannot hang on the easy case.
abort_out is one cycle, because 13.7 latches the decision by changing state and a level would abort the next transaction as the machine passed back through LEAD.
The request is edge-qualified, because the level is still asserted when the abort completes. Without that, the same request is serviced twice and the second pass zeroes the partial count while leaving the truncation flag set — a report claiming a word was truncated after zero bits. That is the second bug in this module caused by a level outliving the event it described, the first being Chapter 13.9's frame_done.
Reset is checked between clock edges, because the release must not need one — and the case where it fails is exactly the case where releasing the bus matters most.
The abort is verified at every bit position in a frame, and every abort is followed by a full recovery transaction, because a master that stops cleanly and wedges is no better than one that stops dirtily — and the abort path is the least exercised logic in the design.
17. What Comes Next
Every block exists, and every block has been verified alone.
Chapter 13.11 — Synthesizable Master Architecture and RTL Review assembles them, adds the register interface software actually sees, and closes with the review that would gate the design. It also runs the test that found the one bug none of the unit testbenches could: a transfer checked against a slave that sees nothing but pins.
Continue learning
Related tutorials
- Related topic
Partial and Aborted Transactions
Chip select deasserting mid-frame: why an abort is legitimate, why an empty frame and a partial word need different responses, and the guard that classifies a frame's ending so a partial word never commits.
- Related topic
Transfer Width, Partial Frames, and Aborts
Five different faults produce identical pins, so the slave needs one policy for all of them: report and let software decide. Why complete words and partials must travel on separate channels, why the receive path is cleared at the transaction start rather than its end, and a framing block verified in three HDLs across 34 words and 9 partials.
- Related topic
Reset Behaviour and Safe Idle
A slave's reset lands mid-transaction, and the fact it most needs is destroyed by the reset that created the situation because the synchronisers reset to deselected. Recovering that fact exactly from timing instead, why refusing one transaction beats guessing, and a safe-idle gate verified in three HDLs.
- Related topic
Launch and Sample Edges
One edge of each bit time places a bit on the wire, the other captures it, and they must never be the same edge. Why the separation is forced, why it buys half a period, and how RTL maps physical edges onto those roles.
