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 specify | Because |
|---|---|
| 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 go | a 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 reads | there 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 bus | I²C bus | |
|---|---|---|
| Duration of an access | one clock cycle | hundreds of microseconds |
| Can it fail? | no | yes, in several ways |
| Who may delay it? | nobody | any 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
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
// -----------------------------------------------------------------------------
// 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 // -----------------------------------------------------------------------------
// 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 -- ---------------------------------------------------------------------------
-- 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.
| # | Test | Property |
|---|---|---|
| T1 | registers hold what was written | the baseline |
| T2 | the atomic start | only CMD starts a transaction |
| T3 | the order does not matter | same transfer, configured in three orders |
| T4 | the write buffer auto-increments | a payload pushed to one address |
| T5 | the read buffer fills and drains | the controller writes, the host reads |
| T6 | done and ok are separate bits | §2's central claim, executed |
| T7 | a success looks different in every field | what makes the split useful |
| T8 | the byte count is a third question | a NACK part way through reports the partial count |
| T9 | the status fields do not overlap | the count is high, the flags are low |
| T10 | a new command clears the previous result | no stale done on the next poll |
| T11 | a command while busy is refused | not queued, not silently dropped |
| T12 | CMD reads as zero, not junk | write-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.
`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 `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 -- ---------------------------------------------------------------------------
-- 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
| Design | SystemVerilog | Verilog-2001 | VHDL | Finish |
|---|---|---|---|---|
i2c_cmd_regs | PASS 12/12 | PASS 12/12 | PASS 12/12 | 2050 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 defect | Result |
|---|---|---|
| M1 | any register write starts a transaction, not only CMD | SURVIVED → invalid mutant |
| M2 | a command is accepted while busy instead of refused | KILLED (3 failures) |
| M3 | a new command no longer clears the previous result | KILLED (3) |
| M4 | collapse done and ok — report ok as done | KILLED (3) |
| M5 | shift the byte-count field so it overlaps the flags | KILLED (5) |
| M6 | the write pointer never wraps | SURVIVED → equivalent at the tested parameter |
| M7 | CMD reads back junk instead of zero | KILLED (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:
R_LEN: r_len <= reg_wdata; <-- line 138, matches first
...
R_CMD, R_LEN: begin ... end <-- line 148, never reached for R_LENIn 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:
R_LEN: begin r_len <= reg_wdata; cmd_valid <= 1'b1; end -> KILLEDSo 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:
txbuf[tx_wptr[2:0]] <-- 3 bits, always
tx_wptr <= (tx_wptr + 1 >= N_BUF) ? 0 : tx_wptr + 1At N_BUF = 8 the explicit wrap and the natural three-bit truncation agree forever, so no stimulus can separate them. Enumerated:
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 -> DIFFERSSo 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.
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
// 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
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.
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.
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
- Related topic
Register Maps — From Datasheet to Transactions
The specification describes the register-map pattern in one sentence and then hands every detail of it to the device designer. Explains the six questions a datasheet must answer before a driver can be written, why each has at least two plausible answers that exist in real parts, and how choosing wrong fails in ways that look like bus problems.
- Related topic
Decomposing an I²C Master — From Requirements to Architecture
An I²C master is not one state machine, and the reason is structural rather than stylistic: the protocol imposes four independent time bases that change on four unrelated events. Derives the block structure from the normative obligations, establishes the wired-AND bus model every later chapter is written against, and shows why the framing generator cannot live inside the bit engine.
- Related topic
The SCL Timing Generator — Phases, Strobes and the Readback Rule
Where Table 10's microseconds become counts of system-clock cycles. Derives the period budget that must include rise and fall time, shows why rounding down is always illegal and rounding up always legal, and builds a generator that leaves its low phase only when the line actually reads back high — which implements clock stretching and clock synchronization with no extra logic.
- Related topic
SDA Open-Drain Control and Line Ownership in RTL
The transmitted bit is the inverse of the drive enable, and that inversion belongs in exactly one place. Makes SDA ownership an explicit signal rather than an implication of the state encoding, shows why an ownership conflict must be reported rather than resolved silently, and derives arbitration detection from three signals the block already has — including the intent bit without which every read looks like a lost arbitration.
