I²C · Module 8
The I²C Write Transaction End to End
Seven modules built the pieces. This one runs a write from START to STOP as a single continuous story, counts every bit on the wire, and builds the transaction-level sequencer that issues it — in SystemVerilog, Verilog and VHDL.
Every mechanism this course has built so far exists to serve two transactions. This chapter is the first of them.
A write is the simpler of the two, and it is simpler for one specific reason: the direction never changes. The master holds the transmitter role from the first bit of the address to the last bit of the payload, and the addressed slave holds the receiver role for the whole transfer. Nothing hands over. That single property is why a write is the right place to assemble the whole stack, and why Module 9's read — which does hand over, mid-frame, on a bus where both ends can pull low — is a harder chapter.
What is new here is not a mechanism. It is sequencing: the layer that decides which byte goes next, what to do with each answer, and when the transfer is over.
1. The Write, in One Sentence
The specification is unusually terse about the write, and the terseness is the point.
Three clauses, and each one is a design constraint rather than a description.
"Master-transmitter transmits to slave-receiver" fixes the roles for the whole transfer. Chapter 7.3 derived the general rule; for a write it collapses to something you can hold in your head — the master drives all eight data bits of every byte, and the slave drives every ninth bit.
"The transfer direction is not changed" is a prohibition, and it is the clause people misread. It does not say the direction cannot change on the bus; it says it cannot change within one addressing. To change direction you must re-address, which means a repeated START and a new address byte — exactly the mechanism Chapter 5.4 built. A write, as a frame, has one direction and one direction only.
"The slave receiver acknowledges each byte" is the clause that makes a write verifiable byte by byte. Every byte has an answer, so a master never has to guess whether a write landed. Chapter 7.4 established the asymmetry that follows: in a write, every acknowledge should be an ACK, and any NACK is information. In a read, the final NACK is normal. A write that ends with a NACK on its last byte did not complete.
2. The Frame, Event by Event
Here is a complete single-register write: device 0x48, register 0x10, value 0x2F. Nine events, and every one of them has a chapter behind it.
Two things about that diagram are worth stating explicitly, because a sequence diagram hides them.
The dashed arrows are not messages. They are a single bit each, in the ninth clock slot of the byte above them, on the same two wires. Nothing separate happens. The diagram draws them as returns because that is what they mean, but on the wire the acknowledge is the ninth pulse of the byte it answers — there is no gap, no turnaround, no idle time.
The register pointer is not part of I²C. The address byte, the acknowledges and the framing are protocol. The byte 0x10 meaning "register 16" is a convention invented by that device's designer and written in its datasheet. I²C transports bytes; what the first payload byte means is entirely the device's business. This is the single most common source of confusion for engineers coming from a bus with an address phase — I²C's address byte addresses the device, never a location inside it.
3. Every Bit Accounted For
The chapter's title claims every bit is accounted for, so here is the accounting for the frame above.
| segment | SCL pulses | driven by | established in |
|---|---|---|---|
| S | none | master | 5.2 |
| address byte 0x90 — 8 bits | 8 | master | 6.1 |
| its acknowledge | 1 | slave 0x48 | 7.2 |
| pointer byte 0x10 — 8 bits | 8 | master | 7.1 |
| its acknowledge | 1 | slave 0x48 | 7.3 |
| data byte 0x2F — 8 bits | 8 | master | 7.1 |
| its acknowledge | 1 | slave 0x48 | 7.3 |
| P | none | master | 5.3 |
| total | 27 |
Twenty-seven clock pulses for two payload bytes. That number, and what it implies, is Chapter 8.3's entire subject.
Neither framing condition consumes a clock pulse. S and P are defined by SDA moving while SCL is high — they live in the gaps, not in the pulse count. This is why the pulse count of a frame is always an exact multiple of nine, and why a capture whose pulse count is not a multiple of nine contains either a truncated byte or a framing event you have not spotted.
Here is the first byte at bit resolution. Each interval is one bit, so each interval is one SCL pulse.
Address byte 0x90 — 1001 0000 — then the slave's ACK
9 cyclesThe drives SDA row is the part worth staring at. It changes exactly once in the byte, at the boundary into the ninth interval, and it changes because of a rule that is not in the data — Chapter 7.2 established that the transmitter must release SDA before the ninth slot so the receiver can answer. Nothing in the byte's value says when to let go. It is the slot number.
4. Where a Write Can Fail
A write has exactly three outcomes that a sequencer must distinguish, and conflating any two of them produces a driver that reports the wrong fault.
| outcome | what the wire shows | what it means | what the master should do |
|---|---|---|---|
| complete | every byte ACKed, then P | the whole payload landed | nothing |
| address NACK | the address byte NACKed | no device at that address answered | there is nothing to retry — abort |
| data NACK | byte k NACKed after the address ACKed | the device is there, and refused byte k | retry is meaningful; bytes 0..k−1 landed |
The distinction between the last two is the one that matters in the field, and it is the distinction a summary "write failed" return code destroys.
An address NACK is a wiring or configuration fault. Nobody claimed the address. Chapter 6.4 covered the causes: the device is unpowered, the strap pins select a different address, the address in the driver was not shifted, or the device is not on the bus at all. Retrying will produce the same result forever, which is why a sequencer that retries an address NACK turns a static fault into a busy loop.
A data NACK means the device is present and said no. Chapter 7.4 and §3.1.6 give the two reasons a receiver produces one: it got data it does not understand, or it cannot accept any more. Both are transfer-specific, so both may well succeed on a retry — after the device has serviced whatever occupied it, or with a payload it does understand.
And there is the number the caller actually needs, which is neither of the above:
How many bytes landed. A five-byte write that was NACKed on byte three wrote two bytes. Those two writes are not undone — I²C has no rollback and no transaction abort. Whatever registers they hit hold their new values, and a driver that reports only "failed" leaves the device in a state its caller believes it is not in. This is why the sequencer below exposes bytes_written as a first-class output rather than a debug counter.
5. Sequencing Is a Layer, Not a Detail
Everything in the previous four sections is already built. Chapter 5.5 emits framing, Chapter 7.1 transfers a byte, Chapter 7.2 owns the ninth slot. What no chapter has built is the thing that decides what happens next.
That layer has a clean contract, and defining it clean is most of the design:
- it issues commands — send an S, send this byte, send a P — and never touches SDA or SCL;
- it consumes completions — the S finished, the byte finished, here is the answer;
- it holds the transaction state — which byte we are on, how many were accepted, what went wrong.
The payoff of that split is that the sequencer contains no timing at all. It has no idea what the SCL frequency is, no idea whether the slave stretched the clock, and no notion of a nanosecond. It is a pure transaction-level machine, and that is exactly the level a UVM sequence operates at — §9 makes that correspondence explicit.
6. The Write Sequencer in Three Languages
The design below is that layer. It is worth reading the state list before the code, because the state count is where the one non-obvious decision lives.
| state | waiting for | why it exists |
|---|---|---|
WS_IDLE | a request | |
WS_START | framing_done | the S must complete before a byte may be sent |
WS_ADDR | byte_done | the address byte is in flight; its answer decides everything |
WS_DATA | byte_done | a payload byte is in flight |
WS_FETCH | one clock | the payload source needs a cycle to present the next byte |
WS_STOP | framing_done | the P must complete before the transaction is done |
WS_FETCH is the state that looks removable and is not. §6a defends it, and the mutation table in §8 shows exactly what disappears without it.
// A master-transmitter's TRANSACTION-level sequencer. It issues framing and byte
// commands to the lower layers built in Modules 5 and 7 and reacts to each
// acknowledge; it contains no bit-level logic of its own.
//
// The specification's write format is one sentence: "Master-transmitter transmits
// to slave-receiver. The transfer direction is not changed. The slave receiver
// acknowledges each byte." This block is that sentence plus what to do when the
// slave does not acknowledge.
module i2c_write_sequencer #(
parameter int LEN_W = 8
)(
input logic clk,
input logic rst_n,
// ---- transaction request ----
input logic req, // pulse: begin a write
input logic [6:0] req_addr, // seven-bit address, as the datasheet states it
input logic [LEN_W-1:0] req_len, // data bytes; ZERO is a legal address-only probe
// ---- commands to the framing sequencer (5.5) and byte engine (7.1) ----
output logic cmd_start, // pulse: emit S
output logic cmd_stop, // pulse: emit P
output logic cmd_byte, // pulse: transmit one byte
output logic [7:0] cmd_byte_data,
// ---- completions from those layers ----
input logic framing_done, // pulse: an S or P completed
input logic byte_done, // pulse: a byte AND its ninth slot completed
input logic ack_received, // the answer to the byte just sent
// ---- payload source ----
output logic [LEN_W-1:0] data_index, // which payload byte is wanted
input logic [7:0] data_in,
// ---- status ----
output logic busy,
output logic done, // pulse: the transaction has ended
output logic addr_nacked, // nobody answered the address byte
output logic data_nacked, // a slave refused a data byte
output logic [LEN_W-1:0] bytes_written // payload bytes actually acknowledged
);
typedef enum logic [2:0] {
WS_IDLE,
WS_START, // waiting for the S to complete
WS_ADDR, // the address byte is in flight
WS_DATA, // a payload byte is in flight
WS_FETCH, // one cycle for the payload source to present the next byte
WS_STOP // waiting for the P to complete
} state_e;
state_e state;
// The address byte is the seven-bit address with R/W = 0 appended -- one field
// on the wire, exactly as Chapter 6.1 established.
logic [7:0] addr_byte;
assign addr_byte = {req_addr, 1'b0};
always_ff @(posedge clk) begin
if (!rst_n) begin
state <= WS_IDLE;
cmd_start <= 1'b0;
cmd_stop <= 1'b0;
cmd_byte <= 1'b0;
cmd_byte_data <= 8'h00;
data_index <= '0;
done <= 1'b0;
addr_nacked <= 1'b0;
data_nacked <= 1'b0;
bytes_written <= '0;
end else begin
// Commands are single-cycle pulses: the layers below count events.
cmd_start <= 1'b0;
cmd_stop <= 1'b0;
cmd_byte <= 1'b0;
done <= 1'b0;
case (state)
WS_IDLE:
if (req) begin
// A new transaction clears the previous one's verdict, so a
// consumer can never read a stale NACK as this transfer's.
addr_nacked <= 1'b0;
data_nacked <= 1'b0;
bytes_written <= '0;
data_index <= '0;
cmd_start <= 1'b1;
state <= WS_START;
end
WS_START:
if (framing_done) begin
cmd_byte <= 1'b1;
cmd_byte_data <= addr_byte;
state <= WS_ADDR;
end
WS_ADDR:
if (byte_done) begin
if (!ack_received) begin
// Condition 1: nobody is there. Abort with a STOP -- the
// specification allows a STOP or a repeated START, and a
// write with no target has nothing to retry.
addr_nacked <= 1'b1;
cmd_stop <= 1'b1;
state <= WS_STOP;
end else if (req_len == '0) begin
// An ADDRESS-ONLY write. Not a degenerate case: this is
// exactly how an address scan probes for a device.
cmd_stop <= 1'b1;
state <= WS_STOP;
end else begin
cmd_byte <= 1'b1;
cmd_byte_data <= data_in;
state <= WS_DATA;
end
end
WS_DATA:
if (byte_done) begin
if (!ack_received) begin
// Condition 3 or 4: the slave did not understand the byte
// or cannot take another. bytes_written deliberately does
// NOT count this one -- it was refused.
data_nacked <= 1'b1;
cmd_stop <= 1'b1;
state <= WS_STOP;
end else begin
bytes_written <= bytes_written + 1'b1;
if ((data_index + 1'b1) == req_len) begin
cmd_stop <= 1'b1;
state <= WS_STOP;
end else begin
// Advance the index and give the payload source ONE
// CYCLE to present the new byte. data_in is a function
// of data_index, so latching it in the same cycle as
// the increment would re-send the byte just sent -- a
// classic off-by-one that only a payload of DISTINCT
// bytes can expose.
data_index <= data_index + 1'b1;
state <= WS_FETCH;
end
end
end
WS_FETCH: begin
cmd_byte <= 1'b1;
cmd_byte_data <= data_in; // now indexed by the NEW data_index
state <= WS_DATA;
end
WS_STOP:
if (framing_done) begin
done <= 1'b1;
state <= WS_IDLE;
end
default: state <= WS_IDLE;
endcase
end
end
assign busy = (state != WS_IDLE);
endmodule module i2c_write_sequencer_tb;
localparam int LEN_W = 8;
logic clk = 1'b0, rst_n;
logic req;
logic [6:0] req_addr;
logic [LEN_W-1:0] req_len;
logic cmd_start, cmd_stop, cmd_byte;
logic [7:0] cmd_byte_data;
logic framing_done, byte_done, ack_received;
logic [LEN_W-1:0] data_index;
logic [7:0] data_in;
logic busy, done, addr_nacked, data_nacked;
logic [LEN_W-1:0] bytes_written;
int errors = 0;
i2c_write_sequencer #(.LEN_W(LEN_W)) dut (.*);
always #5 clk = ~clk;
initial begin #80000; $display("FAIL: watchdog expired"); $finish; end
// ---- a payload the sequencer reads through data_index ----
logic [7:0] payload [0:15];
assign data_in = payload[data_index[3:0]];
// ---- counts and a log of what was actually put on the wire ----
int n_start = 0, n_stop = 0, n_byte = 0, n_done = 0;
// Module scope: the completion wait below is count-based rather than level-based,
// because `done` is still asserted at the negedge after it fires -- so a plain
// `wait (done)` in the NEXT transaction returns immediately on the stale pulse.
int base_done;
logic [7:0] wire_log [0:31];
int n_wire = 0;
always @(posedge clk) if (rst_n) begin
if (cmd_start) n_start++;
if (cmd_stop) n_stop++;
if (done) n_done++;
if (cmd_byte) begin
if (n_wire < 32) wire_log[n_wire] = cmd_byte_data;
n_wire++;
n_byte++;
end
end
// ---- a model of the layers below: Module 5's framing sequencer and Module 7's
// byte engine. TWO independent processes, because a single always block with
// embedded waits can only service one command at a time and silently drops
// the next one -- which cost a debugging round during development.
int nack_on_byte; // wire byte index the slave refuses (0 = the address)
int byte_seen;
logic model_en = 1'b0;
// The model OWNS byte_seen and clears it on request. The stimulus could simply
// assign it here, but VHDL permits only one driver per signal, so the VHDL
// version must use a request -- and keeping all three testbenches structurally
// identical is worth more than the one line it saves.
logic byte_rst = 1'b0;
initial forever begin
@(posedge clk);
if (rst_n && model_en && (cmd_start || cmd_stop)) begin
repeat (3) @(posedge clk);
framing_done <= 1'b1; @(posedge clk); framing_done <= 1'b0;
end
end
initial forever begin
@(posedge clk);
if (byte_rst) byte_seen = 0;
else if (rst_n && model_en && cmd_byte) begin
repeat (4) @(posedge clk);
ack_received <= (byte_seen != nack_on_byte);
byte_done <= 1'b1; @(posedge clk); byte_done <= 1'b0;
byte_seen++;
end
end
task automatic run_write(input logic [6:0] a, input int len, input int nack_idx);
nack_on_byte = nack_idx;
byte_rst = 1'b1; @(negedge clk); byte_rst = 1'b0;
n_wire = 0;
req_addr = a; req_len = len[LEN_W-1:0];
base_done = n_done;
@(negedge clk); // let the request fields settle before asserting req
req = 1'b1; @(negedge clk); req = 1'b0;
wait (n_done == base_done + 1);
@(negedge clk);
endtask
initial begin
rst_n = 1'b0; req = 1'b0; req_addr = 7'h00; req_len = '0;
framing_done = 1'b0; byte_done = 1'b0; ack_received = 1'b0;
nack_on_byte = -1; byte_seen = 0;
for (int i = 0; i < 16; i++) payload[i] = 8'hA0 + i[7:0];
repeat (3) @(negedge clk);
if (busy !== 1'b0) begin $display("FAIL: busy out of reset"); errors++; end
rst_n = 1'b1; model_en = 1'b1; @(negedge clk);
// ---- 1: a three-byte write, everything acknowledged.
run_write(7'h48, 3, -1);
if (n_start != 1 || n_stop != 1) begin
$display("FAIL: expected one S and one P, saw %0d and %0d", n_start, n_stop); errors++; end
if (n_byte != 4) begin
$display("FAIL: a 3-byte write must put 4 bytes on the wire, saw %0d", n_byte); errors++; end
if (wire_log[0] !== 8'h90) begin
$display("FAIL: address byte was 0x%02h, expected 0x90 (0x48 << 1, write)", wire_log[0]); errors++; end
if (wire_log[1] !== 8'hA0 || wire_log[2] !== 8'hA1 || wire_log[3] !== 8'hA2) begin
$display("FAIL: payload was 0x%02h,0x%02h,0x%02h", wire_log[1], wire_log[2], wire_log[3]); errors++; end
if (bytes_written !== 8'd3) begin
$display("FAIL: bytes_written = %0d, expected 3", bytes_written); errors++; end
if (addr_nacked !== 1'b0 || data_nacked !== 1'b0) begin
$display("FAIL: spurious NACK report on a clean write"); errors++; end
// ---- 2: the ADDRESS is NACKed. The transfer must abort immediately, with a
// STOP and NO payload bytes on the wire.
n_start = 0; n_stop = 0; n_byte = 0;
run_write(7'h7A, 3, 0);
if (addr_nacked !== 1'b1) begin $display("FAIL: an unanswered address was not reported"); errors++; end
if (data_nacked !== 1'b0) begin $display("FAIL: data_nacked set on an address NACK"); errors++; end
if (n_byte != 1) begin
$display("FAIL: after an address NACK, %0d bytes went out; expected only the address", n_byte);
errors++; end
if (bytes_written !== 8'd0) begin
$display("FAIL: bytes_written = %0d after an address NACK", bytes_written); errors++; end
if (n_stop != 1) begin $display("FAIL: an aborted write must still issue a STOP"); errors++; end
// ---- 3: the SECOND payload byte is refused (wire byte index 2).
n_start = 0; n_stop = 0; n_byte = 0;
run_write(7'h48, 4, 2);
if (data_nacked !== 1'b1) begin $display("FAIL: a refused data byte was not reported"); errors++; end
if (addr_nacked !== 1'b0) begin $display("FAIL: addr_nacked set on a data NACK"); errors++; end
// One payload byte was accepted before the refusal; the refused one is NOT counted.
if (bytes_written !== 8'd1) begin
$display("FAIL: bytes_written = %0d, expected 1 (the refused byte is not written)", bytes_written);
errors++; end
if (n_byte != 3) begin
$display("FAIL: %0d bytes on the wire, expected 3 (addr + accepted + refused)", n_byte);
errors++; end
if (n_stop != 1) begin $display("FAIL: no STOP after a data NACK"); errors++; end
// ---- 4: an ADDRESS-ONLY write. This is how an address scan probes, so it
// must be a first-class case rather than a degenerate one.
n_start = 0; n_stop = 0; n_byte = 0;
run_write(7'h50, 0, -1);
if (n_byte != 1) begin
$display("FAIL: an address-only write put %0d bytes on the wire", n_byte); errors++; end
if (addr_nacked !== 1'b0) begin $display("FAIL: probe of a present device reported a NACK"); errors++; end
if (bytes_written !== 8'd0) begin
$display("FAIL: an address-only write wrote %0d payload bytes", bytes_written); errors++; end
if (n_start != 1 || n_stop != 1) begin
$display("FAIL: a probe must still be framed by one S and one P"); errors++; end
// ---- 5: an address-only probe of an ABSENT device -- the scan's negative case.
run_write(7'h7B, 0, 0);
if (addr_nacked !== 1'b1) begin
$display("FAIL: probe of an absent device did not report a NACK"); errors++; end
// ---- 6: back-to-back transactions must not inherit a verdict. This follows
// a NACKed probe with a clean write.
n_start = 0; n_stop = 0; n_byte = 0;
run_write(7'h48, 2, -1);
if (addr_nacked !== 1'b0 || data_nacked !== 1'b0) begin
$display("FAIL: a clean write inherited the previous transaction's NACK"); errors++; end
if (bytes_written !== 8'd2) begin
$display("FAIL: bytes_written = %0d on the follow-up write", bytes_written); errors++; end
if (n_done != 6) begin
$display("FAIL: %0d done pulses over 6 transactions", n_done); errors++; end
if (busy !== 1'b0) begin $display("FAIL: still busy after the last transaction"); errors++; end
if (errors == 0)
$display("PASS: address byte shifted, payload in order, NACK phase reported, probe and abort framed");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule // A master-transmitter's TRANSACTION-level sequencer. It issues framing and byte
// commands to the lower layers built in Modules 5 and 7 and reacts to each
// acknowledge; it contains no bit-level logic of its own.
//
// The specification's write format is one sentence: "Master-transmitter transmits
// to slave-receiver. The transfer direction is not changed. The slave receiver
// acknowledges each byte." This block is that sentence plus what to do when the
// slave does not acknowledge.
module i2c_write_sequencer #(
parameter integer LEN_W = 8
)(
input wire clk,
input wire rst_n,
// ---- transaction request ----
input wire req, // pulse: begin a write
input wire [6:0] req_addr, // seven-bit address, as the datasheet states it
input wire [LEN_W-1:0] req_len, // data bytes; ZERO is a legal address-only probe
// ---- commands to the framing sequencer (5.5) and byte engine (7.1) ----
output reg cmd_start, // pulse: emit S
output reg cmd_stop, // pulse: emit P
output reg cmd_byte, // pulse: transmit one byte
output reg [7:0] cmd_byte_data,
// ---- completions from those layers ----
input wire framing_done, // pulse: an S or P completed
input wire byte_done, // pulse: a byte AND its ninth slot completed
input wire ack_received, // the answer to the byte just sent
// ---- payload source ----
output reg [LEN_W-1:0] data_index, // which payload byte is wanted
input wire [7:0] data_in,
// ---- status ----
output wire busy,
output reg done, // pulse: the transaction has ended
output reg addr_nacked, // nobody answered the address byte
output reg data_nacked, // a slave refused a data byte
output reg [LEN_W-1:0] bytes_written // payload bytes actually acknowledged
);
localparam WS_IDLE = 3'd0;
localparam WS_START = 3'd1; // waiting for the S to complete
localparam WS_ADDR = 3'd2; // the address byte is in flight
localparam WS_DATA = 3'd3; // a payload byte is in flight
localparam WS_FETCH = 3'd4; // one cycle for the payload source to present the next byte
localparam WS_STOP = 3'd5; // waiting for the P to complete
reg [2:0] state;
// The address byte is the seven-bit address with R/W = 0 appended -- one field
// on the wire, exactly as Chapter 6.1 established.
wire [7:0] addr_byte = {req_addr, 1'b0};
always @(posedge clk) begin
if (!rst_n) begin
state <= WS_IDLE;
cmd_start <= 1'b0;
cmd_stop <= 1'b0;
cmd_byte <= 1'b0;
cmd_byte_data <= 8'h00;
data_index <= {LEN_W{1'b0}};
done <= 1'b0;
addr_nacked <= 1'b0;
data_nacked <= 1'b0;
bytes_written <= {LEN_W{1'b0}};
end else begin
// Commands are single-cycle pulses: the layers below count events.
cmd_start <= 1'b0;
cmd_stop <= 1'b0;
cmd_byte <= 1'b0;
done <= 1'b0;
case (state)
WS_IDLE:
if (req) begin
// A new transaction clears the previous one's verdict, so a
// consumer can never read a stale NACK as this transfer's.
addr_nacked <= 1'b0;
data_nacked <= 1'b0;
bytes_written <= {LEN_W{1'b0}};
data_index <= {LEN_W{1'b0}};
cmd_start <= 1'b1;
state <= WS_START;
end
WS_START:
if (framing_done) begin
cmd_byte <= 1'b1;
cmd_byte_data <= addr_byte;
state <= WS_ADDR;
end
WS_ADDR:
if (byte_done) begin
if (!ack_received) begin
// Condition 1: nobody is there. Abort with a STOP -- the
// specification allows a STOP or a repeated START, and a
// write with no target has nothing to retry.
addr_nacked <= 1'b1;
cmd_stop <= 1'b1;
state <= WS_STOP;
end else if (req_len == {LEN_W{1'b0}}) begin
// An ADDRESS-ONLY write. Not a degenerate case: this is
// exactly how an address scan probes for a device.
cmd_stop <= 1'b1;
state <= WS_STOP;
end else begin
cmd_byte <= 1'b1;
cmd_byte_data <= data_in;
state <= WS_DATA;
end
end
WS_DATA:
if (byte_done) begin
if (!ack_received) begin
// Condition 3 or 4: the slave did not understand the byte
// or cannot take another. bytes_written deliberately does
// NOT count this one -- it was refused.
data_nacked <= 1'b1;
cmd_stop <= 1'b1;
state <= WS_STOP;
end else begin
bytes_written <= bytes_written + 1'b1;
if ((data_index + 1'b1) == req_len) begin
cmd_stop <= 1'b1;
state <= WS_STOP;
end else begin
// Advance the index and give the payload source ONE
// CYCLE to present the new byte. data_in is a function
// of data_index, so latching it in the same cycle as
// the increment would re-send the byte just sent -- a
// classic off-by-one that only a payload of DISTINCT
// bytes can expose.
data_index <= data_index + 1'b1;
state <= WS_FETCH;
end
end
end
WS_FETCH: begin
cmd_byte <= 1'b1;
cmd_byte_data <= data_in; // now indexed by the NEW data_index
state <= WS_DATA;
end
WS_STOP:
if (framing_done) begin
done <= 1'b1;
state <= WS_IDLE;
end
default: state <= WS_IDLE;
endcase
end
end
assign busy = (state != WS_IDLE);
endmodule module i2c_write_sequencer_tb;
localparam integer LEN_W = 8;
reg clk, rst_n;
reg req;
reg [6:0] req_addr;
reg [LEN_W-1:0] req_len;
wire cmd_start, cmd_stop, cmd_byte;
wire [7:0] cmd_byte_data;
reg framing_done, byte_done, ack_received;
wire [LEN_W-1:0] data_index;
wire [7:0] data_in;
wire busy, done, addr_nacked, data_nacked;
wire [LEN_W-1:0] bytes_written;
integer errors, n_start, n_stop, n_byte, n_done, n_wire, base_done;
integer nack_on_byte, byte_seen, i;
i2c_write_sequencer #(.LEN_W(LEN_W)) dut (.clk(clk), .rst_n(rst_n), .req(req),
.req_addr(req_addr), .req_len(req_len), .cmd_start(cmd_start), .cmd_stop(cmd_stop),
.cmd_byte(cmd_byte), .cmd_byte_data(cmd_byte_data), .framing_done(framing_done),
.byte_done(byte_done), .ack_received(ack_received), .data_index(data_index),
.data_in(data_in), .busy(busy), .done(done), .addr_nacked(addr_nacked),
.data_nacked(data_nacked), .bytes_written(bytes_written));
initial clk = 1'b0;
always #5 clk = ~clk;
initial begin #80000; $display("FAIL: watchdog expired"); $finish; end
// ---- a payload the sequencer reads through data_index ----
reg [7:0] payload [0:15];
assign data_in = payload[data_index[3:0]];
// ---- counts and a log of what was actually put on the wire ----
// The completion wait below is count-based rather than level-based, because done
// is still asserted at the negedge after it fires -- so a plain wait on the level
// would return immediately on the previous transaction's stale pulse.
reg [7:0] wire_log [0:31];
always @(posedge clk) if (rst_n) begin
if (cmd_start) n_start = n_start + 1;
if (cmd_stop) n_stop = n_stop + 1;
if (done) n_done = n_done + 1;
if (cmd_byte) begin
if (n_wire < 32) wire_log[n_wire] = cmd_byte_data;
n_wire = n_wire + 1;
n_byte = n_byte + 1;
end
end
// ---- a model of the layers below: Module 5's framing sequencer and Module 7's
// byte engine. TWO independent processes, because a single always block with
// embedded waits can only service one command at a time and silently drops
// the next one -- which cost a debugging round during development.
reg model_en;
// The model OWNS byte_seen and clears it on request, matching the VHDL version,
// where one driver per signal is a language rule rather than a style choice.
reg byte_rst;
initial forever begin
@(posedge clk);
if (rst_n && model_en && (cmd_start || cmd_stop)) begin
repeat (3) @(posedge clk);
framing_done <= 1'b1; @(posedge clk); framing_done <= 1'b0;
end
end
initial forever begin
@(posedge clk);
if (byte_rst) byte_seen = 0;
else if (rst_n && model_en && cmd_byte) begin
repeat (4) @(posedge clk);
ack_received <= (byte_seen != nack_on_byte);
byte_done <= 1'b1; @(posedge clk); byte_done <= 1'b0;
byte_seen = byte_seen + 1;
end
end
task run_write; input [6:0] a; input integer len; input integer nack_idx; begin
nack_on_byte = nack_idx;
byte_rst = 1'b1; @(negedge clk); byte_rst = 1'b0;
n_wire = 0;
req_addr = a; req_len = len[LEN_W-1:0];
base_done = n_done;
@(negedge clk); // let the request fields settle before asserting req
req = 1'b1; @(negedge clk); req = 1'b0;
wait (n_done == base_done + 1);
@(negedge clk);
end endtask
initial begin
errors = 0; n_start = 0; n_stop = 0; n_byte = 0; n_done = 0; n_wire = 0;
nack_on_byte = -1; byte_seen = 0; model_en = 1'b0; byte_rst = 1'b0;
rst_n = 1'b0; req = 1'b0; req_addr = 7'h00; req_len = {LEN_W{1'b0}};
framing_done = 1'b0; byte_done = 1'b0; ack_received = 1'b0;
nack_on_byte = -1; byte_seen = 0;
for (i = 0; i < 16; i = i + 1) payload[i] = 8'hA0 + i[7:0];
repeat (3) @(negedge clk);
if (busy !== 1'b0) begin $display("FAIL: busy out of reset"); errors = errors + 1; end
rst_n = 1'b1; model_en = 1'b1; @(negedge clk);
// ---- 1: a three-byte write, everything acknowledged.
run_write(7'h48, 3, -1);
if (n_start != 1 || n_stop != 1) begin
$display("FAIL: expected one S and one P, saw %0d and %0d", n_start, n_stop); errors = errors + 1; end
if (n_byte != 4) begin
$display("FAIL: a 3-byte write must put 4 bytes on the wire, saw %0d", n_byte); errors = errors + 1; end
if (wire_log[0] !== 8'h90) begin
$display("FAIL: address byte was 0x%02h, expected 0x90 (0x48 << 1, write)", wire_log[0]); errors = errors + 1; end
if (wire_log[1] !== 8'hA0 || wire_log[2] !== 8'hA1 || wire_log[3] !== 8'hA2) begin
$display("FAIL: payload was 0x%02h,0x%02h,0x%02h", wire_log[1], wire_log[2], wire_log[3]); errors = errors + 1; end
if (bytes_written !== 8'd3) begin
$display("FAIL: bytes_written = %0d, expected 3", bytes_written); errors = errors + 1; end
if (addr_nacked !== 1'b0 || data_nacked !== 1'b0) begin
$display("FAIL: spurious NACK report on a clean write"); errors = errors + 1; end
// ---- 2: the ADDRESS is NACKed. The transfer must abort immediately, with a
// STOP and NO payload bytes on the wire.
n_start = 0; n_stop = 0; n_byte = 0;
run_write(7'h7A, 3, 0);
if (addr_nacked !== 1'b1) begin $display("FAIL: an unanswered address was not reported"); errors = errors + 1; end
if (data_nacked !== 1'b0) begin $display("FAIL: data_nacked set on an address NACK"); errors = errors + 1; end
if (n_byte != 1) begin
$display("FAIL: after an address NACK, %0d bytes went out; expected only the address", n_byte);
errors = errors + 1; end
if (bytes_written !== 8'd0) begin
$display("FAIL: bytes_written = %0d after an address NACK", bytes_written); errors = errors + 1; end
if (n_stop != 1) begin $display("FAIL: an aborted write must still issue a STOP"); errors = errors + 1; end
// ---- 3: the SECOND payload byte is refused (wire byte index 2).
n_start = 0; n_stop = 0; n_byte = 0;
run_write(7'h48, 4, 2);
if (data_nacked !== 1'b1) begin $display("FAIL: a refused data byte was not reported"); errors = errors + 1; end
if (addr_nacked !== 1'b0) begin $display("FAIL: addr_nacked set on a data NACK"); errors = errors + 1; end
// One payload byte was accepted before the refusal; the refused one is NOT counted.
if (bytes_written !== 8'd1) begin
$display("FAIL: bytes_written = %0d, expected 1 (the refused byte is not written)", bytes_written);
errors = errors + 1; end
if (n_byte != 3) begin
$display("FAIL: %0d bytes on the wire, expected 3 (addr + accepted + refused)", n_byte);
errors = errors + 1; end
if (n_stop != 1) begin $display("FAIL: no STOP after a data NACK"); errors = errors + 1; end
// ---- 4: an ADDRESS-ONLY write. This is how an address scan probes, so it
// must be a first-class case rather than a degenerate one.
n_start = 0; n_stop = 0; n_byte = 0;
run_write(7'h50, 0, -1);
if (n_byte != 1) begin
$display("FAIL: an address-only write put %0d bytes on the wire", n_byte); errors = errors + 1; end
if (addr_nacked !== 1'b0) begin $display("FAIL: probe of a present device reported a NACK"); errors = errors + 1; end
if (bytes_written !== 8'd0) begin
$display("FAIL: an address-only write wrote %0d payload bytes", bytes_written); errors = errors + 1; end
if (n_start != 1 || n_stop != 1) begin
$display("FAIL: a probe must still be framed by one S and one P"); errors = errors + 1; end
// ---- 5: an address-only probe of an ABSENT device -- the scan's negative case.
run_write(7'h7B, 0, 0);
if (addr_nacked !== 1'b1) begin
$display("FAIL: probe of an absent device did not report a NACK"); errors = errors + 1; end
// ---- 6: back-to-back transactions must not inherit a verdict. This follows
// a NACKed probe with a clean write.
n_start = 0; n_stop = 0; n_byte = 0;
run_write(7'h48, 2, -1);
if (addr_nacked !== 1'b0 || data_nacked !== 1'b0) begin
$display("FAIL: a clean write inherited the previous transaction's NACK"); errors = errors + 1; end
if (bytes_written !== 8'd2) begin
$display("FAIL: bytes_written = %0d on the follow-up write", bytes_written); errors = errors + 1; end
if (n_done != 6) begin
$display("FAIL: %0d done pulses over 6 transactions", n_done); errors = errors + 1; end
if (busy !== 1'b0) begin $display("FAIL: still busy after the last transaction"); errors = errors + 1; end
if (errors == 0)
$display("PASS: address byte shifted, payload in order, NACK phase reported, probe and abort framed");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- A master-transmitter's TRANSACTION-level sequencer. It issues framing and byte
-- commands to the lower layers built in Modules 5 and 7 and reacts to each
-- acknowledge; it contains no bit-level logic of its own.
entity i2c_write_sequencer is
generic (
LEN_W : positive := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- transaction request
req : in std_logic; -- pulse: begin a write
req_addr : in std_logic_vector(6 downto 0); -- seven-bit address
req_len : in unsigned(LEN_W - 1 downto 0); -- ZERO is a legal probe
-- commands to the framing sequencer (5.5) and byte engine (7.1)
cmd_start : out std_logic;
cmd_stop : out std_logic;
cmd_byte : out std_logic;
cmd_byte_data : out std_logic_vector(7 downto 0);
-- completions from those layers
framing_done : in std_logic;
byte_done : in std_logic;
ack_received : in std_logic;
-- payload source
data_index : out unsigned(LEN_W - 1 downto 0);
data_in : in std_logic_vector(7 downto 0);
-- status
busy : out std_logic;
done : out std_logic;
addr_nacked : out std_logic;
data_nacked : out std_logic;
bytes_written : out unsigned(LEN_W - 1 downto 0)
);
end entity;
architecture rtl of i2c_write_sequencer is
type state_t is (
WS_IDLE,
WS_START, -- waiting for the S to complete
WS_ADDR, -- the address byte is in flight
WS_DATA, -- a payload byte is in flight
WS_FETCH, -- one cycle for the payload source to present the next byte
WS_STOP -- waiting for the P to complete
);
signal state : state_t := WS_IDLE;
constant ZERO : unsigned(LEN_W - 1 downto 0) := (others => '0');
signal idx : unsigned(LEN_W - 1 downto 0) := (others => '0');
signal nwr : unsigned(LEN_W - 1 downto 0) := (others => '0');
-- The address byte is the seven-bit address with R/W = 0 appended -- one field
-- on the wire, exactly as Chapter 6.1 established.
signal addr_byte : std_logic_vector(7 downto 0);
begin
addr_byte <= req_addr & '0';
data_index <= idx;
bytes_written <= nwr;
busy <= '0' when state = WS_IDLE else '1';
process (clk)
begin
if rising_edge(clk) then
if rst_n = '0' then
state <= WS_IDLE;
cmd_start <= '0';
cmd_stop <= '0';
cmd_byte <= '0';
cmd_byte_data <= (others => '0');
idx <= (others => '0');
done <= '0';
addr_nacked <= '0';
data_nacked <= '0';
nwr <= (others => '0');
else
-- Commands are single-cycle pulses: the layers below count events.
cmd_start <= '0';
cmd_stop <= '0';
cmd_byte <= '0';
done <= '0';
case state is
when WS_IDLE =>
if req = '1' then
-- A new transaction clears the previous verdict.
addr_nacked <= '0';
data_nacked <= '0';
nwr <= (others => '0');
idx <= (others => '0');
cmd_start <= '1';
state <= WS_START;
end if;
when WS_START =>
if framing_done = '1' then
cmd_byte <= '1';
cmd_byte_data <= addr_byte;
state <= WS_ADDR;
end if;
when WS_ADDR =>
if byte_done = '1' then
if ack_received = '0' then
-- Condition 1: nobody is there. Abort with a STOP.
addr_nacked <= '1';
cmd_stop <= '1';
state <= WS_STOP;
elsif req_len = ZERO then
-- An ADDRESS-ONLY write: how an address scan probes.
cmd_stop <= '1';
state <= WS_STOP;
else
cmd_byte <= '1';
cmd_byte_data <= data_in;
state <= WS_DATA;
end if;
end if;
when WS_DATA =>
if byte_done = '1' then
if ack_received = '0' then
-- Condition 3 or 4. The refused byte is NOT counted.
data_nacked <= '1';
cmd_stop <= '1';
state <= WS_STOP;
else
nwr <= nwr + 1;
if (idx + 1) = req_len then
cmd_stop <= '1';
state <= WS_STOP;
else
-- Advance the index and give the payload source ONE
-- CYCLE to present the new byte: data_in is a
-- function of data_index, so latching it in the same
-- cycle would re-send the byte just sent.
idx <= idx + 1;
state <= WS_FETCH;
end if;
end if;
end if;
when WS_FETCH =>
cmd_byte <= '1';
cmd_byte_data <= data_in; -- now indexed by the NEW idx
state <= WS_DATA;
when WS_STOP =>
if framing_done = '1' then
done <= '1';
state <= WS_IDLE;
end if;
end case;
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_write_sequencer_tb is
end entity;
architecture sim of i2c_write_sequencer_tb is
constant LEN_W : positive := 8;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal req : std_logic := '0';
signal req_addr : std_logic_vector(6 downto 0) := (others => '0');
signal req_len : unsigned(LEN_W - 1 downto 0) := (others => '0');
signal cmd_start, cmd_stop, cmd_byte : std_logic;
signal cmd_byte_data : std_logic_vector(7 downto 0);
signal framing_done : std_logic := '0';
signal byte_done : std_logic := '0';
signal ack_received : std_logic := '0';
signal data_index : unsigned(LEN_W - 1 downto 0);
signal data_in : std_logic_vector(7 downto 0);
signal busy, done, addr_nacked, data_nacked : std_logic;
signal bytes_written : unsigned(LEN_W - 1 downto 0);
-- a payload the sequencer reads through data_index
type pay_t is array (0 to 15) of std_logic_vector(7 downto 0);
signal payload : pay_t;
-- counts and a log of what was actually put on the wire
signal n_start, n_stop, n_byte, n_done, n_wire : natural := 0;
type log_t is array (0 to 31) of std_logic_vector(7 downto 0);
signal wire_log : log_t := (others => (others => '0'));
signal nack_on_byte : integer := -1;
-- VHDL permits ONE driver per signal, so the stimulus cannot clear a counter that
-- a model process owns. It asks instead, with a request signal it alone drives,
-- and the owning process does the clearing. The Verilog testbenches simply assign
-- from both places; this is a real cross-language difference, not a translation
-- artefact, and the VHDL form is the one where the rule is checked.
signal byte_rst : std_logic := '0';
signal byte_seen : natural := 0;
signal model_en : std_logic := '0';
signal test_done : std_logic := '0';
begin
dut : entity work.i2c_write_sequencer
generic map (LEN_W => LEN_W)
port map (clk => clk, rst_n => rst_n, req => req, req_addr => req_addr,
req_len => req_len, cmd_start => cmd_start, cmd_stop => cmd_stop,
cmd_byte => cmd_byte, cmd_byte_data => cmd_byte_data,
framing_done => framing_done, byte_done => byte_done,
ack_received => ack_received, data_index => data_index,
data_in => data_in, busy => busy, done => done,
addr_nacked => addr_nacked, data_nacked => data_nacked,
bytes_written => bytes_written);
data_in <= payload(to_integer(data_index(3 downto 0)));
clk <= not clk after 5 ns;
watchdog : process
begin
wait for 80 us;
if test_done = '0' then
report "watchdog expired -- the design never reached the expected state"
severity failure;
end if;
wait;
end process;
counters : process (clk)
begin
if rising_edge(clk) then
if rst_n = '1' then
if cmd_start = '1' then n_start <= n_start + 1; end if;
if cmd_stop = '1' then n_stop <= n_stop + 1; end if;
if done = '1' then n_done <= n_done + 1; end if;
if cmd_byte = '1' then
if n_wire < 32 then wire_log(n_wire) <= cmd_byte_data; end if;
n_wire <= n_wire + 1;
n_byte <= n_byte + 1;
end if;
end if;
end if;
end process;
-- A model of the layers below: Module 5's framing sequencer and Module 7's byte
-- engine. TWO independent processes, because one process with embedded waits can
-- only service a single command at a time and silently drops the next.
framing_model : process
begin
wait until rising_edge(clk);
if rst_n = '1' and model_en = '1' and (cmd_start = '1' or cmd_stop = '1') then
for i in 1 to 3 loop wait until rising_edge(clk); end loop;
framing_done <= '1';
wait until rising_edge(clk);
framing_done <= '0';
end if;
end process;
byte_model : process
begin
wait until rising_edge(clk);
if byte_rst = '1' then
byte_seen <= 0;
elsif rst_n = '1' and model_en = '1' and cmd_byte = '1' then
for i in 1 to 4 loop wait until rising_edge(clk); end loop;
if byte_seen = nack_on_byte then ack_received <= '0';
else ack_received <= '1'; end if;
byte_done <= '1';
wait until rising_edge(clk);
byte_done <= '0';
byte_seen <= byte_seen + 1;
end if;
end process;
stim : process
variable errs : natural := 0;
variable base : natural := 0;
variable b_st, b_sp, b_by, b_wire : natural := 0;
procedure waitn (n : in positive) is
begin
for i in 1 to n loop wait until falling_edge(clk); end loop;
end procedure;
-- The completion wait is COUNT-based, not level-based: done is still asserted
-- at the negedge after it fires, so waiting on the level would return at once
-- on the previous transaction's stale pulse.
procedure run_write (a : in std_logic_vector(6 downto 0);
len : in natural; nack_idx : in integer) is
begin
nack_on_byte <= nack_idx;
byte_rst <= '1'; waitn(1); byte_rst <= '0';
req_addr <= a;
req_len <= to_unsigned(len, LEN_W);
-- Snapshot every counter this transaction will be measured against,
-- because the stimulus does not own them and cannot zero them.
base := n_done;
b_st := n_start;
b_sp := n_stop;
b_by := n_byte;
b_wire := n_wire;
waitn(1);
req <= '1'; waitn(1); req <= '0';
wait until n_done = base + 1;
waitn(1);
end procedure;
begin
waitn(3);
if busy /= '0' then
report "busy out of reset" severity error; errs := errs + 1; end if;
for i in 0 to 15 loop
payload(i) <= std_logic_vector(to_unsigned(160 + i, 8));
end loop;
rst_n <= '1'; model_en <= '1'; waitn(1);
-- 1: a three-byte write, everything acknowledged.
run_write("1001000", 3, -1);
if (n_start - b_st) /= 1 or (n_stop - b_sp) /= 1 then
report "expected one S and one P" severity error; errs := errs + 1; end if;
if (n_byte - b_by) /= 4 then
report "a 3-byte write must put 4 bytes on the wire" severity error; errs := errs + 1; end if;
if wire_log(b_wire) /= x"90" then
report "address byte wrong: expected 0x90 (0x48 shifted, write)" severity error;
errs := errs + 1; end if;
if wire_log(b_wire+1) /= x"A0" or wire_log(b_wire+2) /= x"A1"
or wire_log(b_wire+3) /= x"A2" then
report "payload bytes wrong or out of order" severity error; errs := errs + 1; end if;
if bytes_written /= 3 then
report "bytes_written wrong" severity error; errs := errs + 1; end if;
if addr_nacked /= '0' or data_nacked /= '0' then
report "spurious NACK report on a clean write" severity error; errs := errs + 1; end if;
-- 2: the ADDRESS is NACKed -- abort at once, no payload on the wire.
run_write("1111010", 3, 0);
if addr_nacked /= '1' then
report "an unanswered address was not reported" severity error; errs := errs + 1; end if;
if data_nacked /= '0' then
report "data_nacked set on an address NACK" severity error; errs := errs + 1; end if;
if (n_byte - b_by) /= 1 then
report "payload went out after an address NACK" severity error; errs := errs + 1; end if;
if bytes_written /= 0 then
report "bytes_written nonzero after an address NACK" severity error; errs := errs + 1; end if;
if (n_stop - b_sp) /= 1 then
report "an aborted write must still issue a STOP" severity error; errs := errs + 1; end if;
-- 3: the SECOND payload byte is refused (wire byte index 2).
run_write("1001000", 4, 2);
if data_nacked /= '1' then
report "a refused data byte was not reported" severity error; errs := errs + 1; end if;
if addr_nacked /= '0' then
report "addr_nacked set on a data NACK" severity error; errs := errs + 1; end if;
if bytes_written /= 1 then
report "the refused byte must not be counted as written" severity error;
errs := errs + 1; end if;
if (n_byte - b_by) /= 3 then
report "wrong number of bytes on the wire after a data NACK" severity error;
errs := errs + 1; end if;
if (n_stop - b_sp) /= 1 then
report "no STOP after a data NACK" severity error; errs := errs + 1; end if;
-- 4: an ADDRESS-ONLY write -- how an address scan probes.
run_write("1010000", 0, -1);
if (n_byte - b_by) /= 1 then
report "an address-only write put the wrong number of bytes out" severity error;
errs := errs + 1; end if;
if addr_nacked /= '0' then
report "probe of a present device reported a NACK" severity error; errs := errs + 1; end if;
if bytes_written /= 0 then
report "an address-only write wrote payload bytes" severity error; errs := errs + 1; end if;
if (n_start - b_st) /= 1 or (n_stop - b_sp) /= 1 then
report "a probe must still be framed by one S and one P" severity error;
errs := errs + 1; end if;
-- 5: probing an ABSENT device -- the scan's negative case.
run_write("1111011", 0, 0);
if addr_nacked /= '1' then
report "probe of an absent device did not report a NACK" severity error;
errs := errs + 1; end if;
-- 6: back-to-back transactions must not inherit a verdict.
run_write("1001000", 2, -1);
if addr_nacked /= '0' or data_nacked /= '0' then
report "a clean write inherited the previous transaction's NACK" severity error;
errs := errs + 1; end if;
if bytes_written /= 2 then
report "bytes_written wrong on the follow-up write" severity error; errs := errs + 1; end if;
if n_done /= 6 then
report "wrong number of done pulses over 6 transactions" severity error;
errs := errs + 1; end if;
if busy /= '0' then
report "still busy after the last transaction" severity error; errs := errs + 1; end if;
if errs = 0 then
report "i2c_write_sequencer self-check complete: address byte shifted, payload in "
& "order, NACK phase reported, probe and abort framed" severity note;
else
report "i2c_write_sequencer self-check FAILED" severity error;
end if;
test_done <= '1';
wait;
end process;
end architecture;6a. Three Decisions Worth Defending
WS_FETCH exists because data_in is a function of data_index. The payload source is combinational on the index — a memory read, a register-file mux, a FIFO output. So on the cycle the sequencer increments data_index, data_in still shows the old byte; the new one appears a cycle later. Latching cmd_byte_data in the same cycle as the increment therefore re-sends the byte just sent. WS_FETCH spends one clock waiting for the new value, and one clock is free — the byte it precedes takes nine SCL periods, which at 100 kHz is ninety microseconds.
This is a bug worth recognising by shape rather than by memory: an index and the data it selects are never valid in the same cycle. It also hides from a lazy testbench, because a payload of identical bytes passes either way. The testbench sends 0xA0, 0xA1, 0xA2 for that reason, and the mutation that removes WS_FETCH reports 0xa0, 0xa0, 0xa1 — the first byte sent twice and the last never sent at all.
bytes_written counts acknowledged bytes, not attempted ones. The increment sits in the ACK branch, and the NACK branch deliberately leaves it alone. The output therefore answers the question a caller actually has — how much of my payload landed — rather than how far did you get, and those differ by exactly one byte in precisely the case where the answer matters. The mutation that moves the increment into the NACK branch is killed by a single check.
A zero-length request is a legal transaction, not an error. req_len = 0 sends S, the address byte, and P — nothing else. That is not a degenerate case to be rejected; it is exactly how an address scan works. Probing an address means asking whether anyone acknowledges it, and the cheapest way to ask is an address-only write. Chapter 6.4 built the scan; this is the transaction it issues, 127 times. A sequencer that treated req_len = 0 as invalid would make the standard discovery tool unimplementable on top of it.
6b. Verified Execution
All three implementations were compiled and run. The SystemVerilog and Verilog with Icarus Verilog 12.0, the VHDL with NVC 1.23.0.
$ iverilog -g2012 -o a0 i2c_write_sequencer.sv i2c_write_sequencer_tb.sv && ./a0
PASS: address byte shifted, payload in order, NACK phase reported, probe and abort framed
i2c_write_sequencer_tb.sv:174: $finish called at 1700 (1s)
$ iverilog -g2005 -o a1 i2c_write_sequencer.v i2c_write_sequencer_tb.v && ./a1
PASS: address byte shifted, payload in order, NACK phase reported, probe and abort framed
i2c_write_sequencer_tb.v:176: $finish called at 1700 (1s)
$ nvc -a i2c_write_sequencer.vhd i2c_write_sequencer_tb.vhd && nvc -e i2c_write_sequencer_tb
$ nvc -r i2c_write_sequencer_tb --stop-time=200us
** Note: 1700ns+0: i2c_write_sequencer self-check complete: address byte shifted,
payload in order, NACK phase reported, probe and abort framedThe three finish at the same simulated time, 1700 ns. That is not decoration. It means the three testbenches apply identical stimulus with identical cycle counts, so the three implementations are being compared rather than merely each passing its own private test. A divergence in that number is the first thing to investigate when porting between languages, and it caught a real discrepancy while this chapter was being written: the VHDL testbench needed a settling wait that the other two did not have, and the fix was to add the wait to all three rather than delete it from one.
7. What the Testbench Proves
The testbench models the two layers below the sequencer — a framing engine and a byte engine — as two independent processes. That structure is itself a finding, recorded in §11.
| # | stimulus | what it establishes |
|---|---|---|
| 1 | a 3-byte write to 0x48 | the address byte is 0x90: the address shifted left, R/W clear |
| 2 | the same write | the payload appears in order — 0xA0, 0xA1, 0xA2, no repeats |
| 3 | the same write | exactly one S and one P, and bytes_written is 3 |
| 4 | req_len = 0 | an address-only probe: one byte on the wire, correctly framed |
| 5 | the address NACKed | addr_nacked set, data_nacked clear, and a P still emitted |
| 6 | byte 2 of 3 NACKed | data_nacked set, bytes_written is 1, and the transfer aborted |
| 7 | back-to-back transactions | done is a pulse and the second transaction starts clean |
Test 6 is the one that earns its place. It checks a count under a fault, and the count is the thing a caller needs and the thing that is easiest to get wrong — off by one in either direction, depending on whether the refused byte is counted and whether the abort happens before or after the increment.
Test 7 exists because of a defect found during development rather than by design: the testbench's own wait (done) returned on a stale pulse. done was still asserted at the following negative edge, so the next transaction's wait was satisfied immediately by the previous transaction's completion. The fix was to stop waiting on a level and start waiting on a count — wait (n_done == base_done + 1) — which is the general remedy whenever a testbench waits on something a design asserts for exactly one cycle.
8. Mutation Testing
Eight targeted defects were injected into the SystemVerilog sequencer, one at a time, and the testbench re-run against each.
| # | injected defect | outcome |
|---|---|---|
| A1 | the address byte's R/W bit says read | killed — address byte was 0x91, expected 0x90 |
| A2 | an address NACK is ignored and the payload is sent anyway | killed |
| A3 | a zero-length request sends a byte anyway | killed — the probe put 257 bytes on the wire |
| A4 | the payload byte is latched in the same cycle as the index increment | killed — payload was 0xa0, 0xa0, 0xa1 |
| A5 | a refused byte is counted as written | killed — bytes_written 2, expected 1 |
| A6 | the last-byte test is off by one | killed — a 3-byte write put 5 bytes on the wire |
| A7 | cmd_start and cmd_stop are held instead of pulsed | killed — saw 36 S's and 5 P's |
| A8 | no STOP after the final byte | killed — the watchdog expired |
Eight injected, eight killed. Two of them are worth a second look.
A4 is the WS_FETCH mutation, and its failure message — 0xa0, 0xa0, 0xa1 — is the off-by-one made visible. Note what it takes to catch: a payload of distinct bytes. With 0xAA, 0xAA, 0xAA this mutation survives, the design ships, and the fault appears in the field as "the first register of a burst gets written twice and the last one never". Choosing distinct stimulus values is not fussiness.
A8 was killed by the watchdog, not by a check, and that is a legitimate kill worth naming as such. A sequencer that never emits its final STOP never asserts done, so no assertion about the result ever runs — the test simply stops making progress. A testbench without a watchdog would have hung, and a hung test in a regression is indistinguishable from an infrastructure problem. Every testbench in this module has one for that reason, and the VHDL watchdogs were verified to fire by deliberately stalling a design.
9. Verification Connection — A Write Is a Transaction Object
The sequencer's port list is, almost line for line, a UVM sequence_item. That is not a coincidence — it is what "transaction-level" means, and the correspondence is worth making explicit because it is the bridge between this course's RTL and its verification track.
class i2c_write_item extends uvm_sequence_item;
`uvm_object_utils(i2c_write_item)
// ---- the request: exactly the sequencer's req_* inputs ----
rand bit [6:0] addr;
rand bit [7:0] payload [];
// ---- the response: exactly the sequencer's status outputs ----
bit addr_nacked;
bit data_nacked;
int bytes_written;
// A zero-length payload is the address-only probe of section 6a, so the
// constraint permits it deliberately -- a scan is a legal transaction and
// a generator that excluded it could never produce one.
constraint c_len { payload.size() inside {[0:16]}; }
// Not every seven-bit value is a device address. Chapter 6.3's reserved
// ranges must be excluded here rather than filtered in the driver, so that
// an illegal address is UNGENERATABLE instead of merely unused.
constraint c_addr {
addr != 7'h00; // general call / START byte
addr[6:3] != 4'b0000; // 0x00-0x07 reserved
addr[6:3] != 4'b1111; // 0x78-0x7F reserved
}
function new(string name = "i2c_write_item");
super.new(name);
endfunction
// The response side is what a scoreboard compares, so it prints separately
// from the request -- a log line that mixes the two is unreadable when the
// interesting case is a request that got an unexpected response.
function string convert2string();
return $sformatf("WRITE addr=0x%02h len=%0d -> written=%0d%s%s",
addr, payload.size(), bytes_written,
addr_nacked ? " ADDR-NACK" : "",
data_nacked ? " DATA-NACK" : "");
endfunction
endclassThree things about that class are load-bearing, and each maps to something derived earlier in this chapter.
bytes_written is in the response, not derived from the request. A scoreboard that assumed a successful write moved payload.size() bytes could never detect the partial-write case of §4 — the very case that leaves hardware in an unintended state. The DUT must tell the environment how far it got.
The address constraint excludes reserved ranges structurally. Chapter 6.3 established which addresses a device may not own. Putting that in a constraint rather than in the driver means random generation cannot produce an illegal stimulus at all, which is the difference between a testbench that avoids a case and one that cannot express it.
A zero-length payload is explicitly legal. The constraint says [0:16], not [1:16]. If §6a is right that an address-only write is the scan primitive, then a random generator that never produces one leaves the single most common real transaction untested.
And the checker that belongs with it is the one property a write must satisfy under every stimulus:
// The direction bit is set once per addressing and never changes -- the
// specification's "the transfer direction is not changed", made checkable.
//
// The property is written on the ADDRESS byte rather than on every byte,
// because the direction is not re-transmitted: there is nothing to compare
// on a data byte. What must be shown is that no data byte appears with a
// direction other than the one the address byte established.
property p_write_direction_fixed;
@(posedge clk) disable iff (!rst_n)
(addr_phase_done && !dir_is_read) |=> (!dir_is_read throughout (!frame_start[->1]));
endproperty
assert property (p_write_direction_fixed)
else $error("the direction changed without a repeated START");
// Every byte of a write gets exactly one acknowledge slot: nine SCL pulses
// per byte, never eight and never ten. This is Chapter 7.1's byte format
// expressed as a bus-level property rather than a block-level one.
property p_nine_pulses_per_byte;
@(posedge clk) disable iff (!rst_n)
byte_done |-> (pulses_this_byte == 9);
endproperty
assert property (p_nine_pulses_per_byte)
else $error("a byte took %0d SCL pulses, not 9", pulses_this_byte);10. FPGA and ASIC Implications
The sequencer is small and its cost is dominated by one parameter.
Area is LEN_W-driven, not state-driven. Six states need three bits. The registers that matter are data_index and bytes_written, both LEN_W wide, plus the eight-bit cmd_byte_data. At LEN_W = 8 the whole block is roughly 30 flops — negligible on any FPGA, and small enough on an ASIC that the decision of what LEN_W should be is about the longest burst you intend to support, not about area.
There is no timing path to the bus. Every output is registered and every input is a completion pulse from a layer that owns the clocking. The sequencer can therefore be synthesised on the system clock with no relationship to the SCL frequency whatsoever, and its static timing is trivially met. This is the synthesis-level payoff of §5's layering.
The payload interface is the integration risk. data_index out, data_in back, combinationally. If the payload source is a block RAM, that path has a registered output and the one cycle WS_FETCH provides is exactly enough — but only just, and only for a one-cycle latency. A source with two-cycle latency needs a second fetch cycle or a handshake, and the failure mode if you forget is precisely A4's: bytes sent one position stale. On an ASIC with a multi-cycle SRAM, make the interface a valid/ready handshake rather than counting cycles.
Reset must leave the bus alone. The sequencer's reset clears cmd_start, cmd_stop and cmd_byte, which means a reset mid-transaction stops issuing commands — it does not generate a STOP. That is the correct choice for this layer and it has a consequence worth knowing: a master reset in the middle of a write leaves the bus with a slave that is still addressed and a transfer that was never ended. Chapter 5.5 covered the recovery, and the reason it is a separate mechanism is exactly this — a reset cannot be a bus operation, because the block that would perform it is the block being reset.
11. Debugging — The Testbench That Dropped Half Its Commands
Pitfall — one process cannot service two independent command streams
// The FIRST version of this chapter's testbench modelled both lower layers --
// framing and the byte engine -- in a single clocked process:
//
// always @(posedge clk) begin
// if (cmd_start || cmd_stop) begin
// repeat (FRAMING_CYCLES) @(posedge clk);
// framing_done <= 1'b1; @(posedge clk); framing_done <= 1'b0;
// end
// if (cmd_byte) begin
// repeat (BYTE_CYCLES) @(posedge clk);
// byte_done <= 1'b1; ack_received <= slave_answer; @(posedge clk);
// byte_done <= 1'b0;
// end
// end
//
// It looks like two independent responders. It is not. The embedded
// "repeat (...) @(posedge clk)" waits SUSPEND THE WHOLE PROCESS, so while the
// framing model is counting out its cycles, the process is not executing the
// second if-statement at all -- and any cmd_byte that arrives during that
// window is never seen.The first transaction completes perfectly. Everything after it is wrong in a way that looks like a design bug in the sequencer.
The 3-byte write puts two bytes on the wire instead of four. The address-only probe sometimes emits no byte at all. Test 7's back-to-back transactions hang. Every symptom points at the sequencer's state machine -- it looks exactly like a machine that loses track of its byte count, and that is a plausible bug in the code under test.
Time went into the sequencer's WS_DATA branch, which was correct. The clue that resolved it was that the NUMBER of dropped bytes depended on FRAMING_CYCLES, a constant in the testbench. Nothing in the design under test can possibly depend on that value -- it only changes how long the testbench's framing model takes. A design symptom that varies with a testbench-only parameter is a testbench bug, and that inference is worth reaching for early.
A single always block that contains blocking waits is a single thread of control, not a set of concurrent responders. Two command streams that can overlap in time need two processes.
The mental model that produced the bug is that an always block "checks its conditions every clock". It does not -- it executes its body, and a wait inside that body stops the block, conditions and all.
12. Common Misconceptions
"The address byte selects a register." It selects a device. The register pointer is a payload byte whose meaning is invented by that device's datasheet, and some devices have no such concept at all. This is the misconception that makes engineers look for an address phase that does not exist.
"A write that returns an error wrote nothing." It wrote every byte that was acknowledged before the refusal. There is no rollback in I²C, and §4 covers why that matters for devices whose registers must be updated together.
"A NACK means the device is broken." A NACK on the address means nobody answered. A NACK on a data byte means a device that is present and working refused that byte — most often because it is busy, which is a normal condition and frequently the documented behaviour during an internal write cycle. An EEPROM NACKs its address for milliseconds after a page write, deliberately, and polling for the ACK to return is the standard way to know it has finished.
"A zero-length write is meaningless." It is the address scan, and it is the most-issued I²C transaction in the world.
"The acknowledge is a separate transfer." It is the ninth clock pulse of the byte it answers. There is no gap and no turnaround; the only thing that happens is that the transmitter released SDA one bit early.
"The direction bit could change mid-transfer to save a repeated START." The specification forbids it, and the reason is mechanical rather than arbitrary: direction is established by the address byte and there is no other place to encode it. A device cannot be told to start transmitting except by being addressed again — which is what a repeated START is for.
13. Reason It Through
A capture shows S, then one byte, then P, and the byte was ACKed. What happened, and what are the two candidate explanations?
One byte after the S is the address byte, so this is an address-only write and the ACK means a device claimed that address. The two readings are: an address scan probing for devices, which is exactly this transaction; or a driver bug — a write whose payload length was computed as zero, which would send the address, get an ACK, and report success while transferring nothing. The two are indistinguishable from the capture alone, which is why the sequencer exposes bytes_written — a driver that logs it cannot silently succeed at writing nothing.
A five-byte write reports data_nacked with bytes_written = 5. Why is that report self-contradictory?
Because the abort happens instead of the increment. If five bytes were acknowledged, the transaction reached its length and finished normally — there was no byte left to refuse. data_nacked with a full count therefore cannot arise from the design in §6, and if you see it, either bytes_written is counting attempts rather than acknowledgements (mutation A5) or the two outputs are being sampled at different times. The second is the more common integration error: both are registered, and both must be read after done.
Why does the sequencer wait for framing_done after a STOP, when the transaction is over?
Because done must not be asserted while the bus is still being driven. A caller that sees done is entitled to issue the next transaction, and the next transaction begins with a START — which needs the bus idle and the bus-free interval observed. Asserting done at the moment the STOP was requested rather than when it completed would let a caller begin a START into a bus that had not finished stopping. This is the same "the completion is the completion of the mechanism, not of the request" reasoning that Chapter 5.5 applied to the bus-free timer.
Clock stretching adds 50 µs to byte three of a write. What in the sequencer changes?
Nothing at all. byte_done arrives 50 µs later; the sequencer was waiting for it and continues to wait. The value of the layering in §5 is that this question has a one-word answer.
14. Understanding Check
15. Summary
The write is the transfer where nothing hands over. The master transmits from the first address bit to the last payload bit, the addressed slave receives throughout, and the only thing the slave drives is the ninth bit of each byte. The specification's "the transfer direction is not changed" is a prohibition on changing direction within an addressing, and re-addressing via a repeated START is the only way to change it.
Nine pulses per byte, and no pulses for framing. A write of n payload bytes puts n+1 bytes on the wire and takes 9(n+1) SCL pulses. The framing conditions live in the gaps, which is why a frame's pulse count is always a multiple of nine.
A write has three outcomes, not two. Complete; address-NACKed, meaning nobody is there and a retry is futile; and data-NACKed, meaning a present device refused a specific byte and a retry may well work. The byte count is a fourth piece of information and the one a caller most needs, because a refused write has still performed every byte it was given an ACK for.
Sequencing is its own layer. Issue commands, consume completions, hold transaction state, touch no wires. That split is what makes clock stretching free, makes static timing trivial, and makes the block's port list read as a UVM sequence_item — because transaction-level is what it is.
WS_FETCH is the chapter's smallest lesson and its most transferable one. An index and the data it selects are never valid in the same cycle, and a payload of identical bytes will never tell you.
16. What Comes Next
Chapter 8.2 takes the other side of this transfer. §2's diagram gave the slave one job — answer every byte — and that is true at the protocol level and an oversimplification at the design level. A real slave-receiver decides what to do with each byte, maintains a register pointer, and owns the two NACK conditions §3.1.6 grants a receiver. It is where a write becomes a device rather than a transfer.
Chapter 8.3 takes §3's number seriously. Twenty-seven pulses for two bytes of payload is 59% efficiency, and the arithmetic of where the rest goes — and what bursting buys — is the difference between a driver that meets its throughput budget and one that does not.
Continue learning
Related tutorials
- Related topic
The I²C Read Transaction End to End
A read reverses every role a write established, and it does so mid-frame on a bus where neither end can drive high. This chapter walks a read from START to STOP, derives the master's acknowledge policy from the one sentence that governs it, and builds the transaction-level sequencer.
- Related topic
Write-Then-Read — The I²C Register-Pointer Pattern
Almost every real I²C access is this one shape: write the register pointer, repeated START, read the data, without ever letting the bus go. This chapter derives it from the specification's own example and builds the transaction sequencer.
- Related topic
Repeated START — Holding the Bus Between Phases
A repeated START is not a new waveform. It is the START edge again, and what makes it a different event is that the bus was already busy. That single fact is why a classifier needs state and why a monitor that joins late cannot classify what it sees.
- Related topic
START/STOP Timing and Malformed Framing
Three framing margins, each with two anchor events, all of them minimums: the hold after a START, the setup before a repeated START, and the setup before a STOP. Build a sequencer that generates all three and refuses an illegal configuration, then catalogue the malformed framing the margins exist to prevent.
