SPI · Module 10
How to Read an SPI Device Datasheet
A repeatable four-pass route through any SPI peripheral datasheet — pins, timing, commands, registers — ending in the seven numbers a controller needs, and the register block that validates them atomically so no half-applied profile can ever be in force.
Every chapter so far has been about SPI. This module is about your device — and the skill it teaches transfers to parts this tutorial will never mention.
You are handed a 90-page datasheet for a part you have never used and asked how long it will take to bring up. What do you read, in what order, and what do you write down?
There is a route. It has four passes, it takes about an hour, and it ends with a small table of numbers that is the entire specification your RTL needs.
1. Why a Route Is Needed
An SPI peripheral datasheet is not organised for the person bringing it up. It is organised for the person selling it: features first, then electrical characteristics, then — somewhere past the middle — the things you actually need. The command table is often an appendix.
Worse, SPI defines no frame format, so nothing about how to talk to the part is implied by the protocol. Every one of the following must be found:
the mode CPOL and CPHA → Chapter 10.2
the rate maximum SCLK → Chapter 10.3
the timing t_SU, t_HD, t_V → Chapter 10.3
the commands opcodes and encoding → Chapter 10.4
the address width, order → Chapter 10.5
the latency dummy cycles → Chapter 10.5
the registers map and semantics → this chapter, §5Miss any one and the part does not work — and, as Chapter 3.8 showed, it may not fail obviously.
The route below finds all seven, in an order chosen so that each pass narrows what you need from the next.
2. Pass One — The Interface
Goal: confirm it is SPI at all, and find out what kind.
Read only the pin table and the block diagram. Answer four questions:
- How many data pins? Two (MOSI, MISO) is classic SPI. One bidirectional pin is 3-wire mode, which changes the turnaround handling entirely. Four is Quad SPI. A part supporting several must be told which to use.
- Is chip select active low? Almost always, but a part with an active-high select exists and will be inverted relative to every assumption in your controller.
- Are there extra pins? A
HOLD,RESET,WPorBUSYpin is part of the interface even though SPI does not define it. ABUSYoutput in particular changes the driver profoundly — it removes the need for status polling. - What supply, and is it the same as your controller's? A 1.8 V part on a 3.3 V bus needs level shifting, and level shifters add delay to the round-trip budget.
Stop when you can draw the connection. Anything else on the page is for pass two or later.
3. Pass Two — The Timing Diagram and Table
Goal: the mode and the rate.
Find the timing diagram — the one with SCLK, CS and the data lines drawn against labelled intervals. This single figure carries two of the seven numbers, and Chapter 10.2 is devoted to reading the mode off it correctly, because the datasheet very often does not state CPOL and CPHA in words.
Then find the timing table beneath it and extract, with their conditions:
f_SCLK(max) and the load and voltage it assumes
t_SU input setup, data to sampling edge
t_HD input hold
t_V output valid, sampling edge to MISO stable
t_CSS / t_CSH chip-select lead and lag
t_CS(high) minimum deselect between framesChapter 10.3 turns those into a constraint set. For now, write down the conditions along with the numbers — a maximum SCLK quoted at 15 pF and 3.3 V is not the same number on a board presenting 40 pF at 1.8 V.
The trap in this pass is a single maximum frequency on the front page that applies only to one command. Parts commonly run fast reads at 104 MHz and the plain read at 50 MHz or less. Check which opcode each rate belongs to.
4. Pass Three — The Command Table
Goal: the opcodes and their shapes.
The command table is the heart of the datasheet and is usually the least prominent part of it. For each command you intend to use, record four things:
opcode the byte itself
address how many bytes, or none
dummy how many CYCLES, or none
data direction and length rulesThat four-field shape is exactly the transaction specification Chapter 10.6 builds, and recording it in that form from the start saves translating later.
Two things to watch for:
- Dummy latency is quoted in cycles, not bytes. A "6 dummy cycles" entry does not mean a dummy byte, and Chapter 10.5 shows what sending one does to the payload.
- Some commands are only legal in certain states. A page program that must be preceded by a write-enable is not optional politeness; the write is silently discarded without it.
5. Pass Four — The Register Map
Goal: what the device will actually tell you.
Only now is the register map worth reading, because pass three told you how to reach it. Three registers matter more than the rest:
- The identification register. Whatever it is called — device ID, manufacturer ID, WHO_AM_I — this is the register that proves the link works. It has a known constant value, it needs no configuration, and reading it correctly exercises the mode, the rate, the command encoding and the dummy count all at once. It is always the first transaction to bring up.
- The status register. Busy flags, write-enable latches, error bits. This is the register the driver polls.
- The configuration registers that change the interface itself — a part that can switch address width, dummy count or output drive strength can invalidate your profile at runtime, which is worth knowing before it happens.
6. The Route, and What It Produces
The output of the whole route is seven numbers. That is genuinely all a controller needs:
mode 0..3 from pass 2
divisor integer from pass 2, via Chapter 9.4's ceiling
addr_bytes 0..4 from pass 3
dummy_cycles 0..32 from pass 3
page_bits exponent from pass 3, for writes
opcodes a small table from pass 3
id_expected a constant from pass 4Which is why the rest of this chapter builds the thing that holds those numbers and refuses the ones that cannot be right.
7. Building the Device Profile — Three HDLs
The circuit
Circuit. A small register file with a validator in front of it.
State. The five numeric profile fields, plus a profile_ok flag and an error code.
Datapath. No arithmetic beyond range comparisons. Each field is checked against what the controller can implement, and each check owns one bit of the error code — so software reads a single register and learns which datasheet number it entered wrongly, instead of only that something was wrong.
Control. A single load pulse validates and either applies everything or applies nothing. There is no partial state.
Clock and reset. System clock; asynchronous active-low reset. Reset loads the safe profile — slowest divisor, widest address, no dummy, no page rule — with profile_ok low, so an un-initialised controller cannot transact at all, let alone overclock a device.
Enables. profile_ok gates every transaction elsewhere in the controller. It is the one signal that says these numbers were checked.
Timing. Everything is registered off load; the outputs are stable one cycle later and change only on another load.
Synthesis. Around thirty flip-flops and four comparators. The cost is negligible and the property it buys — that no unvalidated profile can ever be in force — is not obtainable in software.
Limitations. It validates ranges, not truth. A perfectly in-range profile that misreads the datasheet is accepted, because no hardware can check a number against a PDF.
// spi_device_profile.sv
//
// Chapter 10.1 -- the datasheet as a checked data structure.
//
// Four numbers decide how a master must talk to a device: the mode, the
// clock divisor, how many address bytes a command carries, and how many
// dummy cycles sit between address and data. This block holds those
// numbers, validates them against what the controller can actually
// implement, and applies them ATOMICALLY -- a profile with any illegal
// field is rejected whole, leaving the previous profile in force.
//
// The atomicity is the point. A block that applied its good fields and
// rejected its bad ones would leave the controller describing no device
// at all -- the new address width beside the old dummy count -- which is
// worse than either profile on its own.
module spi_device_profile #(
parameter int DIV_W = 8,
parameter int DUMMY_W = 6,
parameter int MIN_DIV = 2, // fastest divisor this master supports
parameter int MAX_DUMMY = 32,
parameter int MAX_ADDR_BYTES = 4,
parameter int PAGE_W = 4,
parameter int MAX_PAGE_BITS = 12 // 4 KB, the largest page worth naming
) (
input logic clk,
input logic rst_n,
input logic load, // pulse: validate and apply cfg_*
input logic [1:0] cfg_mode,
input logic [DIV_W-1:0] cfg_div,
input logic [2:0] cfg_addr_bytes,
input logic [DUMMY_W-1:0] cfg_dummy,
input logic [PAGE_W-1:0] cfg_page_bits, // page = 2**page_bits, 0 = none
output logic [1:0] mode,
output logic [DIV_W-1:0] div,
output logic [2:0] addr_bytes,
output logic [DUMMY_W-1:0] dummy,
output logic [PAGE_W-1:0] page_bits,
output logic profile_ok, // a validated profile is in force
output logic [3:0] err // which field was rejected
);
localparam int ERR_DIV = 0;
localparam int ERR_ADDR = 1;
localparam int ERR_DUMMY = 2;
localparam int ERR_PAGE = 3;
logic [3:0] err_c;
// Validation is combinational and per-field. Each bit names the field
// that is wrong, so software reads one register and learns which
// datasheet number it mis-entered -- rather than being told only that
// something was invalid.
//
// The comparisons are written on int' casts rather than sized literals:
// a width cast of a parameter that does not fit silently truncates, and
// a truncated bound is a check that always passes.
always_comb begin
err_c = 4'b0000;
if (int'(cfg_div) < MIN_DIV) err_c[ERR_DIV] = 1'b1;
if (int'(cfg_addr_bytes) > MAX_ADDR_BYTES) err_c[ERR_ADDR] = 1'b1;
if (int'(cfg_dummy) > MAX_DUMMY) err_c[ERR_DUMMY] = 1'b1;
// A page is either absent (0) or between 16 bytes and 4 KB. The
// values 1..3 describe pages of 2, 4 and 8 bytes, which no real
// device has and which nearly always means a byte COUNT was entered
// where an exponent was wanted -- the single most common way this
// field is filled in wrongly. Note the field must be wide enough to
// hold the exponent: a 256-byte page is 8, so three bits cannot
// express the most common page size there is.
if (cfg_page_bits != {PAGE_W{1'b0}} &&
(int'(cfg_page_bits) < 4 || int'(cfg_page_bits) > MAX_PAGE_BITS))
err_c[ERR_PAGE] = 1'b1;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// Reset is the safe profile, not a plausible one: slowest clock,
// widest address, no dummy cycles, no page rule -- and
// profile_ok low, so nothing may issue a transaction with it.
mode <= 2'd0;
div <= {DIV_W{1'b1}};
addr_bytes <= 3'(MAX_ADDR_BYTES);
dummy <= {DUMMY_W{1'b0}};
page_bits <= {PAGE_W{1'b0}};
profile_ok <= 1'b0;
err <= 4'b0000;
end else if (load) begin
err <= err_c;
if (err_c == 4'b0000) begin
mode <= cfg_mode;
div <= cfg_div;
addr_bytes <= cfg_addr_bytes;
dummy <= cfg_dummy;
page_bits <= cfg_page_bits;
profile_ok <= 1'b1;
end else begin
// Rejected whole. Every field keeps its previous value and
// profile_ok is cleared, so the controller cannot transact
// with a profile nobody validated.
profile_ok <= 1'b0;
end
end
end
endmodule// spi_device_profile_tb.sv
//
// The checks that matter are not "a good profile loads". They are:
// * every illegal field is named individually,
// * a rejected load changes NOTHING (atomicity),
// * and a good load after a bad one recovers completely.
`timescale 1ns/1ps
module spi_device_profile_tb;
localparam int DIV_W = 8;
localparam int DUMMY_W = 6;
localparam int PAGE_W = 4;
logic clk = 1'b0;
logic rst_n = 1'b0;
always #5 clk = ~clk;
logic load = 1'b0;
logic [1:0] cfg_mode = 2'd0;
logic [DIV_W-1:0] cfg_div = 8'd4;
logic [2:0] cfg_addr_bytes = 3'd3;
logic [DUMMY_W-1:0] cfg_dummy = 6'd8;
logic [PAGE_W-1:0] cfg_page_bits = 4'd8;
logic [1:0] mode;
logic [DIV_W-1:0] div;
logic [2:0] addr_bytes;
logic [DUMMY_W-1:0] dummy;
logic [PAGE_W-1:0] page_bits;
logic profile_ok;
logic [3:0] err;
int errors = 0;
// Snapshot registers -- declared at module scope. A declaration with an
// initialiser inside a procedural block is a STATIC initialiser in
// SystemVerilog, evaluated once at time zero, which would make every
// atomicity check compare against the reset value.
logic [1:0] s_mode;
logic [DIV_W-1:0] s_div;
logic [2:0] s_addr;
logic [DUMMY_W-1:0] s_dummy;
logic [PAGE_W-1:0] s_page;
spi_device_profile #(
.DIV_W(DIV_W), .DUMMY_W(DUMMY_W),
.MIN_DIV(2), .MAX_DUMMY(32), .MAX_ADDR_BYTES(4),
.PAGE_W(PAGE_W), .MAX_PAGE_BITS(12)
) dut (
.clk(clk), .rst_n(rst_n), .load(load),
.cfg_mode(cfg_mode), .cfg_div(cfg_div),
.cfg_addr_bytes(cfg_addr_bytes), .cfg_dummy(cfg_dummy),
.cfg_page_bits(cfg_page_bits),
.mode(mode), .div(div), .addr_bytes(addr_bytes),
.dummy(dummy), .page_bits(page_bits),
.profile_ok(profile_ok), .err(err)
);
task automatic snapshot;
begin
s_mode = mode; s_div = div; s_addr = addr_bytes;
s_dummy = dummy; s_page = page_bits;
end
endtask
task automatic check_unchanged(input string what);
begin
if (mode != s_mode || div != s_div || addr_bytes != s_addr ||
dummy != s_dummy || page_bits != s_page) begin
$display(" FAIL: %s altered the profile in force", what);
errors++;
end
end
endtask
task automatic do_load(input logic [1:0] m, input logic [DIV_W-1:0] d,
input logic [2:0] ab, input logic [DUMMY_W-1:0] du,
input logic [PAGE_W-1:0] pb);
begin
@(negedge clk);
cfg_mode = m; cfg_div = d; cfg_addr_bytes = ab;
cfg_dummy = du; cfg_page_bits = pb;
load = 1'b1;
@(negedge clk);
load = 1'b0;
@(negedge clk);
end
endtask
initial begin
repeat (3) @(negedge clk);
rst_n = 1'b1;
@(negedge clk);
// 1. Reset is the safe profile and it is NOT usable.
if (profile_ok !== 1'b0) begin
$display(" FAIL: profile_ok set out of reset"); errors++;
end
if (div !== {DIV_W{1'b1}}) begin
$display(" FAIL: reset did not load the slowest divisor"); errors++;
end
// 2. A legal profile applies in full.
do_load(2'd3, 8'd4, 3'd3, 6'd8, 4'd8);
if (!profile_ok || err !== 4'b0000) begin
$display(" FAIL: a legal profile was rejected (err=%b)", err); errors++;
end
if (mode !== 2'd3 || div !== 8'd4 || addr_bytes !== 3'd3 ||
dummy !== 6'd8 || page_bits !== 4'd8) begin
$display(" FAIL: a legal profile did not apply in full"); errors++;
end
$display(" loaded: mode=%0d div=%0d addr_bytes=%0d dummy=%0d page_bits=%0d (page = %0d bytes)",
mode, div, addr_bytes, dummy, page_bits, 1 << page_bits);
// 3. Each illegal field is named on its own, and changes nothing.
snapshot();
do_load(2'd0, 8'd1, 3'd3, 6'd8, 4'd8); // div below MIN_DIV
if (err !== 4'b0001 || profile_ok) begin
$display(" FAIL: divisor 1 not reported as the divisor error (err=%b)", err);
errors++;
end
check_unchanged("a rejected divisor");
do_load(2'd0, 8'd4, 3'd5, 6'd8, 4'd8); // 5 address bytes
if (err !== 4'b0010 || profile_ok) begin
$display(" FAIL: 5 address bytes not reported (err=%b)", err); errors++;
end
check_unchanged("a rejected address width");
do_load(2'd0, 8'd4, 3'd3, 6'd40, 4'd8); // 40 dummy cycles
if (err !== 4'b0100 || profile_ok) begin
$display(" FAIL: 40 dummy cycles not reported (err=%b)", err); errors++;
end
check_unchanged("a rejected dummy count");
do_load(2'd0, 8'd4, 3'd3, 6'd8, 4'd2); // page_bits = 2 -> 4 bytes
if (err !== 4'b1000 || profile_ok) begin
$display(" FAIL: page_bits=2 not reported (err=%b)", err); errors++;
end
check_unchanged("a rejected page size");
// The other end of the same field: an exponent larger than any real
// page. A check written only as "< 4" would accept this.
do_load(2'd0, 8'd4, 3'd3, 6'd8, 4'd15);
if (err !== 4'b1000 || profile_ok) begin
$display(" FAIL: page_bits=15 not reported (err=%b)", err); errors++;
end
check_unchanged("a page exponent above the maximum");
// 4. Several bad fields name themselves together -- software fixes
// the whole entry in one pass instead of discovering one error
// per attempt.
do_load(2'd0, 8'd0, 3'd7, 6'd63, 4'd1);
if (err !== 4'b1111 || profile_ok) begin
$display(" FAIL: four bad fields did not report four bits (err=%b)", err);
errors++;
end
check_unchanged("a profile with four bad fields");
$display(" four bad fields reported together: err=%b", err);
// 5. Recovery: a good profile after a bad one applies completely.
do_load(2'd1, 8'd16, 3'd0, 6'd0, 4'd0);
if (!profile_ok || err !== 4'b0000) begin
$display(" FAIL: a legal profile after a rejection was not accepted");
errors++;
end
if (mode !== 2'd1 || div !== 8'd16 || addr_bytes !== 3'd0 ||
dummy !== 6'd0 || page_bits !== 4'd0) begin
$display(" FAIL: recovery profile did not apply in full"); errors++;
end
// 6. A device with no address phase and no page rule is legal --
// status-register-only parts exist and must not be rejected.
$display(" zero-address, zero-dummy, no-page profile accepted: ok=%0b", profile_ok);
if (errors == 0)
$display("PASS: every illegal field is named individually, a rejected profile leaves the one in force untouched, and a legal profile applies atomically");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmoduleThe testbench spends most of its length on one idea: after a rejected load, every field must hold the value it had before. That is worth more than checking that a good profile loads, because the failure it guards against is subtle — a block that applied its good fields and rejected its bad ones would leave the controller describing no device at all, combining a new address width with an old dummy count. Both fields would be individually plausible and the combination would match nothing.
The page_bits field carries a second lesson. It is an exponent, and the test that catches the common error is the one at each end of its range: 2 is rejected because a 4-byte page does not exist, and 15 is rejected because no page is 32 KB. A check written only as "at least 4" passes the second.
// spi_device_profile.v
//
// Chapter 10.1 -- the datasheet as a checked data structure, in
// Verilog-2001. Same contract as the SystemVerilog: per-field validation
// and an atomic apply, so a profile with any illegal field is rejected
// whole and the previous profile stays in force.
module spi_device_profile #(
parameter DIV_W = 8,
parameter DUMMY_W = 6,
parameter MIN_DIV = 2, // fastest divisor this master supports
parameter MAX_DUMMY = 32,
parameter MAX_ADDR_BYTES = 4,
parameter PAGE_W = 4,
parameter MAX_PAGE_BITS = 12 // 4 KB, the largest page worth naming
) (
input wire clk,
input wire rst_n,
input wire load, // pulse: validate and apply cfg_*
input wire [1:0] cfg_mode,
input wire [DIV_W-1:0] cfg_div,
input wire [2:0] cfg_addr_bytes,
input wire [DUMMY_W-1:0] cfg_dummy,
input wire [PAGE_W-1:0] cfg_page_bits, // page = 2**page_bits, 0 = none
output reg [1:0] mode,
output reg [DIV_W-1:0] div,
output reg [2:0] addr_bytes,
output reg [DUMMY_W-1:0] dummy,
output reg [PAGE_W-1:0] page_bits,
output reg profile_ok, // a validated profile is in force
output reg [3:0] err // which field was rejected
);
localparam ERR_DIV = 0;
localparam ERR_ADDR = 1;
localparam ERR_DUMMY = 2;
localparam ERR_PAGE = 3;
reg [3:0] err_c;
// Per-field validation. Each bit names the field that is wrong, so
// software reads one register and learns which datasheet number it
// mis-entered rather than being told only that something was invalid.
always @(*) begin
err_c = 4'b0000;
if (cfg_div < MIN_DIV) err_c[ERR_DIV] = 1'b1;
if (cfg_addr_bytes > MAX_ADDR_BYTES) err_c[ERR_ADDR] = 1'b1;
if (cfg_dummy > MAX_DUMMY) err_c[ERR_DUMMY] = 1'b1;
// A page is either absent (0) or between 16 bytes and 4 KB. The
// values 1..3 describe pages of 2, 4 and 8 bytes, which no real
// device has and which nearly always means a byte COUNT was entered
// where an exponent was wanted. The field must also be wide enough
// to hold the exponent: a 256-byte page is 8, so three bits cannot
// express the most common page size there is.
if (cfg_page_bits != {PAGE_W{1'b0}} &&
(cfg_page_bits < 4 || cfg_page_bits > MAX_PAGE_BITS))
err_c[ERR_PAGE] = 1'b1;
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// Reset is the safe profile, not a plausible one: slowest clock,
// widest address, no dummy cycles, no page rule -- and
// profile_ok low, so nothing may transact with it.
mode <= 2'd0;
div <= {DIV_W{1'b1}};
addr_bytes <= MAX_ADDR_BYTES[2:0];
dummy <= {DUMMY_W{1'b0}};
page_bits <= {PAGE_W{1'b0}};
profile_ok <= 1'b0;
err <= 4'b0000;
end else if (load) begin
err <= err_c;
if (err_c == 4'b0000) begin
mode <= cfg_mode;
div <= cfg_div;
addr_bytes <= cfg_addr_bytes;
dummy <= cfg_dummy;
page_bits <= cfg_page_bits;
profile_ok <= 1'b1;
end else begin
// Rejected whole. Every field keeps its previous value and
// profile_ok is cleared.
profile_ok <= 1'b0;
end
end
end
endmodule// spi_device_profile_tb.v
//
// The same checks as the SystemVerilog testbench: every illegal field is
// named individually, a rejected load changes nothing, and a good load
// after a bad one recovers completely.
`timescale 1ns/1ps
module spi_device_profile_tb;
parameter DIV_W = 8;
parameter DUMMY_W = 6;
parameter PAGE_W = 4;
reg clk;
reg rst_n;
reg load;
reg [1:0] cfg_mode;
reg [DIV_W-1:0] cfg_div;
reg [2:0] cfg_addr_bytes;
reg [DUMMY_W-1:0] cfg_dummy;
reg [PAGE_W-1:0] cfg_page_bits;
wire [1:0] mode;
wire [DIV_W-1:0] div;
wire [2:0] addr_bytes;
wire [DUMMY_W-1:0] dummy;
wire [PAGE_W-1:0] page_bits;
wire profile_ok;
wire [3:0] err;
integer errors;
reg [1:0] s_mode;
reg [DIV_W-1:0] s_div;
reg [2:0] s_addr;
reg [DUMMY_W-1:0] s_dummy;
reg [PAGE_W-1:0] s_page;
initial begin
clk = 1'b0; rst_n = 1'b0; load = 1'b0; errors = 0;
cfg_mode = 2'd0; cfg_div = 8'd4; cfg_addr_bytes = 3'd3;
cfg_dummy = 6'd8; cfg_page_bits = 4'd8;
end
always #5 clk = ~clk;
spi_device_profile #(
.DIV_W(DIV_W), .DUMMY_W(DUMMY_W),
.MIN_DIV(2), .MAX_DUMMY(32), .MAX_ADDR_BYTES(4),
.PAGE_W(PAGE_W), .MAX_PAGE_BITS(12)
) dut (
.clk(clk), .rst_n(rst_n), .load(load),
.cfg_mode(cfg_mode), .cfg_div(cfg_div),
.cfg_addr_bytes(cfg_addr_bytes), .cfg_dummy(cfg_dummy),
.cfg_page_bits(cfg_page_bits),
.mode(mode), .div(div), .addr_bytes(addr_bytes),
.dummy(dummy), .page_bits(page_bits),
.profile_ok(profile_ok), .err(err)
);
task snapshot;
begin
s_mode = mode; s_div = div; s_addr = addr_bytes;
s_dummy = dummy; s_page = page_bits;
end
endtask
task check_unchanged;
input [8*40:1] what;
begin
if (mode != s_mode || div != s_div || addr_bytes != s_addr ||
dummy != s_dummy || page_bits != s_page) begin
$display(" FAIL: %0s altered the profile in force", what);
errors = errors + 1;
end
end
endtask
task do_load;
input [1:0] m;
input [DIV_W-1:0] d;
input [2:0] ab;
input [DUMMY_W-1:0] du;
input [PAGE_W-1:0] pb;
begin
@(negedge clk);
cfg_mode = m; cfg_div = d; cfg_addr_bytes = ab;
cfg_dummy = du; cfg_page_bits = pb;
load = 1'b1;
@(negedge clk);
load = 1'b0;
@(negedge clk);
end
endtask
initial begin
repeat (3) @(negedge clk);
rst_n = 1'b1;
@(negedge clk);
// 1. Reset is the safe profile and it is NOT usable.
if (profile_ok !== 1'b0) begin
$display(" FAIL: profile_ok set out of reset"); errors = errors + 1;
end
if (div !== {DIV_W{1'b1}}) begin
$display(" FAIL: reset did not load the slowest divisor");
errors = errors + 1;
end
// 2. A legal profile applies in full.
do_load(2'd3, 8'd4, 3'd3, 6'd8, 4'd8);
if (!profile_ok || err !== 4'b0000) begin
$display(" FAIL: a legal profile was rejected (err=%b)", err);
errors = errors + 1;
end
if (mode !== 2'd3 || div !== 8'd4 || addr_bytes !== 3'd3 ||
dummy !== 6'd8 || page_bits !== 4'd8) begin
$display(" FAIL: a legal profile did not apply in full");
errors = errors + 1;
end
$display(" loaded: mode=%0d div=%0d addr_bytes=%0d dummy=%0d page_bits=%0d (page = %0d bytes)",
mode, div, addr_bytes, dummy, page_bits, 1 << page_bits);
// 3. Each illegal field is named on its own, and changes nothing.
snapshot;
do_load(2'd0, 8'd1, 3'd3, 6'd8, 4'd8); // div below MIN_DIV
if (err !== 4'b0001 || profile_ok) begin
$display(" FAIL: divisor 1 not reported as the divisor error (err=%b)", err);
errors = errors + 1;
end
check_unchanged("a rejected divisor");
do_load(2'd0, 8'd4, 3'd5, 6'd8, 4'd8); // 5 address bytes
if (err !== 4'b0010 || profile_ok) begin
$display(" FAIL: 5 address bytes not reported (err=%b)", err);
errors = errors + 1;
end
check_unchanged("a rejected address width");
do_load(2'd0, 8'd4, 3'd3, 6'd40, 4'd8); // 40 dummy cycles
if (err !== 4'b0100 || profile_ok) begin
$display(" FAIL: 40 dummy cycles not reported (err=%b)", err);
errors = errors + 1;
end
check_unchanged("a rejected dummy count");
do_load(2'd0, 8'd4, 3'd3, 6'd8, 4'd2); // page_bits = 2 -> 4 bytes
if (err !== 4'b1000 || profile_ok) begin
$display(" FAIL: page_bits=2 not reported (err=%b)", err);
errors = errors + 1;
end
check_unchanged("a rejected page size");
// The other end of the same field: an exponent larger than any real
// page. A check written only as "< 4" would accept this.
do_load(2'd0, 8'd4, 3'd3, 6'd8, 4'd15);
if (err !== 4'b1000 || profile_ok) begin
$display(" FAIL: page_bits=15 not reported (err=%b)", err);
errors = errors + 1;
end
check_unchanged("a page exponent above the maximum");
// 4. Several bad fields name themselves together.
do_load(2'd0, 8'd0, 3'd7, 6'd63, 4'd1);
if (err !== 4'b1111 || profile_ok) begin
$display(" FAIL: four bad fields did not report four bits (err=%b)", err);
errors = errors + 1;
end
check_unchanged("a profile with four bad fields");
$display(" four bad fields reported together: err=%b", err);
// 5. Recovery: a good profile after a bad one applies completely.
do_load(2'd1, 8'd16, 3'd0, 6'd0, 4'd0);
if (!profile_ok || err !== 4'b0000) begin
$display(" FAIL: a legal profile after a rejection was not accepted");
errors = errors + 1;
end
if (mode !== 2'd1 || div !== 8'd16 || addr_bytes !== 3'd0 ||
dummy !== 6'd0 || page_bits !== 4'd0) begin
$display(" FAIL: recovery profile did not apply in full");
errors = errors + 1;
end
$display(" zero-address, zero-dummy, no-page profile accepted: ok=%0b", profile_ok);
if (errors == 0)
$display("PASS: every illegal field is named individually, a rejected profile leaves the one in force untouched, and a legal profile applies atomically");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule-- spi_device_profile.vhd
--
-- Chapter 10.1 -- the datasheet as a checked data structure, in VHDL.
-- Same contract as the SystemVerilog and Verilog: per-field validation
-- and an atomic apply, so a profile with any illegal field is rejected
-- whole and the previous profile stays in force.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_device_profile is
generic (
DIV_W : positive := 8;
DUMMY_W : positive := 6;
MIN_DIV : natural := 2; -- fastest divisor this master supports
MAX_DUMMY : natural := 32;
MAX_ADDR_BYTES : natural := 4;
PAGE_W : positive := 4;
MAX_PAGE_BITS : natural := 12 -- 4 KB, the largest page worth naming
);
port (
clk : in std_logic;
rst_n : in std_logic;
load : in std_logic; -- pulse: validate and apply cfg_*
cfg_mode : in std_logic_vector(1 downto 0);
cfg_div : in unsigned(DIV_W - 1 downto 0);
cfg_addr_bytes : in unsigned(2 downto 0);
cfg_dummy : in unsigned(DUMMY_W - 1 downto 0);
cfg_page_bits : in unsigned(PAGE_W - 1 downto 0);
mode : out std_logic_vector(1 downto 0);
div : out unsigned(DIV_W - 1 downto 0);
addr_bytes : out unsigned(2 downto 0);
dummy : out unsigned(DUMMY_W - 1 downto 0);
page_bits : out unsigned(PAGE_W - 1 downto 0);
profile_ok : out std_logic; -- a validated profile is in force
err : out std_logic_vector(3 downto 0)
);
end entity;
architecture rtl of spi_device_profile is
constant ERR_DIV : natural := 0;
constant ERR_ADDR : natural := 1;
constant ERR_DUMMY : natural := 2;
constant ERR_PAGE : natural := 3;
-- Declaration initialisers keep the combinational validator below from
-- comparing 'U' before the first reset. They are simulation hygiene,
-- not a substitute for reset: rst_n still loads every register.
signal err_c : std_logic_vector(3 downto 0) := (others => '0');
signal mode_r : std_logic_vector(1 downto 0) := (others => '0');
signal div_r : unsigned(DIV_W - 1 downto 0) := (others => '1');
signal addr_r : unsigned(2 downto 0) := (others => '0');
signal dummy_r : unsigned(DUMMY_W - 1 downto 0) := (others => '0');
signal page_r : unsigned(PAGE_W - 1 downto 0) := (others => '0');
signal ok_r : std_logic := '0';
signal err_r : std_logic_vector(3 downto 0) := (others => '0');
begin
-- Per-field validation. Each bit names the field that is wrong, so
-- software reads one register and learns which datasheet number it
-- mis-entered rather than being told only that something was invalid.
validate : process (cfg_div, cfg_addr_bytes, cfg_dummy, cfg_page_bits)
variable v : std_logic_vector(3 downto 0);
begin
v := (others => '0');
if to_integer(cfg_div) < MIN_DIV then
v(ERR_DIV) := '1';
end if;
if to_integer(cfg_addr_bytes) > MAX_ADDR_BYTES then
v(ERR_ADDR) := '1';
end if;
if to_integer(cfg_dummy) > MAX_DUMMY then
v(ERR_DUMMY) := '1';
end if;
-- A page is either absent (0) or between 16 bytes and 4 KB. The
-- values 1..3 describe pages of 2, 4 and 8 bytes, which no real
-- device has and which nearly always means a byte COUNT was entered
-- where an exponent was wanted. The field must also be wide enough
-- to hold the exponent: a 256-byte page is 8, so three bits cannot
-- express the most common page size there is.
if cfg_page_bits /= 0 and
(to_integer(cfg_page_bits) < 4 or
to_integer(cfg_page_bits) > MAX_PAGE_BITS) then
v(ERR_PAGE) := '1';
end if;
err_c <= v;
end process;
apply : process (clk, rst_n)
begin
if rst_n = '0' then
-- Reset is the safe profile, not a plausible one: slowest clock,
-- widest address, no dummy cycles, no page rule -- and
-- profile_ok low, so nothing may transact with it.
mode_r <= (others => '0');
div_r <= (others => '1');
addr_r <= to_unsigned(MAX_ADDR_BYTES, 3);
dummy_r <= (others => '0');
page_r <= (others => '0');
ok_r <= '0';
err_r <= (others => '0');
elsif rising_edge(clk) then
if load = '1' then
err_r <= err_c;
if err_c = "0000" then
mode_r <= cfg_mode;
div_r <= cfg_div;
addr_r <= cfg_addr_bytes;
dummy_r <= cfg_dummy;
page_r <= cfg_page_bits;
ok_r <= '1';
else
-- Rejected whole. Every field keeps its previous value
-- and profile_ok is cleared.
ok_r <= '0';
end if;
end if;
end if;
end process;
mode <= mode_r;
div <= div_r;
addr_bytes <= addr_r;
dummy <= dummy_r;
page_bits <= page_r;
profile_ok <= ok_r;
err <= err_r;
end architecture;-- spi_device_profile_tb.vhd
--
-- The same checks as the SystemVerilog and Verilog testbenches: every
-- illegal field is named individually, a rejected load changes nothing,
-- and a good load after a bad one recovers completely.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_device_profile_tb is
end entity;
architecture sim of spi_device_profile_tb is
constant DIV_W : positive := 8;
constant DUMMY_W : positive := 6;
constant PAGE_W : positive := 4;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal halt : boolean := false;
signal load : std_logic := '0';
signal cfg_mode : std_logic_vector(1 downto 0) := (others => '0');
signal cfg_div : unsigned(DIV_W - 1 downto 0) := to_unsigned(4, DIV_W);
signal cfg_addr_bytes : unsigned(2 downto 0) := to_unsigned(3, 3);
signal cfg_dummy : unsigned(DUMMY_W - 1 downto 0) := to_unsigned(8, DUMMY_W);
signal cfg_page_bits : unsigned(PAGE_W - 1 downto 0) := to_unsigned(8, PAGE_W);
signal mode : std_logic_vector(1 downto 0);
signal div : unsigned(DIV_W - 1 downto 0);
signal addr_bytes : unsigned(2 downto 0);
signal dummy : unsigned(DUMMY_W - 1 downto 0);
signal page_bits : unsigned(PAGE_W - 1 downto 0);
signal profile_ok : std_logic;
signal err : std_logic_vector(3 downto 0);
signal errors : natural := 0;
begin
clk <= not clk after 5 ns when not halt else '0';
dut : entity work.spi_device_profile
generic map (
DIV_W => DIV_W, DUMMY_W => DUMMY_W,
MIN_DIV => 2, MAX_DUMMY => 32, MAX_ADDR_BYTES => 4,
PAGE_W => PAGE_W, MAX_PAGE_BITS => 12
)
port map (
clk => clk, rst_n => rst_n, load => load,
cfg_mode => cfg_mode, cfg_div => cfg_div,
cfg_addr_bytes => cfg_addr_bytes, cfg_dummy => cfg_dummy,
cfg_page_bits => cfg_page_bits,
mode => mode, div => div, addr_bytes => addr_bytes,
dummy => dummy, page_bits => page_bits,
profile_ok => profile_ok, err => err
);
stim : process
variable errs : natural := 0;
variable s_mode : std_logic_vector(1 downto 0);
variable s_div : unsigned(DIV_W - 1 downto 0);
variable s_addr : unsigned(2 downto 0);
variable s_dummy : unsigned(DUMMY_W - 1 downto 0);
variable s_page : unsigned(PAGE_W - 1 downto 0);
procedure snapshot is
begin
s_mode := mode; s_div := div; s_addr := addr_bytes;
s_dummy := dummy; s_page := page_bits;
end procedure;
procedure check_unchanged(what : string) is
begin
if mode /= s_mode or div /= s_div or addr_bytes /= s_addr or
dummy /= s_dummy or page_bits /= s_page then
report " FAIL: " & what & " altered the profile in force";
errs := errs + 1;
end if;
end procedure;
procedure do_load(m : integer; d : integer; ab : integer;
du : integer; pb : integer) is
begin
wait until falling_edge(clk);
cfg_mode <= std_logic_vector(to_unsigned(m, 2));
cfg_div <= to_unsigned(d, DIV_W);
cfg_addr_bytes <= to_unsigned(ab, 3);
cfg_dummy <= to_unsigned(du, DUMMY_W);
cfg_page_bits <= to_unsigned(pb, PAGE_W);
load <= '1';
wait until falling_edge(clk);
load <= '0';
wait until falling_edge(clk);
end procedure;
begin
wait until falling_edge(clk);
wait until falling_edge(clk);
wait until falling_edge(clk);
rst_n <= '1';
wait until falling_edge(clk);
-- 1. Reset is the safe profile and it is NOT usable.
if profile_ok /= '0' then
report " FAIL: profile_ok set out of reset"; errs := errs + 1;
end if;
if div /= (div'range => '1') then
report " FAIL: reset did not load the slowest divisor";
errs := errs + 1;
end if;
-- 2. A legal profile applies in full.
do_load(3, 4, 3, 8, 8);
if profile_ok /= '1' or err /= "0000" then
report " FAIL: a legal profile was rejected"; errs := errs + 1;
end if;
if mode /= "11" or div /= 4 or addr_bytes /= 3 or
dummy /= 8 or page_bits /= 8 then
report " FAIL: a legal profile did not apply in full";
errs := errs + 1;
end if;
report " loaded: mode=" & integer'image(to_integer(unsigned(mode))) &
" div=" & integer'image(to_integer(div)) &
" addr_bytes=" & integer'image(to_integer(addr_bytes)) &
" dummy=" & integer'image(to_integer(dummy)) &
" page_bits=" & integer'image(to_integer(page_bits)) &
" (page = " & integer'image(2 ** to_integer(page_bits)) & " bytes)";
-- 3. Each illegal field is named on its own, and changes nothing.
snapshot;
do_load(0, 1, 3, 8, 8); -- div below MIN_DIV
if err /= "0001" or profile_ok = '1' then
report " FAIL: divisor 1 not reported as the divisor error";
errs := errs + 1;
end if;
check_unchanged("a rejected divisor");
do_load(0, 4, 5, 8, 8); -- 5 address bytes
if err /= "0010" or profile_ok = '1' then
report " FAIL: 5 address bytes not reported"; errs := errs + 1;
end if;
check_unchanged("a rejected address width");
do_load(0, 4, 3, 40, 8); -- 40 dummy cycles
if err /= "0100" or profile_ok = '1' then
report " FAIL: 40 dummy cycles not reported"; errs := errs + 1;
end if;
check_unchanged("a rejected dummy count");
do_load(0, 4, 3, 8, 2); -- page_bits = 2 -> 4 bytes
if err /= "1000" or profile_ok = '1' then
report " FAIL: page_bits=2 not reported"; errs := errs + 1;
end if;
check_unchanged("a rejected page size");
-- The other end of the same field: an exponent larger than any real
-- page. A check written only as "< 4" would accept this.
do_load(0, 4, 3, 8, 15);
if err /= "1000" or profile_ok = '1' then
report " FAIL: page_bits=15 not reported"; errs := errs + 1;
end if;
check_unchanged("a page exponent above the maximum");
-- 4. Several bad fields name themselves together.
do_load(0, 0, 7, 63, 1);
if err /= "1111" or profile_ok = '1' then
report " FAIL: four bad fields did not report four bits";
errs := errs + 1;
end if;
check_unchanged("a profile with four bad fields");
report " four bad fields reported together: err=1111";
-- 5. Recovery: a good profile after a bad one applies completely.
do_load(1, 16, 0, 0, 0);
if profile_ok /= '1' or err /= "0000" then
report " FAIL: a legal profile after a rejection was not accepted";
errs := errs + 1;
end if;
if mode /= "01" or div /= 16 or addr_bytes /= 0 or
dummy /= 0 or page_bits /= 0 then
report " FAIL: recovery profile did not apply in full";
errs := errs + 1;
end if;
report " zero-address, zero-dummy, no-page profile accepted";
errors <= errs;
if errs = 0 then
report "PASS: every illegal field is named individually, a rejected profile leaves the one in force untouched, and a legal profile applies atomically";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
halt <= true;
wait;
end process;
end architecture;Parity
All three implement the same profile block: identical ports and generics, asynchronous active-low reset loading the safe profile with profile_ok low, combinational per-field validation with one error bit each, and an atomic apply. All three testbenches run the same sequence — a legal profile, each field rejected individually at both ends of its range, four bad fields reported together, and a clean recovery — and produce identical results, including the 256-byte page that a three-bit field could not have held.
8. Why a Verification Engineer Cares
This block is where the UVM concept of a configuration object stops being an abstraction and becomes the design.
// The profile is exactly what uvm_config_db exists to distribute: one
// object, written once by the test, read by every component that needs
// to know how this device behaves. The alternative -- each component
// holding its own copy of the dummy count -- is how a testbench comes
// to disagree with itself.
class spi_device_cfg extends uvm_object;
`uvm_object_utils(spi_device_cfg)
rand bit [1:0] mode;
rand int divisor;
rand int addr_bytes;
rand int dummy_cycles;
rand int page_bits;
bit [23:0] id_expected;
// The SAME constraints the RTL validator enforces. Writing them
// twice is not duplication -- it is the point. If the constraint
// solver can produce a profile the hardware rejects, one of the two
// is wrong, and finding out which is a five-minute job here and a
// week-long job on silicon.
constraint c_legal {
divisor >= 2;
addr_bytes inside {[0:4]};
dummy_cycles inside {[0:32]};
page_bits inside {0, [4:12]};
}
function new(string name = "spi_device_cfg");
super.new(name);
endfunction
endclass
// A real device, described once and reused by every test that touches it.
class cfg_w25q128 extends spi_device_cfg;
`uvm_object_utils(cfg_w25q128)
function new(string name = "cfg_w25q128");
super.new(name);
mode = 2'b00; divisor = 4; addr_bytes = 3;
dummy_cycles = 8; page_bits = 8; id_expected = 24'hEF4018;
endfunction
endclassThe discipline that makes this worth doing is the shared constraint. A randomised profile that the solver considers legal and the hardware rejects is a genuine disagreement about the specification, and it surfaces in seconds.
// 1. ATOMICITY. A rejected load changes nothing. This is the property
// the whole design exists for, and the one a casual implementation
// gets wrong by applying fields as it validates them.
a_atomic : assert property (
@(posedge clk) disable iff (!rst_n)
(load && (err_c != '0)) |=>
($stable(mode) && $stable(div) && $stable(addr_bytes) &&
$stable(dummy) && $stable(page_bits)))
else $error("a rejected profile altered the profile in force");
// 2. A rejected load also clears profile_ok -- rejecting the numbers
// while leaving the controller enabled would be worse than either.
a_reject_disables : assert property (
@(posedge clk) disable iff (!rst_n)
(load && (err_c != '0)) |=> !profile_ok)
else $error("a rejected profile left the controller enabled");
// 3. RESET SAFETY. Out of reset the profile is unusable and the divisor
// is the slowest available -- an un-initialised master cannot
// overclock a device even by accident.
a_reset_safe : assert property (
@(posedge clk) (!rst_n) |=> (!profile_ok && div == '1))
else $error("reset did not load the safe profile");
// 4. HONESTY. Every error bit corresponds to a field that really is out
// of range -- a validator that sets extra bits sends software
// looking at the wrong datasheet line.
a_err_honest : assert property (
@(posedge clk) disable iff (!rst_n)
(load && err_c[0]) |-> (cfg_div < MIN_DIV))
else $error("the divisor error bit was set for a legal divisor");Property 1 is the one to internalise. Atomic configuration is a pattern far wider than SPI: any block whose fields must be mutually consistent has it, and the failure mode — a half-applied configuration that matches nothing — is always harder to debug than an outright rejection.
Coverage should target the boundaries, because random profiles never land on them:
covergroup spi_profile_cg @(posedge clk iff load);
cp_div : coverpoint cfg_div {
bins below_min = {[0:1]}; // must reject
bins at_min = {2}; // must ACCEPT -- the off-by-one
bins normal = {[3:64]};
bins very_slow = {[65:$]};
}
cp_abytes : coverpoint cfg_addr_bytes {
bins none = {0}; // legal: status-only parts
bins one_two = {[1:2]};
bins three = {3}; // the common case
bins four = {4}; // must ACCEPT
bins too_many = {[5:7]}; // must reject
}
// The exponent field, whose wrong entries cluster at both ends.
cp_page : coverpoint cfg_page_bits {
bins none = {0}; // legal
bins byte_count = {[1:3]}; // a COUNT entered as an exponent
bins real_page = {[4:12]};
bins absurd = {[13:15]}; // must reject
}
// Did any test ever load a profile with more than one bad field?
cp_nerr : coverpoint $countones(err_c) {
bins clean = {0};
bins one = {1};
bins many = {[2:4]};
}
endgroupcp_nerr is the coverpoint usually missing. A validator that reports only the first bad field passes every single-error test and forces software to fix its table one entry per attempt — which is a usability defect that no per-field test detects.
9. Why an FPGA or ASIC Engineer Cares
Make the profile a register block, not a set of parameters. Parameters bake one device into the bitstream. A register block lets one controller serve a flash, an ADC and a sensor, and lets a second-source part be supported by a table change rather than a rebuild.
Reset to safe, always. The slowest divisor and a cleared profile_ok cost a few slow boot transactions and remove an entire category of failure — the window between reset release and software configuration, which is exactly where a boot ROM issues its first flash read.
Report which field failed. Four bits of error code turn "configuration rejected" into "the dummy count is out of range", and that difference is the difference between a five-minute fix and an afternoon.
Read the ID register in hardware if you can. A controller that performs the ID read itself on release from reset, and raises a flag if it does not match, catches a mis-stuffed or dead device before any software runs. The cost is a small state machine and a comparator.
Do not let the profile change mid-transaction. profile_ok should gate start, not the transaction already in flight. Changing the address width halfway through a frame produces a transfer that matches no device and is very hard to recognise on a scope.
10. Failure Signature — A New Board Where Only One of Two Identical Devices Answers
Symptom. A board carries two flash devices from different vendors, both advertised as drop-in compatible, on the same bus with separate chip selects. The first answers its ID read correctly. The second returns 0x000000. Both are correctly soldered, both have power, and a scope shows SCLK and MOSI reaching both parts.
What "SCLK and MOSI reach both" establishes. The connection and the clock are fine, so passes one and two of §2–3 are not the problem for the bus. What remains is something about the second device specifically: its mode, its rate, or the command it was sent.
Plausible mechanisms.
- A different ID opcode.
0x9F(JEDEC ID) and0xAB(device ID) and0x90(manufacturer/device ID) are all in use, and they have different dummy counts. A part that only implements one of them returns nothing for the others. - A different dummy count for the same opcode, which shifts the answer rather than blanking it — so this one produces garbage rather than zeros.
- A different maximum SCLK, with the second part slower than the first and the shared bus running at the first part's rate.
- A different mode, if one part is mode 0 and the other mode 3 — though Chapter 3.8 notes those two are often interchangeable in practice, which makes this less likely than it looks.
- The second device genuinely not responding — wrong CS routing, or a part that needs a release-from-power-down command first.
The discriminating observation. All-zeros is the important detail. A wrong dummy count or a wrong mode produces shifted or scrambled data, not clean zeros, because MISO is still being driven. Clean zeros mean nothing is driving MISO at all — so either the device is not selected, or it does not recognise the command and is holding its output disabled.
Check CS at the second device's pin during its frame. If CS asserts correctly, the device is selected and does not recognise the opcode: compare the two ID commands in the two datasheets, and the difference will be in the opcode or its dummy count.
The fix, and the process lesson. Give each device its own profile, including its own ID opcode and dummy count. "Drop-in compatible" describes the pinout and the basic read command; it very rarely extends to the identification commands, the status register bits, or the maximum rate.
Why the investigation goes wrong. Because "identical parts, one works" points at the hardware, and the second device gets reflowed, replaced and probed while the driver keeps sending an opcode that part has never implemented. The word "compatible" on a front page did the damage; the command tables would have shown it in a minute.
11. Common Misconceptions
12. Reason It Through
Work this before reading the answer.
A colleague reports that a new sensor "works, but only sometimes". Reads of its measurement register return plausible values, but roughly one read in eight returns a value that is clearly wrong — and always wrong in the same way, as though the bytes had been rotated. The ID register read, which they ran once at bring-up, was correct.
What is the most likely cause, and what does the ID read having passed actually tell you?
Start with what the ID read proves. It proves the mode is workable, the wiring is right, and the command encoding for that opcode is right. It says nothing about any other command — and in particular nothing about the dummy count of any other command, because the ID command's dummy count may differ.
Now the pattern. "Rotated bytes" is the signature of a latency error: the controller starts capturing at the wrong cycle, so every byte is offset. Chapter 9.4 showed this for a round-trip violation; here the more likely cause is a dummy count that does not match the measurement register's read command.
But why only one read in eight? This is the part that makes it interesting. A constant latency error would corrupt every read, not one in eight. So the latency must be usually right and occasionally wrong — which means something about the device's state changes.
The mechanism that fits. Many sensors have a data-ready or auto-increment behaviour where a read issued while a conversion is in progress returns data from an internal buffer with a different latency, or stalls one cycle. One read in eight lands in that window. Alternatively, the part supports a burst read whose first byte has a different latency from subsequent bytes, and the driver occasionally uses the burst form.
How to discriminate, concretely. Three observations, in order:
- Capture a good read and a bad read on a scope and compare bit positions. If the bad one is shifted by a whole number of bit times, it is a latency problem; if the bits are scrambled rather than shifted, it is a sampling problem and belongs to Chapter 10.3.
- Correlate the failures with the status register. Read status immediately before each measurement read and record it. If every failure follows a particular status value, the device's state is the variable.
- Re-read the command table for the measurement register specifically — not the ID command — and check its dummy count against what the driver sends.
The general lesson, and it is the point of this whole chapter. A successful ID read is a necessary and very incomplete check. It validates one command's shape. Each command in the table has its own address width and its own dummy count, and a driver that assumes one shape for all of them works for exactly as long as the commands happen to agree.
13. Understanding Check
14. Summary
An SPI datasheet is organised to sell the part, not to bring it up, and SPI implies nothing above the clocking — so everything must be found.
The route is four passes: the pin table for the connection, the timing diagram and table for the mode and rate, the command table for opcodes with their address and dummy shapes, and the register map for identification and status. Each pass narrows the next, which is why the order beats reading front to back.
The output is seven numbers, and that is the complete specification a controller needs.
Bring-up starts with the identification register — the only read whose correct answer is known in advance, which turns four uncertainties into one verdict, and whose failure mode distinguishes a dead link (zeros) from a wrong latency (a shift).
In hardware the profile belongs in a register block with a validator, not in parameters: one controller then serves every device, and a second source becomes a table change. Reset loads the safe profile with profile_ok low, and the apply is atomic — a half-applied profile describes no device at all.
In UVM the same object is the configuration object, and giving it the validator's own constraints turns any disagreement between testbench and RTL into an immediate failure.
And a passing ID read proves exactly one command's shape. Every other command carries its own address width and its own dummy count.
15. What Comes Next
The route's second pass asked for CPOL and CPHA, and most datasheets never write those words. They draw a picture instead.
Chapter 10.2 — Identifying the Required SPI Mode makes that reading mechanical: the two observations that determine the mode from any vendor timing diagram, why the usual "try all four" approach cannot distinguish a wrong mode from a wrong command, what to do when the diagram is ambiguous — and the hardware that infers the mode from a live capture, in all three HDLs.
Continue learning
Related tutorials
- Related topic
From Datasheet to Transaction Specification
A converter datasheet worked end to end into a mode, a divisor, a phase schedule and an executable element sequence — with the specification engine that generates it from a profile and the UVM environment that verifies a device described entirely by data.
- Related topic
CS-to-SCLK and SCLK-to-CS Timing
Chip select has timing requirements of its own: the lead before the first clock edge, the lag after the last, and the minimum deselect between transactions. Why violating them breaks a transfer whose every SCLK edge was correct.
- Related topic
Anatomy of a Read Transaction
The four phases of a device read, why a read cannot be a write reversed, when the slave takes and releases MISO, the obligation to have the first data bit valid before any edge can launch it, and the slave read datapath in three HDLs.
- Related topic
Continuous Transfers Under One CS
Holding chip select low across many bytes and what the device assumes: why the frame is the transaction, why the master may legally stop the clock when its data runs dry, and the streaming controller in three HDLs.
