SPI · Module 10
Identifying the Required SPI Mode
The two observations that read CPOL and CPHA off any vendor timing diagram, why the picture is more trustworthy than the prose beside it, why trying all four modes cannot work, and the observer that infers the mode from a live capture.
Chapter 10.1's second pass asked for CPOL and CPHA. Most datasheets never write either word.
The datasheet has a timing diagram and no statement of the mode. How do you get the mode out of the picture — and why is the picture more trustworthy than the sentence next to it?
Two observations settle it. Both are visible in any correctly drawn timing diagram, and neither requires the vendor to have used the words.
1. Why the Words Are Not Enough
Three things go wrong with prose descriptions of the mode, and all three are common.
The mode number may be absent. Many parts simply draw the waveform and say nothing. Sensor datasheets in particular describe the interface entirely pictorially.
The mode number may be stated for the wrong convention. CPOL and CPHA are unambiguous; "SPI mode 0" is a convention built on them, and while it is nearly universal it is not defined in any standard anyone owns. A handful of parts number their modes differently, and a few describe "mode 0" while drawing mode 3.
The prose may describe the SLAVE's view. "Data is output on the rising edge" tells you what the device does with its own output, not what it expects on its input — and those are opposite edges. A reader who converts that sentence directly into CPHA gets the wrong answer, consistently.
The picture has none of these problems, because the picture shows the actual electrical behaviour. So read the picture.
2. Observation One — The Idle Level Gives CPOL
Look at SCLK outside the frame, where chip select is inactive.
SCLK rests low while CS is inactive → CPOL = 0
SCLK rests high while CS is inactive → CPOL = 1That is the entire definition, and it is the easier half of the job. Two cautions:
- Look outside the frame, not at the start of it. A diagram that begins with the first clock edge may not show the idle level at all. Find the part of the figure where CS is deasserted.
- A diagram drawn with SCLK starting from the middle of the page sometimes shows a level that is simply where the artist began the line. If CS is not drawn, treat the idle level as unconfirmed and look for a second figure.
3. Observation Two — The Launching Edge Gives CPHA
This is the half that needs care, and the reliable way to read it is to ignore the sampling arrows and look at where the data changes.
Number the SCLK edges inside the frame from 1.
MOSI changes at CS assertion and on edges 2, 4, 6 (the EVEN edges)
→ data is sampled on the odd edges → CPHA = 0
MOSI changes on edges 1, 3, 5 (the ODD edges)
→ data is sampled on the even edges → CPHA = 1The distinguishing visual is the first bit. With CPHA=0 the first data bit is already valid before the first clock edge — it appears when CS falls. With CPHA=1 the first data bit appears on the first clock edge. A diagram in which the data line changes simultaneously with CS is showing you CPHA=0, and that is often the quickest read of the whole figure.
4. Reading It Off the Figure
Reading CPOL and CPHA off a capture
10 cyclesWork the figure in the order of §2 and §3. Outside the frame SCLK sits low, so CPOL = 0. The first data bit appears at the moment CS falls rather than on the first edge, and every later change lands on an even edge, so CPHA = 0. The mode is 0.
Change one thing at a time to see the others. If SCLK rested high with the same data behaviour, the mode would be 2. If the data first changed on edge 1 instead of at CS, it would be 1 — and with SCLK resting high as well, 3.
5. Why "Try All Four" Does Not Work
The tempting shortcut is to sweep the mode until something responds. It fails for a specific reason worth understanding.
A wrong mode and a wrong command produce the same symptom. Both return data that is wrong. So a sweep over four modes, with any other detail also wrong, reports four failures and tells you nothing — and the engineer concludes the part is dead.
Two of the four modes often appear to work. Modes 0 and 3 differ only in the idle level of SCLK; the sampling relationship is identical. Many devices tolerate either, because what matters to the shift register is the edge sequence, not where the clock rested beforehand. So a sweep frequently finds two modes that work, which tells you nothing about which the datasheet specified — and the one you pick may be the one that fails at temperature or at a higher rate.
A sweep can appear to succeed and still be wrong. A mode that is wrong by CPHA shifts the data by half a bit time. At a low clock rate, with a slow device and generous margins, that can still sample correctly — so the sweep succeeds on the bench at 1 MHz and fails in the field at 20 MHz.
The two observations of §2 and §3 take a minute and give the answer the vendor intended. That is strictly better than a search whose success criterion cannot distinguish the cases.
6. Building the Mode Inferrer — Three HDLs
The circuit
Circuit. An oversampling observer that turns the two observations into logic.
State. A count of MOSI transitions attributed to odd edges, to even edges, and to the interval before any edge; the parity of the most recent edge; and the last-sampled idle level.
Datapath. CPOL is a single sample taken while CS is inactive. CPHA is a comparison of the two transition counters — the parity that MOSI changes on is CPHA.
Control. Evidence accumulates through the frame and the verdict is published on the rising edge of CS. Reporting mid-frame would produce a judgement based on one or two transitions that then changed as the frame continued.
Clock and reset. An oversampling clock much faster than SCLK; asynchronous active-low reset. Every bus input passes through a two-stage synchroniser, because an inference built on a metastable sample is worse than no inference.
Enables. mode_valid is withheld when the frame carried too few transitions to decide. An all-ones payload contains no phase information at all, and the honest output is silence.
Timing. The attribution window is the subtle part. A bit is launched at an edge, so a MOSI transition seen in the same oversampling cycle as an edge belongs to that edge — while a transition seen a cycle or two later, from real clock-to-out, belongs to the edge already recorded. Both must land on the same parity, and getting it wrong inverts CPHA in every mode while leaving CPOL correct.
Synthesis. Three counters, two synchronisers and a comparator. It is small enough to leave permanently instantiated on a debug build.
Limitations. It cannot decide CPHA from a payload with no transitions, and it reports conflict rather than guessing when a capture shows transitions on both parities.
// spi_mode_infer.sv
//
// Chapter 10.2 -- reading CPOL and CPHA off a capture instead of guessing.
//
// A vendor timing diagram tells you the mode in two observations, and both
// can be made by logic:
//
// CPOL is the level SCLK rests at while CS is inactive. One sample.
//
// CPHA is the edge on which data is LAUNCHED. Number the SCLK edges
// inside a frame from 1. With CPHA=0 the first bit is launched at CS
// assertion and later bits on the trailing edges -- 2, 4, 6, the EVEN
// edges. With CPHA=1 every bit is launched on a leading edge -- 1, 3, 5,
// the ODD ones. So the parity of the edges that MOSI changes after is
// CPHA itself.
//
// This block oversamples the bus, counts transitions by edge parity, and
// reports the mode at the end of the frame. It exists because the usual
// alternative -- trying all four modes until one works -- cannot
// distinguish a wrong mode from a wrong command, and Chapter 3.8's failure
// signature is subtle enough that "it mostly works" is a common verdict.
module spi_mode_infer #(
parameter int CNT_W = 8,
parameter int MIN_EVIDENCE = 2 // transitions needed before reporting
) (
input logic clk, // oversampling clock, much faster than SCLK
input logic rst_n,
input logic sclk, // observed bus
input logic mosi,
input logic cs_n,
output logic [1:0] mode, // {CPOL, CPHA} -- the mode number
output logic mode_valid, // a frame produced enough evidence
output logic conflict // transitions on BOTH parities
);
// Two-stage synchronisers. The bus is asynchronous to this clock, and
// an inference built on a metastable sample is worse than no inference.
logic sclk_s1, sclk_s2, sclk_q;
logic mosi_s1, mosi_s2, mosi_q;
logic cs_s1, cs_s2, cs_q;
logic [CNT_W-1:0] odd_cnt; // transitions following an odd edge
logic [CNT_W-1:0] even_cnt; // transitions following an even edge
logic [CNT_W-1:0] pre_cnt; // transitions before any edge (CS launch)
logic any_edge; // at least one SCLK edge seen this frame
logic edge_odd; // parity of the most recent edge
logic cpol_r;
wire sclk_edge = (sclk_s2 != sclk_q);
// Parity of the edge this cycle belongs to. A bit is launched AT an
// edge, so a MOSI transition seen in the same cycle as an edge belongs
// to that edge, not to the previous one -- while a transition seen a
// cycle or two later (real clock-to-out) belongs to the edge already
// recorded. Both cases must land on the same parity, and getting this
// wrong inverts CPHA in every mode while leaving CPOL correct.
wire next_odd = any_edge ? ~edge_odd : 1'b1;
wire cur_odd = sclk_edge ? next_odd : edge_odd;
wire have_edge = any_edge | sclk_edge;
wire mosi_edge = (mosi_s2 != mosi_q);
wire in_frame = (cs_s2 == 1'b0);
wire cs_fall = (cs_q == 1'b1) && (cs_s2 == 1'b0);
wire cs_rise = (cs_q == 1'b0) && (cs_s2 == 1'b1);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
{sclk_s1, sclk_s2, sclk_q} <= 3'b000;
{mosi_s1, mosi_s2, mosi_q} <= 3'b000;
{cs_s1, cs_s2, cs_q} <= 3'b111;
odd_cnt <= {CNT_W{1'b0}};
even_cnt <= {CNT_W{1'b0}};
pre_cnt <= {CNT_W{1'b0}};
any_edge <= 1'b0;
edge_odd <= 1'b0;
cpol_r <= 1'b0;
mode <= 2'd0;
mode_valid <= 1'b0;
conflict <= 1'b0;
end else begin
sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
cs_s1 <= cs_n; cs_s2 <= cs_s1; cs_q <= cs_s2;
// CPOL is simply the idle level. Sampling it continuously while
// CS is inactive means the last sample before the frame is the
// one that counts, which is exactly the definition.
if (cs_s2 == 1'b1)
cpol_r <= sclk_s2;
if (cs_fall) begin
// A new frame: discard the previous frame's evidence. Mode
// is a property of one device, and two devices on the same
// bus may legitimately differ.
odd_cnt <= {CNT_W{1'b0}};
even_cnt <= {CNT_W{1'b0}};
pre_cnt <= {CNT_W{1'b0}};
any_edge <= 1'b0;
edge_odd <= 1'b0;
end else if (in_frame) begin
if (sclk_edge) begin
any_edge <= 1'b1;
// Edge 1 is odd, 2 even, and so on.
edge_odd <= next_odd;
end
if (mosi_edge) begin
if (!have_edge) pre_cnt <= pre_cnt + 1'b1;
else if (cur_odd) odd_cnt <= odd_cnt + 1'b1;
else even_cnt <= even_cnt + 1'b1;
end
end
if (cs_rise) begin
// Report at the end of the frame, when the evidence is
// complete. A mid-frame verdict would be based on one or
// two transitions and would change as the frame went on.
if ((odd_cnt + even_cnt + pre_cnt) >= CNT_W'(MIN_EVIDENCE)) begin
mode <= {cpol_r, (odd_cnt > even_cnt)};
mode_valid <= 1'b1;
end else begin
// Not enough transitions to tell. An all-ones or
// all-zeros payload carries no phase information at all,
// which is a real limit of the method and not a bug.
mode_valid <= 1'b0;
end
// Transitions on both parities mean the capture is not a
// clean single-mode frame -- a mismatched slave, a glitching
// clock, or a trigger that caught two frames.
conflict <= (odd_cnt != {CNT_W{1'b0}}) &&
(even_cnt != {CNT_W{1'b0}});
end
end
end
endmodule// spi_mode_infer_tb.sv
//
// The testbench drives real frames in all four modes with a behavioural
// master, and requires the inference to name each one. It then drives the
// two captures that carry no phase information -- an all-ones payload --
// and requires the block to say so rather than guess.
`timescale 1ns/1ps
module spi_mode_infer_tb;
localparam int CNT_W = 8;
localparam time H = 20ns; // SCLK half period
localparam time TCLK = 2ns; // oversampling period (10x per half)
logic clk = 1'b0;
logic rst_n = 1'b0;
always #(TCLK/2) clk = ~clk;
logic sclk = 1'b0;
logic mosi = 1'b0;
logic cs_n = 1'b1;
logic [1:0] mode;
logic mode_valid;
logic conflict;
int errors = 0;
spi_mode_infer #(.CNT_W(CNT_W), .MIN_EVIDENCE(2)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(sclk), .mosi(mosi), .cs_n(cs_n),
.mode(mode), .mode_valid(mode_valid), .conflict(conflict)
);
// A behavioural master. Leading edge means "away from idle", trailing
// means "back to idle" -- the definitions from Chapter 2.2, which are
// what make one body of code serve all four modes.
// tco models the slave's clock-to-out: MOSI changes tco after the
// launching edge rather than exactly on it. Zero is the idealised
// capture; a non-zero value is what a scope actually shows.
task automatic drive_frame_tco(input logic cpol, input logic cpha,
input logic [7:0] data, input time tco);
int i;
begin
sclk = cpol;
cs_n = 1'b1;
#(H*2);
cs_n = 1'b0;
if (cpha == 1'b0) begin // CPHA=0 launches at CS
#tco; mosi = data[7]; #(H-tco);
end else begin
#H;
end
for (i = 0; i < 8; i++) begin
sclk = ~cpol; // leading edge
if (cpha == 1'b1) begin
#tco; mosi = data[7-i]; #(H-tco);
end else begin
#H;
end
sclk = cpol; // trailing edge
if (cpha == 1'b0 && i < 7) begin
#tco; mosi = data[6-i]; #(H-tco);
end else begin
#H;
end
end
cs_n = 1'b1;
#(H*2);
end
endtask
task automatic drive_frame(input logic cpol, input logic cpha,
input logic [7:0] data);
begin
drive_frame_tco(cpol, cpha, data, 0ns);
end
endtask
task automatic expect_mode(input logic cpol, input logic cpha,
input logic [7:0] data);
logic [1:0] want;
begin
want = {cpol, cpha};
drive_frame(cpol, cpha, data);
if (!mode_valid) begin
$display(" FAIL: mode %0d frame produced no verdict", want);
errors++;
end else if (mode !== want) begin
$display(" FAIL: mode %0d frame was read as mode %0d", want, mode);
errors++;
end else begin
$display(" mode %0d frame (data 0x%02h) inferred as mode %0d cpol=%0b cpha=%0b",
want, data, mode, mode[1], mode[0]);
end
if (conflict) begin
$display(" FAIL: clean mode %0d frame reported a conflict", want);
errors++;
end
end
endtask
initial begin
repeat (5) @(negedge clk);
rst_n = 1'b1;
@(negedge clk);
// 1. All four modes, with a payload that transitions often enough
// to carry phase information.
expect_mode(1'b0, 1'b0, 8'hA5);
expect_mode(1'b0, 1'b1, 8'hA5);
expect_mode(1'b1, 1'b0, 8'hA5);
expect_mode(1'b1, 1'b1, 8'hA5);
// 2. A different payload must not change the verdict -- the mode is
// a property of the clocking, not of the data.
expect_mode(1'b0, 1'b1, 8'h5A);
expect_mode(1'b1, 1'b0, 8'h33);
// 3. A payload with no transitions carries no phase information.
// The honest answer is "cannot tell", and a block that guessed
// would be right half the time and confidently wrong the rest.
drive_frame(1'b0, 1'b0, 8'hFF);
if (mode_valid) begin
$display(" FAIL: an all-ones payload produced a phase verdict");
errors++;
end else begin
$display(" all-ones payload: no verdict offered, as it must be");
end
// 4. A real capture has clock-to-out: MOSI changes a few
// nanoseconds AFTER its launching edge, not exactly on it. The
// attribution must put both cases on the same parity, so every
// mode is re-run with a delay of a quarter of a half period.
for (int m = 0; m < 4; m++) begin
drive_frame_tco(m[1], m[0], 8'hA5, H/4);
if (!mode_valid || mode !== m[1:0]) begin
$display(" FAIL: with clock-to-out, mode %0d was read as %0d (valid=%0b)",
m, mode, mode_valid);
errors++;
end
end
$display(" all four modes re-inferred with a %0t clock-to-out delay", H/4);
// 5. CPOL survives the case above: it needs no data transitions at
// all, only the idle level, so the two halves of the answer have
// genuinely different evidence requirements.
drive_frame(1'b1, 1'b1, 8'hA5);
if (!mode_valid || mode[1] !== 1'b1) begin
$display(" FAIL: CPOL=1 not recovered from the idle level");
errors++;
end
if (errors == 0)
$display("PASS: all four modes are inferred from the edge parity of MOSI transitions, the verdict is independent of the payload, and a payload with no transitions is reported as no verdict rather than a guess");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmoduleThree things in that testbench are worth more than the rest.
It drives real frames in all four modes with one behavioural master, parameterised only by CPOL and CPHA — which is possible because "leading edge" and "trailing edge" are defined relative to the idle level (Chapter 2.2). One body of code covering four modes is a sign the abstraction is right.
It re-runs every mode with a clock-to-out delay, because a real capture never has MOSI changing exactly on its edge. That test is what proves the attribution window handles both cases rather than only the idealised one.
And it requires an all-ones payload to produce no verdict. A block that guessed there would be right half the time and confidently wrong the rest, which is worse than useless in a diagnostic.
// spi_mode_infer.v
//
// Chapter 10.2 -- reading CPOL and CPHA off a capture, in Verilog-2001.
//
// CPOL is the level SCLK rests at while CS is inactive: one sample.
// CPHA is the parity of the edges MOSI is launched on -- number the SCLK
// edges inside a frame from 1, and CPHA=0 launches at CS assertion and on
// the EVEN edges, CPHA=1 on the ODD ones.
module spi_mode_infer #(
parameter CNT_W = 8,
parameter MIN_EVIDENCE = 2 // transitions needed before reporting
) (
input wire clk, // oversampling clock, much faster than SCLK
input wire rst_n,
input wire sclk, // observed bus
input wire mosi,
input wire cs_n,
output reg [1:0] mode, // {CPOL, CPHA} -- the mode number
output reg mode_valid, // a frame produced enough evidence
output reg conflict // transitions on BOTH parities
);
// Two-stage synchronisers. The bus is asynchronous to this clock, and
// an inference built on a metastable sample is worse than none.
reg sclk_s1, sclk_s2, sclk_q;
reg mosi_s1, mosi_s2, mosi_q;
reg cs_s1, cs_s2, cs_q;
reg [CNT_W-1:0] odd_cnt; // transitions following an odd edge
reg [CNT_W-1:0] even_cnt; // transitions following an even edge
reg [CNT_W-1:0] pre_cnt; // transitions before any edge (CS launch)
reg any_edge; // at least one SCLK edge seen this frame
reg edge_odd; // parity of the most recent edge
reg cpol_r;
wire sclk_edge = (sclk_s2 != sclk_q);
wire mosi_edge = (mosi_s2 != mosi_q);
wire in_frame = (cs_s2 == 1'b0);
wire cs_fall = (cs_q == 1'b1) && (cs_s2 == 1'b0);
wire cs_rise = (cs_q == 1'b0) && (cs_s2 == 1'b1);
// Parity of the edge this cycle belongs to. A bit is launched AT an
// edge, so a MOSI transition seen in the same cycle as an edge belongs
// to that edge, not the previous one -- while a transition seen a cycle
// or two later (real clock-to-out) belongs to the edge already
// recorded. Both must land on the same parity; getting this wrong
// inverts CPHA in every mode while leaving CPOL correct.
wire next_odd = any_edge ? ~edge_odd : 1'b1;
wire cur_odd = sclk_edge ? next_odd : edge_odd;
wire have_edge = any_edge | sclk_edge;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_s1 <= 1'b0; sclk_s2 <= 1'b0; sclk_q <= 1'b0;
mosi_s1 <= 1'b0; mosi_s2 <= 1'b0; mosi_q <= 1'b0;
cs_s1 <= 1'b1; cs_s2 <= 1'b1; cs_q <= 1'b1;
odd_cnt <= {CNT_W{1'b0}};
even_cnt <= {CNT_W{1'b0}};
pre_cnt <= {CNT_W{1'b0}};
any_edge <= 1'b0;
edge_odd <= 1'b0;
cpol_r <= 1'b0;
mode <= 2'd0;
mode_valid <= 1'b0;
conflict <= 1'b0;
end else begin
sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
cs_s1 <= cs_n; cs_s2 <= cs_s1; cs_q <= cs_s2;
// CPOL is the idle level. Sampling continuously while CS is
// inactive means the last sample before the frame is the one
// that counts, which is exactly the definition.
if (cs_s2 == 1'b1)
cpol_r <= sclk_s2;
if (cs_fall) begin
// A new frame: discard the previous frame's evidence. Mode
// is a property of one device, and two devices on the same
// bus may legitimately differ.
odd_cnt <= {CNT_W{1'b0}};
even_cnt <= {CNT_W{1'b0}};
pre_cnt <= {CNT_W{1'b0}};
any_edge <= 1'b0;
edge_odd <= 1'b0;
end else if (in_frame) begin
if (sclk_edge) begin
any_edge <= 1'b1;
edge_odd <= next_odd; // edge 1 is odd, 2 even, ...
end
if (mosi_edge) begin
if (!have_edge) pre_cnt <= pre_cnt + 1'b1;
else if (cur_odd) odd_cnt <= odd_cnt + 1'b1;
else even_cnt <= even_cnt + 1'b1;
end
end
if (cs_rise) begin
// Report at the end of the frame, when the evidence is
// complete. A mid-frame verdict would rest on one or two
// transitions and would change as the frame went on.
if ((odd_cnt + even_cnt + pre_cnt) >= MIN_EVIDENCE) begin
mode <= {cpol_r, (odd_cnt > even_cnt)};
mode_valid <= 1'b1;
end else begin
// Not enough transitions to tell. An all-ones or
// all-zeros payload carries no phase information at all,
// which is a real limit of the method and not a bug.
mode_valid <= 1'b0;
end
conflict <= (odd_cnt != {CNT_W{1'b0}}) &&
(even_cnt != {CNT_W{1'b0}});
end
end
end
endmodule// spi_mode_infer_tb.v
//
// The same checks as the SystemVerilog testbench: all four modes inferred
// from real frames, a verdict independent of the payload, both attribution
// windows (a transition on the edge and one delayed by clock-to-out), and
// an honest "cannot tell" for a payload with no transitions.
`timescale 1ns/1ps
module spi_mode_infer_tb;
parameter CNT_W = 8;
parameter H = 20; // SCLK half period, ns
parameter TCLK = 2; // oversampling period, ns (10x per half)
reg clk;
reg rst_n;
reg sclk;
reg mosi;
reg cs_n;
wire [1:0] mode;
wire mode_valid;
wire conflict;
integer errors;
integer m;
initial begin
clk = 1'b0; rst_n = 1'b0;
sclk = 1'b0; mosi = 1'b0; cs_n = 1'b1;
errors = 0;
end
always #(TCLK/2) clk = ~clk;
spi_mode_infer #(.CNT_W(CNT_W), .MIN_EVIDENCE(2)) dut (
.clk(clk), .rst_n(rst_n),
.sclk(sclk), .mosi(mosi), .cs_n(cs_n),
.mode(mode), .mode_valid(mode_valid), .conflict(conflict)
);
// A behavioural master. Leading edge means "away from idle", trailing
// means "back to idle" -- the definitions that let one body of code
// serve all four modes. tco models the slave's clock-to-out.
task drive_frame_tco;
input cpol;
input cpha;
input [7:0] data;
input integer tco;
integer i;
begin
sclk = cpol;
cs_n = 1'b1;
#(H*2);
cs_n = 1'b0;
if (cpha == 1'b0) begin // CPHA=0 launches at CS
#(tco); mosi = data[7]; #(H-tco);
end else begin
#(H);
end
for (i = 0; i < 8; i = i + 1) begin
sclk = ~cpol; // leading edge
if (cpha == 1'b1) begin
#(tco); mosi = data[7-i]; #(H-tco);
end else begin
#(H);
end
sclk = cpol; // trailing edge
if (cpha == 1'b0 && i < 7) begin
#(tco); mosi = data[6-i]; #(H-tco);
end else begin
#(H);
end
end
cs_n = 1'b1;
#(H*2);
end
endtask
task drive_frame;
input cpol;
input cpha;
input [7:0] data;
begin
drive_frame_tco(cpol, cpha, data, 0);
end
endtask
task expect_mode;
input cpol;
input cpha;
input [7:0] data;
reg [1:0] want;
begin
want = {cpol, cpha};
drive_frame(cpol, cpha, data);
if (!mode_valid) begin
$display(" FAIL: mode %0d frame produced no verdict", want);
errors = errors + 1;
end else if (mode !== want) begin
$display(" FAIL: mode %0d frame was read as mode %0d", want, mode);
errors = errors + 1;
end else begin
$display(" mode %0d frame (data 0x%02h) inferred as mode %0d cpol=%0b cpha=%0b",
want, data, mode, mode[1], mode[0]);
end
if (conflict) begin
$display(" FAIL: clean mode %0d frame reported a conflict", want);
errors = errors + 1;
end
end
endtask
initial begin
repeat (5) @(negedge clk);
rst_n = 1'b1;
@(negedge clk);
// 1. All four modes, with a payload that transitions often enough
// to carry phase information.
expect_mode(1'b0, 1'b0, 8'hA5);
expect_mode(1'b0, 1'b1, 8'hA5);
expect_mode(1'b1, 1'b0, 8'hA5);
expect_mode(1'b1, 1'b1, 8'hA5);
// 2. A different payload must not change the verdict -- the mode is
// a property of the clocking, not of the data.
expect_mode(1'b0, 1'b1, 8'h5A);
expect_mode(1'b1, 1'b0, 8'h33);
// 3. A payload with no transitions carries no phase information.
// The honest answer is "cannot tell", and a block that guessed
// would be right half the time and confidently wrong the rest.
drive_frame(1'b0, 1'b0, 8'hFF);
if (mode_valid) begin
$display(" FAIL: an all-ones payload produced a phase verdict");
errors = errors + 1;
end else begin
$display(" all-ones payload: no verdict offered, as it must be");
end
// 4. A real capture has clock-to-out: MOSI changes a few nanoseconds
// AFTER its launching edge, not exactly on it. The attribution
// must put both cases on the same parity.
for (m = 0; m < 4; m = m + 1) begin
drive_frame_tco(m[1], m[0], 8'hA5, H/4);
if (!mode_valid || mode !== m[1:0]) begin
$display(" FAIL: with clock-to-out, mode %0d was read as %0d (valid=%0b)",
m, mode, mode_valid);
errors = errors + 1;
end
end
$display(" all four modes re-inferred with a %0d ns clock-to-out delay", H/4);
// 5. CPOL survives the no-transition case: it needs no data
// transitions at all, only the idle level, so the two halves of
// the answer have genuinely different evidence requirements.
drive_frame(1'b1, 1'b1, 8'hA5);
if (!mode_valid || mode[1] !== 1'b1) begin
$display(" FAIL: CPOL=1 not recovered from the idle level");
errors = errors + 1;
end
if (errors == 0)
$display("PASS: all four modes are inferred from the edge parity of MOSI transitions, the verdict is independent of the payload, and a payload with no transitions is reported as no verdict rather than a guess");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule-- spi_mode_infer.vhd
--
-- Chapter 10.2 -- reading CPOL and CPHA off a capture, in VHDL.
--
-- CPOL is the level SCLK rests at while CS is inactive: one sample.
-- CPHA is the parity of the edges MOSI is launched on -- number the SCLK
-- edges inside a frame from 1, and CPHA=0 launches at CS assertion and on
-- the EVEN edges, CPHA=1 on the ODD ones.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_mode_infer is
generic (
CNT_W : positive := 8;
MIN_EVIDENCE : natural := 2 -- transitions needed before reporting
);
port (
clk : in std_logic; -- oversampling clock, much faster than SCLK
rst_n : in std_logic;
sclk : in std_logic; -- observed bus
mosi : in std_logic;
cs_n : in std_logic;
mode : out std_logic_vector(1 downto 0); -- {CPOL, CPHA}
mode_valid : out std_logic;
conflict : out std_logic
);
end entity;
architecture rtl of spi_mode_infer is
-- Two-stage synchronisers. The bus is asynchronous to this clock, and
-- an inference built on a metastable sample is worse than none.
signal sclk_s1, sclk_s2, sclk_q : std_logic := '0';
signal mosi_s1, mosi_s2, mosi_q : std_logic := '0';
signal cs_s1, cs_s2, cs_q : std_logic := '1';
signal odd_cnt : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal even_cnt : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal pre_cnt : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal any_edge : std_logic := '0';
signal edge_odd : std_logic := '0';
signal cpol_r : std_logic := '0';
signal mode_r : std_logic_vector(1 downto 0) := (others => '0');
signal valid_r : std_logic := '0';
signal conf_r : std_logic := '0';
signal sclk_edge : std_logic;
signal mosi_edge : std_logic;
signal cs_fall : std_logic;
signal cs_rise : std_logic;
signal next_odd : std_logic;
signal cur_odd : std_logic;
signal have_edge : std_logic;
begin
sclk_edge <= '1' when sclk_s2 /= sclk_q else '0';
mosi_edge <= '1' when mosi_s2 /= mosi_q else '0';
cs_fall <= '1' when cs_q = '1' and cs_s2 = '0' else '0';
cs_rise <= '1' when cs_q = '0' and cs_s2 = '1' else '0';
-- Parity of the edge this cycle belongs to. A bit is launched AT an
-- edge, so a MOSI transition seen in the same cycle as an edge belongs
-- to that edge, not the previous one -- while a transition seen a cycle
-- or two later (real clock-to-out) belongs to the edge already
-- recorded. Both must land on the same parity; getting this wrong
-- inverts CPHA in every mode while leaving CPOL correct.
next_odd <= not edge_odd when any_edge = '1' else '1';
cur_odd <= next_odd when sclk_edge = '1' else edge_odd;
have_edge <= any_edge or sclk_edge;
observe : process (clk, rst_n)
begin
if rst_n = '0' then
sclk_s1 <= '0'; sclk_s2 <= '0'; sclk_q <= '0';
mosi_s1 <= '0'; mosi_s2 <= '0'; mosi_q <= '0';
cs_s1 <= '1'; cs_s2 <= '1'; cs_q <= '1';
odd_cnt <= (others => '0');
even_cnt <= (others => '0');
pre_cnt <= (others => '0');
any_edge <= '0';
edge_odd <= '0';
cpol_r <= '0';
mode_r <= (others => '0');
valid_r <= '0';
conf_r <= '0';
elsif rising_edge(clk) then
sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
cs_s1 <= cs_n; cs_s2 <= cs_s1; cs_q <= cs_s2;
-- CPOL is the idle level. Sampling continuously while CS is
-- inactive means the last sample before the frame is the one
-- that counts, which is exactly the definition.
if cs_s2 = '1' then
cpol_r <= sclk_s2;
end if;
if cs_fall = '1' then
-- A new frame: discard the previous frame's evidence. Mode
-- is a property of one device, and two devices on the same
-- bus may legitimately differ.
odd_cnt <= (others => '0');
even_cnt <= (others => '0');
pre_cnt <= (others => '0');
any_edge <= '0';
edge_odd <= '0';
elsif cs_s2 = '0' then
if sclk_edge = '1' then
any_edge <= '1'; -- edge 1 is odd, 2 even, ...
edge_odd <= next_odd;
end if;
if mosi_edge = '1' then
if have_edge = '0' then
pre_cnt <= pre_cnt + 1;
elsif cur_odd = '1' then
odd_cnt <= odd_cnt + 1;
else
even_cnt <= even_cnt + 1;
end if;
end if;
end if;
if cs_rise = '1' then
-- Report at the end of the frame, when the evidence is
-- complete. A mid-frame verdict would rest on one or two
-- transitions and would change as the frame went on.
if to_integer(odd_cnt) + to_integer(even_cnt) +
to_integer(pre_cnt) >= MIN_EVIDENCE then
mode_r(1) <= cpol_r;
if odd_cnt > even_cnt then
mode_r(0) <= '1';
else
mode_r(0) <= '0';
end if;
valid_r <= '1';
else
-- Not enough transitions to tell. An all-ones or
-- all-zeros payload carries no phase information at all,
-- which is a real limit of the method and not a bug.
valid_r <= '0';
end if;
if odd_cnt /= 0 and even_cnt /= 0 then
conf_r <= '1';
else
conf_r <= '0';
end if;
end if;
end if;
end process;
mode <= mode_r;
mode_valid <= valid_r;
conflict <= conf_r;
end architecture;-- spi_mode_infer_tb.vhd
--
-- The same checks as the SystemVerilog and Verilog testbenches: all four
-- modes inferred from real frames, a verdict independent of the payload,
-- both attribution windows (a transition on the edge and one delayed by
-- clock-to-out), and an honest "cannot tell" for a flat payload.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_mode_infer_tb is
end entity;
architecture sim of spi_mode_infer_tb is
constant CNT_W : positive := 8;
constant H : time := 20 ns; -- SCLK half period
constant TCLK : time := 2 ns; -- oversampling period
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal halt : boolean := false;
signal sclk : std_logic := '0';
signal mosi : std_logic := '0';
signal cs_n : std_logic := '1';
signal mode : std_logic_vector(1 downto 0);
signal mode_valid : std_logic;
signal conflict : std_logic;
signal errors : natural := 0;
begin
clk <= not clk after TCLK / 2 when not halt else '0';
dut : entity work.spi_mode_infer
generic map (CNT_W => CNT_W, MIN_EVIDENCE => 2)
port map (
clk => clk, rst_n => rst_n,
sclk => sclk, mosi => mosi, cs_n => cs_n,
mode => mode, mode_valid => mode_valid, conflict => conflict
);
stim : process
variable errs : natural := 0;
variable mv : std_logic_vector(1 downto 0);
-- A behavioural master. Leading edge means "away from idle",
-- trailing means "back to idle" -- the definitions that let one
-- body of code serve all four modes. tco models clock-to-out.
procedure drive_frame_tco(cpol : std_logic; cpha : std_logic;
data : std_logic_vector(7 downto 0);
tco : time) is
begin
sclk <= cpol;
cs_n <= '1';
wait for H * 2;
cs_n <= '0';
if cpha = '0' then -- CPHA=0 launches at CS
wait for tco; mosi <= data(7); wait for H - tco;
else
wait for H;
end if;
for i in 0 to 7 loop
sclk <= not cpol; -- leading edge
if cpha = '1' then
wait for tco; mosi <= data(7 - i); wait for H - tco;
else
wait for H;
end if;
sclk <= cpol; -- trailing edge
if cpha = '0' and i < 7 then
wait for tco; mosi <= data(6 - i); wait for H - tco;
else
wait for H;
end if;
end loop;
cs_n <= '1';
wait for H * 2;
end procedure;
procedure drive_frame(cpol : std_logic; cpha : std_logic;
data : std_logic_vector(7 downto 0)) is
begin
drive_frame_tco(cpol, cpha, data, 0 ns);
end procedure;
procedure expect_mode(cpol : std_logic; cpha : std_logic;
data : std_logic_vector(7 downto 0)) is
variable want : std_logic_vector(1 downto 0);
begin
want := cpol & cpha;
drive_frame(cpol, cpha, data);
if mode_valid /= '1' then
report " FAIL: mode " &
integer'image(to_integer(unsigned(want))) &
" frame produced no verdict";
errs := errs + 1;
elsif mode /= want then
report " FAIL: mode " &
integer'image(to_integer(unsigned(want))) &
" frame was read as mode " &
integer'image(to_integer(unsigned(mode)));
errs := errs + 1;
else
report " mode " & integer'image(to_integer(unsigned(want))) &
" frame inferred as mode " &
integer'image(to_integer(unsigned(mode)));
end if;
if conflict = '1' then
report " FAIL: a clean frame reported a conflict";
errs := errs + 1;
end if;
end procedure;
begin
for i in 0 to 4 loop
wait until falling_edge(clk);
end loop;
rst_n <= '1';
wait until falling_edge(clk);
-- 1. All four modes, with a payload that transitions often enough
-- to carry phase information.
expect_mode('0', '0', x"A5");
expect_mode('0', '1', x"A5");
expect_mode('1', '0', x"A5");
expect_mode('1', '1', x"A5");
-- 2. A different payload must not change the verdict -- the mode is
-- a property of the clocking, not of the data.
expect_mode('0', '1', x"5A");
expect_mode('1', '0', x"33");
-- 3. A payload with no transitions carries no phase information.
-- The honest answer is "cannot tell", and a block that guessed
-- would be right half the time and confidently wrong the rest.
drive_frame('0', '0', x"FF");
if mode_valid = '1' then
report " FAIL: an all-ones payload produced a phase verdict";
errs := errs + 1;
else
report " all-ones payload: no verdict offered, as it must be";
end if;
-- 4. A real capture has clock-to-out: MOSI changes a few
-- nanoseconds AFTER its launching edge, not exactly on it. The
-- attribution must put both cases on the same parity.
for m in 0 to 3 loop
mv := std_logic_vector(to_unsigned(m, 2));
drive_frame_tco(mv(1), mv(0), x"A5", H / 4);
if mode_valid /= '1' or mode /= mv then
report " FAIL: with clock-to-out, mode " &
integer'image(m) & " was read as " &
integer'image(to_integer(unsigned(mode)));
errs := errs + 1;
end if;
end loop;
report " all four modes re-inferred with a clock-to-out delay";
-- 5. CPOL survives the no-transition case: it needs no data
-- transitions at all, only the idle level, so the two halves of
-- the answer have genuinely different evidence requirements.
drive_frame('1', '1', x"A5");
if mode_valid /= '1' or mode(1) /= '1' then
report " FAIL: CPOL=1 not recovered from the idle level";
errs := errs + 1;
end if;
errors <= errs;
if errs = 0 then
report "PASS: all four modes are inferred from the edge parity of MOSI transitions, the verdict is independent of the payload, and a payload with no transitions is reported as no verdict rather than a guess";
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 observer: identical ports and generics, two-stage synchronisers on every bus input, CPOL from the idle level, CPHA from the parity of MOSI transitions, a verdict published on CS release, and no verdict when the evidence is insufficient. All three testbenches drive the same frames — four modes at two payloads, all four modes again with a clock-to-out delay, and an all-ones frame — and finish at the same simulation time with identical results.
7. Why a Verification Engineer Cares
// 1. DETERMINISM. The same capture always yields the same verdict.
// An observer whose answer depends on when it was reset is not
// measuring the bus.
a_stable_verdict : assert property (
@(posedge clk) disable iff (!rst_n)
(mode_valid && !cs_fall) |=> $stable(mode))
else $error("the verdict changed without a new frame");
// 2. HONESTY. A verdict is only published when the frame carried
// enough evidence. This is the property that separates a measurement
// from a guess.
a_no_guessing : assert property (
@(posedge clk) disable iff (!rst_n)
(cs_rise && (odd_cnt + even_cnt + pre_cnt) < MIN_EVIDENCE)
|=> !mode_valid)
else $error("a verdict was published without evidence");
// 3. CPOL needs no data at all -- it is the idle level, and it must be
// recoverable even from a frame that carries no phase information.
a_cpol_independent : assert property (
@(posedge clk) disable iff (!rst_n)
(cs_fall) |-> (cpol_r == $past(sclk_s2)))
else $error("CPOL was not taken from the idle level");
// 4. The conflict flag means what it says: transitions on both
// parities, which a clean single-mode frame can never produce.
a_conflict_real : assert property (
@(posedge clk) disable iff (!rst_n)
(cs_rise && conflict) |-> (odd_cnt != 0 && even_cnt != 0))
else $error("conflict asserted without transitions on both parities");Property 2 is the one that generalises. Any inference block must be able to say "I do not know" — and the temptation to always produce an answer is strong, because an output that is sometimes invalid is harder to consume. But a diagnostic that is confidently wrong half the time costs more debugging than it saves, and the all-ones payload makes that case reachable in every real system.
Property 3 records something the design makes easy to miss: the two halves of the answer have different evidence requirements. CPOL needs no data transitions whatever; CPHA needs several. A testbench that only checks the mode as a pair will never notice that one half is still correct when the other cannot be determined.
Coverage must reach the cases a directed test would skip:
covergroup spi_mode_cg @(posedge cs_rise);
cp_mode : coverpoint {cpol_r, cpha_inferred} {
bins m0 = {2'b00}; bins m1 = {2'b01};
bins m2 = {2'b10}; bins m3 = {2'b11};
}
// The payload decides whether CPHA is decidable at all. A suite
// using only 0xA5 never exercises the "cannot tell" path, and the
// honesty property above is then never checked.
cp_payload : coverpoint transitions_in_frame {
bins none = {0}; // no phase information
bins one = {1}; // below MIN_EVIDENCE
bins few = {[2:3]};
bins many = {[4:$]};
}
// Clock-to-out places the transition in a different oversampling
// cycle from its edge. Both windows must give the same parity.
cp_tco : coverpoint tco_class {
bins on_edge = {TCO_ZERO};
bins just_after = {TCO_SMALL};
bins late = {TCO_LARGE};
}
x_mode_payload : cross cp_mode, cp_payload;
x_mode_tco : cross cp_mode, cp_tco;
endgroupcp_payload is the coverpoint that matters and the one most often absent. The mode bins fill immediately from any directed suite; the payload bins are what prove the block behaves when the data cannot answer the question.
8. Why an FPGA or ASIC Engineer Cares
Instantiate the inferrer on the debug build. It costs a few hundred gates and answers, in hardware, the question that otherwise needs a scope and an afternoon: what mode is this link actually running in? On a board with several devices it answers it per chip select.
Make the mode a profile field, not a parameter. Chapter 10.1 argued this for the whole profile, and the mode is the field where it pays first: a controller serving two devices in different modes must switch between them per transaction, and a parameter cannot.
Apply the mode through reset or between frames, never during one. Chapter 8.1 made the same point about polarity: a mode change mid-frame produces a transfer matching no device, and the resulting capture is very hard to read.
Prefer mode 3 over mode 0 when a part accepts both, on a bus with other devices. SCLK idling high means the line is not sitting at the level a glitch would have to reach to look like an edge — and, more practically, it makes a stuck-low clock visibly distinct from an idle one on a scope.
Record the idle level in a status bit. One flip-flop sampling SCLK while CS is inactive turns "is the controller configured for the mode we think?" into a register read.
9. Failure Signature — A Link That Works at 1 MHz and Fails Above 8 MHz
Symptom. Bring-up succeeds at a low clock rate. Every register reads correctly, the ID matches, and the part behaves. Raising SCLK works up to around 8 MHz and then reads become corrupted — not intermittently, but consistently above a threshold. The device is rated far higher.
What the low-rate success establishes. The wiring is right, the command encoding is right and the device is alive. So the fault is something that only matters as the period shrinks — which is a short list.
Plausible mechanisms.
- A CPHA error. Sampling half a bit time away from the intended point still works while the bit time is long compared with every delay in the system. As the period shrinks, the half-bit error stops being absorbed by the margin. This is the mechanism that best fits a sharp threshold.
- The round-trip limit of Chapter 9.4, which also produces a sharp threshold — but shifts data by whole bits rather than corrupting it.
- Signal integrity, which usually degrades gradually rather than at a clean threshold.
- A divisor that exceeds the device's true maximum for the specific command in use.
The discriminating observation. Compare a good capture at 1 MHz with a bad one at 10 MHz and look at where MOSI changes relative to the edges, not at the data. If the controller is launching on the wrong parity of edge, that is visible in both captures — it was simply tolerated at the lower rate. If the launch parity is identical in both and only the returned data is wrong, the problem is on the return path and belongs to Chapter 9.4.
Then check the mode directly: the inferrer of §6, or a single flip-flop sampling SCLK while CS is inactive plus one look at the first data transition.
The fix, and why it is often mis-applied. Correct the mode. The mis-application is to lower the clock and ship, because the link works at 4 MHz — which leaves a design whose margin is a half bit time and which fails on the first slower device or warmer enclosure.
Why the investigation goes wrong. Because a rate-dependent failure looks like a timing problem, and the whole investigation goes to trace lengths and terminations — which are real concerns from Chapter 9.4 and are simply not this. A mode error is a logical fault that presents as a timing symptom, and that mismatch between appearance and cause is what makes it expensive.
10. Common Misconceptions
11. Reason It Through
Work this before reading the answer.
A datasheet's timing diagram shows SCLK idling high. The text says: "Data on SDI is latched on the falling edge of SCLK. Data on SDO is updated on the rising edge of SCLK."
What mode is this, and which of the two sentences did the work?
Start with CPOL. SCLK idles high, so CPOL = 1. That is observation one and it is settled by the picture alone.
Now identify which sentence describes the master's view. SDI is the device's input — which is MOSI, the line the master drives. SDO is the device's output — MISO. The master's data is latched by the device on the falling edge of SCLK.
Translate the sampling edge into CPHA. With CPOL=1 the clock idles high, so the leading edge — the first edge of the frame, away from idle — is a falling edge. The device latches on falling edges, which are the leading ones, which are edges 1, 3, 5. Sampling on odd edges means CPHA = 0.
So the mode is 2 (CPOL=1, CPHA=0).
Which sentence did the work? The first one — the SDI sentence, describing the input. The SDO sentence describes the device's output timing and is the trap: it says "updated on the rising edge", and a reader who converts that into CPHA gets the opposite answer.
And why it is not a contradiction. The device samples its input on falling edges and updates its output on rising edges. Those are opposite edges, and they must be: that is exactly the full-duplex structure, where each side launches on one edge and samples on the other so that data is stable when it is looked at. A device that both sampled and launched on the same edge would have no setup time at all.
The general lesson. When a datasheet gives you two edge statements, one of them is about the master's data and one is about the device's. Identify which pin is the device's input before converting anything into CPHA — and if the pin names are SDI and SDO rather than MOSI and MISO, that naming is itself the clue, because they are named from the device's point of view.
12. Understanding Check
13. Summary
Most datasheets do not state CPOL and CPHA, and when they do the statement may describe the device's output rather than its input — opposite edges, and a consistently wrong answer. The picture is more reliable than the prose.
Observation one: the level SCLK rests at while CS is inactive is CPOL. Look outside the frame, not at its start.
Observation two: the parity of the edges MOSI changes on is CPHA. Changing at CS assertion and on even edges is 0; changing on odd edges is 1. The quickest tell is the first bit — valid before any edge means CPHA=0.
Read the transitions, not the sampling arrows, because every diagram must draw the transitions while the annotations vary by vendor.
Trying all four modes cannot work: a wrong mode and a wrong command look identical, modes 0 and 3 usually both appear to work, and a CPHA error passes at a low rate and fails at speed.
In hardware the same two observations are a small oversampling observer — CPOL from one sample, CPHA from the parity of MOSI transitions — with the subtlety being the attribution window, since a transition may arrive on its edge or a clock-to-out later and both must land on the same parity.
And it must be able to say no verdict: an all-ones payload carries no phase information, CPOL remains recoverable, and a guess that is confidently wrong half the time is worse than silence.
14. What Comes Next
The mode is one of the two numbers pass two of the route was after. The other is the rate — and behind it a table of setup, hold and valid times whose conditions matter as much as their values.
Chapter 10.3 — Extracting Setup, Hold, and Maximum SCLK turns that table into a constraint set you can actually close: which numbers belong to which direction, why a maximum frequency is meaningless without its load and corner, how to combine the device's numbers with the board's into the budget of Chapter 9.4 — and the monitor that measures the real link against the datasheet, in all three HDLs.
Continue learning
Related tutorials
- Related topic
CPOL/CPHA-Aware Edge Control
All four SPI modes from one datapath: why the launch and capture assignment depends on CPHA alone, why the two phases are not symmetric, and why CPHA=0 needs both a preload and a suppressed final launch.
- Related topic
CPOL/CPHA Handling in a Slave
Three mismatches with three observabilities: polarity from a level before a bit moves, phase from a motion and provably not from the edge count, bit order not at all. Includes the wrong answer this chapter shipped first, why it was wrong, and mode logic verified in three HDLs against both a combinational and a registered master output.
- Related topic
Extracting Setup, Hold, and Maximum SCLK
Which timing-table rows constrain you and which constrain the device, why a number without its load and corner is not a specification, how a delay on one line alone destroys margin, and the monitor that measures the real link against the datasheet.
- 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.
