Skip to content
VLSI Mentor

I²C · Module 16

Page Writes and Write-Cycle Boundaries

Reads walk the whole array; writes walk a page. Works out why a write burst that runs off the end of a page wraps back to the start of the same page and destroys the bytes it just sent, why the commit happens at the STOP rather than per byte, and why the device then stops answering entirely.

Chapter 16.2 established that a sequential read walks the whole array and rolls over at the end of it. A write burst does not work that way, and the difference is the most surprising asymmetry in the device.

A write does not walk the array. It walks a page — and when it reaches the end of that page it does not continue into the next one. It goes back to the start of the same page and begins overwriting the bytes it just sent.

Sixteen bytes into a sixteen-byte page: perfect. The same sixteen bytes starting one location later: twelve of them destroy four of the others. Every byte acknowledged, the STOP accepted, the write cycle completed normally, and nothing anywhere says a thing went wrong.

This chapter works out exactly why, computes what actually lands where, and then deals with the second half of a page write — the internal write cycle, during which the device stops answering the bus at all.

1. Where the Page Comes From

The reason for the page is physical, and knowing it makes the behaviour follow.

An EEPROM cell is not written by a bus transaction. It is written by a comparatively slow, high-voltage internal operation that programs a whole row of the array at once. Doing that per byte would be both slow and destructive — the array has a finite number of write cycles, and a row rewritten once per byte wears out sixteen times faster than a row rewritten once.

So the device buffers. Bytes arriving on the bus land in a small RAM — the page buffer — and nothing reaches the array until the transaction ends. The buffer is exactly one row wide, because a row is what the internal operation can program in one go.

And that is where the wrap comes from. The buffer has PAGE_SIZE slots and the counter that indexes it is log2(PAGE_SIZE) bits wide. There are no higher bits to carry into.

2. What Actually Lands Where

The formula is easy to nod at and easy to get wrong, so here is a case worked all the way through. A 16-byte page, a write starting at word address 0x08, and a master that sends twenty bytes.

First, the page. The frozen high bits are 0x08 & ~15 = 0x00, so the page containing 0x08 is 0x00..0x0F. Note that the write starts in the middle of it — the page boundary is a property of the array, not of where the write began.

Azvya Education Pvt. Ltd.VLSI Mentor
twenty bytes written from 0x08, PAGE_SIZE = 16 — computed, not assumed
   bytes  0..7   ->  0x08..0x0F     the rest of the page, as the master intended
   bytes  8..15  ->  0x00..0x07     WRAPPED to the start of the SAME page
   bytes 16..19  ->  0x08..0x0B     over bytes 0..3, which are now lost

   net contents of the page after the STOP:

     0x00..0x07   hold bytes  8..15
     0x08..0x0B   hold bytes 16..19    <- written twice; bytes 0..3 are gone
     0x0C..0x0F   hold bytes  4..7

Read that result carefully, because three things in it are counter-intuitive.

Every address in the page was written, and none outside it was touched. The twelve bytes past the page boundary did not extend the write into 0x10 and above. They destroyed the first four bytes of it.

Four locations were written twice. 0x08..0x0B received bytes 0..3 and then bytes 16..19. The master sent twenty bytes and sixteen survive, but not the last sixteen and not the first sixteen — a mixture, ordered by offset rather than by time.

The counter wrapped exactly once. It passed the top offset a single time, at byte 8. But the overwriting continued for as long as the master kept sending. A wrap event and an overwritten location are not the same count, and a device that reported one when it meant the other would be lying in a way that is hard to notice.

3. The Commit, and the Silence That Follows

The second half of a page write is the part that surprises drivers.

Nothing reaches the array until the STOP. The bytes are in the buffer; the array is untouched. A read issued in the middle of a write burst — which would require a repeated START, abandoning the write — returns the array's old contents, because the write has not happened.

At the STOP, the whole buffer is committed at once. One internal operation programs the row. Only the offsets this transfer actually wrote are programmed, which matters more than it looks: the buffer's data half is not cleared between transfers, only its per-byte valid bits. A device that committed the whole buffer would write the previous transfer's leftovers into the offsets this one never touched. §7 has the mutation.

And then the device stops answering. For the duration of the internal write cycle — the datasheet's tWR, typically a few milliseconds — it will not acknowledge anything at all. Not its own address. Not a read. Nothing.

That silence is not an error condition and there is no status bit for it. The device is unreachable, and the only way a master can find out when it is reachable again is to keep asking. Chapter 16.4 is entirely about that, and about what makes it harder than it sounds.

A STOP that delivered no data commits nothing and starts no write cycle. A pointer write followed immediately by a STOP — the first half of a random read, abandoned — must leave the device available. A device that began a multi-millisecond write cycle every time a master set a pointer would be unusable.

4. The Page Write, Drawn

A sequence diagram with three actors: master, the EEPROM's bus interface, and the EEPROM's internal array. The master sends a START and the address byte with write direction; the bus interface acknowledges. The master sends the word address 0x20, acknowledged, which freezes the page base. The master then sends four data bytes, each acknowledged, and each one goes into the page buffer rather than the array. The master sends a STOP. Only then does the bus interface commit the four buffered bytes to the internal array as a single operation, and the device goes busy. A subsequent addressing from the master is not acknowledged, because the device is in its write cycle. After the cycle completes, the device acknowledges again.Write four bytes from 0x20, then the internal write cycleMasterBus interfaceArrayS, addr 0x50 WACK0x20 — word addrACK — page basefrozen4 data bytesACK on eachP — STOPcommit all 4 at onceS, addr 0x50 W — apollno ACK — busywrite cycle doneS, addr 0x50 W — apollACK — availableagain
A page write and the write cycle that follows it. The bytes go to the buffer, the commit happens at the STOP, and the device then declines every addressing until the internal operation finishes.

Note the third actor. The array is a separate participant, and every interesting property in this chapter lives in the gap between it and the bus interface: the buffering, the commit, the write cycle, the silence.

5. The Wrap, at Byte Resolution

Ten bytes into an eight-byte page: the ninth byte is the one that destroys something

10 cycles
A waveform with ten intervals at byte resolution showing a write burst into an eight-byte page. The first interval carries the word address, which freezes the page base at zero. The next eight intervals carry data bytes landing at offsets 0 through 7. The counter then returns to offset 0 rather than advancing to offset 8. The tenth interval carries the ninth data byte, which lands at offset 0 on top of the first byte, and the wrap flag rises at that interval rather than at the counter's return. A row shows the offset sequence and another shows whether the byte landed on a fresh or an already-used offset.one full page, no damageone full page, no damageoverwritingoverwrit…every offset used once: nothing lostevery offset used once:nothing lostbyte 9 lands on offset 0: THIS is the wrapbyte 9 lands on offset 0:THIS is the wrapbyteaddrd0d1d2d3d4d5d6d7d8offset0012345670lands0newnewnewnewnewnewnewnewUSEDwrapackAAAAAAAAAAt0t1t2t3t4t5t6t7t8t9
A ten-byte write into an eight-byte page starting at the page base. The counter returns to offset zero after byte eight, and the ninth byte — the one that lands on an offset already used — is where the wrap is flagged.

The acknowledge row is constant, and that is the point of including it. Every byte is acknowledged, including the one that destroys data. There is no mechanism by which the device could refuse it — the buffer has a slot for it, the transfer is well formed, and "you have sent more bytes than fit in a page" is not something the protocol can express.

Eight bytes into an eight-byte page is clean. The counter returns to offset zero afterwards, and reporting that as a wrap would report a corruption that did not happen. §2's callout, made visual: the wrap row rises at interval 9, not at interval 8.

6. The Page Buffer in Three Languages

A page-buffered write engine with a parameterised page size, commit-at-STOP, a modelled write cycle, and refusal counting — its independent oracle, and both in all three languages.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_page_buffer.sv — the frozen page base, the modular offset, and commit-at-STOP
   // -----------------------------------------------------------------------------
   // i2c_page_buffer.sv
   // EEPROM page buffer and write-cycle behaviour.
   //
   // Chapter 16.2 showed a write counter that advances through the array and rolls
   // over at the top. A real EEPROM does NOT commit each byte as it arrives: the
   // bytes land in a page buffer, and the whole page is committed at the STOP. That
   // one implementation detail produces the most-reported EEPROM bug there is.
   //
   // THE MECHANISM. A page of size P starting at base B covers B..B+P-1. The device
   // latches the HIGH bits of the word address when the write begins and increments
   // only the low log2(P) bits as bytes arrive:
   //
   //     address for byte i  =  (word_addr & ~(P-1)) | ((word_addr + i) & (P-1))
   //
   // So the low bits are a modular counter and the high bits never move. Writing past
   // the end of the page therefore WRAPS to the start of the SAME page and overwrites
   // what was already put there -- it does not continue into the next page.
   //
   // Worked through, P = 16, word address 0x08, 20 bytes. The page is 0x00..0x0F,
   // because 0x08 & ~15 = 0x00:
   //     bytes  0..7   -> 0x08..0x0F
   //     bytes  8..15  -> 0x00..0x07   (wrapped)
   //     bytes 16..19  -> 0x08..0x0B   (over bytes 0..3, which are lost)
   // Four locations are written twice and bytes 0..3 are lost. Note that the counter
   // wraps ONCE in those twenty bytes -- it passes the top offset a single time -- but
   // the overwriting continues for as long as the master keeps sending.
   //
   // WHY IT IS SILENT. Every byte is acknowledged, because the device is happily
   // storing them. The STOP is accepted. The write cycle completes normally. There is
   // no protocol error at any point, and the only evidence is that the data is wrong.
   // A master cannot detect this; it has to know the page size and not exceed it.
   //
   // THE WRITE CYCLE. After the STOP the device becomes busy committing the page and
   // answers nothing at all -- not its address, not a general call. That is the
   // premise Chapter 16.4's acknowledge polling depends on, and it is the second
   // reason a page write is not simply a long write: the bus is usable again
   // immediately, but this device is not.
   //
   // Nothing in this file is in UM10204. Note 2 delegates "all decisions on
   // auto-increment" to the device designer, and the page buffer is one of those
   // decisions. PAGE_SIZE is therefore a parameter, and a master that assumes the
   // wrong value corrupts data with no error anywhere.
   // -----------------------------------------------------------------------------

   module i2c_page_buffer #(
      parameter int PAGE_SIZE   = 16,     // bytes per page; a power of two
      parameter int PAGE_BITS   = 4,      // log2(PAGE_SIZE)
      parameter int N_WORDS     = 256,
      parameter int AW          = 8,
      parameter [6:0]   MY_ADDR     = 7'h50,
      parameter int WRITE_TICKS = 20,     // internal write-cycle length
      parameter int CNT_W       = 8
   ) (
      input  logic              clk,
      input  logic              rst_n,

      input  logic              start_seen,
      input  logic              stop_seen,
      input  logic              byte_valid,
      input  logic [7:0]        byte_in,
      input  logic              is_addr_byte,
      input  logic              read_byte_done,
      input  logic              master_acked,

      output logic              ack,
      output logic [7:0]        tx_byte,
      output logic              tx_valid,
      output logic              busy,            // the internal write cycle is running
      output logic [AW-1:0]     cur_addr,        // where the next byte will land
      output logic [AW-1:0]     page_base,       // the frozen high bits
      output logic              page_wrapped,    // the low counter wrapped within the page
      output logic [CNT_W-1:0]  wraps,           // how many times
      output logic [CNT_W-1:0]  bytes_buffered,  // bytes accepted into the buffer
      output logic [CNT_W-1:0]  bytes_committed, // bytes written to the array at the STOP
      output logic [CNT_W-1:0]  polls_refused,   // addressings declined while busy
      output logic [2:0]        state
   );

      localparam [2:0] S_IDLE  = 3'd0,
                       S_ADDR  = 3'd1,  // addressed for write; word address next
                       S_FILL  = 3'd2,  // filling the page buffer
                       S_BUSY  = 3'd3,  // committing; answering nothing
                       S_READ  = 3'd4;

      localparam [AW-1:0] LOW_MASK  = PAGE_SIZE - 1;
      localparam [AW-1:0] HIGH_MASK = ~(PAGE_SIZE - 1);

      logic [7:0] mem [0:N_WORDS-1];
      // The page buffer, plus a per-byte valid so an unwritten slot is not committed.
      logic [7:0] pbuf [0:PAGE_SIZE-1];
      logic       pvalid [0:PAGE_SIZE-1];
      integer   i;
      logic [31:0] busy_cnt;
      // A wrap is reported on the byte that LANDS on an already-used offset, not on
      // the byte that fills the last slot. Writing exactly PAGE_SIZE bytes from the
      // page base uses every offset once and wraps NOTHING, even though the counter
      // returns to the base afterwards. Flagging the counter's return would report a
      // corruption that did not happen.
      logic wrap_pending;

      // The low-bits offset the next byte will use.
      wire [AW-1:0] cur_off = cur_addr & LOW_MASK;

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            state           <= S_IDLE;
            ack             <= 1'b0;
            tx_byte         <= 8'h00;
            tx_valid        <= 1'b0;
            busy            <= 1'b0;
            cur_addr        <= {AW{1'b0}};
            page_base       <= {AW{1'b0}};
            page_wrapped    <= 1'b0;
            wraps           <= {CNT_W{1'b0}};
            wrap_pending    <= 1'b0;
            bytes_buffered  <= {CNT_W{1'b0}};
            bytes_committed <= {CNT_W{1'b0}};
            polls_refused   <= {CNT_W{1'b0}};
            busy_cnt        <= 32'd0;
            for (i = 0; i < N_WORDS;   i = i + 1) mem[i]    <= 8'h00;
            for (i = 0; i < PAGE_SIZE; i = i + 1) pvalid[i] <= 1'b0;
         end else begin
            ack <= 1'b0;

            // -----------------------------------------------------------------
            // The write cycle. While it runs the device answers NOTHING -- not its
            // address, not anything. Chapter 16.4 is built on this.
            // -----------------------------------------------------------------
            if (state == S_BUSY) begin
               busy_cnt <= busy_cnt + 32'd1;
               // An addressing attempt while busy is refused, and counted so a bench
               // can prove the refusals happened rather than assuming them.
               if (byte_valid && is_addr_byte && (byte_in[7:1] == MY_ADDR))
                  polls_refused <= polls_refused + 1'b1;
               if (busy_cnt + 32'd1 >= WRITE_TICKS) begin
                  busy     <= 1'b0;
                  busy_cnt <= 32'd0;
                  state    <= S_IDLE;
               end

            end else if (start_seen) begin
               state    <= S_IDLE;
               tx_valid <= 1'b0;

            end else if (stop_seen) begin
               // -----------------------------------------------------------------
               // THE COMMIT. Everything buffered is written at once, and only then
               // does the device go busy. A STOP after a pointer with no data commits
               // nothing and starts no write cycle.
               // -----------------------------------------------------------------
               if (state == S_FILL && bytes_buffered != {CNT_W{1'b0}}) begin
                  for (i = 0; i < PAGE_SIZE; i = i + 1) begin
                     if (pvalid[i]) begin
                        mem[page_base + i[AW-1:0]] <= pbuf[i];
                        pvalid[i]                  <= 1'b0;
                     end
                  end
                  bytes_committed <= bytes_committed + bytes_buffered;
                  busy            <= 1'b1;
                  busy_cnt        <= 32'd0;
                  state           <= S_BUSY;
               end else begin
                  state <= S_IDLE;
               end
               tx_valid <= 1'b0;

            end else if (byte_valid) begin
               case (state)

                  S_IDLE: begin
                     if (is_addr_byte && (byte_in[7:1] == MY_ADDR)) begin
                        ack <= 1'b1;
                        if (byte_in[0]) begin
                           tx_byte  <= mem[cur_addr];
                           tx_valid <= 1'b1;
                           state    <= S_READ;
                        end else begin
                           state <= S_ADDR;
                        end
                     end
                  end

                  // The word address arrives, and THIS is where the page base is
                  // frozen. Everything that follows lives inside that page.
                  S_ADDR: begin
                     ack            <= 1'b1;
                     cur_addr       <= byte_in[AW-1:0];
                     page_base      <= byte_in[AW-1:0] & HIGH_MASK;
                     bytes_buffered <= {CNT_W{1'b0}};
                     page_wrapped   <= 1'b0;
                     wrap_pending   <= 1'b0;
                     state          <= S_FILL;
                  end

                  // Bytes land in the buffer at the CURRENT LOW OFFSET, and the low
                  // counter wraps within the page. The high bits do not move, which
                  // is the whole mechanism.
                  S_FILL: begin
                     ack             <= 1'b1;
                     pbuf[cur_off]   <= byte_in;
                     pvalid[cur_off] <= 1'b1;
                     bytes_buffered  <= bytes_buffered + 1'b1;

                     // This byte is landing on an offset this fill has already used,
                     // so THIS is the wrap -- the moment data is destroyed.
                     if (wrap_pending) begin
                        page_wrapped <= 1'b1;
                        wraps        <= wraps + 1'b1;
                        wrap_pending <= 1'b0;
                     end

                     if (cur_off == LOW_MASK) begin
                        // The counter returns to offset zero of the SAME page, not
                        // the next page. Nothing is destroyed yet; the next byte to
                        // arrive is the one that destroys something.
                        cur_addr     <= page_base;
                        wrap_pending <= 1'b1;
                     end else begin
                        cur_addr <= cur_addr + 1'b1;
                     end
                  end

                  default: ;
               endcase

            end else if (read_byte_done && state == S_READ) begin
               // Reads have no page structure: the counter walks the whole array.
               // That asymmetry between reads and writes is the chapter's other point.
               if (cur_addr == N_WORDS - 1) cur_addr <= {AW{1'b0}};
               else                         cur_addr <= cur_addr + 1'b1;
               if (!master_acked) begin
                  tx_valid <= 1'b0;
                  state    <= S_IDLE;
               end else begin
                  if (cur_addr == N_WORDS - 1) tx_byte <= mem[0];
                  else                         tx_byte <= mem[cur_addr + 1'b1];
               end
            end
         end
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_page_buffer_tb.sv — the independent oracle: fifteen scenarios including the worked overrun
   `timescale 1ns/1ps
   // -----------------------------------------------------------------------------
   // i2c_page_buffer_tb.sv
   // Independent oracle for i2c_page_buffer.
   //
   // The bench computes the expected landing address for every byte with its own
   // copy of the page formula, spelled out, and never asks the DUT where a byte went:
   //
   //     expected = (word_addr & ~(P-1)) | ((word_addr + i) & (P-1))
   //
   // Test 3 is the chapter's worked example -- 20 bytes from 0x08 with P = 16 -- and
   // it checks all sixteen locations of the page afterwards, including the four that
   // were written twice and the four that kept their first value.
   // -----------------------------------------------------------------------------
   module i2c_page_buffer_tb;

      localparam [2:0] S_IDLE = 3'd0, S_ADDR = 3'd1, S_FILL = 3'd2,
                       S_BUSY = 3'd3, S_READ = 3'd4;
      localparam [6:0] ADDR = 7'h50;
      localparam integer P  = 16;
      localparam integer NW = 256;
      localparam integer WT = 60;

      logic        clk = 1'b0;
      logic        rst_n = 1'b0;
      logic        start_seen = 1'b0;
      logic        stop_seen = 1'b0;
      logic        byte_valid = 1'b0;
      logic [7:0]  byte_in = 8'h00;
      logic        is_addr_byte = 1'b0;
      logic        read_byte_done = 1'b0;
      logic        master_acked = 1'b0;

      logic       ack, tx_valid, busy, page_wrapped;
      logic [7:0] tx_byte;
      logic [7:0] cur_addr, page_base;
      logic [7:0] wraps, bytes_buffered, bytes_committed, polls_refused;
      logic [2:0] state;

      integer errors = 0;
      integer n, a;
      logic [7:0] got;

      i2c_page_buffer #(.PAGE_SIZE(P), .PAGE_BITS(4), .N_WORDS(NW), .AW(8),
                        .MY_ADDR(ADDR), .WRITE_TICKS(WT), .CNT_W(8)) dut (
         .clk(clk), .rst_n(rst_n), .start_seen(start_seen), .stop_seen(stop_seen),
         .byte_valid(byte_valid), .byte_in(byte_in), .is_addr_byte(is_addr_byte),
         .read_byte_done(read_byte_done), .master_acked(master_acked),
         .ack(ack), .tx_byte(tx_byte), .tx_valid(tx_valid), .busy(busy),
         .cur_addr(cur_addr), .page_base(page_base), .page_wrapped(page_wrapped),
         .wraps(wraps), .bytes_buffered(bytes_buffered),
         .bytes_committed(bytes_committed), .polls_refused(polls_refused),
         .state(state));

      always #5 clk = ~clk;

      // The bench's OWN page formula, written from the mechanism.
      function [7:0] expect_addr (input [7:0] wa, input integer i);
         begin
            expect_addr = (wa & ~(P-1)) | ((wa + i) & (P-1));
         end
      endfunction

      task step; begin @(posedge clk); @(negedge clk); end endtask

      task do_reset;
         begin
            @(negedge clk);
            rst_n = 1'b0; start_seen = 1'b0; stop_seen = 1'b0; byte_valid = 1'b0;
            is_addr_byte = 1'b0; read_byte_done = 1'b0; master_acked = 1'b0;
            repeat (3) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            step;
         end
      endtask

      task ev_start; begin @(negedge clk); start_seen = 1'b1; @(posedge clk); @(negedge clk); start_seen = 1'b0; end endtask
      task ev_stop;  begin @(negedge clk); stop_seen  = 1'b1; @(posedge clk); @(negedge clk); stop_seen  = 1'b0; end endtask

      task send_addr (input rw);
         begin
            @(negedge clk); byte_in = {ADDR, rw}; is_addr_byte = 1'b1; byte_valid = 1'b1;
            @(posedge clk); @(negedge clk); byte_valid = 1'b0; is_addr_byte = 1'b0;
         end
      endtask

      task send_data (input [7:0] b);
         begin
            @(negedge clk); byte_in = b; is_addr_byte = 1'b0; byte_valid = 1'b1;
            @(posedge clk); @(negedge clk); byte_valid = 1'b0;
         end
      endtask

      task take (input do_ack);
         begin
            @(negedge clk); read_byte_done = 1'b1; master_acked = do_ack;
            @(posedge clk); @(negedge clk); read_byte_done = 1'b0;
         end
      endtask

      task wait_idle;
         begin
            n = 0;
            while (busy && n < 200) begin step; n = n + 1; end
         end
      endtask

      // Read one location with a random read. Assumes the device is not busy.
      task read_at (input [7:0] wa);
         begin
            ev_start;
            send_addr(1'b0);
            send_data(wa);
            ev_start;
            send_addr(1'b1);
            got = tx_byte;
            take(1'b0);
            ev_stop;
         end
      endtask

      task ck_int (input [200*8:1] what, input integer g, input integer e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s: got %0d (0x%0h) expected %0d (0x%0h)", what, g, g, e, e);
               errors = errors + 1;
            end
         end
      endtask

      task ck_bit (input [200*8:1] what, input g, input e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s: got %0b expected %0b", what, g, e);
               errors = errors + 1;
            end
         end
      endtask

      initial begin
         $display("=== i2c_page_buffer: the high bits are frozen, so a long write eats itself ===");

         // ----------------------------------------------------------------
         // T1. A write that fits inside a page behaves exactly as expected, and
         //     nothing is committed until the STOP.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h10);                       // word address 0x10, page 0x10..0x1F
         ck_int("T1 page base frozen at 0x10", page_base, 8'h10);
         send_data(8'hA0); send_data(8'hA1); send_data(8'hA2);
         $display("T1  a write inside one page, committed at the STOP");
         ck_int("T1 three bytes buffered", bytes_buffered, 3);
         ck_int("T1 nothing committed yet", bytes_committed, 0);
         ck_bit("T1 not busy yet", busy, 1'b0);
         ev_stop;
         ck_int("T1 three bytes committed", bytes_committed, 3);
         ck_bit("T1 now busy", busy, 1'b1);
         wait_idle;
         read_at(8'h10); ck_int("T1 0x10", got, 8'hA0);
         read_at(8'h11); ck_int("T1 0x11", got, 8'hA1);
         read_at(8'h12); ck_int("T1 0x12", got, 8'hA2);

         // ----------------------------------------------------------------
         // T2. A write that exactly fills a page: 16 bytes from the page base. No
         //     wrap, because the last byte lands on the last offset.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h20);
         for (n = 0; n < P; n = n + 1) send_data(8'h30 + n[7:0]);
         $display("T2  a write that exactly fills a page does not wrap");
         ck_bit("T2 no wrap", page_wrapped, 1'b0);
         ck_int("T2 no wraps counted", wraps, 0);
         ck_int("T2 sixteen buffered", bytes_buffered, P);
         ev_stop;
         wait_idle;
         read_at(8'h20); ck_int("T2 first byte at the base", got, 8'h30);
         read_at(8'h2F); ck_int("T2 last byte at the top",  got, 8'h3F);

         // ----------------------------------------------------------------
         // T3. THE CHAPTER'S WORKED EXAMPLE. 20 bytes from 0x08, page size 16. The
         //     page is 0x00..0x0F. Bytes 8..15 wrap over 0x00..0x07 and bytes 16..19
         //     wrap again over 0x08..0x0B, destroying bytes 0..3.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h08);
         ck_int("T3 page base is 0x00, not 0x08", page_base, 8'h00);
         for (n = 0; n < 20; n = n + 1) send_data(8'h50 + n[7:0]);
         $display("T3  twenty bytes from 0x08 with a sixteen-byte page");
         ck_bit("T3 the buffer wrapped", page_wrapped, 1'b1);
         // The counter passes the top offset exactly ONCE in these twenty bytes, so
         // there is one wrap EVENT -- although the overwriting continues past it.
         ck_int("T3 the counter wrapped once", wraps, 1);
         ck_int("T3 all twenty bytes were accepted", bytes_buffered, 20);
         ev_stop;
         wait_idle;
         // Every location of the page, against the bench's own formula. The last
         // write to a location wins, so walk i upward and keep the final value.
         begin : verify_page
            logic [7:0] expect_mem [0:P-1];
            logic       expect_set [0:P-1];
            for (n = 0; n < P; n = n + 1) begin expect_mem[n] = 8'h00; expect_set[n] = 1'b0; end
            for (n = 0; n < 20; n = n + 1) begin
               a = expect_addr(8'h08, n);
               expect_mem[a] = 8'h50 + n[7:0];
               expect_set[a] = 1'b1;
            end
            for (n = 0; n < P; n = n + 1) begin
               read_at(n[7:0]);
               ck_int("T3 page location", got, expect_mem[n]);
            end
            // and the specific claims the chapter makes
            ck_int("T3 0x00 holds byte 8",  expect_mem[0],  8'h58);
            ck_int("T3 0x07 holds byte 15", expect_mem[7],  8'h5F);
            ck_int("T3 0x08 holds byte 16", expect_mem[8],  8'h60);
            ck_int("T3 0x0B holds byte 19", expect_mem[11], 8'h63);
            ck_int("T3 0x0C holds byte 4",  expect_mem[12], 8'h54);
            ck_int("T3 0x0F holds byte 7",  expect_mem[15], 8'h57);
         end

         // ----------------------------------------------------------------
         // T4. Nothing outside the page is ever touched. Location 0x10 -- the first
         //     of the NEXT page -- must still be zero after T3's 20-byte write.
         // ----------------------------------------------------------------
         $display("T4  no byte escapes the page");
         read_at(8'h10); ck_int("T4 0x10 untouched", got, 8'h00);
         read_at(8'h11); ck_int("T4 0x11 untouched", got, 8'h00);
         read_at(8'hFF); ck_int("T4 0xFF untouched", got, 8'h00);

         // ----------------------------------------------------------------
         // T5. EVERY BYTE IS ACKNOWLEDGED, including the ones that wrap. That is why
         //     the corruption is silent: there is no protocol error to observe.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h0C);                       // four bytes from the page top
         for (n = 0; n < 12; n = n + 1) begin
            send_data(8'h70 + n[7:0]);
            ck_bit("T5 every byte acknowledged, wrapping or not", ack, 1'b1);
         end
         $display("T5  the wrap is acknowledged exactly like a normal byte");
         ck_bit("T5 it did wrap", page_wrapped, 1'b1);
         ev_stop;
         wait_idle;

         // ----------------------------------------------------------------
         // T6. THE WRITE CYCLE. After the STOP the device answers nothing at all,
         //     and every addressing attempt is refused. This is Chapter 16.4's
         //     premise, established here.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h40);
         send_data(8'h99);
         ev_stop;
         ck_bit("T6 busy after the STOP", busy, 1'b1);
         ck_int("T6 in the busy state", state, S_BUSY);
         $display("T6  the device answers nothing during its write cycle");
         // Five poll attempts, all inside the write cycle -- WRITE_TICKS is 60 and a
         // poll costs about four edges, so the cycle is still running at the end.
         for (n = 0; n < 5; n = n + 1) begin
            ev_start;
            send_addr(1'b0);
            ck_bit("T6 refused while busy", ack, 1'b0);
         end
         ck_int("T6 five refusals counted", polls_refused, 5);
         ck_bit("T6 still busy", busy, 1'b1);
         wait_idle;
         $display("T6  and answers again once the cycle completes");
         ev_start;
         send_addr(1'b0);
         ck_bit("T6 acknowledged after the cycle", ack, 1'b1);
         ev_stop;

         // ----------------------------------------------------------------
         // T7. A pointer with NO data commits nothing and starts no write cycle.
         //     That is what makes a random read's dummy write harmless, and it is
         //     also what acknowledge polling relies on not doing.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h50);
         ev_stop;
         $display("T7  a pointer with no data starts no write cycle");
         ck_bit("T7 not busy", busy, 1'b0);
         ck_int("T7 nothing committed", bytes_committed, 0);
         ck_int("T7 back to idle", state, S_IDLE);

         // ----------------------------------------------------------------
         // T8. READS HAVE NO PAGE STRUCTURE. A sequential read walks straight across
         //     a page boundary, which is the asymmetry that makes the write
         //     behaviour surprising.
         // ----------------------------------------------------------------
         do_reset;
         // fill two adjacent pages, one page-write each
         ev_start; send_addr(1'b0); send_data(8'h00);
         for (n = 0; n < P; n = n + 1) send_data(8'h80 + n[7:0]);
         ev_stop; wait_idle;
         ev_start; send_addr(1'b0); send_data(8'h10);
         for (n = 0; n < P; n = n + 1) send_data(8'h90 + n[7:0]);
         ev_stop; wait_idle;
         ev_start;
         send_addr(1'b0);
         send_data(8'h0E);                       // two before the page boundary
         ev_start;
         send_addr(1'b1);
         $display("T8  a sequential READ crosses the page boundary freely");
         ck_int("T8 0x0E", tx_byte, 8'h8E); take(1'b1);
         ck_int("T8 0x0F", tx_byte, 8'h8F); take(1'b1);
         ck_int("T8 0x10 -- the next PAGE", tx_byte, 8'h90); take(1'b1);
         ck_int("T8 0x11", tx_byte, 8'h91); take(1'b0);
         ev_stop;

         // ----------------------------------------------------------------
         // T9. A smaller page makes the wrap happen sooner, and the page base moves
         //     with it. Same formula, different modulus.
         // ----------------------------------------------------------------
         $display("T9  the wrap point follows the page size, checked by formula");
         ck_int("T9 P=16, addr 0x08, byte 8 lands at 0x00",  expect_addr(8'h08, 8),  8'h00);
         ck_int("T9 P=16, addr 0x08, byte 7 lands at 0x0F",  expect_addr(8'h08, 7),  8'h0F);
         ck_int("T9 P=16, addr 0x1F, byte 1 lands at 0x10",  expect_addr(8'h1F, 1),  8'h10);
         ck_int("T9 P=16, addr 0x30, byte 15 lands at 0x3F", expect_addr(8'h30, 15), 8'h3F);
         ck_int("T9 P=16, addr 0x30, byte 16 lands at 0x30", expect_addr(8'h30, 16), 8'h30);

         // ----------------------------------------------------------------
         // T10. Two page writes in a row, each to its own page. Nothing leaks: the
         //      second write's base is frozen from its own word address.
         // ----------------------------------------------------------------
         do_reset;
         ev_start; send_addr(1'b0); send_data(8'h60); send_data(8'hC1); send_data(8'hC2);
         ev_stop; wait_idle;
         ck_int("T10 first page base", page_base, 8'h60);
         ev_start; send_addr(1'b0); send_data(8'h75); send_data(8'hD1); send_data(8'hD2);
         ck_int("T10 second page base is its own", page_base, 8'h70);
         ev_stop; wait_idle;
         $display("T10 each page write freezes its own base");
         read_at(8'h60); ck_int("T10 0x60", got, 8'hC1);
         read_at(8'h61); ck_int("T10 0x61", got, 8'hC2);
         read_at(8'h75); ck_int("T10 0x75", got, 8'hD1);
         read_at(8'h76); ck_int("T10 0x76", got, 8'hD2);

         // ----------------------------------------------------------------
         // T11. A START mid-fill abandons the buffer without committing. The bytes
         //      already accepted are lost, which is correct: only a STOP commits.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h90);
         send_data(8'hE1); send_data(8'hE2);
         ck_int("T11 two bytes buffered", bytes_buffered, 2);
         ev_start;                               // abandon
         $display("T11 a START mid-fill abandons the buffer uncommitted");
         ck_int("T11 nothing committed", bytes_committed, 0);
         ck_bit("T11 not busy", busy, 1'b0);
         ck_int("T11 back to expecting an address", state, S_IDLE);
         ev_stop;
         read_at(8'h90); ck_int("T11 0x90 never written", got, 8'h00);

         // ----------------------------------------------------------------
         // T12. The full committed count across several writes, so the commit
         //      accounting is shown to accumulate rather than reset.
         // ----------------------------------------------------------------
         do_reset;
         ev_start; send_addr(1'b0); send_data(8'h00); send_data(8'h01); ev_stop; wait_idle;
         ev_start; send_addr(1'b0); send_data(8'h10); send_data(8'h02); send_data(8'h03); ev_stop; wait_idle;
         ev_start; send_addr(1'b0); send_data(8'h20); send_data(8'h04); ev_stop; wait_idle;
         $display("T12 commit accounting accumulates across write cycles");
         ck_int("T12 four bytes committed in total", bytes_committed, 4);

         // ----------------------------------------------------------------
         // T13. WHERE THE COUNTER SITS AFTER EXACTLY ONE FULL PAGE. Writing PAGE_SIZE
         //      bytes from the page base uses every offset once and destroys nothing.
         //      The low counter has returned to offset zero, and the HIGH BITS HAVE
         //      NOT MOVED -- the counter is back at the page base, not at the start of
         //      the next page. That is the whole mechanism, stated as an assertion.
         // ----------------------------------------------------------------
         do_reset;
         ev_start; send_addr(1'b0); send_data(8'h20);
         for (a = 0; a < P; a = a + 1) send_data(8'hC0 + a[7:0]);
         $display("T13 after exactly one full page the counter is back at the base");
         ck_int("T13 the page base is unchanged", page_base, 8'h20);
         ck_int("T13 the counter returned to the base, not the next page", cur_addr, 8'h20);
         ck_int("T13 every offset was used once", bytes_buffered, P);
         ck_bit("T13 and nothing was destroyed", page_wrapped, 1'b0);
         ev_stop; wait_idle;

         // ----------------------------------------------------------------
         // T14. A PARTIAL PAGE WRITE MUST NOT COMMIT WHAT IT DID NOT SEND. The page
         //      buffer is PAGE_SIZE wide whatever the master puts in it, and NOTHING
         //      clears the data half of it between transfers -- only the per-byte
         //      valid bits are cleared. So a device that committed the whole buffer
         //      would write the PREVIOUS transfer's leftovers into the offsets this
         //      one never touched.
         //
         //      The test therefore fills one page completely and then writes a few
         //      bytes to a DIFFERENT page. Filling and then rewriting the same page
         //      cannot show this: the leftovers there are the values already in
         //      memory, so committing them changes nothing and the bug hides.
         // ----------------------------------------------------------------
         do_reset;
         // Fill the page at 0x40 completely, which leaves that pattern in the buffer.
         ev_start; send_addr(1'b0); send_data(8'h40);
         for (a = 0; a < P; a = a + 1) send_data(8'h50 + a[7:0]);
         ev_stop; wait_idle;
         // Now write three bytes to a different page. Offsets 3..15 of the buffer still
         // hold the previous page's data, and must not reach memory.
         ev_start; send_addr(1'b0); send_data(8'h60);
         send_data(8'hE0); send_data(8'hE1); send_data(8'hE2);
         ev_stop; wait_idle;
         $display("T14 a partial page write commits only the bytes it was sent");
         ck_int("T14 three bytes buffered by the second write", bytes_buffered, 3);
         ck_int("T14 and only three more committed", bytes_committed, P + 3);
         read_at(8'h60); ck_int("T14 offset 0 of the new page", got, 8'hE0);
         read_at(8'h61); ck_int("T14 offset 1 of the new page", got, 8'hE1);
         read_at(8'h62); ck_int("T14 offset 2 of the new page", got, 8'hE2);
         for (a = 3; a < P; a = a + 1) begin
            read_at(8'h60 + a[7:0]);
            ck_int("T14 an offset never sent is still erased", got, 8'h00);
         end
         // And the page that filled the buffer is of course untouched.
         for (a = 0; a < P; a = a + 1) begin
            read_at(8'h40 + a[7:0]);
            ck_int("T14 the first page is intact", got, 8'h50 + a[7:0]);
         end

         // ----------------------------------------------------------------
         // T15. THE WRITE CYCLE LASTS AS LONG AS IT SAYS IT DOES. A device that came
         //      back one tick early would pass every other test here and still lose a
         //      byte in a real part, because the internal write would not be finished.
         //      The duration is the datasheet's tWR and it is the number Chapter 16.4
         //      spends the whole chapter not being able to measure from the bus.
         // ----------------------------------------------------------------
         do_reset;
         ev_start; send_addr(1'b0); send_data(8'h80); send_data(8'h99); ev_stop;
         n = 0;
         while (busy && n < 400) begin step; n = n + 1; end
         $display("T15 the write cycle runs for its configured duration");
         ck_int("T15 the cycle lasted exactly WRITE_TICKS", n, WT);
         ck_bit("T15 and the device answers again afterwards", busy, 1'b0);
         read_at(8'h80);
         ck_int("T15 the byte survived the cycle", got, 8'h99);

         if (errors == 0)
            $display("=== i2c_page_buffer: ALL CHECKS PASSED ===");
         else
            $display("=== i2c_page_buffer: %0d CHECK(S) FAILED ===", errors);
         $finish;
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_page_buffer.v — the same page-buffer engine in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_page_buffer.sv
   // EEPROM page buffer and write-cycle behaviour.
   //
   // Chapter 16.2 showed a write counter that advances through the array and rolls
   // over at the top. A real EEPROM does NOT commit each byte as it arrives: the
   // bytes land in a page buffer, and the whole page is committed at the STOP. That
   // one implementation detail produces the most-reported EEPROM bug there is.
   //
   // THE MECHANISM. A page of size P starting at base B covers B..B+P-1. The device
   // latches the HIGH bits of the word address when the write begins and increments
   // only the low log2(P) bits as bytes arrive:
   //
   //     address for byte i  =  (word_addr & ~(P-1)) | ((word_addr + i) & (P-1))
   //
   // So the low bits are a modular counter and the high bits never move. Writing past
   // the end of the page therefore WRAPS to the start of the SAME page and overwrites
   // what was already put there -- it does not continue into the next page.
   //
   // Worked through, P = 16, word address 0x08, 20 bytes. The page is 0x00..0x0F,
   // because 0x08 & ~15 = 0x00:
   //     bytes  0..7   -> 0x08..0x0F
   //     bytes  8..15  -> 0x00..0x07   (wrapped)
   //     bytes 16..19  -> 0x08..0x0B   (over bytes 0..3, which are lost)
   // Four locations are written twice and bytes 0..3 are lost. Note that the counter
   // wraps ONCE in those twenty bytes -- it passes the top offset a single time -- but
   // the overwriting continues for as long as the master keeps sending.
   //
   // WHY IT IS SILENT. Every byte is acknowledged, because the device is happily
   // storing them. The STOP is accepted. The write cycle completes normally. There is
   // no protocol error at any point, and the only evidence is that the data is wrong.
   // A master cannot detect this; it has to know the page size and not exceed it.
   //
   // THE WRITE CYCLE. After the STOP the device becomes busy committing the page and
   // answers nothing at all -- not its address, not a general call. That is the
   // premise Chapter 16.4's acknowledge polling depends on, and it is the second
   // reason a page write is not simply a long write: the bus is usable again
   // immediately, but this device is not.
   //
   // Nothing in this file is in UM10204. Note 2 delegates "all decisions on
   // auto-increment" to the device designer, and the page buffer is one of those
   // decisions. PAGE_SIZE is therefore a parameter, and a master that assumes the
   // wrong value corrupts data with no error anywhere.
   // -----------------------------------------------------------------------------

   // (Verilog-2001 -- structurally identical to the SystemVerilog above.)
   module i2c_page_buffer #(
      parameter PAGE_SIZE   = 16,     // bytes per page; a power of two
      parameter PAGE_BITS   = 4,      // log2(PAGE_SIZE)
      parameter N_WORDS     = 256,
      parameter AW          = 8,
      parameter [6:0]   MY_ADDR     = 7'h50,
      parameter WRITE_TICKS = 20,     // internal write-cycle length
      parameter CNT_W       = 8
   ) (
      input  wire              clk,
      input  wire              rst_n,

      input  wire              start_seen,
      input  wire              stop_seen,
      input  wire              byte_valid,
      input  wire [7:0]        byte_in,
      input  wire              is_addr_byte,
      input  wire              read_byte_done,
      input  wire              master_acked,

      output reg               ack,
      output reg  [7:0]        tx_byte,
      output reg               tx_valid,
      output reg               busy,            // the internal write cycle is running
      output reg  [AW-1:0]     cur_addr,        // where the next byte will land
      output reg  [AW-1:0]     page_base,       // the frozen high bits
      output reg               page_wrapped,    // the low counter wrapped within the page
      output reg  [CNT_W-1:0]  wraps,           // how many times
      output reg  [CNT_W-1:0]  bytes_buffered,  // bytes accepted into the buffer
      output reg  [CNT_W-1:0]  bytes_committed, // bytes written to the array at the STOP
      output reg  [CNT_W-1:0]  polls_refused,   // addressings declined while busy
      output reg  [2:0]        state
   );

      localparam [2:0] S_IDLE  = 3'd0,
                       S_ADDR  = 3'd1,  // addressed for write; word address next
                       S_FILL  = 3'd2,  // filling the page buffer
                       S_BUSY  = 3'd3,  // committing; answering nothing
                       S_READ  = 3'd4;

      localparam [AW-1:0] LOW_MASK  = PAGE_SIZE - 1;
      localparam [AW-1:0] HIGH_MASK = ~(PAGE_SIZE - 1);

      reg [7:0] mem [0:N_WORDS-1];
      // The page buffer, plus a per-byte valid so an unwritten slot is not committed.
      reg [7:0] pbuf [0:PAGE_SIZE-1];
      reg       pvalid [0:PAGE_SIZE-1];
      integer   i;
      reg [31:0] busy_cnt;
      // A wrap is reported on the byte that LANDS on an already-used offset, not on
      // the byte that fills the last slot. Writing exactly PAGE_SIZE bytes from the
      // page base uses every offset once and wraps NOTHING, even though the counter
      // returns to the base afterwards. Flagging the counter's return would report a
      // corruption that did not happen.
      reg wrap_pending;

      // The low-bits offset the next byte will use.
      wire [AW-1:0] cur_off = cur_addr & LOW_MASK;

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            state           <= S_IDLE;
            ack             <= 1'b0;
            tx_byte         <= 8'h00;
            tx_valid        <= 1'b0;
            busy            <= 1'b0;
            cur_addr        <= {AW{1'b0}};
            page_base       <= {AW{1'b0}};
            page_wrapped    <= 1'b0;
            wraps           <= {CNT_W{1'b0}};
            wrap_pending    <= 1'b0;
            bytes_buffered  <= {CNT_W{1'b0}};
            bytes_committed <= {CNT_W{1'b0}};
            polls_refused   <= {CNT_W{1'b0}};
            busy_cnt        <= 32'd0;
            for (i = 0; i < N_WORDS;   i = i + 1) mem[i]    <= 8'h00;
            for (i = 0; i < PAGE_SIZE; i = i + 1) pvalid[i] <= 1'b0;
         end else begin
            ack <= 1'b0;

            // -----------------------------------------------------------------
            // The write cycle. While it runs the device answers NOTHING -- not its
            // address, not anything. Chapter 16.4 is built on this.
            // -----------------------------------------------------------------
            if (state == S_BUSY) begin
               busy_cnt <= busy_cnt + 32'd1;
               // An addressing attempt while busy is refused, and counted so a bench
               // can prove the refusals happened rather than assuming them.
               if (byte_valid && is_addr_byte && (byte_in[7:1] == MY_ADDR))
                  polls_refused <= polls_refused + 1'b1;
               if (busy_cnt + 32'd1 >= WRITE_TICKS) begin
                  busy     <= 1'b0;
                  busy_cnt <= 32'd0;
                  state    <= S_IDLE;
               end

            end else if (start_seen) begin
               state    <= S_IDLE;
               tx_valid <= 1'b0;

            end else if (stop_seen) begin
               // -----------------------------------------------------------------
               // THE COMMIT. Everything buffered is written at once, and only then
               // does the device go busy. A STOP after a pointer with no data commits
               // nothing and starts no write cycle.
               // -----------------------------------------------------------------
               if (state == S_FILL && bytes_buffered != {CNT_W{1'b0}}) begin
                  for (i = 0; i < PAGE_SIZE; i = i + 1) begin
                     if (pvalid[i]) begin
                        mem[page_base + i[AW-1:0]] <= pbuf[i];
                        pvalid[i]                  <= 1'b0;
                     end
                  end
                  bytes_committed <= bytes_committed + bytes_buffered;
                  busy            <= 1'b1;
                  busy_cnt        <= 32'd0;
                  state           <= S_BUSY;
               end else begin
                  state <= S_IDLE;
               end
               tx_valid <= 1'b0;

            end else if (byte_valid) begin
               case (state)

                  S_IDLE: begin
                     if (is_addr_byte && (byte_in[7:1] == MY_ADDR)) begin
                        ack <= 1'b1;
                        if (byte_in[0]) begin
                           tx_byte  <= mem[cur_addr];
                           tx_valid <= 1'b1;
                           state    <= S_READ;
                        end else begin
                           state <= S_ADDR;
                        end
                     end
                  end

                  // The word address arrives, and THIS is where the page base is
                  // frozen. Everything that follows lives inside that page.
                  S_ADDR: begin
                     ack            <= 1'b1;
                     cur_addr       <= byte_in[AW-1:0];
                     page_base      <= byte_in[AW-1:0] & HIGH_MASK;
                     bytes_buffered <= {CNT_W{1'b0}};
                     page_wrapped   <= 1'b0;
                     wrap_pending   <= 1'b0;
                     state          <= S_FILL;
                  end

                  // Bytes land in the buffer at the CURRENT LOW OFFSET, and the low
                  // counter wraps within the page. The high bits do not move, which
                  // is the whole mechanism.
                  S_FILL: begin
                     ack             <= 1'b1;
                     pbuf[cur_off]   <= byte_in;
                     pvalid[cur_off] <= 1'b1;
                     bytes_buffered  <= bytes_buffered + 1'b1;

                     // This byte is landing on an offset this fill has already used,
                     // so THIS is the wrap -- the moment data is destroyed.
                     if (wrap_pending) begin
                        page_wrapped <= 1'b1;
                        wraps        <= wraps + 1'b1;
                        wrap_pending <= 1'b0;
                     end

                     if (cur_off == LOW_MASK) begin
                        // The counter returns to offset zero of the SAME page, not
                        // the next page. Nothing is destroyed yet; the next byte to
                        // arrive is the one that destroys something.
                        cur_addr     <= page_base;
                        wrap_pending <= 1'b1;
                     end else begin
                        cur_addr <= cur_addr + 1'b1;
                     end
                  end

                  default: ;
               endcase

            end else if (read_byte_done && state == S_READ) begin
               // Reads have no page structure: the counter walks the whole array.
               // That asymmetry between reads and writes is the chapter's other point.
               if (cur_addr == N_WORDS - 1) cur_addr <= {AW{1'b0}};
               else                         cur_addr <= cur_addr + 1'b1;
               if (!master_acked) begin
                  tx_valid <= 1'b0;
                  state    <= S_IDLE;
               end else begin
                  if (cur_addr == N_WORDS - 1) tx_byte <= mem[0];
                  else                         tx_byte <= mem[cur_addr + 1'b1];
               end
            end
         end
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_page_buffer_tb.v — the same oracle in Verilog-2001
   `timescale 1ns/1ps
   // -----------------------------------------------------------------------------
   // i2c_page_buffer_tb.sv
   // Independent oracle for i2c_page_buffer.
   //
   // The bench computes the expected landing address for every byte with its own
   // copy of the page formula, spelled out, and never asks the DUT where a byte went:
   //
   //     expected = (word_addr & ~(P-1)) | ((word_addr + i) & (P-1))
   //
   // Test 3 is the chapter's worked example -- 20 bytes from 0x08 with P = 16 -- and
   // it checks all sixteen locations of the page afterwards, including the four that
   // were written twice and the four that kept their first value.
   // -----------------------------------------------------------------------------
   // (Verilog-2001 -- structurally identical to the SystemVerilog above.)
   module i2c_page_buffer_tb;

      localparam [2:0] S_IDLE = 3'd0, S_ADDR = 3'd1, S_FILL = 3'd2,
                       S_BUSY = 3'd3, S_READ = 3'd4;
      localparam [6:0] ADDR = 7'h50;
      localparam integer P  = 16;
      localparam integer NW = 256;
      localparam integer WT = 60;

      reg        clk = 1'b0;
      reg        rst_n = 1'b0;
      reg        start_seen = 1'b0;
      reg        stop_seen = 1'b0;
      reg        byte_valid = 1'b0;
      reg [7:0]  byte_in = 8'h00;
      reg        is_addr_byte = 1'b0;
      reg        read_byte_done = 1'b0;
      reg        master_acked = 1'b0;

      wire       ack, tx_valid, busy, page_wrapped;
      wire [7:0] tx_byte;
      wire [7:0] cur_addr, page_base;
      wire [7:0] wraps, bytes_buffered, bytes_committed, polls_refused;
      wire [2:0] state;

      integer errors = 0;
      integer n, a;
      reg [7:0] got;

      i2c_page_buffer #(.PAGE_SIZE(P), .PAGE_BITS(4), .N_WORDS(NW), .AW(8),
                        .MY_ADDR(ADDR), .WRITE_TICKS(WT), .CNT_W(8)) dut (
         .clk(clk), .rst_n(rst_n), .start_seen(start_seen), .stop_seen(stop_seen),
         .byte_valid(byte_valid), .byte_in(byte_in), .is_addr_byte(is_addr_byte),
         .read_byte_done(read_byte_done), .master_acked(master_acked),
         .ack(ack), .tx_byte(tx_byte), .tx_valid(tx_valid), .busy(busy),
         .cur_addr(cur_addr), .page_base(page_base), .page_wrapped(page_wrapped),
         .wraps(wraps), .bytes_buffered(bytes_buffered),
         .bytes_committed(bytes_committed), .polls_refused(polls_refused),
         .state(state));

      always #5 clk = ~clk;

      // The bench's OWN page formula, written from the mechanism.
      function [7:0] expect_addr (input [7:0] wa, input integer i);
         begin
            expect_addr = (wa & ~(P-1)) | ((wa + i) & (P-1));
         end
      endfunction

      task step; begin @(posedge clk); @(negedge clk); end endtask

      task do_reset;
         begin
            @(negedge clk);
            rst_n = 1'b0; start_seen = 1'b0; stop_seen = 1'b0; byte_valid = 1'b0;
            is_addr_byte = 1'b0; read_byte_done = 1'b0; master_acked = 1'b0;
            repeat (3) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            step;
         end
      endtask

      task ev_start; begin @(negedge clk); start_seen = 1'b1; @(posedge clk); @(negedge clk); start_seen = 1'b0; end endtask
      task ev_stop;  begin @(negedge clk); stop_seen  = 1'b1; @(posedge clk); @(negedge clk); stop_seen  = 1'b0; end endtask

      task send_addr (input rw);
         begin
            @(negedge clk); byte_in = {ADDR, rw}; is_addr_byte = 1'b1; byte_valid = 1'b1;
            @(posedge clk); @(negedge clk); byte_valid = 1'b0; is_addr_byte = 1'b0;
         end
      endtask

      task send_data (input [7:0] b);
         begin
            @(negedge clk); byte_in = b; is_addr_byte = 1'b0; byte_valid = 1'b1;
            @(posedge clk); @(negedge clk); byte_valid = 1'b0;
         end
      endtask

      task take (input do_ack);
         begin
            @(negedge clk); read_byte_done = 1'b1; master_acked = do_ack;
            @(posedge clk); @(negedge clk); read_byte_done = 1'b0;
         end
      endtask

      task wait_idle;
         begin
            n = 0;
            while (busy && n < 200) begin step; n = n + 1; end
         end
      endtask

      // Read one location with a random read. Assumes the device is not busy.
      task read_at (input [7:0] wa);
         begin
            ev_start;
            send_addr(1'b0);
            send_data(wa);
            ev_start;
            send_addr(1'b1);
            got = tx_byte;
            take(1'b0);
            ev_stop;
         end
      endtask

      task ck_int (input [200*8:1] what, input integer g, input integer e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s: got %0d (0x%0h) expected %0d (0x%0h)", what, g, g, e, e);
               errors = errors + 1;
            end
         end
      endtask

      task ck_bit (input [200*8:1] what, input g, input e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s: got %0b expected %0b", what, g, e);
               errors = errors + 1;
            end
         end
      endtask

      initial begin
         $display("=== i2c_page_buffer: the high bits are frozen, so a long write eats itself ===");

         // ----------------------------------------------------------------
         // T1. A write that fits inside a page behaves exactly as expected, and
         //     nothing is committed until the STOP.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h10);                       // word address 0x10, page 0x10..0x1F
         ck_int("T1 page base frozen at 0x10", page_base, 8'h10);
         send_data(8'hA0); send_data(8'hA1); send_data(8'hA2);
         $display("T1  a write inside one page, committed at the STOP");
         ck_int("T1 three bytes buffered", bytes_buffered, 3);
         ck_int("T1 nothing committed yet", bytes_committed, 0);
         ck_bit("T1 not busy yet", busy, 1'b0);
         ev_stop;
         ck_int("T1 three bytes committed", bytes_committed, 3);
         ck_bit("T1 now busy", busy, 1'b1);
         wait_idle;
         read_at(8'h10); ck_int("T1 0x10", got, 8'hA0);
         read_at(8'h11); ck_int("T1 0x11", got, 8'hA1);
         read_at(8'h12); ck_int("T1 0x12", got, 8'hA2);

         // ----------------------------------------------------------------
         // T2. A write that exactly fills a page: 16 bytes from the page base. No
         //     wrap, because the last byte lands on the last offset.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h20);
         for (n = 0; n < P; n = n + 1) send_data(8'h30 + n[7:0]);
         $display("T2  a write that exactly fills a page does not wrap");
         ck_bit("T2 no wrap", page_wrapped, 1'b0);
         ck_int("T2 no wraps counted", wraps, 0);
         ck_int("T2 sixteen buffered", bytes_buffered, P);
         ev_stop;
         wait_idle;
         read_at(8'h20); ck_int("T2 first byte at the base", got, 8'h30);
         read_at(8'h2F); ck_int("T2 last byte at the top",  got, 8'h3F);

         // ----------------------------------------------------------------
         // T3. THE CHAPTER'S WORKED EXAMPLE. 20 bytes from 0x08, page size 16. The
         //     page is 0x00..0x0F. Bytes 8..15 wrap over 0x00..0x07 and bytes 16..19
         //     wrap again over 0x08..0x0B, destroying bytes 0..3.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h08);
         ck_int("T3 page base is 0x00, not 0x08", page_base, 8'h00);
         for (n = 0; n < 20; n = n + 1) send_data(8'h50 + n[7:0]);
         $display("T3  twenty bytes from 0x08 with a sixteen-byte page");
         ck_bit("T3 the buffer wrapped", page_wrapped, 1'b1);
         // The counter passes the top offset exactly ONCE in these twenty bytes, so
         // there is one wrap EVENT -- although the overwriting continues past it.
         ck_int("T3 the counter wrapped once", wraps, 1);
         ck_int("T3 all twenty bytes were accepted", bytes_buffered, 20);
         ev_stop;
         wait_idle;
         // Every location of the page, against the bench's own formula. The last
         // write to a location wins, so walk i upward and keep the final value.
         begin : verify_page
            reg [7:0] expect_mem [0:P-1];
            reg       expect_set [0:P-1];
            for (n = 0; n < P; n = n + 1) begin expect_mem[n] = 8'h00; expect_set[n] = 1'b0; end
            for (n = 0; n < 20; n = n + 1) begin
               a = expect_addr(8'h08, n);
               expect_mem[a] = 8'h50 + n[7:0];
               expect_set[a] = 1'b1;
            end
            for (n = 0; n < P; n = n + 1) begin
               read_at(n[7:0]);
               ck_int("T3 page location", got, expect_mem[n]);
            end
            // and the specific claims the chapter makes
            ck_int("T3 0x00 holds byte 8",  expect_mem[0],  8'h58);
            ck_int("T3 0x07 holds byte 15", expect_mem[7],  8'h5F);
            ck_int("T3 0x08 holds byte 16", expect_mem[8],  8'h60);
            ck_int("T3 0x0B holds byte 19", expect_mem[11], 8'h63);
            ck_int("T3 0x0C holds byte 4",  expect_mem[12], 8'h54);
            ck_int("T3 0x0F holds byte 7",  expect_mem[15], 8'h57);
         end

         // ----------------------------------------------------------------
         // T4. Nothing outside the page is ever touched. Location 0x10 -- the first
         //     of the NEXT page -- must still be zero after T3's 20-byte write.
         // ----------------------------------------------------------------
         $display("T4  no byte escapes the page");
         read_at(8'h10); ck_int("T4 0x10 untouched", got, 8'h00);
         read_at(8'h11); ck_int("T4 0x11 untouched", got, 8'h00);
         read_at(8'hFF); ck_int("T4 0xFF untouched", got, 8'h00);

         // ----------------------------------------------------------------
         // T5. EVERY BYTE IS ACKNOWLEDGED, including the ones that wrap. That is why
         //     the corruption is silent: there is no protocol error to observe.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h0C);                       // four bytes from the page top
         for (n = 0; n < 12; n = n + 1) begin
            send_data(8'h70 + n[7:0]);
            ck_bit("T5 every byte acknowledged, wrapping or not", ack, 1'b1);
         end
         $display("T5  the wrap is acknowledged exactly like a normal byte");
         ck_bit("T5 it did wrap", page_wrapped, 1'b1);
         ev_stop;
         wait_idle;

         // ----------------------------------------------------------------
         // T6. THE WRITE CYCLE. After the STOP the device answers nothing at all,
         //     and every addressing attempt is refused. This is Chapter 16.4's
         //     premise, established here.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h40);
         send_data(8'h99);
         ev_stop;
         ck_bit("T6 busy after the STOP", busy, 1'b1);
         ck_int("T6 in the busy state", state, S_BUSY);
         $display("T6  the device answers nothing during its write cycle");
         // Five poll attempts, all inside the write cycle -- WRITE_TICKS is 60 and a
         // poll costs about four edges, so the cycle is still running at the end.
         for (n = 0; n < 5; n = n + 1) begin
            ev_start;
            send_addr(1'b0);
            ck_bit("T6 refused while busy", ack, 1'b0);
         end
         ck_int("T6 five refusals counted", polls_refused, 5);
         ck_bit("T6 still busy", busy, 1'b1);
         wait_idle;
         $display("T6  and answers again once the cycle completes");
         ev_start;
         send_addr(1'b0);
         ck_bit("T6 acknowledged after the cycle", ack, 1'b1);
         ev_stop;

         // ----------------------------------------------------------------
         // T7. A pointer with NO data commits nothing and starts no write cycle.
         //     That is what makes a random read's dummy write harmless, and it is
         //     also what acknowledge polling relies on not doing.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h50);
         ev_stop;
         $display("T7  a pointer with no data starts no write cycle");
         ck_bit("T7 not busy", busy, 1'b0);
         ck_int("T7 nothing committed", bytes_committed, 0);
         ck_int("T7 back to idle", state, S_IDLE);

         // ----------------------------------------------------------------
         // T8. READS HAVE NO PAGE STRUCTURE. A sequential read walks straight across
         //     a page boundary, which is the asymmetry that makes the write
         //     behaviour surprising.
         // ----------------------------------------------------------------
         do_reset;
         // fill two adjacent pages, one page-write each
         ev_start; send_addr(1'b0); send_data(8'h00);
         for (n = 0; n < P; n = n + 1) send_data(8'h80 + n[7:0]);
         ev_stop; wait_idle;
         ev_start; send_addr(1'b0); send_data(8'h10);
         for (n = 0; n < P; n = n + 1) send_data(8'h90 + n[7:0]);
         ev_stop; wait_idle;
         ev_start;
         send_addr(1'b0);
         send_data(8'h0E);                       // two before the page boundary
         ev_start;
         send_addr(1'b1);
         $display("T8  a sequential READ crosses the page boundary freely");
         ck_int("T8 0x0E", tx_byte, 8'h8E); take(1'b1);
         ck_int("T8 0x0F", tx_byte, 8'h8F); take(1'b1);
         ck_int("T8 0x10 -- the next PAGE", tx_byte, 8'h90); take(1'b1);
         ck_int("T8 0x11", tx_byte, 8'h91); take(1'b0);
         ev_stop;

         // ----------------------------------------------------------------
         // T9. A smaller page makes the wrap happen sooner, and the page base moves
         //     with it. Same formula, different modulus.
         // ----------------------------------------------------------------
         $display("T9  the wrap point follows the page size, checked by formula");
         ck_int("T9 P=16, addr 0x08, byte 8 lands at 0x00",  expect_addr(8'h08, 8),  8'h00);
         ck_int("T9 P=16, addr 0x08, byte 7 lands at 0x0F",  expect_addr(8'h08, 7),  8'h0F);
         ck_int("T9 P=16, addr 0x1F, byte 1 lands at 0x10",  expect_addr(8'h1F, 1),  8'h10);
         ck_int("T9 P=16, addr 0x30, byte 15 lands at 0x3F", expect_addr(8'h30, 15), 8'h3F);
         ck_int("T9 P=16, addr 0x30, byte 16 lands at 0x30", expect_addr(8'h30, 16), 8'h30);

         // ----------------------------------------------------------------
         // T10. Two page writes in a row, each to its own page. Nothing leaks: the
         //      second write's base is frozen from its own word address.
         // ----------------------------------------------------------------
         do_reset;
         ev_start; send_addr(1'b0); send_data(8'h60); send_data(8'hC1); send_data(8'hC2);
         ev_stop; wait_idle;
         ck_int("T10 first page base", page_base, 8'h60);
         ev_start; send_addr(1'b0); send_data(8'h75); send_data(8'hD1); send_data(8'hD2);
         ck_int("T10 second page base is its own", page_base, 8'h70);
         ev_stop; wait_idle;
         $display("T10 each page write freezes its own base");
         read_at(8'h60); ck_int("T10 0x60", got, 8'hC1);
         read_at(8'h61); ck_int("T10 0x61", got, 8'hC2);
         read_at(8'h75); ck_int("T10 0x75", got, 8'hD1);
         read_at(8'h76); ck_int("T10 0x76", got, 8'hD2);

         // ----------------------------------------------------------------
         // T11. A START mid-fill abandons the buffer without committing. The bytes
         //      already accepted are lost, which is correct: only a STOP commits.
         // ----------------------------------------------------------------
         do_reset;
         ev_start;
         send_addr(1'b0);
         send_data(8'h90);
         send_data(8'hE1); send_data(8'hE2);
         ck_int("T11 two bytes buffered", bytes_buffered, 2);
         ev_start;                               // abandon
         $display("T11 a START mid-fill abandons the buffer uncommitted");
         ck_int("T11 nothing committed", bytes_committed, 0);
         ck_bit("T11 not busy", busy, 1'b0);
         ck_int("T11 back to expecting an address", state, S_IDLE);
         ev_stop;
         read_at(8'h90); ck_int("T11 0x90 never written", got, 8'h00);

         // ----------------------------------------------------------------
         // T12. The full committed count across several writes, so the commit
         //      accounting is shown to accumulate rather than reset.
         // ----------------------------------------------------------------
         do_reset;
         ev_start; send_addr(1'b0); send_data(8'h00); send_data(8'h01); ev_stop; wait_idle;
         ev_start; send_addr(1'b0); send_data(8'h10); send_data(8'h02); send_data(8'h03); ev_stop; wait_idle;
         ev_start; send_addr(1'b0); send_data(8'h20); send_data(8'h04); ev_stop; wait_idle;
         $display("T12 commit accounting accumulates across write cycles");
         ck_int("T12 four bytes committed in total", bytes_committed, 4);

         // ----------------------------------------------------------------
         // T13. WHERE THE COUNTER SITS AFTER EXACTLY ONE FULL PAGE. Writing PAGE_SIZE
         //      bytes from the page base uses every offset once and destroys nothing.
         //      The low counter has returned to offset zero, and the HIGH BITS HAVE
         //      NOT MOVED -- the counter is back at the page base, not at the start of
         //      the next page. That is the whole mechanism, stated as an assertion.
         // ----------------------------------------------------------------
         do_reset;
         ev_start; send_addr(1'b0); send_data(8'h20);
         for (a = 0; a < P; a = a + 1) send_data(8'hC0 + a[7:0]);
         $display("T13 after exactly one full page the counter is back at the base");
         ck_int("T13 the page base is unchanged", page_base, 8'h20);
         ck_int("T13 the counter returned to the base, not the next page", cur_addr, 8'h20);
         ck_int("T13 every offset was used once", bytes_buffered, P);
         ck_bit("T13 and nothing was destroyed", page_wrapped, 1'b0);
         ev_stop; wait_idle;

         // ----------------------------------------------------------------
         // T14. A PARTIAL PAGE WRITE MUST NOT COMMIT WHAT IT DID NOT SEND. The page
         //      buffer is PAGE_SIZE wide whatever the master puts in it, and NOTHING
         //      clears the data half of it between transfers -- only the per-byte
         //      valid bits are cleared. So a device that committed the whole buffer
         //      would write the PREVIOUS transfer's leftovers into the offsets this
         //      one never touched.
         //
         //      The test therefore fills one page completely and then writes a few
         //      bytes to a DIFFERENT page. Filling and then rewriting the same page
         //      cannot show this: the leftovers there are the values already in
         //      memory, so committing them changes nothing and the bug hides.
         // ----------------------------------------------------------------
         do_reset;
         // Fill the page at 0x40 completely, which leaves that pattern in the buffer.
         ev_start; send_addr(1'b0); send_data(8'h40);
         for (a = 0; a < P; a = a + 1) send_data(8'h50 + a[7:0]);
         ev_stop; wait_idle;
         // Now write three bytes to a different page. Offsets 3..15 of the buffer still
         // hold the previous page's data, and must not reach memory.
         ev_start; send_addr(1'b0); send_data(8'h60);
         send_data(8'hE0); send_data(8'hE1); send_data(8'hE2);
         ev_stop; wait_idle;
         $display("T14 a partial page write commits only the bytes it was sent");
         ck_int("T14 three bytes buffered by the second write", bytes_buffered, 3);
         ck_int("T14 and only three more committed", bytes_committed, P + 3);
         read_at(8'h60); ck_int("T14 offset 0 of the new page", got, 8'hE0);
         read_at(8'h61); ck_int("T14 offset 1 of the new page", got, 8'hE1);
         read_at(8'h62); ck_int("T14 offset 2 of the new page", got, 8'hE2);
         for (a = 3; a < P; a = a + 1) begin
            read_at(8'h60 + a[7:0]);
            ck_int("T14 an offset never sent is still erased", got, 8'h00);
         end
         // And the page that filled the buffer is of course untouched.
         for (a = 0; a < P; a = a + 1) begin
            read_at(8'h40 + a[7:0]);
            ck_int("T14 the first page is intact", got, 8'h50 + a[7:0]);
         end

         // ----------------------------------------------------------------
         // T15. THE WRITE CYCLE LASTS AS LONG AS IT SAYS IT DOES. A device that came
         //      back one tick early would pass every other test here and still lose a
         //      byte in a real part, because the internal write would not be finished.
         //      The duration is the datasheet's tWR and it is the number Chapter 16.4
         //      spends the whole chapter not being able to measure from the bus.
         // ----------------------------------------------------------------
         do_reset;
         ev_start; send_addr(1'b0); send_data(8'h80); send_data(8'h99); ev_stop;
         n = 0;
         while (busy && n < 400) begin step; n = n + 1; end
         $display("T15 the write cycle runs for its configured duration");
         ck_int("T15 the cycle lasted exactly WRITE_TICKS", n, WT);
         ck_bit("T15 and the device answers again afterwards", busy, 1'b0);
         read_at(8'h80);
         ck_int("T15 the byte survived the cycle", got, 8'h99);

         if (errors == 0)
            $display("=== i2c_page_buffer: ALL CHECKS PASSED ===");
         else
            $display("=== i2c_page_buffer: %0d CHECK(S) FAILED ===", errors);
         $finish;
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_page_buffer.vhd — the same page-buffer engine in VHDL-2008
   -- ---------------------------------------------------------------------------
   -- i2c_page_buffer.vhd
   -- EEPROM page buffer and write-cycle behaviour.
   -- Behavioural twin of i2c_page_buffer.sv / .v.
   --
   -- THE MECHANISM. A page of size P starting at base B covers B..B+P-1. The device
   -- latches the HIGH bits of the word address when the write begins and increments
   -- only the low log2(P) bits as bytes arrive:
   --
   --     address for byte i  =  (word_addr and not (P-1)) or ((word_addr + i) and (P-1))
   --
   -- So the low bits are a modular counter and the high bits never move. Writing past
   -- the end of the page WRAPS to the start of the SAME page and overwrites what was
   -- already put there -- it does not continue into the next page.
   --
   -- Worked through, P = 16, word address 0x08, 20 bytes. The page is 0x00..0x0F:
   --     bytes  0..7   -> 0x08..0x0F
   --     bytes  8..15  -> 0x00..0x07   (wrapped)
   --     bytes 16..19  -> 0x08..0x0B   (over bytes 0..3, which are lost)
   -- The counter wraps ONCE in those twenty bytes; the overwriting continues past it.
   --
   -- WHY IT IS SILENT. Every byte is acknowledged, the STOP is accepted, the write
   -- cycle completes. There is no protocol error at any point.
   --
   -- THE WRITE CYCLE. After the STOP the device answers nothing at all -- not its
   -- address, not anything. That is the premise Chapter 16.4's acknowledge polling
   -- depends on.
   --
   -- Nothing here is in UM10204: note 2 delegates "all decisions on auto-increment"
   -- to the device designer, and the page buffer is one of those decisions.
   -- ---------------------------------------------------------------------------

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

   entity i2c_page_buffer is
      generic (
         PAGE_SIZE   : integer := 16;
         PAGE_BITS   : integer := 4;
         N_WORDS     : integer := 256;
         AW          : integer := 8;
         MY_ADDR     : std_logic_vector(6 downto 0) := "1010000";   -- 0x50
         WRITE_TICKS : integer := 20;
         CNT_W       : integer := 8
      );
      port (
         clk   : in std_logic;
         rst_n : in std_logic;

         start_seen     : in std_logic;
         stop_seen      : in std_logic;
         byte_valid     : in std_logic;
         byte_in        : in std_logic_vector(7 downto 0);
         is_addr_byte   : in std_logic;
         read_byte_done : in std_logic;
         master_acked   : in std_logic;

         ack             : out std_logic;
         tx_byte         : out std_logic_vector(7 downto 0);
         tx_valid        : out std_logic;
         busy            : out std_logic;
         cur_addr        : out unsigned(AW-1 downto 0);
         page_base       : out unsigned(AW-1 downto 0);
         page_wrapped    : out std_logic;
         wraps           : out unsigned(CNT_W-1 downto 0);
         bytes_buffered  : out unsigned(CNT_W-1 downto 0);
         bytes_committed : out unsigned(CNT_W-1 downto 0);
         polls_refused   : out unsigned(CNT_W-1 downto 0);
         state           : out unsigned(2 downto 0)
      );
   end entity i2c_page_buffer;

   architecture rtl of i2c_page_buffer is

      constant ST_IDLE : integer := 0;
      constant ST_ADDR : integer := 1;   -- addressed for write; word address next
      constant ST_FILL : integer := 2;   -- filling the page buffer
      constant ST_BUSY : integer := 3;   -- committing; answering nothing
      constant ST_READ : integer := 4;

      type mem_arr  is array (0 to N_WORDS-1)   of std_logic_vector(7 downto 0);
      type page_arr is array (0 to PAGE_SIZE-1) of std_logic_vector(7 downto 0);
      type vld_arr  is array (0 to PAGE_SIZE-1) of std_logic;

      signal mem    : mem_arr  := (others => (others => '0'));
      signal pbuf   : page_arr := (others => (others => '0'));
      signal pvalid : vld_arr  := (others => '0');

      signal st       : integer := ST_IDLE;
      signal ca       : integer := 0;
      signal pb       : integer := 0;
      signal n_buf    : integer := 0;
      signal n_com    : integer := 0;
      signal n_wrap   : integer := 0;
      signal n_poll   : integer := 0;
      signal busy_cnt : integer := 0;

      -- A wrap is reported on the byte that LANDS on an already-used offset, not on
      -- the byte that fills the last slot. Writing exactly PAGE_SIZE bytes from the
      -- page base uses every offset once and wraps NOTHING, even though the counter
      -- returns to the base afterwards.
      signal wrap_pending : std_logic := '0';

   begin

      state           <= to_unsigned(st, 3);
      cur_addr        <= to_unsigned(ca, AW);
      page_base       <= to_unsigned(pb, AW);
      wraps           <= to_unsigned(n_wrap, CNT_W);
      bytes_buffered  <= to_unsigned(n_buf, CNT_W);
      bytes_committed <= to_unsigned(n_com, CNT_W);
      polls_refused   <= to_unsigned(n_poll, CNT_W);

      process (clk, rst_n)
         variable off : integer;
      begin
         if rst_n = '0' then
            st           <= ST_IDLE;
            ack          <= '0';
            tx_byte      <= (others => '0');
            tx_valid     <= '0';
            busy         <= '0';
            ca           <= 0;
            pb           <= 0;
            page_wrapped <= '0';
            wrap_pending <= '0';
            n_buf        <= 0;
            n_com        <= 0;
            n_wrap       <= 0;
            n_poll       <= 0;
            busy_cnt     <= 0;
            mem          <= (others => (others => '0'));
            pvalid       <= (others => '0');

         elsif rising_edge(clk) then
            ack <= '0';
            off := ca mod PAGE_SIZE;

            -- The write cycle. While it runs the device answers NOTHING.
            if st = ST_BUSY then
               busy_cnt <= busy_cnt + 1;
               if byte_valid = '1' and is_addr_byte = '1'
                  and byte_in(7 downto 1) = MY_ADDR then
                  n_poll <= n_poll + 1;
               end if;
               if busy_cnt + 1 >= WRITE_TICKS then
                  busy     <= '0';
                  busy_cnt <= 0;
                  st       <= ST_IDLE;
               end if;

            elsif start_seen = '1' then
               st       <= ST_IDLE;
               tx_valid <= '0';

            elsif stop_seen = '1' then
               -- THE COMMIT. Everything buffered is written at once, and only then
               -- does the device go busy. A pointer with no data commits nothing.
               if st = ST_FILL and n_buf /= 0 then
                  for k in 0 to PAGE_SIZE-1 loop
                     if pvalid(k) = '1' then
                        mem((pb + k) mod N_WORDS) <= pbuf(k);
                        pvalid(k)                 <= '0';
                     end if;
                  end loop;
                  n_com    <= n_com + n_buf;
                  busy     <= '1';
                  busy_cnt <= 0;
                  st       <= ST_BUSY;
               else
                  st <= ST_IDLE;
               end if;
               tx_valid <= '0';

            elsif byte_valid = '1' then
               case st is

                  when ST_IDLE =>
                     if is_addr_byte = '1' and byte_in(7 downto 1) = MY_ADDR then
                        ack <= '1';
                        if byte_in(0) = '1' then
                           tx_byte  <= mem(ca);
                           tx_valid <= '1';
                           st       <= ST_READ;
                        else
                           st <= ST_ADDR;
                        end if;
                     end if;

                  -- The word address arrives, and THIS is where the page base is
                  -- frozen. Everything that follows lives inside that page.
                  when ST_ADDR =>
                     ack          <= '1';
                     ca           <= to_integer(unsigned(byte_in));
                     pb           <= (to_integer(unsigned(byte_in)) / PAGE_SIZE) * PAGE_SIZE;
                     n_buf        <= 0;
                     page_wrapped <= '0';
                     wrap_pending <= '0';
                     st           <= ST_FILL;

                  -- Bytes land at the CURRENT LOW OFFSET, and the low counter wraps
                  -- within the page. The high bits do not move.
                  when ST_FILL =>
                     ack          <= '1';
                     pbuf(off)    <= byte_in;
                     pvalid(off)  <= '1';
                     n_buf        <= n_buf + 1;

                     -- This byte lands on an offset this fill has already used, so
                     -- THIS is the wrap -- the moment data is destroyed.
                     if wrap_pending = '1' then
                        page_wrapped <= '1';
                        n_wrap       <= n_wrap + 1;
                        wrap_pending <= '0';
                     end if;

                     if off = PAGE_SIZE - 1 then
                        -- The counter returns to offset zero of the SAME page.
                        ca           <= pb;
                        wrap_pending <= '1';
                     else
                        ca <= ca + 1;
                     end if;

                  when others =>
                     null;

               end case;

            elsif read_byte_done = '1' and st = ST_READ then
               -- Reads have no page structure: the counter walks the whole array.
               if ca = N_WORDS - 1 then
                  ca <= 0;
               else
                  ca <= ca + 1;
               end if;
               if master_acked = '0' then
                  tx_valid <= '0';
                  st       <= ST_IDLE;
               else
                  if ca = N_WORDS - 1 then
                     tx_byte <= mem(0);
                  else
                     tx_byte <= mem(ca + 1);
                  end if;
               end if;
            end if;
         end if;
      end process;

   end architecture rtl;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_page_buffer_tb.vhd — the same oracle in VHDL-2008
   -- ---------------------------------------------------------------------------
   -- i2c_page_buffer_tb.vhd
   -- Independent oracle for i2c_page_buffer. Behavioural twin of the SystemVerilog
   -- and Verilog benches.
   --
   -- The bench computes the expected landing address for every byte with its own copy
   -- of the page formula and never asks the DUT where a byte went:
   --
   --     expected = (word_addr and not (P-1)) or ((word_addr + i) and (P-1))
   --
   -- Test 3 is the chapter's worked example -- 20 bytes from 0x08 with P = 16 -- and
   -- checks all sixteen locations of the page afterwards, including the four written
   -- twice and the four that kept their first value.
   -- ---------------------------------------------------------------------------

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

   entity i2c_page_buffer_tb is
   end entity i2c_page_buffer_tb;

   architecture sim of i2c_page_buffer_tb is

      constant TCLK : time := 10 ns;
      constant ADDR : std_logic_vector(6 downto 0) := "1010000";   -- 0x50
      constant P    : integer := 16;
      constant NW   : integer := 256;
      constant WT   : integer := 60;

      constant ST_IDLE : integer := 0;
      constant ST_ADDR : integer := 1;
      constant ST_FILL : integer := 2;
      constant ST_BUSY : integer := 3;
      constant ST_READ : integer := 4;

      signal clk            : std_logic := '0';
      signal rst_n          : std_logic := '0';
      signal start_seen     : std_logic := '0';
      signal stop_seen      : std_logic := '0';
      signal byte_valid     : std_logic := '0';
      signal byte_in        : std_logic_vector(7 downto 0) := (others => '0');
      signal is_addr_byte   : std_logic := '0';
      signal read_byte_done : std_logic := '0';
      signal master_acked   : std_logic := '0';

      signal ack, tx_valid, busy, page_wrapped : std_logic;
      signal tx_byte   : std_logic_vector(7 downto 0);
      signal cur_addr  : unsigned(7 downto 0);
      signal page_base : unsigned(7 downto 0);
      signal wraps, bytes_buffered, bytes_committed, polls_refused : unsigned(7 downto 0);
      signal st_o : unsigned(2 downto 0);

      signal halt : boolean := false;

   begin

      dut : entity work.i2c_page_buffer
         generic map (PAGE_SIZE => P, PAGE_BITS => 4, N_WORDS => NW, AW => 8,
                      MY_ADDR => ADDR, WRITE_TICKS => WT, CNT_W => 8)
         port map (clk => clk, rst_n => rst_n, start_seen => start_seen,
            stop_seen => stop_seen, byte_valid => byte_valid, byte_in => byte_in,
            is_addr_byte => is_addr_byte, read_byte_done => read_byte_done,
            master_acked => master_acked,
            ack => ack, tx_byte => tx_byte, tx_valid => tx_valid, busy => busy,
            cur_addr => cur_addr, page_base => page_base,
            page_wrapped => page_wrapped, wraps => wraps,
            bytes_buffered => bytes_buffered, bytes_committed => bytes_committed,
            polls_refused => polls_refused, state => st_o);

      clkgen : process
      begin
         while not halt loop
            clk <= '0'; wait for TCLK/2;
            clk <= '1'; wait for TCLK/2;
         end loop;
         wait;
      end process;

      stim : process
         variable err : integer := 0;
         variable got : std_logic_vector(7 downto 0);
         variable n   : integer;
         variable a   : integer;
         type page_mem is array (0 to P-1) of integer;
         variable expect_mem : page_mem;

         -- The bench's OWN page formula, written from the mechanism.
         function expect_addr (wa : integer; i : integer) return integer is
         begin
            return ((wa / P) * P) + ((wa + i) mod P);
         end function;

         procedure ck_int (what : string; g : integer; e : integer) is
         begin
            if g /= e then
               report "  FAIL " & what & ": got " & integer'image(g)
                      & " expected " & integer'image(e) severity note;
               err := err + 1;
            end if;
         end procedure;

         procedure ck_bit (what : string; g : std_logic; e : std_logic) is
         begin
            if g /= e then
               report "  FAIL " & what & ": got " & std_logic'image(g)
                      & " expected " & std_logic'image(e) severity note;
               err := err + 1;
            end if;
         end procedure;

         procedure step is
         begin
            wait until rising_edge(clk); wait until falling_edge(clk);
         end procedure;

         procedure do_reset is
         begin
            wait until falling_edge(clk);
            rst_n <= '0'; start_seen <= '0'; stop_seen <= '0'; byte_valid <= '0';
            is_addr_byte <= '0'; read_byte_done <= '0'; master_acked <= '0';
            for k in 0 to 2 loop wait until rising_edge(clk); end loop;
            wait until falling_edge(clk);
            rst_n <= '1';
            step;
         end procedure;

         procedure ev_start is
         begin
            wait until falling_edge(clk); start_seen <= '1';
            wait until rising_edge(clk); wait until falling_edge(clk); start_seen <= '0';
         end procedure;

         procedure ev_stop is
         begin
            wait until falling_edge(clk); stop_seen <= '1';
            wait until rising_edge(clk); wait until falling_edge(clk); stop_seen <= '0';
         end procedure;

         procedure send_addr (rw : std_logic) is
         begin
            wait until falling_edge(clk);
            byte_in <= ADDR & rw; is_addr_byte <= '1'; byte_valid <= '1';
            wait until rising_edge(clk); wait until falling_edge(clk);
            byte_valid <= '0'; is_addr_byte <= '0';
         end procedure;

         procedure send_data (b : integer) is
         begin
            wait until falling_edge(clk);
            byte_in <= std_logic_vector(to_unsigned(b, 8));
            is_addr_byte <= '0'; byte_valid <= '1';
            wait until rising_edge(clk); wait until falling_edge(clk);
            byte_valid <= '0';
         end procedure;

         procedure take (do_ack : std_logic) is
         begin
            wait until falling_edge(clk);
            read_byte_done <= '1'; master_acked <= do_ack;
            wait until rising_edge(clk); wait until falling_edge(clk);
            read_byte_done <= '0';
         end procedure;

         procedure wait_idle is
         begin
            n := 0;
            while busy = '1' and n < 300 loop step; n := n + 1; end loop;
         end procedure;

         procedure read_at (wa : integer) is
         begin
            ev_start;
            send_addr('0');
            send_data(wa);
            ev_start;
            send_addr('1');
            got := tx_byte;
            take('0');
            ev_stop;
         end procedure;

      begin
         report "=== i2c_page_buffer: the high bits are frozen, so a long write eats itself ==="
                severity note;

         -- T1. A write inside one page, committed at the STOP.
         do_reset;
         ev_start;
         send_addr('0');
         send_data(16#10#);
         ck_int("T1 page base frozen at 0x10", to_integer(page_base), 16#10#);
         send_data(16#A0#); send_data(16#A1#); send_data(16#A2#);
         report "T1  a write inside one page, committed at the STOP" severity note;
         ck_int("T1 three bytes buffered", to_integer(bytes_buffered), 3);
         ck_int("T1 nothing committed yet", to_integer(bytes_committed), 0);
         ck_bit("T1 not busy yet", busy, '0');
         ev_stop;
         ck_int("T1 three bytes committed", to_integer(bytes_committed), 3);
         ck_bit("T1 now busy", busy, '1');
         wait_idle;
         read_at(16#10#); ck_int("T1 0x10", to_integer(unsigned(got)), 16#A0#);
         read_at(16#11#); ck_int("T1 0x11", to_integer(unsigned(got)), 16#A1#);
         read_at(16#12#); ck_int("T1 0x12", to_integer(unsigned(got)), 16#A2#);

         -- T2. A write that exactly fills a page does not wrap.
         do_reset;
         ev_start;
         send_addr('0');
         send_data(16#20#);
         for k in 0 to P-1 loop send_data(16#30# + k); end loop;
         report "T2  a write that exactly fills a page does not wrap" severity note;
         ck_bit("T2 no wrap", page_wrapped, '0');
         ck_int("T2 no wraps counted", to_integer(wraps), 0);
         ck_int("T2 sixteen buffered", to_integer(bytes_buffered), P);
         ev_stop;
         wait_idle;
         read_at(16#20#); ck_int("T2 first byte at the base", to_integer(unsigned(got)), 16#30#);
         read_at(16#2F#); ck_int("T2 last byte at the top", to_integer(unsigned(got)), 16#3F#);

         -- T3. THE WORKED EXAMPLE: 20 bytes from 0x08, page size 16.
         do_reset;
         ev_start;
         send_addr('0');
         send_data(16#08#);
         ck_int("T3 page base is 0x00, not 0x08", to_integer(page_base), 16#00#);
         for k in 0 to 19 loop send_data(16#50# + k); end loop;
         report "T3  twenty bytes from 0x08 with a sixteen-byte page" severity note;
         ck_bit("T3 the buffer wrapped", page_wrapped, '1');
         -- The counter passes the top offset exactly ONCE in these twenty bytes.
         ck_int("T3 the counter wrapped once", to_integer(wraps), 1);
         ck_int("T3 all twenty bytes were accepted", to_integer(bytes_buffered), 20);
         ev_stop;
         wait_idle;
         for k in 0 to P-1 loop expect_mem(k) := 0; end loop;
         for k in 0 to 19 loop
            a := expect_addr(16#08#, k);
            expect_mem(a) := 16#50# + k;
         end loop;
         for k in 0 to P-1 loop
            read_at(k);
            ck_int("T3 page location", to_integer(unsigned(got)), expect_mem(k));
         end loop;
         ck_int("T3 0x00 holds byte 8",  expect_mem(0),  16#58#);
         ck_int("T3 0x07 holds byte 15", expect_mem(7),  16#5F#);
         ck_int("T3 0x08 holds byte 16", expect_mem(8),  16#60#);
         ck_int("T3 0x0B holds byte 19", expect_mem(11), 16#63#);
         ck_int("T3 0x0C holds byte 4",  expect_mem(12), 16#54#);
         ck_int("T3 0x0F holds byte 7",  expect_mem(15), 16#57#);

         -- T4. No byte escapes the page.
         report "T4  no byte escapes the page" severity note;
         read_at(16#10#); ck_int("T4 0x10 untouched", to_integer(unsigned(got)), 0);
         read_at(16#11#); ck_int("T4 0x11 untouched", to_integer(unsigned(got)), 0);
         read_at(16#FF#); ck_int("T4 0xFF untouched", to_integer(unsigned(got)), 0);

         -- T5. Every byte is acknowledged, wrapping or not.
         do_reset;
         ev_start;
         send_addr('0');
         send_data(16#0C#);
         for k in 0 to 11 loop
            send_data(16#70# + k);
            ck_bit("T5 every byte acknowledged, wrapping or not", ack, '1');
         end loop;
         report "T5  the wrap is acknowledged exactly like a normal byte" severity note;
         ck_bit("T5 it did wrap", page_wrapped, '1');
         ev_stop;
         wait_idle;

         -- T6. THE WRITE CYCLE: the device answers nothing.
         do_reset;
         ev_start;
         send_addr('0');
         send_data(16#40#);
         send_data(16#99#);
         ev_stop;
         ck_bit("T6 busy after the STOP", busy, '1');
         ck_int("T6 in the busy state", to_integer(st_o), ST_BUSY);
         report "T6  the device answers nothing during its write cycle" severity note;
         for k in 0 to 4 loop
            ev_start;
            send_addr('0');
            ck_bit("T6 refused while busy", ack, '0');
         end loop;
         ck_int("T6 five refusals counted", to_integer(polls_refused), 5);
         ck_bit("T6 still busy", busy, '1');
         wait_idle;
         report "T6  and answers again once the cycle completes" severity note;
         ev_start;
         send_addr('0');
         ck_bit("T6 acknowledged after the cycle", ack, '1');
         ev_stop;

         -- T7. A pointer with no data starts no write cycle.
         do_reset;
         ev_start;
         send_addr('0');
         send_data(16#50#);
         ev_stop;
         report "T7  a pointer with no data starts no write cycle" severity note;
         ck_bit("T7 not busy", busy, '0');
         ck_int("T7 nothing committed", to_integer(bytes_committed), 0);
         ck_int("T7 back to idle", to_integer(st_o), ST_IDLE);

         -- T8. READS have no page structure.
         do_reset;
         ev_start; send_addr('0'); send_data(16#00#);
         for k in 0 to P-1 loop send_data(16#80# + k); end loop;
         ev_stop; wait_idle;
         ev_start; send_addr('0'); send_data(16#10#);
         for k in 0 to P-1 loop send_data(16#90# + k); end loop;
         ev_stop; wait_idle;
         ev_start;
         send_addr('0');
         send_data(16#0E#);
         ev_start;
         send_addr('1');
         report "T8  a sequential READ crosses the page boundary freely" severity note;
         ck_int("T8 0x0E", to_integer(unsigned(tx_byte)), 16#8E#); take('1');
         ck_int("T8 0x0F", to_integer(unsigned(tx_byte)), 16#8F#); take('1');
         ck_int("T8 0x10 -- the next PAGE", to_integer(unsigned(tx_byte)), 16#90#); take('1');
         ck_int("T8 0x11", to_integer(unsigned(tx_byte)), 16#91#); take('0');
         ev_stop;

         -- T9. The wrap point follows the page size, checked by formula.
         report "T9  the wrap point follows the page size, checked by formula"
                severity note;
         ck_int("T9 P=16, addr 0x08, byte 8 lands at 0x00",  expect_addr(16#08#, 8),  16#00#);
         ck_int("T9 P=16, addr 0x08, byte 7 lands at 0x0F",  expect_addr(16#08#, 7),  16#0F#);
         ck_int("T9 P=16, addr 0x1F, byte 1 lands at 0x10",  expect_addr(16#1F#, 1),  16#10#);
         ck_int("T9 P=16, addr 0x30, byte 15 lands at 0x3F", expect_addr(16#30#, 15), 16#3F#);
         ck_int("T9 P=16, addr 0x30, byte 16 lands at 0x30", expect_addr(16#30#, 16), 16#30#);

         -- T10. Each page write freezes its own base.
         do_reset;
         ev_start; send_addr('0'); send_data(16#60#); send_data(16#C1#); send_data(16#C2#);
         ev_stop; wait_idle;
         ck_int("T10 first page base", to_integer(page_base), 16#60#);
         ev_start; send_addr('0'); send_data(16#75#); send_data(16#D1#); send_data(16#D2#);
         ck_int("T10 second page base is its own", to_integer(page_base), 16#70#);
         ev_stop; wait_idle;
         report "T10 each page write freezes its own base" severity note;
         read_at(16#60#); ck_int("T10 0x60", to_integer(unsigned(got)), 16#C1#);
         read_at(16#61#); ck_int("T10 0x61", to_integer(unsigned(got)), 16#C2#);
         read_at(16#75#); ck_int("T10 0x75", to_integer(unsigned(got)), 16#D1#);
         read_at(16#76#); ck_int("T10 0x76", to_integer(unsigned(got)), 16#D2#);

         -- T11. A START mid-fill abandons the buffer uncommitted.
         do_reset;
         ev_start;
         send_addr('0');
         send_data(16#90#);
         send_data(16#E1#); send_data(16#E2#);
         ck_int("T11 two bytes buffered", to_integer(bytes_buffered), 2);
         ev_start;
         report "T11 a START mid-fill abandons the buffer uncommitted" severity note;
         ck_int("T11 nothing committed", to_integer(bytes_committed), 0);
         ck_bit("T11 not busy", busy, '0');
         ck_int("T11 back to expecting an address", to_integer(st_o), ST_IDLE);
         ev_stop;
         read_at(16#90#); ck_int("T11 0x90 never written", to_integer(unsigned(got)), 0);

         -- T12. Commit accounting accumulates.
         do_reset;
         ev_start; send_addr('0'); send_data(16#00#); send_data(16#01#); ev_stop; wait_idle;
         ev_start; send_addr('0'); send_data(16#10#); send_data(16#02#); send_data(16#03#);
         ev_stop; wait_idle;
         ev_start; send_addr('0'); send_data(16#20#); send_data(16#04#); ev_stop; wait_idle;
         report "T12 commit accounting accumulates across write cycles" severity note;
         ck_int("T12 four bytes committed in total", to_integer(bytes_committed), 4);

         -- T13. WHERE THE COUNTER SITS AFTER EXACTLY ONE FULL PAGE. Writing PAGE_SIZE
         --      bytes from the page base uses every offset once and destroys nothing.
         --      The low counter has returned to offset zero, and the HIGH BITS HAVE NOT
         --      MOVED -- the counter is back at the page base, not at the start of the
         --      next page. That is the whole mechanism, stated as an assertion.
         do_reset;
         ev_start; send_addr('0'); send_data(16#20#);
         for k in 0 to P-1 loop send_data(16#C0# + k); end loop;
         report "T13 after exactly one full page the counter is back at the base"
                severity note;
         ck_int("T13 the page base is unchanged", to_integer(page_base), 16#20#);
         ck_int("T13 the counter returned to the base, not the next page",
                to_integer(cur_addr), 16#20#);
         ck_int("T13 every offset was used once", to_integer(bytes_buffered), P);
         ck_bit("T13 and nothing was destroyed", page_wrapped, '0');
         ev_stop; wait_idle;

         -- T14. A PARTIAL PAGE WRITE MUST NOT COMMIT WHAT IT DID NOT SEND. The page
         --      buffer is PAGE_SIZE wide whatever the master puts in it, and NOTHING
         --      clears the data half of it between transfers -- only the per-byte valid
         --      bits are cleared. So a device that committed the whole buffer would
         --      write the PREVIOUS transfer's leftovers into the offsets this one never
         --      touched.
         --
         --      The test therefore fills one page completely and then writes a few
         --      bytes to a DIFFERENT page. Filling and then rewriting the same page
         --      cannot show this: the leftovers there are the values already in memory,
         --      so committing them changes nothing and the bug hides.
         do_reset;
         -- Fill the page at 0x40 completely, which leaves that pattern in the buffer.
         ev_start; send_addr('0'); send_data(16#40#);
         for k in 0 to P-1 loop send_data(16#50# + k); end loop;
         ev_stop; wait_idle;
         -- Now write three bytes to a different page. Offsets 3..15 of the buffer still
         -- hold the previous page's data, and must not reach memory.
         ev_start; send_addr('0'); send_data(16#60#);
         send_data(16#E0#); send_data(16#E1#); send_data(16#E2#);
         ev_stop; wait_idle;
         report "T14 a partial page write commits only the bytes it was sent"
                severity note;
         ck_int("T14 three bytes buffered by the second write",
                to_integer(bytes_buffered), 3);
         ck_int("T14 and only three more committed",
                to_integer(bytes_committed), P + 3);
         read_at(16#60#);
         ck_int("T14 offset 0 of the new page", to_integer(unsigned(got)), 16#E0#);
         read_at(16#61#);
         ck_int("T14 offset 1 of the new page", to_integer(unsigned(got)), 16#E1#);
         read_at(16#62#);
         ck_int("T14 offset 2 of the new page", to_integer(unsigned(got)), 16#E2#);
         for k in 3 to P-1 loop
            read_at(16#60# + k);
            ck_int("T14 an offset never sent is still erased",
                   to_integer(unsigned(got)), 0);
         end loop;
         -- And the page that filled the buffer is of course untouched.
         for k in 0 to P-1 loop
            read_at(16#40# + k);
            ck_int("T14 the first page is intact",
                   to_integer(unsigned(got)), 16#50# + k);
         end loop;

         -- T15. THE WRITE CYCLE LASTS AS LONG AS IT SAYS IT DOES. A device that came
         --      back one tick early would pass every other test here and still lose a
         --      byte in a real part, because the internal write would not be finished.
         --      The duration is the datasheet's tWR and it is the number Chapter 16.4
         --      spends the whole chapter not being able to measure from the bus.
         do_reset;
         ev_start; send_addr('0'); send_data(16#80#); send_data(16#99#); ev_stop;
         n := 0;
         while busy = '1' and n < 400 loop step; n := n + 1; end loop;
         report "T15 the write cycle runs for its configured duration" severity note;
         ck_int("T15 the cycle lasted exactly WRITE_TICKS", n, WT);
         ck_bit("T15 and the device answers again afterwards", busy, '0');
         read_at(16#80#);
         ck_int("T15 the byte survived the cycle", to_integer(unsigned(got)), 16#99#);

         if err = 0 then
            report "=== i2c_page_buffer: ALL CHECKS PASSED ===" severity note;
         else
            report "=== i2c_page_buffer: " & integer'image(err)
                   & " CHECK(S) FAILED ===" severity note;
         end if;
         halt <= true;
         wait;
      end process;

   end architecture sim;

6a. Decisions Worth Defending

The page base is frozen when the word address arrives, and never again. page_base <= byte_in & HIGH_MASK in S_ADDR. Everything that follows lives inside that page whatever the master sends. Mutation C2-1 stores the full address as the base instead, so fills cross into the next page, and twenty-three checks fail.

The low counter wraps within the page; it does not carry. When the offset reaches LOW_MASK the next address is page_base, not cur_addr + 1. Mutation C2-2 lets it carry and is killed — by an assertion on cur_addr after exactly one full page, which is the only place the difference shows.

A wrap is flagged on the byte that lands on a used offset, not on the counter's return. That is what wrap_pending is for: the counter's return sets it, and the next byte to arrive consumes it. Writing exactly PAGE_SIZE bytes from the base therefore reports zero wraps, correctly. This is the design's most-revised decision and §2's callout explains why; mutation C2-3 restores the naive form and three checks fail.

Nothing reaches the array until the STOP, and then the whole buffer goes at once. The commit loop runs in the stop_seen branch. Mutation C2-4 writes each byte to the array as it arrives — which is a plausible simplification and destroys the whole model — and is killed by test 11, the one that abandons a fill with a START and then proves nothing reached the array.

Only the offsets this fill wrote are committed. pvalid is a per-byte valid bit, cleared at reset and after every commit; the buffer's data half is never cleared. Mutation C2-5 commits all slots regardless, and §7 has an important note about why that survived the first version of the test.

A STOP with no data bytes commits nothing and starts no write cycle. The commit branch tests bytes_buffered != 0. Mutation C2-7 drops the test, so setting a pointer makes the device unavailable for milliseconds, and two checks fail.

While busy the device answers nothing, and the refusals are counted. polls_refused increments on every addressing that matches while state == S_BUSY. It is not a functional output; it exists so a bench can prove the refusals happened rather than assuming the silence. Mutation C2-8 refuses without counting and is killed by one check.

The write cycle's duration is a parameter and is asserted exactly. WRITE_TICKS models tWR. Mutation C2-9 ends the cycle one tick early — a defect that would pass every functional test and lose a byte in a real part — and is killed by the test that counts the ticks.

Reads have no page structure at all. The read path walks the whole array and wraps at N_WORDS - 1, exactly as Chapter 16.2 described. That asymmetry between reads and writes is the chapter's other point, and mutation C2-10 makes reads wrap within the page instead, which one check catches.

6b. Verified Execution

Azvya Education Pvt. Ltd.VLSI Mentor
terminal — three simulators, fifteen scenarios, one finish time
   $ iverilog -g2012 -o d i2c_page_buffer.sv i2c_page_buffer_tb.sv && ./d
   === i2c_page_buffer: the high bits are frozen, so a long write eats itself ===
   T1  a write inside one page, committed at the STOP
   T2  a write that exactly fills a page does not wrap
   T3  twenty bytes from 0x08 with a sixteen-byte page
   T4  no byte escapes the page
   T5  the wrap is acknowledged exactly like a normal byte
   T6  the device answers nothing during its write cycle
   T6  and answers again once the cycle completes
   T7  a pointer with no data starts no write cycle
   T8  a sequential READ crosses the page boundary freely
   T9  the wrap point follows the page size, checked by formula
   T10 each page write freezes its own base
   T11 a START mid-fill abandons the buffer uncommitted
   T12 commit accounting accumulates across write cycles
   T13 after exactly one full page the counter is back at the base
   T14 a partial page write commits only the bytes it was sent
   T15 the write cycle runs for its configured duration
   === i2c_page_buffer: ALL CHECKS PASSED ===

   $ iverilog -g2005 -o v i2c_page_buffer.v i2c_page_buffer_tb.v && ./v
   === i2c_page_buffer: the high bits are frozen, so a long write eats itself ===
   T1  a write inside one page, committed at the STOP
   T2  a write that exactly fills a page does not wrap
   T3  twenty bytes from 0x08 with a sixteen-byte page
   T4  no byte escapes the page
   T5  the wrap is acknowledged exactly like a normal byte
   T6  the device answers nothing during its write cycle
   T6  and answers again once the cycle completes
   T7  a pointer with no data starts no write cycle
   T8  a sequential READ crosses the page boundary freely
   T9  the wrap point follows the page size, checked by formula
   T10 each page write freezes its own base
   T11 a START mid-fill abandons the buffer uncommitted
   T12 commit accounting accumulates across write cycles
   T13 after exactly one full page the counter is back at the base
   T14 a partial page write commits only the bytes it was sent
   T15 the write cycle runs for its configured duration
   === i2c_page_buffer: ALL CHECKS PASSED ===

   $ nvc --std=2008 -a i2c_page_buffer.vhd i2c_page_buffer_tb.vhd
   $ nvc --std=2008 -e i2c_page_buffer_tb && nvc --std=2008 -r i2c_page_buffer_tb --stop-time=300us
   === i2c_page_buffer: the high bits are frozen, so a long write eats itself ===
   T1  a write inside one page, committed at the STOP
   T2  a write that exactly fills a page does not wrap
   T3  twenty bytes from 0x08 with a sixteen-byte page
   T4  no byte escapes the page
   T5  the wrap is acknowledged exactly like a normal byte
   T6  the device answers nothing during its write cycle
   T6  and answers again once the cycle completes
   T7  a pointer with no data starts no write cycle
   T8  a sequential READ crosses the page boundary freely
   T9  the wrap point follows the page size, checked by formula
   T10 each page write freezes its own base
   T11 a START mid-fill abandons the buffer uncommitted
   T12 commit accounting accumulates across write cycles
   T13 after exactly one full page the counter is back at the base
   T14 a partial page write commits only the bytes it was sent
   T15 the write cycle runs for its configured duration
   === i2c_page_buffer: ALL CHECKS PASSED ===

6c. What The Testbench Proves

#scenariowhat it establishes
1a write that fits inside one pagethe ordinary case; nothing committed until the STOP
2exactly sixteen bytes from the page baseno wrap — the last byte lands on the last offset
3twenty bytes from 0x08, page size 16§2's worked example, location by location
4location 0x10 after that writenothing outside the page is ever touched
5the acknowledge on every byte, wrapping ones includedwhy the corruption is silent — no protocol error exists
6every addressing during the write cyclerefused, and the refusals counted
7a pointer with no data, then a STOPcommits nothing; starts no write cycle
8a read burst across a page boundaryreads have no page structure at all
9a smaller pagethe wrap happens sooner and the base moves — same formula
10two page writes, each to its own pageeach base frozen from its own word address; nothing leaks
11a START mid-fillthe buffer is abandoned without committing — only a STOP commits
12commit accounting over three writesaccumulates rather than resetting
13exactly PAGE_SIZE bytes from the basecounter back at the base, zero wraps
14a partial write to a page after a full write to anotheronly the sent bytes are committed
15the write cycle's durationexactly WRITE_TICKS; not one tick less

Test 3 is §2's arithmetic, executed. It writes twenty bytes from 0x08 into a sixteen-byte page and then reads back every location in the page against a formula the bench computes independently, asserting the specific mixture: bytes 8..15 at 0x00..0x07, bytes 16..19 at 0x08..0x0B, bytes 4..7 at 0x0C..0x0F. It also asserts wraps is one while twenty bytes were accepted — the event-versus-location distinction of §2, as two numbers that must differ.

Test 2 and test 13 are the pair that stops the wrap flag from lying. Exactly one full page from the base uses every offset once, so page_wrapped must stay low and the counter must end at the page base. That combination looks like a wrap and is not one.

Test 5 asserts that nothing goes wrong on the wire. Every byte of the overrunning write is acknowledged, including the ones that destroy data. It is the test that documents why this whole failure class is invisible to a protocol analyser.

Test 14 had to be rewritten before it worked, and the reason is worth reading in §7. Filling a page and then rewriting the same page cannot detect a device that commits its whole buffer, because the leftovers there are the values already in memory.

Test 15 counts ticks. A write cycle one tick short is invisible to every functional test in the suite — the data is committed, the device comes back, everything reads correctly — and in a real part it means the internal programming operation was cut off. The only way to catch it is to assert the duration.

7. Mutation Testing

Twelve defects injected into the SystemVerilog page-buffer engine.

#injected defectoutcome
C2-1the page's high bits not frozen; fills cross pageskilled — 23 checks
C2-2the low counter carries into the high bitskilled — test 13
C2-3the wrap reported on the counter's returnkilled — 3 checks
C2-4each byte committed as it arriveskilled — test 11
C2-5the whole buffer committed, valid bits ignoredkilled — test 14
C2-6no write cycle; the device answers immediatelykilled — 9 checks
C2-7a write cycle started by a data-less STOPkilled — 2 checks
C2-8polls refused during the cycle but not countedkilled — 1 check
C2-9the write cycle ends one tick earlykilled — test 15
C2-10reads wrap within the page tookilled — 1 check
C2-11the buffer indexed by the full address, not the offsetkilled — 32 checks
C2-12the buffered-byte count carried into the next page writekilled — 3 checks

Twelve of twelve, and the last one to fall taught the most.

C2-5 survived a test that was written specifically to kill it. The first version of test 14 filled a page completely with a pattern and then rewrote the first three bytes of the same page. Under the mutation, the commit wrote all sixteen slots — but slots 3..15 still held the pattern from the first write, which is exactly what was already in memory at those locations. Committing them changed nothing. The mutation was unobservable.

The fix was to write the second, partial transfer to a different page. The buffer's slots 3..15 then still hold the previous page's data, and committing them writes that data into locations it has no business in. The test asserts those offsets are still erased, and the mutation dies.

That is a general trap in testing any cached or buffered write path: a stale-data bug hides whenever the stale data happens to equal the correct data. Rewriting the same region is the one case where it always does.

C2-3 is the mutation that corresponds to a real bug this design had. Reporting the wrap when the counter returns to the base makes an exactly-full page write report a corruption that did not happen. It was found the same way it is killed — by test 13 asserting a zero — and it is in the table because the naive form is the one most people would write first.

C2-9 kills with one check, and that check exists only because someone counted. Ending the write cycle one tick early leaves every functional property intact. The data is committed, the polls are refused, the device returns. Nothing distinguishes it except the number of ticks, so the only test that can catch it is the one that measures the duration rather than the outcome.

C2-2's single check is the cur_addr assertion after exactly one full page, and it is the only place in the suite where letting the counter carry is visible. The committed data is identical either way, because the commit uses page_base rather than cur_addr — so the defect shows up in one output, at one moment, and nowhere else.

8. Verification Connection — Modelling a Device That Stops Answering

A page-buffered write is hard to verify for two reasons that have nothing to do with the data path: the transfer's effect is deferred to the STOP, and the device then leaves the bus for a period the bench has to model.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_page_write_uvm.sv — a deferred-effect scoreboard and the busy window
   // The reference model for a page write cannot be a function of the transfer
   // alone, because the transfer's effect does not happen during the transfer. It
   // happens at the STOP, all at once, and only for the offsets that were written.
   // So the model needs the same two-part structure the device has: a buffer, and a
   // commit.
   class page_write_model extends uvm_component;
      `uvm_component_utils(page_write_model)

      protected int  page_size;
      protected int  n_words;
      protected byte mem[int];

      // The buffer, mirroring the device: data that PERSISTS between transfers and
      // valid bits that do NOT. Getting this asymmetry wrong in the model is the
      // mutation C2-5 of the verification environment: a model that cleared its
      // buffer data between transfers would agree with a broken device.
      protected byte buf_data[];
      protected bit  buf_valid[];
      protected int  page_base;
      protected int  cur_off;

      function new(string name, uvm_component parent);
         super.new(name, parent);
      endfunction

      function void configure(int psize, int words);
         page_size = psize;
         n_words   = words;
         buf_data  = new[psize];
         buf_valid = new[psize];
      endfunction

      // The word address freezes the page and positions the offset. Both come from
      // the same byte, split by the page mask.
      function void set_address(int wa);
         page_base = wa & ~(page_size - 1);
         cur_off   = wa &  (page_size - 1);
         foreach (buf_valid[i]) buf_valid[i] = 0;   // valid bits only
      endfunction

      function void data_byte(byte b);
         buf_data[cur_off]  = b;
         buf_valid[cur_off] = 1;
         cur_off = (cur_off + 1) & (page_size - 1);  // modular: the wrap, in one line
      endfunction

      // The commit. Only valid offsets reach memory, and the base -- not the running
      // counter -- supplies the high bits.
      function void stop();
         foreach (buf_valid[i])
            if (buf_valid[i]) begin
               mem[page_base + i] = buf_data[i];
               buf_valid[i]       = 0;
            end
      endfunction

      function byte read(int addr);
         return mem.exists(addr) ? mem[addr] : 8'h00;
      endfunction
   endclass

   // Coverage for the page boundary. None of this is generated automatically, and
   // none of it is interesting on a bus that only ever writes aligned full pages --
   // which is what a randomly-generated regression will mostly do unless told not to.
   covergroup page_cg (int page_size) with function sample (int start_off, int n);
      option.per_instance = 1;

      // Where in the page the write began. The mid-page start is the case that makes
      // the page boundary visible at all.
      cp_start : coverpoint start_off {
         bins base      = {0};
         bins mid[4]    = {[1:page_size-2]};
         bins last      = {page_size-1};
      }

      // How the write relates to the page it is in. "Exactly full from the base" is
      // the bin that separates a clean fill from a wrap, and it is the one a random
      // generator hits least often.
      cp_extent : coverpoint n {
         bins partial       = {[1:3]};
         bins exactly_page  = {page_size};
         bins one_over      = {page_size+1};
         bins well_over[2]  = {[page_size+2:3*page_size]};
      }

      // And the cross that matters: an overrun from a mid-page start is section 2's
      // worked example, and it is a different bug from an overrun from the base.
      x_start_extent : cross cp_start, cp_extent;
   endgroup

Three points, and a warning.

The model must mirror the buffer's asymmetry, not just its size. Data persists between transfers; valid bits do not. A model that cleared both would predict zeros at the untouched offsets and therefore agree with a broken device — it would pass mutation C2-5 alongside the RTL. A reference model has to be structured like the thing it models, not merely produce the same answers on the cases someone thought of.

The commit uses page_base, not the running counter. That is the line that makes the high bits frozen in the model as well as in the device, and it is one character away from a model that crosses pages.

The coverage bins are where the work is. A randomly generated write regression will overwhelmingly produce writes that fit in a page, because most random lengths are short. The interesting bins — exactly one full page from the base, one byte over, a large overrun from a mid-page start — need constraints aimed at them, and the cross is what distinguishes §2's worked example from the simpler overrun from the base.

9. FPGA and ASIC Implications

The page buffer is a small RAM with a modular counter, and the counter width is the page size. That is the whole implementation, and it is much cheaper than the alternative of writing through to the array. It also means the page size is fixed in silicon: it is the array's row width, not a configuration choice.

The per-byte valid bits are not optional. Without them the commit has to write the whole row, and the row's untouched offsets get whatever the previous transfer left in the buffer. §7's C2-5 is that device.

The commit is a single operation and it must be atomic with respect to the bus. If the internal write can be interrupted by a new addressing, a master that polls too eagerly can corrupt the row it just wrote. The standard answer is what this design does: refuse everything until the operation completes, which is why the device goes silent rather than busy-flagging.

tWR is a real number and a driver's timeout has to exceed it. A few milliseconds is an eternity on a bus whose byte time is 22.5 µs at 400 kHz. A driver that treats an unacknowledged address as a missing device will declare the EEPROM absent immediately after every successful write.

Write endurance is per page, and a wrapped write costs double. §2's example wrote four locations twice in one transaction. On a part rated for a million cycles per page, a driver that routinely overruns pages is consuming endurance at twice the rate the datasheet's arithmetic assumes.

Align writes to page boundaries when you control the layout. A record that fits in a page but straddles two of them needs two transactions and two write cycles. A record aligned to the page costs one of each, which is both faster and less wearing — and it removes the overrun hazard entirely, because a page-aligned write of at most PAGE_SIZE bytes cannot wrap.

Report the wrap if you have a status bit to spare. A wrapped write is always a driver bug. It is silent by construction, so the only way it ever gets found is if the device says so.

10. Debugging — The Record That Was Corrupted by a Later One

Symptom

A product appends 12-byte event records to an EEPROM log at successive offsets. Most records read back correctly. Roughly one record in four reads back with its first four bytes wrong -- and the wrong bytes turn out to be the tail of a record written LATER. The record that appears to have caused the damage reads back perfectly.

Root Cause

A 12-byte record written into a 32-byte page at an offset of 24. The device freezes the page's high bits at the word address and increments only the low five bits, so the record's last four bytes wrap to the start of the SAME page rather than continuing into the next one -- landing on top of an EARLIER record. Every byte was acknowledged because the page buffer has a slot for each of them and 'that crosses a page boundary' is not something the protocol can express. The one-in-four rate is arithmetic, not chance: the offset cycle has a period of eight and exactly two of those eight offsets wrap. And the record that wrapped reads back correctly, which is why the corruption looked unrelated to it -- the damage is always to a neighbour, never to the writer.

Fix
Align records to the page: pad the record to 16 bytes so two fit exactly in a 32-byte page and no record can ever straddle a boundary. If the record size cannot change, split each write at the page boundary into two transactions with acknowledge polling between them. For the regression: write a full period of records -- eight of them -- and verify every one afterwards, which is what a single-record test structurally cannot do.

Three things generalise.

The corrupted record was not the one being written. A wrapping write destroys its neighbour's data and leaves its own intact, because its twelve bytes still land on twelve distinct offsets. So the record that reads back wrong is never the record whose write caused the problem, which sends an investigation to the wrong place.

The one-in-four rate was arithmetic and looked like chance. The offset cycle has a period of eight records, and exactly two of those eight offsets wrap. Nothing intermittent is involved, and nothing about the rate suggests a page boundary until the cycle is written out.

The existing test wrote one record. A single record at offset zero fits in its page and cannot fail. Testing a buffered write path needs a full period of the offset cycle, not one example — the same lesson §7 drew about testing across pages rather than within one.

11. Common Misconceptions

"A write burst continues into the next page." It wraps to the start of the same page. The high bits of the address are frozen when the word address arrives. §1 and §2.

"Writing past a page boundary loses the surplus bytes." The surplus bytes are written — over the earlier bytes of the same write. Nothing is lost at the end; things are lost at the beginning. §2.

"The bytes are written as they arrive." Nothing reaches the array until the STOP. A read in the middle of a write burst returns the old contents. §3.

"A page write of exactly PAGE_SIZE bytes wraps." It uses every offset once and destroys nothing. The counter's return to the base is not a wrap. §2's callout.

"The device sets a busy flag during the write cycle." It stops answering entirely — no acknowledge, for anything. There is no flag to read, because reading it would require an acknowledge. §3.

"A write cycle starts whenever a STOP follows a write addressing." Only if data bytes were sent. A pointer write followed by a STOP commits nothing. §3.

"The page size is configurable." It is the array's row width, fixed in silicon. §9.

"Reads are paged too." Reads walk the whole array and wrap at its end. The asymmetry is deliberate and is the chapter's second point. §6a.

"A wrapped write is detectable by the master." Every byte is acknowledged and the write cycle completes normally. Without a status bit on the device, nothing reports it. §5 and §9.

"Rewriting the same region tests the buffer's valid bits." It cannot: the stale data there equals the correct data, so committing it changes nothing. The test has to write to a different page. §7.

"A write cycle one tick short is harmless in simulation and in silicon." In simulation it is invisible to every functional test. In silicon the internal programming operation is cut off. §7's C2-9.

12. Reason It Through

A master writes twenty bytes starting at 0x08 on a part with a sixteen-byte page. What is at 0x08 afterwards?

Byte 16 of the write. The page is 0x00..0x0F; bytes 0..7 land at 0x08..0x0F, bytes 8..15 wrap to 0x00..0x07, and bytes 16..19 land at 0x08..0x0B on top of bytes 0..3. So 0x08 holds byte 16, and bytes 0..3 are gone. §2.

How many times did the counter wrap in that write, and how many locations were overwritten?

The counter wrapped once — it passed the top offset a single time, at byte 8. Four locations were overwritten. The two counts are different and a device that conflated them would be reporting something untrue. §2.

Why does writing exactly sixteen bytes from 0x00 into a sixteen-byte page report no wrap?

Because every offset was used once and nothing was destroyed. The counter sits back at the base afterwards, which looks like a wrap and is not one. The wrap must be flagged on the byte that lands on an already-used offset — which, in this write, never arrives. §2's callout.

A master issues a read immediately after the last data byte of a write burst, using a repeated START. What does it get?

The array's old contents, because the write has not happened yet — the bytes are still in the buffer and the commit is triggered by the STOP the master has not sent. It also abandons the write: the repeated START resets the bus logic, and whether the buffered bytes survive to a later STOP is a device convention the datasheet has to state. §3 and §6c test 2.

Why does a device go silent during the write cycle rather than acknowledging and setting a busy bit?

Because reading a busy bit requires an acknowledge, and the internal operation must be atomic with respect to the bus. Answering an addressing would mean either interrupting the programming operation or maintaining bus logic through it; going silent is both simpler and safer. §3 and §9.

A test fills a page, then rewrites the first three bytes of the same page, and asserts the rest of the page is unchanged. What can it not detect?

A device that commits its whole buffer regardless of which offsets were written. The buffer's remaining slots hold the values from the first write, which are exactly what is already in memory at those locations — so committing them is a no-op. The test has to write the second transfer to a different page. §7.

Why must a record size be a divisor or a multiple of the page size?

So a record never straddles a page boundary. A 12-byte record in a 32-byte page starts at a different offset in every record — the cycle is 0, 12, 24, 4, 16, 28, 8, 20 — and two of those eight offsets wrap. A 16-byte record in the same page always starts at offset 0 or 16 and never wraps. §10.

13. Understanding Check

14. Summary

Reads walk the array; writes walk a page. That asymmetry is the chapter, and it exists because an EEPROM row is programmed as a unit by a slow internal operation.

The high bits of the address are frozen when the word address arrives, and only the low log2(PAGE_SIZE) bits increment. address(i) = (word_addr & ~(PAGE_SIZE-1)) | ((word_addr + i) & (PAGE_SIZE-1)).

So a write that runs off the end of a page wraps back to the start of the same page and overwrites the bytes it already sent. Twenty bytes from 0x08 into a sixteen-byte page leave bytes 0..3 destroyed, four locations written twice, and nothing at all outside the page.

The counter wraps once; the overwriting continues. A wrap event and an overwritten location are different counts.

And writing exactly PAGE_SIZE bytes from the base destroys nothing, so the wrap must be flagged on the byte that lands on a used offset — not on the counter's return to the base.

Nothing reaches the array until the STOP. A read mid-burst returns the old contents, and a STOP with no data bytes commits nothing and starts no write cycle.

Only the offsets this transfer wrote are committed. The buffer's data half is never cleared, so without per-byte valid bits the commit would write the previous transfer's leftovers.

Then the device stops answering entirely — no acknowledge for anything, for the duration of tWR — because the internal operation has to be atomic with respect to the bus and reading a busy flag would require an acknowledge.

Every one of these failures is silent. Every byte is acknowledged, every frame is well formed, and the protocol has no way to express "that is more than a page".

Two testing lessons, both learned the hard way here. A stale-data bug hides whenever the stale data equals the correct data, so a buffered write path has to be tested across regions rather than within one. And a write cycle one tick short passes every functional test — only counting the ticks catches it.

15. What Comes Next

The device is silent and the master has to wait. Chapter 16.4 is about how it waits, and the answer is less comfortable than it looks.

The technique is simple: address the device and see whether it answers. The problem is what a lack of answer means. A busy device, an absent device and a wedged device are indistinguishable on the wire — all three produce exactly no acknowledge — so a master that polls until it gets a response is, in the failure case, waiting forever for a device that will never reply.

Which turns a loop into a design decision: how long to wait, and what to report when the wait ends. The honest answer to the second is harder than it sounds, because the one thing a master must not do is claim to know which of the three it was.

Continue learning

Related tutorials