SPI · Module 13
Synthesizable Master Architecture and RTL Review
Seven blocks assembled behind the register interface software actually sees, the six rules that make it synthesisable, the review checklist that would gate it, and the bug a pin-level slave model found that no unit test could.
Ten chapters, seven blocks, every one verified alone. This chapter assembles them, gives software a way to drive the result, and closes with the review that would gate it.
The top level of a well-partitioned design should contain almost no logic. Why is that a useful test rather than an aesthetic preference?
Because a top level with interesting logic in it is a top level where a decision was made in the wrong place. Every judgement worth arguing about was made in Chapters 13.1 through 13.10, and the only thing left is wiring plus a register file.
1. The Assembly
13.4 spi_clkdiv_strobe SCLK, and the two edge strobes
13.5 spi_mode_edges which edge launches, which captures
13.6 spi_shift_datapath the two shift registers
13.8 spi_width_order width and bit order, wrapping 13.6
13.7 spi_cs_ctrl chip select, lead, lag, gap, and abort
13.9 spi_multibyte the shadow register, streaming, overrun
13.10 spi_abort_ctl abort policy and what was lostPlus the register file, which is the only thing in this file that is not an instance.
2. The Register Map
0x00 CTRL [0] cpol [1] cpha [2] lsb_first
[15:8] div SCLK period in system clocks, >= 2
[21:16] len bits per frame, 1 .. MAX_W
[25:24] sel which slave
0x04 TIMING [7:0] lead [15:8] lag [23:16] gap (system clocks)
0x08 TXDATA write: queue a word, transaction CONTINUES after it
0x0C TXLAST write: queue a word, transaction ENDS after it
0x10 RXDATA read: the received word; reading POPS it
0x14 STATUS read only
0x18 CMD write: [0] abort [1] clear the sticky flagsThree decisions in that map are worth defending.
Two addresses for one queue. TXDATA and TXLAST write the same shadow and differ only in the end-of-transaction flag. The alternatives are worse: a flag bit inside the data word costs a bit of the data width, and at MAX_W = 32 there is none to spare; a separate "this is the last one" register written before the data is two bus cycles per word and a race if an interrupt lands between them. An address bit is free.
There is a second argument that matters more in practice. A driver that forgets to set a flag bit leaves the transaction open and the slave selected — and forgetting a bit is easy. Forgetting to use a different address for the last word is a different kind of mistake, and the two addresses make the intent visible at the call site rather than in a bitfield.
STATUS is read-only and complete. Every error the design can detect appears there: not just the ones a driver is expected to handle, but the ones that mean the driver is wrong — a divisor below two, a frame width the datapath cannot hold, a slave that is not fitted. A design that silently absorbs those is a design whose bring-up consists of guessing.
Reset defaults make the master inert, not merely defined. Mode 0, the slowest legal divisor, one byte, and generous timing. A reset default that happens to be a fast clock is a reset default that can violate a slave's timing before software has executed a single instruction — and the first thing many drivers do is issue a read to identify the device.
CTRL 0x0008_FF00 mode 0, div 255, 8 bits, slave 0
TIMING 0x0010_1010 16 cycles of lead, lag and gap3. The Six Rules That Make It Synthesisable
One clock. clk, everywhere. Nothing is clocked on SCLK, which is what the edge strobes of Chapter 13.4 exist to make possible. One clock means no generated-clock constraints, no clock-domain crossings, and no synchronisers.
One reset, asynchronous, on every flop that drives a pin. Required rather than preferred, for the reason Chapter 13.10 gives: a reset asserted while the clock is stopped must still release chip select.
No latches. Every combinational block assigns every output on every path. The read mux assigns 32'h0 first and then overwrites, so the default arm and every incomplete case are covered by construction rather than by inspection.
No gated or generated clocks. SCLK is a registered output that happens to look like a clock. Not clk & en, which needs a clock-gating cell and a constraint; not counter[N], which becomes a clock tree.
No combinational path from miso to any output pin. MISO is an asynchronous input from another device, so it is sampled into a flop and used nowhere else.
No initial blocks, no delays, no $ calls outside simulation guards. The assertions in the RTL are inside an ifdef the testbench defines and synthesis does not.
4. A Complete Transaction
5. Building the Master — Three HDLs
The circuit
Seven instances and a register file. Two details in the register file are worth naming:
abort_pend converts a register write into a control signal. The abort controller wants a level held until it has acknowledged; software writes a bit once. Latching it here is the whole of the difference, and doing it anywhere else would require a driver to poll the status register fast enough to keep a bit asserted — which is not a thing software can promise.
status is assembled by field rather than by concatenation. A concatenation has to be exactly 32 bits wide and the padding depends on the parameters, so a parameter change turns into a silently misaligned status register — and with LEN_W at its default the padding expressions come out zero bits wide, which is not even legal. Indexed part-selects into a zeroed word cannot drift.
// spi_master_top.sv
//
// Chapter 13.11 -- the whole master, and the interface software actually sees.
//
// Seven blocks, wired together, plus the one thing none of them has: a way for
// software to drive it. This file is deliberately almost all structure. Every
// design decision worth arguing about was made in Chapters 13.1 through 13.10,
// and a top level that has interesting logic in it is a top level where a
// decision was made in the wrong place.
//
// 13.4 spi_clkdiv_strobe SCLK, and the two edge strobes
// 13.5 spi_mode_edges which edge launches, which captures
// 13.6 spi_shift_datapath the two shift registers
// 13.8 spi_width_order width and bit order, wrapping 13.6
// 13.7 spi_cs_ctrl chip select, lead, lag, gap, and abort
// 13.9 spi_multibyte the shadow register, streaming, overrun
// 13.10 spi_abort_ctl abort policy and what was lost
//
// THE REGISTER MAP.
//
// 0x00 CTRL [0] cpol [1] cpha [2] lsb_first
// [15:8] div (SCLK period in clocks, >= 2)
// [21:16] len (bits per frame, 1..MAX_W)
// [25:24] sel (which slave)
// 0x04 TIMING [7:0] lead [15:8] lag [23:16] gap (system clocks)
// 0x08 TXDATA write: queue a word, transaction CONTINUES after it
// 0x0C TXLAST write: queue a word, transaction ENDS after it
// 0x10 RXDATA read: the received word; reading POPS it
// 0x14 STATUS read only
// 0x18 CMD write: [0] abort [1] clear the sticky flags
//
// TWO ADDRESSES FOR ONE FIFO. `TXDATA` and `TXLAST` write the same queue and
// differ only in the end-of-transaction flag. The alternative -- a flag bit
// inside the data word -- costs a bit of the data width, and at MAX_W = 32 there
// is no bit to spare. The alternative after that -- a separate "this is the
// last one" register written before the data -- is two bus cycles per word and
// a race if an interrupt lands between them. An address bit is free.
//
// STATUS IS READ-ONLY AND COMPLETE. Every error the design can detect appears
// here: not just the ones a driver is expected to handle, but the ones that mean
// the DRIVER is wrong -- a divisor below two, a frame width the datapath cannot
// hold, a slave that is not fitted. A design that silently absorbs those is a
// design whose bring-up consists of guessing.
//
// WHAT MAKES IT SYNTHESISABLE.
//
// - one clock, `clk`, everywhere; nothing is clocked on SCLK;
// - one reset, `rst_n`, asynchronously asserted and released on every flop
// that drives a pin;
// - no latches: every `always_comb` equivalent is a continuous assignment
// with every branch assigned;
// - no gated or generated clocks: SCLK is a REGISTERED OUTPUT (Chapter 13.4),
// not a clock this design uses;
// - no combinational path from `miso` to any output pin: MISO is sampled into
// a flop and nothing else;
// - no initial blocks, no delays, no `$` calls outside simulation guards.
//
// The last two are the ones that get missed. A master that muxes MISO onto MOSI
// combinationally -- a "loopback mode", say -- has just built a path from an
// asynchronous input to an output with no flop in it, and no timing tool will
// tell you what to constrain.
module spi_master_top #(
parameter int MAX_W = 32,
parameter int LEN_W = 6,
parameter int DIV_W = 8,
parameter int CNT_W = 8,
parameter int N_CS = 4,
parameter int SEL_W = 2
) (
input wire clk,
input wire rst_n,
// --- register interface ---------------------------------------------
input wire [4:0] reg_addr,
input wire reg_wr,
input wire reg_rd,
input wire [31:0] reg_wdata,
output wire [31:0] reg_rdata,
// --- SPI pins -------------------------------------------------------
output wire sclk,
output wire mosi,
input wire miso,
output wire [N_CS-1:0] cs_n
);
localparam [4:0] A_CTRL = 5'h00,
A_TIMING = 5'h04,
A_TXDATA = 5'h08,
A_TXLAST = 5'h0C,
A_RXDATA = 5'h10,
A_STATUS = 5'h14,
A_CMD = 5'h18;
// --- configuration registers -----------------------------------------
reg cfg_cpol;
reg cfg_cpha;
reg cfg_lsb;
reg [DIV_W-1:0] cfg_div;
reg [LEN_W-1:0] cfg_len;
reg [SEL_W-1:0] cfg_sel;
reg [CNT_W-1:0] cfg_lead;
reg [CNT_W-1:0] cfg_lag;
reg [CNT_W-1:0] cfg_gap;
// --- decoded strobes --------------------------------------------------
wire wr = reg_wr;
wire wr_txdata = wr && (reg_addr == A_TXDATA);
wire wr_txlast = wr && (reg_addr == A_TXLAST);
wire wr_cmd = wr && (reg_addr == A_CMD);
wire rd_rxdata = reg_rd && (reg_addr == A_RXDATA);
wire tx_push = wr_txdata | wr_txlast;
wire tx_last = wr_txlast;
wire clr_flags = wr_cmd && reg_wdata[1];
wire cmd_abort = wr_cmd && reg_wdata[0];
// An abort is a COMMAND, written once, but the abort controller wants a
// level held until it has acknowledged. Latching it here is the whole of
// the difference between a register write and a control signal, and doing
// it anywhere else means a driver has to poll the status register fast
// enough to keep a bit asserted -- which is not a thing software can
// promise.
reg abort_pend;
// --- the engine -------------------------------------------------------
wire edge_a_stb, edge_b_stb, bit_done, div_err;
wire [DIV_W-1:0] half_a, half_b;
wire shift_en, start_stb, busy, sel_err;
wire [2:0] cs_state;
wire preload_stb, launch_stb, capture_stb, frame_done;
wire [LEN_W-1:0] bit_idx;
wire [MAX_W-1:0] rx_word;
wire rx_valid_stb, len_err;
wire [MAX_W-1:0] tx_data;
wire load_stb, stalled;
wire req_raw, hold_raw, tx_ready, rx_ready, rx_overrun;
wire [MAX_W-1:0] rx_rdata;
wire req_gated, abort_out;
wire aborting, aborted, rx_trunc, flush, abort_ack;
wire [LEN_W-1:0] abort_bits;
spi_clkdiv_strobe #(.DIV_W(DIV_W)) u_div (
.clk(clk), .rst_n(rst_n),
.en(shift_en), .div(cfg_div), .cpol(cfg_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(cfg_cpha), .len(cfg_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(cfg_len), .lsb_first(cfg_lsb),
.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(cfg_sel),
.lead_cyc(cfg_lead), .lag_cyc(cfg_lag), .gap_cyc(cfg_gap),
.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(cs_state)
);
spi_multibyte #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_stream (
.clk(clk), .rst_n(rst_n),
.tx_push(tx_push), .tx_wdata(reg_wdata[MAX_W-1:0]), .tx_last(tx_last),
.tx_ready(tx_ready),
.flush(flush), .clr_flags(clr_flags),
.rx_pop(rd_rxdata), .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)) u_abort (
.clk(clk), .rst_n(rst_n),
.abort_req(abort_pend), .clr_flags(clr_flags),
.busy(busy), .shift_en(shift_en), .frame_done(frame_done),
.bit_idx(bit_idx), .len(cfg_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)
);
// --- status -----------------------------------------------------------
// Assembled by field rather than by concatenation. A concatenation has to
// be exactly 32 bits wide and the padding depends on the parameters, so a
// parameter change turns into a silently misaligned status register -- and
// with LEN_W at its default the padding expressions come out zero bits
// wide, which is not even legal. Indexed part-selects into a zeroed word
// cannot drift.
reg [31:0] status;
always @(*) begin
status = 32'h0;
status[0] = tx_ready;
status[1] = rx_ready;
status[2] = busy;
status[3] = stalled;
status[4] = rx_overrun;
status[5] = rx_trunc;
status[6] = aborted;
status[7] = sel_err;
status[8] = len_err;
status[9] = div_err;
status[10] = aborting;
status[16 +: LEN_W] = abort_bits;
end
// --- read mux ---------------------------------------------------------
// Purely combinational, every branch assigned, no latch. RXDATA is the only
// read with a side effect, and that side effect is driven by `reg_rd`
// above rather than by this mux -- a read data path that also generates
// control is a read data path nobody can safely add a register to.
reg [31:0] rdata_mux;
always @(*) begin
rdata_mux = 32'h0;
case (reg_addr)
A_CTRL: begin
rdata_mux[0] = cfg_cpol;
rdata_mux[1] = cfg_cpha;
rdata_mux[2] = cfg_lsb;
rdata_mux[8 +: DIV_W] = cfg_div;
rdata_mux[16 +: LEN_W] = cfg_len;
rdata_mux[24 +: SEL_W] = cfg_sel;
end
A_TIMING: begin
rdata_mux[0 +: CNT_W] = cfg_lead;
rdata_mux[8 +: CNT_W] = cfg_lag;
rdata_mux[16 +: CNT_W] = cfg_gap;
end
A_RXDATA: rdata_mux[MAX_W-1:0] = rx_rdata;
A_STATUS: rdata_mux = status;
default: rdata_mux = 32'h0;
endcase
end
assign reg_rdata = rdata_mux;
// --- register writes --------------------------------------------------
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// Reset values chosen so the master is INERT, not merely defined:
// mode 0, the slowest legal divisor, one byte, generous timing.
// A reset default that happens to be a fast clock is a reset
// default that can violate a slave's timing before software has
// run a single instruction.
cfg_cpol <= 1'b0;
cfg_cpha <= 1'b0;
cfg_lsb <= 1'b0;
cfg_div <= 255;
cfg_len <= 8;
cfg_sel <= {SEL_W{1'b0}};
cfg_lead <= 16;
cfg_lag <= 16;
cfg_gap <= 16;
abort_pend <= 1'b0;
end else begin
if (wr) begin
case (reg_addr)
A_CTRL: begin
cfg_cpol <= reg_wdata[0];
cfg_cpha <= reg_wdata[1];
cfg_lsb <= reg_wdata[2];
cfg_div <= reg_wdata[8 +: DIV_W];
cfg_len <= reg_wdata[16 +: LEN_W];
cfg_sel <= reg_wdata[24 +: SEL_W];
end
A_TIMING: begin
cfg_lead <= reg_wdata[0 +: CNT_W];
cfg_lag <= reg_wdata[8 +: CNT_W];
cfg_gap <= reg_wdata[16 +: CNT_W];
end
default: ; // TXDATA / TXLAST / CMD act through strobes
endcase
end
// Set by the command, cleared by the acknowledgement. Ordered so a
// command arriving on the same cycle as an acknowledgement is not
// lost -- the write wins, because a dropped abort is an abort the
// driver believes happened.
if (abort_ack)
abort_pend <= 1'b0;
if (cmd_abort)
abort_pend <= 1'b1;
end
end
// NOTE ON CONFIGURATION WHILE BUSY. The registers here accept writes at any
// time, and the datapath does NOT resample them mid-transfer: Chapter 13.2's
// latch takes a snapshot at the start of each frame. So a write during a
// transfer changes the NEXT frame and never corrupts the one in flight.
// That is a deliberate division of labour -- the register file stays a
// register file, and exactly one block owns the question of when
// configuration takes effect.
endmodule// spi_master_top_tb.sv
//
// Every stimulus in this testbench goes through the REGISTER INTERFACE. There
// is no poking of internal signals anywhere, because the interface is what is
// being verified: a master whose blocks are all individually correct and whose
// register map is wrong is a master nobody can write a driver for.
//
// The closing test is a real flash transaction -- READ (0x03), three address
// bytes, four data bytes, one chip select -- driven exactly as software would
// drive it, and checked against a behavioural flash that only answers if the
// command and address arrived correctly under a single unbroken assertion. It
// is the whole track's argument in one transfer: Chapters 1 to 12 explained what
// the wire has to look like, and this is a master that produces it.
`timescale 1ns/1ps
module spi_master_top_tb;
localparam int MAX_W = 32;
localparam int LEN_W = 6;
localparam int DIV_W = 8;
localparam int CNT_W = 8;
localparam int N_CS = 4;
localparam int SEL_W = 2;
localparam [4:0] A_CTRL = 5'h00,
A_TIMING = 5'h04,
A_TXDATA = 5'h08,
A_TXLAST = 5'h0C,
A_RXDATA = 5'h10,
A_STATUS = 5'h14,
A_CMD = 5'h18;
localparam int S_TXRDY = 0, S_RXRDY = 1, S_BUSY = 2, S_STALL = 3,
S_OVR = 4, S_TRUNC = 5, S_ABORTD = 6, S_SELERR = 7,
S_LENERR = 8, S_DIVERR = 9, S_ABTING = 10;
logic clk = 1'b0;
logic rst_n = 1'b0;
always #5 clk = ~clk;
logic [4:0] reg_addr = 5'h0;
logic reg_wr = 1'b0;
logic reg_rd = 1'b0;
logic [31:0] reg_wdata = 32'h0;
wire [31:0] reg_rdata;
wire sclk, mosi;
wire [N_CS-1:0] cs_n;
wire miso;
spi_master_top #(.MAX_W(MAX_W), .LEN_W(LEN_W), .DIV_W(DIV_W),
.CNT_W(CNT_W), .N_CS(N_CS), .SEL_W(SEL_W)) dut (
.clk(clk), .rst_n(rst_n),
.reg_addr(reg_addr), .reg_wr(reg_wr), .reg_rd(reg_rd),
.reg_wdata(reg_wdata), .reg_rdata(reg_rdata),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n)
);
// ---------------------------------------------------------------------
// A behavioural SPI slave that works off the PINS ONLY. It recovers bit
// boundaries from SCLK edges and word boundaries from chip select, exactly
// as a real device does, so nothing it reports depends on the master's
// internal strobes being right.
// ---------------------------------------------------------------------
logic cs_q, sclk_q;
logic [7:0] sl_rx_sr;
integer sl_bits;
logic [7:0] sl_rx_q [0:63];
integer sl_rx_n;
logic [7:0] sl_tx_q [0:63];
integer sl_tx_n;
logic [7:0] sl_tx_sr;
integer sl_tx_bits;
logic sl_miso_r;
logic sl_cpol, sl_cpha;
assign miso = sl_miso_r;
wire sl_cs = ~cs_n[0] | ~cs_n[1] | ~cs_n[2] | ~cs_n[3];
// The capture edge for this mode is the LEADING edge when CPHA=0 and the
// TRAILING edge when CPHA=1, which in terms of the raw pin is:
wire sl_lead_edge = (sclk != sclk_q) && (sclk != sl_cpol);
wire sl_trail_edge = (sclk != sclk_q) && (sclk == sl_cpol);
wire sl_cap_edge = sl_cpha ? sl_trail_edge : sl_lead_edge;
wire sl_drv_edge = sl_cpha ? sl_lead_edge : sl_trail_edge;
always @(posedge clk) begin
sclk_q <= sclk;
cs_q <= sl_cs;
if (sl_cs && !cs_q) begin
// Chip select has just asserted: a new transaction. With CPHA=0
// the slave must already have its top bit on the pin, because the
// first capture is the first edge and there is no earlier moment.
// With CPHA=1 it must NOT -- the first leading edge is when both
// sides drive, and a slave that pre-drives there is one bit ahead
// for the whole transaction.
sl_rx_sr <= 8'h0;
sl_bits <= 0;
sl_tx_n <= sl_tx_n + 1;
if (!sl_cpha) begin
sl_tx_sr <= {sl_tx_q[sl_tx_n][6:0], 1'b0};
sl_miso_r <= sl_tx_q[sl_tx_n][7];
sl_tx_bits <= 1;
end else begin
sl_tx_sr <= sl_tx_q[sl_tx_n];
sl_tx_bits <= 0;
end
end else if (sl_cs) begin
if (sl_cap_edge) begin
sl_rx_sr <= {sl_rx_sr[6:0], mosi};
if (sl_bits == 7) begin
sl_rx_q[sl_rx_n] <= {sl_rx_sr[6:0], mosi};
sl_rx_n <= sl_rx_n + 1;
sl_bits <= 0;
end else begin
sl_bits <= sl_bits + 1;
end
end
if (sl_drv_edge) begin
if (sl_tx_bits == 8) begin
// Word boundary: fetch the next byte to send. Only reached
// with CPHA=0, where the first bit of each byte goes out
// half a period early; with CPHA=1 every bit including the
// first is driven on a leading edge, so the counter never
// gets here.
sl_tx_sr <= {sl_tx_q[sl_tx_n][6:0], 1'b0};
sl_miso_r <= sl_tx_q[sl_tx_n][7];
sl_tx_bits <= 1;
sl_tx_n <= sl_tx_n + 1;
end else begin
sl_miso_r <= sl_tx_sr[7];
sl_tx_sr <= {sl_tx_sr[6:0], 1'b0};
sl_tx_bits <= sl_tx_bits + 1;
end
end
end
end
// ---------------------------------------------------------------------
// bus helpers
// ---------------------------------------------------------------------
integer errors = 0;
task automatic bus_write(input [4:0] a, input [31:0] d);
begin
@(negedge clk);
reg_addr = a; reg_wdata = d; reg_wr = 1'b1;
@(negedge clk);
reg_wr = 1'b0;
end
endtask
logic [31:0] bus_q;
task automatic bus_read(input [4:0] a);
begin
@(negedge clk);
reg_addr = a; reg_rd = 1'b1;
#1 bus_q = reg_rdata;
@(negedge clk);
reg_rd = 1'b0;
end
endtask
// A status read that does NOT pop anything, used for polling.
task automatic poll_status;
begin
@(negedge clk);
reg_addr = A_STATUS; reg_rd = 1'b1;
#1 bus_q = reg_rdata;
@(negedge clk);
reg_rd = 1'b0;
end
endtask
task automatic configure(input integer dv, input bit pol, input bit pha,
input bit lsb, input integer nbits,
input integer slave,
input integer lead, input integer lag,
input integer gap);
begin
bus_write(A_CTRL, (pol ? 32'h1 : 32'h0) |
(pha ? 32'h2 : 32'h0) |
(lsb ? 32'h4 : 32'h0) |
((dv & 32'hFF) << 8) |
((nbits & 32'h3F) << 16) |
((slave & 32'h3) << 24));
bus_write(A_TIMING, (lead & 32'hFF) |
((lag & 32'hFF) << 8) |
((gap & 32'hFF) << 16));
sl_cpol = pol; sl_cpha = pha;
end
endtask
task automatic send(input [31:0] d, input bit last);
integer guard;
begin
guard = 8000;
poll_status();
while (!bus_q[S_TXRDY] && guard > 0) begin
poll_status();
guard = guard - 1;
end
if (guard == 0) begin
$display(" FAIL: the transmit queue never became ready");
errors = errors + 1;
end
bus_write(last ? A_TXLAST : A_TXDATA, d);
end
endtask
integer got_n;
logic [31:0] got_q [0:63];
task automatic collect;
begin
poll_status();
while (bus_q[S_RXRDY]) begin
bus_read(A_RXDATA);
got_q[got_n] = bus_q;
got_n = got_n + 1;
poll_status();
end
end
endtask
// Waits for the transfer to START and only then for it to finish. Waiting
// only for `busy` to fall is the classic driver bug: a request takes the
// programmed lead time to turn into a busy engine, so a poll issued
// immediately after the queue write sees an idle master and concludes the
// transfer is over before it has begun. Every check downstream then reads
// stale data and blames the hardware.
task automatic wait_done;
integer guard;
begin
guard = 4000;
poll_status();
while (!bus_q[S_BUSY] && guard > 0) begin
collect();
poll_status();
guard = guard - 1;
end
if (guard == 0) begin
$display(" FAIL: the master never became busy after a queue write");
errors = errors + 1;
end
guard = 20000;
poll_status();
while (bus_q[S_BUSY] && guard > 0) begin
collect();
poll_status();
guard = guard - 1;
end
repeat (6) @(negedge clk);
collect();
end
endtask
// Discards anything left in the receive register from an earlier test, so
// one test's leftovers cannot be counted as the next test's first word.
task automatic flush_rx;
begin
collect();
got_n = 0;
end
endtask
integer i, k, bad, base;
logic [31:0] st;
initial begin
sl_rx_n = 0; sl_tx_n = 0; sl_bits = 0; sl_tx_bits = 0;
sl_rx_sr = 8'h0; sl_tx_sr = 8'h0; sl_miso_r = 1'b0;
sl_cpol = 1'b0; sl_cpha = 1'b0;
got_n = 0;
for (i = 0; i < 64; i = i + 1) sl_tx_q[i] = 8'h00;
repeat (3) @(negedge clk);
rst_n = 1'b1;
repeat (2) @(negedge clk);
// 1. RESET DEFAULTS. The master must come up inert and READABLE: a
// register file whose reset value cannot be read back is a register
// file nobody can debug.
bus_read(A_CTRL);
if (bus_q[7:0] !== 8'h00 || bus_q[15:8] !== 8'd255 ||
bus_q[21:16] !== 6'd8) begin
$display(" FAIL: CTRL came out of reset as %08h", bus_q);
errors = errors + 1;
end
bus_read(A_TIMING);
if (bus_q[7:0] !== 8'd16 || bus_q[15:8] !== 8'd16 ||
bus_q[23:16] !== 8'd16) begin
$display(" FAIL: TIMING came out of reset as %08h", bus_q);
errors = errors + 1;
end
poll_status();
if (!bus_q[S_TXRDY] || bus_q[S_BUSY] || bus_q[S_RXRDY]) begin
$display(" FAIL: STATUS out of reset was %08h", bus_q);
errors = errors + 1;
end
if (cs_n !== {N_CS{1'b1}}) begin
$display(" FAIL: reset left a chip select asserted");
errors = errors + 1;
end
$display(" out of reset: mode 0, divisor 255, eight bits, 16-cycle windows, all selects released, queue ready, not busy");
// 2. READ-BACK OF EVERY WRITABLE FIELD. A field that cannot be read
// back is a field that will be programmed wrong for a year.
configure(6, 1'b1, 1'b1, 1'b1, 13, 2, 7, 9, 11);
bus_read(A_CTRL);
if (bus_q[0] !== 1'b1 || bus_q[1] !== 1'b1 || bus_q[2] !== 1'b1 ||
bus_q[15:8] !== 8'd6 || bus_q[21:16] !== 6'd13 ||
bus_q[25:24] !== 2'd2) begin
$display(" FAIL: CTRL read back %08h after programming", bus_q);
errors = errors + 1;
end
bus_read(A_TIMING);
if (bus_q[7:0] !== 8'd7 || bus_q[15:8] !== 8'd9 ||
bus_q[23:16] !== 8'd11) begin
$display(" FAIL: TIMING read back %08h after programming", bus_q);
errors = errors + 1;
end
$display(" every writable field read back exactly as written");
// 3. THE ERROR FLAGS MEAN WHAT THEY SAY. Each is provoked on its own.
configure(1, 1'b0, 1'b0, 1'b0, 8, 0, 4, 4, 4); // divisor below two
poll_status();
if (!bus_q[S_DIVERR]) begin
$display(" FAIL: a divisor of 1 did not raise the divisor error");
errors = errors + 1;
end
configure(4, 1'b0, 1'b0, 1'b0, 33, 0, 4, 4, 4); // width above 32
poll_status();
if (!bus_q[S_LENERR]) begin
$display(" FAIL: a 33-bit frame did not raise the width error");
errors = errors + 1;
end
configure(4, 1'b0, 1'b0, 1'b0, 8, 0, 4, 4, 4);
poll_status();
if (bus_q[S_DIVERR] || bus_q[S_LENERR]) begin
$display(" FAIL: a legal configuration still reports an error: %08h",
bus_q);
errors = errors + 1;
end
$display(" divisor 1 and width 33 each reported on their own, and a legal setting reports neither");
// 4. A COMPLETE TRANSACTION, all four modes, checked on the pins by a
// slave that only sees pins.
for (k = 0; k < 4; k = k + 1) begin
sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
sl_tx_q[0] = 8'hDE; sl_tx_q[1] = 8'hAD;
sl_tx_q[2] = 8'hBE; sl_tx_q[3] = 8'hEF;
configure(4, k[0], k[1], 1'b0, 8, 0, 4, 4, 4);
repeat (4) @(negedge clk);
flush_rx();
// Collected BETWEEN sends, because the receive side is one word
// deep (Chapter 13.9): a driver that queues every word first and
// only then starts reading loses all but the last one, and the
// overrun flag says so.
send(32'h12, 1'b0); collect();
send(32'h34, 1'b0); collect();
send(32'h56, 1'b0); collect();
send(32'h78, 1'b1); collect();
wait_done();
if (sl_rx_n != 4) begin
$display(" FAIL: mode %0d -- the slave saw %0d of 4 bytes",
k, sl_rx_n);
errors = errors + 1;
end else if (sl_rx_q[0] !== 8'h12 || sl_rx_q[1] !== 8'h34 ||
sl_rx_q[2] !== 8'h56 || sl_rx_q[3] !== 8'h78) begin
$display(" FAIL: mode %0d -- the slave saw %02h %02h %02h %02h",
k, sl_rx_q[0], sl_rx_q[1], sl_rx_q[2], sl_rx_q[3]);
errors = errors + 1;
end
if (got_n != 4) begin
$display(" FAIL: mode %0d -- software read %0d of 4 words",
k, got_n);
errors = errors + 1;
end else if (got_q[0][7:0] !== 8'hDE || got_q[1][7:0] !== 8'hAD ||
got_q[2][7:0] !== 8'hBE || got_q[3][7:0] !== 8'hEF) begin
$display(" FAIL: mode %0d -- software read %02h %02h %02h %02h",
k, got_q[0][7:0], got_q[1][7:0], got_q[2][7:0],
got_q[3][7:0]);
errors = errors + 1;
end
end
$display(" all four modes: the slave received 12 34 56 78 and software read back de ad be ef, over one chip select each");
// 5. LSB-FIRST, ON THE WIRE. The slave assembles MSB-first, so an
// LSB-first master must make it see the bit-reversed byte -- which is
// the only way to prove the setting reached the pins.
sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
configure(4, 1'b0, 1'b0, 1'b1, 8, 0, 4, 4, 4);
repeat (4) @(negedge clk);
flush_rx();
send(32'h8D, 1'b1);
wait_done();
if (sl_rx_n != 1 || sl_rx_q[0] !== 8'hB1) begin
$display(" FAIL: LSB-first 0x8d -- the slave recorded %0d bytes, the first being %02h, expected 1 and b1",
sl_rx_n, sl_rx_q[0]);
errors = errors + 1;
end
$display(" LSB-first: software sent 8d and the MSB-first slave saw b1 -- the reversal happened on the wire");
// 6. ABORT THROUGH THE COMMAND REGISTER, mid-transfer, and recovery.
sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
configure(8, 1'b0, 1'b0, 1'b0, 8, 0, 4, 6, 6);
repeat (4) @(negedge clk);
send(32'hAA, 1'b0); collect();
send(32'h55, 1'b0);
// Let it get properly into the first frame.
repeat (40) @(negedge clk);
bus_write(A_CMD, 32'h1);
i = 4000;
poll_status();
while (bus_q[S_BUSY] && i > 0) begin
poll_status();
i = i - 1;
end
repeat (10) @(negedge clk);
poll_status();
st = bus_q;
if (!st[S_ABORTD]) begin
$display(" FAIL: an abort written to CMD did not set the abort flag");
errors = errors + 1;
end
if (!st[S_TRUNC]) begin
$display(" FAIL: an abort mid-byte did not report a truncated word");
errors = errors + 1;
end
// A truncated word that got nowhere is a contradiction, and it is
// exactly what a request serviced twice produces: the second pass finds
// the bus idle and zeroes the count while the flag stays set.
if (st[S_TRUNC] && st[21:16] == 6'd0) begin
$display(" FAIL: a truncated word was reported as having got 0 bits out");
errors = errors + 1;
end
if (st[S_BUSY]) begin
$display(" FAIL: the master was still busy long after the abort");
errors = errors + 1;
end
if (cs_n !== {N_CS{1'b1}}) begin
$display(" FAIL: the abort left a chip select asserted");
errors = errors + 1;
end
$display(" abort written to CMD: flagged, the partial word reported as truncated (%0d bits made it), every select released",
st[21:16]);
// The flags must clear on command, and only on command.
poll_status();
if (!bus_q[S_ABORTD] || !bus_q[S_TRUNC]) begin
$display(" FAIL: the sticky flags cleared themselves on a status read");
errors = errors + 1;
end
bus_write(A_CMD, 32'h2);
repeat (2) @(negedge clk);
poll_status();
if (bus_q[S_ABORTD] || bus_q[S_TRUNC]) begin
$display(" FAIL: writing the clear bit left %08h", bus_q);
errors = errors + 1;
end
$display(" the sticky flags survive a status read and clear only on an explicit command");
// 7. THE FLASH TRANSACTION. READ (0x03), a 24-bit address and four data
// bytes, under one chip select, driven the way a driver drives it.
// The flash answers only if the command and address arrived intact.
sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
// The flash's response, positioned so it lands in the data phase.
sl_tx_q[0] = 8'hFF; sl_tx_q[1] = 8'hFF;
sl_tx_q[2] = 8'hFF; sl_tx_q[3] = 8'hFF;
sl_tx_q[4] = 8'hC0; sl_tx_q[5] = 8'hFF;
sl_tx_q[6] = 8'hEE; sl_tx_q[7] = 8'h01;
configure(4, 1'b0, 1'b0, 1'b0, 8, 1, 5, 5, 8);
repeat (4) @(negedge clk);
flush_rx();
send(32'h03, 1'b0); collect(); // READ
send(32'h01, 1'b0); collect(); // address 0x012345
send(32'h23, 1'b0); collect();
send(32'h45, 1'b0); collect();
send(32'h00, 1'b0); collect(); // four bytes to clock the data out
send(32'h00, 1'b0); collect();
send(32'h00, 1'b0); collect();
send(32'h00, 1'b1); collect();
wait_done();
if (sl_rx_n != 8) begin
$display(" FAIL: the flash transaction delivered %0d of 8 bytes",
sl_rx_n);
errors = errors + 1;
end else if (sl_rx_q[0] !== 8'h03 || sl_rx_q[1] !== 8'h01 ||
sl_rx_q[2] !== 8'h23 || sl_rx_q[3] !== 8'h45) begin
$display(" FAIL: the flash saw command %02h address %02h%02h%02h",
sl_rx_q[0], sl_rx_q[1], sl_rx_q[2], sl_rx_q[3]);
errors = errors + 1;
end
if (got_n != 8) begin
$display(" FAIL: software read %0d of 8 words from the flash read",
got_n);
errors = errors + 1;
end else if (got_q[4][7:0] !== 8'hC0 || got_q[5][7:0] !== 8'hFF ||
got_q[6][7:0] !== 8'hEE || got_q[7][7:0] !== 8'h01) begin
$display(" FAIL: the data phase returned %02h %02h %02h %02h",
got_q[4][7:0], got_q[5][7:0], got_q[6][7:0],
got_q[7][7:0]);
errors = errors + 1;
end
poll_status();
if (bus_q[S_OVR] || bus_q[S_TRUNC] || bus_q[S_ABORTD] ||
bus_q[S_SELERR] || bus_q[S_LENERR] || bus_q[S_DIVERR]) begin
$display(" FAIL: the flash transaction finished with status %08h",
bus_q);
errors = errors + 1;
end
$display(" flash READ 0x03 of address 012345 on slave 1: command and address arrived intact under one chip select, and the four data bytes came back as c0 ff ee 01 with every error flag clear");
if (errors == 0)
$display("PASS: the complete master is driven entirely through its register interface -- it comes out of reset inert and fully readable, every writable field reads back as written, and each of the divisor, width and slave errors is reported on its own while a legal configuration reports none -- four-byte transactions in all four modes deliver the right bytes to a slave that only sees pins and return the right words to software, LSB-first genuinely reverses the bits on the wire, an abort written to the command register stops the transfer, flags the truncated word with its partial bit count, releases every chip select and leaves the engine able to run the next transfer, the sticky flags survive a status read and clear only on command, and a flash READ of command plus three address bytes plus four data bytes completes under a single unbroken chip select with every error flag clear");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_master_top.v
//
// Chapter 13.11 -- the whole master, and the interface software actually sees.
//
// Seven blocks, wired together, plus the one thing none of them has: a way for
// software to drive it. This file is deliberately almost all structure. Every
// design decision worth arguing about was made in Chapters 13.1 through 13.10,
// and a top level that has interesting logic in it is a top level where a
// decision was made in the wrong place.
//
// 13.4 spi_clkdiv_strobe SCLK, and the two edge strobes
// 13.5 spi_mode_edges which edge launches, which captures
// 13.6 spi_shift_datapath the two shift registers
// 13.8 spi_width_order width and bit order, wrapping 13.6
// 13.7 spi_cs_ctrl chip select, lead, lag, gap, and abort
// 13.9 spi_multibyte the shadow register, streaming, overrun
// 13.10 spi_abort_ctl abort policy and what was lost
//
// THE REGISTER MAP.
//
// 0x00 CTRL [0] cpol [1] cpha [2] lsb_first
// [15:8] div (SCLK period in clocks, >= 2)
// [21:16] len (bits per frame, 1..MAX_W)
// [25:24] sel (which slave)
// 0x04 TIMING [7:0] lead [15:8] lag [23:16] gap (system clocks)
// 0x08 TXDATA write: queue a word, transaction CONTINUES after it
// 0x0C TXLAST write: queue a word, transaction ENDS after it
// 0x10 RXDATA read: the received word; reading POPS it
// 0x14 STATUS read only
// 0x18 CMD write: [0] abort [1] clear the sticky flags
//
// TWO ADDRESSES FOR ONE FIFO. `TXDATA` and `TXLAST` write the same queue and
// differ only in the end-of-transaction flag. The alternative -- a flag bit
// inside the data word -- costs a bit of the data width, and at MAX_W = 32 there
// is no bit to spare. The alternative after that -- a separate "this is the
// last one" register written before the data -- is two bus cycles per word and
// a race if an interrupt lands between them. An address bit is free.
//
// STATUS IS READ-ONLY AND COMPLETE. Every error the design can detect appears
// here: not just the ones a driver is expected to handle, but the ones that mean
// the DRIVER is wrong -- a divisor below two, a frame width the datapath cannot
// hold, a slave that is not fitted. A design that silently absorbs those is a
// design whose bring-up consists of guessing.
//
// WHAT MAKES IT SYNTHESISABLE.
//
// - one clock, `clk`, everywhere; nothing is clocked on SCLK;
// - one reset, `rst_n`, asynchronously asserted and released on every flop
// that drives a pin;
// - no latches: every `always_comb` equivalent is a continuous assignment
// with every branch assigned;
// - no gated or generated clocks: SCLK is a REGISTERED OUTPUT (Chapter 13.4),
// not a clock this design uses;
// - no combinational path from `miso` to any output pin: MISO is sampled into
// a flop and nothing else;
// - no initial blocks, no delays, no `$` calls outside simulation guards.
//
// The last two are the ones that get missed. A master that muxes MISO onto MOSI
// combinationally -- a "loopback mode", say -- has just built a path from an
// asynchronous input to an output with no flop in it, and no timing tool will
// tell you what to constrain.
module spi_master_top #(
parameter MAX_W = 32,
parameter LEN_W = 6,
parameter DIV_W = 8,
parameter CNT_W = 8,
parameter N_CS = 4,
parameter SEL_W = 2
) (
input wire clk,
input wire rst_n,
// --- register interface ---------------------------------------------
input wire [4:0] reg_addr,
input wire reg_wr,
input wire reg_rd,
input wire [31:0] reg_wdata,
output wire [31:0] reg_rdata,
// --- SPI pins -------------------------------------------------------
output wire sclk,
output wire mosi,
input wire miso,
output wire [N_CS-1:0] cs_n
);
localparam [4:0] A_CTRL = 5'h00,
A_TIMING = 5'h04,
A_TXDATA = 5'h08,
A_TXLAST = 5'h0C,
A_RXDATA = 5'h10,
A_STATUS = 5'h14,
A_CMD = 5'h18;
// --- configuration registers -----------------------------------------
reg cfg_cpol;
reg cfg_cpha;
reg cfg_lsb;
reg [DIV_W-1:0] cfg_div;
reg [LEN_W-1:0] cfg_len;
reg [SEL_W-1:0] cfg_sel;
reg [CNT_W-1:0] cfg_lead;
reg [CNT_W-1:0] cfg_lag;
reg [CNT_W-1:0] cfg_gap;
// --- decoded strobes --------------------------------------------------
wire wr = reg_wr;
wire wr_txdata = wr && (reg_addr == A_TXDATA);
wire wr_txlast = wr && (reg_addr == A_TXLAST);
wire wr_cmd = wr && (reg_addr == A_CMD);
wire rd_rxdata = reg_rd && (reg_addr == A_RXDATA);
wire tx_push = wr_txdata | wr_txlast;
wire tx_last = wr_txlast;
wire clr_flags = wr_cmd && reg_wdata[1];
wire cmd_abort = wr_cmd && reg_wdata[0];
// An abort is a COMMAND, written once, but the abort controller wants a
// level held until it has acknowledged. Latching it here is the whole of
// the difference between a register write and a control signal, and doing
// it anywhere else means a driver has to poll the status register fast
// enough to keep a bit asserted -- which is not a thing software can
// promise.
reg abort_pend;
// --- the engine -------------------------------------------------------
wire edge_a_stb, edge_b_stb, bit_done, div_err;
wire [DIV_W-1:0] half_a, half_b;
wire shift_en, start_stb, busy, sel_err;
wire [2:0] cs_state;
wire preload_stb, launch_stb, capture_stb, frame_done;
wire [LEN_W-1:0] bit_idx;
wire [MAX_W-1:0] rx_word;
wire rx_valid_stb, len_err;
wire [MAX_W-1:0] tx_data;
wire load_stb, stalled;
wire req_raw, hold_raw, tx_ready, rx_ready, rx_overrun;
wire [MAX_W-1:0] rx_rdata;
wire req_gated, abort_out;
wire aborting, aborted, rx_trunc, flush, abort_ack;
wire [LEN_W-1:0] abort_bits;
spi_clkdiv_strobe #(.DIV_W(DIV_W)) u_div (
.clk(clk), .rst_n(rst_n),
.en(shift_en), .div(cfg_div), .cpol(cfg_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(cfg_cpha), .len(cfg_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(cfg_len), .lsb_first(cfg_lsb),
.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(cfg_sel),
.lead_cyc(cfg_lead), .lag_cyc(cfg_lag), .gap_cyc(cfg_gap),
.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(cs_state)
);
spi_multibyte #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_stream (
.clk(clk), .rst_n(rst_n),
.tx_push(tx_push), .tx_wdata(reg_wdata[MAX_W-1:0]), .tx_last(tx_last),
.tx_ready(tx_ready),
.flush(flush), .clr_flags(clr_flags),
.rx_pop(rd_rxdata), .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)) u_abort (
.clk(clk), .rst_n(rst_n),
.abort_req(abort_pend), .clr_flags(clr_flags),
.busy(busy), .shift_en(shift_en), .frame_done(frame_done),
.bit_idx(bit_idx), .len(cfg_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)
);
// --- status -----------------------------------------------------------
// Assembled by field rather than by concatenation. A concatenation has to
// be exactly 32 bits wide and the padding depends on the parameters, so a
// parameter change turns into a silently misaligned status register -- and
// with LEN_W at its default the padding expressions come out zero bits
// wide, which is not even legal. Indexed part-selects into a zeroed word
// cannot drift.
reg [31:0] status;
always @(*) begin
status = 32'h0;
status[0] = tx_ready;
status[1] = rx_ready;
status[2] = busy;
status[3] = stalled;
status[4] = rx_overrun;
status[5] = rx_trunc;
status[6] = aborted;
status[7] = sel_err;
status[8] = len_err;
status[9] = div_err;
status[10] = aborting;
status[16 +: LEN_W] = abort_bits;
end
// --- read mux ---------------------------------------------------------
// Purely combinational, every branch assigned, no latch. RXDATA is the only
// read with a side effect, and that side effect is driven by `reg_rd`
// above rather than by this mux -- a read data path that also generates
// control is a read data path nobody can safely add a register to.
reg [31:0] rdata_mux;
always @(*) begin
rdata_mux = 32'h0;
case (reg_addr)
A_CTRL: begin
rdata_mux[0] = cfg_cpol;
rdata_mux[1] = cfg_cpha;
rdata_mux[2] = cfg_lsb;
rdata_mux[8 +: DIV_W] = cfg_div;
rdata_mux[16 +: LEN_W] = cfg_len;
rdata_mux[24 +: SEL_W] = cfg_sel;
end
A_TIMING: begin
rdata_mux[0 +: CNT_W] = cfg_lead;
rdata_mux[8 +: CNT_W] = cfg_lag;
rdata_mux[16 +: CNT_W] = cfg_gap;
end
A_RXDATA: rdata_mux[MAX_W-1:0] = rx_rdata;
A_STATUS: rdata_mux = status;
default: rdata_mux = 32'h0;
endcase
end
assign reg_rdata = rdata_mux;
// --- register writes --------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// Reset values chosen so the master is INERT, not merely defined:
// mode 0, the slowest legal divisor, one byte, generous timing.
// A reset default that happens to be a fast clock is a reset
// default that can violate a slave's timing before software has
// run a single instruction.
cfg_cpol <= 1'b0;
cfg_cpha <= 1'b0;
cfg_lsb <= 1'b0;
cfg_div <= 255;
cfg_len <= 8;
cfg_sel <= {SEL_W{1'b0}};
cfg_lead <= 16;
cfg_lag <= 16;
cfg_gap <= 16;
abort_pend <= 1'b0;
end else begin
if (wr) begin
case (reg_addr)
A_CTRL: begin
cfg_cpol <= reg_wdata[0];
cfg_cpha <= reg_wdata[1];
cfg_lsb <= reg_wdata[2];
cfg_div <= reg_wdata[8 +: DIV_W];
cfg_len <= reg_wdata[16 +: LEN_W];
cfg_sel <= reg_wdata[24 +: SEL_W];
end
A_TIMING: begin
cfg_lead <= reg_wdata[0 +: CNT_W];
cfg_lag <= reg_wdata[8 +: CNT_W];
cfg_gap <= reg_wdata[16 +: CNT_W];
end
default: ; // TXDATA / TXLAST / CMD act through strobes
endcase
end
// Set by the command, cleared by the acknowledgement. Ordered so a
// command arriving on the same cycle as an acknowledgement is not
// lost -- the write wins, because a dropped abort is an abort the
// driver believes happened.
if (abort_ack)
abort_pend <= 1'b0;
if (cmd_abort)
abort_pend <= 1'b1;
end
end
// NOTE ON CONFIGURATION WHILE BUSY. The registers here accept writes at any
// time, and the datapath does NOT resample them mid-transfer: Chapter 13.2's
// latch takes a snapshot at the start of each frame. So a write during a
// transfer changes the NEXT frame and never corrupts the one in flight.
// That is a deliberate division of labour -- the register file stays a
// register file, and exactly one block owns the question of when
// configuration takes effect.
endmodule// spi_master_top_tb.v
//
// Every stimulus in this testbench goes through the REGISTER INTERFACE. There
// is no poking of internal signals anywhere, because the interface is what is
// being verified: a master whose blocks are all individually correct and whose
// register map is wrong is a master nobody can write a driver for.
//
// The closing test is a real flash transaction -- READ (0x03), three address
// bytes, four data bytes, one chip select -- driven exactly as software would
// drive it, and checked against a behavioural flash that only answers if the
// command and address arrived correctly under a single unbroken assertion. It
// is the whole track's argument in one transfer: Chapters 1 to 12 explained what
// the wire has to look like, and this is a master that produces it.
`timescale 1ns/1ps
module spi_master_top_tb;
localparam MAX_W = 32;
localparam LEN_W = 6;
localparam DIV_W = 8;
localparam CNT_W = 8;
localparam N_CS = 4;
localparam SEL_W = 2;
localparam [4:0] A_CTRL = 5'h00,
A_TIMING = 5'h04,
A_TXDATA = 5'h08,
A_TXLAST = 5'h0C,
A_RXDATA = 5'h10,
A_STATUS = 5'h14,
A_CMD = 5'h18;
localparam S_TXRDY = 0, S_RXRDY = 1, S_BUSY = 2, S_STALL = 3,
S_OVR = 4, S_TRUNC = 5, S_ABORTD = 6, S_SELERR = 7,
S_LENERR = 8, S_DIVERR = 9, S_ABTING = 10;
reg clk;
reg rst_n;
always #5 clk = ~clk;
reg [4:0] reg_addr;
reg reg_wr;
reg reg_rd;
reg [31:0] reg_wdata;
wire [31:0] reg_rdata;
wire sclk, mosi;
wire [N_CS-1:0] cs_n;
wire miso;
spi_master_top #(.MAX_W(MAX_W), .LEN_W(LEN_W), .DIV_W(DIV_W),
.CNT_W(CNT_W), .N_CS(N_CS), .SEL_W(SEL_W)) dut (
.clk(clk), .rst_n(rst_n),
.reg_addr(reg_addr), .reg_wr(reg_wr), .reg_rd(reg_rd),
.reg_wdata(reg_wdata), .reg_rdata(reg_rdata),
.sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n)
);
// ---------------------------------------------------------------------
// A behavioural SPI slave that works off the PINS ONLY. It recovers bit
// boundaries from SCLK edges and word boundaries from chip select, exactly
// as a real device does, so nothing it reports depends on the master's
// internal strobes being right.
// ---------------------------------------------------------------------
reg cs_q, sclk_q;
reg [7:0] sl_rx_sr;
integer sl_bits;
reg [7:0] sl_rx_q [0:63];
integer sl_rx_n;
reg [7:0] sl_tx_q [0:63];
integer sl_tx_n;
reg [7:0] sl_tx_sr;
integer sl_tx_bits;
reg sl_miso_r;
reg sl_cpol, sl_cpha;
assign miso = sl_miso_r;
wire sl_cs = ~cs_n[0] | ~cs_n[1] | ~cs_n[2] | ~cs_n[3];
// The capture edge for this mode is the LEADING edge when CPHA=0 and the
// TRAILING edge when CPHA=1, which in terms of the raw pin is:
wire sl_lead_edge = (sclk != sclk_q) && (sclk != sl_cpol);
wire sl_trail_edge = (sclk != sclk_q) && (sclk == sl_cpol);
wire sl_cap_edge = sl_cpha ? sl_trail_edge : sl_lead_edge;
wire sl_drv_edge = sl_cpha ? sl_lead_edge : sl_trail_edge;
always @(posedge clk) begin
sclk_q <= sclk;
cs_q <= sl_cs;
if (sl_cs && !cs_q) begin
// Chip select has just asserted: a new transaction. With CPHA=0
// the slave must already have its top bit on the pin, because the
// first capture is the first edge and there is no earlier moment.
// With CPHA=1 it must NOT -- the first leading edge is when both
// sides drive, and a slave that pre-drives there is one bit ahead
// for the whole transaction.
sl_rx_sr <= 8'h0;
sl_bits <= 0;
sl_tx_n <= sl_tx_n + 1;
if (!sl_cpha) begin
sl_tx_sr <= {sl_tx_q[sl_tx_n][6:0], 1'b0};
sl_miso_r <= sl_tx_q[sl_tx_n][7];
sl_tx_bits <= 1;
end else begin
sl_tx_sr <= sl_tx_q[sl_tx_n];
sl_tx_bits <= 0;
end
end else if (sl_cs) begin
if (sl_cap_edge) begin
sl_rx_sr <= {sl_rx_sr[6:0], mosi};
if (sl_bits == 7) begin
sl_rx_q[sl_rx_n] <= {sl_rx_sr[6:0], mosi};
sl_rx_n <= sl_rx_n + 1;
sl_bits <= 0;
end else begin
sl_bits <= sl_bits + 1;
end
end
if (sl_drv_edge) begin
if (sl_tx_bits == 8) begin
// Word boundary: fetch the next byte to send. Only reached
// with CPHA=0, where the first bit of each byte goes out
// half a period early; with CPHA=1 every bit including the
// first is driven on a leading edge, so the counter never
// gets here.
sl_tx_sr <= {sl_tx_q[sl_tx_n][6:0], 1'b0};
sl_miso_r <= sl_tx_q[sl_tx_n][7];
sl_tx_bits <= 1;
sl_tx_n <= sl_tx_n + 1;
end else begin
sl_miso_r <= sl_tx_sr[7];
sl_tx_sr <= {sl_tx_sr[6:0], 1'b0};
sl_tx_bits <= sl_tx_bits + 1;
end
end
end
end
// ---------------------------------------------------------------------
// bus helpers
// ---------------------------------------------------------------------
integer errors;
task bus_write;
input [4:0] a;
input [31:0] d;
begin
@(negedge clk);
reg_addr = a; reg_wdata = d; reg_wr = 1'b1;
@(negedge clk);
reg_wr = 1'b0;
end
endtask
reg [31:0] bus_q;
task bus_read;
input [4:0] a;
begin
@(negedge clk);
reg_addr = a; reg_rd = 1'b1;
#1 bus_q = reg_rdata;
@(negedge clk);
reg_rd = 1'b0;
end
endtask
// A status read that does NOT pop anything, used for polling.
task poll_status;
begin
@(negedge clk);
reg_addr = A_STATUS; reg_rd = 1'b1;
#1 bus_q = reg_rdata;
@(negedge clk);
reg_rd = 1'b0;
end
endtask
task configure;
input integer dv;
input pol;
input pha;
input lsb;
input integer nbits;
input integer slave;
input integer lead;
input integer lag;
input integer gap;
begin
bus_write(A_CTRL, (pol ? 32'h1 : 32'h0) |
(pha ? 32'h2 : 32'h0) |
(lsb ? 32'h4 : 32'h0) |
((dv & 32'hFF) << 8) |
((nbits & 32'h3F) << 16) |
((slave & 32'h3) << 24));
bus_write(A_TIMING, (lead & 32'hFF) |
((lag & 32'hFF) << 8) |
((gap & 32'hFF) << 16));
sl_cpol = pol; sl_cpha = pha;
end
endtask
task send;
input [31:0] d;
input last;
integer guard;
begin
guard = 8000;
poll_status();
while (!bus_q[S_TXRDY] && guard > 0) begin
poll_status();
guard = guard - 1;
end
if (guard == 0) begin
$display(" FAIL: the transmit queue never became ready");
errors = errors + 1;
end
bus_write(last ? A_TXLAST : A_TXDATA, d);
end
endtask
integer got_n;
reg [31:0] got_q [0:63];
task collect;
begin
poll_status();
while (bus_q[S_RXRDY]) begin
bus_read(A_RXDATA);
got_q[got_n] = bus_q;
got_n = got_n + 1;
poll_status();
end
end
endtask
// Waits for the transfer to START and only then for it to finish. Waiting
// only for `busy` to fall is the classic driver bug: a request takes the
// programmed lead time to turn into a busy engine, so a poll issued
// immediately after the queue write sees an idle master and concludes the
// transfer is over before it has begun. Every check downstream then reads
// stale data and blames the hardware.
task wait_done;
integer guard;
begin
guard = 4000;
poll_status();
while (!bus_q[S_BUSY] && guard > 0) begin
collect();
poll_status();
guard = guard - 1;
end
if (guard == 0) begin
$display(" FAIL: the master never became busy after a queue write");
errors = errors + 1;
end
guard = 20000;
poll_status();
while (bus_q[S_BUSY] && guard > 0) begin
collect();
poll_status();
guard = guard - 1;
end
repeat (6) @(negedge clk);
collect();
end
endtask
// Discards anything left in the receive register from an earlier test, so
// one test's leftovers cannot be counted as the next test's first word.
task flush_rx;
begin
collect();
got_n = 0;
end
endtask
integer i, k, bad, base;
reg [31:0] st;
initial begin
sl_rx_n = 0; sl_tx_n = 0; sl_bits = 0; sl_tx_bits = 0;
sl_rx_sr = 8'h0; sl_tx_sr = 8'h0; sl_miso_r = 1'b0;
sl_cpol = 1'b0; sl_cpha = 1'b0;
got_n = 0;
for (i = 0; i < 64; i = i + 1) sl_tx_q[i] = 8'h00;
repeat (3) @(negedge clk);
rst_n = 1'b1;
repeat (2) @(negedge clk);
// 1. RESET DEFAULTS. The master must come up inert and READABLE: a
// register file whose reset value cannot be read back is a register
// file nobody can debug.
bus_read(A_CTRL);
if (bus_q[7:0] !== 8'h00 || bus_q[15:8] !== 8'd255 ||
bus_q[21:16] !== 6'd8) begin
$display(" FAIL: CTRL came out of reset as %08h", bus_q);
errors = errors + 1;
end
bus_read(A_TIMING);
if (bus_q[7:0] !== 8'd16 || bus_q[15:8] !== 8'd16 ||
bus_q[23:16] !== 8'd16) begin
$display(" FAIL: TIMING came out of reset as %08h", bus_q);
errors = errors + 1;
end
poll_status();
if (!bus_q[S_TXRDY] || bus_q[S_BUSY] || bus_q[S_RXRDY]) begin
$display(" FAIL: STATUS out of reset was %08h", bus_q);
errors = errors + 1;
end
if (cs_n !== {N_CS{1'b1}}) begin
$display(" FAIL: reset left a chip select asserted");
errors = errors + 1;
end
$display(" out of reset: mode 0, divisor 255, eight bits, 16-cycle windows, all selects released, queue ready, not busy");
// 2. READ-BACK OF EVERY WRITABLE FIELD. A field that cannot be read
// back is a field that will be programmed wrong for a year.
configure(6, 1'b1, 1'b1, 1'b1, 13, 2, 7, 9, 11);
bus_read(A_CTRL);
if (bus_q[0] !== 1'b1 || bus_q[1] !== 1'b1 || bus_q[2] !== 1'b1 ||
bus_q[15:8] !== 8'd6 || bus_q[21:16] !== 6'd13 ||
bus_q[25:24] !== 2'd2) begin
$display(" FAIL: CTRL read back %08h after programming", bus_q);
errors = errors + 1;
end
bus_read(A_TIMING);
if (bus_q[7:0] !== 8'd7 || bus_q[15:8] !== 8'd9 ||
bus_q[23:16] !== 8'd11) begin
$display(" FAIL: TIMING read back %08h after programming", bus_q);
errors = errors + 1;
end
$display(" every writable field read back exactly as written");
// 3. THE ERROR FLAGS MEAN WHAT THEY SAY. Each is provoked on its own.
configure(1, 1'b0, 1'b0, 1'b0, 8, 0, 4, 4, 4); // divisor below two
poll_status();
if (!bus_q[S_DIVERR]) begin
$display(" FAIL: a divisor of 1 did not raise the divisor error");
errors = errors + 1;
end
configure(4, 1'b0, 1'b0, 1'b0, 33, 0, 4, 4, 4); // width above 32
poll_status();
if (!bus_q[S_LENERR]) begin
$display(" FAIL: a 33-bit frame did not raise the width error");
errors = errors + 1;
end
configure(4, 1'b0, 1'b0, 1'b0, 8, 0, 4, 4, 4);
poll_status();
if (bus_q[S_DIVERR] || bus_q[S_LENERR]) begin
$display(" FAIL: a legal configuration still reports an error: %08h",
bus_q);
errors = errors + 1;
end
$display(" divisor 1 and width 33 each reported on their own, and a legal setting reports neither");
// 4. A COMPLETE TRANSACTION, all four modes, checked on the pins by a
// slave that only sees pins.
for (k = 0; k < 4; k = k + 1) begin
sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
sl_tx_q[0] = 8'hDE; sl_tx_q[1] = 8'hAD;
sl_tx_q[2] = 8'hBE; sl_tx_q[3] = 8'hEF;
configure(4, k[0], k[1], 1'b0, 8, 0, 4, 4, 4);
repeat (4) @(negedge clk);
flush_rx();
// Collected BETWEEN sends, because the receive side is one word
// deep (Chapter 13.9): a driver that queues every word first and
// only then starts reading loses all but the last one, and the
// overrun flag says so.
send(32'h12, 1'b0); collect();
send(32'h34, 1'b0); collect();
send(32'h56, 1'b0); collect();
send(32'h78, 1'b1); collect();
wait_done();
if (sl_rx_n != 4) begin
$display(" FAIL: mode %0d -- the slave saw %0d of 4 bytes",
k, sl_rx_n);
errors = errors + 1;
end else if (sl_rx_q[0] !== 8'h12 || sl_rx_q[1] !== 8'h34 ||
sl_rx_q[2] !== 8'h56 || sl_rx_q[3] !== 8'h78) begin
$display(" FAIL: mode %0d -- the slave saw %02h %02h %02h %02h",
k, sl_rx_q[0], sl_rx_q[1], sl_rx_q[2], sl_rx_q[3]);
errors = errors + 1;
end
if (got_n != 4) begin
$display(" FAIL: mode %0d -- software read %0d of 4 words",
k, got_n);
errors = errors + 1;
end else if (got_q[0][7:0] !== 8'hDE || got_q[1][7:0] !== 8'hAD ||
got_q[2][7:0] !== 8'hBE || got_q[3][7:0] !== 8'hEF) begin
$display(" FAIL: mode %0d -- software read %02h %02h %02h %02h",
k, got_q[0][7:0], got_q[1][7:0], got_q[2][7:0],
got_q[3][7:0]);
errors = errors + 1;
end
end
$display(" all four modes: the slave received 12 34 56 78 and software read back de ad be ef, over one chip select each");
// 5. LSB-FIRST, ON THE WIRE. The slave assembles MSB-first, so an
// LSB-first master must make it see the bit-reversed byte -- which is
// the only way to prove the setting reached the pins.
sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
configure(4, 1'b0, 1'b0, 1'b1, 8, 0, 4, 4, 4);
repeat (4) @(negedge clk);
flush_rx();
send(32'h8D, 1'b1);
wait_done();
if (sl_rx_n != 1 || sl_rx_q[0] !== 8'hB1) begin
$display(" FAIL: LSB-first 0x8d -- the slave recorded %0d bytes, the first being %02h, expected 1 and b1",
sl_rx_n, sl_rx_q[0]);
errors = errors + 1;
end
$display(" LSB-first: software sent 8d and the MSB-first slave saw b1 -- the reversal happened on the wire");
// 6. ABORT THROUGH THE COMMAND REGISTER, mid-transfer, and recovery.
sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
configure(8, 1'b0, 1'b0, 1'b0, 8, 0, 4, 6, 6);
repeat (4) @(negedge clk);
send(32'hAA, 1'b0); collect();
send(32'h55, 1'b0);
// Let it get properly into the first frame.
repeat (40) @(negedge clk);
bus_write(A_CMD, 32'h1);
i = 4000;
poll_status();
while (bus_q[S_BUSY] && i > 0) begin
poll_status();
i = i - 1;
end
repeat (10) @(negedge clk);
poll_status();
st = bus_q;
if (!st[S_ABORTD]) begin
$display(" FAIL: an abort written to CMD did not set the abort flag");
errors = errors + 1;
end
if (!st[S_TRUNC]) begin
$display(" FAIL: an abort mid-byte did not report a truncated word");
errors = errors + 1;
end
// A truncated word that got nowhere is a contradiction, and it is
// exactly what a request serviced twice produces: the second pass finds
// the bus idle and zeroes the count while the flag stays set.
if (st[S_TRUNC] && st[21:16] == 6'd0) begin
$display(" FAIL: a truncated word was reported as having got 0 bits out");
errors = errors + 1;
end
if (st[S_BUSY]) begin
$display(" FAIL: the master was still busy long after the abort");
errors = errors + 1;
end
if (cs_n !== {N_CS{1'b1}}) begin
$display(" FAIL: the abort left a chip select asserted");
errors = errors + 1;
end
$display(" abort written to CMD: flagged, the partial word reported as truncated (%0d bits made it), every select released",
st[21:16]);
// The flags must clear on command, and only on command.
poll_status();
if (!bus_q[S_ABORTD] || !bus_q[S_TRUNC]) begin
$display(" FAIL: the sticky flags cleared themselves on a status read");
errors = errors + 1;
end
bus_write(A_CMD, 32'h2);
repeat (2) @(negedge clk);
poll_status();
if (bus_q[S_ABORTD] || bus_q[S_TRUNC]) begin
$display(" FAIL: writing the clear bit left %08h", bus_q);
errors = errors + 1;
end
$display(" the sticky flags survive a status read and clear only on an explicit command");
// 7. THE FLASH TRANSACTION. READ (0x03), a 24-bit address and four data
// bytes, under one chip select, driven the way a driver drives it.
// The flash answers only if the command and address arrived intact.
sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
// The flash's response, positioned so it lands in the data phase.
sl_tx_q[0] = 8'hFF; sl_tx_q[1] = 8'hFF;
sl_tx_q[2] = 8'hFF; sl_tx_q[3] = 8'hFF;
sl_tx_q[4] = 8'hC0; sl_tx_q[5] = 8'hFF;
sl_tx_q[6] = 8'hEE; sl_tx_q[7] = 8'h01;
configure(4, 1'b0, 1'b0, 1'b0, 8, 1, 5, 5, 8);
repeat (4) @(negedge clk);
flush_rx();
send(32'h03, 1'b0); collect(); // READ
send(32'h01, 1'b0); collect(); // address 0x012345
send(32'h23, 1'b0); collect();
send(32'h45, 1'b0); collect();
send(32'h00, 1'b0); collect(); // four bytes to clock the data out
send(32'h00, 1'b0); collect();
send(32'h00, 1'b0); collect();
send(32'h00, 1'b1); collect();
wait_done();
if (sl_rx_n != 8) begin
$display(" FAIL: the flash transaction delivered %0d of 8 bytes",
sl_rx_n);
errors = errors + 1;
end else if (sl_rx_q[0] !== 8'h03 || sl_rx_q[1] !== 8'h01 ||
sl_rx_q[2] !== 8'h23 || sl_rx_q[3] !== 8'h45) begin
$display(" FAIL: the flash saw command %02h address %02h%02h%02h",
sl_rx_q[0], sl_rx_q[1], sl_rx_q[2], sl_rx_q[3]);
errors = errors + 1;
end
if (got_n != 8) begin
$display(" FAIL: software read %0d of 8 words from the flash read",
got_n);
errors = errors + 1;
end else if (got_q[4][7:0] !== 8'hC0 || got_q[5][7:0] !== 8'hFF ||
got_q[6][7:0] !== 8'hEE || got_q[7][7:0] !== 8'h01) begin
$display(" FAIL: the data phase returned %02h %02h %02h %02h",
got_q[4][7:0], got_q[5][7:0], got_q[6][7:0],
got_q[7][7:0]);
errors = errors + 1;
end
poll_status();
if (bus_q[S_OVR] || bus_q[S_TRUNC] || bus_q[S_ABORTD] ||
bus_q[S_SELERR] || bus_q[S_LENERR] || bus_q[S_DIVERR]) begin
$display(" FAIL: the flash transaction finished with status %08h",
bus_q);
errors = errors + 1;
end
$display(" flash READ 0x03 of address 012345 on slave 1: command and address arrived intact under one chip select, and the four data bytes came back as c0 ff ee 01 with every error flag clear");
if (errors == 0)
$display("PASS: the complete master is driven entirely through its register interface -- it comes out of reset inert and fully readable, every writable field reads back as written, and each of the divisor, width and slave errors is reported on its own while a legal configuration reports none -- four-byte transactions in all four modes deliver the right bytes to a slave that only sees pins and return the right words to software, LSB-first genuinely reverses the bits on the wire, an abort written to the command register stops the transfer, flags the truncated word with its partial bit count, releases every chip select and leaves the engine able to run the next transfer, the sticky flags survive a status read and clear only on command, and a flash READ of command plus three address bytes plus four data bytes completes under a single unbroken chip select with every error flag clear");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
clk = 1'b0;
rst_n = 1'b0;
reg_addr = 5'h0;
reg_wr = 1'b0;
reg_rd = 1'b0;
reg_wdata = 32'h0;
errors = 0;
end
endmodule-- spi_master_top.vhd
--
-- Chapter 13.11 -- the whole master, and the interface software actually sees.
--
-- Seven blocks, wired together, plus the one thing none of them has: a way for
-- software to drive it. This file is deliberately almost all structure. Every
-- design decision worth arguing about was made in Chapters 13.1 through 13.10,
-- and a top level that has interesting logic in it is a top level where a
-- decision was made in the wrong place.
--
-- 13.4 spi_clkdiv_strobe SCLK, and the two edge strobes
-- 13.5 spi_mode_edges which edge launches, which captures
-- 13.6 spi_shift_datapath the two shift registers
-- 13.8 spi_width_order width and bit order, wrapping 13.6
-- 13.7 spi_cs_ctrl chip select, lead, lag, gap, and abort
-- 13.9 spi_multibyte the shadow register, streaming, overrun
-- 13.10 spi_abort_ctl abort policy and what was lost
--
-- THE REGISTER MAP.
--
-- 0x00 CTRL [0] cpol [1] cpha [2] lsb_first
-- [15:8] div (SCLK period in clocks, >= 2)
-- [21:16] len (bits per frame, 1..MAX_W)
-- [25:24] sel (which slave)
-- 0x04 TIMING [7:0] lead [15:8] lag [23:16] gap (system clocks)
-- 0x08 TXDATA write: queue a word, transaction CONTINUES after it
-- 0x0C TXLAST write: queue a word, transaction ENDS after it
-- 0x10 RXDATA read: the received word; reading POPS it
-- 0x14 STATUS read only
-- 0x18 CMD write: [0] abort [1] clear the sticky flags
--
-- TWO ADDRESSES FOR ONE FIFO. `TXDATA` and `TXLAST` write the same queue and
-- differ only in the end-of-transaction flag. A flag bit inside the data word
-- costs a bit of the data width, and at MAX_W = 32 there is none to spare. A
-- separate "this is the last one" register written first is two bus cycles per
-- word and a race if an interrupt lands between them. An address bit is free.
--
-- STATUS IS READ-ONLY AND COMPLETE. Every error the design can detect appears
-- here: not just the ones a driver is expected to handle, but the ones that mean
-- the DRIVER is wrong -- a divisor below two, a frame width the datapath cannot
-- hold, a slave that is not fitted. A design that silently absorbs those is a
-- design whose bring-up consists of guessing.
--
-- WHAT MAKES IT SYNTHESISABLE.
--
-- - one clock, `clk`, everywhere; nothing is clocked on SCLK;
-- - one reset, `rst_n`, asynchronous, on every flop that drives a pin;
-- - no latches: every combinational process assigns every output on every
-- path;
-- - no gated or generated clocks: SCLK is a REGISTERED OUTPUT (Chapter 13.4),
-- not a clock this design uses;
-- - no combinational path from `miso` to any output pin: MISO is sampled into
-- a flop and nothing else.
--
-- The last two are the ones that get missed. A master that muxes MISO onto MOSI
-- combinationally -- a "loopback mode", say -- has just built a path from an
-- asynchronous input to an output with no flop in it, and no timing tool will
-- tell you what to constrain.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_master_top is
generic (
MAX_W : positive := 32;
LEN_W : positive := 6;
DIV_W : positive := 8;
CNT_W : positive := 8;
N_CS : positive := 4;
SEL_W : positive := 2
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- register interface
reg_addr : in unsigned(4 downto 0);
reg_wr : in std_logic;
reg_rd : in std_logic;
reg_wdata : in std_logic_vector(31 downto 0);
reg_rdata : out std_logic_vector(31 downto 0);
-- SPI pins
sclk : out std_logic;
mosi : out std_logic;
miso : in std_logic;
cs_n : out std_logic_vector(N_CS - 1 downto 0)
);
end entity;
architecture rtl of spi_master_top is
constant A_CTRL : natural := 16#00#;
constant A_TIMING : natural := 16#04#;
constant A_TXDATA : natural := 16#08#;
constant A_TXLAST : natural := 16#0C#;
constant A_RXDATA : natural := 16#10#;
constant A_STATUS : natural := 16#14#;
constant A_CMD : natural := 16#18#;
-- configuration registers
signal cfg_cpol : std_logic := '0';
signal cfg_cpha : std_logic := '0';
signal cfg_lsb : std_logic := '0';
signal cfg_div : unsigned(DIV_W - 1 downto 0) := (others => '0');
signal cfg_len : unsigned(LEN_W - 1 downto 0) := (others => '0');
signal cfg_sel : unsigned(SEL_W - 1 downto 0) := (others => '0');
signal cfg_lead : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal cfg_lag : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal cfg_gap : unsigned(CNT_W - 1 downto 0) := (others => '0');
-- decoded strobes
signal wr_txdata, wr_txlast, wr_cmd, rd_rxdata : std_logic;
signal tx_push, tx_last, clr_flags, cmd_abort : std_logic;
-- An abort is a COMMAND, written once, but the abort controller wants a
-- level held until it has acknowledged. Latching it here is the whole of
-- the difference between a register write and a control signal, and doing it
-- anywhere else means a driver has to poll the status register fast enough
-- to keep a bit asserted -- which is not a thing software can promise.
signal abort_pend : std_logic := '0';
-- the engine
signal edge_a_stb, edge_b_stb, bit_done, div_err : std_logic;
signal half_a, half_b : unsigned(DIV_W - 1 downto 0);
signal shift_en, start_stb, busy, sel_err : std_logic;
signal cs_state : unsigned(2 downto 0);
signal preload_stb, launch_stb, capture_stb, frame_done : std_logic;
signal bit_idx : unsigned(LEN_W - 1 downto 0);
signal rx_word : std_logic_vector(MAX_W - 1 downto 0);
signal rx_valid_stb, len_err : std_logic;
signal tx_data : std_logic_vector(MAX_W - 1 downto 0);
signal load_stb, stalled : std_logic;
signal req_raw, hold_raw, tx_ready, rx_ready, rx_overrun : std_logic;
signal rx_rdata : std_logic_vector(MAX_W - 1 downto 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);
signal status : std_logic_vector(31 downto 0);
signal tx_word : std_logic_vector(MAX_W - 1 downto 0);
begin
tx_word <= reg_wdata(MAX_W - 1 downto 0);
wr_txdata <= '1' when reg_wr = '1' and to_integer(reg_addr) = A_TXDATA
else '0';
wr_txlast <= '1' when reg_wr = '1' and to_integer(reg_addr) = A_TXLAST
else '0';
wr_cmd <= '1' when reg_wr = '1' and to_integer(reg_addr) = A_CMD
else '0';
rd_rxdata <= '1' when reg_rd = '1' and to_integer(reg_addr) = A_RXDATA
else '0';
tx_push <= wr_txdata or wr_txlast;
tx_last <= wr_txlast;
clr_flags <= wr_cmd and reg_wdata(1);
cmd_abort <= wr_cmd and reg_wdata(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 => cfg_div, cpol => cfg_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 => cfg_cpha, len => cfg_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 => cfg_len, lsb_first => cfg_lsb,
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 => cfg_sel,
lead_cyc => cfg_lead, lag_cyc => cfg_lag,
gap_cyc => cfg_gap,
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 => cs_state);
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_word, tx_last => tx_last,
tx_ready => tx_ready,
flush => flush, clr_flags => clr_flags,
rx_pop => rd_rxdata, 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);
u_abort : entity work.spi_abort_ctl
generic map (LEN_W => LEN_W)
port map (clk => clk, rst_n => rst_n,
abort_req => abort_pend, clr_flags => clr_flags,
busy => busy, shift_en => shift_en, frame_done => frame_done,
bit_idx => bit_idx, len => cfg_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);
-- Assembled by field rather than by concatenation. A concatenation has to be
-- exactly 32 bits wide and the padding depends on the generics, so a generic
-- change turns into a silently misaligned status register.
status_p : process (tx_ready, rx_ready, busy, stalled, rx_overrun, rx_trunc,
aborted, sel_err, len_err, div_err, aborting, abort_bits)
variable s : std_logic_vector(31 downto 0);
begin
s := (others => '0');
s(0) := tx_ready;
s(1) := rx_ready;
s(2) := busy;
s(3) := stalled;
s(4) := rx_overrun;
s(5) := rx_trunc;
s(6) := aborted;
s(7) := sel_err;
s(8) := len_err;
s(9) := div_err;
s(10) := aborting;
s(16 + LEN_W - 1 downto 16) := std_logic_vector(abort_bits);
status <= s;
end process;
-- Purely combinational, every branch assigned, no latch. RXDATA is the only
-- read with a side effect, and that side effect is driven by `rd_rxdata`
-- above rather than by this mux -- a read data path that also generates
-- control is a read data path nobody can safely add a register to.
read_p : process (reg_addr, cfg_cpol, cfg_cpha, cfg_lsb, cfg_div, cfg_len,
cfg_sel, cfg_lead, cfg_lag, cfg_gap, rx_rdata, status)
variable d : std_logic_vector(31 downto 0);
begin
d := (others => '0');
case to_integer(reg_addr) is
when A_CTRL =>
d(0) := cfg_cpol;
d(1) := cfg_cpha;
d(2) := cfg_lsb;
d(8 + DIV_W - 1 downto 8) := std_logic_vector(cfg_div);
d(16 + LEN_W - 1 downto 16) := std_logic_vector(cfg_len);
d(24 + SEL_W - 1 downto 24) := std_logic_vector(cfg_sel);
when A_TIMING =>
d(0 + CNT_W - 1 downto 0) := std_logic_vector(cfg_lead);
d(8 + CNT_W - 1 downto 8) := std_logic_vector(cfg_lag);
d(16 + CNT_W - 1 downto 16) := std_logic_vector(cfg_gap);
when A_RXDATA =>
d(MAX_W - 1 downto 0) := rx_rdata;
when A_STATUS =>
d := status;
when others =>
d := (others => '0');
end case;
reg_rdata <= d;
end process;
write_p : process (clk, rst_n)
begin
if rst_n = '0' then
-- Reset values chosen so the master is INERT, not merely defined:
-- mode 0, the slowest legal divisor, one byte, generous timing. A
-- reset default that happens to be a fast clock is a reset default
-- that can violate a slave's timing before software has run a single
-- instruction.
cfg_cpol <= '0';
cfg_cpha <= '0';
cfg_lsb <= '0';
cfg_div <= to_unsigned(255, DIV_W);
cfg_len <= to_unsigned(8, LEN_W);
cfg_sel <= (others => '0');
cfg_lead <= to_unsigned(16, CNT_W);
cfg_lag <= to_unsigned(16, CNT_W);
cfg_gap <= to_unsigned(16, CNT_W);
abort_pend <= '0';
elsif rising_edge(clk) then
if reg_wr = '1' then
case to_integer(reg_addr) is
when A_CTRL =>
cfg_cpol <= reg_wdata(0);
cfg_cpha <= reg_wdata(1);
cfg_lsb <= reg_wdata(2);
cfg_div <= unsigned(reg_wdata(8 + DIV_W - 1 downto 8));
cfg_len <= unsigned(reg_wdata(16 + LEN_W - 1
downto 16));
cfg_sel <= unsigned(reg_wdata(24 + SEL_W - 1
downto 24));
when A_TIMING =>
cfg_lead <= unsigned(reg_wdata(0 + CNT_W - 1 downto 0));
cfg_lag <= unsigned(reg_wdata(8 + CNT_W - 1 downto 8));
cfg_gap <= unsigned(reg_wdata(16 + CNT_W - 1
downto 16));
when others =>
null; -- TXDATA / TXLAST / CMD act through strobes
end case;
end if;
-- Set by the command, cleared by the acknowledgement. Ordered so a
-- command arriving on the same cycle as an acknowledgement is not
-- lost -- the write wins, because a dropped abort is an abort the
-- driver believes happened.
if abort_ack = '1' then
abort_pend <= '0';
end if;
if cmd_abort = '1' then
abort_pend <= '1';
end if;
end if;
end process;
-- NOTE ON CONFIGURATION WHILE BUSY. The registers here accept writes at any
-- time, and the datapath does NOT resample them mid-transfer: Chapter 13.2's
-- latch takes a snapshot at the start of each frame. So a write during a
-- transfer changes the NEXT frame and never corrupts the one in flight.
-- That is a deliberate division of labour -- the register file stays a
-- register file, and exactly one block owns the question of when
-- configuration takes effect.
end architecture;-- spi_master_top_tb.vhd
--
-- Every stimulus in this testbench goes through the REGISTER INTERFACE. There is
-- no poking of internal signals anywhere, because the interface is what is being
-- verified: a master whose blocks are all individually correct and whose register
-- map is wrong is a master nobody can write a driver for.
--
-- The closing test is a real flash transaction -- READ (0x03), three address
-- bytes, four data bytes, one chip select -- driven exactly as software would
-- drive it, and checked against a behavioural flash that only answers if the
-- command and address arrived correctly under a single unbroken assertion.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_master_top_tb is
end entity;
architecture sim of spi_master_top_tb is
constant MAX_W : positive := 32;
constant LEN_W : positive := 6;
constant DIV_W : positive := 8;
constant CNT_W : positive := 8;
constant N_CS : positive := 4;
constant SEL_W : positive := 2;
constant A_CTRL : natural := 16#00#;
constant A_TIMING : natural := 16#04#;
constant A_TXDATA : natural := 16#08#;
constant A_TXLAST : natural := 16#0C#;
constant A_RXDATA : natural := 16#10#;
constant A_STATUS : natural := 16#14#;
constant A_CMD : natural := 16#18#;
constant S_TXRDY : natural := 0;
constant S_RXRDY : natural := 1;
constant S_BUSY : natural := 2;
constant S_OVR : natural := 4;
constant S_TRUNC : natural := 5;
constant S_ABORTD : natural := 6;
constant S_SELERR : natural := 7;
constant S_LENERR : natural := 8;
constant S_DIVERR : natural := 9;
type byte_array is array (0 to 63) of std_logic_vector(7 downto 0);
type word_array is array (0 to 63) of std_logic_vector(31 downto 0);
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal halt : boolean := false;
signal reg_addr : unsigned(4 downto 0) := (others => '0');
signal reg_wr : std_logic := '0';
signal reg_rd : std_logic := '0';
signal reg_wdata : std_logic_vector(31 downto 0) := (others => '0');
signal reg_rdata : std_logic_vector(31 downto 0);
signal sclk, mosi, miso : std_logic;
signal cs_n : std_logic_vector(N_CS - 1 downto 0);
-- A behavioural SPI slave that works off the PINS ONLY. It recovers bit
-- boundaries from SCLK edges and word boundaries from chip select, exactly
-- as a real device does.
signal cs_q, sclk_q : std_logic := '0';
signal sl_rx_sr : std_logic_vector(7 downto 0) := (others => '0');
signal sl_bits : natural := 0;
signal sl_rx_q : byte_array := (others => (others => '0'));
signal sl_rx_n : natural := 0;
signal sl_tx_q : byte_array := (others => (others => '0'));
signal sl_tx_n : natural := 0;
signal sl_tx_sr : std_logic_vector(7 downto 0) := (others => '0');
signal sl_tx_bits : natural := 0;
signal sl_miso_r : std_logic := '0';
signal sl_cpol, sl_cpha : std_logic := '0';
signal sl_clr : std_logic := '0';
signal sl_cs : std_logic;
signal sl_lead_edge, sl_trail_edge, sl_cap_edge, sl_drv_edge : std_logic;
signal errors : natural := 0;
function hex2(v : std_logic_vector(7 downto 0)) return string is
constant DIGITS : string(1 to 16) := "0123456789abcdef";
variable r : string(1 to 2);
begin
r(1) := DIGITS(to_integer(unsigned(v(7 downto 4))) + 1);
r(2) := DIGITS(to_integer(unsigned(v(3 downto 0))) + 1);
return r;
end function;
function hex8(v : std_logic_vector(31 downto 0)) return string is
constant DIGITS : string(1 to 16) := "0123456789abcdef";
variable u : unsigned(31 downto 0);
variable r : string(1 to 8);
begin
u := unsigned(v);
for k in 8 downto 1 loop
r(k) := DIGITS(to_integer(u(3 downto 0)) + 1);
u := shift_right(u, 4);
end loop;
return r;
end function;
begin
clk <= not clk after 5 ns when not halt else '0';
dut : entity work.spi_master_top
generic map (MAX_W => MAX_W, LEN_W => LEN_W, DIV_W => DIV_W,
CNT_W => CNT_W, N_CS => N_CS, SEL_W => SEL_W)
port map (clk => clk, rst_n => rst_n,
reg_addr => reg_addr, reg_wr => reg_wr, reg_rd => reg_rd,
reg_wdata => reg_wdata, reg_rdata => reg_rdata,
sclk => sclk, mosi => mosi, miso => miso, cs_n => cs_n);
miso <= sl_miso_r;
sl_cs <= '0' when cs_n = (cs_n'range => '1') else '1';
-- The capture edge is the LEADING edge when CPHA=0 and the TRAILING edge
-- when CPHA=1, which in terms of the raw pin is:
sl_lead_edge <= '1' when sclk /= sclk_q and sclk /= sl_cpol else '0';
sl_trail_edge <= '1' when sclk /= sclk_q and sclk = sl_cpol else '0';
sl_cap_edge <= sl_trail_edge when sl_cpha = '1' else sl_lead_edge;
sl_drv_edge <= sl_lead_edge when sl_cpha = '1' else sl_trail_edge;
slave_p : process (clk)
begin
if rising_edge(clk) then
sclk_q <= sclk;
cs_q <= sl_cs;
if sl_clr = '1' then
sl_rx_n <= 0;
sl_tx_n <= 0;
sl_bits <= 0;
sl_tx_bits <= 0;
sl_rx_sr <= (others => '0');
sl_tx_sr <= (others => '0');
sl_miso_r <= '0';
elsif sl_cs = '1' and cs_q = '0' then
-- A new transaction. With CPHA=0 the slave must already have its
-- top bit on the pin, because the first capture is the first
-- edge and there is no earlier moment. With CPHA=1 it must NOT
-- -- the first leading edge is when both sides drive, and a
-- slave that pre-drives there is one bit ahead for the whole
-- transaction.
sl_rx_sr <= (others => '0');
sl_bits <= 0;
sl_tx_n <= sl_tx_n + 1;
if sl_cpha = '0' then
sl_tx_sr <= sl_tx_q(sl_tx_n)(6 downto 0) & '0';
sl_miso_r <= sl_tx_q(sl_tx_n)(7);
sl_tx_bits <= 1;
else
sl_tx_sr <= sl_tx_q(sl_tx_n);
sl_tx_bits <= 0;
end if;
elsif sl_cs = '1' then
if sl_cap_edge = '1' then
sl_rx_sr <= sl_rx_sr(6 downto 0) & mosi;
if sl_bits = 7 then
sl_rx_q(sl_rx_n) <= sl_rx_sr(6 downto 0) & mosi;
sl_rx_n <= sl_rx_n + 1;
sl_bits <= 0;
else
sl_bits <= sl_bits + 1;
end if;
end if;
if sl_drv_edge = '1' then
if sl_tx_bits = 8 then
-- Word boundary. Only reached with CPHA=0, where the
-- first bit of each byte goes out half a period early.
sl_tx_sr <= sl_tx_q(sl_tx_n)(6 downto 0) & '0';
sl_miso_r <= sl_tx_q(sl_tx_n)(7);
sl_tx_bits <= 1;
sl_tx_n <= sl_tx_n + 1;
else
sl_miso_r <= sl_tx_sr(7);
sl_tx_sr <= sl_tx_sr(6 downto 0) & '0';
sl_tx_bits <= sl_tx_bits + 1;
end if;
end if;
end if;
end if;
end process;
stim : process
variable errs : natural := 0;
variable guard : natural;
variable bus_q : std_logic_vector(31 downto 0);
variable got_q : word_array;
variable got_n : natural := 0;
variable st : std_logic_vector(31 downto 0);
variable w : std_logic_vector(31 downto 0);
procedure bus_write(a : natural; d : std_logic_vector(31 downto 0)) is
begin
wait until falling_edge(clk);
reg_addr <= to_unsigned(a, 5); reg_wdata <= d; reg_wr <= '1';
wait until falling_edge(clk);
reg_wr <= '0';
end procedure;
procedure bus_read(a : natural) is
begin
wait until falling_edge(clk);
reg_addr <= to_unsigned(a, 5); reg_rd <= '1';
wait for 1 ns;
bus_q := reg_rdata;
wait until falling_edge(clk);
reg_rd <= '0';
end procedure;
-- A status read, which pops nothing.
procedure poll_status is
begin
bus_read(A_STATUS);
end procedure;
procedure configure(dv : natural; pol : std_logic; pha : std_logic;
lsb : std_logic; nbits : natural; slave : natural;
lead : natural; lag : natural; gap : natural) is
variable d : std_logic_vector(31 downto 0);
begin
d := (others => '0');
d(0) := pol; d(1) := pha; d(2) := lsb;
d(15 downto 8) := std_logic_vector(to_unsigned(dv, 8));
d(21 downto 16) := std_logic_vector(to_unsigned(nbits, 6));
d(25 downto 24) := std_logic_vector(to_unsigned(slave, 2));
bus_write(A_CTRL, d);
d := (others => '0');
d(7 downto 0) := std_logic_vector(to_unsigned(lead, 8));
d(15 downto 8) := std_logic_vector(to_unsigned(lag, 8));
d(23 downto 16) := std_logic_vector(to_unsigned(gap, 8));
bus_write(A_TIMING, d);
sl_cpol <= pol; sl_cpha <= pha;
end procedure;
procedure collect is
begin
poll_status;
while bus_q(S_RXRDY) = '1' loop
bus_read(A_RXDATA);
got_q(got_n) := bus_q;
got_n := got_n + 1;
poll_status;
end loop;
end procedure;
procedure send(d : std_logic_vector(31 downto 0); last : std_logic) is
begin
guard := 8000;
poll_status;
while bus_q(S_TXRDY) = '0' and guard > 0 loop
poll_status;
guard := guard - 1;
end loop;
if guard = 0 then
report " FAIL: the transmit queue never became ready";
errs := errs + 1;
end if;
if last = '1' then bus_write(A_TXLAST, d);
else bus_write(A_TXDATA, d); end if;
end procedure;
-- Waits for the transfer to START and only then for it to finish.
-- Waiting only for `busy` to fall is the classic driver bug: a request
-- takes the programmed lead time to turn into a busy engine, so a poll
-- issued immediately after the queue write sees an idle master and
-- concludes the transfer is over before it has begun.
procedure wait_done is
begin
guard := 4000;
poll_status;
while bus_q(S_BUSY) = '0' and guard > 0 loop
collect;
poll_status;
guard := guard - 1;
end loop;
if guard = 0 then
report " FAIL: the master never became busy after a queue write";
errs := errs + 1;
end if;
guard := 20000;
poll_status;
while bus_q(S_BUSY) = '1' and guard > 0 loop
collect;
poll_status;
guard := guard - 1;
end loop;
for j in 1 to 6 loop wait until falling_edge(clk); end loop;
collect;
end procedure;
-- Discards anything left in the receive register from an earlier test.
procedure flush_rx is
begin
collect;
got_n := 0;
end procedure;
procedure clear_slave is
begin
sl_clr <= '1';
wait until falling_edge(clk);
wait until falling_edge(clk);
sl_clr <= '0';
wait until falling_edge(clk);
end procedure;
variable pol_v, pha_v : std_logic;
variable bad : natural;
constant ALL_HIGH : std_logic_vector(N_CS - 1 downto 0)
:= (others => '1');
begin
for j in 1 to 3 loop wait until falling_edge(clk); end loop;
rst_n <= '1';
for j in 1 to 2 loop wait until falling_edge(clk); end loop;
-- 1. RESET DEFAULTS. The master must come up inert and READABLE: a
-- register file whose reset value cannot be read back is a register
-- file nobody can debug.
bus_read(A_CTRL);
if bus_q(7 downto 0) /= x"00" or bus_q(15 downto 8) /= x"FF" or
bus_q(21 downto 16) /= "001000" then
report " FAIL: CTRL came out of reset as " & hex8(bus_q);
errs := errs + 1;
end if;
bus_read(A_TIMING);
if bus_q(7 downto 0) /= x"10" or bus_q(15 downto 8) /= x"10" or
bus_q(23 downto 16) /= x"10" then
report " FAIL: TIMING came out of reset as " & hex8(bus_q);
errs := errs + 1;
end if;
poll_status;
if bus_q(S_TXRDY) /= '1' or bus_q(S_BUSY) /= '0' or
bus_q(S_RXRDY) /= '0' then
report " FAIL: STATUS out of reset was " & hex8(bus_q);
errs := errs + 1;
end if;
if cs_n /= ALL_HIGH then
report " FAIL: reset left a chip select asserted";
errs := errs + 1;
end if;
report " out of reset: mode 0, divisor 255, eight bits, 16-cycle windows, all selects released, queue ready, not busy";
-- 2. READ-BACK OF EVERY WRITABLE FIELD.
configure(6, '1', '1', '1', 13, 2, 7, 9, 11);
bus_read(A_CTRL);
if bus_q(0) /= '1' or bus_q(1) /= '1' or bus_q(2) /= '1' or
bus_q(15 downto 8) /= x"06" or bus_q(21 downto 16) /= "001101" or
bus_q(25 downto 24) /= "10" then
report " FAIL: CTRL read back " & hex8(bus_q) & " after programming";
errs := errs + 1;
end if;
bus_read(A_TIMING);
if bus_q(7 downto 0) /= x"07" or bus_q(15 downto 8) /= x"09" or
bus_q(23 downto 16) /= x"0B" then
report " FAIL: TIMING read back " & hex8(bus_q) &
" after programming";
errs := errs + 1;
end if;
report " every writable field read back exactly as written";
-- 3. THE ERROR FLAGS MEAN WHAT THEY SAY.
configure(1, '0', '0', '0', 8, 0, 4, 4, 4); -- divisor below two
poll_status;
if bus_q(S_DIVERR) /= '1' then
report " FAIL: a divisor of 1 did not raise the divisor error";
errs := errs + 1;
end if;
configure(4, '0', '0', '0', 33, 0, 4, 4, 4); -- width above 32
poll_status;
if bus_q(S_LENERR) /= '1' then
report " FAIL: a 33-bit frame did not raise the width error";
errs := errs + 1;
end if;
configure(4, '0', '0', '0', 8, 0, 4, 4, 4);
poll_status;
if bus_q(S_DIVERR) = '1' or bus_q(S_LENERR) = '1' then
report " FAIL: a legal configuration still reports an error: " &
hex8(bus_q);
errs := errs + 1;
end if;
report " divisor 1 and width 33 each reported on their own, and a legal setting reports neither";
-- 4. A COMPLETE TRANSACTION, all four modes, checked on the pins by a
-- slave that only sees pins.
for k in 0 to 3 loop
if (k mod 2) = 1 then pol_v := '1'; else pol_v := '0'; end if;
if k >= 2 then pha_v := '1'; else pha_v := '0'; end if;
clear_slave;
sl_tx_q(0) <= x"DE"; sl_tx_q(1) <= x"AD";
sl_tx_q(2) <= x"BE"; sl_tx_q(3) <= x"EF";
configure(4, pol_v, pha_v, '0', 8, 0, 4, 4, 4);
for j in 1 to 4 loop wait until falling_edge(clk); end loop;
flush_rx;
-- Collected BETWEEN sends, because the receive side is one word deep
-- (Chapter 13.9): a driver that queues every word first and only
-- then starts reading loses all but the last one.
send(x"00000012", '0'); collect;
send(x"00000034", '0'); collect;
send(x"00000056", '0'); collect;
send(x"00000078", '1'); collect;
wait_done;
if sl_rx_n /= 4 then
report " FAIL: mode " & integer'image(k) &
" -- the slave saw " & integer'image(sl_rx_n) &
" of 4 bytes";
errs := errs + 1;
elsif sl_rx_q(0) /= x"12" or sl_rx_q(1) /= x"34" or
sl_rx_q(2) /= x"56" or sl_rx_q(3) /= x"78" then
report " FAIL: mode " & integer'image(k) &
" -- the slave saw " & hex2(sl_rx_q(0)) & " " &
hex2(sl_rx_q(1)) & " " & hex2(sl_rx_q(2)) & " " &
hex2(sl_rx_q(3));
errs := errs + 1;
end if;
if got_n /= 4 then
report " FAIL: mode " & integer'image(k) &
" -- software read " & integer'image(got_n) &
" of 4 words";
errs := errs + 1;
elsif got_q(0)(7 downto 0) /= x"DE" or
got_q(1)(7 downto 0) /= x"AD" or
got_q(2)(7 downto 0) /= x"BE" or
got_q(3)(7 downto 0) /= x"EF" then
report " FAIL: mode " & integer'image(k) &
" -- software read " & hex2(got_q(0)(7 downto 0)) & " " &
hex2(got_q(1)(7 downto 0)) & " " &
hex2(got_q(2)(7 downto 0)) & " " &
hex2(got_q(3)(7 downto 0));
errs := errs + 1;
end if;
end loop;
report " all four modes: the slave received 12 34 56 78 and software read back de ad be ef, over one chip select each";
-- 5. LSB-FIRST, ON THE WIRE. The slave assembles MSB-first, so an
-- LSB-first master must make it see the bit-reversed byte -- the only
-- way to prove the setting reached the pins.
clear_slave;
configure(4, '0', '0', '1', 8, 0, 4, 4, 4);
for j in 1 to 4 loop wait until falling_edge(clk); end loop;
flush_rx;
send(x"0000008D", '1');
wait_done;
if sl_rx_n /= 1 or sl_rx_q(0) /= x"B1" then
report " FAIL: LSB-first 0x8d -- the slave recorded " &
integer'image(sl_rx_n) & " bytes, the first being " &
hex2(sl_rx_q(0)) & ", expected 1 and b1";
errs := errs + 1;
end if;
report " LSB-first: software sent 8d and the MSB-first slave saw b1 -- the reversal happened on the wire";
-- 6. ABORT THROUGH THE COMMAND REGISTER, mid-transfer, and recovery.
clear_slave;
configure(8, '0', '0', '0', 8, 0, 4, 6, 6);
for j in 1 to 4 loop wait until falling_edge(clk); end loop;
flush_rx;
send(x"000000AA", '0'); collect;
send(x"00000055", '0');
for j in 1 to 40 loop wait until falling_edge(clk); end loop;
bus_write(A_CMD, x"00000001");
guard := 4000;
poll_status;
while bus_q(S_BUSY) = '1' and guard > 0 loop
poll_status;
guard := guard - 1;
end loop;
for j in 1 to 10 loop wait until falling_edge(clk); end loop;
poll_status;
st := bus_q;
if st(S_ABORTD) /= '1' then
report " FAIL: an abort written to CMD did not set the abort flag";
errs := errs + 1;
end if;
if st(S_TRUNC) /= '1' then
report " FAIL: an abort mid-byte did not report a truncated word";
errs := errs + 1;
end if;
-- A truncated word that got nowhere is a contradiction, and it is
-- exactly what a request serviced twice produces.
if st(S_TRUNC) = '1' and st(21 downto 16) = "000000" then
report " FAIL: a truncated word was reported as having got 0 bits out";
errs := errs + 1;
end if;
if st(S_BUSY) = '1' then
report " FAIL: the master was still busy long after the abort";
errs := errs + 1;
end if;
if cs_n /= ALL_HIGH then
report " FAIL: the abort left a chip select asserted";
errs := errs + 1;
end if;
report " abort written to CMD: flagged, the partial word reported as truncated (" &
integer'image(to_integer(unsigned(st(21 downto 16)))) &
" bits made it), every select released";
-- The flags must clear on command, and only on command.
poll_status;
if bus_q(S_ABORTD) /= '1' or bus_q(S_TRUNC) /= '1' then
report " FAIL: the sticky flags cleared themselves on a status read";
errs := errs + 1;
end if;
bus_write(A_CMD, x"00000002");
for j in 1 to 2 loop wait until falling_edge(clk); end loop;
poll_status;
if bus_q(S_ABORTD) = '1' or bus_q(S_TRUNC) = '1' then
report " FAIL: writing the clear bit left " & hex8(bus_q);
errs := errs + 1;
end if;
report " the sticky flags survive a status read and clear only on an explicit command";
-- 7. THE FLASH TRANSACTION. READ (0x03), a 24-bit address and four data
-- bytes, under one chip select, driven the way a driver drives it.
clear_slave;
sl_tx_q(0) <= x"FF"; sl_tx_q(1) <= x"FF";
sl_tx_q(2) <= x"FF"; sl_tx_q(3) <= x"FF";
sl_tx_q(4) <= x"C0"; sl_tx_q(5) <= x"FF";
sl_tx_q(6) <= x"EE"; sl_tx_q(7) <= x"01";
configure(4, '0', '0', '0', 8, 1, 5, 5, 8);
for j in 1 to 4 loop wait until falling_edge(clk); end loop;
flush_rx;
send(x"00000003", '0'); collect; -- READ
send(x"00000001", '0'); collect; -- address 0x012345
send(x"00000023", '0'); collect;
send(x"00000045", '0'); collect;
send(x"00000000", '0'); collect; -- four bytes to clock data out
send(x"00000000", '0'); collect;
send(x"00000000", '0'); collect;
send(x"00000000", '1'); collect;
wait_done;
if sl_rx_n /= 8 then
report " FAIL: the flash transaction delivered " &
integer'image(sl_rx_n) & " of 8 bytes";
errs := errs + 1;
elsif sl_rx_q(0) /= x"03" or sl_rx_q(1) /= x"01" or
sl_rx_q(2) /= x"23" or sl_rx_q(3) /= x"45" then
report " FAIL: the flash saw command " & hex2(sl_rx_q(0)) &
" address " & hex2(sl_rx_q(1)) & hex2(sl_rx_q(2)) &
hex2(sl_rx_q(3));
errs := errs + 1;
end if;
if got_n /= 8 then
report " FAIL: software read " & integer'image(got_n) &
" of 8 words from the flash read";
errs := errs + 1;
elsif got_q(4)(7 downto 0) /= x"C0" or got_q(5)(7 downto 0) /= x"FF" or
got_q(6)(7 downto 0) /= x"EE" or got_q(7)(7 downto 0) /= x"01" then
report " FAIL: the data phase returned " &
hex2(got_q(4)(7 downto 0)) & " " &
hex2(got_q(5)(7 downto 0)) & " " &
hex2(got_q(6)(7 downto 0)) & " " &
hex2(got_q(7)(7 downto 0));
errs := errs + 1;
end if;
poll_status;
if bus_q(S_OVR) = '1' or bus_q(S_TRUNC) = '1' or
bus_q(S_ABORTD) = '1' or bus_q(S_SELERR) = '1' or
bus_q(S_LENERR) = '1' or bus_q(S_DIVERR) = '1' then
report " FAIL: the flash transaction finished with status " &
hex8(bus_q);
errs := errs + 1;
end if;
report " flash READ 0x03 of address 012345 on slave 1: command and address arrived intact under one chip select, and the four data bytes came back as c0 ff ee 01 with every error flag clear";
errors <= errs;
if errs = 0 then
report "PASS: the complete master is driven entirely through its register interface -- it comes out of reset inert and fully readable, every writable field reads back as written, and each of the divisor, width and slave errors is reported on its own while a legal configuration reports none -- four-byte transactions in all four modes deliver the right bytes to a slave that only sees pins and return the right words to software, LSB-first genuinely reverses the bits on the wire, an abort written to the command register stops the transfer, flags the truncated word with its partial bit count, releases every chip select and leaves the engine able to run the next transfer, the sticky flags survive a status read and clear only on command, and a flash READ of command plus three address bytes plus four data bytes completes under a single unbroken chip select with every error flag clear";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
halt <= true;
wait;
end process;
end architecture;Parity
All three implementations are driven entirely through the register interface — no internal signal is poked anywhere — and all three: come out of reset inert and fully readable; read back every writable field as written; report the divisor, width and slave errors each on their own while a legal configuration reports none; deliver 12 34 56 78 to a pin-level slave and read back de ad be ef in all four modes; send 0x8D LSB-first and have an MSB-first slave see 0xB1; abort mid-byte with six bits reported as having made it, every select released and the engine still working afterwards; keep the sticky flags across a status read and clear them only on command; and complete a flash READ of 0x03 plus a 24-bit address plus four data bytes under a single unbroken chip select with every error flag clear.
6. Why a Verification Engineer Cares
// A top-level assertion set should not restate the blocks' properties: they are
// already bound to the blocks. What belongs here is the REGISTER CONTRACT, which
// no block can state, and the pin invariants that are properties of the assembly
// rather than of any one part.
module spi_master_top_sva #(
parameter int MAX_W = 32,
parameter int N_CS = 4
) (
input logic clk,
input logic rst_n,
input logic [4:0] reg_addr,
input logic reg_wr,
input logic reg_rd,
input logic [31:0] reg_wdata,
input logic [31:0] reg_rdata,
input logic sclk,
input logic mosi,
input logic miso,
input logic [N_CS-1:0] cs_n
);
localparam [4:0] A_CTRL = 5'h00, A_TIMING = 5'h04, A_TXDATA = 5'h08,
A_TXLAST = 5'h0C, A_RXDATA = 5'h10, A_STATUS = 5'h14,
A_CMD = 5'h18;
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// A write to a configuration register must be readable back. Stated for the
// whole register rather than field by field, because a field that reads back
// in the wrong bit position is exactly the failure a concatenated status
// word produces.
property p_readback(addr, mask);
logic [31:0] w;
(reg_wr && reg_addr == addr, w = reg_wdata & mask) |=>
##[1:$] (reg_rd && reg_addr == addr) |-> (reg_rdata & mask) == w;
endproperty
a_ctrl_readback: assert property (p_readback(A_CTRL, 32'h0303_FF07));
a_timing_readback: assert property (p_readback(A_TIMING, 32'h00FF_FFFF));
// STATUS is read-only: a write to it must change nothing that is readable.
a_status_ro: assert property (
reg_wr && reg_addr == A_STATUS |=> $stable(reg_rdata) || reg_rd
);
// A read of RXDATA is the ONLY read with a side effect. Every other read
// must leave the receive-ready bit alone.
a_only_rxdata_pops: assert property (
reg_rd && reg_addr != A_RXDATA |=> $stable(rx_ready)
);
// Reset defaults must be INERT: the slowest legal divisor and generous
// timing. A design that comes up fast can violate a slave before software
// runs, and this is the only place that can be asserted.
a_reset_inert: assert property (
$rose(rst_n) |-> ##1 (cfg_div >= 8'd128) && (cfg_lead >= 8'd8)
);
// Pin invariants that belong to the assembly. At most one select low, ever.
a_one_hot: assert property ($countones(~cs_n) <= 1);
// No clock edge with nothing selected -- requirement R1 of Chapter 13.1,
// asserted at the only level where both signals exist as pins.
a_no_edge_deselected: assert property (
&cs_n |-> $stable(sclk)
);
// MISO must not reach any output pin combinationally. Not expressible as a
// temporal assertion -- it is a structural property -- so it is stated here
// as a comment and checked by lint. Worth writing down at the point a
// reviewer will look for it.
// lint: assert no_comb_path(miso -> {mosi, sclk, cs_n})
endmodule// At the top level the coverage question changes. The blocks' internal cases are
// covered by their own models; what needs covering here is the CONFIGURATION
// SPACE as software can reach it, and the register access PATTERNS that a driver
// will actually produce.
covergroup cg_master_top @(posedge clk);
// The four modes must each carry real traffic, not merely be written.
mode: coverpoint {cfg_cpol, cfg_cpha} iff (frame_start) {
bins mode0 = {2'b00};
bins mode1 = {2'b10};
bins mode2 = {2'b01};
bins mode3 = {2'b11};
}
// Transaction shapes, named after the things drivers actually do. A suite
// that only sends single bytes has not tested the master a flash needs.
shape: coverpoint txn_shape iff (txn_end) {
bins single_byte = {1};
bins cmd_plus_data = {2};
bins cmd_addr_data = {[5:8]};
bins page_sized = {[9:$]};
}
// Which register was written, and in what order relative to a transaction.
// A configuration write DURING a transaction is legal and must be seen.
wr_when: coverpoint {wr_addr, busy} iff (reg_wr) {
bins ctrl_idle = {{5'h00, 1'b0}};
bins ctrl_busy = {{5'h00, 1'b1}};
bins timing_idle = {{5'h04, 1'b0}};
bins timing_busy = {{5'h04, 1'b1}};
bins txdata = {{5'h08, 1'b0}, {5'h08, 1'b1}};
bins txlast = {{5'h0C, 1'b0}, {5'h0C, 1'b1}};
bins cmd_abort = {{5'h18, 1'b1}};
bins cmd_idle = {{5'h18, 1'b0}};
}
// Each error flag must have been raised ALONE. A configuration that trips
// two at once cannot show that either is wired correctly.
solo_error: coverpoint status[9:7] iff (|status[9:7]) {
bins sel_only = {3'b001};
bins len_only = {3'b010};
bins div_only = {3'b100};
}
// And the clean case, because a status register that always reports
// something is a status register nobody reads.
all_clear: coverpoint (status[9:4] == 6'b000000) iff (txn_end) {
bins clean = {1};
}
// Whether the slave model that observed this transaction was PIN-LEVEL.
// A suite whose every check came from a strobe-driven model cannot find a
// fault in the strobes, and this coverpoint is what records that.
observer: coverpoint slave_is_pin_level iff (txn_end) {
bins pin_level = {1};
bins strobe_fed = {0};
}
x_mode_shape: cross mode, shape;
x_shape_observer: cross shape, observer;
endgroup7. Why an FPGA or ASIC Engineer Cares
The whole master is roughly 250 flops. Counting them by block at the defaults:
13.4 divider 13
13.5 mode logic 12
13.6 datapath 68 two 32-bit registers plus counters
13.8 width and order 0 entirely combinational
13.7 chip select 20
13.9 streaming 70 shadow plus receive holding register
13.10 abort policy 10
top register file 50
---------------------------
243The two 32-bit shift registers and the two 32-bit holding registers are two-thirds of it, which is worth knowing: a design that only ever needs byte transfers can set MAX_W = 8 and the master drops to about 100 flops without any structural change.
Timing closure has exactly one candidate. shift_en gates the divider and is the only signal in the design on a path that matters at the bit rate. Everything else is once-per-frame. If the master fails timing, that is where to look, and one-hot encoding the chip-select controller's state makes it a flop output rather than a decode.
The combinational blocks are the two barrel shifters, and both are off the per-bit path. The transmit-side alignment and reversal are evaluated on the load cycle; the receive-side reversal on the read. If either ever mattered, the receive side can be registered for free — the received word is stable from rx_valid_stb until the next load.
Lint and CDC are clean by construction rather than by effort. One clock means no CDC report at all. No latches, because every combinational block assigns every output on every path. No unconstrained paths, because MISO enters a flop. Those are the three reports that usually consume a week, and the reason they are empty is the six rules of §3 rather than any cleanup.
8. The RTL Review Checklist
This is the review that would gate the design, in the order a reviewer should ask. Each item names the chapter that answers it, so a "no" has somewhere to go.
Clocking and reset
[ ] one clock domain, and SCLK is not a clock 13.4
[ ] nothing clocked on SCLK 13.4
[ ] async reset on every flop that drives a pin 13.10
[ ] reset defaults leave the master inert, not just defined 13.11
[ ] reset releases chip select with no clock edge 13.10Pins
[ ] MOSI is a flop, not a mux off the shift register 13.6
[ ] MISO enters one flop and reaches no output pin 13.6
[ ] at most one select asserted, ever 13.7
[ ] no SCLK edge with nothing selected 13.1
[ ] no clock edge after the select is released, on abort 13.10Timing intervals
[ ] lead, lag and gap are programmable and MEASURED 13.7
[ ] intervals are checked two-sided: not short, not long 13.7
[ ] counters load with the interval minus one 13.7
[ ] the select is held across every frame of a transaction 13.7Configuration
[ ] configuration is snapshotted at frame start 13.2
[ ] all fields snapshot together, on one condition 13.2
[ ] a mid-transfer write is accepted and REPORTED 13.2
[ ] every writable field reads back 13.11
[ ] illegal divisor, width and select are reported 13.4 13.8 13.7Data path
[ ] two shift registers, not one circulating 13.6
[ ] the receive register is cleared on load 13.6
[ ] bit order is one self-inverse transform at the boundaries 13.8
[ ] the valid pulse coincides with complete data 13.6
[ ] a narrow frame leaves zeros above it 13.6Modes
[ ] the launch and capture assignment depends on CPHA alone 13.5
[ ] CPHA=0 preloads, and the preload ADVANCES the register 13.5
[ ] CPHA=0's final launch is suppressed 13.5
[ ] the frame-done level is not asserted on the start cycle 13.5Streaming and stopping
[ ] the shadow is freed at load, not at frame completion 13.9
[ ] a stall is reported and is not treated as an error 13.9
[ ] an overrun is sticky and clears only on command 13.9
[ ] the transaction tail is not counted as a stall 13.9
[ ] the abort request is edge-qualified 13.10
[ ] the abort reports a partial bit count, and it is non-zero
whenever truncation is flagged 13.10
[ ] the engine recovers after every abort 13.10Verification
[ ] at least one slave model is PIN-LEVEL and shares nothing
with the design 13.11
[ ] bit order is checked on the WIRE, not in loopback 13.8
[ ] both directions are compared against an independent pattern 13.6
[ ] every error flag has been raised ALONE 13.1
[ ] coverage bins margins relative to limits, not absolute values 13.19. Failure Signature — A Master That Passes Every Test And Fails Review
Symptom. A master passes its entire regression: all four modes, every width, loopback and independent-slave data checks, abort at every bit position, measured intervals within limits. It is submitted for review and rejected on three items.
This is worth working through because it is the most common outcome of a design that was verified without being reviewed, and none of the three failures is a functional bug.
Item 1: SCLK is counter[2]. It works — the frequency is right and the duty cycle is even at power-of-two divisors. It is a divided clock, so synthesis infers a clock tree, the tool asks for a create_generated_clock constraint, and anything a future engineer clocks on it is in a second domain whose frequency is a runtime parameter. The fix is a registered output, which also makes odd divisors possible — so the reviewer's objection fixes a functional limitation the tests never exercised because the suite only used even divisors.
Item 2: the receive register is read directly by software. It works, as long as software reads between frames. During a frame the register is being shifted, so a read that lands mid-frame returns a partially-shifted word — and in a streaming transaction there is no "between frames" for software to aim at. The tests never caught it because the testbench polled a status bit before reading, which a real driver under interrupt load will not do reliably. The fix is a holding register, which also makes the overrun flag meaningful.
Item 3: cs_n is synchronously reset. It works in every test, because every test has a running clock. The reviewer's objection is about the case the tests cannot contain: a reset asserted when the clock has stopped. The fix is one attribute on one always block.
What the three have in common. All three are cases where the test environment is more forgiving than the real one — even divisors, a polling driver, a running clock — and none of them can be found by a suite that does not deliberately model the environment's hostility. That is what a review is for, and it is why the checklist in §8 is organised by property rather than by block: a property that no test can reach still has to be argued for.
The lesson for a suite. Every item in §8 that a test cannot check is an item that needs a reviewer, and knowing which is which is worth writing down. Three in that list are unreachable by simulation: the reset-with-no-clock case, the no-combinational-path-from-MISO case, and the SCLK-is-not-a-clock case. Everything else in the list is checkable, and everything checkable should be checked rather than reviewed.
10. Common Misconceptions
"A top level with no logic in it is a sign the design was over-partitioned." It is a sign the decisions were made where they belong. The test is whether any block could be replaced independently — and here each can, because each was verified against pins rather than against its neighbours.
"A register map is an implementation detail that can be changed later." It is the only part of the design software depends on, and it is the part that cannot be changed later without changing every driver. The two-address queue and the read-only status register are both decisions that would be expensive to reverse.
"Reset defaults only need to be defined, not safe." The first thing many drivers do is read a device ID, and they do it before configuring anything. A default of the fastest divisor means that read happens at a frequency the slave may not support.
"If the suite passes, the design is done." Three properties in §8 cannot be reached by simulation at all — reset with no clock, no combinational path from MISO, and SCLK not being a clock — and all three are design-review items. A passing suite is necessary and not sufficient, and knowing precisely which properties it cannot reach is more useful than assuming it reaches everything.
"A pin-level slave model is a nice-to-have when an ideal one is easier." It is the only component in the suite capable of finding a fault in the design's own abstractions, and it found one here that eleven unit testbenches missed. It is not a cleaner model — it is a differently founded one, and that is the point.
11. Reason It Through
Why two register addresses for the transmit queue rather than a flag bit?
Because at 32 bits of data there is no bit to spare, because a separate flag register is two bus cycles and a race, and because forgetting to use a different address is a more visible mistake than forgetting to set a bit. The consequence of forgetting is a transaction left open with the slave selected, so making the mistake visible at the call site is worth an address bit.
Why is the status word assembled by field rather than by concatenation?
Because a concatenation must be exactly 32 bits and the padding depends on the parameters, so a parameter change silently misaligns every flag above the changed field. With LEN_W at its default the padding expressions come out zero bits wide, which is not even legal — and indexed part-selects into a zeroed word cannot drift no matter what the parameters do.
What breaks if the register file's outputs feed the SCLK-rate logic directly, with no snapshot?
A configuration write lands mid-frame and changes the bit period, the width or the mode partway through a transfer. The frame's boundaries move, the slave is out of step, and the transfer completes returning plausible garbage. Because the corruption depends on when the write landed relative to the frame, it is not reproducible — which is Chapter 13.2's subject and the reason the snapshot exists.
Which properties in the review checklist can a simulation not check, and why?
Three. Reset with no clock running, because a testbench that stops the clock stops the simulation's notion of time and cannot then observe anything. The absence of a combinational path from MISO to an output, because that is a structural property of the netlist rather than a behaviour. And SCLK not being a clock, because functionally a divided clock and a registered output are indistinguishable — the difference is what the synthesis tool infers. All three are review items, and writing down that they are is more useful than hoping a test covers them.
A design passes every functional test and the reviewer rejects three items, none of which is a functional bug. What do the three have in common?
Each is a case where the test environment is more forgiving than the real one: only even divisors were used, so a divided clock looked fine; the testbench polled before reading, so a directly-read shift register looked fine; every test had a running clock, so a synchronous reset on the select pins looked fine. A suite tests the design against the environment it models, and a review tests the design against the environment it will meet — which is why the checklist is organised by property rather than by block.
12. Understanding Check
13. Summary
The top level is seven instances and a register file, and that is the test of the partition: every decision worth arguing about was made in a block, and logic at the top would mean one was made in the wrong place.
The register map's three defended decisions are two addresses for one queue — no data bit to spare, no two-cycle race, and a mistake that is visible at the call site; a read-only and complete status register, reporting the errors that mean the driver is wrong as well as the ones it is expected to handle; and reset defaults that leave the master inert, because many drivers read a device ID before configuring anything.
Six rules make it synthesisable: one clock with nothing clocked on SCLK, one asynchronous reset on every pin-driving flop, no latches, no gated or generated clocks, no combinational path from MISO to any output, and no simulation constructs outside guards. The last two are the ones that fail review, and both fail because of features added later — a loopback mode muxing MISO to MOSI, or a debug bit driving SCLK.
A pin-level slave model found a bug eleven unit testbenches missed: a runt ninth clock pulse on every CPHA=1 frame, invisible from inside the master because every internal count correctly agreed it was not there. A testbench built from the design's own abstractions cannot find bugs in those abstractions, and the insurance is one model that shares nothing with the design.
The whole master is about 250 flops, two-thirds of them the four 32-bit registers — so a byte-only configuration drops to roughly 100 without structural change. One signal matters for timing: shift_en, which gates the divider. Lint and CDC are empty by construction rather than by cleanup.
The review checklist is organised by property rather than by block, and three of its items cannot be checked by simulation at all: reset with the clock stopped, the absence of a combinational MISO path, and SCLK not being a clock. Knowing which properties a suite cannot reach is more useful than assuming it reaches everything — and the three functional-looking review rejections of §9 all came from the test environment being more forgiving than the real one.
14. What Comes Next
This closes Module 13, and with it the first complete design in the track. Thirteen modules have gone from what SPI is, through what a device expects, to a master that produces it — verified in three languages, reviewed against the properties a simulation cannot reach, and driven the way software will drive it.
What the module did not build is the other end of the wire. A master decides when everything happens; a slave is told, and has to recover every boundary from pins it does not control — no clock of its own, no say in when chip select falls, and a setup time it must meet against an edge that arrives whenever the master feels like it. Every simplification this module enjoyed by owning the clock is a problem there, and the shift in perspective is the point: the next module builds the device that has to survive a master like the one just designed.
Continue learning
Related tutorials
- Related topic
Synthesizable Slave Architecture and RTL Review
Nine blocks assembled and wired to the master of Chapter 13.11 byte for byte, with nothing in between. They interoperate on the first attempt — and then integration finds two things neither module's own bench could: a truncated last half-period at every divisor, and a master's MOSI arriving one cycle after the edge that launched it.
- Related topic
Mode Mismatch and Its Failure Signature
What happens when the two ends disagree about the mode. The distinct signature each mismatch produces, how to tell polarity from phase disagreement from the data alone, and the monitor and coverage work that catches it.
- Related topic
Command Encoding and Register Access
How a command byte packs direction, auto-increment and a register address, why the polarity of the read/write bit differs between parts and silently turns reads into destructive writes, and the codec that encodes and decodes any convention.
- Related topic
The System-Side Interface
A slave cannot ask the master to wait and cannot choose when its buffer changes, so it needs two mechanisms and neither is a buffer: a configured transmit default that makes an underrun recognisable at the master, and a sequence lock that makes a torn read detectable rather than merely unlikely.
