Skip to content
VLSI Mentor

I²C · Module 17

The Master Command Interface — Register Model and On-Chip Bus

The one block in an I²C master that UM10204 says nothing about, which makes it harder rather than easier. Derives what software must be able to express and what the master must report back, why done and ok are two bits rather than one, why only the command register may start a transfer, and why an on-chip register bus forces a post-then-poll handshake.

Every other block in this module implements a specification. This one has none.

UM10204 describes a bus between chips. How a CPU on one of those chips asks its own I²C peripheral to perform a transfer is entirely outside the document — there is no normative register map, no command encoding, no status word, no interrupt. Chapter 17.1 listed this as the first of seven things the specification hands to the designer.

That sounds like freedom and is closer to the opposite. When a specification fixes the answer, getting it right is a matter of reading carefully. When it fixes nothing, the design has to be derived from what the protocol makes possible — and a register map that fails to express something the bus can do will silently make that capability unreachable from software, no matter how correct the rest of the master is.

1. What Software Must Be Able to Say

Start from the protocol rather than from a register map, and the required expressiveness falls out.

The host must be able to specifyBecause
a target address and a direction§3.1.3 — the address byte carries seven bits and R/W̄
how many bytes, and which way they goa transaction is a byte sequence, not a single access
whether to end with a STOP or a repeated START§3.1.10 format 3 requires the choice to be expressible
the data to write, and somewhere to put readsthere has to be a payload path

Four requirements, and the third is the one that gets omitted. A register map with a "start transfer" bit and no way to say "do not release the bus when this ends" cannot express a combined transaction — which means the register-map devices of Module 16, the ones this master exists to talk to, cannot be read correctly. The capability is absent from software while being perfectly present in the hardware.

2. What the Master Must Report Back

This is where most register maps are thin, and the thinness has a characteristic shape.

Four separate things have to be readable:

That the transaction finished. A level, not an event, because software may poll at any time.

Whether it succeeded — a distinct bit, for the reason above.

Which failure, if it failed. Chapter 17.11 owns the taxonomy; this block owns only the field it lands in.

How many bytes actually moved. Not the number requested. A four-byte write NACKed after the second byte moved two, and a driver that cannot discover that has no idea what state the target's register map is now in. This is the field most often missing entirely.

3. The Atomic Start

One design decision in this block prevents a whole class of bug, and it is cheap.

The command register is written last, and writing it is what starts the transaction. Nothing else in the map starts one.

The alternative — where any write to a configuration register might begin a transfer — has a race with software that is invisible in review and appears under optimisation. The driver writes address, length, control and payload, then the command. A compiler is entitled to reorder those stores, since to the compiler they are unrelated writes to unrelated addresses. If a configuration write can start a transfer, a reordered store begins the transaction before the length has been programmed.

With a single atomic trigger, software may write the other registers in any order, and a reordering compiler cannot produce a wrong transaction. The property is worth stating as an invariant rather than a convenience, because it is what makes the block safe to drive from C.

4. A Board-Level Bus Is Not an On-Chip Bus

The distinction that trips up integration, stated plainly:

On-chip register busI²C bus
Duration of an accessone clock cyclehundreds of microseconds
Can it fail?noyes, in several ways
Who may delay it?nobodyany target, by stretching

A master's host interface faces the first; its pins face the second. Because the two disagree about duration by five orders of magnitude, the interface must be a post-then-poll handshake rather than a blocking access. A register map that pretended an I²C transfer could complete within a register write would have to stall the on-chip bus for the entire transaction — freezing a CPU for hundreds of microseconds, and deadlocking outright if the target stretches forever.

So the shape of this block is not a style choice either. It is the consequence of bridging two buses whose access times differ by a factor of 100,000.

5. The Register Map

A block diagram in three columns. The left column lists four writable configuration registers: ADDR holding the target address and direction, LEN holding the byte count, CTRL holding the stop-or-repeated-start choice, and TX0 the auto-incrementing write buffer. All four feed into a central command register labelled CMD, which is write-only and marked as the atomic trigger. The CMD register feeds the transaction controller on the right. The transaction controller returns results into two read-only registers, STATUS and ERR, and into the RX0 read buffer, which software drains.ADDR — 0x0[7:1] address, [0] R/WLEN — 0x1byte countCTRL — 0x2[0] end with STOPTX0 — 0x3write buffer, auto-incCMD — 0x7write-only: the atomic startTransaction ctrlchapter 17.8STATUS — 0x5done, ok, busy, bytesERR — 0x6which failureRX0 — 0x4read buffer, auto-inc12
Figure 1 — eight registers, and the command register deliberately last. The configuration registers on the left may be written in any order; only a write to CMD starts a transfer. The two status registers are read-only and are the only path by which software learns what happened on the wire.

Both buffers auto-increment, which is the same convention the target devices of Module 16 use — and it is worth noticing that this is the host side of the pattern rather than the target side. Software pushes a payload with repeated writes to one address and drains a result the same way, so a burst needs no per-byte addressing.

6. The Command Interface, in Three Languages

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_regs.sv — eight registers, and the atomic start
   // -----------------------------------------------------------------------------
   // i2c_cmd_regs.sv
   // The host command interface: the one block in this module UM10204 says nothing about.
   //
   // Everything else in Module 17 implements a specification. This block has none. The
   // register map, the command encoding, the handshake and the status bits are all
   // invented -- which does not make them arbitrary, because the protocol constrains what
   // the host must be able to express and what the block must be able to report back.
   //
   // WHAT THE HOST MUST BE ABLE TO SAY, derived from the protocol rather than guessed:
   //   a target address and a direction              §3.1.3, the address byte
   //   how many bytes, and which way they go         a transaction is a byte sequence
   //   whether to end with a STOP or a repeated START §3.1.10 format 3 needs the choice
   //   the data to write, and somewhere to put reads
   //
   // WHAT THE BLOCK MUST BE ABLE TO REPORT, and this is where most register maps are thin:
   //   that the transaction FINISHED                 -- and separately,
   //   whether it SUCCEEDED                          -- which is not the same question
   //   WHICH failure, if it failed                   -- Chapter 17.11's taxonomy
   //   how many bytes actually moved                 -- because a transaction can stop
   //                                                    part way through on a NACK
   //
   // THE DISTINCTION THAT MATTERS MOST. `done` and `ok` are separate bits. A transaction
   // that was NACKed on its address is finished and did not succeed; a single `done` bit
   // forces the driver to infer success from the absence of an error, which breaks the
   // moment a new error code is added. Chapter 16.4's argument about `outcome_known`
   // applies here in a milder form: report the outcome and the confidence separately.
   //
   // AND THE COMMAND IS ATOMIC. The command register is written LAST, after the address,
   // the length and the data, and writing it is what starts the transaction. A design in
   // which any register write could start one has a race with software that writes them in
   // a different order -- and that race is invisible until the compiler reorders two
   // stores.
   //
   // A BOARD-LEVEL BUS AND AN ON-CHIP REGISTER BUS DIFFER IN ONE RESPECT that matters
   // here: an on-chip write completes in a cycle and cannot fail, while an I²C transaction
   // takes hundreds of microseconds and can. So the interface is necessarily a
   // POST-then-POLL handshake rather than a blocking access, and a register map that
   // pretended otherwise would have to stall the on-chip bus for the whole transaction.
   // -----------------------------------------------------------------------------

   module i2c_cmd_regs #(
      parameter int N_BUF = 8,       // bytes of write and read buffer
      parameter int CNT_W = 16
   ) (
      input  logic            clk,
      input  logic            rst_n,

      // ---- the on-chip register bus: address, write data, strobe, read data ----
      input  logic [3:0]      reg_addr,
      input  logic [7:0]      reg_wdata,
      input  logic            reg_we,
      input  logic            reg_re,
      output logic [7:0]      reg_rdata,

      // ---- to the transaction controller -------------------------------------
      output logic            cmd_valid,      // one cycle: start this transaction
      output logic [6:0]      cmd_addr,
      output logic            cmd_read,
      output logic [3:0]      cmd_len,
      output logic            cmd_stop,       // end with a STOP rather than a repeated START
      output logic [7:0]      tx_data,        // the byte at tx_index
      input  logic [3:0]      tx_index,
      input  logic [7:0]      rx_data,
      input  logic [3:0]      rx_index,
      input  logic            rx_we,

      // ---- from the transaction controller ------------------------------------
      input  logic            txn_done,
      input  logic            txn_ok,
      input  logic [5:0]      txn_err,
      input  logic [3:0]      txn_bytes,
      input  logic            txn_busy,

      output logic [CNT_W-1:0] commands_issued
   );

      // The map. Eight registers, and the command register is deliberately last.
      localparam [3:0] R_ADDR   = 4'h0,   // [7:1] target address, [0] direction
                       R_LEN    = 4'h1,   // byte count
                       R_CTRL   = 4'h2,   // [0] end with STOP
                       R_TX0    = 4'h3,   // write buffer, auto-incrementing
                       R_RX0    = 4'h4,   // read buffer, auto-incrementing
                       R_STATUS = 4'h5,   // read-only
                       R_ERR    = 4'h6,   // read-only
                       R_CMD    = 4'h7;   // WRITE-ONLY, and writing it starts the transfer

      logic [7:0] txbuf [0:N_BUF-1];
      logic [7:0] rxbuf [0:N_BUF-1];
      integer   i;

      logic [7:0] r_addr, r_len, r_ctrl;
      logic [3:0] tx_wptr, rx_rptr;
      logic       s_done, s_ok;
      logic [5:0] s_err;
      logic [3:0] s_bytes;

      assign tx_data = txbuf[tx_index[2:0]];

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            cmd_valid       <= 1'b0;
            cmd_addr        <= 7'h00;
            cmd_read        <= 1'b0;
            cmd_len         <= 4'd0;
            cmd_stop        <= 1'b1;
            reg_rdata       <= 8'h00;
            r_addr          <= 8'h00;
            r_len           <= 8'h00;
            r_ctrl          <= 8'h01;      // default: end with a STOP
            tx_wptr         <= 4'd0;
            rx_rptr         <= 4'd0;
            s_done          <= 1'b0;
            s_ok            <= 1'b0;
            s_err           <= 6'd0;
            s_bytes         <= 4'd0;
            commands_issued <= {CNT_W{1'b0}};
            for (i = 0; i < N_BUF; i = i + 1) begin
               txbuf[i] <= 8'h00;
               rxbuf[i] <= 8'h00;
            end
         end else begin
            cmd_valid <= 1'b0;

            // ---- the controller's results land here -------------------------
            if (rx_we) rxbuf[rx_index[2:0]] <= rx_data;
            if (txn_done) begin
               s_done  <= 1'b1;
               s_ok    <= txn_ok;
               s_err   <= txn_err;
               s_bytes <= txn_bytes;
            end

            // ---- writes ------------------------------------------------------
            if (reg_we) begin
               case (reg_addr)
                  R_ADDR: r_addr <= reg_wdata;
                  R_LEN:  r_len  <= reg_wdata;
                  R_CTRL: r_ctrl <= reg_wdata;
                  R_TX0: begin
                     // Auto-incrementing write port, so a driver pushes a payload with
                     // repeated stores to one address -- which is what a memcpy-shaped
                     // loop wants, and what Chapter 16.1's question 1 is about, seen from
                     // the host side of the bus rather than the target side.
                     txbuf[tx_wptr[2:0]] <= reg_wdata;
                     tx_wptr <= (tx_wptr + 4'd1 >= N_BUF[3:0]) ? 4'd0 : tx_wptr + 4'd1;
                  end
                  R_CMD: begin
                     // THE ATOMIC START. Nothing else in this map starts a transaction, so
                     // software may write the address, length, control and payload in any
                     // order -- and a compiler may reorder them -- without a race.
                     //
                     // A command arriving while the controller is busy is REFUSED rather
                     // than queued. Queueing would need a command FIFO and a policy for
                     // what a second command means while the first is mid-transaction, and
                     // the honest minimum is to say no.
                     if (!txn_busy) begin
                        cmd_valid       <= 1'b1;
                        cmd_addr        <= r_addr[7:1];
                        cmd_read        <= r_addr[0];
                        cmd_len         <= r_len[3:0];
                        cmd_stop        <= r_ctrl[0];
                        commands_issued <= commands_issued + 1'b1;
                        // Clear the previous result, so a poll cannot see a stale `done`
                        // from the last transaction and conclude this one finished
                        // instantly. That is the most common driver bug against a register
                        // map of this shape.
                        s_done  <= 1'b0;
                        s_ok    <= 1'b0;
                        s_err   <= 6'd0;
                        s_bytes <= 4'd0;
                        tx_wptr <= 4'd0;
                        rx_rptr <= 4'd0;
                     end
                  end
                  default: ;
               endcase
            end

            // ---- reads -------------------------------------------------------
            if (reg_re) begin
               case (reg_addr)
                  R_ADDR:   reg_rdata <= r_addr;
                  R_LEN:    reg_rdata <= r_len;
                  R_CTRL:   reg_rdata <= r_ctrl;
                  R_RX0: begin
                     reg_rdata <= rxbuf[rx_rptr[2:0]];
                     rx_rptr   <= (rx_rptr + 4'd1 >= N_BUF[3:0]) ? 4'd0 : rx_rptr + 4'd1;
                  end
                  // `done` and `ok` are SEPARATE BITS. A transaction NACKed on its address
                  // is finished and did not succeed, and a single bit cannot say that.
                  //
                  // Built as ONE concatenation rather than as an OR of three shifted
                  // masks. That is not a style preference: an OR of masks lets two fields
                  // overlap silently, and the first version of this line did -- the byte
                  // count was placed at bit 1 and corrupted `ok` and `busy` for every
                  // transaction that moved more than one byte. A concatenation of the
                  // exact widths cannot overlap, because the widths have to add up to
                  // eight or the elaborator says so.
                  //   [7:4] bytes transferred   [2] busy   [1] ok   [0] done
                  R_STATUS: reg_rdata <= {s_bytes, 1'b0, txn_busy, s_ok, s_done};
                  // The latched error from the last transaction, OR the one the bus is
                  // reporting RIGHT NOW. Both are needed and neither is sufficient.
                  //
                  // A transaction error has to persist after the transaction ends, or a
                  // driver that polls `done` and then reads the code finds it gone. But a
                  // STUCK LINE is diagnosed BETWEEN transactions -- Chapter 17.11 cannot
                  // distinguish a held line from a clocked one while a transfer is open --
                  // so a register that only latched at completion would never show a stuck
                  // bus at all. Which is exactly what the first version of this line did:
                  // the codes existed inside the error manager and were invisible to
                  // software until some later transaction happened to fail.
                  R_ERR:    reg_rdata <= {2'b00, s_err | txn_err};
                  R_CMD:    reg_rdata <= 8'h00;   // write-only: reads as zero, not as junk
                  default:  reg_rdata <= 8'h00;
               endcase
            end
         end
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_regs.v — the same block in Verilog-2001
   // -----------------------------------------------------------------------------
   // i2c_cmd_regs.sv
   // The host command interface: the one block in this module UM10204 says nothing about.
   //
   // Everything else in Module 17 implements a specification. This block has none. The
   // register map, the command encoding, the handshake and the status bits are all
   // invented -- which does not make them arbitrary, because the protocol constrains what
   // the host must be able to express and what the block must be able to report back.
   //
   // WHAT THE HOST MUST BE ABLE TO SAY, derived from the protocol rather than guessed:
   //   a target address and a direction              §3.1.3, the address byte
   //   how many bytes, and which way they go         a transaction is a byte sequence
   //   whether to end with a STOP or a repeated START §3.1.10 format 3 needs the choice
   //   the data to write, and somewhere to put reads
   //
   // WHAT THE BLOCK MUST BE ABLE TO REPORT, and this is where most register maps are thin:
   //   that the transaction FINISHED                 -- and separately,
   //   whether it SUCCEEDED                          -- which is not the same question
   //   WHICH failure, if it failed                   -- Chapter 17.11's taxonomy
   //   how many bytes actually moved                 -- because a transaction can stop
   //                                                    part way through on a NACK
   //
   // THE DISTINCTION THAT MATTERS MOST. `done` and `ok` are separate bits. A transaction
   // that was NACKed on its address is finished and did not succeed; a single `done` bit
   // forces the driver to infer success from the absence of an error, which breaks the
   // moment a new error code is added. Chapter 16.4's argument about `outcome_known`
   // applies here in a milder form: report the outcome and the confidence separately.
   //
   // AND THE COMMAND IS ATOMIC. The command register is written LAST, after the address,
   // the length and the data, and writing it is what starts the transaction. A design in
   // which any register write could start one has a race with software that writes them in
   // a different order -- and that race is invisible until the compiler reorders two
   // stores.
   //
   // A BOARD-LEVEL BUS AND AN ON-CHIP REGISTER BUS DIFFER IN ONE RESPECT that matters
   // here: an on-chip write completes in a cycle and cannot fail, while an I²C transaction
   // takes hundreds of microseconds and can. So the interface is necessarily a
   // POST-then-POLL handshake rather than a blocking access, and a register map that
   // pretended otherwise would have to stall the on-chip bus for the whole transaction.
   // -----------------------------------------------------------------------------

   // (Verilog-2001 -- structurally identical to the SystemVerilog above.)
   module i2c_cmd_regs #(
      parameter N_BUF = 8,       // bytes of write and read buffer
      parameter CNT_W = 16
   ) (
      input  wire            clk,
      input  wire            rst_n,

      // ---- the on-chip register bus: address, write data, strobe, read data ----
      input  wire [3:0]      reg_addr,
      input  wire [7:0]      reg_wdata,
      input  wire            reg_we,
      input  wire            reg_re,
      output reg  [7:0]      reg_rdata,

      // ---- to the transaction controller -------------------------------------
      output reg             cmd_valid,      // one cycle: start this transaction
      output reg  [6:0]      cmd_addr,
      output reg             cmd_read,
      output reg  [3:0]      cmd_len,
      output reg             cmd_stop,       // end with a STOP rather than a repeated START
      output wire [7:0]      tx_data,        // the byte at tx_index
      input  wire [3:0]      tx_index,
      input  wire [7:0]      rx_data,
      input  wire [3:0]      rx_index,
      input  wire            rx_we,

      // ---- from the transaction controller ------------------------------------
      input  wire            txn_done,
      input  wire            txn_ok,
      input  wire [5:0]      txn_err,
      input  wire [3:0]      txn_bytes,
      input  wire            txn_busy,

      output reg  [CNT_W-1:0] commands_issued
   );

      // The map. Eight registers, and the command register is deliberately last.
      localparam [3:0] R_ADDR   = 4'h0,   // [7:1] target address, [0] direction
                       R_LEN    = 4'h1,   // byte count
                       R_CTRL   = 4'h2,   // [0] end with STOP
                       R_TX0    = 4'h3,   // write buffer, auto-incrementing
                       R_RX0    = 4'h4,   // read buffer, auto-incrementing
                       R_STATUS = 4'h5,   // read-only
                       R_ERR    = 4'h6,   // read-only
                       R_CMD    = 4'h7;   // WRITE-ONLY, and writing it starts the transfer

      reg [7:0] txbuf [0:N_BUF-1];
      reg [7:0] rxbuf [0:N_BUF-1];
      integer   i;

      reg [7:0] r_addr, r_len, r_ctrl;
      reg [3:0] tx_wptr, rx_rptr;
      reg       s_done, s_ok;
      reg [5:0] s_err;
      reg [3:0] s_bytes;

      assign tx_data = txbuf[tx_index[2:0]];

      always @(posedge clk or negedge rst_n) begin
         if (!rst_n) begin
            cmd_valid       <= 1'b0;
            cmd_addr        <= 7'h00;
            cmd_read        <= 1'b0;
            cmd_len         <= 4'd0;
            cmd_stop        <= 1'b1;
            reg_rdata       <= 8'h00;
            r_addr          <= 8'h00;
            r_len           <= 8'h00;
            r_ctrl          <= 8'h01;      // default: end with a STOP
            tx_wptr         <= 4'd0;
            rx_rptr         <= 4'd0;
            s_done          <= 1'b0;
            s_ok            <= 1'b0;
            s_err           <= 6'd0;
            s_bytes         <= 4'd0;
            commands_issued <= {CNT_W{1'b0}};
            for (i = 0; i < N_BUF; i = i + 1) begin
               txbuf[i] <= 8'h00;
               rxbuf[i] <= 8'h00;
            end
         end else begin
            cmd_valid <= 1'b0;

            // ---- the controller's results land here -------------------------
            if (rx_we) rxbuf[rx_index[2:0]] <= rx_data;
            if (txn_done) begin
               s_done  <= 1'b1;
               s_ok    <= txn_ok;
               s_err   <= txn_err;
               s_bytes <= txn_bytes;
            end

            // ---- writes ------------------------------------------------------
            if (reg_we) begin
               case (reg_addr)
                  R_ADDR: r_addr <= reg_wdata;
                  R_LEN:  r_len  <= reg_wdata;
                  R_CTRL: r_ctrl <= reg_wdata;
                  R_TX0: begin
                     // Auto-incrementing write port, so a driver pushes a payload with
                     // repeated stores to one address -- which is what a memcpy-shaped
                     // loop wants, and what Chapter 16.1's question 1 is about, seen from
                     // the host side of the bus rather than the target side.
                     txbuf[tx_wptr[2:0]] <= reg_wdata;
                     tx_wptr <= (tx_wptr + 4'd1 >= N_BUF[3:0]) ? 4'd0 : tx_wptr + 4'd1;
                  end
                  R_CMD: begin
                     // THE ATOMIC START. Nothing else in this map starts a transaction, so
                     // software may write the address, length, control and payload in any
                     // order -- and a compiler may reorder them -- without a race.
                     //
                     // A command arriving while the controller is busy is REFUSED rather
                     // than queued. Queueing would need a command FIFO and a policy for
                     // what a second command means while the first is mid-transaction, and
                     // the honest minimum is to say no.
                     if (!txn_busy) begin
                        cmd_valid       <= 1'b1;
                        cmd_addr        <= r_addr[7:1];
                        cmd_read        <= r_addr[0];
                        cmd_len         <= r_len[3:0];
                        cmd_stop        <= r_ctrl[0];
                        commands_issued <= commands_issued + 1'b1;
                        // Clear the previous result, so a poll cannot see a stale `done`
                        // from the last transaction and conclude this one finished
                        // instantly. That is the most common driver bug against a register
                        // map of this shape.
                        s_done  <= 1'b0;
                        s_ok    <= 1'b0;
                        s_err   <= 6'd0;
                        s_bytes <= 4'd0;
                        tx_wptr <= 4'd0;
                        rx_rptr <= 4'd0;
                     end
                  end
                  default: ;
               endcase
            end

            // ---- reads -------------------------------------------------------
            if (reg_re) begin
               case (reg_addr)
                  R_ADDR:   reg_rdata <= r_addr;
                  R_LEN:    reg_rdata <= r_len;
                  R_CTRL:   reg_rdata <= r_ctrl;
                  R_RX0: begin
                     reg_rdata <= rxbuf[rx_rptr[2:0]];
                     rx_rptr   <= (rx_rptr + 4'd1 >= N_BUF[3:0]) ? 4'd0 : rx_rptr + 4'd1;
                  end
                  // `done` and `ok` are SEPARATE BITS. A transaction NACKed on its address
                  // is finished and did not succeed, and a single bit cannot say that.
                  //
                  // Built as ONE concatenation rather than as an OR of three shifted
                  // masks. That is not a style preference: an OR of masks lets two fields
                  // overlap silently, and the first version of this line did -- the byte
                  // count was placed at bit 1 and corrupted `ok` and `busy` for every
                  // transaction that moved more than one byte. A concatenation of the
                  // exact widths cannot overlap, because the widths have to add up to
                  // eight or the elaborator says so.
                  //   [7:4] bytes transferred   [2] busy   [1] ok   [0] done
                  R_STATUS: reg_rdata <= {s_bytes, 1'b0, txn_busy, s_ok, s_done};
                  // The latched error from the last transaction, OR the one the bus is
                  // reporting RIGHT NOW. Both are needed and neither is sufficient.
                  //
                  // A transaction error has to persist after the transaction ends, or a
                  // driver that polls `done` and then reads the code finds it gone. But a
                  // STUCK LINE is diagnosed BETWEEN transactions -- Chapter 17.11 cannot
                  // distinguish a held line from a clocked one while a transfer is open --
                  // so a register that only latched at completion would never show a stuck
                  // bus at all. Which is exactly what the first version of this line did:
                  // the codes existed inside the error manager and were invisible to
                  // software until some later transaction happened to fail.
                  R_ERR:    reg_rdata <= {2'b00, s_err | txn_err};
                  R_CMD:    reg_rdata <= 8'h00;   // write-only: reads as zero, not as junk
                  default:  reg_rdata <= 8'h00;
               endcase
            end
         end
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_regs.vhd — the same block in VHDL
   -- ---------------------------------------------------------------------------
   -- i2c_cmd_regs.vhd
   -- The host command interface: the one block in this module UM10204 says nothing about.
   -- Behavioural twin of i2c_cmd_regs.sv / .v.
   --
   -- Everything else in Module 17 implements a specification. This block has none. The register
   -- map, the command encoding, the handshake and the status bits are all invented -- which does
   -- not make them arbitrary, because the protocol constrains what the host must be able to
   -- express and what the block must report back.
   --
   -- THE DISTINCTION THAT MATTERS MOST. `done` and `ok` are separate bits. A transaction NACKed
   -- on its address is finished and did not succeed; a single `done` bit forces the driver to
   -- infer success from the absence of an error, which breaks the moment a code is added.
   --
   -- AND THE COMMAND IS ATOMIC. The command register is written LAST, and writing it is what
   -- starts the transaction. A design in which any register write could start one races with
   -- software that writes them in a different order -- and that race is invisible until the
   -- compiler reorders two stores.
   --
   -- A BOARD-LEVEL BUS AND AN ON-CHIP REGISTER BUS DIFFER IN ONE RESPECT that matters here: an
   -- on-chip write completes in a cycle and cannot fail, while an I²C transaction takes hundreds
   -- of microseconds and can. So the interface is necessarily POST-then-POLL rather than a
   -- blocking access.
   -- ---------------------------------------------------------------------------

   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_cmd_regs is
      generic (
         N_BUF : integer := 8;
         CNT_W : integer := 16
      );
      port (
         clk   : in std_logic;
         rst_n : in std_logic;

         reg_addr  : in  std_logic_vector(3 downto 0);
         reg_wdata : in  std_logic_vector(7 downto 0);
         reg_we    : in  std_logic;
         reg_re    : in  std_logic;
         reg_rdata : out std_logic_vector(7 downto 0);

         cmd_valid : out std_logic;
         cmd_addr  : out std_logic_vector(6 downto 0);
         cmd_read  : out std_logic;
         cmd_len   : out unsigned(3 downto 0);
         cmd_stop  : out std_logic;

         tx_data  : out std_logic_vector(7 downto 0);
         tx_index : in  unsigned(3 downto 0);
         rx_data  : in  std_logic_vector(7 downto 0);
         rx_index : in  unsigned(3 downto 0);
         rx_we    : in  std_logic;

         txn_done  : in std_logic;
         txn_ok    : in std_logic;
         txn_err   : in std_logic_vector(5 downto 0);
         txn_bytes : in unsigned(3 downto 0);
         txn_busy  : in std_logic;

         commands_issued : out unsigned(CNT_W-1 downto 0)
      );
   end entity i2c_cmd_regs;

   architecture rtl of i2c_cmd_regs is

      -- The map. Eight registers, and the command register is deliberately last.
      --
      -- Named IDX_* rather than R_* because VHDL identifiers are CASE-INSENSITIVE: a constant
      -- IDX_ADDR is the same name as the signal r_addr, and a constant REG_ADDR is the same name
      -- as the port reg_addr. This is the third collision of that kind in the module, and the
      -- error message always names the OTHER declaration, which is the confusing part.
      constant IDX_ADDR   : std_logic_vector(3 downto 0) := x"0";
      constant IDX_LEN    : std_logic_vector(3 downto 0) := x"1";
      constant IDX_CTRL   : std_logic_vector(3 downto 0) := x"2";
      constant IDX_TX0    : std_logic_vector(3 downto 0) := x"3";
      constant IDX_RX0    : std_logic_vector(3 downto 0) := x"4";
      constant IDX_STATUS : std_logic_vector(3 downto 0) := x"5";
      constant IDX_ERR    : std_logic_vector(3 downto 0) := x"6";
      constant IDX_CMD    : std_logic_vector(3 downto 0) := x"7";


      -- A metavalue-safe index. At time zero, before any reset has propagated, an unsigned
      -- signal still reads 'U' -- and `to_integer` on that emits a NUMERIC_STD warning and
      -- returns 0 anyway. Converting explicitly keeps the transcript clean and says what is
      -- meant: an index that is not yet valid selects slot zero, which nothing reads.
      function safe_idx (v : unsigned) return integer is
      begin
         for i in v'range loop
            if v(i) /= '0' and v(i) /= '1' then return 0; end if;
         end loop;
         return to_integer(v);
      end function;

      type buf_t is array (0 to N_BUF-1) of std_logic_vector(7 downto 0);
      signal txbuf, rxbuf : buf_t := (others => (others => '0'));

      signal r_addr, r_len, r_ctrl : std_logic_vector(7 downto 0) := (others => '0');
      signal tx_wptr, rx_rptr : unsigned(3 downto 0) := (others => '0');
      signal s_done, s_ok : std_logic := '0';
      signal s_err : std_logic_vector(5 downto 0) := (others => '0');
      signal s_bytes : unsigned(3 downto 0) := (others => '0');
      signal n_cmds : unsigned(CNT_W-1 downto 0) := (others => '0');

   begin

      tx_data <= txbuf(safe_idx(tx_index(2 downto 0)));
      commands_issued <= n_cmds;

      process (clk, rst_n)
      begin
         if rst_n = '0' then
            cmd_valid <= '0';
            cmd_addr  <= (others => '0');
            cmd_read  <= '0';
            cmd_len   <= (others => '0');
            cmd_stop  <= '1';
            reg_rdata <= (others => '0');
            r_addr    <= (others => '0');
            r_len     <= (others => '0');
            r_ctrl    <= x"01";          -- default: end with a STOP
            tx_wptr   <= (others => '0');
            rx_rptr   <= (others => '0');
            s_done    <= '0';
            s_ok      <= '0';
            s_err     <= (others => '0');
            s_bytes   <= (others => '0');
            n_cmds    <= (others => '0');
            txbuf     <= (others => (others => '0'));
            rxbuf     <= (others => (others => '0'));
         elsif rising_edge(clk) then
            cmd_valid <= '0';

            -- ---- the controller's results land here -------------------------
            if rx_we = '1' then
               rxbuf(safe_idx(rx_index(2 downto 0))) <= rx_data;
            end if;
            if txn_done = '1' then
               s_done  <= '1';
               s_ok    <= txn_ok;
               s_err   <= txn_err;
               s_bytes <= txn_bytes;
            end if;

            -- ---- writes ------------------------------------------------------
            if reg_we = '1' then
               if reg_addr = IDX_ADDR then
                  r_addr <= reg_wdata;
               elsif reg_addr = IDX_LEN then
                  r_len <= reg_wdata;
               elsif reg_addr = IDX_CTRL then
                  r_ctrl <= reg_wdata;
               elsif reg_addr = IDX_TX0 then
                  -- Auto-incrementing write port, so a driver pushes a payload with repeated
                  -- stores to one address -- the shape a memcpy loop has.
                  txbuf(to_integer(tx_wptr(2 downto 0))) <= reg_wdata;
                  if (tx_wptr + 1) >= to_unsigned(N_BUF, 4) then tx_wptr <= (others => '0');
                  else                                           tx_wptr <= tx_wptr + 1;
                  end if;
               elsif reg_addr = IDX_CMD then
                  -- THE ATOMIC START. Nothing else in this map starts a transaction, so
                  -- software may write the address, length, control and payload in any order --
                  -- and a compiler may reorder them -- without a race.
                  --
                  -- A command arriving while the controller is busy is REFUSED rather than
                  -- queued: queueing needs a policy for what a second command means
                  -- mid-transaction, and the honest minimum is to say no.
                  if txn_busy = '0' then
                     cmd_valid <= '1';
                     cmd_addr  <= r_addr(7 downto 1);
                     cmd_read  <= r_addr(0);
                     cmd_len   <= unsigned(r_len(3 downto 0));
                     cmd_stop  <= r_ctrl(0);
                     n_cmds    <= n_cmds + 1;
                     -- Clear the previous result, so a poll cannot see a stale `done` from the
                     -- last transaction and conclude this one finished instantly. That is the
                     -- most common driver bug against a register map of this shape.
                     s_done  <= '0';
                     s_ok    <= '0';
                     s_err   <= (others => '0');
                     s_bytes <= (others => '0');
                     tx_wptr <= (others => '0');
                     rx_rptr <= (others => '0');
                  end if;
               end if;
            end if;

            -- ---- reads -------------------------------------------------------
            if reg_re = '1' then
               if reg_addr = IDX_ADDR then
                  reg_rdata <= r_addr;
               elsif reg_addr = IDX_LEN then
                  reg_rdata <= r_len;
               elsif reg_addr = IDX_CTRL then
                  reg_rdata <= r_ctrl;
               elsif reg_addr = IDX_RX0 then
                  reg_rdata <= rxbuf(to_integer(rx_rptr(2 downto 0)));
                  if (rx_rptr + 1) >= to_unsigned(N_BUF, 4) then rx_rptr <= (others => '0');
                  else                                           rx_rptr <= rx_rptr + 1;
                  end if;
               elsif reg_addr = IDX_STATUS then
                  -- Built as ONE concatenation rather than as an OR of three shifted masks. An
                  -- OR of masks lets two fields overlap silently, and the first version of this
                  -- register DID: the byte count sat at bit 1 and corrupted `ok` and `busy` for
                  -- every transaction that moved more than one byte. A concatenation of the
                  -- exact widths cannot overlap, because the widths have to add up to eight.
                  --   [7:4] bytes transferred   [2] busy   [1] ok   [0] done
                  reg_rdata <= std_logic_vector(s_bytes) & '0' & txn_busy & s_ok & s_done;
               elsif reg_addr = IDX_ERR then
                  -- The latched error from the last transaction, OR the one the bus is
                  -- reporting RIGHT NOW. Both are needed and neither is sufficient: a
                  -- transaction error must persist after the transaction ends, but a STUCK LINE
                  -- is diagnosed BETWEEN transactions, so a register that only latched at
                  -- completion would never show a stuck bus at all.
                  reg_rdata <= "00" & (s_err or txn_err);
               elsif reg_addr = IDX_CMD then
                  reg_rdata <= (others => '0');   -- write-only: reads as zero, not as junk
               else
                  reg_rdata <= (others => '0');
               end if;
            end if;
         end if;
      end process;

   end architecture rtl;

6a. The testbenches

Twelve tests. The bench drives the register bus cycle-by-cycle and models the transaction controller's replies, so it never waits on an unbounded event — every wait is a clock wait, which is why no watchdog appears.

#TestProperty
T1registers hold what was writtenthe baseline
T2the atomic startonly CMD starts a transaction
T3the order does not mattersame transfer, configured in three orders
T4the write buffer auto-incrementsa payload pushed to one address
T5the read buffer fills and drainsthe controller writes, the host reads
T6done and ok are separate bits§2's central claim, executed
T7a success looks different in every fieldwhat makes the split useful
T8the byte count is a third questiona NACK part way through reports the partial count
T9the status fields do not overlapthe count is high, the flags are low
T10a new command clears the previous resultno stale done on the next poll
T11a command while busy is refusednot queued, not silently dropped
T12CMD reads as zero, not junkwrite-only means defined-on-read

T10 is the one that repays attention. Without it a driver that issues a command and immediately polls would read the previous transaction's done bit, conclude this one finished instantly, and read a stale result — the most common driver bug against a register map of this shape, and entirely a hardware responsibility to prevent.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_regs_tb.sv — twelve tests over the register contract
   `timescale 1ns/1ps
   // -----------------------------------------------------------------------------
   // i2c_cmd_regs_tb.sv
   // Independent oracle for i2c_cmd_regs.
   //
   // The bench is the HOST: it writes and reads registers over the on-chip bus and never
   // touches the block's internals. It also plays the transaction controller, so it can
   // return results the register map has to report faithfully -- including the case a
   // register map most often gets wrong, a transaction that finished and failed.
   // -----------------------------------------------------------------------------
   module i2c_cmd_regs_tb;

      localparam integer NB = 8;
      localparam [3:0] R_ADDR = 4'h0, R_LEN = 4'h1, R_CTRL = 4'h2, R_TX0 = 4'h3,
                       R_RX0 = 4'h4, R_STATUS = 4'h5, R_ERR = 4'h6, R_CMD = 4'h7;

      logic clk = 1'b0, rst_n = 1'b0;
      logic [3:0] reg_addr = 4'h0;
      logic [7:0] reg_wdata = 8'h00;
      logic reg_we = 1'b0, reg_re = 1'b0;
      logic [7:0] reg_rdata;

      logic cmd_valid, cmd_read, cmd_stop;
      logic [6:0] cmd_addr;
      logic [3:0] cmd_len;
      logic [7:0] tx_data;
      logic [15:0] n_cmds;

      logic [3:0] tx_index = 4'd0, rx_index = 4'd0;
      logic [7:0] rx_data = 8'h00;
      logic rx_we = 1'b0;
      logic txn_done = 1'b0, txn_ok = 1'b0, txn_busy = 1'b0;
      logic [5:0] txn_err = 6'd0;
      logic [3:0] txn_bytes = 4'd0;

      i2c_cmd_regs #(.N_BUF(NB), .CNT_W(16)) dut (
         .clk(clk), .rst_n(rst_n),
         .reg_addr(reg_addr), .reg_wdata(reg_wdata), .reg_we(reg_we), .reg_re(reg_re),
         .reg_rdata(reg_rdata),
         .cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
         .cmd_len(cmd_len), .cmd_stop(cmd_stop),
         .tx_data(tx_data), .tx_index(tx_index),
         .rx_data(rx_data), .rx_index(rx_index), .rx_we(rx_we),
         .txn_done(txn_done), .txn_ok(txn_ok), .txn_err(txn_err),
         .txn_bytes(txn_bytes), .txn_busy(txn_busy),
         .commands_issued(n_cmds));

      always #5 clk = ~clk;

      integer errors = 0;
      integer k;
      logic [7:0] got;
      logic saw_cmd;

      // Catch the one-cycle command pulse, which a host polling registers could miss.
      always @(posedge clk) if (rst_n && cmd_valid) saw_cmd <= 1'b1;

      task step; begin @(posedge clk); @(negedge clk); end endtask

      task do_reset;
         begin
            @(negedge clk);
            rst_n = 1'b0; reg_we = 1'b0; reg_re = 1'b0;
            tx_index = 4'd0; rx_index = 4'd0; rx_data = 8'h00; rx_we = 1'b0;
            txn_done = 1'b0; txn_ok = 1'b0; txn_busy = 1'b0;
            txn_err = 6'd0; txn_bytes = 4'd0; saw_cmd = 1'b0;
            repeat (3) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            step;
         end
      endtask

      task wr (input [3:0] a, input [7:0] d);
         begin
            @(negedge clk); reg_addr = a; reg_wdata = d; reg_we = 1'b1;
            @(posedge clk); @(negedge clk); reg_we = 1'b0;
         end
      endtask

      task rd (input [3:0] a);
         begin
            @(negedge clk); reg_addr = a; reg_re = 1'b1;
            @(posedge clk); @(negedge clk); reg_re = 1'b0;
            got = reg_rdata;
         end
      endtask

      // The controller's answer, delivered the way the controller delivers it.
      task finish_txn (input ok, input [5:0] e, input [3:0] nb);
         begin
            @(negedge clk); txn_busy = 1'b0; txn_ok = ok; txn_err = e; txn_bytes = nb;
            txn_done = 1'b1;
            @(posedge clk); @(negedge clk); txn_done = 1'b0;
         end
      endtask

      task ck_int (input [200*8:1] what, input integer g, input integer e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s: got %0d (0x%0h) expected %0d (0x%0h)", what, g, g, e, e);
               errors = errors + 1;
            end
         end
      endtask

      task ck_bit (input [200*8:1] what, input g, input e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s: got %0b expected %0b", what, g, e);
               errors = errors + 1;
            end
         end
      endtask

      initial begin
         $display("=== i2c_cmd_regs: the one block the specification says nothing about ===");

         // ----------------------------------------------------------------
         // T1. Registers hold what was written, and read back.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA1);      // target 0x50, read direction
         wr(R_LEN,  8'h03);
         wr(R_CTRL, 8'h01);
         rd(R_ADDR); ck_int("T1 the address register reads back", got, 8'hA1);
         rd(R_LEN);  ck_int("T1 the length register reads back", got, 8'h03);
         rd(R_CTRL); ck_int("T1 the control register reads back", got, 8'h01);
         $display("T1  the configuration registers hold what the host wrote");
         ck_bit("T1 and no command was started by any of them", saw_cmd, 1'b0);

         // ----------------------------------------------------------------
         // T2. THE ATOMIC START. Only the command register starts a transaction, so the
         //     host may write the others in any order -- and a compiler may reorder them.
         // ----------------------------------------------------------------
         wr(R_CMD, 8'h01);
         // One extra cycle before looking at `saw_cmd`. The command pulse is registered,
         // so it is high during the cycle AFTER the register write is latched -- and the
         // monitor that records it samples at the edge that sets it, so it sees the pulse
         // one edge later again. A host polling a status register has exactly this
         // problem, which is why `cmd_valid` is a handshake to the controller and not
         // something software is expected to catch.
         step;
         $display("T2  only the command register starts a transaction");
         ck_bit("T2 now a command was issued", saw_cmd, 1'b1);
         ck_int("T2 with the address that was configured", cmd_addr, 7'h50);
         ck_bit("T2 and the direction", cmd_read, 1'b1);
         ck_int("T2 and the length", cmd_len, 3);
         ck_bit("T2 and the STOP choice", cmd_stop, 1'b1);
         ck_int("T2 one command issued", n_cmds, 1);

         // ----------------------------------------------------------------
         // T3. THE ORDER DOES NOT MATTER, demonstrated. The same transaction, configured
         //     backwards, produces the same command.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_CTRL, 8'h00);      // repeated START rather than a STOP
         wr(R_LEN,  8'h05);
         wr(R_ADDR, 8'h48);      // target 0x24, write direction
         wr(R_CMD,  8'hFF);      // the value written is irrelevant; the write is the event
         $display("T3  configured in the reverse order, the command is identical");
         ck_int("T3 address", cmd_addr, 7'h24);
         ck_bit("T3 direction is write", cmd_read, 1'b0);
         ck_int("T3 length", cmd_len, 5);
         ck_bit("T3 and it will NOT end with a STOP", cmd_stop, 1'b0);

         // ----------------------------------------------------------------
         // T4. THE WRITE BUFFER AUTO-INCREMENTS, so a payload is pushed with repeated
         //     stores to one address -- which is the shape a memcpy loop has.
         // ----------------------------------------------------------------
         do_reset;
         for (k = 0; k < NB; k = k + 1) wr(R_TX0, 8'h10 + k[7:0]);
         $display("T4  the write buffer auto-increments, so a payload is one store repeated");
         for (k = 0; k < NB; k = k + 1) begin
            @(negedge clk); tx_index = k[3:0]; #1;
            ck_int("T4 the controller sees the byte the host pushed", tx_data, 8'h10 + k);
         end

         // ----------------------------------------------------------------
         // T5. THE READ BUFFER IS FILLED BY THE CONTROLLER and drained by the host, also
         //     auto-incrementing.
         // ----------------------------------------------------------------
         do_reset;
         for (k = 0; k < 4; k = k + 1) begin
            @(negedge clk); rx_index = k[3:0]; rx_data = 8'hC0 + k[7:0]; rx_we = 1'b1;
            @(posedge clk); @(negedge clk); rx_we = 1'b0;
         end
         $display("T5  the read buffer is filled by the controller and drained by the host");
         for (k = 0; k < 4; k = k + 1) begin
            rd(R_RX0);
            ck_int("T5 the host reads the byte the controller stored", got, 8'hC0 + k);
         end

         // ----------------------------------------------------------------
         // T6. DONE AND OK ARE SEPARATE BITS. This is the distinction a register map most
         //     often collapses, and the case that proves it is a transaction that finished
         //     and failed: NACKed on its address.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h02); wr(R_CMD, 8'h01);
         @(negedge clk); txn_busy = 1'b1; step;
         rd(R_STATUS);
         ck_bit("T6 busy while it runs", got[2], 1'b1);
         ck_bit("T6 not done yet", got[0], 1'b0);
         finish_txn(1'b0, 6'b000001, 4'd0);   // address NACK, nothing transferred
         rd(R_STATUS);
         $display("T6  a transaction can be finished and not have succeeded");
         ck_bit("T6 done", got[0], 1'b1);
         ck_bit("T6 and NOT ok", got[1], 1'b0);
         ck_bit("T6 no longer busy", got[2], 1'b0);
         ck_int("T6 zero bytes moved", got[7:4], 0);
         rd(R_ERR);
         ck_int("T6 and the error code says which failure", got, 8'h01);

         // ----------------------------------------------------------------
         // T7. A SUCCESS LOOKS DIFFERENT IN EVERY FIELD, which is what makes the split
         //     worth the bit.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h04); wr(R_CMD, 8'h01);
         @(negedge clk); txn_busy = 1'b1; step;
         finish_txn(1'b1, 6'b000000, 4'd4);
         rd(R_STATUS);
         $display("T7  a success reports done, ok, and the full byte count");
         ck_bit("T7 done", got[0], 1'b1);
         ck_bit("T7 ok", got[1], 1'b1);
         ck_int("T7 four bytes moved", got[7:4], 4);
         rd(R_ERR); ck_int("T7 and no error code", got, 8'h00);

         // ----------------------------------------------------------------
         // T8. THE BYTE COUNT IS A SEPARATE QUESTION AGAIN. A transaction NACKed part way
         //     through moved SOME bytes, and neither `done` nor `ok` can say how many.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h06); wr(R_CMD, 8'h01);
         @(negedge clk); txn_busy = 1'b1; step;
         finish_txn(1'b0, 6'b000010, 4'd3);   // data NACK on the fourth byte
         rd(R_STATUS);
         $display("T8  a partial transfer reports how far it got, which no flag can");
         ck_bit("T8 done", got[0], 1'b1);
         ck_bit("T8 not ok", got[1], 1'b0);
         ck_int("T8 three of the six bytes moved", got[7:4], 3);
         rd(R_ERR); ck_int("T8 a data NACK, not an address NACK", got, 8'h02);

         // ----------------------------------------------------------------
         // T9. THE STATUS FIELDS DO NOT OVERLAP. The byte count is four bits high in the
         //     word, and a large count must not disturb the flags -- which the first
         //     version of this register DID, because it was built by OR-ing shifted masks.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h0F); wr(R_CMD, 8'h01);
         @(negedge clk); txn_busy = 1'b1; step;
         finish_txn(1'b1, 6'd0, 4'd15);       // the largest count the field can hold
         rd(R_STATUS);
         $display("T9  the largest byte count does not disturb the status flags");
         ck_int("T9 fifteen bytes", got[7:4], 15);
         ck_bit("T9 done is still correct", got[0], 1'b1);
         ck_bit("T9 ok is still correct", got[1], 1'b1);
         ck_bit("T9 busy is still correct", got[2], 1'b0);
         ck_int("T9 the whole word", got, 8'hF3);

         // ----------------------------------------------------------------
         // T10. A NEW COMMAND CLEARS THE PREVIOUS RESULT. Without this a driver that
         //      starts a transaction and immediately polls sees the LAST transaction's
         //      `done` and concludes this one finished instantly -- which is the most
         //      common bug written against a register map of this shape.
         // ----------------------------------------------------------------
         rd(R_STATUS);
         ck_bit("T10 the old result is still visible before the new command", got[0], 1'b1);
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h01); wr(R_CMD, 8'h01);
         rd(R_STATUS);
         $display("T10 a new command clears the previous result, so a poll cannot be fooled");
         ck_bit("T10 done was cleared", got[0], 1'b0);
         ck_bit("T10 ok was cleared", got[1], 1'b0);
         ck_int("T10 the byte count was cleared", got[7:4], 0);
         rd(R_ERR); ck_int("T10 and the error code", got, 8'h00);

         // ----------------------------------------------------------------
         // T11. A COMMAND WHILE BUSY IS REFUSED, not queued. Queueing would need a policy
         //      for what a second command means mid-transaction, and the honest minimum is
         //      to say no.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h02); wr(R_CMD, 8'h01);
         @(negedge clk); txn_busy = 1'b1; step;
         k = n_cmds;
         wr(R_CMD, 8'h01);
         wr(R_CMD, 8'h01);
         $display("T11 a command arriving while busy is refused rather than queued");
         ck_int("T11 no further commands were issued", n_cmds, k);
         @(negedge clk); txn_busy = 1'b0; step;
         wr(R_CMD, 8'h01);
         ck_int("T11 and one is accepted once it is free", n_cmds, k + 1);

         // ----------------------------------------------------------------
         // T12. The command register is write-only and reads as zero rather than as junk,
         //      and reset leaves the map in a defined state with nothing pending.
         // ----------------------------------------------------------------
         rd(R_CMD);
         $display("T12 the command register is write-only and reads as zero");
         ck_int("T12 reads as zero", got, 8'h00);
         do_reset;
         ck_int("T12 no commands after reset", n_cmds, 0);
         ck_bit("T12 no command pending", cmd_valid, 1'b0);
         rd(R_STATUS); ck_int("T12 status is clear", got, 8'h00);
         rd(R_ERR);    ck_int("T12 no errors", got, 8'h00);
         rd(R_CTRL);   ck_int("T12 and the default is to end with a STOP", got, 8'h01);

         if (errors == 0)
            $display("=== i2c_cmd_regs: ALL CHECKS PASSED ===");
         else
            $display("=== i2c_cmd_regs: %0d CHECK(S) FAILED ===", errors);
         $finish;
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_regs_tb.v — the same twelve tests in Verilog-2001
   `timescale 1ns/1ps
   // -----------------------------------------------------------------------------
   // i2c_cmd_regs_tb.sv
   // Independent oracle for i2c_cmd_regs.
   //
   // The bench is the HOST: it writes and reads registers over the on-chip bus and never
   // touches the block's internals. It also plays the transaction controller, so it can
   // return results the register map has to report faithfully -- including the case a
   // register map most often gets wrong, a transaction that finished and failed.
   // -----------------------------------------------------------------------------
   // (Verilog-2001 -- structurally identical to the SystemVerilog above.)
   module i2c_cmd_regs_tb;

      localparam integer NB = 8;
      localparam [3:0] R_ADDR = 4'h0, R_LEN = 4'h1, R_CTRL = 4'h2, R_TX0 = 4'h3,
                       R_RX0 = 4'h4, R_STATUS = 4'h5, R_ERR = 4'h6, R_CMD = 4'h7;

      reg clk = 1'b0, rst_n = 1'b0;
      reg [3:0] reg_addr = 4'h0;
      reg [7:0] reg_wdata = 8'h00;
      reg reg_we = 1'b0, reg_re = 1'b0;
      wire [7:0] reg_rdata;

      wire cmd_valid, cmd_read, cmd_stop;
      wire [6:0] cmd_addr;
      wire [3:0] cmd_len;
      wire [7:0] tx_data;
      wire [15:0] n_cmds;

      reg [3:0] tx_index = 4'd0, rx_index = 4'd0;
      reg [7:0] rx_data = 8'h00;
      reg rx_we = 1'b0;
      reg txn_done = 1'b0, txn_ok = 1'b0, txn_busy = 1'b0;
      reg [5:0] txn_err = 6'd0;
      reg [3:0] txn_bytes = 4'd0;

      i2c_cmd_regs #(.N_BUF(NB), .CNT_W(16)) dut (
         .clk(clk), .rst_n(rst_n),
         .reg_addr(reg_addr), .reg_wdata(reg_wdata), .reg_we(reg_we), .reg_re(reg_re),
         .reg_rdata(reg_rdata),
         .cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
         .cmd_len(cmd_len), .cmd_stop(cmd_stop),
         .tx_data(tx_data), .tx_index(tx_index),
         .rx_data(rx_data), .rx_index(rx_index), .rx_we(rx_we),
         .txn_done(txn_done), .txn_ok(txn_ok), .txn_err(txn_err),
         .txn_bytes(txn_bytes), .txn_busy(txn_busy),
         .commands_issued(n_cmds));

      always #5 clk = ~clk;

      integer errors = 0;
      integer k;
      reg [7:0] got;
      reg saw_cmd;

      // Catch the one-cycle command pulse, which a host polling registers could miss.
      always @(posedge clk) if (rst_n && cmd_valid) saw_cmd <= 1'b1;

      task step; begin @(posedge clk); @(negedge clk); end endtask

      task do_reset;
         begin
            @(negedge clk);
            rst_n = 1'b0; reg_we = 1'b0; reg_re = 1'b0;
            tx_index = 4'd0; rx_index = 4'd0; rx_data = 8'h00; rx_we = 1'b0;
            txn_done = 1'b0; txn_ok = 1'b0; txn_busy = 1'b0;
            txn_err = 6'd0; txn_bytes = 4'd0; saw_cmd = 1'b0;
            repeat (3) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            step;
         end
      endtask

      task wr (input [3:0] a, input [7:0] d);
         begin
            @(negedge clk); reg_addr = a; reg_wdata = d; reg_we = 1'b1;
            @(posedge clk); @(negedge clk); reg_we = 1'b0;
         end
      endtask

      task rd (input [3:0] a);
         begin
            @(negedge clk); reg_addr = a; reg_re = 1'b1;
            @(posedge clk); @(negedge clk); reg_re = 1'b0;
            got = reg_rdata;
         end
      endtask

      // The controller's answer, delivered the way the controller delivers it.
      task finish_txn (input ok, input [5:0] e, input [3:0] nb);
         begin
            @(negedge clk); txn_busy = 1'b0; txn_ok = ok; txn_err = e; txn_bytes = nb;
            txn_done = 1'b1;
            @(posedge clk); @(negedge clk); txn_done = 1'b0;
         end
      endtask

      task ck_int (input [200*8:1] what, input integer g, input integer e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s: got %0d (0x%0h) expected %0d (0x%0h)", what, g, g, e, e);
               errors = errors + 1;
            end
         end
      endtask

      task ck_bit (input [200*8:1] what, input g, input e);
         begin
            if (g !== e) begin
               $display("  FAIL %0s: got %0b expected %0b", what, g, e);
               errors = errors + 1;
            end
         end
      endtask

      initial begin
         $display("=== i2c_cmd_regs: the one block the specification says nothing about ===");

         // ----------------------------------------------------------------
         // T1. Registers hold what was written, and read back.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA1);      // target 0x50, read direction
         wr(R_LEN,  8'h03);
         wr(R_CTRL, 8'h01);
         rd(R_ADDR); ck_int("T1 the address register reads back", got, 8'hA1);
         rd(R_LEN);  ck_int("T1 the length register reads back", got, 8'h03);
         rd(R_CTRL); ck_int("T1 the control register reads back", got, 8'h01);
         $display("T1  the configuration registers hold what the host wrote");
         ck_bit("T1 and no command was started by any of them", saw_cmd, 1'b0);

         // ----------------------------------------------------------------
         // T2. THE ATOMIC START. Only the command register starts a transaction, so the
         //     host may write the others in any order -- and a compiler may reorder them.
         // ----------------------------------------------------------------
         wr(R_CMD, 8'h01);
         // One extra cycle before looking at `saw_cmd`. The command pulse is registered,
         // so it is high during the cycle AFTER the register write is latched -- and the
         // monitor that records it samples at the edge that sets it, so it sees the pulse
         // one edge later again. A host polling a status register has exactly this
         // problem, which is why `cmd_valid` is a handshake to the controller and not
         // something software is expected to catch.
         step;
         $display("T2  only the command register starts a transaction");
         ck_bit("T2 now a command was issued", saw_cmd, 1'b1);
         ck_int("T2 with the address that was configured", cmd_addr, 7'h50);
         ck_bit("T2 and the direction", cmd_read, 1'b1);
         ck_int("T2 and the length", cmd_len, 3);
         ck_bit("T2 and the STOP choice", cmd_stop, 1'b1);
         ck_int("T2 one command issued", n_cmds, 1);

         // ----------------------------------------------------------------
         // T3. THE ORDER DOES NOT MATTER, demonstrated. The same transaction, configured
         //     backwards, produces the same command.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_CTRL, 8'h00);      // repeated START rather than a STOP
         wr(R_LEN,  8'h05);
         wr(R_ADDR, 8'h48);      // target 0x24, write direction
         wr(R_CMD,  8'hFF);      // the value written is irrelevant; the write is the event
         $display("T3  configured in the reverse order, the command is identical");
         ck_int("T3 address", cmd_addr, 7'h24);
         ck_bit("T3 direction is write", cmd_read, 1'b0);
         ck_int("T3 length", cmd_len, 5);
         ck_bit("T3 and it will NOT end with a STOP", cmd_stop, 1'b0);

         // ----------------------------------------------------------------
         // T4. THE WRITE BUFFER AUTO-INCREMENTS, so a payload is pushed with repeated
         //     stores to one address -- which is the shape a memcpy loop has.
         // ----------------------------------------------------------------
         do_reset;
         for (k = 0; k < NB; k = k + 1) wr(R_TX0, 8'h10 + k[7:0]);
         $display("T4  the write buffer auto-increments, so a payload is one store repeated");
         for (k = 0; k < NB; k = k + 1) begin
            @(negedge clk); tx_index = k[3:0]; #1;
            ck_int("T4 the controller sees the byte the host pushed", tx_data, 8'h10 + k);
         end

         // ----------------------------------------------------------------
         // T5. THE READ BUFFER IS FILLED BY THE CONTROLLER and drained by the host, also
         //     auto-incrementing.
         // ----------------------------------------------------------------
         do_reset;
         for (k = 0; k < 4; k = k + 1) begin
            @(negedge clk); rx_index = k[3:0]; rx_data = 8'hC0 + k[7:0]; rx_we = 1'b1;
            @(posedge clk); @(negedge clk); rx_we = 1'b0;
         end
         $display("T5  the read buffer is filled by the controller and drained by the host");
         for (k = 0; k < 4; k = k + 1) begin
            rd(R_RX0);
            ck_int("T5 the host reads the byte the controller stored", got, 8'hC0 + k);
         end

         // ----------------------------------------------------------------
         // T6. DONE AND OK ARE SEPARATE BITS. This is the distinction a register map most
         //     often collapses, and the case that proves it is a transaction that finished
         //     and failed: NACKed on its address.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h02); wr(R_CMD, 8'h01);
         @(negedge clk); txn_busy = 1'b1; step;
         rd(R_STATUS);
         ck_bit("T6 busy while it runs", got[2], 1'b1);
         ck_bit("T6 not done yet", got[0], 1'b0);
         finish_txn(1'b0, 6'b000001, 4'd0);   // address NACK, nothing transferred
         rd(R_STATUS);
         $display("T6  a transaction can be finished and not have succeeded");
         ck_bit("T6 done", got[0], 1'b1);
         ck_bit("T6 and NOT ok", got[1], 1'b0);
         ck_bit("T6 no longer busy", got[2], 1'b0);
         ck_int("T6 zero bytes moved", got[7:4], 0);
         rd(R_ERR);
         ck_int("T6 and the error code says which failure", got, 8'h01);

         // ----------------------------------------------------------------
         // T7. A SUCCESS LOOKS DIFFERENT IN EVERY FIELD, which is what makes the split
         //     worth the bit.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h04); wr(R_CMD, 8'h01);
         @(negedge clk); txn_busy = 1'b1; step;
         finish_txn(1'b1, 6'b000000, 4'd4);
         rd(R_STATUS);
         $display("T7  a success reports done, ok, and the full byte count");
         ck_bit("T7 done", got[0], 1'b1);
         ck_bit("T7 ok", got[1], 1'b1);
         ck_int("T7 four bytes moved", got[7:4], 4);
         rd(R_ERR); ck_int("T7 and no error code", got, 8'h00);

         // ----------------------------------------------------------------
         // T8. THE BYTE COUNT IS A SEPARATE QUESTION AGAIN. A transaction NACKed part way
         //     through moved SOME bytes, and neither `done` nor `ok` can say how many.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h06); wr(R_CMD, 8'h01);
         @(negedge clk); txn_busy = 1'b1; step;
         finish_txn(1'b0, 6'b000010, 4'd3);   // data NACK on the fourth byte
         rd(R_STATUS);
         $display("T8  a partial transfer reports how far it got, which no flag can");
         ck_bit("T8 done", got[0], 1'b1);
         ck_bit("T8 not ok", got[1], 1'b0);
         ck_int("T8 three of the six bytes moved", got[7:4], 3);
         rd(R_ERR); ck_int("T8 a data NACK, not an address NACK", got, 8'h02);

         // ----------------------------------------------------------------
         // T9. THE STATUS FIELDS DO NOT OVERLAP. The byte count is four bits high in the
         //     word, and a large count must not disturb the flags -- which the first
         //     version of this register DID, because it was built by OR-ing shifted masks.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h0F); wr(R_CMD, 8'h01);
         @(negedge clk); txn_busy = 1'b1; step;
         finish_txn(1'b1, 6'd0, 4'd15);       // the largest count the field can hold
         rd(R_STATUS);
         $display("T9  the largest byte count does not disturb the status flags");
         ck_int("T9 fifteen bytes", got[7:4], 15);
         ck_bit("T9 done is still correct", got[0], 1'b1);
         ck_bit("T9 ok is still correct", got[1], 1'b1);
         ck_bit("T9 busy is still correct", got[2], 1'b0);
         ck_int("T9 the whole word", got, 8'hF3);

         // ----------------------------------------------------------------
         // T10. A NEW COMMAND CLEARS THE PREVIOUS RESULT. Without this a driver that
         //      starts a transaction and immediately polls sees the LAST transaction's
         //      `done` and concludes this one finished instantly -- which is the most
         //      common bug written against a register map of this shape.
         // ----------------------------------------------------------------
         rd(R_STATUS);
         ck_bit("T10 the old result is still visible before the new command", got[0], 1'b1);
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h01); wr(R_CMD, 8'h01);
         rd(R_STATUS);
         $display("T10 a new command clears the previous result, so a poll cannot be fooled");
         ck_bit("T10 done was cleared", got[0], 1'b0);
         ck_bit("T10 ok was cleared", got[1], 1'b0);
         ck_int("T10 the byte count was cleared", got[7:4], 0);
         rd(R_ERR); ck_int("T10 and the error code", got, 8'h00);

         // ----------------------------------------------------------------
         // T11. A COMMAND WHILE BUSY IS REFUSED, not queued. Queueing would need a policy
         //      for what a second command means mid-transaction, and the honest minimum is
         //      to say no.
         // ----------------------------------------------------------------
         do_reset;
         wr(R_ADDR, 8'hA0); wr(R_LEN, 8'h02); wr(R_CMD, 8'h01);
         @(negedge clk); txn_busy = 1'b1; step;
         k = n_cmds;
         wr(R_CMD, 8'h01);
         wr(R_CMD, 8'h01);
         $display("T11 a command arriving while busy is refused rather than queued");
         ck_int("T11 no further commands were issued", n_cmds, k);
         @(negedge clk); txn_busy = 1'b0; step;
         wr(R_CMD, 8'h01);
         ck_int("T11 and one is accepted once it is free", n_cmds, k + 1);

         // ----------------------------------------------------------------
         // T12. The command register is write-only and reads as zero rather than as junk,
         //      and reset leaves the map in a defined state with nothing pending.
         // ----------------------------------------------------------------
         rd(R_CMD);
         $display("T12 the command register is write-only and reads as zero");
         ck_int("T12 reads as zero", got, 8'h00);
         do_reset;
         ck_int("T12 no commands after reset", n_cmds, 0);
         ck_bit("T12 no command pending", cmd_valid, 1'b0);
         rd(R_STATUS); ck_int("T12 status is clear", got, 8'h00);
         rd(R_ERR);    ck_int("T12 no errors", got, 8'h00);
         rd(R_CTRL);   ck_int("T12 and the default is to end with a STOP", got, 8'h01);

         if (errors == 0)
            $display("=== i2c_cmd_regs: ALL CHECKS PASSED ===");
         else
            $display("=== i2c_cmd_regs: %0d CHECK(S) FAILED ===", errors);
         $finish;
      end

   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_regs_tb.vhd — the same twelve tests in VHDL
   -- ---------------------------------------------------------------------------
   -- i2c_cmd_regs_tb.vhd
   -- Independent oracle for i2c_cmd_regs. Behavioural twin of the SV and Verilog benches.
   --
   -- The bench is the HOST: it writes and reads registers over the on-chip bus and never touches
   -- the block's internals. It also plays the transaction controller, so it can return results
   -- the register map has to report faithfully -- including the case a register map most often
   -- gets wrong, a transaction that finished and failed.
   -- ---------------------------------------------------------------------------

   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_cmd_regs_tb is
   end entity i2c_cmd_regs_tb;

   architecture sim of i2c_cmd_regs_tb is

      constant TCLK : time := 10 ns;
      constant NB   : integer := 8;
      constant IDX_ADDR   : std_logic_vector(3 downto 0) := x"0";
      constant IDX_LEN    : std_logic_vector(3 downto 0) := x"1";
      constant IDX_CTRL   : std_logic_vector(3 downto 0) := x"2";
      constant IDX_TX0    : std_logic_vector(3 downto 0) := x"3";
      constant IDX_RX0    : std_logic_vector(3 downto 0) := x"4";
      constant IDX_STATUS : std_logic_vector(3 downto 0) := x"5";
      constant IDX_ERR    : std_logic_vector(3 downto 0) := x"6";
      constant IDX_CMD    : std_logic_vector(3 downto 0) := x"7";

      signal clk, rst_n : std_logic := '0';
      signal raddr : std_logic_vector(3 downto 0) := (others => '0');
      signal rwdata : std_logic_vector(7 downto 0) := (others => '0');
      signal rwe, rre : std_logic := '0';
      signal rrdata : std_logic_vector(7 downto 0);

      signal cmd_valid, cmd_read, cmd_stop : std_logic;
      signal cmd_addr : std_logic_vector(6 downto 0);
      signal cmd_len : unsigned(3 downto 0);
      signal tx_data : std_logic_vector(7 downto 0);
      signal n_cmds : unsigned(15 downto 0);

      signal tx_index, rx_index : unsigned(3 downto 0) := (others => '0');
      signal rx_data : std_logic_vector(7 downto 0) := (others => '0');
      signal rx_we : std_logic := '0';
      signal txn_done, txn_ok, txn_busy : std_logic := '0';
      signal txn_err : std_logic_vector(5 downto 0) := (others => '0');
      signal txn_bytes : unsigned(3 downto 0) := (others => '0');

      signal saw_cmd : std_logic := '0';
      signal halt : boolean := false;

   begin

      dut : entity work.i2c_cmd_regs
         generic map (N_BUF => NB, CNT_W => 16)
         port map (clk => clk, rst_n => rst_n,
            reg_addr => raddr, reg_wdata => rwdata, reg_we => rwe, reg_re => rre,
            reg_rdata => rrdata,
            cmd_valid => cmd_valid, cmd_addr => cmd_addr, cmd_read => cmd_read,
            cmd_len => cmd_len, cmd_stop => cmd_stop,
            tx_data => tx_data, tx_index => tx_index,
            rx_data => rx_data, rx_index => rx_index, rx_we => rx_we,
            txn_done => txn_done, txn_ok => txn_ok, txn_err => txn_err,
            txn_bytes => txn_bytes, txn_busy => txn_busy,
            commands_issued => n_cmds);

      clkgen : process
      begin
         while not halt loop
            clk <= '0'; wait for TCLK/2;
            clk <= '1'; wait for TCLK/2;
         end loop;
         wait;
      end process;

      -- Catch the one-cycle command pulse, which a host polling registers could miss.
      catch : process (clk, rst_n)
      begin
         if rst_n = '0' then
            saw_cmd <= '0';
         elsif rising_edge(clk) then
            if cmd_valid = '1' then saw_cmd <= '1'; end if;
         end if;
      end process;

      stim : process
         variable err : integer := 0;
         variable k : integer;
         variable got : std_logic_vector(7 downto 0);

         procedure ck_int (what : string; g : integer; e : integer) is
         begin
            if g /= e then
               report "  FAIL " & what & ": got " & integer'image(g)
                      & " expected " & integer'image(e) severity note;
               err := err + 1;
            end if;
         end procedure;

         procedure ck_bit (what : string; g : std_logic; e : std_logic) is
         begin
            if g /= e then
               report "  FAIL " & what & ": got " & std_logic'image(g)
                      & " expected " & std_logic'image(e) severity note;
               err := err + 1;
            end if;
         end procedure;

         procedure step is
         begin
            wait until rising_edge(clk); wait until falling_edge(clk);
         end procedure;

         procedure do_reset is
         begin
            wait until falling_edge(clk);
            rst_n <= '0'; rwe <= '0'; rre <= '0';
            tx_index <= (others => '0'); rx_index <= (others => '0');
            rx_data <= (others => '0'); rx_we <= '0';
            txn_done <= '0'; txn_ok <= '0'; txn_busy <= '0';
            txn_err <= (others => '0'); txn_bytes <= (others => '0');
            for i in 0 to 2 loop wait until rising_edge(clk); end loop;
            wait until falling_edge(clk); rst_n <= '1';
            step;
         end procedure;

         procedure wr (a : std_logic_vector(3 downto 0); d : integer) is
         begin
            wait until falling_edge(clk);
            raddr <= a; rwdata <= std_logic_vector(to_unsigned(d, 8)); rwe <= '1';
            wait until rising_edge(clk); wait until falling_edge(clk); rwe <= '0';
         end procedure;

         procedure rd (a : std_logic_vector(3 downto 0)) is
         begin
            wait until falling_edge(clk);
            raddr <= a; rre <= '1';
            wait until rising_edge(clk); wait until falling_edge(clk); rre <= '0';
            got := rrdata;
         end procedure;

         procedure finish_txn (ok : std_logic; e : integer; nb : integer) is
         begin
            wait until falling_edge(clk);
            txn_busy <= '0'; txn_ok <= ok;
            txn_err <= std_logic_vector(to_unsigned(e, 6));
            txn_bytes <= to_unsigned(nb, 4);
            txn_done <= '1';
            wait until rising_edge(clk); wait until falling_edge(clk); txn_done <= '0';
         end procedure;

      begin
         report "=== i2c_cmd_regs: the one block the specification says nothing about ==="
                severity note;

         -- T1. Registers hold what was written, and read back.
         do_reset;
         wr(IDX_ADDR, 16#A1#);        -- target 0x50, read direction
         wr(IDX_LEN,  16#03#);
         wr(IDX_CTRL, 16#01#);
         rd(IDX_ADDR); ck_int("T1 the address register reads back",
                              to_integer(unsigned(got)), 16#A1#);
         rd(IDX_LEN);  ck_int("T1 the length register reads back",
                              to_integer(unsigned(got)), 16#03#);
         rd(IDX_CTRL); ck_int("T1 the control register reads back",
                              to_integer(unsigned(got)), 16#01#);
         report "T1  the configuration registers hold what the host wrote" severity note;
         ck_bit("T1 and no command was started by any of them", saw_cmd, '0');

         -- T2. THE ATOMIC START: only the command register starts a transaction.
         wr(IDX_CMD, 16#01#);
         -- One extra cycle before looking at `saw_cmd`. The command pulse is registered, so it
         -- is high during the cycle AFTER the register write is latched -- and the monitor that
         -- records it samples at the edge that sets it.
         step;
         report "T2  only the command register starts a transaction" severity note;
         ck_bit("T2 now a command was issued", saw_cmd, '1');
         ck_int("T2 with the address that was configured",
                to_integer(unsigned(cmd_addr)), 16#50#);
         ck_bit("T2 and the direction", cmd_read, '1');
         ck_int("T2 and the length", to_integer(cmd_len), 3);
         ck_bit("T2 and the STOP choice", cmd_stop, '1');
         ck_int("T2 one command issued", to_integer(n_cmds), 1);

         -- T3. THE ORDER DOES NOT MATTER: the same transaction, configured backwards.
         do_reset;
         wr(IDX_CTRL, 16#00#);        -- repeated START rather than a STOP
         wr(IDX_LEN,  16#05#);
         wr(IDX_ADDR, 16#48#);        -- target 0x24, write direction
         wr(IDX_CMD,  16#FF#);        -- the value is irrelevant; the write is the event
         report "T3  configured in the reverse order, the command is identical" severity note;
         ck_int("T3 address", to_integer(unsigned(cmd_addr)), 16#24#);
         ck_bit("T3 direction is write", cmd_read, '0');
         ck_int("T3 length", to_integer(cmd_len), 5);
         ck_bit("T3 and it will NOT end with a STOP", cmd_stop, '0');

         -- T4. THE WRITE BUFFER AUTO-INCREMENTS, so a payload is one store repeated.
         do_reset;
         for j in 0 to NB-1 loop wr(IDX_TX0, 16#10# + j); end loop;
         report "T4  the write buffer auto-increments, so a payload is one store repeated"
                severity note;
         for j in 0 to NB-1 loop
            wait until falling_edge(clk);
            tx_index <= to_unsigned(j, 4);
            wait for 1 ns;
            ck_int("T4 the controller sees the byte the host pushed",
                   to_integer(unsigned(tx_data)), 16#10# + j);
         end loop;

         -- T5. THE READ BUFFER is filled by the controller and drained by the host.
         do_reset;
         for j in 0 to 3 loop
            wait until falling_edge(clk);
            rx_index <= to_unsigned(j, 4);
            rx_data <= std_logic_vector(to_unsigned(16#C0# + j, 8));
            rx_we <= '1';
            wait until rising_edge(clk); wait until falling_edge(clk); rx_we <= '0';
         end loop;
         report "T5  the read buffer is filled by the controller and drained by the host"
                severity note;
         for j in 0 to 3 loop
            rd(IDX_RX0);
            ck_int("T5 the host reads the byte the controller stored",
                   to_integer(unsigned(got)), 16#C0# + j);
         end loop;

         -- T6. DONE AND OK ARE SEPARATE BITS -- the case that proves it is a transaction that
         --     finished and failed.
         do_reset;
         wr(IDX_ADDR, 16#A0#); wr(IDX_LEN, 16#02#); wr(IDX_CMD, 16#01#);
         wait until falling_edge(clk); txn_busy <= '1'; step;
         rd(IDX_STATUS);
         ck_bit("T6 busy while it runs", got(2), '1');
         ck_bit("T6 not done yet", got(0), '0');
         finish_txn('0', 1, 0);       -- address NACK, nothing transferred
         rd(IDX_STATUS);
         report "T6  a transaction can be finished and not have succeeded" severity note;
         ck_bit("T6 done", got(0), '1');
         ck_bit("T6 and NOT ok", got(1), '0');
         ck_bit("T6 no longer busy", got(2), '0');
         ck_int("T6 zero bytes moved", to_integer(unsigned(got(7 downto 4))), 0);
         rd(IDX_ERR);
         ck_int("T6 and the error code says which failure",
                to_integer(unsigned(got)), 16#01#);

         -- T7. A SUCCESS looks different in every field.
         do_reset;
         wr(IDX_ADDR, 16#A0#); wr(IDX_LEN, 16#04#); wr(IDX_CMD, 16#01#);
         wait until falling_edge(clk); txn_busy <= '1'; step;
         finish_txn('1', 0, 4);
         rd(IDX_STATUS);
         report "T7  a success reports done, ok, and the full byte count" severity note;
         ck_bit("T7 done", got(0), '1');
         ck_bit("T7 ok", got(1), '1');
         ck_int("T7 four bytes moved", to_integer(unsigned(got(7 downto 4))), 4);
         rd(IDX_ERR); ck_int("T7 and no error code", to_integer(unsigned(got)), 0);

         -- T8. THE BYTE COUNT IS A SEPARATE QUESTION AGAIN: a partial transfer moved SOME
         --     bytes, and neither `done` nor `ok` can say how many.
         do_reset;
         wr(IDX_ADDR, 16#A0#); wr(IDX_LEN, 16#06#); wr(IDX_CMD, 16#01#);
         wait until falling_edge(clk); txn_busy <= '1'; step;
         finish_txn('0', 2, 3);       -- data NACK on the fourth byte
         rd(IDX_STATUS);
         report "T8  a partial transfer reports how far it got, which no flag can"
                severity note;
         ck_bit("T8 done", got(0), '1');
         ck_bit("T8 not ok", got(1), '0');
         ck_int("T8 three of the six bytes moved",
                to_integer(unsigned(got(7 downto 4))), 3);
         rd(IDX_ERR); ck_int("T8 a data NACK, not an address NACK",
                             to_integer(unsigned(got)), 16#02#);

         -- T9. THE STATUS FIELDS DO NOT OVERLAP. The first version of this register let the
         --     byte count corrupt the flags, because it was built by OR-ing shifted masks.
         do_reset;
         wr(IDX_ADDR, 16#A0#); wr(IDX_LEN, 16#0F#); wr(IDX_CMD, 16#01#);
         wait until falling_edge(clk); txn_busy <= '1'; step;
         finish_txn('1', 0, 15);      -- the largest count the field can hold
         rd(IDX_STATUS);
         report "T9  the largest byte count does not disturb the status flags" severity note;
         ck_int("T9 fifteen bytes", to_integer(unsigned(got(7 downto 4))), 15);
         ck_bit("T9 done is still correct", got(0), '1');
         ck_bit("T9 ok is still correct", got(1), '1');
         ck_bit("T9 busy is still correct", got(2), '0');
         ck_int("T9 the whole word", to_integer(unsigned(got)), 16#F3#);

         -- T10. A NEW COMMAND CLEARS THE PREVIOUS RESULT. Without this a driver that starts a
         --      transaction and immediately polls sees the LAST transaction's `done`.
         rd(IDX_STATUS);
         ck_bit("T10 the old result is still visible before the new command", got(0), '1');
         wr(IDX_ADDR, 16#A0#); wr(IDX_LEN, 16#01#); wr(IDX_CMD, 16#01#);
         rd(IDX_STATUS);
         report "T10 a new command clears the previous result, so a poll cannot be fooled"
                severity note;
         ck_bit("T10 done was cleared", got(0), '0');
         ck_bit("T10 ok was cleared", got(1), '0');
         ck_int("T10 the byte count was cleared",
                to_integer(unsigned(got(7 downto 4))), 0);
         rd(IDX_ERR); ck_int("T10 and the error code", to_integer(unsigned(got)), 0);

         -- T11. A COMMAND WHILE BUSY IS REFUSED, not queued.
         do_reset;
         wr(IDX_ADDR, 16#A0#); wr(IDX_LEN, 16#02#); wr(IDX_CMD, 16#01#);
         wait until falling_edge(clk); txn_busy <= '1'; step;
         k := to_integer(n_cmds);
         wr(IDX_CMD, 16#01#);
         wr(IDX_CMD, 16#01#);
         report "T11 a command arriving while busy is refused rather than queued"
                severity note;
         ck_int("T11 no further commands were issued", to_integer(n_cmds), k);
         wait until falling_edge(clk); txn_busy <= '0'; step;
         wr(IDX_CMD, 16#01#);
         ck_int("T11 and one is accepted once it is free", to_integer(n_cmds), k + 1);

         -- T12. The command register is write-only and reads as zero, and reset leaves the map
         --      in a defined state with nothing pending.
         rd(IDX_CMD);
         report "T12 the command register is write-only and reads as zero" severity note;
         ck_int("T12 reads as zero", to_integer(unsigned(got)), 0);
         do_reset;
         ck_int("T12 no commands after reset", to_integer(n_cmds), 0);
         ck_bit("T12 no command pending", cmd_valid, '0');
         rd(IDX_STATUS); ck_int("T12 status is clear", to_integer(unsigned(got)), 0);
         rd(IDX_ERR);    ck_int("T12 no errors", to_integer(unsigned(got)), 0);
         rd(IDX_CTRL);   ck_int("T12 and the default is to end with a STOP",
                                to_integer(unsigned(got)), 1);

         if err = 0 then
            report "=== i2c_cmd_regs: ALL CHECKS PASSED ===" severity note;
         else
            report "=== i2c_cmd_regs: " & integer'image(err)
                   & " CHECK(S) FAILED ===" severity note;
         end if;
         halt <= true;
         wait;
      end process;

   end architecture sim;

6b. Execution

DesignSystemVerilogVerilog-2001VHDLFinish
i2c_cmd_regsPASS 12/12PASS 12/12PASS 12/122050 ns, all three

7. Mutation Testing — Including Two That Did Not Die

Seven defects were injected. Five were killed immediately; two survived, and both survivors turned out to be findings rather than testbench weaknesses — which is only knowable by investigating each one instead of writing a stronger test reflexively.

#Injected defectResult
M1any register write starts a transaction, not only CMDSURVIVED → invalid mutant
M2a command is accepted while busy instead of refusedKILLED (3 failures)
M3a new command no longer clears the previous resultKILLED (3)
M4collapse done and ok — report ok as doneKILLED (3)
M5shift the byte-count field so it overlaps the flagsKILLED (5)
M6the write pointer never wrapsSURVIVED → equivalent at the tested parameter
M7CMD reads back junk instead of zeroKILLED (2)

M1 — an unreachable mutant, not an untested property

The mutation added R_LEN to the command register's case item, intending to make a length write also start a transfer. It survived, and the reason is a Verilog semantic rather than a gap in the bench:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
R_LEN:  r_len <= reg_wdata;     <-- line 138, matches first
...
R_CMD, R_LEN: begin ... end     <-- line 148, never reached for R_LEN

In a case, the first matching item wins. R_LEN already has its own branch eight lines earlier, so the mutated branch is dead code for that address and the mutant cannot change behaviour at all.

The property is nonetheless tested. Rewriting the mutation to be reachable — putting the start action inside R_LEN's own winning branch — kills it at once:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
R_LEN: begin r_len <= reg_wdata; cmd_valid <= 1'b1; end   ->  KILLED

So T2 does discriminate; the first mutation simply never took effect. This is worth showing because the two outcomes are indistinguishable from the score alone, and treating an unreachable mutant as a coverage hole leads to writing a test for a property that is already covered.

M6 — provably equivalent at N_BUF = 8, and a real bug at N_BUF = 5

Removing the explicit wrap survived because the buffer is indexed with the low three bits only:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
txbuf[tx_wptr[2:0]]        <-- 3 bits, always
tx_wptr <= (tx_wptr + 1 >= N_BUF) ? 0 : tx_wptr + 1

At N_BUF = 8 the explicit wrap and the natural three-bit truncation agree forever, so no stimulus can separate them. Enumerated:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
N_BUF=8  original = 0 1 2 3 4 5 6 7 0 1 2 3
N_BUF=8  mutant   = 0 1 2 3 4 5 6 7 0 1 2 3   -> INDISTINGUISHABLE

N_BUF=5  original = 0 1 2 3 4 0 1 2 3 4 0 1
N_BUF=5  mutant   = 0 1 2 3 4 5 6 7 0 1 2 3   -> DIFFERS

So this is a genuine equivalent mutant at the tested parameter, and a real defect at any N_BUF that is not a power of two. Two conclusions follow, and the second is the more useful one.

The explicit wrap is dead code at the default configuration. It is defensive, it costs a comparator, and it earns nothing unless someone parameterises the buffer to a non-power-of-two depth.

The parameter space is part of the test space. A block verified only at N_BUF = 8 has not been verified for N_BUF = 5, and no amount of stimulus at the default will reveal it. That is a different class of gap from a missing test — it is a missing configuration — and it is the reason Chapter 17.13 treats parameterisation as a verification obligation rather than a convenience.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
valid non-equivalent mutants: 6      killed: 6      survived: 0
documented invalid mutant:    1      (M1, unreachable case item)
documented equivalent mutant: 1      (M6, at N_BUF = 8)
baseline PASS before injection; PASS after restore.

8. Verification Connection — Where a Register Model Fits, and Where It Stops

Azvya Education Pvt. Ltd.VLSI Mentor
cmd_regs_ral.sv — the adapter boundary for a master's own registers
   // This block is a register map, so uvm_reg models it almost perfectly -- and the
   // "almost" is the whole point. The map's STRUCTURE is exactly what a register
   // model is for:
   //
   //   ADDR, LEN, CTRL      -> uvm_reg with RW fields, predicted trivially
   //   STATUS, ERR          -> RO fields, updated by the DUT rather than by a write
   //   CMD                  -> WO field, and a write to it has a SIDE EFFECT
   //
   // The side effect is where the model stops. In RAL terms, writing CMD does not
   // merely change a field's value: it launches an activity on a different bus that
   // takes hundreds of microseconds, may fail, and ends by changing STATUS, ERR and
   // the RX buffer. No uvm_reg field expresses that.
   //
   // Consequences for a real environment:
   //
   //  1. CMD must be modelled with a side-effect callback, not as a plain WO field.
   //     A predictor that treats it as storage will predict a stale STATUS forever.
   //
   //  2. STATUS's `done` bit is updated by the DUT asynchronously to any register
   //     access, so mirror_value goes stale the instant a transaction completes.
   //     The sequence must read it, never trust the mirror -- uvm_reg::read() with
   //     UVM_FRONTDOOR, never get_mirrored_value().
   //
   //  3. The interesting properties of this block are SEQUENCES OF ACCESSES, not
   //     field values: "a new command clears the previous result" (T10) is a
   //     statement about two transactions, and there is no field for it. It belongs
   //     in a sequence library with a scoreboard, above the register layer.
   //
   // So: use RAL for the map, and do not expect it to express the handshake. The
   // post-then-poll shape of section 4 is a protocol between software and hardware,
   // and protocols live in sequences.

9. FPGA and ASIC Implications

On an FPGA, the two buffers are the only interesting synthesis question. At N_BUF = 8 they infer as distributed RAM or flops; push N_BUF to 256 for a page-write EEPROM and the tools will want block RAM, which changes the read path from combinational to registered — and tx_data is currently a combinational read of txbuf[tx_index], so that conversion adds a cycle that Chapter 17.7's byte engine would have to absorb. The parameter therefore changes the interface timing, not just the area, which is the second appearance of §7's lesson.

On an ASIC, this block is where the firmware/hardware contract is written down, and the reset values are part of it: CTRL resets to end-with-STOP, so a master that is reset mid-transaction comes up configured for the safe case rather than for an open bus. The register bus here is deliberately a trivial address/data/strobe interface rather than APB or AHB, because wrapping it is an integration task and Chapter 17.13 keeps that seam explicit. A real SoC adds an interrupt from done, and the level-versus-pulse choice there is the same argument as §2's: a level can be polled, a pulse can be missed.

10. Debugging — The Driver That Read a Result From the Previous Transaction

Symptom

A driver performs a sequence of single-byte register reads from a sensor in a loop. It works correctly at a 10 ms polling interval. Compiled with optimisation raised from -O0 to -O2, roughly one read in twenty returns the value the previous read returned, and the STATUS register reports success every time. Lowering the optimisation level makes the problem disappear.

Root Cause

A stale done bit, and a driver that polled without first observing the transaction start. The hardware revision in use does not clear done on a new command, so between the CMD write and the master actually asserting busy there is a window -- tens of nanoseconds -- in which STATUS still describes the previous transaction. The driver read inside that window. Optimisation did not introduce the bug; it shortened the instruction sequence enough to enter a window that had always been there. The 10 ms polling interval was irrelevant, because the race is between the CMD write and the first STATUS read, not between transactions.

Fix
On this hardware, the driver must wait for busy to assert before treating done as meaningful -- poll for busy set, then for done set. That is the only correct sequence against a master that does not clear its status on command. The durable fix is the hardware one, and it is test T10: clearing done, ok, err and the byte count on the command write removes the window entirely and makes the naive driver correct. Note which way the dependency runs -- the hardware change lets software be simple, and the software workaround is needed only because the hardware omitted a four-line clear.

Three generalisations.

Optimisation did not cause it. The race existed at every optimisation level; -O2 merely closed the instruction gap enough to fall into it. A bug that appears with optimisation is almost never a compiler bug and almost always a timing window that was always open.

"Read done" is not a complete protocol. The complete one is "observe the transaction start, then read done" — and whether software must do that depends on a hardware decision it cannot see. This is what makes T10 worth its four lines of RTL: it moves an obligation from every driver to one register file.

The plausible data is what hid it. Returning the previous read's value looks like a sensor glitch, not a status race, because the value is a real reading from a real register. A wrong value that looks wrong gets found in an afternoon.

11. Common Misconceptions

"The specification defines a master's register interface." It defines none of it. This is the one block in Module 17 with no normative content. §1.

"A start bit is enough to launch a transfer." Not unless the map can also say do not release the bus. Without that, no combined transaction — and therefore no correct register-map device read. §1.

"done implies success." It implies completion. A NACKed address is done and failed, and inferring success from the absence of a known error breaks when a new error code is added. §2.

"The byte count equals the length requested." Only on success. A NACK part way through moves fewer bytes, and without a reported count the driver cannot know what the target's map now contains. §2.

"Any register write can safely start the transfer." A compiler may reorder the configuration stores, so a non-atomic trigger can launch a transaction before the length is programmed. §3.

"A second command should be queued." Queueing needs a policy for what a second command means mid-transaction. Refusing is a decision software can handle; silently dropping is a bug. §3.

"An I²C access can be a blocking register read." It takes hundreds of microseconds and a target may stretch indefinitely. Blocking would freeze the CPU and could deadlock. §4.

"A write-only register can read as anything." Then a read-modify-write of a neighbouring field, or a driver that dumps the map for debug, sees junk. Reading as zero is a specified behaviour. §6a, T12.

"A surviving mutant means the testbench is weak." Two survived here; one was unreachable and one was provably equivalent at the tested parameter. Both were findings, and writing a new test for either would have been wasted work. §7.

"Verifying at the default parameter verifies the block." N_BUF = 8 hides a real defect that appears at N_BUF = 5, because eight is a power of two and the index truncates. §7.

"uvm_reg models this block." It models the map. Writing CMD launches an activity on another bus — a side effect no field expresses — and the block's interesting properties are sequences of accesses rather than field values. §8.

12. Reason It Through

A master's register map has ADDR, LEN, TX, RX, STATUS and a START bit. Which Module 16 device can it not read correctly, and why?

Any register-map device — which is nearly all of them. A correct read is a combined transaction: write the pointer, repeated START, then read. With no way to say "end this phase without a STOP", the master must release the bus between the two phases, and any other master may then take it and move the pointer. §1.

Why are done and ok two bits rather than one plus an error code?

Because a driver written against "no error means success" encodes the error set that existed when it was compiled. Add a code later and old drivers classify the new failure as success. An explicit ok bit cannot be invalidated by extending the taxonomy. §2.

Software writes LEN after CMD by mistake. On a map with an atomic start, what happens?

The transaction runs with the old length, which is wrong but deterministic and debuggable. On a map without an atomic start, writing LEN could launch a second transaction — a far worse failure, and one that appears only under whatever instruction ordering the compiler chose that day. §3.

Why can a master's host interface not simply stall the on-chip bus until the transfer completes?

Because the two buses differ in access time by about five orders of magnitude, and a target may stretch the clock indefinitely. Stalling would freeze the CPU for hundreds of microseconds and deadlock on a stuck bus. §4.

A mutation survives. What must you establish before writing a stronger test?

Whether the mutant is reachable, and whether it is behaviourally equivalent under the configuration tested. M1 was unreachable because an earlier case item shadowed it; M6 was equivalent because eight is a power of two. Neither needed a new test, and both looked identical to a coverage hole from the score alone. §7.

The write pointer's explicit wrap is dead code at N_BUF = 8. Should it be removed?

No — but the reason is not "defensive coding is good". It is load-bearing at any non-power-of-two N_BUF, so removing it narrows the block's valid parameter range without recording that it has been narrowed. The correct response is a test at a non-power-of-two depth, which converts dead code into covered code. §7.

Why must a UVM sequence read STATUS through the front door rather than trusting the mirror?

Because the DUT updates done asynchronously to any register access, so the mirror is stale from the instant a transaction completes. The mirror reflects what the model last saw, and the interesting event happened on a different bus. §8.

13. Understanding Check

14. Summary

This is the only block in Module 17 with no specification behind it, and that makes it a derivation: the register map must be deduced from what the protocol lets a master do, because a map that cannot express a capability makes it unreachable from software.

Four things software must be able to say — address and direction, byte count, STOP-or-repeated-START, and the payload — and the third is the one whose omission silently removes combined transactions, and with them every correct register-map device read.

Four things the master must report — finished, succeeded, which failure, and how many bytes actually moved. done and ok are separate bits because a driver that infers success from the absence of a known error breaks when the taxonomy grows.

The byte count is a third question again. A transaction NACKed part way moved fewer bytes than requested, and without that count software cannot know what state the target's register map is in.

Only the command register starts a transfer. That single decision removes a race with any compiler that reorders the configuration stores — and the race is invisible in review, because it is a property of generated code rather than of source order.

A command arriving while busy is refused rather than queued, because queueing needs a policy for what a second command means mid-transaction, and refusal is the honest minimum.

An on-chip register access and an I²C transfer differ in duration by five orders of magnitude, so the interface is necessarily post-then-poll. Blocking would stall a CPU for hundreds of microseconds and deadlock on a stretched clock.

Seven mutants, five killed, and both survivors were findings rather than gaps. M1 was unreachable — an earlier case item shadowed it, and the reachable rewrite died at once. M6 was provably equivalent at N_BUF = 8, and a real defect at N_BUF = 5.

So the parameter space is part of the test space. A block verified only at its default configuration has not been verified at the others, and no amount of stimulus at the default will show it.

A stale status bit is a hardware omission that every driver then has to work around. Clearing the result on the command write is four lines of RTL that make the naive polling loop correct — and its absence produces a bug that appears when a compiler shortens the gap between two instructions.

15. What Comes Next

Software can now express a transaction and read its outcome. Nothing yet turns that into edges on a wire.

Chapter 17.3 builds the first block that touches copper: the SCL timing generator. It is where Table 10's minimums become counts of system-clock cycles, and where two arithmetic mistakes are waiting — a period budget that forgets the rise and fall times produces a clock that is legal on paper and too fast on a real bus, and rounding a phase count down produces a phase shorter than a specified minimum.

It also introduces the two instants the entire datapath is built around: the drive point inside the low phase and the sample point inside the high phase. The generator exposes them as strobes, and every block after it contains no timing of its own.

Continue learning

Related tutorials