SPI · Module 11
Boot and Flash Transaction Analysis
Reading a real 25-series boot capture back against the datasheet: classifying frames from their opcodes, checking length against shape, and tracking the write-enable latch across frames so a well-formed program that follows no WREN is caught.
Six chapters have built the pieces. This one uses them on the artefact an engineer actually holds: a capture.
A logic analyser gives you bytes and chip-select edges. Which faults can you find inside a single frame, and which need you to look across frames?
The distinction matters more than it sounds. Every fault in this module so far is one of two kinds, and the second kind is invisible to any tool — or any person — examining one frame at a time.
1. What a Capture Gives You, and What It Does Not
A capture is a sequence of frames, each a chip-select assertion, some bytes, and a release. From that alone you can determine:
✓ the opcode of each frame its first byte
✓ the shape that opcode implies from the datasheet
✓ whether the frame's length matches a count
✓ the address, for commands that have one
✓ the payload length length minus the shape's overheadAnd from that alone you cannot determine:
✗ whether the data written was correct you see it, not what was intended
✗ whether an operation actually took a discarded write looks identical
✗ whether the device was in 4-byte mode the capture is consistent either wayThe first list is a per-frame analysis. The second needs either knowledge of intent or — for the middle one — state carried between frames, which is the point of this chapter.
2. Per-Frame Analysis: Shape Against Length
Each opcode implies a shape (Chapter 10.5), and the shape gives every boundary inside the frame:
opcode class addr dummy the rest is payload
0x03 READ 3 0
0x0B FAST READ 3 1 ← one dummy BYTE, 8 cycles
0x05 RDSR 0 0
0x06 WREN 0 0 exactly 1 byte, no payload
0x02 PAGE PROGRAM 3 0 modifies — needs WEL
0x20 SECTOR ERASE 3 0 modifies — needs WEL
0x9F RDID 0 0
0xAB RDP 0 0Three checks fall out, and each catches a distinct real fault.
An unknown opcode invalidates everything after it. Without the shape, the address and payload boundaries are unknown, so no other field in that frame can be reported. A decoder that guessed would produce a plausible address from arbitrary bytes.
A frame shorter than its shape is mis-framed or truncated. A 0x03 frame of three bytes has only two address bytes — so the "address" a naive decoder reports is built from the wrong bytes and looks entirely reasonable.
A frame longer than an exact-length opcode means a missed chip-select edge. WREN is one byte. A WREN frame carrying four more bytes is almost certainly two frames the analyser merged because it missed a CS edge — and recognising that is what stops you debugging a device that is behaving correctly.
The fast read's one dummy byte is the detail that most often produces an off-by-one in hand analysis. Its payload is one byte shorter than the same frame length would give a plain read, and a person counting bytes in a capture viewer gets this wrong routinely.
3. Cross-Frame Analysis: the Latch
Here is the fault that no per-frame check can find.
Chapter 11.4 established that a program or erase issued with WEL clear is discarded silently. Now consider what that looks like in a capture: a perfectly well-formed page program frame, correct opcode, correct address, correct payload length, correctly framed. Nothing about it is wrong.
What is wrong is the frame that should have preceded it and did not.
So the decoder must mirror the device's own latch:
WREN (exactly 1 byte) → set the latch
WRDI → clear the latch
a modifying operation → clear the latch (the DEVICE does this)
a MALFORMED WREN → do NOT set itThat third line is the one people forget when reasoning by hand. Two page programs after one WREN means the second was discarded, because the device cleared the latch when the first completed. A capture showing WREN, PP, PP is a capture showing one successful program.
The fourth line is subtler and matters for the same reason. A WREN frame that is malformed — carrying payload, because the analyser merged two frames — must not arm the decoder's latch. If it did, one mis-framed capture would hide a real missing-WREN fault in the frame after it, and the decoder would report nothing where the device discarded a write.
4. The Signature in a Capture
Two programs, one WREN
8 cyclesThe WEL trace falling at cell 3 without any WRDI is the behaviour to internalise: the device disarms itself. And the second PP at cell 4 is byte-for-byte as valid as the first — the only difference is what happened two frames earlier.
5. A Worked Boot Capture
Here is the capture §6's decoder is checked against, written as an engineer would annotate it:
# frame decode verdict
1 AB xx xx xx RDP, 3 payload bytes ok
2 9F EF 40 18 RDID, 3 bytes — the ID ok
3 03 00 00 00 + 12 bytes READ 0x000000, 12 payload ok
4 0B 00 10 00 + dummy + 16 FAST READ 0x001000, 16 ok
5 05 xx RDSR, 1 byte ok
6 06 WREN ok
7 02 00 20 00 + 8 bytes PP 0x002000, 8 bytes ok
8 02 00 21 00 + 8 bytes PP 0x002100, 8 bytes NO WREN
9 20 00 30 00 SE 0x003000 NO WRENFrames 1 to 7 are a textbook boot-and-write sequence: wake, identify, read a header, fast-read an image, check status, arm, program. Frames 8 and 9 are the discovery — two modifying operations that will not happen, both perfectly well formed.
Note frame 4 in particular. It is the fast read, and its payload is 16 bytes from 17 bytes after the address, because one of them is the dummy byte. A hand count that forgets the dummy byte reports 17 and then cannot reconcile the total.
6. Building the Capture Decoder — Three HDLs
The circuit
Circuit. A frame parser with a combinational shape lookup and one bit of cross-frame state.
State. The frame's first byte, a byte count, an address accumulator, and the mirrored write-enable latch.
Datapath. The shape is decoded combinationally from the opcode, so no table memory is needed. The address accumulates by shifting left as bytes arrive, which reconstructs a most-significant-first address without knowing its width in advance.
Control. Bytes are counted through the frame; at the chip-select release the verdict is computed and published as a pulse.
Clock and reset. System clock; asynchronous active-low reset.
Enables. anomaly names the fault, and the checks are ordered by how much each invalidates the rest of the decode — an unknown opcode first, because without the shape nothing else means anything.
Timing. One frame's verdict per frame_end, one cycle later.
Synthesis. A shift register, three counters, a comparator chain and one flip-flop for the latch.
Limitations. It knows the opcodes it was given. It also cannot detect a device in 4-byte addressing mode, because a capture is consistent with either interpretation — a 0x03 frame with four address bytes and n payload bytes is indistinguishable from one with three address bytes and n+1. Resolving that needs knowledge the capture does not contain, which is an honest limit rather than a defect.
The payload guard. The payload is the frame length minus opcode, address and dummy — and it is guarded, because a short frame would underflow. A 16-bit underflow reports around 65 000 payload bytes, which looks like a decoder bug rather than a short frame and sends the investigation to the wrong place entirely.
// flash_capture_decode.sv
//
// Chapter 11.7 -- reading a 25-series capture back against the datasheet.
//
// A logic analyser gives you bytes and chip-select edges. Turning that into
// "this is a fast read of 0x001000 for 64 bytes, and the page program three
// frames later was never write-enabled" is the skill this chapter is about,
// and it is mechanisable.
//
// The decoder does three things a person does by hand:
//
// * CLASSIFIES each frame from its first byte, and derives the shape that
// opcode implies -- how many address bytes, how many dummy bytes;
// * CHECKS the frame's actual length against that shape, so a frame that
// is too short for its own opcode is reported rather than mis-parsed;
// * and TRACKS STATE ACROSS FRAMES, which is the part that finds real
// bugs. A page program or erase that follows no WREN is the single most
// common fault in a flash capture, and it is invisible inside any one
// frame -- the frame itself is perfectly well formed.
//
// That cross-frame check mirrors the device: WREN sets the latch, and the
// device clears it when a modifying operation completes. So the decoder
// clears its own flag on a modifying frame, which means two programs after
// one WREN reports an anomaly on the second -- exactly as the hardware
// behaves.
module flash_capture_decode (
input logic clk,
input logic rst_n,
input logic frame_start, // chip select asserted
input logic frame_end, // chip select released
input logic byte_valid,
input logic [7:0] byte_in,
output logic [3:0] cmd_class,
output logic [23:0] cmd_addr,
output logic [15:0] data_bytes,
output logic frame_valid, // pulse at frame_end: decode is ready
output logic [2:0] anomaly,
output logic [15:0] frames_seen,
output logic [15:0] anomalies_seen
);
// Command classes.
localparam logic [3:0] C_UNKNOWN = 4'd0;
localparam logic [3:0] C_READ = 4'd1; // 0x03
localparam logic [3:0] C_FREAD = 4'd2; // 0x0B
localparam logic [3:0] C_RDSR = 4'd3; // 0x05
localparam logic [3:0] C_WREN = 4'd4; // 0x06
localparam logic [3:0] C_WRDI = 4'd5; // 0x04
localparam logic [3:0] C_PP = 4'd6; // 0x02
localparam logic [3:0] C_SE = 4'd7; // 0x20
localparam logic [3:0] C_BE = 4'd8; // 0xD8
localparam logic [3:0] C_CE = 4'd9; // 0xC7
localparam logic [3:0] C_RDID = 4'd10; // 0x9F
localparam logic [3:0] C_RDP = 4'd11; // 0xAB
// Anomalies, in detection order.
localparam logic [2:0] A_NONE = 3'd0;
localparam logic [2:0] A_UNKNOWN = 3'd1;
localparam logic [2:0] A_SHORT = 3'd2;
localparam logic [2:0] A_NO_WREN = 3'd3;
localparam logic [2:0] A_LONG = 3'd4;
logic [7:0] op_byte;
logic have_op;
logic [15:0] n_bytes; // bytes in this frame, opcode included
logic [23:0] addr_acc;
logic [15:0] addr_taken; // address bytes captured so far
logic wren_latched; // mirrors the device's WEL
// Shape of the current opcode. Decoded combinationally from the first
// byte, which is why the decoder needs no table memory.
logic [3:0] sh_class;
logic [15:0] sh_addr;
logic [15:0] sh_dummy;
logic sh_exact; // the frame must be exactly opcode-length
logic sh_modifies; // needs WEL, and clears it
always_comb begin
sh_class = C_UNKNOWN;
sh_addr = 16'd0;
sh_dummy = 16'd0;
sh_exact = 1'b0;
sh_modifies = 1'b0;
case (op_byte)
8'h03: begin sh_class = C_READ; sh_addr = 3; end
8'h0B: begin sh_class = C_FREAD; sh_addr = 3; sh_dummy = 1; end
8'h05: begin sh_class = C_RDSR; end
8'h06: begin sh_class = C_WREN; sh_exact = 1'b1; end
8'h04: begin sh_class = C_WRDI; sh_exact = 1'b1; end
8'h02: begin sh_class = C_PP; sh_addr = 3; sh_modifies = 1'b1; end
8'h20: begin sh_class = C_SE; sh_addr = 3; sh_modifies = 1'b1; end
8'hD8: begin sh_class = C_BE; sh_addr = 3; sh_modifies = 1'b1; end
8'hC7: begin sh_class = C_CE; sh_exact = 1'b1; sh_modifies = 1'b1; end
8'h9F: begin sh_class = C_RDID; end
8'hAB: begin sh_class = C_RDP; end
default: sh_class = C_UNKNOWN;
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
op_byte <= 8'h00;
have_op <= 1'b0;
n_bytes <= 16'd0;
addr_acc <= 24'd0;
addr_taken <= 16'd0;
wren_latched <= 1'b0;
cmd_class <= C_UNKNOWN;
cmd_addr <= 24'd0;
data_bytes <= 16'd0;
frame_valid <= 1'b0;
anomaly <= A_NONE;
frames_seen <= 16'd0;
anomalies_seen <= 16'd0;
end else begin
frame_valid <= 1'b0;
if (frame_start) begin
have_op <= 1'b0;
n_bytes <= 16'd0;
addr_acc <= 24'd0;
addr_taken <= 16'd0;
end else if (byte_valid) begin
n_bytes <= n_bytes + 16'd1;
if (!have_op) begin
op_byte <= byte_in;
have_op <= 1'b1;
end else if (addr_taken < sh_addr) begin
// Addresses are most significant byte first, so
// shifting left as bytes arrive reconstructs them in
// order without knowing the width in advance.
addr_acc <= {addr_acc[15:0], byte_in};
addr_taken <= addr_taken + 16'd1;
end
end
if (frame_end) begin
frames_seen <= frames_seen + 16'd1;
cmd_class <= sh_class;
cmd_addr <= addr_acc;
frame_valid <= 1'b1;
// Payload is whatever is left after opcode, address and
// dummy. Guarded, because a short frame would underflow --
// and a 16-bit underflow reports 65 000 data bytes, which
// looks like a decoder bug rather than a short frame.
if (n_bytes > (16'd1 + sh_addr + sh_dummy))
data_bytes <= n_bytes - 16'd1 - sh_addr - sh_dummy;
else
data_bytes <= 16'd0;
// The anomaly checks, in order of how much they invalidate
// the rest of the decode.
if (sh_class == C_UNKNOWN) begin
// Nothing else can be trusted: without the shape, the
// address and payload boundaries are unknown.
anomaly <= A_UNKNOWN;
anomalies_seen <= anomalies_seen + 16'd1;
end else if (n_bytes < (16'd1 + sh_addr + sh_dummy)) begin
anomaly <= A_SHORT;
anomalies_seen <= anomalies_seen + 16'd1;
end else if (sh_exact && n_bytes != 16'd1) begin
// A WREN with payload means the capture is
// mis-framed -- a missed chip-select edge merges two
// frames and produces exactly this.
anomaly <= A_LONG;
anomalies_seen <= anomalies_seen + 16'd1;
end else if (sh_modifies && !wren_latched) begin
// THE CROSS-FRAME CHECK. This frame is well formed;
// what is wrong is the frame that should have preceded
// it and did not.
anomaly <= A_NO_WREN;
anomalies_seen <= anomalies_seen + 16'd1;
end else begin
anomaly <= A_NONE;
end
// Mirror the device's latch: WREN sets it, WRDI clears it,
// and a completed modifying operation clears it too.
if (sh_class == C_WREN && n_bytes == 16'd1)
wren_latched <= 1'b1;
else if (sh_class == C_WRDI)
wren_latched <= 1'b0;
else if (sh_modifies)
wren_latched <= 1'b0;
end
end
end
endmodule// flash_capture_decode_tb.sv
//
// A realistic boot capture is replayed frame by frame and every decode is
// checked against what a person reading the datasheet would write down.
// Then the malformed frames are injected: an unknown opcode, a frame too
// short for its own shape, a WREN with payload, and -- the one that matters
// -- a perfectly well-formed page program that follows no WREN.
`timescale 1ns/1ps
module flash_capture_decode_tb;
localparam logic [3:0] C_UNKNOWN = 4'd0;
localparam logic [3:0] C_READ = 4'd1;
localparam logic [3:0] C_FREAD = 4'd2;
localparam logic [3:0] C_RDSR = 4'd3;
localparam logic [3:0] C_WREN = 4'd4;
localparam logic [3:0] C_WRDI = 4'd5;
localparam logic [3:0] C_PP = 4'd6;
localparam logic [3:0] C_SE = 4'd7;
localparam logic [3:0] C_BE = 4'd8;
localparam logic [3:0] C_CE = 4'd9;
localparam logic [3:0] C_RDID = 4'd10;
localparam logic [3:0] C_RDP = 4'd11;
localparam logic [2:0] A_NONE = 3'd0;
localparam logic [2:0] A_UNKNOWN = 3'd1;
localparam logic [2:0] A_SHORT = 3'd2;
localparam logic [2:0] A_NO_WREN = 3'd3;
localparam logic [2:0] A_LONG = 3'd4;
logic clk = 1'b0;
logic rst_n = 1'b0;
always #5 clk = ~clk;
logic frame_start = 1'b0;
logic frame_end = 1'b0;
logic byte_valid = 1'b0;
logic [7:0] byte_in = 8'h00;
logic [3:0] cmd_class;
logic [23:0] cmd_addr;
logic [15:0] data_bytes;
logic frame_valid;
logic [2:0] anomaly;
logic [15:0] frames_seen, anomalies_seen;
int errors = 0;
flash_capture_decode dut (
.clk(clk), .rst_n(rst_n),
.frame_start(frame_start), .frame_end(frame_end),
.byte_valid(byte_valid), .byte_in(byte_in),
.cmd_class(cmd_class), .cmd_addr(cmd_addr),
.data_bytes(data_bytes), .frame_valid(frame_valid),
.anomaly(anomaly),
.frames_seen(frames_seen), .anomalies_seen(anomalies_seen)
);
task automatic tick;
begin @(negedge clk); end
endtask
task automatic open_frame;
begin
@(negedge clk);
frame_start = 1'b1;
@(negedge clk);
frame_start = 1'b0;
end
endtask
task automatic send(input logic [7:0] b);
begin
@(negedge clk);
byte_in = b; byte_valid = 1'b1;
@(negedge clk);
byte_valid = 1'b0;
end
endtask
task automatic close_frame;
begin
@(negedge clk);
frame_end = 1'b1;
@(negedge clk);
frame_end = 1'b0;
@(negedge clk);
end
endtask
// Replay one frame: an opcode, some address bytes, some filler.
task automatic frame(input logic [7:0] op, input int addr_n,
input logic [23:0] a, input int extra);
begin
open_frame();
send(op);
if (addr_n >= 3) send(a[23:16]);
if (addr_n >= 2) send(a[15:8]);
if (addr_n >= 1) send(a[7:0]);
for (int i = 0; i < extra; i++) send(8'(8'hA0 + i[7:0]));
close_frame();
end
endtask
task automatic expect_decode(input string what, input logic [3:0] cls,
input logic [23:0] a, input int dbytes,
input logic [2:0] anom);
begin
if (cmd_class !== cls) begin
$display(" FAIL: %s decoded as class %0d, expected %0d",
what, cmd_class, cls);
errors++;
end
if (cmd_addr !== a) begin
$display(" FAIL: %s address 0x%06h, expected 0x%06h",
what, cmd_addr, a);
errors++;
end
if (data_bytes !== 16'(dbytes)) begin
$display(" FAIL: %s reported %0d data bytes, expected %0d",
what, data_bytes, dbytes);
errors++;
end
if (anomaly !== anom) begin
$display(" FAIL: %s anomaly %0d, expected %0d",
what, anomaly, anom);
errors++;
end
$display(" %-24s class=%-2d addr=0x%06h data=%0d anomaly=%0d",
what, cmd_class, cmd_addr, data_bytes, anomaly);
end
endtask
initial begin
repeat (3) @(negedge clk);
rst_n = 1'b1;
@(negedge clk);
// ---- a realistic boot capture -------------------------------------
// 1. Release from power-down, then three dummy bytes. It carries no
// address, so everything after the opcode is payload.
frame(8'hAB, 0, 24'h000000, 3);
expect_decode("RDP (0xAB)", C_RDP, 24'h000000, 3, A_NONE);
// 2. Read the JEDEC ID -- three bytes back, no address.
frame(8'h9F, 0, 24'h000000, 3);
expect_decode("RDID (0x9F)", C_RDID, 24'h000000, 3, A_NONE);
// 3. A plain read of the header: three address bytes, no dummy.
frame(8'h03, 3, 24'h000000, 12);
expect_decode("READ 0x000000, 12 B", C_READ, 24'h000000, 12, A_NONE);
// 4. A fast read of the image: three address bytes AND one dummy
// byte, so the payload is one byte SHORTER than the same frame
// length would give a plain read. Getting this wrong by one is
// the whole reason to decode rather than eyeball.
frame(8'h0B, 3, 24'h001000, 17);
expect_decode("FAST READ 0x001000", C_FREAD, 24'h001000, 16, A_NONE);
// 5. A status read.
frame(8'h05, 0, 24'h000000, 1);
expect_decode("RDSR (0x05)", C_RDSR, 24'h000000, 1, A_NONE);
// 6. WREN -- exactly one byte, no payload.
frame(8'h06, 0, 24'h000000, 0);
expect_decode("WREN (0x06)", C_WREN, 24'h000000, 0, A_NONE);
// 7. A page program that correctly follows the WREN above.
frame(8'h02, 3, 24'h002000, 8);
expect_decode("PP 0x002000, 8 B", C_PP, 24'h002000, 8, A_NONE);
// ---- now the faults ----------------------------------------------
// 8. A SECOND page program with no intervening WREN. The frame
// itself is perfectly well formed -- this is the check no
// single-frame decoder can make, and the commonest real fault.
frame(8'h02, 3, 24'h002100, 8);
expect_decode("PP with no WREN", C_PP, 24'h002100, 8, A_NO_WREN);
// 9. The same for an erase.
frame(8'h20, 3, 24'h003000, 0);
expect_decode("SE with no WREN", C_SE, 24'h003000, 0, A_NO_WREN);
// 10. WREN then erase -- correct, and it proves the latch really is
// being tracked rather than the anomaly being unconditional.
frame(8'h06, 0, 24'h000000, 0);
expect_decode("WREN again", C_WREN, 24'h000000, 0, A_NONE);
frame(8'h20, 3, 24'h003000, 0);
expect_decode("SE after WREN", C_SE, 24'h003000, 0, A_NONE);
// 11. WRDI cancels the latch, so a program after it is an anomaly
// again -- the decoder mirrors the device in both directions.
frame(8'h06, 0, 24'h000000, 0);
frame(8'h04, 0, 24'h000000, 0);
expect_decode("WRDI (0x04)", C_WRDI, 24'h000000, 0, A_NONE);
frame(8'h02, 3, 24'h004000, 4);
expect_decode("PP after WRDI", C_PP, 24'h004000, 4, A_NO_WREN);
// 12. An unknown opcode. Nothing after it can be trusted, because
// the shape -- and therefore every boundary -- is unknown.
frame(8'h77, 0, 24'h000000, 4);
if (cmd_class !== C_UNKNOWN || anomaly !== A_UNKNOWN) begin
$display(" FAIL: opcode 0x77 was not reported unknown (class=%0d anomaly=%0d)",
cmd_class, anomaly);
errors++;
end
$display(" %-24s class=%0d anomaly=%0d", "unknown opcode 0x77",
cmd_class, anomaly);
// 13. A frame too SHORT for its own shape: a read with only two
// address bytes. A decoder without this check would report a
// plausible address built from the wrong bytes.
frame(8'h03, 2, 24'h00ABCD, 0);
if (anomaly !== A_SHORT) begin
$display(" FAIL: a 3-byte read frame was not reported short (anomaly=%0d)",
anomaly);
errors++;
end
if (data_bytes !== 16'd0) begin
$display(" FAIL: a short frame reported %0d data bytes -- an underflow",
data_bytes);
errors++;
end
$display(" %-24s anomaly=%0d, data_bytes=%0d (no underflow)",
"READ missing an address", anomaly, data_bytes);
// 14. A WREN carrying payload. This is what a MISSED chip-select
// edge looks like: two frames merged into one.
frame(8'h06, 0, 24'h000000, 4);
if (anomaly !== A_LONG) begin
$display(" FAIL: a WREN with payload was not reported (anomaly=%0d)",
anomaly);
errors++;
end
$display(" %-24s anomaly=%0d", "WREN with payload", anomaly);
// 15. A WREN that was itself mis-framed must NOT arm the latch --
// otherwise one mis-framed capture hides a real missing-WREN
// fault in the frame after it.
frame(8'h02, 3, 24'h005000, 4);
if (anomaly !== A_NO_WREN) begin
$display(" FAIL: a program after a MALFORMED WREN was accepted (anomaly=%0d)",
anomaly);
errors++;
end
$display(" %-24s anomaly=%0d -- a malformed WREN does not arm the latch",
"PP after bad WREN", anomaly);
// 16. The running totals.
$display(" totals: %0d frames decoded, %0d anomalies",
frames_seen, anomalies_seen);
if (frames_seen !== 16'd18) begin
$display(" FAIL: %0d frames counted, expected 18", frames_seen);
errors++;
end
if (anomalies_seen !== 16'd7) begin
$display(" FAIL: %0d anomalies counted, expected 7", anomalies_seen);
errors++;
end
if (errors == 0)
$display("PASS: every frame in the capture is classified from its opcode with the address and payload boundaries its shape implies, a fast read's dummy byte is excluded from the payload, a frame shorter than its own shape is reported without underflowing, a WREN carrying payload is reported as a mis-framed capture and does not arm the latch, and a well-formed program or erase that follows no WREN is reported by tracking the latch across frames exactly as the device does");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmoduleThe testbench replays §5's capture frame by frame and checks every decode against what a person reading the datasheet would write down — the class, the address, the payload length and the verdict.
Then it injects the faults, and the sequence of latch tests is the interesting part:
PPafterWREN— no anomaly. The latch was armed.- A second
PP—NO_WREN. The device cleared the latch after the first. SEwith noWREN—NO_WREN, so erases are covered as well as programs.WRENthenSE— no anomaly, which proves the latch is genuinely tracked rather than the anomaly being unconditional after the first one.WREN,WRDI,PP—NO_WREN, becauseWRDIcancels the arming. The decoder mirrors the device in both directions.- A malformed
WRENthenPP—NO_WREN, because the malformed frame did not arm the latch. Had it armed, one mis-framed capture would hide a real fault.
That fourth case is the one that makes the test set complete. Without it, a decoder that simply reported NO_WREN on every modifying frame after the first would pass every other latch test.
The remaining checks cover the per-frame faults of §2 — an unknown opcode, a read frame missing an address byte, and a WREN carrying payload — with the short-frame case explicitly asserting that the payload count is zero rather than an underflow.
// flash_capture_decode.v
//
// Chapter 11.7 -- reading a 25-series capture back against the datasheet,
// in Verilog-2001.
//
// A logic analyser gives you bytes and chip-select edges. Turning that into
// "this is a fast read of 0x001000 for 64 bytes, and the page program three
// frames later was never write-enabled" is the skill this chapter is about,
// and it is mechanisable.
//
// The decoder does three things a person does by hand:
//
// * CLASSIFIES each frame from its first byte, and derives the shape that
// opcode implies -- how many address bytes, how many dummy bytes;
// * CHECKS the frame's actual length against that shape, so a frame that
// is too short for its own opcode is reported rather than mis-parsed;
// * and TRACKS STATE ACROSS FRAMES, which is the part that finds real
// bugs. A page program or erase that follows no WREN is the single most
// common fault in a flash capture, and it is invisible inside any one
// frame -- the frame itself is perfectly well formed.
//
// That cross-frame check mirrors the device: WREN sets the latch, and the
// device clears it when a modifying operation completes. So the decoder
// clears its own flag on a modifying frame, which means two programs after
// one WREN reports an anomaly on the second -- exactly as the hardware
// behaves.
module flash_capture_decode (
input wire clk,
input wire rst_n,
input wire frame_start, // chip select asserted
input wire frame_end, // chip select released
input wire byte_valid,
input wire [7:0] byte_in,
output reg [3:0] cmd_class,
output reg [23:0] cmd_addr,
output reg [15:0] data_bytes,
output reg frame_valid, // pulse at frame_end: decode is ready
output reg [2:0] anomaly,
output reg [15:0] frames_seen,
output reg [15:0] anomalies_seen
);
// Command classes.
localparam [3:0] C_UNKNOWN = 4'd0;
localparam [3:0] C_READ = 4'd1; // 0x03
localparam [3:0] C_FREAD = 4'd2; // 0x0B
localparam [3:0] C_RDSR = 4'd3; // 0x05
localparam [3:0] C_WREN = 4'd4; // 0x06
localparam [3:0] C_WRDI = 4'd5; // 0x04
localparam [3:0] C_PP = 4'd6; // 0x02
localparam [3:0] C_SE = 4'd7; // 0x20
localparam [3:0] C_BE = 4'd8; // 0xD8
localparam [3:0] C_CE = 4'd9; // 0xC7
localparam [3:0] C_RDID = 4'd10; // 0x9F
localparam [3:0] C_RDP = 4'd11; // 0xAB
// Anomalies, in detection order.
localparam [2:0] A_NONE = 3'd0;
localparam [2:0] A_UNKNOWN = 3'd1;
localparam [2:0] A_SHORT = 3'd2;
localparam [2:0] A_NO_WREN = 3'd3;
localparam [2:0] A_LONG = 3'd4;
reg [7:0] op_byte;
reg have_op;
reg [15:0] n_bytes; // bytes in this frame, opcode included
reg [23:0] addr_acc;
reg [15:0] addr_taken; // address bytes captured so far
reg wren_latched; // mirrors the device's WEL
// Shape of the current opcode. Decoded combinationally from the first
// byte, which is why the decoder needs no table memory.
reg [3:0] sh_class;
reg [15:0] sh_addr;
reg [15:0] sh_dummy;
reg sh_exact; // the frame must be exactly opcode-length
reg sh_modifies; // needs WEL, and clears it
always @(*) begin
sh_class = C_UNKNOWN;
sh_addr = 16'd0;
sh_dummy = 16'd0;
sh_exact = 1'b0;
sh_modifies = 1'b0;
case (op_byte)
8'h03: begin sh_class = C_READ; sh_addr = 3; end
8'h0B: begin sh_class = C_FREAD; sh_addr = 3; sh_dummy = 1; end
8'h05: begin sh_class = C_RDSR; end
8'h06: begin sh_class = C_WREN; sh_exact = 1'b1; end
8'h04: begin sh_class = C_WRDI; sh_exact = 1'b1; end
8'h02: begin sh_class = C_PP; sh_addr = 3; sh_modifies = 1'b1; end
8'h20: begin sh_class = C_SE; sh_addr = 3; sh_modifies = 1'b1; end
8'hD8: begin sh_class = C_BE; sh_addr = 3; sh_modifies = 1'b1; end
8'hC7: begin sh_class = C_CE; sh_exact = 1'b1; sh_modifies = 1'b1; end
8'h9F: begin sh_class = C_RDID; end
8'hAB: begin sh_class = C_RDP; end
default: sh_class = C_UNKNOWN;
endcase
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
op_byte <= 8'h00;
have_op <= 1'b0;
n_bytes <= 16'd0;
addr_acc <= 24'd0;
addr_taken <= 16'd0;
wren_latched <= 1'b0;
cmd_class <= C_UNKNOWN;
cmd_addr <= 24'd0;
data_bytes <= 16'd0;
frame_valid <= 1'b0;
anomaly <= A_NONE;
frames_seen <= 16'd0;
anomalies_seen <= 16'd0;
end else begin
frame_valid <= 1'b0;
if (frame_start) begin
have_op <= 1'b0;
n_bytes <= 16'd0;
addr_acc <= 24'd0;
addr_taken <= 16'd0;
end else if (byte_valid) begin
n_bytes <= n_bytes + 16'd1;
if (!have_op) begin
op_byte <= byte_in;
have_op <= 1'b1;
end else if (addr_taken < sh_addr) begin
// Addresses are most significant byte first, so
// shifting left as bytes arrive reconstructs them in
// order without knowing the width in advance.
addr_acc <= {addr_acc[15:0], byte_in};
addr_taken <= addr_taken + 16'd1;
end
end
if (frame_end) begin
frames_seen <= frames_seen + 16'd1;
cmd_class <= sh_class;
cmd_addr <= addr_acc;
frame_valid <= 1'b1;
// Payload is whatever is left after opcode, address and
// dummy. Guarded, because a short frame would underflow --
// and a 16-bit underflow reports 65 000 data bytes, which
// looks like a decoder bug rather than a short frame.
if (n_bytes > (16'd1 + sh_addr + sh_dummy))
data_bytes <= n_bytes - 16'd1 - sh_addr - sh_dummy;
else
data_bytes <= 16'd0;
// The anomaly checks, in order of how much they invalidate
// the rest of the decode.
if (sh_class == C_UNKNOWN) begin
// Nothing else can be trusted: without the shape, the
// address and payload boundaries are unknown.
anomaly <= A_UNKNOWN;
anomalies_seen <= anomalies_seen + 16'd1;
end else if (n_bytes < (16'd1 + sh_addr + sh_dummy)) begin
anomaly <= A_SHORT;
anomalies_seen <= anomalies_seen + 16'd1;
end else if (sh_exact && n_bytes != 16'd1) begin
// A WREN with payload means the capture is
// mis-framed -- a missed chip-select edge merges two
// frames and produces exactly this.
anomaly <= A_LONG;
anomalies_seen <= anomalies_seen + 16'd1;
end else if (sh_modifies && !wren_latched) begin
// THE CROSS-FRAME CHECK. This frame is well formed;
// what is wrong is the frame that should have preceded
// it and did not.
anomaly <= A_NO_WREN;
anomalies_seen <= anomalies_seen + 16'd1;
end else begin
anomaly <= A_NONE;
end
// Mirror the device's latch: WREN sets it, WRDI clears it,
// and a completed modifying operation clears it too.
if (sh_class == C_WREN && n_bytes == 16'd1)
wren_latched <= 1'b1;
else if (sh_class == C_WRDI)
wren_latched <= 1'b0;
else if (sh_modifies)
wren_latched <= 1'b0;
end
end
end
endmodule// flash_capture_decode_tb.v
//
// The same checks as the SystemVerilog testbench: a realistic boot capture
// replayed frame by frame, then the malformed frames -- an unknown opcode, a
// frame too short for its own shape, a WREN with payload, and a perfectly
// well-formed page program that follows no WREN.
`timescale 1ns/1ps
module flash_capture_decode_tb;
localparam [3:0] C_UNKNOWN = 4'd0;
localparam [3:0] C_READ = 4'd1;
localparam [3:0] C_FREAD = 4'd2;
localparam [3:0] C_RDSR = 4'd3;
localparam [3:0] C_WREN = 4'd4;
localparam [3:0] C_WRDI = 4'd5;
localparam [3:0] C_PP = 4'd6;
localparam [3:0] C_SE = 4'd7;
localparam [3:0] C_BE = 4'd8;
localparam [3:0] C_CE = 4'd9;
localparam [3:0] C_RDID = 4'd10;
localparam [3:0] C_RDP = 4'd11;
localparam [2:0] A_NONE = 3'd0;
localparam [2:0] A_UNKNOWN = 3'd1;
localparam [2:0] A_SHORT = 3'd2;
localparam [2:0] A_NO_WREN = 3'd3;
localparam [2:0] A_LONG = 3'd4;
reg clk;
reg rst_n;
reg frame_start;
reg frame_end;
reg byte_valid;
reg [7:0] byte_in;
wire [3:0] cmd_class;
wire [23:0] cmd_addr;
wire [15:0] data_bytes;
wire frame_valid;
wire [2:0] anomaly;
wire [15:0] frames_seen, anomalies_seen;
integer errors;
integer k;
initial begin
clk = 1'b0; rst_n = 1'b0;
frame_start = 1'b0; frame_end = 1'b0;
byte_valid = 1'b0; byte_in = 8'h00;
errors = 0;
end
always #5 clk = ~clk;
flash_capture_decode dut (
.clk(clk), .rst_n(rst_n),
.frame_start(frame_start), .frame_end(frame_end),
.byte_valid(byte_valid), .byte_in(byte_in),
.cmd_class(cmd_class), .cmd_addr(cmd_addr),
.data_bytes(data_bytes), .frame_valid(frame_valid),
.anomaly(anomaly),
.frames_seen(frames_seen), .anomalies_seen(anomalies_seen)
);
task open_frame;
begin
@(negedge clk);
frame_start = 1'b1;
@(negedge clk);
frame_start = 1'b0;
end
endtask
task send;
input [7:0] b;
begin
@(negedge clk);
byte_in = b; byte_valid = 1'b1;
@(negedge clk);
byte_valid = 1'b0;
end
endtask
task close_frame;
begin
@(negedge clk);
frame_end = 1'b1;
@(negedge clk);
frame_end = 1'b0;
@(negedge clk);
end
endtask
// Replay one frame: an opcode, some address bytes, some filler.
task frame;
input [7:0] op;
input integer addr_n;
input [23:0] a;
input integer extra;
integer i;
begin
open_frame;
send(op);
if (addr_n >= 3) send(a[23:16]);
if (addr_n >= 2) send(a[15:8]);
if (addr_n >= 1) send(a[7:0]);
for (i = 0; i < extra; i = i + 1) send(8'hA0 + i[7:0]);
close_frame;
end
endtask
task expect_decode;
input [8*24:1] what;
input [3:0] cls;
input [23:0] a;
input integer dbytes;
input [2:0] anom;
begin
if (cmd_class !== cls) begin
$display(" FAIL: %0s decoded as class %0d, expected %0d",
what, cmd_class, cls);
errors = errors + 1;
end
if (cmd_addr !== a) begin
$display(" FAIL: %0s address 0x%06h, expected 0x%06h",
what, cmd_addr, a);
errors = errors + 1;
end
if (data_bytes !== dbytes) begin
$display(" FAIL: %0s reported %0d data bytes, expected %0d",
what, data_bytes, dbytes);
errors = errors + 1;
end
if (anomaly !== anom) begin
$display(" FAIL: %0s anomaly %0d, expected %0d",
what, anomaly, anom);
errors = errors + 1;
end
$display(" %0s class=%0d addr=0x%06h data=%0d anomaly=%0d",
what, cmd_class, cmd_addr, data_bytes, anomaly);
end
endtask
initial begin
repeat (3) @(negedge clk);
rst_n = 1'b1;
@(negedge clk);
// ---- a realistic boot capture -------------------------------------
// 1. Release from power-down, then three dummy bytes.
frame(8'hAB, 0, 24'h000000, 3);
expect_decode("RDP (0xAB) ", C_RDP, 24'h000000, 3, A_NONE);
// 2. Read the JEDEC ID -- three bytes back, no address.
frame(8'h9F, 0, 24'h000000, 3);
expect_decode("RDID (0x9F) ", C_RDID, 24'h000000, 3, A_NONE);
// 3. A plain read of the header: three address bytes, no dummy.
frame(8'h03, 3, 24'h000000, 12);
expect_decode("READ 0x000000, 12 B ", C_READ, 24'h000000, 12, A_NONE);
// 4. A fast read: three address bytes AND one dummy byte, so the
// payload is one byte SHORTER than the same frame length would
// give a plain read.
frame(8'h0B, 3, 24'h001000, 17);
expect_decode("FAST READ 0x001000 ", C_FREAD, 24'h001000, 16, A_NONE);
// 5. A status read.
frame(8'h05, 0, 24'h000000, 1);
expect_decode("RDSR (0x05) ", C_RDSR, 24'h000000, 1, A_NONE);
// 6. WREN -- exactly one byte, no payload.
frame(8'h06, 0, 24'h000000, 0);
expect_decode("WREN (0x06) ", C_WREN, 24'h000000, 0, A_NONE);
// 7. A page program that correctly follows the WREN above.
frame(8'h02, 3, 24'h002000, 8);
expect_decode("PP 0x002000, 8 B ", C_PP, 24'h002000, 8, A_NONE);
// ---- now the faults ----------------------------------------------
// 8. A SECOND page program with no intervening WREN. The frame
// itself is perfectly well formed.
frame(8'h02, 3, 24'h002100, 8);
expect_decode("PP with no WREN ", C_PP, 24'h002100, 8, A_NO_WREN);
// 9. The same for an erase.
frame(8'h20, 3, 24'h003000, 0);
expect_decode("SE with no WREN ", C_SE, 24'h003000, 0, A_NO_WREN);
// 10. WREN then erase -- correct, proving the latch is tracked.
frame(8'h06, 0, 24'h000000, 0);
expect_decode("WREN again ", C_WREN, 24'h000000, 0, A_NONE);
frame(8'h20, 3, 24'h003000, 0);
expect_decode("SE after WREN ", C_SE, 24'h003000, 0, A_NONE);
// 11. WRDI cancels the latch.
frame(8'h06, 0, 24'h000000, 0);
frame(8'h04, 0, 24'h000000, 0);
expect_decode("WRDI (0x04) ", C_WRDI, 24'h000000, 0, A_NONE);
frame(8'h02, 3, 24'h004000, 4);
expect_decode("PP after WRDI ", C_PP, 24'h004000, 4, A_NO_WREN);
// 12. An unknown opcode: nothing after it can be trusted.
frame(8'h77, 0, 24'h000000, 4);
if (cmd_class !== C_UNKNOWN || anomaly !== A_UNKNOWN) begin
$display(" FAIL: opcode 0x77 was not reported unknown");
errors = errors + 1;
end
$display(" unknown opcode 0x77 class=%0d anomaly=%0d",
cmd_class, anomaly);
// 13. A frame too SHORT for its own shape.
frame(8'h03, 2, 24'h00ABCD, 0);
if (anomaly !== A_SHORT) begin
$display(" FAIL: a 3-byte read frame was not reported short");
errors = errors + 1;
end
if (data_bytes !== 16'd0) begin
$display(" FAIL: a short frame reported %0d data bytes -- an underflow",
data_bytes);
errors = errors + 1;
end
$display(" READ missing an address anomaly=%0d, data_bytes=%0d (no underflow)",
anomaly, data_bytes);
// 14. A WREN carrying payload -- what a MISSED chip-select edge
// looks like.
frame(8'h06, 0, 24'h000000, 4);
if (anomaly !== A_LONG) begin
$display(" FAIL: a WREN with payload was not reported");
errors = errors + 1;
end
$display(" WREN with payload anomaly=%0d", anomaly);
// 15. A mis-framed WREN must NOT arm the latch.
frame(8'h02, 3, 24'h005000, 4);
if (anomaly !== A_NO_WREN) begin
$display(" FAIL: a program after a MALFORMED WREN was accepted");
errors = errors + 1;
end
$display(" PP after bad WREN anomaly=%0d -- a malformed WREN does not arm the latch",
anomaly);
// 16. The running totals.
$display(" totals: %0d frames decoded, %0d anomalies",
frames_seen, anomalies_seen);
if (frames_seen !== 16'd18) begin
$display(" FAIL: %0d frames counted, expected 18", frames_seen);
errors = errors + 1;
end
if (anomalies_seen !== 16'd7) begin
$display(" FAIL: %0d anomalies counted, expected 7", anomalies_seen);
errors = errors + 1;
end
if (errors == 0)
$display("PASS: every frame in the capture is classified from its opcode with the address and payload boundaries its shape implies, a fast read's dummy byte is excluded from the payload, a frame shorter than its own shape is reported without underflowing, a WREN carrying payload is reported as a mis-framed capture and does not arm the latch, and a well-formed program or erase that follows no WREN is reported by tracking the latch across frames exactly as the device does");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule-- flash_capture_decode.vhd
--
-- Chapter 11.7 -- reading a 25-series capture back against the datasheet,
-- in VHDL.
--
-- A logic analyser gives you bytes and chip-select edges. Turning that into
-- "this is a fast read of 0x001000 for 64 bytes, and the page program three
-- frames later was never write-enabled" is mechanisable.
--
-- The decoder does three things a person does by hand:
--
-- * CLASSIFIES each frame from its first byte, and derives the shape that
-- opcode implies -- how many address bytes, how many dummy bytes;
-- * CHECKS the frame's actual length against that shape, so a frame too
-- short for its own opcode is reported rather than mis-parsed;
-- * and TRACKS STATE ACROSS FRAMES, which is the part that finds real
-- bugs. A page program or erase that follows no WREN is the commonest
-- fault in a flash capture, and it is invisible inside any one frame --
-- the frame itself is perfectly well formed.
--
-- That cross-frame check mirrors the device: WREN sets the latch, and the
-- device clears it when a modifying operation completes.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity flash_capture_decode is
port (
clk : in std_logic;
rst_n : in std_logic;
frame_start : in std_logic; -- chip select asserted
frame_end : in std_logic; -- chip select released
byte_valid : in std_logic;
byte_in : in unsigned(7 downto 0);
cmd_class : out unsigned(3 downto 0);
cmd_addr : out unsigned(23 downto 0);
data_bytes : out unsigned(15 downto 0);
frame_valid : out std_logic; -- pulse at frame_end
anomaly : out unsigned(2 downto 0);
frames_seen : out unsigned(15 downto 0);
anomalies_seen : out unsigned(15 downto 0)
);
end entity;
architecture rtl of flash_capture_decode is
-- Command classes.
constant C_UNKNOWN : unsigned(3 downto 0) := x"0";
constant C_READ : unsigned(3 downto 0) := x"1"; -- 0x03
constant C_FREAD : unsigned(3 downto 0) := x"2"; -- 0x0B
constant C_RDSR : unsigned(3 downto 0) := x"3"; -- 0x05
constant C_WREN : unsigned(3 downto 0) := x"4"; -- 0x06
constant C_WRDI : unsigned(3 downto 0) := x"5"; -- 0x04
constant C_PP : unsigned(3 downto 0) := x"6"; -- 0x02
constant C_SE : unsigned(3 downto 0) := x"7"; -- 0x20
constant C_BE : unsigned(3 downto 0) := x"8"; -- 0xD8
constant C_CE : unsigned(3 downto 0) := x"9"; -- 0xC7
constant C_RDID : unsigned(3 downto 0) := x"A"; -- 0x9F
constant C_RDP : unsigned(3 downto 0) := x"B"; -- 0xAB
-- Anomalies, in detection order.
constant A_NONE : unsigned(2 downto 0) := "000";
constant A_UNKNOWN : unsigned(2 downto 0) := "001";
constant A_SHORT : unsigned(2 downto 0) := "010";
constant A_NO_WREN : unsigned(2 downto 0) := "011";
constant A_LONG : unsigned(2 downto 0) := "100";
signal op_byte : unsigned(7 downto 0) := (others => '0');
signal have_op : std_logic := '0';
signal n_bytes : unsigned(15 downto 0) := (others => '0');
signal addr_acc : unsigned(23 downto 0) := (others => '0');
signal addr_taken : unsigned(15 downto 0) := (others => '0');
signal wren_latched : std_logic := '0'; -- mirrors the device's WEL
-- Shape of the current opcode, decoded combinationally from the first
-- byte, which is why the decoder needs no table memory.
signal sh_class : unsigned(3 downto 0) := C_UNKNOWN;
signal sh_addr : unsigned(15 downto 0) := (others => '0');
signal sh_dummy : unsigned(15 downto 0) := (others => '0');
signal sh_exact : std_logic := '0'; -- frame must be opcode-length
signal sh_modifies : std_logic := '0'; -- needs WEL, and clears it
signal class_r : unsigned(3 downto 0) := C_UNKNOWN;
signal addr_r : unsigned(23 downto 0) := (others => '0');
signal data_r : unsigned(15 downto 0) := (others => '0');
signal fv_r : std_logic := '0';
signal anom_r : unsigned(2 downto 0) := A_NONE;
signal fr_r : unsigned(15 downto 0) := (others => '0');
signal an_r : unsigned(15 downto 0) := (others => '0');
begin
shape : process (op_byte)
begin
sh_class <= C_UNKNOWN;
sh_addr <= (others => '0');
sh_dummy <= (others => '0');
sh_exact <= '0';
sh_modifies <= '0';
case to_integer(op_byte) is
when 16#03# => sh_class <= C_READ;
sh_addr <= to_unsigned(3, 16);
when 16#0B# => sh_class <= C_FREAD;
sh_addr <= to_unsigned(3, 16);
sh_dummy <= to_unsigned(1, 16);
when 16#05# => sh_class <= C_RDSR;
when 16#06# => sh_class <= C_WREN; sh_exact <= '1';
when 16#04# => sh_class <= C_WRDI; sh_exact <= '1';
when 16#02# => sh_class <= C_PP;
sh_addr <= to_unsigned(3, 16);
sh_modifies <= '1';
when 16#20# => sh_class <= C_SE;
sh_addr <= to_unsigned(3, 16);
sh_modifies <= '1';
when 16#D8# => sh_class <= C_BE;
sh_addr <= to_unsigned(3, 16);
sh_modifies <= '1';
when 16#C7# => sh_class <= C_CE; sh_exact <= '1';
sh_modifies <= '1';
when 16#9F# => sh_class <= C_RDID;
when 16#AB# => sh_class <= C_RDP;
when others => sh_class <= C_UNKNOWN;
end case;
end process;
decode : process (clk, rst_n)
variable need : natural;
begin
if rst_n = '0' then
op_byte <= (others => '0');
have_op <= '0';
n_bytes <= (others => '0');
addr_acc <= (others => '0');
addr_taken <= (others => '0');
wren_latched <= '0';
class_r <= C_UNKNOWN;
addr_r <= (others => '0');
data_r <= (others => '0');
fv_r <= '0';
anom_r <= A_NONE;
fr_r <= (others => '0');
an_r <= (others => '0');
elsif rising_edge(clk) then
fv_r <= '0';
if frame_start = '1' then
have_op <= '0';
n_bytes <= (others => '0');
addr_acc <= (others => '0');
addr_taken <= (others => '0');
elsif byte_valid = '1' then
n_bytes <= n_bytes + 1;
if have_op = '0' then
op_byte <= byte_in;
have_op <= '1';
elsif addr_taken < sh_addr then
-- Addresses are most significant byte first, so shifting
-- left as bytes arrive reconstructs them in order
-- without knowing the width in advance.
addr_acc <= addr_acc(15 downto 0) & byte_in;
addr_taken <= addr_taken + 1;
end if;
end if;
if frame_end = '1' then
fr_r <= fr_r + 1;
class_r <= sh_class;
addr_r <= addr_acc;
fv_r <= '1';
-- Payload is whatever is left after opcode, address and
-- dummy. Guarded, because a short frame would underflow --
-- and an underflow reports 65 000 data bytes, which looks
-- like a decoder bug rather than a short frame.
need := 1 + to_integer(sh_addr) + to_integer(sh_dummy);
if to_integer(n_bytes) > need then
data_r <= to_unsigned(to_integer(n_bytes) - need, 16);
else
data_r <= (others => '0');
end if;
-- The anomaly checks, in order of how much they invalidate
-- the rest of the decode.
if sh_class = C_UNKNOWN then
-- Nothing else can be trusted: without the shape, the
-- address and payload boundaries are unknown.
anom_r <= A_UNKNOWN;
an_r <= an_r + 1;
elsif to_integer(n_bytes) < need then
anom_r <= A_SHORT;
an_r <= an_r + 1;
elsif sh_exact = '1' and n_bytes /= 1 then
-- A WREN with payload means the capture is mis-framed --
-- a missed chip-select edge merges two frames and
-- produces exactly this.
anom_r <= A_LONG;
an_r <= an_r + 1;
elsif sh_modifies = '1' and wren_latched = '0' then
-- THE CROSS-FRAME CHECK. This frame is well formed;
-- what is wrong is the frame that should have preceded
-- it and did not.
anom_r <= A_NO_WREN;
an_r <= an_r + 1;
else
anom_r <= A_NONE;
end if;
-- Mirror the device's latch: WREN sets it, WRDI clears it,
-- and a completed modifying operation clears it too.
if sh_class = C_WREN and n_bytes = 1 then
wren_latched <= '1';
elsif sh_class = C_WRDI then
wren_latched <= '0';
elsif sh_modifies = '1' then
wren_latched <= '0';
end if;
end if;
end if;
end process;
cmd_class <= class_r;
cmd_addr <= addr_r;
data_bytes <= data_r;
frame_valid <= fv_r;
anomaly <= anom_r;
frames_seen <= fr_r;
anomalies_seen <= an_r;
end architecture;-- flash_capture_decode_tb.vhd
--
-- The same checks as the SystemVerilog and Verilog testbenches: a realistic
-- boot capture replayed frame by frame, then the malformed frames -- an
-- unknown opcode, a frame too short for its own shape, a WREN with payload,
-- and a perfectly well-formed page program that follows no WREN.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity flash_capture_decode_tb is
end entity;
architecture sim of flash_capture_decode_tb is
constant C_UNKNOWN : unsigned(3 downto 0) := x"0";
constant C_READ : unsigned(3 downto 0) := x"1";
constant C_FREAD : unsigned(3 downto 0) := x"2";
constant C_RDSR : unsigned(3 downto 0) := x"3";
constant C_WREN : unsigned(3 downto 0) := x"4";
constant C_WRDI : unsigned(3 downto 0) := x"5";
constant C_PP : unsigned(3 downto 0) := x"6";
constant C_SE : unsigned(3 downto 0) := x"7";
constant C_RDID : unsigned(3 downto 0) := x"A";
constant C_RDP : unsigned(3 downto 0) := x"B";
constant A_NONE : unsigned(2 downto 0) := "000";
constant A_UNKNOWN : unsigned(2 downto 0) := "001";
constant A_SHORT : unsigned(2 downto 0) := "010";
constant A_NO_WREN : unsigned(2 downto 0) := "011";
constant A_LONG : unsigned(2 downto 0) := "100";
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal halt : boolean := false;
signal frame_start : std_logic := '0';
signal frame_end : std_logic := '0';
signal byte_valid : std_logic := '0';
signal byte_in : unsigned(7 downto 0) := (others => '0');
signal cmd_class : unsigned(3 downto 0);
signal cmd_addr : unsigned(23 downto 0);
signal data_bytes : unsigned(15 downto 0);
signal frame_valid : std_logic;
signal anomaly : unsigned(2 downto 0);
signal frames_seen : unsigned(15 downto 0);
signal anomalies_seen : unsigned(15 downto 0);
signal errors : natural := 0;
begin
clk <= not clk after 5 ns when not halt else '0';
dut : entity work.flash_capture_decode
port map (
clk => clk, rst_n => rst_n,
frame_start => frame_start, frame_end => frame_end,
byte_valid => byte_valid, byte_in => byte_in,
cmd_class => cmd_class, cmd_addr => cmd_addr,
data_bytes => data_bytes, frame_valid => frame_valid,
anomaly => anomaly,
frames_seen => frames_seen, anomalies_seen => anomalies_seen
);
stim : process
variable errs : natural := 0;
procedure open_frame is
begin
wait until falling_edge(clk);
frame_start <= '1';
wait until falling_edge(clk);
frame_start <= '0';
end procedure;
procedure send(b : unsigned(7 downto 0)) is
begin
wait until falling_edge(clk);
byte_in <= b;
byte_valid <= '1';
wait until falling_edge(clk);
byte_valid <= '0';
end procedure;
procedure close_frame is
begin
wait until falling_edge(clk);
frame_end <= '1';
wait until falling_edge(clk);
frame_end <= '0';
wait until falling_edge(clk);
end procedure;
-- Replay one frame: an opcode, some address bytes, some filler.
procedure frame(op : natural; addr_n : natural; a : natural;
extra : natural) is
variable av : unsigned(23 downto 0);
begin
av := to_unsigned(a, 24);
open_frame;
send(to_unsigned(op, 8));
if addr_n >= 3 then send(av(23 downto 16)); end if;
if addr_n >= 2 then send(av(15 downto 8)); end if;
if addr_n >= 1 then send(av(7 downto 0)); end if;
for i in 0 to extra - 1 loop
send(to_unsigned((16#A0# + i) mod 256, 8));
end loop;
close_frame;
end procedure;
procedure expect_decode(what : string; cls : unsigned(3 downto 0);
a : natural; dbytes : natural;
anom : unsigned(2 downto 0)) is
begin
if cmd_class /= cls then
report " FAIL: " & what & " decoded as class " &
integer'image(to_integer(cmd_class));
errs := errs + 1;
end if;
if to_integer(cmd_addr) /= a then
report " FAIL: " & what & " address is wrong";
errs := errs + 1;
end if;
if to_integer(data_bytes) /= dbytes then
report " FAIL: " & what & " reported " &
integer'image(to_integer(data_bytes)) &
" data bytes, expected " & integer'image(dbytes);
errs := errs + 1;
end if;
if anomaly /= anom then
report " FAIL: " & what & " anomaly " &
integer'image(to_integer(anomaly)) & ", expected " &
integer'image(to_integer(anom));
errs := errs + 1;
end if;
report " " & what & " class=" &
integer'image(to_integer(cmd_class)) & " data=" &
integer'image(to_integer(data_bytes)) & " anomaly=" &
integer'image(to_integer(anomaly));
end procedure;
begin
for k in 0 to 2 loop
wait until falling_edge(clk);
end loop;
rst_n <= '1';
wait until falling_edge(clk);
-- ---- a realistic boot capture ------------------------------------
-- 1. Release from power-down, then three dummy bytes.
frame(16#AB#, 0, 0, 3);
expect_decode("RDP (0xAB)", C_RDP, 0, 3, A_NONE);
-- 2. Read the JEDEC ID -- three bytes back, no address.
frame(16#9F#, 0, 0, 3);
expect_decode("RDID (0x9F)", C_RDID, 0, 3, A_NONE);
-- 3. A plain read of the header.
frame(16#03#, 3, 0, 12);
expect_decode("READ 0x000000, 12 B", C_READ, 0, 12, A_NONE);
-- 4. A fast read: three address bytes AND one dummy byte, so the
-- payload is one byte SHORTER than the same frame length would
-- give a plain read.
frame(16#0B#, 3, 16#001000#, 17);
expect_decode("FAST READ 0x001000", C_FREAD, 16#001000#, 16, A_NONE);
-- 5. A status read.
frame(16#05#, 0, 0, 1);
expect_decode("RDSR (0x05)", C_RDSR, 0, 1, A_NONE);
-- 6. WREN -- exactly one byte, no payload.
frame(16#06#, 0, 0, 0);
expect_decode("WREN (0x06)", C_WREN, 0, 0, A_NONE);
-- 7. A page program that correctly follows the WREN above.
frame(16#02#, 3, 16#002000#, 8);
expect_decode("PP 0x002000, 8 B", C_PP, 16#002000#, 8, A_NONE);
-- ---- now the faults ----------------------------------------------
-- 8. A SECOND page program with no intervening WREN. The frame
-- itself is perfectly well formed.
frame(16#02#, 3, 16#002100#, 8);
expect_decode("PP with no WREN", C_PP, 16#002100#, 8, A_NO_WREN);
-- 9. The same for an erase.
frame(16#20#, 3, 16#003000#, 0);
expect_decode("SE with no WREN", C_SE, 16#003000#, 0, A_NO_WREN);
-- 10. WREN then erase -- correct, proving the latch is tracked.
frame(16#06#, 0, 0, 0);
expect_decode("WREN again", C_WREN, 0, 0, A_NONE);
frame(16#20#, 3, 16#003000#, 0);
expect_decode("SE after WREN", C_SE, 16#003000#, 0, A_NONE);
-- 11. WRDI cancels the latch.
frame(16#06#, 0, 0, 0);
frame(16#04#, 0, 0, 0);
expect_decode("WRDI (0x04)", C_WRDI, 0, 0, A_NONE);
frame(16#02#, 3, 16#004000#, 4);
expect_decode("PP after WRDI", C_PP, 16#004000#, 4, A_NO_WREN);
-- 12. An unknown opcode: nothing after it can be trusted.
frame(16#77#, 0, 0, 4);
if cmd_class /= C_UNKNOWN or anomaly /= A_UNKNOWN then
report " FAIL: opcode 0x77 was not reported unknown";
errs := errs + 1;
end if;
report " unknown opcode 0x77 class=" &
integer'image(to_integer(cmd_class)) & " anomaly=" &
integer'image(to_integer(anomaly));
-- 13. A frame too SHORT for its own shape.
frame(16#03#, 2, 16#00ABCD#, 0);
if anomaly /= A_SHORT then
report " FAIL: a 3-byte read frame was not reported short";
errs := errs + 1;
end if;
if data_bytes /= 0 then
report " FAIL: a short frame reported data bytes -- an underflow";
errs := errs + 1;
end if;
report " READ missing an address anomaly=" &
integer'image(to_integer(anomaly)) & ", data_bytes=" &
integer'image(to_integer(data_bytes)) & " (no underflow)";
-- 14. A WREN carrying payload -- what a MISSED chip-select edge
-- looks like.
frame(16#06#, 0, 0, 4);
if anomaly /= A_LONG then
report " FAIL: a WREN with payload was not reported";
errs := errs + 1;
end if;
report " WREN with payload anomaly=" &
integer'image(to_integer(anomaly));
-- 15. A mis-framed WREN must NOT arm the latch.
frame(16#02#, 3, 16#005000#, 4);
if anomaly /= A_NO_WREN then
report " FAIL: a program after a MALFORMED WREN was accepted";
errs := errs + 1;
end if;
report " PP after bad WREN anomaly=" &
integer'image(to_integer(anomaly)) &
" -- a malformed WREN does not arm the latch";
-- 16. The running totals.
report " totals: " & integer'image(to_integer(frames_seen)) &
" frames decoded, " &
integer'image(to_integer(anomalies_seen)) & " anomalies";
if to_integer(frames_seen) /= 18 then
report " FAIL: the frame count is wrong"; errs := errs + 1;
end if;
if to_integer(anomalies_seen) /= 7 then
report " FAIL: the anomaly count is wrong"; errs := errs + 1;
end if;
errors <= errs;
if errs = 0 then
report "PASS: every frame in the capture is classified from its opcode with the address and payload boundaries its shape implies, a fast read's dummy byte is excluded from the payload, a frame shorter than its own shape is reported without underflowing, a WREN carrying payload is reported as a mis-framed capture and does not arm the latch, and a well-formed program or erase that follows no WREN is reported by tracking the latch across frames exactly as the device does";
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 decoder: identical ports, a combinational shape lookup, an address reconstructed by shifting, a guarded payload count, anomaly checks ordered by severity, and a mirrored write-enable latch that a malformed WREN does not arm. All three testbenches replay the same eighteen frames and report the same seven anomalies with identical classes, addresses and payload counts throughout.
7. Why a Verification Engineer Cares
// 1. NO UNDERFLOW. A frame shorter than its own shape reports zero
// payload bytes, not a wrapped count. An underflow reports ~65000
// bytes and looks like a decoder bug rather than a short frame.
a_no_underflow : assert property (
@(posedge clk) disable iff (!rst_n)
(frame_valid && (n_bytes < 1 + sh_addr + sh_dummy))
|-> (data_bytes == 0))
else $error("a short frame produced an underflowed payload count");
// 2. THE LATCH MIRRORS THE DEVICE. Set by a well-formed WREN, cleared
// by WRDI, and cleared by a completed modifying operation -- which
// is what makes a second program after one WREN an anomaly.
a_latch_mirrors : assert property (
@(posedge clk) disable iff (!rst_n)
(frame_valid && is_modifying(cmd_class)) |=> !wren_latched)
else $error("the latch survived a modifying operation");
// 3. A MALFORMED WREN DOES NOT ARM. Otherwise one mis-framed capture
// hides a real missing-WREN fault in the frame after it.
a_bad_wren_inert : assert property (
@(posedge clk) disable iff (!rst_n)
(frame_valid && (cmd_class == C_WREN) && (anomaly == A_LONG))
|=> !wren_latched)
else $error("a malformed WREN armed the latch");
// 4. UNKNOWN OPCODES SUPPRESS EVERYTHING ELSE. Without the shape, the
// address and payload boundaries are unknown, so reporting them
// would be reporting arbitrary bytes as a decode.
a_unknown_first : assert property (
@(posedge clk) disable iff (!rst_n)
(frame_valid && (cmd_class == C_UNKNOWN))
|-> (anomaly == A_UNKNOWN))
else $error("an unknown opcode reported some other anomaly");
// 5. ADDRESS RECONSTRUCTION. The accumulated address equals the address
// bytes in the order they arrived, most significant first. A decoder
// that assembled them the other way reports a valid-looking address
// that is wrong -- and gets believed.
a_addr_msb_first : assert property (
@(posedge clk) disable iff (!rst_n)
(frame_valid && (sh_addr == 3))
|-> (cmd_addr == {addr_byte0, addr_byte1, addr_byte2}))
else $error("the address was not reconstructed most significant first");
// 6. EVERY FRAME PRODUCES EXACTLY ONE VERDICT -- so the frame count and
// the sum of the anomaly counts can be reconciled, which is how a
// dropped frame is detected at all.
a_one_verdict_per_frame : assert property (
@(posedge clk) disable iff (!rst_n)
(frame_end) |=> frame_valid)
else $error("a frame ended without producing a verdict");Property 4 encodes an idea worth carrying beyond this decoder: when one field invalidates the interpretation of the others, its check must come first and must suppress them. A decoder that reported an unknown opcode and an address is offering a number that cannot mean anything, and the number will be used.
Property 3 is the one that distinguishes a thorough decoder from a plausible one. It is a second-order property — about how one fault affects the detection of a later fault — and those are the properties that decide whether a diagnostic tool can be trusted on a messy capture, which is the only kind that needs one.
Coverage must include frame sequences, not just frame types:
covergroup flash_capture_cg @(posedge frame_valid);
cp_class : coverpoint cmd_class {
bins reads = {C_READ, C_FREAD};
bins status = {C_RDSR, C_RDID, C_RDP};
bins latch = {C_WREN, C_WRDI};
bins modifies = {C_PP, C_SE, C_BE, C_CE};
bins unknown = {C_UNKNOWN};
}
cp_anomaly : coverpoint anomaly {
bins clean = {A_NONE};
bins unknown = {A_UNKNOWN};
bins short = {A_SHORT};
bins no_wren = {A_NO_WREN};
bins long = {A_LONG};
}
// The SEQUENCE is what this decoder exists for. A coverage model
// over frame types alone cannot express the fault the chapter is
// about, because that fault is a property of a PAIR of frames.
cp_sequence : coverpoint seq_class {
bins wren_then_mod = {S_ARMED}; // clean
bins mod_with_no_wren = {S_UNARMED}; // the fault
bins two_mods_one_wren = {S_SECOND_MOD}; // the subtle fault
bins wren_wrdi_mod = {S_CANCELLED}; // the inverse
bins bad_wren_then_mod = {S_BAD_ARM}; // the second-order one
}
// Payload length relative to the shape -- the boundary between a
// valid frame and a short one is one byte wide.
cp_len_vs_shape : coverpoint len_class {
bins shorter_than_shape = {L_SHORT};
bins exactly_shape = {L_EXACT}; // zero payload, valid
bins one_more = {L_PLUS_ONE};
bins many_more = {L_PAYLOAD};
}
x_class_anomaly : cross cp_class, cp_anomaly;
endgroupcp_sequence is the coverpoint this whole chapter argues for. A coverage model over frame types cannot express the fault the decoder exists to find, because that fault is a property of a pair of frames — and bad_wren_then_mod is the bin that proves the decoder handles a messy capture rather than only a clean one.
8. Why an FPGA or ASIC Engineer Cares
Put the decoder on the debug build. It is a few hundred gates and it answers, continuously and in hardware, a question that otherwise needs a capture, a datasheet and an hour. On a system that writes flash in the field, a counter of NO_WREN anomalies is a direct measure of a class of bug that is otherwise invisible.
Check the shape, not just the opcode. An opcode is one byte; the fault is usually in the length. A frame that does not match its own shape is mis-framed or truncated, and both are worth knowing.
Order the checks by severity. An unknown opcode must suppress the address and payload, because reporting them would be reporting arbitrary bytes as a decode — and a plausible number gets believed.
Guard every subtraction in a decoder. The payload count is a difference of counts and can go negative on a malformed frame. An unsigned underflow reports a number that looks like a tool bug, which is the worst kind of wrong answer because it discredits the tool.
Mirror device state, including its self-clearing. The write-enable latch is cleared by the device, and a decoder that does not model that misses the second-program fault entirely. The general form: a monitor that models a device must model how the device changes its own state, not just how commands change it.
Do not let a malformed frame update mirrored state. One mis-framed capture then hides real faults in every frame after it — which turns a diagnostic tool into a source of false confidence.
Count frames and anomalies. Two totals that can be reconciled against a capture's frame count are how a dropped frame is detected, and neither costs more than a counter.
9. Failure Signature — A Capture That Contradicts Itself
Symptom. A capture of a flash write sequence is taken to investigate occasional data loss. The capture shows a WREN, then a page program, then a status read returning WIP set — everything correct. But the data at that address is unchanged, and the capture was taken at exactly the moment of a known failure.
What "the capture looks correct" establishes. Either the capture is complete and the write really should have happened, or the capture is missing something. Those are the only two possibilities, and distinguishing them is the whole job.
Plausible mechanisms.
- A frame the analyser missed. If a
WRDIor another modifying operation occurred between theWRENand the program, the latch was clear by the time the program arrived — and a capture that dropped that frame shows a correct-looking sequence. This fits exactly. - A merged pair of frames, where a missed chip-select edge made two frames look like one long one. A
WRENframe carrying payload is the signature, and a decoder that armed its latch on it would report nothing. - Block protection covering that address, so the operation was accepted and discarded despite WEL being set. WIP would not set in that case — so the status read showing WIP set argues against it.
- The program crossing a page boundary (Chapter 11.3), so it wrapped and the bytes landed elsewhere in the page. This fits "data unchanged at that address" if the address examined was the tail of the transfer.
- The write going to a different address than the one read back.
The discriminating observation. WIP set after the program is the strongest clue in the capture and it cuts the candidates sharply: WIP set means the device started doing something. A discarded write never sets WIP. So the operation was performed — and the question is not whether it happened but where its bytes went.
That points directly at the page-wrap mechanism. Compute the program's start address modulo the page size and add the payload length: if the sum exceeds the page size, the transfer wrapped, and the bytes past the boundary overwrote the start of the same page rather than continuing.
If WIP had been clear, the diagnosis would invert completely: the write was discarded, and the investigation goes to the latch — a dropped frame in the capture, or block protection.
The lesson about captures. A capture is evidence, not ground truth. An analyser can miss a frame, merge two, or start mid-transaction, and a capture that looks correct may be a capture that is incomplete. The defence is to check the capture's internal consistency — do the frame counts reconcile, does every modifying frame have an armed latch, does every frame match its own shape — and a decoder that reports a WREN carrying payload has told you the capture itself is suspect before you draw any conclusion from it.
10. Common Misconceptions
11. Reason It Through
Work this before reading the answer.
A capture of a boot sequence shows:
Azvya Education Pvt. Ltd.VLSI MentorSnippet1 AB (1 byte) 2 9F EF 40 18 (4 bytes) 3 0B 00 00 00 FF 5A 5A C0 DE (9 bytes)Frame 3 was intended to read the 4-byte image magic from address 0. The boot ROM reports a magic mismatch. What is wrong, and what should frame 3 have contained?
Decode frame 3 by its shape. 0x0B is a fast read: one opcode byte, three address bytes, one dummy byte, then payload.
0B opcode
00 00 00 address 0x000000
FF dummy byte
5A 5A C0 payload -- 3 bytes
DE payload -- the 4th byteWait: count the frame. Nine bytes total, minus 1 opcode, minus 3 address, minus 1 dummy = 4 payload bytes: 5A 5A C0 DE.
So the payload is 0x5A5AC0DE, which is exactly Chapter 11.5's magic value. The data in flash is correct.
Then why did the boot ROM report a mismatch? Because the boot ROM must have read the payload starting one byte earlier or later than the decoder did. There are two candidates and they are distinguishable:
- If the boot ROM ignored the dummy byte, it would take
FF 5A 5A C0as the magic — a shifted value. That matches a mismatch. - If the boot ROM expected a plain read, it sent
0x0Bbut parsed as though there were no dummy phase, with the same result.
Both are the same fault: the opcode says fast read and the parsing says plain read. This is precisely Chapter 11.2's failure signature — an opcode and a dummy count that disagree — and the capture contains the proof, because the dummy byte FF is visible in it.
What should frame 3 have contained? Either of two consistent forms:
fast read, correctly parsed: 0B 00 00 00 FF 5A 5A C0 DE (9 bytes)
plain read instead: 03 00 00 00 5A 5A C0 DE (8 bytes)The capture shows the first. The boot ROM behaved as though it were the second.
Now the detail that makes this a good exercise. Look at frame 1: a bare AB with no payload bytes at all. 0xAB is release-from-power-down, and on many parts the plain form is a single byte — so this is legal. But some parts require three dummy bytes before returning a device ID with that opcode, and a frame of one byte would then be a different command than intended.
That cannot be resolved from the capture alone, and saying so is the right answer: frame 1 is consistent with a correct wake-up and also consistent with a truncated one, and only the datasheet for the specific part settles it.
And frame 2 is a clean RDID returning EF 40 18 — a real manufacturer and device code, which confirms the link is working and the latency for that command is right. Which is the point Chapter 10.1 made: a successful ID read validates one command's shape and says nothing about any other command's dummy count. Frame 2 passing is exactly why frame 3's failure was surprising.
The general lesson. Decode every frame by the shape its own opcode implies, and compare against what the consumer did. When the data in a capture is correct and the consumer disagrees, the fault is in the parsing rather than the device — and the dummy byte sitting visibly in the capture is usually the evidence.
12. Understanding Check
13. Summary
A capture gives frames; a datasheet gives shapes; together they give a decode. Faults divide into two kinds, and the division is the chapter's point.
Per-frame faults are found by checking the frame's length against the shape its opcode implies. An unknown opcode invalidates every other field and must suppress them. A frame shorter than its shape is mis-framed or truncated, and its payload count must be guarded against underflow — a wrapped count reads as a tool bug and discredits the tool. A frame longer than an exact-length opcode is two frames the analyser merged.
Cross-frame faults need state. A page program or erase issued with the write-enable latch clear is discarded silently, and the frame itself is perfectly well formed — so the decoder must mirror the device's latch, including the fact that the device clears it on completion. That is what makes WREN, PP, PP one successful program and one discarded one.
And a malformed WREN must not arm the mirrored latch, or one mis-framed capture hides real faults in everything after it.
The fast read's payload is the frame length minus five, not four, because of its dummy byte — the commonest hand-analysis error in a flash capture.
In a write capture, WIP is the most decisive bit available: set means the device started work, which rules out a discarded write and redirects the question to where the bytes went.
And a capture is evidence, not ground truth. An analyser can drop a frame, merge two, or begin mid-transaction, so a capture's internal consistency is the first thing to establish and the last thing to assume.
14. What Comes Next
This closes Module 11, and with it the single-line SPI story. Everything so far has moved one bit per clock edge on one data line, and every ceiling the track has met — the round trip, the array access time, signal integrity — is a limit on how fast that one line can be clocked.
Module 12 — Dual, Quad, Octal SPI, and XIP takes the other direction. When frequency cannot rise, width can: the same four-wire package repurposes its pins to move two, four or eight bits per edge, multiplying throughput without touching SCLK at all. It changes the dummy-cycle arithmetic of Chapter 10.5, it turns MOSI and MISO into bidirectional lanes with the turnaround problem of Chapter 8.4, and at its end it lets a processor execute code directly out of flash — which is the point at which a serial interface stops being a peripheral bus and becomes part of the memory system.
Continue learning
Related tutorials
- Related topic
Boot from SPI Flash
The first-fetch sequence a boot ROM issues: waking a part previous software may have left asleep, verifying the device before trusting it, and why an image length must never be used before the header carrying it has been verified.
- Related topic
Launch and Sample Edges
One edge of each bit time places a bit on the wire, the other captures it, and they must never be the same edge. Why the separation is forced, why it buys half a period, and how RTL maps physical edges onto those roles.
- Related topic
Deriving Mode Behaviour from CPOL and CPHA
The four SPI modes are a two-bit truth table you can rebuild in seconds. The standard numbering, the derivation, the complete mode decoder in three HDLs, and the assertions that keep a configurable design honest.
- Related topic
Command, Address, and Data Phases
How a device layers a transaction onto a raw byte stream: why the opcode decides the shape of everything after it, how a slave tracks phases with no phase marker, and the sequencer that requires in three HDLs.
