Skip to content
VLSI Mentor

I²C · Module 3

I²C Inside an SoC — Controller, Peripherals and the Software View

Follow a transfer from software to the conductor. A driver writes registers; a controller peripheral turns that into bus activity; completion and failure come back as status and interrupts. Build the register shell in three languages and see where UVM RAL fits.

Module 2 ended at a pin. Chapter 3.3 ended at the board outside that pin. This chapter goes the other way — inward, through the package, to the thing that actually decides when SDA and SCL move.

The question it answers is one every embedded and SoC engineer eventually needs: what actually happens between a line of driver code and a level on a conductor? The answer has more layers than most people expect, and each layer exists to hide the one below it. Getting the layering right is what lets a driver author ignore rise times and an RTL designer ignore register maps.

1. The Stack

Start with the whole path, then take it apart.

A vertical stack showing the path from software to the bus. Application and driver software performs memory-mapped register accesses across the SoC interconnect to an I2C controller peripheral. The controller contains a register block and a bit-level engine. The engine produces open-drain pull-low intent, which reaches an I O pad, which drives the external SDA and SCL conductors to an external target device.Driver softwarereads and writes registersSoC interconnectmemory-mapped accessRegister blockthe software contractBit-level engineframing and timing (Module 17)I/O padpull LOW, or releaseSDA / SCLthe board, Chapter 3.3Interruptcompletion without pollingExternal targetanswers when namedload/storecommanddrive low12
Figure 1 \u2014 the layers between a driver and a conductor. Each layer talks only to its neighbours, and each exists to hide the one below it. Software sees registers; the controller sees a command; the I/O boundary sees pull-low intent; the board sees a voltage.

Read it downward once. Software performs ordinary loads and stores to addresses that happen to belong to a peripheral. The interconnect carries those accesses. The register block is where they land — and this is the software-visible contract, the only part of the stack a driver author has to understand. The engine turns a register-level command into the actual sequence of edges on two conductors. The pad converts pull-low intent into pin behaviour, exactly as Chapter 2.3 established. The board is Chapter 3.3.

Two things are worth noticing immediately. The layers are narrow: each talks only to its neighbours. And the abstraction runs one way: software cannot see edges, and the engine cannot see the driver\u0027s intent — only the command it was given.

2. Software Does Not Toggle the Bus

The most common misconception about this stack is that a CPU drives SDA and SCL directly, one edge at a time. Worth addressing head-on, because the truth is a better systems lesson than the myth.

In the ordinary hardware-controller model the flow is:

  1. Software configures the controller once — enable it, set the speed.
  2. Software describes a transfer: which target, which direction, what payload.
  3. Software starts it with a single register write.
  4. The controller executes the entire transfer autonomously — every edge, every bit, at bus speed.
  5. Status changes, and software learns about it by polling or by interrupt.
  6. Software consumes the result.

Notice what software never does: it never waits for an individual bit, never times an edge, and never reasons about a rising edge\u0027s duration. A transfer that takes tens of microseconds on the wire costs software a few register accesses and a notification.

That division exists because the two domains run at incompatible speeds and with incompatible notions of time. A bus bit period is glacial by CPU standards and unforgiving in its timing requirements; software is fast and cannot be relied upon to be punctual. Hardware is good at precise slow things, software is good at imprecise fast things, and the register block is the seam between them.

3. The Register Interface

The register block is the software contract, so it is worth designing rather than listing. This is a deliberately generic educational map — it mimics no specific vendor peripheral, and real ones have more registers, deeper FIFOs and more configuration. The six below are the minimum that makes the architecture visible.

AddressRegisterAccessWhy it exists
0CTRLread/writeenable the controller; configuration lives here
1STATUSread, write-one-to-clearbusy, done, error — how software learns what happened
2TARGETread/writewhich target this transfer concerns
3TXDATAread/writethe payload offered to the controller
4RXDATAread-onlythe payload the controller received
5COMMANDwrite-onlystart a transfer, and say which direction

Four design decisions in that table are worth defending, because each one is a general register-design lesson:

STATUS flags are write-one-to-clear. done and error are set by hardware and cleared by software writing a one to the bit it has observed. Software cannot simply be trusted to have seen a flag, and hardware cannot know when it has — so acknowledgement has to be explicit. This is the same pattern as the sticky fault bit in Chapter 1.3.

RXDATA is read-only. Software writing received data would mean nothing. Writes are ignored rather than treated as errors, which is the conventional and more forgiving choice.

COMMAND is write-only and self-clearing. It is an action, not a state. Reading it back would invite software to treat it as a mode.

busy and done are separate bits. busy says a transfer is in progress; done says one finished and software has not acknowledged it yet. Collapsing them loses the ability to distinguish "nothing has happened yet" from "something finished and you missed it".

4. The Command/Status Shell in RTL

Now build it. The block below is the software-facing half of an I\u00B2C controller: it accepts register accesses, validates a command, hands an abstract request to an engine, and turns the engine\u0027s completion into software-visible status.

The bit-level engine is deliberately abstract. It appears as a handshake — eng_req out, eng_done/eng_error/eng_rdata back — and nothing here implements framing, addressing, acknowledgement or timing. Its implementation is Module 17, after START/STOP, addressing, ACK/NACK, clock stretching and arbitration have been established. Treating this as a controller would produce something that compiles and cannot talk to a device.

What the shell does own is a real and genuinely tricky contract:

  • A command is accepted only when enabled and not busy. A command written while busy is ignored — a defined behaviour, deliberately chosen rather than left unspecified.
  • eng_req is a one-cycle pulse, not a level, so the engine sees exactly one request per accepted command.
  • RXDATA is latched only on a read, so a failed or write transfer cannot disturb data software has not consumed.
  • done and error are sticky until acknowledged.

4a. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_shell.sv \u2014 the software-facing half of a controller (SYNTHESIZABLE; engine is abstract)
   module i2c_cmd_shell (
       input  logic       clk,
       input  logic       rst_n,
       // --- host register interface (minimal: no full on-chip bus protocol) ----
       input  logic       reg_wr,
       input  logic [2:0] reg_addr,
       input  logic [7:0] reg_wdata,
       output logic [7:0] reg_rdata,
       // --- abstract engine interface (the bit-level engine is Module 17) ------
       output logic       eng_req,
       output logic [6:0] eng_target,
       output logic       eng_is_read,
       output logic [7:0] eng_wdata,
       input  logic       eng_done,
       input  logic       eng_error,
       input  logic [7:0] eng_rdata
   );
       localparam logic [2:0] A_CTRL = 3'd0, A_STATUS = 3'd1, A_TARGET = 3'd2,
                              A_TXDATA = 3'd3, A_RXDATA = 3'd4, A_COMMAND = 3'd5;

       logic       enable;
       logic       busy, done_f, err_f;
       logic [6:0] target_q;
       logic [7:0] tx_q, rx_q;
       logic       is_read_q;

       always_ff @(posedge clk) begin
           if (!rst_n) begin
               enable <= 1'b0; busy <= 1'b0; done_f <= 1'b0; err_f <= 1'b0;
               target_q <= '0; tx_q <= '0; rx_q <= '0; is_read_q <= 1'b0;
               eng_req <= 1'b0;
           end else begin
               eng_req <= 1'b0;                       // one-cycle request pulse

               if (reg_wr) begin
                   case (reg_addr)
                       A_CTRL:   enable   <= reg_wdata[0];
                       A_TARGET: target_q <= reg_wdata[6:0];
                       A_TXDATA: tx_q     <= reg_wdata;
                       A_STATUS: begin                // write-one-to-clear flags
                           if (reg_wdata[1]) done_f <= 1'b0;
                           if (reg_wdata[2]) err_f  <= 1'b0;
                       end
                       A_COMMAND: begin
                           // ACCEPTED ONLY WHEN ENABLED AND NOT BUSY. A command
                           // written while busy is IGNORED -- a defined behaviour,
                           // not an unspecified one.
                           if (reg_wdata[0] && enable && !busy) begin
                               busy      <= 1'b1;
                               is_read_q <= reg_wdata[1];
                               eng_req   <= 1'b1;
                           end
                       end
                       default: ;                      // RXDATA is read-only
                   endcase
               end

               if (busy && eng_done) begin
                   busy   <= 1'b0;
                   done_f <= 1'b1;
                   if (eng_error) err_f <= 1'b1;
                   if (is_read_q) rx_q <= eng_rdata;   // latched ONLY on a read
               end
           end
       end

       assign eng_target  = target_q;
       assign eng_is_read = is_read_q;
       assign eng_wdata   = tx_q;

       always_comb begin
           case (reg_addr)
               A_CTRL:    reg_rdata = {7'b0, enable};
               A_STATUS:  reg_rdata = {5'b0, err_f, done_f, busy};
               A_TARGET:  reg_rdata = {1'b0, target_q};
               A_TXDATA:  reg_rdata = tx_q;
               A_RXDATA:  reg_rdata = rx_q;
               default:   reg_rdata = 8'h00;           // COMMAND is write-only
           endcase
       end
   endmodule

The testbench is written as a software model: it performs register writes and reads exactly as a driver would, and a separate task plays the engine. That framing is the point — it tests the contract software depends on rather than internal signals.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_shell_tb.sv \u2014 self-checking: a driver-shaped sequence, including error injection and a command issued while busy
   module i2c_cmd_shell_tb;
       logic clk = 1'b0, rst_n;
       logic reg_wr; logic [2:0] reg_addr; logic [7:0] reg_wdata, reg_rdata;
       logic eng_req, eng_is_read, eng_done, eng_error;
       logic [6:0] eng_target; logic [7:0] eng_wdata, eng_rdata;
       int errors = 0;

       i2c_cmd_shell dut (.*);
       always #5 clk = ~clk;

       // --- software model: register write / read ------------------------------
       task automatic wr(input logic [2:0] a, input logic [7:0] d);
           reg_addr = a; reg_wdata = d; reg_wr = 1'b1;
           @(negedge clk); reg_wr = 1'b0;
       endtask
       task automatic rd(input logic [2:0] a, input logic [7:0] exp, input string what);
           reg_addr = a; #1;
           if (reg_rdata !== exp) begin
               $error("%s: reg[%0d]=%h expected %h", what, a, reg_rdata, exp); errors++; end
       endtask
       // --- engine model: complete a transfer after a couple of cycles ---------
       task automatic engine_completes(input logic err, input logic [7:0] data);
           repeat (2) @(negedge clk);
           eng_rdata = data; eng_error = err; eng_done = 1'b1;
           @(negedge clk); eng_done = 1'b0; eng_error = 1'b0;
       endtask

       initial begin
           rst_n=0; reg_wr=0; reg_addr='0; reg_wdata='0;
           eng_done=0; eng_error=0; eng_rdata='0;
           repeat (2) @(negedge clk);
           rd(3'd1, 8'h00, "reset: status clear");                    // 1 reset
           rd(3'd4, 8'h00, "reset: rxdata clear");
           rst_n=1; @(negedge clk);

           // 2 -- a command while DISABLED must be ignored
           wr(3'd5, 8'h01);
           if (eng_req!==1'b0) begin $error("command accepted while disabled"); errors++; end
           rd(3'd1, 8'h00, "disabled: still idle");

           wr(3'd0, 8'h01); rd(3'd0, 8'h01, "enable set");            // 3 configure
           wr(3'd2, 8'h48); rd(3'd2, 8'h48, "target id programmed");
           wr(3'd3, 8'h3C); rd(3'd3, 8'h3C, "tx data loaded");

           // 4 -- WRITE command: engine sees the request and the operands
           wr(3'd5, 8'h01);
           if (eng_req!==1'b1)        begin $error("eng_req not pulsed"); errors++; end
           if (eng_target!==7'h48)    begin $error("wrong target to engine"); errors++; end
           if (eng_is_read!==1'b0)    begin $error("wrong direction to engine"); errors++; end
           if (eng_wdata!==8'h3C)     begin $error("wrong wdata to engine"); errors++; end
           rd(3'd1, 8'h01, "busy asserted");
           @(negedge clk);
           if (eng_req!==1'b0) begin $error("eng_req is a level, not a pulse"); errors++; end

           // 5 -- a SECOND command while busy must be ignored (defined behaviour)
           wr(3'd5, 8'h01);
           if (eng_req!==1'b0) begin $error("command accepted while busy"); errors++; end
           rd(3'd1, 8'h01, "still busy, unchanged");

           engine_completes(1'b0, 8'h00);                              // 6 complete
           rd(3'd1, 8'h02, "done set, busy cleared");
           rd(3'd4, 8'h00, "a WRITE must not change rxdata");           // 7 no latch
           wr(3'd1, 8'h02); rd(3'd1, 8'h00, "done write-one-to-clear");

           // 8 -- READ command: rxdata latches the engine's data
           wr(3'd5, 8'h03);                                            // go + is_read
           if (eng_is_read!==1'b1) begin $error("read direction not passed"); errors++; end
           engine_completes(1'b0, 8'hA5);
           rd(3'd1, 8'h02, "read completed");
           rd(3'd4, 8'hA5, "rxdata latched on read");
           wr(3'd1, 8'h02);

           // 9 -- ERROR injection: engine reports failure, status records it
           wr(3'd5, 8'h01);
           engine_completes(1'b1, 8'h00);
           rd(3'd1, 8'h06, "done + error set");
           rd(3'd4, 8'hA5, "a failed write must not disturb rxdata");
           wr(3'd1, 8'h06); rd(3'd1, 8'h00, "both flags cleared");

           // 10 -- RXDATA is read-only
           wr(3'd4, 8'hFF); rd(3'd4, 8'hA5, "rxdata is read-only");

           if (errors==0) $display("PASS: command/status shell -- accept, busy, complete, read latch, error, W1C, read-only");
           else           $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule

4b. Verilog

Same architecture and the same twelve checks. reg/wire typing, localparam address constants, and $display reporting.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_shell.v \u2014 the same shell in Verilog (SYNTHESIZABLE)
   module i2c_cmd_shell (
       input  wire       clk,
       input  wire       rst_n,
       input  wire       reg_wr,
       input  wire [2:0] reg_addr,
       input  wire [7:0] reg_wdata,
       output reg  [7:0] reg_rdata,
       output reg        eng_req,
       output wire [6:0] eng_target,
       output wire       eng_is_read,
       output wire [7:0] eng_wdata,
       input  wire       eng_done,
       input  wire       eng_error,
       input  wire [7:0] eng_rdata
   );
       localparam A_CTRL=3'd0, A_STATUS=3'd1, A_TARGET=3'd2,
                  A_TXDATA=3'd3, A_RXDATA=3'd4, A_COMMAND=3'd5;

       reg       enable, busy, done_f, err_f, is_read_q;
       reg [6:0] target_q;
       reg [7:0] tx_q, rx_q;

       always @(posedge clk) begin
           if (!rst_n) begin
               enable<=1'b0; busy<=1'b0; done_f<=1'b0; err_f<=1'b0;
               target_q<=7'h00; tx_q<=8'h00; rx_q<=8'h00; is_read_q<=1'b0;
               eng_req<=1'b0;
           end else begin
               eng_req <= 1'b0;                        // one-cycle request pulse
               if (reg_wr) begin
                   case (reg_addr)
                       A_CTRL:   enable   <= reg_wdata[0];
                       A_TARGET: target_q <= reg_wdata[6:0];
                       A_TXDATA: tx_q     <= reg_wdata;
                       A_STATUS: begin                 // write-one-to-clear
                           if (reg_wdata[1]) done_f <= 1'b0;
                           if (reg_wdata[2]) err_f  <= 1'b0;
                       end
                       A_COMMAND: if (reg_wdata[0] && enable && !busy) begin
                           busy <= 1'b1; is_read_q <= reg_wdata[1]; eng_req <= 1'b1;
                       end
                       default: ;                       // RXDATA is read-only
                   endcase
               end
               if (busy && eng_done) begin
                   busy <= 1'b0; done_f <= 1'b1;
                   if (eng_error) err_f <= 1'b1;
                   if (is_read_q) rx_q <= eng_rdata;    // latched ONLY on a read
               end
           end
       end

       assign eng_target  = target_q;
       assign eng_is_read = is_read_q;
       assign eng_wdata   = tx_q;

       always @(*) begin
           case (reg_addr)
               A_CTRL:   reg_rdata = {7'b0, enable};
               A_STATUS: reg_rdata = {5'b0, err_f, done_f, busy};
               A_TARGET: reg_rdata = {1'b0, target_q};
               A_TXDATA: reg_rdata = tx_q;
               A_RXDATA: reg_rdata = rx_q;
               default:  reg_rdata = 8'h00;             // COMMAND is write-only
           endcase
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_shell_tb.v \u2014 the same driver-shaped sequence in Verilog idiom
   module i2c_cmd_shell_tb;
       reg        clk, rst_n, reg_wr, eng_done, eng_error;
       reg  [2:0] reg_addr;
       reg  [7:0] reg_wdata, eng_rdata;
       wire [7:0] reg_rdata, eng_wdata;
       wire       eng_req, eng_is_read;
       wire [6:0] eng_target;
       integer    errors;

       i2c_cmd_shell dut (
           .clk(clk), .rst_n(rst_n), .reg_wr(reg_wr), .reg_addr(reg_addr),
           .reg_wdata(reg_wdata), .reg_rdata(reg_rdata), .eng_req(eng_req),
           .eng_target(eng_target), .eng_is_read(eng_is_read), .eng_wdata(eng_wdata),
           .eng_done(eng_done), .eng_error(eng_error), .eng_rdata(eng_rdata));

       initial clk = 1'b0;
       always #5 clk = ~clk;

       task wr; input [2:0] a; input [7:0] d;
           begin reg_addr=a; reg_wdata=d; reg_wr=1'b1; @(negedge clk); reg_wr=1'b0; end
       endtask
       task rd; input [2:0] a; input [7:0] exp; input [8*40:1] what;
           begin
               reg_addr=a; #1;
               if (reg_rdata !== exp) begin
                   $display("FAIL: %0s: reg[%0d]=%h expected %h", what, a, reg_rdata, exp);
                   errors=errors+1;
               end
           end
       endtask
       task engine_completes; input err; input [7:0] data;
           begin
               repeat (2) @(negedge clk);
               eng_rdata=data; eng_error=err; eng_done=1'b1;
               @(negedge clk); eng_done=1'b0; eng_error=1'b0;
           end
       endtask

       initial begin
           errors=0; rst_n=0; reg_wr=0; reg_addr=0; reg_wdata=0;
           eng_done=0; eng_error=0; eng_rdata=0;
           repeat (2) @(negedge clk);
           rd(3'd1, 8'h00, "reset: status clear");
           rd(3'd4, 8'h00, "reset: rxdata clear");
           rst_n=1; @(negedge clk);

           wr(3'd5, 8'h01);                                  // command while disabled
           if (eng_req!==1'b0) begin $display("FAIL: accepted while disabled"); errors=errors+1; end
           rd(3'd1, 8'h00, "disabled: still idle");

           wr(3'd0, 8'h01); rd(3'd0, 8'h01, "enable set");
           wr(3'd2, 8'h48); rd(3'd2, 8'h48, "target id programmed");
           wr(3'd3, 8'h3C); rd(3'd3, 8'h3C, "tx data loaded");

           wr(3'd5, 8'h01);                                  // WRITE command
           if (eng_req!==1'b1)     begin $display("FAIL: eng_req not pulsed"); errors=errors+1; end
           if (eng_target!==7'h48) begin $display("FAIL: wrong target"); errors=errors+1; end
           if (eng_is_read!==1'b0) begin $display("FAIL: wrong direction"); errors=errors+1; end
           if (eng_wdata!==8'h3C)  begin $display("FAIL: wrong wdata"); errors=errors+1; end
           rd(3'd1, 8'h01, "busy asserted");
           @(negedge clk);
           if (eng_req!==1'b0) begin $display("FAIL: eng_req is a level"); errors=errors+1; end

           wr(3'd5, 8'h01);                                  // second command while busy
           if (eng_req!==1'b0) begin $display("FAIL: accepted while busy"); errors=errors+1; end
           rd(3'd1, 8'h01, "still busy, unchanged");

           engine_completes(1'b0, 8'h00);
           rd(3'd1, 8'h02, "done set, busy cleared");
           rd(3'd4, 8'h00, "a WRITE must not change rxdata");
           wr(3'd1, 8'h02); rd(3'd1, 8'h00, "done write-one-to-clear");

           wr(3'd5, 8'h03);                                  // READ command
           if (eng_is_read!==1'b1) begin $display("FAIL: read direction not passed"); errors=errors+1; end
           engine_completes(1'b0, 8'hA5);
           rd(3'd1, 8'h02, "read completed");
           rd(3'd4, 8'hA5, "rxdata latched on read");
           wr(3'd1, 8'h02);

           wr(3'd5, 8'h01);                                  // ERROR injection
           engine_completes(1'b1, 8'h00);
           rd(3'd1, 8'h06, "done + error set");
           rd(3'd4, 8'hA5, "a failed write must not disturb rxdata");
           wr(3'd1, 8'h06); rd(3'd1, 8'h00, "both flags cleared");

           wr(3'd4, 8'hFF); rd(3'd4, 8'hA5, "rxdata is read-only");

           if (errors==0) $display("PASS: command/status shell -- accept, busy, complete, read latch, error, W1C, read-only");
           else           $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule

4c. VHDL

Two VHDL choices are worth naming. The address comparisons use named constants in an if/elsif chain rather than a case, because comparing std_logic_vector against named constants reads more clearly than a case over a vector. And eng_req is driven from an internal signal req_i rather than assigned in the process directly, because the register block reads its own busy state — the same readable-output issue Chapter 2.3 met, solved the same portable way.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_shell.vhd \u2014 the same shell in VHDL (SYNTHESIZABLE; internal signal for the readable request)
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_cmd_shell is
       port (
           clk         : in  std_logic;
           rst_n       : in  std_logic;
           reg_wr      : in  std_logic;
           reg_addr    : in  std_logic_vector(2 downto 0);
           reg_wdata   : in  std_logic_vector(7 downto 0);
           reg_rdata   : out std_logic_vector(7 downto 0);
           eng_req     : out std_logic;
           eng_target  : out std_logic_vector(6 downto 0);
           eng_is_read : out std_logic;
           eng_wdata   : out std_logic_vector(7 downto 0);
           eng_done    : in  std_logic;
           eng_error   : in  std_logic;
           eng_rdata   : in  std_logic_vector(7 downto 0)
       );
   end entity;

   architecture rtl of i2c_cmd_shell is
       constant A_CTRL    : std_logic_vector(2 downto 0) := "000";
       constant A_STATUS  : std_logic_vector(2 downto 0) := "001";
       constant A_TARGET  : std_logic_vector(2 downto 0) := "010";
       constant A_TXDATA  : std_logic_vector(2 downto 0) := "011";
       constant A_RXDATA  : std_logic_vector(2 downto 0) := "100";
       constant A_COMMAND : std_logic_vector(2 downto 0) := "101";

       signal enable, busy, done_f, err_f, is_read_q : std_logic := '0';
       signal target_q : std_logic_vector(6 downto 0) := (others => '0');
       signal tx_q, rx_q : std_logic_vector(7 downto 0) := (others => '0');
       signal req_i : std_logic := '0';
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   enable <= '0'; busy <= '0'; done_f <= '0'; err_f <= '0';
                   target_q <= (others => '0'); tx_q <= (others => '0');
                   rx_q <= (others => '0'); is_read_q <= '0'; req_i <= '0';
               else
                   req_i <= '0';                              -- one-cycle pulse
                   if reg_wr = '1' then
                       if reg_addr = A_CTRL then
                           enable <= reg_wdata(0);
                       elsif reg_addr = A_TARGET then
                           target_q <= reg_wdata(6 downto 0);
                       elsif reg_addr = A_TXDATA then
                           tx_q <= reg_wdata;
                       elsif reg_addr = A_STATUS then         -- write-one-to-clear
                           if reg_wdata(1) = '1' then done_f <= '0'; end if;
                           if reg_wdata(2) = '1' then err_f  <= '0'; end if;
                       elsif reg_addr = A_COMMAND then
                           if reg_wdata(0) = '1' and enable = '1' and busy = '0' then
                               busy <= '1'; is_read_q <= reg_wdata(1); req_i <= '1';
                           end if;
                       end if;                                 -- RXDATA is read-only
                   end if;
                   if busy = '1' and eng_done = '1' then
                       busy <= '0'; done_f <= '1';
                       if eng_error = '1' then err_f <= '1'; end if;
                       if is_read_q = '1' then rx_q <= eng_rdata; end if;
                   end if;
               end if;
           end if;
       end process;

       eng_req     <= req_i;
       eng_target  <= target_q;
       eng_is_read <= is_read_q;
       eng_wdata   <= tx_q;

       reg_rdata <= "0000000" & enable                     when reg_addr = A_CTRL   else
                    "00000" & err_f & done_f & busy         when reg_addr = A_STATUS else
                    "0" & target_q                          when reg_addr = A_TARGET else
                    tx_q                                    when reg_addr = A_TXDATA else
                    rx_q                                    when reg_addr = A_RXDATA else
                    (others => '0');
   end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_shell_tb.vhd \u2014 the same sequence with assert report severity
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_cmd_shell_tb is
   end entity;

   architecture sim of i2c_cmd_shell_tb is
       signal clk       : std_logic := '0';
       signal rst_n     : std_logic := '0';
       signal reg_wr    : std_logic := '0';
       signal reg_addr  : std_logic_vector(2 downto 0) := (others => '0');
       signal reg_wdata : std_logic_vector(7 downto 0) := (others => '0');
       signal reg_rdata : std_logic_vector(7 downto 0);
       signal eng_req     : std_logic;
       signal eng_target  : std_logic_vector(6 downto 0);
       signal eng_is_read : std_logic;
       signal eng_wdata   : std_logic_vector(7 downto 0);
       signal eng_done  : std_logic := '0';
       signal eng_error : std_logic := '0';
       signal eng_rdata : std_logic_vector(7 downto 0) := (others => '0');
   begin
       dut : entity work.i2c_cmd_shell
           port map (clk=>clk, rst_n=>rst_n, reg_wr=>reg_wr, reg_addr=>reg_addr,
                     reg_wdata=>reg_wdata, reg_rdata=>reg_rdata, eng_req=>eng_req,
                     eng_target=>eng_target, eng_is_read=>eng_is_read, eng_wdata=>eng_wdata,
                     eng_done=>eng_done, eng_error=>eng_error, eng_rdata=>eng_rdata);

       clk <= not clk after 5 ns;

       stim : process
           procedure wr (a : in std_logic_vector(2 downto 0);
                         d : in std_logic_vector(7 downto 0)) is
           begin
               reg_addr <= a; reg_wdata <= d; reg_wr <= '1';
               wait until falling_edge(clk);
               reg_wr <= '0';
           end procedure;
           procedure rd (a : in std_logic_vector(2 downto 0);
                         exp : in std_logic_vector(7 downto 0);
                         what : in string) is
           begin
               reg_addr <= a;
               wait for 1 ns;
               assert reg_rdata = exp report "i2c_cmd_shell: " & what severity error;
           end procedure;
           procedure engine_completes (err : in std_logic;
                                       data : in std_logic_vector(7 downto 0)) is
           begin
               wait until falling_edge(clk);
               wait until falling_edge(clk);
               eng_rdata <= data; eng_error <= err; eng_done <= '1';
               wait until falling_edge(clk);
               eng_done <= '0'; eng_error <= '0';
           end procedure;
       begin
           wait until falling_edge(clk);
           wait until falling_edge(clk);
           rd("001", x"00", "reset: status must be clear");
           rd("100", x"00", "reset: rxdata must be clear");
           rst_n <= '1';
           wait until falling_edge(clk);

           wr("101", x"01");                                -- command while disabled
           assert eng_req = '0' report "i2c_cmd_shell: accepted while disabled" severity error;
           rd("001", x"00", "disabled: must still be idle");

           wr("000", x"01"); rd("000", x"01", "enable did not set");
           wr("010", x"48"); rd("010", x"48", "target id did not program");
           wr("011", x"3C"); rd("011", x"3C", "tx data did not load");

           wr("101", x"01");                                -- WRITE command
           assert eng_req = '1'          report "i2c_cmd_shell: eng_req not pulsed"   severity error;
           assert eng_target = "1001000" report "i2c_cmd_shell: wrong target"         severity error;
           assert eng_is_read = '0'      report "i2c_cmd_shell: wrong direction"      severity error;
           assert eng_wdata = x"3C"      report "i2c_cmd_shell: wrong wdata"          severity error;
           rd("001", x"01", "busy must be asserted");
           wait until falling_edge(clk);
           assert eng_req = '0' report "i2c_cmd_shell: eng_req is a level" severity error;

           wr("101", x"01");                                -- second command while busy
           assert eng_req = '0' report "i2c_cmd_shell: accepted while busy" severity error;
           rd("001", x"01", "must still be busy and unchanged");

           engine_completes('0', x"00");
           rd("001", x"02", "done must be set and busy cleared");
           rd("100", x"00", "a WRITE must not change rxdata");
           wr("001", x"02"); rd("001", x"00", "done write-one-to-clear failed");

           wr("101", x"03");                                -- READ command
           assert eng_is_read = '1' report "i2c_cmd_shell: read direction not passed" severity error;
           engine_completes('0', x"A5");
           rd("001", x"02", "read did not complete");
           rd("100", x"A5", "rxdata did not latch on read");
           wr("001", x"02");

           wr("101", x"01");                                -- ERROR injection
           engine_completes('1', x"00");
           rd("001", x"06", "done and error must both be set");
           rd("100", x"A5", "a failed write must not disturb rxdata");
           wr("001", x"06"); rd("001", x"00", "both flags did not clear");

           wr("100", x"FF"); rd("100", x"A5", "rxdata must be read-only");

           report "i2c_cmd_shell self-check complete" severity note;
           wait;
       end process;
   end architecture;

4d. Cross-Language Comparison

Register addresses, bit positions, reset values, the accept condition, the one-cycle request pulse, the read-latch rule, the write-one-to-clear semantics and the twelve-step stimulus are identical across all three, and all three testbenches complete at the same simulated time.

ConcernSystemVerilogVerilogVHDL
Address decodecase over reg_addrcase over reg_addrif/elsif over named constants
Read muxalways_comb + casealways @(*) + caseconcurrent conditional assignment
Readable request outputoutput read directlyoutput read directlyinternal req_i, port driven from it
Failure reporting$error with a count$display with a countassert ... report ... severity error

i2c_cmd_shell \u2014 register write, engine request, completion, acknowledgement

10 cycles
Ten clock cycles of the command shell simulation. Software writes the COMMAND register in the second cycle. In the third cycle the engine request pulses for exactly one cycle and busy is asserted. Busy remains asserted while the engine works. The engine asserts done in the sixth cycle, after which busy clears and the status done flag is set. In the ninth cycle software writes the STATUS register to clear the flag, and it is clear in the tenth.controller busycontroller busyawaiting acknowledgeawaiting acknowledgesoftware writes COMMANDsoftware writes COMMANDone-cycle request; busy setone-cycle request; busy setdone set, busy cleareddone set, busy clearedclkreg_wrreg_addridleCMDidleidleidleidleidleidleSTATidleeng_reqbusyeng_donests_donet0t1t2t3t4t5t6t7t8t9
Figure 2 \u2014 the shell\u0027s command lifecycle, taken from the testbench above. Software writes COMMAND; one cycle later the engine sees a single-cycle request and busy is set; the engine completes; busy clears and done is set until software acknowledges it by writing one to the bit. This is an RTL simulation of the register handshake \u2014 it is not I\u00B2C bus timing.

This is an RTL simulation figure of the register handshake, and the classification matters. Every signal on it is a port of the shell, and the cycle counts are the shell\u0027s real behaviour. It is not I\u00B2C bus timing — the engine\u0027s two-cycle completion here is a testbench convenience, where a real transfer takes as long as the bus takes. Confusing the two is exactly the layering error this chapter exists to prevent.

5. Assertions — The Shell\u0027s Contract as Properties

Four properties state the contract the shell promises software. They are small on purpose: Module 21 owns the protocol assertion library, and these are about the register interface.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_cmd_shell_props.sv — four register-contract properties (CHECKER; simulation only)
   module i2c_cmd_shell_props (
       input logic       clk, rst_n,
       input logic       reg_wr,
       input logic [2:0] reg_addr,
       input logic [7:0] reg_wdata,
       input logic       eng_req, eng_done,
       input logic       busy, done_f
   );
       localparam logic [2:0] A_COMMAND = 3'd5;

       // Previous-cycle history, so each property can talk about "the cycle before".
       logic acc_cmd_q, eng_req_q, busy_q, eng_done_q, done_f_q;
       always_ff @(posedge clk) begin
           acc_cmd_q  <= reg_wr && reg_addr == A_COMMAND && reg_wdata[0] && !busy;
           eng_req_q  <= eng_req;
           busy_q     <= busy;
           eng_done_q <= eng_done;
           done_f_q   <= done_f;
       end

       // CLOCKED IMMEDIATE assertions. Every signal here is synchronous to clk, so
       // sampling at the edge is well defined -- unlike Chapter 2.5's electrical
       // invariants on a resolved net, which needed deferral to avoid a transient.
       always_ff @(posedge clk) begin
           if (rst_n) begin
               // 1 -- a request is issued ONLY for an accepted command.
               assert (!eng_req || acc_cmd_q)
                   else $error("eng_req asserted without an accepted COMMAND write");
               // 2 -- a request is always accompanied by busy.
               assert (!eng_req || busy)
                   else $error("engine requested while the interface does not report busy");
               // 3 -- SINGLE OUTSTANDING: no request in the cycle after a request.
               assert (!(eng_req && eng_req_q))
                   else $error("a second request was issued immediately after the first");
               // 4 -- done is set ONLY by a completing transfer.
               assert (!(done_f && !done_f_q) || (busy_q && eng_done_q))
                   else $error("done_f rose without the engine completing a transfer");
           end
       end
   endmodule

Each property states something the shell promises software, and each is small enough to read in one pass:

  1. A request is issued only for an accepted command. Nothing other than a legal COMMAND write can poke the engine.
  2. A request is always accompanied by busy. Software never sees a transfer begin without the interface reporting it.
  3. Single outstanding. This shell is single-entry, so no second request may follow immediately.
  4. done is set only by a completing transfer. The flag cannot appear without the engine having finished.

Two things about the form are worth more than the properties themselves.

These are clocked immediate assertions, and that is a deliberate choice. The idiomatic way to express such invariants is a concurrent assertion, which reads more directly and holds continuously:

Azvya Education Pvt. Ltd.VLSI Mentor
the same invariant as a concurrent assertion — the idiomatic form (needs a simulator with SVA support)
   // Property 1 written concurrently. $past replaces the explicit history
   // register, and the assertion is checked on every clock without a surrounding
   // always block. This is the form to reach for in a real environment.
   property req_only_on_accepted_command;
       @(posedge clk) disable iff (!rst_n)
       eng_req |-> $past(reg_wr && reg_addr == A_COMMAND && reg_wdata[0] && !busy);
   endproperty
   assert property (req_only_on_accepted_command)
       else $error("eng_req asserted without an accepted COMMAND write");

The clocked-immediate version in the checker above exists because it runs in any simulator, including ones with no SVA support, while expressing exactly the same four invariants — the explicit history registers do by hand what $past does by construction. Knowing both forms is practical: the concurrent one is what you write when the tool allows it, and the immediate one is what you fall back to without giving up the check.

They are clocked, unlike Chapter 2.5\u0027s. Chapter 2.5 needed deferred assertions because its invariants were about a resolved net that settles within a time step. Here every signal is synchronous to clk, so there is a well-defined sampling point and no transient to avoid. The right assertion construct follows from what the signals are, not from habit — and recognising which situation you are in is the transferable skill.

6. Verification Connection — RAL and Cross-Interface Checking

This chapter is where I\u00B2C verification stops being a single-interface problem, and it is worth seeing the shape before Module 19 builds it.

The difficulty is that a single transfer now has three separate observable faces, and correctness is a statement about all three agreeing:

Azvya Education Pvt. Ltd.VLSI Mentor
one transfer, three faces \u2014 architecture, not code
   CPU-bus sequence          "configure target 0x48, write 0x3C"
          |
          v
   register model (RAL)      mirrored register state, predicted after each access
          |
          v
   I2C controller DUT        the shell plus the engine
          |
          v
   I2C interface             the resolved SDA/SCL conductors
          |
          v
   target responder          a model of the real device

   monitor(s) ------------------> scoreboard
                                  correlates:
                                    register command
                                    bus transaction
                                    status / interrupt result

Why a register model earns its place. A register model — UVM\u0027s RAL — holds a mirror of what the hardware\u0027s registers should contain, updated by prediction as accesses go by. That lets a test say "configure the target" rather than "write 0x48 to offset 2", and it lets a scoreboard check register state without re-reading the device.

The shell in §4 exercises the parts of RAL that are easy to get wrong, and they are worth naming concretely:

  • STATUS is not simply readable state. busy is set and cleared by hardware, done and error are set by hardware and cleared by software writing one. A model that treats STATUS as an ordinary read/write register will mispredict it constantly — the access policies have to be declared per field, not per register.
  • RXDATA is read-only and hardware-updated. Prediction cannot know its value from the access stream alone; it comes from the bus. So a scoreboard has to learn it from the I\u00B2C-side monitor, not from the register side.
  • COMMAND is write-only with a side effect. Writing it does not store a value, it starts a transfer. A naive mirror would record a value that no read will ever return.

A concise sketch of the intent, not an implementation:

Azvya Education Pvt. Ltd.VLSI Mentor
illustrative only \u2014 the field access policies that make prediction correct
   // The point is the POLICIES, not the syntax. Each one encodes something the
   // hardware does that a default read/write mirror would get wrong.
   class i2c_status_reg extends uvm_reg;
       rand uvm_reg_field busy;    // "RO"  -- hardware owns it entirely
       rand uvm_reg_field done;    // "W1C" -- hardware sets, software clears with 1
       rand uvm_reg_field error;   // "W1C" -- same
       // A model that declared these "RW" would predict the wrong value after
       // every transfer, and the scoreboard would report failures that are
       // artefacts of the model rather than defects in the design.
   endclass

Why the scoreboard has to correlate three things. A register write says a transfer was requested. The bus says what happened. Status and interrupt say what the hardware reported. A design can be wrong in the gap between any two: it can report success for a transfer the bus shows failing, report the wrong byte count, latch read data from the wrong transfer, or set done without the engine having completed. None of those is visible from one interface alone — which is precisely why this is the chapter where verification becomes a multi-interface problem.

A conceptual sequence, showing architecture rather than boilerplate:

Azvya Education Pvt. Ltd.VLSI Mentor
illustrative only \u2014 a sequence expressing intent, not register offsets
   // Written against the register model, so it survives a change of register map
   // and reads as the operation it performs.
   task body();
       regmodel.CTRL.write(status, 8'h01);          // enable
       regmodel.TARGET.write(status, 7'h48);        // which device
       regmodel.TXDATA.write(status, 8'h3C);        // payload
       regmodel.COMMAND.write(status, 8'h01);       // go -- a write transfer
       wait_for_done();                             // interrupt or polled status
       regmodel.STATUS.read(status, sts);
       // The scoreboard -- not this sequence -- is what confirms the I2C side
       // actually carried 0x3C to 0x48. A sequence that checked only STATUS would
       // pass against a controller that reported success and drove nothing.
   endtask

That last comment is the chapter\u0027s verification thesis: a test that only reads status verifies the status register, not the bus.

7. The Error Model — Where Bus Failures Become Software Facts

A controller has to report failure, because things on a board go wrong in ways software has to handle. What belongs in Module 3 is the categories and the boundary crossing, not the detection mechanisms.

CategoryRoughly what happenedWhere it is developed
No responsethe named target did not answerModule 7
Bus busy / not freethe bus was not available to startModules 5 and 13
Lost to another controlleranother controller was active, this one gave wayModule 13
Timeoutthe transfer did not progress within a boundModules 11 and 12
Configuration / disabledsoftware asked for something the controller cannot dohere

The architectural point is the one the table makes by existing: a bus-level event becomes a software-visible status bit. Software has no way to observe an unanswered address or a line held too long — those are events on a conductor. The controller observes them and translates them into a flag a driver can branch on.

That translation is a genuine design responsibility with two failure modes worth naming. A controller that reports too little leaves software unable to distinguish "the device is absent" from "the bus is broken" — different remedies entirely. A controller that reports too much makes every driver handle conditions it cannot act on. Where a real peripheral draws that line is a design decision, and error here is deliberately one bit because the shell has one abstract engine; a real one has several.

8. FPGA and ASIC Integration

The same stack, implemented two different ways. Both inherit the boundary from Module 2 and neither changes it.

On an FPGA, the controller is a soft peripheral in fabric. Its register interface attaches to whatever bus the design uses — a soft CPU\u0027s peripheral bus, or a state machine driving the registers directly. SDA and SCL are top-level inout ports constrained to package pins, and the open-drain behaviour lives at the I/O buffer exactly as Chapter 2.3 established: intent in fabric, output-enable at the boundary, pull-ups on the board. Two integration realities: the I/O bank voltage must suit the pull-up rail from Chapter 3.3, and sda_in/scl_in are asynchronous inputs that need synchronising before logic uses them — flagged here, developed in Module 19.

On an ASIC, the controller is a peripheral on the SoC interconnect. Its register block is mapped into the address space; its interrupt joins the interrupt controller; a high-throughput design may attach DMA, though for control traffic the payloads are small enough that it rarely earns its place. The pins reach the outside through pad-ring cells chosen for open-drain capability and sink strength, as Chapter 2.3 covered. Two additional realities: clock and reset domains — the controller runs from a peripheral clock unrelated to the bus, which is where the bit-timing generator gets its reference; and power domains — if the controller can be powered down while the bus stays active, the pad behaviour in that state is a system question, not an RTL one.

Neither is developed further here. Module 19 owns implementation engineering for both.

9. Debugging — The Driver That Trusted a Status Bit

Every transfer reported success and no device was ever written

Pitfall \u2014 verifying the register interface and calling it a verified controller
Buggy Code
// A new I2C controller peripheral. The register interface is clean and the driver
// is straightforward:
//
//   write CTRL    = enable
//   write TARGET  = 0x48
//   write TXDATA  = 0x3C
//   write COMMAND = go
//   poll  STATUS  until done
//   check STATUS.error == 0   -> report success
//
// The block-level regression is thorough about the REGISTER contract: reset
// values, read-back, write-one-to-clear, command-while-busy, read-only fields.
// All of it passes. The driver reports success on every transfer. The register
// interface is, in fact, correct.
//
// What nothing in that regression does is look at SDA or SCL.
Symptom

Software sees a perfectly healthy peripheral: enable works, commands are accepted, busy asserts, done arrives, error stays clear, and RXDATA returns plausible values on reads. Every layer software can observe says the transfer happened. The devices on the board disagree. The PMIC is never configured, so rails stay at their defaults -- which on this board is close enough to correct that the board boots. The EEPROM read returns the value RXDATA was left holding from reset or from an earlier transfer, which looks like a plausible board identifier. The first hard symptom appears weeks later on a board variant where the default rails are wrong, and it presents as a power problem. On a scope SDA and SCL never move at all. The bus is idle throughout.

Root Cause

The shell was verified; the engine was not connected. Because the shell's completion path is driven by eng_done, and the abstract engine in the block-level environment was a testbench model that simply asserted eng_done a couple of cycles after eng_req, every status transition software depends on was produced WITHOUT ANY BUS ACTIVITY. In the integrated design the engine's request input had been left unconnected during integration, so it never ran -- but nothing downstream of the shell was being checked, so nothing noticed. The failure is a VERIFICATION SCOPE error rather than a logic error. Each layer was correct in isolation and the seam between them was never tested. The status bits are not lying: they faithfully report what the shell was told, and the shell was told a transfer completed. Chapter 3.4's verification thesis is exactly this -- a test that reads only status verifies the status register.

Fix
// Close the loop across the two interfaces. The scoreboard must correlate what was
// REQUESTED with what APPEARED ON THE BUS, not merely with what was REPORTED:
//
//   1. Put a monitor on the I2C interface and reconstruct transfers from the
//      resolved SDA/SCL conductors -- Chapter 2.5's rule, the resolved bus and
//      never any participant's intent.
//   2. Attach a target responder model at the addressed identity so there is
//      something to answer.
//   3. Have the scoreboard require, for every accepted COMMAND: a matching
//      transaction on the bus, carrying the programmed target and payload, AND a
//      status outcome consistent with what the responder did.
//   4. Add the test that fails immediately here: assert a transfer to a target
//      identity for which NO responder exists, and require that the controller
//      reports a failure. A controller wired to nothing passes an "expect success"
//      test and fails an "expect no-response" test -- which is why negative cases
//      catch integration faults that positive ones cannot.
//
// The lab-level equivalent, once hardware exists: put a scope or analyser on the
// bus and confirm activity. A status bit is evidence about the peripheral's
// internal state, never about the conductor.

The engineering lesson: status is a report, not an observation. A controller\u0027s status register tells you what the controller believes, and a controller can believe a transfer succeeded while driving nothing — most easily when the block below it was abstract during verification and real during integration. The general rule for any layered peripheral is that each layer must be verified against the layer below it, not only against its own contract, and the cheapest test that catches a disconnected lower layer is a negative one: ask for something that should fail and require that it does.

10. Common Misconceptions

11. Reason It Through

Work these through before reading the answers.

Software writes COMMAND while STATUS.busy is set. Which block should define what happens, and what are the reasonable options?

Reasoning. The hardware must define it, because software cannot make a race disappear by being careful — the write may land in the same cycle a transfer is completing. Reasonable options are to ignore the write (what the shell in §4 does), to accept and queue it, or to accept and flag a fault. What is not acceptable is leaving it unspecified, because then the behaviour depends on internal timing and differs between implementations. Notice that "software should not do that" is not a design: a driver can be wrong, and a peripheral that behaves unpredictably when it is makes the bug unreproducible.

A driver reports every transfer succeeding and no device on the board is being configured. Where do you look first?

Answer. At the bus, with an instrument. Status is a report from the controller about its own state, and §9 shows how it can be entirely self-consistent while nothing reaches a conductor. Confirming activity on SDA and SCL takes a minute and eliminates the whole class of "the controller believes it worked" faults — whereas reading the driver again cannot, because the driver is correct.

Why does a register model need per-field access policies rather than one policy per register?

Reasoning. Because fields inside one register can have different owners. In STATUS, busy is written only by hardware, while done and error are set by hardware and cleared by software writing a one. A single register-level policy cannot express that, so prediction would be wrong for at least one field after every transfer — and the resulting failures look like design bugs while being model bugs, which is an expensive kind of noise.

A test issues a transfer to an identity for which no target model exists, and the controller reports success. Is the controller wrong?

Reasoning. Almost certainly yes, and this is the negative test from §9. An unanswered target should produce a no-response error, so reporting success means either the controller is not detecting the condition or — more commonly — it is not actually driving the bus at all. This is why negative tests find integration faults that positive tests cannot: a controller connected to nothing passes every "expect success" test.

12. Understanding Check

13. Summary

Between a driver and a conductor sit several narrow layers, each hiding the one below. Software performs loads and stores; the interconnect carries them; the register block is the software-visible contract; the engine turns a command into edges; the I/O boundary pulls conductors LOW or releases them; the board is everything outside the pin.

Software does not toggle the bus. It configures, describes a transfer, starts it with one register write, and learns the outcome from status or an interrupt. Bit-banging is a fallback with real costs, not the normal model — the split exists because hardware is good at precise slow things and software is not.

The register interface is where the design decisions live: STATUS flags are write-one-to-clear because acknowledgement must be explicit, RXDATA is read-only and hardware-updated, COMMAND is a write-only action rather than state, and busy and done are separate so a completed transfer cannot be silently missed.

The shell built in §4 owns a real contract — accept only when enabled and not busy, one-cycle request pulse, latch read data only on a read, sticky status until acknowledged — and it hands an abstract engine the work. That engine is Module 17.

For verification, this is where a single interface stops being enough. A transfer has three faces — command, bus activity, reported status — and correctness means all three agree. A register model needs per-field access policies or it mispredicts every transfer, and a test that reads only status verifies the status register. The debug case shows the consequence: a controller can report clean success while its bus never moves.

Errors cross the boundary by design: a bus-level event software cannot observe becomes a flag it can branch on, and how finely a controller reports is a genuine design decision.

14. What Comes Next

Module 3 is complete. The bus has participants with defined responsibilities, a topology that shares two conductors among many devices, a real schematic with real electrical constraints, and a path from software down to the pin.

What it still has no account of is time. Every chapter so far has said "the controller generates the clock" and "the transfer proceeds" without once saying how fast, how a bit is delimited, when a level is valid, or what a receiver is entitled to assume about when to look. Module 4 — Timing Fundamentals supplies that: the bit cell, the data-valid rule that governs every bit on the bus, and how to read a timing diagram — which is the last thing needed before the framing and addressing mechanics that Modules 5 through 8 build on top.

Browse the full path on the I\u00B2C tutorials index. For the boundary this chapter\u0027s stack terminates at, see Open-Drain Outputs; for the board outside it, Wiring a Real I\u00B2C Bus.

Continue learning

Related tutorials