I²C · Module 17
The Byte Engine and the ACK Slot — Where SDA Changes Hands
A byte is nine bit slots and the ninth differs from the eight in exactly one respect: who owns SDA. Builds the ownership flip as a mirror image for reads and writes, keeps the inverted acknowledge polarity in one place, and shows why an engine that holds the line one slot too long is invisible to every per-byte check.
Chapter 17.6 built one bit and stopped there deliberately. This chapter builds eight of them — and then a ninth that is not simply the ninth bit.
1. Nine Slots, and Only One Thing Differs
So a byte is nine bit slots, and the ninth differs from the eight in exactly one respect: who owns SDA. The clock is the same, the sampling instant is the same, the data-valid rule is the same.
That is why this block wraps the bit engine rather than replacing it. Eight slots with the shift register driving, one slot with the direction reversed.
2. The Ownership Flip Is the Whole Block
| Direction | Slots 0–7 | Slot 8 |
|---|---|---|
| Write | master drives from the shift register | master releases, reads the target's answer |
| Read | master releases, shifts in the target's bits | master drives its own ACK or NACK |
The two cases are mirror images, and the code says so in one expression rather than as two paths that happen to agree:
slot 8: tx_en <= ~dir_write the direction reverses, whichever way it was
cur_bit <= dir_write ? 1'b1 writing: release so the target can answer
: ~ack_to_send reading: send our own answerWriting it as one mirrored expression rather than two branches is worth more than it looks: two branches can drift apart under maintenance while continuing to pass tests that only exercise one of them.
3. MSB First, and Why the Shift Direction Has a Second Consequence
So the shift register shifts left and the bit transmitted is always bit 7.
There is a second consequence worth noting now because Chapter 17.10 depends on it: a partially transmitted byte is left-justified in the register. A byte abandoned mid-flight to arbitration loss therefore has its transmitted bits in the high positions and its untransmitted bits in the low ones, which is exactly what has to be reported when a master loses and must retry.
The engine also shifts in what was on the line — even while transmitting. While receiving that is the received data; while transmitting it is a record of what actually reached the bus, which is not necessarily what was intended. That is the same one-sample-two-consumers argument as 17.6 §3a, one layer up.
4. The ACK Polarity Is Inverted Relative to Intuition
The receiver pulls SDA LOW to acknowledge. So:
sampled ZERO -> ACK (acknowledged)
sampled ONE -> NACK (not acknowledged)A sampled one is a NACK because nobody pulled the line down — a NACK is the absence of a response, not a signal in its own right. That is why a missing device and a refusing device are indistinguishable, which Module 7 established at protocol level.
This block reports ack as the protocol event — 1 means acknowledged — and keeps the inversion in one place, for exactly the reason Chapter 17.4 keeps the SDA inversion in one place. Two inversions in a design are one sign error waiting to happen; two inversions in different blocks are two.
5. A Write Byte and a Read Byte
Who drives SDA in each of the nine slots
9 cyclesRead the two rows as one rule seen twice: the transmitter releases for slot 8. On a write the master is the transmitter; on a read the target is. Nothing else in the byte depends on direction.
6. The Byte Engine, in Three Languages
// -----------------------------------------------------------------------------
// i2c_byte_engine.sv
// Eight bits, then the ninth slot in which SDA changes hands.
//
// §3.1.4, verbatim: "The acknowledge takes place after every byte. ... The master
// generates all clock pulses, including the acknowledge ninth clock pulse." And: "the
// transmitter releases the SDA line during the acknowledge clock pulse so the receiver
// can pull the SDA line LOW and it remains stable LOW during the HIGH period of this
// clock pulse."
//
// So a byte is NINE bit slots and the ninth is different from the eight in exactly one
// respect: who owns SDA. Everything else -- the clock, the sampling instant, the data
// rule -- is identical. That is why this block wraps the bit engine rather than
// replacing it: eight slots with the shift register driving, one slot with the
// direction reversed.
//
// THE OWNERSHIP FLIP IS THE WHOLE BLOCK. Writing a byte: the master drives bits 0-7
// and RELEASES for bit 8, then reads the target's answer. Reading a byte: the master
// releases for bits 0-7 and DRIVES bit 8, sending its own ACK or NACK. The two cases
// are mirror images and the code says so, which is worth more than two separate paths
// that happen to agree.
//
// MSB FIRST. §3.1.3: "Data is transferred with the Most Significant Bit (MSB) first."
// So the shift register shifts LEFT and the bit transmitted is always bit 7, which also
// means a partially transmitted byte is left-justified in the register -- relevant to
// Chapter 17.10, where a byte abandoned to arbitration loss has to be reported.
//
// THE ACK POLARITY IS INVERTED RELATIVE TO INTUITION. The receiver pulls SDA LOW to
// acknowledge, so a sampled ZERO is an ACK and a sampled ONE is a NACK. This block
// reports `ack` as the protocol event (1 = acknowledged) and keeps the inversion in one
// place, for the same reason Chapter 17.4 keeps the SDA inversion in one place.
// -----------------------------------------------------------------------------
module i2c_byte_engine #(
parameter int CNT_W = 16
) (
input logic clk,
input logic rst_n,
// From the SCL generator, passed through to the bit engine.
input logic drive_point,
input logic sample_point,
// Command. `go` starts one nine-slot byte.
input logic go,
input logic dir_write, // 1 = master transmits the 8 bits and reads ACK
// 0 = master receives the 8 bits and sends ACK
input logic [7:0] tx_byte,
input logic ack_to_send, // when receiving: 1 = ACK (pull low), 0 = NACK
input logic sda_in,
input logic scl_high, // SCL read back, for the bit engine's release rule
input logic abort, // arbitration lost: release SDA immediately
output logic sda_req,
output logic sda_bit,
output logic [7:0] rx_byte,
output logic ack, // 1 = the byte WAS acknowledged
output logic ack_valid, // one cycle, when the ninth slot completes
output logic byte_done, // one cycle
output logic busy,
output logic [3:0] bit_index, // 0..8
output logic driving,
output logic [CNT_W-1:0] bytes_done,
output logic [CNT_W-1:0] acks,
output logic [CNT_W-1:0] nacks
);
logic [7:0] shreg;
logic active;
logic tx_en;
logic cur_bit;
logic rx_bit, rx_valid, bit_done;
i2c_bit_engine #(.CNT_W(CNT_W)) u_bit (
.clk(clk), .rst_n(rst_n),
.drive_point(drive_point), .sample_point(sample_point),
.active(active), .tx_en(tx_en), .tx_bit(cur_bit), .abort(abort),
.sda_in(sda_in), .scl_high(scl_high),
.sda_req(sda_req), .sda_bit(sda_bit),
.rx_bit(rx_bit), .rx_valid(rx_valid), .bit_done(bit_done),
.driving(driving),
.bits_driven(), .bits_sampled());
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
shreg <= 8'h00;
active <= 1'b0;
tx_en <= 1'b0;
cur_bit <= 1'b1;
rx_byte <= 8'h00;
ack <= 1'b0;
ack_valid <= 1'b0;
byte_done <= 1'b0;
busy <= 1'b0;
bit_index <= 4'd0;
bytes_done <= {CNT_W{1'b0}};
acks <= {CNT_W{1'b0}};
nacks <= {CNT_W{1'b0}};
end else begin
ack_valid <= 1'b0;
byte_done <= 1'b0;
if (abort) begin
// §3.1.8 obligation 2, propagated up: the byte is over. The partially
// transmitted value is left in the shift register, left-justified, because
// §3.1.3's MSB-first order means the bits that DID reach the bus are the
// high ones -- and Chapter 17.10 has to report how far it got.
busy <= 1'b0;
active <= 1'b0;
tx_en <= 1'b0;
bit_index <= 4'd0;
end else if (go && !busy) begin
busy <= 1'b1;
active <= 1'b1;
bit_index <= 4'd0;
shreg <= tx_byte;
// Slot 0: for a write the master drives the MSB; for a read it releases.
tx_en <= dir_write;
cur_bit <= dir_write ? tx_byte[7] : 1'b1;
end else if (busy) begin
// One slot completes at each sample point.
if (bit_done) begin
if (bit_index < 4'd8) begin
// A data slot. Shift in what was on the line -- which, when
// transmitting, is what WE put there, and the shift register's low
// bits are therefore a record of what actually reached the bus.
shreg <= {shreg[6:0], rx_bit};
if (bit_index == 4'd7) begin
rx_byte <= {shreg[6:0], rx_bit};
// THE FLIP. Slot 8 reverses the direction of SDA: a transmitting
// master releases so the target can answer, and a receiving
// master drives its own answer.
tx_en <= ~dir_write;
cur_bit <= dir_write ? 1'b1 : ~ack_to_send;
bit_index <= 4'd8;
end else begin
bit_index <= bit_index + 4'd1;
tx_en <= dir_write;
cur_bit <= dir_write ? shreg[6] : 1'b1;
end
end else begin
// Slot 8 has completed. When writing, the sampled value IS the
// target's answer, and a LOW means acknowledged. When reading, the
// master sent the answer, so the event is what it chose to send.
ack <= dir_write ? ~rx_bit : ack_to_send;
ack_valid <= 1'b1;
if (dir_write ? ~rx_bit : ack_to_send) acks <= acks + 1'b1;
else nacks <= nacks + 1'b1;
byte_done <= 1'b1;
bytes_done <= bytes_done + 1'b1;
busy <= 1'b0;
active <= 1'b0;
tx_en <= 1'b0;
bit_index <= 4'd0;
end
end
end
end
end
endmodule // -----------------------------------------------------------------------------
// i2c_byte_engine.sv
// Eight bits, then the ninth slot in which SDA changes hands.
//
// §3.1.4, verbatim: "The acknowledge takes place after every byte. ... The master
// generates all clock pulses, including the acknowledge ninth clock pulse." And: "the
// transmitter releases the SDA line during the acknowledge clock pulse so the receiver
// can pull the SDA line LOW and it remains stable LOW during the HIGH period of this
// clock pulse."
//
// So a byte is NINE bit slots and the ninth is different from the eight in exactly one
// respect: who owns SDA. Everything else -- the clock, the sampling instant, the data
// rule -- is identical. That is why this block wraps the bit engine rather than
// replacing it: eight slots with the shift register driving, one slot with the
// direction reversed.
//
// THE OWNERSHIP FLIP IS THE WHOLE BLOCK. Writing a byte: the master drives bits 0-7
// and RELEASES for bit 8, then reads the target's answer. Reading a byte: the master
// releases for bits 0-7 and DRIVES bit 8, sending its own ACK or NACK. The two cases
// are mirror images and the code says so, which is worth more than two separate paths
// that happen to agree.
//
// MSB FIRST. §3.1.3: "Data is transferred with the Most Significant Bit (MSB) first."
// So the shift register shifts LEFT and the bit transmitted is always bit 7, which also
// means a partially transmitted byte is left-justified in the register -- relevant to
// Chapter 17.10, where a byte abandoned to arbitration loss has to be reported.
//
// THE ACK POLARITY IS INVERTED RELATIVE TO INTUITION. The receiver pulls SDA LOW to
// acknowledge, so a sampled ZERO is an ACK and a sampled ONE is a NACK. This block
// reports `ack` as the protocol event (1 = acknowledged) and keeps the inversion in one
// place, for the same reason Chapter 17.4 keeps the SDA inversion in one place.
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_byte_engine #(
parameter CNT_W = 16
) (
input wire clk,
input wire rst_n,
// From the SCL generator, passed through to the bit engine.
input wire drive_point,
input wire sample_point,
// Command. `go` starts one nine-slot byte.
input wire go,
input wire dir_write, // 1 = master transmits the 8 bits and reads ACK
// 0 = master receives the 8 bits and sends ACK
input wire [7:0] tx_byte,
input wire ack_to_send, // when receiving: 1 = ACK (pull low), 0 = NACK
input wire sda_in,
input wire scl_high, // SCL read back, for the bit engine's release rule
input wire abort, // arbitration lost: release SDA immediately
output wire sda_req,
output wire sda_bit,
output reg [7:0] rx_byte,
output reg ack, // 1 = the byte WAS acknowledged
output reg ack_valid, // one cycle, when the ninth slot completes
output reg byte_done, // one cycle
output reg busy,
output reg [3:0] bit_index, // 0..8
output wire driving,
output reg [CNT_W-1:0] bytes_done,
output reg [CNT_W-1:0] acks,
output reg [CNT_W-1:0] nacks
);
reg [7:0] shreg;
reg active;
reg tx_en;
reg cur_bit;
wire rx_bit, rx_valid, bit_done;
i2c_bit_engine #(.CNT_W(CNT_W)) u_bit (
.clk(clk), .rst_n(rst_n),
.drive_point(drive_point), .sample_point(sample_point),
.active(active), .tx_en(tx_en), .tx_bit(cur_bit), .abort(abort),
.sda_in(sda_in), .scl_high(scl_high),
.sda_req(sda_req), .sda_bit(sda_bit),
.rx_bit(rx_bit), .rx_valid(rx_valid), .bit_done(bit_done),
.driving(driving),
.bits_driven(), .bits_sampled());
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
shreg <= 8'h00;
active <= 1'b0;
tx_en <= 1'b0;
cur_bit <= 1'b1;
rx_byte <= 8'h00;
ack <= 1'b0;
ack_valid <= 1'b0;
byte_done <= 1'b0;
busy <= 1'b0;
bit_index <= 4'd0;
bytes_done <= {CNT_W{1'b0}};
acks <= {CNT_W{1'b0}};
nacks <= {CNT_W{1'b0}};
end else begin
ack_valid <= 1'b0;
byte_done <= 1'b0;
if (abort) begin
// §3.1.8 obligation 2, propagated up: the byte is over. The partially
// transmitted value is left in the shift register, left-justified, because
// §3.1.3's MSB-first order means the bits that DID reach the bus are the
// high ones -- and Chapter 17.10 has to report how far it got.
busy <= 1'b0;
active <= 1'b0;
tx_en <= 1'b0;
bit_index <= 4'd0;
end else if (go && !busy) begin
busy <= 1'b1;
active <= 1'b1;
bit_index <= 4'd0;
shreg <= tx_byte;
// Slot 0: for a write the master drives the MSB; for a read it releases.
tx_en <= dir_write;
cur_bit <= dir_write ? tx_byte[7] : 1'b1;
end else if (busy) begin
// One slot completes at each sample point.
if (bit_done) begin
if (bit_index < 4'd8) begin
// A data slot. Shift in what was on the line -- which, when
// transmitting, is what WE put there, and the shift register's low
// bits are therefore a record of what actually reached the bus.
shreg <= {shreg[6:0], rx_bit};
if (bit_index == 4'd7) begin
rx_byte <= {shreg[6:0], rx_bit};
// THE FLIP. Slot 8 reverses the direction of SDA: a transmitting
// master releases so the target can answer, and a receiving
// master drives its own answer.
tx_en <= ~dir_write;
cur_bit <= dir_write ? 1'b1 : ~ack_to_send;
bit_index <= 4'd8;
end else begin
bit_index <= bit_index + 4'd1;
tx_en <= dir_write;
cur_bit <= dir_write ? shreg[6] : 1'b1;
end
end else begin
// Slot 8 has completed. When writing, the sampled value IS the
// target's answer, and a LOW means acknowledged. When reading, the
// master sent the answer, so the event is what it chose to send.
ack <= dir_write ? ~rx_bit : ack_to_send;
ack_valid <= 1'b1;
if (dir_write ? ~rx_bit : ack_to_send) acks <= acks + 1'b1;
else nacks <= nacks + 1'b1;
byte_done <= 1'b1;
bytes_done <= bytes_done + 1'b1;
busy <= 1'b0;
active <= 1'b0;
tx_en <= 1'b0;
bit_index <= 4'd0;
end
end
end
end
end
endmodule -- ---------------------------------------------------------------------------
-- i2c_byte_engine.vhd
-- Eight bits, then the ninth slot in which SDA changes hands.
-- Behavioural twin of i2c_byte_engine.sv / .v.
--
-- §3.1.4, verbatim: "The acknowledge takes place after every byte. ... The master generates
-- all clock pulses, including the acknowledge ninth clock pulse." And: "the transmitter
-- releases the SDA line during the acknowledge clock pulse so the receiver can pull the SDA
-- line LOW and it remains stable LOW during the HIGH period of this clock pulse."
--
-- So a byte is NINE bit slots and the ninth differs from the eight in exactly one respect:
-- who owns SDA. Everything else -- the clock, the sampling instant, the data rule -- is
-- identical, which is why this block WRAPS the bit engine rather than replacing it.
--
-- THE OWNERSHIP FLIP IS THE WHOLE BLOCK. Writing a byte: the master drives bits 0-7 and
-- RELEASES for bit 8, then reads the target's answer. Reading: the master releases for bits
-- 0-7 and DRIVES bit 8, sending its own ACK or NACK. Mirror images, and the code says so.
--
-- MSB FIRST. §3.1.3: "Data is transferred with the Most Significant Bit (MSB) first."
--
-- AND THE ACK POLARITY IS INVERTED RELATIVE TO INTUITION: the receiver pulls SDA LOW to
-- acknowledge, so a sampled ZERO is an ACK. This block reports `ack` as the protocol event
-- (1 = acknowledged) and keeps the inversion in one place.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_byte_engine is
generic (
CNT_W : integer := 16
);
port (
clk : in std_logic;
rst_n : in std_logic;
drive_point : in std_logic;
sample_point : in std_logic;
go : in std_logic;
dir_write : in std_logic; -- '1' master transmits the 8 bits and reads the ACK
tx_byte : in std_logic_vector(7 downto 0);
ack_to_send : in std_logic; -- when receiving: '1' = ACK (pull low), '0' = NACK
sda_in : in std_logic;
scl_high : in std_logic;
abort : in std_logic;
sda_req : out std_logic;
sda_bit : out std_logic;
rx_byte : out std_logic_vector(7 downto 0);
ack : out std_logic; -- '1' = the byte WAS acknowledged
ack_valid : out std_logic;
byte_done : out std_logic;
busy : out std_logic;
bit_index : out unsigned(3 downto 0);
driving : out std_logic;
bytes_done : out unsigned(CNT_W-1 downto 0);
acks : out unsigned(CNT_W-1 downto 0);
nacks : out unsigned(CNT_W-1 downto 0)
);
end entity i2c_byte_engine;
architecture rtl of i2c_byte_engine is
signal shreg : std_logic_vector(7 downto 0) := (others => '0');
signal act : std_logic := '0';
signal txen : std_logic := '0';
signal cur_bit : std_logic := '1';
signal bsy : std_logic := '0';
signal bidx : unsigned(3 downto 0) := (others => '0');
signal n_byt, n_ack, n_nak : unsigned(CNT_W-1 downto 0) := (others => '0');
signal rx_bit, rx_valid, bit_dn : std_logic;
begin
busy <= bsy;
bit_index <= bidx;
bytes_done <= n_byt;
acks <= n_ack;
nacks <= n_nak;
u_bit : entity work.i2c_bit_engine
generic map (CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
drive_point => drive_point, sample_point => sample_point,
active => act, tx_en => txen, tx_bit => cur_bit, abort => abort,
sda_in => sda_in, scl_high => scl_high,
sda_req => sda_req, sda_bit => sda_bit,
rx_bit => rx_bit, rx_valid => rx_valid, bit_done => bit_dn,
driving => driving,
bits_driven => open, bits_sampled => open);
process (clk, rst_n)
variable acked : std_logic;
begin
if rst_n = '0' then
shreg <= (others => '0');
act <= '0';
txen <= '0';
cur_bit <= '1';
rx_byte <= (others => '0');
ack <= '0';
ack_valid <= '0';
byte_done <= '0';
bsy <= '0';
bidx <= (others => '0');
n_byt <= (others => '0');
n_ack <= (others => '0');
n_nak <= (others => '0');
elsif rising_edge(clk) then
ack_valid <= '0';
byte_done <= '0';
if abort = '1' then
-- §3.1.8 obligation 2, propagated up: the byte is over. The partially
-- transmitted value is left in the shift register, left-justified, because
-- §3.1.3's MSB-first order means the bits that DID reach the bus are the high
-- ones -- and Chapter 17.10 has to report how far it got.
bsy <= '0';
act <= '0';
txen <= '0';
bidx <= (others => '0');
elsif go = '1' and bsy = '0' then
bsy <= '1';
act <= '1';
bidx <= (others => '0');
shreg <= tx_byte;
-- Slot 0: for a write the master drives the MSB; for a read it releases.
txen <= dir_write;
if dir_write = '1' then cur_bit <= tx_byte(7); else cur_bit <= '1'; end if;
elsif bsy = '1' then
-- One slot completes at each sample point.
if bit_dn = '1' then
if bidx < 8 then
-- A data slot. Shift in what was on the line -- which, when transmitting,
-- is what WE put there, so the shift register's low bits are a record of
-- what actually reached the bus.
shreg <= shreg(6 downto 0) & rx_bit;
if bidx = 7 then
rx_byte <= shreg(6 downto 0) & rx_bit;
-- THE FLIP. Slot 8 reverses the direction of SDA.
txen <= not dir_write;
if dir_write = '1' then cur_bit <= '1';
else cur_bit <= not ack_to_send;
end if;
bidx <= to_unsigned(8, 4);
else
bidx <= bidx + 1;
txen <= dir_write;
if dir_write = '1' then cur_bit <= shreg(6); else cur_bit <= '1'; end if;
end if;
else
-- Slot 8 has completed. When writing, the sampled value IS the target's
-- answer and a LOW means acknowledged. When reading, the master sent the
-- answer, so the event is what it chose to send.
if dir_write = '1' then acked := not rx_bit; else acked := ack_to_send; end if;
ack <= acked;
ack_valid <= '1';
if acked = '1' then n_ack <= n_ack + 1; else n_nak <= n_nak + 1; end if;
byte_done <= '1';
n_byt <= n_byt + 1;
bsy <= '0';
act <= '0';
txen <= '0';
bidx <= (others => '0');
end if;
end if;
end if;
end if;
end process;
end architecture rtl;6a. The testbenches
Thirteen checks. The bench instantiates a real target model that finds its own edges from the wires, rather than a responder told when to reply. That distinction matters: a target that is scripted to acknowledge at slot 8 will acknowledge even if the master's ninth pulse never happens, and mutation M1 below removes exactly that pulse.
| # | Test | Property |
|---|---|---|
| T1 | an address byte, acknowledged by a real target | which found its own edges |
| T2 | nine slots, not eight | §3.1.4's ninth clock pulse |
| T3 | a data byte written and read back out of the target's register | |
| T4 | the ownership flip, writing | master drives 0–7, releases slot 8 |
| T5 | a read | master releases 0–7, drives slot 8 |
| T6 | the mirror image, checked in the ninth slot | |
| T7 | ACK polarity is inverted | a sampled zero is an acknowledge |
| T8 | a NACK from the master ends the read, and the target lets go | |
| T9 | a NACK from the target on a wrong address | |
| T10 | MSB first | an asymmetric byte proves order; 0x80 proves the top bit |
| T11 | the data-valid rule held throughout | over every byte so far |
| T12 | a complete transaction, framed | START, address, data, STOP |
| T13 | the engine let go of SDA between bytes | added after M10; see §7 |
`timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_byte_engine_tb.sv
// Independent oracle for i2c_byte_engine, integrated against a pin-level target.
//
// This bench assembles everything built so far -- the SCL generator, the SDA owner and
// inverter, the framer, the bit engine and the byte engine -- and puts a
// i2c_target_model on the other end of a wired-AND bus. The target is driven by nothing
// but the two wires: it finds its own edges, samples where §3.1.2 permits sampling and
// drives where §3.1.2 permits driving.
//
// That is the point. A byte-level model handed "a byte arrived" cannot be wrong about
// WHEN, and so cannot catch a master that is right about every byte and wrong about
// every instant. This one can, and it does not know what the master's states are called.
//
// It is also where the SCL HANDOVER is exercised for the first time. Three blocks want
// to drive SCL at different moments -- the framer during START and STOP, the generator
// during bytes -- and the bench's `own_scl` task performs the overlapping handoff that
// Chapter 17.12 builds into the master.
// -----------------------------------------------------------------------------
module i2c_byte_engine_tb;
localparam integer NL = 9, NH = 5, NSU = 3, NSMP = 2;
localparam integer NHD = 4, NSUA = 4, NSUO = 4, NBUF = 4;
localparam [6:0] TADDR = 7'h50;
logic clk = 1'b0, rst_n = 1'b0;
// ---- the master's pieces ------------------------------------------------
logic gen_idle_low = 1'b0, scl_yield = 1'b0;
logic do_start = 1'b0, do_restart = 1'b0, do_stop = 1'b0;
logic go = 1'b0, dir_write = 1'b1, ack_to_send = 1'b1;
logic [7:0] tx_byte = 8'h00;
logic g_scl_low, drive_point, sample_point, g_rise, g_fall, g_stretch;
logic [15:0] g_stretch_cyc, g_bits;
logic [1:0] g_phase;
logic f_sda_req, f_sda_bit, f_scl_low, f_busy, f_done, f_bus_free, f_started, f_sw;
logic [15:0] n_sta, n_rs, n_sto;
logic [3:0] f_state;
logic b_sda_req, b_sda_bit, b_driving;
logic [7:0] rx_byte;
logic ack, ack_valid, byte_done, b_busy;
logic [3:0] bit_index;
logic [15:0] bytes_done, n_acks, n_nacks;
wire [3:0] req = {2'b00, b_sda_req, f_sda_req};
wire [3:0] bit_val = {2'b00, b_sda_bit, f_sda_bit};
logic m_sda_low, sda_owned, sda_tx, arb_now, arb_lost, sda_conflict;
logic [3:0] grant;
logic [15:0] n_conflicts, n_arb;
// THE CLOCK RUNS ONLY WHILE A BYTE IS IN FLIGHT. A generator left enabled between
// bytes keeps pulsing SCL, and every pulse is a bit slot the target counts and the
// master does not -- so the two fall out of step and the next byte read comes back
// as all ones. So the enable is derived from the byte engine's own activity, which
// is what the controller of Chapter 17.12 does.
wire gen_enable = scl_yield && (b_busy || go);
// The master's SCL contribution: the framer and the generator, wired-AND locally.
wire m_scl_low = f_scl_low | g_scl_low;
// ---- the bus and the target --------------------------------------------
logic t_scl_low, t_sda_low;
logic scl, sda;
logic [1:0] scl_in, sda_in, scl_rbl, sda_rbl;
logic [7:0] scl_h, sda_h;
logic load_en = 1'b0;
logic [7:0] load_addr = 8'h00, load_data = 8'h00;
i2c_line_model #(.N_DEV(2)) bus (
.scl_drive_low({t_scl_low, m_scl_low}),
.sda_drive_low({t_sda_low, m_sda_low}),
.scl(scl), .sda(sda), .scl_in(scl_in), .sda_in(sda_in),
.scl_released_but_low(scl_rbl), .sda_released_but_low(sda_rbl),
.scl_holders(scl_h), .sda_holders(sda_h));
i2c_scl_gen #(.N_LOW(NL), .N_HIGH(NH), .N_SU(NSU), .N_SAMP(NSMP), .CNT_W(16)) u_scl (
.clk(clk), .rst_n(rst_n), .enable(gen_enable), .idle_low(gen_idle_low),
.scl_in(scl_in[0]), .scl_drive_low(g_scl_low),
.drive_point(drive_point), .sample_point(sample_point),
.scl_rising(g_rise), .scl_falling(g_fall),
.stretching(g_stretch), .stretch_cycles(g_stretch_cyc),
.bits_generated(g_bits), .phase(g_phase));
i2c_framer #(.N_HD_STA(NHD), .N_SU_STA(NSUA), .N_SU_STO(NSUO),
.N_BUF(NBUF), .N_SU_DAT(NSU), .CNT_W(16)) u_fr (
.clk(clk), .rst_n(rst_n),
.do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
.scl_in(scl_in[0]), .sda_in(sda_in[0]), .scl_yield(scl_yield),
.sda_req(f_sda_req), .sda_bit(f_sda_bit), .scl_drive_low(f_scl_low),
.busy(f_busy), .done(f_done), .bus_free(f_bus_free), .started(f_started),
.stretch_wait(f_sw), .starts(n_sta), .restarts(n_rs), .stops(n_sto),
.state(f_state));
i2c_byte_engine #(.CNT_W(16)) dut (
.clk(clk), .rst_n(rst_n),
.drive_point(drive_point), .sample_point(sample_point),
.go(go), .dir_write(dir_write), .tx_byte(tx_byte), .ack_to_send(ack_to_send),
.sda_in(sda_in[0]), .scl_high(scl_in[0]), .abort(1'b0),
.sda_req(b_sda_req), .sda_bit(b_sda_bit),
.rx_byte(rx_byte), .ack(ack), .ack_valid(ack_valid), .byte_done(byte_done),
.busy(b_busy), .bit_index(bit_index), .driving(b_driving),
.bytes_done(bytes_done), .acks(n_acks), .nacks(n_nacks));
i2c_sda_ctrl #(.N_OWNER(4), .CNT_W(16)) u_sda (
.clk(clk), .rst_n(rst_n), .req(req), .bit_val(bit_val),
.sda_in(sda_in[0]), .scl_in(scl_in[0]), .tx_active(b_driving),
.sda_drive_low(m_sda_low),
.grant(grant), .owned(sda_owned), .tx_bit(sda_tx),
.owner_conflict(sda_conflict), .conflicts(n_conflicts),
.arb_loss_now(arb_now), .arb_lost(arb_lost), .arb_losses(n_arb),
.arb_clear(1'b0));
logic t_sel, t_dirrd, t_wvalid;
logic [7:0] t_lastwr;
logic [15:0] t_rx, t_tx, t_nsta, t_nsto;
logic [2:0] t_state;
i2c_target_model #(.MY_ADDR(TADDR), .ACK_ADDR(1'b1), .STRETCH_AFTER(0),
.NACK_AT(0), .N_MEM(16), .CNT_W(16)) u_tgt (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.scl_drive_low(t_scl_low), .sda_drive_low(t_sda_low),
.load_en(load_en), .load_addr(load_addr), .load_data(load_data),
.selected(t_sel), .dir_read(t_dirrd), .last_written(t_lastwr),
.write_valid(t_wvalid), .bytes_rx(t_rx), .bytes_tx(t_tx),
.n_starts(t_nsta), .n_stops(t_nsto), .state(t_state));
// ---- the oracle ---------------------------------------------------------
logic m_start, m_stop, m_bitv, m_byte, m_ackv, m_intr, m_mid, m_bit, m_ack;
logic [7:0] m_byteval;
logic [3:0] m_bidx;
logic [15:0] m_nsta, m_nsto, m_nbyte, m_nmid;
i2c_proto_mon #(.CNT_W(16)) mon (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.start_seen(m_start), .stop_seen(m_stop),
.bit_seen(m_bit), .bit_val(m_bitv),
.byte_seen(m_byte), .byte_val(m_byteval),
.ack_seen(m_ack), .ack_val(m_ackv),
.in_transfer(m_intr), .framing_midbyte(m_mid), .bit_index(m_bidx),
.n_starts(m_nsta), .n_stops(m_nsto), .n_bytes(m_nbyte), .n_midbyte(m_nmid));
always #5 clk = ~clk;
integer errors = 0;
integer n, k;
logic [7:0] got;
// §3.1.2 watchdog: SDA may only change while SCL is low, EXCEPT at framing.
integer bad_sda_change;
logic scl_l, sda_l;
always @(negedge clk) begin
if (rst_n) begin
if (scl && scl_l && (sda != sda_l) && !m_start && !m_stop) begin
// A change while SCL is high that the monitor is not about to call framing
// is impossible -- so this counts the ones that are not at a byte boundary
// either, which is what a corrupted bit would look like.
if (m_bidx != 4'd0) bad_sda_change = bad_sda_change + 1;
end
scl_l = scl; sda_l = sda;
end
end
task step; begin @(posedge clk); @(negedge clk); end endtask
task do_reset;
begin
@(negedge clk);
rst_n = 1'b0;
gen_idle_low = 1'b0; scl_yield = 1'b0;
do_start = 1'b0; do_restart = 1'b0; do_stop = 1'b0;
go = 1'b0; dir_write = 1'b1; ack_to_send = 1'b1; tx_byte = 8'h00;
load_en = 1'b0;
bad_sda_change = 0; scl_l = 1'b1; sda_l = 1'b1;
repeat (3) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
step;
end
endtask
task preload (input [7:0] a, input [7:0] d);
begin
@(negedge clk); load_en = 1'b1; load_addr = a; load_data = d;
@(posedge clk); @(negedge clk); load_en = 1'b0;
end
endtask
task wait_frame (input integer max_cycles);
begin
n = 0;
while (!f_done && n < max_cycles) begin step; n = n + 1; end
if (n >= max_cycles) begin
$display(" FAIL wait_frame: framer stuck in state %0d", f_state);
errors = errors + 1;
end
end
endtask
// THE HANDOVER, in both directions. Asserting `scl_yield` and `gen_enable` in the
// same cycle means the framer releases SCL and the generator drives it low at the
// same clock edge, so the line never rises in between. Reversing it takes the clock
// back while the generator is still parked low, so again no gap appears.
task clock_to_generator;
begin @(negedge clk); scl_yield = 1'b1; gen_idle_low = 1'b1; end
endtask
task clock_to_framer;
begin
// The generator is already parked low between bytes, so taking the clock back
// is just a matter of the framer picking up the hold before the generator lets
// go -- in that order, so the line never rises in between.
n = 0;
while (g_phase != 2'd0 && n < 200) begin step; n = n + 1; end
@(negedge clk); scl_yield = 1'b0; // the framer holds SCL low now
step;
@(negedge clk); gen_idle_low = 1'b0; // and the generator lets go
end
endtask
// Advance to the ninth slot AND past its drive point, which is where ownership has
// actually changed hands. Checking at the instant bit_index becomes 8 is too early:
// the flip happens at the drive point inside slot 8's low phase, and until then
// `driving` still reflects slot 7.
task to_ack_slot;
begin
n = 0;
while (bit_index != 4'd8 && n < 800) begin step; n = n + 1; end
n = 0;
while (!drive_point && n < 800) begin step; n = n + 1; end
step;
end
endtask
task send_byte (input [7:0] b);
begin
@(negedge clk); tx_byte = b; dir_write = 1'b1; go = 1'b1;
@(posedge clk); @(negedge clk); go = 1'b0;
n = 0;
while (!byte_done && n < 800) begin step; n = n + 1; end
if (n >= 800) begin
$display(" FAIL send_byte(%02h): never completed (bit %0d)", b, bit_index);
errors = errors + 1;
end
end
endtask
task recv_byte (input do_ack);
begin
@(negedge clk); dir_write = 1'b0; ack_to_send = do_ack; go = 1'b1;
@(posedge clk); @(negedge clk); go = 1'b0;
n = 0;
while (!byte_done && n < 800) begin step; n = n + 1; end
if (n >= 800) begin
$display(" FAIL recv_byte: never completed (bit %0d)", bit_index);
errors = errors + 1;
end
got = rx_byte;
end
endtask
task ck_int (input [200*8:1] what, input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0d (0x%0h) expected %0d (0x%0h)", what, g, g, e, e);
errors = errors + 1;
end
end
endtask
task ck_bit (input [200*8:1] what, input g, input e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0b expected %0b", what, g, e);
errors = errors + 1;
end
end
endtask
// ---- the engine must let go of SDA between bytes ------------------------
//
// A byte engine that finishes a byte and keeps requesting SDA holds the line into
// the gap before the next byte. That is invisible in the per-byte checks: the next
// byte's `go` overwrites the request, the gap sits inside the SCL low phase, and if
// the held value is low it even helps a STOP that may follow. But the master is
// driving a line it no longer owns, and the next block that wants SDA -- the framer,
// for a repeated START -- collides with it.
//
// The window has to be stated carefully, because the bit engine's DEFERRED RELEASE
// is correct and required: after deactivation it keeps holding SDA until SCL is low,
// so that releasing cannot make the line rise during the high phase and manufacture
// a STOP. So `sda_req` staying high immediately after `byte_done` is not a defect.
//
// The real invariant is: once the byte is done AND SCL has actually been observed
// low, the request must be gone.
integer held_after_byte = 0;
reg between_bytes = 1'b0;
reg seen_low = 1'b0;
always @(posedge clk) begin
if (!rst_n) begin
held_after_byte <= 0; between_bytes <= 1'b0; seen_low <= 1'b0;
end else begin
if (byte_done) begin between_bytes <= 1'b1; seen_low <= 1'b0; end
else if (go) between_bytes <= 1'b0;
if (between_bytes && !scl_in[0]) seen_low <= 1'b1;
if (between_bytes && seen_low && !go && b_sda_req)
held_after_byte <= held_after_byte + 1;
end
end
initial begin
$display("=== i2c_byte_engine: eight bits, then the slot where SDA changes hands ===");
// ----------------------------------------------------------------
// T1. An address byte, acknowledged by a real target that found its own edges.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
send_byte({TADDR, 1'b0});
$display("T1 an address byte, acknowledged by a pin-level target");
ck_bit("T1 acknowledged", ack, 1'b1);
ck_bit("T1 the target selected itself", t_sel, 1'b1);
ck_int("T1 the monitor saw one byte", m_nbyte, 1);
ck_int("T1 and it was the address with the write bit", m_byteval, {TADDR, 1'b0});
// Eight, not nine: the generator counts a bit when its HIGH phase ends, and the
// byte completes at the SAMPLE point inside the ninth high phase. So at the
// instant the byte is done, the ninth pulse is still in progress.
ck_int("T1 eight pulses completed, with the ninth still in flight", g_bits, 8);
// ----------------------------------------------------------------
// T2. NINE SLOTS, NOT EIGHT. §3.1.4: "The master generates all clock pulses,
// including the acknowledge ninth clock pulse." A byte costs nine.
// ----------------------------------------------------------------
$display("T2 a byte costs nine clock pulses, not eight");
ck_int("T2 the bit index returned to zero", bit_index, 0);
ck_int("T2 one byte completed", bytes_done, 1);
// ----------------------------------------------------------------
// T3. A data byte, written and read back out of the target's own register.
// ----------------------------------------------------------------
send_byte(8'h5A);
$display("T3 a data byte reaches the target and is acknowledged");
ck_bit("T3 acknowledged", ack, 1'b1);
ck_int("T3 the target received it", t_lastwr, 8'h5A);
ck_int("T3 and counted it", t_rx, 1);
ck_int("T3 the monitor agrees", m_byteval, 8'h5A);
// ----------------------------------------------------------------
// T4. THE OWNERSHIP FLIP, when writing. The master drives slots 0-7 and RELEASES
// for slot 8 so the target can answer. §3.1.4, and the test looks at the
// master's own driving signal in the ninth slot.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
@(negedge clk); tx_byte = {TADDR, 1'b0}; dir_write = 1'b1; go = 1'b1;
@(posedge clk); @(negedge clk); go = 1'b0;
to_ack_slot;
$display("T4 writing: the master releases SDA for the ninth slot");
ck_int("T4 in the ninth slot", bit_index, 8);
ck_bit("T4 the master is no longer driving", b_driving, 1'b0);
n = 0;
while (!byte_done && n < 800) begin step; n = n + 1; end
ck_bit("T4 and the target's ACK was read", ack, 1'b1);
// ----------------------------------------------------------------
// T5. A READ. The master releases slots 0-7 and DRIVES slot 8 -- the mirror
// image. The byte comes from the target's memory.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'hC3);
preload(8'h01, 8'h7E);
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
send_byte({TADDR, 1'b1}); // address with the read bit
ck_bit("T5 the read address was acknowledged", ack, 1'b1);
ck_bit("T5 the target knows it is a read", t_dirrd, 1'b1);
recv_byte(1'b1); // ACK: send me another
$display("T5 reading: the master releases the data slots and drives the ninth");
ck_int("T5 the first byte came from the target", got, 8'hC3);
ck_int("T5 the target counted a transmit", t_tx, 1);
// ----------------------------------------------------------------
// T6. THE MIRROR IMAGE, checked in the ninth slot. When reading, the master
// drives slot 8 with its own answer.
// ----------------------------------------------------------------
@(negedge clk); dir_write = 1'b0; ack_to_send = 1'b1; go = 1'b1;
@(posedge clk); @(negedge clk); go = 1'b0;
to_ack_slot;
$display("T6 reading: the master drives the ninth slot with its own answer");
ck_int("T6 in the ninth slot", bit_index, 8);
ck_bit("T6 and now the master IS driving", b_driving, 1'b1);
n = 0;
while (!byte_done && n < 800) begin step; n = n + 1; end
ck_int("T6 the second byte arrived", rx_byte, 8'h7E);
// ----------------------------------------------------------------
// T7. ACK POLARITY IS INVERTED. The receiver pulls SDA LOW to acknowledge, so
// the monitor -- which reads the line -- sees a ZERO for an ACK.
// ----------------------------------------------------------------
$display("T7 an acknowledge is a LOW on the wire, the inverse of intuition");
ck_bit("T7 the wire carried a zero in the ack slot", m_ackv, 1'b0);
ck_bit("T7 and the engine reports it as acknowledged", ack, 1'b1);
// ----------------------------------------------------------------
// T8. A NACK from the master ends the read, and the target lets go.
// ----------------------------------------------------------------
recv_byte(1'b0); // NACK: I am finished
$display("T8 the master's NACK ends the read and the target releases SDA");
ck_bit("T8 the engine reports a NACK", ack, 1'b0);
ck_bit("T8 the wire carried a one in the ack slot", m_ackv, 1'b1);
ck_bit("T8 the target has released SDA", t_sda_low, 1'b0);
ck_int("T8 the NACK was counted", n_nacks, 1);
// ----------------------------------------------------------------
// T9. A NACK from the TARGET, on a wrong address. The address is not
// acknowledged, and the engine must report that rather than proceeding.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
send_byte({7'h51, 1'b0}); // not our target
$display("T9 an unaddressed target does not answer, and the engine says so");
ck_bit("T9 not acknowledged", ack, 1'b0);
ck_bit("T9 the target did not select itself", t_sel, 1'b0);
ck_int("T9 the monitor saw the byte all the same", m_nbyte, 1);
ck_bit("T9 and the ack slot carried a one", m_ackv, 1'b1);
// ----------------------------------------------------------------
// T10. MSB FIRST. §3.1.3. An asymmetric byte proves the order, and 0x80 proves
// it in the strongest way: only the first bit is set.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
send_byte({TADDR, 1'b0});
send_byte(8'h80);
$display("T10 bits go out most significant first");
ck_int("T10 the target received 0x80", t_lastwr, 8'h80);
ck_int("T10 and the monitor read 0x80 off the wire", m_byteval, 8'h80);
send_byte(8'h01);
ck_int("T10 and 0x01 the other way round", t_lastwr, 8'h01);
// ----------------------------------------------------------------
// T11. THE DATA-VALID RULE HELD THROUGHOUT. Over every byte so far, SDA changed
// while SCL was high only at framing -- never inside a byte.
// ----------------------------------------------------------------
$display("T11 SDA never changed inside a byte while SCL was high");
ck_int("T11 no mid-byte changes", bad_sda_change, 0);
ck_int("T11 no owner conflicts", n_conflicts, 0);
ck_int("T11 and no spurious arbitration losses", n_arb, 0);
// ----------------------------------------------------------------
// T12. A COMPLETE TRANSACTION, framed. START, address, data, STOP -- with the
// clock handed from the framer to the generator and back again.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
send_byte({TADDR, 1'b0});
send_byte(8'h42);
clock_to_framer;
@(negedge clk); do_stop = 1'b1; @(posedge clk); @(negedge clk); do_stop = 1'b0;
wait_frame(600);
$display("T12 a whole transaction, with the clock handed over and handed back");
ck_int("T12 one START", m_nsta, 1);
ck_int("T12 one STOP", m_nsto, 1);
ck_int("T12 two bytes", m_nbyte, 2);
ck_int("T12 no mid-byte framing", m_nmid, 0);
ck_bit("T12 the transfer is closed", m_intr, 1'b0);
ck_int("T12 the target saw one START and one STOP", t_nsta + t_nsto, 2);
ck_int("T12 and received the data byte", t_lastwr, 8'h42);
ck_bit("T12 both lines released at the end", m_scl_low | m_sda_low, 1'b0);
ck_int("T13 the engine let go of SDA between bytes", held_after_byte, 0);
if (errors == 0)
$display("=== i2c_byte_engine: ALL CHECKS PASSED ===");
else
$display("=== i2c_byte_engine: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule `timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_byte_engine_tb.sv
// Independent oracle for i2c_byte_engine, integrated against a pin-level target.
//
// This bench assembles everything built so far -- the SCL generator, the SDA owner and
// inverter, the framer, the bit engine and the byte engine -- and puts a
// i2c_target_model on the other end of a wired-AND bus. The target is driven by nothing
// but the two wires: it finds its own edges, samples where §3.1.2 permits sampling and
// drives where §3.1.2 permits driving.
//
// That is the point. A byte-level model handed "a byte arrived" cannot be wrong about
// WHEN, and so cannot catch a master that is right about every byte and wrong about
// every instant. This one can, and it does not know what the master's states are called.
//
// It is also where the SCL HANDOVER is exercised for the first time. Three blocks want
// to drive SCL at different moments -- the framer during START and STOP, the generator
// during bytes -- and the bench's `own_scl` task performs the overlapping handoff that
// Chapter 17.12 builds into the master.
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_byte_engine_tb;
localparam integer NL = 9, NH = 5, NSU = 3, NSMP = 2;
localparam integer NHD = 4, NSUA = 4, NSUO = 4, NBUF = 4;
localparam [6:0] TADDR = 7'h50;
reg clk = 1'b0, rst_n = 1'b0;
// ---- the master's pieces ------------------------------------------------
reg gen_idle_low = 1'b0, scl_yield = 1'b0;
reg do_start = 1'b0, do_restart = 1'b0, do_stop = 1'b0;
reg go = 1'b0, dir_write = 1'b1, ack_to_send = 1'b1;
reg [7:0] tx_byte = 8'h00;
wire g_scl_low, drive_point, sample_point, g_rise, g_fall, g_stretch;
wire [15:0] g_stretch_cyc, g_bits;
wire [1:0] g_phase;
wire f_sda_req, f_sda_bit, f_scl_low, f_busy, f_done, f_bus_free, f_started, f_sw;
wire [15:0] n_sta, n_rs, n_sto;
wire [3:0] f_state;
wire b_sda_req, b_sda_bit, b_driving;
wire [7:0] rx_byte;
wire ack, ack_valid, byte_done, b_busy;
wire [3:0] bit_index;
wire [15:0] bytes_done, n_acks, n_nacks;
wire [3:0] req = {2'b00, b_sda_req, f_sda_req};
wire [3:0] bit_val = {2'b00, b_sda_bit, f_sda_bit};
wire m_sda_low, sda_owned, sda_tx, arb_now, arb_lost, sda_conflict;
wire [3:0] grant;
wire [15:0] n_conflicts, n_arb;
// THE CLOCK RUNS ONLY WHILE A BYTE IS IN FLIGHT. A generator left enabled between
// bytes keeps pulsing SCL, and every pulse is a bit slot the target counts and the
// master does not -- so the two fall out of step and the next byte read comes back
// as all ones. So the enable is derived from the byte engine's own activity, which
// is what the controller of Chapter 17.12 does.
wire gen_enable = scl_yield && (b_busy || go);
// The master's SCL contribution: the framer and the generator, wired-AND locally.
wire m_scl_low = f_scl_low | g_scl_low;
// ---- the bus and the target --------------------------------------------
wire t_scl_low, t_sda_low;
wire scl, sda;
wire [1:0] scl_in, sda_in, scl_rbl, sda_rbl;
wire [7:0] scl_h, sda_h;
reg load_en = 1'b0;
reg [7:0] load_addr = 8'h00, load_data = 8'h00;
i2c_line_model #(.N_DEV(2)) bus (
.scl_drive_low({t_scl_low, m_scl_low}),
.sda_drive_low({t_sda_low, m_sda_low}),
.scl(scl), .sda(sda), .scl_in(scl_in), .sda_in(sda_in),
.scl_released_but_low(scl_rbl), .sda_released_but_low(sda_rbl),
.scl_holders(scl_h), .sda_holders(sda_h));
i2c_scl_gen #(.N_LOW(NL), .N_HIGH(NH), .N_SU(NSU), .N_SAMP(NSMP), .CNT_W(16)) u_scl (
.clk(clk), .rst_n(rst_n), .enable(gen_enable), .idle_low(gen_idle_low),
.scl_in(scl_in[0]), .scl_drive_low(g_scl_low),
.drive_point(drive_point), .sample_point(sample_point),
.scl_rising(g_rise), .scl_falling(g_fall),
.stretching(g_stretch), .stretch_cycles(g_stretch_cyc),
.bits_generated(g_bits), .phase(g_phase));
i2c_framer #(.N_HD_STA(NHD), .N_SU_STA(NSUA), .N_SU_STO(NSUO),
.N_BUF(NBUF), .N_SU_DAT(NSU), .CNT_W(16)) u_fr (
.clk(clk), .rst_n(rst_n),
.do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
.scl_in(scl_in[0]), .sda_in(sda_in[0]), .scl_yield(scl_yield),
.sda_req(f_sda_req), .sda_bit(f_sda_bit), .scl_drive_low(f_scl_low),
.busy(f_busy), .done(f_done), .bus_free(f_bus_free), .started(f_started),
.stretch_wait(f_sw), .starts(n_sta), .restarts(n_rs), .stops(n_sto),
.state(f_state));
i2c_byte_engine #(.CNT_W(16)) dut (
.clk(clk), .rst_n(rst_n),
.drive_point(drive_point), .sample_point(sample_point),
.go(go), .dir_write(dir_write), .tx_byte(tx_byte), .ack_to_send(ack_to_send),
.sda_in(sda_in[0]), .scl_high(scl_in[0]), .abort(1'b0),
.sda_req(b_sda_req), .sda_bit(b_sda_bit),
.rx_byte(rx_byte), .ack(ack), .ack_valid(ack_valid), .byte_done(byte_done),
.busy(b_busy), .bit_index(bit_index), .driving(b_driving),
.bytes_done(bytes_done), .acks(n_acks), .nacks(n_nacks));
i2c_sda_ctrl #(.N_OWNER(4), .CNT_W(16)) u_sda (
.clk(clk), .rst_n(rst_n), .req(req), .bit_val(bit_val),
.sda_in(sda_in[0]), .scl_in(scl_in[0]), .tx_active(b_driving),
.sda_drive_low(m_sda_low),
.grant(grant), .owned(sda_owned), .tx_bit(sda_tx),
.owner_conflict(sda_conflict), .conflicts(n_conflicts),
.arb_loss_now(arb_now), .arb_lost(arb_lost), .arb_losses(n_arb),
.arb_clear(1'b0));
wire t_sel, t_dirrd, t_wvalid;
wire [7:0] t_lastwr;
wire [15:0] t_rx, t_tx, t_nsta, t_nsto;
wire [2:0] t_state;
i2c_target_model #(.MY_ADDR(TADDR), .ACK_ADDR(1'b1), .STRETCH_AFTER(0),
.NACK_AT(0), .N_MEM(16), .CNT_W(16)) u_tgt (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.scl_drive_low(t_scl_low), .sda_drive_low(t_sda_low),
.load_en(load_en), .load_addr(load_addr), .load_data(load_data),
.selected(t_sel), .dir_read(t_dirrd), .last_written(t_lastwr),
.write_valid(t_wvalid), .bytes_rx(t_rx), .bytes_tx(t_tx),
.n_starts(t_nsta), .n_stops(t_nsto), .state(t_state));
// ---- the oracle ---------------------------------------------------------
wire m_start, m_stop, m_bitv, m_byte, m_ackv, m_intr, m_mid, m_bit, m_ack;
wire [7:0] m_byteval;
wire [3:0] m_bidx;
wire [15:0] m_nsta, m_nsto, m_nbyte, m_nmid;
i2c_proto_mon #(.CNT_W(16)) mon (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.start_seen(m_start), .stop_seen(m_stop),
.bit_seen(m_bit), .bit_val(m_bitv),
.byte_seen(m_byte), .byte_val(m_byteval),
.ack_seen(m_ack), .ack_val(m_ackv),
.in_transfer(m_intr), .framing_midbyte(m_mid), .bit_index(m_bidx),
.n_starts(m_nsta), .n_stops(m_nsto), .n_bytes(m_nbyte), .n_midbyte(m_nmid));
always #5 clk = ~clk;
integer errors = 0;
integer n, k;
reg [7:0] got;
// §3.1.2 watchdog: SDA may only change while SCL is low, EXCEPT at framing.
integer bad_sda_change;
reg scl_l, sda_l;
always @(negedge clk) begin
if (rst_n) begin
if (scl && scl_l && (sda != sda_l) && !m_start && !m_stop) begin
// A change while SCL is high that the monitor is not about to call framing
// is impossible -- so this counts the ones that are not at a byte boundary
// either, which is what a corrupted bit would look like.
if (m_bidx != 4'd0) bad_sda_change = bad_sda_change + 1;
end
scl_l = scl; sda_l = sda;
end
end
task step; begin @(posedge clk); @(negedge clk); end endtask
task do_reset;
begin
@(negedge clk);
rst_n = 1'b0;
gen_idle_low = 1'b0; scl_yield = 1'b0;
do_start = 1'b0; do_restart = 1'b0; do_stop = 1'b0;
go = 1'b0; dir_write = 1'b1; ack_to_send = 1'b1; tx_byte = 8'h00;
load_en = 1'b0;
bad_sda_change = 0; scl_l = 1'b1; sda_l = 1'b1;
repeat (3) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
step;
end
endtask
task preload (input [7:0] a, input [7:0] d);
begin
@(negedge clk); load_en = 1'b1; load_addr = a; load_data = d;
@(posedge clk); @(negedge clk); load_en = 1'b0;
end
endtask
task wait_frame (input integer max_cycles);
begin
n = 0;
while (!f_done && n < max_cycles) begin step; n = n + 1; end
if (n >= max_cycles) begin
$display(" FAIL wait_frame: framer stuck in state %0d", f_state);
errors = errors + 1;
end
end
endtask
// THE HANDOVER, in both directions. Asserting `scl_yield` and `gen_enable` in the
// same cycle means the framer releases SCL and the generator drives it low at the
// same clock edge, so the line never rises in between. Reversing it takes the clock
// back while the generator is still parked low, so again no gap appears.
task clock_to_generator;
begin @(negedge clk); scl_yield = 1'b1; gen_idle_low = 1'b1; end
endtask
task clock_to_framer;
begin
// The generator is already parked low between bytes, so taking the clock back
// is just a matter of the framer picking up the hold before the generator lets
// go -- in that order, so the line never rises in between.
n = 0;
while (g_phase != 2'd0 && n < 200) begin step; n = n + 1; end
@(negedge clk); scl_yield = 1'b0; // the framer holds SCL low now
step;
@(negedge clk); gen_idle_low = 1'b0; // and the generator lets go
end
endtask
// Advance to the ninth slot AND past its drive point, which is where ownership has
// actually changed hands. Checking at the instant bit_index becomes 8 is too early:
// the flip happens at the drive point inside slot 8's low phase, and until then
// `driving` still reflects slot 7.
task to_ack_slot;
begin
n = 0;
while (bit_index != 4'd8 && n < 800) begin step; n = n + 1; end
n = 0;
while (!drive_point && n < 800) begin step; n = n + 1; end
step;
end
endtask
task send_byte (input [7:0] b);
begin
@(negedge clk); tx_byte = b; dir_write = 1'b1; go = 1'b1;
@(posedge clk); @(negedge clk); go = 1'b0;
n = 0;
while (!byte_done && n < 800) begin step; n = n + 1; end
if (n >= 800) begin
$display(" FAIL send_byte(%02h): never completed (bit %0d)", b, bit_index);
errors = errors + 1;
end
end
endtask
task recv_byte (input do_ack);
begin
@(negedge clk); dir_write = 1'b0; ack_to_send = do_ack; go = 1'b1;
@(posedge clk); @(negedge clk); go = 1'b0;
n = 0;
while (!byte_done && n < 800) begin step; n = n + 1; end
if (n >= 800) begin
$display(" FAIL recv_byte: never completed (bit %0d)", bit_index);
errors = errors + 1;
end
got = rx_byte;
end
endtask
task ck_int (input [200*8:1] what, input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0d (0x%0h) expected %0d (0x%0h)", what, g, g, e, e);
errors = errors + 1;
end
end
endtask
task ck_bit (input [200*8:1] what, input g, input e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0b expected %0b", what, g, e);
errors = errors + 1;
end
end
endtask
// ---- the engine must let go of SDA between bytes ------------------------
//
// A byte engine that finishes a byte and keeps requesting SDA holds the line into
// the gap before the next byte. That is invisible in the per-byte checks: the next
// byte's `go` overwrites the request, the gap sits inside the SCL low phase, and if
// the held value is low it even helps a STOP that may follow. But the master is
// driving a line it no longer owns, and the next block that wants SDA -- the framer,
// for a repeated START -- collides with it.
//
// The window has to be stated carefully, because the bit engine's DEFERRED RELEASE
// is correct and required: after deactivation it keeps holding SDA until SCL is low,
// so that releasing cannot make the line rise during the high phase and manufacture
// a STOP. So `sda_req` staying high immediately after `byte_done` is not a defect.
//
// The real invariant is: once the byte is done AND SCL has actually been observed
// low, the request must be gone.
integer held_after_byte;
reg between_bytes;
reg seen_low;
always @(posedge clk) begin
if (!rst_n) begin
held_after_byte <= 0; between_bytes <= 1'b0; seen_low <= 1'b0;
end else begin
if (byte_done) begin between_bytes <= 1'b1; seen_low <= 1'b0; end
else if (go) between_bytes <= 1'b0;
if (between_bytes && !scl_in[0]) seen_low <= 1'b1;
if (between_bytes && seen_low && !go && b_sda_req)
held_after_byte <= held_after_byte + 1;
end
end
initial begin
$display("=== i2c_byte_engine: eight bits, then the slot where SDA changes hands ===");
// ----------------------------------------------------------------
// T1. An address byte, acknowledged by a real target that found its own edges.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
send_byte({TADDR, 1'b0});
$display("T1 an address byte, acknowledged by a pin-level target");
ck_bit("T1 acknowledged", ack, 1'b1);
ck_bit("T1 the target selected itself", t_sel, 1'b1);
ck_int("T1 the monitor saw one byte", m_nbyte, 1);
ck_int("T1 and it was the address with the write bit", m_byteval, {TADDR, 1'b0});
// Eight, not nine: the generator counts a bit when its HIGH phase ends, and the
// byte completes at the SAMPLE point inside the ninth high phase. So at the
// instant the byte is done, the ninth pulse is still in progress.
ck_int("T1 eight pulses completed, with the ninth still in flight", g_bits, 8);
// ----------------------------------------------------------------
// T2. NINE SLOTS, NOT EIGHT. §3.1.4: "The master generates all clock pulses,
// including the acknowledge ninth clock pulse." A byte costs nine.
// ----------------------------------------------------------------
$display("T2 a byte costs nine clock pulses, not eight");
ck_int("T2 the bit index returned to zero", bit_index, 0);
ck_int("T2 one byte completed", bytes_done, 1);
// ----------------------------------------------------------------
// T3. A data byte, written and read back out of the target's own register.
// ----------------------------------------------------------------
send_byte(8'h5A);
$display("T3 a data byte reaches the target and is acknowledged");
ck_bit("T3 acknowledged", ack, 1'b1);
ck_int("T3 the target received it", t_lastwr, 8'h5A);
ck_int("T3 and counted it", t_rx, 1);
ck_int("T3 the monitor agrees", m_byteval, 8'h5A);
// ----------------------------------------------------------------
// T4. THE OWNERSHIP FLIP, when writing. The master drives slots 0-7 and RELEASES
// for slot 8 so the target can answer. §3.1.4, and the test looks at the
// master's own driving signal in the ninth slot.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
@(negedge clk); tx_byte = {TADDR, 1'b0}; dir_write = 1'b1; go = 1'b1;
@(posedge clk); @(negedge clk); go = 1'b0;
to_ack_slot;
$display("T4 writing: the master releases SDA for the ninth slot");
ck_int("T4 in the ninth slot", bit_index, 8);
ck_bit("T4 the master is no longer driving", b_driving, 1'b0);
n = 0;
while (!byte_done && n < 800) begin step; n = n + 1; end
ck_bit("T4 and the target's ACK was read", ack, 1'b1);
// ----------------------------------------------------------------
// T5. A READ. The master releases slots 0-7 and DRIVES slot 8 -- the mirror
// image. The byte comes from the target's memory.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'hC3);
preload(8'h01, 8'h7E);
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
send_byte({TADDR, 1'b1}); // address with the read bit
ck_bit("T5 the read address was acknowledged", ack, 1'b1);
ck_bit("T5 the target knows it is a read", t_dirrd, 1'b1);
recv_byte(1'b1); // ACK: send me another
$display("T5 reading: the master releases the data slots and drives the ninth");
ck_int("T5 the first byte came from the target", got, 8'hC3);
ck_int("T5 the target counted a transmit", t_tx, 1);
// ----------------------------------------------------------------
// T6. THE MIRROR IMAGE, checked in the ninth slot. When reading, the master
// drives slot 8 with its own answer.
// ----------------------------------------------------------------
@(negedge clk); dir_write = 1'b0; ack_to_send = 1'b1; go = 1'b1;
@(posedge clk); @(negedge clk); go = 1'b0;
to_ack_slot;
$display("T6 reading: the master drives the ninth slot with its own answer");
ck_int("T6 in the ninth slot", bit_index, 8);
ck_bit("T6 and now the master IS driving", b_driving, 1'b1);
n = 0;
while (!byte_done && n < 800) begin step; n = n + 1; end
ck_int("T6 the second byte arrived", rx_byte, 8'h7E);
// ----------------------------------------------------------------
// T7. ACK POLARITY IS INVERTED. The receiver pulls SDA LOW to acknowledge, so
// the monitor -- which reads the line -- sees a ZERO for an ACK.
// ----------------------------------------------------------------
$display("T7 an acknowledge is a LOW on the wire, the inverse of intuition");
ck_bit("T7 the wire carried a zero in the ack slot", m_ackv, 1'b0);
ck_bit("T7 and the engine reports it as acknowledged", ack, 1'b1);
// ----------------------------------------------------------------
// T8. A NACK from the master ends the read, and the target lets go.
// ----------------------------------------------------------------
recv_byte(1'b0); // NACK: I am finished
$display("T8 the master's NACK ends the read and the target releases SDA");
ck_bit("T8 the engine reports a NACK", ack, 1'b0);
ck_bit("T8 the wire carried a one in the ack slot", m_ackv, 1'b1);
ck_bit("T8 the target has released SDA", t_sda_low, 1'b0);
ck_int("T8 the NACK was counted", n_nacks, 1);
// ----------------------------------------------------------------
// T9. A NACK from the TARGET, on a wrong address. The address is not
// acknowledged, and the engine must report that rather than proceeding.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
send_byte({7'h51, 1'b0}); // not our target
$display("T9 an unaddressed target does not answer, and the engine says so");
ck_bit("T9 not acknowledged", ack, 1'b0);
ck_bit("T9 the target did not select itself", t_sel, 1'b0);
ck_int("T9 the monitor saw the byte all the same", m_nbyte, 1);
ck_bit("T9 and the ack slot carried a one", m_ackv, 1'b1);
// ----------------------------------------------------------------
// T10. MSB FIRST. §3.1.3. An asymmetric byte proves the order, and 0x80 proves
// it in the strongest way: only the first bit is set.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
send_byte({TADDR, 1'b0});
send_byte(8'h80);
$display("T10 bits go out most significant first");
ck_int("T10 the target received 0x80", t_lastwr, 8'h80);
ck_int("T10 and the monitor read 0x80 off the wire", m_byteval, 8'h80);
send_byte(8'h01);
ck_int("T10 and 0x01 the other way round", t_lastwr, 8'h01);
// ----------------------------------------------------------------
// T11. THE DATA-VALID RULE HELD THROUGHOUT. Over every byte so far, SDA changed
// while SCL was high only at framing -- never inside a byte.
// ----------------------------------------------------------------
$display("T11 SDA never changed inside a byte while SCL was high");
ck_int("T11 no mid-byte changes", bad_sda_change, 0);
ck_int("T11 no owner conflicts", n_conflicts, 0);
ck_int("T11 and no spurious arbitration losses", n_arb, 0);
// ----------------------------------------------------------------
// T12. A COMPLETE TRANSACTION, framed. START, address, data, STOP -- with the
// clock handed from the framer to the generator and back again.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); do_start = 1'b1; @(posedge clk); @(negedge clk); do_start = 1'b0;
wait_frame(400);
clock_to_generator;
send_byte({TADDR, 1'b0});
send_byte(8'h42);
clock_to_framer;
@(negedge clk); do_stop = 1'b1; @(posedge clk); @(negedge clk); do_stop = 1'b0;
wait_frame(600);
$display("T12 a whole transaction, with the clock handed over and handed back");
ck_int("T12 one START", m_nsta, 1);
ck_int("T12 one STOP", m_nsto, 1);
ck_int("T12 two bytes", m_nbyte, 2);
ck_int("T12 no mid-byte framing", m_nmid, 0);
ck_bit("T12 the transfer is closed", m_intr, 1'b0);
ck_int("T12 the target saw one START and one STOP", t_nsta + t_nsto, 2);
ck_int("T12 and received the data byte", t_lastwr, 8'h42);
ck_bit("T12 both lines released at the end", m_scl_low | m_sda_low, 1'b0);
ck_int("T13 the engine let go of SDA between bytes", held_after_byte, 0);
if (errors == 0)
$display("=== i2c_byte_engine: ALL CHECKS PASSED ===");
else
$display("=== i2c_byte_engine: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule -- ---------------------------------------------------------------------------
-- i2c_byte_engine_tb.vhd
-- Independent oracle for i2c_byte_engine, integrated against a pin-level target.
-- Behavioural twin of the SV and Verilog benches.
--
-- This bench assembles everything built so far -- the SCL generator, the SDA owner and
-- inverter, the framer, the bit engine and the byte engine -- and puts an i2c_target_model
-- on the other end of a wired-AND bus. The target is driven by nothing but the two wires.
--
-- That is the point. A byte-level model handed "a byte arrived" cannot be wrong about WHEN,
-- and so cannot catch a master that is right about every byte and wrong about every instant.
-- This one can, and it does not know what the master's states are called.
--
-- It is also where the SCL HANDOVER is exercised for the first time.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_byte_engine_tb is
end entity i2c_byte_engine_tb;
architecture sim of i2c_byte_engine_tb is
constant TCLK : time := 10 ns;
constant NL : integer := 9;
constant NH : integer := 5;
constant NSU : integer := 3;
constant NSMP : integer := 2;
constant NHD : integer := 4;
constant NSUA : integer := 4;
constant NSUO : integer := 4;
constant NBUF : integer := 4;
constant TADDR : std_logic_vector(6 downto 0) := "1010000"; -- 0x50
signal clk, rst_n : std_logic := '0';
signal gen_idle_low, scl_yield : std_logic := '0';
signal do_start, do_restart, do_stop : std_logic := '0';
signal go, dir_write, ack_to_send : std_logic := '0';
signal tx_byte : std_logic_vector(7 downto 0) := (others => '0');
signal g_scl_low, drive_point, sample_point, g_rise, g_fall, g_stretch : std_logic;
signal g_scyc, g_bits : unsigned(15 downto 0);
signal g_phase : unsigned(1 downto 0);
signal f_sda_req, f_sda_bit, f_scl_low : std_logic;
signal f_busy, f_done, f_bus_free, f_started, f_sw : std_logic;
signal n_sta, n_rs, n_sto : unsigned(15 downto 0);
signal f_state : unsigned(3 downto 0);
signal b_sda_req, b_sda_bit, b_driving, b_busy, b_ack, b_ackv, b_done : std_logic;
signal rx_byte : std_logic_vector(7 downto 0);
signal bit_index : unsigned(3 downto 0);
signal bytes_done, n_acks, n_nacks : unsigned(15 downto 0);
signal req, bit_val, grant : std_logic_vector(3 downto 0);
signal m_sda_low, sda_owned, sda_tx, arb_now, arb_lost, sda_conflict : std_logic;
signal n_conflicts, n_arb : unsigned(15 downto 0);
signal gen_enable, m_scl_low : std_logic;
signal t_scl_low, t_sda_low : std_logic;
signal scl_drv, sda_drv : std_logic_vector(1 downto 0);
signal scl, sda : std_logic;
signal scl_in, sda_in, scl_rbl, sda_rbl : std_logic_vector(1 downto 0);
signal scl_h, sda_h : unsigned(7 downto 0);
signal load_en : std_logic := '0';
signal load_addr, load_data : std_logic_vector(7 downto 0) := (others => '0');
signal t_sel, t_dirrd, t_wvalid : std_logic;
signal t_lastwr : std_logic_vector(7 downto 0);
signal t_rx, t_tx, t_nsta, t_nsto : unsigned(15 downto 0);
signal t_state : unsigned(2 downto 0);
signal mo_start, mo_stop, mo_bit, mo_bitv, mo_byte, mo_ack, mo_ackv : std_logic;
signal mo_intr, mo_mid : std_logic;
signal mo_byteval : std_logic_vector(7 downto 0);
signal mo_bidx : unsigned(3 downto 0);
signal mo_nsta, mo_nsto, mo_nbyte, mo_nmid : unsigned(15 downto 0);
signal bad_sda_change : integer := 0;
signal halt : boolean := false;
-- The engine must let go of SDA between bytes. The window has to be stated
-- carefully, because the bit engine's DEFERRED RELEASE is correct and required:
-- after deactivation it holds SDA until SCL is low, so that releasing cannot make
-- the line rise during the high phase and manufacture a STOP. So sda_req staying
-- high immediately after byte_done is not a defect. The real invariant is: once the
-- byte is done AND SCL has actually been observed low, the request must be gone.
signal held_after_byte : integer := 0;
-- SIGNALS, not variables: the SystemVerilog and Verilog versions hold these in
-- registers, so they take effect on the NEXT edge. A VHDL variable updates
-- immediately and would evaluate the guard one cycle earlier, catching the
-- legitimate deferred-release cycle and reporting a defect that is not there.
signal between_bytes, seen_low : boolean := false;
begin
-- THE CLOCK RUNS ONLY WHILE A BYTE IS IN FLIGHT. A generator left enabled between bytes
-- keeps pulsing SCL, and every pulse is a bit slot the target counts and the master does
-- not -- so the two fall out of step and the next read comes back as all ones.
gen_enable <= scl_yield and (b_busy or go);
-- The master's SCL contribution: the framer and the generator, wired-AND locally.
m_scl_low <= f_scl_low or g_scl_low;
scl_drv <= t_scl_low & m_scl_low;
sda_drv <= t_sda_low & m_sda_low;
bus_m : entity work.i2c_line_model
generic map (N_DEV => 2)
port map (scl_drive_low => scl_drv, sda_drive_low => sda_drv,
scl => scl, sda => sda, scl_in => scl_in, sda_in => sda_in,
scl_released_but_low => scl_rbl, sda_released_but_low => sda_rbl,
scl_holders => scl_h, sda_holders => sda_h);
u_scl : entity work.i2c_scl_gen
generic map (N_LOW => NL, N_HIGH => NH, N_SU => NSU, N_SAMP => NSMP, CNT_W => 16)
port map (clk => clk, rst_n => rst_n, enable => gen_enable, idle_low => gen_idle_low,
scl_in => scl_in(0), scl_drive_low => g_scl_low,
drive_point => drive_point, sample_point => sample_point,
scl_rising => g_rise, scl_falling => g_fall,
stretching => g_stretch, stretch_cycles => g_scyc,
bits_generated => g_bits, phase => g_phase);
u_fr : entity work.i2c_framer
generic map (N_HD_STA => NHD, N_SU_STA => NSUA, N_SU_STO => NSUO,
N_BUF => NBUF, N_SU_DAT => NSU, CNT_W => 16)
port map (clk => clk, rst_n => rst_n,
do_start => do_start, do_restart => do_restart, do_stop => do_stop,
scl_in => scl_in(0), sda_in => sda_in(0), scl_yield => scl_yield,
sda_req => f_sda_req, sda_bit => f_sda_bit, scl_drive_low => f_scl_low,
busy => f_busy, done => f_done, bus_free => f_bus_free, started => f_started,
stretch_wait => f_sw, starts => n_sta, restarts => n_rs, stops => n_sto,
state => f_state);
dut : entity work.i2c_byte_engine
generic map (CNT_W => 16)
port map (clk => clk, rst_n => rst_n,
drive_point => drive_point, sample_point => sample_point,
go => go, dir_write => dir_write, tx_byte => tx_byte, ack_to_send => ack_to_send,
sda_in => sda_in(0), scl_high => scl_in(0), abort => '0',
sda_req => b_sda_req, sda_bit => b_sda_bit,
rx_byte => rx_byte, ack => b_ack, ack_valid => b_ackv, byte_done => b_done,
busy => b_busy, bit_index => bit_index, driving => b_driving,
bytes_done => bytes_done, acks => n_acks, nacks => n_nacks);
req <= "00" & b_sda_req & f_sda_req;
bit_val <= "00" & b_sda_bit & f_sda_bit;
u_sda : entity work.i2c_sda_ctrl
generic map (N_OWNER => 4, CNT_W => 16)
port map (clk => clk, rst_n => rst_n, req => req, bit_val => bit_val,
sda_in => sda_in(0), scl_in => scl_in(0), tx_active => b_driving,
sda_drive_low => m_sda_low,
grant => grant, owned => sda_owned, tx_bit => sda_tx,
owner_conflict => sda_conflict, conflicts => n_conflicts,
arb_loss_now => arb_now, arb_lost => arb_lost, arb_losses => n_arb,
arb_clear => '0');
u_tgt : entity work.i2c_target_model
generic map (MY_ADDR => TADDR, ACK_ADDR => '1', STRETCH_AFTER => 0,
NACK_AT => 0, N_MEM => 16, CNT_W => 16)
port map (clk => clk, rst_n => rst_n, scl => scl, sda => sda,
scl_drive_low => t_scl_low, sda_drive_low => t_sda_low,
load_en => load_en, load_addr => load_addr, load_data => load_data,
selected => t_sel, dir_read => t_dirrd, last_written => t_lastwr,
write_valid => t_wvalid, bytes_rx => t_rx, bytes_tx => t_tx,
n_starts => t_nsta, n_stops => t_nsto, state => t_state);
mon : entity work.i2c_proto_mon
generic map (CNT_W => 16)
port map (clk => clk, rst_n => rst_n, scl => scl, sda => sda,
start_seen => mo_start, stop_seen => mo_stop, bit_seen => mo_bit,
bit_val => mo_bitv, byte_seen => mo_byte, byte_val => mo_byteval,
ack_seen => mo_ack, ack_val => mo_ackv, in_transfer => mo_intr,
framing_midbyte => mo_mid, bit_index => mo_bidx,
n_starts => mo_nsta, n_stops => mo_nsto, n_bytes => mo_nbyte,
n_midbyte => mo_nmid);
clkgen : process
begin
while not halt loop
clk <= '0'; wait for TCLK/2;
clk <= '1'; wait for TCLK/2;
end loop;
wait;
end process;
-- §3.1.2 watchdog: SDA may only change while SCL is low, EXCEPT at framing.
meas : process (clk, rst_n)
variable scl_l, sda_l : std_logic := '1';
begin
if rst_n = '0' then
scl_l := '1'; sda_l := '1'; bad_sda_change <= 0;
elsif falling_edge(clk) then
if scl = '1' and scl_l = '1' and sda /= sda_l
and mo_start = '0' and mo_stop = '0' and mo_bidx /= 0 then
bad_sda_change <= bad_sda_change + 1;
end if;
scl_l := scl; sda_l := sda;
end if;
end process;
letgo_obs : process (clk, rst_n)
begin
if rst_n = '0' then
held_after_byte <= 0; between_bytes <= false; seen_low <= false;
elsif rising_edge(clk) then
if b_done = '1' then
between_bytes <= true; seen_low <= false;
elsif go = '1' then
between_bytes <= false;
end if;
if between_bytes and scl_in(0) = '0' then seen_low <= true; end if;
if between_bytes and seen_low and go = '0' and b_sda_req = '1' then
held_after_byte <= held_after_byte + 1;
end if;
end if;
end process;
stim : process
variable err : integer := 0;
variable n : integer;
variable got : std_logic_vector(7 downto 0);
procedure ck_int (what : string; g : integer; e : integer) is
begin
if g /= e then
report " FAIL " & what & ": got " & integer'image(g)
& " expected " & integer'image(e) severity note;
err := err + 1;
end if;
end procedure;
procedure ck_bit (what : string; g : std_logic; e : std_logic) is
begin
if g /= e then
report " FAIL " & what & ": got " & std_logic'image(g)
& " expected " & std_logic'image(e) severity note;
err := err + 1;
end if;
end procedure;
procedure step is
begin
wait until rising_edge(clk); wait until falling_edge(clk);
end procedure;
procedure do_reset is
begin
wait until falling_edge(clk);
rst_n <= '0';
gen_idle_low <= '0'; scl_yield <= '0';
do_start <= '0'; do_restart <= '0'; do_stop <= '0';
go <= '0'; dir_write <= '1'; ack_to_send <= '1'; tx_byte <= (others => '0');
load_en <= '0';
for i in 0 to 2 loop wait until rising_edge(clk); end loop;
wait until falling_edge(clk); rst_n <= '1';
step;
end procedure;
procedure preload (a : integer; d : integer) is
begin
wait until falling_edge(clk);
load_en <= '1';
load_addr <= std_logic_vector(to_unsigned(a, 8));
load_data <= std_logic_vector(to_unsigned(d, 8));
wait until rising_edge(clk); wait until falling_edge(clk);
load_en <= '0';
end procedure;
procedure pulse_start is
begin
wait until falling_edge(clk); do_start <= '1';
wait until rising_edge(clk); wait until falling_edge(clk); do_start <= '0';
end procedure;
procedure pulse_stop is
begin
wait until falling_edge(clk); do_stop <= '1';
wait until rising_edge(clk); wait until falling_edge(clk); do_stop <= '0';
end procedure;
procedure wait_frame (max_cycles : integer) is
begin
n := 0;
while f_done = '0' and n < max_cycles loop step; n := n + 1; end loop;
if n >= max_cycles then
report " FAIL wait_frame: framer stuck in state "
& integer'image(to_integer(f_state)) severity note;
err := err + 1;
end if;
end procedure;
-- THE HANDOVER, in both directions. Yielding and enabling in the same cycle means the
-- framer releases SCL and the generator drives it low at the same clock edge.
procedure clock_to_generator is
begin
wait until falling_edge(clk); scl_yield <= '1'; gen_idle_low <= '1';
end procedure;
procedure clock_to_framer is
begin
n := 0;
while to_integer(g_phase) /= 0 and n < 200 loop step; n := n + 1; end loop;
wait until falling_edge(clk); scl_yield <= '0';
step;
wait until falling_edge(clk); gen_idle_low <= '0';
end procedure;
procedure send_byte (b : integer) is
begin
wait until falling_edge(clk);
tx_byte <= std_logic_vector(to_unsigned(b, 8)); dir_write <= '1'; go <= '1';
wait until rising_edge(clk); wait until falling_edge(clk); go <= '0';
n := 0;
while b_done = '0' and n < 800 loop step; n := n + 1; end loop;
if n >= 800 then
report " FAIL send_byte: never completed" severity note;
err := err + 1;
end if;
end procedure;
procedure recv_byte (do_ack : std_logic) is
begin
wait until falling_edge(clk);
dir_write <= '0'; ack_to_send <= do_ack; go <= '1';
wait until rising_edge(clk); wait until falling_edge(clk); go <= '0';
n := 0;
while b_done = '0' and n < 800 loop step; n := n + 1; end loop;
if n >= 800 then
report " FAIL recv_byte: never completed" severity note;
err := err + 1;
end if;
got := rx_byte;
end procedure;
-- Advance to the ninth slot AND past its drive point, which is where ownership has
-- actually changed hands. Checking at the instant bit_index becomes 8 is too early.
procedure to_ack_slot is
begin
n := 0;
while bit_index /= 8 and n < 800 loop step; n := n + 1; end loop;
n := 0;
while drive_point = '0' and n < 800 loop step; n := n + 1; end loop;
step;
end procedure;
begin
report "=== i2c_byte_engine: eight bits, then the slot where SDA changes hands ==="
severity note;
-- T1. An address byte, acknowledged by a real target that found its own edges.
do_reset;
pulse_start;
wait_frame(400);
clock_to_generator;
send_byte(16#50# * 2);
report "T1 an address byte, acknowledged by a pin-level target" severity note;
ck_bit("T1 acknowledged", b_ack, '1');
ck_bit("T1 the target selected itself", t_sel, '1');
ck_int("T1 the monitor saw one byte", to_integer(mo_nbyte), 1);
ck_int("T1 and it was the address with the write bit",
to_integer(unsigned(mo_byteval)), 16#A0#);
-- Eight, not nine: the generator counts a bit when its HIGH phase ends, and the byte
-- completes at the SAMPLE point inside the ninth high phase.
ck_int("T1 eight pulses completed, with the ninth still in flight",
to_integer(g_bits), 8);
-- T2. NINE SLOTS, NOT EIGHT. §3.1.4: "including the acknowledge ninth clock pulse."
report "T2 a byte costs nine clock pulses, not eight" severity note;
ck_int("T2 the bit index returned to zero", to_integer(bit_index), 0);
ck_int("T2 one byte completed", to_integer(bytes_done), 1);
-- T3. A data byte, read back out of the target's own register.
send_byte(16#5A#);
report "T3 a data byte reaches the target and is acknowledged" severity note;
ck_bit("T3 acknowledged", b_ack, '1');
ck_int("T3 the target received it", to_integer(unsigned(t_lastwr)), 16#5A#);
ck_int("T3 and counted it", to_integer(t_rx), 1);
ck_int("T3 the monitor agrees", to_integer(unsigned(mo_byteval)), 16#5A#);
-- T4. THE OWNERSHIP FLIP, when writing: the master RELEASES for slot 8.
do_reset;
pulse_start;
wait_frame(400);
clock_to_generator;
wait until falling_edge(clk);
tx_byte <= x"A0"; dir_write <= '1'; go <= '1';
wait until rising_edge(clk); wait until falling_edge(clk); go <= '0';
to_ack_slot;
report "T4 writing: the master releases SDA for the ninth slot" severity note;
ck_int("T4 in the ninth slot", to_integer(bit_index), 8);
ck_bit("T4 the master is no longer driving", b_driving, '0');
n := 0;
while b_done = '0' and n < 800 loop step; n := n + 1; end loop;
ck_bit("T4 and the target's ACK was read", b_ack, '1');
-- T5. A READ: the master releases slots 0-7 and DRIVES slot 8 -- the mirror image.
do_reset;
preload(0, 16#C3#);
preload(1, 16#7E#);
pulse_start;
wait_frame(400);
clock_to_generator;
send_byte(16#A1#);
ck_bit("T5 the read address was acknowledged", b_ack, '1');
ck_bit("T5 the target knows it is a read", t_dirrd, '1');
recv_byte('1');
report "T5 reading: the master releases the data slots and drives the ninth"
severity note;
ck_int("T5 the first byte came from the target",
to_integer(unsigned(got)), 16#C3#);
ck_int("T5 the target counted a transmit", to_integer(t_tx), 1);
-- T6. THE MIRROR IMAGE, checked in the ninth slot.
wait until falling_edge(clk);
dir_write <= '0'; ack_to_send <= '1'; go <= '1';
wait until rising_edge(clk); wait until falling_edge(clk); go <= '0';
to_ack_slot;
report "T6 reading: the master drives the ninth slot with its own answer"
severity note;
ck_int("T6 in the ninth slot", to_integer(bit_index), 8);
ck_bit("T6 and now the master IS driving", b_driving, '1');
n := 0;
while b_done = '0' and n < 800 loop step; n := n + 1; end loop;
ck_int("T6 the second byte arrived", to_integer(unsigned(rx_byte)), 16#7E#);
-- T7. ACK POLARITY IS INVERTED: the monitor, reading the line, sees a ZERO for an ACK.
report "T7 an acknowledge is a LOW on the wire, the inverse of intuition"
severity note;
ck_bit("T7 the wire carried a zero in the ack slot", mo_ackv, '0');
ck_bit("T7 and the engine reports it as acknowledged", b_ack, '1');
-- T8. A NACK from the master ends the read, and the target lets go.
recv_byte('0');
report "T8 the master's NACK ends the read and the target releases SDA"
severity note;
ck_bit("T8 the engine reports a NACK", b_ack, '0');
ck_bit("T8 the wire carried a one in the ack slot", mo_ackv, '1');
ck_bit("T8 the target has released SDA", t_sda_low, '0');
ck_int("T8 the NACK was counted", to_integer(n_nacks), 1);
-- T9. A NACK from the TARGET, on a wrong address.
do_reset;
pulse_start;
wait_frame(400);
clock_to_generator;
send_byte(16#A2#); -- address 0x51, write
report "T9 an unaddressed target does not answer, and the engine says so"
severity note;
ck_bit("T9 not acknowledged", b_ack, '0');
ck_bit("T9 the target did not select itself", t_sel, '0');
ck_int("T9 the monitor saw the byte all the same", to_integer(mo_nbyte), 1);
ck_bit("T9 and the ack slot carried a one", mo_ackv, '1');
-- T10. MSB FIRST. §3.1.3, proved with 0x80 and 0x01.
do_reset;
pulse_start;
wait_frame(400);
clock_to_generator;
send_byte(16#A0#);
send_byte(16#80#);
report "T10 bits go out most significant first" severity note;
ck_int("T10 the target received 0x80", to_integer(unsigned(t_lastwr)), 16#80#);
ck_int("T10 and the monitor read 0x80 off the wire",
to_integer(unsigned(mo_byteval)), 16#80#);
send_byte(16#01#);
ck_int("T10 and 0x01 the other way round",
to_integer(unsigned(t_lastwr)), 16#01#);
-- T11. THE DATA-VALID RULE HELD THROUGHOUT.
report "T11 SDA never changed inside a byte while SCL was high" severity note;
ck_int("T11 no mid-byte changes", bad_sda_change, 0);
ck_int("T11 no owner conflicts", to_integer(n_conflicts), 0);
ck_int("T11 and no spurious arbitration losses", to_integer(n_arb), 0);
-- T12. A COMPLETE TRANSACTION, framed, with the clock handed over and handed back.
do_reset;
pulse_start;
wait_frame(400);
clock_to_generator;
send_byte(16#A0#);
send_byte(16#42#);
clock_to_framer;
pulse_stop;
wait_frame(600);
report "T12 a whole transaction, with the clock handed over and handed back"
severity note;
ck_int("T12 one START", to_integer(mo_nsta), 1);
ck_int("T12 one STOP", to_integer(mo_nsto), 1);
ck_int("T12 two bytes", to_integer(mo_nbyte), 2);
ck_int("T12 no mid-byte framing", to_integer(mo_nmid), 0);
ck_bit("T12 the transfer is closed", mo_intr, '0');
ck_int("T12 the target saw one START and one STOP",
to_integer(t_nsta) + to_integer(t_nsto), 2);
ck_int("T12 and received the data byte",
to_integer(unsigned(t_lastwr)), 16#42#);
ck_bit("T12 both lines released at the end", m_scl_low or m_sda_low, '0');
ck_int("T13 the engine let go of SDA between bytes", held_after_byte, 0);
if err = 0 then
report "=== i2c_byte_engine: ALL CHECKS PASSED ===" severity note;
else
report "=== i2c_byte_engine: " & integer'image(err)
& " CHECK(S) FAILED ===" severity note;
end if;
halt <= true;
wait;
end process;
end architecture sim;6b. Execution
| Design | SystemVerilog | Verilog-2001 | VHDL | Finish |
|---|---|---|---|---|
i2c_byte_engine | PASS 13/13 | PASS 13/13 | PASS 13/13 | 17380 ns, all three |
7. Mutation Testing
Ten defects, each aimed at one claim above.
| # | Injected defect | Expected detection | Result |
|---|---|---|---|
| M1 | eight slots instead of nine — no acknowledge pulse at all | T2 | KILLED (22) |
| M2 | no ownership flip — master drives through the target's ACK slot | T4, T6 | KILLED (6) |
| M3 | LSB first — shift the wrong way | T10 | KILLED (19) |
| M4 | transmit bit 0 instead of the MSB | T10 | KILLED (12) |
| M5 | ACK polarity inverted on a write | T7, T9 | KILLED (6) |
| M6 | the master's own ACK sent with wrong polarity on a read | T8 | KILLED (4) |
| M7 | the received byte latched one slot early | T3, T5 | KILLED (3) |
| M8 | data slots transmit the wrong shift-register bit | T3, T10 | KILLED (19) |
| M9 | byte_done never fires | T1, T12 | KILLED (14) |
| M10 | the engine holds SDA past the end of the byte | T13 after strengthening | KILLED (2) |
baseline: PASS (verified before injecting anything)
killed: 10 survived: 0 score: 10/10
restored: PASSM10 took three attempts to state correctly
This one is instructive about mutation construction, not just about the design.
Attempt one set tx_en high at byte completion. It survived — and inspecting 17.6 shows why: tx_en is read only inside the bit engine's active branch, at the drive point. With active low it is dead. An equivalent mutant.
Attempt two kept the engine active instead. It also survived, for the mirror reason: staying active with tx_en low means the engine keeps releasing the line, which is what it was going to do anyway.
Attempt three set both. Now the master genuinely holds its acknowledge bit past the end of the byte — and it still survived, which made it a real testbench gap rather than an equivalent mutant.
Why nothing saw it: the next byte's go overwrites the request, the gap between bytes sits inside the SCL low phase, and if the held value happens to be low it even helps a STOP that may follow. No per-byte check can observe it. But the master is driving a line it no longer owns, and the next block that wants SDA — the framer, for a repeated START — collides with it.
Stating the invariant required care, because the obvious version is wrong:
WRONG: after byte_done, sda_req must be lowThat fails on the correct design, because 17.6 §4's deferred release is required: after deactivation the bit engine deliberately holds SDA until SCL is low, so releasing cannot make the line rise in the high phase and manufacture a STOP.
RIGHT: once the byte is done AND SCL has been observed low, sda_req must be gone8. Verification Connection — What a Byte Monitor Knows, and What Needs Context
// Chapter 17.6 §8 said a bit-level monitor can report a slot, a value and a
// direction, and nothing about position or meaning. A BYTE monitor is the next
// layer, and it is worth being exact about what it adds and what it still cannot
// know -- because this is where most I²C environments over-reach.
//
// WHAT A BYTE MONITOR CAN REPORT, from the wires plus its own bit counting:
//
// eight bits, MSB first, as a byte value
// the level in the ninth slot -> acknowledged or not
// which side was driving each part -> inferred from the framing it tracked
//
// WHAT IT CANNOT REPORT, and each needs transaction context from 17.8:
//
// whether this byte was an ADDRESS or DATA. Identical on the wire. The only
// difference is that it was the first byte after a START -- which is framing
// state, not byte state.
//
// whether a NACK was an ERROR. A NACK on an address is a failure; a NACK from
// the MASTER on the last byte of a read is CORRECT and required. Same level in
// the same slot, opposite meanings.
//
// WHO sent the acknowledge. The monitor sees SDA low in slot 8. Whether the
// target pulled it (a write) or the master did (a read) depends on the
// direction bit of an address byte it may not have seen.
//
// SO THE TRANSACTION ITEM IS ASSEMBLED ABOVE THIS LAYER. A byte monitor emits
// byte-level items; a predictor turns a START plus an address plus N data bytes
// plus a STOP into one transaction. Trying to emit transaction items directly from
// a byte monitor forces it to re-implement the framing tracker it already depends
// on, and the duplicate is what goes stale.
//
// COVERAGE this block makes reachable:
//
// cover: a write byte NACKed by the target (T9)
// cover: a read byte the MASTER nacked (T8) -- correct behaviour
// cover: a read byte the master ACKed (T5) -- more bytes to come
// cover: byte value 0x80 and 0x01 (T10) -- the MSB/LSB extremes
//
// The 0x80 and 0x01 bins are not decoration: they are the only values whose
// transmission distinguishes a correct shift direction from a reversed one when
// the byte is otherwise symmetric.9. FPGA and ASIC Implications
On an FPGA, the shift register and the four-bit slot index are trivial. The interesting consequence is at the block's input: tx_byte is captured when go asserts, so the host data path has one SCL period per byte — tens of microseconds — to present the next byte. That is an enormous margin, and it is why an I²C master needs no FIFO to keep up with the bus. Chapter 17.2 §9 noted that a large N_BUF pushes the buffers into block RAM and makes the read registered; that extra cycle is absorbed here without difficulty for the same reason.
On an ASIC, the ownership flip is where the pad's output enable toggles mid-byte, once per byte, every byte. That is the highest-frequency ownership change anywhere in the design, and it is worth knowing that it happens inside the SCL low phase — at the drive point, never during the high phase — because a pad enable toggling while the line is high would create exactly the framing edges 17.6 §4 works to avoid. The structural guarantee that SDA is written only at the drive point is what makes the pad's enable safe to toggle at all.
Reset leaves the engine inactive with SDA released, which is the same requirement as every other block: a master coming out of reset must not be holding the bus down.
10. Debugging — The Read That Was Off by One Bit, and Only Sometimes
A master reads a 16-bit value from a sensor as two bytes. The value is usually correct. Roughly one reading in eight is exactly double the expected value, with the least significant bit of the low byte being zero. The high byte is always correct. Reading the same register twice in a row returns a correct value and then a doubled one, alternating irregularly.
The received byte was latched on the slot index rather than on the bit engine's completion pulse, so on any bit where the sample point was delayed the latch ran before the final bit had been sampled. The shift register then contributed its previous contents and the newest bit was lost, shifting the byte left by one. Nothing was wrong with the sensor, the bus, the framing or the acknowledge -- and nothing was wrong with the master's timing either, in the sense that every edge it produced was legal. The defect was a dependency on a counter reaching a value instead of on the event that says the value is ready, which is the same class as the wrong-anchor defects of Chapters 17.3 and 17.5.
Latch the byte on bit_done for the final slot, not on the slot index alone -- which is what the published engine does: every slot transition is inside if (bit_done). Then note the environment failure that let it survive: a target model that never stretches makes the two designs identical, because without a stretch the sample point and the slot index advance together. Test T3 reads a byte back out of a real target model, and the stretch tests of Chapter 17.3 T6 to T8 are what make the timing of that latch observable. A bench built from a scripted responder cannot fail the original design at all.Three generalisations.
A counter reaching a value is not an event. bit_index == 7 says which slot we are in, not that the slot has finished. The engine advances on bit_done for exactly this reason, and every state change in the published block is inside that condition.
The high byte was always correct, which sent the investigation to the sensor. A fault that appears on one byte of two looks like a property of what that byte contains. It was a property of when the target chose to stretch, which correlates with position only accidentally.
A permissive target model made two different designs identical. Without a stretch, the sample point and the slot index advance in lockstep, so the correct and incorrect latch conditions cannot be distinguished. This is the third time in the module that an environment which never withholds anything has certified a defect — 17.1 §10, 17.5 §9, and here.
11. Common Misconceptions
"A byte is eight bit slots." It is nine. §3.1.4 requires the master to generate the acknowledge pulse as well. §1.
"The ACK is bit 9 of the byte." It is a different ownership phase that occupies a clock pulse. Treating it as data means driving it from the shift register, during the slot the transmitter must release. §1.
"Reads and writes need separate datapaths." They are mirror images differing in where the ownership boundary falls. One mirrored expression, not two branches that may drift apart. §2.
"A sampled one in the ACK slot means the target said no." It means nobody pulled the line down. A NACK is the absence of a response, which is why a missing device and a refusing one are indistinguishable. §4.
"ACK polarity can be handled wherever it is needed." Then the inversion exists in several places and one of them is wrong. Keep it in one block, as with the SDA inversion. §4.
"The shift register only matters while receiving." It also records what actually reached the bus while transmitting, which is what arbitration reporting needs. §3.
"MSB-first is just a convention to get right." It also determines that a partially transmitted byte is left-justified, which is what a master must report after losing arbitration mid-byte. §3.
"Latch the byte when the slot index reaches seven." The index says which slot, not that it has finished. Latch on the completion pulse, or a stretched final bit loses the newest bit. §10.
"If the bits on the bus are right, the master read them right." The bus carried correct data and the master assembled it shifted by one. Wire-level correctness and assembled-value correctness are different claims. §10.
"A byte monitor can emit transactions." It cannot tell an address from data, or an error NACK from a required one, without framing context it does not own. §8.
12. Reason It Through
Why does the byte engine wrap the bit engine rather than reimplementing it?
Because the ninth slot differs from the eight in exactly one respect — who owns SDA. Clock, sampling instant and data rule are identical, so eight slots plus one reversed slot is the whole difference. §1.
A read returns eight correct bits and the target then behaves as though the transfer was abandoned. Which ownership mistake do you check?
The ninth slot. On a read the master must drive its own acknowledge there; if it released instead, the line floats high and the target reads a NACK and lets go — bits correct, transfer ended. §2.
Why is a sampled one in the acknowledge slot not a signal from the target?
Because it is the pull-up, not a device. A NACK is the absence of anyone pulling the line down, which is why a device that is absent and one that refuses look identical. §4.
What does MSB-first imply about a byte abandoned to arbitration loss?
That it is left-justified: the bits already transmitted occupy the high positions. That is the form a master must report when it loses mid-byte and has to retry. §3.
Mutation attempts one and two for M10 both survived for opposite reasons. What were they?
Setting tx_en alone is dead because tx_en is read only while active. Keeping active alone means continuing to release, which was going to happen anyway. Only setting both produces a master that actually holds the line. §7.
Why is "after byte_done, sda_req must be low" the wrong invariant?
Because the bit engine's deferred release is required: it holds SDA until SCL is low so that releasing cannot make the line rise during the high phase. The correct invariant waits for SCL to have been observed low. §7.
Why did the VHDL version of that check report a defect the SystemVerilog version did not?
Because it used variables, which update immediately, where the SystemVerilog used registers, which update on the next edge. The VHDL guard therefore evaluated one cycle earlier and caught the legitimate deferred-release cycle. §7.
Why can a bench with a non-stretching target not distinguish latching on bit_index == 7 from latching on bit_done?
Because without a stretch the sample point and the slot index advance together, so both conditions are true in the same cycle. The distinction only appears when the sample is delayed. §10.
13. Understanding Check
14. Summary
A byte is nine bit slots, and the ninth differs from the eight in exactly one respect: who owns SDA. Clock, sampling instant and data rule are identical, which is why this block wraps the bit engine instead of replacing it.
The acknowledge is an ownership phase, not a ninth data bit. Treating it as data means driving it from the shift register during the slot the transmitter must release.
Writes and reads are mirror images. The transmitter releases for slot 8 — the master on a write, the target on a read — and one mirrored expression is safer than two branches that can drift apart.
MSB first has a second consequence: a partially transmitted byte is left-justified, which is the form a master must report after losing arbitration mid-byte.
The engine shifts in the line even while transmitting, so the register records what actually reached the bus rather than what was intended.
ACK polarity is inverted and belongs in one place. A sampled zero is an acknowledge; a sampled one is the absence of anyone pulling the line down, which is why a missing device and a refusing one are indistinguishable.
Ten mutants, ten killed — and M10 needed three attempts to state, with the first two surviving as equivalent mutants for opposite reasons.
The real gap M10 exposed was invisible to every per-byte check, because the next byte overwrites the request, the gap sits in the low phase, and a held low even helps a following STOP.
Stating that invariant required allowing the deferred release. "No request after byte_done" is wrong; "no request once SCL has been observed low" is right — and the VHDL port needed signals rather than variables to evaluate it at the same instant.
Latch on the event, not the counter. bit_index == 7 says which slot is current, not that it is complete, and a stretched final bit loses its newest bit — a defect a non-stretching target model cannot expose.
15. What Comes Next
Bytes now move in both directions, with the acknowledge handled correctly at each end. What no block yet decides is which bytes, in what order, and what to do when one is refused.
Chapter 17.8 builds the transaction controller: the address byte and its direction bit, the payload count, the master's acknowledge policy on a read, the final NACK that ends it, and the decision to issue a STOP. It is also the first block that has to act on a refusal — and the specification does not say what a master should do with a NACK on a data byte mid-burst, so that becomes a stated design decision rather than a derivation.
Repeated START stays out of it. That is 17.9, for the reason this module has used throughout: one architectural responsibility per chapter.
Continue learning
Related tutorials
- Related topic
Decomposing an I²C Master — From Requirements to Architecture
An I²C master is not one state machine, and the reason is structural rather than stylistic: the protocol imposes four independent time bases that change on four unrelated events. Derives the block structure from the normative obligations, establishes the wired-AND bus model every later chapter is written against, and shows why the framing generator cannot live inside the bit engine.
- 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 SCL Timing Generator — Phases, Strobes and the Readback Rule
Where Table 10's microseconds become counts of system-clock cycles. Derives the period budget that must include rise and fall time, shows why rounding down is always illegal and rounding up always legal, and builds a generator that leaves its low phase only when the line actually reads back high — which implements clock stretching and clock synchronization with no extra logic.
- Related topic
SDA Open-Drain Control and Line Ownership in RTL
The transmitted bit is the inverse of the drive enable, and that inversion belongs in exactly one place. Makes SDA ownership an explicit signal rather than an implication of the state encoding, shows why an ownership conflict must be reported rather than resolved silently, and derives arbitration detection from three signals the block already has — including the intent bit without which every read looks like a lost arbitration.
