Skip to content
VLSI Mentor

SPI · Module 11

Serial Flash Architecture

Why a serial flash has three nested granularities — page, sector, block — what each one constrains, why erase and program are asymmetric, and the address decomposition whose boundary distances nest and are never zero.

Module 10 taught how to read any SPI device. This module takes the one device almost everyone meets, and it is worth its own module for a specific reason: a serial flash has more behaviour hiding behind a four-wire interface than any other part on a typical board.

The interface is four wires and the commands are one byte each. So why is a flash driver the hardest SPI driver to write?

Because the interface is simple and the device is not. Flash has three nested granularities, an asymmetry between writing and erasing, and operations whose durations differ by five orders of magnitude — none of which is visible on the wires.

1. What Makes Flash Different

Every device in Modules 1 to 10 shared one property: a transaction did what it said, immediately. Flash breaks that in three ways.

Writing cannot set a bit to 1. A flash cell can be programmed from 1 to 0 but not back. Restoring a 1 requires an erase, which works on a large region. This single asymmetry is the root of nearly everything else.

Operations take a long time. A page program is hundreds of microseconds; a sector erase, tens of milliseconds; a chip erase, seconds. Meanwhile the device will not answer anything except a status read, so Chapter 11.4's polling loop is not optional.

The granularity of writing and erasing differ. You program pages (typically 256 bytes) and erase sectors (typically 4 KB) or blocks (typically 64 KB). Those numbers are not the same, and the mismatch is why flash needs a translation layer to look like memory.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   read      any length, any address     — the only free-form operation
   program   within ONE page             — 1 → 0 only
   erase     a whole sector or block     — 0 → 1, everywhere in it

2. The Three Granularities, and Why They Nest

The sizes are not arbitrary and their relationship is the part that matters:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   page     256 B      the program granularity — the write buffer's size
   sector     4 KB     the smallest erase — 16 pages
   block     64 KB     the large erase — 16 sectors, 256 pages

Each unit lies wholly inside the next. That nesting is a structural guarantee, not a coincidence, and it has two consequences a driver relies on constantly:

  • The distances nest. From any address, the distance to the next page boundary is never greater than the distance to the next sector boundary, which is never greater than the distance to the next block boundary. So clipping a transfer to the nearest boundary is a single comparison against the smallest relevant distance.
  • Alignment nests the other way. An address on a block boundary is necessarily on a sector boundary and a page boundary too. So a block-aligned buffer is aligned for every operation.

A geometry where either failed would not be a flash geometry, and §6's testbench asserts both across tens of thousands of addresses — because they are the architecture rather than arithmetic.

3. The Command Interface

Everything a flash does is reachable through about a dozen opcodes, and they fall into four groups by shape — which is exactly the Chapter 10.5 classification:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   opcode only            WREN 0x06   WRDI 0x04   CE 0xC7
   opcode + data          RDSR 0x05   RDID 0x9F   RDP 0xAB
   opcode + address       SE 0x20     BE 0xD8
   opcode + address
        + data            READ 0x03   PP 0x02
   opcode + address
        + dummy + data    FAST READ 0x0B

Two observations are worth making now because they shape the rest of the module.

The opcodes are unrelated values, not a bit field. Unlike the packed register-access encoding of Chapter 10.4, there is no read/write bit here: 0x03 and 0x02 are a read and a program with nothing in common. A flash controller needs a table, not a bit-field codec.

Five of the twelve modify the device, and every one of those requires a separate WREN command first. That is Chapter 11.4's subject, and it is the commonest source of silent failure in the whole module.

4. The Structure, in One Picture

A serial flash's internal structure. The SPI interface feeds a command decoder, which routes to a read path, a page program buffer, or an erase controller. The array is divided into blocks, each containing sectors, each containing pages. Reads act at byte granularity, programs at page granularity, and erases at sector or block granularity. A status register reports work in progress.SPI interfaceSCLK, MOSI, MISO, CSCommand decodeone opcode, four shapesStatus registerWIP, WEL — the onlyreadable state while busyRead pathany address, any lengthPage buffer256 B — a program fillsthis, then commitsErase controla whole sector or block atoncePage — 256 Bprogram granularitySector — 4 KB16 pages — smallest eraseBlock — 64 KB16 sectors — large erasebytes0x03, 0x0B0x020x20, 0xD80x05reads any bytecommits one pageeraseserasesWIP while busy12
Figure 1 — the device behind the four wires. One address selects a byte inside a page inside a sector inside a block, and each operation acts at a different level of that hierarchy: reads at the byte, programs at the page, erases at the sector or block.

The picture explains the asymmetry that §1 stated. A read comes straight off the array at any granularity. A program must pass through one buffer, which is why it is page-limited. An erase acts on a whole region because that is how the physical process works. Three paths, three granularities, one interface.

5. The Consequence: Read-Modify-Write

Put §1 and §2 together and a fact falls out that governs every flash driver ever written.

Changing one byte from 0x00 to 0xFF requires erasing 4 KB.

The byte cannot be set to 1 in place, so its sector must be erased — and erasing the sector destroys the other 4095 bytes. So the operation becomes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   1. read the whole sector into RAM       4 KB of buffer
   2. modify the byte in RAM
   3. erase the sector                     tens of milliseconds
   4. program it back, page by page        16 page programs

That is one byte changed at the cost of 4 KB of RAM, one erase, sixteen programs and a great deal of time. The consequences are why flash is used the way it is:

  • Append-only structures avoid it entirely. Writing new data to erased space needs no erase at all — programming 1s to 0s is exactly what a program does.
  • Wear levelling exists because each erase wears the cells, and a naive read-modify-write erases the same sector repeatedly.
  • File systems for flash (and the translation layers inside SSDs) are almost entirely about not doing this.

6. Building the Geometry Decoder — Three HDLs

The circuit

Circuit. A purely combinational address decomposition.

State. None. Every output is a function of the address alone.

Datapath. Masks and shifts only. Page, sector and block sizes are always powers of two, so the offset within a unit is a mask, the index is a shift, and the distance to the boundary is a subtract. There is no divider anywhere, which matters because a modulo against a 24-bit address would infer one.

Control. None.

Clock and reset. Neither — like Chapter 10.4's codec, this is a wiring problem and a register would add latency without adding anything.

Enables. Three alignment flags, one per granularity, for a driver to test cheaply.

Timing. Combinational from the address; the consumer registers what it needs.

Synthesis. Three subtractors and three comparators. Tens of gates.

Limitations. One geometry, fixed at elaboration. A device that reports its own geometry — most modern parts do, through a discoverable parameter table — needs the sizes as inputs, which turns each fixed mask into a variable one and each shift into a barrel shifter.

The off-by-one worth naming. The distance to a boundary is the size minus the offset, and because the offset is strictly less than the size, that distance is never zero. An address sitting exactly on a page boundary has a full page ahead of it, not none. Reporting zero there makes a splitter emit a zero-length burst and stall — the failure Chapter 9.3's termination property exists to catch.

Azvya Education Pvt. Ltd.VLSI Mentor
flash_geometry.sv — one address, three granularities
// flash_geometry.sv
//
// Chapter 11.1 -- the address decomposition a serial flash imposes.
//
// A flash is not a flat array. One address sits simultaneously inside a
// PAGE (the program granularity), a SECTOR (the smallest erase) and a
// BLOCK (the large erase), and each of those has its own boundary that
// operations may not cross. This block decomposes an address into all
// three and reports the distance to each boundary.
//
// The two properties that make this the architecture rather than just
// arithmetic are NESTING properties, and both are asserted by the
// testbench over the whole address space:
//
//   * the distances nest -- to_page_end <= to_sector_end <= to_block_end,
//     always, because a page lies wholly inside a sector and a sector
//     wholly inside a block;
//   * alignment nests the other way -- block-aligned implies
//     sector-aligned implies page-aligned.
//
// A geometry in which either fails is not a flash geometry, and a driver
// written against one that does not nest will eventually cross a boundary
// it believed it was inside.
//
// Everything is masks and shifts. A modulo against a power of two is a
// mask, and page, sector and block sizes are always powers of two -- so
// there is no divider anywhere in this block.

module flash_geometry #(
    parameter int ADDR_W      = 24,
    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,

    // Which page, sector and block the address falls in.
    output logic [ADDR_W-1:0] page_index,
    output logic [ADDR_W-1:0] sector_index,
    output logic [ADDR_W-1:0] block_index,

    // Where inside each it falls.
    output logic [ADDR_W-1:0] page_offset,
    output logic [ADDR_W-1:0] sector_offset,
    output logic [ADDR_W-1:0] block_offset,

    // How many bytes remain before each boundary. These are what a
    // transfer must be clipped against, and they nest.
    output logic [ADDR_W-1:0] to_page_end,
    output logic [ADDR_W-1:0] to_sector_end,
    output logic [ADDR_W-1:0] to_block_end,

    output logic              page_aligned,
    output logic              sector_aligned,
    output logic              block_aligned
);

    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 [ADDR_W-1:0] PAGE_SIZE   = ADDR_W'(1) << PAGE_BITS;
    localparam logic [ADDR_W-1:0] SECTOR_SIZE = ADDR_W'(1) << SECTOR_BITS;
    localparam logic [ADDR_W-1:0] BLOCK_SIZE  = ADDR_W'(1) << BLOCK_BITS;

    assign page_offset   = addr & PAGE_MASK;
    assign sector_offset = addr & SECTOR_MASK;
    assign block_offset  = addr & BLOCK_MASK;

    assign page_index   = addr >> PAGE_BITS;
    assign sector_index = addr >> SECTOR_BITS;
    assign block_index  = addr >> BLOCK_BITS;

    // Distance to a boundary is the size minus the offset. Because the
    // offset is strictly less than the size, this is never zero -- an
    // address exactly on a boundary has a FULL unit ahead of it, not none.
    // Reporting zero there is the classic off-by-one, and it makes a
    // splitter emit a zero-length burst and stall.
    assign to_page_end   = PAGE_SIZE   - page_offset;
    assign to_sector_end = SECTOR_SIZE - sector_offset;
    assign to_block_end  = BLOCK_SIZE  - block_offset;

    assign page_aligned   = (page_offset   == {ADDR_W{1'b0}});
    assign sector_aligned = (sector_offset == {ADDR_W{1'b0}});
    assign block_aligned  = (block_offset  == {ADDR_W{1'b0}});

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
flash_geometry_tb.sv — the nesting invariants, swept
// flash_geometry_tb.sv
//
// Directed cases at every boundary, then the two NESTING invariants swept
// across a wide span of the address space. The invariants are what make
// this a test of the architecture rather than of the arithmetic.

`timescale 1ns/1ps

module flash_geometry_tb;

    localparam int ADDR_W      = 24;
    localparam int PAGE_BITS   = 8;
    localparam int SECTOR_BITS = 12;
    localparam int BLOCK_BITS  = 16;

    logic [ADDR_W-1:0] addr = {ADDR_W{1'b0}};

    logic [ADDR_W-1:0] page_index, sector_index, block_index;
    logic [ADDR_W-1:0] page_offset, sector_offset, block_offset;
    logic [ADDR_W-1:0] to_page_end, to_sector_end, to_block_end;
    logic              page_aligned, sector_aligned, block_aligned;

    int errors = 0;

    flash_geometry #(
        .ADDR_W(ADDR_W), .PAGE_BITS(PAGE_BITS),
        .SECTOR_BITS(SECTOR_BITS), .BLOCK_BITS(BLOCK_BITS)
    ) dut (
        .addr(addr),
        .page_index(page_index), .sector_index(sector_index),
        .block_index(block_index),
        .page_offset(page_offset), .sector_offset(sector_offset),
        .block_offset(block_offset),
        .to_page_end(to_page_end), .to_sector_end(to_sector_end),
        .to_block_end(to_block_end),
        .page_aligned(page_aligned), .sector_aligned(sector_aligned),
        .block_aligned(block_aligned)
    );

    task automatic chk(input string what, input int got, input int want);
        begin
            if (got != want) begin
                $display("  FAIL: at 0x%06h, %s = %0d, expected %0d",
                         addr, what, got, want);
                errors++;
            end
        end
    endtask

    task automatic at(input logic [ADDR_W-1:0] a);
        begin
            addr = a;
            #1;
        end
    endtask

    initial begin
        // 1. Address zero: the start of everything. Every distance is a
        //    full unit, not zero -- the off-by-one this guards against.
        at(24'h000000);
        chk("to_page_end",   to_page_end,   256);
        chk("to_sector_end", to_sector_end, 4096);
        chk("to_block_end",  to_block_end,  65536);
        if (!page_aligned || !sector_aligned || !block_aligned) begin
            $display("  FAIL: address 0 is not reported aligned to everything");
            errors++;
        end
        $display("  0x000000: page=%0d sector=%0d block=%0d  to_end = %0d / %0d / %0d",
                 page_index, sector_index, block_index,
                 to_page_end, to_sector_end, to_block_end);

        // 2. The last byte of the first page.
        at(24'h0000FF);
        chk("page_offset", page_offset, 255);
        chk("to_page_end", to_page_end, 1);
        chk("page_index",  page_index,  0);
        if (page_aligned) begin
            $display("  FAIL: 0xFF reported page-aligned"); errors++;
        end

        // 3. The first byte of the second page.
        at(24'h000100);
        chk("page_index",   page_index,   1);
        chk("to_page_end",  to_page_end,  256);
        chk("sector_index", sector_index, 0);
        if (!page_aligned || sector_aligned) begin
            $display("  FAIL: 0x100 alignment wrong (page=%0b sector=%0b)",
                     page_aligned, sector_aligned);
            errors++;
        end

        // 4. The last byte of the first sector, and the first of the next.
        at(24'h000FFF);
        chk("to_sector_end", to_sector_end, 1);
        chk("to_page_end",   to_page_end,   1);
        at(24'h001000);
        chk("sector_index",  sector_index,  1);
        chk("to_sector_end", to_sector_end, 4096);
        if (!sector_aligned || !page_aligned) begin
            $display("  FAIL: a sector boundary must also be a page boundary");
            errors++;
        end

        // 5. The block boundary.
        at(24'h00FFFF);
        chk("to_block_end", to_block_end, 1);
        at(24'h010000);
        chk("block_index",   block_index,   1);
        chk("sector_index",  sector_index,  16);
        chk("page_index",    page_index,    256);
        chk("to_block_end",  to_block_end,  65536);
        if (!block_aligned || !sector_aligned || !page_aligned) begin
            $display("  FAIL: a block boundary must be aligned to everything");
            errors++;
        end
        $display("  0x010000: page=%0d sector=%0d block=%0d  all three aligned",
                 page_index, sector_index, block_index);

        // 6. A mid-range address with nothing special about it.
        at(24'h0123AB);
        chk("page_offset",   page_offset,   24'hAB);
        chk("to_page_end",   to_page_end,   256 - 24'hAB);
        chk("sector_offset", sector_offset, 24'h3AB);
        chk("to_sector_end", to_sector_end, 4096 - 24'h3AB);
        $display("  0x0123AB: offsets = %0d / %0d / %0d", page_offset,
                 sector_offset, block_offset);

        // 7. NESTING INVARIANT ONE -- the distances nest. A page lies
        //    wholly inside a sector and a sector wholly inside a block, so
        //    the distance to the nearer boundary is never the larger.
        for (int i = 0; i < 20000; i++) begin
            at(ADDR_W'(i * 37));       // a stride coprime with every size
            if (!(to_page_end <= to_sector_end &&
                  to_sector_end <= to_block_end)) begin
                $display("  FAIL: distances do not nest at 0x%06h (%0d, %0d, %0d)",
                         addr, to_page_end, to_sector_end, to_block_end);
                errors++;
            end
        end

        // 8. NESTING INVARIANT TWO -- alignment nests the other way.
        for (int i = 0; i < 20000; i++) begin
            at(ADDR_W'(i * 16));       // a stride that hits real boundaries
            if (block_aligned && !(sector_aligned && page_aligned)) begin
                $display("  FAIL: block-aligned but not sector/page aligned at 0x%06h", addr);
                errors++;
            end
            if (sector_aligned && !page_aligned) begin
                $display("  FAIL: sector-aligned but not page-aligned at 0x%06h", addr);
                errors++;
            end
        end

        // 9. No distance is ever zero, and none ever exceeds its unit.
        for (int i = 0; i < 20000; i++) begin
            at(ADDR_W'(i * 53));
            if (to_page_end == 0 || to_page_end > 256 ||
                to_sector_end == 0 || to_sector_end > 4096 ||
                to_block_end == 0 || to_block_end > 65536) begin
                $display("  FAIL: a distance is out of range at 0x%06h", addr);
                errors++;
            end
        end
        $display("  60000 addresses swept: distances nest, alignment nests, no distance is zero");

        if (errors == 0)
            $display("PASS: every boundary distance and index is exact, an address on a boundary has a full unit ahead of it rather than none, the three distances always nest, and alignment to a larger unit always implies alignment to the smaller");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule

The directed cases check every boundary, but the two sweeps are what make this a test of the architecture rather than of the arithmetic.

The first asserts that the distances nest — to_page_end ≤ to_sector_end ≤ to_block_end — at twenty thousand addresses on a stride coprime with every unit size, so the sweep lands at every offset within every unit. The second asserts that alignment nests the other way: block-aligned implies sector- and page-aligned. A third confirms no distance is ever zero or larger than its own unit.

Those three properties are close to a complete specification, and they hold for any legal flash geometry rather than for the particular numbers chosen. A parameter set that violated them would not describe a flash.

Azvya Education Pvt. Ltd.VLSI Mentor
flash_geometry.v — the same decomposition in Verilog-2001
// flash_geometry.v
//
// Chapter 11.1 -- the address decomposition a serial flash imposes, in
// Verilog-2001.
//
// One address sits simultaneously inside a PAGE (the program
// granularity), a SECTOR (the smallest erase) and a BLOCK (the large
// erase). This block decomposes it into all three and reports the
// distance to each boundary.
//
// The two properties that make this the architecture rather than just
// arithmetic are NESTING properties: the distances nest, because a page
// lies wholly inside a sector and a sector inside a block; and alignment
// nests the other way, so block-aligned implies sector- and page-aligned.
//
// Everything is masks and shifts. Page, sector and block sizes are always
// powers of two, so a modulo is a mask and there is no divider here.

module flash_geometry #(
    parameter ADDR_W      = 24,
    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,

    output wire [ADDR_W-1:0] page_index,
    output wire [ADDR_W-1:0] sector_index,
    output wire [ADDR_W-1:0] block_index,

    output wire [ADDR_W-1:0] page_offset,
    output wire [ADDR_W-1:0] sector_offset,
    output wire [ADDR_W-1:0] block_offset,

    output wire [ADDR_W-1:0] to_page_end,
    output wire [ADDR_W-1:0] to_sector_end,
    output wire [ADDR_W-1:0] to_block_end,

    output wire              page_aligned,
    output wire              sector_aligned,
    output wire              block_aligned
);

    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 [ADDR_W-1:0] PAGE_SIZE   = (1 << PAGE_BITS);
    localparam [ADDR_W-1:0] SECTOR_SIZE = (1 << SECTOR_BITS);
    localparam [ADDR_W-1:0] BLOCK_SIZE  = (1 << BLOCK_BITS);

    assign page_offset   = addr & PAGE_MASK;
    assign sector_offset = addr & SECTOR_MASK;
    assign block_offset  = addr & BLOCK_MASK;

    assign page_index   = addr >> PAGE_BITS;
    assign sector_index = addr >> SECTOR_BITS;
    assign block_index  = addr >> BLOCK_BITS;

    // Distance to a boundary is the size minus the offset. Because the
    // offset is strictly less than the size this is never zero -- an
    // address exactly on a boundary has a FULL unit ahead of it, not none.
    // Reporting zero there makes a splitter emit a zero-length burst and
    // stall.
    assign to_page_end   = PAGE_SIZE   - page_offset;
    assign to_sector_end = SECTOR_SIZE - sector_offset;
    assign to_block_end  = BLOCK_SIZE  - block_offset;

    assign page_aligned   = (page_offset   == {ADDR_W{1'b0}});
    assign sector_aligned = (sector_offset == {ADDR_W{1'b0}});
    assign block_aligned  = (block_offset  == {ADDR_W{1'b0}});

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
flash_geometry_tb.v — the same sweeps in Verilog-2001
// flash_geometry_tb.v
//
// The same checks as the SystemVerilog testbench: directed cases at every
// boundary, then the two nesting invariants swept across the address
// space.

`timescale 1ns/1ps

module flash_geometry_tb;

    parameter ADDR_W      = 24;
    parameter PAGE_BITS   = 8;
    parameter SECTOR_BITS = 12;
    parameter BLOCK_BITS  = 16;

    reg [ADDR_W-1:0] addr;

    wire [ADDR_W-1:0] page_index, sector_index, block_index;
    wire [ADDR_W-1:0] page_offset, sector_offset, block_offset;
    wire [ADDR_W-1:0] to_page_end, to_sector_end, to_block_end;
    wire              page_aligned, sector_aligned, block_aligned;

    integer errors;
    integer i;

    initial begin
        addr = {ADDR_W{1'b0}};
        errors = 0;
    end

    flash_geometry #(
        .ADDR_W(ADDR_W), .PAGE_BITS(PAGE_BITS),
        .SECTOR_BITS(SECTOR_BITS), .BLOCK_BITS(BLOCK_BITS)
    ) dut (
        .addr(addr),
        .page_index(page_index), .sector_index(sector_index),
        .block_index(block_index),
        .page_offset(page_offset), .sector_offset(sector_offset),
        .block_offset(block_offset),
        .to_page_end(to_page_end), .to_sector_end(to_sector_end),
        .to_block_end(to_block_end),
        .page_aligned(page_aligned), .sector_aligned(sector_aligned),
        .block_aligned(block_aligned)
    );

    task chk;
        input [8*20:1] what;
        input integer  got;
        input integer  want;
        begin
            if (got != want) begin
                $display("  FAIL: at 0x%06h, %0s = %0d, expected %0d",
                         addr, what, got, want);
                errors = errors + 1;
            end
        end
    endtask

    task at;
        input [ADDR_W-1:0] a;
        begin
            addr = a;
            #1;
        end
    endtask

    initial begin
        #1;
        // 1. Address zero: every distance is a full unit, not zero.
        at(24'h000000);
        chk("to_page_end",   to_page_end,   256);
        chk("to_sector_end", to_sector_end, 4096);
        chk("to_block_end",  to_block_end,  65536);
        if (!page_aligned || !sector_aligned || !block_aligned) begin
            $display("  FAIL: address 0 is not reported aligned to everything");
            errors = errors + 1;
        end
        $display("  0x000000: page=%0d sector=%0d block=%0d  to_end = %0d / %0d / %0d",
                 page_index, sector_index, block_index,
                 to_page_end, to_sector_end, to_block_end);

        // 2. The last byte of the first page.
        at(24'h0000FF);
        chk("page_offset", page_offset, 255);
        chk("to_page_end", to_page_end, 1);
        chk("page_index",  page_index,  0);
        if (page_aligned) begin
            $display("  FAIL: 0xFF reported page-aligned"); errors = errors + 1;
        end

        // 3. The first byte of the second page.
        at(24'h000100);
        chk("page_index",   page_index,   1);
        chk("to_page_end",  to_page_end,  256);
        chk("sector_index", sector_index, 0);
        if (!page_aligned || sector_aligned) begin
            $display("  FAIL: 0x100 alignment wrong");
            errors = errors + 1;
        end

        // 4. The sector boundary.
        at(24'h000FFF);
        chk("to_sector_end", to_sector_end, 1);
        chk("to_page_end",   to_page_end,   1);
        at(24'h001000);
        chk("sector_index",  sector_index,  1);
        chk("to_sector_end", to_sector_end, 4096);
        if (!sector_aligned || !page_aligned) begin
            $display("  FAIL: a sector boundary must also be a page boundary");
            errors = errors + 1;
        end

        // 5. The block boundary.
        at(24'h00FFFF);
        chk("to_block_end", to_block_end, 1);
        at(24'h010000);
        chk("block_index",   block_index,   1);
        chk("sector_index",  sector_index,  16);
        chk("page_index",    page_index,    256);
        chk("to_block_end",  to_block_end,  65536);
        if (!block_aligned || !sector_aligned || !page_aligned) begin
            $display("  FAIL: a block boundary must be aligned to everything");
            errors = errors + 1;
        end
        $display("  0x010000: page=%0d sector=%0d block=%0d  all three aligned",
                 page_index, sector_index, block_index);

        // 6. A mid-range address with nothing special about it.
        at(24'h0123AB);
        chk("page_offset",   page_offset,   24'hAB);
        chk("to_page_end",   to_page_end,   256 - 24'hAB);
        chk("sector_offset", sector_offset, 24'h3AB);
        chk("to_sector_end", to_sector_end, 4096 - 24'h3AB);
        $display("  0x0123AB: offsets = %0d / %0d / %0d", page_offset,
                 sector_offset, block_offset);

        // 7. NESTING INVARIANT ONE -- the distances nest.
        for (i = 0; i < 20000; i = i + 1) begin
            at(i * 37);                // a stride coprime with every size
            if (!(to_page_end <= to_sector_end &&
                  to_sector_end <= to_block_end)) begin
                $display("  FAIL: distances do not nest at 0x%06h", addr);
                errors = errors + 1;
            end
        end

        // 8. NESTING INVARIANT TWO -- alignment nests the other way.
        for (i = 0; i < 20000; i = i + 1) begin
            at(i * 16);
            if (block_aligned && !(sector_aligned && page_aligned)) begin
                $display("  FAIL: block-aligned but not sector/page aligned at 0x%06h", addr);
                errors = errors + 1;
            end
            if (sector_aligned && !page_aligned) begin
                $display("  FAIL: sector-aligned but not page-aligned at 0x%06h", addr);
                errors = errors + 1;
            end
        end

        // 9. No distance is ever zero, and none ever exceeds its unit.
        for (i = 0; i < 20000; i = i + 1) begin
            at(i * 53);
            if (to_page_end == 0 || to_page_end > 256 ||
                to_sector_end == 0 || to_sector_end > 4096 ||
                to_block_end == 0 || to_block_end > 65536) begin
                $display("  FAIL: a distance is out of range at 0x%06h", addr);
                errors = errors + 1;
            end
        end
        $display("  60000 addresses swept: distances nest, alignment nests, no distance is zero");

        if (errors == 0)
            $display("PASS: every boundary distance and index is exact, an address on a boundary has a full unit ahead of it rather than none, the three distances always nest, and alignment to a larger unit always implies alignment to the smaller");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
flash_geometry.vhd — the same decomposition in VHDL
-- flash_geometry.vhd
--
-- Chapter 11.1 -- the address decomposition a serial flash imposes, in
-- VHDL.
--
-- One address sits simultaneously inside a PAGE (the program
-- granularity), a SECTOR (the smallest erase) and a BLOCK (the large
-- erase). This block decomposes it into all three and reports the
-- distance to each boundary.
--
-- The two properties that make this the architecture rather than just
-- arithmetic are NESTING properties: the distances nest, because a page
-- lies wholly inside a sector and a sector inside a block; and alignment
-- nests the other way, so block-aligned implies sector- and page-aligned.
--
-- Everything is masks and shifts. Page, sector and block sizes are always
-- powers of two, so a modulo is a mask and there is no divider here.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity flash_geometry is
    generic (
        ADDR_W      : positive := 24;
        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);

        page_index     : out unsigned(ADDR_W - 1 downto 0);
        sector_index   : out unsigned(ADDR_W - 1 downto 0);
        block_index    : out unsigned(ADDR_W - 1 downto 0);

        page_offset    : out unsigned(ADDR_W - 1 downto 0);
        sector_offset  : out unsigned(ADDR_W - 1 downto 0);
        block_offset   : out unsigned(ADDR_W - 1 downto 0);

        to_page_end    : out unsigned(ADDR_W - 1 downto 0);
        to_sector_end  : out unsigned(ADDR_W - 1 downto 0);
        to_block_end   : out unsigned(ADDR_W - 1 downto 0);

        page_aligned   : out std_logic;
        sector_aligned : out std_logic;
        block_aligned  : out std_logic
    );
end entity;

architecture rtl of flash_geometry is

    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   : unsigned(ADDR_W - 1 downto 0) :=
        to_unsigned(2 ** PAGE_BITS, ADDR_W);
    constant SECTOR_SIZE : unsigned(ADDR_W - 1 downto 0) :=
        to_unsigned(2 ** SECTOR_BITS, ADDR_W);
    constant BLOCK_SIZE  : unsigned(ADDR_W - 1 downto 0) :=
        to_unsigned(2 ** BLOCK_BITS, ADDR_W);

    -- Declaration initialisers keep the alignment comparisons below from
    -- testing 'U' at time zero. This block has no reset -- it is purely
    -- combinational -- so the initialiser is the only place the question
    -- can be answered.
    signal p_off : unsigned(ADDR_W - 1 downto 0) := (others => '0');
    signal s_off : unsigned(ADDR_W - 1 downto 0) := (others => '0');
    signal b_off : unsigned(ADDR_W - 1 downto 0) := (others => '0');

begin

    p_off <= addr and PAGE_MASK;
    s_off <= addr and SECTOR_MASK;
    b_off <= addr and BLOCK_MASK;

    page_offset   <= p_off;
    sector_offset <= s_off;
    block_offset  <= b_off;

    page_index   <= shift_right(addr, PAGE_BITS);
    sector_index <= shift_right(addr, SECTOR_BITS);
    block_index  <= shift_right(addr, BLOCK_BITS);

    -- Distance to a boundary is the size minus the offset. Because the
    -- offset is strictly less than the size this is never zero -- an
    -- address exactly on a boundary has a FULL unit ahead of it, not none.
    -- Reporting zero there makes a splitter emit a zero-length burst and
    -- stall.
    to_page_end   <= PAGE_SIZE   - p_off;
    to_sector_end <= SECTOR_SIZE - s_off;
    to_block_end  <= BLOCK_SIZE  - b_off;

    page_aligned   <= '1' when p_off = 0 else '0';
    sector_aligned <= '1' when s_off = 0 else '0';
    block_aligned  <= '1' when b_off = 0 else '0';

end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
flash_geometry_tb.vhd — the same sweeps in VHDL
-- flash_geometry_tb.vhd
--
-- The same checks as the SystemVerilog and Verilog testbenches: directed
-- cases at every boundary, then the two nesting invariants swept across
-- the address space.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity flash_geometry_tb is
end entity;

architecture sim of flash_geometry_tb is

    constant ADDR_W      : positive := 24;
    constant PAGE_BITS   : positive := 8;
    constant SECTOR_BITS : positive := 12;
    constant BLOCK_BITS  : positive := 16;

    signal addr : unsigned(ADDR_W - 1 downto 0) := (others => '0');

    signal page_index, sector_index, block_index    : unsigned(ADDR_W - 1 downto 0);
    signal page_offset, sector_offset, block_offset : unsigned(ADDR_W - 1 downto 0);
    signal to_page_end, to_sector_end, to_block_end : unsigned(ADDR_W - 1 downto 0);
    signal page_aligned, sector_aligned, block_aligned : std_logic;

    signal errors : natural := 0;

begin

    dut : entity work.flash_geometry
        generic map (ADDR_W => ADDR_W, PAGE_BITS => PAGE_BITS,
                     SECTOR_BITS => SECTOR_BITS, BLOCK_BITS => BLOCK_BITS)
        port map (
            addr => addr,
            page_index => page_index, sector_index => sector_index,
            block_index => block_index,
            page_offset => page_offset, sector_offset => sector_offset,
            block_offset => block_offset,
            to_page_end => to_page_end, to_sector_end => to_sector_end,
            to_block_end => to_block_end,
            page_aligned => page_aligned, sector_aligned => sector_aligned,
            block_aligned => block_aligned
        );

    stim : process
        variable errs : natural := 0;

        procedure at(a : natural) is
        begin
            addr <= to_unsigned(a, ADDR_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. Address zero: every distance is a full unit, not zero.
        at(16#000000#);
        chk("to_page_end",   to_integer(to_page_end),   256);
        chk("to_sector_end", to_integer(to_sector_end), 4096);
        chk("to_block_end",  to_integer(to_block_end),  65536);
        if page_aligned /= '1' or sector_aligned /= '1' or
           block_aligned /= '1' then
            report "  FAIL: address 0 is not reported aligned to everything";
            errs := errs + 1;
        end if;
        report "  0x000000: to_end = " &
               integer'image(to_integer(to_page_end)) & " / " &
               integer'image(to_integer(to_sector_end)) & " / " &
               integer'image(to_integer(to_block_end));

        -- 2. The last byte of the first page.
        at(16#0000FF#);
        chk("page_offset", to_integer(page_offset), 255);
        chk("to_page_end", to_integer(to_page_end), 1);
        chk("page_index",  to_integer(page_index),  0);
        if page_aligned = '1' then
            report "  FAIL: 0xFF reported page-aligned"; errs := errs + 1;
        end if;

        -- 3. The first byte of the second page.
        at(16#000100#);
        chk("page_index",   to_integer(page_index),   1);
        chk("to_page_end",  to_integer(to_page_end),  256);
        chk("sector_index", to_integer(sector_index), 0);
        if page_aligned /= '1' or sector_aligned = '1' then
            report "  FAIL: 0x100 alignment wrong"; errs := errs + 1;
        end if;

        -- 4. The sector boundary.
        at(16#000FFF#);
        chk("to_sector_end", to_integer(to_sector_end), 1);
        chk("to_page_end",   to_integer(to_page_end),   1);
        at(16#001000#);
        chk("sector_index",  to_integer(sector_index),  1);
        chk("to_sector_end", to_integer(to_sector_end), 4096);
        if sector_aligned /= '1' or page_aligned /= '1' then
            report "  FAIL: a sector boundary must also be a page boundary";
            errs := errs + 1;
        end if;

        -- 5. The block boundary.
        at(16#00FFFF#);
        chk("to_block_end", to_integer(to_block_end), 1);
        at(16#010000#);
        chk("block_index",   to_integer(block_index),   1);
        chk("sector_index",  to_integer(sector_index),  16);
        chk("page_index",    to_integer(page_index),    256);
        chk("to_block_end",  to_integer(to_block_end),  65536);
        if block_aligned /= '1' or sector_aligned /= '1' or
           page_aligned /= '1' then
            report "  FAIL: a block boundary must be aligned to everything";
            errs := errs + 1;
        end if;
        report "  0x010000: page=" & integer'image(to_integer(page_index)) &
               " sector=" & integer'image(to_integer(sector_index)) &
               " block=" & integer'image(to_integer(block_index)) &
               "  all three aligned";

        -- 6. A mid-range address with nothing special about it.
        at(16#0123AB#);
        chk("page_offset",   to_integer(page_offset),   16#AB#);
        chk("to_page_end",   to_integer(to_page_end),   256 - 16#AB#);
        chk("sector_offset", to_integer(sector_offset), 16#3AB#);
        chk("to_sector_end", to_integer(to_sector_end), 4096 - 16#3AB#);
        report "  0x0123AB: offsets = " &
               integer'image(to_integer(page_offset)) & " / " &
               integer'image(to_integer(sector_offset)) & " / " &
               integer'image(to_integer(block_offset));

        -- 7. NESTING INVARIANT ONE -- the distances nest.
        for i in 0 to 19999 loop
            at((i * 37) mod 2 ** ADDR_W);   -- a stride coprime with every size
            if not (to_page_end <= to_sector_end and
                    to_sector_end <= to_block_end) then
                report "  FAIL: distances do not nest"; errs := errs + 1;
            end if;
        end loop;

        -- 8. NESTING INVARIANT TWO -- alignment nests the other way.
        for i in 0 to 19999 loop
            at((i * 16) mod 2 ** ADDR_W);
            if block_aligned = '1' and
               not (sector_aligned = '1' and page_aligned = '1') then
                report "  FAIL: block-aligned but not sector/page aligned";
                errs := errs + 1;
            end if;
            if sector_aligned = '1' and page_aligned /= '1' then
                report "  FAIL: sector-aligned but not page-aligned";
                errs := errs + 1;
            end if;
        end loop;

        -- 9. No distance is ever zero, and none ever exceeds its unit.
        for i in 0 to 19999 loop
            at((i * 53) mod 2 ** ADDR_W);
            if to_page_end = 0 or to_integer(to_page_end) > 256 or
               to_sector_end = 0 or to_integer(to_sector_end) > 4096 or
               to_block_end = 0 or to_integer(to_block_end) > 65536 then
                report "  FAIL: a distance is out of range"; errs := errs + 1;
            end if;
        end loop;
        report "  60000 addresses swept: distances nest, alignment nests, no distance is zero";

        errors <= errs;
        if errs = 0 then
            report "PASS: every boundary distance and index is exact, an address on a boundary has a full unit ahead of it rather than none, the three distances always nest, and alignment to a larger unit always implies alignment to the smaller";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;
        wait;
    end process;

end architecture;

Parity

All three implement the same decoder: identical ports and generics, purely combinational, masks and shifts with no divider, a boundary distance that is never zero, and three alignment flags. All three testbenches check the same boundaries and run the same three sweeps over sixty thousand addresses, reporting identical values throughout — a 256-byte distance at address zero, a distance of 1 at 0x0000FF, and every alignment flag set at 0x010000.

7. Why a Verification Engineer Cares

Azvya Education Pvt. Ltd.VLSI Mentor
flash_geometry.sva — the architecture as properties
   // 1. NESTING OF DISTANCES. A page lies wholly inside a sector and a
   //    sector wholly inside a block, so the distance to the nearer
   //    boundary is never the larger. This is the structural guarantee a
   //    driver relies on when it clips a transfer to one comparison.
   a_distances_nest : assert property (
       @(posedge clk)
           (to_page_end <= to_sector_end) && (to_sector_end <= to_block_end))
       else $error("the boundary distances do not nest");

   // 2. NESTING OF ALIGNMENT, the other way round. A block-aligned buffer
   //    is aligned for every operation.
   a_alignment_nests : assert property (
       @(posedge clk)
           (block_aligned |-> (sector_aligned && page_aligned)) &&
           (sector_aligned |-> page_aligned))
       else $error("alignment to a larger unit did not imply the smaller");

   // 3. NO ZERO DISTANCE. An address ON a boundary has a full unit ahead
   //    of it. Reporting zero makes a splitter emit a zero-length burst
   //    and stall -- a hang rather than a wrong answer.
   a_never_zero : assert property (
       @(posedge clk)
           (to_page_end != 0) && (to_sector_end != 0) && (to_block_end != 0))
       else $error("a boundary distance of zero was reported");

   // 4. AND NEVER MORE THAN THE UNIT. The other end of the same check --
   //    a distance larger than the unit means the mask is wrong.
   a_within_unit : assert property (
       @(posedge clk)
           (to_page_end   <= (1 << PAGE_BITS))   &&
           (to_sector_end <= (1 << SECTOR_BITS)) &&
           (to_block_end  <= (1 << BLOCK_BITS)))
       else $error("a boundary distance exceeded its own unit");

   // 5. RECONSTRUCTION. Index times size plus offset returns the address.
   //    This catches a shift and a mask that disagree about the boundary,
   //    which no individual output check would notice.
   a_reconstructs : assert property (
       @(posedge clk)
           (addr == ((page_index << PAGE_BITS) | page_offset)))
       else $error("the index and offset do not reconstruct the address");

Property 5 is the one to copy into any decomposition design. When a design splits a value into parts, the reconstruction is the specification — and it catches the class of error where two outputs are each individually plausible and mutually inconsistent, which per-output checks structurally cannot.

Properties 1 and 2 are worth stating because they are invariants of the geometry, not of the implementation. They hold for every real flash, so asserting them turns a wrong parameter set into an immediate failure rather than a subtly wrong driver.

Coverage should target position within each unit, not raw addresses:

Azvya Education Pvt. Ltd.VLSI Mentor
flash_geometry_cg.sv — offsets, not addresses
   covergroup flash_geometry_cg @(posedge clk);
       // Position within the page is what every clipping decision turns
       // on. Random 24-bit addresses hit the interesting offsets almost
       // never -- offset 0 and offset 255 are one part in 256 each.
       cp_page_off : coverpoint page_offset {
           bins at_start = {0};                 // full unit ahead
           bins early    = {[1:63]};
           bins middle   = {[64:191]};
           bins late     = {[192:254]};
           bins at_end   = {255};               // one byte ahead
       }

       cp_sector_off : coverpoint sector_offset {
           bins at_start = {0};
           bins mid      = {[1:4094]};
           bins at_end   = {4095};
       }

       // The alignment combinations. Only four of the eight are reachable,
       // and a coverage report showing five would itself be the bug.
       cp_align : coverpoint {block_aligned, sector_aligned, page_aligned} {
           bins none        = {3'b000};
           bins page_only   = {3'b001};
           bins page_sector = {3'b011};
           bins all_three   = {3'b111};
           illegal_bins impossible = {3'b010, 3'b100, 3'b101, 3'b110};
       }
   endgroup

The illegal_bins in cp_align are the interesting part. Four of the eight combinations are structurally impossible given the nesting, and declaring them illegal turns a violated invariant into a coverage failure as well as an assertion failure — two independent detectors for one property.

8. Why an FPGA or ASIC Engineer Cares

Decompose once, in one place. Page, sector and block arithmetic appears in the splitter, the erase planner and the wear-levelling layer. Three implementations of the same masks will eventually disagree, and the one that disagrees will be the one that erases the wrong sector.

Never write a modulo against an address. Every flash unit is a power of two, so a mask gives the same answer for free. addr % 4096 and addr & 12'hFFF are the same value and very different hardware.

Report the distance, not the boundary. A consumer that receives "the next boundary is at 0x1000" must subtract; one that receives "4096 bytes remain" can clip immediately. The subtract belongs here, once.

Expose alignment flags. Three bits let a driver test a buffer's suitability without arithmetic, and they make the nesting property visible in a register dump.

If the part reports its own geometry, read it. Modern serial flash carries a discoverable parameter table giving page, sector and block sizes. A controller that reads it instead of assuming 256/4K/64K supports parts it was never built for — at the cost of turning fixed masks into variable ones.

9. Failure Signature — A Log File That Destroys Its Own Flash

Symptom. A product keeps a small event log in flash. It works. Eighteen months into deployment, units begin returning with a flash that reports success on every write and reads back stale data — the log stops advancing, but nothing reports an error.

What "writes succeed but data is stale" establishes. The interface is fine, the driver is issuing well-formed operations, and the device is acknowledging them. So the cells have stopped accepting programs, which is wear.

Plausible mechanisms.

  • Read-modify-write on a fixed location. If the log keeps a head pointer at a fixed address and updates it per event, every update erases that pointer's sector. At 100 000 cycles, a once-per-minute event exhausts it in about two months of continuous operation.
  • A log that rewrites a header on each append, which has the same effect for the same reason.
  • A sector used as scratch by something else, sharing the wear budget.
  • No wear levelling across an otherwise correct append-only design, so one sector takes every erase.
  • A power-loss recovery path that erases and rewrites on every boot — harmless at one boot a day, fatal on a device that resets frequently.

The discriminating observation. Read the flash's own erase counters if the part provides them; most do not. Failing that, the decisive test is where the staleness is. If reads from the log's data area are fine and only the pointer or header is stale, the fixed-location update is confirmed — the worn sector is precisely the one being erased repeatedly.

Then count: erases per event times events per day times days deployed. If that number is within an order of magnitude of 100 000, the mechanism is wear and no amount of driver debugging will help.

The fix, and why it must be a redesign. Append-only. Writing new records into erased space requires no erase at all, because programming only clears bits and erased flash is all ones. The head position is then found by scanning for the first erased record rather than stored anywhere. An erase happens once per sector filled, not once per event — which for the numbers above is a reduction of four orders of magnitude.

Why this reaches deployment. Because the wear limit is invisible in testing. A test suite performs thousands of writes, not hundreds of thousands, and the flash behaves perfectly throughout. The failure arrives only after a duration no test runs for — and by then it is in the field, in hardware, in a form that a firmware update cannot repair because the cells are gone.

10. Common Misconceptions

11. Reason It Through

Work this before reading the answer.

A driver must write 600 bytes to address 0x001F80 on a flash with 256-byte pages, 4 KB sectors and 64 KB blocks. The region has already been erased.

How many operations does it take, and what are their addresses and lengths? Does any sector or block boundary matter here?

Start with the page offset. 0x001F80 modulo 256 is 0x80, so the address is 128 bytes into a page. The distance to the next page boundary is 256 − 128 = 128.

First program: 128 bytes at 0x001F80. It fills the page to its end at 0x002000.

Then whole pages. 600 − 128 = 472 bytes remain, starting at a page boundary:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   program 1:  0x001F80  128 bytes   (fills the partial page)
   program 2:  0x002000  256 bytes   (a whole page)
   program 3:  0x002100  216 bytes   (the remainder)

Three programs, and the lengths sum to 600 — which is the conservation property from Chapter 9.3 and the first thing to check.

Does a sector boundary matter? 0x002000 is 8192, which is 2 × 4096 — so the transfer does cross a sector boundary, right at the start of program 2.

And here is the point: it does not matter. Sector boundaries constrain erase, not program. A program is limited by the page only. So crossing a sector boundary mid-transfer is harmless, and a driver that split at sector boundaries would issue more operations than necessary for no benefit.

Does a block boundary matter? 0x010000 is the first block boundary and the transfer ends at 0x0021D8, far short of it. No.

What if the region had NOT been erased? Then the answer changes completely, and this is the more important half. Programming only clears bits, so writing into non-erased space produces the bitwise AND of the old and new contents — not an error, and not the new data. Because the transfer spans two sectors, both would need erasing, and each erase destroys 4 KB:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   sector 1 (0x001000-0x001FFF)   read 4 KB, erase, reprogram 16 pages
   sector 2 (0x002000-0x002FFF)   read 4 KB, erase, reprogram 16 pages

So 600 bytes into erased space is three operations, and the same 600 bytes into used space is two sector reads, two erases and thirty-two programs. That ratio is the entire reason flash is used append-only wherever possible.

The general lesson. Know which boundary constrains which operation — page for program, sector or block for erase — and know whether the target is erased, because that single fact changes the cost by a factor of ten.

12. Understanding Check

13. Summary

A serial flash is a simple interface over a device that is not simple, and three properties account for it.

A program can only clear bits. Restoring a 1 requires an erase of a whole region, which is the root of everything else.

The granularities differ and NEST: pages of 256 bytes for programming, sectors of 4 KB and blocks of 64 KB for erasing, each lying wholly inside the next. The distances nest one way and alignment the other, and those two guarantees are what let a driver clip a transfer with a single comparison.

Operations take microseconds to seconds, during which the device answers nothing but a status read.

The command set is about a dozen opcodes in four shapes, and they are unrelated values rather than a bit field — a controller needs a table. Five of them modify the device and every one requires a separate write-enable.

Putting the asymmetry and the granularities together gives read-modify-write: changing one byte costs 4 KB of RAM, one erase and sixteen programs. Avoiding it is what append-only structures, wear levelling and flash file systems are all for — and a loop that does not avoid it can exhaust a sector's 100 000 erase cycles in a day.

In hardware the geometry is a combinational decomposition — masks and shifts, no divider — whose boundary distance is never zero, and whose specification is three invariants: the distances nest, alignment nests the other way, and index times size plus offset reconstructs the address.

14. What Comes Next

Reads are the one operation with no constraints at all — and the one with two commands that do the same thing.

Chapter 11.2 — Normal Read, Fast Read, and Dummy Cycles asks why a device offers both, why the faster command must insert latency cycles the slower one does not, and — the part almost everyone gets wrong — why fast read is not always faster. It ends with the selector that chooses between them by transfer time rather than clock rate, in all three HDLs.

Continue learning