I²C · Module 17
Deriving the Master FSM — The State Machine Designed Last
There is no state machine in the finished master's top level. Every state already exists inside a block that needed it, and integration is wiring plus three decisions. Shows what integration verification catches that no block bench can, and proves one mutant equivalent by demonstrating the contention it changes never occurs.
Chapter 17.1 claimed the FSM should be designed last, and that a master written FSM-first gets rewritten. This chapter is where that claim is settled.
1. The Claim: There Is No State Machine in This File
Every state in the finished master already exists inside a block that needed it. The top level is wiring plus three decisions.
That is not an accident of this particular design. It is what happens when the decomposition follows the protocol's four time bases rather than its vocabulary:
| Time base | Advances on | Lives in |
|---|---|---|
| TIME | the divider tick | i2c_scl_gen's phase |
| BIT | the SCL edge, read back | i2c_bit_engine |
| BYTE | the byte boundary | i2c_byte_engine's slot index |
| TRANSACTION | the host command | i2c_txn_ctrl's phase |
Those four change on unrelated events. A single FSM holding all four must take the product of their states and gets four sets of transitions out of each one — and the counters that inevitably get bolted on to make it tractable are the other three machines, admitted late and without their invariants.
2. The Three Decisions This File Actually Makes
Decision 1 — who owns SDA
Four blocks want it: the framer, the bit engine, the byte engine's acknowledge (which drives through the bit engine), and recovery. Chapter 17.4's arbiter resolves them by priority and reports any overlap as the design error it is.
owner 0 the framer -- highest: a START must pre-empt a data bit
owner 1 the bit engine
owner 2 (unused here -- the byte engine drives through the bit engine)
owner 3 recoveryDecision 2 — who owns SCL
Three contributors, wired-AND locally exactly as the bus would do it:
scl_drive_low = f_scl_low | g_scl_low | rec_scl_lowThe handover overlaps — Chapter 17.8's controller asserts scl_yield and the generator's enable in the same cycle — so the line never rises in between. Chapter 17.8 §3 explained why the direction of that overlap matters: if SCL rises between owners, the framer's next act is to pull SDA low, and that is a START rather than the intended STOP.
Decision 3 — when recovery may run
Never during a transfer. Chapter 17.11 §10 debugged exactly this: both lines are low most of the time while clocking, so a diagnosis taken mid-transfer reads a transmitted zero as a stuck line and clocks nine pulses into a live byte.
.start_recovery(start_recovery && !in_transfer)One AND gate, and it is the difference between a recovery feature and a corruption source.
And the pins are three signals per line
drive_low out, the line in, and the pad's output enable derived from the first. An inout in synthesizable RTL can be neither synthesized nor simulated against a second driver — Chapter 17.1 §4's three-signal form, now at the top level where it meets the pad.
3. The Whole Master
4. What Stays Outside
Naming what is not at this level is what prevents state explosion:
| Stays inside a block | Because |
|---|---|
| timing counters | the generator owns every Table 10 number |
| the shift register and slot index | the byte engine owns bit position |
| the synchronisers | they belong at the pad boundary |
| host data storage | the register file owns the buffers |
| the pad's output enable | derived from drive_low, not a separate decision |
A top level that reached into any of these would be re-implementing a block it already instantiates.
5. The Master, in Three Languages
// -----------------------------------------------------------------------------
// i2c_master.sv
// The whole master, and the FSM that was designed LAST.
//
// THE CLAIM OF THIS CHAPTER. There is no state machine in this file. Every state in the
// finished master already exists inside a block that needed it, and the top level is
// wiring plus three decisions. That is not an accident of this particular design: it is
// what happens when the decomposition follows the protocol's four TIME BASES rather than
// its vocabulary.
//
// the divider tick -> i2c_scl_gen's phase (Chapter 17.3)
// the SCL edge, read back -> i2c_bit_engine (Chapter 17.6)
// the byte boundary -> i2c_byte_engine's slot (Chapter 17.7)
// the host command -> i2c_txn_ctrl's phase (Chapter 17.8)
//
// Those four change on unrelated events. A single FSM holding all four has to take the
// product of their states and gets four sets of transitions out of each one, and the
// counters that inevitably get bolted on to make it tractable ARE the other three
// machines, admitted late and without their invariants.
//
// THE THREE DECISIONS THIS FILE ACTUALLY MAKES:
//
// 1. WHO OWNS SDA. Four blocks want it -- the framer, the bit engine, the byte
// engine's acknowledge, and recovery -- and Chapter 17.4's arbiter resolves them by
// priority while reporting any overlap as the design error it is.
//
// 2. WHO OWNS SCL. The framer and the generator, handed over with a one-cycle overlap
// in both directions so the line never rises in between. Chapter 17.8's controller
// sequences the handover because it is the only block that knows which is needed.
//
// 3. WHEN RECOVERY MAY RUN. Never during a transfer, because Chapter 17.11's stuck-line
// detector cannot distinguish a held line from a clocked one while a transfer is in
// progress -- both lines are low most of the time.
//
// AND THE PINS ARE THREE SIGNALS PER LINE, not one. `drive_low` out, the line in, and the
// pad's output enable derived from the first. An `inout` in synthesizable RTL cannot be
// driven by two blocks in simulation without resolution hazards and cannot be synthesized
// at all; Chapter 17.4 §I makes the argument.
// -----------------------------------------------------------------------------
module i2c_master #(
parameter int N_LOW = 8,
parameter int N_HIGH = 4,
parameter int N_SU = 2,
parameter int N_SAMP = 2,
parameter int N_HD_STA = 3,
parameter int N_SU_STA = 3,
parameter int N_SU_STO = 3,
parameter int N_BUF = 3,
parameter int STRETCH_LIMIT = 0,
parameter int N_PULSES = 9,
parameter int N_BYTES = 8,
parameter int CNT_W = 16
) (
input logic clk,
input logic rst_n,
// ---- the host register bus ----------------------------------------------
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,
// ---- the bus pins: three signals per line -------------------------------
output logic scl_drive_low,
input logic scl_in,
output logic sda_drive_low,
input logic sda_in,
// ---- recovery, which the host must ask for explicitly -------------------
input logic start_recovery,
output logic recovering,
output logic recovered,
output logic escalate,
// ---- observability ------------------------------------------------------
output logic txn_busy,
output logic txn_done,
output logic txn_ok,
output logic [5:0] err,
output logic arb_lost,
output logic stretch_seen,
output logic [CNT_W-1:0] transactions,
output logic [CNT_W-1:0] stretch_cycles,
output logic [CNT_W-1:0] arb_losses,
output logic [CNT_W-1:0] sda_conflicts,
output logic [3:0] txn_state
);
// ---- Chapter 17.3: the clock -------------------------------------------
logic g_scl_low, drive_point, sample_point, g_rise, g_fall, g_stretch;
logic [CNT_W-1:0] g_scyc, g_bits;
logic [1:0] g_phase;
logic gen_enable, gen_idle_low;
i2c_scl_gen #(.N_LOW(N_LOW), .N_HIGH(N_HIGH), .N_SU(N_SU), .N_SAMP(N_SAMP),
.CNT_W(CNT_W)) u_scl (
.clk(clk), .rst_n(rst_n), .enable(gen_enable), .idle_low(gen_idle_low),
.scl_in(scl_in), .scl_drive_low(g_scl_low),
.drive_point(drive_point), .sample_point(sample_point),
.scl_rising(g_rise), .scl_falling(g_fall),
.stretching(g_stretch), .stretch_cycles(g_scyc),
.bits_generated(g_bits), .phase(g_phase));
// ---- Chapter 17.5: framing ---------------------------------------------
logic f_sda_req, f_sda_bit, f_scl_low, f_busy, f_done, f_bus_free, f_started, f_sw;
logic [CNT_W-1:0] n_sta, n_rs, n_sto;
logic [3:0] f_state;
logic do_start, do_restart, do_stop, scl_yield;
i2c_framer #(.N_HD_STA(N_HD_STA), .N_SU_STA(N_SU_STA), .N_SU_STO(N_SU_STO),
.N_BUF(N_BUF), .N_SU_DAT(N_SU), .CNT_W(CNT_W)) u_fr (
.clk(clk), .rst_n(rst_n),
.do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
.scl_in(scl_in), .sda_in(sda_in), .scl_yield(scl_yield),
.sda_req(f_sda_req), .sda_bit(f_sda_bit), .scl_drive_low(f_scl_low),
.busy(f_busy), .done(f_done), .bus_free(f_bus_free), .started(f_started),
.stretch_wait(f_sw), .starts(n_sta), .restarts(n_rs), .stops(n_sto),
.state(f_state));
// ---- Chapters 17.6 and 17.7: the datapath ------------------------------
logic b_sda_req, b_sda_bit, b_driving, b_busy, b_ack, b_ackv, b_done;
logic [7:0] b_rx;
logic [3:0] b_bidx;
logic [CNT_W-1:0] b_bytes, b_acks, b_nacks;
logic byte_go, byte_dir_write, byte_ack_send;
logic [7:0] byte_tx;
i2c_byte_engine #(.CNT_W(CNT_W)) u_by (
.clk(clk), .rst_n(rst_n),
.drive_point(drive_point), .sample_point(sample_point),
.go(byte_go), .dir_write(byte_dir_write), .tx_byte(byte_tx),
.ack_to_send(byte_ack_send),
.sda_in(sda_in), .scl_high(scl_in), .abort(arb_lost),
.sda_req(b_sda_req), .sda_bit(b_sda_bit),
.rx_byte(b_rx), .ack(b_ack), .ack_valid(b_ackv), .byte_done(b_done),
.busy(b_busy), .bit_index(b_bidx), .driving(b_driving),
.bytes_done(b_bytes), .acks(b_acks), .nacks(b_nacks));
// ---- Chapter 17.10: feedback. Declared here, ahead of the error manager that
// consumes `stretch_timeout`, because Verilog binds ports at elaboration and a
// wire used before its declaration is an elaboration error rather than a warning.
logic scl_held, stretch_to;
logic [CNT_W-1:0] fb_ev, fb_long;
// ---- Chapter 17.11: recovery, which also wants the pins ----------------
logic rec_scl_low, rec_sda_req, rec_sda_bit;
logic err_valid;
logic [CNT_W-1:0] rec_pulses;
logic [2:0] rec_state;
logic addr_nack, data_nack;
// DECISION 3: recovery may not run during a transfer. The stuck-line detector cannot
// tell a held line from a clocked one while a transfer is open, because both lines are
// low most of the time.
wire in_transfer = f_started || b_busy || txn_busy;
i2c_err_mgr #(.N_PULSES(N_PULSES), .N_HALF(N_LOW), .CNT_W(CNT_W)) u_err (
.clk(clk), .rst_n(rst_n),
.addr_nack(addr_nack), .data_nack(data_nack), .arb_lost(arb_lost),
.stretch_timeout(stretch_to), .in_transfer(in_transfer),
.scl_in(scl_in), .sda_in(sda_in),
.start_recovery(start_recovery && !in_transfer), .clear(txn_done),
.recovering(recovering), .rec_scl_low(rec_scl_low),
.rec_sda_req(rec_sda_req), .rec_sda_bit(rec_sda_bit),
.err(err), .err_valid(err_valid), .pulses_issued(rec_pulses),
.recovered(recovered), .escalate(escalate), .state(rec_state));
// ---- DECISION 1: who owns SDA ------------------------------------------
// Owner 0 the framer, 1 the bit engine, 2 unused here (the byte engine drives through
// the bit engine), 3 recovery. Lowest index wins, and any overlap is reported.
wire [3:0] sda_req_v = {rec_sda_req, 1'b0, b_sda_req, f_sda_req};
wire [3:0] sda_bit_v = {rec_sda_bit, 1'b0, b_sda_bit, f_sda_bit};
logic [3:0] grant;
logic sda_owned, sda_tx, arb_now, arb_lost_w;
i2c_sda_ctrl #(.N_OWNER(4), .CNT_W(CNT_W)) u_sda (
.clk(clk), .rst_n(rst_n), .req(sda_req_v), .bit_val(sda_bit_v),
.sda_in(sda_in), .scl_in(scl_in), .tx_active(b_driving),
.sda_drive_low(sda_drive_low),
.grant(grant), .owned(sda_owned), .tx_bit(sda_tx),
.owner_conflict(), .conflicts(sda_conflicts),
.arb_loss_now(arb_now), .arb_lost(arb_lost_w), .arb_losses(arb_losses),
.arb_clear(txn_done));
assign arb_lost = arb_lost_w;
// ---- DECISION 2: who owns SCL ------------------------------------------
// Three contributors, wired-AND locally exactly as the bus would. The handover
// OVERLAPS -- Chapter 17.8's controller asserts `scl_yield` and the generator's enable
// in the same cycle -- so the line never rises between owners.
assign scl_drive_low = f_scl_low | g_scl_low | rec_scl_low;
i2c_bus_feedback #(.STRETCH_LIMIT(STRETCH_LIMIT), .CNT_W(CNT_W)) u_fb (
.clk(clk), .rst_n(rst_n),
.scl_release(~scl_drive_low), .sda_release(~sda_drive_low),
.tx_active(b_driving),
.scl_in(scl_in), .sda_in(sda_in),
.scl_held(scl_held), .stretch_seen(stretch_seen), .stretch_cycles(stretch_cycles),
.stretch_events(fb_ev), .longest_stretch(fb_long), .stretch_timeout(stretch_to),
.arb_loss_now(), .arb_lost(), .arb_losses(), .clear(txn_done));
// ---- Chapter 17.2: the host interface ----------------------------------
logic cmd_valid, cmd_read, cmd_stop;
logic [6:0] cmd_addr;
logic [3:0] cmd_len, tx_index, rx_index;
logic [7:0] tx_data, rx_data;
logic rx_we;
logic [3:0] txn_bytes;
logic [CNT_W-1:0] n_cmds;
i2c_cmd_regs #(.N_BUF(N_BYTES), .CNT_W(CNT_W)) u_reg (
.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(err),
.txn_bytes(txn_bytes), .txn_busy(txn_busy),
.commands_issued(n_cmds));
// ---- Chapter 17.8: the phase sequencer --------------------------------
i2c_txn_ctrl #(.CNT_W(CNT_W)) u_txn (
.clk(clk), .rst_n(rst_n),
.cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
.cmd_len(cmd_len), .cmd_stop(cmd_stop),
.do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
.scl_yield(scl_yield), .frame_done(f_done), .frame_busy(f_busy),
.bus_free(f_bus_free), .frame_started(f_started),
.gen_enable(gen_enable), .gen_idle_low(gen_idle_low),
.byte_go(byte_go), .byte_dir_write(byte_dir_write), .byte_tx(byte_tx),
.byte_ack_send(byte_ack_send), .byte_done(b_done), .byte_busy(b_busy),
.byte_ack(b_ack), .byte_rx(b_rx),
.tx_data(tx_data), .tx_index(tx_index),
.rx_data(rx_data), .rx_index(rx_index), .rx_we(rx_we),
.arb_lost(arb_lost),
.txn_done(txn_done), .txn_ok(txn_ok), .txn_err(),
.txn_bytes(txn_bytes), .txn_busy(txn_busy),
.addr_nack(addr_nack), .data_nack(data_nack),
.state(txn_state), .transactions(transactions));
endmodule // -----------------------------------------------------------------------------
// i2c_master.sv
// The whole master, and the FSM that was designed LAST.
//
// THE CLAIM OF THIS CHAPTER. There is no state machine in this file. Every state in the
// finished master already exists inside a block that needed it, and the top level is
// wiring plus three decisions. That is not an accident of this particular design: it is
// what happens when the decomposition follows the protocol's four TIME BASES rather than
// its vocabulary.
//
// the divider tick -> i2c_scl_gen's phase (Chapter 17.3)
// the SCL edge, read back -> i2c_bit_engine (Chapter 17.6)
// the byte boundary -> i2c_byte_engine's slot (Chapter 17.7)
// the host command -> i2c_txn_ctrl's phase (Chapter 17.8)
//
// Those four change on unrelated events. A single FSM holding all four has to take the
// product of their states and gets four sets of transitions out of each one, and the
// counters that inevitably get bolted on to make it tractable ARE the other three
// machines, admitted late and without their invariants.
//
// THE THREE DECISIONS THIS FILE ACTUALLY MAKES:
//
// 1. WHO OWNS SDA. Four blocks want it -- the framer, the bit engine, the byte
// engine's acknowledge, and recovery -- and Chapter 17.4's arbiter resolves them by
// priority while reporting any overlap as the design error it is.
//
// 2. WHO OWNS SCL. The framer and the generator, handed over with a one-cycle overlap
// in both directions so the line never rises in between. Chapter 17.8's controller
// sequences the handover because it is the only block that knows which is needed.
//
// 3. WHEN RECOVERY MAY RUN. Never during a transfer, because Chapter 17.11's stuck-line
// detector cannot distinguish a held line from a clocked one while a transfer is in
// progress -- both lines are low most of the time.
//
// AND THE PINS ARE THREE SIGNALS PER LINE, not one. `drive_low` out, the line in, and the
// pad's output enable derived from the first. An `inout` in synthesizable RTL cannot be
// driven by two blocks in simulation without resolution hazards and cannot be synthesized
// at all; Chapter 17.4 §I makes the argument.
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_master #(
parameter N_LOW = 8,
parameter N_HIGH = 4,
parameter N_SU = 2,
parameter N_SAMP = 2,
parameter N_HD_STA = 3,
parameter N_SU_STA = 3,
parameter N_SU_STO = 3,
parameter N_BUF = 3,
parameter STRETCH_LIMIT = 0,
parameter N_PULSES = 9,
parameter N_BYTES = 8,
parameter CNT_W = 16
) (
input wire clk,
input wire rst_n,
// ---- the host register bus ----------------------------------------------
input wire [3:0] reg_addr,
input wire [7:0] reg_wdata,
input wire reg_we,
input wire reg_re,
output wire [7:0] reg_rdata,
// ---- the bus pins: three signals per line -------------------------------
output wire scl_drive_low,
input wire scl_in,
output wire sda_drive_low,
input wire sda_in,
// ---- recovery, which the host must ask for explicitly -------------------
input wire start_recovery,
output wire recovering,
output wire recovered,
output wire escalate,
// ---- observability ------------------------------------------------------
output wire txn_busy,
output wire txn_done,
output wire txn_ok,
output wire [5:0] err,
output wire arb_lost,
output wire stretch_seen,
output wire [CNT_W-1:0] transactions,
output wire [CNT_W-1:0] stretch_cycles,
output wire [CNT_W-1:0] arb_losses,
output wire [CNT_W-1:0] sda_conflicts,
output wire [3:0] txn_state
);
// ---- Chapter 17.3: the clock -------------------------------------------
wire g_scl_low, drive_point, sample_point, g_rise, g_fall, g_stretch;
wire [CNT_W-1:0] g_scyc, g_bits;
wire [1:0] g_phase;
wire gen_enable, gen_idle_low;
i2c_scl_gen #(.N_LOW(N_LOW), .N_HIGH(N_HIGH), .N_SU(N_SU), .N_SAMP(N_SAMP),
.CNT_W(CNT_W)) u_scl (
.clk(clk), .rst_n(rst_n), .enable(gen_enable), .idle_low(gen_idle_low),
.scl_in(scl_in), .scl_drive_low(g_scl_low),
.drive_point(drive_point), .sample_point(sample_point),
.scl_rising(g_rise), .scl_falling(g_fall),
.stretching(g_stretch), .stretch_cycles(g_scyc),
.bits_generated(g_bits), .phase(g_phase));
// ---- Chapter 17.5: framing ---------------------------------------------
wire f_sda_req, f_sda_bit, f_scl_low, f_busy, f_done, f_bus_free, f_started, f_sw;
wire [CNT_W-1:0] n_sta, n_rs, n_sto;
wire [3:0] f_state;
wire do_start, do_restart, do_stop, scl_yield;
i2c_framer #(.N_HD_STA(N_HD_STA), .N_SU_STA(N_SU_STA), .N_SU_STO(N_SU_STO),
.N_BUF(N_BUF), .N_SU_DAT(N_SU), .CNT_W(CNT_W)) u_fr (
.clk(clk), .rst_n(rst_n),
.do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
.scl_in(scl_in), .sda_in(sda_in), .scl_yield(scl_yield),
.sda_req(f_sda_req), .sda_bit(f_sda_bit), .scl_drive_low(f_scl_low),
.busy(f_busy), .done(f_done), .bus_free(f_bus_free), .started(f_started),
.stretch_wait(f_sw), .starts(n_sta), .restarts(n_rs), .stops(n_sto),
.state(f_state));
// ---- Chapters 17.6 and 17.7: the datapath ------------------------------
wire b_sda_req, b_sda_bit, b_driving, b_busy, b_ack, b_ackv, b_done;
wire [7:0] b_rx;
wire [3:0] b_bidx;
wire [CNT_W-1:0] b_bytes, b_acks, b_nacks;
wire byte_go, byte_dir_write, byte_ack_send;
wire [7:0] byte_tx;
i2c_byte_engine #(.CNT_W(CNT_W)) u_by (
.clk(clk), .rst_n(rst_n),
.drive_point(drive_point), .sample_point(sample_point),
.go(byte_go), .dir_write(byte_dir_write), .tx_byte(byte_tx),
.ack_to_send(byte_ack_send),
.sda_in(sda_in), .scl_high(scl_in), .abort(arb_lost),
.sda_req(b_sda_req), .sda_bit(b_sda_bit),
.rx_byte(b_rx), .ack(b_ack), .ack_valid(b_ackv), .byte_done(b_done),
.busy(b_busy), .bit_index(b_bidx), .driving(b_driving),
.bytes_done(b_bytes), .acks(b_acks), .nacks(b_nacks));
// ---- Chapter 17.10: feedback. Declared here, ahead of the error manager that
// consumes `stretch_timeout`, because Verilog binds ports at elaboration and a
// wire used before its declaration is an elaboration error rather than a warning.
wire scl_held, stretch_to;
wire [CNT_W-1:0] fb_ev, fb_long;
// ---- Chapter 17.11: recovery, which also wants the pins ----------------
wire rec_scl_low, rec_sda_req, rec_sda_bit;
wire err_valid;
wire [CNT_W-1:0] rec_pulses;
wire [2:0] rec_state;
wire addr_nack, data_nack;
// DECISION 3: recovery may not run during a transfer. The stuck-line detector cannot
// tell a held line from a clocked one while a transfer is open, because both lines are
// low most of the time.
wire in_transfer = f_started || b_busy || txn_busy;
i2c_err_mgr #(.N_PULSES(N_PULSES), .N_HALF(N_LOW), .CNT_W(CNT_W)) u_err (
.clk(clk), .rst_n(rst_n),
.addr_nack(addr_nack), .data_nack(data_nack), .arb_lost(arb_lost),
.stretch_timeout(stretch_to), .in_transfer(in_transfer),
.scl_in(scl_in), .sda_in(sda_in),
.start_recovery(start_recovery && !in_transfer), .clear(txn_done),
.recovering(recovering), .rec_scl_low(rec_scl_low),
.rec_sda_req(rec_sda_req), .rec_sda_bit(rec_sda_bit),
.err(err), .err_valid(err_valid), .pulses_issued(rec_pulses),
.recovered(recovered), .escalate(escalate), .state(rec_state));
// ---- DECISION 1: who owns SDA ------------------------------------------
// Owner 0 the framer, 1 the bit engine, 2 unused here (the byte engine drives through
// the bit engine), 3 recovery. Lowest index wins, and any overlap is reported.
wire [3:0] sda_req_v = {rec_sda_req, 1'b0, b_sda_req, f_sda_req};
wire [3:0] sda_bit_v = {rec_sda_bit, 1'b0, b_sda_bit, f_sda_bit};
wire [3:0] grant;
wire sda_owned, sda_tx, arb_now, arb_lost_w;
i2c_sda_ctrl #(.N_OWNER(4), .CNT_W(CNT_W)) u_sda (
.clk(clk), .rst_n(rst_n), .req(sda_req_v), .bit_val(sda_bit_v),
.sda_in(sda_in), .scl_in(scl_in), .tx_active(b_driving),
.sda_drive_low(sda_drive_low),
.grant(grant), .owned(sda_owned), .tx_bit(sda_tx),
.owner_conflict(), .conflicts(sda_conflicts),
.arb_loss_now(arb_now), .arb_lost(arb_lost_w), .arb_losses(arb_losses),
.arb_clear(txn_done));
assign arb_lost = arb_lost_w;
// ---- DECISION 2: who owns SCL ------------------------------------------
// Three contributors, wired-AND locally exactly as the bus would. The handover
// OVERLAPS -- Chapter 17.8's controller asserts `scl_yield` and the generator's enable
// in the same cycle -- so the line never rises between owners.
assign scl_drive_low = f_scl_low | g_scl_low | rec_scl_low;
i2c_bus_feedback #(.STRETCH_LIMIT(STRETCH_LIMIT), .CNT_W(CNT_W)) u_fb (
.clk(clk), .rst_n(rst_n),
.scl_release(~scl_drive_low), .sda_release(~sda_drive_low),
.tx_active(b_driving),
.scl_in(scl_in), .sda_in(sda_in),
.scl_held(scl_held), .stretch_seen(stretch_seen), .stretch_cycles(stretch_cycles),
.stretch_events(fb_ev), .longest_stretch(fb_long), .stretch_timeout(stretch_to),
.arb_loss_now(), .arb_lost(), .arb_losses(), .clear(txn_done));
// ---- Chapter 17.2: the host interface ----------------------------------
wire cmd_valid, cmd_read, cmd_stop;
wire [6:0] cmd_addr;
wire [3:0] cmd_len, tx_index, rx_index;
wire [7:0] tx_data, rx_data;
wire rx_we;
wire [3:0] txn_bytes;
wire [CNT_W-1:0] n_cmds;
i2c_cmd_regs #(.N_BUF(N_BYTES), .CNT_W(CNT_W)) u_reg (
.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(err),
.txn_bytes(txn_bytes), .txn_busy(txn_busy),
.commands_issued(n_cmds));
// ---- Chapter 17.8: the phase sequencer --------------------------------
i2c_txn_ctrl #(.CNT_W(CNT_W)) u_txn (
.clk(clk), .rst_n(rst_n),
.cmd_valid(cmd_valid), .cmd_addr(cmd_addr), .cmd_read(cmd_read),
.cmd_len(cmd_len), .cmd_stop(cmd_stop),
.do_start(do_start), .do_restart(do_restart), .do_stop(do_stop),
.scl_yield(scl_yield), .frame_done(f_done), .frame_busy(f_busy),
.bus_free(f_bus_free), .frame_started(f_started),
.gen_enable(gen_enable), .gen_idle_low(gen_idle_low),
.byte_go(byte_go), .byte_dir_write(byte_dir_write), .byte_tx(byte_tx),
.byte_ack_send(byte_ack_send), .byte_done(b_done), .byte_busy(b_busy),
.byte_ack(b_ack), .byte_rx(b_rx),
.tx_data(tx_data), .tx_index(tx_index),
.rx_data(rx_data), .rx_index(rx_index), .rx_we(rx_we),
.arb_lost(arb_lost),
.txn_done(txn_done), .txn_ok(txn_ok), .txn_err(),
.txn_bytes(txn_bytes), .txn_busy(txn_busy),
.addr_nack(addr_nack), .data_nack(data_nack),
.state(txn_state), .transactions(transactions));
endmodule -- ---------------------------------------------------------------------------
-- i2c_master.vhd
-- The whole master, and the FSM that was designed LAST.
-- Behavioural twin of i2c_master.sv / .v.
--
-- THE CLAIM OF THIS CHAPTER. There is no state machine in this file. Every state in the
-- finished master already exists inside a block that needed it, and the top level is wiring
-- plus three decisions. That is what happens when the decomposition follows the protocol's four
-- TIME BASES rather than its vocabulary:
--
-- the divider tick -> i2c_scl_gen's phase (Chapter 17.3)
-- the SCL edge, read back -> i2c_bit_engine (Chapter 17.6)
-- the byte boundary -> i2c_byte_engine's slot (Chapter 17.7)
-- the host command -> i2c_txn_ctrl's phase (Chapter 17.8)
--
-- Those four change on unrelated events. A single FSM holding all four takes the product of
-- their states and gets four sets of transitions out of each one, and the counters that
-- inevitably get bolted on to make it tractable ARE the other three machines, admitted late and
-- without their invariants.
--
-- THE THREE DECISIONS THIS FILE ACTUALLY MAKES:
-- 1. WHO OWNS SDA -- four blocks want it, resolved by priority with overlap reported.
-- 2. WHO OWNS SCL -- the framer and the generator, handed over with a one-cycle overlap in
-- both directions so the line never rises in between.
-- 3. WHEN RECOVERY MAY RUN -- never during a transfer, because the stuck-line detector
-- cannot distinguish a held line from a clocked one while a transfer is in progress.
--
-- AND THE PINS ARE THREE SIGNALS PER LINE, not one. In VHDL an `inout std_logic` would work in
-- simulation because std_logic is resolved -- and that is exactly the trap: it would also
-- resolve two accidental drivers to 'X' with no error, and it does not synthesise. The
-- drive-low / line-in / pad form says what the hardware is.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_master is
generic (
N_LOW : integer := 8;
N_HIGH : integer := 4;
N_SU : integer := 2;
N_SAMP : integer := 2;
N_HD_STA : integer := 3;
N_SU_STA : integer := 3;
N_SU_STO : integer := 3;
N_BUF : integer := 3;
STRETCH_LIMIT : integer := 0;
N_PULSES : integer := 9;
N_BYTES : 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);
scl_drive_low : out std_logic;
scl_in : in std_logic;
sda_drive_low : out std_logic;
sda_in : in std_logic;
start_recovery : in std_logic;
recovering : out std_logic;
recovered : out std_logic;
escalate : out std_logic;
txn_busy : out std_logic;
txn_done : out std_logic;
txn_ok : out std_logic;
err : out std_logic_vector(5 downto 0);
arb_lost : out std_logic;
stretch_seen : out std_logic;
transactions : out unsigned(CNT_W-1 downto 0);
stretch_cycles : out unsigned(CNT_W-1 downto 0);
arb_losses : out unsigned(CNT_W-1 downto 0);
sda_conflicts : out unsigned(CNT_W-1 downto 0);
txn_state : out unsigned(3 downto 0)
);
end entity i2c_master;
architecture rtl of i2c_master is
-- Chapter 17.3
signal g_scl_low, drive_point, sample_point, g_rise, g_fall, g_stretch : std_logic;
signal g_scyc, g_bits : unsigned(CNT_W-1 downto 0);
signal g_phase : unsigned(1 downto 0);
signal gen_enable, gen_idle_low : std_logic;
-- Chapter 17.5
signal f_sda_req, f_sda_bit, f_scl_low : std_logic;
signal f_busy, f_done, f_bus_free, f_started, f_sw : std_logic;
signal n_sta, n_rs, n_sto : unsigned(CNT_W-1 downto 0);
signal f_state : unsigned(3 downto 0);
signal do_start, do_restart, do_stop, scl_yield : std_logic;
-- Chapters 17.6 and 17.7
signal b_sda_req, b_sda_bit, b_driving, b_busy, b_ack, b_ackv, b_done : std_logic;
signal b_rx : std_logic_vector(7 downto 0);
signal b_bidx : unsigned(3 downto 0);
signal b_bytes, b_acks, b_nacks : unsigned(CNT_W-1 downto 0);
signal byte_go, byte_dir_write, byte_ack_send : std_logic;
signal byte_tx : std_logic_vector(7 downto 0);
-- Chapter 17.10, declared ahead of the error manager that consumes stretch_to
signal scl_held, stretch_to : std_logic;
signal fb_ev, fb_long : unsigned(CNT_W-1 downto 0);
-- Chapter 17.11
signal rec_scl_low, rec_sda_req, rec_sda_bit, err_valid : std_logic;
signal rec_pulses : unsigned(CNT_W-1 downto 0);
signal rec_state : unsigned(2 downto 0);
signal addr_nack, data_nack : std_logic;
signal in_transfer : std_logic;
-- the SDA arbiter
signal sda_req_v, sda_bit_v, grant : std_logic_vector(3 downto 0);
signal sda_owned, sda_tx, arb_now, arb_lost_w : std_logic;
-- Chapter 17.2
signal cmd_valid, cmd_read, cmd_stop : std_logic;
signal cmd_addr : std_logic_vector(6 downto 0);
signal cmd_len, tx_index, rx_index, txn_bytes : unsigned(3 downto 0);
signal tx_data, rx_data : std_logic_vector(7 downto 0);
signal rx_we : std_logic;
signal n_cmds : unsigned(CNT_W-1 downto 0);
signal busy_i, done_i, ok_i : std_logic;
signal err_i : std_logic_vector(5 downto 0);
begin
txn_busy <= busy_i;
txn_done <= done_i;
txn_ok <= ok_i;
err <= err_i;
arb_lost <= arb_lost_w;
-- DECISION 3: recovery may not run during a transfer. The stuck-line detector cannot tell a
-- held line from a clocked one while a transfer is open, because both lines are low most of
-- the time.
in_transfer <= f_started or b_busy or busy_i;
u_scl : entity work.i2c_scl_gen
generic map (N_LOW => N_LOW, N_HIGH => N_HIGH, N_SU => N_SU, N_SAMP => N_SAMP,
CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n, enable => gen_enable, idle_low => gen_idle_low,
scl_in => scl_in, scl_drive_low => g_scl_low,
drive_point => drive_point, sample_point => sample_point,
scl_rising => g_rise, scl_falling => g_fall,
stretching => g_stretch, stretch_cycles => g_scyc,
bits_generated => g_bits, phase => g_phase);
u_fr : entity work.i2c_framer
generic map (N_HD_STA => N_HD_STA, N_SU_STA => N_SU_STA, N_SU_STO => N_SU_STO,
N_BUF => N_BUF, N_SU_DAT => N_SU, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
do_start => do_start, do_restart => do_restart, do_stop => do_stop,
scl_in => scl_in, sda_in => sda_in, scl_yield => scl_yield,
sda_req => f_sda_req, sda_bit => f_sda_bit, scl_drive_low => f_scl_low,
busy => f_busy, done => f_done, bus_free => f_bus_free, started => f_started,
stretch_wait => f_sw, starts => n_sta, restarts => n_rs, stops => n_sto,
state => f_state);
u_by : entity work.i2c_byte_engine
generic map (CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
drive_point => drive_point, sample_point => sample_point,
go => byte_go, dir_write => byte_dir_write, tx_byte => byte_tx,
ack_to_send => byte_ack_send,
sda_in => sda_in, scl_high => scl_in, abort => arb_lost_w,
sda_req => b_sda_req, sda_bit => b_sda_bit,
rx_byte => b_rx, ack => b_ack, ack_valid => b_ackv, byte_done => b_done,
busy => b_busy, bit_index => b_bidx, driving => b_driving,
bytes_done => b_bytes, acks => b_acks, nacks => b_nacks);
u_err : entity work.i2c_err_mgr
generic map (N_PULSES => N_PULSES, N_HALF => N_LOW, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
addr_nack => addr_nack, data_nack => data_nack, arb_lost => arb_lost_w,
stretch_timeout => stretch_to, in_transfer => in_transfer,
scl_in => scl_in, sda_in => sda_in,
start_recovery => (start_recovery and (not in_transfer)), clear => done_i,
recovering => recovering, rec_scl_low => rec_scl_low,
rec_sda_req => rec_sda_req, rec_sda_bit => rec_sda_bit,
err => err_i, err_valid => err_valid, pulses_issued => rec_pulses,
recovered => recovered, escalate => escalate, state => rec_state);
-- ---- DECISION 1: who owns SDA ------------------------------------------
-- Owner 0 the framer, 1 the bit engine, 2 unused here (the byte engine drives through the
-- bit engine), 3 recovery. Lowest index wins, and any overlap is reported.
sda_req_v <= rec_sda_req & '0' & b_sda_req & f_sda_req;
sda_bit_v <= rec_sda_bit & '0' & b_sda_bit & f_sda_bit;
u_sda : entity work.i2c_sda_ctrl
generic map (N_OWNER => 4, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n, req => sda_req_v, bit_val => sda_bit_v,
sda_in => sda_in, scl_in => scl_in, tx_active => b_driving,
sda_drive_low => sda_drive_low,
grant => grant, owned => sda_owned, tx_bit => sda_tx,
owner_conflict => open, conflicts => sda_conflicts,
arb_loss_now => arb_now, arb_lost => arb_lost_w, arb_losses => arb_losses,
arb_clear => done_i);
-- ---- DECISION 2: who owns SCL ------------------------------------------
-- Three contributors, wired-AND locally exactly as the bus would. The handover OVERLAPS --
-- Chapter 17.8's controller asserts `scl_yield` and the generator's enable in the same
-- cycle -- so the line never rises between owners.
scl_drive_low <= f_scl_low or g_scl_low or rec_scl_low;
u_fb : entity work.i2c_bus_feedback
generic map (STRETCH_LIMIT => STRETCH_LIMIT, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
scl_release => not (f_scl_low or g_scl_low or rec_scl_low),
sda_release => not (sda_req_v(0) or sda_req_v(1) or sda_req_v(3)),
tx_active => b_driving,
scl_in => scl_in, sda_in => sda_in,
scl_held => scl_held, stretch_seen => stretch_seen,
stretch_cycles => stretch_cycles,
stretch_events => fb_ev, longest_stretch => fb_long,
stretch_timeout => stretch_to,
arb_loss_now => open, arb_lost => open, arb_losses => open,
clear => done_i);
u_reg : entity work.i2c_cmd_regs
generic map (N_BUF => N_BYTES, CNT_W => CNT_W)
port map (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 => done_i, txn_ok => ok_i, txn_err => err_i,
txn_bytes => txn_bytes, txn_busy => busy_i,
commands_issued => n_cmds);
u_txn : entity work.i2c_txn_ctrl
generic map (CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
cmd_valid => cmd_valid, cmd_addr => cmd_addr, cmd_read => cmd_read,
cmd_len => cmd_len, cmd_stop => cmd_stop,
do_start => do_start, do_restart => do_restart, do_stop => do_stop,
scl_yield => scl_yield, frame_done => f_done, frame_busy => f_busy,
bus_free => f_bus_free, frame_started => f_started,
gen_enable => gen_enable, gen_idle_low => gen_idle_low,
byte_go => byte_go, byte_dir_write => byte_dir_write, byte_tx => byte_tx,
byte_ack_send => byte_ack_send, byte_done => b_done, byte_busy => b_busy,
byte_ack => b_ack, byte_rx => b_rx,
tx_data => tx_data, tx_index => tx_index,
rx_data => rx_data, rx_index => rx_index, rx_we => rx_we,
arb_lost => arb_lost_w,
txn_done => done_i, txn_ok => ok_i, txn_err => open,
txn_bytes => txn_bytes, txn_busy => busy_i,
addr_nack => addr_nack, data_nack => data_nack,
state => txn_state, transactions => transactions);
end architecture rtl;5a. The testbenches
Thirteen tests, driven through the register map — the bench writes four registers and polls, which is exactly what a driver does. Nothing reaches inside the master except the observers of §6.
| # | Test | Property |
|---|---|---|
| T1 | a reset master drives nothing | not one block holds a line |
| T2 | a write, from four register writes and a poll | the whole driver |
| T3 | a read, payload back through the register map | |
| T4 | an address NACK reaches the driver as a distinct code | not as a timeout |
| T5 | a stretching target holds SCL for twelve cycles after every ACK | |
| T6 | a data NACK from that same target | a different code |
| T7 | a combined transaction, composed by the driver | write with no STOP, then read |
| T8 | recovery of a stuck SDA | and it reached the wire; see §6 |
| T9 | a stuck SCL gets no pulses | §3.1.16's central asymmetry |
| T10 | recovery is refused during a transfer | and drives nothing; see §6 |
| T11 | the data-valid rule held over the whole run | checked from the wire |
| T12 | a long sequence — six transactions | nothing accumulates |
| T13 | arbitration, and the latch that must clear | see §6 |
`timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_master_tb.sv
// Independent oracle for the complete i2c_master.
//
// The bench is the HOST. It writes registers and polls status, exactly as a driver would,
// and it never looks inside the master. On the other side of a wired-AND bus sit two
// pin-level targets and a protocol monitor that sees only the two wires.
//
// So every test here is an end-to-end statement: a driver-visible action produces a
// wire-visible result. That is the only level at which the claim of Chapter 17.12 can be
// checked -- that the blocks compose into a master with no state machine added at the
// top -- because a claim about composition cannot be tested on any one component.
// -----------------------------------------------------------------------------
module i2c_master_tb;
localparam integer NL = 8, NH = 4, NSU = 2, NSMP = 2;
localparam integer NHD = 3, NSUA = 3, NSUO = 3, NBF = 3;
localparam [6:0] TADDR = 7'h50, TADDR2 = 7'h22;
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 start_recovery = 1'b0;
logic [7:0] reg_rdata;
logic m_scl_low, m_sda_low;
logic recovering, recovered, escalate;
logic txn_busy, txn_done, txn_ok, arb_lost, stretch_seen;
logic [5:0] err;
logic [15:0] n_txn, n_scyc, n_arb, n_conf;
logic [3:0] txn_state;
logic t1_scl, t1_sda, t2_scl, t2_sda;
logic fault_sda = 1'b0, fault_scl = 1'b0;
logic scl, sda;
logic [3:0] scl_in, sda_in, scl_rbl, sda_rbl;
logic [7:0] scl_h, sda_h;
i2c_line_model #(.N_DEV(4)) bus (
.scl_drive_low({fault_scl, t2_scl, t1_scl, m_scl_low}),
.sda_drive_low({fault_sda, t2_sda, t1_sda, m_sda_low}),
.scl(scl), .sda(sda), .scl_in(scl_in), .sda_in(sda_in),
.scl_released_but_low(scl_rbl), .sda_released_but_low(sda_rbl),
.scl_holders(scl_h), .sda_holders(sda_h));
i2c_master #(.N_LOW(NL), .N_HIGH(NH), .N_SU(NSU), .N_SAMP(NSMP),
.N_HD_STA(NHD), .N_SU_STA(NSUA), .N_SU_STO(NSUO), .N_BUF(NBF),
.STRETCH_LIMIT(0), .N_PULSES(9), .N_BYTES(8), .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),
.scl_drive_low(m_scl_low), .scl_in(scl_in[0]),
.sda_drive_low(m_sda_low), .sda_in(sda_in[0]),
.start_recovery(start_recovery),
.recovering(recovering), .recovered(recovered), .escalate(escalate),
.txn_busy(txn_busy), .txn_done(txn_done), .txn_ok(txn_ok), .err(err),
.arb_lost(arb_lost), .stretch_seen(stretch_seen),
.transactions(n_txn), .stretch_cycles(n_scyc), .arb_losses(n_arb),
.sda_conflicts(n_conf), .txn_state(txn_state));
logic load_en = 1'b0; reg [7:0] load_addr = 8'h00, load_data = 8'h00;
logic t1_sel, t1_rd, t1_wv; wire [7:0] t1_lw;
logic [15:0] t1_rx, t1_tx, t1_nsta, t1_nsto; wire [2:0] t1_st;
i2c_target_model #(.MY_ADDR(TADDR), .ACK_ADDR(1'b1), .STRETCH_AFTER(0),
.NACK_AT(0), .N_MEM(16), .CNT_W(16)) u_t1 (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.scl_drive_low(t1_scl), .sda_drive_low(t1_sda),
.load_en(load_en), .load_addr(load_addr), .load_data(load_data),
.selected(t1_sel), .dir_read(t1_rd), .last_written(t1_lw), .write_valid(t1_wv),
.bytes_rx(t1_rx), .bytes_tx(t1_tx), .n_starts(t1_nsta), .n_stops(t1_nsto),
.state(t1_st));
// A second target that stretches after every acknowledge -- §3.1.6's byte-level
// handshake -- and refuses its third data byte.
logic t2_sel, t2_rd, t2_wv; wire [7:0] t2_lw;
logic [15:0] t2_rx, t2_tx, t2_nsta, t2_nsto; wire [2:0] t2_st;
i2c_target_model #(.MY_ADDR(TADDR2), .ACK_ADDR(1'b1), .STRETCH_AFTER(12),
.NACK_AT(3), .N_MEM(16), .CNT_W(16)) u_t2 (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.scl_drive_low(t2_scl), .sda_drive_low(t2_sda),
.load_en(1'b0), .load_addr(8'h00), .load_data(8'h00),
.selected(t2_sel), .dir_read(t2_rd), .last_written(t2_lw), .write_valid(t2_wv),
.bytes_rx(t2_rx), .bytes_tx(t2_tx), .n_starts(t2_nsta), .n_stops(t2_nsto),
.state(t2_st));
logic mo_start, mo_stop, mo_bit, mo_bitv, mo_byte, mo_ack, mo_ackv, mo_intr, mo_mid;
logic [7:0] mo_byteval;
logic [3:0] mo_bidx;
logic [15:0] mo_nsta, mo_nsto, mo_nbyte, mo_nmid;
i2c_proto_mon #(.CNT_W(16)) mon (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.start_seen(mo_start), .stop_seen(mo_stop), .bit_seen(mo_bit), .bit_val(mo_bitv),
.byte_seen(mo_byte), .byte_val(mo_byteval), .ack_seen(mo_ack), .ack_val(mo_ackv),
.in_transfer(mo_intr), .framing_midbyte(mo_mid), .bit_index(mo_bidx),
.n_starts(mo_nsta), .n_stops(mo_nsto), .n_bytes(mo_nbyte), .n_midbyte(mo_nmid));
always #5 clk = ~clk;
integer errors = 0;
integer n, k;
logic [7:0] got, st;
integer bad_change;
logic scl_l, sda_l;
always @(negedge clk) begin
if (rst_n) begin
if (scl && scl_l && (sda != sda_l) && !mo_start && !mo_stop && mo_bidx > 4'd1)
bad_change = bad_change + 1;
scl_l = scl; sda_l = sda;
end
end
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; start_recovery = 1'b0;
fault_sda = 1'b0; fault_scl = 1'b0; load_en = 1'b0;
bad_change = 0; scl_l = 1'b1; sda_l = 1'b1;
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
task preload (input [7:0] a, input [7:0] d);
begin
@(negedge clk); load_en = 1'b1; load_addr = a; load_data = d;
@(posedge clk); @(negedge clk); load_en = 1'b0;
end
endtask
// A driver, written the way a driver is written: post the command, then POLL.
task xfer (input [6:0] a, input rd_dir, input [3:0] len, input stp);
begin
wr(R_ADDR, {a, rd_dir});
wr(R_LEN, {4'd0, len});
wr(R_CTRL, {7'd0, stp});
wr(R_CMD, 8'h01);
n = 0;
st = 8'h00;
while (!st[0] && n < 12000) begin
rd(R_STATUS); st = got; n = n + 1;
end
if (n >= 12000) begin
$display(" FAIL xfer: never completed (txn_state %0d)", txn_state);
errors = errors + 1;
end
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
// ---- integration-level observers ---------------------------------------
//
// Three properties of the TOP LEVEL are invisible to the block benches, because each
// one is about whether a block's output actually reaches a pin:
//
// * recovery must really clock the bus (its SCL contribution must be in the OR)
// * recovery must really be able to frame it (its SDA contribution likewise)
// * recovery must drive NOTHING when it is refused
//
// A block bench proves the block behaves; only the integration can prove it is wired.
integer rec_drove_scl = 0, rec_drove_sda = 0;
always @(posedge clk) begin
if (!rst_n) begin rec_drove_scl <= 0; rec_drove_sda <= 0; end
else if (recovering) begin
if (m_scl_low) rec_drove_scl <= rec_drove_scl + 1;
if (m_sda_low) rec_drove_sda <= rec_drove_sda + 1;
end
end
initial begin
$display("=== i2c_master: the blocks compose, and nothing is added at the top ===");
// ----------------------------------------------------------------
// T1. A reset master drives nothing. Not one of the blocks holds a line out of
// reset, which is what a shared bus depends on and what §3.1.16 has no remedy
// for if it fails.
// ----------------------------------------------------------------
do_reset;
$display("T1 a reset master drives neither line");
ck_bit("T1 SCL released", m_scl_low, 1'b0);
ck_bit("T1 SDA released", m_sda_low, 1'b0);
ck_bit("T1 both lines high", scl & sda, 1'b1);
for (k = 0; k < 60; k = k + 1) begin
step;
if (m_scl_low || m_sda_low) begin
$display(" FAIL T1 the idle master drove a line");
errors = errors + 1;
end
end
ck_int("T1 nothing on the wire", mo_nsta + mo_nsto + mo_nbyte, 0);
// ----------------------------------------------------------------
// T2. A WRITE, from four register writes and a poll. This is the whole driver.
// ----------------------------------------------------------------
do_reset;
wr(R_TX0, 8'hA5);
xfer(TADDR, 1'b0, 4'd1, 1'b1);
$display("T2 a one-byte write, from four register writes and a poll");
ck_bit("T2 done", st[0], 1'b1);
ck_bit("T2 and ok", st[1], 1'b1);
ck_int("T2 one byte moved", st[7:4], 1);
ck_int("T2 the target received it", t1_lw, 8'hA5);
ck_int("T2 two bytes on the wire", mo_nbyte, 2);
ck_int("T2 one START and one STOP", mo_nsta + mo_nsto, 2);
// ----------------------------------------------------------------
// T3. A READ, and the payload comes back through the register map.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'h3C);
preload(8'h01, 8'h5E);
xfer(TADDR, 1'b1, 4'd2, 1'b1);
rd(R_RX0); ck_int("T3 the first byte", got, 8'h3C);
rd(R_RX0); ck_int("T3 the second byte", got, 8'h5E);
$display("T3 a two-byte read, returned through the register map");
ck_bit("T3 ok", st[1], 1'b1);
ck_int("T3 two bytes moved", st[7:4], 2);
// ----------------------------------------------------------------
// T4. AN ADDRESS NACK reaches the driver as a distinct code, not as a timeout.
// ----------------------------------------------------------------
do_reset;
wr(R_TX0, 8'hFF);
xfer(7'h7A, 1'b0, 4'd2, 1'b1);
rd(R_ERR);
$display("T4 an address NACK reaches the driver as its own code");
ck_bit("T4 done", st[0], 1'b1);
ck_bit("T4 and not ok", st[1], 1'b0);
ck_int("T4 zero bytes moved", st[7:4], 0);
ck_int("T4 the address-NACK code", got, 8'h01);
ck_int("T4 the bus was framed anyway", mo_nsto, 1);
// ----------------------------------------------------------------
// T5. A STRETCHING TARGET holds SCL for twelve cycles after every acknowledge --
// §3.1.6's handshake -- and the transfer still completes.
// ----------------------------------------------------------------
do_reset;
wr(R_TX0, 8'h11);
wr(R_TX0, 8'h22);
xfer(TADDR2, 1'b0, 4'd2, 1'b1);
$display("T5 a target that stretches after every byte is survived, not fought");
ck_bit("T5 ok", st[1], 1'b1);
ck_int("T5 two bytes moved", st[7:4], 2);
ck_int("T5 the target received both", t2_rx, 2);
ck_int("T5 the last was 0x22", t2_lw, 8'h22);
ck_bit("T5 and the stretch was noticed", stretch_seen, 1'b1);
if (n_scyc < 12) begin
$display(" FAIL T5 only %0d stretch cycles counted", n_scyc);
errors = errors + 1;
end
// ----------------------------------------------------------------
// T6. A DATA NACK from that same target, which refuses its third data byte -- a
// write-protect pin or a read-only location, in Chapter 16.1's terms.
// ----------------------------------------------------------------
do_reset;
for (k = 0; k < 4; k = k + 1) wr(R_TX0, 8'h40 + k[7:0]);
xfer(TADDR2, 1'b0, 4'd4, 1'b1);
rd(R_ERR);
$display("T6 a data NACK is a different code from an address NACK");
ck_bit("T6 done", st[0], 1'b1);
ck_bit("T6 not ok", st[1], 1'b0);
ck_int("T6 the data-NACK code, not the address one", got, 8'h02);
ck_int("T6 two of the four bytes were accepted", st[7:4], 2);
ck_int("T6 and the bus was framed", mo_nsto, 1);
// ----------------------------------------------------------------
// T7. A COMBINED TRANSACTION, composed by the driver: write with no STOP, then read.
// Two STARTs and ONE STOP, which is what makes it one transaction.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'h91);
wr(R_TX0, 8'h00);
xfer(TADDR, 1'b0, 4'd1, 1'b0); // no STOP: hold the bus
ck_bit("T7 phase one ok", st[1], 1'b1);
ck_int("T7 no STOP yet", mo_nsto, 0);
xfer(TADDR, 1'b1, 4'd1, 1'b1); // the turnaround and the read
rd(R_RX0);
$display("T7 a combined transaction, composed from two commands by the driver");
ck_int("T7 the byte came back", got, 8'h91);
ck_int("T7 two STARTs", mo_nsta, 2);
ck_int("T7 and exactly one STOP", mo_nsto, 1);
ck_bit("T7 the bus is idle", mo_intr, 1'b0);
// ----------------------------------------------------------------
// T8. RECOVERY OF A STUCK SDA. A fault injector holds SDA low with no transfer in
// progress, and the master clocks it free. §3.1.16.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); fault_sda = 1'b1;
for (k = 0; k < 10; k = k + 1) step;
rd(R_ERR);
ck_bit("T8 a stuck SDA was diagnosed", got[4], 1'b1);
@(negedge clk); start_recovery = 1'b1;
step;
@(negedge clk); start_recovery = 1'b0;
n = 0;
while (!recovering && n < 500) begin step; n = n + 1; end
for (k = 0; k < 60; k = k + 1) step;
@(negedge clk); fault_sda = 1'b0; // the wedged device finally lets go
n = 0;
while (recovering && n < 3000) begin step; n = n + 1; end
$display("T8 a stuck SDA is clocked free, and the bus is left framed");
ck_bit("T8 recovered", recovered, 1'b1);
ck_bit("T8 no escalation", escalate, 1'b0);
// Wired, not merely willing: recovery must have driven the CLOCK through the
// top-level OR, and must have driven SDA to build the closing STOP. A master
// whose recovery block is correct but whose contribution is missing from the
// pin path reports "recovered" having put nothing on the bus.
if (rec_drove_scl == 0) begin
$display(" FAIL T8 recovery never drove SCL onto the bus");
errors = errors + 1;
end
if (rec_drove_sda == 0) begin
$display(" FAIL T8 recovery never drove SDA, so it cannot have framed the bus");
errors = errors + 1;
end
ck_bit("T8 SDA is free", sda, 1'b1);
ck_bit("T8 and nothing is being driven", m_scl_low | m_sda_low, 1'b0);
// ----------------------------------------------------------------
// T9. A STUCK SCL GETS NO PULSES. The central asymmetry of §3.1.16: the nine
// pulses ARE pulses on SCL, so none can reach a line something else is holding.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); fault_scl = 1'b1; fault_sda = 1'b1;
for (k = 0; k < 10; k = k + 1) step;
rd(R_ERR);
ck_bit("T9 a stuck SCL was diagnosed", got[5], 1'b1);
@(negedge clk); start_recovery = 1'b1;
step;
@(negedge clk); start_recovery = 1'b0;
n = 0;
while (recovering && n < 3000) begin step; n = n + 1; end
$display("T9 a stuck SCL gets no pulses and escalates immediately");
ck_bit("T9 escalated", escalate, 1'b1);
ck_bit("T9 not recovered", recovered, 1'b0);
ck_bit("T9 and the master never drove SCL against the holder", m_scl_low, 1'b0);
// ----------------------------------------------------------------
// T10. RECOVERY IS REFUSED DURING A TRANSFER. Both lines are low most of the time
// while clocking, so a detector that ran then would report a stuck bus on
// every byte -- and clocking a live transfer would destroy it.
// ----------------------------------------------------------------
do_reset;
wr(R_TX0, 8'h5C);
wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
n = 0;
while (!txn_busy && n < 500) begin step; n = n + 1; end
@(negedge clk); start_recovery = 1'b1;
for (k = 0; k < 20; k = k + 1) step;
@(negedge clk); start_recovery = 1'b0;
$display("T10 recovery asked for during a transfer is refused, not run");
ck_bit("T10 recovery did not start", recovering, 1'b0);
// And it drove nothing. "recovering is low" is the block saying it declined;
// these two say the pins agree, which is the property that matters to the
// device whose live byte would otherwise be clocked apart.
ck_int("T10 recovery drove no clock", rec_drove_scl, 0);
ck_int("T10 recovery drove no data", rec_drove_sda, 0);
n = 0;
while (!txn_done && n < 8000) begin step; n = n + 1; end
ck_bit("T10 and the transfer completed normally", txn_ok, 1'b1);
ck_int("T10 with the byte delivered", t1_lw, 8'h5C);
// ----------------------------------------------------------------
// T11. THE DATA-VALID RULE HELD OVER THE WHOLE RUN. §3.1.2, checked from the wire
// rather than argued from the code.
// ----------------------------------------------------------------
$display("T11 SDA never changed mid-byte while SCL was high, over the whole run");
ck_int("T11 no violations", bad_change, 0);
ck_int("T11 no SDA owner conflicts, across every block", n_conf, 0);
ck_int("T11 no arbitration losses on a single-master bus", n_arb, 0);
ck_int("T11 and no mid-byte framing", mo_nmid, 0);
// ----------------------------------------------------------------
// T12. A LONG SEQUENCE, to show nothing accumulates. Six transactions, alternating
// direction, each one checked.
// ----------------------------------------------------------------
do_reset;
for (k = 0; k < 8; k = k + 1) preload(k[7:0], 8'hE0 + k[7:0]);
for (k = 0; k < 3; k = k + 1) begin
wr(R_TX0, 8'h30 + k[7:0]);
xfer(TADDR, 1'b0, 4'd1, 1'b1);
ck_bit("T12 the write succeeded", st[1], 1'b1);
xfer(TADDR, 1'b1, 4'd1, 1'b1);
ck_bit("T12 the read succeeded", st[1], 1'b1);
end
$display("T12 six transactions back to back, and nothing accumulates");
ck_int("T12 six transactions", n_txn, 6);
ck_int("T12 six STARTs and six STOPs", mo_nsta + mo_nsto, 12);
ck_int("T12 no conflicts", n_conf, 0);
ck_int("T12 no violations", bad_change, 0);
ck_bit("T12 and the master is idle with both lines released",
m_scl_low | m_sda_low, 1'b0);
// ----------------------------------------------------------------
// T13. ARBITRATION, AND THE LATCH THAT MUST CLEAR. The fault injector acts as a
// competing master: it pulls SDA low while this master is transmitting a one
// with SCL high, which is §3.1.8's exact condition. Two things then have to
// be true, and the second is the one no block bench can check -- `arb_lost`
// is a STICKY latch, and the top level is what decides when it clears. A
// master that never clears it reports the first contest forever and a driver
// can never distinguish a new loss from an old one.
// ----------------------------------------------------------------
do_reset;
wr(R_TX0, 8'hFF); // all ones: every bit is losable
wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
n = 0;
while (!txn_busy && n < 500) begin step; n = n + 1; end
// Hold SDA down through the address phase: whatever one this master sends, the bus
// shows a zero, and while SCL is high that is a lost arbitration.
@(negedge clk); fault_sda = 1'b1;
n = 0;
while (!arb_lost && n < 4000) begin step; n = n + 1; end
$display("T13 a competing zero on SDA is an arbitration loss, and the latch clears");
ck_bit("T13 arbitration was reported lost", arb_lost, 1'b1);
if (n_arb < 1) begin
$display(" FAIL T13 the loss was not counted");
errors = errors + 1;
end
@(negedge clk); fault_sda = 1'b0;
n = 0;
while (txn_busy && n < 8000) begin step; n = n + 1; end
// Now a clean transaction on a quiet bus. The latch must be gone, or every later
// transfer inherits this one's verdict.
wr(R_TX0, 8'h2A);
wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
n = 0;
while (!txn_done && n < 8000) begin step; n = n + 1; end
ck_bit("T13 and the latch cleared for the next transaction", arb_lost, 1'b0);
ck_bit("T13 which then succeeded on a quiet bus", txn_ok, 1'b1);
if (errors == 0)
$display("=== i2c_master: ALL CHECKS PASSED ===");
else
$display("=== i2c_master: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule `timescale 1ns/1ps
// -----------------------------------------------------------------------------
// i2c_master_tb.sv
// Independent oracle for the complete i2c_master.
//
// The bench is the HOST. It writes registers and polls status, exactly as a driver would,
// and it never looks inside the master. On the other side of a wired-AND bus sit two
// pin-level targets and a protocol monitor that sees only the two wires.
//
// So every test here is an end-to-end statement: a driver-visible action produces a
// wire-visible result. That is the only level at which the claim of Chapter 17.12 can be
// checked -- that the blocks compose into a master with no state machine added at the
// top -- because a claim about composition cannot be tested on any one component.
// -----------------------------------------------------------------------------
// (Verilog-2001 -- structurally identical to the SystemVerilog above.)
module i2c_master_tb;
localparam integer NL = 8, NH = 4, NSU = 2, NSMP = 2;
localparam integer NHD = 3, NSUA = 3, NSUO = 3, NBF = 3;
localparam [6:0] TADDR = 7'h50, TADDR2 = 7'h22;
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;
reg start_recovery = 1'b0;
wire [7:0] reg_rdata;
wire m_scl_low, m_sda_low;
wire recovering, recovered, escalate;
wire txn_busy, txn_done, txn_ok, arb_lost, stretch_seen;
wire [5:0] err;
wire [15:0] n_txn, n_scyc, n_arb, n_conf;
wire [3:0] txn_state;
wire t1_scl, t1_sda, t2_scl, t2_sda;
reg fault_sda = 1'b0, fault_scl = 1'b0;
wire scl, sda;
wire [3:0] scl_in, sda_in, scl_rbl, sda_rbl;
wire [7:0] scl_h, sda_h;
i2c_line_model #(.N_DEV(4)) bus (
.scl_drive_low({fault_scl, t2_scl, t1_scl, m_scl_low}),
.sda_drive_low({fault_sda, t2_sda, t1_sda, m_sda_low}),
.scl(scl), .sda(sda), .scl_in(scl_in), .sda_in(sda_in),
.scl_released_but_low(scl_rbl), .sda_released_but_low(sda_rbl),
.scl_holders(scl_h), .sda_holders(sda_h));
i2c_master #(.N_LOW(NL), .N_HIGH(NH), .N_SU(NSU), .N_SAMP(NSMP),
.N_HD_STA(NHD), .N_SU_STA(NSUA), .N_SU_STO(NSUO), .N_BUF(NBF),
.STRETCH_LIMIT(0), .N_PULSES(9), .N_BYTES(8), .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),
.scl_drive_low(m_scl_low), .scl_in(scl_in[0]),
.sda_drive_low(m_sda_low), .sda_in(sda_in[0]),
.start_recovery(start_recovery),
.recovering(recovering), .recovered(recovered), .escalate(escalate),
.txn_busy(txn_busy), .txn_done(txn_done), .txn_ok(txn_ok), .err(err),
.arb_lost(arb_lost), .stretch_seen(stretch_seen),
.transactions(n_txn), .stretch_cycles(n_scyc), .arb_losses(n_arb),
.sda_conflicts(n_conf), .txn_state(txn_state));
reg load_en = 1'b0; reg [7:0] load_addr = 8'h00, load_data = 8'h00;
wire t1_sel, t1_rd, t1_wv; wire [7:0] t1_lw;
wire [15:0] t1_rx, t1_tx, t1_nsta, t1_nsto; wire [2:0] t1_st;
i2c_target_model #(.MY_ADDR(TADDR), .ACK_ADDR(1'b1), .STRETCH_AFTER(0),
.NACK_AT(0), .N_MEM(16), .CNT_W(16)) u_t1 (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.scl_drive_low(t1_scl), .sda_drive_low(t1_sda),
.load_en(load_en), .load_addr(load_addr), .load_data(load_data),
.selected(t1_sel), .dir_read(t1_rd), .last_written(t1_lw), .write_valid(t1_wv),
.bytes_rx(t1_rx), .bytes_tx(t1_tx), .n_starts(t1_nsta), .n_stops(t1_nsto),
.state(t1_st));
// A second target that stretches after every acknowledge -- §3.1.6's byte-level
// handshake -- and refuses its third data byte.
wire t2_sel, t2_rd, t2_wv; wire [7:0] t2_lw;
wire [15:0] t2_rx, t2_tx, t2_nsta, t2_nsto; wire [2:0] t2_st;
i2c_target_model #(.MY_ADDR(TADDR2), .ACK_ADDR(1'b1), .STRETCH_AFTER(12),
.NACK_AT(3), .N_MEM(16), .CNT_W(16)) u_t2 (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.scl_drive_low(t2_scl), .sda_drive_low(t2_sda),
.load_en(1'b0), .load_addr(8'h00), .load_data(8'h00),
.selected(t2_sel), .dir_read(t2_rd), .last_written(t2_lw), .write_valid(t2_wv),
.bytes_rx(t2_rx), .bytes_tx(t2_tx), .n_starts(t2_nsta), .n_stops(t2_nsto),
.state(t2_st));
wire mo_start, mo_stop, mo_bit, mo_bitv, mo_byte, mo_ack, mo_ackv, mo_intr, mo_mid;
wire [7:0] mo_byteval;
wire [3:0] mo_bidx;
wire [15:0] mo_nsta, mo_nsto, mo_nbyte, mo_nmid;
i2c_proto_mon #(.CNT_W(16)) mon (
.clk(clk), .rst_n(rst_n), .scl(scl), .sda(sda),
.start_seen(mo_start), .stop_seen(mo_stop), .bit_seen(mo_bit), .bit_val(mo_bitv),
.byte_seen(mo_byte), .byte_val(mo_byteval), .ack_seen(mo_ack), .ack_val(mo_ackv),
.in_transfer(mo_intr), .framing_midbyte(mo_mid), .bit_index(mo_bidx),
.n_starts(mo_nsta), .n_stops(mo_nsto), .n_bytes(mo_nbyte), .n_midbyte(mo_nmid));
always #5 clk = ~clk;
integer errors = 0;
integer n, k;
reg [7:0] got, st;
integer bad_change;
reg scl_l, sda_l;
always @(negedge clk) begin
if (rst_n) begin
if (scl && scl_l && (sda != sda_l) && !mo_start && !mo_stop && mo_bidx > 4'd1)
bad_change = bad_change + 1;
scl_l = scl; sda_l = sda;
end
end
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; start_recovery = 1'b0;
fault_sda = 1'b0; fault_scl = 1'b0; load_en = 1'b0;
bad_change = 0; scl_l = 1'b1; sda_l = 1'b1;
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
task preload (input [7:0] a, input [7:0] d);
begin
@(negedge clk); load_en = 1'b1; load_addr = a; load_data = d;
@(posedge clk); @(negedge clk); load_en = 1'b0;
end
endtask
// A driver, written the way a driver is written: post the command, then POLL.
task xfer (input [6:0] a, input rd_dir, input [3:0] len, input stp);
begin
wr(R_ADDR, {a, rd_dir});
wr(R_LEN, {4'd0, len});
wr(R_CTRL, {7'd0, stp});
wr(R_CMD, 8'h01);
n = 0;
st = 8'h00;
while (!st[0] && n < 12000) begin
rd(R_STATUS); st = got; n = n + 1;
end
if (n >= 12000) begin
$display(" FAIL xfer: never completed (txn_state %0d)", txn_state);
errors = errors + 1;
end
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
// ---- integration-level observers ---------------------------------------
//
// Three properties of the TOP LEVEL are invisible to the block benches, because each
// one is about whether a block's output actually reaches a pin:
//
// * recovery must really clock the bus (its SCL contribution must be in the OR)
// * recovery must really be able to frame it (its SDA contribution likewise)
// * recovery must drive NOTHING when it is refused
//
// A block bench proves the block behaves; only the integration can prove it is wired.
integer rec_drove_scl, rec_drove_sda;
always @(posedge clk) begin
if (!rst_n) begin rec_drove_scl <= 0; rec_drove_sda <= 0; end
else if (recovering) begin
if (m_scl_low) rec_drove_scl <= rec_drove_scl + 1;
if (m_sda_low) rec_drove_sda <= rec_drove_sda + 1;
end
end
initial begin
$display("=== i2c_master: the blocks compose, and nothing is added at the top ===");
// ----------------------------------------------------------------
// T1. A reset master drives nothing. Not one of the blocks holds a line out of
// reset, which is what a shared bus depends on and what §3.1.16 has no remedy
// for if it fails.
// ----------------------------------------------------------------
do_reset;
$display("T1 a reset master drives neither line");
ck_bit("T1 SCL released", m_scl_low, 1'b0);
ck_bit("T1 SDA released", m_sda_low, 1'b0);
ck_bit("T1 both lines high", scl & sda, 1'b1);
for (k = 0; k < 60; k = k + 1) begin
step;
if (m_scl_low || m_sda_low) begin
$display(" FAIL T1 the idle master drove a line");
errors = errors + 1;
end
end
ck_int("T1 nothing on the wire", mo_nsta + mo_nsto + mo_nbyte, 0);
// ----------------------------------------------------------------
// T2. A WRITE, from four register writes and a poll. This is the whole driver.
// ----------------------------------------------------------------
do_reset;
wr(R_TX0, 8'hA5);
xfer(TADDR, 1'b0, 4'd1, 1'b1);
$display("T2 a one-byte write, from four register writes and a poll");
ck_bit("T2 done", st[0], 1'b1);
ck_bit("T2 and ok", st[1], 1'b1);
ck_int("T2 one byte moved", st[7:4], 1);
ck_int("T2 the target received it", t1_lw, 8'hA5);
ck_int("T2 two bytes on the wire", mo_nbyte, 2);
ck_int("T2 one START and one STOP", mo_nsta + mo_nsto, 2);
// ----------------------------------------------------------------
// T3. A READ, and the payload comes back through the register map.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'h3C);
preload(8'h01, 8'h5E);
xfer(TADDR, 1'b1, 4'd2, 1'b1);
rd(R_RX0); ck_int("T3 the first byte", got, 8'h3C);
rd(R_RX0); ck_int("T3 the second byte", got, 8'h5E);
$display("T3 a two-byte read, returned through the register map");
ck_bit("T3 ok", st[1], 1'b1);
ck_int("T3 two bytes moved", st[7:4], 2);
// ----------------------------------------------------------------
// T4. AN ADDRESS NACK reaches the driver as a distinct code, not as a timeout.
// ----------------------------------------------------------------
do_reset;
wr(R_TX0, 8'hFF);
xfer(7'h7A, 1'b0, 4'd2, 1'b1);
rd(R_ERR);
$display("T4 an address NACK reaches the driver as its own code");
ck_bit("T4 done", st[0], 1'b1);
ck_bit("T4 and not ok", st[1], 1'b0);
ck_int("T4 zero bytes moved", st[7:4], 0);
ck_int("T4 the address-NACK code", got, 8'h01);
ck_int("T4 the bus was framed anyway", mo_nsto, 1);
// ----------------------------------------------------------------
// T5. A STRETCHING TARGET holds SCL for twelve cycles after every acknowledge --
// §3.1.6's handshake -- and the transfer still completes.
// ----------------------------------------------------------------
do_reset;
wr(R_TX0, 8'h11);
wr(R_TX0, 8'h22);
xfer(TADDR2, 1'b0, 4'd2, 1'b1);
$display("T5 a target that stretches after every byte is survived, not fought");
ck_bit("T5 ok", st[1], 1'b1);
ck_int("T5 two bytes moved", st[7:4], 2);
ck_int("T5 the target received both", t2_rx, 2);
ck_int("T5 the last was 0x22", t2_lw, 8'h22);
ck_bit("T5 and the stretch was noticed", stretch_seen, 1'b1);
if (n_scyc < 12) begin
$display(" FAIL T5 only %0d stretch cycles counted", n_scyc);
errors = errors + 1;
end
// ----------------------------------------------------------------
// T6. A DATA NACK from that same target, which refuses its third data byte -- a
// write-protect pin or a read-only location, in Chapter 16.1's terms.
// ----------------------------------------------------------------
do_reset;
for (k = 0; k < 4; k = k + 1) wr(R_TX0, 8'h40 + k[7:0]);
xfer(TADDR2, 1'b0, 4'd4, 1'b1);
rd(R_ERR);
$display("T6 a data NACK is a different code from an address NACK");
ck_bit("T6 done", st[0], 1'b1);
ck_bit("T6 not ok", st[1], 1'b0);
ck_int("T6 the data-NACK code, not the address one", got, 8'h02);
ck_int("T6 two of the four bytes were accepted", st[7:4], 2);
ck_int("T6 and the bus was framed", mo_nsto, 1);
// ----------------------------------------------------------------
// T7. A COMBINED TRANSACTION, composed by the driver: write with no STOP, then read.
// Two STARTs and ONE STOP, which is what makes it one transaction.
// ----------------------------------------------------------------
do_reset;
preload(8'h00, 8'h91);
wr(R_TX0, 8'h00);
xfer(TADDR, 1'b0, 4'd1, 1'b0); // no STOP: hold the bus
ck_bit("T7 phase one ok", st[1], 1'b1);
ck_int("T7 no STOP yet", mo_nsto, 0);
xfer(TADDR, 1'b1, 4'd1, 1'b1); // the turnaround and the read
rd(R_RX0);
$display("T7 a combined transaction, composed from two commands by the driver");
ck_int("T7 the byte came back", got, 8'h91);
ck_int("T7 two STARTs", mo_nsta, 2);
ck_int("T7 and exactly one STOP", mo_nsto, 1);
ck_bit("T7 the bus is idle", mo_intr, 1'b0);
// ----------------------------------------------------------------
// T8. RECOVERY OF A STUCK SDA. A fault injector holds SDA low with no transfer in
// progress, and the master clocks it free. §3.1.16.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); fault_sda = 1'b1;
for (k = 0; k < 10; k = k + 1) step;
rd(R_ERR);
ck_bit("T8 a stuck SDA was diagnosed", got[4], 1'b1);
@(negedge clk); start_recovery = 1'b1;
step;
@(negedge clk); start_recovery = 1'b0;
n = 0;
while (!recovering && n < 500) begin step; n = n + 1; end
for (k = 0; k < 60; k = k + 1) step;
@(negedge clk); fault_sda = 1'b0; // the wedged device finally lets go
n = 0;
while (recovering && n < 3000) begin step; n = n + 1; end
$display("T8 a stuck SDA is clocked free, and the bus is left framed");
ck_bit("T8 recovered", recovered, 1'b1);
ck_bit("T8 no escalation", escalate, 1'b0);
// Wired, not merely willing: recovery must have driven the CLOCK through the
// top-level OR, and must have driven SDA to build the closing STOP.
if (rec_drove_scl == 0) begin
$display(" FAIL T8 recovery never drove SCL onto the bus");
errors = errors + 1;
end
if (rec_drove_sda == 0) begin
$display(" FAIL T8 recovery never drove SDA, so it cannot have framed the bus");
errors = errors + 1;
end
ck_bit("T8 SDA is free", sda, 1'b1);
ck_bit("T8 and nothing is being driven", m_scl_low | m_sda_low, 1'b0);
// ----------------------------------------------------------------
// T9. A STUCK SCL GETS NO PULSES. The central asymmetry of §3.1.16: the nine
// pulses ARE pulses on SCL, so none can reach a line something else is holding.
// ----------------------------------------------------------------
do_reset;
@(negedge clk); fault_scl = 1'b1; fault_sda = 1'b1;
for (k = 0; k < 10; k = k + 1) step;
rd(R_ERR);
ck_bit("T9 a stuck SCL was diagnosed", got[5], 1'b1);
@(negedge clk); start_recovery = 1'b1;
step;
@(negedge clk); start_recovery = 1'b0;
n = 0;
while (recovering && n < 3000) begin step; n = n + 1; end
$display("T9 a stuck SCL gets no pulses and escalates immediately");
ck_bit("T9 escalated", escalate, 1'b1);
ck_bit("T9 not recovered", recovered, 1'b0);
ck_bit("T9 and the master never drove SCL against the holder", m_scl_low, 1'b0);
// ----------------------------------------------------------------
// T10. RECOVERY IS REFUSED DURING A TRANSFER. Both lines are low most of the time
// while clocking, so a detector that ran then would report a stuck bus on
// every byte -- and clocking a live transfer would destroy it.
// ----------------------------------------------------------------
do_reset;
wr(R_TX0, 8'h5C);
wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
n = 0;
while (!txn_busy && n < 500) begin step; n = n + 1; end
@(negedge clk); start_recovery = 1'b1;
for (k = 0; k < 20; k = k + 1) step;
@(negedge clk); start_recovery = 1'b0;
$display("T10 recovery asked for during a transfer is refused, not run");
ck_bit("T10 recovery did not start", recovering, 1'b0);
// And it drove nothing -- the property that matters to the device whose live
// byte would otherwise be clocked apart.
ck_int("T10 recovery drove no clock", rec_drove_scl, 0);
ck_int("T10 recovery drove no data", rec_drove_sda, 0);
n = 0;
while (!txn_done && n < 8000) begin step; n = n + 1; end
ck_bit("T10 and the transfer completed normally", txn_ok, 1'b1);
ck_int("T10 with the byte delivered", t1_lw, 8'h5C);
// ----------------------------------------------------------------
// T11. THE DATA-VALID RULE HELD OVER THE WHOLE RUN. §3.1.2, checked from the wire
// rather than argued from the code.
// ----------------------------------------------------------------
$display("T11 SDA never changed mid-byte while SCL was high, over the whole run");
ck_int("T11 no violations", bad_change, 0);
ck_int("T11 no SDA owner conflicts, across every block", n_conf, 0);
ck_int("T11 no arbitration losses on a single-master bus", n_arb, 0);
ck_int("T11 and no mid-byte framing", mo_nmid, 0);
// ----------------------------------------------------------------
// T12. A LONG SEQUENCE, to show nothing accumulates. Six transactions, alternating
// direction, each one checked.
// ----------------------------------------------------------------
do_reset;
for (k = 0; k < 8; k = k + 1) preload(k[7:0], 8'hE0 + k[7:0]);
for (k = 0; k < 3; k = k + 1) begin
wr(R_TX0, 8'h30 + k[7:0]);
xfer(TADDR, 1'b0, 4'd1, 1'b1);
ck_bit("T12 the write succeeded", st[1], 1'b1);
xfer(TADDR, 1'b1, 4'd1, 1'b1);
ck_bit("T12 the read succeeded", st[1], 1'b1);
end
$display("T12 six transactions back to back, and nothing accumulates");
ck_int("T12 six transactions", n_txn, 6);
ck_int("T12 six STARTs and six STOPs", mo_nsta + mo_nsto, 12);
ck_int("T12 no conflicts", n_conf, 0);
ck_int("T12 no violations", bad_change, 0);
ck_bit("T12 and the master is idle with both lines released",
m_scl_low | m_sda_low, 1'b0);
// ----------------------------------------------------------------
// T13. ARBITRATION, AND THE LATCH THAT MUST CLEAR. The fault injector acts as a
// competing master: it pulls SDA low while this master is transmitting a one
// with SCL high, which is §3.1.8's exact condition. Two things then have to
// be true, and the second is the one no block bench can check -- `arb_lost`
// is a STICKY latch, and the top level is what decides when it clears. A
// master that never clears it reports the first contest forever and a driver
// can never distinguish a new loss from an old one.
// ----------------------------------------------------------------
do_reset;
wr(R_TX0, 8'hFF); // all ones: every bit is losable
wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
n = 0;
while (!txn_busy && n < 500) begin step; n = n + 1; end
// Hold SDA down through the address phase: whatever one this master sends, the bus
// shows a zero, and while SCL is high that is a lost arbitration.
@(negedge clk); fault_sda = 1'b1;
n = 0;
while (!arb_lost && n < 4000) begin step; n = n + 1; end
$display("T13 a competing zero on SDA is an arbitration loss, and the latch clears");
ck_bit("T13 arbitration was reported lost", arb_lost, 1'b1);
if (n_arb < 1) begin
$display(" FAIL T13 the loss was not counted");
errors = errors + 1;
end
@(negedge clk); fault_sda = 1'b0;
n = 0;
while (txn_busy && n < 8000) begin step; n = n + 1; end
// Now a clean transaction on a quiet bus. The latch must be gone, or every later
// transfer inherits this one's verdict.
wr(R_TX0, 8'h2A);
wr(R_ADDR, {TADDR, 1'b0}); wr(R_LEN, 8'h01); wr(R_CTRL, 8'h01); wr(R_CMD, 8'h01);
n = 0;
while (!txn_done && n < 8000) begin step; n = n + 1; end
ck_bit("T13 and the latch cleared for the next transaction", arb_lost, 1'b0);
ck_bit("T13 which then succeeded on a quiet bus", txn_ok, 1'b1);
if (errors == 0)
$display("=== i2c_master: ALL CHECKS PASSED ===");
else
$display("=== i2c_master: %0d CHECK(S) FAILED ===", errors);
$finish;
end
endmodule -- ---------------------------------------------------------------------------
-- i2c_master_tb.vhd
-- Independent oracle for the complete i2c_master. Behavioural twin of the SV and Verilog
-- benches.
--
-- The bench is the HOST. It writes registers and polls status, exactly as a driver would, and
-- it never looks inside the master. On the other side of a wired-AND bus sit two pin-level
-- targets and a protocol monitor that sees only the two wires.
--
-- So every test here is an end-to-end statement: a driver-visible action produces a
-- wire-visible result. That is the only level at which the claim of Chapter 17.12 can be
-- checked -- that the blocks compose into a master with no state machine added at the top --
-- because a claim about composition cannot be tested on any one component.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity i2c_master_tb is
end entity i2c_master_tb;
architecture sim of i2c_master_tb is
constant TCLK : time := 10 ns;
constant NL : integer := 8;
constant NH : integer := 4;
constant NSU : integer := 2;
constant NSMP : integer := 2;
constant NHD : integer := 3;
constant NSUA : integer := 3;
constant NSUO : integer := 3;
constant NBF : integer := 3;
constant TADDR : std_logic_vector(6 downto 0) := "1010000"; -- 0x50
constant TADDR2 : std_logic_vector(6 downto 0) := "0100010"; -- 0x22
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, start_recovery : std_logic := '0';
signal rrdata : std_logic_vector(7 downto 0);
signal m_scl_low, m_sda_low : std_logic;
signal recovering, recovered, escalate : std_logic;
signal txn_busy, txn_done, txn_ok, arb_lost, stretch_seen : std_logic;
signal err : std_logic_vector(5 downto 0);
signal n_txn, n_scyc, n_arb, n_conf : unsigned(15 downto 0);
signal txn_state : unsigned(3 downto 0);
signal t1_scl, t1_sda, t2_scl, t2_sda : std_logic;
signal fault_sda, fault_scl : std_logic := '0';
signal scl_drv, sda_drv : std_logic_vector(3 downto 0);
signal scl, sda : std_logic;
signal scl_in, sda_in, scl_rbl, sda_rbl : std_logic_vector(3 downto 0);
signal scl_h, sda_h : unsigned(7 downto 0);
signal load_en : std_logic := '0';
signal load_addr, load_data : std_logic_vector(7 downto 0) := (others => '0');
signal t1_sel, t1_rd, t1_wv : std_logic;
signal t1_lw : std_logic_vector(7 downto 0);
signal t1_rx, t1_tx, t1_nsta, t1_nsto : unsigned(15 downto 0);
signal t1_st : unsigned(2 downto 0);
signal t2_sel, t2_rd, t2_wv : std_logic;
signal t2_lw : std_logic_vector(7 downto 0);
signal t2_rx, t2_tx, t2_nsta, t2_nsto : unsigned(15 downto 0);
signal t2_st : unsigned(2 downto 0);
signal mo_start, mo_stop, mo_bit, mo_bitv, mo_byte, mo_ack, mo_ackv : std_logic;
signal mo_intr, mo_mid : std_logic;
signal mo_byteval : std_logic_vector(7 downto 0);
signal mo_bidx : unsigned(3 downto 0);
signal mo_nsta, mo_nsto, mo_nbyte, mo_nmid : unsigned(15 downto 0);
signal bad_change : integer := 0;
signal halt : boolean := false;
-- ---- integration-level observers ---------------------------------------
-- Three properties of the TOP LEVEL are invisible to the block benches, because each
-- is about whether a block's output actually reaches a pin: recovery must really clock
-- the bus, must really be able to frame it, and must drive NOTHING when refused.
-- A block bench proves the block behaves; only the integration proves it is wired.
signal rec_drove_scl, rec_drove_sda : integer := 0;
begin
scl_drv <= fault_scl & t2_scl & t1_scl & m_scl_low;
sda_drv <= fault_sda & t2_sda & t1_sda & m_sda_low;
bus_m : entity work.i2c_line_model
generic map (N_DEV => 4)
port map (scl_drive_low => scl_drv, sda_drive_low => sda_drv,
scl => scl, sda => sda, scl_in => scl_in, sda_in => sda_in,
scl_released_but_low => scl_rbl, sda_released_but_low => sda_rbl,
scl_holders => scl_h, sda_holders => sda_h);
dut : entity work.i2c_master
generic map (N_LOW => NL, N_HIGH => NH, N_SU => NSU, N_SAMP => NSMP,
N_HD_STA => NHD, N_SU_STA => NSUA, N_SU_STO => NSUO, N_BUF => NBF,
STRETCH_LIMIT => 0, N_PULSES => 9, N_BYTES => 8, 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,
scl_drive_low => m_scl_low, scl_in => scl_in(0),
sda_drive_low => m_sda_low, sda_in => sda_in(0),
start_recovery => start_recovery,
recovering => recovering, recovered => recovered, escalate => escalate,
txn_busy => txn_busy, txn_done => txn_done, txn_ok => txn_ok, err => err,
arb_lost => arb_lost, stretch_seen => stretch_seen,
transactions => n_txn, stretch_cycles => n_scyc, arb_losses => n_arb,
sda_conflicts => n_conf, txn_state => txn_state);
u_t1 : entity work.i2c_target_model
generic map (MY_ADDR => TADDR, ACK_ADDR => '1', STRETCH_AFTER => 0,
NACK_AT => 0, N_MEM => 16, CNT_W => 16)
port map (clk => clk, rst_n => rst_n, scl => scl, sda => sda,
scl_drive_low => t1_scl, sda_drive_low => t1_sda,
load_en => load_en, load_addr => load_addr, load_data => load_data,
selected => t1_sel, dir_read => t1_rd, last_written => t1_lw,
write_valid => t1_wv, bytes_rx => t1_rx, bytes_tx => t1_tx,
n_starts => t1_nsta, n_stops => t1_nsto, state => t1_st);
-- A second target that stretches after every acknowledge -- §3.1.6's byte-level handshake,
-- the most common real target behaviour a master must survive -- and refuses its third
-- data byte.
u_t2 : entity work.i2c_target_model
generic map (MY_ADDR => TADDR2, ACK_ADDR => '1', STRETCH_AFTER => 12,
NACK_AT => 3, N_MEM => 16, CNT_W => 16)
port map (clk => clk, rst_n => rst_n, scl => scl, sda => sda,
scl_drive_low => t2_scl, sda_drive_low => t2_sda,
load_en => '0', load_addr => x"00", load_data => x"00",
selected => t2_sel, dir_read => t2_rd, last_written => t2_lw,
write_valid => t2_wv, bytes_rx => t2_rx, bytes_tx => t2_tx,
n_starts => t2_nsta, n_stops => t2_nsto, state => t2_st);
rec_obs : process (clk, rst_n)
begin
if rst_n = '0' then
rec_drove_scl <= 0; rec_drove_sda <= 0;
elsif rising_edge(clk) then
if recovering = '1' then
if m_scl_low = '1' then rec_drove_scl <= rec_drove_scl + 1; end if;
if m_sda_low = '1' then rec_drove_sda <= rec_drove_sda + 1; end if;
end if;
end if;
end process;
mon : entity work.i2c_proto_mon
generic map (CNT_W => 16)
port map (clk => clk, rst_n => rst_n, scl => scl, sda => sda,
start_seen => mo_start, stop_seen => mo_stop, bit_seen => mo_bit,
bit_val => mo_bitv, byte_seen => mo_byte, byte_val => mo_byteval,
ack_seen => mo_ack, ack_val => mo_ackv, in_transfer => mo_intr,
framing_midbyte => mo_mid, bit_index => mo_bidx,
n_starts => mo_nsta, n_stops => mo_nsto, n_bytes => mo_nbyte,
n_midbyte => mo_nmid);
clkgen : process
begin
while not halt loop
clk <= '0'; wait for TCLK/2;
clk <= '1'; wait for TCLK/2;
end loop;
wait;
end process;
-- The §3.1.2 watchdog, over the whole run.
meas : process (clk, rst_n)
variable scl_l, sda_l : std_logic := '1';
begin
if rst_n = '0' then
scl_l := '1'; sda_l := '1'; bad_change <= 0;
elsif falling_edge(clk) then
if scl = '1' and scl_l = '1' and sda /= sda_l
and mo_start = '0' and mo_stop = '0' and mo_bidx > 1 then
bad_change <= bad_change + 1;
end if;
scl_l := scl; sda_l := sda;
end if;
end process;
stim : process
variable errn : integer := 0;
variable n : integer;
variable got, st : 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;
errn := errn + 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;
errn := errn + 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'; start_recovery <= '0';
fault_sda <= '0'; fault_scl <= '0'; load_en <= '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 preload (a : integer; d : integer) is
begin
wait until falling_edge(clk);
load_en <= '1';
load_addr <= std_logic_vector(to_unsigned(a, 8));
load_data <= std_logic_vector(to_unsigned(d, 8));
wait until rising_edge(clk); wait until falling_edge(clk); load_en <= '0';
end procedure;
-- A driver, written the way a driver is written: post the command, then POLL.
procedure xfer (a : std_logic_vector(6 downto 0); rd_dir : std_logic;
len : integer; stp : std_logic) is
variable av : std_logic_vector(7 downto 0);
begin
av := a & rd_dir;
wr(IDX_ADDR, to_integer(unsigned(av)));
wr(IDX_LEN, len);
if stp = '1' then wr(IDX_CTRL, 1); else wr(IDX_CTRL, 0); end if;
wr(IDX_CMD, 1);
n := 0;
st := (others => '0');
while st(0) = '0' and n < 12000 loop
rd(IDX_STATUS); st := got; n := n + 1;
end loop;
if n >= 12000 then
report " FAIL xfer: never completed (txn_state "
& integer'image(to_integer(txn_state)) & ")" severity note;
errn := errn + 1;
end if;
end procedure;
begin
report "=== i2c_master: the blocks compose, and nothing is added at the top ==="
severity note;
-- T1. A reset master drives neither line. Not one of the blocks holds a line out of
-- reset, which is what a shared bus depends on.
do_reset;
report "T1 a reset master drives neither line" severity note;
ck_bit("T1 SCL released", m_scl_low, '0');
ck_bit("T1 SDA released", m_sda_low, '0');
ck_bit("T1 both lines high", scl and sda, '1');
for j in 0 to 59 loop
step;
if m_scl_low = '1' or m_sda_low = '1' then
report " FAIL T1 the idle master drove a line" severity note;
errn := errn + 1;
end if;
end loop;
ck_int("T1 nothing on the wire",
to_integer(mo_nsta) + to_integer(mo_nsto) + to_integer(mo_nbyte), 0);
-- T2. A WRITE, from four register writes and a poll. This is the whole driver.
do_reset;
wr(IDX_TX0, 16#A5#);
xfer(TADDR, '0', 1, '1');
report "T2 a one-byte write, from four register writes and a poll" severity note;
ck_bit("T2 done", st(0), '1');
ck_bit("T2 and ok", st(1), '1');
ck_int("T2 one byte moved", to_integer(unsigned(st(7 downto 4))), 1);
ck_int("T2 the target received it", to_integer(unsigned(t1_lw)), 16#A5#);
ck_int("T2 two bytes on the wire", to_integer(mo_nbyte), 2);
ck_int("T2 one START and one STOP",
to_integer(mo_nsta) + to_integer(mo_nsto), 2);
-- T3. A READ, and the payload comes back through the register map.
do_reset;
preload(0, 16#3C#);
preload(1, 16#5E#);
xfer(TADDR, '1', 2, '1');
rd(IDX_RX0); ck_int("T3 the first byte", to_integer(unsigned(got)), 16#3C#);
rd(IDX_RX0); ck_int("T3 the second byte", to_integer(unsigned(got)), 16#5E#);
report "T3 a two-byte read, returned through the register map" severity note;
ck_bit("T3 ok", st(1), '1');
ck_int("T3 two bytes moved", to_integer(unsigned(st(7 downto 4))), 2);
-- T4. AN ADDRESS NACK reaches the driver as its own code, not as a timeout.
do_reset;
wr(IDX_TX0, 16#FF#);
xfer("1111010", '0', 2, '1');
rd(IDX_ERR);
report "T4 an address NACK reaches the driver as its own code" severity note;
ck_bit("T4 done", st(0), '1');
ck_bit("T4 and not ok", st(1), '0');
ck_int("T4 zero bytes moved", to_integer(unsigned(st(7 downto 4))), 0);
ck_int("T4 the address-NACK code", to_integer(unsigned(got)), 16#01#);
ck_int("T4 the bus was framed anyway", to_integer(mo_nsto), 1);
-- T5. A STRETCHING TARGET is survived, not fought.
do_reset;
wr(IDX_TX0, 16#11#);
wr(IDX_TX0, 16#22#);
xfer(TADDR2, '0', 2, '1');
report "T5 a target that stretches after every byte is survived, not fought"
severity note;
ck_bit("T5 ok", st(1), '1');
ck_int("T5 two bytes moved", to_integer(unsigned(st(7 downto 4))), 2);
ck_int("T5 the target received both", to_integer(t2_rx), 2);
ck_int("T5 the last was 0x22", to_integer(unsigned(t2_lw)), 16#22#);
ck_bit("T5 and the stretch was noticed", stretch_seen, '1');
if to_integer(n_scyc) < 12 then
report " FAIL T5 only " & integer'image(to_integer(n_scyc))
& " stretch cycles counted" severity note;
errn := errn + 1;
end if;
-- T6. A DATA NACK is a different code from an address NACK.
do_reset;
for j in 0 to 3 loop wr(IDX_TX0, 16#40# + j); end loop;
xfer(TADDR2, '0', 4, '1');
rd(IDX_ERR);
report "T6 a data NACK is a different code from an address NACK" severity note;
ck_bit("T6 done", st(0), '1');
ck_bit("T6 not ok", st(1), '0');
ck_int("T6 the data-NACK code, not the address one",
to_integer(unsigned(got)), 16#02#);
ck_int("T6 two of the four bytes were accepted",
to_integer(unsigned(st(7 downto 4))), 2);
ck_int("T6 and the bus was framed", to_integer(mo_nsto), 1);
-- T7. A COMBINED TRANSACTION, composed by the driver: two STARTs and ONE STOP.
do_reset;
preload(0, 16#91#);
wr(IDX_TX0, 16#00#);
xfer(TADDR, '0', 1, '0'); -- no STOP: hold the bus
ck_bit("T7 phase one ok", st(1), '1');
ck_int("T7 no STOP yet", to_integer(mo_nsto), 0);
xfer(TADDR, '1', 1, '1'); -- the turnaround and the read
rd(IDX_RX0);
report "T7 a combined transaction, composed from two commands by the driver"
severity note;
ck_int("T7 the byte came back", to_integer(unsigned(got)), 16#91#);
ck_int("T7 two STARTs", to_integer(mo_nsta), 2);
ck_int("T7 and exactly one STOP", to_integer(mo_nsto), 1);
ck_bit("T7 the bus is idle", mo_intr, '0');
-- T8. RECOVERY OF A STUCK SDA. §3.1.16.
do_reset;
wait until falling_edge(clk); fault_sda <= '1';
for j in 0 to 9 loop step; end loop;
rd(IDX_ERR);
ck_bit("T8 a stuck SDA was diagnosed", got(4), '1');
wait until falling_edge(clk); start_recovery <= '1';
step;
wait until falling_edge(clk); start_recovery <= '0';
n := 0;
while recovering = '0' and n < 500 loop step; n := n + 1; end loop;
for j in 0 to 59 loop step; end loop;
wait until falling_edge(clk); fault_sda <= '0'; -- the wedged device lets go
n := 0;
while recovering = '1' and n < 3000 loop step; n := n + 1; end loop;
report "T8 a stuck SDA is clocked free, and the bus is left framed" severity note;
ck_bit("T8 recovered", recovered, '1');
ck_bit("T8 no escalation", escalate, '0');
-- Wired, not merely willing: recovery must have driven the CLOCK through the
-- top-level OR, and SDA to build the closing STOP.
if rec_drove_scl = 0 then
report " FAIL T8 recovery never drove SCL onto the bus" severity note;
errn := errn + 1;
end if;
if rec_drove_sda = 0 then
report " FAIL T8 recovery never drove SDA, so it cannot have framed the bus"
severity note;
errn := errn + 1;
end if;
ck_bit("T8 SDA is free", sda, '1');
ck_bit("T8 and nothing is being driven", m_scl_low or m_sda_low, '0');
-- T9. A STUCK SCL GETS NO PULSES -- the central asymmetry of §3.1.16.
do_reset;
wait until falling_edge(clk); fault_scl <= '1'; fault_sda <= '1';
for j in 0 to 9 loop step; end loop;
rd(IDX_ERR);
ck_bit("T9 a stuck SCL was diagnosed", got(5), '1');
wait until falling_edge(clk); start_recovery <= '1';
step;
wait until falling_edge(clk); start_recovery <= '0';
n := 0;
while recovering = '1' and n < 3000 loop step; n := n + 1; end loop;
report "T9 a stuck SCL gets no pulses and escalates immediately" severity note;
ck_bit("T9 escalated", escalate, '1');
ck_bit("T9 not recovered", recovered, '0');
ck_bit("T9 and the master never drove SCL against the holder", m_scl_low, '0');
-- T10. RECOVERY IS REFUSED DURING A TRANSFER.
do_reset;
wr(IDX_TX0, 16#5C#);
wr(IDX_ADDR, 16#A0#); wr(IDX_LEN, 1); wr(IDX_CTRL, 1); wr(IDX_CMD, 1);
n := 0;
while txn_busy = '0' and n < 500 loop step; n := n + 1; end loop;
wait until falling_edge(clk); start_recovery <= '1';
for j in 0 to 19 loop step; end loop;
wait until falling_edge(clk); start_recovery <= '0';
report "T10 recovery asked for during a transfer is refused, not run" severity note;
ck_bit("T10 recovery did not start", recovering, '0');
-- And it drove nothing -- the property that matters to the device whose live
-- byte would otherwise be clocked apart.
ck_int("T10 recovery drove no clock", rec_drove_scl, 0);
ck_int("T10 recovery drove no data", rec_drove_sda, 0);
n := 0;
while txn_done = '0' and n < 8000 loop step; n := n + 1; end loop;
ck_bit("T10 and the transfer completed normally", txn_ok, '1');
ck_int("T10 with the byte delivered", to_integer(unsigned(t1_lw)), 16#5C#);
-- T11. THE DATA-VALID RULE HELD OVER THE WHOLE RUN.
report "T11 SDA never changed mid-byte while SCL was high, over the whole run"
severity note;
ck_int("T11 no violations", bad_change, 0);
ck_int("T11 no SDA owner conflicts, across every block", to_integer(n_conf), 0);
ck_int("T11 no arbitration losses on a single-master bus", to_integer(n_arb), 0);
ck_int("T11 and no mid-byte framing", to_integer(mo_nmid), 0);
-- T12. A LONG SEQUENCE, to show nothing accumulates.
do_reset;
for j in 0 to 7 loop preload(j, 16#E0# + j); end loop;
for j in 0 to 2 loop
wr(IDX_TX0, 16#30# + j);
xfer(TADDR, '0', 1, '1');
ck_bit("T12 the write succeeded", st(1), '1');
xfer(TADDR, '1', 1, '1');
ck_bit("T12 the read succeeded", st(1), '1');
end loop;
report "T12 six transactions back to back, and nothing accumulates" severity note;
ck_int("T12 six transactions", to_integer(n_txn), 6);
ck_int("T12 six STARTs and six STOPs",
to_integer(mo_nsta) + to_integer(mo_nsto), 12);
ck_int("T12 no conflicts", to_integer(n_conf), 0);
ck_int("T12 no violations", bad_change, 0);
ck_bit("T12 and the master is idle with both lines released",
m_scl_low or m_sda_low, '0');
-- T13. ARBITRATION, AND THE LATCH THAT MUST CLEAR. The fault injector acts as a
-- competing master: it pulls SDA low while this master transmits a one with
-- SCL high, which is §3.1.8's exact condition. The second half is what no
-- block bench can check -- arb_lost is a STICKY latch and the top level
-- decides when it clears. A master that never clears it reports the first
-- contest forever.
do_reset;
wr(IDX_TX0, 16#FF#); -- all ones: every bit is losable
wr(IDX_ADDR, to_integer(unsigned(TADDR & '0')));
wr(IDX_LEN, 1); wr(IDX_CTRL, 1); wr(IDX_CMD, 1);
n := 0;
while txn_busy = '0' and n < 500 loop step; n := n + 1; end loop;
wait until falling_edge(clk); fault_sda <= '1';
n := 0;
while arb_lost = '0' and n < 4000 loop step; n := n + 1; end loop;
report "T13 a competing zero on SDA is an arbitration loss, and the latch clears"
severity note;
ck_bit("T13 arbitration was reported lost", arb_lost, '1');
if to_integer(n_arb) < 1 then
report " FAIL T13 the loss was not counted" severity note;
errn := errn + 1;
end if;
wait until falling_edge(clk); fault_sda <= '0';
n := 0;
while txn_busy = '1' and n < 8000 loop step; n := n + 1; end loop;
-- A clean transaction on a quiet bus: the latch must be gone.
wr(IDX_TX0, 16#2A#);
wr(IDX_ADDR, to_integer(unsigned(TADDR & '0')));
wr(IDX_LEN, 1); wr(IDX_CTRL, 1); wr(IDX_CMD, 1);
n := 0;
while txn_done = '0' and n < 8000 loop step; n := n + 1; end loop;
ck_bit("T13 and the latch cleared for the next transaction", arb_lost, '0');
ck_bit("T13 which then succeeded on a quiet bus", txn_ok, '1');
if errn = 0 then
report "=== i2c_master: ALL CHECKS PASSED ===" severity note;
else
report "=== i2c_master: " & integer'image(errn)
& " CHECK(S) FAILED ===" severity note;
end if;
halt <= true;
wait;
end process;
end architecture sim;5b. Execution
| Design | SystemVerilog | Verilog-2001 | VHDL | Finish |
|---|---|---|---|---|
i2c_master | PASS 13/13 | PASS 13/13 | PASS 13/13 | 44220 ns, all three |
6. Mutation Testing — What Only Integration Can Catch
Eight defects, all of them at the wiring level, because that is the only thing this file contains. Five survived the original bench — the highest survival rate anywhere in the module, and the reason is instructive: every block had been verified, so the bench had been written as if the blocks were the thing under test.
| # | Injected defect | Expected detection | Result |
|---|---|---|---|
| M1 | recovery allowed during a transfer | T10 after strengthening | KILLED (3) |
| M2 | the SCL contributions ANDed instead of ORed | T2, T3, T12 | KILLED (38) |
| M3 | recovery dropped from the SCL path | T8 after strengthening | KILLED (2) |
| M4 | the framer dropped from the SCL path | T2, T7 | KILLED (34) |
| M5 | SDA priority reversed | — | EQUIVALENT, proven; see below |
| M6 | arbitration judged without transmit intent | T3, T12 | KILLED (36) |
| M7 | recovery's SDA contribution dropped | T8 after strengthening | KILLED (2) |
| M8 | the arbitration latch never cleared | T13 new | KILLED (3) |
valid non-equivalent mutants: 7 killed: 7 survived: 0
documented equivalent mutant: 1 (M5, proven by measurement)
baseline PASS before injection; PASS after restore.The four survivors were all "is it wired?" questions
M1, M3, M7 and M8 share a shape that no block bench can reach:
The fix was three observers that watch the pins while a block believes it is acting:
rec_drove_scl cycles where recovery was active AND the master's SCL was driven
rec_drove_sda the same for SDAT8 now requires both to be non-zero — recovery must have clocked the bus and driven SDA to build its closing STOP. T10 requires both to be zero during a refused request, which is the property that matters to the device whose live byte would otherwise be clocked apart.
M8 needed a new test entirely. arb_lost is a sticky latch and the top level decides when it clears — arb_clear(txn_done). Nothing in the original bench ever lost arbitration, because a single-master bus cannot. T13 uses the fault injector as a competing master: it holds SDA low while the master transmits 0xFF, so every bit is losable, and then checks the second half that no block bench can see — that a subsequent clean transaction finds the latch cleared. A master that never clears it reports the first contest forever, and a driver cannot distinguish a new loss from an old one.
Note also where T13 had to be placed: after T12, because T11 asserts n_arb == 0 on a single-master bus. That false-positive check is worth keeping, so the arbitration test goes last rather than weakening it.
M5 is equivalent, and the proof is a measurement
Reversing the framer's and bit engine's relative priority survived. Rather than assume or assert, the question was measured — instrument the top level and count the cycles in which both request SDA:
PROBE simultaneous framer+bit-engine SDA requests: 0 cycles
(over the entire thirteen-test regression)They never contend. During a framing sequence the byte engine is inactive; during a byte the framer is idle. So the priority never resolves anything, and reversing it cannot change behaviour.
That is a real finding about the design rather than a gap in the bench:
7. Verification Connection — What an Integration Bench Is For
// Every block in this master has its own verified bench. So what is the top-level
// environment FOR? Section 6 answers it empirically: five of eight wiring defects
// survived a bench written as though the blocks were under test.
//
// THREE CLASSES OF CHECK BELONG ONLY HERE.
//
// 1. CONNECTIVITY UNDER ACTIVITY. Not "is the net present" -- a netlist check finds
// that -- but "does this block's output reach the pin WHILE the block believes it
// is acting". The observers of section 6 are exactly this, and they are cheap:
//
// assert property (@(posedge clk) recovering && rec_expects_clock
// |-> ##[0:2] scl_drive_low);
//
// 2. STICKY STATE ACROSS TRANSACTIONS. arb_lost, the error codes, the byte counters:
// a block bench runs one scenario and checks the outcome. Only a sequence of
// transactions can show that transaction N+1 does not inherit N's verdict, and
// that is a top-level decision (arb_clear, and the command register's clear-on-
// write of section 17.2 T10).
//
// 3. CROSS-BLOCK TIMING HANDOVERS. The SCL handover overlaps by one cycle in both
// directions. Neither the framer's bench nor the generator's can test it, because
// each has only one of the two owners. The property is:
//
// never (SCL rises while a transfer is open and neither owner is driving)
//
// which is a statement about two blocks' outputs and the bus at once.
//
// WHAT DOES NOT BELONG HERE: re-testing block behaviour. A top-level bench that
// re-checks tHD;STA is slow, redundant, and will be deleted the first time it is in
// somebody's way -- taking its connectivity checks with it. Keep them separable.
//
// THE DRIVER IS THE STIMULUS. This bench writes four registers and polls, which is
// what firmware does. That is deliberate: a top-level bench that pokes internal
// signals to set up a scenario is testing a configuration the product cannot reach.8. FPGA and ASIC Implications
On an FPGA, this file is where the three-signal pin form meets the actual tri-state buffer. scl_drive_low and sda_drive_low drive output enables with data inputs tied low; scl_in and sda_in come from the buffers' input pins through two-flop synchronisers. The synchronisers belong here rather than inside a block, because there is exactly one pad per line and duplicating the synchroniser per consumer would let two blocks disagree about what the bus did.
Timing closure is trivial — the longest combinational path is the SDA arbiter's four-request priority cone plus the pad — and that is the decomposition paying off again: no block contains a path that crosses another block.
On an ASIC, the same structure maps onto two open-drain pad cells, and the integration-level requirement is that escalate reach something able to act. A reset controller, a PMIC sequencer, or a firmware-visible status bit; Chapter 17.11 §9 made the point and this is the level at which it is wired or not.
The register interface is deliberately a trivial address/data/strobe port rather than APB or AHB. Wrapping it is an integration task with its own verification, and keeping the seam explicit means the master can be dropped into either without editing anything inside it.
9. Debugging — The Master That Recovered Nothing, Successfully
An FPGA design integrates a verified I2C master. Every block-level regression passes. On the board, normal transactions work perfectly. When a sensor occasionally wedges the bus by holding SDA low, the driver's recovery routine reports success -- the master sets its recovered flag and clears its error -- and the bus remains dead. A scope shows no activity at all on SCL during the reported recovery.
A missing term in one OR expression at the top level. The error manager was correct, verified, and unwired: its clock contribution never reached the pad, so nine pulses were generated onto a dangling net. Every block bench passed because each one connects the block under test to a bus model directly -- the very wiring the integration omitted is the wiring a block bench supplies for itself. The defect is structurally invisible below the top level, and the symptom is worse than a failure because the master reports success.
Restore the term, and then add the check that would have caught it: while recovery is active, the master's SCL output must actually be driven at some point. That is an integration-level observer -- count cycles where recovering and the pin-facing drive are both asserted, and require the count to be non-zero -- and it is what test T8 now asserts, along with the same check on SDA for the closing STOP. Note that a netlist connectivity check would NOT have caught it: the net exists and is driven by two of the three sources, so nothing is dangling. The property is about activity, not connectivity.Three generalisations.
Every block was correct and the system did nothing. Block-level verification is necessary and says nothing about composition. The wiring a block bench supplies for itself is precisely the wiring the integration can omit.
A connectivity check would have passed. The net existed and had two drivers. The missing one is only detectable by asking whether it ever contributed, which is a question about activity over time rather than about structure.
Reporting success was worse than failing. A recovery that escalated would have sent the driver to a hardware reset, which would have worked. Reporting recovered left the bus dead with no further action taken — and the flag was truthful about the block's internal state.
10. Common Misconceptions
"An I²C master is a big state machine." The finished top level has no case statement. Every state lives in the block whose time base it belongs to. §1.
"The FSM should be designed first." Design it first and it must hold four unrelated time bases, so it acquires counters — which are the other three machines, admitted late and untested. §1.
"Integration is just wiring, so it needs no verification." Five of eight wiring defects survived a bench that had been written as though the blocks were under test. §6.
"If every block passes, the system works." Every block was correct in §9 and the system drove nothing. Block benches supply the very wiring the integration can omit. §9.
"A netlist connectivity check catches a missing contribution." Not when the net has other drivers. The property is whether a source ever contributed, which is activity, not structure. §9.
"Priority in the SDA arbiter sequences normal operation." It never resolves anything: measured, the framer and bit engine contend for zero cycles across the whole regression. It exists to keep the bus defined when a bug occurs. §6.
"A surviving mutant means a missing test." M5 survives because the contention it reorders never happens. That was established by measurement, not assumed. §6.
"Recovery can be gated in software." The gate is one AND with !in_transfer in hardware. Software cannot see the transfer state with the timing resolution required. §2.
"Synchronisers belong in the blocks that read the lines." One pad per line means one synchroniser per line, or two blocks can disagree about what the bus did. §8.
"A top-level bench should re-check block behaviour." It becomes slow and redundant and gets deleted — taking its connectivity checks with it. §7.
11. Reason It Through
Why does designing the FSM first produce a design that gets rewritten?
Because it must hold four states that change on four unrelated events, so it needs either four sets of transitions per state or bolted-on counters. Those counters are the other three machines without their invariants. §1.
Name the three decisions the top level makes, and why each cannot live in a block.
Who owns SDA, because four blocks want it and no one of them can see the others. Who owns SCL, because the handover spans two blocks. When recovery may run, because only the top level sees both the request and the transfer state. §2.
A verified recovery block reports nine pulses issued and the scope shows an idle SCL. Where is the defect?
In the top-level OR that combines SCL contributions — recovery's term is missing, so its pulses reach a net that goes nowhere. The block is correct and unwired. §9.
Why would a netlist connectivity check not have found it?
Because the net exists and is driven by the other two sources. Nothing is dangling; what is missing is one source's contribution, which is only visible as activity over time. §9.
Why did four wiring mutants survive a bench in which every block was already verified?
Because each block bench connects its DUT to a bus model itself — supplying exactly the wiring the integration can omit. A block behaving correctly is independent of whether its outputs reach a pin. §6.
M5 reversed the SDA priority and nothing changed. How was that established rather than assumed?
By instrumenting the top level and counting cycles in which the framer and bit engine both request SDA. The count was zero across the whole regression, so the priority never resolves anything. §6.
If the priority is never exercised, why keep it?
Because it keeps the bus in a defined state when a bug does produce simultaneous requests — which Chapter 17.10 §10 shows happens with two software threads. The resolving half is a safety net; owner_conflict is the load-bearing half. §6.
Why is T13 placed after T12 rather than with the other feedback tests?
Because T11 asserts zero arbitration losses on a single-master bus — a false-positive check worth keeping. Injecting a competing master earlier would break it. §6.
12. Understanding Check
13. Summary
There is no state machine in the finished master's top level. Every state already exists inside a block that needed it, and integration is wiring plus three decisions.
That follows from decomposing by time base rather than by vocabulary. The four bases change on four unrelated events, so a single FSM would take their product and then acquire counters — which are the other three machines without their invariants.
The three decisions are who owns SDA, who owns SCL, and when recovery may run. Each spans blocks that cannot see one another, which is exactly why they cannot live lower down.
The SCL handover overlaps deliberately, so the line never rises between owners and a framer's SDA fall cannot become a START where a STOP was meant.
One AND gate separates a recovery feature from a corruption source — start_recovery && !in_transfer.
Eight wiring mutants, and five survived the original bench. The highest survival rate in the module, because the bench had been written as though the already-verified blocks were the thing under test.
Four of the five were "is it wired?" questions. A block bench proves the block behaves; it connects the DUT to a bus model itself, which is the very wiring the integration can omit.
The fix was observers that watch the pins while a block believes it is acting — recovery must actually drive the clock and SDA when it runs, and must drive nothing when it is refused.
The fifth needed a whole new test, because arb_lost is sticky and only a sequence of transactions shows that the next one does not inherit the last one's verdict — which required a competing master, and placement after the test that asserts a quiet bus.
And one mutant is equivalent, proven by measurement: the framer and bit engine contend for zero cycles across the whole regression, so reversing their priority changes nothing. The arbiter's resolving half is a safety net for a bug; its reporting half is what earns its keep.
14. What Comes Next
The master works, end to end, verified from the register interface down to the pins. What it is not yet is a configurable engineering block.
Chapter 17.13 closes the module by turning the parameters into a coherent set: cycle counts derived from a system frequency and a speed mode by the ceiling rule, elaboration-time guards that refuse an illegal configuration, counter widths that follow from the counts rather than being guessed, and the achieved frequency reported as an output because it is generally not the one that was requested.
It is also where two findings from earlier in the module come due. Chapter 17.2 §7 showed a defect invisible at N_BUF = 8; Chapter 17.3 §10 showed a period formula unfalsifiable at 100 MHz. Both said the same thing — the parameter space is part of the test space — and the final chapter is where that stops being an observation and becomes a test.
Continue learning
Related tutorials
- 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 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.
- 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.
