SPI · Module 11
Page Program, Page Boundaries, and Erase
Why a page program cannot cross a page boundary, what the device really does when you try — it overwrites what you just wrote — how erase granularity differs, and the guard that reports exactly which bytes would be destroyed.
Chapter 11.2 finished the easy half. Reads are unconstrained; writing is where flash stops resembling memory.
You program 300 bytes starting at a page boundary on a device with 256-byte pages. The device acknowledges every bit and reports no error. What is in the flash afterwards?
Not 300 bytes. The first 256 land correctly, and the remaining 44 overwrite the first 44 of the same page. No error, no warning, and a read-back of the first 44 bytes returns the wrong data.
1. Why a Program Cannot Cross a Page
Chapter 11.1 gave the mechanism: a program does not write to the array. It fills an internal page buffer and then commits that buffer in a single physical event.
The buffer is one page. Its address counter is as wide as the page and no wider, so when the counter passes the last byte it does what every counter of finite width does — it wraps to zero. The bytes after the boundary land at the start of the same buffer, overwriting what is already there.
program 300 bytes at 0x000000, 256-byte page:
bytes 0..255 → buffer[0..255] correct
bytes 256..299 → buffer[0..43] OVERWRITES the first 44Then the buffer commits. The page contains bytes 256–299 in its first 44 positions and bytes 44–255 after them — a page that is partly new data and partly old, in an order nothing expects.
The device is not malfunctioning. The datasheet says the address wraps within the page, and it does. The fault is entirely the caller's, which is why the check must live in the controller or the driver.
2. Watching the Wrap
The page buffer's counter wrapping
10 cyclesTwo details in that figure are worth stating explicitly.
The wrap is to the start of the page, not the start of the transfer. If the program began at 0x0000F0, the excess lands at 0x000000 — the page boundary — not at 0xF0. A driver that assumes the wrap returns to its own starting address computes the damage in the wrong place.
Nothing on the wires changes. The bytes after the boundary look identical to the bytes before it. There is no signal, no status bit and no acknowledgement difference. The only way to know is to have done the arithmetic.
3. The Safe Split
The remedy is the one Chapter 9.3 built: clip at the boundary and continue in a new operation.
to_page_end = page_size − (addr & page_mask)
safe_len = min(len, to_page_end)And then — this is the part that catches people — the second operation is a complete new program, which means its own WREN, its own opcode and its own busy wait. Chapter 11.4 covers why, but the consequence belongs here: splitting a program is not free, and a 600-byte write that crosses two boundaries costs three full write cycles rather than one.
4. Reporting the Damage, Not Just Refusing
A guard could simply reject a crossing program. The block in §6 does more: it reports which address the excess would land on and how many bytes it would clobber.
That choice is deliberate and worth arguing for. A driver told "this operation is unsafe" can only refuse. A driver told "44 bytes at 0x000000 would be destroyed" can log something a person can act on — and during bring-up, when a wrong length is being tracked down, that difference is hours.
It also makes the guard testable in a way a refusal is not. "Rejects a crossing program" is one bit; "reports 16 bytes clobbered at 0x000000" is a value that can be wrong in interesting ways, and §6's sweep checks it at every offset.
A program longer than a whole page wraps more than once and ends up having touched every byte of the page. The guard caps the reported damage at the page size, because "the whole page" is the actionable number and the arithmetic excess is not.
5. Erase — A Different Granularity Entirely
Programming clears bits; only an erase sets them. And erase works on a unit sixteen to two hundred and fifty-six times larger than a program.
sector erase 0x20 4 KB tens of milliseconds
block erase 0xD8 64 KB hundreds of milliseconds
chip erase 0xC7 whole secondsThree properties, each of which surprises someone.
An erase address is not an address, it is a selector. A sector erase given 0x001234 erases the sector containing that address — all of 0x001000 to 0x001FFF. The low bits are ignored entirely. So passing an unaligned address is not an error at the device, and it will not be reported: it simply erases 4 KB starting somewhere the caller may not have intended.
Which is why the guard flags misalignment. The device cannot, because from its point of view nothing is wrong. A caller passing 0x001234 to a sector erase almost certainly believes something false about what it is erasing.
A chip erase takes no address at all. It is a one-byte command, and no address can be misaligned for it.
That diagram is the cost of Chapter 11.1's asymmetry drawn out: one erase and sixteen programs, each needing its own write-enable, to change however few bytes prompted it.
6. Building the Program Guard — Three HDLs
The circuit
Circuit. Two independent combinational calculations — one for programs, one for erases — sharing an address input.
State. None.
Datapath. Masks and subtracts. The safe length is the smaller of the request and the distance to the page boundary; the wrap address is the page base; the damage is the excess, capped at a page. On the erase side, the base is the address with the unit's low bits cleared and the misalignment flag is whether those bits were non-zero.
Control. None. The erase kind selects which unit applies.
Clock and reset. Neither.
Enables. crosses_page and erase_misaligned are the two flags a driver acts on.
Timing. Combinational; the consumer registers what it needs.
Synthesis. A comparator, two subtractors, a mux over three unit sizes. Small.
Limitations. One geometry, fixed at elaboration, and no knowledge of whether the target is erased. The guard can say a program will wrap; it cannot say a program will produce the bitwise AND of old and new data, because that depends on contents it cannot see.
// flash_prog_guard.sv
//
// Chapter 11.3 -- page programs, boundary wrap, and erase granularity.
//
// A page program does not write to the array directly. It fills an internal
// PAGE BUFFER whose address counter is only as wide as the page, so a
// program that runs past the page boundary does not continue into the next
// page -- the counter WRAPS and the excess bytes overwrite the beginning of
// the same page. The device reports no error. The first bytes written are
// silently replaced by the last ones.
//
// This block does two things about that:
//
// * it reports the SAFE length -- how many bytes may be programmed from
// this address without wrapping;
// * and it reports the DAMAGE -- exactly which address the excess lands
// on and how many bytes it would clobber.
//
// Reporting the damage rather than only refusing the operation is the
// deliberate choice. A driver that knows a program would destroy 40 bytes
// at a named address can log something useful; one that only knows the
// operation was rejected cannot.
//
// Erase is the other half. Erase acts on a whole sector or block whatever
// address it is given, so an unaligned erase address is not an error at the
// device -- it simply erases the unit CONTAINING that address, which is
// almost never what a caller passing an unaligned address intended. The
// block reports the base the erase will really act on, and flags the
// misalignment.
module flash_prog_guard #(
parameter int ADDR_W = 24,
parameter int LEN_W = 16,
parameter int PAGE_BITS = 8, // 256 B program granularity
parameter int SECTOR_BITS = 12, // 4 KB smallest erase
parameter int BLOCK_BITS = 16 // 64 KB large erase
) (
input logic [ADDR_W-1:0] addr,
input logic [LEN_W-1:0] len,
input logic [1:0] erase_kind, // 0 program, 1 sector, 2 block, 3 chip
// Program side.
output logic [LEN_W-1:0] safe_len, // bytes that fit before the wrap
output logic crosses_page,
output logic [ADDR_W-1:0] wrap_addr, // where the excess lands
output logic [LEN_W-1:0] wrap_len, // how many bytes it clobbers
// Erase side.
output logic [ADDR_W-1:0] erase_base, // the unit the erase really hits
output logic [ADDR_W-1:0] erase_size,
output logic erase_misaligned
);
localparam logic [1:0] E_PROGRAM = 2'd0;
localparam logic [1:0] E_SECTOR = 2'd1;
localparam logic [1:0] E_BLOCK = 2'd2;
localparam logic [1:0] E_CHIP = 2'd3;
localparam logic [ADDR_W-1:0] PAGE_MASK = ADDR_W'((1 << PAGE_BITS) - 1);
localparam logic [ADDR_W-1:0] SECTOR_MASK = ADDR_W'((1 << SECTOR_BITS) - 1);
localparam logic [ADDR_W-1:0] BLOCK_MASK = ADDR_W'((1 << BLOCK_BITS) - 1);
localparam logic [LEN_W-1:0] PAGE_SIZE = LEN_W'(1) << PAGE_BITS;
logic [LEN_W-1:0] page_offset;
logic [LEN_W-1:0] to_page_end;
logic [LEN_W-1:0] excess;
always_comb begin
page_offset = LEN_W'(addr & PAGE_MASK);
to_page_end = PAGE_SIZE - page_offset;
// The whole program fits exactly when it does not exceed the
// distance to the boundary. Note that to_page_end is never zero --
// an address on a page boundary has a full page ahead of it -- so
// safe_len is zero only for a zero-length program.
crosses_page = (len > to_page_end);
safe_len = crosses_page ? to_page_end : len;
// The damage. The excess wraps to the START of the same page, not
// to the next one, because the buffer's counter is page-wide.
excess = crosses_page ? (len - to_page_end) : {LEN_W{1'b0}};
wrap_addr = addr & ~PAGE_MASK;
// A program longer than a page wraps more than once and ends up
// having touched every byte of the page. Capping here reports the
// bytes DAMAGED rather than the arithmetic excess, which is the
// number a caller can act on.
wrap_len = (excess > PAGE_SIZE) ? PAGE_SIZE : excess;
end
always_comb begin
case (erase_kind)
E_SECTOR: begin
erase_base = addr & ~SECTOR_MASK;
erase_size = ADDR_W'(1) << SECTOR_BITS;
erase_misaligned = ((addr & SECTOR_MASK) != {ADDR_W{1'b0}});
end
E_BLOCK: begin
erase_base = addr & ~BLOCK_MASK;
erase_size = ADDR_W'(1) << BLOCK_BITS;
erase_misaligned = ((addr & BLOCK_MASK) != {ADDR_W{1'b0}});
end
E_CHIP: begin
// A chip erase takes no address at all, so no address can
// be misaligned for it.
erase_base = {ADDR_W{1'b0}};
erase_size = {ADDR_W{1'b1}};
erase_misaligned = 1'b0;
end
default: begin // E_PROGRAM -- not an erase
erase_base = addr;
erase_size = {ADDR_W{1'b0}};
erase_misaligned = 1'b0;
end
endcase
end
endmodule// flash_prog_guard_tb.sv
//
// Directed cases for the wrap and for each erase granularity, then two
// sweeps. The important property is the EXCLUSIVE one: either the whole
// program fits or it crosses, never both and never neither.
`timescale 1ns/1ps
module flash_prog_guard_tb;
localparam int ADDR_W = 24;
localparam int LEN_W = 16;
localparam int PAGE_BITS = 8;
localparam int SECTOR_BITS = 12;
localparam int BLOCK_BITS = 16;
localparam int PAGE_SIZE = 256;
localparam logic [1:0] E_PROGRAM = 2'd0;
localparam logic [1:0] E_SECTOR = 2'd1;
localparam logic [1:0] E_BLOCK = 2'd2;
localparam logic [1:0] E_CHIP = 2'd3;
logic [ADDR_W-1:0] addr = {ADDR_W{1'b0}};
logic [LEN_W-1:0] len = {LEN_W{1'b0}};
logic [1:0] erase_kind = E_PROGRAM;
logic [LEN_W-1:0] safe_len;
logic crosses_page;
logic [ADDR_W-1:0] wrap_addr;
logic [LEN_W-1:0] wrap_len;
logic [ADDR_W-1:0] erase_base, erase_size;
logic erase_misaligned;
int errors = 0;
int sw_off, sw_expect;
flash_prog_guard #(
.ADDR_W(ADDR_W), .LEN_W(LEN_W), .PAGE_BITS(PAGE_BITS),
.SECTOR_BITS(SECTOR_BITS), .BLOCK_BITS(BLOCK_BITS)
) dut (
.addr(addr), .len(len), .erase_kind(erase_kind),
.safe_len(safe_len), .crosses_page(crosses_page),
.wrap_addr(wrap_addr), .wrap_len(wrap_len),
.erase_base(erase_base), .erase_size(erase_size),
.erase_misaligned(erase_misaligned)
);
task automatic prog(input logic [ADDR_W-1:0] a, input int l);
begin
erase_kind = E_PROGRAM; addr = a; len = LEN_W'(l); #1;
end
endtask
task automatic chk(input string what, input int got, input int want);
begin
if (got != want) begin
$display(" FAIL: addr=0x%06h len=%0d: %s = %0d, expected %0d",
addr, len, what, got, want);
errors++;
end
end
endtask
initial begin
// 1. A program wholly inside a page.
prog(24'h000000, 256);
chk("safe_len", safe_len, 256);
if (crosses_page) begin
$display(" FAIL: a full page from its start was reported as crossing");
errors++;
end
chk("wrap_len", wrap_len, 0);
$display(" 0x000000 + 256: fits exactly, safe_len=%0d, no wrap", safe_len);
// 2. One byte too many. This is the off-by-one that matters: 256
// from a page start fits and 257 does not.
prog(24'h000000, 257);
if (!crosses_page) begin
$display(" FAIL: 257 bytes from a page start does not fit a 256-byte page");
errors++;
end
chk("safe_len", safe_len, 256);
chk("wrap_len", wrap_len, 1);
chk("wrap_addr", wrap_addr, 24'h000000);
$display(" 0x000000 + 257: safe_len=%0d, %0d byte wraps to 0x%06h",
safe_len, wrap_len, wrap_addr);
// 3. The classic case: a program starting part-way into a page.
// Sixteen bytes fit; the remaining sixteen overwrite the START
// of the same page -- bytes the caller may just have written.
prog(24'h0000F0, 32);
chk("safe_len", safe_len, 16);
chk("wrap_len", wrap_len, 16);
chk("wrap_addr", wrap_addr, 24'h000000);
if (!crosses_page) begin
$display(" FAIL: 0xF0 + 32 must cross a 256-byte page boundary");
errors++;
end
$display(" 0x0000F0 + 32: safe_len=%0d, %0d bytes clobber 0x%06h",
safe_len, wrap_len, wrap_addr);
// 4. A program longer than a whole page wraps more than once and
// ends up having touched every byte of the page. The damage is
// capped at the page size, which is the number a caller can use.
prog(24'h000100, 700);
chk("safe_len", safe_len, 256);
chk("wrap_len", wrap_len, 256);
chk("wrap_addr", wrap_addr, 24'h000100);
$display(" 0x000100 + 700: damage capped at %0d bytes -- the whole page",
wrap_len);
// 5. A zero-length program neither fits nor crosses in any useful
// sense; it must simply report nothing to do.
prog(24'h0000F0, 0);
chk("safe_len", safe_len, 0);
chk("wrap_len", wrap_len, 0);
if (crosses_page) begin
$display(" FAIL: a zero-length program was reported as crossing");
errors++;
end
// 6. Erase granularity. A sector erase acts on the sector
// CONTAINING the address, aligned or not.
erase_kind = E_SECTOR; addr = 24'h001000; #1;
chk("erase_base", erase_base, 24'h001000);
chk("erase_size", erase_size, 4096);
if (erase_misaligned) begin
$display(" FAIL: an aligned sector erase was flagged misaligned");
errors++;
end
addr = 24'h001234; #1;
chk("erase_base", erase_base, 24'h001000);
if (!erase_misaligned) begin
$display(" FAIL: 0x001234 is not a sector boundary and was not flagged");
errors++;
end
$display(" sector erase at 0x001234: acts on 0x%06h, %0d bytes, misaligned=%0b",
erase_base, erase_size, erase_misaligned);
// 7. A block erase on the same address hits a much larger unit --
// which is the point of reporting the size as well as the base.
erase_kind = E_BLOCK; addr = 24'h001234; #1;
chk("erase_base", erase_base, 24'h000000);
chk("erase_size", erase_size, 65536);
if (!erase_misaligned) begin
$display(" FAIL: 0x001234 is not a block boundary and was not flagged");
errors++;
end
$display(" block erase at 0x001234: acts on 0x%06h, %0d bytes",
erase_base, erase_size);
erase_kind = E_BLOCK; addr = 24'h010000; #1;
chk("erase_base", erase_base, 24'h010000);
if (erase_misaligned) begin
$display(" FAIL: an aligned block erase was flagged misaligned");
errors++;
end
// 8. A chip erase takes no address, so no address can be
// misaligned for it.
erase_kind = E_CHIP; addr = 24'h00ABCD; #1;
chk("erase_base", erase_base, 24'h000000);
if (erase_misaligned) begin
$display(" FAIL: a chip erase cannot be misaligned"); errors++;
end
$display(" chip erase: acts on 0x%06h, the whole device",
erase_base);
// 9. THE EXCLUSIVE PROPERTY. Across every offset and a range of
// lengths, exactly one of "the whole program fits" and "it
// crosses" must hold. A guard for which both or neither can be
// true has an unreachable or a contradictory case.
for (int off = 0; off < 256; off++) begin
for (int l = 0; l <= 300; l += 7) begin
prog(ADDR_W'(24'h002000 + off), l);
if ((safe_len == LEN_W'(l)) == crosses_page) begin
$display(" FAIL: off=%0d len=%0d -- fits and crosses are not exclusive",
off, l);
errors++;
end
if (safe_len > LEN_W'(256)) begin
$display(" FAIL: off=%0d len=%0d -- safe_len %0d exceeds a page",
off, l, safe_len);
errors++;
end
if (safe_len > LEN_W'(l)) begin
$display(" FAIL: off=%0d len=%0d -- safe_len exceeds the request",
off, l);
errors++;
end
// The safe part must never itself cross the boundary.
sw_expect = 256 - off;
if (crosses_page && safe_len !== LEN_W'(sw_expect)) begin
$display(" FAIL: off=%0d len=%0d -- safe_len %0d should be %0d",
off, l, safe_len, sw_expect);
errors++;
end
// The wrap always lands on the page start, never elsewhere.
if (wrap_addr !== ((24'h002000 + off) & ~24'h0000FF)) begin
$display(" FAIL: off=%0d -- wrap lands at 0x%06h", off, wrap_addr);
errors++;
end
end
end
$display(" 11008 (offset, length) pairs swept: fits and crosses are always exclusive, the safe part never crosses, and the wrap always lands on the page start");
if (errors == 0)
$display("PASS: the safe length stops exactly at the page boundary, a program crossing it reports the page start as the wrap address and the true number of bytes clobbered, a program longer than a page reports whole-page damage, each erase kind reports the unit it really acts on with misalignment flagged, and fitting and crossing are mutually exclusive across the whole sweep");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmoduleThe directed cases pin the off-by-one at both ends: 256 bytes from a page start fits and 257 does not, and the classic 0x0000F0 + 32 reports sixteen bytes safe and sixteen clobbering 0x000000. A program of 700 bytes reports damage capped at 256 — the whole page.
But the sweep carries the specification. Across all 256 offsets and 43 lengths it asserts an exclusive property:
exactly one of "the whole program fits" and "it crosses the boundary" holds.
Never both, never neither. A guard for which both could be true has a contradictory case; one for which neither could be true has an unreachable one. The same sweep also requires the safe length to stop exactly at the boundary whenever it crosses, and the wrap to land on the page start at every offset — eleven thousand checks of a property that would otherwise rest on three examples.
// flash_prog_guard.v
//
// Chapter 11.3 -- page programs, boundary wrap, and erase granularity, in
// Verilog-2001.
//
// A page program fills an internal PAGE BUFFER whose address counter is
// only as wide as the page, so a program running past the page boundary
// does not continue into the next page -- the counter WRAPS and the excess
// bytes overwrite the beginning of the same page. The device reports no
// error.
//
// This block reports the SAFE length -- how many bytes may be programmed
// from this address without wrapping -- and the DAMAGE, meaning exactly
// which address the excess lands on and how many bytes it clobbers.
// Reporting the damage rather than only refusing lets a driver log
// something useful.
//
// Erase acts on a whole sector or block whatever address it is given, so an
// unaligned erase address is not an error at the device -- it erases the
// unit CONTAINING that address, which is almost never what a caller passing
// an unaligned address intended.
module flash_prog_guard #(
parameter ADDR_W = 24,
parameter LEN_W = 16,
parameter PAGE_BITS = 8, // 256 B program granularity
parameter SECTOR_BITS = 12, // 4 KB smallest erase
parameter BLOCK_BITS = 16 // 64 KB large erase
) (
input wire [ADDR_W-1:0] addr,
input wire [LEN_W-1:0] len,
input wire [1:0] erase_kind, // 0 program, 1 sector, 2 block, 3 chip
// Program side.
output reg [LEN_W-1:0] safe_len, // bytes that fit before the wrap
output reg crosses_page,
output reg [ADDR_W-1:0] wrap_addr, // where the excess lands
output reg [LEN_W-1:0] wrap_len, // how many bytes it clobbers
// Erase side.
output reg [ADDR_W-1:0] erase_base, // the unit the erase really hits
output reg [ADDR_W-1:0] erase_size,
output reg erase_misaligned
);
localparam [1:0] E_PROGRAM = 2'd0;
localparam [1:0] E_SECTOR = 2'd1;
localparam [1:0] E_BLOCK = 2'd2;
localparam [1:0] E_CHIP = 2'd3;
localparam [ADDR_W-1:0] PAGE_MASK = (1 << PAGE_BITS) - 1;
localparam [ADDR_W-1:0] SECTOR_MASK = (1 << SECTOR_BITS) - 1;
localparam [ADDR_W-1:0] BLOCK_MASK = (1 << BLOCK_BITS) - 1;
localparam [LEN_W-1:0] PAGE_SIZE = (1 << PAGE_BITS);
reg [LEN_W-1:0] page_offset;
reg [LEN_W-1:0] to_page_end;
reg [LEN_W-1:0] excess;
always @(*) begin
page_offset = addr & PAGE_MASK;
to_page_end = PAGE_SIZE - page_offset;
// The whole program fits exactly when it does not exceed the
// distance to the boundary. to_page_end is never zero -- an address
// on a page boundary has a full page ahead of it -- so safe_len is
// zero only for a zero-length program.
crosses_page = (len > to_page_end);
if (crosses_page) safe_len = to_page_end;
else safe_len = len;
// The damage. The excess wraps to the START of the same page, not
// to the next one, because the buffer's counter is page-wide.
if (crosses_page) excess = len - to_page_end;
else excess = {LEN_W{1'b0}};
wrap_addr = addr & ~PAGE_MASK;
// A program longer than a page wraps more than once and ends up
// having touched every byte of the page. Capping reports the bytes
// DAMAGED rather than the arithmetic excess.
if (excess > PAGE_SIZE) wrap_len = PAGE_SIZE;
else wrap_len = excess;
end
always @(*) begin
case (erase_kind)
E_SECTOR: begin
erase_base = addr & ~SECTOR_MASK;
erase_size = (1 << SECTOR_BITS);
erase_misaligned = ((addr & SECTOR_MASK) != {ADDR_W{1'b0}});
end
E_BLOCK: begin
erase_base = addr & ~BLOCK_MASK;
erase_size = (1 << BLOCK_BITS);
erase_misaligned = ((addr & BLOCK_MASK) != {ADDR_W{1'b0}});
end
E_CHIP: begin
// A chip erase takes no address at all, so no address can
// be misaligned for it.
erase_base = {ADDR_W{1'b0}};
erase_size = {ADDR_W{1'b1}};
erase_misaligned = 1'b0;
end
default: begin // E_PROGRAM -- not an erase
erase_base = addr;
erase_size = {ADDR_W{1'b0}};
erase_misaligned = 1'b0;
end
endcase
end
endmodule// flash_prog_guard_tb.v
//
// The same checks as the SystemVerilog testbench, including the exclusive
// property: either the whole program fits or it crosses, never both and
// never neither.
`timescale 1ns/1ps
module flash_prog_guard_tb;
parameter ADDR_W = 24;
parameter LEN_W = 16;
parameter PAGE_BITS = 8;
parameter SECTOR_BITS = 12;
parameter BLOCK_BITS = 16;
localparam [1:0] E_PROGRAM = 2'd0;
localparam [1:0] E_SECTOR = 2'd1;
localparam [1:0] E_BLOCK = 2'd2;
localparam [1:0] E_CHIP = 2'd3;
reg [ADDR_W-1:0] addr;
reg [LEN_W-1:0] len;
reg [1:0] erase_kind;
wire [LEN_W-1:0] safe_len;
wire crosses_page;
wire [ADDR_W-1:0] wrap_addr;
wire [LEN_W-1:0] wrap_len;
wire [ADDR_W-1:0] erase_base, erase_size;
wire erase_misaligned;
integer errors;
integer off, l;
integer sw_expect;
initial begin
addr = {ADDR_W{1'b0}}; len = {LEN_W{1'b0}}; erase_kind = E_PROGRAM;
errors = 0;
end
flash_prog_guard #(
.ADDR_W(ADDR_W), .LEN_W(LEN_W), .PAGE_BITS(PAGE_BITS),
.SECTOR_BITS(SECTOR_BITS), .BLOCK_BITS(BLOCK_BITS)
) dut (
.addr(addr), .len(len), .erase_kind(erase_kind),
.safe_len(safe_len), .crosses_page(crosses_page),
.wrap_addr(wrap_addr), .wrap_len(wrap_len),
.erase_base(erase_base), .erase_size(erase_size),
.erase_misaligned(erase_misaligned)
);
task prog;
input [ADDR_W-1:0] a;
input integer ln;
begin
erase_kind = E_PROGRAM; addr = a; len = ln[LEN_W-1:0]; #1;
end
endtask
task chk;
input [8*20:1] what;
input integer got;
input integer want;
begin
if (got != want) begin
$display(" FAIL: addr=0x%06h len=%0d: %0s = %0d, expected %0d",
addr, len, what, got, want);
errors = errors + 1;
end
end
endtask
initial begin
#1;
// 1. A program wholly inside a page.
prog(24'h000000, 256);
chk("safe_len", safe_len, 256);
if (crosses_page) begin
$display(" FAIL: a full page from its start was reported as crossing");
errors = errors + 1;
end
chk("wrap_len", wrap_len, 0);
$display(" 0x000000 + 256: fits exactly, safe_len=%0d, no wrap", safe_len);
// 2. One byte too many -- the off-by-one that matters.
prog(24'h000000, 257);
if (!crosses_page) begin
$display(" FAIL: 257 bytes from a page start does not fit a 256-byte page");
errors = errors + 1;
end
chk("safe_len", safe_len, 256);
chk("wrap_len", wrap_len, 1);
chk("wrap_addr", wrap_addr, 24'h000000);
$display(" 0x000000 + 257: safe_len=%0d, %0d byte wraps to 0x%06h",
safe_len, wrap_len, wrap_addr);
// 3. The classic case: a program starting part-way into a page.
prog(24'h0000F0, 32);
chk("safe_len", safe_len, 16);
chk("wrap_len", wrap_len, 16);
chk("wrap_addr", wrap_addr, 24'h000000);
if (!crosses_page) begin
$display(" FAIL: 0xF0 + 32 must cross a 256-byte page boundary");
errors = errors + 1;
end
$display(" 0x0000F0 + 32: safe_len=%0d, %0d bytes clobber 0x%06h",
safe_len, wrap_len, wrap_addr);
// 4. A program longer than a whole page: damage capped at the page.
prog(24'h000100, 700);
chk("safe_len", safe_len, 256);
chk("wrap_len", wrap_len, 256);
chk("wrap_addr", wrap_addr, 24'h000100);
$display(" 0x000100 + 700: damage capped at %0d bytes -- the whole page",
wrap_len);
// 5. A zero-length program reports nothing to do.
prog(24'h0000F0, 0);
chk("safe_len", safe_len, 0);
chk("wrap_len", wrap_len, 0);
if (crosses_page) begin
$display(" FAIL: a zero-length program was reported as crossing");
errors = errors + 1;
end
// 6. Erase granularity.
erase_kind = E_SECTOR; addr = 24'h001000; #1;
chk("erase_base", erase_base, 24'h001000);
chk("erase_size", erase_size, 4096);
if (erase_misaligned) begin
$display(" FAIL: an aligned sector erase was flagged misaligned");
errors = errors + 1;
end
addr = 24'h001234; #1;
chk("erase_base", erase_base, 24'h001000);
if (!erase_misaligned) begin
$display(" FAIL: 0x001234 is not a sector boundary and was not flagged");
errors = errors + 1;
end
$display(" sector erase at 0x001234: acts on 0x%06h, %0d bytes, misaligned=%0b",
erase_base, erase_size, erase_misaligned);
// 7. A block erase on the same address hits a much larger unit.
erase_kind = E_BLOCK; addr = 24'h001234; #1;
chk("erase_base", erase_base, 24'h000000);
chk("erase_size", erase_size, 65536);
if (!erase_misaligned) begin
$display(" FAIL: 0x001234 is not a block boundary and was not flagged");
errors = errors + 1;
end
$display(" block erase at 0x001234: acts on 0x%06h, %0d bytes",
erase_base, erase_size);
erase_kind = E_BLOCK; addr = 24'h010000; #1;
chk("erase_base", erase_base, 24'h010000);
if (erase_misaligned) begin
$display(" FAIL: an aligned block erase was flagged misaligned");
errors = errors + 1;
end
// 8. A chip erase takes no address.
erase_kind = E_CHIP; addr = 24'h00ABCD; #1;
chk("erase_base", erase_base, 24'h000000);
if (erase_misaligned) begin
$display(" FAIL: a chip erase cannot be misaligned");
errors = errors + 1;
end
$display(" chip erase: acts on 0x%06h, the whole device",
erase_base);
// 9. THE EXCLUSIVE PROPERTY, swept.
for (off = 0; off < 256; off = off + 1) begin
for (l = 0; l <= 300; l = l + 7) begin
prog(24'h002000 + off, l);
if ((safe_len == l) == crosses_page) begin
$display(" FAIL: off=%0d len=%0d -- fits and crosses are not exclusive",
off, l);
errors = errors + 1;
end
if (safe_len > 256) begin
$display(" FAIL: off=%0d len=%0d -- safe_len %0d exceeds a page",
off, l, safe_len);
errors = errors + 1;
end
if (safe_len > l) begin
$display(" FAIL: off=%0d len=%0d -- safe_len exceeds the request",
off, l);
errors = errors + 1;
end
sw_expect = 256 - off;
if (crosses_page && safe_len !== sw_expect) begin
$display(" FAIL: off=%0d len=%0d -- safe_len %0d should be %0d",
off, l, safe_len, sw_expect);
errors = errors + 1;
end
if (wrap_addr !== ((24'h002000 + off) & ~24'h0000FF)) begin
$display(" FAIL: off=%0d -- wrap lands at 0x%06h", off, wrap_addr);
errors = errors + 1;
end
end
end
$display(" 11008 (offset, length) pairs swept: fits and crosses are always exclusive, the safe part never crosses, and the wrap always lands on the page start");
if (errors == 0)
$display("PASS: the safe length stops exactly at the page boundary, a program crossing it reports the page start as the wrap address and the true number of bytes clobbered, a program longer than a page reports whole-page damage, each erase kind reports the unit it really acts on with misalignment flagged, and fitting and crossing are mutually exclusive across the whole sweep");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule-- flash_prog_guard.vhd
--
-- Chapter 11.3 -- page programs, boundary wrap, and erase granularity, in
-- VHDL.
--
-- A page program fills an internal PAGE BUFFER whose address counter is
-- only as wide as the page, so a program running past the page boundary
-- does not continue into the next page -- the counter WRAPS and the excess
-- bytes overwrite the beginning of the same page. The device reports no
-- error.
--
-- This block reports the SAFE length -- how many bytes may be programmed
-- from this address without wrapping -- and the DAMAGE, meaning exactly
-- which address the excess lands on and how many bytes it clobbers.
--
-- Erase acts on a whole sector or block whatever address it is given, so an
-- unaligned erase address is not an error at the device -- it erases the
-- unit CONTAINING that address, which is almost never what a caller passing
-- an unaligned address intended.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity flash_prog_guard is
generic (
ADDR_W : positive := 24;
LEN_W : positive := 16;
PAGE_BITS : positive := 8; -- 256 B program granularity
SECTOR_BITS : positive := 12; -- 4 KB smallest erase
BLOCK_BITS : positive := 16 -- 64 KB large erase
);
port (
addr : in unsigned(ADDR_W - 1 downto 0);
len : in unsigned(LEN_W - 1 downto 0);
erase_kind : in unsigned(1 downto 0); -- 0 prog 1 sect 2 blk 3 chip
-- Program side.
safe_len : out unsigned(LEN_W - 1 downto 0);
crosses_page : out std_logic;
wrap_addr : out unsigned(ADDR_W - 1 downto 0);
wrap_len : out unsigned(LEN_W - 1 downto 0);
-- Erase side.
erase_base : out unsigned(ADDR_W - 1 downto 0);
erase_size : out unsigned(ADDR_W - 1 downto 0);
erase_misaligned : out std_logic
);
end entity;
architecture rtl of flash_prog_guard is
constant E_PROGRAM : unsigned(1 downto 0) := "00";
constant E_SECTOR : unsigned(1 downto 0) := "01";
constant E_BLOCK : unsigned(1 downto 0) := "10";
constant E_CHIP : unsigned(1 downto 0) := "11";
constant PAGE_MASK : unsigned(ADDR_W - 1 downto 0) :=
to_unsigned(2 ** PAGE_BITS - 1, ADDR_W);
constant SECTOR_MASK : unsigned(ADDR_W - 1 downto 0) :=
to_unsigned(2 ** SECTOR_BITS - 1, ADDR_W);
constant BLOCK_MASK : unsigned(ADDR_W - 1 downto 0) :=
to_unsigned(2 ** BLOCK_BITS - 1, ADDR_W);
constant PAGE_SIZE : natural := 2 ** PAGE_BITS;
begin
program_side : process (addr, len)
variable p_off : natural;
variable to_end : natural;
variable cross : boolean;
variable excess : natural;
begin
p_off := to_integer(addr and PAGE_MASK);
to_end := PAGE_SIZE - p_off;
-- The whole program fits exactly when it does not exceed the
-- distance to the boundary. to_end is never zero -- an address on a
-- page boundary has a full page ahead of it -- so safe_len is zero
-- only for a zero-length program.
cross := to_integer(len) > to_end;
if cross then
crosses_page <= '1';
safe_len <= to_unsigned(to_end, LEN_W);
excess := to_integer(len) - to_end;
else
crosses_page <= '0';
safe_len <= len;
excess := 0;
end if;
-- The excess wraps to the START of the same page, not to the next
-- one, because the buffer's counter is page-wide.
wrap_addr <= addr and not PAGE_MASK;
-- A program longer than a page wraps more than once and ends up
-- having touched every byte of the page. Capping reports the bytes
-- DAMAGED rather than the arithmetic excess.
if excess > PAGE_SIZE then
wrap_len <= to_unsigned(PAGE_SIZE, LEN_W);
else
wrap_len <= to_unsigned(excess, LEN_W);
end if;
end process;
erase_side : process (addr, erase_kind)
begin
case erase_kind is
when E_SECTOR =>
erase_base <= addr and not SECTOR_MASK;
erase_size <= to_unsigned(2 ** SECTOR_BITS, ADDR_W);
if (addr and SECTOR_MASK) /= 0 then
erase_misaligned <= '1';
else
erase_misaligned <= '0';
end if;
when E_BLOCK =>
erase_base <= addr and not BLOCK_MASK;
erase_size <= to_unsigned(2 ** BLOCK_BITS, ADDR_W);
if (addr and BLOCK_MASK) /= 0 then
erase_misaligned <= '1';
else
erase_misaligned <= '0';
end if;
when E_CHIP =>
-- A chip erase takes no address at all, so no address can
-- be misaligned for it.
erase_base <= (others => '0');
erase_size <= (others => '1');
erase_misaligned <= '0';
when others => -- E_PROGRAM -- not an erase
erase_base <= addr;
erase_size <= (others => '0');
erase_misaligned <= '0';
end case;
end process;
end architecture;-- flash_prog_guard_tb.vhd
--
-- The same checks as the SystemVerilog and Verilog testbenches, including
-- the exclusive property: either the whole program fits or it crosses,
-- never both and never neither.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity flash_prog_guard_tb is
end entity;
architecture sim of flash_prog_guard_tb is
constant ADDR_W : positive := 24;
constant LEN_W : positive := 16;
constant PAGE_BITS : positive := 8;
constant SECTOR_BITS : positive := 12;
constant BLOCK_BITS : positive := 16;
constant E_PROGRAM : unsigned(1 downto 0) := "00";
constant E_SECTOR : unsigned(1 downto 0) := "01";
constant E_BLOCK : unsigned(1 downto 0) := "10";
constant E_CHIP : unsigned(1 downto 0) := "11";
signal addr : unsigned(ADDR_W - 1 downto 0) := (others => '0');
signal len : unsigned(LEN_W - 1 downto 0) := (others => '0');
signal erase_kind : unsigned(1 downto 0) := E_PROGRAM;
signal safe_len : unsigned(LEN_W - 1 downto 0);
signal crosses_page : std_logic;
signal wrap_addr : unsigned(ADDR_W - 1 downto 0);
signal wrap_len : unsigned(LEN_W - 1 downto 0);
signal erase_base : unsigned(ADDR_W - 1 downto 0);
signal erase_size : unsigned(ADDR_W - 1 downto 0);
signal erase_misaligned : std_logic;
signal errors : natural := 0;
begin
dut : entity work.flash_prog_guard
generic map (ADDR_W => ADDR_W, LEN_W => LEN_W, PAGE_BITS => PAGE_BITS,
SECTOR_BITS => SECTOR_BITS, BLOCK_BITS => BLOCK_BITS)
port map (
addr => addr, len => len, erase_kind => erase_kind,
safe_len => safe_len, crosses_page => crosses_page,
wrap_addr => wrap_addr, wrap_len => wrap_len,
erase_base => erase_base, erase_size => erase_size,
erase_misaligned => erase_misaligned
);
stim : process
variable errs : natural := 0;
variable sw_expect : natural;
procedure prog(a : natural; ln : natural) is
begin
erase_kind <= E_PROGRAM;
addr <= to_unsigned(a, ADDR_W);
len <= to_unsigned(ln, LEN_W);
wait for 1 ns;
end procedure;
procedure chk(what : string; got : natural; want : natural) is
begin
if got /= want then
report " FAIL: " & what & " = " & integer'image(got) &
", expected " & integer'image(want);
errs := errs + 1;
end if;
end procedure;
begin
wait for 1 ns;
-- 1. A program wholly inside a page.
prog(16#000000#, 256);
chk("safe_len", to_integer(safe_len), 256);
if crosses_page = '1' then
report " FAIL: a full page from its start was reported as crossing";
errs := errs + 1;
end if;
chk("wrap_len", to_integer(wrap_len), 0);
report " 0x000000 + 256: fits exactly, safe_len=" &
integer'image(to_integer(safe_len)) & ", no wrap";
-- 2. One byte too many -- the off-by-one that matters.
prog(16#000000#, 257);
if crosses_page /= '1' then
report " FAIL: 257 bytes from a page start does not fit a 256-byte page";
errs := errs + 1;
end if;
chk("safe_len", to_integer(safe_len), 256);
chk("wrap_len", to_integer(wrap_len), 1);
chk("wrap_addr", to_integer(wrap_addr), 16#000000#);
report " 0x000000 + 257: safe_len=" &
integer'image(to_integer(safe_len)) & ", " &
integer'image(to_integer(wrap_len)) & " byte wraps to 0x000000";
-- 3. The classic case: a program starting part-way into a page.
prog(16#0000F0#, 32);
chk("safe_len", to_integer(safe_len), 16);
chk("wrap_len", to_integer(wrap_len), 16);
chk("wrap_addr", to_integer(wrap_addr), 16#000000#);
if crosses_page /= '1' then
report " FAIL: 0xF0 + 32 must cross a 256-byte page boundary";
errs := errs + 1;
end if;
report " 0x0000F0 + 32: safe_len=" &
integer'image(to_integer(safe_len)) & ", " &
integer'image(to_integer(wrap_len)) &
" bytes clobber 0x000000";
-- 4. A program longer than a whole page: damage capped at the page.
prog(16#000100#, 700);
chk("safe_len", to_integer(safe_len), 256);
chk("wrap_len", to_integer(wrap_len), 256);
chk("wrap_addr", to_integer(wrap_addr), 16#000100#);
report " 0x000100 + 700: damage capped at " &
integer'image(to_integer(wrap_len)) &
" bytes -- the whole page";
-- 5. A zero-length program reports nothing to do.
prog(16#0000F0#, 0);
chk("safe_len", to_integer(safe_len), 0);
chk("wrap_len", to_integer(wrap_len), 0);
if crosses_page = '1' then
report " FAIL: a zero-length program was reported as crossing";
errs := errs + 1;
end if;
-- 6. Erase granularity.
erase_kind <= E_SECTOR; addr <= to_unsigned(16#001000#, ADDR_W);
wait for 1 ns;
chk("erase_base", to_integer(erase_base), 16#001000#);
chk("erase_size", to_integer(erase_size), 4096);
if erase_misaligned = '1' then
report " FAIL: an aligned sector erase was flagged misaligned";
errs := errs + 1;
end if;
addr <= to_unsigned(16#001234#, ADDR_W);
wait for 1 ns;
chk("erase_base", to_integer(erase_base), 16#001000#);
if erase_misaligned /= '1' then
report " FAIL: 0x001234 is not a sector boundary and was not flagged";
errs := errs + 1;
end if;
report " sector erase at 0x001234: acts on 0x001000, " &
integer'image(to_integer(erase_size)) &
" bytes, misaligned";
-- 7. A block erase on the same address hits a much larger unit.
erase_kind <= E_BLOCK; addr <= to_unsigned(16#001234#, ADDR_W);
wait for 1 ns;
chk("erase_base", to_integer(erase_base), 16#000000#);
chk("erase_size", to_integer(erase_size), 65536);
if erase_misaligned /= '1' then
report " FAIL: 0x001234 is not a block boundary and was not flagged";
errs := errs + 1;
end if;
report " block erase at 0x001234: acts on 0x000000, " &
integer'image(to_integer(erase_size)) & " bytes";
erase_kind <= E_BLOCK; addr <= to_unsigned(16#010000#, ADDR_W);
wait for 1 ns;
chk("erase_base", to_integer(erase_base), 16#010000#);
if erase_misaligned = '1' then
report " FAIL: an aligned block erase was flagged misaligned";
errs := errs + 1;
end if;
-- 8. A chip erase takes no address.
erase_kind <= E_CHIP; addr <= to_unsigned(16#00ABCD#, ADDR_W);
wait for 1 ns;
chk("erase_base", to_integer(erase_base), 16#000000#);
if erase_misaligned = '1' then
report " FAIL: a chip erase cannot be misaligned";
errs := errs + 1;
end if;
report " chip erase: acts on 0x000000, the whole device";
-- 9. THE EXCLUSIVE PROPERTY, swept.
for off in 0 to 255 loop
for k in 0 to 42 loop
prog(16#002000# + off, k * 7);
if (to_integer(safe_len) = k * 7) = (crosses_page = '1') then
report " FAIL: fits and crosses are not exclusive";
errs := errs + 1;
end if;
if to_integer(safe_len) > 256 then
report " FAIL: safe_len exceeds a page"; errs := errs + 1;
end if;
if to_integer(safe_len) > k * 7 then
report " FAIL: safe_len exceeds the request";
errs := errs + 1;
end if;
sw_expect := 256 - off;
if crosses_page = '1' and to_integer(safe_len) /= sw_expect then
report " FAIL: safe_len does not stop at the boundary";
errs := errs + 1;
end if;
if to_integer(wrap_addr) /= (16#002000# + off) / 256 * 256 then
report " FAIL: the wrap does not land on the page start";
errs := errs + 1;
end if;
end loop;
end loop;
report " 11008 (offset, length) pairs swept: fits and crosses are always exclusive, the safe part never crosses, and the wrap always lands on the page start";
errors <= errs;
if errs = 0 then
report "PASS: the safe length stops exactly at the page boundary, a program crossing it reports the page start as the wrap address and the true number of bytes clobbered, a program longer than a page reports whole-page damage, each erase kind reports the unit it really acts on with misalignment flagged, and fitting and crossing are mutually exclusive across the whole sweep";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
wait;
end process;
end architecture;Parity
All three implement the same guard: identical ports and generics, purely combinational, a safe length that is the minimum of the request and the distance to the page boundary, a wrap address that is the page base, damage capped at the page size, and per-unit erase bases with misalignment flagged. All three testbenches run the same directed cases and the same 11 008-pair sweep, reporting identical values — 16 safe and 16 clobbered at 0x0000F0 + 32, and a sector erase at 0x001234 acting on 0x001000.
7. Why a Verification Engineer Cares
// 1. EXCLUSIVITY. Exactly one of "fits" and "crosses" holds. Both true
// is a contradiction; neither true is an unreachable state. This one
// property rules out a whole class of boundary-logic error.
a_exclusive : assert property (
@(posedge clk)
((safe_len == len) != crosses_page))
else $error("fitting and crossing are not mutually exclusive");
// 2. CONTAINMENT. The safe part never itself crosses the boundary --
// which is the entire point of computing it.
a_safe_contained : assert property (
@(posedge clk)
(((addr & PAGE_MASK) + safe_len) <= PAGE_SIZE))
else $error("the safe length crosses the page boundary");
// 3. The wrap lands on the PAGE START, not on the transfer's start. A
// guard that reported the caller's own address would name the wrong
// bytes as damaged -- and be believed.
a_wrap_at_page : assert property (
@(posedge clk)
(wrap_addr == (addr & ~PAGE_MASK)))
else $error("the wrap address is not the page base");
// 4. DAMAGE IS BOUNDED by the page. A program many pages long wraps
// repeatedly and touches every byte once; reporting more than a page
// would be arithmetically true and operationally meaningless.
a_damage_bounded : assert property (
@(posedge clk)
(wrap_len <= PAGE_SIZE))
else $error("reported damage exceeds one page");
// 5. ERASE ACTS ON A UNIT BOUNDARY, always -- the low bits of the
// address are ignored by the device, so the guard must report the
// base it will really hit rather than the address it was given.
a_erase_aligned : assert property (
@(posedge clk)
((erase_kind == E_SECTOR) |-> ((erase_base & SECTOR_MASK) == 0)) &&
((erase_kind == E_BLOCK) |-> ((erase_base & BLOCK_MASK) == 0)))
else $error("the reported erase base is not unit-aligned");
// 6. And misalignment is reported whenever the caller's address was not
// the base -- because the DEVICE cannot report it.
a_misalign_honest : assert property (
@(posedge clk)
((erase_kind == E_SECTOR) |->
(erase_misaligned == ((addr & SECTOR_MASK) != 0))))
else $error("misalignment was not reported honestly");Property 1 is the one to lift into any boundary-logic design. An exclusive pair of outcomes is a cheap, complete statement about a decision that would otherwise need enumerating — and it catches both directions of error at once, which a check on either outcome alone does not.
Property 3 guards against an error that is easy to make and very convincing when made. Reporting the caller's starting address as the wrap destination produces a plausible number that is wrong, and a driver logging it sends the investigation to the wrong 44 bytes.
Coverage must reach the boundary offsets, which random addresses miss:
covergroup flash_prog_cg @(posedge clk iff prog_req);
// Offset within the page decides the safe length entirely. Offset 0
// and offset 255 are one part in 256 each under random addressing.
cp_off : coverpoint page_offset {
bins at_start = {0};
bins early = {[1:127]};
bins late = {[128:254]};
bins last_byte = {255};
}
// Length relative to the distance remaining -- the only comparison
// that matters, and one that raw length coverage cannot express.
cp_fit : coverpoint fit_class {
bins zero = {L_ZERO};
bins well_within = {L_UNDER};
bins exact_fit = {L_EXACT}; // must NOT cross
bins one_over = {L_PLUS_ONE}; // must cross
bins over_a_page = {L_MULTI}; // damage capped
}
cp_erase : coverpoint erase_kind {
bins program = {E_PROGRAM};
bins sector = {E_SECTOR};
bins block = {E_BLOCK};
bins chip = {E_CHIP};
}
// Aligned and unaligned for each erase kind. A suite that only ever
// erases aligned addresses never exercises the flag at all.
cp_erase_align : coverpoint erase_misaligned {
bins aligned = {0};
bins unaligned = {1};
}
x_off_fit : cross cp_off, cp_fit;
x_erase_align : cross cp_erase, cp_erase_align;
endgroupcp_fit's exact_fit and one_over bins are the pair that must both be hit: they are one byte apart and must behave differently. And x_erase_align is the cross that catches the comfortable suite — a test set that always erases aligned addresses reports full coverage of the erase kinds and has never once checked the flag.
8. Why an FPGA or ASIC Engineer Cares
Put the split in the controller, not the driver. Chapter 9.3 argued that splitting belongs wherever the page size is known. A controller told the page size removes the entire class of bug where a block-write helper is reused with a length nobody re-checked.
Report the damage, not just the refusal. Three extra output words — the wrap address and the byte count — turn a rejected operation into a diagnosable one. During bring-up that is the difference between minutes and an afternoon.
Flag erase misalignment even though the device will not. A caller passing an unaligned sector-erase address believes something false. The device erases 4 KB regardless and reports nothing, so the controller is the only place the caller's mistaken belief can be caught.
Expose the erase size alongside the base. A block erase and a sector erase on the same address hit units differing by a factor of sixteen. A driver that sees only the base cannot tell how much it is about to destroy.
Never compute a boundary distance as zero. An address on a boundary has a full unit ahead of it. A zero-length program is accepted, does nothing, and loops forever.
Remember that splitting multiplies the write-enable and polling cost. Three programs mean three WREN commands and three busy waits, not one. A design that budgets one write cycle per transfer will miss its timing on any crossing transfer.
9. Failure Signature — A Firmware Update That Corrupts Its Own First Bytes
Symptom. A firmware updater writes an image to flash in chunks handed to it by a transport layer. Verification after writing passes. The device then fails to boot, and a hex dump shows the image is correct except that scattered short runs near the start of certain pages contain bytes that belong hundreds of bytes later.
What "verification passed" establishes. This is the observation that makes the case, and it is the same trap as Chapter 10.4's read-back: verification passed because it compared the flash against what the updater intended to write, chunk by chunk, using the same chunk boundaries. If a chunk's tail wrapped onto its own page start, a subsequent chunk rewrote that region and the final comparison of that chunk succeeded. The corruption is in a region no single chunk comparison covers.
Plausible mechanisms.
- Chunks not aligned to pages. If the transport delivers 512-byte or 1500-byte chunks and the updater programs each as one operation, every chunk that starts mid-page and exceeds the boundary wraps. This fits scattered damage near page starts exactly.
- A chunk longer than a page, which wraps more than once and damages the whole page.
- A splitter that clips at the wrong boundary — sector rather than page — so programs of up to 4 KB are issued and every one wraps.
- A missing erase, which would produce a bitwise AND rather than transposed bytes, so it does not fit.
- A wrap computed from the chunk's start rather than the page start, so the driver's own damage log points at the wrong bytes.
The discriminating observation. Take the addresses of the corrupted runs and compute each modulo 256. If every corrupted run begins at offset 0 of a page and its length equals the tail of a chunk that crossed into it, the page-wrap mechanism is confirmed — and the run lengths will match chunk_len − to_page_end for the chunk that crossed.
Then check the chunk boundaries directly: if any chunk length is not a divisor of the page size, or any chunk start is not page-aligned, the updater is issuing crossing programs.
The fix, and why it is at the right layer. The updater must split every chunk at page boundaries before issuing programs — not because the transport chose badly, but because a page program's length limit has nothing to do with the transport's chunk size and no layer above the flash driver should have to know it. That is the argument for putting the split in the controller: it is the only place that knows the page size, and every caller above it is then correct by construction.
And fix the verification. Verify by reading back the whole image against the whole source, not chunk against chunk. A per-chunk comparison cannot see damage that a later chunk repaired, which is precisely why this reached a boot failure with a passing verify.
10. Common Misconceptions
11. Reason It Through
Work this before reading the answer.
A driver must write 1000 bytes to address 0x0013C0 on a device with 256-byte pages and 4 KB sectors. The target region is already erased.
How many page programs, at what addresses and lengths? How many
WRENcommands and busy waits? And what changes if the region is not erased?
Find the page offset. 0x0013C0 is 5056; 5056 modulo 256 is 192. So the address is 192 bytes into a page and the distance to the boundary is 256 − 192 = 64.
Split it.
program 1: 0x0013C0 64 bytes (fills the partial page)
program 2: 0x001400 256 bytes
program 3: 0x001500 256 bytes
program 4: 0x001600 256 bytes
program 5: 0x001700 168 bytes (the remainder)Check conservation: 64 + 256 + 256 + 256 + 168 = 1000. Good — and that check is the first thing to do, because an off-by-one in the first piece is otherwise invisible.
Five programs, so five WREN commands and five busy waits. At a few hundred microseconds each, the write cycles dominate the transfer completely — the SPI traffic is perhaps 1000 byte times while the programming is on the order of a millisecond and a half.
Does a sector boundary matter? The transfer spans 0x0013C0 to 0x0017A7, and 0x001000–0x001FFF is one sector. It stays inside. But even if it crossed, it would not matter for programming: sector boundaries constrain erase, and this region is already erased.
Now the unerased case, which changes everything. Programming only clears bits, so writing into used space yields the bitwise AND of old and new — not an error and not the new data. The region spans one sector, so:
1. read 0x001000–0x001FFF into RAM 4 KB buffer
2. patch bytes 0x3C0..0x7A7 in RAM
3. WREN + sector erase 0x001000 + poll tens of ms
4. program 16 pages, each WREN + poll 16 write cyclesOne erase and sixteen programs, against five programs when erased. And the erase destroys 4 KB to change 1000 bytes.
The comparison worth carrying:
erased target: 5 programs, 5 WREN, 5 waits, ~1.5 ms
unerased target: 1 erase + 16 programs, 17 WREN, 17 waits, ~50 msA factor of thirty in time and a 4 KB RAM requirement, from the single question of whether the target was erased.
The general lesson, and it is the module's central one. The cost of a flash write is decided less by the number of bytes than by whether an erase is needed — which is why every practical flash design is organised to write into already-erased space, and why Chapter 11.1's append-only structures exist.
12. Understanding Check
13. Summary
A page program fills an internal page buffer whose address counter is page-wide, so a program crossing the boundary does not fail and does not continue into the next page — it wraps and overwrites the start of the same page, silently.
The wrap lands on the page base, not the caller's starting address, and nothing on the wires distinguishes the bytes after the boundary from those before it.
The remedy is to clip at page_size − (addr & page_mask), which is never zero — an address on a boundary has a full page ahead of it, and computing zero produces a zero-length program and an endless loop. Each piece of a split is a complete write cycle with its own write-enable and busy wait.
A guard should report the damage — which address, how many bytes, capped at a page — rather than only refusing, because a refusal cannot be logged usefully and cannot be tested interestingly.
Erase is a different granularity and a different kind of operation. Its address is a selector: the low bits are ignored and the unit containing the address is erased whole, so an unaligned erase address is never an error at the device and only the controller can catch the caller's false belief. A chip erase takes no address at all.
For verification, the property that nearly specifies the program side is exclusivity — exactly one of "fits" and "crosses" holds — and the coverage that matters is the pair one byte apart: an exact fit and one byte over.
And the cost of a flash write is decided less by its length than by whether an erase is needed: five programs against one erase and sixteen programs, for the same 1000 bytes.
14. What Comes Next
Every write in this chapter was preceded by a WREN and followed by a busy wait, and both were treated as given.
Chapter 11.4 — Status Registers, WIP/WEL, and Polling explains why. A write issued without the write-enable latch set is discarded silently — acknowledged bit by bit and simply not performed — and a device that has failed mid-erase is indistinguishable from one still working. The chapter builds the sequencer that confirms the latch before issuing anything and bounds the busy wait so a failure is reported rather than hung on, in all three HDLs.
Continue learning
Related tutorials
- 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.
- Related topic
Dummy Phases and Read Latency
Why a device needs turnaround before it can answer, why dummy is counted in clock cycles rather than bytes, how its length grows with frequency, and the one-byte data offset a mismatch produces.
