I²C · Module 18
The Receive Datapath — Capturing Written Bytes
The subject is the hand-off, not the value. A byte delivered twice is as bad as one dropped and both are invisible to a test that only compares bytes — and the acknowledge pulse is not a data bit, so a receive path left enabled through it displaces every byte after the first.
The target is addressed, the direction is write, and it has acknowledged (18.5). Bytes are now arriving.
1. What Makes This Different From the Address Block
Both shift eight bits in. The difference is what completion produces.
| 18.4 address block | this block | |
|---|---|---|
| completion produces | a decision — match or not | data something else must consume |
| the byte itself is | of diagnostic interest only | the entire point |
| the failure mode is | the wrong decision | a byte dropped, or delivered twice |
So the contract is deliberately narrow:
rx_valid is ONE cycle, exactly once per received byte
rx_byte is stable when rx_valid is high2. Why a Pulse Rather Than a Flag
A level-based "byte available" flag would need a consumer acknowledgement to clear — and then this block has two masters:
the bus which never waits
the consumer which mightA one-cycle pulse plus a register downstream is the right shape for everything else, and Chapter 18.10 is where a target that genuinely cannot keep up gets to stretch the clock instead. Those are the two honest options; hoping is not one.
Mutation M5 turns the pulse into a level and fails fourteen checks.
3. Sampled at the Rising Edge, Enabled From Above
The sampling instant is scl_rise, for the same reason as every other received bit: §3.1.2 makes SDA stable only while SCL is HIGH. This block has no timing of its own.
And it is enabled, not self-starting:
Mutation M6 drops the gate and fails seven checks; M7 samples on any cycle rather than the rising edge and fails twenty-nine.
4. The Acknowledge Pulse Is Not a Data Bit
This is the boundary that bites hardest, and it surfaced as a testbench bug before it could surface as a design one.
A byte is nine clock pulses (17.7): eight data and one acknowledge. The acknowledge pulse clocks SDA exactly like a data bit does — so a receive path left enabled through it shifts it in.
So rx_enable must be dropped for the ninth pulse. That is Chapter 18.10's job, and this chapter's benches model it — the gen_ack_slot task drops the enable, because a bench that left it high would reproduce the defect in its own stimulus and then blame the design.
Mutation M3 makes the block consume nine bits itself and fails twenty-two checks.
5. Framing Discards a Partial Byte
A repeated START or a STOP part-way through a byte means the byte will never complete.
The block counts abandonments separately (n_partial), because a target that silently discards partial bytes and one that never receives any look identical from outside. Mutation M8 removes the discard and fails four checks.
6. The Hand-Off, and the Displacement It Prevents
What an ungated receiver delivers: correct, then progressively displaced
7 cycles7. The Receive Datapath, in Three Languages
// -----------------------------------------------------------------------------
// i2c_slave_rx.sv
// The receive datapath: eight observed bits into one byte, handed off exactly once.
//
// WHAT MAKES THIS DIFFERENT FROM THE ADDRESS BLOCK, which also shifts eight bits in.
// The address block's completion produces a DECISION -- match or not -- and the byte
// itself is only of diagnostic interest. This block's completion produces DATA that
// something else must consume, and the hand-off is the part that goes wrong: a byte
// delivered twice is as bad as a byte dropped, and both are invisible to a test that
// only checks the value.
//
// So the contract is deliberately narrow:
//
// rx_valid is ONE cycle, exactly once per received byte
// rx_byte is stable when rx_valid is high
//
// A level-based "byte available" flag would need a consumer acknowledgement to clear,
// and then the block has two masters: the bus, which never waits, and the consumer,
// which might. Chapter 18.10 is where a slave that genuinely cannot keep up gets to
// stretch the clock; a one-cycle pulse plus a register downstream is the right shape for
// everything else.
//
// SAMPLED AT THE RISING EDGE, for the same reason as every other received bit: §3.1.2
// makes SDA stable only while SCL is HIGH, so `scl_rise` is the one instant a received
// bit is guaranteed valid. This block has no timing of its own.
//
// ENABLED, NOT SELF-STARTING. It shifts only when the layer above says a data byte is
// expected -- selected, write direction, address phase over. Without that gate it would
// shift the address byte as data, and the first data byte of every write would be the
// address. The gate is the whole reason the address block and this one can share a bus
// without sharing a bit counter.
//
// AND FRAMING RESETS IT MID-BYTE. A repeated START or a STOP arriving part-way through a
// byte means the byte will never complete. The partial contents must be discarded, not
// held over to be completed by the NEXT transfer's first three bits -- which is what a
// design that only resets its counter on completion would do.
// -----------------------------------------------------------------------------
module i2c_slave_rx #(
parameter int CNT_W = 16
) (
input logic clk,
input logic rst_n,
// From Chapter 18.2.
input logic scl_rise,
input logic sda_q,
// From Chapter 18.3 -- framing discards a partial byte.
input logic start_pulse,
input logic stop_pulse,
// From the layer above (18.10/18.11): a data byte is expected now.
input logic rx_enable,
output logic receiving, // a byte is in progress -- the `mid_byte` report
output logic [3:0] bit_index, // 0..7
output logic [7:0] rx_byte,
output logic rx_valid, // ONE cycle, exactly once per byte
output logic byte_done, // the same instant, for the acknowledge block
output logic [CNT_W-1:0] n_bytes,
output logic [CNT_W-1:0] n_partial // bytes abandoned to framing
);
logic [7:0] shreg;
wire [7:0] shreg_next = {shreg[6:0], sda_q};
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
receiving <= 1'b0;
bit_index <= 4'd0;
rx_byte <= 8'h00;
rx_valid <= 1'b0;
byte_done <= 1'b0;
shreg <= 8'h00;
n_bytes <= {CNT_W{1'b0}};
n_partial <= {CNT_W{1'b0}};
end else begin
rx_valid <= 1'b0;
byte_done <= 1'b0;
if (start_pulse || stop_pulse) begin
// Discard a partial byte. Holding it would let the next transfer's first
// bits complete this one, producing a byte that never existed on the wire.
if (receiving) n_partial <= n_partial + 1'b1;
receiving <= 1'b0;
bit_index <= 4'd0;
shreg <= 8'h00;
end else if (rx_enable && scl_rise) begin
shreg <= shreg_next;
receiving <= 1'b1;
if (bit_index == 4'd7) begin
// Complete. One pulse, one byte, and the counter returns to zero so the
// ninth (acknowledge) pulse is not mistaken for a data bit.
receiving <= 1'b0;
bit_index <= 4'd0;
rx_byte <= shreg_next;
rx_valid <= 1'b1;
byte_done <= 1'b1;
n_bytes <= n_bytes + 1'b1;
end else begin
bit_index <= bit_index + 4'd1;
end
end
end
end
endmodule // -----------------------------------------------------------------------------
// i2c_slave_rx.v
// The receive datapath: eight observed bits into one byte, handed off exactly once.
//
// WHAT MAKES THIS DIFFERENT FROM THE ADDRESS BLOCK, which also shifts eight bits in.
// The address block's completion produces a DECISION -- match or not -- and the byte
// itself is only of diagnostic interest. This block's completion produces DATA that
// something else must consume, and the hand-off is the part that goes wrong: a byte
// delivered twice is as bad as a byte dropped, and both are invisible to a test that
// only checks the value.
//
// So the contract is deliberately narrow:
//
// rx_valid is ONE cycle, exactly once per received byte
// rx_byte is stable when rx_valid is high
//
// A level-based "byte available" flag would need a consumer acknowledgement to clear,
// and then the block has two masters: the bus, which never waits, and the consumer,
// which might. Chapter 18.10 is where a slave that genuinely cannot keep up gets to
// stretch the clock; a one-cycle pulse plus a register downstream is the right shape for
// everything else.
//
// SAMPLED AT THE RISING EDGE, for the same reason as every other received bit: §3.1.2
// makes SDA stable only while SCL is HIGH, so `scl_rise` is the one instant a received
// bit is guaranteed valid. This block has no timing of its own.
//
// ENABLED, NOT SELF-STARTING. It shifts only when the layer above says a data byte is
// expected -- selected, write direction, address phase over. Without that gate it would
// shift the address byte as data, and the first data byte of every write would be the
// address. The gate is the whole reason the address block and this one can share a bus
// without sharing a bit counter.
//
// AND FRAMING RESETS IT MID-BYTE. A repeated START or a STOP arriving part-way through a
// byte means the byte will never complete. The partial contents must be discarded, not
// held over to be completed by the NEXT transfer's first three bits -- which is what a
// design that only resets its counter on completion would do.
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
// -----------------------------------------------------------------------------
module i2c_slave_rx #(
parameter integer CNT_W = 16
) (
input wire clk,
input wire rst_n,
// From Chapter 18.2.
input wire scl_rise,
input wire sda_q,
// From Chapter 18.3 -- framing discards a partial byte.
input wire start_pulse,
input wire stop_pulse,
// From the layer above (18.10/18.11): a data byte is expected now.
input wire rx_enable,
output reg receiving, // a byte is in progress -- the `mid_byte` report
output reg [3:0] bit_index, // 0..7
output reg [7:0] rx_byte,
output reg rx_valid, // ONE cycle, exactly once per byte
output reg byte_done, // the same instant, for the acknowledge block
output reg [CNT_W-1:0] n_bytes,
output reg [CNT_W-1:0] n_partial // bytes abandoned to framing
);
reg [7:0] shreg;
wire [7:0] shreg_next = {shreg[6:0], sda_q};
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
receiving <= 1'b0;
bit_index <= 4'd0;
rx_byte <= 8'h00;
rx_valid <= 1'b0;
byte_done <= 1'b0;
shreg <= 8'h00;
n_bytes <= {CNT_W{1'b0}};
n_partial <= {CNT_W{1'b0}};
end else begin
rx_valid <= 1'b0;
byte_done <= 1'b0;
if (start_pulse || stop_pulse) begin
// Discard a partial byte. Holding it would let the next transfer's first
// bits complete this one, producing a byte that never existed on the wire.
if (receiving) n_partial <= n_partial + 1'b1;
receiving <= 1'b0;
bit_index <= 4'd0;
shreg <= 8'h00;
end else if (rx_enable && scl_rise) begin
shreg <= shreg_next;
receiving <= 1'b1;
if (bit_index == 4'd7) begin
// Complete. One pulse, one byte, and the counter returns to zero so the
// ninth (acknowledge) pulse is not mistaken for a data bit.
receiving <= 1'b0;
bit_index <= 4'd0;
rx_byte <= shreg_next;
rx_valid <= 1'b1;
byte_done <= 1'b1;
n_bytes <= n_bytes + 1'b1;
end else begin
bit_index <= bit_index + 4'd1;
end
end
end
end
endmodule -- -----------------------------------------------------------------------------
-- i2c_slave_rx.vhd
-- The receive datapath: eight observed bits into one byte, handed off exactly once.
-- Same ports, generic, reset values and pulse timing as the SystemVerilog and Verilog.
-- -----------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_slave_rx is
generic (
CNT_W : positive := 16
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- From Chapter 18.2.
scl_rise : in std_logic;
sda_q : in std_logic;
-- From Chapter 18.3 -- framing discards a partial byte.
start_pulse : in std_logic;
stop_pulse : in std_logic;
-- From the layer above: a data byte is expected now.
rx_enable : in std_logic;
receiving : out std_logic;
bit_index : out unsigned(3 downto 0);
rx_byte : out std_logic_vector(7 downto 0);
rx_valid : out std_logic;
byte_done : out std_logic;
n_bytes : out unsigned(CNT_W-1 downto 0);
n_partial : out unsigned(CNT_W-1 downto 0)
);
end entity i2c_slave_rx;
architecture rtl of i2c_slave_rx is
signal shreg : std_logic_vector(7 downto 0) := (others => '0');
signal shreg_next : std_logic_vector(7 downto 0);
signal r_rcv, r_valid, r_done : std_logic := '0';
signal r_idx : unsigned(3 downto 0) := (others => '0');
signal r_byte : std_logic_vector(7 downto 0) := (others => '0');
signal c_b, c_p : unsigned(CNT_W-1 downto 0) := (others => '0');
begin
shreg_next <= shreg(6 downto 0) & sda_q;
process (clk, rst_n)
begin
if rst_n = '0' then
r_rcv <= '0';
r_idx <= (others => '0');
r_byte <= (others => '0');
r_valid <= '0';
r_done <= '0';
shreg <= (others => '0');
c_b <= (others => '0');
c_p <= (others => '0');
elsif rising_edge(clk) then
r_valid <= '0';
r_done <= '0';
if start_pulse = '1' or stop_pulse = '1' then
-- Discard a partial byte. Holding it would let the next transfer's first bits
-- complete this one, producing a byte that never existed on the wire.
if r_rcv = '1' then c_p <= c_p + 1; end if;
r_rcv <= '0';
r_idx <= (others => '0');
shreg <= (others => '0');
elsif rx_enable = '1' and scl_rise = '1' then
shreg <= shreg_next;
r_rcv <= '1';
if r_idx = 7 then
-- Complete. One pulse, one byte, and the index returns to zero so the
-- ninth (acknowledge) pulse is not mistaken for a data bit.
r_rcv <= '0';
r_idx <= (others => '0');
r_byte <= shreg_next;
r_valid <= '1';
r_done <= '1';
c_b <= c_b + 1;
else
r_idx <= r_idx + 1;
end if;
end if;
end if;
end process;
receiving <= r_rcv;
bit_index <= r_idx;
rx_byte <= r_byte;
rx_valid <= r_valid;
byte_done <= r_done;
n_bytes <= c_b;
n_partial <= c_p;
end architecture rtl;7a. The testbenches
Thirteen tests. The observer is the interesting part: it logs every delivery, catches the pulse being high two cycles running, and — added after mutation testing — watches byte_done separately.
| # | Test | Property |
|---|---|---|
| T1 | reset has delivered nothing | |
| T2 | disabled means deaf | which is what keeps the address byte out |
| T3 | one byte in, one delivery out | and the pulse is one cycle |
| T4 | MSB first, with 0x80 then 0x01 | the two values that distinguish the orders |
| T5 | all zeros and all ones | |
| T6 | four bytes, in order, none dropped or duplicated | |
| T7 | exactly eight bits — and the ninth pulse delivers nothing | |
| T8 | a STOP mid-byte discards the partial | and the next byte is its own |
| T9 | a repeated START mid-byte does the same | |
| T10 | the byte is stable in the cycle the pulse is asserted | |
| T11 | disabling mid-byte stops the shift and delivers nothing | |
| T12 | reset mid-byte clears the partial and the counters | |
| T13 | byte_done fires once per byte, alongside rx_valid | added after M10; see §8 |
T8 does something worth copying: after the abandoned partial it starts a fresh transfer and checks the next byte arrives complete and correct. Asserting only that nothing was delivered would pass a design that held the partial and completed it later from the next transfer's bits.
// -----------------------------------------------------------------------------
// i2c_slave_rx_tb.sv
// Independent oracle for i2c_slave_rx. The hand-off is what is under test, not the value.
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module i2c_slave_rx_tb;
localparam integer HALF = 8;
logic clk = 1'b0, rst_n = 1'b0;
logic m_scl = 1'b1, m_sda = 1'b1;
logic rx_enable = 1'b1;
wire scl_pin = m_scl, sda_pin = m_sda;
logic scl_q, sda_q, scl_rise, scl_fall, sda_rise, sda_fall;
logic start_pulse, restart_pulse, stop_pulse, bus_active, fmid;
logic [15:0] n_sta, n_rs, n_sto;
logic receiving, rx_valid, byte_done;
logic [3:0] bit_index;
logic [7:0] rx_byte;
logic [15:0] n_bytes, n_partial;
integer errors = 0, n, k;
// ---- the hand-off observer. A byte delivered twice is as bad as one dropped, and
// both are invisible to a test that only compares the value.
integer n_valid_obs = 0, wide = 0;
reg [7:0] log [0:15];
integer n_log = 0;
logic v_d = 1'b0, bd_d = 1'b0;
// `byte_done` gets its own observer. It is the pulse Chapter 18.5's acknowledge block
// ARMS on, and this bench does not instantiate that block -- so without a check here
// a receiver whose bytes are all correct and whose byte_done never fires would pass
// everything while leaving the target permanently unable to acknowledge.
integer n_done_obs = 0, done_wide = 0, done_mismatch = 0;
always @(posedge clk) begin
if (rst_n) begin
if (rx_valid) begin
n_valid_obs <= n_valid_obs + 1;
log[n_log[3:0]] = rx_byte; n_log = n_log + 1;
end
if (rx_valid & v_d) wide <= wide + 1;
if (byte_done) n_done_obs <= n_done_obs + 1;
if (byte_done & bd_d) done_wide <= done_wide + 1;
// The two pulses must be simultaneous: the acknowledge decision is taken from
// the byte, so a byte_done that did not coincide with its rx_valid would arm a
// slot for data the consumer has not been handed.
if (byte_done !== rx_valid) done_mismatch <= done_mismatch + 1;
end
v_d <= rx_valid; bd_d <= byte_done;
end
i2c_slave_sync #(.SYNC_DEPTH(2)) u_sync (
.clk(clk), .rst_n(rst_n), .scl_pin(scl_pin), .sda_pin(sda_pin),
.scl_q(scl_q), .sda_q(sda_q), .scl_rise(scl_rise), .scl_fall(scl_fall),
.sda_rise(sda_rise), .sda_fall(sda_fall));
i2c_slave_framing #(.CNT_W(16)) u_frm (
.clk(clk), .rst_n(rst_n), .scl_q(scl_q),
.sda_rise(sda_rise), .sda_fall(sda_fall),
.start_pulse(start_pulse), .restart_pulse(restart_pulse), .stop_pulse(stop_pulse),
.bus_active(bus_active), .mid_byte(receiving), .framing_midbyte(fmid),
.n_starts(n_sta), .n_restarts(n_rs), .n_stops(n_sto));
i2c_slave_rx #(.CNT_W(16)) dut (
.clk(clk), .rst_n(rst_n), .scl_rise(scl_rise), .sda_q(sda_q),
.start_pulse(start_pulse), .stop_pulse(stop_pulse), .rx_enable(rx_enable),
.receiving(receiving), .bit_index(bit_index), .rx_byte(rx_byte),
.rx_valid(rx_valid), .byte_done(byte_done),
.n_bytes(n_bytes), .n_partial(n_partial));
always #5 clk = ~clk;
task step; begin @(posedge clk); @(negedge clk); end endtask
task phase; begin for (n = 0; n < HALF; n = n + 1) step; end endtask
task do_reset;
begin
@(negedge clk); rst_n = 1'b0; m_scl = 1'b1; m_sda = 1'b1; rx_enable = 1'b1;
n_valid_obs = 0; wide = 0; n_log = 0;
n_done_obs = 0; done_wide = 0; done_mismatch = 0;
step; step; @(negedge clk); rst_n = 1'b1; phase;
end
endtask
task gen_start; begin
@(negedge clk); m_sda = 1'b1; m_scl = 1'b1; phase;
@(negedge clk); m_sda = 1'b0; phase;
@(negedge clk); m_scl = 1'b0; phase; end
endtask
task gen_stop; begin
@(negedge clk); m_scl = 1'b0; m_sda = 1'b0; phase;
@(negedge clk); m_scl = 1'b1; phase;
@(negedge clk); m_sda = 1'b1; phase; end
endtask
task gen_restart; begin
@(negedge clk); m_scl = 1'b0; m_sda = 1'b1; phase;
@(negedge clk); m_scl = 1'b1; phase;
@(negedge clk); m_sda = 1'b0; phase;
@(negedge clk); m_scl = 1'b0; phase; end
endtask
task gen_bit (input b); begin
@(negedge clk); m_scl = 1'b0; phase;
@(negedge clk); m_sda = b; phase;
@(negedge clk); m_scl = 1'b1; phase;
@(negedge clk); m_scl = 1'b0; phase; end
endtask
task gen_byte (input [7:0] d);
begin for (k = 7; k >= 0; k = k - 1) gen_bit(d[k]); end
endtask
// The ninth pulse. `rx_enable` is dropped for it, which is what the transaction layer
// of Chapter 18.10 does: the acknowledge pulse is not a data bit, and a receive path
// left enabled through it shifts it in -- so every byte after the first is displaced
// by one bit. The gate is the contract, and this task models it.
task gen_ack_slot; begin
@(negedge clk); m_scl = 1'b0; rx_enable = 1'b0; phase;
@(negedge clk); m_sda = 1'b1; phase;
@(negedge clk); m_scl = 1'b1; phase;
@(negedge clk); m_scl = 1'b0; phase;
@(negedge clk); rx_enable = 1'b1; end
endtask
task ck_bit (input [200*8:1] w, input g, input e);
begin if (g !== e) begin
$display(" FAIL %0s: got %0b expected %0b", w, g, e); errors = errors + 1; end end
endtask
task ck_int (input [200*8:1] w, input integer g, input integer e);
begin if (g !== e) begin
$display(" FAIL %0s: got %0d expected %0d", w, g, e); errors = errors + 1; end end
endtask
initial begin
$display("=== i2c_slave_rx: eight observed bits, handed off exactly once ===");
// T1. Reset delivers nothing.
do_reset;
$display("T1 a reset receiver has delivered nothing");
ck_int("T1 no bytes", n_valid_obs, 0);
ck_bit("T1 not receiving", receiving, 1'b0);
// T2. DISABLED MEANS DEAF. Without the gate this block would shift the address
// byte as data, and the first data byte of every write would be the address.
do_reset;
@(negedge clk); rx_enable = 1'b0;
gen_start; gen_byte(8'hA5);
$display("T2 disabled, it shifts nothing -- which is what keeps the address out");
ck_int("T2 no byte delivered", n_valid_obs, 0);
ck_bit("T2 and it never started receiving", receiving, 1'b0);
// T3. One byte, exactly once.
do_reset;
gen_start; gen_byte(8'hA5);
$display("T3 one byte in, one delivery out");
ck_int("T3 exactly one delivery", n_valid_obs, 1);
ck_int("T3 the value is right", log[0], 8'hA5);
ck_int("T3 the pulse was one cycle", wide, 0);
ck_int("T3 the counter agrees", n_bytes, 1);
// T4. MSB first, proved with an asymmetric byte. 0x80 and 0x01 are the two values
// whose transmission distinguishes a correct shift direction from a reversed
// one; a symmetric byte passes under either.
do_reset;
gen_start; gen_byte(8'h80); gen_ack_slot; gen_byte(8'h01);
$display("T4 MSB first, proved with 0x80 then 0x01");
ck_int("T4 first byte", log[0], 8'h80);
ck_int("T4 second byte", log[1], 8'h01);
// T5. The extremes.
do_reset;
gen_start; gen_byte(8'h00); gen_ack_slot; gen_byte(8'hFF);
$display("T5 all zeros and all ones");
ck_int("T5 zeros", log[0], 8'h00);
ck_int("T5 ones", log[1], 8'hFF);
// T6. Four bytes in a row, in order, none dropped and none duplicated.
do_reset;
gen_start;
gen_byte(8'h11); gen_ack_slot;
gen_byte(8'h22); gen_ack_slot;
gen_byte(8'h33); gen_ack_slot;
gen_byte(8'h44); gen_ack_slot;
$display("T6 four bytes, in order, none dropped or duplicated");
ck_int("T6 four deliveries", n_valid_obs, 4);
ck_int("T6 b0", log[0], 8'h11);
ck_int("T6 b1", log[1], 8'h22);
ck_int("T6 b2", log[2], 8'h33);
ck_int("T6 b3", log[3], 8'h44);
// T7. EXACTLY EIGHT BITS. A ninth shift would consume the acknowledge slot as data.
do_reset;
gen_start;
for (k = 0; k < 7; k = k + 1) gen_bit(1'b1);
ck_bit("T7 still receiving after seven", receiving, 1'b1);
ck_int("T7 index reached seven", bit_index, 7);
gen_bit(1'b1);
ck_bit("T7 done after exactly eight", receiving, 1'b0);
ck_int("T7 one delivery", n_valid_obs, 1);
gen_ack_slot;
ck_int("T7 and the ninth pulse delivered nothing", n_valid_obs, 1);
// T8. A STOP MID-BYTE DISCARDS THE PARTIAL. Holding it would let the NEXT
// transfer's first bits complete this one, producing a byte that was never
// on the wire.
do_reset;
gen_start;
gen_bit(1'b1); gen_bit(1'b0); gen_bit(1'b1); // three bits only
ck_bit("T8 a byte is in progress", receiving, 1'b1);
gen_stop;
$display("T8 a STOP mid-byte discards the partial rather than holding it");
ck_int("T8 nothing was delivered", n_valid_obs, 0);
ck_int("T8 and the abandonment was counted", n_partial, 1);
// Now a fresh transfer: its first byte must be its own, not five bits of it.
gen_start; gen_byte(8'h5A);
ck_int("T8 the next byte is complete and correct", log[0], 8'h5A);
ck_int("T8 delivered once", n_valid_obs, 1);
// T9. A REPEATED START MID-BYTE does the same.
do_reset;
gen_start;
gen_bit(1'b1); gen_bit(1'b1);
gen_restart;
$display("T9 a repeated START mid-byte also discards the partial");
ck_int("T9 nothing delivered", n_valid_obs, 0);
ck_int("T9 counted as abandoned", n_partial, 1);
// T10. The value is stable when the pulse is high -- a consumer that registers
// rx_byte on rx_valid must get the right byte.
do_reset;
gen_start; gen_byte(8'h96);
$display("T10 the byte is stable in the cycle the pulse is asserted");
ck_int("T10 the logged value matches", log[0], 8'h96);
ck_int("T10 and the output still holds it", rx_byte, 8'h96);
// T11. Disabling mid-byte stops the shift where it is, and does not deliver.
do_reset;
gen_start;
gen_bit(1'b1); gen_bit(1'b0);
@(negedge clk); rx_enable = 1'b0;
gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1);
gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1);
$display("T11 disabling mid-byte stops the shift and delivers nothing");
ck_int("T11 no delivery", n_valid_obs, 0);
// T12. Reset mid-byte clears everything.
do_reset;
gen_start; gen_bit(1'b1); gen_bit(1'b0);
@(negedge clk); rst_n = 1'b0; step; step; @(negedge clk); rst_n = 1'b1; phase;
$display("T12 reset mid-byte clears the partial and the counters");
ck_bit("T12 not receiving", receiving, 1'b0);
ck_int("T12 counters cleared", n_bytes, 0);
// ----------------------------------------------------------------
// T13. byte_done IS THE INTERFACE TO THE ACKNOWLEDGE BLOCK. One pulse, one cycle,
// simultaneous with rx_valid, once per byte. Chapter 18.5 arms its slot on
// this signal and nothing else, so a receiver with perfect bytes and no
// byte_done leaves the target unable to acknowledge anything at all.
// ----------------------------------------------------------------
do_reset;
gen_start;
gen_byte(8'h3C); gen_ack_slot;
gen_byte(8'hC3); gen_ack_slot;
$display("T13 byte_done fires once per byte, one cycle, alongside rx_valid");
ck_int("T13 two byte_done pulses", n_done_obs, 2);
ck_int("T13 and two deliveries", n_valid_obs, 2);
ck_int("T13 neither pulse was wide", done_wide, 0);
ck_int("T13 and they were always simultaneous", done_mismatch, 0);
if (errors == 0) $display("=== i2c_slave_rx: ALL CHECKS PASSED ===");
else $display("=== i2c_slave_rx: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule // -----------------------------------------------------------------------------
// i2c_slave_rx_tb.v
// Independent oracle for i2c_slave_rx. The hand-off is what is under test, not the value.
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module i2c_slave_rx_tb;
localparam integer HALF = 8;
reg clk = 1'b0, rst_n = 1'b0;
reg m_scl = 1'b1, m_sda = 1'b1;
reg rx_enable = 1'b1;
wire scl_pin = m_scl, sda_pin = m_sda;
wire scl_q, sda_q, scl_rise, scl_fall, sda_rise, sda_fall;
wire start_pulse, restart_pulse, stop_pulse, bus_active, fmid;
wire [15:0] n_sta, n_rs, n_sto;
wire receiving, rx_valid, byte_done;
wire [3:0] bit_index;
wire [7:0] rx_byte;
wire [15:0] n_bytes, n_partial;
integer errors = 0, n, k;
// ---- the hand-off observer. A byte delivered twice is as bad as one dropped, and
// both are invisible to a test that only compares the value.
integer n_valid_obs = 0, wide = 0;
reg [7:0] log [0:15];
integer n_log = 0;
reg v_d = 1'b0, bd_d = 1'b0;
// `byte_done` gets its own observer. It is the pulse Chapter 18.5's acknowledge block
// ARMS on, and this bench does not instantiate that block -- so without a check here
// a receiver whose bytes are all correct and whose byte_done never fires would pass
// everything while leaving the target permanently unable to acknowledge.
integer n_done_obs = 0, done_wide = 0, done_mismatch = 0;
always @(posedge clk) begin
if (rst_n) begin
if (rx_valid) begin
n_valid_obs <= n_valid_obs + 1;
log[n_log[3:0]] = rx_byte; n_log = n_log + 1;
end
if (rx_valid & v_d) wide <= wide + 1;
if (byte_done) n_done_obs <= n_done_obs + 1;
if (byte_done & bd_d) done_wide <= done_wide + 1;
// The two pulses must be simultaneous: the acknowledge decision is taken from
// the byte, so a byte_done that did not coincide with its rx_valid would arm a
// slot for data the consumer has not been handed.
if (byte_done !== rx_valid) done_mismatch <= done_mismatch + 1;
end
v_d <= rx_valid; bd_d <= byte_done;
end
i2c_slave_sync #(.SYNC_DEPTH(2)) u_sync (
.clk(clk), .rst_n(rst_n), .scl_pin(scl_pin), .sda_pin(sda_pin),
.scl_q(scl_q), .sda_q(sda_q), .scl_rise(scl_rise), .scl_fall(scl_fall),
.sda_rise(sda_rise), .sda_fall(sda_fall));
i2c_slave_framing #(.CNT_W(16)) u_frm (
.clk(clk), .rst_n(rst_n), .scl_q(scl_q),
.sda_rise(sda_rise), .sda_fall(sda_fall),
.start_pulse(start_pulse), .restart_pulse(restart_pulse), .stop_pulse(stop_pulse),
.bus_active(bus_active), .mid_byte(receiving), .framing_midbyte(fmid),
.n_starts(n_sta), .n_restarts(n_rs), .n_stops(n_sto));
i2c_slave_rx #(.CNT_W(16)) dut (
.clk(clk), .rst_n(rst_n), .scl_rise(scl_rise), .sda_q(sda_q),
.start_pulse(start_pulse), .stop_pulse(stop_pulse), .rx_enable(rx_enable),
.receiving(receiving), .bit_index(bit_index), .rx_byte(rx_byte),
.rx_valid(rx_valid), .byte_done(byte_done),
.n_bytes(n_bytes), .n_partial(n_partial));
always #5 clk = ~clk;
task step; begin @(posedge clk); @(negedge clk); end endtask
task phase; begin for (n = 0; n < HALF; n = n + 1) step; end endtask
task do_reset;
begin
@(negedge clk); rst_n = 1'b0; m_scl = 1'b1; m_sda = 1'b1; rx_enable = 1'b1;
n_valid_obs = 0; wide = 0; n_log = 0;
n_done_obs = 0; done_wide = 0; done_mismatch = 0;
step; step; @(negedge clk); rst_n = 1'b1; phase;
end
endtask
task gen_start; begin
@(negedge clk); m_sda = 1'b1; m_scl = 1'b1; phase;
@(negedge clk); m_sda = 1'b0; phase;
@(negedge clk); m_scl = 1'b0; phase; end
endtask
task gen_stop; begin
@(negedge clk); m_scl = 1'b0; m_sda = 1'b0; phase;
@(negedge clk); m_scl = 1'b1; phase;
@(negedge clk); m_sda = 1'b1; phase; end
endtask
task gen_restart; begin
@(negedge clk); m_scl = 1'b0; m_sda = 1'b1; phase;
@(negedge clk); m_scl = 1'b1; phase;
@(negedge clk); m_sda = 1'b0; phase;
@(negedge clk); m_scl = 1'b0; phase; end
endtask
task gen_bit (input b); begin
@(negedge clk); m_scl = 1'b0; phase;
@(negedge clk); m_sda = b; phase;
@(negedge clk); m_scl = 1'b1; phase;
@(negedge clk); m_scl = 1'b0; phase; end
endtask
task gen_byte (input [7:0] d);
begin for (k = 7; k >= 0; k = k - 1) gen_bit(d[k]); end
endtask
// The ninth pulse. `rx_enable` is dropped for it, which is what the transaction layer
// of Chapter 18.10 does: the acknowledge pulse is not a data bit, and a receive path
// left enabled through it shifts it in -- so every byte after the first is displaced
// by one bit. The gate is the contract, and this task models it.
task gen_ack_slot; begin
@(negedge clk); m_scl = 1'b0; rx_enable = 1'b0; phase;
@(negedge clk); m_sda = 1'b1; phase;
@(negedge clk); m_scl = 1'b1; phase;
@(negedge clk); m_scl = 1'b0; phase;
@(negedge clk); rx_enable = 1'b1; end
endtask
task ck_bit (input [200*8:1] w, input g, input e);
begin if (g !== e) begin
$display(" FAIL %0s: got %0b expected %0b", w, g, e); errors = errors + 1; end end
endtask
task ck_int (input [200*8:1] w, input integer g, input integer e);
begin if (g !== e) begin
$display(" FAIL %0s: got %0d expected %0d", w, g, e); errors = errors + 1; end end
endtask
initial begin
$display("=== i2c_slave_rx: eight observed bits, handed off exactly once ===");
// T1. Reset delivers nothing.
do_reset;
$display("T1 a reset receiver has delivered nothing");
ck_int("T1 no bytes", n_valid_obs, 0);
ck_bit("T1 not receiving", receiving, 1'b0);
// T2. DISABLED MEANS DEAF. Without the gate this block would shift the address
// byte as data, and the first data byte of every write would be the address.
do_reset;
@(negedge clk); rx_enable = 1'b0;
gen_start; gen_byte(8'hA5);
$display("T2 disabled, it shifts nothing -- which is what keeps the address out");
ck_int("T2 no byte delivered", n_valid_obs, 0);
ck_bit("T2 and it never started receiving", receiving, 1'b0);
// T3. One byte, exactly once.
do_reset;
gen_start; gen_byte(8'hA5);
$display("T3 one byte in, one delivery out");
ck_int("T3 exactly one delivery", n_valid_obs, 1);
ck_int("T3 the value is right", log[0], 8'hA5);
ck_int("T3 the pulse was one cycle", wide, 0);
ck_int("T3 the counter agrees", n_bytes, 1);
// T4. MSB first, proved with an asymmetric byte. 0x80 and 0x01 are the two values
// whose transmission distinguishes a correct shift direction from a reversed
// one; a symmetric byte passes under either.
do_reset;
gen_start; gen_byte(8'h80); gen_ack_slot; gen_byte(8'h01);
$display("T4 MSB first, proved with 0x80 then 0x01");
ck_int("T4 first byte", log[0], 8'h80);
ck_int("T4 second byte", log[1], 8'h01);
// T5. The extremes.
do_reset;
gen_start; gen_byte(8'h00); gen_ack_slot; gen_byte(8'hFF);
$display("T5 all zeros and all ones");
ck_int("T5 zeros", log[0], 8'h00);
ck_int("T5 ones", log[1], 8'hFF);
// T6. Four bytes in a row, in order, none dropped and none duplicated.
do_reset;
gen_start;
gen_byte(8'h11); gen_ack_slot;
gen_byte(8'h22); gen_ack_slot;
gen_byte(8'h33); gen_ack_slot;
gen_byte(8'h44); gen_ack_slot;
$display("T6 four bytes, in order, none dropped or duplicated");
ck_int("T6 four deliveries", n_valid_obs, 4);
ck_int("T6 b0", log[0], 8'h11);
ck_int("T6 b1", log[1], 8'h22);
ck_int("T6 b2", log[2], 8'h33);
ck_int("T6 b3", log[3], 8'h44);
// T7. EXACTLY EIGHT BITS. A ninth shift would consume the acknowledge slot as data.
do_reset;
gen_start;
for (k = 0; k < 7; k = k + 1) gen_bit(1'b1);
ck_bit("T7 still receiving after seven", receiving, 1'b1);
ck_int("T7 index reached seven", bit_index, 7);
gen_bit(1'b1);
ck_bit("T7 done after exactly eight", receiving, 1'b0);
ck_int("T7 one delivery", n_valid_obs, 1);
gen_ack_slot;
ck_int("T7 and the ninth pulse delivered nothing", n_valid_obs, 1);
// T8. A STOP MID-BYTE DISCARDS THE PARTIAL. Holding it would let the NEXT
// transfer's first bits complete this one, producing a byte that was never
// on the wire.
do_reset;
gen_start;
gen_bit(1'b1); gen_bit(1'b0); gen_bit(1'b1); // three bits only
ck_bit("T8 a byte is in progress", receiving, 1'b1);
gen_stop;
$display("T8 a STOP mid-byte discards the partial rather than holding it");
ck_int("T8 nothing was delivered", n_valid_obs, 0);
ck_int("T8 and the abandonment was counted", n_partial, 1);
// Now a fresh transfer: its first byte must be its own, not five bits of it.
gen_start; gen_byte(8'h5A);
ck_int("T8 the next byte is complete and correct", log[0], 8'h5A);
ck_int("T8 delivered once", n_valid_obs, 1);
// T9. A REPEATED START MID-BYTE does the same.
do_reset;
gen_start;
gen_bit(1'b1); gen_bit(1'b1);
gen_restart;
$display("T9 a repeated START mid-byte also discards the partial");
ck_int("T9 nothing delivered", n_valid_obs, 0);
ck_int("T9 counted as abandoned", n_partial, 1);
// T10. The value is stable when the pulse is high -- a consumer that registers
// rx_byte on rx_valid must get the right byte.
do_reset;
gen_start; gen_byte(8'h96);
$display("T10 the byte is stable in the cycle the pulse is asserted");
ck_int("T10 the logged value matches", log[0], 8'h96);
ck_int("T10 and the output still holds it", rx_byte, 8'h96);
// T11. Disabling mid-byte stops the shift where it is, and does not deliver.
do_reset;
gen_start;
gen_bit(1'b1); gen_bit(1'b0);
@(negedge clk); rx_enable = 1'b0;
gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1);
gen_bit(1'b1); gen_bit(1'b1); gen_bit(1'b1);
$display("T11 disabling mid-byte stops the shift and delivers nothing");
ck_int("T11 no delivery", n_valid_obs, 0);
// T12. Reset mid-byte clears everything.
do_reset;
gen_start; gen_bit(1'b1); gen_bit(1'b0);
@(negedge clk); rst_n = 1'b0; step; step; @(negedge clk); rst_n = 1'b1; phase;
$display("T12 reset mid-byte clears the partial and the counters");
ck_bit("T12 not receiving", receiving, 1'b0);
ck_int("T12 counters cleared", n_bytes, 0);
// ----------------------------------------------------------------
// T13. byte_done IS THE INTERFACE TO THE ACKNOWLEDGE BLOCK. One pulse, one cycle,
// simultaneous with rx_valid, once per byte. Chapter 18.5 arms its slot on
// this signal and nothing else, so a receiver with perfect bytes and no
// byte_done leaves the target unable to acknowledge anything at all.
// ----------------------------------------------------------------
do_reset;
gen_start;
gen_byte(8'h3C); gen_ack_slot;
gen_byte(8'hC3); gen_ack_slot;
$display("T13 byte_done fires once per byte, one cycle, alongside rx_valid");
ck_int("T13 two byte_done pulses", n_done_obs, 2);
ck_int("T13 and two deliveries", n_valid_obs, 2);
ck_int("T13 neither pulse was wide", done_wide, 0);
ck_int("T13 and they were always simultaneous", done_mismatch, 0);
if (errors == 0) $display("=== i2c_slave_rx: ALL CHECKS PASSED ===");
else $display("=== i2c_slave_rx: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule -- -----------------------------------------------------------------------------
-- i2c_slave_rx_tb.vhd
-- Independent oracle for i2c_slave_rx. Behavioural twin of the SystemVerilog and Verilog
-- benches. The HAND-OFF is what is under test, not the value: a byte delivered twice is
-- as bad as one dropped, and both are invisible to a test that only compares bytes.
-- -----------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_slave_rx_tb is
end entity i2c_slave_rx_tb;
architecture sim of i2c_slave_rx_tb is
constant HALF : positive := 8;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal m_scl : std_logic := '1';
signal m_sda : std_logic := '1';
signal rx_enable : std_logic := '1';
signal scl_q, sda_q : std_logic;
signal scl_rise, scl_fall, sda_rise, sda_fall : std_logic;
signal start_pulse, restart_pulse, stop_pulse, bus_active, fmid : std_logic;
signal n_sta, n_rs, n_sto : unsigned(15 downto 0);
signal receiving, rx_valid, byte_done : std_logic;
signal bit_index : unsigned(3 downto 0);
signal rx_byte : std_logic_vector(7 downto 0);
signal n_bytes, n_partial : unsigned(15 downto 0);
signal halt : boolean := false;
type log_t is array (0 to 15) of std_logic_vector(7 downto 0);
signal logmem : log_t := (others => (others => '0'));
signal n_log : integer := 0;
signal n_valid_obs, wide : integer := 0;
-- byte_done gets its own observer: it is the pulse Chapter 18.5's acknowledge
-- block ARMS on, and this bench does not instantiate that block.
signal n_done_obs, done_wide, done_mismatch : integer := 0;
signal clr : boolean := false;
begin
u_sync : entity work.i2c_slave_sync
generic map (SYNC_DEPTH => 2)
port map (clk => clk, rst_n => rst_n, scl_pin => m_scl, sda_pin => m_sda,
scl_q => scl_q, sda_q => sda_q, scl_rise => scl_rise, scl_fall => scl_fall,
sda_rise => sda_rise, sda_fall => sda_fall);
u_frm : entity work.i2c_slave_framing
generic map (CNT_W => 16)
port map (clk => clk, rst_n => rst_n, scl_q => scl_q,
sda_rise => sda_rise, sda_fall => sda_fall,
start_pulse => start_pulse, restart_pulse => restart_pulse,
stop_pulse => stop_pulse, bus_active => bus_active,
mid_byte => receiving, framing_midbyte => fmid,
n_starts => n_sta, n_restarts => n_rs, n_stops => n_sto);
dut : entity work.i2c_slave_rx
generic map (CNT_W => 16)
port map (clk => clk, rst_n => rst_n, scl_rise => scl_rise, sda_q => sda_q,
start_pulse => start_pulse, stop_pulse => stop_pulse, rx_enable => rx_enable,
receiving => receiving, bit_index => bit_index, rx_byte => rx_byte,
rx_valid => rx_valid, byte_done => byte_done,
n_bytes => n_bytes, n_partial => n_partial);
clkgen : process
begin
while not halt loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
obs : process (clk, clr)
variable v_d, bd_d : std_logic := '0';
begin
if clr then
n_valid_obs <= 0; wide <= 0; n_log <= 0;
n_done_obs <= 0; done_wide <= 0; done_mismatch <= 0;
elsif rising_edge(clk) then
if rst_n = '1' then
if rx_valid = '1' then
n_valid_obs <= n_valid_obs + 1;
logmem(n_log mod 16) <= rx_byte;
n_log <= n_log + 1;
end if;
if rx_valid = '1' and v_d = '1' then wide <= wide + 1; end if;
if byte_done = '1' then n_done_obs <= n_done_obs + 1; end if;
if byte_done = '1' and bd_d = '1' then done_wide <= done_wide + 1; end if;
-- The two pulses must be simultaneous.
if byte_done /= rx_valid then done_mismatch <= done_mismatch + 1; end if;
end if;
v_d := rx_valid; bd_d := byte_done;
end if;
end process;
stim : process
variable err : integer := 0;
variable k : integer;
procedure step is
begin
wait until rising_edge(clk); wait until falling_edge(clk);
end procedure;
procedure phase is
begin
for i in 1 to HALF loop step; end loop;
end procedure;
procedure do_reset is
begin
wait until falling_edge(clk);
rst_n <= '0'; m_scl <= '1'; m_sda <= '1'; rx_enable <= '1';
clr <= true; wait for 1 ns; clr <= false;
step; step;
wait until falling_edge(clk); rst_n <= '1';
phase;
end procedure;
procedure gen_start is
begin
wait until falling_edge(clk); m_sda <= '1'; m_scl <= '1'; phase;
wait until falling_edge(clk); m_sda <= '0'; phase;
wait until falling_edge(clk); m_scl <= '0'; phase;
end procedure;
procedure gen_restart is
begin
wait until falling_edge(clk); m_scl <= '0'; m_sda <= '1'; phase;
wait until falling_edge(clk); m_scl <= '1'; phase;
wait until falling_edge(clk); m_sda <= '0'; phase;
wait until falling_edge(clk); m_scl <= '0'; phase;
end procedure;
procedure gen_stop is
begin
wait until falling_edge(clk); m_scl <= '0'; m_sda <= '0'; phase;
wait until falling_edge(clk); m_scl <= '1'; phase;
wait until falling_edge(clk); m_sda <= '1'; phase;
end procedure;
procedure gen_bit (b : std_logic) is
begin
wait until falling_edge(clk); m_scl <= '0'; phase;
wait until falling_edge(clk); m_sda <= b; phase;
wait until falling_edge(clk); m_scl <= '1'; phase;
wait until falling_edge(clk); m_scl <= '0'; phase;
end procedure;
procedure gen_byte (d : std_logic_vector(7 downto 0)) is
begin
for i in 7 downto 0 loop gen_bit(d(i)); end loop;
end procedure;
-- The ninth pulse. rx_enable is dropped for it, which is what the transaction layer
-- of Chapter 18.10 does: the acknowledge pulse is not a data bit, and a receive path
-- left enabled through it shifts it in -- so every byte after the first is displaced.
procedure gen_ack_slot is
begin
wait until falling_edge(clk); m_scl <= '0'; rx_enable <= '0'; phase;
wait until falling_edge(clk); m_sda <= '1'; phase;
wait until falling_edge(clk); m_scl <= '1'; phase;
wait until falling_edge(clk); m_scl <= '0'; phase;
wait until falling_edge(clk); rx_enable <= '1';
end procedure;
procedure ck_bit (what : string; g : std_logic; e : std_logic) is
begin
if g /= e then
report " FAIL " & what severity note;
err := err + 1;
end if;
end procedure;
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;
begin
report "=== i2c_slave_rx: eight observed bits, handed off exactly once ==="
severity note;
-- T1. Reset delivers nothing.
do_reset;
report "T1 a reset receiver has delivered nothing" severity note;
ck_int("T1 no bytes", n_valid_obs, 0);
ck_bit("T1 not receiving", receiving, '0');
-- T2. Disabled means deaf.
do_reset;
wait until falling_edge(clk); rx_enable <= '0';
gen_start; gen_byte(x"A5");
report "T2 disabled, it shifts nothing -- which is what keeps the address out"
severity note;
ck_int("T2 no byte delivered", n_valid_obs, 0);
ck_bit("T2 and it never started receiving", receiving, '0');
-- T3. One byte, exactly once.
do_reset;
gen_start; gen_byte(x"A5");
report "T3 one byte in, one delivery out" severity note;
ck_int("T3 exactly one delivery", n_valid_obs, 1);
ck_int("T3 the value is right", to_integer(unsigned(logmem(0))), 16#A5#);
ck_int("T3 the pulse was one cycle", wide, 0);
ck_int("T3 the counter agrees", to_integer(n_bytes), 1);
-- T4. MSB first, proved with 0x80 then 0x01.
do_reset;
gen_start; gen_byte(x"80"); gen_ack_slot; gen_byte(x"01");
report "T4 MSB first, proved with 0x80 then 0x01" severity note;
ck_int("T4 first byte", to_integer(unsigned(logmem(0))), 16#80#);
ck_int("T4 second byte", to_integer(unsigned(logmem(1))), 16#01#);
-- T5. The extremes.
do_reset;
gen_start; gen_byte(x"00"); gen_ack_slot; gen_byte(x"FF");
report "T5 all zeros and all ones" severity note;
ck_int("T5 zeros", to_integer(unsigned(logmem(0))), 0);
ck_int("T5 ones", to_integer(unsigned(logmem(1))), 255);
-- T6. Four bytes in order.
do_reset;
gen_start;
gen_byte(x"11"); gen_ack_slot;
gen_byte(x"22"); gen_ack_slot;
gen_byte(x"33"); gen_ack_slot;
gen_byte(x"44"); gen_ack_slot;
report "T6 four bytes, in order, none dropped or duplicated" severity note;
ck_int("T6 four deliveries", n_valid_obs, 4);
ck_int("T6 b0", to_integer(unsigned(logmem(0))), 16#11#);
ck_int("T6 b1", to_integer(unsigned(logmem(1))), 16#22#);
ck_int("T6 b2", to_integer(unsigned(logmem(2))), 16#33#);
ck_int("T6 b3", to_integer(unsigned(logmem(3))), 16#44#);
-- T7. Exactly eight bits.
do_reset;
gen_start;
for k in 0 to 6 loop gen_bit('1'); end loop;
ck_bit("T7 still receiving after seven", receiving, '1');
ck_int("T7 index reached seven", to_integer(bit_index), 7);
gen_bit('1');
ck_bit("T7 done after exactly eight", receiving, '0');
ck_int("T7 one delivery", n_valid_obs, 1);
gen_ack_slot;
ck_int("T7 and the ninth pulse delivered nothing", n_valid_obs, 1);
-- T8. A STOP mid-byte discards the partial.
do_reset;
gen_start;
gen_bit('1'); gen_bit('0'); gen_bit('1');
ck_bit("T8 a byte is in progress", receiving, '1');
gen_stop;
report "T8 a STOP mid-byte discards the partial rather than holding it"
severity note;
ck_int("T8 nothing was delivered", n_valid_obs, 0);
ck_int("T8 and the abandonment was counted", to_integer(n_partial), 1);
gen_start; gen_byte(x"5A");
ck_int("T8 the next byte is complete and correct",
to_integer(unsigned(logmem(0))), 16#5A#);
ck_int("T8 delivered once", n_valid_obs, 1);
-- T9. A repeated START mid-byte does the same.
do_reset;
gen_start;
gen_bit('1'); gen_bit('1');
gen_restart;
report "T9 a repeated START mid-byte also discards the partial" severity note;
ck_int("T9 nothing delivered", n_valid_obs, 0);
ck_int("T9 counted as abandoned", to_integer(n_partial), 1);
-- T10. The value is stable while the pulse is asserted.
do_reset;
gen_start; gen_byte(x"96");
report "T10 the byte is stable in the cycle the pulse is asserted" severity note;
ck_int("T10 the logged value matches", to_integer(unsigned(logmem(0))), 16#96#);
ck_int("T10 and the output still holds it", to_integer(unsigned(rx_byte)), 16#96#);
-- T11. Disabling mid-byte stops the shift and delivers nothing.
do_reset;
gen_start;
gen_bit('1'); gen_bit('0');
wait until falling_edge(clk); rx_enable <= '0';
for k in 0 to 6 loop gen_bit('1'); end loop;
report "T11 disabling mid-byte stops the shift and delivers nothing" severity note;
ck_int("T11 no delivery", n_valid_obs, 0);
-- T12. Reset mid-byte clears everything.
do_reset;
gen_start; gen_bit('1'); gen_bit('0');
wait until falling_edge(clk); rst_n <= '0'; step; step;
wait until falling_edge(clk); rst_n <= '1'; phase;
report "T12 reset mid-byte clears the partial and the counters" severity note;
ck_bit("T12 not receiving", receiving, '0');
ck_int("T12 counters cleared", to_integer(n_bytes), 0);
-- T13. byte_done is the interface to the acknowledge block.
do_reset;
gen_start;
gen_byte(x"3C"); gen_ack_slot;
gen_byte(x"C3"); gen_ack_slot;
report "T13 byte_done fires once per byte, one cycle, alongside rx_valid"
severity note;
ck_int("T13 two byte_done pulses", n_done_obs, 2);
ck_int("T13 and two deliveries", n_valid_obs, 2);
ck_int("T13 neither pulse was wide", done_wide, 0);
ck_int("T13 and they were always simultaneous", done_mismatch, 0);
if err = 0 then
report "=== i2c_slave_rx: ALL CHECKS PASSED ===" severity note;
else
report "=== i2c_slave_rx: " & integer'image(err) & " CHECK(S) FAILED ==="
severity note;
end if;
halt <= true;
wait;
end process;
end architecture sim;7b. Execution
| Design | SystemVerilog | Verilog-2001 | VHDL | Finish |
|---|---|---|---|---|
i2c_slave_rx | PASS 13/13 | PASS 13/13 | PASS 13/13 | 58130 ns, all three |
8. Mutation Testing
Ten defects.
| # | Injected defect | Expected detection | Result |
|---|---|---|---|
| M1 | LSB first | T4 | KILLED (9) |
| M2 | seven bits per byte | T7 | KILLED (15) |
| M3 | nine bits — the acknowledge slot shifted in as data | T7 | KILLED (22) |
| M4 | the delivered byte loses its last bit | T3, T4 | KILLED (12) |
| M5 | rx_valid becomes a level | T3 | KILLED (14) |
| M6 | the enable gate dropped — the address is received as data | T2 | KILLED (7) |
| M7 | sampled on any cycle, not at the rising edge | T3–T6 | KILLED (29) |
| M8 | framing does not discard a partial byte | T8 | KILLED (4) |
| M9 | the bit index not cleared on completion | T6 | KILLED (10) |
| M10 | byte_done never emitted | T13 new | KILLED (3) |
baseline: PASS (verified before injecting anything)
valid mutants: 10 killed: 10 survived: 0 equivalent: 0 invalid: 0
restored: PASSM10 survived, and it is the fourth instance of one pattern
Silencing byte_done left every byte correct, every delivery counted, every order right — and the suite passed. rx_valid was checked exhaustively; byte_done was not checked at all.
T13 adds three checks on it: one pulse per byte, never two cycles wide, and always simultaneous with rx_valid. The last is the one that matters most — the acknowledge decision is taken from the byte, so a byte_done that did not coincide with its rx_valid would arm a slot for data the consumer has not been handed.
This is the same shape as Module 17.6 §7's bit_done, and the fourth survivor in this module to be an unexamined output or the untested half of a property:
| Chapter | Survivor | What was unexamined |
|---|---|---|
| 18.2 | M4, M5 | line independence in one direction; the level, not the pulse |
| 18.3 | M10 | the START branch of the mid-byte report |
| 18.5 | M8 | framing inputs tied to zero |
| 18.6 | M10 | byte_done, with no consumer instantiated |
9. Verification Connection — The Predictor's Ordering Problem
// A receive-path scoreboard is where a target environment first has to care about
// ORDER rather than content, and the distinction is sharper than it looks.
//
// a monitor sees : bytes 0x11, 0x22, 0x33 on the wire, in that order
// the DUT delivers : 0x11, 0x22, 0x33 -- but delivered HOW MANY TIMES, and WHEN?
//
// A scoreboard written as an unordered multiset comparison passes a DUT that delivers
// 0x11 twice and 0x22 never, if the payload happens to contain a repeat. A scoreboard
// written as a QUEUE does not -- which is why the bench here logs into an indexed array
// and compares position by position rather than checking membership.
//
// THE HARDER CASE IS A PARTIAL BYTE. When framing abandons a byte mid-flight the
// predictor must NOT expect a delivery -- and it must also not expect the partial bits
// to appear prepended to the next byte. Both are wrong in different directions:
//
// wrong 1: predict a byte -> the DUT correctly delivered nothing, FAIL
// wrong 2: predict the next byte as (partial | next bits) -> models the BUG
//
// So the predictor needs the framing events, not only the data bits, which means the
// pin-level monitor has to emit framing as first-class items rather than only bytes.
// A byte-only monitor cannot describe an aborted transfer at all.
//
// AND THE ENABLE IS ENVIRONMENT KNOWLEDGE. Whether a given ninth pulse is an
// acknowledge or a data bit is not on the wire -- it depends on byte position, which
// the monitor tracks because it followed the frame from the START. That is the same
// fact Chapter 17.7 §8 established for the master's byte monitor, and it is why a
// monitor is a state machine rather than a sampler.
//
// COVERAGE:
//
// cover: a single-byte write
// cover: a multi-byte write (>= 4 bytes) catches index-clear bugs (M9)
// cover: 0x00 and 0xFF the extremes
// cover: 0x80 and 0x01 the order-distinguishing pair
// cover: a byte abandoned by a STOP T8
// cover: a byte abandoned by a repeated START T9
// illegal_bin: two deliveries in consecutive cycles10. FPGA and ASIC Implications
On an FPGA this is nine flops and a comparator — nothing. The interesting property is the interface: rx_valid is one cycle, so the consumer must register it. That is a deliberate cost shifted downstream, and Chapter 18.9 is where it is paid: the register file latches on the pulse.
The alternative — a level plus a consumer handshake — would put a bus-paced producer and a logic-paced consumer in the same flow-control loop, and the bus does not wait. A target that needs the consumer to be slow must stretch instead (18.10), which is a protocol mechanism rather than a datapath one.
On an ASIC, the byte-per-SCL-period rate makes this the slowest datapath in the design: at 400 kHz a byte arrives every ~22 µs, so there is no throughput consideration at any plausible system clock. What does matter is that the pulse is one cycle at the system clock, not one SCL period — so a consumer clocked from a different domain cannot sample it reliably and needs either the same clock or its own handshake. This module keeps everything in one domain deliberately; Module 19.6 owns the case where that is not true.
Reset clears the shift register and both counters, and leaves nothing pending. There is no bus-facing output here at all, which is why this block cannot damage a bus even when it is wrong — Chapter 18.1 §5's observation about where the risk lives.
11. Debugging — The Register Writes That Landed One Bit Out
A target with a register file is written four bytes at a time. The first register of every burst receives the correct value. The second, third and fourth receive values that are recognisably related to the intended ones but wrong -- each looking like the intended byte shifted left, with a bit borrowed from somewhere. Single-byte writes are always correct. Reads are always correct.
The receive path was enabled through the acknowledge pulse, so it shifted the acknowledge bit in as though it were data. A byte is nine clock pulses and only eight of them are data; an ungated receiver therefore consumes nine bits per byte and every byte after the first is displaced by one additional bit. Nothing was wrong with the register file, the pointer, the acknowledge generator or the master. The first byte was correct because nothing precedes it, which is exactly the pattern that makes the symptom look like a consumer bug rather than a receiver one.
Drop rx_enable for the ninth pulse -- the acknowledge slot is not a data bit, and the transaction layer of Chapter 18.10 owns that gating. Note that the same defect can live in the BENCH: a testbench whose acknowledge-slot task leaves the enable asserted reproduces the displacement in its own stimulus and then reports it as a design failure, which is how this boundary was first encountered here. The regression that pins it is T7, which drives eight bits, checks the delivery, then drives the ninth pulse and asserts that nothing further was delivered.Three generalisations.
A displacement that grows by one per byte names its own cause. One extra bit per byte boundary, and there is exactly one extra pulse per byte. Recognising the arithmetic goes straight to the gate.
The first byte being correct is what misdirects. Any defect that accumulates at byte boundaries leaves the first instance clean, and a clean first byte argues strongly that the receiver works.
The same bug can live in the bench. A bench that leaves the enable asserted through its own acknowledge-slot task produces the identical displacement — and then blames the design. Stimulus that models the gating is part of the contract, not a convenience.
12. Common Misconceptions
"Receiving a byte is the same job as receiving an address." Completion produces data a consumer must take, not a decision. The hand-off is the part that fails. §1.
"If the byte value is right, the receive path is right." A byte delivered twice has the right value both times. Value tests cannot see duplication or loss. §1.
"A byte_available flag is friendlier than a pulse." It needs a consumer acknowledgement to clear, which puts a bus-paced producer in a flow-control loop with a logic-paced consumer. The bus does not wait. §2.
"A byte is eight clock pulses." It is nine. The ninth is the acknowledge, and a receiver enabled through it shifts it in as data. §4.
"An ungated receiver corrupts everything." It gets the first byte right and displaces each later one by one more bit — which is why the symptom points at the consumer. §4, §11.
"A partial byte can just stay in the register." Then the next transfer's first bits complete it, producing a byte assembled from two transactions. §5.
"Counting abandoned partials is telemetry." A target that silently discards partials and one that never receives any look identical from outside. §5.
"An output with no consumer in the bench is still tested." It is not tested at all. byte_done was correct in every test and the suite could not see it being silenced. §8.
"A multiset comparison is a fine scoreboard." It passes a DUT that delivers one byte twice and another never, if the payload repeats. Compare as a queue. §9.
13. Reason It Through
Why is the hand-off harder to verify than the value?
Because a duplicated delivery has the correct value, and a test that compares bytes cannot distinguish one delivery from two. Only counting and position can. §1.
Why a one-cycle pulse rather than a level with a handshake?
A handshake would make the consumer's pace part of the bus's flow control, and the bus never waits. A target that genuinely needs time must stretch, which is a protocol mechanism. §2.
Nine pulses per byte, eight of them data. What happens to the ninth in an ungated receiver, and what is the visible pattern?
It is shifted in as a data bit, so each byte after the first is displaced by one additional bit — a displacement that grows by one per byte, with the first byte correct. §4, §11.
Why must a partial byte be discarded rather than left in the register?
Because the next transfer's first bits would complete it, delivering a byte that never existed on the wire and was assembled from two different transactions. §5.
Mutation M10 silenced byte_done and everything passed. Why, and what is the general rule?
Because this bench instantiates no consumer for it, so nothing observed it. An output with no consumer in the bench is an output nobody is checking, and it needs an explicit observer. §8.
Why must byte_done be simultaneous with rx_valid rather than merely near it?
Because 18.5 latches its acknowledge decision when the slot arms, and the decision is derived from the byte. A byte_done out of step would arm a slot for data the consumer has not been handed. §8.
Why can a byte-only monitor not describe an aborted transfer?
Because there is no byte to report — the abandonment is a framing event, and a predictor that sees only bytes cannot know to expect nothing. §9.
14. Understanding Check
15. Summary
The subject is the hand-off, not the value. A byte delivered twice is as bad as one dropped, and both are invisible to a test that compares bytes.
So the contract is narrow: rx_valid is one cycle, exactly once per byte, with rx_byte stable while it is high.
A pulse rather than a flag, because a level with a consumer handshake would put a bus-paced producer in a flow-control loop with a logic-paced consumer — and the bus never waits. A target that needs time must stretch.
Sampled at the rising edge and enabled from above. Without the gate, the address byte is received as data and every later byte is displaced.
The acknowledge pulse is not a data bit. A byte is nine pulses and eight are data; a receiver enabled through the ninth consumes it and each byte after the first is displaced by one more bit — with the first byte always correct, which is what misdirects the diagnosis.
Framing discards a partial byte, or the next transfer's bits complete it and deliver a byte assembled from two transactions.
Ten mutants, ten killed — after M10 silenced byte_done and the entire suite passed.
An output with no consumer in the bench is an output nobody is checking. byte_done now has its own observer: one pulse per byte, never two cycles wide, and always simultaneous with rx_valid.
That is the fourth survivor in this module of one family — an unexamined output or the untested half of a property. Line independence in one direction, the level rather than the pulse, the START branch of a report, tied-off framing inputs, and now an output with nowhere to go.
16. What Comes Next
The target can take bytes in. Chapter 18.7 makes it give them out, and that is the harder direction.
On a write the target receives and has a whole bit slot to think. On a read the master begins clocking immediately after the acknowledge, and the target must already have the first bit on SDA before the first rising edge — because §3.1.2 forbids changing SDA once SCL is high. The byte has to be fetched before it is needed, which is a structural consequence of not owning the clock.
It is also where a transmitted one stops being an abstraction: sending 0xFF means driving nothing at all.
Continue learning
Related tutorials
- Related topic
Sampling SDA and SCL — Edge Detection Inside a Slave
The block every other one in a target is written against: two asynchronous wires turned into five synchronous facts. Three of its decisions produce defects nothing later can recover from — what the samplers reset to, which signals the edge pulses come from, and why the synchroniser belongs here and nowhere else.
- Related topic
START and STOP Detection Inside a Slave
Detection is two AND gates. The substance is what a START resets, and the distinction that is not on the wire at all — a first START and a repeated START are the identical edge, separated only by state the target keeps itself.
- Related topic
Address Shift Register, Address Match and Direction Decode
Three jobs that fail separately: receiving eight bits, deciding whether they name this device, and deciding whether to answer. A design with one state called address cannot tell you which of them broke — and the direction bit governs everything after the byte has left the wire.
- Related topic
ACK Generation and Its Timing Window
Where most slave designs first fail. The acknowledge has three parts, the window is bounded at both ends by falling edges, and asserting early is not early — SDA falling while SCL is high is a START, so a premature acknowledge restarts the transaction instead of acknowledging a byte.
