I²C · Module 16
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.
Chapter 16.1 established that the register-map pattern is one sentence of specification and six questions the datasheet has to answer. This chapter takes the device that sentence actually names — a serial memory — and works it out completely.
A serial EEPROM is the purest form of the pattern. There is no functional behaviour to speak of: it holds bytes, and every interesting thing about it is a convention concerning the pointer. Which makes it the ideal place to find out what those conventions cost when a master gets one wrong.
That is the mechanism the whole chapter uses, and the emphasised sentence is a requirement people routinely miss: the read phase of a combined transfer ends with the master declining the last byte, before the repeated START, not after it.
1. Two Reads That Are Identical on the Wire
An EEPROM offers two read transactions, and the distinction between them is not visible in any capture.
The current-address read. START, address with the read bit, one or more bytes, STOP. No pointer is sent. The device serves from wherever its internal counter already is.
The random read. START, address with the write bit, the word address bytes, repeated START, address with the read bit, one or more bytes, STOP. The write phase exists only for its effect on the pointer — it carries no data at all.
Now look at the second half of a random read in isolation: START, address with the read bit, bytes, STOP. That is a current-address read. Byte for byte.
The device cannot tell them apart either, except by having watched what came before. There is nothing in the read phase that says "a pointer was just written"; the only difference is history. This is why an EEPROM's behaviour depends on transaction sequence in a way that a stateless protocol view cannot capture.
2. How Wide Is the Word Address?
This is the question with the worst failure mode in the module, and it is not one of Chapter 16.1's six — it is prior to all of them.
A 256-byte part needs one address byte. A 32-kilobyte part needs two. And the master has to know which, because the device decides how many bytes it will consume as the pointer before it decides anything else.
Consider a master that believes a part takes one address byte, talking to a part that takes two. The master sends:
| byte | the master means | the device does |
|---|---|---|
| 1 | address, write | acknowledges |
| 2 | word address 0x10 | takes it as the high half of the pointer |
| 3 | data 0xAB | takes it as the low half — the pointer is now 0x10AB |
| 4 | data 0xCD | writes 0xCD at 0x10AB |
| 5 | data 0xEF | writes 0xEF at 0x10AC |
Every byte is acknowledged. The frame is well formed. A scope shows a perfectly conforming transfer. And the master has written two bytes instead of three, at an address it never named, having silently donated its first data byte to the pointer.
The reverse mistake is just as bad and fails differently. A master that sends two address bytes to a one-byte part has its high byte taken as the pointer and its low byte taken as the first data byte — so the data lands at the address the high byte named, shifted by one position in the payload.
There is no acknowledge, no NACK, no status bit and no timing difference that distinguishes either case from correct operation. The only evidence is that the data is in the wrong place, and you have to read it back from the right place to find out.
3. An Address Past the End of the Array
A master that believes a part is larger than it is sends a pointer the device cannot hold. What happens then is worth being precise about, because the answer is neither "an error" nor "undefined".
The excess bits are not refused and not reported, because the wires for them do not exist. The device's counter is as wide as its array, so a pointer that overflows it simply wraps: on a 1024-word part, word address 0x0834 lands at 0x0034.
the master sends 0x08 0x34
assembled pointer 0x0834 = 2100
the device's counter 10 bits = 0..1023
what it holds 0x0834 AND 0x03FF = 0x0034 = 52
so the access lands at 52, and 52 is a real location holding real data.That is the mechanism behind "my write went to the wrong place". It is also why the counter must be masked to the array rather than merely assembled: a truncated word address — a master that sent the high byte and then stopped — otherwise leaves the counter outside the array, and the next read is an out-of-range access rather than a merely wrong one.
4. A Random Read, Drawn
The write phase sends zero data bytes. That is the part people find surprising. It is a write transaction by direction bit and it writes nothing; its entire purpose is to leave the pointer somewhere. A device that required at least one data byte after the pointer could not support a random read.
The pointer survives the repeated START, and note 4 requires the bus logic not to. Exactly the distinction Chapter 16.1 §4 drew: the frame-level state machine resets, the device's counter does not.
The last acknowledge before the read is the EEPROM's. It is answering its own address, and at that instant it is still the receiver. Only the two data bytes' acknowledges belong to the master.
5. A Sequential Read, at Byte Resolution
Sequential read across the array boundary: 0xFE, 0xFF, 0x00, 0x01
9 cyclesTwo properties of that figure matter for a driver.
The rollover is invisible to the master. Bytes three and four arrive exactly like bytes one and two, acknowledged the same way, at the same rate. A master reading four bytes from 0xFE on a 256-byte part gets two bytes from the end of the array and two from the beginning of it, and nothing in the transfer says so. The roll row in the figure is a device output added for the testbench — a real part does not have it.
The master decides where the burst ends, and it ends with a NACK. The device will serve bytes indefinitely; the only thing that stops it is the master declining one. Which is why format 3's emphasised sentence matters: if a repeated START is going to follow, that NACK comes before it.
6. The EEPROM Addressing Model in Three Languages
A word-address engine parameterised on the number of address bytes and the array size, its independent oracle, and both in all three languages. The bench instantiates three parts: a 256-byte part expecting two address bytes, a 256-byte part expecting one, and a 1024-word part expecting two — the last of which is the only one on which the high address byte can be shown to do anything.
// -----------------------------------------------------------------------------
// i2c_eeprom_addressing.sv
// EEPROM word-address handling: multi-byte pointers, the three read forms, and
// the rollover at the end of the array (UM10204 3.1.10 notes 1 and 2).
//
// Chapter 16.1 established the register-pointer model with a ONE-byte pointer,
// which is all an eight-register part needs. An EEPROM has more locations than a
// byte can name, and the specification's note 1 says only that "the internal
// memory location must be written during the first data byte" -- it does not say
// how many bytes the location takes.
//
// So a 64 kbit EEPROM sends a SIXTEEN-bit word address as two bytes, high byte
// first, and the master must know that from the datasheet. There is nothing on the
// bus that distinguishes a two-byte word address followed by data from a one-byte
// word address followed by data: both are "address byte, then some data bytes".
// Getting the width wrong writes the low half of the pointer into location zero.
//
// Three read forms, and the difference between them is what the master omits:
//
// CURRENT ADDRESS READ S, addr+R, data...
// No word address at all. Serves from wherever the internal counter is,
// which is only meaningful because the counter persists -- Chapter 16.1's
// question 3. A read whose result depends on transaction history.
//
// RANDOM READ S, addr+W, hi, lo, Sr, addr+R, data...
// A dummy write that sets the counter, then a repeated START and a read.
// This is UM10204's combined format, and the write phase carries no data --
// its entire purpose is the side effect on the pointer.
//
// SEQUENTIAL READ either of the above, with the master ACKing to continue
// Auto-increment across the whole array, not just within a page. Reads do
// not have the page-boundary behaviour that writes do -- Chapter 16.3 is
// about why those two differ.
//
// The rollover is a device convention: at the top of the array the counter returns
// to zero, so a sequential read that runs off the end silently starts again at the
// beginning rather than failing.
// -----------------------------------------------------------------------------
module i2c_eeprom_addressing #(
parameter int ADDR_BYTES = 2, // word-address width in bytes
parameter int N_WORDS = 256, // array size, for the rollover
parameter int AW = 16, // word-address register width
parameter [6:0] MY_ADDR = 7'h50,
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,
// A tiny backing array, written by the bench so reads have something to return.
input logic load_en,
input logic [AW-1:0] load_addr,
input logic [7:0] load_data,
output logic ack,
output logic [7:0] tx_byte,
output logic tx_valid,
output logic [AW-1:0] word_addr, // the internal address counter
output logic [1:0] addr_bytes_got, // how many pointer bytes have arrived
output logic rolled_over, // the counter wrapped at the array end
output logic was_random_read,// this read was preceded by a pointer write
output logic [2:0] state,
output logic [CNT_W-1:0] bytes_read,
output logic [CNT_W-1:0] bytes_written
);
localparam [2:0] S_IDLE = 3'd0,
S_ADDRH = 3'd1, // collecting word-address bytes
S_DATA = 3'd2, // word address complete; data may follow
S_READ = 3'd3;
// Sized bounds. A part-select of a parameter reads as zero on some tools.
localparam [AW-1:0] ADDR_TOP = N_WORDS - 1;
// The counter lives INSIDE the array. A 256-byte part has an 8-bit counter, so
// a two-byte protocol pointer carries more bits than the device owns and the
// excess wraps. Masking at assembly is what real hardware does -- the wires for
// those bits are simply not there -- and it is also what stops an out-of-range
// array index, which Verilog would read as X and VHDL would refuse outright.
localparam [AW-1:0] ADDR_MASK = N_WORDS - 1;
logic [7:0] mem [0:N_WORDS-1];
integer i;
// Advance with the documented rollover: at the top of the array, back to zero.
task advance_addr;
begin
if (word_addr == ADDR_TOP) begin
word_addr <= {AW{1'b0}};
rolled_over <= 1'b1;
end else begin
word_addr <= word_addr + 1'b1;
end
end
endtask
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;
word_addr <= {AW{1'b0}};
addr_bytes_got <= 2'd0;
rolled_over <= 1'b0;
was_random_read <= 1'b0;
bytes_read <= {CNT_W{1'b0}};
bytes_written <= {CNT_W{1'b0}};
for (i = 0; i < N_WORDS; i = i + 1) mem[i] <= 8'h00;
end else begin
ack <= 1'b0;
// The bench's back door, so a read has something to return.
if (load_en) mem[load_addr] <= load_data;
if (start_seen) begin
// Note 4 again: any START returns the device to expecting an address.
// Crucially the word-address COUNTER survives -- that is what makes
// the random read work, because the pointer set in the write phase
// must still be there after the repeated START.
state <= S_IDLE;
tx_valid <= 1'b0;
addr_bytes_got <= 2'd0;
end else if (stop_seen) begin
state <= S_IDLE;
tx_valid <= 1'b0;
addr_bytes_got <= 2'd0;
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
// A read. Whether this is a CURRENT ADDRESS read or the
// second half of a RANDOM read depends only on whether a
// pointer write preceded it -- and the device cannot tell
// the difference except by having seen it.
tx_byte <= mem[word_addr];
tx_valid <= 1'b1;
state <= S_READ;
end else begin
// A write: the word address comes next, ADDR_BYTES of it.
addr_bytes_got <= 2'd0;
was_random_read <= 1'b0;
state <= S_ADDRH;
end
end
end
// Collect the word address, high byte first. This is the state a
// master with the wrong ADDR_BYTES gets wrong: it sends one byte
// and starts writing data, and the data's first byte is consumed
// as the low half of the pointer.
S_ADDRH: begin
ack <= 1'b1;
if (ADDR_BYTES == 1) begin
word_addr <= {{(AW-8){1'b0}}, byte_in} & ADDR_MASK;
addr_bytes_got <= 2'd1;
was_random_read <= 1'b1;
state <= S_DATA;
end else begin
if (addr_bytes_got == 2'd0) begin
// High byte, shifted into place and masked to the array.
word_addr <= {byte_in, {(AW-8){1'b0}}} & ADDR_MASK;
addr_bytes_got <= 2'd1;
end else begin
// Low byte completes the pointer, masked the same way.
word_addr <= {word_addr[AW-1:8], byte_in} & ADDR_MASK;
addr_bytes_got <= 2'd2;
was_random_read <= 1'b1;
state <= S_DATA;
end
end
end
// Data bytes, if any. A random read sends none -- the write phase
// exists only for its effect on the pointer.
S_DATA: begin
ack <= 1'b1;
mem[word_addr] <= byte_in;
bytes_written <= bytes_written + 1'b1;
advance_addr;
end
default: ;
endcase
end else if (read_byte_done && state == S_READ) begin
bytes_read <= bytes_read + 1'b1;
advance_addr;
if (!master_acked) begin
tx_valid <= 1'b0;
state <= S_IDLE;
end else begin
// Sequential read: the next byte comes from the advanced counter,
// computed here so it is ready for the next transfer.
if (word_addr == ADDR_TOP) tx_byte <= mem[0];
else tx_byte <= mem[word_addr + 1'b1];
end
end
end
end
endmodule `timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_eeprom_addressing_tb.sv
// Independent oracle for i2c_eeprom_addressing.
//
// Two instances with DIFFERENT word-address widths see the same byte stream,
// because the whole point of the chapter is that the bus cannot tell them apart.
// Test 6 drives a two-byte sequence into both and shows the one-byte instance
// consuming the data as the low half of its pointer -- a corruption with no
// protocol error anywhere.
//
// dut2 : ADDR_BYTES = 2 a 64 kbit-class part
// dut1 : ADDR_BYTES = 1 a 2 kbit-class part
// -----------------------------------------------------------------------------
module i2c_eeprom_addressing_tb;
localparam [2:0] S_IDLE = 3'd0, S_ADDRH = 3'd1, S_DATA = 3'd2, S_READ = 3'd3;
localparam [6:0] ADDR = 7'h50;
localparam integer NW = 256;
localparam integer AW = 16;
// A two-byte word address on a 256-byte part is very nearly a contradiction:
// every bit the high byte carries is masked away again, because the device only
// owns eight address bits. So the bench also instantiates a part where the high
// byte genuinely survives -- 1024 words, ten address bits -- which is the only
// configuration in which the high half of the pointer can be shown to work.
localparam integer NWB = 1024;
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 load_en = 1'b0;
logic [AW-1:0] load_addr = 0;
logic [7:0] load_data = 8'h00;
logic a2_ack, a2_txv, a2_roll, a2_rand;
logic [7:0] a2_tx;
logic [AW-1:0] a2_wa;
logic [1:0] a2_got;
logic [2:0] a2_state;
logic [7:0] a2_rd, a2_wr;
logic a1_ack, a1_txv, a1_roll, a1_rand;
logic [7:0] a1_tx;
logic [AW-1:0] a1_wa;
logic [1:0] a1_got;
logic [2:0] a1_state;
logic [7:0] a1_rd, a1_wr;
logic ab_ack, ab_txv, ab_roll, ab_rand;
logic [7:0] ab_tx;
logic [AW-1:0] ab_wa;
logic [1:0] ab_got;
logic [2:0] ab_state;
logic [7:0] ab_rd, ab_wr;
integer errors = 0;
integer n;
i2c_eeprom_addressing #(.ADDR_BYTES(2), .N_WORDS(NW), .AW(AW),
.MY_ADDR(ADDR), .CNT_W(8)) dut2 (
.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),
.load_en(load_en), .load_addr(load_addr), .load_data(load_data),
.ack(a2_ack), .tx_byte(a2_tx), .tx_valid(a2_txv), .word_addr(a2_wa),
.addr_bytes_got(a2_got), .rolled_over(a2_roll),
.was_random_read(a2_rand), .state(a2_state),
.bytes_read(a2_rd), .bytes_written(a2_wr));
i2c_eeprom_addressing #(.ADDR_BYTES(1), .N_WORDS(NW), .AW(AW),
.MY_ADDR(ADDR), .CNT_W(8)) dut1 (
.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),
.load_en(load_en), .load_addr(load_addr), .load_data(load_data),
.ack(a1_ack), .tx_byte(a1_tx), .tx_valid(a1_txv), .word_addr(a1_wa),
.addr_bytes_got(a1_got), .rolled_over(a1_roll),
.was_random_read(a1_rand), .state(a1_state),
.bytes_read(a1_rd), .bytes_written(a1_wr));
// The 1024-word part: two address bytes, and ten address bits to put them in.
i2c_eeprom_addressing #(.ADDR_BYTES(2), .N_WORDS(NWB), .AW(AW),
.MY_ADDR(ADDR), .CNT_W(8)) dutB (
.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),
.load_en(load_en), .load_addr(load_addr), .load_data(load_data),
.ack(ab_ack), .tx_byte(ab_tx), .tx_valid(ab_txv), .word_addr(ab_wa),
.addr_bytes_got(ab_got), .rolled_over(ab_roll),
.was_random_read(ab_rand), .state(ab_state),
.bytes_read(ab_rd), .bytes_written(ab_wr));
always #5 clk = ~clk;
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;
load_en = 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
// Preload both instances' arrays through the back door.
task preload (input integer a, input [7:0] d);
begin
@(negedge clk); load_en = 1'b1; load_addr = a[AW-1:0]; load_data = d;
@(posedge clk); @(negedge clk); load_en = 1'b0;
end
endtask
task ck_int (input [200*8:1] what, input integer got, input integer exp);
begin
if (got !== exp) begin
$display(" FAIL %0s: got %0d (0x%0h) expected %0d (0x%0h)", what, got, got, exp, exp);
errors = errors + 1;
end
end
endtask
task ck_bit (input [200*8:1] what, input got, input exp);
begin
if (got !== exp) begin
$display(" FAIL %0s: got %0b expected %0b", what, got, exp);
errors = errors + 1;
end
end
endtask
initial begin
$display("=== i2c_eeprom_addressing: three read forms and a pointer the bus cannot size ===");
// ----------------------------------------------------------------
// T1. A two-byte word address is assembled high byte first.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(1'b0);
ck_int("T1 collecting the word address", a2_state, S_ADDRH);
send_data(8'h00); // high byte
ck_int("T1 one pointer byte so far", a2_got, 1);
send_data(8'h2A); // low byte
$display("T1 a two-byte word address, high byte first");
ck_int("T1 pointer assembled", a2_wa, 16'h002A);
ck_int("T1 two pointer bytes", a2_got, 2);
ck_int("T1 now accepting data", a2_state, S_DATA);
ev_stop;
// ----------------------------------------------------------------
// T2. A WRITE: pointer then data, with the counter advancing.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h10); // word address 0x0010
send_data(8'hA0); send_data(8'hA1); send_data(8'hA2);
$display("T2 a write: pointer, then data, counter advancing");
ck_int("T2 three bytes written", a2_wr, 3);
ck_int("T2 counter advanced past them", a2_wa, 16'h0013);
ev_stop;
// read them back with a random read
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h10);
ev_start;
send_addr(1'b1);
ck_int("T2 read back 0xA0", a2_tx, 8'hA0); take(1'b1);
ck_int("T2 read back 0xA1", a2_tx, 8'hA1); take(1'b1);
ck_int("T2 read back 0xA2", a2_tx, 8'hA2); take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T3. RANDOM READ. The write phase carries NO data -- its whole purpose is
// the side effect on the pointer, and then a repeated START turns the
// transfer around. This is UM10204's combined format.
// ----------------------------------------------------------------
do_reset;
preload(16'h0080, 8'h5A);
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h80); // pointer only, no data
ck_int("T3 pointer set, no data written", a2_wr, 0);
ck_bit("T3 marked as a pointer write", a2_rand, 1'b1);
ev_start; // repeated START
send_addr(1'b1);
$display("T3 a random read: a dummy write for its side effect, then Sr");
ck_bit("T3 serving", a2_txv, 1'b1);
ck_int("T3 serves the addressed location", a2_tx, 8'h5A);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T4. CURRENT ADDRESS READ. No word address at all. It works only because
// the counter survived the STOP, so the result depends on history.
// ----------------------------------------------------------------
do_reset;
preload(16'h0040, 8'h11);
preload(16'h0041, 8'h22);
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h40);
ev_stop; // pointer left at 0x0040
ck_int("T4 counter survived the STOP", a2_wa, 16'h0040);
ev_start;
send_addr(1'b1); // a bare read, no pointer
$display("T4 a current address read: no pointer, so history decides");
ck_int("T4 serves 0x0040", a2_tx, 8'h11);
take(1'b1);
ck_int("T4 then 0x0041", a2_tx, 8'h22);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T5. SEQUENTIAL READ across many locations. Reads auto-increment through
// the whole array -- there is no page boundary on a read.
// ----------------------------------------------------------------
do_reset;
for (n = 0; n < 20; n = n + 1) preload(16'h0000 + n[AW-1:0], 8'h70 + n[7:0]);
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h00);
ev_start;
send_addr(1'b1);
$display("T5 a sequential read runs across the array, not a page");
for (n = 0; n < 20; n = n + 1) begin
ck_int("T5 sequential byte", a2_tx, 8'h70 + n[7:0]);
take(n < 19);
end
ck_int("T5 twenty bytes served", a2_rd, 20);
ev_stop;
// ----------------------------------------------------------------
// T6. THE CHAPTER'S CENTRAL HAZARD. The same two-byte pointer sequence
// driven into a ONE-byte device. Nothing on the bus is wrong; every
// byte is acknowledged; and the second pointer byte is consumed as
// DATA at the location the first byte named.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(1'b0);
ck_bit("T6 both devices acknowledged", a2_ack, 1'b1);
ck_bit("T6 both devices acknowledged (1-byte)", a1_ack, 1'b1);
send_data(8'h01); // intended as the HIGH byte
ck_int("T6 two-byte device: half a pointer", a2_got, 1);
ck_int("T6 one-byte device: a COMPLETE pointer", a1_got, 1);
ck_int("T6 one-byte device points at 0x0001", a1_wa, 16'h0001);
ck_int("T6 one-byte device now expects data", a1_state, S_DATA);
send_data(8'h20); // intended as the LOW byte
$display("T6 a two-byte pointer sent to a one-byte device");
// 0x0120 is beyond a 256-byte array, so the counter -- which is only as wide
// as the array -- holds 0x20. A pointer wider than the device wraps into it.
ck_int("T6 two-byte pointer 0x0120 wraps to 0x0020", a2_wa, 16'h0020);
ck_bit("T6 one-byte device acknowledged it as data", a1_ack, 1'b1);
ck_int("T6 one-byte device WROTE the low byte as data", a1_wr, 1);
ev_stop;
// and the corruption is at location 1, which nothing on the bus reported
ev_start;
send_addr(1'b0);
send_data(8'h01);
ev_start;
send_addr(1'b1);
ck_int("T6 location 0x0001 holds the stray pointer byte", a1_tx, 8'h20);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T7. THE ROLLOVER. A sequential read from the last location returns to
// zero rather than failing, so a read that runs off the end silently
// starts again.
// ----------------------------------------------------------------
do_reset;
preload(16'h00FF, 8'hEE);
preload(16'h0000, 8'h01);
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'hFF); // the last location
ev_start;
send_addr(1'b1);
$display("T7 a sequential read rolls over instead of ending");
ck_int("T7 the last location", a2_tx, 8'hEE);
take(1'b1);
ck_int("T7 back at location zero", a2_tx, 8'h01);
ck_bit("T7 rollover reported", a2_roll, 1'b1);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T8. A write also rolls over at the array end. Chapter 16.3 shows that a
// write ALSO wraps at a page boundary, which is a different and much
// smaller modulus -- two wraps, two causes.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'hFE);
send_data(8'hD0); send_data(8'hD1); send_data(8'hD2);
$display("T8 a write rolls over at the end of the array too");
ck_bit("T8 rollover reported", a2_roll, 1'b1);
ck_int("T8 counter is back near zero", a2_wa, 16'h0001);
ev_stop;
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h00);
ev_start;
send_addr(1'b1);
ck_int("T8 location zero got the third byte", a2_tx, 8'hD2);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T9. A repeated START abandons a partial word address. Half a pointer is
// not a pointer, and the counter must not be left half-updated for the
// read that follows.
// ----------------------------------------------------------------
do_reset;
preload(16'h0000, 8'h99);
ev_start;
send_addr(1'b0);
send_data(8'h07); // high byte only
ck_int("T9 half a pointer collected", a2_got, 1);
ev_start; // abandon it
$display("T9 a repeated START abandons a partial word address");
ck_int("T9 pointer-byte count cleared", a2_got, 0);
ck_int("T9 back to expecting an address", a2_state, S_IDLE);
send_addr(1'b1);
// the counter still holds whatever the partial write left, which is exactly
// why a master must never rely on an abandoned pointer
ck_bit("T9 serving something", a2_txv, 1'b1);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T10. A wrong device address is ignored by both instances.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); byte_in = {7'h51, 1'b0}; is_addr_byte = 1'b1; byte_valid = 1'b1;
@(posedge clk); @(negedge clk); byte_valid = 1'b0; is_addr_byte = 1'b0;
$display("T10 another device's address is ignored");
ck_bit("T10 two-byte instance silent", a2_ack, 1'b0);
ck_bit("T10 one-byte instance silent", a1_ack, 1'b0);
ck_int("T10 both idle", a2_state, S_IDLE);
// ----------------------------------------------------------------
// T11. A random read followed by a sequential read with no new pointer:
// the second read continues from where the first stopped.
// ----------------------------------------------------------------
do_reset;
for (n = 0; n < 8; n = n + 1) preload(16'h0030 + n[AW-1:0], 8'hB0 + n[7:0]);
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h30);
ev_start;
send_addr(1'b1);
ck_int("T11 first read", a2_tx, 8'hB0); take(1'b1);
ck_int("T11 second read", a2_tx, 8'hB1); take(1'b0);
ev_stop;
$display("T11 a later bare read continues where the last one stopped");
ev_start;
send_addr(1'b1); // bare read, no pointer
ck_int("T11 continues at 0x0032", a2_tx, 8'hB2); take(1'b1);
ck_int("T11 and 0x0033", a2_tx, 8'hB3); take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T12. Counters. Reads and writes are counted separately, because a random
// read's write phase writes nothing and must not inflate the count.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h05);
send_data(8'hC0); send_data(8'hC1);
ev_stop;
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h05); // pointer only
ev_start;
send_addr(1'b1);
take(1'b1); take(1'b0);
ev_stop;
$display("T12 a random read's write phase writes nothing");
ck_int("T12 two bytes written in total", a2_wr, 2);
ck_int("T12 two bytes read", a2_rd, 2);
// ----------------------------------------------------------------
// T13. THE HIGH ADDRESS BYTE, ON A PART THAT HAS SOMEWHERE TO PUT IT. On the
// 256-byte part above, every bit the high byte carries is masked away
// again -- the device owns eight address bits and the protocol handed it
// sixteen. So nothing there can tell a device that assembles the pointer
// correctly from one that throws the high byte away.
//
// On the 1024-word part it matters: word address 0x0210 has to land at
// 0x0210, not at 0x0010.
// ----------------------------------------------------------------
do_reset;
ev_start; send_addr(1'b0);
send_data(8'h02); // high byte
ck_int("T13 the high byte is in place and masked to the array", ab_wa, 16'h0200);
send_data(8'h10); // low byte completes it
$display("T13 the high address byte, on a part with somewhere to put it");
ck_int("T13 the assembled pointer keeps both halves", ab_wa, 16'h0210);
// The SAME two bytes, at the same instant, on the 256-byte part: the high
// half is gone, because that part has no bits to hold it. One master, one
// byte stream, two different locations -- decided entirely by the part.
ck_int("T13 the 256-byte part holds only the low half", a2_wa, 16'h0010);
send_data(8'h7E); // and a data byte lands there
ev_stop;
// Read it back with a random read, to prove the byte went where the pointer said.
ev_start; send_addr(1'b0); send_data(8'h02); send_data(8'h10);
ev_start; send_addr(1'b1);
ck_int("T13 and the data byte went to that address", ab_tx, 8'h7E);
take(1'b0); ev_stop;
// ----------------------------------------------------------------
// T14. AN ADDRESS BEYOND THE END OF THE ARRAY. A master that believes the
// part is larger than it is sends a pointer the device cannot hold. The
// excess bits are not refused and not reported -- the wires for them do
// not exist -- so the access silently WRAPS to somewhere inside the
// array. This is the mechanism behind "my write went to the wrong
// place", and it is also why the counter must be masked rather than
// merely assembled.
// ----------------------------------------------------------------
do_reset;
preload(16'h0000, 8'hA5);
preload(16'h0034, 8'h5A);
// 0x0834 on a 1024-word part: the top two bits are gone, leaving 0x0034.
ev_start; send_addr(1'b0); send_data(8'h08); send_data(8'h34);
$display("T14 an address past the end of the array wraps into it");
ck_int("T14 0x0834 wrapped to 0x0034 on the 1024-word part", ab_wa, 16'h0034);
ev_start; send_addr(1'b1);
ck_int("T14 and reads the byte that lives there", ab_tx, 8'h5A);
take(1'b0); ev_stop;
// A truncated word address: the master sent the high byte and then stopped.
// The counter must still hold a value INSIDE the array, or the next read is
// an out-of-range access rather than a wrong one.
do_reset;
preload(16'h0000, 8'hA5);
ev_start; send_addr(1'b0); send_data(8'h08); ev_stop;
ck_int("T14 a truncated pointer is still inside the array", ab_wa, 16'h0000);
ev_start; send_addr(1'b1);
ck_int("T14 so the read that follows is wrong, not undefined", ab_tx, 8'hA5);
take(1'b0); ev_stop;
ck_int("T14 the 256-byte part did the same", a2_wa, 16'h0001);
// ----------------------------------------------------------------
// T15. WHAT was_random_read ACTUALLY MEANS. The device cannot tell a current
// address read from the read half of a random read by looking at the
// read -- they are byte-for-byte identical on the bus. The only
// difference is whether a pointer write came first, and the flag records
// exactly that and nothing more. A flag that were set on every read, or
// that survived into the next transaction, would say nothing.
// ----------------------------------------------------------------
do_reset;
preload(16'h0005, 8'h77);
// A bare current-address read. No pointer write preceded it.
ev_start; send_addr(1'b1);
$display("T15 the flag records that a pointer write came first, nothing more");
ck_bit("T15 a current-address read is not a random read", a2_rand, 1'b0);
take(1'b0); ev_stop;
// Now a real random read.
ev_start; send_addr(1'b0); send_data(8'h00); send_data(8'h05);
ck_bit("T15 a completed pointer write makes the next read random", a2_rand, 1'b1);
ev_start; send_addr(1'b1);
ck_int("T15 and it reads the addressed byte", a2_tx, 8'h77);
take(1'b0); ev_stop;
ck_bit("T15 the flag is still set after the read", a2_rand, 1'b1);
// A new write phase must clear it BEFORE the new pointer is complete --
// otherwise the flag is describing the previous transaction.
ev_start; send_addr(1'b0);
send_data(8'h00); // high byte only: pointer incomplete
ck_bit("T15 a new write phase clears it at once", a2_rand, 1'b0);
ck_int("T15 with the pointer still half-collected", a2_got, 2'd1);
send_data(8'h05); // low byte completes it
ck_bit("T15 and it is set again once the pointer is complete", a2_rand, 1'b1);
ev_stop;
if (errors == 0)
$display("=== i2c_eeprom_addressing: ALL CHECKS PASSED ===");
else
$display("=== i2c_eeprom_addressing: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule // -----------------------------------------------------------------------------
// i2c_eeprom_addressing.sv
// EEPROM word-address handling: multi-byte pointers, the three read forms, and
// the rollover at the end of the array (UM10204 3.1.10 notes 1 and 2).
//
// Chapter 16.1 established the register-pointer model with a ONE-byte pointer,
// which is all an eight-register part needs. An EEPROM has more locations than a
// byte can name, and the specification's note 1 says only that "the internal
// memory location must be written during the first data byte" -- it does not say
// how many bytes the location takes.
//
// So a 64 kbit EEPROM sends a SIXTEEN-bit word address as two bytes, high byte
// first, and the master must know that from the datasheet. There is nothing on the
// bus that distinguishes a two-byte word address followed by data from a one-byte
// word address followed by data: both are "address byte, then some data bytes".
// Getting the width wrong writes the low half of the pointer into location zero.
//
// Three read forms, and the difference between them is what the master omits:
//
// CURRENT ADDRESS READ S, addr+R, data...
// No word address at all. Serves from wherever the internal counter is,
// which is only meaningful because the counter persists -- Chapter 16.1's
// question 3. A read whose result depends on transaction history.
//
// RANDOM READ S, addr+W, hi, lo, Sr, addr+R, data...
// A dummy write that sets the counter, then a repeated START and a read.
// This is UM10204's combined format, and the write phase carries no data --
// its entire purpose is the side effect on the pointer.
//
// SEQUENTIAL READ either of the above, with the master ACKing to continue
// Auto-increment across the whole array, not just within a page. Reads do
// not have the page-boundary behaviour that writes do -- Chapter 16.3 is
// about why those two differ.
//
// The rollover is a device convention: at the top of the array the counter returns
// to zero, so a sequential read that runs off the end silently starts again at the
// beginning rather than failing.
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_eeprom_addressing #(
parameter ADDR_BYTES = 2, // word-address width in bytes
parameter N_WORDS = 256, // array size, for the rollover
parameter AW = 16, // word-address register width
parameter [6:0] MY_ADDR = 7'h50,
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,
// A tiny backing array, written by the bench so reads have something to return.
input wire load_en,
input wire [AW-1:0] load_addr,
input wire [7:0] load_data,
output reg ack,
output reg [7:0] tx_byte,
output reg tx_valid,
output reg [AW-1:0] word_addr, // the internal address counter
output reg [1:0] addr_bytes_got, // how many pointer bytes have arrived
output reg rolled_over, // the counter wrapped at the array end
output reg was_random_read,// this read was preceded by a pointer write
output reg [2:0] state,
output reg [CNT_W-1:0] bytes_read,
output reg [CNT_W-1:0] bytes_written
);
localparam [2:0] S_IDLE = 3'd0,
S_ADDRH = 3'd1, // collecting word-address bytes
S_DATA = 3'd2, // word address complete; data may follow
S_READ = 3'd3;
// Sized bounds. A part-select of a parameter reads as zero on some tools.
localparam [AW-1:0] ADDR_TOP = N_WORDS - 1;
// The counter lives INSIDE the array. A 256-byte part has an 8-bit counter, so
// a two-byte protocol pointer carries more bits than the device owns and the
// excess wraps. Masking at assembly is what real hardware does -- the wires for
// those bits are simply not there -- and it is also what stops an out-of-range
// array index, which Verilog would read as X and VHDL would refuse outright.
localparam [AW-1:0] ADDR_MASK = N_WORDS - 1;
reg [7:0] mem [0:N_WORDS-1];
integer i;
// Advance with the documented rollover: at the top of the array, back to zero.
task advance_addr;
begin
if (word_addr == ADDR_TOP) begin
word_addr <= {AW{1'b0}};
rolled_over <= 1'b1;
end else begin
word_addr <= word_addr + 1'b1;
end
end
endtask
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;
word_addr <= {AW{1'b0}};
addr_bytes_got <= 2'd0;
rolled_over <= 1'b0;
was_random_read <= 1'b0;
bytes_read <= {CNT_W{1'b0}};
bytes_written <= {CNT_W{1'b0}};
for (i = 0; i < N_WORDS; i = i + 1) mem[i] <= 8'h00;
end else begin
ack <= 1'b0;
// The bench's back door, so a read has something to return.
if (load_en) mem[load_addr] <= load_data;
if (start_seen) begin
// Note 4 again: any START returns the device to expecting an address.
// Crucially the word-address COUNTER survives -- that is what makes
// the random read work, because the pointer set in the write phase
// must still be there after the repeated START.
state <= S_IDLE;
tx_valid <= 1'b0;
addr_bytes_got <= 2'd0;
end else if (stop_seen) begin
state <= S_IDLE;
tx_valid <= 1'b0;
addr_bytes_got <= 2'd0;
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
// A read. Whether this is a CURRENT ADDRESS read or the
// second half of a RANDOM read depends only on whether a
// pointer write preceded it -- and the device cannot tell
// the difference except by having seen it.
tx_byte <= mem[word_addr];
tx_valid <= 1'b1;
state <= S_READ;
end else begin
// A write: the word address comes next, ADDR_BYTES of it.
addr_bytes_got <= 2'd0;
was_random_read <= 1'b0;
state <= S_ADDRH;
end
end
end
// Collect the word address, high byte first. This is the state a
// master with the wrong ADDR_BYTES gets wrong: it sends one byte
// and starts writing data, and the data's first byte is consumed
// as the low half of the pointer.
S_ADDRH: begin
ack <= 1'b1;
if (ADDR_BYTES == 1) begin
word_addr <= {{(AW-8){1'b0}}, byte_in} & ADDR_MASK;
addr_bytes_got <= 2'd1;
was_random_read <= 1'b1;
state <= S_DATA;
end else begin
if (addr_bytes_got == 2'd0) begin
// High byte, shifted into place and masked to the array.
word_addr <= {byte_in, {(AW-8){1'b0}}} & ADDR_MASK;
addr_bytes_got <= 2'd1;
end else begin
// Low byte completes the pointer, masked the same way.
word_addr <= {word_addr[AW-1:8], byte_in} & ADDR_MASK;
addr_bytes_got <= 2'd2;
was_random_read <= 1'b1;
state <= S_DATA;
end
end
end
// Data bytes, if any. A random read sends none -- the write phase
// exists only for its effect on the pointer.
S_DATA: begin
ack <= 1'b1;
mem[word_addr] <= byte_in;
bytes_written <= bytes_written + 1'b1;
advance_addr;
end
default: ;
endcase
end else if (read_byte_done && state == S_READ) begin
bytes_read <= bytes_read + 1'b1;
advance_addr;
if (!master_acked) begin
tx_valid <= 1'b0;
state <= S_IDLE;
end else begin
// Sequential read: the next byte comes from the advanced counter,
// computed here so it is ready for the next transfer.
if (word_addr == ADDR_TOP) tx_byte <= mem[0];
else tx_byte <= mem[word_addr + 1'b1];
end
end
end
end
endmodule `timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_eeprom_addressing_tb.sv
// Independent oracle for i2c_eeprom_addressing.
//
// Two instances with DIFFERENT word-address widths see the same byte stream,
// because the whole point of the chapter is that the bus cannot tell them apart.
// Test 6 drives a two-byte sequence into both and shows the one-byte instance
// consuming the data as the low half of its pointer -- a corruption with no
// protocol error anywhere.
//
// dut2 : ADDR_BYTES = 2 a 64 kbit-class part
// dut1 : ADDR_BYTES = 1 a 2 kbit-class part
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_eeprom_addressing_tb;
localparam [2:0] S_IDLE = 3'd0, S_ADDRH = 3'd1, S_DATA = 3'd2, S_READ = 3'd3;
localparam [6:0] ADDR = 7'h50;
localparam integer NW = 256;
localparam integer AW = 16;
// A two-byte word address on a 256-byte part is very nearly a contradiction:
// every bit the high byte carries is masked away again, because the device only
// owns eight address bits. So the bench also instantiates a part where the high
// byte genuinely survives -- 1024 words, ten address bits -- which is the only
// configuration in which the high half of the pointer can be shown to work.
localparam integer NWB = 1024;
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;
reg load_en = 1'b0;
reg [AW-1:0] load_addr = 0;
reg [7:0] load_data = 8'h00;
wire a2_ack, a2_txv, a2_roll, a2_rand;
wire [7:0] a2_tx;
wire [AW-1:0] a2_wa;
wire [1:0] a2_got;
wire [2:0] a2_state;
wire [7:0] a2_rd, a2_wr;
wire a1_ack, a1_txv, a1_roll, a1_rand;
wire [7:0] a1_tx;
wire [AW-1:0] a1_wa;
wire [1:0] a1_got;
wire [2:0] a1_state;
wire [7:0] a1_rd, a1_wr;
wire ab_ack, ab_txv, ab_roll, ab_rand;
wire [7:0] ab_tx;
wire [AW-1:0] ab_wa;
wire [1:0] ab_got;
wire [2:0] ab_state;
wire [7:0] ab_rd, ab_wr;
integer errors = 0;
integer n;
i2c_eeprom_addressing #(.ADDR_BYTES(2), .N_WORDS(NW), .AW(AW),
.MY_ADDR(ADDR), .CNT_W(8)) dut2 (
.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),
.load_en(load_en), .load_addr(load_addr), .load_data(load_data),
.ack(a2_ack), .tx_byte(a2_tx), .tx_valid(a2_txv), .word_addr(a2_wa),
.addr_bytes_got(a2_got), .rolled_over(a2_roll),
.was_random_read(a2_rand), .state(a2_state),
.bytes_read(a2_rd), .bytes_written(a2_wr));
i2c_eeprom_addressing #(.ADDR_BYTES(1), .N_WORDS(NW), .AW(AW),
.MY_ADDR(ADDR), .CNT_W(8)) dut1 (
.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),
.load_en(load_en), .load_addr(load_addr), .load_data(load_data),
.ack(a1_ack), .tx_byte(a1_tx), .tx_valid(a1_txv), .word_addr(a1_wa),
.addr_bytes_got(a1_got), .rolled_over(a1_roll),
.was_random_read(a1_rand), .state(a1_state),
.bytes_read(a1_rd), .bytes_written(a1_wr));
// The 1024-word part: two address bytes, and ten address bits to put them in.
i2c_eeprom_addressing #(.ADDR_BYTES(2), .N_WORDS(NWB), .AW(AW),
.MY_ADDR(ADDR), .CNT_W(8)) dutB (
.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),
.load_en(load_en), .load_addr(load_addr), .load_data(load_data),
.ack(ab_ack), .tx_byte(ab_tx), .tx_valid(ab_txv), .word_addr(ab_wa),
.addr_bytes_got(ab_got), .rolled_over(ab_roll),
.was_random_read(ab_rand), .state(ab_state),
.bytes_read(ab_rd), .bytes_written(ab_wr));
always #5 clk = ~clk;
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;
load_en = 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
// Preload both instances' arrays through the back door.
task preload (input integer a, input [7:0] d);
begin
@(negedge clk); load_en = 1'b1; load_addr = a[AW-1:0]; load_data = d;
@(posedge clk); @(negedge clk); load_en = 1'b0;
end
endtask
task ck_int (input [200*8:1] what, input integer got, input integer exp);
begin
if (got !== exp) begin
$display(" FAIL %0s: got %0d (0x%0h) expected %0d (0x%0h)", what, got, got, exp, exp);
errors = errors + 1;
end
end
endtask
task ck_bit (input [200*8:1] what, input got, input exp);
begin
if (got !== exp) begin
$display(" FAIL %0s: got %0b expected %0b", what, got, exp);
errors = errors + 1;
end
end
endtask
initial begin
$display("=== i2c_eeprom_addressing: three read forms and a pointer the bus cannot size ===");
// ----------------------------------------------------------------
// T1. A two-byte word address is assembled high byte first.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(1'b0);
ck_int("T1 collecting the word address", a2_state, S_ADDRH);
send_data(8'h00); // high byte
ck_int("T1 one pointer byte so far", a2_got, 1);
send_data(8'h2A); // low byte
$display("T1 a two-byte word address, high byte first");
ck_int("T1 pointer assembled", a2_wa, 16'h002A);
ck_int("T1 two pointer bytes", a2_got, 2);
ck_int("T1 now accepting data", a2_state, S_DATA);
ev_stop;
// ----------------------------------------------------------------
// T2. A WRITE: pointer then data, with the counter advancing.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h10); // word address 0x0010
send_data(8'hA0); send_data(8'hA1); send_data(8'hA2);
$display("T2 a write: pointer, then data, counter advancing");
ck_int("T2 three bytes written", a2_wr, 3);
ck_int("T2 counter advanced past them", a2_wa, 16'h0013);
ev_stop;
// read them back with a random read
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h10);
ev_start;
send_addr(1'b1);
ck_int("T2 read back 0xA0", a2_tx, 8'hA0); take(1'b1);
ck_int("T2 read back 0xA1", a2_tx, 8'hA1); take(1'b1);
ck_int("T2 read back 0xA2", a2_tx, 8'hA2); take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T3. RANDOM READ. The write phase carries NO data -- its whole purpose is
// the side effect on the pointer, and then a repeated START turns the
// transfer around. This is UM10204's combined format.
// ----------------------------------------------------------------
do_reset;
preload(16'h0080, 8'h5A);
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h80); // pointer only, no data
ck_int("T3 pointer set, no data written", a2_wr, 0);
ck_bit("T3 marked as a pointer write", a2_rand, 1'b1);
ev_start; // repeated START
send_addr(1'b1);
$display("T3 a random read: a dummy write for its side effect, then Sr");
ck_bit("T3 serving", a2_txv, 1'b1);
ck_int("T3 serves the addressed location", a2_tx, 8'h5A);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T4. CURRENT ADDRESS READ. No word address at all. It works only because
// the counter survived the STOP, so the result depends on history.
// ----------------------------------------------------------------
do_reset;
preload(16'h0040, 8'h11);
preload(16'h0041, 8'h22);
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h40);
ev_stop; // pointer left at 0x0040
ck_int("T4 counter survived the STOP", a2_wa, 16'h0040);
ev_start;
send_addr(1'b1); // a bare read, no pointer
$display("T4 a current address read: no pointer, so history decides");
ck_int("T4 serves 0x0040", a2_tx, 8'h11);
take(1'b1);
ck_int("T4 then 0x0041", a2_tx, 8'h22);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T5. SEQUENTIAL READ across many locations. Reads auto-increment through
// the whole array -- there is no page boundary on a read.
// ----------------------------------------------------------------
do_reset;
for (n = 0; n < 20; n = n + 1) preload(16'h0000 + n[AW-1:0], 8'h70 + n[7:0]);
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h00);
ev_start;
send_addr(1'b1);
$display("T5 a sequential read runs across the array, not a page");
for (n = 0; n < 20; n = n + 1) begin
ck_int("T5 sequential byte", a2_tx, 8'h70 + n[7:0]);
take(n < 19);
end
ck_int("T5 twenty bytes served", a2_rd, 20);
ev_stop;
// ----------------------------------------------------------------
// T6. THE CHAPTER'S CENTRAL HAZARD. The same two-byte pointer sequence
// driven into a ONE-byte device. Nothing on the bus is wrong; every
// byte is acknowledged; and the second pointer byte is consumed as
// DATA at the location the first byte named.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(1'b0);
ck_bit("T6 both devices acknowledged", a2_ack, 1'b1);
ck_bit("T6 both devices acknowledged (1-byte)", a1_ack, 1'b1);
send_data(8'h01); // intended as the HIGH byte
ck_int("T6 two-byte device: half a pointer", a2_got, 1);
ck_int("T6 one-byte device: a COMPLETE pointer", a1_got, 1);
ck_int("T6 one-byte device points at 0x0001", a1_wa, 16'h0001);
ck_int("T6 one-byte device now expects data", a1_state, S_DATA);
send_data(8'h20); // intended as the LOW byte
$display("T6 a two-byte pointer sent to a one-byte device");
// 0x0120 is beyond a 256-byte array, so the counter -- which is only as wide
// as the array -- holds 0x20. A pointer wider than the device wraps into it.
ck_int("T6 two-byte pointer 0x0120 wraps to 0x0020", a2_wa, 16'h0020);
ck_bit("T6 one-byte device acknowledged it as data", a1_ack, 1'b1);
ck_int("T6 one-byte device WROTE the low byte as data", a1_wr, 1);
ev_stop;
// and the corruption is at location 1, which nothing on the bus reported
ev_start;
send_addr(1'b0);
send_data(8'h01);
ev_start;
send_addr(1'b1);
ck_int("T6 location 0x0001 holds the stray pointer byte", a1_tx, 8'h20);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T7. THE ROLLOVER. A sequential read from the last location returns to
// zero rather than failing, so a read that runs off the end silently
// starts again.
// ----------------------------------------------------------------
do_reset;
preload(16'h00FF, 8'hEE);
preload(16'h0000, 8'h01);
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'hFF); // the last location
ev_start;
send_addr(1'b1);
$display("T7 a sequential read rolls over instead of ending");
ck_int("T7 the last location", a2_tx, 8'hEE);
take(1'b1);
ck_int("T7 back at location zero", a2_tx, 8'h01);
ck_bit("T7 rollover reported", a2_roll, 1'b1);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T8. A write also rolls over at the array end. Chapter 16.3 shows that a
// write ALSO wraps at a page boundary, which is a different and much
// smaller modulus -- two wraps, two causes.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'hFE);
send_data(8'hD0); send_data(8'hD1); send_data(8'hD2);
$display("T8 a write rolls over at the end of the array too");
ck_bit("T8 rollover reported", a2_roll, 1'b1);
ck_int("T8 counter is back near zero", a2_wa, 16'h0001);
ev_stop;
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h00);
ev_start;
send_addr(1'b1);
ck_int("T8 location zero got the third byte", a2_tx, 8'hD2);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T9. A repeated START abandons a partial word address. Half a pointer is
// not a pointer, and the counter must not be left half-updated for the
// read that follows.
// ----------------------------------------------------------------
do_reset;
preload(16'h0000, 8'h99);
ev_start;
send_addr(1'b0);
send_data(8'h07); // high byte only
ck_int("T9 half a pointer collected", a2_got, 1);
ev_start; // abandon it
$display("T9 a repeated START abandons a partial word address");
ck_int("T9 pointer-byte count cleared", a2_got, 0);
ck_int("T9 back to expecting an address", a2_state, S_IDLE);
send_addr(1'b1);
// the counter still holds whatever the partial write left, which is exactly
// why a master must never rely on an abandoned pointer
ck_bit("T9 serving something", a2_txv, 1'b1);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T10. A wrong device address is ignored by both instances.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); byte_in = {7'h51, 1'b0}; is_addr_byte = 1'b1; byte_valid = 1'b1;
@(posedge clk); @(negedge clk); byte_valid = 1'b0; is_addr_byte = 1'b0;
$display("T10 another device's address is ignored");
ck_bit("T10 two-byte instance silent", a2_ack, 1'b0);
ck_bit("T10 one-byte instance silent", a1_ack, 1'b0);
ck_int("T10 both idle", a2_state, S_IDLE);
// ----------------------------------------------------------------
// T11. A random read followed by a sequential read with no new pointer:
// the second read continues from where the first stopped.
// ----------------------------------------------------------------
do_reset;
for (n = 0; n < 8; n = n + 1) preload(16'h0030 + n[AW-1:0], 8'hB0 + n[7:0]);
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h30);
ev_start;
send_addr(1'b1);
ck_int("T11 first read", a2_tx, 8'hB0); take(1'b1);
ck_int("T11 second read", a2_tx, 8'hB1); take(1'b0);
ev_stop;
$display("T11 a later bare read continues where the last one stopped");
ev_start;
send_addr(1'b1); // bare read, no pointer
ck_int("T11 continues at 0x0032", a2_tx, 8'hB2); take(1'b1);
ck_int("T11 and 0x0033", a2_tx, 8'hB3); take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T12. Counters. Reads and writes are counted separately, because a random
// read's write phase writes nothing and must not inflate the count.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h05);
send_data(8'hC0); send_data(8'hC1);
ev_stop;
ev_start;
send_addr(1'b0);
send_data(8'h00); send_data(8'h05); // pointer only
ev_start;
send_addr(1'b1);
take(1'b1); take(1'b0);
ev_stop;
$display("T12 a random read's write phase writes nothing");
ck_int("T12 two bytes written in total", a2_wr, 2);
ck_int("T12 two bytes read", a2_rd, 2);
// ----------------------------------------------------------------
// T13. THE HIGH ADDRESS BYTE, ON A PART THAT HAS SOMEWHERE TO PUT IT. On the
// 256-byte part above, every bit the high byte carries is masked away
// again -- the device owns eight address bits and the protocol handed it
// sixteen. So nothing there can tell a device that assembles the pointer
// correctly from one that throws the high byte away.
//
// On the 1024-word part it matters: word address 0x0210 has to land at
// 0x0210, not at 0x0010.
// ----------------------------------------------------------------
do_reset;
ev_start; send_addr(1'b0);
send_data(8'h02); // high byte
ck_int("T13 the high byte is in place and masked to the array", ab_wa, 16'h0200);
send_data(8'h10); // low byte completes it
$display("T13 the high address byte, on a part with somewhere to put it");
ck_int("T13 the assembled pointer keeps both halves", ab_wa, 16'h0210);
// The SAME two bytes, at the same instant, on the 256-byte part: the high
// half is gone, because that part has no bits to hold it. One master, one
// byte stream, two different locations -- decided entirely by the part.
ck_int("T13 the 256-byte part holds only the low half", a2_wa, 16'h0010);
send_data(8'h7E); // and a data byte lands there
ev_stop;
// Read it back with a random read, to prove the byte went where the pointer said.
ev_start; send_addr(1'b0); send_data(8'h02); send_data(8'h10);
ev_start; send_addr(1'b1);
ck_int("T13 and the data byte went to that address", ab_tx, 8'h7E);
take(1'b0); ev_stop;
// ----------------------------------------------------------------
// T14. AN ADDRESS BEYOND THE END OF THE ARRAY. A master that believes the
// part is larger than it is sends a pointer the device cannot hold. The
// excess bits are not refused and not reported -- the wires for them do
// not exist -- so the access silently WRAPS to somewhere inside the
// array. This is the mechanism behind "my write went to the wrong
// place", and it is also why the counter must be masked rather than
// merely assembled.
// ----------------------------------------------------------------
do_reset;
preload(16'h0000, 8'hA5);
preload(16'h0034, 8'h5A);
// 0x0834 on a 1024-word part: the top two bits are gone, leaving 0x0034.
ev_start; send_addr(1'b0); send_data(8'h08); send_data(8'h34);
$display("T14 an address past the end of the array wraps into it");
ck_int("T14 0x0834 wrapped to 0x0034 on the 1024-word part", ab_wa, 16'h0034);
ev_start; send_addr(1'b1);
ck_int("T14 and reads the byte that lives there", ab_tx, 8'h5A);
take(1'b0); ev_stop;
// A truncated word address: the master sent the high byte and then stopped.
// The counter must still hold a value INSIDE the array, or the next read is
// an out-of-range access rather than a wrong one.
do_reset;
preload(16'h0000, 8'hA5);
ev_start; send_addr(1'b0); send_data(8'h08); ev_stop;
ck_int("T14 a truncated pointer is still inside the array", ab_wa, 16'h0000);
ev_start; send_addr(1'b1);
ck_int("T14 so the read that follows is wrong, not undefined", ab_tx, 8'hA5);
take(1'b0); ev_stop;
ck_int("T14 the 256-byte part did the same", a2_wa, 16'h0001);
// ----------------------------------------------------------------
// T15. WHAT was_random_read ACTUALLY MEANS. The device cannot tell a current
// address read from the read half of a random read by looking at the
// read -- they are byte-for-byte identical on the bus. The only
// difference is whether a pointer write came first, and the flag records
// exactly that and nothing more. A flag that were set on every read, or
// that survived into the next transaction, would say nothing.
// ----------------------------------------------------------------
do_reset;
preload(16'h0005, 8'h77);
// A bare current-address read. No pointer write preceded it.
ev_start; send_addr(1'b1);
$display("T15 the flag records that a pointer write came first, nothing more");
ck_bit("T15 a current-address read is not a random read", a2_rand, 1'b0);
take(1'b0); ev_stop;
// Now a real random read.
ev_start; send_addr(1'b0); send_data(8'h00); send_data(8'h05);
ck_bit("T15 a completed pointer write makes the next read random", a2_rand, 1'b1);
ev_start; send_addr(1'b1);
ck_int("T15 and it reads the addressed byte", a2_tx, 8'h77);
take(1'b0); ev_stop;
ck_bit("T15 the flag is still set after the read", a2_rand, 1'b1);
// A new write phase must clear it BEFORE the new pointer is complete --
// otherwise the flag is describing the previous transaction.
ev_start; send_addr(1'b0);
send_data(8'h00); // high byte only: pointer incomplete
ck_bit("T15 a new write phase clears it at once", a2_rand, 1'b0);
ck_int("T15 with the pointer still half-collected", a2_got, 2'd1);
send_data(8'h05); // low byte completes it
ck_bit("T15 and it is set again once the pointer is complete", a2_rand, 1'b1);
ev_stop;
if (errors == 0)
$display("=== i2c_eeprom_addressing: ALL CHECKS PASSED ===");
else
$display("=== i2c_eeprom_addressing: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule -- ---------------------------------------------------------------------------
-- i2c_eeprom_addressing.vhd
-- EEPROM word-address handling: multi-byte pointers, the three read forms, and
-- the rollover at the end of the array (UM10204 3.1.10 notes 1 and 2).
-- Behavioural twin of i2c_eeprom_addressing.sv / .v.
--
-- Note 1 says only that "the internal memory location must be written during the
-- first data byte" -- it does not say how many bytes the location takes. A 64 kbit
-- EEPROM sends a SIXTEEN-bit word address as two bytes, high byte first, and the
-- master must know that from the datasheet. Nothing on the bus distinguishes a
-- two-byte word address followed by data from a one-byte one followed by data.
--
-- Three read forms, differing only in what the master omits:
-- CURRENT ADDRESS READ S, addr+R, data... (no word address at all)
-- RANDOM READ S, addr+W, hi, lo, Sr, addr+R, data...
-- SEQUENTIAL READ either of the above, ACKing to continue
--
-- The rollover is a device convention: at the top of the array the counter returns
-- to zero, so a sequential read that runs off the end starts again silently.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_eeprom_addressing is
generic (
ADDR_BYTES : integer := 2; -- pointer width
N_WORDS : integer := 256;
AW : integer := 16;
MY_ADDR : std_logic_vector(6 downto 0) := "1010000"; -- 0x50
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;
-- A back door so reads have something to return.
load_en : in std_logic;
load_addr : in unsigned(AW-1 downto 0);
load_data : in std_logic_vector(7 downto 0);
ack : out std_logic;
tx_byte : out std_logic_vector(7 downto 0);
tx_valid : out std_logic;
word_addr : out unsigned(AW-1 downto 0);
addr_bytes_got : out unsigned(1 downto 0);
rolled_over : out std_logic;
was_random_read : out std_logic;
state : out unsigned(2 downto 0);
bytes_read : out unsigned(CNT_W-1 downto 0);
bytes_written : out unsigned(CNT_W-1 downto 0)
);
end entity i2c_eeprom_addressing;
architecture rtl of i2c_eeprom_addressing is
constant ST_IDLE : integer := 0;
constant ST_ADDRH : integer := 1; -- collecting word-address bytes
constant ST_DATA : integer := 2; -- word address complete; data may follow
constant ST_READ : integer := 3;
constant ADDR_TOP : integer := N_WORDS - 1;
-- The counter lives INSIDE the array. A 256-byte part has an 8-bit counter, so
-- a two-byte protocol pointer carries more bits than the device owns and the
-- excess wraps. Masking at assembly is what real hardware does, and it is also
-- what keeps the array index in range -- which VHDL refuses outright and
-- Verilog would quietly read as X.
type mem_arr is array (0 to N_WORDS-1) of std_logic_vector(7 downto 0);
signal mem : mem_arr := (others => (others => '0'));
signal st : integer := ST_IDLE;
signal wa : integer := 0;
signal got : integer := 0;
signal n_rd : integer := 0;
signal n_wr : integer := 0;
begin
state <= to_unsigned(st, 3);
word_addr <= to_unsigned(wa, AW);
addr_bytes_got <= to_unsigned(got, 2);
bytes_read <= to_unsigned(n_rd, CNT_W);
bytes_written <= to_unsigned(n_wr, CNT_W);
process (clk, rst_n)
variable nxt : integer;
begin
if rst_n = '0' then
st <= ST_IDLE;
ack <= '0';
tx_byte <= (others => '0');
tx_valid <= '0';
wa <= 0;
got <= 0;
rolled_over <= '0';
was_random_read <= '0';
n_rd <= 0;
n_wr <= 0;
mem <= (others => (others => '0'));
elsif rising_edge(clk) then
ack <= '0';
if load_en = '1' then
mem(to_integer(load_addr)) <= load_data;
end if;
if start_seen = '1' then
-- Note 4: any START returns the device to expecting an address. The
-- word-address COUNTER survives, which is what makes a random read
-- work across the repeated START.
st <= ST_IDLE;
tx_valid <= '0';
got <= 0;
elsif stop_seen = '1' then
st <= ST_IDLE;
tx_valid <= '0';
got <= 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
-- Whether this is a CURRENT ADDRESS read or the second
-- half of a RANDOM read depends only on what preceded it.
tx_byte <= mem(wa);
tx_valid <= '1';
st <= ST_READ;
else
got <= 0;
was_random_read <= '0';
st <= ST_ADDRH;
end if;
end if;
-- Collect the word address, high byte first. A master with the
-- wrong ADDR_BYTES gets this wrong: it sends one byte and starts
-- writing data, and the data's first byte becomes the low pointer.
when ST_ADDRH =>
ack <= '1';
if ADDR_BYTES = 1 then
wa <= to_integer(unsigned(byte_in)) mod N_WORDS;
got <= 1;
was_random_read <= '1';
st <= ST_DATA;
else
if got = 0 then
wa <= (to_integer(unsigned(byte_in)) * 256) mod N_WORDS;
got <= 1;
else
wa <= ((wa / 256) * 256
+ to_integer(unsigned(byte_in))) mod N_WORDS;
got <= 2;
was_random_read <= '1';
st <= ST_DATA;
end if;
end if;
-- Data bytes, if any. A random read sends none.
when ST_DATA =>
ack <= '1';
mem(wa) <= byte_in;
n_wr <= n_wr + 1;
if wa = ADDR_TOP then
wa <= 0;
rolled_over <= '1';
else
wa <= wa + 1;
end if;
when others =>
null;
end case;
elsif read_byte_done = '1' and st = ST_READ then
n_rd <= n_rd + 1;
if wa = ADDR_TOP then
wa <= 0;
rolled_over <= '1';
nxt := 0;
else
wa <= wa + 1;
nxt := wa + 1;
end if;
if master_acked = '0' then
tx_valid <= '0';
st <= ST_IDLE;
else
tx_byte <= mem(nxt);
end if;
end if;
end if;
end process;
end architecture rtl; -- ---------------------------------------------------------------------------
-- i2c_eeprom_addressing_tb.vhd
-- Independent oracle for i2c_eeprom_addressing. Behavioural twin of the
-- SystemVerilog and Verilog benches.
--
-- Two instances with DIFFERENT word-address widths see the same byte stream,
-- because the whole point of the chapter is that the bus cannot tell them apart.
-- Test 6 drives a two-byte sequence into both and shows the one-byte instance
-- consuming the data as the low half of its pointer -- a corruption with no
-- protocol error anywhere.
--
-- dut2 : ADDR_BYTES = 2 a 64 kbit-class part
-- dut1 : ADDR_BYTES = 1 a 2 kbit-class part
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_eeprom_addressing_tb is
end entity i2c_eeprom_addressing_tb;
architecture sim of i2c_eeprom_addressing_tb is
constant TCLK : time := 10 ns;
constant ADDR : std_logic_vector(6 downto 0) := "1010000"; -- 0x50
constant NW : integer := 256;
constant AW : integer := 16;
-- A two-byte word address on a 256-byte part is very nearly a contradiction:
-- every bit the high byte carries is masked away again, because the device only
-- owns eight address bits. So the bench also instantiates a part where the high
-- byte genuinely survives -- 1024 words, ten address bits -- which is the only
-- configuration in which the high half of the pointer can be shown to work.
constant NWB : integer := 1024;
constant ST_IDLE : integer := 0;
constant ST_ADDRH : integer := 1;
constant ST_DATA : integer := 2;
constant ST_READ : integer := 3;
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 load_en : std_logic := '0';
signal load_addr : unsigned(AW-1 downto 0) := (others => '0');
signal load_data : std_logic_vector(7 downto 0) := (others => '0');
signal a2_ack, a2_txv, a2_roll, a2_rand : std_logic;
signal a2_tx : std_logic_vector(7 downto 0);
signal a2_wa : unsigned(AW-1 downto 0);
signal a2_got : unsigned(1 downto 0);
signal a2_state : unsigned(2 downto 0);
signal a2_rd, a2_wr : unsigned(7 downto 0);
signal a1_ack, a1_txv, a1_roll, a1_rand : std_logic;
signal a1_tx : std_logic_vector(7 downto 0);
signal a1_wa : unsigned(AW-1 downto 0);
signal a1_got : unsigned(1 downto 0);
signal a1_state : unsigned(2 downto 0);
signal a1_rd, a1_wr : unsigned(7 downto 0);
signal ab_ack, ab_txv, ab_roll, ab_rand : std_logic;
signal ab_tx : std_logic_vector(7 downto 0);
signal ab_wa : unsigned(AW-1 downto 0);
signal ab_got : unsigned(1 downto 0);
signal ab_state : unsigned(2 downto 0);
signal ab_rd, ab_wr : unsigned(7 downto 0);
signal halt : boolean := false;
begin
dut2 : entity work.i2c_eeprom_addressing
generic map (ADDR_BYTES => 2, N_WORDS => NW, AW => AW,
MY_ADDR => ADDR, 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, load_en => load_en,
load_addr => load_addr, load_data => load_data,
ack => a2_ack, tx_byte => a2_tx, tx_valid => a2_txv, word_addr => a2_wa,
addr_bytes_got => a2_got, rolled_over => a2_roll,
was_random_read => a2_rand, state => a2_state,
bytes_read => a2_rd, bytes_written => a2_wr);
dut1 : entity work.i2c_eeprom_addressing
generic map (ADDR_BYTES => 1, N_WORDS => NW, AW => AW,
MY_ADDR => ADDR, 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, load_en => load_en,
load_addr => load_addr, load_data => load_data,
ack => a1_ack, tx_byte => a1_tx, tx_valid => a1_txv, word_addr => a1_wa,
addr_bytes_got => a1_got, rolled_over => a1_roll,
was_random_read => a1_rand, state => a1_state,
bytes_read => a1_rd, bytes_written => a1_wr);
-- The 1024-word part: two address bytes, and ten address bits to put them in.
dutB : entity work.i2c_eeprom_addressing
generic map (ADDR_BYTES => 2, N_WORDS => NWB, AW => AW,
MY_ADDR => ADDR, 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, load_en => load_en,
load_addr => load_addr, load_data => load_data,
ack => ab_ack, tx_byte => ab_tx, tx_valid => ab_txv, word_addr => ab_wa,
addr_bytes_got => ab_got, rolled_over => ab_roll,
was_random_read => ab_rand, state => ab_state,
bytes_read => ab_rd, bytes_written => ab_wr);
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;
procedure ck_int (what : string; got : integer; exp : integer) is
begin
if got /= exp then
report " FAIL " & what & ": got " & integer'image(got)
& " expected " & integer'image(exp) severity note;
err := err + 1;
end if;
end procedure;
procedure ck_bit (what : string; got : std_logic; exp : std_logic) is
begin
if got /= exp then
report " FAIL " & what & ": got " & std_logic'image(got)
& " expected " & std_logic'image(exp) 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';
load_en <= '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 : std_logic_vector(7 downto 0)) is
begin
wait until falling_edge(clk);
byte_in <= b; 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 preload (a : integer; d : integer) is
begin
wait until falling_edge(clk);
load_en <= '1'; load_addr <= to_unsigned(a, AW);
load_data <= std_logic_vector(to_unsigned(d, 8));
wait until rising_edge(clk); wait until falling_edge(clk);
load_en <= '0';
end procedure;
begin
report "=== i2c_eeprom_addressing: three read forms and a pointer the bus cannot size ==="
severity note;
-- T1. A two-byte word address is assembled high byte first.
do_reset;
ev_start;
send_addr('0');
ck_int("T1 collecting the word address", to_integer(a2_state), ST_ADDRH);
send_data(x"00");
ck_int("T1 one pointer byte so far", to_integer(a2_got), 1);
send_data(x"2A");
report "T1 a two-byte word address, high byte first" severity note;
ck_int("T1 pointer assembled", to_integer(a2_wa), 16#002A#);
ck_int("T1 two pointer bytes", to_integer(a2_got), 2);
ck_int("T1 now accepting data", to_integer(a2_state), ST_DATA);
ev_stop;
-- T2. A WRITE: pointer then data, with the counter advancing.
do_reset;
ev_start;
send_addr('0');
send_data(x"00"); send_data(x"10");
send_data(x"A0"); send_data(x"A1"); send_data(x"A2");
report "T2 a write: pointer, then data, counter advancing" severity note;
ck_int("T2 three bytes written", to_integer(a2_wr), 3);
ck_int("T2 counter advanced past them", to_integer(a2_wa), 16#0013#);
ev_stop;
ev_start;
send_addr('0');
send_data(x"00"); send_data(x"10");
ev_start;
send_addr('1');
ck_int("T2 read back 0xA0", to_integer(unsigned(a2_tx)), 16#A0#); take('1');
ck_int("T2 read back 0xA1", to_integer(unsigned(a2_tx)), 16#A1#); take('1');
ck_int("T2 read back 0xA2", to_integer(unsigned(a2_tx)), 16#A2#); take('0');
ev_stop;
-- T3. RANDOM READ: the write phase carries NO data.
do_reset;
preload(16#0080#, 16#5A#);
ev_start;
send_addr('0');
send_data(x"00"); send_data(x"80");
ck_int("T3 pointer set, no data written", to_integer(a2_wr), 0);
ck_bit("T3 marked as a pointer write", a2_rand, '1');
ev_start;
send_addr('1');
report "T3 a random read: a dummy write for its side effect, then Sr"
severity note;
ck_bit("T3 serving", a2_txv, '1');
ck_int("T3 serves the addressed location", to_integer(unsigned(a2_tx)), 16#5A#);
take('0');
ev_stop;
-- T4. CURRENT ADDRESS READ: no word address, so history decides.
do_reset;
preload(16#0040#, 16#11#);
preload(16#0041#, 16#22#);
ev_start;
send_addr('0');
send_data(x"00"); send_data(x"40");
ev_stop;
ck_int("T4 counter survived the STOP", to_integer(a2_wa), 16#0040#);
ev_start;
send_addr('1');
report "T4 a current address read: no pointer, so history decides"
severity note;
ck_int("T4 serves 0x0040", to_integer(unsigned(a2_tx)), 16#11#);
take('1');
ck_int("T4 then 0x0041", to_integer(unsigned(a2_tx)), 16#22#);
take('0');
ev_stop;
-- T5. SEQUENTIAL READ across many locations.
do_reset;
for k in 0 to 19 loop preload(k, 16#70# + k); end loop;
ev_start;
send_addr('0');
send_data(x"00"); send_data(x"00");
ev_start;
send_addr('1');
report "T5 a sequential read runs across the array, not a page" severity note;
for k in 0 to 19 loop
ck_int("T5 sequential byte", to_integer(unsigned(a2_tx)), 16#70# + k);
if k < 19 then take('1'); else take('0'); end if;
end loop;
ck_int("T5 twenty bytes served", to_integer(a2_rd), 20);
ev_stop;
-- T6. THE CENTRAL HAZARD: a two-byte pointer into a one-byte device.
do_reset;
ev_start;
send_addr('0');
ck_bit("T6 both devices acknowledged", a2_ack, '1');
ck_bit("T6 both devices acknowledged (1-byte)", a1_ack, '1');
send_data(x"01");
ck_int("T6 two-byte device: half a pointer", to_integer(a2_got), 1);
ck_int("T6 one-byte device: a COMPLETE pointer", to_integer(a1_got), 1);
ck_int("T6 one-byte device points at 0x0001", to_integer(a1_wa), 16#0001#);
ck_int("T6 one-byte device now expects data", to_integer(a1_state), ST_DATA);
send_data(x"20");
report "T6 a two-byte pointer sent to a one-byte device" severity note;
-- 0x0120 is beyond a 256-byte array, so the counter -- only as wide as the
-- array -- holds 0x20. A pointer wider than the device wraps into it.
ck_int("T6 two-byte pointer 0x0120 wraps to 0x0020",
to_integer(a2_wa), 16#0020#);
ck_bit("T6 one-byte device acknowledged it as data", a1_ack, '1');
ck_int("T6 one-byte device WROTE the low byte as data", to_integer(a1_wr), 1);
ev_stop;
ev_start;
send_addr('0');
send_data(x"01");
ev_start;
send_addr('1');
ck_int("T6 location 0x0001 holds the stray pointer byte",
to_integer(unsigned(a1_tx)), 16#20#);
take('0');
ev_stop;
-- T7. THE ROLLOVER on a read.
do_reset;
preload(16#00FF#, 16#EE#);
preload(16#0000#, 16#01#);
ev_start;
send_addr('0');
send_data(x"00"); send_data(x"FF");
ev_start;
send_addr('1');
report "T7 a sequential read rolls over instead of ending" severity note;
ck_int("T7 the last location", to_integer(unsigned(a2_tx)), 16#EE#);
take('1');
ck_int("T7 back at location zero", to_integer(unsigned(a2_tx)), 16#01#);
ck_bit("T7 rollover reported", a2_roll, '1');
take('0');
ev_stop;
-- T8. A write rolls over at the array end too.
do_reset;
ev_start;
send_addr('0');
send_data(x"00"); send_data(x"FE");
send_data(x"D0"); send_data(x"D1"); send_data(x"D2");
report "T8 a write rolls over at the end of the array too" severity note;
ck_bit("T8 rollover reported", a2_roll, '1');
ck_int("T8 counter is back near zero", to_integer(a2_wa), 16#0001#);
ev_stop;
ev_start;
send_addr('0');
send_data(x"00"); send_data(x"00");
ev_start;
send_addr('1');
ck_int("T8 location zero got the third byte",
to_integer(unsigned(a2_tx)), 16#D2#);
take('0');
ev_stop;
-- T9. A repeated START abandons a partial word address.
do_reset;
preload(16#0000#, 16#99#);
ev_start;
send_addr('0');
send_data(x"07");
ck_int("T9 half a pointer collected", to_integer(a2_got), 1);
ev_start;
report "T9 a repeated START abandons a partial word address" severity note;
ck_int("T9 pointer-byte count cleared", to_integer(a2_got), 0);
ck_int("T9 back to expecting an address", to_integer(a2_state), ST_IDLE);
send_addr('1');
ck_bit("T9 serving something", a2_txv, '1');
take('0');
ev_stop;
-- T10. A wrong device address is ignored by both instances.
do_reset;
wait until falling_edge(clk);
byte_in <= "10100010"; is_addr_byte <= '1'; byte_valid <= '1';
wait until rising_edge(clk); wait until falling_edge(clk);
byte_valid <= '0'; is_addr_byte <= '0';
report "T10 another device's address is ignored" severity note;
ck_bit("T10 two-byte instance silent", a2_ack, '0');
ck_bit("T10 one-byte instance silent", a1_ack, '0');
ck_int("T10 both idle", to_integer(a2_state), ST_IDLE);
-- T11. A later bare read continues where the last one stopped.
do_reset;
for k in 0 to 7 loop preload(16#0030# + k, 16#B0# + k); end loop;
ev_start;
send_addr('0');
send_data(x"00"); send_data(x"30");
ev_start;
send_addr('1');
ck_int("T11 first read", to_integer(unsigned(a2_tx)), 16#B0#); take('1');
ck_int("T11 second read", to_integer(unsigned(a2_tx)), 16#B1#); take('0');
ev_stop;
report "T11 a later bare read continues where the last one stopped"
severity note;
ev_start;
send_addr('1');
ck_int("T11 continues at 0x0032", to_integer(unsigned(a2_tx)), 16#B2#); take('1');
ck_int("T11 and 0x0033", to_integer(unsigned(a2_tx)), 16#B3#); take('0');
ev_stop;
-- T12. A random read's write phase writes nothing.
do_reset;
ev_start;
send_addr('0');
send_data(x"00"); send_data(x"05");
send_data(x"C0"); send_data(x"C1");
ev_stop;
ev_start;
send_addr('0');
send_data(x"00"); send_data(x"05");
ev_start;
send_addr('1');
take('1'); take('0');
ev_stop;
report "T12 a random read's write phase writes nothing" severity note;
ck_int("T12 two bytes written in total", to_integer(a2_wr), 2);
ck_int("T12 two bytes read", to_integer(a2_rd), 2);
-- T13. THE HIGH ADDRESS BYTE, ON A PART THAT HAS SOMEWHERE TO PUT IT. On the
-- 256-byte part above, every bit the high byte carries is masked away
-- again -- the device owns eight address bits and the protocol handed it
-- sixteen. So nothing there can tell a device that assembles the pointer
-- correctly from one that throws the high byte away.
--
-- On the 1024-word part it matters: word address 0x0210 has to land at
-- 0x0210, not at 0x0010.
do_reset;
ev_start; send_addr('0');
send_data(x"02"); -- high byte
ck_int("T13 the high byte is in place and masked to the array",
to_integer(ab_wa), 16#0200#);
send_data(x"10"); -- low byte completes it
report "T13 the high address byte, on a part with somewhere to put it"
severity note;
ck_int("T13 the assembled pointer keeps both halves",
to_integer(ab_wa), 16#0210#);
-- The SAME two bytes, at the same instant, on the 256-byte part: the high half
-- is gone, because that part has no bits to hold it. One master, one byte
-- stream, two different locations -- decided entirely by the part.
ck_int("T13 the 256-byte part holds only the low half",
to_integer(a2_wa), 16#0010#);
send_data(x"7E"); -- and a data byte lands there
ev_stop;
-- Read it back with a random read, to prove the byte went where the pointer said.
ev_start; send_addr('0'); send_data(x"02"); send_data(x"10");
ev_start; send_addr('1');
ck_int("T13 and the data byte went to that address",
to_integer(unsigned(ab_tx)), 16#7E#);
take('0'); ev_stop;
-- T14. AN ADDRESS BEYOND THE END OF THE ARRAY. A master that believes the part
-- is larger than it is sends a pointer the device cannot hold. The excess
-- bits are not refused and not reported -- the wires for them do not
-- exist -- so the access silently WRAPS to somewhere inside the array.
-- This is the mechanism behind "my write went to the wrong place", and it
-- is also why the counter must be masked rather than merely assembled.
do_reset;
preload(16#0000#, 16#A5#);
preload(16#0034#, 16#5A#);
-- 0x0834 on a 1024-word part: the top two bits are gone, leaving 0x0034.
ev_start; send_addr('0'); send_data(x"08"); send_data(x"34");
report "T14 an address past the end of the array wraps into it" severity note;
ck_int("T14 0x0834 wrapped to 0x0034 on the 1024-word part",
to_integer(ab_wa), 16#0034#);
ev_start; send_addr('1');
ck_int("T14 and reads the byte that lives there",
to_integer(unsigned(ab_tx)), 16#5A#);
take('0'); ev_stop;
-- A truncated word address: the master sent the high byte and then stopped.
-- The counter must still hold a value INSIDE the array, or the next read is an
-- out-of-range access rather than a wrong one.
do_reset;
preload(16#0000#, 16#A5#);
ev_start; send_addr('0'); send_data(x"08"); ev_stop;
ck_int("T14 a truncated pointer is still inside the array",
to_integer(ab_wa), 16#0000#);
ev_start; send_addr('1');
ck_int("T14 so the read that follows is wrong, not undefined",
to_integer(unsigned(ab_tx)), 16#A5#);
take('0'); ev_stop;
ck_int("T14 the 256-byte part did the same", to_integer(a2_wa), 16#0001#);
-- T15. WHAT was_random_read ACTUALLY MEANS. The device cannot tell a current
-- address read from the read half of a random read by looking at the read
-- -- they are byte-for-byte identical on the bus. The only difference is
-- whether a pointer write came first, and the flag records exactly that
-- and nothing more. A flag that were set on every read, or that survived
-- into the next transaction, would say nothing.
do_reset;
preload(16#0005#, 16#77#);
-- A bare current-address read. No pointer write preceded it.
ev_start; send_addr('1');
report "T15 the flag records that a pointer write came first, nothing more"
severity note;
ck_bit("T15 a current-address read is not a random read", a2_rand, '0');
take('0'); ev_stop;
-- Now a real random read.
ev_start; send_addr('0'); send_data(x"00"); send_data(x"05");
ck_bit("T15 a completed pointer write makes the next read random",
a2_rand, '1');
ev_start; send_addr('1');
ck_int("T15 and it reads the addressed byte",
to_integer(unsigned(a2_tx)), 16#77#);
take('0'); ev_stop;
ck_bit("T15 the flag is still set after the read", a2_rand, '1');
-- A new write phase must clear it BEFORE the new pointer is complete --
-- otherwise the flag is describing the previous transaction.
ev_start; send_addr('0');
send_data(x"00"); -- high byte only: pointer incomplete
ck_bit("T15 a new write phase clears it at once", a2_rand, '0');
ck_int("T15 with the pointer still half-collected", to_integer(a2_got), 1);
send_data(x"05"); -- low byte completes it
ck_bit("T15 and it is set again once the pointer is complete", a2_rand, '1');
ev_stop;
if err = 0 then
report "=== i2c_eeprom_addressing: ALL CHECKS PASSED ===" severity note;
else
report "=== i2c_eeprom_addressing: " & integer'image(err)
& " CHECK(S) FAILED ===" severity note;
end if;
halt <= true;
wait;
end process;
end architecture sim;6a. Decisions Worth Defending
The counter is masked to the array at assembly, not merely assembled. ADDR_MASK is N_WORDS - 1, applied at every point the pointer is written. This is what real hardware does — the wires for the excess bits are not there — and it is also what stops an out-of-range array index. §3 and the callout there; mutation B2-1 removes the mask and three checks fail.
The low address byte completes the pointer rather than replacing it. {word_addr[AW-1:8], byte_in} keeps the high half that arrived in the previous byte. Mutation B2-2 replaces the whole pointer with the low byte, and it is provably unobservable on a 256-byte part — the final mask discards the high half anyway — which is precisely why the 1024-word instance exists. §7.
The word-address counter survives a START; the frame state does not. The START branch resets state and addr_bytes_got and deliberately leaves word_addr alone, because the random read depends on the pointer set in the write phase still being there after the turnaround. Mutation B2-4 clears it and seventeen checks fail.
addr_bytes_got is an output, so a bench can see a half-collected pointer. It is how test 15 catches a flag that describes the previous transaction rather than this one, and it is how a debug capture distinguishes "the master sent one byte" from "the device wanted one byte".
was_random_read records history and nothing else. §1's whole point is that the device cannot tell the two reads apart by looking at the read, so the flag is set when a pointer write completes and cleared when a new write phase begins. Two mutations attack it from opposite sides — B2-7 sets it on a bare read, B2-8 leaves it set from a previous transaction — and each is killed by one check.
The rollover is reported as well as performed. On a power-of-two array the counter would wrap on its own; rolled_over exists so a bench can prove the array boundary was reached rather than inferring it. Mutation B2-5 removes the wrap branch entirely and four checks fail; B2-6 wraps silently and two fail.
The next sequential byte is prefetched from the advanced counter, honouring the rollover. if (word_addr == ADDR_TOP) tx_byte <= mem[0] rather than indexing word_addr + 1, which on a full array would be an out-of-range read. Mutation B2-10 prefetches from the un-advanced counter and twenty-five checks fail.
The counter advances on every byte read, including the one the master NACKs. That is Chapter 16.1's question 5, answered the same way here for the same reason: it decides what the next current-address read returns. Mutation B2-11 skips the final advance and twenty-three checks fail.
6b. Verified Execution
$ iverilog -g2012 -o d i2c_eeprom_addressing.sv i2c_eeprom_addressing_tb.sv && ./d
=== i2c_eeprom_addressing: three read forms and a pointer the bus cannot size ===
T1 a two-byte word address, high byte first
T2 a write: pointer, then data, counter advancing
T3 a random read: a dummy write for its side effect, then Sr
T4 a current address read: no pointer, so history decides
T5 a sequential read runs across the array, not a page
T6 a two-byte pointer sent to a one-byte device
T7 a sequential read rolls over instead of ending
T8 a write rolls over at the end of the array too
T9 a repeated START abandons a partial word address
T10 another device's address is ignored
T11 a later bare read continues where the last one stopped
T12 a random read's write phase writes nothing
T13 the high address byte, on a part with somewhere to put it
T14 an address past the end of the array wraps into it
T15 the flag records that a pointer write came first, nothing more
=== i2c_eeprom_addressing: ALL CHECKS PASSED ===
$ iverilog -g2005 -o v i2c_eeprom_addressing.v i2c_eeprom_addressing_tb.v && ./v
=== i2c_eeprom_addressing: three read forms and a pointer the bus cannot size ===
T1 a two-byte word address, high byte first
T2 a write: pointer, then data, counter advancing
T3 a random read: a dummy write for its side effect, then Sr
T4 a current address read: no pointer, so history decides
T5 a sequential read runs across the array, not a page
T6 a two-byte pointer sent to a one-byte device
T7 a sequential read rolls over instead of ending
T8 a write rolls over at the end of the array too
T9 a repeated START abandons a partial word address
T10 another device's address is ignored
T11 a later bare read continues where the last one stopped
T12 a random read's write phase writes nothing
T13 the high address byte, on a part with somewhere to put it
T14 an address past the end of the array wraps into it
T15 the flag records that a pointer write came first, nothing more
=== i2c_eeprom_addressing: ALL CHECKS PASSED ===
$ nvc --std=2008 -a i2c_eeprom_addressing.vhd i2c_eeprom_addressing_tb.vhd
$ nvc --std=2008 -e i2c_eeprom_addressing_tb && nvc --std=2008 -r i2c_eeprom_addressing_tb --stop-time=300us
=== i2c_eeprom_addressing: three read forms and a pointer the bus cannot size ===
T1 a two-byte word address, high byte first
T2 a write: pointer, then data, counter advancing
T3 a random read: a dummy write for its side effect, then Sr
T4 a current address read: no pointer, so history decides
T5 a sequential read runs across the array, not a page
T6 a two-byte pointer sent to a one-byte device
T7 a sequential read rolls over instead of ending
T8 a write rolls over at the end of the array too
T9 a repeated START abandons a partial word address
T10 another device's address is ignored
T11 a later bare read continues where the last one stopped
T12 a random read's write phase writes nothing
T13 the high address byte, on a part with somewhere to put it
T14 an address past the end of the array wraps into it
T15 the flag records that a pointer write came first, nothing more
=== i2c_eeprom_addressing: ALL CHECKS PASSED ===6c. What The Testbench Proves
| # | scenario | what it establishes |
|---|---|---|
| 1 | a two-byte word address | assembled high byte first |
| 2 | a write: pointer then data | the counter advances once per byte written |
| 3 | a random read | the write phase carries no data; the repeated START turns it around |
| 4 | a current-address read, no word address at all | works only because the counter survived the STOP |
| 5 | a sequential read across many locations | reads auto-increment through the whole array — no page boundary |
| 6 | the same two-byte pointer sequence into a one-byte device | every byte acknowledged; the second pointer byte is consumed as data |
| 7 | a sequential read from the last location | rollover to zero rather than a failure, and it is reported |
| 8 | a write at the array end | writes roll over too — a different modulus from Chapter 16.3's |
| 9 | a repeated START mid-word-address | half a pointer is not a pointer; the counter is not left half-updated |
| 10 | a wrong device address | ignored by both instances |
| 11 | a random read then a sequential read with no new pointer | the second read continues where the first stopped |
| 12 | read and write byte counts | counted separately — a random read's write phase writes nothing |
| 13 | a two-byte pointer on the 1024-word part | the high byte survives and matters |
| 14 | an address past the end of the array | wraps into it, silently; a truncated pointer stays in range |
| 15 | was_random_read across four transitions | the flag records history, and only history |
Test 6 is §2's failure mode, run. The two-byte pointer sequence driven into a one-byte device: every byte is acknowledged, nothing on the bus is wrong, and the second pointer byte is written as data at the location the first byte named. It is the test that turns a paragraph of warning into an executable fact. The mirror-image case — a one-byte master against a two-byte device, §2's table — is the one §10 debugs; the bench drives the direction above because it is the direction in which the surplus byte's destination is assertable.
Test 13 is the only test in the module that can see the high address byte at all. On both 256-byte instances every bit it carries is masked away, so two distinct defects are invisible there. §7 records both.
Test 14 tests the truncated pointer as well as the overflowing one, because they fail differently: an overflowing pointer wraps to a real location, while an unmasked truncated pointer leaves the counter outside the array and the next read is out of range rather than merely wrong.
Test 15 checks was_random_read at four points in one sequence — after a bare read, after a completed pointer write, after the read that followed it, and in the middle of the next write phase. A flag like this is only meaningful if it is wrong at none of those points, and two mutations attack two of them.
7. Mutation Testing
Twelve defects injected into the SystemVerilog addressing engine.
| # | injected defect | outcome |
|---|---|---|
| B2-1 | the assembled pointer not masked to the array | killed — 3 checks |
| B2-2 | the low address byte overwrites the high half | killed — test 13 |
| B2-3 | the first address byte taken as the low half | killed — 54 checks |
| B2-4 | the word-address counter cleared on a repeated START | killed — 17 checks |
| B2-5 | no rollover at the top of the array | killed — 4 checks |
| B2-6 | rollover performed but never reported | killed — 2 checks |
| B2-7 | a current-address read reported as a random read | killed — test 15 |
| B2-8 | was_random_read left set from a previous transaction | killed — test 15 |
| B2-9 | the counter does not advance after a written byte | killed — 7 checks |
| B2-10 | the next sequential byte prefetched from the un-advanced counter | killed — 25 checks |
| B2-11 | no advance on the final, NACKed byte of a read | killed — 23 checks |
| B2-12 | the address-byte count carried across a repeated START | killed — 1 check |
Twelve of twelve — after a bench change that is the most instructive result in the chapter.
B2-1 and B2-2 both survived the original suite, and neither was a bench hole in the ordinary sense. They were unobservable by construction on the parts the bench instantiated. Work through B2-2 on a 256-byte part: the correct expression is (high << 8 | low) & 0x00FF, and the mutation is low & 0x00FF. Those are equal for every input, because the mask discards the high half in both cases. No test could distinguish them, however the bench was written.
The fix was not a new assertion. It was a new device: a 1024-word part, on which
ADDR_MASKis0x03FFand the high byte contributes two real bits. Both mutations die immediately there.
That is a distinction worth holding onto. A surviving mutant is usually a missing test; occasionally it is a missing configuration, and no amount of assertion-writing against the existing configuration will fix it.
And it carries a design lesson about the part, not the model. A two-byte word address on a 256-byte part is a protocol handing a device more address bits than it owns. That is the same phenomenon as §3's out-of-range access, seen from the other end, and it is why the high byte on a small part is genuinely uninformative rather than merely redundant.
B2-3's fifty-four checks are the signature of a shifted frame. Taking the first address byte as the low half puts every subsequent byte in the wrong slot for the rest of the run. Compare B2-12's single check: carrying addr_bytes_got across a repeated START matters in exactly one place, and the bench looks at it in exactly one place.
B2-6 kills with two checks and is the one a real suite is likeliest to miss. The array size is a power of two, so the counter wraps correctly whether or not the wrap branch reports it — the only evidence is the rolled_over output itself. A bench that checked the returned bytes and not the flag would score zero on B2-6 while proving the data path correct.
8. Verification Connection — Layered Sequences and the Reference Model
An EEPROM's difficulty is not the individual transfer; it is that the meaning of a transfer depends on what preceded it. That is a sequence-layering problem, and it is where a UVM environment earns its cost.
// The item a driver consumes: one complete I2C transfer, framing included. Note
// that a random read is ONE item, not two -- the repeated START is internal to it.
// Modelling it as two items would let the sequencer interleave another master's
// transfer between the halves, which is exactly the hazard of section 1's callout
// and not something a bench should be able to do by accident.
class eeprom_xfer extends uvm_sequence_item;
`uvm_object_utils(eeprom_xfer)
rand bit [6:0] dev_addr;
rand bit [15:0] word_addr;
rand int n_addr_bytes; // 1 or 2 -- section 2's question
rand bit send_pointer; // 0 = current-address read, 1 = random read
rand bit is_read;
rand byte payload[];
byte rdata[];
bit nacked;
// A current-address read does not send a pointer, and a write always does.
constraint c_form {
if (!is_read) send_pointer == 1;
n_addr_bytes inside {1, 2};
payload.size() inside {[1:16]};
}
endclass
// The reference model. Its whole job is to hold the ONE piece of state the
// protocol does not carry: the device's internal counter. Without it, a
// current-address read cannot be predicted at all -- and a scoreboard that cannot
// predict a current-address read will either skip those transfers or, worse,
// check them against a pointer it assumed.
class eeprom_model extends uvm_component;
`uvm_component_utils(eeprom_model)
protected byte mem[int];
protected int ptr; // the device's counter, mirrored
protected int n_words;
protected int addr_mask;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void configure(int words);
n_words = words;
addr_mask = words - 1; // section 3: the counter is masked, not clipped
endfunction
// Assemble a pointer exactly as the device does, masking included. Getting
// this wrong in the model produces a scoreboard that disagrees with a correct
// device, which is the most expensive kind of bug in a verification
// environment: it looks like an RTL failure.
protected function int assemble(bit [15:0] wa, int nbytes);
int v = (nbytes == 1) ? (wa & 16'h00FF) : wa;
return v & addr_mask;
endfunction
function void predict(eeprom_xfer t, output byte exp[]);
if (t.send_pointer)
ptr = assemble(t.word_addr, t.n_addr_bytes);
if (t.is_read) begin
exp = new[t.payload.size()];
foreach (exp[i]) begin
exp[i] = mem.exists(ptr) ? mem[ptr] : 8'h00;
ptr = (ptr == n_words - 1) ? 0 : ptr + 1; // the rollover
end
end else begin
foreach (t.payload[i]) begin
mem[ptr] = t.payload[i];
ptr = (ptr == n_words - 1) ? 0 : ptr + 1;
end
end
endfunction
endclass
// A layered sequence: the API a test wants, expressed in terms of transfers.
// "Read sixteen bytes from 0x0210" is one call here and one transfer on the bus.
class eeprom_read_seq extends uvm_sequence #(eeprom_xfer);
`uvm_object_utils(eeprom_read_seq)
rand bit [15:0] start_addr;
rand int n_bytes;
rand int n_addr_bytes;
function new(string name = "eeprom_read_seq"); super.new(name); endfunction
virtual task body();
eeprom_xfer t = eeprom_xfer::type_id::create("t");
start_item(t);
if (!t.randomize() with {
dev_addr == 7'h50;
is_read == 1;
send_pointer == 1; // a random read
word_addr == local::start_addr;
n_addr_bytes == local::n_addr_bytes;
payload.size() == local::n_bytes; // sized, contents unused on a read
})
`uvm_error("EEPROM", "read transfer randomization failed")
finish_item(t);
endtask
endclassThree things this structure buys, and one trap it does not remove.
A reference model that mirrors the pointer is not optional here. A scoreboard for a stateless device can predict a transfer from the transfer alone. This one cannot: §1's current-address read carries no address, so its expected data is a function of every transfer since the last pointer write. A model without ptr has to skip those transfers, which means the most history-dependent transaction on the bus is the one that goes unchecked.
n_addr_bytes belongs in the item, and randomizing it is the point. §2's failure modes are a disagreement between master and device, so a bench that hard-codes the right number can never produce them. Constraining it to the device's real width for most tests and deliberately violating it in a directed test is how tests 9 and 10 of §6c become part of a regression rather than one-off experiments.
One transfer, not two, for a random read. Modelling the write phase and the read phase as separate items lets the sequencer put another master's transfer between them — which is legal on the bus and catastrophic for the read, and is a bug the bench introduced. Keeping the repeated START inside the item makes atomicity structural.
9. FPGA and ASIC Implications
Derive the counter width from the array size, and mask every write to it. An address wider than the array is not a theoretical problem: it arrives whenever a master is configured for the wrong part number, and an unmasked counter turns that into an out-of-range memory access. In an FPGA a BRAM will return whatever the address bits it does have select; in an ASIC the memory compiler's behaviour is unspecified.
The pointer needs a reset value and it should be zero. A current-address read is legal as the very first transaction after power-on, and it will serve from wherever the counter starts. Leaving that undefined makes the device's first transfer unpredictable in a way no driver can compensate for.
Prefetch costs one read port and removes a stretch. Serving a sequential read from a registered copy, fetched when the counter advanced, means the next byte is ready before the master clocks it. Fetching on demand instead means either a combinational read from the array — which constrains the memory — or clock stretching on every byte of every burst. The second is legal and multiplies a burst's duration.
Two address bytes are not free: the pointer's collection is a state. S_ADDRH has to know whether it is expecting the high or the low half, which is one bit of state and one output worth exposing. On a part that supports both widths by strap or by part number, that state machine is the only place the width appears, and it is worth parameterising rather than branching.
Report the rollover if you can afford a flag. A system that regularly rolls over is a system whose driver is reading past the end of the array, and that is a driver bug being masked by a device convention. One bit and one status register location turn an invisible failure into a logged one.
The write-protect pin interacts with all of this and none of it is on the bus. A part with WP asserted will typically acknowledge the address and the word address and then NACK or silently discard the data. Which of those it does is Chapter 16.1's question 4 again, and the pin's state is invisible to the master either way.
10. Debugging — The Firmware Update That Corrupted the First Byte
A product stores a 16-byte configuration blob in an external EEPROM. A hardware revision moves to a larger second-source EEPROM, pin-compatible and at the same device address. After the change, writes appear to succeed -- every byte acknowledged -- but reading the blob back with the product's own firmware returns data that does not match what was written. A single-byte write followed by a single-byte read still passes the factory self-test.
Section 2's failure mode. The new part expects two word-address bytes and the driver sends one, so the device consumed the blob's first data byte as the low half of the pointer. Fifteen of the sixteen bytes were written, at an address the master never named -- and because 0x4000 exceeds a 4096-byte array, the masking of section 3 put them at 0x0000 rather than anywhere near the intended 0x0040. Nothing violates the specification: the device consumed the number of address bytes its datasheet specifies and acknowledged every byte it received. The self-test passed by coincidence, having written no data at all.
Send two word-address bytes for the new part, and make the address width a property of the part number in the driver rather than a constant. For the regression: write a known pattern and verify it with an INDEPENDENTLY computed address -- reading back through the same driver repeats whatever mistake the write made, which is why the self-test was useless. And make the self-test write at least two data bytes, so a driver that donates its first byte to the pointer cannot pass it.Three things generalise.
The passing test was passing for the wrong reason. The single-byte write wrote no data at all — its one data byte became half the pointer — and the single-byte read returned a coincidence. A self-test whose payload is one byte cannot detect a driver that donates its first byte to the pointer, because there is nothing left to detect it with.
The address was wrong twice over. First by construction, because the first data byte became the pointer's low half; then again by masking, because the resulting 0x4000 is outside a 4096-byte array and section 3's wrap put the data at 0x0000. The blob was neither where the driver asked nor anywhere near it.
Every byte was acknowledged, and the acknowledge was honest. The device received eighteen bytes and acknowledged eighteen bytes. The protocol has no way to express "I expected a different number of address bytes", because the number is not part of the protocol.
"Pin-compatible, same address" again. As in Chapter 16.1 §10, the parts agree on everything a pinout or an address table would record, and differ on a convention that only a transaction longer than the pointer can reveal.
11. Common Misconceptions
"A current-address read and a random read are different transactions." The read halves are byte-for-byte identical. The difference is entirely whether a pointer write preceded it, and the device knows only because it watched. §1.
"The write phase of a random read carries data." It carries the word address and nothing else. It is a write by direction bit that writes nothing. §4.
"The number of word-address bytes is implied by the device address." It is a property of the part, stated in its datasheet and invisible on the bus. A master that guesses wrong is acknowledged all the way through. §2.
"A wrong number of address bytes produces a NACK." It produces a perfectly conforming transfer that writes the wrong number of bytes to an address the master never named. §2 and §10.
"An address past the end of the array is an error." It wraps into the array, silently, and lands on real data. §3.
"A 32 kbit part and a 2 kbit part with the same address are interchangeable." They differ in the number of address bytes, which changes the meaning of every byte after the first. §10.
"Rollover in a sequential read is visible to the master." Nothing in the transfer marks it. The bytes after the wrap arrive exactly like the bytes before it. §5.
"A repeated START in a random read could be a STOP and a START." On a single-master bus, usually. On a shared bus, another master can set its own pointer in the gap and the read serves from that. §1.
"A mutation that survives means the testbench needs another assertion." Sometimes it means the testbench needs another device. Two mutations here are unobservable on a 256-byte part for arithmetic reasons, whatever assertions are written. §7.
"A reference model can predict a read from the read." Not a current-address read, whose expected data depends on every transfer since the last pointer write. The model has to mirror the pointer. §8.
12. Reason It Through
A capture shows: START, 0xA1, three bytes, STOP. What address were those bytes read from?
Unknowable from the capture. It is a current-address read, so the address is wherever the device's counter happened to be — which depends on every transfer since the last pointer write, including transfers from other masters. You would need the preceding history, or a device that reports its counter. §1.
A master sends one word-address byte to a part that expects two, then three data bytes. How many bytes are written, and where?
Two, at the address formed by taking the word-address byte as the high half of the pointer and the master's first data byte as the low half. The first data byte is consumed by the pointer and never stored. §2.
Why does a read-back with the same driver not detect that?
Because the read makes the identical mistake: its single address byte becomes the high half and its first "data" position becomes the low half, so it reads from the same wrong address the write used. Two identical errors cancel, and the corruption presents as a one-byte shift rather than as a wrong location. §10.
Why must the pointer be masked to the array rather than range-checked and refused?
Because that is what the hardware does — the wires for the excess bits do not exist — and because a refusal has nowhere to go. There is no status code in a word-address write. Masking also keeps a truncated pointer inside the array, so a master that sends the high byte and then stops leaves the device at a real location rather than outside its memory. §3.
On a 256-byte part expecting two address bytes, name a defect no testbench can detect.
Discarding the high address byte entirely. The final pointer is masked to eight bits, so the high byte's contribution is zero either way — the correct expression and the defective one are arithmetically equal for every input. It takes a part with more than 256 words to observe. §7.
Why is rolled_over worth a flip-flop on a real part?
Because the rollover is otherwise invisible, and a system that rolls over routinely has a driver reading past the end of the array. One bit turns a silent driver bug into a logged event. §5 and §9.
Why should a random read be one sequence item rather than two?
Because two items can be interleaved by the sequencer, and a transfer from another master between the pointer write and the read makes the read serve from that master's address. The repeated START's atomicity is a protocol property, and a bench that can violate it will. §8.
13. Understanding Check
14. Summary
A serial memory is the register-map pattern with nothing else in it, which makes it the clearest place to see what the pointer conventions cost.
A current-address read and the read half of a random read are byte-for-byte identical. The only difference is whether a pointer write preceded it, and the device knows only because it watched. There is nothing in the read that encodes it.
The write phase of a random read carries no data. It is a write by direction bit whose entire purpose is to move the pointer.
How many word-address bytes a part takes is in its datasheet and nowhere on the bus. A master that gets it wrong is acknowledged on every byte, and its first data byte becomes half the pointer.
The reverse mistake fails differently, with the data landing one position shifted in the payload — and a read-back that repeats the same error cannot detect either.
An address past the end of the array wraps into it, because the counter is only as wide as the array and the excess bits have no wires. 0x0834 on a 1024-word part is 0x0034, a real location with real data.
So the counter must be masked at assembly, not merely assembled, which also keeps a truncated pointer inside the array. VHDL's bounds check found this where Verilog returned X.
A sequential read rolls over silently at the end of the array. The bytes after the wrap arrive exactly like the bytes before it, and a rolled_over flag is one bit that turns a driver bug into a logged event.
Two mutations here were unobservable by construction rather than untested. On a 256-byte part the final mask makes the correct and defective expressions arithmetically equal. The fix was a new device in the bench — a 1024-word part — not a new assertion.
And a reference model has to mirror the pointer, or the most history-dependent transaction on the bus is the one it cannot check.
15. What Comes Next
Reads have no structure: the counter walks the whole array and wraps at the end of it. Writes do not work that way at all.
Chapter 16.3 introduces the page buffer, and with it the most surprising asymmetry in the whole device. A write burst does not walk the array — it walks a page, and the high bits of the address are frozen the moment the word address arrives. A burst that runs off the end of a page does not continue into the next one; it wraps back to the start of the same page and begins overwriting the bytes it just sent.
Which means a sixteen-byte write to a sixteen-byte page can be perfect, and the same sixteen bytes starting one location later can destroy fifteen of themselves — with every byte acknowledged, and no indication anywhere that anything went wrong.
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
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.
- 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.
