I²C · Module 18
The Register Interface Behind the Slave
Where Module 16's design decisions stop being a specification and become flops. Builds the register file, the pointer and the auto-increment, and shows why the pointer surviving a repeated START is a wiring decision rather than a promise.
The protocol engine is finished. 18.3 frames, 18.4 matches, 18.6 receives, 18.5 acknowledges, 18.7 transmits and 18.8 listens. None of it has anything to talk to.
1. Six Questions, Now Six Decisions
Chapter 16.1 showed that the specification describes the register-map pattern in one sentence and then delegates the whole of it:
§3.1.10 note 1 "to control a serial memory. The internal memory location
must be written during the first data byte."
§3.1.10 note 2 "All decisions on auto-increment or decrement of previously
accessed memory locations, etc., are taken by the designer
of the device."That second note is the only place in UM10204 that hands a design decision to the reader in so many words. 16.1 turned it into six questions to ask a datasheet. This block is where we are the designer of the device, so the same six become decisions to make and defend:
| # | the question 16.1 asked | this block's answer | where it lives |
|---|---|---|---|
| 1 | does the pointer auto-increment? | yes, after every accepted data byte | §3 |
| 2 | what happens at the end of the map? | wrap | §6 |
| 3 | does the pointer survive a STOP or a repeated START? | yes | §2 |
| 4 | what does a write to a read-only register do? | NACK, so the master learns | §5 |
| 5 | where does the pointer sit after a read? | one past the last byte served | §4 |
| 6 | does a refused write advance the pointer? | no | §5 |
2. The Pointer Survives a Repeated START — Structurally
16.1 §4 made the argument at protocol level: a combined transfer — write the pointer, repeated START, read from it — only works because the pointer written in the first phase is still there in the second.
§3.1.10 note 4 requires a device to reset its bus logic on a START. It says nothing about application state, and it could not: a device that cleared the pointer on a START would be unable to implement the combined format the specification itself describes two pages earlier.
The separation this creates is worth naming, because 18.10 is built entirely around it:
| cleared by a START | examples | |
|---|---|---|
| protocol state | yes — note 4 requires it | the bit counter, the shift register, the address match, the direction bit |
| application state | no | the pointer, the register contents |
A block that holds both kinds cannot obey the rule for one without breaking it for the other. Splitting them across a module boundary is how the rule becomes enforceable.
3. The First Data Byte Is the Pointer
That is note 1, and it is the one normative sentence this block implements. The distinction between pointer byte and data byte is not something this block can work out for itself — it needs to know which byte of the write phase it is looking at, and byte position is transaction state.
So rx_is_pointer is an input, and 18.10 produces it. This block asks what is this byte for?, never where are we in the transfer?
After the pointer byte, each accepted data byte lands at the pointer and the pointer advances — decision 1. Three bytes after one pointer therefore fill three consecutive registers, which is the sequential-write pattern 16.3 described from the master's side.
4. The Read Must Be Combinational
18.7 §1 established the pre-fetch problem: the transmit datapath must have the next byte in the same cycle its request pulses, because the first bit has to be on the wire before the master's next rising edge.
That is a requirement this block has to meet, not a preference:
assign rd_data = mem[pointer % N_REG];And wr_accept is combinational for the same shape of reason, one block over: 18.5 §3 has to latch the acknowledge decision in the cycle the byte completes, so "may this byte be written?" must be answerable without a clock edge. Test T8 asserts it tracks the pointer with no cycle in between.
5. A Refused Write Is a NACK, and It Does Not Advance
Decision 4: a write to a read-only register is refused, which propagates to 18.5 as a NACK. The alternative — accept it and discard the byte — is legal and worse: the master is told the write succeeded and has no way to find out otherwise.
Decision 6 is the one that looks arbitrary and is not:
accepted write -> the byte lands, and the pointer advances
refused write -> nothing lands, and the pointer does NOT advanceThis is 16.1 §6a's argument, and it was a recommendation there. Here it is four lines of RTL and mutation M3, which fails four checks.
6. The Map Wraps — and Why a Power of Two Hides the Decision
Decision 2. The pointer is masked into range on every use, so the byte after the last register lands at register zero. Clamping is the other conforming choice; 16.1 §5 showed the two differ at exactly one address.
So the bench runs two register files at once: the main one at eight registers with register 2 read-only, and a five-register one with no protection whose only job is to make the wrap observable. Mutation M7 — truncation instead of modulo — fails six checks on the second instance and would have failed none on the first.
There is one more consequence, and it is the one that survived a mutation pass:
7. The Register File, in Three Languages
One clocked block, two continuous assignments and a generate loop. The VHDL version differs structurally in two places, and both are the language refusing to let something be implicit — an out port is not readable, and an out-of-range array index is a run-time error rather than a silent wrap.
// -----------------------------------------------------------------------------
// i2c_slave_regs.sv
// The register file behind the protocol engine -- and the pointer, which is the
// interesting part.
//
// WHAT MODULE 16 ESTABLISHED, seen now from inside the device. Chapter 16.1 showed that
// the specification describes the register-map pattern in ONE sentence and delegates
// every detail:
//
// §3.1.10 note 1: "to control a serial memory. The internal memory location must be
// written during the first data byte."
// note 2: "All decisions on auto-increment or decrement of previously accessed memory
// locations, etc., are taken by the designer of the device."
//
// So this block is where "the designer of the device" is US, and the six questions
// Chapter 16.1 asked a datasheet are now six decisions to make and document:
//
// 1. does the pointer auto-increment? YES, after every data byte
// 2. what happens at the end of the map? WRAP, and it is a parameter choice
// 3. does the pointer survive a STOP? YES -- see below, it is the subtle one
// 4. what does a write to a read-only do? NACK, so the master learns
// 5. where does the pointer sit after a read? one past the last byte read
// 6. does a refused write advance it? NO
//
// THE POINTER SURVIVES A REPEATED START, AND THAT IS THE WHOLE POINT. Chapter 16.1 §4
// made the argument at protocol level; here it becomes a wiring decision. A combined
// transfer -- write the pointer, repeated START, read from it -- only works because the
// pointer written in the first phase is still there in the second. §3.1.10 note 4
// requires a device to reset its BUS LOGIC on a START; it says nothing about application
// state, and a device that cleared the pointer there could not implement the combined
// format the specification itself describes.
//
// So this block takes NO framing inputs at all. It cannot clear the pointer on a START
// because it cannot see one -- which is the structural way to guarantee the property
// rather than remembering not to violate it. Chapter 18.10 owns the separation of
// protocol state from application state; this block is the application side of it.
//
// THE FIRST DATA BYTE IS THE POINTER. On a write, byte 0 of the data phase loads the
// pointer and bytes 1..n are data. That is note 1, and it is the only normative sentence
// this block implements.
//
// COMBINATIONAL READ, because Chapter 18.7 needs it. The transmit datapath pre-fetches on
// a falling edge and must have the byte in the same cycle -- so `rd_data` is a
// combinational function of the pointer, not a registered one. A register file that
// needed a cycle would force Chapter 18.10 to stretch the clock for every read byte.
// -----------------------------------------------------------------------------
module i2c_slave_regs #(
parameter int N_REG = 8, // registers in the map
parameter int RO_MASK = 0, // bit i set => register i is read-only
parameter int CNT_W = 16
) (
input logic clk,
input logic rst_n,
// ---- the protocol engine's side -----------------------------------------
// A received byte. `rx_is_pointer` distinguishes the pointer byte from data, and it is
// the ONLY phase information this block takes. There is deliberately no
// `write_phase_started` input: a port this block does not need is a port that invites
// a future maintainer to clear the pointer on one, which would break the combined
// transfer -- see the note on decision 3 above.
input logic rx_valid,
input logic [7:0] rx_byte,
// Asserted with rx_valid when this is the FIRST data byte of the write phase.
input logic rx_is_pointer,
// Can this byte be accepted? Read combinationally so Chapter 18.5 can latch it as the
// acknowledge decision in the same cycle the byte completes.
output logic wr_accept,
// The transmit side. Combinational, for Chapter 18.7's pre-fetch.
output logic [7:0] rd_data,
// One cycle: a byte was taken by the transmitter, so advance.
input logic rd_taken,
// ---- the application's side ---------------------------------------------
// FLAT, not an unpacked array. An unpacked array port is SystemVerilog only, and this
// design has to exist in Verilog-2001 and VHDL with the same interface -- so the map
// is exposed as one packed vector and the consumer slices it. Register i is
// reg_flat[8*i +: 8].
output logic [8*N_REG-1:0] reg_flat,
output logic [7:0] pointer,
output logic [CNT_W-1:0] n_writes,
output logic [CNT_W-1:0] n_refused,
output logic [CNT_W-1:0] n_reads
);
logic [7:0] mem [0:N_REG-1];
integer i;
// The pointer is masked into range on every use, which is decision 2: the map WRAPS.
// Clamping is the other conforming choice and Chapter 16.1 §5 showed the two differ at
// exactly one address -- which is why the behaviour is stated here rather than left to
// whatever the arithmetic happens to do.
wire [7:0] ptr_in_range = pointer % N_REG;
// Read-only is a property of the ADDRESSED register, evaluated now. Decision 4: a
// write to a read-only location is NACKed, so the master learns it failed -- rather
// than accepted and discarded, which tells the master nothing.
wire is_ro = ((RO_MASK >> ptr_in_range) & 1) != 0;
assign wr_accept = ~is_ro;
assign rd_data = mem[ptr_in_range];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pointer <= 8'h00;
n_writes <= {CNT_W{1'b0}};
n_refused <= {CNT_W{1'b0}};
n_reads <= {CNT_W{1'b0}};
for (i = 0; i < N_REG; i = i + 1) mem[i] <= 8'h00;
end else begin
// NOTE what is absent: any reaction to framing. This block cannot see a START or
// a STOP, so it cannot clear the pointer on one -- which is how decision 3 is
// guaranteed structurally instead of by remembering not to break it.
if (rx_valid) begin
if (rx_is_pointer) begin
// Note 1: the internal location is written during the first data byte.
pointer <= rx_byte;
end else if (!is_ro) begin
mem[ptr_in_range] <= rx_byte;
// Decision 1: advance after every accepted data byte.
pointer <= pointer + 8'd1;
n_writes <= n_writes + 1'b1;
end else begin
// Decision 6: a refused write does NOT advance the pointer. Advancing
// would scatter a stubborn master's remaining bytes across the map --
// Chapter 16.1 §6a's argument, now enforced in hardware.
n_refused <= n_refused + 1'b1;
end
end
// ELSE IF, not a second `if`. A received byte and a served byte cannot occur in
// the same cycle, because the direction bit fixes which of the two is possible
// for the whole phase -- so this is not a priority the protocol can exercise.
// It is written as a priority anyway so the behaviour on an illegal input is
// DEFINED rather than a consequence of statement order: two `if`s assigning
// `pointer` in one block leave the later one silently winning, and a maintainer
// reordering them would change behaviour with no diagnostic.
else if (rd_taken) begin
// Decision 5: after a read the pointer sits one past the byte just served.
pointer <= pointer + 8'd1;
n_reads <= n_reads + 1'b1;
end
end
end
// Continuous assigns, not `always @(*)`: an `always @(*)` block that reads a memory
// does not reliably re-evaluate when an element changes, because a memory element is
// not in the implicit sensitivity list in every simulator. A generate of continuous
// assignments has no such ambiguity.
genvar gi;
generate
for (gi = 0; gi < N_REG; gi = gi + 1) begin : g_flat
assign reg_flat[8*gi +: 8] = mem[gi];
end
endgenerate
endmodule // -----------------------------------------------------------------------------
// i2c_slave_regs.v
// The register file behind the protocol engine -- and the pointer, which is the
// interesting part.
//
// WHAT MODULE 16 ESTABLISHED, seen now from inside the device. Chapter 16.1 showed that
// the specification describes the register-map pattern in ONE sentence and delegates
// every detail:
//
// §3.1.10 note 1: "to control a serial memory. The internal memory location must be
// written during the first data byte."
// note 2: "All decisions on auto-increment or decrement of previously accessed memory
// locations, etc., are taken by the designer of the device."
//
// So this block is where "the designer of the device" is US, and the six questions
// Chapter 16.1 asked a datasheet are now six decisions to make and document:
//
// 1. does the pointer auto-increment? YES, after every data byte
// 2. what happens at the end of the map? WRAP, and it is a parameter choice
// 3. does the pointer survive a STOP? YES -- see below, it is the subtle one
// 4. what does a write to a read-only do? NACK, so the master learns
// 5. where does the pointer sit after a read? one past the last byte read
// 6. does a refused write advance it? NO
//
// THE POINTER SURVIVES A REPEATED START, AND THAT IS THE WHOLE POINT. Chapter 16.1 §4
// made the argument at protocol level; here it becomes a wiring decision. A combined
// transfer -- write the pointer, repeated START, read from it -- only works because the
// pointer written in the first phase is still there in the second. §3.1.10 note 4
// requires a device to reset its BUS LOGIC on a START; it says nothing about application
// state, and a device that cleared the pointer there could not implement the combined
// format the specification itself describes.
//
// So this block takes NO framing inputs at all. It cannot clear the pointer on a START
// because it cannot see one -- which is the structural way to guarantee the property
// rather than remembering not to violate it. Chapter 18.10 owns the separation of
// protocol state from application state; this block is the application side of it.
//
// THE FIRST DATA BYTE IS THE POINTER. On a write, byte 0 of the data phase loads the
// pointer and bytes 1..n are data. That is note 1, and it is the only normative sentence
// this block implements.
//
// COMBINATIONAL READ, because Chapter 18.7 needs it. The transmit datapath pre-fetches on
// a falling edge and must have the byte in the same cycle -- so `rd_data` is a
// combinational function of the pointer, not a registered one. A register file that
// needed a cycle would force Chapter 18.10 to stretch the clock for every read byte.
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
// -----------------------------------------------------------------------------
module i2c_slave_regs #(
parameter integer N_REG = 8, // registers in the map
parameter integer RO_MASK = 0, // bit i set => register i is read-only
parameter integer CNT_W = 16
) (
input wire clk,
input wire rst_n,
// ---- the protocol engine's side -----------------------------------------
// A received byte. `rx_is_pointer` distinguishes the pointer byte from data, and it is
// the ONLY phase information this block takes. There is deliberately no
// `write_phase_started` input: a port this block does not need is a port that invites
// a future maintainer to clear the pointer on one, which would break the combined
// transfer -- see the note on decision 3 above.
input wire rx_valid,
input wire [7:0] rx_byte,
// Asserted with rx_valid when this is the FIRST data byte of the write phase.
input wire rx_is_pointer,
// Can this byte be accepted? Read combinationally so Chapter 18.5 can latch it as the
// acknowledge decision in the same cycle the byte completes.
output wire wr_accept,
// The transmit side. Combinational, for Chapter 18.7's pre-fetch.
output wire [7:0] rd_data,
// One cycle: a byte was taken by the transmitter, so advance.
input wire rd_taken,
// ---- the application's side ---------------------------------------------
// FLAT, not an unpacked array. An unpacked array port is SystemVerilog only, and this
// design has to exist in Verilog-2001 and VHDL with the same interface -- so the map
// is exposed as one packed vector and the consumer slices it. Register i is
// reg_flat[8*i +: 8].
output wire [8*N_REG-1:0] reg_flat,
output reg [7:0] pointer,
output reg [CNT_W-1:0] n_writes,
output reg [CNT_W-1:0] n_refused,
output reg [CNT_W-1:0] n_reads
);
reg [7:0] mem [0:N_REG-1];
integer i;
// The pointer is masked into range on every use, which is decision 2: the map WRAPS.
// Clamping is the other conforming choice and Chapter 16.1 §5 showed the two differ at
// exactly one address -- which is why the behaviour is stated here rather than left to
// whatever the arithmetic happens to do.
wire [7:0] ptr_in_range = pointer % N_REG;
// Read-only is a property of the ADDRESSED register, evaluated now. Decision 4: a
// write to a read-only location is NACKed, so the master learns it failed -- rather
// than accepted and discarded, which tells the master nothing.
wire is_ro = ((RO_MASK >> ptr_in_range) & 1) != 0;
assign wr_accept = ~is_ro;
assign rd_data = mem[ptr_in_range];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pointer <= 8'h00;
n_writes <= {CNT_W{1'b0}};
n_refused <= {CNT_W{1'b0}};
n_reads <= {CNT_W{1'b0}};
for (i = 0; i < N_REG; i = i + 1) mem[i] <= 8'h00;
end else begin
// NOTE what is absent: any reaction to framing. This block cannot see a START or
// a STOP, so it cannot clear the pointer on one -- which is how decision 3 is
// guaranteed structurally instead of by remembering not to break it.
if (rx_valid) begin
if (rx_is_pointer) begin
// Note 1: the internal location is written during the first data byte.
pointer <= rx_byte;
end else if (!is_ro) begin
mem[ptr_in_range] <= rx_byte;
// Decision 1: advance after every accepted data byte.
pointer <= pointer + 8'd1;
n_writes <= n_writes + 1'b1;
end else begin
// Decision 6: a refused write does NOT advance the pointer. Advancing
// would scatter a stubborn master's remaining bytes across the map --
// Chapter 16.1 §6a's argument, now enforced in hardware.
n_refused <= n_refused + 1'b1;
end
end
// ELSE IF, not a second `if`. A received byte and a served byte cannot occur in
// the same cycle, because the direction bit fixes which of the two is possible
// for the whole phase -- so this is not a priority the protocol can exercise.
// It is written as a priority anyway so the behaviour on an illegal input is
// DEFINED rather than a consequence of statement order: two `if`s assigning
// `pointer` in one block leave the later one silently winning, and a maintainer
// reordering them would change behaviour with no diagnostic.
else if (rd_taken) begin
// Decision 5: after a read the pointer sits one past the byte just served.
pointer <= pointer + 8'd1;
n_reads <= n_reads + 1'b1;
end
end
end
// Continuous assigns, not `always @(*)`: an `always @(*)` block that reads a memory
// does not reliably re-evaluate when an element changes, because a memory element is
// not in the implicit sensitivity list in every simulator. A generate of continuous
// assignments has no such ambiguity.
genvar gi;
generate
for (gi = 0; gi < N_REG; gi = gi + 1) begin : g_flat
assign reg_flat[8*gi +: 8] = mem[gi];
end
endgenerate
endmodule -- -----------------------------------------------------------------------------
-- i2c_slave_regs.vhd
-- The register file behind the protocol engine -- the same design in VHDL.
--
-- Two VHDL-specific notes, both of which are the language enforcing something the other
-- two languages leave to discipline:
--
-- 1. AN `out` PORT IS NOT READABLE in VHDL-93, so the pointer and the three counters
-- exist as internal signals and are copied to their ports. That is a nuisance here,
-- but it is the same rule that makes a VHDL design state plainly which signals are
-- internal state and which are merely observable.
--
-- 2. THE MAP IS A CONSTRAINED ARRAY whose index type is `natural`, so `mem(ptr)` with an
-- out-of-range `ptr` is a RUN-TIME ERROR rather than a silently wrapped access. The
-- modulo is therefore not an optimisation in this language -- it is what keeps the
-- design legal, and a mutant that removes it crashes instead of misbehaving.
-- -----------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_slave_regs is
generic (
N_REG : positive := 8; -- registers in the map
RO_MASK : natural := 0; -- bit i set => register i is read-only
CNT_W : positive := 16
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- the protocol engine's side
rx_valid : in std_logic;
rx_byte : in std_logic_vector(7 downto 0);
rx_is_pointer : in std_logic;
wr_accept : out std_logic;
rd_data : out std_logic_vector(7 downto 0);
rd_taken : in std_logic;
-- the application's side
reg_flat : out std_logic_vector(8*N_REG-1 downto 0);
pointer : out std_logic_vector(7 downto 0);
n_writes : out unsigned(CNT_W-1 downto 0);
n_refused : out unsigned(CNT_W-1 downto 0);
n_reads : out unsigned(CNT_W-1 downto 0)
);
end entity i2c_slave_regs;
architecture rtl of i2c_slave_regs is
type mem_t is array (0 to N_REG-1) of std_logic_vector(7 downto 0);
signal mem : mem_t := (others => (others => '0'));
signal ptr : unsigned(7 downto 0) := (others => '0');
signal nw : unsigned(CNT_W-1 downto 0) := (others => '0');
signal nr : unsigned(CNT_W-1 downto 0) := (others => '0');
signal nf : unsigned(CNT_W-1 downto 0) := (others => '0');
-- The pointer masked into range on every use: the map WRAPS, which is decision 2.
signal ptr_in_range : natural range 0 to N_REG-1;
signal is_ro : std_logic;
begin
ptr_in_range <= to_integer(ptr) mod N_REG;
is_ro <= '1' when ((RO_MASK / (2 ** ptr_in_range)) mod 2) = 1 else '0';
wr_accept <= not is_ro;
rd_data <= mem(ptr_in_range);
pointer <= std_logic_vector(ptr);
n_writes <= nw;
n_refused <= nf;
n_reads <= nr;
g_flat : for i in 0 to N_REG-1 generate
reg_flat(8*i+7 downto 8*i) <= mem(i);
end generate g_flat;
process (clk, rst_n)
begin
if rst_n = '0' then
ptr <= (others => '0');
nw <= (others => '0');
nf <= (others => '0');
nr <= (others => '0');
mem <= (others => (others => '0'));
elsif rising_edge(clk) then
-- NOTE what is absent: any reaction to framing. This block cannot see a START or
-- a STOP, so it cannot clear the pointer on one.
if rx_valid = '1' then
if rx_is_pointer = '1' then
ptr <= unsigned(rx_byte);
elsif is_ro = '0' then
mem(ptr_in_range) <= rx_byte;
ptr <= ptr + 1;
nw <= nw + 1;
else
-- A refused write does NOT advance the pointer.
nf <= nf + 1;
end if;
elsif rd_taken = '1' then
ptr <= ptr + 1;
nr <= nr + 1;
end if;
end if;
end process;
end architecture rtl;8. The Testbench
Fourteen tests across two instances.
| test | what it establishes |
|---|---|
| T1 | a reset map is zero, the pointer is zero, nothing is counted |
| T2 | the pointer byte loads the pointer, writes no register, and is not counted as a write |
| T3 | the next byte lands at the pointer, and the pointer advances |
| T4 | three bytes after one pointer fill three consecutive registers — and the read-only one in the middle refuses |
| T5 | rd_data follows the pointer with no extra cycle |
| T6 | a served byte advances the pointer, so the next read is the next register |
| T7 | a read-only register refuses, and the pointer does not advance |
| T8 | wr_accept tracks the pointer combinationally, as 18.5 requires |
| T9 | forty idle cycles change nothing: the pointer and map are application state |
| T10 | at N_REG = 5 the map wraps — the byte after the last lands at zero |
| T11 | the wrap is a modulo, not a truncation: pointer 6, 7 and 9 address registers 1, 2 and 4 |
| T12 | a simultaneous write and read — which the protocol cannot produce — is defined |
| T13 | a pointer past the end of the map still meets the read-only bit |
| T14 | reset clears the map, the pointer and every counter |
Two of those deserve their own note.
T12 tests an input the protocol cannot generate. A received byte and a served byte cannot occur in the same cycle, because the direction bit fixes which is possible for the whole phase. The design writes the two pointer updates as an explicit priority anyway:
if (rx_valid) ... // a write wins
else if (rd_taken) ... // a read only if there was no writeTwo separate if statements assigning the same register in one block leave the later one silently winning, and a maintainer reordering them changes behaviour with no diagnostic anywhere. Writing it as a priority makes the illegal case defined, and T12 pins the definition. Mutation M11 swaps the order and fails four checks — which it could not do if the bench refused to drive an illegal input.
T11 is the test the second instance exists for. At five registers, pointer 6 addresses register 1 and pointer 9 addresses register 4; a truncation to three bits would address 6 and 1, which is out of range in two of three cases and wrong in the third.
// -----------------------------------------------------------------------------
// i2c_slave_regs_tb.sv
// Independent oracle for i2c_slave_regs.
//
// TWO INSTANCES, and the second one is the point. The main DUT has N_REG = 8, which is a
// power of two -- and at a power of two a modulo wrap and a plain bit truncation are
// INDISTINGUISHABLE. A bench that only ever ran at N_REG = 8 could not tell the documented
// wrap from whatever the arithmetic happened to do, and a mutation replacing the modulo
// with a truncation would be a genuinely equivalent mutant rather than a killed one.
//
// So a second instance runs at N_REG = 5, where the two differ at every address from five
// upward. That is Module 17.2's M6 lesson applied before the fact rather than after it.
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module i2c_slave_regs_tb;
localparam int N8 = 8;
localparam int RO8 = 8'h04; // register 2 is read-only
localparam int N5 = 5;
logic clk = 1'b0;
logic rst_n = 1'b0;
// main instance
logic rx_valid = 1'b0;
logic [7:0] rx_byte = 8'h00;
logic rx_is_pointer = 1'b0;
logic rd_taken = 1'b0;
logic wr_accept;
logic [7:0] rd_data;
logic [8*N8-1:0] reg_flat;
logic [7:0] pointer;
logic [15:0] n_writes, n_refused, n_reads;
// wrap instance
logic w_rx_valid = 1'b0;
logic [7:0] w_rx_byte = 8'h00;
logic w_rx_is_pointer = 1'b0;
logic w_rd_taken = 1'b0;
logic w_wr_accept;
logic [7:0] w_rd_data;
logic [8*N5-1:0] w_reg_flat;
logic [7:0] w_pointer;
logic [15:0] w_n_writes, w_n_refused, w_n_reads;
int errors = 0;
int k, r, exp_acc;
i2c_slave_regs #(.N_REG(N8), .RO_MASK(RO8), .CNT_W(16)) dut (
.clk(clk), .rst_n(rst_n),
.rx_valid(rx_valid), .rx_byte(rx_byte), .rx_is_pointer(rx_is_pointer),
.wr_accept(wr_accept), .rd_data(rd_data), .rd_taken(rd_taken),
.reg_flat(reg_flat), .pointer(pointer),
.n_writes(n_writes), .n_refused(n_refused), .n_reads(n_reads)
);
i2c_slave_regs #(.N_REG(N5), .RO_MASK(0), .CNT_W(16)) wdut (
.clk(clk), .rst_n(rst_n),
.rx_valid(w_rx_valid), .rx_byte(w_rx_byte), .rx_is_pointer(w_rx_is_pointer),
.wr_accept(w_wr_accept), .rd_data(w_rd_data), .rd_taken(w_rd_taken),
.reg_flat(w_reg_flat), .pointer(w_pointer),
.n_writes(w_n_writes), .n_refused(w_n_refused), .n_reads(w_n_reads)
);
always #5 clk = ~clk;
initial begin
repeat (20000) @(posedge clk);
$display(" FAIL watchdog: the bench did not finish");
$fatal(1);
end
// ---------------------------------------------------------------- helpers
function automatic [7:0] slice8 (input [8*N8-1:0] f, input int idx);
slice8 = f[8*idx +: 8];
endfunction
function automatic [7:0] wslice8 (input [8*N5-1:0] f, input int idx);
wslice8 = f[8*idx +: 8];
endfunction
task automatic do_reset;
begin
@(negedge clk);
rst_n = 1'b0;
rx_valid = 1'b0; rx_is_pointer = 1'b0; rd_taken = 1'b0; rx_byte = 8'h00;
w_rx_valid = 1'b0; w_rx_is_pointer = 1'b0; w_rd_taken = 1'b0; w_rx_byte = 8'h00;
repeat (2) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
@(posedge clk); #1;
end
endtask
// One received byte. `is_ptr` marks the pointer byte of the write phase.
task automatic rx (input [7:0] b, input bit is_ptr);
begin
@(negedge clk);
rx_byte = b; rx_is_pointer = is_ptr; rx_valid = 1'b1;
@(posedge clk);
@(negedge clk);
rx_valid = 1'b0; rx_is_pointer = 1'b0;
#1;
end
endtask
task automatic wrx (input [7:0] b, input bit is_ptr);
begin
@(negedge clk);
w_rx_byte = b; w_rx_is_pointer = is_ptr; w_rx_valid = 1'b1;
@(posedge clk);
@(negedge clk);
w_rx_valid = 1'b0; w_rx_is_pointer = 1'b0;
#1;
end
endtask
// One byte served to the transmitter.
task automatic take;
begin
@(negedge clk); rd_taken = 1'b1;
@(posedge clk);
@(negedge clk); rd_taken = 1'b0;
#1;
end
endtask
task automatic wtake;
begin
@(negedge clk); w_rd_taken = 1'b1;
@(posedge clk);
@(negedge clk); w_rd_taken = 1'b0;
#1;
end
endtask
task automatic ck (input string what, input int got, input int exp);
begin
if (got !== exp) begin
$display(" FAIL %0s: got %0d expected %0d", what, got, exp);
errors++;
end
end
endtask
// A separate checker for the indexed sweeps: it carries the index as an argument rather
// than formatting it into the message, so the Verilog and VHDL benches can print the
// same line without a run-time string formatter, which neither language has.
task automatic ck_idx (input string what, input int idx, input int got, input int exp);
begin
if (got !== exp) begin
$display(" FAIL %0s[%0d]: got %0d expected %0d", what, idx, got, exp);
errors++;
end
end
endtask
// -------------------------------------------------------------------- tests
initial begin
$display("=== i2c_slave_regs: the register file, and the pointer that survives a restart ===");
// ---- T1. Reset.
do_reset();
$display("T1 a reset map is zero, the pointer is zero, and nothing has been counted");
ck("T1 pointer", pointer, 0);
for (k = 0; k < N8; k++) ck_idx("T1 reg", k, slice8(reg_flat, k), 0);
ck("T1 writes", n_writes, 0);
ck("T1 refused", n_refused, 0);
ck("T1 reads", n_reads, 0);
// ---- T2. The pointer byte is a pointer, not data.
do_reset();
rx(8'h03, 1'b1);
$display("T2 the first data byte of a write loads the pointer and writes nothing");
ck("T2 pointer loaded", pointer, 3);
for (k = 0; k < N8; k++) ck_idx("T2 reg untouched", k, slice8(reg_flat, k), 0);
ck("T2 not counted as a write", n_writes, 0);
// ---- T3. A data byte writes the addressed register and advances.
rx(8'hA5, 1'b0);
$display("T3 the next byte lands at the pointer, and the pointer advances");
ck("T3 reg 3 written", slice8(reg_flat, 3), 8'hA5);
ck("T3 pointer advanced", pointer, 4);
ck("T3 one write counted", n_writes, 1);
// ---- T4. Sequential write.
do_reset();
rx(8'h00, 1'b1);
rx(8'h11, 1'b0);
rx(8'h22, 1'b0);
rx(8'h33, 1'b0);
$display("T4 three bytes after one pointer land in three consecutive registers");
ck("T4 reg 0", slice8(reg_flat, 0), 8'h11);
ck("T4 reg 1", slice8(reg_flat, 1), 8'h22);
ck("T4 reg 2 is read-only, refused", slice8(reg_flat, 2), 8'h00);
ck("T4 pointer still at 2", pointer, 2);
ck("T4 two writes", n_writes, 2);
ck("T4 one refusal", n_refused, 1);
// ---- T5. The read is combinational on the pointer.
do_reset();
rx(8'h00, 1'b1); rx(8'h77, 1'b0); // reg 0 = 0x77, pointer now 1
rx(8'h00, 1'b1); // point back at 0
$display("T5 rd_data follows the pointer with no extra cycle: a combinational read");
ck("T5 rd_data is reg 0", rd_data, 8'h77);
ck("T5 and the pointer is 0", pointer, 0);
// ---- T6. A served byte advances the pointer.
do_reset();
rx(8'h00, 1'b1);
rx(8'hAA, 1'b0); rx(8'hBB, 1'b0); // reg0=AA, reg1=BB, pointer 2
rx(8'h00, 1'b1); // repointed to 0 -- no framing needed
ck("T6 first read byte", rd_data, 8'hAA);
take();
$display("T6 a served byte advances the pointer, so the next read is the next register");
ck("T6 pointer advanced", pointer, 1);
ck("T6 second read byte", rd_data, 8'hBB);
ck("T6 one read counted", n_reads, 1);
// ---- T7. A read-only register refuses, and does not advance.
do_reset();
rx(8'h02, 1'b1); // point at the read-only register
$display("T7 a read-only register refuses the byte and the pointer does NOT advance");
ck("T7 wr_accept is low", wr_accept, 0);
rx(8'hFF, 1'b0);
ck("T7 memory unchanged", slice8(reg_flat, 2), 8'h00);
ck("T7 pointer unchanged", pointer, 2);
ck("T7 refusal counted", n_refused, 1);
ck("T7 no write counted", n_writes, 0);
// ---- T8. wr_accept is combinational, so 18.5 can latch it in the same cycle.
do_reset();
rx(8'h01, 1'b1);
ck("T8 writable register accepts", wr_accept, 1);
rx(8'h02, 1'b1);
$display("T8 wr_accept tracks the pointer with no extra cycle, as 18.5 requires");
ck("T8 read-only register refuses", wr_accept, 0);
rx(8'h03, 1'b1);
ck("T8 and accepts again", wr_accept, 1);
// ---- T9. The pointer survives, because this block cannot see framing.
do_reset();
rx(8'h05, 1'b1);
rx(8'h5A, 1'b0);
repeat (40) @(posedge clk); #1;
$display("T9 forty idle cycles: the pointer and the map are application state");
ck("T9 pointer intact", pointer, 6);
ck("T9 reg 5 intact", slice8(reg_flat, 5), 8'h5A);
// ---- T10. The map wraps, on the non-power-of-two instance.
do_reset();
wrx(8'h04, 1'b1); // last register of a five-entry map
wrx(8'hE1, 1'b0); // writes reg 4, pointer -> 5
wrx(8'hE2, 1'b0); // 5 % 5 = 0, so this wraps to reg 0
$display("T10 at N_REG=5 the map wraps: the byte after the last one lands at zero");
ck("T10 reg 4", wslice8(w_reg_flat, 4), 8'hE1);
ck("T10 wrapped to reg 0", wslice8(w_reg_flat, 0), 8'hE2);
ck("T10 pointer is 6 raw", w_pointer, 6);
// ---- T11. The wrap is a modulo, not a truncation.
do_reset();
wrx(8'h00, 1'b1);
wrx(8'hD0, 1'b0); wrx(8'hD1, 1'b0); wrx(8'hD2, 1'b0);
wrx(8'hD3, 1'b0); wrx(8'hD4, 1'b0); // fill 0..4, pointer -> 5
wrx(8'h06, 1'b1); // 6 % 5 = 1
$display("T11 a modulo, not a truncation: pointer 6 addresses register 1 of five");
ck("T11 rd_data is reg 1", w_rd_data, 8'hD1);
wrx(8'h07, 1'b1); // 7 % 5 = 2
ck("T11 pointer 7 addresses reg 2", w_rd_data, 8'hD2);
wrx(8'h09, 1'b1); // 9 % 5 = 4
ck("T11 pointer 9 addresses reg 4", w_rd_data, 8'hD4);
// ---- T12. Priority on an input the protocol cannot produce.
do_reset();
rx(8'h00, 1'b1);
@(negedge clk);
rx_byte = 8'h9C; rx_is_pointer = 1'b0; rx_valid = 1'b1; rd_taken = 1'b1;
@(posedge clk);
@(negedge clk);
rx_valid = 1'b0; rd_taken = 1'b0;
#1;
$display("T12 an illegal simultaneous write and read is DEFINED: the write wins");
ck("T12 the byte was written", slice8(reg_flat, 0), 8'h9C);
ck("T12 the pointer advanced once", pointer, 1);
ck("T12 one write", n_writes, 1);
ck("T12 and no read", n_reads, 0);
// ---- T13. The read-only mask applies to the ALIAS, not just the direct address.
// Added after mutation M10 survived: evaluating read-only against the raw pointer
// rather than the wrapped one passed every other test in this list, because nothing
// ever pointed past the end of the map at a protected register.
do_reset();
rx(8'h0A, 1'b1); // 10 % 8 = 2, the read-only register
$display("T13 a pointer past the end of the map still meets the read-only bit");
ck("T13 the alias refuses", wr_accept, 0);
rx(8'h5C, 1'b0);
ck("T13 memory unchanged", slice8(reg_flat, 2), 8'h00);
ck("T13 pointer unchanged", pointer, 10);
ck("T13 refusal counted", n_refused, 1);
rx(8'h09, 1'b1); // 9 % 8 = 1, writable
ck("T13 a writable alias accepts", wr_accept, 1);
rx(8'h6D, 1'b0);
ck("T13 and the byte lands in register 1", slice8(reg_flat, 1), 8'h6D);
ck("T13 pointer advanced past it", pointer, 10);
// ---- T14. Reset clears the map and the counters.
rx(8'h00, 1'b1); rx(8'h44, 1'b0);
ck("T14 written before reset", slice8(reg_flat, 0), 8'h44);
@(negedge clk); rst_n = 1'b0; repeat (2) @(posedge clk); #1;
$display("T14 reset clears the map, the pointer and every counter");
for (k = 0; k < N8; k++) ck_idx("T14 reg cleared", k, slice8(reg_flat, k), 0);
ck("T14 pointer cleared", pointer, 0);
ck("T14 writes cleared", n_writes, 0);
ck("T14 reads cleared", n_reads, 0);
ck("T14 refusals cleared", n_refused, 0);
// ---- T15. EVERY REGISTER INDEX DECODES TO ITSELF, AND ONLY TO ITSELF.
// T7 and T13 prove the read-only bit is honoured at register 2 and at its alias
// 10. Both would still pass if the mask were indexed with a pointer that had lost
// its top bit, because register 2 is the only protected entry and index 2 survives
// any truncation that keeps the low two bits. The index bit that separates
// register 2 from register 6 is never exercised by them, so an alias between those
// two registers -- register 6 silently becoming read-only -- is invisible to this
// list. Registers 6 and 7 are in fact never written by any test above.
//
// The property is per-index, and it has two halves. For every register in the map,
// wr_accept must follow THAT register's own mask bit; and an accepted byte must
// land in THAT register and no other. Checking all eight against their own mask
// bits is what makes every bit of the index load-bearing.
do_reset();
for (r = 0; r < N8; r++) begin
rx(r[7:0], 1'b1);
exp_acc = (((RO8 >> r) & 1) != 0) ? 0 : 1;
ck_idx("T15 wr_accept follows this index's own mask bit", r, wr_accept, exp_acc);
end
do_reset();
for (r = 0; r < N8; r++) begin
if (((RO8 >> r) & 1) == 0) begin
rx(r[7:0], 1'b1);
rx(8'hB0 + r[7:0], 1'b0);
ck_idx("T15 the byte landed in this register", r,
slice8(reg_flat, r), 8'hB0 + r[7:0]);
end
end
$display("T15 all eight register indices decode distinctly; only register 2 refused");
ck("T15 seven registers accepted a byte", n_writes, N8 - 1);
ck("T15 and nothing else was refused", n_refused, 0);
ck("T15 the protected register is still clear", slice8(reg_flat, 2), 8'h00);
// The power-of-two wrap boundary. The byte that filled register 7 left the pointer
// at 8, so the next data byte must land at register 0. T10 proves the wrap at
// N_REG = 5, where a truncating implementation gets it wrong; 8 % 8 is the case a
// truncating implementation gets right by accident, which is why it is worth
// stating that the documented behaviour holds here too.
rx(8'hC0, 1'b0);
ck("T15 pointer 8 wrapped to register 0", slice8(reg_flat, 0), 8'hC0);
ck("T15 and that write was counted", n_writes, N8);
if (errors == 0) $display("=== i2c_slave_regs: ALL CHECKS PASSED ===");
else $display("=== i2c_slave_regs: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule // -----------------------------------------------------------------------------
// i2c_slave_regs_tb.v
// Independent oracle for i2c_slave_regs.
//
// TWO INSTANCES, and the second one is the point. The main DUT has N_REG = 8, which is a
// power of two -- and at a power of two a modulo wrap and a plain bit truncation are
// INDISTINGUISHABLE. A bench that only ever ran at N_REG = 8 could not tell the documented
// wrap from whatever the arithmetic happened to do, and a mutation replacing the modulo
// with a truncation would be a genuinely equivalent mutant rather than a killed one.
//
// So a second instance runs at N_REG = 5, where the two differ at every address from five
// upward. That is Module 17.2's M6 lesson applied before the fact rather than after it.
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module i2c_slave_regs_tb;
localparam integer N8 = 8;
localparam integer RO8 = 8'h04; // register 2 is read-only
localparam integer N5 = 5;
reg clk = 1'b0;
reg rst_n = 1'b0;
// main instance
reg rx_valid = 1'b0;
reg [7:0] rx_byte = 8'h00;
reg rx_is_pointer = 1'b0;
reg rd_taken = 1'b0;
wire wr_accept;
wire [7:0] rd_data;
wire [8*N8-1:0] reg_flat;
wire [7:0] pointer;
wire [15:0] n_writes, n_refused, n_reads;
// wrap instance
reg w_rx_valid = 1'b0;
reg [7:0] w_rx_byte = 8'h00;
reg w_rx_is_pointer = 1'b0;
reg w_rd_taken = 1'b0;
wire w_wr_accept;
wire [7:0] w_rd_data;
wire [8*N5-1:0] w_reg_flat;
wire [7:0] w_pointer;
wire [15:0] w_n_writes, w_n_refused, w_n_reads;
integer errors = 0;
integer k, r, exp_acc;
i2c_slave_regs #(.N_REG(N8), .RO_MASK(RO8), .CNT_W(16)) dut (
.clk(clk), .rst_n(rst_n),
.rx_valid(rx_valid), .rx_byte(rx_byte), .rx_is_pointer(rx_is_pointer),
.wr_accept(wr_accept), .rd_data(rd_data), .rd_taken(rd_taken),
.reg_flat(reg_flat), .pointer(pointer),
.n_writes(n_writes), .n_refused(n_refused), .n_reads(n_reads)
);
i2c_slave_regs #(.N_REG(N5), .RO_MASK(0), .CNT_W(16)) wdut (
.clk(clk), .rst_n(rst_n),
.rx_valid(w_rx_valid), .rx_byte(w_rx_byte), .rx_is_pointer(w_rx_is_pointer),
.wr_accept(w_wr_accept), .rd_data(w_rd_data), .rd_taken(w_rd_taken),
.reg_flat(w_reg_flat), .pointer(w_pointer),
.n_writes(w_n_writes), .n_refused(w_n_refused), .n_reads(w_n_reads)
);
always #5 clk = ~clk;
initial begin
repeat (20000) @(posedge clk);
$display(" FAIL watchdog: the bench did not finish");
$fatal(1);
end
// ---------------------------------------------------------------- helpers
function [7:0] slice8 (input [8*N8-1:0] f, input integer idx);
slice8 = f[8*idx +: 8];
endfunction
function [7:0] wslice8 (input [8*N5-1:0] f, input integer idx);
wslice8 = f[8*idx +: 8];
endfunction
task do_reset;
begin
@(negedge clk);
rst_n = 1'b0;
rx_valid = 1'b0; rx_is_pointer = 1'b0; rd_taken = 1'b0; rx_byte = 8'h00;
w_rx_valid = 1'b0; w_rx_is_pointer = 1'b0; w_rd_taken = 1'b0; w_rx_byte = 8'h00;
repeat (2) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
@(posedge clk); #1;
end
endtask
// One received byte. `is_ptr` marks the pointer byte of the write phase.
task rx (input [7:0] b, input is_ptr);
begin
@(negedge clk);
rx_byte = b; rx_is_pointer = is_ptr; rx_valid = 1'b1;
@(posedge clk);
@(negedge clk);
rx_valid = 1'b0; rx_is_pointer = 1'b0;
#1;
end
endtask
task wrx (input [7:0] b, input is_ptr);
begin
@(negedge clk);
w_rx_byte = b; w_rx_is_pointer = is_ptr; w_rx_valid = 1'b1;
@(posedge clk);
@(negedge clk);
w_rx_valid = 1'b0; w_rx_is_pointer = 1'b0;
#1;
end
endtask
// One byte served to the transmitter.
task take;
begin
@(negedge clk); rd_taken = 1'b1;
@(posedge clk);
@(negedge clk); rd_taken = 1'b0;
#1;
end
endtask
task wtake;
begin
@(negedge clk); w_rd_taken = 1'b1;
@(posedge clk);
@(negedge clk); w_rd_taken = 1'b0;
#1;
end
endtask
task ck (input [200*8:1] what, input integer got, input integer exp);
begin
if (got !== exp) begin
$display(" FAIL %0s: got %0d expected %0d", what, got, exp);
errors = errors + 1;
end
end
endtask
// A separate checker for the indexed sweeps: it carries the index as an argument rather
// than formatting it into the message, so the Verilog and VHDL benches can print the
// same line without a run-time string formatter, which neither language has.
task ck_idx (input [200*8:1] what, input integer idx, input integer got, input integer exp);
begin
if (got !== exp) begin
$display(" FAIL %0s[%0d]: got %0d expected %0d", what, idx, got, exp);
errors = errors + 1;
end
end
endtask
// -------------------------------------------------------------------- tests
initial begin
$display("=== i2c_slave_regs: the register file, and the pointer that survives a restart ===");
// ---- T1. Reset.
do_reset();
$display("T1 a reset map is zero, the pointer is zero, and nothing has been counted");
ck("T1 pointer", pointer, 0);
for (k = 0; k < N8; k = k + 1) ck_idx("T1 reg", k, slice8(reg_flat, k), 0);
ck("T1 writes", n_writes, 0);
ck("T1 refused", n_refused, 0);
ck("T1 reads", n_reads, 0);
// ---- T2. The pointer byte is a pointer, not data.
do_reset();
rx(8'h03, 1'b1);
$display("T2 the first data byte of a write loads the pointer and writes nothing");
ck("T2 pointer loaded", pointer, 3);
for (k = 0; k < N8; k = k + 1) ck_idx("T2 reg untouched", k, slice8(reg_flat, k), 0);
ck("T2 not counted as a write", n_writes, 0);
// ---- T3. A data byte writes the addressed register and advances.
rx(8'hA5, 1'b0);
$display("T3 the next byte lands at the pointer, and the pointer advances");
ck("T3 reg 3 written", slice8(reg_flat, 3), 8'hA5);
ck("T3 pointer advanced", pointer, 4);
ck("T3 one write counted", n_writes, 1);
// ---- T4. Sequential write.
do_reset();
rx(8'h00, 1'b1);
rx(8'h11, 1'b0);
rx(8'h22, 1'b0);
rx(8'h33, 1'b0);
$display("T4 three bytes after one pointer land in three consecutive registers");
ck("T4 reg 0", slice8(reg_flat, 0), 8'h11);
ck("T4 reg 1", slice8(reg_flat, 1), 8'h22);
ck("T4 reg 2 is read-only, refused", slice8(reg_flat, 2), 8'h00);
ck("T4 pointer still at 2", pointer, 2);
ck("T4 two writes", n_writes, 2);
ck("T4 one refusal", n_refused, 1);
// ---- T5. The read is combinational on the pointer.
do_reset();
rx(8'h00, 1'b1); rx(8'h77, 1'b0); // reg 0 = 0x77, pointer now 1
rx(8'h00, 1'b1); // point back at 0
$display("T5 rd_data follows the pointer with no extra cycle: a combinational read");
ck("T5 rd_data is reg 0", rd_data, 8'h77);
ck("T5 and the pointer is 0", pointer, 0);
// ---- T6. A served byte advances the pointer.
do_reset();
rx(8'h00, 1'b1);
rx(8'hAA, 1'b0); rx(8'hBB, 1'b0); // reg0=AA, reg1=BB, pointer 2
rx(8'h00, 1'b1); // repointed to 0 -- no framing needed
ck("T6 first read byte", rd_data, 8'hAA);
take();
$display("T6 a served byte advances the pointer, so the next read is the next register");
ck("T6 pointer advanced", pointer, 1);
ck("T6 second read byte", rd_data, 8'hBB);
ck("T6 one read counted", n_reads, 1);
// ---- T7. A read-only register refuses, and does not advance.
do_reset();
rx(8'h02, 1'b1); // point at the read-only register
$display("T7 a read-only register refuses the byte and the pointer does NOT advance");
ck("T7 wr_accept is low", wr_accept, 0);
rx(8'hFF, 1'b0);
ck("T7 memory unchanged", slice8(reg_flat, 2), 8'h00);
ck("T7 pointer unchanged", pointer, 2);
ck("T7 refusal counted", n_refused, 1);
ck("T7 no write counted", n_writes, 0);
// ---- T8. wr_accept is combinational, so 18.5 can latch it in the same cycle.
do_reset();
rx(8'h01, 1'b1);
ck("T8 writable register accepts", wr_accept, 1);
rx(8'h02, 1'b1);
$display("T8 wr_accept tracks the pointer with no extra cycle, as 18.5 requires");
ck("T8 read-only register refuses", wr_accept, 0);
rx(8'h03, 1'b1);
ck("T8 and accepts again", wr_accept, 1);
// ---- T9. The pointer survives, because this block cannot see framing.
do_reset();
rx(8'h05, 1'b1);
rx(8'h5A, 1'b0);
repeat (40) @(posedge clk); #1;
$display("T9 forty idle cycles: the pointer and the map are application state");
ck("T9 pointer intact", pointer, 6);
ck("T9 reg 5 intact", slice8(reg_flat, 5), 8'h5A);
// ---- T10. The map wraps, on the non-power-of-two instance.
do_reset();
wrx(8'h04, 1'b1); // last register of a five-entry map
wrx(8'hE1, 1'b0); // writes reg 4, pointer -> 5
wrx(8'hE2, 1'b0); // 5 % 5 = 0, so this wraps to reg 0
$display("T10 at N_REG=5 the map wraps: the byte after the last one lands at zero");
ck("T10 reg 4", wslice8(w_reg_flat, 4), 8'hE1);
ck("T10 wrapped to reg 0", wslice8(w_reg_flat, 0), 8'hE2);
ck("T10 pointer is 6 raw", w_pointer, 6);
// ---- T11. The wrap is a modulo, not a truncation.
do_reset();
wrx(8'h00, 1'b1);
wrx(8'hD0, 1'b0); wrx(8'hD1, 1'b0); wrx(8'hD2, 1'b0);
wrx(8'hD3, 1'b0); wrx(8'hD4, 1'b0); // fill 0..4, pointer -> 5
wrx(8'h06, 1'b1); // 6 % 5 = 1
$display("T11 a modulo, not a truncation: pointer 6 addresses register 1 of five");
ck("T11 rd_data is reg 1", w_rd_data, 8'hD1);
wrx(8'h07, 1'b1); // 7 % 5 = 2
ck("T11 pointer 7 addresses reg 2", w_rd_data, 8'hD2);
wrx(8'h09, 1'b1); // 9 % 5 = 4
ck("T11 pointer 9 addresses reg 4", w_rd_data, 8'hD4);
// ---- T12. Priority on an input the protocol cannot produce.
do_reset();
rx(8'h00, 1'b1);
@(negedge clk);
rx_byte = 8'h9C; rx_is_pointer = 1'b0; rx_valid = 1'b1; rd_taken = 1'b1;
@(posedge clk);
@(negedge clk);
rx_valid = 1'b0; rd_taken = 1'b0;
#1;
$display("T12 an illegal simultaneous write and read is DEFINED: the write wins");
ck("T12 the byte was written", slice8(reg_flat, 0), 8'h9C);
ck("T12 the pointer advanced once", pointer, 1);
ck("T12 one write", n_writes, 1);
ck("T12 and no read", n_reads, 0);
// ---- T13. The read-only mask applies to the ALIAS, not just the direct address.
// Added after mutation M10 survived: evaluating read-only against the raw pointer
// rather than the wrapped one passed every other test in this list, because nothing
// ever pointed past the end of the map at a protected register.
do_reset();
rx(8'h0A, 1'b1); // 10 % 8 = 2, the read-only register
$display("T13 a pointer past the end of the map still meets the read-only bit");
ck("T13 the alias refuses", wr_accept, 0);
rx(8'h5C, 1'b0);
ck("T13 memory unchanged", slice8(reg_flat, 2), 8'h00);
ck("T13 pointer unchanged", pointer, 10);
ck("T13 refusal counted", n_refused, 1);
rx(8'h09, 1'b1); // 9 % 8 = 1, writable
ck("T13 a writable alias accepts", wr_accept, 1);
rx(8'h6D, 1'b0);
ck("T13 and the byte lands in register 1", slice8(reg_flat, 1), 8'h6D);
ck("T13 pointer advanced past it", pointer, 10);
// ---- T14. Reset clears the map and the counters.
rx(8'h00, 1'b1); rx(8'h44, 1'b0);
ck("T14 written before reset", slice8(reg_flat, 0), 8'h44);
@(negedge clk); rst_n = 1'b0; repeat (2) @(posedge clk); #1;
$display("T14 reset clears the map, the pointer and every counter");
for (k = 0; k < N8; k = k + 1) ck_idx("T14 reg cleared", k, slice8(reg_flat, k), 0);
ck("T14 pointer cleared", pointer, 0);
ck("T14 writes cleared", n_writes, 0);
ck("T14 reads cleared", n_reads, 0);
ck("T14 refusals cleared", n_refused, 0);
// ---- T15. EVERY REGISTER INDEX DECODES TO ITSELF, AND ONLY TO ITSELF.
// T7 and T13 prove the read-only bit is honoured at register 2 and at its alias
// 10. Both would still pass if the mask were indexed with a pointer that had lost
// its top bit, because register 2 is the only protected entry and index 2 survives
// any truncation that keeps the low two bits. The index bit that separates
// register 2 from register 6 is never exercised by them, so an alias between those
// two registers -- register 6 silently becoming read-only -- is invisible to this
// list. Registers 6 and 7 are in fact never written by any test above.
//
// The property is per-index, and it has two halves. For every register in the map,
// wr_accept must follow THAT register's own mask bit; and an accepted byte must
// land in THAT register and no other. Checking all eight against their own mask
// bits is what makes every bit of the index load-bearing.
do_reset;
for (r = 0; r < N8; r = r + 1) begin
rx(r[7:0], 1'b1);
exp_acc = (((RO8 >> r) & 1) != 0) ? 0 : 1;
ck_idx("T15 wr_accept follows this index's own mask bit", r, wr_accept, exp_acc);
end
do_reset;
for (r = 0; r < N8; r = r + 1) begin
if (((RO8 >> r) & 1) == 0) begin
rx(r[7:0], 1'b1);
rx(8'hB0 + r[7:0], 1'b0);
ck_idx("T15 the byte landed in this register", r,
slice8(reg_flat, r), 8'hB0 + r[7:0]);
end
end
$display("T15 all eight register indices decode distinctly; only register 2 refused");
ck("T15 seven registers accepted a byte", n_writes, N8 - 1);
ck("T15 and nothing else was refused", n_refused, 0);
ck("T15 the protected register is still clear", slice8(reg_flat, 2), 8'h00);
// The power-of-two wrap boundary. The byte that filled register 7 left the pointer
// at 8, so the next data byte must land at register 0. T10 proves the wrap at
// N_REG = 5, where a truncating implementation gets it wrong; 8 % 8 is the case a
// truncating implementation gets right by accident, which is why it is worth
// stating that the documented behaviour holds here too.
rx(8'hC0, 1'b0);
ck("T15 pointer 8 wrapped to register 0", slice8(reg_flat, 0), 8'hC0);
ck("T15 and that write was counted", n_writes, N8);
if (errors == 0) $display("=== i2c_slave_regs: ALL CHECKS PASSED ===");
else $display("=== i2c_slave_regs: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule -- -----------------------------------------------------------------------------
-- i2c_slave_regs_tb.vhd
-- Independent oracle for i2c_slave_regs. Behavioural twin of the SystemVerilog and
-- Verilog benches: two instances, thirteen tests, the same finish time.
--
-- The second instance runs at N_REG = 5, because at the main instance's N_REG = 8 a modulo
-- wrap and a bit truncation are indistinguishable -- so a bench that only ever ran at a
-- power of two could not tell the documented behaviour from an accident of arithmetic.
-- -----------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_slave_regs_tb is
end entity i2c_slave_regs_tb;
architecture sim of i2c_slave_regs_tb is
constant N8 : positive := 8;
constant RO8 : natural := 4; -- register 2 is read-only
constant N5 : positive := 5;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
-- main instance
signal rx_valid : std_logic := '0';
signal rx_byte : std_logic_vector(7 downto 0) := (others => '0');
signal rx_is_pointer : std_logic := '0';
signal rd_taken : std_logic := '0';
signal wr_accept : std_logic;
signal rd_data : std_logic_vector(7 downto 0);
signal reg_flat : std_logic_vector(8*N8-1 downto 0);
signal pointer : std_logic_vector(7 downto 0);
signal n_writes, n_refused, n_reads : unsigned(15 downto 0);
-- wrap instance
signal w_rx_valid : std_logic := '0';
signal w_rx_byte : std_logic_vector(7 downto 0) := (others => '0');
signal w_rx_is_pointer : std_logic := '0';
signal w_rd_taken : std_logic := '0';
signal w_wr_accept : std_logic;
signal w_rd_data : std_logic_vector(7 downto 0);
signal w_reg_flat : std_logic_vector(8*N5-1 downto 0);
signal w_pointer : std_logic_vector(7 downto 0);
signal w_n_writes, w_n_refused, w_n_reads : unsigned(15 downto 0);
signal halt : boolean := false;
begin
dut : entity work.i2c_slave_regs
generic map (N_REG => N8, RO_MASK => RO8, CNT_W => 16)
port map (clk => clk, rst_n => rst_n,
rx_valid => rx_valid, rx_byte => rx_byte, rx_is_pointer => rx_is_pointer,
wr_accept => wr_accept, rd_data => rd_data, rd_taken => rd_taken,
reg_flat => reg_flat, pointer => pointer,
n_writes => n_writes, n_refused => n_refused, n_reads => n_reads);
wdut : entity work.i2c_slave_regs
generic map (N_REG => N5, RO_MASK => 0, CNT_W => 16)
port map (clk => clk, rst_n => rst_n,
rx_valid => w_rx_valid, rx_byte => w_rx_byte, rx_is_pointer => w_rx_is_pointer,
wr_accept => w_wr_accept, rd_data => w_rd_data, rd_taken => w_rd_taken,
reg_flat => w_reg_flat, pointer => w_pointer,
n_writes => w_n_writes, n_refused => w_n_refused, n_reads => w_n_reads);
clkgen : process
begin
while not halt loop
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
stim : process
variable err : integer := 0;
variable exp_acc : integer;
function slice8 (f : std_logic_vector; idx : integer) return integer is
begin
return to_integer(unsigned(f(8*idx+7 downto 8*idx)));
end function;
function b2i (b : std_logic) return integer is
begin
if b = '1' then return 1; else return 0; end if;
end function;
procedure ck (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_idx (what : string; idx : integer; g : integer; e : integer) is
begin
if g /= e then
report " FAIL " & what & "[" & integer'image(idx) & "]: got "
& integer'image(g) & " expected " & integer'image(e) severity note;
err := err + 1;
end if;
end procedure;
procedure do_reset is
begin
wait until falling_edge(clk);
rst_n <= '0';
rx_valid <= '0'; rx_is_pointer <= '0'; rd_taken <= '0'; rx_byte <= (others => '0');
w_rx_valid <= '0'; w_rx_is_pointer <= '0'; w_rd_taken <= '0';
w_rx_byte <= (others => '0');
wait until rising_edge(clk); wait until rising_edge(clk);
wait until falling_edge(clk); rst_n <= '1';
wait until rising_edge(clk); wait for 1 ns;
end procedure;
procedure rx (b : integer; is_ptr : std_logic) is
begin
wait until falling_edge(clk);
rx_byte <= std_logic_vector(to_unsigned(b, 8));
rx_is_pointer <= is_ptr; rx_valid <= '1';
wait until rising_edge(clk);
wait until falling_edge(clk);
rx_valid <= '0'; rx_is_pointer <= '0';
wait for 1 ns;
end procedure;
procedure wrx (b : integer; is_ptr : std_logic) is
begin
wait until falling_edge(clk);
w_rx_byte <= std_logic_vector(to_unsigned(b, 8));
w_rx_is_pointer <= is_ptr; w_rx_valid <= '1';
wait until rising_edge(clk);
wait until falling_edge(clk);
w_rx_valid <= '0'; w_rx_is_pointer <= '0';
wait for 1 ns;
end procedure;
procedure take is
begin
wait until falling_edge(clk); rd_taken <= '1';
wait until rising_edge(clk);
wait until falling_edge(clk); rd_taken <= '0';
wait for 1 ns;
end procedure;
begin
report "=== i2c_slave_regs: the register file, and the pointer that survives a restart ==="
severity note;
-- T1.
do_reset;
report "T1 a reset map is zero, the pointer is zero, and nothing has been counted"
severity note;
ck("T1 pointer", to_integer(unsigned(pointer)), 0);
for k in 0 to N8-1 loop
ck_idx("T1 reg", k, slice8(reg_flat, k), 0);
end loop;
ck("T1 writes", to_integer(n_writes), 0);
ck("T1 refused", to_integer(n_refused), 0);
ck("T1 reads", to_integer(n_reads), 0);
-- T2.
do_reset;
rx(3, '1');
report "T2 the first data byte of a write loads the pointer and writes nothing"
severity note;
ck("T2 pointer loaded", to_integer(unsigned(pointer)), 3);
for k in 0 to N8-1 loop
ck_idx("T2 reg untouched", k, slice8(reg_flat, k), 0);
end loop;
ck("T2 not counted as a write", to_integer(n_writes), 0);
-- T3.
rx(16#A5#, '0');
report "T3 the next byte lands at the pointer, and the pointer advances" severity note;
ck("T3 reg 3 written", slice8(reg_flat, 3), 16#A5#);
ck("T3 pointer advanced", to_integer(unsigned(pointer)), 4);
ck("T3 one write counted", to_integer(n_writes), 1);
-- T4.
do_reset;
rx(0, '1'); rx(16#11#, '0'); rx(16#22#, '0'); rx(16#33#, '0');
report "T4 three bytes after one pointer land in three consecutive registers"
severity note;
ck("T4 reg 0", slice8(reg_flat, 0), 16#11#);
ck("T4 reg 1", slice8(reg_flat, 1), 16#22#);
ck("T4 reg 2 is read-only, refused", slice8(reg_flat, 2), 0);
ck("T4 pointer still at 2", to_integer(unsigned(pointer)), 2);
ck("T4 two writes", to_integer(n_writes), 2);
ck("T4 one refusal", to_integer(n_refused), 1);
-- T5.
do_reset;
rx(0, '1'); rx(16#77#, '0');
rx(0, '1');
report "T5 rd_data follows the pointer with no extra cycle: a combinational read"
severity note;
ck("T5 rd_data is reg 0", to_integer(unsigned(rd_data)), 16#77#);
ck("T5 and the pointer is 0", to_integer(unsigned(pointer)), 0);
-- T6.
do_reset;
rx(0, '1'); rx(16#AA#, '0'); rx(16#BB#, '0');
rx(0, '1');
ck("T6 first read byte", to_integer(unsigned(rd_data)), 16#AA#);
take;
report "T6 a served byte advances the pointer, so the next read is the next register"
severity note;
ck("T6 pointer advanced", to_integer(unsigned(pointer)), 1);
ck("T6 second read byte", to_integer(unsigned(rd_data)), 16#BB#);
ck("T6 one read counted", to_integer(n_reads), 1);
-- T7.
do_reset;
rx(2, '1');
report "T7 a read-only register refuses the byte and the pointer does NOT advance"
severity note;
ck("T7 wr_accept is low", b2i(wr_accept), 0);
rx(16#FF#, '0');
ck("T7 memory unchanged", slice8(reg_flat, 2), 0);
ck("T7 pointer unchanged", to_integer(unsigned(pointer)), 2);
ck("T7 refusal counted", to_integer(n_refused), 1);
ck("T7 no write counted", to_integer(n_writes), 0);
-- T8.
do_reset;
rx(1, '1');
ck("T8 writable register accepts", b2i(wr_accept), 1);
rx(2, '1');
report "T8 wr_accept tracks the pointer with no extra cycle, as 18.5 requires"
severity note;
ck("T8 read-only register refuses", b2i(wr_accept), 0);
rx(3, '1');
ck("T8 and accepts again", b2i(wr_accept), 1);
-- T9.
do_reset;
rx(5, '1'); rx(16#5A#, '0');
for k in 1 to 40 loop wait until rising_edge(clk); end loop;
wait for 1 ns;
report "T9 forty idle cycles: the pointer and the map are application state"
severity note;
ck("T9 pointer intact", to_integer(unsigned(pointer)), 6);
ck("T9 reg 5 intact", slice8(reg_flat, 5), 16#5A#);
-- T10.
do_reset;
wrx(4, '1'); wrx(16#E1#, '0'); wrx(16#E2#, '0');
report "T10 at N_REG=5 the map wraps: the byte after the last one lands at zero"
severity note;
ck("T10 reg 4", slice8(w_reg_flat, 4), 16#E1#);
ck("T10 wrapped to reg 0", slice8(w_reg_flat, 0), 16#E2#);
ck("T10 pointer is 6 raw", to_integer(unsigned(w_pointer)), 6);
-- T11.
do_reset;
wrx(0, '1');
wrx(16#D0#, '0'); wrx(16#D1#, '0'); wrx(16#D2#, '0');
wrx(16#D3#, '0'); wrx(16#D4#, '0');
wrx(6, '1');
report "T11 a modulo, not a truncation: pointer 6 addresses register 1 of five"
severity note;
ck("T11 rd_data is reg 1", to_integer(unsigned(w_rd_data)), 16#D1#);
wrx(7, '1');
ck("T11 pointer 7 addresses reg 2", to_integer(unsigned(w_rd_data)), 16#D2#);
wrx(9, '1');
ck("T11 pointer 9 addresses reg 4", to_integer(unsigned(w_rd_data)), 16#D4#);
-- T12.
do_reset;
rx(0, '1');
wait until falling_edge(clk);
rx_byte <= std_logic_vector(to_unsigned(16#9C#, 8));
rx_is_pointer <= '0'; rx_valid <= '1'; rd_taken <= '1';
wait until rising_edge(clk);
wait until falling_edge(clk);
rx_valid <= '0'; rd_taken <= '0';
wait for 1 ns;
report "T12 an illegal simultaneous write and read is DEFINED: the write wins"
severity note;
ck("T12 the byte was written", slice8(reg_flat, 0), 16#9C#);
ck("T12 the pointer advanced once", to_integer(unsigned(pointer)), 1);
ck("T12 one write", to_integer(n_writes), 1);
ck("T12 and no read", to_integer(n_reads), 0);
-- T13. The read-only mask applies to the ALIAS, not just the direct address.
-- Added after mutation M10 survived: evaluating read-only against the raw pointer
-- rather than the wrapped one passed every other test in this list.
do_reset;
rx(16#0A#, '1');
report "T13 a pointer past the end of the map still meets the read-only bit"
severity note;
ck("T13 the alias refuses", b2i(wr_accept), 0);
rx(16#5C#, '0');
ck("T13 memory unchanged", slice8(reg_flat, 2), 0);
ck("T13 pointer unchanged", to_integer(unsigned(pointer)), 10);
ck("T13 refusal counted", to_integer(n_refused), 1);
rx(16#09#, '1');
ck("T13 a writable alias accepts", b2i(wr_accept), 1);
rx(16#6D#, '0');
ck("T13 and the byte lands in register 1", slice8(reg_flat, 1), 16#6D#);
ck("T13 pointer advanced past it", to_integer(unsigned(pointer)), 10);
-- T14.
rx(0, '1'); rx(16#44#, '0');
ck("T14 written before reset", slice8(reg_flat, 0), 16#44#);
wait until falling_edge(clk); rst_n <= '0';
wait until rising_edge(clk); wait until rising_edge(clk); wait for 1 ns;
report "T14 reset clears the map, the pointer and every counter" severity note;
for k in 0 to N8-1 loop
ck_idx("T14 reg cleared", k, slice8(reg_flat, k), 0);
end loop;
ck("T14 pointer cleared", to_integer(unsigned(pointer)), 0);
ck("T14 writes cleared", to_integer(n_writes), 0);
ck("T14 reads cleared", to_integer(n_reads), 0);
ck("T14 refusals cleared", to_integer(n_refused), 0);
-- T15. Every register index decodes to itself, and only to itself.
-- T7 and T13 prove the read-only bit is honoured at register 2 and at its alias
-- 10. Both would still pass if the mask were indexed with a pointer that had lost
-- its top bit, because register 2 is the only protected entry and index 2 survives
-- any truncation that keeps the low two bits. The index bit that separates
-- register 2 from register 6 is never exercised by them, so an alias between those
-- two registers -- register 6 silently becoming read-only -- is invisible to this
-- list. Registers 6 and 7 are in fact never written by any test above.
--
-- The property is per-index, and it has two halves. For every register in the map,
-- wr_accept must follow THAT register's own mask bit; and an accepted byte must
-- land in THAT register and no other. Checking all eight against their own mask
-- bits is what makes every bit of the index load-bearing.
do_reset;
for r in 0 to N8-1 loop
rx(r, '1');
if ((RO8 / 2**r) mod 2) = 1 then exp_acc := 0; else exp_acc := 1; end if;
ck_idx("T15 wr_accept follows this index's own mask bit", r,
b2i(wr_accept), exp_acc);
end loop;
do_reset;
for r in 0 to N8-1 loop
if ((RO8 / 2**r) mod 2) = 0 then
rx(r, '1');
rx(16#B0# + r, '0');
ck_idx("T15 the byte landed in this register", r,
slice8(reg_flat, r), 16#B0# + r);
end if;
end loop;
report "T15 all eight register indices decode distinctly; only register 2 refused"
severity note;
ck("T15 seven registers accepted a byte", to_integer(n_writes), N8 - 1);
ck("T15 and nothing else was refused", to_integer(n_refused), 0);
ck("T15 the protected register is still clear", slice8(reg_flat, 2), 0);
-- The power-of-two wrap boundary. The byte that filled register 7 left the pointer
-- at 8, so the next data byte must land at register 0. T10 proves the wrap at
-- N_REG = 5, where a truncating implementation gets it wrong; 8 mod 8 is the case a
-- truncating implementation gets right by accident, which is why it is worth
-- stating that the documented behaviour holds here too.
rx(16#C0#, '0');
ck("T15 pointer 8 wrapped to register 0", slice8(reg_flat, 0), 16#C0#);
ck("T15 and that write was counted", to_integer(n_writes), N8);
if err = 0 then
report "=== i2c_slave_regs: ALL CHECKS PASSED ===" severity note;
else
report "=== i2c_slave_regs: " & integer'image(err) & " CHECK(S) FAILED ==="
severity note;
end if;
halt <= true;
wait;
end process;
end architecture sim;All three languages finish at the same instant:
i2c_slave_regs_tb.sv ALL CHECKS PASSED $finish at 1596000
i2c_slave_regs_tb.v ALL CHECKS PASSED $finish at 1596000
i2c_slave_regs_tb.vhd ALL CHECKS PASSED stopped at 1596 ns9. Mutation Testing
Fifteen injections: fourteen valid mutants, all killed; one equivalent and discarded. One of the fourteen survived the first pass and produced a new test.
| # | Injected defect | Expected detection | Result |
|---|---|---|---|
| M1 | the pointer byte is written as data instead of loading the pointer | T2 onward | KILLED (35) |
| M2 | the pointer does not auto-increment after an accepted write | T4 | KILLED (18) |
| M3 | a refused write advances the pointer anyway | T7 | KILLED (4) |
| M4 | a read-only write is accepted and discarded rather than NACKed | T7, T8 | KILLED (4) |
| M5 | a read-only write is accepted and actually lands | T4, T7 | KILLED (12) |
| M6 | the map does not wrap: the pointer is used raw | T10 | KILLED (9) |
| M7 | the map truncates instead of wrapping | T11 — second instance only | KILLED (6) |
| M8 | a served read byte does not advance the pointer | T6 | KILLED (3) |
| M9 | the read is registered, breaking 18.7's pre-fetch | T5 | KILLED (7) |
| M10 | read-only checked against the raw pointer, not the wrapped one | T13 new | KILLED (5) |
| M11 | the write and read pointer updates swapped in priority | T12 | KILLED (4) |
| M12 | reset does not clear the map | T14 | KILLED (5) |
| M13 | the write address is the raw pointer | T10 | KILLED (3) |
| M14 | the read address is the raw pointer | T11 | KILLED (4) |
| — | mem[ptr_in_range] rewritten as mem[pointer % N_REG] | — | EQUIVALENT — discarded |
baseline: PASS (verified before injecting anything)
valid mutants: 14 killed: 14 survived: 0 equivalent: 1 invalid: 0
restored: PASSThe equivalent mutant, and why it is not a kill
ptr_in_range is pointer % N_REG, so substituting one for the other changes the text and not the circuit. It failed nothing, and scoring it as a survivor would have sent me looking for a missing test that cannot exist.
M10 survived, and the gap was real
is_ro was checked against the wrapped pointer in the design. The mutant checked it against the raw one, and every one of the original twelve tests passed.
RO_MASK = 0x04, N_REG = 8 -> register 2 is read-only
point at 2 both versions refuse -- tested
point at 10 10 % 8 = 2, so the correct design refuses
the mutant computes RO_MASK >> 10 = 0, and ACCEPTST13 closes it from both sides: pointer 10 must refuse (the alias is protected) and pointer 9 must accept and land in register 1 (the mask does not falsely trigger on a different alias). The second half matters as much as the first — a mutant that made every wrapped address read-only would pass a test that only checked the refusal.
10. Verification Connection — Properties of a Pointer
The interesting properties are about what does not change:
// Not synthesisable. Icarus rejects SVA, so these document the intent the
// bench checks procedurally.
// A refused write changes nothing at all.
property refusal_is_inert;
@(posedge clk) disable iff (!rst_n)
(rx_valid && !rx_is_pointer && !wr_accept)
|=> ($stable(pointer) && $stable(reg_flat));
endproperty
// The addressed register is always in range, whatever the pointer says.
property address_always_in_range;
@(posedge clk) (pointer % N_REG) < N_REG;
endproperty
// Idleness is not an event: nothing moves without rx_valid or rd_taken.
property quiet_means_unchanged;
@(posedge clk) disable iff (!rst_n)
(!rx_valid && !rd_taken) |=> $stable(pointer);
endpropertyThe third is the one worth writing even though it looks trivial, because it is the assertion form of "this block cannot see framing". A future revision that added a framing input and cleared the pointer on it would fail this property immediately, whereas a directed test would only catch it if someone remembered to issue a START.
T9 is its procedural cousin: forty idle cycles, then assert the pointer and the map are exactly what they were.
11. FPGA and ASIC Implications
The register file is flops, not memory, and that is deliberate. N_REG bytes of flip-flop is cheap at the sizes an I²C peripheral needs — eight to thirty-two registers — and it is the only realisation that gives a combinational read.
RO_MASK is a parameter, so read-only costs nothing. The mask is elaborated away — is_ro becomes a constant per register — so protection is wiring, not logic. A run-time-programmable mask would be a different design with real cost.
The three counters are diagnostics. CNT_W exists so a production build can shrink or remove them. n_refused in particular is the number worth keeping: a non-zero refusal count on a shipped device means a master is writing where it should not, and nothing else in the system will report that.
The flat vector is an interface compromise. An unpacked array port is SystemVerilog only, so the map is exposed as one packed vector that the consumer slices — register i at reg_flat[8*i +: 8]. The three languages then have genuinely the same interface, which is what makes the three benches comparable.
12. Debugging — The Write That Lands One Register Late
Symptom. A driver writes four configuration bytes starting at register 0. Registers 0 and 1 are correct, register 2 keeps its default, and register 3 contains what should have been in register 2. The last byte is in register 4, which the driver never addressed. The transfer was NACKed somewhere in the middle, and the driver ignored it.
What it is not. Not the pointer load, not the auto-increment, not the wrap. The first two bytes prove all three work.
What it is. Register 2 is read-only, and the refusal advanced the pointer — mutation M3. The master's byte 2 was correctly refused; byte 3 then landed at register 3 instead of 2, and everything after it was displaced by one.
Why the symptom points away from the cause. The visible damage is in registers 3 and 4, which are writable and behaving correctly. The register that misbehaved is the one that looks untouched. A driver author reading this map sees "register 2 did not take my write and everything after it shifted" and reasonably suspects the auto-increment.
The fix is four lines. The test is T7, which asserts the pointer is unchanged after a refusal, and T4, which puts the read-only register in the middle of a sequential write so a displacement is visible rather than merely possible.
13. Common Misconceptions
"The I²C specification defines register auto-increment." It defines nothing about it — §3.1.10 note 2 explicitly assigns the decision to the device designer. Only the first-data-byte-is-the-pointer rule is normative.
"A START resets the slave, so the pointer must be cleared." Note 4 requires the bus logic to reset. Clearing application state would make the combined transfer the specification describes impossible.
"Accepting and discarding a write to a read-only register is safer than NACKing it." It is quieter, not safer. The master is told the write succeeded and has no way to discover otherwise.
"Wrapping and truncating the pointer are the same thing." Only when the map size is a power of two — which is why this bench runs a second instance at five registers.
"A registered read is fine; it is only one cycle." It is one cycle the transmitter does not have. The byte must be present in the cycle it is requested, or the slave must stretch the clock for every byte of every read.
"A read-only register is protected however you address it." Only if the mask is evaluated against the wrapped pointer. Check it against the raw one and every alias past the end of the map is writable — mutation M10, which survived a full pass.
14. Reason It Through
The specification says a START resets the slave. Why does the pointer survive one?
Because note 4 resets bus logic, and the pointer is application state. A device that cleared it could not implement the combined transfer the specification itself describes. §2.
Why does this block have no framing input rather than an ignored one?
So the property is structural. A port it does not have cannot be misused by a later revision; an ignored port is a promise, and promises are not enforced by tools. §2.
A map has eight registers and register 2 is read-only. What must a write to register 10 do, and why is that not obvious?
Refuse, because 10 wraps to 2 and an alias of a protected register is protected. It is not obvious because the read-only check and the wrap are separate pieces of logic, and testing each one separately proves nothing about the pair. §6, §9.
Why does a refused write not advance the pointer?
Because a master that ignores the NACK would then have its remaining bytes land one register off, scattering data across addresses it never named. Not advancing confines the damage to one address. §5.
Why must rd_data be combinational, and what is the cost of getting it wrong?
18.7 needs the byte in the cycle it asks. A registered read forces a clock stretch on every byte of every read. §4.
Fourteen mutants were killed and one was discarded. Why is discarding it the stronger result?
Because it was an identity — ptr_in_range is pointer % N_REG — so no test could distinguish it. Counting it would inflate the score and send someone hunting for an impossible test. §9.
15. Understanding Check
16. Summary
The specification delegates almost all of this, in one sentence that names the device designer. Six decisions follow, every one of them conformant and every one of them requiring documentation rather than intuition.
The pointer survives a repeated START, because note 4 resets bus logic and the pointer is application state — and a device that cleared it could not implement the combined transfer the specification itself describes.
And it survives structurally. This block has no framing input, so the property is guaranteed by an absent wire rather than by a designer remembering not to use one.
The first data byte is the pointer — the one normative rule here — and it is not counted as a write, because it wrote nothing.
The read is combinational, because 18.7 needs the byte in the cycle it asks. A registered read is a clock stretch on every byte of every read, and it is what block RAM gives you whether or not you asked.
A refused write neither lands nor advances. Advancing would scatter a stubborn master's remaining bytes across registers it never addressed; refusing without advancing confines the loss to one address and counts it.
The map wraps, and the bench runs a second instance at five registers to prove it — because at a power of two the documented behaviour and an accident of truncation are the same function.
The read-only mask must wrap with the address. Mutation M10 checked it against the raw pointer, survived twelve tests, and made every aliased address writable — the protection bypassed by arithmetic.
Fourteen valid mutants, fourteen killed, one discarded as equivalent. The discard is the honest part: ptr_in_range is pointer % N_REG, so no test could ever have distinguished it.
And a fourth survivor shape, which no coverage metric reports: two features tested thoroughly and never tested together. Every line ran, every branch went both ways, and the intersection was empty.
17. What Comes Next
Every block exists. What does not exist is the thing that tells them where in a transaction they are — which byte is the pointer, when the receive path may shift, whether this is a write phase or a read phase, and what a repeated START means for each of them.
Chapter 18.10 builds it: the transaction layer, the separation of protocol state from application state that this chapter's missing ports depend on, and clock stretching — the target's only legitimate way to say not yet.
Continue learning
Related tutorials
- Related topic
Register Maps — From Datasheet to Transactions
The specification describes the register-map pattern in one sentence and then hands every detail of it to the device designer. Explains the six questions a datasheet must answer before a driver can be written, why each has at least two plausible answers that exist in real parts, and how choosing wrong fails in ways that look like bus problems.
- 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.
