I²C · Module 17
Repeated START and Combined Transactions in RTL
A combined transfer is not a write, a STOP and a read — it is one transaction whose direction reverses without the bus ever going free, and the difference is what stops another master moving the pointer in between. Builds the two-phase sequencer, and shows why the case most often got wrong is a failure in phase one, where the bus is still held.
Chapter 17.8's controller executes one phase. This chapter builds the block that owns two of them, and the reason it must exist is not convenience.
1. The Pattern, and Why It Is the Dominant One
That is the register-pointer access of Chapter 10.1 and Chapter 16.1: write the location, turn the bus around, read from it. It is the most common transaction shape on the entire bus, because almost every I²C device is a register map.
START -> address+W -> pointer byte -> repeated START -> address+R -> data... -> STOP2. Why It Needs Its Own Block
Nothing in the transaction controller guarantees that a second phase follows the first. On a multi-master bus that guarantee is the entire point.
So atomicity has to be enforced by something that owns both phases. This block issues phase one with stop = 0, waits, and issues phase two — and it refuses a new descriptor in between.
3. The Case a Two-Phase Sequencer Most Often Gets Wrong
If the pointer write is NACKed, there is no point reading — the target never took the address.
But the bus is still held. The sequencer cannot simply report the failure and stop:
So a failed phase one must frame the bus before giving up. That is what the abandon path does, and it does it the cheapest legal way: a zero-length write with a STOP. It re-addresses the target, which will NACK again, and then stops — and the NACK is harmless, because the whole point of the descriptor is the STOP.
The alternative, releasing the lines directly, is not available. A master that just lets go leaves every device mid-byte with its own bit counter part-way through, and no device has any way to resynchronise.
4. The Sequence, and the Property That Matters
Two STARTs, one STOP — the bus is never released in between
8 cyclesThe bus owned row is the invariant. It goes low exactly once, at the end — and a design that releases it between the phases is the defect that this whole block exists to prevent.
Two figures rather than one, because the success path and the failure path are different arguments and a composite would obscure both.
5. The Two-Phase Sequencer, in Three Languages
// -----------------------------------------------------------------------------
// i2c_combined_txn.sv
// Two phases, one transaction, and the bus never released in between.
//
// THE PATTERN. §3.1.10 format 3: "During a change of direction within a transfer, the
// START condition and the slave address are both repeated, but with the R/W bit
// reversed." That is the register-pointer access of Chapter 10.1 and Chapter 16.1 --
// write the location, turn the bus around, read from it -- and it is the dominant
// transaction shape on the whole bus.
//
// WHY IT NEEDS ITS OWN BLOCK, above the transaction controller. The controller of
// Chapter 17.8 executes ONE phase. Nothing in it guarantees that a second phase follows
// the first, and on a multi-master bus that guarantee is the entire point:
//
// §3.1.10 format 3 holds the bus across the turnaround. A STOP there would release the
// arbitration this master already won, and another master may take the bus in the gap
// and write its OWN pointer. The read that follows then returns that master's data --
// plausible, wrong, and with no error anywhere. Chapter 10.3 is the general statement;
// Chapter 16.2 §1 is where it bites.
//
// So atomicity has to be enforced by something that owns BOTH phases, and that is this
// block. It issues phase one with `stop = 0`, waits, and issues phase two -- and it will
// not accept a new descriptor in between.
//
// AND IT HAS TO HANDLE A FAILURE IN PHASE ONE. If the pointer write is NACKed there is
// no point reading: the target never took the address. But the bus is still HELD, so the
// block cannot simply report and stop -- it must frame the bus before giving up, or it
// leaves every device believing a transfer is in progress. That is the case a two-phase
// sequencer most often gets wrong, and it is the reason `abandon` exists below.
// -----------------------------------------------------------------------------
module i2c_combined_txn #(
parameter int CNT_W = 16
) (
input logic clk,
input logic rst_n,
// ---- the descriptor: one combined access --------------------------------
input logic seq_valid,
input logic [6:0] seq_addr,
input logic [7:0] seq_ptr, // the byte written in phase one
input logic [3:0] seq_rdlen, // bytes read in phase two; 0 = write-only
input logic seq_is_read, // 0 = a plain write of seq_ptr then payload
// ---- to the transaction controller --------------------------------------
output logic cmd_valid,
output logic [6:0] cmd_addr,
output logic cmd_read,
output logic [3:0] cmd_len,
output logic cmd_stop,
input logic txn_done,
input logic txn_ok,
input logic [5:0] txn_err,
input logic [3:0] txn_bytes,
input logic txn_busy,
// ---- results ------------------------------------------------------------
output logic seq_done,
output logic seq_ok,
output logic [5:0] seq_err,
output logic [3:0] seq_bytes,
output logic seq_busy,
output logic abandoned, // phase one failed and the bus was framed anyway
output logic [1:0] phase,
output logic [2:0] state,
output logic [CNT_W-1:0] sequences
);
localparam [2:0] Q_IDLE = 3'd0,
Q_P1 = 3'd1, // phase one: write the pointer, NO stop
Q_P1W = 3'd2, // waiting for phase one
Q_P2 = 3'd3, // phase two: the turnaround and the read
Q_P2W = 3'd4,
Q_ABND = 3'd5, // phase one failed: frame the bus before reporting
Q_ABW = 3'd6,
Q_DONE = 3'd7;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= Q_IDLE;
cmd_valid <= 1'b0;
cmd_addr <= 7'h00;
cmd_read <= 1'b0;
cmd_len <= 4'd0;
cmd_stop <= 1'b1;
seq_done <= 1'b0;
seq_ok <= 1'b0;
seq_err <= 6'd0;
seq_bytes <= 4'd0;
seq_busy <= 1'b0;
abandoned <= 1'b0;
phase <= 2'd0;
sequences <= {CNT_W{1'b0}};
end else begin
cmd_valid <= 1'b0;
seq_done <= 1'b0;
case (state)
Q_IDLE: begin
seq_busy <= 1'b0;
if (seq_valid && !txn_busy) begin
seq_err <= 6'd0;
seq_ok <= 1'b0;
seq_bytes <= 4'd0;
abandoned <= 1'b0;
seq_busy <= 1'b1;
phase <= 2'd1;
// Phase one: address with the WRITE direction, one data byte -- the
// pointer -- and crucially NO STOP. The bus stays held.
cmd_addr <= seq_addr;
cmd_read <= 1'b0;
cmd_len <= 4'd1;
cmd_stop <= ~seq_is_read; // a plain write ends here
cmd_valid <= 1'b1;
state <= Q_P1;
end
end
Q_P1: if (txn_busy) state <= Q_P1W;
Q_P1W: begin
if (txn_done) begin
seq_err <= seq_err | txn_err;
seq_bytes <= seq_bytes + txn_bytes;
if (!txn_ok) begin
// Phase one failed. There is nothing to read -- the target never
// took the pointer -- but the bus is HELD, so it cannot simply be
// abandoned. Frame it first.
if (seq_is_read) state <= Q_ABND;
else state <= Q_DONE; // a plain write already framed
end else if (!seq_is_read) begin
seq_ok <= 1'b1;
state <= Q_DONE;
end else begin
phase <= 2'd2;
// Phase two: the SAME address with the READ direction. The
// controller sees a transfer already open and issues a REPEATED
// START rather than a START, which is what keeps the two phases one
// transaction.
cmd_addr <= seq_addr;
cmd_read <= 1'b1;
cmd_len <= seq_rdlen;
cmd_stop <= 1'b1;
cmd_valid <= 1'b1;
state <= Q_P2;
end
end
end
Q_P2: if (txn_busy) state <= Q_P2W;
Q_P2W: begin
if (txn_done) begin
seq_err <= seq_err | txn_err;
seq_bytes <= seq_bytes + txn_bytes;
seq_ok <= txn_ok;
state <= Q_DONE;
end
end
Q_ABND: begin
// A zero-length write with a STOP: the cheapest legal way to frame a bus
// this master is holding. It re-addresses the target, which will NACK
// again, and then stops -- and the NACK is harmless because the whole
// point is the STOP.
//
// The alternative, releasing the lines directly, is not available: a
// master that just lets go leaves every device mid-transfer, and Chapter
// 15.4 shows there is no protocol remedy for a bus in that state.
abandoned <= 1'b1;
cmd_addr <= seq_addr;
cmd_read <= 1'b0;
cmd_len <= 4'd0;
cmd_stop <= 1'b1;
cmd_valid <= 1'b1;
state <= Q_ABW;
end
Q_ABW: begin
if (txn_done) begin
// The abandon phase's own errors are not the sequence's errors: the
// failure being reported is phase one's, and re-reporting the NACK
// from the framing access would tell the host about a transfer it
// never asked for.
seq_ok <= 1'b0;
state <= Q_DONE;
end
end
Q_DONE: begin
seq_done <= 1'b1;
seq_busy <= 1'b0;
phase <= 2'd0;
sequences <= sequences + 1'b1;
state <= Q_IDLE;
end
default: state <= Q_IDLE;
endcase
end
end
endmodule // -----------------------------------------------------------------------------
// i2c_combined_txn.sv
// Two phases, one transaction, and the bus never released in between.
//
// THE PATTERN. §3.1.10 format 3: "During a change of direction within a transfer, the
// START condition and the slave address are both repeated, but with the R/W bit
// reversed." That is the register-pointer access of Chapter 10.1 and Chapter 16.1 --
// write the location, turn the bus around, read from it -- and it is the dominant
// transaction shape on the whole bus.
//
// WHY IT NEEDS ITS OWN BLOCK, above the transaction controller. The controller of
// Chapter 17.8 executes ONE phase. Nothing in it guarantees that a second phase follows
// the first, and on a multi-master bus that guarantee is the entire point:
//
// §3.1.10 format 3 holds the bus across the turnaround. A STOP there would release the
// arbitration this master already won, and another master may take the bus in the gap
// and write its OWN pointer. The read that follows then returns that master's data --
// plausible, wrong, and with no error anywhere. Chapter 10.3 is the general statement;
// Chapter 16.2 §1 is where it bites.
//
// So atomicity has to be enforced by something that owns BOTH phases, and that is this
// block. It issues phase one with `stop = 0`, waits, and issues phase two -- and it will
// not accept a new descriptor in between.
//
// AND IT HAS TO HANDLE A FAILURE IN PHASE ONE. If the pointer write is NACKed there is
// no point reading: the target never took the address. But the bus is still HELD, so the
// block cannot simply report and stop -- it must frame the bus before giving up, or it
// leaves every device believing a transfer is in progress. That is the case a two-phase
// sequencer most often gets wrong, and it is the reason `abandon` exists below.
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_combined_txn #(
parameter CNT_W = 16
) (
input wire clk,
input wire rst_n,
// ---- the descriptor: one combined access --------------------------------
input wire seq_valid,
input wire [6:0] seq_addr,
input wire [7:0] seq_ptr, // the byte written in phase one
input wire [3:0] seq_rdlen, // bytes read in phase two; 0 = write-only
input wire seq_is_read, // 0 = a plain write of seq_ptr then payload
// ---- to the transaction controller --------------------------------------
output reg cmd_valid,
output reg [6:0] cmd_addr,
output reg cmd_read,
output reg [3:0] cmd_len,
output reg cmd_stop,
input wire txn_done,
input wire txn_ok,
input wire [5:0] txn_err,
input wire [3:0] txn_bytes,
input wire txn_busy,
// ---- results ------------------------------------------------------------
output reg seq_done,
output reg seq_ok,
output reg [5:0] seq_err,
output reg [3:0] seq_bytes,
output reg seq_busy,
output reg abandoned, // phase one failed and the bus was framed anyway
output reg [1:0] phase,
output reg [2:0] state,
output reg [CNT_W-1:0] sequences
);
localparam [2:0] Q_IDLE = 3'd0,
Q_P1 = 3'd1, // phase one: write the pointer, NO stop
Q_P1W = 3'd2, // waiting for phase one
Q_P2 = 3'd3, // phase two: the turnaround and the read
Q_P2W = 3'd4,
Q_ABND = 3'd5, // phase one failed: frame the bus before reporting
Q_ABW = 3'd6,
Q_DONE = 3'd7;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= Q_IDLE;
cmd_valid <= 1'b0;
cmd_addr <= 7'h00;
cmd_read <= 1'b0;
cmd_len <= 4'd0;
cmd_stop <= 1'b1;
seq_done <= 1'b0;
seq_ok <= 1'b0;
seq_err <= 6'd0;
seq_bytes <= 4'd0;
seq_busy <= 1'b0;
abandoned <= 1'b0;
phase <= 2'd0;
sequences <= {CNT_W{1'b0}};
end else begin
cmd_valid <= 1'b0;
seq_done <= 1'b0;
case (state)
Q_IDLE: begin
seq_busy <= 1'b0;
if (seq_valid && !txn_busy) begin
seq_err <= 6'd0;
seq_ok <= 1'b0;
seq_bytes <= 4'd0;
abandoned <= 1'b0;
seq_busy <= 1'b1;
phase <= 2'd1;
// Phase one: address with the WRITE direction, one data byte -- the
// pointer -- and crucially NO STOP. The bus stays held.
cmd_addr <= seq_addr;
cmd_read <= 1'b0;
cmd_len <= 4'd1;
cmd_stop <= ~seq_is_read; // a plain write ends here
cmd_valid <= 1'b1;
state <= Q_P1;
end
end
Q_P1: if (txn_busy) state <= Q_P1W;
Q_P1W: begin
if (txn_done) begin
seq_err <= seq_err | txn_err;
seq_bytes <= seq_bytes + txn_bytes;
if (!txn_ok) begin
// Phase one failed. There is nothing to read -- the target never
// took the pointer -- but the bus is HELD, so it cannot simply be
// abandoned. Frame it first.
if (seq_is_read) state <= Q_ABND;
else state <= Q_DONE; // a plain write already framed
end else if (!seq_is_read) begin
seq_ok <= 1'b1;
state <= Q_DONE;
end else begin
phase <= 2'd2;
// Phase two: the SAME address with the READ direction. The
// controller sees a transfer already open and issues a REPEATED
// START rather than a START, which is what keeps the two phases one
// transaction.
cmd_addr <= seq_addr;
cmd_read <= 1'b1;
cmd_len <= seq_rdlen;
cmd_stop <= 1'b1;
cmd_valid <= 1'b1;
state <= Q_P2;
end
end
end
Q_P2: if (txn_busy) state <= Q_P2W;
Q_P2W: begin
if (txn_done) begin
seq_err <= seq_err | txn_err;
seq_bytes <= seq_bytes + txn_bytes;
seq_ok <= txn_ok;
state <= Q_DONE;
end
end
Q_ABND: begin
// A zero-length write with a STOP: the cheapest legal way to frame a bus
// this master is holding. It re-addresses the target, which will NACK
// again, and then stops -- and the NACK is harmless because the whole
// point is the STOP.
//
// The alternative, releasing the lines directly, is not available: a
// master that just lets go leaves every device mid-transfer, and Chapter
// 15.4 shows there is no protocol remedy for a bus in that state.
abandoned <= 1'b1;
cmd_addr <= seq_addr;
cmd_read <= 1'b0;
cmd_len <= 4'd0;
cmd_stop <= 1'b1;
cmd_valid <= 1'b1;
state <= Q_ABW;
end
Q_ABW: begin
if (txn_done) begin
// The abandon phase's own errors are not the sequence's errors: the
// failure being reported is phase one's, and re-reporting the NACK
// from the framing access would tell the host about a transfer it
// never asked for.
seq_ok <= 1'b0;
state <= Q_DONE;
end
end
Q_DONE: begin
seq_done <= 1'b1;
seq_busy <= 1'b0;
phase <= 2'd0;
sequences <= sequences + 1'b1;
state <= Q_IDLE;
end
default: state <= Q_IDLE;
endcase
end
end
endmodule -- ---------------------------------------------------------------------------
-- i2c_combined_txn.vhd
-- Two phases, one transaction, and the bus never released in between.
-- Behavioural twin of i2c_combined_txn.sv / .v.
--
-- THE PATTERN. §3.1.10 format 3: "During a change of direction within a transfer, the START
-- condition and the slave address are both repeated, but with the R/W bit reversed." That is
-- the register-pointer access of Chapter 10.1 and Chapter 16.1 -- write the location, turn the
-- bus around, read from it -- and it is the dominant transaction shape on the whole bus.
--
-- WHY IT NEEDS ITS OWN BLOCK, above the transaction controller. The controller of Chapter 17.8
-- executes ONE phase, and nothing in it guarantees that a second follows the first. On a
-- multi-master bus that guarantee is the entire point: a STOP at the turnaround would release
-- the arbitration this master already won, another master could take the bus in the gap and
-- write its OWN pointer, and the read that follows would return that master's data --
-- plausible, wrong, and with no error anywhere.
--
-- AND IT HAS TO HANDLE A FAILURE IN PHASE ONE. If the pointer write is NACKed there is no
-- point reading. But the bus is still HELD, so the block cannot simply report and stop -- it
-- must frame the bus first, or it leaves every device believing a transfer is in progress.
-- That is the case a two-phase sequencer most often gets wrong.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_combined_txn is
generic (
CNT_W : integer := 16
);
port (
clk : in std_logic;
rst_n : in std_logic;
seq_valid : in std_logic;
seq_addr : in std_logic_vector(6 downto 0);
seq_ptr : in std_logic_vector(7 downto 0);
seq_rdlen : in unsigned(3 downto 0);
seq_is_read : in std_logic;
cmd_valid : out std_logic;
cmd_addr : out std_logic_vector(6 downto 0);
cmd_read : out std_logic;
cmd_len : out unsigned(3 downto 0);
cmd_stop : out std_logic;
txn_done : in std_logic;
txn_ok : in std_logic;
txn_err : in std_logic_vector(5 downto 0);
txn_bytes : in unsigned(3 downto 0);
txn_busy : in std_logic;
seq_done : out std_logic;
seq_ok : out std_logic;
seq_err : out std_logic_vector(5 downto 0);
seq_bytes : out unsigned(3 downto 0);
seq_busy : out std_logic;
abandoned : out std_logic;
phase : out unsigned(1 downto 0);
state : out unsigned(2 downto 0);
sequences : out unsigned(CNT_W-1 downto 0)
);
end entity i2c_combined_txn;
architecture rtl of i2c_combined_txn is
constant Q_IDLE : integer := 0;
constant Q_P1 : integer := 1; -- phase one: write the pointer, NO stop
constant Q_P1W : integer := 2;
constant Q_P2 : integer := 3; -- phase two: the turnaround and the read
constant Q_P2W : integer := 4;
constant Q_ABND : integer := 5; -- phase one failed: frame the bus before reporting
constant Q_ABW : integer := 6;
constant Q_DONE : integer := 7;
signal st : integer range 0 to 7 := Q_IDLE;
signal ph : unsigned(1 downto 0) := (others => '0');
signal e_i : std_logic_vector(5 downto 0) := (others => '0');
signal nb : unsigned(3 downto 0) := (others => '0');
signal bsy, okk, abnd : std_logic := '0';
signal n_seq : unsigned(CNT_W-1 downto 0) := (others => '0');
begin
phase <= ph;
state <= to_unsigned(st, 3);
seq_err <= e_i;
seq_bytes <= nb;
seq_busy <= bsy;
seq_ok <= okk;
abandoned <= abnd;
sequences <= n_seq;
process (clk, rst_n)
begin
if rst_n = '0' then
st <= Q_IDLE;
cmd_valid <= '0';
cmd_addr <= (others => '0');
cmd_read <= '0';
cmd_len <= (others => '0');
cmd_stop <= '1';
seq_done <= '0';
okk <= '0';
e_i <= (others => '0');
nb <= (others => '0');
bsy <= '0';
abnd <= '0';
ph <= (others => '0');
n_seq <= (others => '0');
elsif rising_edge(clk) then
cmd_valid <= '0';
seq_done <= '0';
case st is
when Q_IDLE =>
bsy <= '0';
if seq_valid = '1' and txn_busy = '0' then
e_i <= (others => '0');
okk <= '0';
nb <= (others => '0');
abnd <= '0';
bsy <= '1';
ph <= to_unsigned(1, 2);
-- Phase one: address with the WRITE direction, one data byte -- the
-- pointer -- and crucially NO STOP. The bus stays held.
cmd_addr <= seq_addr;
cmd_read <= '0';
cmd_len <= to_unsigned(1, 4);
cmd_stop <= not seq_is_read; -- a plain write ends here
cmd_valid <= '1';
st <= Q_P1;
end if;
when Q_P1 =>
if txn_busy = '1' then st <= Q_P1W; end if;
when Q_P1W =>
if txn_done = '1' then
e_i <= e_i or txn_err;
nb <= nb + txn_bytes;
if txn_ok = '0' then
-- Phase one failed. There is nothing to read -- the target never took
-- the pointer -- but the bus is HELD, so it cannot simply be abandoned.
if seq_is_read = '1' then st <= Q_ABND;
else st <= Q_DONE; -- a plain write already framed
end if;
elsif seq_is_read = '0' then
okk <= '1';
st <= Q_DONE;
else
ph <= to_unsigned(2, 2);
-- Phase two: the SAME address with the READ direction. The controller
-- sees a transfer already open and issues a REPEATED START rather than a
-- START, which is what keeps the two phases one transaction.
cmd_addr <= seq_addr;
cmd_read <= '1';
cmd_len <= seq_rdlen;
cmd_stop <= '1';
cmd_valid <= '1';
st <= Q_P2;
end if;
end if;
when Q_P2 =>
if txn_busy = '1' then st <= Q_P2W; end if;
when Q_P2W =>
if txn_done = '1' then
e_i <= e_i or txn_err;
nb <= nb + txn_bytes;
okk <= txn_ok;
st <= Q_DONE;
end if;
when Q_ABND =>
-- A zero-length write with a STOP: the cheapest legal way to frame a bus this
-- master is holding. It re-addresses the target, which will NACK again, and
-- then stops -- and the NACK is harmless because the whole point is the STOP.
--
-- The alternative, releasing the lines directly, is not available: a master
-- that just lets go leaves every device mid-transfer, and Chapter 15.4 shows
-- there is no protocol remedy for a bus in that state.
abnd <= '1';
cmd_addr <= seq_addr;
cmd_read <= '0';
cmd_len <= (others => '0');
cmd_stop <= '1';
cmd_valid <= '1';
st <= Q_ABW;
when Q_ABW =>
if txn_done = '1' then
-- The abandon phase's own errors are not the sequence's errors: the failure
-- being reported is phase one's, and re-reporting the NACK from the framing
-- access would tell the host about a transfer it never asked for.
okk <= '0';
st <= Q_DONE;
end if;
when Q_DONE =>
seq_done <= '1';
bsy <= '0';
ph <= (others => '0');
n_seq <= n_seq + 1;
st <= Q_IDLE;
when others =>
st <= Q_IDLE;
end case;
end if;
end process;
end architecture rtl;5a. The testbenches
Thirteen tests. The bench carries three devices: a normal target at 0x50, an absent address at 0x51, and a target at 0x53 that acknowledges its address and then refuses the pointer byte. The third was added after mutation testing; see §6.
| # | Test | Property |
|---|---|---|
| T1 | the pattern — write a pointer, turn around, read | from one descriptor |
| T2 | the property that matters — two STARTs, exactly one STOP | |
| T3 | the address is repeated with the direction reversed | §3.1.10 format 3, literally |
| T4 | a multi-byte read in phase two | the ACK policy across the boundary |
| T5 | a write-only descriptor is one phase, and ends with a STOP | same block |
| T6 | phase one fails — nothing at 0x51, so phase two is not attempted | |
| T7 | and the bus is framed anyway | the abandon path ran |
| T8 | a descriptor arriving mid-sequence is ignored | accepting one would insert a phase |
| T9 | a stretching target across the turnaround | |
| T10 | two sequences back to back, nothing leaks | |
| T11 | the byte count spans both phases | one host operation |
| T12 | the invariants — no mid-byte framing anywhere | |
| T13 | phase one fails on a data NACK | see §6 |
`timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_combined_txn_tb.sv
// Independent oracle for i2c_combined_txn.
//
// The sequencer sits on top of the complete master of Chapter 17.8 and turns a single
// descriptor into two phases. So the bench issues ONE descriptor and then checks, on the
// wire, that the bus was never released in between -- which is the only property that
// distinguishes a combined transaction from two separate ones, and the only one that
// matters on a shared bus.
//
// The monitor's START and STOP counts are the evidence. Two STARTs and ONE STOP is a
// combined transaction; two STARTs and two STOPs is two transactions that happen to be
// adjacent, and on a multi-master bus those behave completely differently.
// -----------------------------------------------------------------------------
module i2c_combined_txn_tb;
localparam integer NL = 8, NH = 4, NSU = 2, NSMP = 2;
localparam integer NHD = 3, NSUA = 3, NSUO = 3, NBUF = 3;
localparam [6:0] TADDR = 7'h50;
localparam [3:0] S_IDLE = 4'd0, S_DONE = 4'd8;
logic clk = 1'b0, rst_n = 1'b0;
logic [7:0] payload [0:7];
// The descriptor the bench issues, and the sequencer's answer.
logic seq_valid = 1'b0, seq_is_read = 1'b0;
logic [6:0] seq_addr = TADDR;
logic [7:0] seq_ptr = 8'h00;
logic [3:0] seq_rdlen = 4'd1;
logic seq_done, seq_ok, seq_busy, abandoned;
logic [5:0] seq_err;
logic [3:0] seq_bytes;
logic [1:0] seq_phase;
logic [2:0] seq_state;
logic [15:0] n_seq;
// The sequencer owns the controller's command port.
logic cmd_valid, cmd_read, cmd_stop;
logic [6:0] cmd_addr;
logic [3:0] cmd_len;
// ---- the master ---------------------------------------------------------
logic do_start, do_restart, do_stop, scl_yield, gen_enable, gen_idle_low;
logic byte_go, byte_dir_write, byte_ack_send;
logic [7:0] byte_tx;
logic [3:0] tx_index, rx_index, tstate;
logic [7:0] rx_data;
logic rx_we, txn_done, txn_ok, txn_busy, addr_nack, data_nack;
logic [5:0] txn_err;
logic [3:0] txn_bytes;
logic [15:0] n_txn;
logic g_scl_low, drive_point, sample_point, g_rise, g_fall, g_stretch;
logic [15:0] g_scyc, 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, b_busy, b_ack, b_ackv, b_done;
logic [7:0] b_rx;
logic [3:0] b_bidx;
logic [15:0] b_bytes, b_acks, b_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_conf;
logic [3:0] grant;
logic [15:0] n_conf, n_arb;
wire m_scl_low = f_scl_low | g_scl_low;
logic t_scl_low, t_sda_low, scl, sda;
logic [3:0] scl_in, sda_in, scl_rbl, sda_rbl;
logic [7:0] scl_h, sda_h;
logic load_en = 1'b0; reg [7:0] load_addr = 8'h00, load_data = 8'h00;
logic tgt_hold_scl = 1'b0;
// Which target to talk to in a given test: the acknowledging one, or one configured
// to refuse. Both sit on the bus; only one of them owns TADDR at a time, selected by
// the parameterised address of the second instance.
// A THIRD device: a target that acknowledges its own address and then REFUSES its
// FIRST data byte -- which in a combined transaction is the POINTER. That makes phase
// one fail on a DATA nack rather than an address NACK, and the two are not
// interchangeable: on an address NACK the abandon path re-addresses a target that
// refuses again, so nothing it sends can reach the bus. On a data NACK the target
// ACKNOWLEDGES the abandon path's address, and anything the abandon path sends is
// really transmitted. Without this device the abandon descriptor's length is
// unobservable.
localparam [6:0] TADDR_NACKPTR = 7'h53;
logic t3_scl_low, t3_sda_low, t3_sel, t3_dirrd, t3_wv;
logic [7:0] t3_lw;
logic [15:0] t3_rx, t3_tx, t3_nsta, t3_nsto;
logic [2:0] t3_st;
i2c_line_model #(.N_DEV(4)) bus (
.scl_drive_low({t3_scl_low, tgt_hold_scl, t_scl_low, m_scl_low}),
.sda_drive_low({t3_sda_low, 1'b0, 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_scyc),
.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)) u_by (
.clk(clk), .rst_n(rst_n),
.drive_point(drive_point), .sample_point(sample_point),
.go(byte_go), .dir_write(byte_dir_write), .tx_byte(byte_tx),
.ack_to_send(byte_ack_send),
.sda_in(sda_in[0]), .scl_high(scl_in[0]), .abort(arb_lost),
.sda_req(b_sda_req), .sda_bit(b_sda_bit),
.rx_byte(b_rx), .ack(b_ack), .ack_valid(b_ackv), .byte_done(b_done),
.busy(b_busy), .bit_index(b_bidx), .driving(b_driving),
.bytes_done(b_bytes), .acks(b_acks), .nacks(b_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_conf), .conflicts(n_conf),
.arb_loss_now(arb_now), .arb_lost(arb_lost), .arb_losses(n_arb),
.arb_clear(txn_done));
i2c_txn_ctrl #(.CNT_W(16)) u_txn (
.clk(clk), .rst_n(rst_n),
.cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
.cmd_len(cmd_len), .cmd_stop(cmd_stop),
.do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
.scl_yield(scl_yield), .frame_done(f_done), .frame_busy(f_busy),
.bus_free(f_bus_free), .frame_started(f_started),
.gen_enable(gen_enable), .gen_idle_low(gen_idle_low),
.byte_go(byte_go), .byte_dir_write(byte_dir_write), .byte_tx(byte_tx),
.byte_ack_send(byte_ack_send), .byte_done(b_done), .byte_busy(b_busy),
.byte_ack(b_ack), .byte_rx(b_rx),
.tx_data(payload[tx_index[2:0]]), .tx_index(tx_index),
.rx_data(rx_data), .rx_index(rx_index), .rx_we(rx_we),
.arb_lost(arb_lost),
.txn_done(txn_done), .txn_ok(txn_ok), .txn_err(txn_err),
.txn_bytes(txn_bytes), .txn_busy(txn_busy),
.addr_nack(addr_nack), .data_nack(data_nack),
.state(tstate), .transactions(n_txn));
i2c_combined_txn #(.CNT_W(16)) dut (
.clk(clk), .rst_n(rst_n),
.seq_valid(seq_valid), .seq_addr(seq_addr), .seq_ptr(seq_ptr),
.seq_rdlen(seq_rdlen), .seq_is_read(seq_is_read),
.cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
.cmd_len(cmd_len), .cmd_stop(cmd_stop),
.txn_done(txn_done), .txn_ok(txn_ok), .txn_err(txn_err),
.txn_bytes(txn_bytes), .txn_busy(txn_busy),
.seq_done(seq_done), .seq_ok(seq_ok), .seq_err(seq_err),
.seq_bytes(seq_bytes), .seq_busy(seq_busy), .abandoned(abandoned),
.phase(seq_phase), .state(seq_state), .sequences(n_seq));
// Phase one's single data byte is the pointer, so the payload's first slot follows the
// descriptor rather than being written by hand.
always @(*) payload[0] = seq_ptr;
logic t_sel, t_dirrd, t_wv;
logic [7:0] t_lw;
logic [15:0] t_rx, t_tx, t_nsta, t_nsto;
logic [2:0] t_st;
i2c_target_model #(.MY_ADDR(TADDR_NACKPTR), .ACK_ADDR(1'b1), .STRETCH_AFTER(0),
.NACK_AT(1), .N_MEM(16), .CNT_W(16)) u_tgt3 (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.scl_drive_low(t3_scl_low), .sda_drive_low(t3_sda_low),
.load_en(1'b0), .load_addr(8'h00), .load_data(8'h00),
.selected(t3_sel), .dir_read(t3_dirrd), .last_written(t3_lw), .write_valid(t3_wv),
.bytes_rx(t3_rx), .bytes_tx(t3_tx), .n_starts(t3_nsta), .n_stops(t3_nsto),
.state(t3_st));
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_lw), .write_valid(t_wv),
.bytes_rx(t_rx), .bytes_tx(t_tx), .n_starts(t_nsta), .n_stops(t_nsto),
.state(t_st));
logic m_start, m_stop, m_bit, m_bitv, m_byte, m_ack, m_ackv, m_intr, m_mid;
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 [15:0] ack_hist;
integer n_acks_seen;
logic [7:0] rxlog [0:7];
integer n_rx;
// Was the bus ever released between the two phases? Recorded from the wire.
integer stops_during;
always @(negedge clk) begin
if (rst_n) begin
if (m_ack) begin ack_hist = {ack_hist[14:0], m_ackv}; n_acks_seen = n_acks_seen + 1; end
if (rx_we) begin rxlog[n_rx[2:0]] = rx_data; n_rx = n_rx + 1; end
// A STOP while the sequencer is between phases is the failure this whole block
// exists to prevent.
if (m_stop && seq_phase == 2'd1) stops_during = stops_during + 1;
end
end
task step; begin @(posedge clk); @(negedge clk); end endtask
task do_reset;
begin
@(negedge clk);
rst_n = 1'b0; seq_valid = 1'b0; seq_is_read = 1'b0;
seq_addr = TADDR; seq_ptr = 8'h00; seq_rdlen = 4'd1;
load_en = 1'b0; tgt_hold_scl = 1'b0;
ack_hist = 16'h0000; n_acks_seen = 0; n_rx = 0; stops_during = 0;
for (k = 0; k < 8; k = k + 1) payload[k] = 8'h00;
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 issue_seq (input [6:0] a, input [7:0] p, input [3:0] rl, input rd);
begin
@(negedge clk);
seq_addr = a; seq_ptr = p; seq_rdlen = rl; seq_is_read = rd; seq_valid = 1'b1;
@(posedge clk); @(negedge clk); seq_valid = 1'b0;
end
endtask
task wait_seq (input integer max_cycles);
begin
n = 0;
while (!seq_done && n < max_cycles) begin step; n = n + 1; end
if (n >= max_cycles) begin
$display(" FAIL wait_seq: stuck in seq state %0d, txn state %0d",
seq_state, tstate);
errors = errors + 1;
end
end
endtask
task ck_int (input [200*8:1] what, input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0d (0x%0h) expected %0d (0x%0h)", what, g, g, e, e);
errors = errors + 1;
end
end
endtask
task ck_bit (input [200*8:1] what, input g, input e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0b expected %0b", what, g, e);
errors = errors + 1;
end
end
endtask
initial begin
$display("=== i2c_combined_txn: two phases, and the bus never released between them ===");
// ----------------------------------------------------------------
// T1. THE PATTERN. Write a pointer, turn the bus around, read one byte -- from one
// descriptor. §3.1.10 format 3, and Chapter 16.1's register access.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'h4D);
issue_seq(TADDR, 8'h00, 4'd1, 1'b1);
wait_seq(8000);
$display("T1 write a pointer, turn the bus around, read: one descriptor");
ck_bit("T1 succeeded", seq_ok, 1'b1);
ck_int("T1 the byte came back", rxlog[0], 8'h4D);
ck_int("T1 two phases ran", n_seq, 1);
ck_bit("T1 not abandoned", abandoned, 1'b0);
// ----------------------------------------------------------------
// T2. THE PROPERTY THAT MATTERS. Two STARTs and exactly ONE STOP. Two STOPs would
// mean two transactions, and on a shared bus another master could take the bus
// in the gap and write its own pointer.
// ----------------------------------------------------------------
$display("T2 two STARTs and exactly one STOP: one transaction, not two");
ck_int("T2 two STARTs", m_nsta, 2);
ck_int("T2 one STOP", m_nsto, 1);
ck_int("T2 and no STOP happened between the phases", stops_during, 0);
ck_int("T2 the framer logged one initial START", n_sta, 1);
ck_int("T2 and one REPEATED START", n_rs, 1);
// ----------------------------------------------------------------
// T3. THE ADDRESS IS REPEATED WITH THE DIRECTION REVERSED, which is the literal
// text of format 3. Both address bytes are on the wire and differ by one bit.
// ----------------------------------------------------------------
$display("T3 the address appears twice, differing only in the direction bit");
ck_int("T3 four acknowledge slots: two addresses and two data", n_acks_seen, 4);
// history, most recent first: [0] the read's data NACK, [1] read address ACK,
// [2] the pointer byte's ACK, [3] the write address ACK
ck_bit("T3 the write address was acknowledged", ack_hist[3], 1'b0);
ck_bit("T3 the pointer byte was acknowledged", ack_hist[2], 1'b0);
ck_bit("T3 the read address was acknowledged", ack_hist[1], 1'b0);
ck_bit("T3 and the single read byte was NOT", ack_hist[0], 1'b1);
// ----------------------------------------------------------------
// T4. A MULTI-BYTE READ in phase two, with the ACK policy across the boundary.
// ----------------------------------------------------------------
do_reset;
for (k = 0; k < 3; k = k + 1) preload(k[7:0], 8'h60 + k[7:0]);
issue_seq(TADDR, 8'h00, 4'd3, 1'b1);
wait_seq(10000);
$display("T4 three bytes read in phase two, acknowledged correctly");
ck_bit("T4 succeeded", seq_ok, 1'b1);
ck_int("T4 three bytes stored", n_rx, 3);
ck_int("T4 the first", rxlog[0], 8'h60);
ck_int("T4 the last", rxlog[2], 8'h62);
ck_int("T4 two STARTs", m_nsta, 2);
ck_int("T4 one STOP", m_nsto, 1);
ck_bit("T4 the last read byte was NOT acknowledged", ack_hist[0], 1'b1);
ck_bit("T4 but the one before it was", ack_hist[1], 1'b0);
// ----------------------------------------------------------------
// T5. A WRITE-ONLY DESCRIPTOR is one phase, and ends with a STOP. The same block
// handles it, because a pointer write with nothing after it is the degenerate
// case rather than a different operation.
// ----------------------------------------------------------------
do_reset;
issue_seq(TADDR, 8'h7B, 4'd0, 1'b0);
wait_seq(6000);
$display("T5 a write-only descriptor is one phase and frames itself");
ck_bit("T5 succeeded", seq_ok, 1'b1);
ck_int("T5 one START", m_nsta, 1);
ck_int("T5 one STOP", m_nsto, 1);
ck_int("T5 the target received the byte", t_lw, 8'h7B);
ck_int("T5 and one byte was counted", seq_bytes, 1);
// ----------------------------------------------------------------
// T6. PHASE ONE FAILS. Nothing is at 0x51, so the pointer write is NACKed -- and
// there is no point reading. But the bus is HELD, and this is the case a
// two-phase sequencer most often gets wrong.
// ----------------------------------------------------------------
do_reset;
issue_seq(7'h51, 8'h00, 4'd2, 1'b1);
wait_seq(10000);
$display("T6 phase one fails, so phase two is not attempted");
ck_bit("T6 did not succeed", seq_ok, 1'b0);
ck_bit("T6 reported as an address NACK", seq_err[0], 1'b1);
ck_int("T6 no bytes were read", n_rx, 0);
// ----------------------------------------------------------------
// T7. AND THE BUS IS FRAMED ANYWAY. A sequencer that reported the failure and
// stopped would leave every device on the bus believing a transfer was in
// progress -- and Chapter 15.4 shows there is no protocol remedy for that.
// ----------------------------------------------------------------
$display("T7 and the bus is framed before giving up, not simply abandoned");
ck_bit("T7 the abandon path ran", abandoned, 1'b1);
ck_bit("T7 the bus is idle", m_intr, 1'b0);
if (m_nsto < 1) begin
$display(" FAIL T7 the bus was left unframed after a failed phase one");
errors = errors + 1;
end
ck_bit("T7 both lines released", m_scl_low | m_sda_low, 1'b0);
// ----------------------------------------------------------------
// T8. A DESCRIPTOR ARRIVING MID-SEQUENCE IS IGNORED. Accepting one would insert a
// phase into a transaction already in flight, which is exactly the atomicity
// this block exists to guarantee.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'h2A);
issue_seq(TADDR, 8'h00, 4'd1, 1'b1);
n = 0;
while (seq_phase != 2'd1 && n < 2000) begin step; n = n + 1; end
k = n_seq;
issue_seq(TADDR, 8'h05, 4'd1, 1'b1); // a second descriptor, mid-sequence
issue_seq(TADDR, 8'h06, 4'd1, 1'b1);
wait_seq(10000);
$display("T8 a descriptor arriving mid-sequence is ignored, not interleaved");
ck_int("T8 only one sequence completed", n_seq, k + 1);
ck_int("T8 two STARTs, not four", m_nsta, 2);
ck_int("T8 one STOP", m_nsto, 1);
ck_int("T8 and the original pointer was used", rxlog[0], 8'h2A);
// ----------------------------------------------------------------
// T9. A STRETCHING TARGET across the turnaround. The repeated START releases SCL,
// which is precisely where §3.1.6's byte-level handshake happens, so this is
// the most likely place on the whole bus for a stretch to occur.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'h8E);
issue_seq(TADDR, 8'h00, 4'd1, 1'b1);
n = 0;
while (n_acks_seen < 2 && n < 4000) begin step; n = n + 1; end
// One extra step, so the three language variants finish at the same time.
// `n_acks_seen` is a signal in the VHDL twin, read by the stimulus a delta after the
// logging process assigns it, so the loop there exits one cycle later and the stretch
// is injected one cycle later too. Keeping the step in all three is cheaper than
// letting the benches differ, and the stretch lands in the same place either way.
step;
@(negedge clk); tgt_hold_scl = 1'b1; // stretch at the turnaround
for (k = 0; k < 40; k = k + 1) step;
@(negedge clk); tgt_hold_scl = 1'b0;
wait_seq(12000);
$display("T9 a stretch at the turnaround, which is where stretches happen");
ck_bit("T9 still succeeded", seq_ok, 1'b1);
ck_int("T9 the byte came back intact", rxlog[0], 8'h8E);
ck_int("T9 still two STARTs and one STOP", m_nsta + m_nsto, 3);
ck_int("T9 and the stretch did not become a STOP", stops_during, 0);
// ----------------------------------------------------------------
// T10. TWO SEQUENCES BACK TO BACK. Each frames itself, and nothing leaks: the
// second must issue a fresh START rather than continuing the first.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'hA3);
preload(8'h01, 8'hB4);
issue_seq(TADDR, 8'h00, 4'd1, 1'b1);
wait_seq(8000);
ck_int("T10 the first sequence read its byte", rxlog[0], 8'hA3);
issue_seq(TADDR, 8'h01, 4'd1, 1'b1);
wait_seq(8000);
$display("T10 two sequences back to back, each framing itself");
ck_int("T10 two sequences", n_seq, 2);
ck_int("T10 four STARTs in total", m_nsta, 4);
ck_int("T10 and two STOPs, one per sequence", m_nsto, 2);
ck_int("T10 two initial STARTs", n_sta, 2);
ck_int("T10 and two repeated STARTs", n_rs, 2);
// ----------------------------------------------------------------
// T11. THE BYTE COUNT SPANS BOTH PHASES, because the host asked for one operation
// and wants to know how much of it happened.
// ----------------------------------------------------------------
do_reset;
for (k = 0; k < 4; k = k + 1) preload(k[7:0], 8'h50 + k[7:0]);
issue_seq(TADDR, 8'h00, 4'd4, 1'b1);
wait_seq(12000);
$display("T11 the byte count spans both phases of the one operation");
ck_int("T11 one pointer byte plus four read bytes", seq_bytes, 5);
ck_bit("T11 succeeded", seq_ok, 1'b1);
ck_int("T11 four bytes reached the host", n_rx, 4);
// ----------------------------------------------------------------
// ----------------------------------------------------------------
// T13. PHASE ONE FAILS ON A DATA NACK, not an address NACK. The target takes its
// address and then refuses the pointer byte, so the abandon path addresses a
// target that WILL acknowledge -- and therefore anything the abandon
// descriptor carries is really transmitted onto the bus.
//
// This is the only configuration in which the abandon descriptor's LENGTH is
// observable. With an address-NACK failure the re-addressed target refuses
// again and no data byte can leave, so a sequencer that framed the bus with a
// four-byte write instead of a zero-length probe would look identical.
// ----------------------------------------------------------------
do_reset;
issue_seq(TADDR_NACKPTR, 8'h00, 4'd2, 1'b1);
wait_seq(10000);
$display("T13 phase one can fail on a DATA nack, and the abandon path must stay empty");
ck_bit("T13 did not succeed", seq_ok, 1'b0);
ck_bit("T13 reported as a data NACK, not an address NACK", seq_err[1], 1'b1);
ck_bit("T13 and the address-NACK bit is clear", seq_err[0], 1'b0);
ck_bit("T13 the abandon path ran", abandoned, 1'b1);
ck_int("T13 no bytes were read", n_rx, 0);
// The refusing target saw the pointer byte of phase one, and the abandon path must
// have added NO data bytes of its own -- it is a zero-length framing probe.
ck_int("T13 the target received exactly the one refused pointer byte", t3_rx, 1);
if (m_nsto < 1) begin
$display(" FAIL T13 the bus was left unframed after a data-NACK failure");
errors = errors + 1;
end
// T12. THE INVARIANTS. No mid-byte framing anywhere -- every repeated START in
// this bench follows a complete byte and its acknowledge -- and nobody ever
// fought for SDA.
// ----------------------------------------------------------------
$display("T12 no mid-byte framing and no contention, across every sequence");
ck_int("T12 no mid-byte framing", m_nmid, 0);
ck_int("T12 no SDA owner conflicts", n_conf, 0);
ck_int("T12 no arbitration losses", n_arb, 0);
ck_bit("T12 idle", seq_busy, 1'b0);
ck_int("T12 and the sequencer is at rest", seq_state, 0);
if (errors == 0)
$display("=== i2c_combined_txn: ALL CHECKS PASSED ===");
else
$display("=== i2c_combined_txn: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule `timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_combined_txn_tb.sv
// Independent oracle for i2c_combined_txn.
//
// The sequencer sits on top of the complete master of Chapter 17.8 and turns a single
// descriptor into two phases. So the bench issues ONE descriptor and then checks, on the
// wire, that the bus was never released in between -- which is the only property that
// distinguishes a combined transaction from two separate ones, and the only one that
// matters on a shared bus.
//
// The monitor's START and STOP counts are the evidence. Two STARTs and ONE STOP is a
// combined transaction; two STARTs and two STOPs is two transactions that happen to be
// adjacent, and on a multi-master bus those behave completely differently.
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_combined_txn_tb;
localparam integer NL = 8, NH = 4, NSU = 2, NSMP = 2;
localparam integer NHD = 3, NSUA = 3, NSUO = 3, NBUF = 3;
localparam [6:0] TADDR = 7'h50;
localparam [3:0] S_IDLE = 4'd0, S_DONE = 4'd8;
reg clk = 1'b0, rst_n = 1'b0;
reg [7:0] payload [0:7];
// The descriptor the bench issues, and the sequencer's answer.
reg seq_valid = 1'b0, seq_is_read = 1'b0;
reg [6:0] seq_addr = TADDR;
reg [7:0] seq_ptr = 8'h00;
reg [3:0] seq_rdlen = 4'd1;
wire seq_done, seq_ok, seq_busy, abandoned;
wire [5:0] seq_err;
wire [3:0] seq_bytes;
wire [1:0] seq_phase;
wire [2:0] seq_state;
wire [15:0] n_seq;
// The sequencer owns the controller's command port.
wire cmd_valid, cmd_read, cmd_stop;
wire [6:0] cmd_addr;
wire [3:0] cmd_len;
// ---- the master ---------------------------------------------------------
wire do_start, do_restart, do_stop, scl_yield, gen_enable, gen_idle_low;
wire byte_go, byte_dir_write, byte_ack_send;
wire [7:0] byte_tx;
wire [3:0] tx_index, rx_index, tstate;
wire [7:0] rx_data;
wire rx_we, txn_done, txn_ok, txn_busy, addr_nack, data_nack;
wire [5:0] txn_err;
wire [3:0] txn_bytes;
wire [15:0] n_txn;
wire g_scl_low, drive_point, sample_point, g_rise, g_fall, g_stretch;
wire [15:0] g_scyc, 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, b_busy, b_ack, b_ackv, b_done;
wire [7:0] b_rx;
wire [3:0] b_bidx;
wire [15:0] b_bytes, b_acks, b_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_conf;
wire [3:0] grant;
wire [15:0] n_conf, n_arb;
wire m_scl_low = f_scl_low | g_scl_low;
wire t_scl_low, t_sda_low, scl, sda;
wire [3: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;
reg tgt_hold_scl = 1'b0;
// Which target to talk to in a given test: the acknowledging one, or one configured
// to refuse. Both sit on the bus; only one of them owns TADDR at a time, selected by
// the parameterised address of the second instance.
// A THIRD device: a target that acknowledges its own address and then REFUSES its
// FIRST data byte -- which in a combined transaction is the POINTER. That makes phase
// one fail on a DATA nack rather than an address NACK, and the two are not
// interchangeable: on an address NACK the abandon path re-addresses a target that
// refuses again, so nothing it sends can reach the bus. On a data NACK the target
// ACKNOWLEDGES the abandon path's address, and anything the abandon path sends is
// really transmitted. Without this device the abandon descriptor's length is
// unobservable.
localparam [6:0] TADDR_NACKPTR = 7'h53;
wire t3_scl_low, t3_sda_low, t3_sel, t3_dirrd, t3_wv;
wire [7:0] t3_lw;
wire [15:0] t3_rx, t3_tx, t3_nsta, t3_nsto;
wire [2:0] t3_st;
i2c_line_model #(.N_DEV(4)) bus (
.scl_drive_low({t3_scl_low, tgt_hold_scl, t_scl_low, m_scl_low}),
.sda_drive_low({t3_sda_low, 1'b0, 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_scyc),
.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)) u_by (
.clk(clk), .rst_n(rst_n),
.drive_point(drive_point), .sample_point(sample_point),
.go(byte_go), .dir_write(byte_dir_write), .tx_byte(byte_tx),
.ack_to_send(byte_ack_send),
.sda_in(sda_in[0]), .scl_high(scl_in[0]), .abort(arb_lost),
.sda_req(b_sda_req), .sda_bit(b_sda_bit),
.rx_byte(b_rx), .ack(b_ack), .ack_valid(b_ackv), .byte_done(b_done),
.busy(b_busy), .bit_index(b_bidx), .driving(b_driving),
.bytes_done(b_bytes), .acks(b_acks), .nacks(b_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_conf), .conflicts(n_conf),
.arb_loss_now(arb_now), .arb_lost(arb_lost), .arb_losses(n_arb),
.arb_clear(txn_done));
i2c_txn_ctrl #(.CNT_W(16)) u_txn (
.clk(clk), .rst_n(rst_n),
.cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
.cmd_len(cmd_len), .cmd_stop(cmd_stop),
.do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
.scl_yield(scl_yield), .frame_done(f_done), .frame_busy(f_busy),
.bus_free(f_bus_free), .frame_started(f_started),
.gen_enable(gen_enable), .gen_idle_low(gen_idle_low),
.byte_go(byte_go), .byte_dir_write(byte_dir_write), .byte_tx(byte_tx),
.byte_ack_send(byte_ack_send), .byte_done(b_done), .byte_busy(b_busy),
.byte_ack(b_ack), .byte_rx(b_rx),
.tx_data(payload[tx_index[2:0]]), .tx_index(tx_index),
.rx_data(rx_data), .rx_index(rx_index), .rx_we(rx_we),
.arb_lost(arb_lost),
.txn_done(txn_done), .txn_ok(txn_ok), .txn_err(txn_err),
.txn_bytes(txn_bytes), .txn_busy(txn_busy),
.addr_nack(addr_nack), .data_nack(data_nack),
.state(tstate), .transactions(n_txn));
i2c_combined_txn #(.CNT_W(16)) dut (
.clk(clk), .rst_n(rst_n),
.seq_valid(seq_valid), .seq_addr(seq_addr), .seq_ptr(seq_ptr),
.seq_rdlen(seq_rdlen), .seq_is_read(seq_is_read),
.cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
.cmd_len(cmd_len), .cmd_stop(cmd_stop),
.txn_done(txn_done), .txn_ok(txn_ok), .txn_err(txn_err),
.txn_bytes(txn_bytes), .txn_busy(txn_busy),
.seq_done(seq_done), .seq_ok(seq_ok), .seq_err(seq_err),
.seq_bytes(seq_bytes), .seq_busy(seq_busy), .abandoned(abandoned),
.phase(seq_phase), .state(seq_state), .sequences(n_seq));
// Phase one's single data byte is the pointer, so the payload's first slot follows the
// descriptor rather than being written by hand.
always @(*) payload[0] = seq_ptr;
wire t_sel, t_dirrd, t_wv;
wire [7:0] t_lw;
wire [15:0] t_rx, t_tx, t_nsta, t_nsto;
wire [2:0] t_st;
i2c_target_model #(.MY_ADDR(TADDR_NACKPTR), .ACK_ADDR(1'b1), .STRETCH_AFTER(0),
.NACK_AT(1), .N_MEM(16), .CNT_W(16)) u_tgt3 (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.scl_drive_low(t3_scl_low), .sda_drive_low(t3_sda_low),
.load_en(1'b0), .load_addr(8'h00), .load_data(8'h00),
.selected(t3_sel), .dir_read(t3_dirrd), .last_written(t3_lw), .write_valid(t3_wv),
.bytes_rx(t3_rx), .bytes_tx(t3_tx), .n_starts(t3_nsta), .n_stops(t3_nsto),
.state(t3_st));
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_lw), .write_valid(t_wv),
.bytes_rx(t_rx), .bytes_tx(t_tx), .n_starts(t_nsta), .n_stops(t_nsto),
.state(t_st));
wire m_start, m_stop, m_bit, m_bitv, m_byte, m_ack, m_ackv, m_intr, m_mid;
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 [15:0] ack_hist;
integer n_acks_seen;
reg [7:0] rxlog [0:7];
integer n_rx;
// Was the bus ever released between the two phases? Recorded from the wire.
integer stops_during;
always @(negedge clk) begin
if (rst_n) begin
if (m_ack) begin ack_hist = {ack_hist[14:0], m_ackv}; n_acks_seen = n_acks_seen + 1; end
if (rx_we) begin rxlog[n_rx[2:0]] = rx_data; n_rx = n_rx + 1; end
// A STOP while the sequencer is between phases is the failure this whole block
// exists to prevent.
if (m_stop && seq_phase == 2'd1) stops_during = stops_during + 1;
end
end
task step; begin @(posedge clk); @(negedge clk); end endtask
task do_reset;
begin
@(negedge clk);
rst_n = 1'b0; seq_valid = 1'b0; seq_is_read = 1'b0;
seq_addr = TADDR; seq_ptr = 8'h00; seq_rdlen = 4'd1;
load_en = 1'b0; tgt_hold_scl = 1'b0;
ack_hist = 16'h0000; n_acks_seen = 0; n_rx = 0; stops_during = 0;
for (k = 0; k < 8; k = k + 1) payload[k] = 8'h00;
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 issue_seq (input [6:0] a, input [7:0] p, input [3:0] rl, input rd);
begin
@(negedge clk);
seq_addr = a; seq_ptr = p; seq_rdlen = rl; seq_is_read = rd; seq_valid = 1'b1;
@(posedge clk); @(negedge clk); seq_valid = 1'b0;
end
endtask
task wait_seq (input integer max_cycles);
begin
n = 0;
while (!seq_done && n < max_cycles) begin step; n = n + 1; end
if (n >= max_cycles) begin
$display(" FAIL wait_seq: stuck in seq state %0d, txn state %0d",
seq_state, tstate);
errors = errors + 1;
end
end
endtask
task ck_int (input [200*8:1] what, input integer g, input integer e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0d (0x%0h) expected %0d (0x%0h)", what, g, g, e, e);
errors = errors + 1;
end
end
endtask
task ck_bit (input [200*8:1] what, input g, input e);
begin
if (g !== e) begin
$display(" FAIL %0s: got %0b expected %0b", what, g, e);
errors = errors + 1;
end
end
endtask
initial begin
$display("=== i2c_combined_txn: two phases, and the bus never released between them ===");
// ----------------------------------------------------------------
// T1. THE PATTERN. Write a pointer, turn the bus around, read one byte -- from one
// descriptor. §3.1.10 format 3, and Chapter 16.1's register access.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'h4D);
issue_seq(TADDR, 8'h00, 4'd1, 1'b1);
wait_seq(8000);
$display("T1 write a pointer, turn the bus around, read: one descriptor");
ck_bit("T1 succeeded", seq_ok, 1'b1);
ck_int("T1 the byte came back", rxlog[0], 8'h4D);
ck_int("T1 two phases ran", n_seq, 1);
ck_bit("T1 not abandoned", abandoned, 1'b0);
// ----------------------------------------------------------------
// T2. THE PROPERTY THAT MATTERS. Two STARTs and exactly ONE STOP. Two STOPs would
// mean two transactions, and on a shared bus another master could take the bus
// in the gap and write its own pointer.
// ----------------------------------------------------------------
$display("T2 two STARTs and exactly one STOP: one transaction, not two");
ck_int("T2 two STARTs", m_nsta, 2);
ck_int("T2 one STOP", m_nsto, 1);
ck_int("T2 and no STOP happened between the phases", stops_during, 0);
ck_int("T2 the framer logged one initial START", n_sta, 1);
ck_int("T2 and one REPEATED START", n_rs, 1);
// ----------------------------------------------------------------
// T3. THE ADDRESS IS REPEATED WITH THE DIRECTION REVERSED, which is the literal
// text of format 3. Both address bytes are on the wire and differ by one bit.
// ----------------------------------------------------------------
$display("T3 the address appears twice, differing only in the direction bit");
ck_int("T3 four acknowledge slots: two addresses and two data", n_acks_seen, 4);
// history, most recent first: [0] the read's data NACK, [1] read address ACK,
// [2] the pointer byte's ACK, [3] the write address ACK
ck_bit("T3 the write address was acknowledged", ack_hist[3], 1'b0);
ck_bit("T3 the pointer byte was acknowledged", ack_hist[2], 1'b0);
ck_bit("T3 the read address was acknowledged", ack_hist[1], 1'b0);
ck_bit("T3 and the single read byte was NOT", ack_hist[0], 1'b1);
// ----------------------------------------------------------------
// T4. A MULTI-BYTE READ in phase two, with the ACK policy across the boundary.
// ----------------------------------------------------------------
do_reset;
for (k = 0; k < 3; k = k + 1) preload(k[7:0], 8'h60 + k[7:0]);
issue_seq(TADDR, 8'h00, 4'd3, 1'b1);
wait_seq(10000);
$display("T4 three bytes read in phase two, acknowledged correctly");
ck_bit("T4 succeeded", seq_ok, 1'b1);
ck_int("T4 three bytes stored", n_rx, 3);
ck_int("T4 the first", rxlog[0], 8'h60);
ck_int("T4 the last", rxlog[2], 8'h62);
ck_int("T4 two STARTs", m_nsta, 2);
ck_int("T4 one STOP", m_nsto, 1);
ck_bit("T4 the last read byte was NOT acknowledged", ack_hist[0], 1'b1);
ck_bit("T4 but the one before it was", ack_hist[1], 1'b0);
// ----------------------------------------------------------------
// T5. A WRITE-ONLY DESCRIPTOR is one phase, and ends with a STOP. The same block
// handles it, because a pointer write with nothing after it is the degenerate
// case rather than a different operation.
// ----------------------------------------------------------------
do_reset;
issue_seq(TADDR, 8'h7B, 4'd0, 1'b0);
wait_seq(6000);
$display("T5 a write-only descriptor is one phase and frames itself");
ck_bit("T5 succeeded", seq_ok, 1'b1);
ck_int("T5 one START", m_nsta, 1);
ck_int("T5 one STOP", m_nsto, 1);
ck_int("T5 the target received the byte", t_lw, 8'h7B);
ck_int("T5 and one byte was counted", seq_bytes, 1);
// ----------------------------------------------------------------
// T6. PHASE ONE FAILS. Nothing is at 0x51, so the pointer write is NACKed -- and
// there is no point reading. But the bus is HELD, and this is the case a
// two-phase sequencer most often gets wrong.
// ----------------------------------------------------------------
do_reset;
issue_seq(7'h51, 8'h00, 4'd2, 1'b1);
wait_seq(10000);
$display("T6 phase one fails, so phase two is not attempted");
ck_bit("T6 did not succeed", seq_ok, 1'b0);
ck_bit("T6 reported as an address NACK", seq_err[0], 1'b1);
ck_int("T6 no bytes were read", n_rx, 0);
// ----------------------------------------------------------------
// T7. AND THE BUS IS FRAMED ANYWAY. A sequencer that reported the failure and
// stopped would leave every device on the bus believing a transfer was in
// progress -- and Chapter 15.4 shows there is no protocol remedy for that.
// ----------------------------------------------------------------
$display("T7 and the bus is framed before giving up, not simply abandoned");
ck_bit("T7 the abandon path ran", abandoned, 1'b1);
ck_bit("T7 the bus is idle", m_intr, 1'b0);
if (m_nsto < 1) begin
$display(" FAIL T7 the bus was left unframed after a failed phase one");
errors = errors + 1;
end
ck_bit("T7 both lines released", m_scl_low | m_sda_low, 1'b0);
// ----------------------------------------------------------------
// T8. A DESCRIPTOR ARRIVING MID-SEQUENCE IS IGNORED. Accepting one would insert a
// phase into a transaction already in flight, which is exactly the atomicity
// this block exists to guarantee.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'h2A);
issue_seq(TADDR, 8'h00, 4'd1, 1'b1);
n = 0;
while (seq_phase != 2'd1 && n < 2000) begin step; n = n + 1; end
k = n_seq;
issue_seq(TADDR, 8'h05, 4'd1, 1'b1); // a second descriptor, mid-sequence
issue_seq(TADDR, 8'h06, 4'd1, 1'b1);
wait_seq(10000);
$display("T8 a descriptor arriving mid-sequence is ignored, not interleaved");
ck_int("T8 only one sequence completed", n_seq, k + 1);
ck_int("T8 two STARTs, not four", m_nsta, 2);
ck_int("T8 one STOP", m_nsto, 1);
ck_int("T8 and the original pointer was used", rxlog[0], 8'h2A);
// ----------------------------------------------------------------
// T9. A STRETCHING TARGET across the turnaround. The repeated START releases SCL,
// which is precisely where §3.1.6's byte-level handshake happens, so this is
// the most likely place on the whole bus for a stretch to occur.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'h8E);
issue_seq(TADDR, 8'h00, 4'd1, 1'b1);
n = 0;
while (n_acks_seen < 2 && n < 4000) begin step; n = n + 1; end
// One extra step, so the three language variants finish at the same time.
// `n_acks_seen` is a signal in the VHDL twin, read by the stimulus a delta after the
// logging process assigns it, so the loop there exits one cycle later and the stretch
// is injected one cycle later too. Keeping the step in all three is cheaper than
// letting the benches differ, and the stretch lands in the same place either way.
step;
@(negedge clk); tgt_hold_scl = 1'b1; // stretch at the turnaround
for (k = 0; k < 40; k = k + 1) step;
@(negedge clk); tgt_hold_scl = 1'b0;
wait_seq(12000);
$display("T9 a stretch at the turnaround, which is where stretches happen");
ck_bit("T9 still succeeded", seq_ok, 1'b1);
ck_int("T9 the byte came back intact", rxlog[0], 8'h8E);
ck_int("T9 still two STARTs and one STOP", m_nsta + m_nsto, 3);
ck_int("T9 and the stretch did not become a STOP", stops_during, 0);
// ----------------------------------------------------------------
// T10. TWO SEQUENCES BACK TO BACK. Each frames itself, and nothing leaks: the
// second must issue a fresh START rather than continuing the first.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'hA3);
preload(8'h01, 8'hB4);
issue_seq(TADDR, 8'h00, 4'd1, 1'b1);
wait_seq(8000);
ck_int("T10 the first sequence read its byte", rxlog[0], 8'hA3);
issue_seq(TADDR, 8'h01, 4'd1, 1'b1);
wait_seq(8000);
$display("T10 two sequences back to back, each framing itself");
ck_int("T10 two sequences", n_seq, 2);
ck_int("T10 four STARTs in total", m_nsta, 4);
ck_int("T10 and two STOPs, one per sequence", m_nsto, 2);
ck_int("T10 two initial STARTs", n_sta, 2);
ck_int("T10 and two repeated STARTs", n_rs, 2);
// ----------------------------------------------------------------
// T11. THE BYTE COUNT SPANS BOTH PHASES, because the host asked for one operation
// and wants to know how much of it happened.
// ----------------------------------------------------------------
do_reset;
for (k = 0; k < 4; k = k + 1) preload(k[7:0], 8'h50 + k[7:0]);
issue_seq(TADDR, 8'h00, 4'd4, 1'b1);
wait_seq(12000);
$display("T11 the byte count spans both phases of the one operation");
ck_int("T11 one pointer byte plus four read bytes", seq_bytes, 5);
ck_bit("T11 succeeded", seq_ok, 1'b1);
ck_int("T11 four bytes reached the host", n_rx, 4);
// ----------------------------------------------------------------
// ----------------------------------------------------------------
// T13. PHASE ONE FAILS ON A DATA NACK, not an address NACK. The target takes its
// address and then refuses the pointer byte, so the abandon path addresses a
// target that WILL acknowledge -- and therefore anything the abandon
// descriptor carries is really transmitted onto the bus.
//
// This is the only configuration in which the abandon descriptor's LENGTH is
// observable. With an address-NACK failure the re-addressed target refuses
// again and no data byte can leave, so a sequencer that framed the bus with a
// four-byte write instead of a zero-length probe would look identical.
// ----------------------------------------------------------------
do_reset;
issue_seq(TADDR_NACKPTR, 8'h00, 4'd2, 1'b1);
wait_seq(10000);
$display("T13 phase one can fail on a DATA nack, and the abandon path must stay empty");
ck_bit("T13 did not succeed", seq_ok, 1'b0);
ck_bit("T13 reported as a data NACK, not an address NACK", seq_err[1], 1'b1);
ck_bit("T13 and the address-NACK bit is clear", seq_err[0], 1'b0);
ck_bit("T13 the abandon path ran", abandoned, 1'b1);
ck_int("T13 no bytes were read", n_rx, 0);
// The refusing target saw the pointer byte of phase one, and the abandon path must
// have added NO data bytes of its own -- it is a zero-length framing probe.
ck_int("T13 the target received exactly the one refused pointer byte", t3_rx, 1);
if (m_nsto < 1) begin
$display(" FAIL T13 the bus was left unframed after a data-NACK failure");
errors = errors + 1;
end
// T12. THE INVARIANTS. No mid-byte framing anywhere -- every repeated START in
// this bench follows a complete byte and its acknowledge -- and nobody ever
// fought for SDA.
// ----------------------------------------------------------------
$display("T12 no mid-byte framing and no contention, across every sequence");
ck_int("T12 no mid-byte framing", m_nmid, 0);
ck_int("T12 no SDA owner conflicts", n_conf, 0);
ck_int("T12 no arbitration losses", n_arb, 0);
ck_bit("T12 idle", seq_busy, 1'b0);
ck_int("T12 and the sequencer is at rest", seq_state, 0);
if (errors == 0)
$display("=== i2c_combined_txn: ALL CHECKS PASSED ===");
else
$display("=== i2c_combined_txn: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule -- ---------------------------------------------------------------------------
-- i2c_combined_txn_tb.vhd
-- Independent oracle for i2c_combined_txn. Behavioural twin of the SV and Verilog benches.
--
-- The sequencer sits on top of the complete master of Chapter 17.8 and turns a single
-- descriptor into two phases. So the bench issues ONE descriptor and then checks, on the wire,
-- that the bus was never released in between -- which is the only property that distinguishes a
-- combined transaction from two separate ones, and the only one that matters on a shared bus.
--
-- The monitor's START and STOP counts are the evidence. Two STARTs and ONE STOP is a combined
-- transaction; two STARTs and two STOPs is two transactions that happen to be adjacent, and on
-- a multi-master bus those behave completely differently.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_combined_txn_tb is
end entity i2c_combined_txn_tb;
architecture sim of i2c_combined_txn_tb is
constant TCLK : time := 10 ns;
constant NL : integer := 8;
constant NH : integer := 4;
constant NSU : integer := 2;
constant NSMP : integer := 2;
constant NHD : integer := 3;
constant NSUA : integer := 3;
constant NSUO : integer := 3;
constant NBUF : integer := 3;
constant TADDR : std_logic_vector(6 downto 0) := "1010000"; -- 0x50
constant S_IDLE : integer := 0;
signal clk, rst_n : std_logic := '0';
-- The descriptor the bench issues, and the sequencer's answer.
signal seq_valid, seq_is_read : std_logic := '0';
signal seq_addr : std_logic_vector(6 downto 0) := TADDR;
signal seq_ptr : std_logic_vector(7 downto 0) := (others => '0');
signal seq_rdlen : unsigned(3 downto 0) := to_unsigned(1, 4);
signal seq_done, seq_ok, seq_busy, abandoned : std_logic;
signal seq_err : std_logic_vector(5 downto 0);
signal seq_bytes : unsigned(3 downto 0);
signal seq_phase : unsigned(1 downto 0);
signal seq_state : unsigned(2 downto 0);
signal n_seq : unsigned(15 downto 0);
-- The sequencer owns the controller's command port.
signal cmd_valid, cmd_read, cmd_stop : std_logic;
signal cmd_addr : std_logic_vector(6 downto 0);
signal cmd_len : unsigned(3 downto 0);
-- Was the bus ever released between the two phases? Recorded from the wire.
signal stops_during : integer := 0;
type pay_t is array (0 to 7) of std_logic_vector(7 downto 0);
signal payload : pay_t := (others => (others => '0'));
signal do_start, do_restart, do_stop, scl_yield, gen_enable, gen_idle_low : std_logic;
signal byte_go, byte_dir_write, byte_ack_send : std_logic;
signal byte_tx : std_logic_vector(7 downto 0);
signal tx_index, rx_index, tstate, txn_bytes : unsigned(3 downto 0);
signal rx_data : std_logic_vector(7 downto 0);
signal rx_we, txn_done, txn_ok, txn_busy, addr_nack, data_nack : std_logic;
signal txn_err : std_logic_vector(5 downto 0);
signal n_txn : unsigned(15 downto 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 b_rx : std_logic_vector(7 downto 0);
signal b_bidx : unsigned(3 downto 0);
signal b_bytes, b_acks, b_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_conf : std_logic;
signal n_conf, n_arb : unsigned(15 downto 0);
signal m_scl_low : std_logic;
signal tgt_hold_scl : std_logic := '0';
signal t_scl_low, t_sda_low : std_logic;
signal scl_drv, sda_drv : std_logic_vector(3 downto 0);
-- A THIRD device: a target that acknowledges its own address and then REFUSES its
-- FIRST data byte -- the POINTER in a combined transaction. That makes phase one fail
-- on a DATA nack rather than an address NACK, and the two are not interchangeable: on
-- an address NACK the abandon path re-addresses a target that refuses again, so
-- nothing it sends can reach the bus. On a data NACK the target ACKNOWLEDGES the
-- abandon path's address, so anything that path carries is really transmitted. Without
-- this device the abandon descriptor's length is unobservable.
constant TADDR_NACKPTR : std_logic_vector(6 downto 0) := "1010011"; -- 0x53
signal t3_scl_low, t3_sda_low, t3_sel, t3_dirrd, t3_wv : std_logic;
signal t3_lw : std_logic_vector(7 downto 0);
signal t3_rx, t3_tx, t3_nsta, t3_nsto : unsigned(15 downto 0);
signal t3_st : unsigned(2 downto 0);
signal scl, sda : std_logic;
signal scl_in, sda_in, scl_rbl, sda_rbl : std_logic_vector(3 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_wv : std_logic;
signal t_lw : std_logic_vector(7 downto 0);
signal t_rx, t_tx, t_nsta, t_nsto : unsigned(15 downto 0);
signal t_st : 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);
-- Records the acknowledge value of every byte the monitor sees, so the ACK POLICY can be
-- checked as a SEQUENCE rather than one byte at a time.
signal ack_hist : std_logic_vector(15 downto 0) := (others => '0');
signal n_acks_seen, n_rx : integer := 0;
type log_t is array (0 to 7) of std_logic_vector(7 downto 0);
signal rxlog : log_t := (others => (others => '0'));
-- A metavalue-safe index. At time zero, before any reset has propagated, an unsigned
-- signal still reads 'U' -- and `to_integer` on that emits a NUMERIC_STD warning and
-- returns 0 anyway. Converting explicitly keeps the transcript clean and says what is
-- meant: an index that is not yet valid selects slot zero, which nothing reads.
function safe_idx (v : unsigned) return integer is
begin
for i in v'range loop
if v(i) /= '0' and v(i) /= '1' then return 0; end if;
end loop;
return to_integer(v);
end function;
signal halt : boolean := false;
begin
m_scl_low <= f_scl_low or g_scl_low;
scl_drv <= t3_scl_low & tgt_hold_scl & t_scl_low & m_scl_low;
sda_drv <= t3_sda_low & '0' & t_sda_low & m_sda_low;
bus_m : entity work.i2c_line_model
generic map (N_DEV => 4)
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);
u_by : 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 => byte_go, dir_write => byte_dir_write, tx_byte => byte_tx,
ack_to_send => byte_ack_send,
sda_in => sda_in(0), scl_high => scl_in(0), abort => arb_lost,
sda_req => b_sda_req, sda_bit => b_sda_bit,
rx_byte => b_rx, ack => b_ack, ack_valid => b_ackv, byte_done => b_done,
busy => b_busy, bit_index => b_bidx, driving => b_driving,
bytes_done => b_bytes, acks => b_acks, nacks => b_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_conf, conflicts => n_conf,
arb_loss_now => arb_now, arb_lost => arb_lost, arb_losses => n_arb,
arb_clear => txn_done);
u_txn : entity work.i2c_txn_ctrl
generic map (CNT_W => 16)
port map (clk => clk, rst_n => rst_n,
cmd_valid => cmd_valid, cmd_addr => cmd_addr, cmd_read => cmd_read,
cmd_len => cmd_len, cmd_stop => cmd_stop,
do_start => do_start, do_restart => do_restart, do_stop => do_stop,
scl_yield => scl_yield, frame_done => f_done, frame_busy => f_busy,
bus_free => f_bus_free, frame_started => f_started,
gen_enable => gen_enable, gen_idle_low => gen_idle_low,
byte_go => byte_go, byte_dir_write => byte_dir_write, byte_tx => byte_tx,
byte_ack_send => byte_ack_send, byte_done => b_done, byte_busy => b_busy,
byte_ack => b_ack, byte_rx => b_rx,
tx_data => payload(safe_idx(tx_index(2 downto 0))), tx_index => tx_index,
rx_data => rx_data, rx_index => rx_index, rx_we => rx_we,
arb_lost => arb_lost,
txn_done => txn_done, txn_ok => txn_ok, txn_err => txn_err,
txn_bytes => txn_bytes, txn_busy => txn_busy,
addr_nack => addr_nack, data_nack => data_nack,
state => tstate, transactions => n_txn);
dut : entity work.i2c_combined_txn
generic map (CNT_W => 16)
port map (clk => clk, rst_n => rst_n,
seq_valid => seq_valid, seq_addr => seq_addr, seq_ptr => seq_ptr,
seq_rdlen => seq_rdlen, seq_is_read => seq_is_read,
cmd_valid => cmd_valid, cmd_addr => cmd_addr, cmd_read => cmd_read,
cmd_len => cmd_len, cmd_stop => cmd_stop,
txn_done => txn_done, txn_ok => txn_ok, txn_err => txn_err,
txn_bytes => txn_bytes, txn_busy => txn_busy,
seq_done => seq_done, seq_ok => seq_ok, seq_err => seq_err,
seq_bytes => seq_bytes, seq_busy => seq_busy, abandoned => abandoned,
phase => seq_phase, state => seq_state, sequences => n_seq);
-- Phase one's single data byte is the pointer, so the payload's first slot follows the
-- descriptor rather than being written by hand.
-- The whole array is driven, not just element 0: an undriven element of a signal array
-- sits at 'U', and the controller does read the next index speculatively.
payload <= (0 => seq_ptr, others => (others => '0'));
u_tgt3 : entity work.i2c_target_model
generic map (MY_ADDR => TADDR_NACKPTR, ACK_ADDR => '1', STRETCH_AFTER => 0,
NACK_AT => 1, N_MEM => 16, CNT_W => 16)
port map (clk => clk, rst_n => rst_n, scl => scl, sda => sda,
scl_drive_low => t3_scl_low, sda_drive_low => t3_sda_low,
load_en => '0', load_addr => (others => '0'), load_data => (others => '0'),
selected => t3_sel, dir_read => t3_dirrd, last_written => t3_lw,
write_valid => t3_wv, bytes_rx => t3_rx, bytes_tx => t3_tx,
n_starts => t3_nsta, n_stops => t3_nsto, state => t3_st);
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_lw,
write_valid => t_wv, bytes_rx => t_rx, bytes_tx => t_tx,
n_starts => t_nsta, n_stops => t_nsto, state => t_st);
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;
logp : process (clk, rst_n)
begin
if rst_n = '0' then
ack_hist <= (others => '0');
n_acks_seen <= 0;
n_rx <= 0;
stops_during <= 0;
elsif falling_edge(clk) then
if mo_ack = '1' then
ack_hist <= ack_hist(14 downto 0) & mo_ackv;
n_acks_seen <= n_acks_seen + 1;
end if;
if rx_we = '1' then
rxlog(n_rx mod 8) <= rx_data;
n_rx <= n_rx + 1;
end if;
-- A STOP while the sequencer is between phases is the failure this whole block
-- exists to prevent.
if mo_stop = '1' and seq_phase = 1 then
stops_during <= stops_during + 1;
end if;
end if;
end process;
stim : process
variable err : integer := 0;
variable n : integer;
-- A separate saved value, because `wait_seq` and `issue_seq` both use `n` for their own
-- loop counts and would overwrite anything stashed there.
variable saved : integer;
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'; seq_valid <= '0'; seq_is_read <= '0';
seq_addr <= TADDR; seq_ptr <= (others => '0');
seq_rdlen <= to_unsigned(1, 4);
load_en <= '0'; tgt_hold_scl <= '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 issue_seq (a : std_logic_vector(6 downto 0); pp : integer;
rl : integer; rdd : std_logic) is
begin
wait until falling_edge(clk);
seq_addr <= a;
seq_ptr <= std_logic_vector(to_unsigned(pp, 8));
seq_rdlen <= to_unsigned(rl, 4);
seq_is_read <= rdd;
seq_valid <= '1';
wait until rising_edge(clk); wait until falling_edge(clk); seq_valid <= '0';
end procedure;
procedure wait_seq (max_cycles : integer) is
begin
n := 0;
while seq_done = '0' and n < max_cycles loop step; n := n + 1; end loop;
if n >= max_cycles then
report " FAIL wait_seq: stuck in seq state "
& integer'image(to_integer(seq_state)) & ", txn state "
& integer'image(to_integer(tstate)) severity note;
err := err + 1;
end if;
end procedure;
begin
report "=== i2c_combined_txn: two phases, and the bus never released between them ==="
severity note;
-- T1. THE PATTERN: write a pointer, turn the bus around, read -- from one descriptor.
do_reset;
preload(0, 16#4D#);
issue_seq(TADDR, 0, 1, '1');
wait_seq(8000);
report "T1 write a pointer, turn the bus around, read: one descriptor" severity note;
ck_bit("T1 succeeded", seq_ok, '1');
ck_int("T1 the byte came back", to_integer(unsigned(rxlog(0))), 16#4D#);
ck_int("T1 two phases ran", to_integer(n_seq), 1);
ck_bit("T1 not abandoned", abandoned, '0');
-- T2. THE PROPERTY THAT MATTERS: two STARTs and exactly ONE STOP.
report "T2 two STARTs and exactly one STOP: one transaction, not two" severity note;
ck_int("T2 two STARTs", to_integer(mo_nsta), 2);
ck_int("T2 one STOP", to_integer(mo_nsto), 1);
ck_int("T2 and no STOP happened between the phases", stops_during, 0);
ck_int("T2 the framer logged one initial START", to_integer(n_sta), 1);
ck_int("T2 and one REPEATED START", to_integer(n_rs), 1);
-- T3. THE ADDRESS IS REPEATED WITH THE DIRECTION REVERSED -- the literal text of
-- format 3. Both address bytes are on the wire and differ by one bit.
report "T3 the address appears twice, differing only in the direction bit"
severity note;
ck_int("T3 four acknowledge slots: two addresses and two data", n_acks_seen, 4);
ck_bit("T3 the write address was acknowledged", ack_hist(3), '0');
ck_bit("T3 the pointer byte was acknowledged", ack_hist(2), '0');
ck_bit("T3 the read address was acknowledged", ack_hist(1), '0');
ck_bit("T3 and the single read byte was NOT", ack_hist(0), '1');
-- T4. A MULTI-BYTE READ in phase two, with the ACK policy across the boundary.
do_reset;
for j in 0 to 2 loop preload(j, 16#60# + j); end loop;
issue_seq(TADDR, 0, 3, '1');
wait_seq(10000);
report "T4 three bytes read in phase two, acknowledged correctly" severity note;
ck_bit("T4 succeeded", seq_ok, '1');
ck_int("T4 three bytes stored", n_rx, 3);
ck_int("T4 the first", to_integer(unsigned(rxlog(0))), 16#60#);
ck_int("T4 the last", to_integer(unsigned(rxlog(2))), 16#62#);
ck_int("T4 two STARTs", to_integer(mo_nsta), 2);
ck_int("T4 one STOP", to_integer(mo_nsto), 1);
ck_bit("T4 the last read byte was NOT acknowledged", ack_hist(0), '1');
ck_bit("T4 but the one before it was", ack_hist(1), '0');
-- T5. A WRITE-ONLY DESCRIPTOR is one phase and frames itself.
do_reset;
issue_seq(TADDR, 16#7B#, 0, '0');
wait_seq(6000);
report "T5 a write-only descriptor is one phase and frames itself" severity note;
ck_bit("T5 succeeded", seq_ok, '1');
ck_int("T5 one START", to_integer(mo_nsta), 1);
ck_int("T5 one STOP", to_integer(mo_nsto), 1);
ck_int("T5 the target received the byte", to_integer(unsigned(t_lw)), 16#7B#);
ck_int("T5 and one byte was counted", to_integer(seq_bytes), 1);
-- T6. PHASE ONE FAILS, so phase two is not attempted.
do_reset;
issue_seq("1010001", 0, 2, '1');
wait_seq(10000);
report "T6 phase one fails, so phase two is not attempted" severity note;
ck_bit("T6 did not succeed", seq_ok, '0');
ck_bit("T6 reported as an address NACK", seq_err(0), '1');
ck_int("T6 no bytes were read", n_rx, 0);
-- T7. AND THE BUS IS FRAMED ANYWAY. A sequencer that reported the failure and stopped
-- would leave every device believing a transfer was in progress.
report "T7 and the bus is framed before giving up, not simply abandoned"
severity note;
ck_bit("T7 the abandon path ran", abandoned, '1');
ck_bit("T7 the bus is idle", mo_intr, '0');
if to_integer(mo_nsto) < 1 then
report " FAIL T7 the bus was left unframed after a failed phase one" severity note;
err := err + 1;
end if;
ck_bit("T7 both lines released", m_scl_low or m_sda_low, '0');
-- T8. A DESCRIPTOR ARRIVING MID-SEQUENCE IS IGNORED, not interleaved.
do_reset;
preload(0, 16#2A#);
issue_seq(TADDR, 0, 1, '1');
n := 0;
while seq_phase /= 1 and n < 2000 loop step; n := n + 1; end loop;
saved := to_integer(n_seq);
issue_seq(TADDR, 5, 1, '1');
issue_seq(TADDR, 6, 1, '1');
wait_seq(10000);
report "T8 a descriptor arriving mid-sequence is ignored, not interleaved"
severity note;
ck_int("T8 only one sequence completed", to_integer(n_seq), saved + 1);
ck_int("T8 two STARTs, not four", to_integer(mo_nsta), 2);
ck_int("T8 one STOP", to_integer(mo_nsto), 1);
ck_int("T8 and the original pointer was used",
to_integer(unsigned(rxlog(0))), 16#2A#);
-- T9. A STRETCHING TARGET across the turnaround -- the most likely place on the whole
-- bus for a stretch, because §3.1.6's byte-level handshake happens exactly there.
do_reset;
preload(0, 16#8E#);
issue_seq(TADDR, 0, 1, '1');
n := 0;
while n_acks_seen < 2 and n < 4000 loop step; n := n + 1; end loop;
wait until falling_edge(clk); tgt_hold_scl <= '1';
for j in 0 to 39 loop step; end loop;
wait until falling_edge(clk); tgt_hold_scl <= '0';
wait_seq(12000);
report "T9 a stretch at the turnaround, which is where stretches happen"
severity note;
ck_bit("T9 still succeeded", seq_ok, '1');
ck_int("T9 the byte came back intact",
to_integer(unsigned(rxlog(0))), 16#8E#);
ck_int("T9 still two STARTs and one STOP",
to_integer(mo_nsta) + to_integer(mo_nsto), 3);
ck_int("T9 and the stretch did not become a STOP", stops_during, 0);
-- T10. TWO SEQUENCES BACK TO BACK, each framing itself.
do_reset;
preload(0, 16#A3#);
preload(1, 16#B4#);
issue_seq(TADDR, 0, 1, '1');
wait_seq(8000);
ck_int("T10 the first sequence read its byte",
to_integer(unsigned(rxlog(0))), 16#A3#);
issue_seq(TADDR, 1, 1, '1');
wait_seq(8000);
report "T10 two sequences back to back, each framing itself" severity note;
ck_int("T10 two sequences", to_integer(n_seq), 2);
ck_int("T10 four STARTs in total", to_integer(mo_nsta), 4);
ck_int("T10 and two STOPs, one per sequence", to_integer(mo_nsto), 2);
ck_int("T10 two initial STARTs", to_integer(n_sta), 2);
ck_int("T10 and two repeated STARTs", to_integer(n_rs), 2);
-- T11. THE BYTE COUNT SPANS BOTH PHASES, because the host asked for one operation.
do_reset;
for j in 0 to 3 loop preload(j, 16#50# + j); end loop;
issue_seq(TADDR, 0, 4, '1');
wait_seq(12000);
report "T11 the byte count spans both phases of the one operation" severity note;
ck_int("T11 one pointer byte plus four read bytes", to_integer(seq_bytes), 5);
ck_bit("T11 succeeded", seq_ok, '1');
ck_int("T11 four bytes reached the host", n_rx, 4);
-- T13. PHASE ONE FAILS ON A DATA NACK, not an address NACK. The target takes its
-- address and then refuses the pointer byte, so the abandon path addresses a
-- target that WILL acknowledge -- and anything the abandon descriptor carries
-- is really transmitted. This is the only configuration in which the abandon
-- descriptor's LENGTH is observable at all.
do_reset;
issue_seq(TADDR_NACKPTR, 0, 2, '1');
wait_seq(10000);
report "T13 phase one can fail on a DATA nack, and the abandon path must stay empty"
severity note;
ck_bit("T13 did not succeed", seq_ok, '0');
ck_bit("T13 reported as a data NACK, not an address NACK", seq_err(1), '1');
ck_bit("T13 and the address-NACK bit is clear", seq_err(0), '0');
ck_bit("T13 the abandon path ran", abandoned, '1');
ck_int("T13 no bytes were read", n_rx, 0);
ck_int("T13 the target received exactly the one refused pointer byte",
to_integer(t3_rx), 1);
if to_integer(mo_nsto) < 1 then
report " FAIL T13 the bus was left unframed after a data-NACK failure"
severity note;
err := err + 1;
end if;
-- T12. THE INVARIANTS: no mid-byte framing anywhere, and no contention.
report "T12 no mid-byte framing and no contention, across every sequence"
severity note;
ck_int("T12 no mid-byte framing", to_integer(mo_nmid), 0);
ck_int("T12 no SDA owner conflicts", to_integer(n_conf), 0);
ck_int("T12 no arbitration losses", to_integer(n_arb), 0);
ck_bit("T12 idle", seq_busy, '0');
ck_int("T12 and the sequencer is at rest", to_integer(seq_state), 0);
if err = 0 then
report "=== i2c_combined_txn: ALL CHECKS PASSED ===" severity note;
else
report "=== i2c_combined_txn: " & integer'image(err)
& " CHECK(S) FAILED ===" severity note;
end if;
halt <= true;
wait;
end process;
end architecture sim;5b. Execution
| Design | SystemVerilog | Verilog-2001 | VHDL | Finish |
|---|---|---|---|---|
i2c_combined_txn | PASS 13/13 | PASS 13/13 | PASS 13/13 | 48590 ns, all three |
6. Mutation Testing — and a Mutant Equivalent Only Under the Tested Failure
Eleven defects.
| # | Injected defect | Expected detection | Result |
|---|---|---|---|
| M1 | phase one ends with a STOP, releasing the bus | T2 | KILLED (12) |
| M2 | phase two repeats the address without reversing the direction | T3 | KILLED (11) |
| M3 | a failed phase one goes straight to done, bus left held | T7 | KILLED (7) |
| M4 | phase two runs although the pointer write was NACKed | T6 | KILLED (5) |
| M5 | the byte count reports only phase two | T11 | KILLED (2) |
| M6 | a descriptor arriving mid-sequence is accepted | T8 | KILLED (18) |
| M7 | phase two omits the final STOP | T2 | KILLED (8) |
| M8 | phase one writes no pointer byte | T1 | KILLED (10) |
| M9 | phase-one errors discarded rather than accumulated | T6 | KILLED (3) |
| M10 | the abandon path sends data bytes instead of a zero-length probe | T13 after strengthening | KILLED (2) |
| M11 | the abandon path omits the STOP | T7 | KILLED (5) |
baseline: PASS (verified before injecting anything)
killed: 11 survived: 0 score: 11/11
restored: PASSM10 — equivalent under one failure mode and a real defect under another
The mutation makes the abandon descriptor carry four data bytes instead of zero. It survived, and the reason is specific rather than general.
The bench's only phase-one failure was an address NACK, at 0x51. On that path the abandon descriptor re-addresses a target that refuses again, so 17.8's controller takes its address-NACK exit immediately and never sends a data byte — whatever length the descriptor claims. The mutant is genuinely unobservable there.
But phase one can also fail on a data NACK: a target that takes its address and then refuses the pointer byte. On that path the abandon descriptor's address is acknowledged, so its data bytes really are transmitted onto a bus the master was supposed to be quietly framing.
The fix was a third device at 0x53 with NACK_AT(1), and T13, which asserts the error is a data NACK rather than an address NACK, that the abandon path ran, and that the target received exactly one byte — the refused pointer, and nothing the abandon path added.
This is the same shape as 17.2 §7's N_BUF = 8 and 17.3 §10's 100 MHz: a defect invisible under the tested configuration. Here the "configuration" is not a parameter but which of two failure modes the test chose.
7. Verification Connection — One Item or a Sequence?
// The tempting shape is one item with a flag:
//
// class i2c_item; rand bit combined; rand byte ptr; ... endclass
//
// It is wrong, and the reason is that the two phases have INDEPENDENT properties.
// Phase one has its own direction (always write), its own length (always one, the
// pointer), and its own outcome. Phase two has a different direction, a different
// length, and a different outcome. A single item either flattens all of that into
// fields that only apply half the time, or grows a nested structure that IS a
// sequence with a different name.
//
// THE RIGHT SHAPE is a sequence of two ordinary transaction items, with the bus
// held across the boundary expressed as a property of the FIRST item:
//
// task body();
// `uvm_do_with(req, { addr == a; is_read == 0; length == 1;
// wdata[0] == ptr; end_with_stop == 0; }) // <-- held
// `uvm_do_with(req, { addr == a; is_read == 1; length == n;
// end_with_stop == 1; })
// endtask
//
// `end_with_stop == 0` on the first item is the whole combined transaction, which is
// also exactly what Chapter 17.2's CTRL register bit expresses -- so the sequence
// mirrors the hardware interface rather than inventing a parallel vocabulary.
//
// WHAT THE SCOREBOARD MUST CHECK, and it is not the repeated START:
//
// the number of STOPs between the two phases is ZERO
//
// Checking that a repeated START appeared is weaker: a master that emitted a STOP and
// then a START also produced a START, and on a quiet bus the read would still return
// the right data. The defect only shows when another master interferes in the gap --
// which is precisely the condition a directed test does not create. So the property
// has to be asserted on the ABSENCE of a STOP, not on the presence of a START.
//
// COVERAGE worth carrying:
//
// cover: a combined transfer whose phase one was ADDRESS-NACKed
// cover: a combined transfer whose phase one was DATA-NACKed <-- section 6
// cover: a combined transfer with a stretch across the turnaround (T9)
//
// The second bin is the one whose absence hid a real defect.8. FPGA and ASIC Implications
On an FPGA this block adds a six-state machine and no datapath, so its cost is negligible. The interaction worth knowing is with 17.5's framer: the repeated-START sequence releases SCL and waits for the readback, and across that wait the synchroniser's two-cycle latency applies. The consequence is the same benign one as elsewhere — tSU;STA gets longer, and it is a minimum.
On an ASIC, the relevant point is the firmware contract. A combined transfer must be expressible as one command from software, because if the driver issues two separate commands there is a window between them in which the hardware has no obligation to keep the bus — and an interrupt landing in that window can delay the second command indefinitely while the bus stays held. That is why this block takes a single descriptor rather than exposing the two phases to software. Chapter 17.2's atomic start makes each command race-free; this block makes the pair atomic, which software cannot do for itself.
The abandoned output belongs in a status register. A driver that sees a failure plus abandoned knows the bus was framed and is safe to use; a failure without it means the bus state is unknown, which is the one condition that justifies Chapter 17.11's recovery sequence.
9. Debugging — The Read That Returned Another Master's Data
A system has two masters on one bus: a main SoC and a small power-management controller that occasionally reads a temperature sensor. The SoC reads a 16-bit calibration value from an EEPROM at boot. Nearly always it is correct. About once in several hundred boots it reads a value that is plausible but wrong -- and the wrong value is always the same one. No error is reported by either master, and the bus shows no arbitration loss.
The SoC performed a combined transfer as a write, a STOP and a read. The STOP released the arbitration it had already won, and on a multi-master bus another master is entitled to take the bus in that gap -- which it did, and its pointer write landed on the EEPROM. The read that followed was addressed correctly, acknowledged correctly, and returned the wrong location, with no error anywhere because nothing illegal happened. Every device on the bus behaved correctly, including the power controller. The defect was that the SoC gave up the bus in the middle of an operation that had to be atomic.
Issue the pointer write and the read as ONE command with the bus held -- the descriptor this block takes, with phase one carrying end_with_stop = 0. That makes the turnaround a repeated START, and no other master can interleave, because arbitration was never released. Then note which check would have caught it in verification: not 'a repeated START appeared', because the broken version also produced a START. The property is that the number of STOPs between the two phases is ZERO. And it is only observable with a second master in the environment, since on a quiet bus the two-transaction version returns correct data every time.Three generalisations.
Nothing illegal happened. Both masters conformed, the EEPROM conformed, and no error bit was set anywhere. The failure was a consequence of releasing the bus — an action that is always legal and was, here, wrong.
The test that catches it asserts an absence. "A repeated START appeared" passes on the broken design too. "No STOP occurred between the phases" is the real property, and absences are easy to omit from a test plan.
A quiet bus cannot expose it. With one master the two-transaction version returns correct data on every run, forever. The environment needs a competitor, which is the same class of environment gap as the non-stretching targets of 17.1 §10 and 17.7 §10.
10. Common Misconceptions
"A combined transfer is a write, a STOP and a read." That releases the arbitration already won and lets another master take the bus in the gap. It is one transaction with the direction reversed and no STOP. §2 and §9.
"A repeated START is a new transaction that skips the bus-free interval." It is the same transaction continuing. The master never stops owning the bus, which is why do_restart requires a transfer open where do_start requires the bus free. §2.
"If the read returns the right data, the transfer was atomic." On a quiet bus a non-atomic version returns the right data every time. Correct data is not evidence of atomicity. §9.
"Check that the repeated START appeared." A broken master that emitted a STOP and a START also produced a START. The property is the absence of a STOP between the phases. §7 and §9.
"If phase one fails, report and stop." The bus is still held. Walking away leaves every device mid-transfer, and there is no protocol remedy for that. §3.
"Releasing the two lines is a clean way out." It leaves every device with a part-way bit counter and no way to resynchronise. Frame the bus with a STOP instead. §3.
"The abandon path's contents do not matter since the target refuses anyway." Only on an address NACK. On a data-NACK failure the target acknowledges the abandon descriptor, and anything it carries is really transmitted. §6.
"A combined transfer can be two commands from software." Then there is a window between them in which the hardware has no obligation to hold the bus, and an interrupt in that window holds it indefinitely. §8.
"One transaction item with a combined flag is enough." The two phases have independent directions, lengths and outcomes. A single item either flattens them or becomes a sequence under another name. §7.
11. Reason It Through
Why is a STOP between the two phases of a register read a correctness bug rather than an inefficiency?
Because it releases arbitration this master already won. Another master may take the bus and write its own pointer, so the read returns data from a location this master never selected — with no error anywhere, since nothing illegal occurred. §2 and §9.
Phase one is NACKed. Why can the sequencer not simply report the failure?
Because the bus is still held. Reporting and stopping leaves every device believing a transfer is in progress, and Chapter 15.4 shows there is no protocol remedy for that state. §3.
Why is the abandon descriptor a zero-length write rather than a direct release of the lines?
A direct release leaves every device mid-byte with no way to resynchronise. A zero-length write with a STOP produces a real STOP, which every device recognises. The NACK it earns is irrelevant — the STOP is the point. §3.
M10 made the abandon path send four bytes and survived. Under what failure mode is it harmless, and under which is it a real defect?
Harmless on an address NACK, because the re-addressed target refuses again and the controller exits before sending data. A real defect on a data NACK, where the target acknowledges the abandon descriptor and its bytes are actually transmitted. §6.
Why is "a repeated START appeared" a weaker check than "no STOP appeared between the phases"?
Because a master that emits a STOP and then a START has also produced a START. Only the absence of the STOP distinguishes the atomic version from the broken one. §7.
Why can a single-master environment never expose the §9 defect?
Because with nobody to interleave, the two-transaction version returns correct data on every run. The failure requires a competitor taking the bus in the gap. §9.
Why must a combined transfer be one command from software rather than two?
Because between two commands the hardware has no obligation to keep the bus, and an interrupt landing there holds the bus for as long as the interrupt takes. Software cannot make the pair atomic; the hardware must. §8.
12. Understanding Check
13. Summary
A combined transfer is one transaction whose direction reverses without the bus going free — §3.1.10 format 3 repeats the START and the address with R/W̄ inverted.
It is the dominant shape on the bus, because almost every I²C device is a register map and a register read requires a pointer write first.
Atomicity is the whole point, and it needs a block that owns both phases. A STOP between them releases arbitration already won; another master may then write its own pointer, and the read returns plausible wrong data with no error anywhere.
The correct check is an absence. "A repeated START appeared" passes on a master that emitted STOP-then-START. "Zero STOPs between the phases" is the real property.
A quiet bus cannot expose the defect at all — a non-atomic implementation returns correct data on every run until a competitor interleaves.
A failed phase one must frame the bus before giving up, because the bus is still held and there is no protocol remedy for a master that walks away mid-transfer.
The abandon path is a zero-length write with a STOP — the cheapest legal framing. Releasing the lines directly is not an option: it leaves every device mid-byte with no way to resynchronise.
Eleven mutants, eleven killed — after M10 survived as equivalent under the tested failure mode.
M10 was harmless on an address NACK and a real defect on a data NACK. Two failure modes reach the same recovery path, and the path's correctness was only observable through the one the bench did not use.
So "which failure mode the test chose" is a configuration in the same sense as a parameter value — the third instance of that shape in this module, after N_BUF = 8 and 100 MHz.
14. What Comes Next
Transactions are now atomic, framed correctly on success and on failure, and the bus is never released where it must be held. What the master still does not do is notice when another device disagrees with it.
Chapter 17.10 builds the feedback block, and it is where the module's central distinction finally pays off completely. Clock stretching and arbitration loss are the same observation — a line this master released that reads back low — differing only in which line it was. One comparator, two protocol features, and a master that implements them as two features has written the same logic twice.
It is also where the asymmetry of arbitration becomes executable: a master can only ever lose by trying to send a one.
Continue learning
Related tutorials
- Related topic
START and STOP Generation — The Framing Sequencer
The edges the bit engine cannot produce, and why that is structural rather than stylistic. Builds the four framing sequences from their Table 10 intervals, shows why every SCL release inside them must wait for the readback or the START is not a START at all, and finds a defect that no bus-level check could have caught.
- Related topic
Repeated START in Practice — Why Not STOP Then START
Two sequences that look nearly identical in a driver's source are completely different on the wire. This chapter measures the difference, builds the passive monitor that tells them apart from two wires alone, and names exactly what a STOP costs on a shared bus.
- Related topic
I²C SDA Arbitration — Wired-AND Decides Bit by Bit
Arbitration with no arbiter, no priority and no protocol exchange — resolved by one asymmetric test each master performs on itself. Settles what 'no information is lost' actually means.
- Related topic
Software Reset and Device ID
Two optional reserved-address services and what optional costs a design that wants to rely on them. Works through the seven-step Device ID read, its 12/9/3 bit packing across three bytes, and the two Remarks that name exactly which events destroy it.
