Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
§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 askedthis block's answerwhere it lives
1does the pointer auto-increment?yes, after every accepted data byte§3
2what happens at the end of the map?wrap§6
3does the pointer survive a STOP or a repeated START?yes§2
4what does a write to a read-only register do?NACK, so the master learns§5
5where does the pointer sit after a read?one past the last byte served§4
6does 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 STARTexamples
protocol stateyes — note 4 requires itthe bit counter, the shift register, the address match, the direction bit
application statenothe 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.

A block diagram. A received byte and a valid pulse from chapter 18.6 feed a pointer-or-data decision, steered by an is-pointer flag from chapter 18.10. The pointer byte loads a pointer register; a data byte writes the register file at the wrapped pointer. A read-only mask gates the write and produces an accept signal for chapter 18.5. The register file also drives a combinational read output to chapter 18.7, whose taken pulse advances the pointer. The register file is exposed to the application as a flat vector. No framing input appears anywhere in the diagram.rx_byte + rx_valid18.6pointer or data?rx_is_pointer — 18.10pointerwrapped mod N_REGRO_MASKwr_accept to 18.5Register fileN_REG bytesrd_datacombinational — 18.7rd_takenadvances the pointerreg_flatto the application12
Figure 1 — the register interface. Note what is absent: there is no framing input, which is how the pointer is guaranteed to survive a repeated START.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
accepted write   ->  the byte lands, and the pointer advances
refused write    ->  nothing lands, and the pointer does NOT advance

This 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.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_regs.sv — the register file, the pointer and the access rules
   // -----------------------------------------------------------------------------
   // 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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_regs.v — the same design in Verilog-2001
   // -----------------------------------------------------------------------------
   // 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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_regs.vhd — the same design in VHDL
   -- -----------------------------------------------------------------------------
   -- 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.

testwhat it establishes
T1a reset map is zero, the pointer is zero, nothing is counted
T2the pointer byte loads the pointer, writes no register, and is not counted as a write
T3the next byte lands at the pointer, and the pointer advances
T4three bytes after one pointer fill three consecutive registers — and the read-only one in the middle refuses
T5rd_data follows the pointer with no extra cycle
T6a served byte advances the pointer, so the next read is the next register
T7a read-only register refuses, and the pointer does not advance
T8wr_accept tracks the pointer combinationally, as 18.5 requires
T9forty idle cycles change nothing: the pointer and map are application state
T10at N_REG = 5 the map wraps — the byte after the last lands at zero
T11the wrap is a modulo, not a truncation: pointer 6, 7 and 9 address registers 1, 2 and 4
T12a simultaneous write and read — which the protocol cannot produce — is defined
T13a pointer past the end of the map still meets the read-only bit
T14reset 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if      (rx_valid) ...      // a write wins
else if (rd_taken) ...      // a read only if there was no write

Two 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.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_regs_tb.sv — the self-checking testbench
   // -----------------------------------------------------------------------------
   // 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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_regs_tb.v — the same tests in Verilog-2001
   // -----------------------------------------------------------------------------
   // 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
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave_regs_tb.vhd — the same tests in VHDL
   -- -----------------------------------------------------------------------------
   -- 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 ns

9. 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 defectExpected detectionResult
M1the pointer byte is written as data instead of loading the pointerT2 onwardKILLED (35)
M2the pointer does not auto-increment after an accepted writeT4KILLED (18)
M3a refused write advances the pointer anywayT7KILLED (4)
M4a read-only write is accepted and discarded rather than NACKedT7, T8KILLED (4)
M5a read-only write is accepted and actually landsT4, T7KILLED (12)
M6the map does not wrap: the pointer is used rawT10KILLED (9)
M7the map truncates instead of wrappingT11 — second instance onlyKILLED (6)
M8a served read byte does not advance the pointerT6KILLED (3)
M9the read is registered, breaking 18.7's pre-fetchT5KILLED (7)
M10read-only checked against the raw pointer, not the wrapped oneT13 newKILLED (5)
M11the write and read pointer updates swapped in priorityT12KILLED (4)
M12reset does not clear the mapT14KILLED (5)
M13the write address is the raw pointerT10KILLED (3)
M14the read address is the raw pointerT11KILLED (4)
—mem[ptr_in_range] rewritten as mem[pointer % N_REG]—EQUIVALENT — discarded
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
baseline: PASS   (verified before injecting anything)
valid mutants: 14   killed: 14   survived: 0   equivalent: 1   invalid: 0
restored: PASS

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 ACCEPTS

T13 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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);
endproperty

The 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