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.
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..7Read 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
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 cyclesThe 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.
// -----------------------------------------------------------------------------
// 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 `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 // -----------------------------------------------------------------------------
// 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 `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 -- ---------------------------------------------------------------------------
-- 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; -- ---------------------------------------------------------------------------
-- 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
$ 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
| # | scenario | what it establishes |
|---|---|---|
| 1 | a write that fits inside one page | the ordinary case; nothing committed until the STOP |
| 2 | exactly sixteen bytes from the page base | no wrap — the last byte lands on the last offset |
| 3 | twenty bytes from 0x08, page size 16 | §2's worked example, location by location |
| 4 | location 0x10 after that write | nothing outside the page is ever touched |
| 5 | the acknowledge on every byte, wrapping ones included | why the corruption is silent — no protocol error exists |
| 6 | every addressing during the write cycle | refused, and the refusals counted |
| 7 | a pointer with no data, then a STOP | commits nothing; starts no write cycle |
| 8 | a read burst across a page boundary | reads have no page structure at all |
| 9 | a smaller page | the wrap happens sooner and the base moves — same formula |
| 10 | two page writes, each to its own page | each base frozen from its own word address; nothing leaks |
| 11 | a START mid-fill | the buffer is abandoned without committing — only a STOP commits |
| 12 | commit accounting over three writes | accumulates rather than resetting |
| 13 | exactly PAGE_SIZE bytes from the base | counter back at the base, zero wraps |
| 14 | a partial write to a page after a full write to another | only the sent bytes are committed |
| 15 | the write cycle's duration | exactly 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 defect | outcome |
|---|---|---|
| C2-1 | the page's high bits not frozen; fills cross pages | killed — 23 checks |
| C2-2 | the low counter carries into the high bits | killed — test 13 |
| C2-3 | the wrap reported on the counter's return | killed — 3 checks |
| C2-4 | each byte committed as it arrives | killed — test 11 |
| C2-5 | the whole buffer committed, valid bits ignored | killed — test 14 |
| C2-6 | no write cycle; the device answers immediately | killed — 9 checks |
| C2-7 | a write cycle started by a data-less STOP | killed — 2 checks |
| C2-8 | polls refused during the cycle but not counted | killed — 1 check |
| C2-9 | the write cycle ends one tick early | killed — test 15 |
| C2-10 | reads wrap within the page too | killed — 1 check |
| C2-11 | the buffer indexed by the full address, not the offset | killed — 32 checks |
| C2-12 | the buffered-byte count carried into the next page write | killed — 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.
// 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;
endgroupThree 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
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.
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.
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
- Related topic
Where I²C Lives — Boards, SoCs and Real Devices
Place the derived bus in a real system: the host controller inside an SoC or FPGA, the regulators, sensors, memories and clock devices attached to it, and what each one is actually doing. The traffic turns out to have a specific shape — control plane, not data plane — and that shape is why the bus remains useful.
- Related topic
EEPROM Access — Word Address, Random Read and Sequential Read
The serial memory the specification names in a footnote, worked out in full. Explains why a current-address read and a random read are byte-for-byte identical on the wire, what happens when a master and a device disagree about how many word-address bytes there are, and why an address past the end of the array wraps silently instead of failing.
- Related topic
UART vs Other Interfaces: Choosing the Right Link
Serial interfaces differ first in where the receiver's timing comes from, then in what organises a shared medium — and capability is paid for in what the system must already provide. A question order for choosing between UART, SPI, I2C, CAN, USB and Ethernet.
- Related topic
Why Chips on a Board Need a Bus
A connection between two chips is not a wire. It is a pin on each package, a routed trace, the board area and layers that trace consumes, and an I/O cell driving it — and all of that is paid for again for every device added. This is the cost structure that makes dedicating an interface per peripheral stop scaling, and that forces a board to share one set of wires instead.
