I²C · Module 16
Register Maps — From Datasheet to Transactions
The specification describes the register-map pattern in one sentence and then hands every detail of it to the device designer. Explains the six questions a datasheet must answer before a driver can be written, why each has at least two plausible answers that exist in real parts, and how choosing wrong fails in ways that look like bus problems.
Almost every I²C device you will ever talk to is a register map. A sensor, an EEPROM, a real-time clock, a codec, a power controller — the transaction is the same shape in all of them: address the device, say which internal location you mean, then read or write bytes there.
It is the most common pattern on the bus by a wide margin. And UM10204 describes it in one sentence, as an aside, inside a note.
That is the whole normative content. Everything else — whether the location advances, what happens at the end of the map, whether the pointer survives a STOP, what a write to a read-only location does — is not in the specification. Not left vague; explicitly delegated, in the next note but one.
Read that as a contract and it says something uncomfortable: the protocol guarantees you can address a location, and nothing at all about what the device does next. Two parts can be byte-for-byte identical on the wire and behave differently, and both conform.
This is why register-map bugs are so hard to see. The bus is working perfectly. Every byte is acknowledged, every frame is well formed, a scope shows nothing wrong — and the data is wrong, because the master and the device disagree about a convention that the specification declines to fix.
This chapter is about identifying those conventions, one at a time, and making them explicit in a design rather than assumed.
1. What the Specification Actually Provides
It is worth being precise about how little this is, because the rest of the module depends on knowing where the protocol stops.
| the spec does give | section |
|---|---|
| a combined format: write, repeated START, read | §3.1.10, format 3 |
| the rule that the internal location goes in the first data byte | §3.1.10, note 1 |
| the requirement that a device reset its bus logic on any START | §3.1.10, note 4 |
| the master-receiver's NACK before a repeated START | §3.1.10, format 3 |
And here is what it does not give, each of which a driver nonetheless has to know:
| the spec does not give | who decides |
|---|---|
| whether the pointer auto-increments | the device designer, note 2 |
| what happens at the end of the map | the device designer |
| whether the pointer survives a STOP | the device designer |
| what a write to a read-only location does | the device designer |
| where the pointer sits after a read ends | the device designer |
| whether a refused write advances the pointer | the device designer |
| how wide the internal location is | the device designer |
Seven rows in the second table, one in the first. That ratio is the chapter.
2. The Six Questions
Each of these has at least two answers that exist in shipping parts. Getting one wrong produces a driver that works on the bench and fails in a way that does not look like a software bug.
Question 1 — does the pointer auto-increment? If it does, a burst read walks up the map and one transaction can fetch a whole block. If it does not, every byte comes from the same location — which is exactly what you want for a status register with a FIFO behind it, and exactly wrong if you assumed a block read.
Question 2 — what happens at the end of the map? Two answers: wrap to the beginning, or clamp at the top. They differ at exactly one address, which is why a driver written against the wrong one passes every test that does not reach the end.
Question 3 — does the pointer survive a STOP? If it does, a bare read with no pointer write is meaningful — it serves from wherever the last transaction left off. If it does not, the same bare read serves location zero, always. The same transaction means two different things.
Question 4 — what does a write to a read-only location do? NACK it, or accept and discard it. The first tells the master something; the second tells it nothing. Both ship.
Question 5 — where does the pointer sit when a read ends? The master's NACK closes the read, and the device still has to decide whether the counter moved past the byte it just served. It is directly observable: if the counter advances, two back-to-back single-byte reads return different registers; if it does not, they return the same one twice.
Question 6 — does a refused write advance the pointer? It follows from question 4 and has to be answered separately. A NACKed data byte ends the write as far as a conforming master is concerned, so there is no next location to move to — the pointer should stay. §6a argues this at length, because the alternative silently writes bytes intended for later locations somewhere else.
3. The Map, Drawn
The three boxes on the right are the same pointer described three ways. That is the point of drawing it: they are not three features, they are three questions about one register, and a datasheet that answers two of them has told you nothing useful about the third.
4. A Pointer Write and a Burst Read, on the Wire
Write pointer 0x02, repeated START, then read three bytes with auto-increment
9 cyclesThree things in that picture are worth stating explicitly.
The pointer row advances past the last byte served. After the third data byte the pointer reads 05, not 04. That is question 5, visible: the counter moved past the byte the master NACKed. A device that did not advance there would show 04 in that column, and the next bare read would return register 4 a second time.
The repeated START must not disturb the pointer. Note 4 requires a device to reset its bus logic on any START. The pointer is not bus logic — it is device state — and the whole combined format depends on that distinction. A device that cleared the pointer on a repeated START would make the format in the picture impossible.
The acknowledge changes owner at interval 5 and not before. The target acknowledges its own address in interval 4 even though the direction bit says read, because at that instant it is still the receiver. Chapter 9.1 derives this; here it matters because a bench that models the wrong owner will not see a read-only NACK correctly.
5. The Two Conventions That Fail Silently
Questions 2 and 3 deserve a section of their own, because they are the two that a bench will not catch by accident.
Wrap versus clamp differ at exactly one address. A map of eight registers behaves identically under both conventions for a pointer at 0 through 6. At 7 they diverge: one goes to 0, the other stays at 7. So a driver written against the wrong convention works perfectly until a burst reaches the top of the map — which, on a device where the interesting registers are low, may be never in testing and routine in production.
Persistence decides whether a bare read means anything. Consider the shortest possible read transaction: START, address with the read bit, one byte, STOP. No pointer is sent. On a device whose pointer persists, that byte comes from wherever the previous transaction left the pointer, and the transaction's meaning depends on the history of the bus. On a device that clears it at the STOP, the same bytes on the wire always return register 0.
Both are defensible. A persistent pointer makes a repeated single-register poll cheap — write the pointer once, then read one byte at a time forever. A pointer that resets makes every transaction self-contained and therefore analysable in isolation. What is not defensible is a datasheet that does not say which.
6. The Register File in Three Languages
A parameterised register-map target, its independent oracle, and both in all three languages. The design's job is not to be clever; it is to make each of the six conventions a named parameter that can be set either way, so the consequences of each choice can be run rather than reasoned about.
// -----------------------------------------------------------------------------
// i2c_register_file.sv
// Generic I2C register-file target (UM10204 3.1.10 notes 1, 2 and 4).
//
// The specification describes this pattern in one sentence, as an example:
//
// "Combined formats can be used, for example, to control a serial memory. The
// internal memory location must be written during the first data byte."
//
// And then it declines to specify anything else about it:
//
// "All decisions on auto-increment or decrement of previously accessed memory
// locations, etc., are taken by the designer of the device."
//
// So every behaviour below except the pointer-in-the-first-byte rule is a DEVICE
// CONVENTION rather than a protocol requirement. That is why each one is a
// parameter with a stated default instead of hard-wired: a block that bakes in
// one manufacturer's choices cannot model another's, and a master written against
// the wrong choice fails in ways that look like bus problems.
//
// The four questions any datasheet must answer, and where each lives here:
//
// 1. Does the pointer auto-increment? AUTO_INC_ON_READ / AUTO_INC_ON_WRITE
// 2. What happens at the end of the map? PTR_WRAPS (wrap) or clamp
// 3. Does the pointer survive a STOP? PTR_PERSISTS
// 4. What does a write to a read-only
// location do? RO_WRITE_NACKS (NACK) or discard
//
// A fifth question follows from the fourth and is answered here rather than
// parameterised: when a write is REFUSED, the pointer does not advance. A NACKed
// data byte ends the write for a conforming master, so there is no next location.
//
// All four have at least two plausible answers that exist in real parts, and
// choosing differently from the device changes nothing visible until it does.
//
// Obligation from note 4, which is protocol and not convention: a device must
// reset its bus logic on ANY START, "even if these START conditions are not
// positioned according to the proper format". So a START mid-transaction returns
// this block to expecting an address, and the pointer-write state is abandoned.
// -----------------------------------------------------------------------------
module i2c_register_file #(
parameter int N_REGS = 8, // registers in the map
parameter int PTR_W = 3, // pointer width, log2(N_REGS)
parameter [6:0] MY_ADDR = 7'h48,
// ---- the four conventions, each with its default stated -----------------
parameter bit AUTO_INC_ON_READ = 1'b1, // question 1
parameter bit AUTO_INC_ON_WRITE = 1'b1,
parameter bit PTR_WRAPS = 1'b1, // question 2: wrap, else clamp
parameter bit PTR_PERSISTS = 1'b1, // question 3: survives a STOP
parameter bit RO_WRITE_NACKS = 1'b1, // question 4: NACK, else discard
// A read-only mask, one bit per register. Bit i set => register i is read-only.
parameter [N_REGS-1:0] RO_MASK = {N_REGS{1'b0}},
parameter int CNT_W = 8
) (
input logic clk,
input logic rst_n,
// ---- byte-level bus interface -------------------------------------------
input logic start_seen, // a START or repeated START
input logic stop_seen,
input logic byte_valid, // byte_in is a complete received byte
input logic [7:0] byte_in,
input logic is_addr_byte, // first byte after a START
input logic read_byte_done, // the master consumed a transmitted byte
input logic master_acked,
// ---- outputs ------------------------------------------------------------
output logic ack,
output logic [7:0] tx_byte,
output logic tx_valid,
output logic selected, // addressed and participating
output logic [PTR_W-1:0] ptr, // the internal location pointer
output logic ptr_wrapped, // the pointer reached the end and wrapped
output logic ptr_clamped, // ...or was held at the top
output logic ro_write_seen, // a write to a read-only register
output logic [2:0] state,
output logic [CNT_W-1:0] writes_applied,
output logic [CNT_W-1:0] reads_served
);
localparam [2:0] S_IDLE = 3'd0, // awaiting an address byte
S_PTR = 3'd1, // addressed for write; next byte is the pointer
S_WRITE = 3'd2, // pointer set; further bytes are data
S_READ = 3'd3; // addressed for read; serving bytes
logic [7:0] regs [0:N_REGS-1];
integer i;
// The last register index, as a sized value. A part-select of a parameter is
// read as zero by some tools, so the bound is a localparam.
localparam [PTR_W-1:0] PTR_MAX = N_REGS - 1;
// Is the register the pointer currently names read-only? Named rather than
// spelled inline at the one place it is tested, because question 4 is a
// datasheet property and a reader looks for it by name.
wire ptr_is_ro = RO_MASK[ptr];
// ---------------------------------------------------------------------
// Advance the pointer according to the configured convention. Wrapping and
// clamping differ at exactly ONE address, and they are reported separately so
// a testbench can tell which convention is in force.
// ---------------------------------------------------------------------
task advance_ptr;
begin
if (ptr == PTR_MAX) begin
if (PTR_WRAPS) begin
ptr <= {PTR_W{1'b0}};
ptr_wrapped <= 1'b1;
end else begin
ptr <= PTR_MAX; // clamp: stay put
ptr_clamped <= 1'b1;
end
end else begin
ptr <= ptr + 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;
selected <= 1'b0;
ptr <= {PTR_W{1'b0}};
ptr_wrapped <= 1'b0;
ptr_clamped <= 1'b0;
ro_write_seen <= 1'b0;
writes_applied <= {CNT_W{1'b0}};
reads_served <= {CNT_W{1'b0}};
for (i = 0; i < N_REGS; i = i + 1) regs[i] <= 8'h00;
end else begin
ack <= 1'b0;
// -----------------------------------------------------------------
// Note 4, which is protocol rather than convention: reset the bus logic
// on ANY START, wherever it lands. A pointer write in progress is
// abandoned, and the device goes back to expecting an address.
// -----------------------------------------------------------------
if (start_seen) begin
state <= S_IDLE;
selected <= 1'b0;
tx_valid <= 1'b0;
end else if (stop_seen) begin
state <= S_IDLE;
selected <= 1'b0;
tx_valid <= 1'b0;
// Question 3. A pointer that does not persist is reset here, which
// makes a bare read meaningless; one that persists makes a bare read
// depend on transaction history.
if (!PTR_PERSISTS) ptr <= {PTR_W{1'b0}};
end else if (byte_valid) begin
case (state)
// The address byte. Bit 0 is the direction, so a read and a write
// to the same device differ by one bit here.
S_IDLE: begin
if (is_addr_byte && (byte_in[7:1] == MY_ADDR)) begin
ack <= 1'b1;
selected <= 1'b1;
if (byte_in[0]) begin
// READ. No pointer byte follows -- this is the "current
// address read": it serves from wherever the pointer
// already is, which is only meaningful if it persists.
tx_byte <= regs[ptr];
tx_valid <= 1'b1;
state <= S_READ;
end else begin
// WRITE. Note 1: the next byte is the internal location.
state <= S_PTR;
end
end
end
// Note 1, implemented: "The internal memory location must be
// written during the first data byte."
S_PTR: begin
ack <= 1'b1;
ptr <= byte_in[PTR_W-1:0];
state <= S_WRITE;
end
// Data bytes. Question 4 decides what a read-only location does.
S_WRITE: begin
if (ptr_is_ro) begin
ro_write_seen <= 1'b1;
if (RO_WRITE_NACKS) begin
// Refuse the byte. The master learns something, which is
// the argument for this convention.
//
// And the pointer does NOT advance. A NACK on a data byte
// ends the write as far as a conforming master is
// concerned, so there is no next location to move to. A
// master that ignores the NACK and keeps sending will pile
// every remaining byte onto this same read-only register
// and be refused each time -- which is the correct
// outcome, because the alternative silently writes the
// bytes it meant for later locations somewhere else.
ack <= 1'b0;
end else begin
// Accept and discard. The master learns nothing, which is
// the argument against it.
ack <= 1'b1;
if (AUTO_INC_ON_WRITE) advance_ptr;
end
end else begin
regs[ptr] <= byte_in;
writes_applied <= writes_applied + 1'b1;
ack <= 1'b1;
if (AUTO_INC_ON_WRITE) advance_ptr;
end
end
default: ; // S_READ is driven by read_byte_done below
endcase
// -----------------------------------------------------------------
// Serving a read. The master's acknowledge decides whether another byte
// follows, exactly as in Chapter 7.4 -- and question 1 decides whether
// the next byte comes from the next location or the same one.
// -----------------------------------------------------------------
end else if (read_byte_done && state == S_READ) begin
reads_served <= reads_served + 1'b1;
if (!master_acked) begin
// The master is finished. It will send a STOP or a repeated START.
tx_valid <= 1'b0;
state <= S_IDLE;
selected <= 1'b0;
if (AUTO_INC_ON_READ) advance_ptr;
end else begin
if (AUTO_INC_ON_READ) begin
advance_ptr;
// The byte for the NEXT transfer comes from the advanced
// pointer, computed here rather than waiting a cycle.
if (ptr == PTR_MAX)
tx_byte <= PTR_WRAPS ? regs[0] : regs[PTR_MAX];
else
tx_byte <= regs[ptr + 1'b1];
end else begin
// No auto-increment: the same location is served again, which is
// the convention a status register with a FIFO behind it uses.
tx_byte <= regs[ptr];
end
end
end
end
end
endmodule `timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_register_file_tb.sv
// Independent oracle for i2c_register_file.
//
// UM10204 note 2 delegates the memory model to the device designer, so the
// interesting tests are not "does a write work" but "does each CONVENTION behave
// as configured, and is the difference observable". The bench therefore
// instantiates the register file TWICE with opposite conventions:
//
// dut_w : wrapping pointer, persists across STOP, read-only writes NACK
// dut_c : clamping pointer, resets on STOP, read-only writes discard
//
// Both see the same byte stream. Tests 6, 7, 8 and 9 are the four questions from
// the datasheet checklist, and each is checked by comparing the two instances --
// because a single instance cannot show that a convention was a choice.
// -----------------------------------------------------------------------------
module i2c_register_file_tb;
localparam [2:0] S_IDLE = 3'd0, S_PTR = 3'd1, S_WRITE = 3'd2, S_READ = 3'd3;
localparam [6:0] ADDR = 7'h48;
localparam integer N = 8;
// Register 5 is read-only in both instances.
localparam [N-1:0] RO = 8'b0010_0000;
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 w_ack, w_txv, w_sel, w_wrap, w_clamp, w_ro;
logic [7:0] w_tx;
logic [2:0] w_ptr, w_state;
logic [7:0] w_writes, w_reads;
logic c_ack, c_txv, c_sel, c_wrap, c_clamp, c_ro;
logic [7:0] c_tx;
logic [2:0] c_ptr, c_state;
logic [7:0] c_writes, c_reads;
integer errors = 0;
integer n;
// Wrapping / persisting / NACK-on-read-only.
i2c_register_file #(
.N_REGS(N), .PTR_W(3), .MY_ADDR(ADDR),
.AUTO_INC_ON_READ(1'b1), .AUTO_INC_ON_WRITE(1'b1),
.PTR_WRAPS(1'b1), .PTR_PERSISTS(1'b1), .RO_WRITE_NACKS(1'b1),
.RO_MASK(RO), .CNT_W(8)
) dut_w (
.clk(clk), .rst_n(rst_n), .start_seen(start_seen), .stop_seen(stop_seen),
.byte_valid(byte_valid), .byte_in(byte_in), .is_addr_byte(is_addr_byte),
.read_byte_done(read_byte_done), .master_acked(master_acked),
.ack(w_ack), .tx_byte(w_tx), .tx_valid(w_txv), .selected(w_sel),
.ptr(w_ptr), .ptr_wrapped(w_wrap), .ptr_clamped(w_clamp),
.ro_write_seen(w_ro), .state(w_state),
.writes_applied(w_writes), .reads_served(w_reads));
// Clamping / non-persisting / discard-on-read-only.
i2c_register_file #(
.N_REGS(N), .PTR_W(3), .MY_ADDR(ADDR),
.AUTO_INC_ON_READ(1'b1), .AUTO_INC_ON_WRITE(1'b1),
.PTR_WRAPS(1'b0), .PTR_PERSISTS(1'b0), .RO_WRITE_NACKS(1'b0),
.RO_MASK(RO), .CNT_W(8)
) dut_c (
.clk(clk), .rst_n(rst_n), .start_seen(start_seen), .stop_seen(stop_seen),
.byte_valid(byte_valid), .byte_in(byte_in), .is_addr_byte(is_addr_byte),
.read_byte_done(read_byte_done), .master_acked(master_acked),
.ack(c_ack), .tx_byte(c_tx), .tx_valid(c_txv), .selected(c_sel),
.ptr(c_ptr), .ptr_wrapped(c_wrap), .ptr_clamped(c_clamp),
.ro_write_seen(c_ro), .state(c_state),
.writes_applied(c_writes), .reads_served(c_reads));
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;
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 [6:0] a, input rw);
begin
@(negedge clk); byte_in = {a, rw}; is_addr_byte = 1'b1; byte_valid = 1'b1;
@(posedge clk); @(negedge clk); byte_valid = 1'b0; is_addr_byte = 1'b0;
end
endtask
task send_data (input [7:0] b);
begin
@(negedge clk); byte_in = b; is_addr_byte = 1'b0; byte_valid = 1'b1;
@(posedge clk); @(negedge clk); byte_valid = 1'b0;
end
endtask
task take (input do_ack);
begin
@(negedge clk); read_byte_done = 1'b1; master_acked = do_ack;
@(posedge clk); @(negedge clk); read_byte_done = 1'b0;
end
endtask
task 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
// A complete pointer-then-write transaction.
task write_regs (input [7:0] p, input integer count, input [7:0] first);
begin
ev_start;
send_addr(ADDR, 1'b0);
send_data(p);
for (n = 0; n < count; n = n + 1) send_data(first + n[7:0]);
ev_stop;
end
endtask
initial begin
$display("=== i2c_register_file: the pattern the spec names and then declines to define ===");
// ----------------------------------------------------------------
// T1. Note 1, the whole model in one transaction: address, pointer, data.
// "The internal memory location must be written during the first data
// byte."
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(ADDR, 1'b0);
ck_bit("T1 addressed and acknowledged", w_ack, 1'b1);
ck_int("T1 awaiting the pointer byte", w_state, S_PTR);
send_data(8'd2); // the pointer
ck_int("T1 pointer took the first data byte", w_ptr, 2);
ck_int("T1 now expecting data", w_state, S_WRITE);
send_data(8'hAA);
$display("T1 address, pointer, data -- the register-map model");
ck_int("T1 one write applied", w_writes, 1);
ev_stop;
// ----------------------------------------------------------------
// T2. Auto-increment on write. Four bytes from pointer 0 land in
// registers 0 to 3, which is note 2's delegation exercised.
// ----------------------------------------------------------------
do_reset;
write_regs(8'd0, 4, 8'h10);
$display("T2 auto-increment on write fills consecutive registers");
ck_int("T2 four writes applied", w_writes, 4);
// read them back to confirm placement
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd0);
ev_start; // repeated START, direction change
send_addr(ADDR, 1'b1);
ck_int("T2 reg 0", w_tx, 8'h10); take(1'b1);
ck_int("T2 reg 1", w_tx, 8'h11); take(1'b1);
ck_int("T2 reg 2", w_tx, 8'h12); take(1'b1);
ck_int("T2 reg 3", w_tx, 8'h13); take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T3. The combined format of §3.1.10: write the pointer, repeated START,
// read. The address is repeated with the R/W bit reversed.
// ----------------------------------------------------------------
do_reset;
write_regs(8'd6, 1, 8'h5A);
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd6);
ev_start;
send_addr(ADDR, 1'b1);
$display("T3 combined format: pointer write, repeated START, read");
ck_bit("T3 selected for read", w_sel, 1'b1);
ck_bit("T3 has a byte to send", w_txv, 1'b1);
ck_int("T3 serving register 6", w_tx, 8'h5A);
ck_int("T3 in the read state", w_state, S_READ);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T4. A wrong address is ignored entirely -- no acknowledge, no pointer
// state, nothing.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(7'h50, 1'b0);
$display("T4 another device's address is ignored");
ck_bit("T4 did not acknowledge", w_ack, 1'b0);
ck_bit("T4 not selected", w_sel, 1'b0);
ck_int("T4 stayed idle", w_state, S_IDLE);
send_data(8'd3);
ck_bit("T4 still silent", w_ack, 1'b0);
ck_int("T4 pointer untouched", w_ptr, 0);
// ----------------------------------------------------------------
// T5. Note 4, which IS protocol: a START mid-transaction resets the bus
// logic. The pointer write in progress is abandoned.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(ADDR, 1'b0);
ck_int("T5 awaiting the pointer", w_state, S_PTR);
ev_start; // START, mid-transaction
$display("T5 a START anywhere resets the bus logic (note 4)");
ck_int("T5 back to expecting an address", w_state, S_IDLE);
ck_bit("T5 no longer selected", w_sel, 1'b0);
// and the next byte is treated as an address, not as a pointer
send_addr(ADDR, 1'b0);
ck_int("T5 that byte was read as an address", w_state, S_PTR);
// ----------------------------------------------------------------
// T6. QUESTION 2: wrap versus clamp. Both instances read past the end of
// the map; they differ at exactly one address, and that is the point.
// ----------------------------------------------------------------
do_reset;
// Register 5 is read-only, and a refused write does not advance the
// pointer, so a single eight-byte burst would stall there. Fill around it.
write_regs(8'd0, 5, 8'h20); // 0..4 = 0x20..0x24
write_regs(8'd6, 2, 8'h26); // 6..7 = 0x26..0x27
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd7); // point at the LAST register
ev_start;
send_addr(ADDR, 1'b1);
$display("T6 question 2: the end of the map -- wrap or clamp");
ck_int("T6 both serve register 7", w_tx, 8'h27);
ck_int("T6 clamping instance agrees so far", c_tx, 8'h27);
take(1'b1); // ask for one more
ck_int("T6 wrapping instance now serves register 0", w_tx, 8'h20);
ck_int("T6 clamping instance serves register 7 again", c_tx, 8'h27);
ck_bit("T6 wrap reported", w_wrap, 1'b1);
ck_bit("T6 clamp reported", c_clamp, 1'b1);
ck_bit("T6 wrapping instance did not clamp", w_clamp, 1'b0);
ck_bit("T6 clamping instance did not wrap", c_wrap, 1'b0);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T7. QUESTION 3: does the pointer survive a STOP? A bare read with no
// pointer byte -- a "current address read" -- is only meaningful if it
// does, and the two instances disagree.
// ----------------------------------------------------------------
do_reset;
write_regs(8'd0, 5, 8'h30); // 0..4 = 0x30..0x34
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd3); // leave the pointer at 3
ev_stop;
$display("T7 question 3: does the pointer survive a STOP");
ck_int("T7 persisting instance kept pointer 3", w_ptr, 3);
ck_int("T7 non-persisting instance reset to 0", c_ptr, 0);
// a bare read, with no pointer byte at all
ev_start;
send_addr(ADDR, 1'b1);
ck_int("T7 persisting reads register 3", w_tx, 8'h33);
ck_int("T7 non-persisting reads register 0", c_tx, 8'h30);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T8. QUESTION 4: a write to a read-only register. Register 5 is read-only
// in both instances; one NACKs the data byte and one accepts and
// discards it. The master can detect the first and not the second.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd5); // the read-only register
send_data(8'hFF);
$display("T8 question 4: writing a read-only register");
ck_bit("T8 NACKing instance refused the byte", w_ack, 1'b0);
ck_bit("T8 discarding instance accepted it", c_ack, 1'b1);
ck_bit("T8 both noticed the attempt (w)", w_ro, 1'b1);
ck_bit("T8 both noticed the attempt (c)", c_ro, 1'b1);
ck_int("T8 neither applied a write", w_writes, 0);
ck_int("T8 neither applied a write (c)", c_writes, 0);
ev_stop;
// and the register really is unchanged
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd5);
ev_start;
send_addr(ADDR, 1'b1);
ck_int("T8 register 5 still zero", w_tx, 8'h00);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T9. A write that walks INTO the read-only register. Registers 4 and 6
// are writable and 5 is not, so a three-byte burst from pointer 4
// writes two registers and is refused in the middle -- which is the
// case a master that ignores the acknowledge silently mis-programs.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd4);
send_data(8'h41); // register 4: accepted
ck_bit("T9 register 4 accepted", w_ack, 1'b1);
send_data(8'h42); // register 5: read-only
$display("T9 a burst write that walks into a read-only register");
ck_bit("T9 register 5 refused", w_ack, 1'b0);
ev_stop;
ck_int("T9 only one write applied", w_writes, 1);
// ----------------------------------------------------------------
// T10. A REFUSED write does not advance the pointer. Three bytes aimed at
// the read-only register 5 are all refused and all land on 5 -- they do
// not walk forward into registers 6 and 7. That is the behaviour that
// stops a master which ignores the acknowledge from writing its later
// bytes to the wrong places.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd5); // point at the read-only register
for (n = 0; n < 3; n = n + 1) begin
send_data(8'hE0 + n[7:0]);
ck_bit("T10 every byte refused", w_ack, 1'b0);
ck_int("T10 pointer never advanced", w_ptr, 5);
end
ev_stop;
$display("T10 a refused write stalls the pointer instead of walking on");
ck_int("T10 nothing was applied", w_writes, 0);
// registers 6 and 7 are untouched, which is the point
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd6);
ev_start;
send_addr(ADDR, 1'b1);
ck_int("T10 register 6 untouched", w_tx, 8'h00); take(1'b1);
ck_int("T10 register 7 untouched", w_tx, 8'h00); take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T11. Read counters. Five bytes taken means five reads served, whatever
// the pointer did.
// ----------------------------------------------------------------
do_reset;
// Five bytes, not eight: register 5 is read-only and a refused write stalls
// the pointer, so a longer burst would not reach registers 6 and 7 anyway.
write_regs(8'd0, 5, 8'h50);
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd0);
ev_start;
send_addr(ADDR, 1'b1);
for (n = 0; n < 4; n = n + 1) take(1'b1);
take(1'b0);
$display("T11 the read counter tracks bytes served");
ck_int("T11 five reads served", w_reads, 5);
ev_stop;
// ----------------------------------------------------------------
// T12. Two transactions back to back, to show nothing leaks. The second
// write must land where its own pointer says, not where the first
// one finished.
// ----------------------------------------------------------------
do_reset;
write_regs(8'd0, 3, 8'h60); // 0,1,2 = 0x60,0x61,0x62
write_regs(8'd6, 2, 8'h70); // 6,7 = 0x70,0x71
$display("T12 two transactions, each honouring its own pointer");
ck_int("T12 five writes in total", w_writes, 5);
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd0);
ev_start;
send_addr(ADDR, 1'b1);
ck_int("T12 reg 0", w_tx, 8'h60); take(1'b1);
ck_int("T12 reg 1", w_tx, 8'h61); take(1'b1);
ck_int("T12 reg 2", w_tx, 8'h62); take(1'b1);
ck_int("T12 reg 3 untouched", w_tx, 8'h00); take(1'b1);
ck_int("T12 reg 4 untouched", w_tx, 8'h00); take(1'b1);
ck_int("T12 reg 5 untouched", w_tx, 8'h00); take(1'b1);
ck_int("T12 reg 6", w_tx, 8'h70); take(1'b1);
ck_int("T12 reg 7", w_tx, 8'h71); take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T13. WHERE DOES THE POINTER SIT WHEN A READ ENDS? The master's NACK ends
// the read, and the datasheet still has to say whether the counter
// moved past the byte it just served. It matters because the next bare
// read starts from wherever this one left it: if the counter advances,
// two back-to-back single-byte reads return DIFFERENT registers, and if
// it does not, they return the same one twice.
//
// This device advances. Nothing in UM10204 requires either answer.
// ----------------------------------------------------------------
do_reset;
write_regs(8'd0, 8, 8'hA0); // 0..7 = 0xA0..0xA7
ev_start; send_addr(ADDR, 1'b0); send_data(8'd2); ev_stop;
ev_start; send_addr(ADDR, 1'b1);
$display("T13 the pointer's position after a read that ended in a NACK");
ck_int("T13 the single byte read is reg 2", w_tx, 8'hA2);
take(1'b0); // NACK: the read ends here
ev_stop;
ck_int("T13 the pointer advanced past it", w_ptr, 3'd3);
// A bare read now, with no pointer write, must therefore serve reg 3.
ev_start; send_addr(ADDR, 1'b1);
ck_int("T13 the next bare read serves reg 3", w_tx, 8'hA3);
take(1'b0); ev_stop;
ck_int("T13 and the pointer advanced again", w_ptr, 3'd4);
if (errors == 0)
$display("=== i2c_register_file: ALL CHECKS PASSED ===");
else
$display("=== i2c_register_file: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule // -----------------------------------------------------------------------------
// i2c_register_file.sv
// Generic I2C register-file target (UM10204 3.1.10 notes 1, 2 and 4).
//
// The specification describes this pattern in one sentence, as an example:
//
// "Combined formats can be used, for example, to control a serial memory. The
// internal memory location must be written during the first data byte."
//
// And then it declines to specify anything else about it:
//
// "All decisions on auto-increment or decrement of previously accessed memory
// locations, etc., are taken by the designer of the device."
//
// So every behaviour below except the pointer-in-the-first-byte rule is a DEVICE
// CONVENTION rather than a protocol requirement. That is why each one is a
// parameter with a stated default instead of hard-wired: a block that bakes in
// one manufacturer's choices cannot model another's, and a master written against
// the wrong choice fails in ways that look like bus problems.
//
// The four questions any datasheet must answer, and where each lives here:
//
// 1. Does the pointer auto-increment? AUTO_INC_ON_READ / AUTO_INC_ON_WRITE
// 2. What happens at the end of the map? PTR_WRAPS (wrap) or clamp
// 3. Does the pointer survive a STOP? PTR_PERSISTS
// 4. What does a write to a read-only
// location do? RO_WRITE_NACKS (NACK) or discard
//
// A fifth question follows from the fourth and is answered here rather than
// parameterised: when a write is REFUSED, the pointer does not advance. A NACKed
// data byte ends the write for a conforming master, so there is no next location.
//
// All four have at least two plausible answers that exist in real parts, and
// choosing differently from the device changes nothing visible until it does.
//
// Obligation from note 4, which is protocol and not convention: a device must
// reset its bus logic on ANY START, "even if these START conditions are not
// positioned according to the proper format". So a START mid-transaction returns
// this block to expecting an address, and the pointer-write state is abandoned.
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_register_file #(
parameter N_REGS = 8, // registers in the map
parameter PTR_W = 3, // pointer width, log2(N_REGS)
parameter [6:0] MY_ADDR = 7'h48,
// ---- the four conventions, each with its default stated -----------------
parameter AUTO_INC_ON_READ = 1'b1, // question 1
parameter AUTO_INC_ON_WRITE = 1'b1,
parameter PTR_WRAPS = 1'b1, // question 2: wrap, else clamp
parameter PTR_PERSISTS = 1'b1, // question 3: survives a STOP
parameter RO_WRITE_NACKS = 1'b1, // question 4: NACK, else discard
// A read-only mask, one bit per register. Bit i set => register i is read-only.
parameter [N_REGS-1:0] RO_MASK = {N_REGS{1'b0}},
parameter CNT_W = 8
) (
input wire clk,
input wire rst_n,
// ---- byte-level bus interface -------------------------------------------
input wire start_seen, // a START or repeated START
input wire stop_seen,
input wire byte_valid, // byte_in is a complete received byte
input wire [7:0] byte_in,
input wire is_addr_byte, // first byte after a START
input wire read_byte_done, // the master consumed a transmitted byte
input wire master_acked,
// ---- outputs ------------------------------------------------------------
output reg ack,
output reg [7:0] tx_byte,
output reg tx_valid,
output reg selected, // addressed and participating
output reg [PTR_W-1:0] ptr, // the internal location pointer
output reg ptr_wrapped, // the pointer reached the end and wrapped
output reg ptr_clamped, // ...or was held at the top
output reg ro_write_seen, // a write to a read-only register
output reg [2:0] state,
output reg [CNT_W-1:0] writes_applied,
output reg [CNT_W-1:0] reads_served
);
localparam [2:0] S_IDLE = 3'd0, // awaiting an address byte
S_PTR = 3'd1, // addressed for write; next byte is the pointer
S_WRITE = 3'd2, // pointer set; further bytes are data
S_READ = 3'd3; // addressed for read; serving bytes
reg [7:0] regs [0:N_REGS-1];
integer i;
// The last register index, as a sized value. A part-select of a parameter is
// read as zero by some tools, so the bound is a localparam.
localparam [PTR_W-1:0] PTR_MAX = N_REGS - 1;
// Is the register the pointer currently names read-only? Named rather than
// spelled inline at the one place it is tested, because question 4 is a
// datasheet property and a reader looks for it by name.
wire ptr_is_ro = RO_MASK[ptr];
// ---------------------------------------------------------------------
// Advance the pointer according to the configured convention. Wrapping and
// clamping differ at exactly ONE address, and they are reported separately so
// a testbench can tell which convention is in force.
// ---------------------------------------------------------------------
task advance_ptr;
begin
if (ptr == PTR_MAX) begin
if (PTR_WRAPS) begin
ptr <= {PTR_W{1'b0}};
ptr_wrapped <= 1'b1;
end else begin
ptr <= PTR_MAX; // clamp: stay put
ptr_clamped <= 1'b1;
end
end else begin
ptr <= ptr + 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;
selected <= 1'b0;
ptr <= {PTR_W{1'b0}};
ptr_wrapped <= 1'b0;
ptr_clamped <= 1'b0;
ro_write_seen <= 1'b0;
writes_applied <= {CNT_W{1'b0}};
reads_served <= {CNT_W{1'b0}};
for (i = 0; i < N_REGS; i = i + 1) regs[i] <= 8'h00;
end else begin
ack <= 1'b0;
// -----------------------------------------------------------------
// Note 4, which is protocol rather than convention: reset the bus logic
// on ANY START, wherever it lands. A pointer write in progress is
// abandoned, and the device goes back to expecting an address.
// -----------------------------------------------------------------
if (start_seen) begin
state <= S_IDLE;
selected <= 1'b0;
tx_valid <= 1'b0;
end else if (stop_seen) begin
state <= S_IDLE;
selected <= 1'b0;
tx_valid <= 1'b0;
// Question 3. A pointer that does not persist is reset here, which
// makes a bare read meaningless; one that persists makes a bare read
// depend on transaction history.
if (!PTR_PERSISTS) ptr <= {PTR_W{1'b0}};
end else if (byte_valid) begin
case (state)
// The address byte. Bit 0 is the direction, so a read and a write
// to the same device differ by one bit here.
S_IDLE: begin
if (is_addr_byte && (byte_in[7:1] == MY_ADDR)) begin
ack <= 1'b1;
selected <= 1'b1;
if (byte_in[0]) begin
// READ. No pointer byte follows -- this is the "current
// address read": it serves from wherever the pointer
// already is, which is only meaningful if it persists.
tx_byte <= regs[ptr];
tx_valid <= 1'b1;
state <= S_READ;
end else begin
// WRITE. Note 1: the next byte is the internal location.
state <= S_PTR;
end
end
end
// Note 1, implemented: "The internal memory location must be
// written during the first data byte."
S_PTR: begin
ack <= 1'b1;
ptr <= byte_in[PTR_W-1:0];
state <= S_WRITE;
end
// Data bytes. Question 4 decides what a read-only location does.
S_WRITE: begin
if (ptr_is_ro) begin
ro_write_seen <= 1'b1;
if (RO_WRITE_NACKS) begin
// Refuse the byte. The master learns something, which is
// the argument for this convention.
//
// And the pointer does NOT advance. A NACK on a data byte
// ends the write as far as a conforming master is
// concerned, so there is no next location to move to. A
// master that ignores the NACK and keeps sending will pile
// every remaining byte onto this same read-only register
// and be refused each time -- which is the correct
// outcome, because the alternative silently writes the
// bytes it meant for later locations somewhere else.
ack <= 1'b0;
end else begin
// Accept and discard. The master learns nothing, which is
// the argument against it.
ack <= 1'b1;
if (AUTO_INC_ON_WRITE) advance_ptr;
end
end else begin
regs[ptr] <= byte_in;
writes_applied <= writes_applied + 1'b1;
ack <= 1'b1;
if (AUTO_INC_ON_WRITE) advance_ptr;
end
end
default: ; // S_READ is driven by read_byte_done below
endcase
// -----------------------------------------------------------------
// Serving a read. The master's acknowledge decides whether another byte
// follows, exactly as in Chapter 7.4 -- and question 1 decides whether
// the next byte comes from the next location or the same one.
// -----------------------------------------------------------------
end else if (read_byte_done && state == S_READ) begin
reads_served <= reads_served + 1'b1;
if (!master_acked) begin
// The master is finished. It will send a STOP or a repeated START.
tx_valid <= 1'b0;
state <= S_IDLE;
selected <= 1'b0;
if (AUTO_INC_ON_READ) advance_ptr;
end else begin
if (AUTO_INC_ON_READ) begin
advance_ptr;
// The byte for the NEXT transfer comes from the advanced
// pointer, computed here rather than waiting a cycle.
if (ptr == PTR_MAX)
tx_byte <= PTR_WRAPS ? regs[0] : regs[PTR_MAX];
else
tx_byte <= regs[ptr + 1'b1];
end else begin
// No auto-increment: the same location is served again, which is
// the convention a status register with a FIFO behind it uses.
tx_byte <= regs[ptr];
end
end
end
end
end
endmodule `timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_register_file_tb.sv
// Independent oracle for i2c_register_file.
//
// UM10204 note 2 delegates the memory model to the device designer, so the
// interesting tests are not "does a write work" but "does each CONVENTION behave
// as configured, and is the difference observable". The bench therefore
// instantiates the register file TWICE with opposite conventions:
//
// dut_w : wrapping pointer, persists across STOP, read-only writes NACK
// dut_c : clamping pointer, resets on STOP, read-only writes discard
//
// Both see the same byte stream. Tests 6, 7, 8 and 9 are the four questions from
// the datasheet checklist, and each is checked by comparing the two instances --
// because a single instance cannot show that a convention was a choice.
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_register_file_tb;
localparam [2:0] S_IDLE = 3'd0, S_PTR = 3'd1, S_WRITE = 3'd2, S_READ = 3'd3;
localparam [6:0] ADDR = 7'h48;
localparam integer N = 8;
// Register 5 is read-only in both instances.
localparam [N-1:0] RO = 8'b0010_0000;
reg clk = 1'b0;
reg rst_n = 1'b0;
reg start_seen = 1'b0;
reg stop_seen = 1'b0;
reg byte_valid = 1'b0;
reg [7:0] byte_in = 8'h00;
reg is_addr_byte = 1'b0;
reg read_byte_done = 1'b0;
reg master_acked = 1'b0;
wire w_ack, w_txv, w_sel, w_wrap, w_clamp, w_ro;
wire [7:0] w_tx;
wire [2:0] w_ptr, w_state;
wire [7:0] w_writes, w_reads;
wire c_ack, c_txv, c_sel, c_wrap, c_clamp, c_ro;
wire [7:0] c_tx;
wire [2:0] c_ptr, c_state;
wire [7:0] c_writes, c_reads;
integer errors = 0;
integer n;
// Wrapping / persisting / NACK-on-read-only.
i2c_register_file #(
.N_REGS(N), .PTR_W(3), .MY_ADDR(ADDR),
.AUTO_INC_ON_READ(1'b1), .AUTO_INC_ON_WRITE(1'b1),
.PTR_WRAPS(1'b1), .PTR_PERSISTS(1'b1), .RO_WRITE_NACKS(1'b1),
.RO_MASK(RO), .CNT_W(8)
) dut_w (
.clk(clk), .rst_n(rst_n), .start_seen(start_seen), .stop_seen(stop_seen),
.byte_valid(byte_valid), .byte_in(byte_in), .is_addr_byte(is_addr_byte),
.read_byte_done(read_byte_done), .master_acked(master_acked),
.ack(w_ack), .tx_byte(w_tx), .tx_valid(w_txv), .selected(w_sel),
.ptr(w_ptr), .ptr_wrapped(w_wrap), .ptr_clamped(w_clamp),
.ro_write_seen(w_ro), .state(w_state),
.writes_applied(w_writes), .reads_served(w_reads));
// Clamping / non-persisting / discard-on-read-only.
i2c_register_file #(
.N_REGS(N), .PTR_W(3), .MY_ADDR(ADDR),
.AUTO_INC_ON_READ(1'b1), .AUTO_INC_ON_WRITE(1'b1),
.PTR_WRAPS(1'b0), .PTR_PERSISTS(1'b0), .RO_WRITE_NACKS(1'b0),
.RO_MASK(RO), .CNT_W(8)
) dut_c (
.clk(clk), .rst_n(rst_n), .start_seen(start_seen), .stop_seen(stop_seen),
.byte_valid(byte_valid), .byte_in(byte_in), .is_addr_byte(is_addr_byte),
.read_byte_done(read_byte_done), .master_acked(master_acked),
.ack(c_ack), .tx_byte(c_tx), .tx_valid(c_txv), .selected(c_sel),
.ptr(c_ptr), .ptr_wrapped(c_wrap), .ptr_clamped(c_clamp),
.ro_write_seen(c_ro), .state(c_state),
.writes_applied(c_writes), .reads_served(c_reads));
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;
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 [6:0] a, input rw);
begin
@(negedge clk); byte_in = {a, rw}; is_addr_byte = 1'b1; byte_valid = 1'b1;
@(posedge clk); @(negedge clk); byte_valid = 1'b0; is_addr_byte = 1'b0;
end
endtask
task send_data (input [7:0] b);
begin
@(negedge clk); byte_in = b; is_addr_byte = 1'b0; byte_valid = 1'b1;
@(posedge clk); @(negedge clk); byte_valid = 1'b0;
end
endtask
task take (input do_ack);
begin
@(negedge clk); read_byte_done = 1'b1; master_acked = do_ack;
@(posedge clk); @(negedge clk); read_byte_done = 1'b0;
end
endtask
task 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
// A complete pointer-then-write transaction.
task write_regs (input [7:0] p, input integer count, input [7:0] first);
begin
ev_start;
send_addr(ADDR, 1'b0);
send_data(p);
for (n = 0; n < count; n = n + 1) send_data(first + n[7:0]);
ev_stop;
end
endtask
initial begin
$display("=== i2c_register_file: the pattern the spec names and then declines to define ===");
// ----------------------------------------------------------------
// T1. Note 1, the whole model in one transaction: address, pointer, data.
// "The internal memory location must be written during the first data
// byte."
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(ADDR, 1'b0);
ck_bit("T1 addressed and acknowledged", w_ack, 1'b1);
ck_int("T1 awaiting the pointer byte", w_state, S_PTR);
send_data(8'd2); // the pointer
ck_int("T1 pointer took the first data byte", w_ptr, 2);
ck_int("T1 now expecting data", w_state, S_WRITE);
send_data(8'hAA);
$display("T1 address, pointer, data -- the register-map model");
ck_int("T1 one write applied", w_writes, 1);
ev_stop;
// ----------------------------------------------------------------
// T2. Auto-increment on write. Four bytes from pointer 0 land in
// registers 0 to 3, which is note 2's delegation exercised.
// ----------------------------------------------------------------
do_reset;
write_regs(8'd0, 4, 8'h10);
$display("T2 auto-increment on write fills consecutive registers");
ck_int("T2 four writes applied", w_writes, 4);
// read them back to confirm placement
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd0);
ev_start; // repeated START, direction change
send_addr(ADDR, 1'b1);
ck_int("T2 reg 0", w_tx, 8'h10); take(1'b1);
ck_int("T2 reg 1", w_tx, 8'h11); take(1'b1);
ck_int("T2 reg 2", w_tx, 8'h12); take(1'b1);
ck_int("T2 reg 3", w_tx, 8'h13); take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T3. The combined format of §3.1.10: write the pointer, repeated START,
// read. The address is repeated with the R/W bit reversed.
// ----------------------------------------------------------------
do_reset;
write_regs(8'd6, 1, 8'h5A);
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd6);
ev_start;
send_addr(ADDR, 1'b1);
$display("T3 combined format: pointer write, repeated START, read");
ck_bit("T3 selected for read", w_sel, 1'b1);
ck_bit("T3 has a byte to send", w_txv, 1'b1);
ck_int("T3 serving register 6", w_tx, 8'h5A);
ck_int("T3 in the read state", w_state, S_READ);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T4. A wrong address is ignored entirely -- no acknowledge, no pointer
// state, nothing.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(7'h50, 1'b0);
$display("T4 another device's address is ignored");
ck_bit("T4 did not acknowledge", w_ack, 1'b0);
ck_bit("T4 not selected", w_sel, 1'b0);
ck_int("T4 stayed idle", w_state, S_IDLE);
send_data(8'd3);
ck_bit("T4 still silent", w_ack, 1'b0);
ck_int("T4 pointer untouched", w_ptr, 0);
// ----------------------------------------------------------------
// T5. Note 4, which IS protocol: a START mid-transaction resets the bus
// logic. The pointer write in progress is abandoned.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(ADDR, 1'b0);
ck_int("T5 awaiting the pointer", w_state, S_PTR);
ev_start; // START, mid-transaction
$display("T5 a START anywhere resets the bus logic (note 4)");
ck_int("T5 back to expecting an address", w_state, S_IDLE);
ck_bit("T5 no longer selected", w_sel, 1'b0);
// and the next byte is treated as an address, not as a pointer
send_addr(ADDR, 1'b0);
ck_int("T5 that byte was read as an address", w_state, S_PTR);
// ----------------------------------------------------------------
// T6. QUESTION 2: wrap versus clamp. Both instances read past the end of
// the map; they differ at exactly one address, and that is the point.
// ----------------------------------------------------------------
do_reset;
// Register 5 is read-only, and a refused write does not advance the
// pointer, so a single eight-byte burst would stall there. Fill around it.
write_regs(8'd0, 5, 8'h20); // 0..4 = 0x20..0x24
write_regs(8'd6, 2, 8'h26); // 6..7 = 0x26..0x27
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd7); // point at the LAST register
ev_start;
send_addr(ADDR, 1'b1);
$display("T6 question 2: the end of the map -- wrap or clamp");
ck_int("T6 both serve register 7", w_tx, 8'h27);
ck_int("T6 clamping instance agrees so far", c_tx, 8'h27);
take(1'b1); // ask for one more
ck_int("T6 wrapping instance now serves register 0", w_tx, 8'h20);
ck_int("T6 clamping instance serves register 7 again", c_tx, 8'h27);
ck_bit("T6 wrap reported", w_wrap, 1'b1);
ck_bit("T6 clamp reported", c_clamp, 1'b1);
ck_bit("T6 wrapping instance did not clamp", w_clamp, 1'b0);
ck_bit("T6 clamping instance did not wrap", c_wrap, 1'b0);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T7. QUESTION 3: does the pointer survive a STOP? A bare read with no
// pointer byte -- a "current address read" -- is only meaningful if it
// does, and the two instances disagree.
// ----------------------------------------------------------------
do_reset;
write_regs(8'd0, 5, 8'h30); // 0..4 = 0x30..0x34
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd3); // leave the pointer at 3
ev_stop;
$display("T7 question 3: does the pointer survive a STOP");
ck_int("T7 persisting instance kept pointer 3", w_ptr, 3);
ck_int("T7 non-persisting instance reset to 0", c_ptr, 0);
// a bare read, with no pointer byte at all
ev_start;
send_addr(ADDR, 1'b1);
ck_int("T7 persisting reads register 3", w_tx, 8'h33);
ck_int("T7 non-persisting reads register 0", c_tx, 8'h30);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T8. QUESTION 4: a write to a read-only register. Register 5 is read-only
// in both instances; one NACKs the data byte and one accepts and
// discards it. The master can detect the first and not the second.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd5); // the read-only register
send_data(8'hFF);
$display("T8 question 4: writing a read-only register");
ck_bit("T8 NACKing instance refused the byte", w_ack, 1'b0);
ck_bit("T8 discarding instance accepted it", c_ack, 1'b1);
ck_bit("T8 both noticed the attempt (w)", w_ro, 1'b1);
ck_bit("T8 both noticed the attempt (c)", c_ro, 1'b1);
ck_int("T8 neither applied a write", w_writes, 0);
ck_int("T8 neither applied a write (c)", c_writes, 0);
ev_stop;
// and the register really is unchanged
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd5);
ev_start;
send_addr(ADDR, 1'b1);
ck_int("T8 register 5 still zero", w_tx, 8'h00);
take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T9. A write that walks INTO the read-only register. Registers 4 and 6
// are writable and 5 is not, so a three-byte burst from pointer 4
// writes two registers and is refused in the middle -- which is the
// case a master that ignores the acknowledge silently mis-programs.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd4);
send_data(8'h41); // register 4: accepted
ck_bit("T9 register 4 accepted", w_ack, 1'b1);
send_data(8'h42); // register 5: read-only
$display("T9 a burst write that walks into a read-only register");
ck_bit("T9 register 5 refused", w_ack, 1'b0);
ev_stop;
ck_int("T9 only one write applied", w_writes, 1);
// ----------------------------------------------------------------
// T10. A REFUSED write does not advance the pointer. Three bytes aimed at
// the read-only register 5 are all refused and all land on 5 -- they do
// not walk forward into registers 6 and 7. That is the behaviour that
// stops a master which ignores the acknowledge from writing its later
// bytes to the wrong places.
// ----------------------------------------------------------------
do_reset;
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd5); // point at the read-only register
for (n = 0; n < 3; n = n + 1) begin
send_data(8'hE0 + n[7:0]);
ck_bit("T10 every byte refused", w_ack, 1'b0);
ck_int("T10 pointer never advanced", w_ptr, 5);
end
ev_stop;
$display("T10 a refused write stalls the pointer instead of walking on");
ck_int("T10 nothing was applied", w_writes, 0);
// registers 6 and 7 are untouched, which is the point
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd6);
ev_start;
send_addr(ADDR, 1'b1);
ck_int("T10 register 6 untouched", w_tx, 8'h00); take(1'b1);
ck_int("T10 register 7 untouched", w_tx, 8'h00); take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T11. Read counters. Five bytes taken means five reads served, whatever
// the pointer did.
// ----------------------------------------------------------------
do_reset;
// Five bytes, not eight: register 5 is read-only and a refused write stalls
// the pointer, so a longer burst would not reach registers 6 and 7 anyway.
write_regs(8'd0, 5, 8'h50);
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd0);
ev_start;
send_addr(ADDR, 1'b1);
for (n = 0; n < 4; n = n + 1) take(1'b1);
take(1'b0);
$display("T11 the read counter tracks bytes served");
ck_int("T11 five reads served", w_reads, 5);
ev_stop;
// ----------------------------------------------------------------
// T12. Two transactions back to back, to show nothing leaks. The second
// write must land where its own pointer says, not where the first
// one finished.
// ----------------------------------------------------------------
do_reset;
write_regs(8'd0, 3, 8'h60); // 0,1,2 = 0x60,0x61,0x62
write_regs(8'd6, 2, 8'h70); // 6,7 = 0x70,0x71
$display("T12 two transactions, each honouring its own pointer");
ck_int("T12 five writes in total", w_writes, 5);
ev_start;
send_addr(ADDR, 1'b0);
send_data(8'd0);
ev_start;
send_addr(ADDR, 1'b1);
ck_int("T12 reg 0", w_tx, 8'h60); take(1'b1);
ck_int("T12 reg 1", w_tx, 8'h61); take(1'b1);
ck_int("T12 reg 2", w_tx, 8'h62); take(1'b1);
ck_int("T12 reg 3 untouched", w_tx, 8'h00); take(1'b1);
ck_int("T12 reg 4 untouched", w_tx, 8'h00); take(1'b1);
ck_int("T12 reg 5 untouched", w_tx, 8'h00); take(1'b1);
ck_int("T12 reg 6", w_tx, 8'h70); take(1'b1);
ck_int("T12 reg 7", w_tx, 8'h71); take(1'b0);
ev_stop;
// ----------------------------------------------------------------
// T13. WHERE DOES THE POINTER SIT WHEN A READ ENDS? The master's NACK ends
// the read, and the datasheet still has to say whether the counter
// moved past the byte it just served. It matters because the next bare
// read starts from wherever this one left it: if the counter advances,
// two back-to-back single-byte reads return DIFFERENT registers, and if
// it does not, they return the same one twice.
//
// This device advances. Nothing in UM10204 requires either answer.
// ----------------------------------------------------------------
do_reset;
write_regs(8'd0, 8, 8'hA0); // 0..7 = 0xA0..0xA7
ev_start; send_addr(ADDR, 1'b0); send_data(8'd2); ev_stop;
ev_start; send_addr(ADDR, 1'b1);
$display("T13 the pointer's position after a read that ended in a NACK");
ck_int("T13 the single byte read is reg 2", w_tx, 8'hA2);
take(1'b0); // NACK: the read ends here
ev_stop;
ck_int("T13 the pointer advanced past it", w_ptr, 3'd3);
// A bare read now, with no pointer write, must therefore serve reg 3.
ev_start; send_addr(ADDR, 1'b1);
ck_int("T13 the next bare read serves reg 3", w_tx, 8'hA3);
take(1'b0); ev_stop;
ck_int("T13 and the pointer advanced again", w_ptr, 3'd4);
if (errors == 0)
$display("=== i2c_register_file: ALL CHECKS PASSED ===");
else
$display("=== i2c_register_file: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule -- ---------------------------------------------------------------------------
-- i2c_register_file.vhd
-- Generic I2C register-file target (UM10204 3.1.10 notes 1, 2 and 4).
-- Behavioural twin of i2c_register_file.sv / .v.
--
-- The specification describes this pattern in one sentence, as an example:
--
-- "Combined formats can be used, for example, to control a serial memory. The
-- internal memory location must be written during the first data byte."
--
-- And then declines to specify anything else about it:
--
-- "All decisions on auto-increment or decrement of previously accessed memory
-- locations, etc., are taken by the designer of the device."
--
-- So every behaviour below except the pointer-in-the-first-byte rule is a DEVICE
-- CONVENTION rather than a protocol requirement, which is why each one is a
-- generic with a stated default.
--
-- The four questions any datasheet must answer:
-- 1. Does the pointer auto-increment? AUTO_INC_ON_READ / AUTO_INC_ON_WRITE
-- 2. What happens at the end of the map? PTR_WRAPS (wrap) or clamp
-- 3. Does the pointer survive a STOP? PTR_PERSISTS
-- 4. What does a write to a read-only
-- location do? RO_WRITE_NACKS (NACK) or discard
--
-- A fifth follows from the fourth and is answered here rather than parameterised:
-- when a write is REFUSED the pointer does not advance. A NACKed data byte ends
-- the write for a conforming master, so there is no next location.
--
-- Note 4, which IS protocol: a device resets its bus logic on ANY START, "even if
-- these START conditions are not positioned according to the proper format".
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_register_file is
generic (
N_REGS : integer := 8;
PTR_W : integer := 3;
MY_ADDR : std_logic_vector(6 downto 0) := "1001000"; -- 0x48
AUTO_INC_ON_READ : std_logic := '1';
AUTO_INC_ON_WRITE : std_logic := '1';
PTR_WRAPS : std_logic := '1';
PTR_PERSISTS : std_logic := '1';
RO_WRITE_NACKS : std_logic := '1';
RO_MASK : std_logic_vector(7 downto 0) := (others => '0');
CNT_W : integer := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
start_seen : in std_logic;
stop_seen : in std_logic;
byte_valid : in std_logic;
byte_in : in std_logic_vector(7 downto 0);
is_addr_byte : in std_logic;
read_byte_done : in std_logic;
master_acked : in std_logic;
ack : out std_logic;
tx_byte : out std_logic_vector(7 downto 0);
tx_valid : out std_logic;
selected : out std_logic;
ptr : out unsigned(PTR_W-1 downto 0);
ptr_wrapped : out std_logic;
ptr_clamped : out std_logic;
ro_write_seen : out std_logic;
state : out unsigned(2 downto 0);
writes_applied : out unsigned(CNT_W-1 downto 0);
reads_served : out unsigned(CNT_W-1 downto 0)
);
end entity i2c_register_file;
architecture rtl of i2c_register_file is
constant ST_IDLE : integer := 0; -- awaiting an address byte
constant ST_PTR : integer := 1; -- addressed for write; next byte is the pointer
constant ST_WRITE : integer := 2; -- pointer set; further bytes are data
constant ST_READ : integer := 3; -- addressed for read; serving bytes
constant PTR_MAX : integer := N_REGS - 1;
type reg_arr is array (0 to N_REGS-1) of std_logic_vector(7 downto 0);
signal regs : reg_arr := (others => (others => '0'));
signal st : integer := ST_IDLE;
signal p : integer := 0;
-- Is the register the pointer currently names read-only? Named rather than
-- spelled inline at the one place it is tested, because question 4 is a
-- datasheet property and a reader looks for it by name.
signal ptr_is_ro : std_logic;
signal n_wr : integer := 0;
signal n_rd : integer := 0;
begin
ptr_is_ro <= RO_MASK(p);
state <= to_unsigned(st, 3);
ptr <= to_unsigned(p, PTR_W);
writes_applied <= to_unsigned(n_wr, CNT_W);
reads_served <= to_unsigned(n_rd, CNT_W);
process (clk, rst_n)
-- Advance the pointer according to the configured convention. Wrapping and
-- clamping differ at exactly ONE address, and they are reported separately
-- so a testbench can tell which convention is in force.
procedure advance_ptr (variable np : inout integer;
variable wr : inout std_logic;
variable cl : inout std_logic) is
begin
if np = PTR_MAX then
if PTR_WRAPS = '1' then
np := 0;
wr := '1';
else
np := PTR_MAX; -- clamp: stay put
cl := '1';
end if;
else
np := np + 1;
end if;
end procedure;
variable np : integer;
variable w_v : std_logic;
variable c_v : std_logic;
variable nxt : integer;
begin
if rst_n = '0' then
st <= ST_IDLE;
ack <= '0';
tx_byte <= (others => '0');
tx_valid <= '0';
selected <= '0';
p <= 0;
ptr_wrapped <= '0';
ptr_clamped <= '0';
ro_write_seen <= '0';
n_wr <= 0;
n_rd <= 0;
regs <= (others => (others => '0'));
elsif rising_edge(clk) then
ack <= '0';
np := p;
w_v := '0';
c_v := '0';
-- Note 4: reset the bus logic on ANY START, wherever it lands. A pointer
-- write in progress is abandoned.
if start_seen = '1' then
st <= ST_IDLE;
selected <= '0';
tx_valid <= '0';
elsif stop_seen = '1' then
st <= ST_IDLE;
selected <= '0';
tx_valid <= '0';
-- Question 3. A pointer that does not persist is reset here, which
-- makes a bare read meaningless.
if PTR_PERSISTS = '0' then
p <= 0;
end if;
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';
selected <= '1';
if byte_in(0) = '1' then
-- READ with no pointer byte: the "current address read".
tx_byte <= regs(p);
tx_valid <= '1';
st <= ST_READ;
else
-- WRITE. Note 1: the next byte is the internal location.
st <= ST_PTR;
end if;
end if;
-- Note 1: "The internal memory location must be written during the
-- first data byte."
when ST_PTR =>
ack <= '1';
p <= to_integer(unsigned(byte_in(PTR_W-1 downto 0)));
st <= ST_WRITE;
-- Data bytes. Question 4 decides what a read-only location does.
when ST_WRITE =>
if ptr_is_ro = '1' then
ro_write_seen <= '1';
if RO_WRITE_NACKS = '1' then
-- Refuse the byte, and do NOT advance: a NACK ends the
-- write for a conforming master, so there is no next
-- location to move to.
ack <= '0';
else
-- Accept and discard.
ack <= '1';
if AUTO_INC_ON_WRITE = '1' then
advance_ptr(np, w_v, c_v);
p <= np;
if w_v = '1' then ptr_wrapped <= '1'; end if;
if c_v = '1' then ptr_clamped <= '1'; end if;
end if;
end if;
else
regs(p) <= byte_in;
n_wr <= n_wr + 1;
ack <= '1';
if AUTO_INC_ON_WRITE = '1' then
advance_ptr(np, w_v, c_v);
p <= np;
if w_v = '1' then ptr_wrapped <= '1'; end if;
if c_v = '1' then ptr_clamped <= '1'; end if;
end if;
end if;
when others =>
null; -- ST_READ is driven below
end case;
-- Serving a read. The master's acknowledge decides whether another byte
-- follows, and question 1 decides where it comes from.
elsif read_byte_done = '1' and st = ST_READ then
n_rd <= n_rd + 1;
if master_acked = '0' then
tx_valid <= '0';
st <= ST_IDLE;
selected <= '0';
if AUTO_INC_ON_READ = '1' then
advance_ptr(np, w_v, c_v);
p <= np;
if w_v = '1' then ptr_wrapped <= '1'; end if;
if c_v = '1' then ptr_clamped <= '1'; end if;
end if;
else
if AUTO_INC_ON_READ = '1' then
advance_ptr(np, w_v, c_v);
p <= np;
if w_v = '1' then ptr_wrapped <= '1'; end if;
if c_v = '1' then ptr_clamped <= '1'; end if;
-- The byte for the NEXT transfer comes from the advanced pointer.
if p = PTR_MAX then
if PTR_WRAPS = '1' then nxt := 0; else nxt := PTR_MAX; end if;
else
nxt := p + 1;
end if;
tx_byte <= regs(nxt);
else
-- No auto-increment: the same location is served again.
tx_byte <= regs(p);
end if;
end if;
end if;
end if;
end process;
end architecture rtl; -- ---------------------------------------------------------------------------
-- i2c_register_file_tb.vhd
-- Independent oracle for i2c_register_file. Behavioural twin of the SystemVerilog
-- and Verilog benches.
--
-- UM10204 note 2 delegates the memory model to the device designer, so the
-- interesting tests are not "does a write work" but "does each CONVENTION behave
-- as configured, and is the difference observable". The bench therefore
-- instantiates the register file TWICE with opposite conventions:
--
-- dut_w : wrapping pointer, persists across STOP, read-only writes NACK
-- dut_c : clamping pointer, resets on STOP, read-only writes discard
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_register_file_tb is
end entity i2c_register_file_tb;
architecture sim of i2c_register_file_tb is
constant TCLK : time := 10 ns;
constant ADDR : std_logic_vector(6 downto 0) := "1001000"; -- 0x48
constant N : integer := 8;
-- Register 5 is read-only in both instances.
constant RO : std_logic_vector(7 downto 0) := "00100000";
constant ST_IDLE : integer := 0;
constant ST_PTR : integer := 1;
constant ST_WRITE : 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 w_ack, w_txv, w_sel, w_wrap, w_clamp, w_ro : std_logic;
signal w_tx : std_logic_vector(7 downto 0);
signal w_ptr : unsigned(2 downto 0);
signal w_st : unsigned(2 downto 0);
signal w_wr, w_rd : unsigned(7 downto 0);
signal c_ack, c_txv, c_sel, c_wrap, c_clamp, c_ro : std_logic;
signal c_tx : std_logic_vector(7 downto 0);
signal c_ptr : unsigned(2 downto 0);
signal c_st : unsigned(2 downto 0);
signal c_wr, c_rd : unsigned(7 downto 0);
signal halt : boolean := false;
begin
dut_w : entity work.i2c_register_file
generic map (N_REGS => N, PTR_W => 3, MY_ADDR => ADDR,
AUTO_INC_ON_READ => '1', AUTO_INC_ON_WRITE => '1',
PTR_WRAPS => '1', PTR_PERSISTS => '1', RO_WRITE_NACKS => '1',
RO_MASK => RO, CNT_W => 8)
port map (clk => clk, rst_n => rst_n, start_seen => start_seen,
stop_seen => stop_seen, byte_valid => byte_valid, byte_in => byte_in,
is_addr_byte => is_addr_byte, read_byte_done => read_byte_done,
master_acked => master_acked,
ack => w_ack, tx_byte => w_tx, tx_valid => w_txv, selected => w_sel,
ptr => w_ptr, ptr_wrapped => w_wrap, ptr_clamped => w_clamp,
ro_write_seen => w_ro, state => w_st,
writes_applied => w_wr, reads_served => w_rd);
dut_c : entity work.i2c_register_file
generic map (N_REGS => N, PTR_W => 3, MY_ADDR => ADDR,
AUTO_INC_ON_READ => '1', AUTO_INC_ON_WRITE => '1',
PTR_WRAPS => '0', PTR_PERSISTS => '0', RO_WRITE_NACKS => '0',
RO_MASK => RO, CNT_W => 8)
port map (clk => clk, rst_n => rst_n, start_seen => start_seen,
stop_seen => stop_seen, byte_valid => byte_valid, byte_in => byte_in,
is_addr_byte => is_addr_byte, read_byte_done => read_byte_done,
master_acked => master_acked,
ack => c_ack, tx_byte => c_tx, tx_valid => c_txv, selected => c_sel,
ptr => c_ptr, ptr_wrapped => c_wrap, ptr_clamped => c_clamp,
ro_write_seen => c_ro, state => c_st,
writes_applied => c_wr, reads_served => c_rd);
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';
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 (a : std_logic_vector(6 downto 0); rw : std_logic) is
begin
wait until falling_edge(clk);
byte_in <= a & 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 write_regs (pp : integer; count : integer; first : integer) is
begin
ev_start;
send_addr(ADDR, '0');
send_data(std_logic_vector(to_unsigned(pp, 8)));
for k in 0 to count-1 loop
send_data(std_logic_vector(to_unsigned(first + k, 8)));
end loop;
ev_stop;
end procedure;
begin
report "=== i2c_register_file: the pattern the spec names and then declines to define ==="
severity note;
-- T1. Note 1: address, pointer, data.
do_reset;
ev_start;
send_addr(ADDR, '0');
ck_bit("T1 addressed and acknowledged", w_ack, '1');
ck_int("T1 awaiting the pointer byte", to_integer(w_st), ST_PTR);
send_data(x"02");
ck_int("T1 pointer took the first data byte", to_integer(w_ptr), 2);
ck_int("T1 now expecting data", to_integer(w_st), ST_WRITE);
send_data(x"AA");
report "T1 address, pointer, data -- the register-map model" severity note;
ck_int("T1 one write applied", to_integer(w_wr), 1);
ev_stop;
-- T2. Auto-increment on write.
do_reset;
write_regs(0, 4, 16#10#);
report "T2 auto-increment on write fills consecutive registers" severity note;
ck_int("T2 four writes applied", to_integer(w_wr), 4);
ev_start; send_addr(ADDR, '0'); send_data(x"00");
ev_start; send_addr(ADDR, '1');
ck_int("T2 reg 0", to_integer(unsigned(w_tx)), 16#10#); take('1');
ck_int("T2 reg 1", to_integer(unsigned(w_tx)), 16#11#); take('1');
ck_int("T2 reg 2", to_integer(unsigned(w_tx)), 16#12#); take('1');
ck_int("T2 reg 3", to_integer(unsigned(w_tx)), 16#13#); take('0');
ev_stop;
-- T3. The combined format.
do_reset;
write_regs(6, 1, 16#5A#);
ev_start; send_addr(ADDR, '0'); send_data(x"06");
ev_start; send_addr(ADDR, '1');
report "T3 combined format: pointer write, repeated START, read" severity note;
ck_bit("T3 selected for read", w_sel, '1');
ck_bit("T3 has a byte to send", w_txv, '1');
ck_int("T3 serving register 6", to_integer(unsigned(w_tx)), 16#5A#);
ck_int("T3 in the read state", to_integer(w_st), ST_READ);
take('0'); ev_stop;
-- T4. A wrong address is ignored entirely.
do_reset;
ev_start;
send_addr("1010000", '0');
report "T4 another device's address is ignored" severity note;
ck_bit("T4 did not acknowledge", w_ack, '0');
ck_bit("T4 not selected", w_sel, '0');
ck_int("T4 stayed idle", to_integer(w_st), ST_IDLE);
send_data(x"03");
ck_bit("T4 still silent", w_ack, '0');
ck_int("T4 pointer untouched", to_integer(w_ptr), 0);
-- T5. Note 4: a START mid-transaction resets the bus logic.
do_reset;
ev_start;
send_addr(ADDR, '0');
ck_int("T5 awaiting the pointer", to_integer(w_st), ST_PTR);
ev_start;
report "T5 a START anywhere resets the bus logic (note 4)" severity note;
ck_int("T5 back to expecting an address", to_integer(w_st), ST_IDLE);
ck_bit("T5 no longer selected", w_sel, '0');
send_addr(ADDR, '0');
ck_int("T5 that byte was read as an address", to_integer(w_st), ST_PTR);
-- T6. QUESTION 2: wrap versus clamp. Register 5 is read-only and a refused
-- write does not advance the pointer, so fill around it.
do_reset;
write_regs(0, 5, 16#20#); -- 0..4 = 0x20..0x24
write_regs(6, 2, 16#26#); -- 6..7 = 0x26..0x27
ev_start; send_addr(ADDR, '0'); send_data(x"07");
ev_start; send_addr(ADDR, '1');
report "T6 question 2: the end of the map -- wrap or clamp" severity note;
ck_int("T6 both serve register 7", to_integer(unsigned(w_tx)), 16#27#);
ck_int("T6 clamping instance agrees so far", to_integer(unsigned(c_tx)), 16#27#);
take('1');
ck_int("T6 wrapping instance now serves register 0",
to_integer(unsigned(w_tx)), 16#20#);
ck_int("T6 clamping instance serves register 7 again",
to_integer(unsigned(c_tx)), 16#27#);
ck_bit("T6 wrap reported", w_wrap, '1');
ck_bit("T6 clamp reported", c_clamp, '1');
ck_bit("T6 wrapping instance did not clamp", w_clamp, '0');
ck_bit("T6 clamping instance did not wrap", c_wrap, '0');
take('0'); ev_stop;
-- T7. QUESTION 3: does the pointer survive a STOP?
do_reset;
write_regs(0, 5, 16#30#);
ev_start; send_addr(ADDR, '0'); send_data(x"03");
ev_stop;
report "T7 question 3: does the pointer survive a STOP" severity note;
ck_int("T7 persisting instance kept pointer 3", to_integer(w_ptr), 3);
ck_int("T7 non-persisting instance reset to 0", to_integer(c_ptr), 0);
ev_start; send_addr(ADDR, '1');
ck_int("T7 persisting reads register 3", to_integer(unsigned(w_tx)), 16#33#);
ck_int("T7 non-persisting reads register 0", to_integer(unsigned(c_tx)), 16#30#);
take('0'); ev_stop;
-- T8. QUESTION 4: writing a read-only register.
do_reset;
ev_start; send_addr(ADDR, '0'); send_data(x"05"); send_data(x"FF");
report "T8 question 4: writing a read-only register" severity note;
ck_bit("T8 NACKing instance refused the byte", w_ack, '0');
ck_bit("T8 discarding instance accepted it", c_ack, '1');
ck_bit("T8 both noticed the attempt (w)", w_ro, '1');
ck_bit("T8 both noticed the attempt (c)", c_ro, '1');
ck_int("T8 neither applied a write", to_integer(w_wr), 0);
ck_int("T8 neither applied a write (c)", to_integer(c_wr), 0);
ev_stop;
ev_start; send_addr(ADDR, '0'); send_data(x"05");
ev_start; send_addr(ADDR, '1');
ck_int("T8 register 5 still zero", to_integer(unsigned(w_tx)), 0);
take('0'); ev_stop;
-- T9. A burst write that walks into the read-only register.
do_reset;
ev_start; send_addr(ADDR, '0'); send_data(x"04");
send_data(x"41");
ck_bit("T9 register 4 accepted", w_ack, '1');
send_data(x"42");
report "T9 a burst write that walks into a read-only register" severity note;
ck_bit("T9 register 5 refused", w_ack, '0');
ev_stop;
ck_int("T9 only one write applied", to_integer(w_wr), 1);
-- T10. A REFUSED write does not advance the pointer.
do_reset;
ev_start; send_addr(ADDR, '0'); send_data(x"05");
for k in 0 to 2 loop
send_data(std_logic_vector(to_unsigned(16#E0# + k, 8)));
ck_bit("T10 every byte refused", w_ack, '0');
ck_int("T10 pointer never advanced", to_integer(w_ptr), 5);
end loop;
ev_stop;
report "T10 a refused write stalls the pointer instead of walking on"
severity note;
ck_int("T10 nothing was applied", to_integer(w_wr), 0);
ev_start; send_addr(ADDR, '0'); send_data(x"06");
ev_start; send_addr(ADDR, '1');
ck_int("T10 register 6 untouched", to_integer(unsigned(w_tx)), 0); take('1');
ck_int("T10 register 7 untouched", to_integer(unsigned(w_tx)), 0); take('0');
ev_stop;
-- T11. Read counters.
do_reset;
write_regs(0, 5, 16#50#);
ev_start; send_addr(ADDR, '0'); send_data(x"00");
ev_start; send_addr(ADDR, '1');
for k in 0 to 3 loop take('1'); end loop;
take('0');
report "T11 the read counter tracks bytes served" severity note;
ck_int("T11 five reads served", to_integer(w_rd), 5);
ev_stop;
-- T12. Two transactions back to back.
do_reset;
write_regs(0, 3, 16#60#);
write_regs(6, 2, 16#70#);
report "T12 two transactions, each honouring its own pointer" severity note;
ck_int("T12 five writes in total", to_integer(w_wr), 5);
ev_start; send_addr(ADDR, '0'); send_data(x"00");
ev_start; send_addr(ADDR, '1');
ck_int("T12 reg 0", to_integer(unsigned(w_tx)), 16#60#); take('1');
ck_int("T12 reg 1", to_integer(unsigned(w_tx)), 16#61#); take('1');
ck_int("T12 reg 2", to_integer(unsigned(w_tx)), 16#62#); take('1');
ck_int("T12 reg 3 untouched", to_integer(unsigned(w_tx)), 0); take('1');
ck_int("T12 reg 4 untouched", to_integer(unsigned(w_tx)), 0); take('1');
ck_int("T12 reg 5 untouched", to_integer(unsigned(w_tx)), 0); take('1');
ck_int("T12 reg 6", to_integer(unsigned(w_tx)), 16#70#); take('1');
ck_int("T12 reg 7", to_integer(unsigned(w_tx)), 16#71#); take('0');
ev_stop;
-- T13. WHERE DOES THE POINTER SIT WHEN A READ ENDS? The master's NACK ends the
-- read, and the datasheet still has to say whether the counter moved past
-- the byte it just served. It matters because the next bare read starts
-- from wherever this one left it: if the counter advances, two
-- back-to-back single-byte reads return DIFFERENT registers, and if it
-- does not, they return the same one twice.
--
-- This device advances. Nothing in UM10204 requires either answer.
do_reset;
write_regs(0, 8, 16#A0#); -- 0..7 = 0xA0..0xA7
ev_start; send_addr(ADDR, '0'); send_data(x"02"); ev_stop;
ev_start; send_addr(ADDR, '1');
report "T13 the pointer's position after a read that ended in a NACK"
severity note;
ck_int("T13 the single byte read is reg 2", to_integer(unsigned(w_tx)), 16#A2#);
take('0'); -- NACK: the read ends here
ev_stop;
ck_int("T13 the pointer advanced past it", to_integer(w_ptr), 3);
-- A bare read now, with no pointer write, must therefore serve reg 3.
ev_start; send_addr(ADDR, '1');
ck_int("T13 the next bare read serves reg 3", to_integer(unsigned(w_tx)), 16#A3#);
take('0'); ev_stop;
ck_int("T13 and the pointer advanced again", to_integer(w_ptr), 4);
if err = 0 then
report "=== i2c_register_file: ALL CHECKS PASSED ===" severity note;
else
report "=== i2c_register_file: " & integer'image(err)
& " CHECK(S) FAILED ===" severity note;
end if;
halt <= true;
wait;
end process;
end architecture sim;6a. Decisions Worth Defending
Every convention is a parameter, and each has a stated default. AUTO_INC_ON_READ, AUTO_INC_ON_WRITE, PTR_WRAPS, PTR_PERSISTS, RO_WRITE_NACKS and RO_MASK. Hard-wiring any of them would make the block a model of one part rather than of the pattern, and the testbench's two instances differ only in those parameters.
Wrapping and clamping are reported separately, not inferred. ptr_wrapped and ptr_clamped are distinct outputs, so a bench can tell which convention is in force rather than deducing it from a pointer value. Mutation A2-12 reports a clamp as a wrap and is killed by two checks — a design that conflated them would pass a suite that only looked at the pointer.
A refused write does not advance the pointer, and that is question 6 answered. This is the decision the design makes rather than parameterises, and the argument is worth reading in full because it is not obvious. A NACK on a data byte ends the write for a conforming master, so there is no "next location" to move to. A master that ignores the NACK and keeps sending will then pile every remaining byte onto the same read-only register and be refused each time — which is the correct outcome, because the alternative silently writes the bytes it intended for later locations somewhere else. Mutation A2-4 advances the pointer there; eight checks fail.
The read-only test is named rather than spelled inline. ptr_is_ro exists because question 4 is a datasheet property and a reader looks for it by name. It is the only place RO_MASK is consulted in the transfer path.
The next read byte is computed at the moment the pointer advances, not a cycle later. A byte-serving target has to have the next byte ready before the master starts clocking it out, so the advance and the prefetch happen together — and the prefetch has to honour question 2, which is why it reads regs[0] or regs[PTR_MAX] depending on PTR_WRAPS rather than simply indexing ptr + 1. Mutation A2-9 prefetches from the un-advanced pointer and nine checks fail.
Note 4 is implemented as the first branch in the clocked block. A START — anywhere, including mid-pointer-write — returns the block to expecting an address. The pointer is deliberately not cleared there, because that is device state rather than bus logic and the combined format depends on it surviving. Mutation A2-2 ignores a misplaced START and twenty checks fail.
PTR_MAX is a sized localparam rather than a part-select of a parameter. A part-select of a parameter evaluates to zero on some tools, which turns every end-of-map test into a test of address zero and hides both question 2 answers at once.
6b. Verified Execution
$ iverilog -g2012 -o d i2c_register_file.sv i2c_register_file_tb.sv && ./d
=== i2c_register_file: the pattern the spec names and then declines to define ===
T1 address, pointer, data -- the register-map model
T2 auto-increment on write fills consecutive registers
T3 combined format: pointer write, repeated START, read
T4 another device's address is ignored
T5 a START anywhere resets the bus logic (note 4)
T6 question 2: the end of the map -- wrap or clamp
T7 question 3: does the pointer survive a STOP
T8 question 4: writing a read-only register
T9 a burst write that walks into a read-only register
T10 a refused write stalls the pointer instead of walking on
T11 the read counter tracks bytes served
T12 two transactions, each honouring its own pointer
T13 the pointer's position after a read that ended in a NACK
=== i2c_register_file: ALL CHECKS PASSED ===
$ iverilog -g2005 -o v i2c_register_file.v i2c_register_file_tb.v && ./v
=== i2c_register_file: the pattern the spec names and then declines to define ===
T1 address, pointer, data -- the register-map model
T2 auto-increment on write fills consecutive registers
T3 combined format: pointer write, repeated START, read
T4 another device's address is ignored
T5 a START anywhere resets the bus logic (note 4)
T6 question 2: the end of the map -- wrap or clamp
T7 question 3: does the pointer survive a STOP
T8 question 4: writing a read-only register
T9 a burst write that walks into a read-only register
T10 a refused write stalls the pointer instead of walking on
T11 the read counter tracks bytes served
T12 two transactions, each honouring its own pointer
T13 the pointer's position after a read that ended in a NACK
=== i2c_register_file: ALL CHECKS PASSED ===
$ nvc --std=2008 -a i2c_register_file.vhd i2c_register_file_tb.vhd
$ nvc --std=2008 -e i2c_register_file_tb && nvc --std=2008 -r i2c_register_file_tb --stop-time=300us
=== i2c_register_file: the pattern the spec names and then declines to define ===
T1 address, pointer, data -- the register-map model
T2 auto-increment on write fills consecutive registers
T3 combined format: pointer write, repeated START, read
T4 another device's address is ignored
T5 a START anywhere resets the bus logic (note 4)
T6 question 2: the end of the map -- wrap or clamp
T7 question 3: does the pointer survive a STOP
T8 question 4: writing a read-only register
T9 a burst write that walks into a read-only register
T10 a refused write stalls the pointer instead of walking on
T11 the read counter tracks bytes served
T12 two transactions, each honouring its own pointer
T13 the pointer's position after a read that ended in a NACK
=== i2c_register_file: ALL CHECKS PASSED ===6c. What The Testbench Proves
| # | scenario | what it establishes |
|---|---|---|
| 1 | address, pointer, data in one transaction | note 1: the first data byte is the location |
| 2 | four bytes written from pointer 0 | auto-increment on writes; they land in registers 0–3 |
| 3 | the combined format of §3.1.10 | pointer write, repeated START, read; address repeated with R/W reversed |
| 4 | a wrong address | ignored entirely — no acknowledge, no pointer state |
| 5 | a START in the middle of a pointer write | note 4: the pointer write in progress is abandoned |
| 6 | both instances read past the end of the map | wrap on one, clamp on the other — differing at one address |
| 7 | a bare read with no pointer byte, on both instances | question 3: persistent serves from history, non-persistent serves zero |
| 8 | a write to read-only register 5, on both instances | question 4: one NACKs, one accepts and discards |
| 9 | a three-byte burst from pointer 4 through register 5 | registers 4 and 6 written, 5 refused |
| 10 | three bytes all aimed at read-only register 5 | question 6: all refused, all on 5 — the pointer did not advance |
| 11 | five bytes taken from a read | five reads served, whatever the pointer did |
| 12 | two transactions back to back | each honours its own pointer |
| 13 | the pointer's position after a read ended by a NACK | question 5: it advanced past the last byte |
Test 7 is the same bytes on the wire with two different meanings. A bare read, driven into both instances at once: the persistent-pointer device serves from wherever the previous transaction left it, and the non-persistent one serves location zero. That is the clearest demonstration in the module of what note 2 costs — identical stimulus, two conforming devices, two different answers, and nothing on the bus to tell them apart.
Tests 6 and 8 work the same way, on questions 2 and 4. Driving both instances from one byte stream and asserting the difference is what makes a device convention concrete rather than a paragraph.
Test 10 exists because a mutation found the question. The original suite filled eight registers starting at zero, walked into the read-only register at index 5, and stalled — which looked like a bench bug and was actually the design asking to have question 6 answered. The fill was shortened, test 9 was written for the walk-through case, and test 10 for the refusal itself.
Test 13 exists for the same reason and asserts the pointer twice: once directly after the NACK, and once by showing the next bare read returns the following register. Asserting the pointer output alone would be weaker, because a device could report a pointer it does not then use.
7. Mutation Testing
Twelve defects injected into the SystemVerilog register file. Each one turns a conforming device into a different conforming device, which is the whole difficulty of this chapter: none of them is a protocol violation.
| # | injected defect | outcome |
|---|---|---|
| A2-1 | the first data byte treated as data, not as the location | killed — 37 checks |
| A2-2 | a misplaced START ignored, breaking note 4 | killed — 20 checks |
| A2-3 | clamp at the top of the map on a part specified to wrap | killed — 2 checks |
| A2-4 | the pointer advanced on a refused write | killed — 8 checks |
| A2-5 | the read-only mask ignored; every write applied | killed — 17 checks |
| A2-6 | the write refused but ro_write_seen never raised | killed — 2 checks |
| A2-7 | the pointer reset at every STOP whatever the convention | killed — 6 checks |
| A2-8 | no advance after the last byte of a read | killed — 3 checks |
| A2-9 | the next read byte prefetched from the un-advanced pointer | killed — 9 checks |
| A2-10 | the direction bit ignored; every addressing treated as a write | killed — 23 checks |
| A2-11 | all eight address bits compared, so a read never matches | killed — 24 checks |
| A2-12 | a clamp reported as a wrap | killed — 2 checks |
Twelve of twelve.
A2-8 was the chapter's most useful survivor. It skips the pointer advance after the final byte of a read — the byte the master NACKed — and the original twelve-test suite passed it. Nothing looked at where the pointer sat once a read had ended, because the read had ended. That is question 5, and it went into §2 as a question a datasheet must answer only because a mutation asked it. Test 13 was written to kill it and does so with three checks.
A2-4 was the second. It advances the pointer on a refused write, and the original suite passed that too. Resolving it required deciding what a refused write means — see §6a — and the answer became question 6.
Both survivors were device conventions the design had implemented without stating. That is the specific failure mode note 2 creates: a designer makes a choice, does not notice it was a choice, and ships a datasheet that omits it.
A2-3 and A2-12 kill with two checks each, and that is the honest measure of question 2. Wrap and clamp differ at one address out of eight, so the evidence for either convention is thin by construction. A suite that never drove the pointer to the top of the map would score zero on both.
A2-1's thirty-seven checks are the signature of breaking note 1. If the first data byte is not the location, every subsequent test's addressing is wrong and almost everything fails. Compare A2-3's two: breadth of failure tells you how deep in the model the defect sits, not how serious it is.
8. Verification Connection — Register Models and What They Cannot Express
A register map is the one place in this curriculum where UVM has a purpose-built facility: uvm_reg. It is worth using, and it is worth knowing precisely where it stops.
// A UVM register model for the device above. The parts uvm_reg handles well are
// the map's STRUCTURE -- names, offsets, field widths, access policies -- and it
// handles them far better than hand-written constants: a mirrored model gives you
// predicted values, automatic scoreboarding of every access, and the built-in
// sequences (uvm_reg_hw_reset_seq, uvm_reg_bit_bash_seq) for free.
//
// What it does NOT hold is the four conventions of section 2 that concern the
// POINTER rather than the registers. uvm_reg's model of a bus is "an access goes
// to an address"; it has no notion of a device-side pointer that persists between
// accesses, auto-increments, wraps or clamps. Those live in the ADAPTER and in the
// sequences, and if you let them live implicitly they become the bugs this chapter
// is about.
class i2c_reg_ctrl extends uvm_reg;
`uvm_object_utils(i2c_reg_ctrl)
rand uvm_reg_field mode; // RW
rand uvm_reg_field enable; // RW
uvm_reg_field status; // RO -- question 4 lives HERE, as a policy
function new(string name = "i2c_reg_ctrl");
super.new(name, 8, UVM_NO_COVERAGE);
endfunction
virtual function void build();
mode = uvm_reg_field::type_id::create("mode");
enable = uvm_reg_field::type_id::create("enable");
status = uvm_reg_field::type_id::create("status");
// configure(parent, size, lsb_pos, access, volatile, reset, has_reset,
// is_rand, individually_accessible)
mode .configure(this, 3, 0, "RW", 0, 3'b000, 1, 1, 0);
enable.configure(this, 1, 3, "RW", 0, 1'b0, 1, 1, 0);
// "RO" is how uvm_reg expresses question 4 -- but note what it expresses:
// that the MODEL will not predict a change. It says nothing about whether
// the DEVICE nacks the write or accepts and discards it, which is the part
// the datasheet has to answer and the part a driver has to handle.
status.configure(this, 4, 4, "RO", 1, 4'h0, 1, 0, 0);
endfunction
endclass
// The adapter turns a uvm_reg access into I2C bus items. This is where note 1
// lives: a register write becomes a two-byte payload, pointer first.
class i2c_reg_adapter extends uvm_reg_adapter;
`uvm_object_utils(i2c_reg_adapter)
function new(string name = "i2c_reg_adapter");
super.new(name);
supports_byte_enable = 0;
provides_responses = 1;
endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
i2c_seq_item it = i2c_seq_item::type_id::create("it");
it.dev_addr = 7'h48;
it.is_read = (rw.kind == UVM_READ);
// Note 1, as code: the internal location is the first data byte of the
// write phase. For a read this is the write half of the combined format,
// and the repeated START that follows is NOT optional -- a STOP there would
// release the bus and, on a device whose pointer does not persist, discard
// the location that was just set.
it.ptr = rw.addr[7:0];
it.use_repeated_start = it.is_read;
if (!it.is_read) it.wdata = rw.data[7:0];
return it;
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item,
ref uvm_reg_bus_op rw);
i2c_seq_item it;
if (!$cast(it, bus_item)) begin
`uvm_fatal("REGADPT", "bus2reg received a non-I2C item")
return;
end
rw.kind = it.is_read ? UVM_READ : UVM_WRITE;
rw.addr = it.ptr;
rw.data = it.rdata;
// A refused write is a bus-level NACK, and it must be reported as a failed
// access rather than silently dropped. A model that maps every transfer to
// UVM_IS_OK cannot distinguish question 4's two answers at all: an
// accept-and-discard device and a nacking device look identical to it.
rw.status = it.nacked ? UVM_NOT_OK : UVM_IS_OK;
endfunction
endclassThree points generalise beyond this block.
uvm_reg models the map, and the pointer is not part of the map. Access policies (RW, RO, WO, W1C) describe registers. Questions 1, 2, 3 and 5 all describe the pointer, and there is no field in a register model that can hold "this device's location counter wraps at the top". If the sequences assume auto-increment and the device clamps, the model's predicted values will be wrong and the mismatch will be reported against the register rather than against the assumption.
An adapter that maps every transfer to UVM_IS_OK erases question 4. A NACKed write and an accepted-and-discarded write produce the same model state — the mirrored value does not change either way, because the field is RO. The difference is entirely in the bus status, so if bus2reg does not propagate the NACK, the two conventions are indistinguishable inside the testbench even though they are distinguishable on the wire.
Coverage has to be written for the conventions, because no automatic model generates it. uvm_reg_hw_reset_seq will read every register and check its reset value; nothing in the built-in library will drive the pointer to the top of the map and check whether it wrapped. That test — §6c's test 6 — has to be written deliberately, and it is the one that distinguishes the two devices in this chapter's bench.
9. FPGA and ASIC Implications
The pointer is a register, and it is the one piece of state that must survive a repeated START. That splits reset into two kinds: bus logic, which note 4 requires be reset on any START, and device state, which must not be. A single synchronous clear driven from START detection will break every combined format on the device, and it will break them in a way that looks like a protocol problem.
Make the map's size a parameter and derive the pointer width from it. A pointer wider than the map indexes locations that do not exist, and what happens then is a synthesis-dependent read of an out-of-range array — X in simulation, whatever the memory compiler chose in silicon. Chapter 16.2 makes this concrete: it is the same bug at a larger scale, and VHDL's bounds check caught it where Verilog silently returned X.
A read-only mask costs one bit per register and buys a real property. Without it, read-only behaviour is scattered through the write path as special cases; with it, question 4 is one parameter and one named signal, and a change of convention is a change of one line.
The prefetch is a real timing constraint, not a modelling convenience. The next byte has to be at the output before the master's first clock edge of the next transfer, which on a 400 kHz bus is roughly 2.5 µs after the acknowledge — ample for any sensible clock, but it does mean the advance and the memory read must both complete in that window. On a device whose registers live in a synchronous RAM rather than flops, that is one cycle of RAM latency to account for, and it is the reason a design serves from a registered copy rather than from the array output directly.
Auto-increment interacts with clock stretching. If the register read needs a slow internal access, the device stretches (Chapter 12.1), and a burst read across a slow region stretches on every byte. That is legal and it changes the transaction's duration by a large factor, which matters if the master has a timeout.
Document the six answers in the register map itself. The most useful line in an I²C device's datasheet is often a single sentence saying "the address pointer increments after every read or write and wraps at the end of the map, and is retained across STOP conditions". It costs one sentence and removes four of the six questions.
10. Debugging — The Driver That Worked Until the Map Grew
A driver reads a block of configuration registers from a sensor in one burst and has worked in production for two years. A hardware revision moves to a pin-compatible part from a second source with the same register map and the same addresses. The driver now returns correct values for the first six registers of the block and implausible values for the last two, which it had always ignored. Reading each of the six registers individually returns correct values on both parts.
Question 2, answered differently by two conforming parts. The driver read eight bytes from a six-register block -- a two-byte overrun that had been present and harmless for two years because the original part clamped and the driver discarded the surplus. The second-source part wraps, so the same overrun now returns registers 0x00 and 0x01. Nothing about either part violates the specification: note 2 delegates this decision to the device designer and the two designers chose differently. The bug was in the driver, and it was two years old.
Read six bytes, not eight -- the overrun was always a bug and the clamp merely hid it. Then make the map's extent explicit in the driver so a burst cannot be requested past it, and record the wrap-or-clamp answer for every part in the same table as its address. For the regression: a test that reads to the last register of the map and one byte past it, asserting the specific behaviour that part's datasheet promises. That test fails on the original part too, which is the point.Three things generalise.
The driver was wrong for two years and passed. The overrun was real from the first release; the original part's clamp turned it into two duplicate bytes that were discarded. A convention the specification declines to fix was silently compensating for a driver bug, which is the most common shape of this class of failure.
"Pin-compatible, same register map" does not mean interchangeable. The two parts agree on every register's address, width and meaning. They differ on one convention that no register-level comparison would surface, and the difference is invisible until a transaction reaches the end of the map.
The fix that matters is the test, not the byte count. Changing eight to six repairs this system. Adding a test that deliberately reads past the last register, and asserting the documented behaviour, is what stops the next part substitution from finding the next convention the hard way.
11. Common Misconceptions
"The I²C specification defines register maps." It names the pattern in one sentence as an example, and note 2 explicitly hands every detail to the device designer. §1.
"Auto-increment is standard behaviour." It is a device convention. A status register with a FIFO behind it deliberately does not increment, and that is correct for its purpose. §2, question 1.
"Reading past the end of a map is an error the device will report." It is not an error. The device wraps or clamps, silently, and which one it does is not specified. §5 and §10.
"A bare read with no pointer write returns register zero." On a device whose pointer does not persist, yes. On one whose pointer does persist, it returns whatever the previous transaction left — and both are conforming. §2, question 3.
"A write to a read-only register will be NACKed." Sometimes. Accept-and-discard is equally common and tells the master nothing. §2, question 4.
"A repeated START resets the device, so the pointer is lost." Note 4 requires the bus logic to reset. The pointer is device state, and the combined format depends on it surviving. §4.
"Once a read ends, the pointer's position does not matter." It decides what the next bare read returns. Two back-to-back single-byte reads return different registers or the same one twice, depending on the answer. §2, question 5.
"A NACKed write has no side effects." Whether it advanced the pointer is a separate question, and a device that advances it will scatter a master's remaining bytes across the map. §2, question 6.
"Both devices conform, so either will work." Both conform. Only one matches your driver. §1.
"A UVM register model captures the device's behaviour." It captures the map's structure. Four of the six questions concern the pointer, which a register model has no field for. §8.
"If the register model's predictions match, the conventions are right." The predictions match only for the accesses the sequences make. Nothing in the built-in library drives the pointer to the end of the map. §8.
12. Reason It Through
A datasheet lists every register's address, width, reset value and meaning, and says nothing else. What can you not yet write a driver for?
Any transaction longer than one byte, and any transaction that does not set the pointer first. You do not know whether the pointer increments, what it does at the top of the map, whether it survives a STOP, or what a write to a read-only location does — so a burst read, a bare read and a write to a status register are all undefined from the driver's side. §1 and §2.
Two parts have identical register maps. One wraps at the end, one clamps. Which transactions distinguish them?
Exactly those that reach the last register and continue. A burst that ends at or before the top behaves identically on both. That is why the difference can survive years of production: it is one address out of the whole map. §5.
Why must the pointer survive a repeated START when note 4 requires a device to reset its bus logic on any START?
Because the pointer is not bus logic. Note 4's requirement is about the frame-level state machine — bit counters, byte boundaries, acknowledge phase — and the combined format of format 3 depends on the location set in the write phase still being there after the turnaround. A device that cleared the pointer there could not implement the format the specification itself describes. §4.
A master writes four bytes starting at a location where the third is read-only, and the device NACKs data byte three. Where should bytes three and four end up?
Nowhere. The NACK ends the write for a conforming master, so there is no fourth byte. If the master ignores the NACK and sends anyway, the correct behaviour is to refuse that byte at the same location — because advancing the pointer would write the byte intended for location N+3 into location N+3 while location N+2 was never written, silently shifting the master's data by one. §2, question 6, and §6a.
Why do two mutations that change a device's behaviour pass a twelve-test suite?
Because both concern what happens after the interesting part of a transaction: where the pointer sits once a read has ended, and whether a refused write moved it. A suite built around "did the transfer produce the right bytes" does not look at either, since neither changes any byte in the transfer that exposed it. §7.
Your UVM register model reports three consecutive register mismatches on a burst read. What is the most likely cause, and why is the report misleading?
The device does not auto-increment and the sequence assumed it does. The model predicted registers N, N+1, N+2 and the device served N three times, so the failure surfaces as three register-level mismatches when the actual defect is a single wrong assumption about the pointer. §8.
A device's datasheet says the pointer "is retained". Is question 3 answered?
Partly. "Retained" across what — a STOP, a repeated START, both? Note 4 already requires it to survive a repeated START, so the sentence is only informative if it means across a STOP. This is the kind of ambiguity worth resolving with a two-transaction experiment rather than a reading. §2 and §6c test 4.
13. Understanding Check
14. Summary
UM10204 describes the register-map pattern in one sentence and delegates everything else. Note 1 gives the rule that the internal location is the first data byte. Note 2 hands "all decisions on auto-increment or decrement of previously accessed memory locations, etc." to the device designer.
So the protocol guarantees you can address a location and nothing about what the device does next. Two parts can be byte-for-byte identical on the wire, behave differently, and both conform.
Six questions have to be answered before a driver can be written: does the pointer increment; what happens at the end of the map; does it survive a STOP; what does a write to a read-only location do; where does it sit when a read ends; and does a refused write advance it.
Each has at least two answers that exist in shipping parts, and choosing wrong produces failures that look like bus problems while the bus is working perfectly.
Wrap and clamp differ at exactly one address, which is why a driver written against the wrong one can pass for years.
Persistence decides whether a bare read means anything at all — the same four bytes on the wire either serve from the last transaction's position or from location zero.
A refused write must not advance the pointer, because a NACK ends the write and advancing would silently shift a stubborn master's remaining data.
Note 4's reset requirement is about bus logic, not device state. The pointer must survive a repeated START, or the combined format the specification itself describes is impossible.
Two of these six questions were found by mutation testing, not by design. Both survived a twelve-test suite because both concern state after the transaction that would expose them — which is exactly how a device convention gets shipped undocumented.
And uvm_reg models the map, not the pointer. Four of the six questions have no field in a register model, so they live in the adapter and the sequences, and an adapter that swallows a NACK erases question 4 entirely.
15. What Comes Next
The generic register map is the pattern. Chapter 16.2 takes the device the specification's own note names — a serial memory — and finds that the pointer has a width, that the width is not always eight bits, and that a master and a device can disagree about it silently.
That disagreement has a specific and nasty shape: a master that sends one address byte to a device expecting two has its first data byte consumed as the low half of the pointer. Every byte after that is off by one, every acknowledge is correct, and the write lands somewhere the master never named.
It also introduces the two reads that are identical on the wire and mean different things — the current-address read and the random read — and the rollover behaviour that turns a read past the end of an array into a read from the beginning of it.
Continue learning
Related tutorials
- Related topic
The Master Command Interface — Register Model and On-Chip Bus
The one block in an I²C master that UM10204 says nothing about, which makes it harder rather than easier. Derives what software must be able to express and what the master must report back, why done and ok are two bits rather than one, why only the command register may start a transfer, and why an on-chip register bus forces a post-then-poll handshake.
- Related topic
The Register Interface Behind the Slave
Where Module 16's design decisions stop being a specification and become flops. Builds the register file, the pointer and the auto-increment, and shows why the pointer surviving a repeated START is a wiring decision rather than a promise.
- Related topic
The START Condition
START is SDA falling while SCL is high, it is generated only by the controller, and it makes the bus busy. Derive what every device must do in response, then build a detector in three languages and find out why its two guard terms and its reset value are all load-bearing.
- Related topic
f(SCL), tLOW and tHIGH — The I²C Clock Envelope
A legal I²C clock is three constraints, not one frequency — and a perfectly compliant 400 kHz clock with a symmetric duty cycle is illegal in Fast-mode. Derives the envelope and an exact identity hidden in Table 10.
