Skip to content
VLSI Mentor

I²C · Module 3

Master and Slave Roles (Controller and Target)

Roles on an I²C bus describe responsibility, not data direction. A controller initiates and times a transfer; a target participates once selected. Either may transmit or receive the payload — and collapsing those two axes produces a design that writes correctly and cannot read.

Module 2 finished with a conductor that several devices can share safely: any participant may pull a line LOW or release it, a pull-up restores HIGH when everyone releases, and the resolved level is something every participant reads rather than assumes. That is a working medium.

A medium is not yet a system. Nothing so far says who is allowed to start a conversation, who decides how long it lasts, or how a device knows the traffic on the wire concerns it. Those are questions about responsibility, and this module answers them — starting here, with the two roles the bus defines and the one distinction that causes more broken I\u00B2C designs than any other.

1. Terminology — Two Names for Each Role

The specification\u0027s vocabulary changed, and both sets are in active use, so it is worth settling before anything else.

Historical termCurrent termWhat it names
mastercontrollerthe participant that initiates and times a transfer
slavetargetthe participant that responds when selected

NXP\u0027s UM10204 now uses controller and target, and this curriculum follows it. The older words are not obsolete in practice: existing datasheets, register-field names, driver source, signal names and a great deal of production RTL use master and slave, and you will read those documents for years. The useful skill is the mapping, not the preference — when a datasheet says "slave address", it means the target\u0027s address, and nothing about the hardware differs.

One deliberate exception: this chapter\u0027s title carries both terms because the curriculum entry does. From §2 onward the prose says controller and target consistently, because mixing vocabulary mid-argument is how a reader loses track of which axis is being discussed — and this chapter is entirely about keeping two axes apart.

2. What a Controller Owns

A controller is the participant that decides a transfer happens. Concretely it owns four responsibilities, and owning them is what makes it the controller:

It initiates. A transfer exists because a controller started one. Nothing begins on an idle bus without a controller acting.

It provides the timing. The controller drives SCL, which is what paces every bit of the exchange. Module 2 established that SCL is a shared node rather than a plain output — a target is permitted to influence it, and Module 12 develops how — but the controller is the participant that generates the clock in the ordinary case.

It selects the participant. The controller names which target the transfer concerns. How that name is encoded and transmitted is Module 6 and Module 7; here it matters only that the choice is the controller\u0027s.

It sets the direction and ends the transfer. The controller decides whether this transfer moves data toward the target or back from it, and the controller terminates it.

3. What a Target May and May Not Do

A target is the participant that waits. It has no way to start anything, and that single limitation defines it.

What a target does: watches the bus, recognises when it has been named, participates for the duration of the transfer, and — depending on the direction the controller chose — either accepts bytes or supplies them.

What a target does not do: begin a transfer, choose the direction, or decide when the exchange ends. A target with data ready and nothing to do about it must wait until a controller asks.

4. Role Is Not Direction — The Distinction That Breaks Designs

Here is the most valuable paragraph in the chapter.

It is extremely tempting to read controller as the one sending data and target as the one receiving it. That mapping is wrong, it is wrong in a way that survives casual testing, and it produces RTL that cannot perform half of the transfers the bus supports.

Role and data direction are independent axes.

Bus roleInitiates a transfer?Can transmit payload?Can receive payload?
ControllerYes — alwaysYes, on a writeYes, on a read
TargetNoYes, on a readYes, on a write

Read the two right-hand columns: both roles can do both things. What differs is the left column, and only the left column.

Work the two cases concretely:

A controller writes to a target. The controller initiated, times the bus and chose the direction; it also supplies the bytes. Controller transmits, target receives.

A controller reads from a target. The controller still initiated, still times the bus, still chose the direction, and still ends the transfer. But the bytes come from the target. Target transmits, controller receives — and the roles have not changed at all.

The second case is where the confusion bites. A target supplying data has not become a controller. It is a target doing exactly what a target does when the controller selected the read direction. The controller is still in charge of a transfer whose payload happens to be flowing toward it.

Two independent axes on an I2C bus. The responsibility axis is fixed: the controller initiates the transfer, provides the clock, selects the target and ends the transfer, while the target waits and participates when selected. The payload direction axis varies: on a write the controller transmits and the target receives, and on a read the target transmits and the controller receives. Roles are unchanged in both cases.Responsibilityfixed for the transferControllerinitiates, times, selects,endsTargetwaits, then participatesPayload directionchosen per transferOn a writecontroller sends, targetacceptsOn a readtarget sends, controlleracceptsThe two axes areindependentdirection never changes who isin chargeor12
Figure 1 \u2014 responsibility versus payload direction. The upper row is fixed for a given transfer: the controller initiates, times and terminates it. The lower row flips with the direction the controller chose. Confusing the two rows is what produces a design that can write and cannot read.

5. RTL Connection — Responsibility Expressed as an Interface

The cleanest way to see that role and direction are separate is to write the two sides\u0027 interfaces and notice what each one has.

This model is architectural. It carries no SDA or SCL, no framing, no addressing and no bit-level timing — those belong to Modules 5 through 8 and to Module 17. What it does carry is the responsibility structure, and it carries it in a form a simulator can check: the controller side has a request port and the target side does not.

One deliberate detail makes the point testable. The target side has a tgt_req input that the design ignores completely. It exists so a testbench can assert it and prove that nothing happens — a target asking for a transfer is not a mechanism the bus has.

5a. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_role_model.sv \u2014 responsibility structure (ARCHITECTURAL MODEL; no SDA/SCL, no framing)
   module i2c_role_model #(
       parameter int ID_WIDTH = 3
   )(
       input  logic                clk,
       input  logic                rst_n,
       // --- controller side: the ONLY side with a request port -----------------
       input  logic                ctrl_req,
       input  logic [ID_WIDTH-1:0] ctrl_target_id,
       input  logic                ctrl_is_read,
       input  logic [7:0]          ctrl_wdata,
       output logic                ctrl_busy,
       output logic                ctrl_done,
       output logic [7:0]          ctrl_rdata,
       // --- target side: participates only when selected -----------------------
       input  logic                tgt_req,        // DELIBERATELY IGNORED
       output logic                tgt_selected,
       output logic                tgt_is_read,
       output logic                tgt_rx_valid,
       output logic [7:0]          tgt_rx_data,
       input  logic [7:0]          tgt_tx_data
   );
       typedef enum logic [1:0] { IDLE, XFER, FINISH } state_e;
       state_e state;
       logic [ID_WIDTH-1:0] id_q;
       logic                rd_q;
       logic [7:0]          wd_q;

       always_ff @(posedge clk) begin
           if (!rst_n) begin
               state <= IDLE; ctrl_busy <= 1'b0; ctrl_done <= 1'b0;
               ctrl_rdata <= '0; tgt_selected <= 1'b0; tgt_is_read <= 1'b0;
               tgt_rx_valid <= 1'b0; tgt_rx_data <= '0;
               id_q <= '0; rd_q <= 1'b0; wd_q <= '0;
           end else begin
               ctrl_done    <= 1'b0;
               tgt_rx_valid <= 1'b0;
               case (state)
                   IDLE: begin
                       // tgt_req is NOT examined. A target cannot start a transfer.
                       if (ctrl_req) begin
                           id_q <= ctrl_target_id; rd_q <= ctrl_is_read; wd_q <= ctrl_wdata;
                           ctrl_busy <= 1'b1; tgt_selected <= 1'b1; tgt_is_read <= ctrl_is_read;
                           state <= XFER;
                       end
                   end
                   XFER: begin
                       if (rd_q) ctrl_rdata <= tgt_tx_data;   // target TRANSMITS
                       else begin tgt_rx_valid <= 1'b1; tgt_rx_data <= wd_q; end
                       state <= FINISH;
                   end
                   FINISH: begin
                       ctrl_busy <= 1'b0; ctrl_done <= 1'b1; tgt_selected <= 1'b0;
                       state <= IDLE;
                   end
                   default: state <= IDLE;
               endcase
           end
       end
   endmodule

The testbench is where the chapter\u0027s thesis becomes a check rather than a claim. It holds tgt_req high for four cycles and requires that nothing starts; then it runs a write and a read and requires that the role outputs are identical in both, while only the payload moves the other way.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_role_model_tb.sv \u2014 self-checking: a target never initiates, and role survives direction
   module i2c_role_model_tb;
       localparam int ID_WIDTH = 3;
       logic clk = 1'b0, rst_n, ctrl_req, ctrl_is_read, tgt_req;
       logic [ID_WIDTH-1:0] ctrl_target_id;
       logic [7:0] ctrl_wdata, ctrl_rdata, tgt_rx_data, tgt_tx_data;
       logic ctrl_busy, ctrl_done, tgt_selected, tgt_is_read, tgt_rx_valid;
       int errors = 0;

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

       task automatic issue(input logic rd, input logic [7:0] d, input logic [ID_WIDTH-1:0] id);
           ctrl_target_id = id; ctrl_is_read = rd; ctrl_wdata = d; ctrl_req = 1'b1;
           @(negedge clk); ctrl_req = 1'b0;
       endtask

       initial begin
           rst_n=0; ctrl_req=0; tgt_req=0; ctrl_is_read=0; ctrl_target_id='0;
           ctrl_wdata='0; tgt_tx_data=8'h5A;
           repeat (2) @(negedge clk);
           if (ctrl_busy!==1'b0 || tgt_selected!==1'b0) begin $error("not idle in reset"); errors++; end
           rst_n=1; @(negedge clk);

           // 1 -- A TARGET CANNOT INITIATE. Hold tgt_req high for several cycles.
           tgt_req = 1'b1;
           repeat (4) begin
               @(negedge clk);
               if (ctrl_busy!==1'b0 || tgt_selected!==1'b0) begin
                   $error("tgt_req started a transfer -- a target must not initiate"); errors++;
               end
           end
           tgt_req = 1'b0;

           // 2 -- WRITE: controller transmits, target receives. Role = controller.
           issue(1'b0, 8'h3C, 3'd2);
           @(negedge clk);                          // transfer cycle
           if (ctrl_busy!==1'b1)     begin $error("busy not asserted after request"); errors++; end
           if (tgt_selected!==1'b1)  begin $error("target not selected"); errors++; end
           if (tgt_is_read!==1'b0)   begin $error("direction wrong on write"); errors++; end
           if (tgt_rx_valid!==1'b1 || tgt_rx_data!==8'h3C) begin
               $error("target did not receive the written byte"); errors++; end
           @(negedge clk);                          // completion cycle
           if (ctrl_done!==1'b1)  begin $error("done not pulsed"); errors++; end
           if (ctrl_busy!==1'b0)  begin $error("busy not cleared"); errors++; end

           // 3 -- READ: target transmits, controller receives. ROLE IS UNCHANGED.
           @(negedge clk);
           tgt_tx_data = 8'hA5;
           issue(1'b1, 8'h00, 3'd5);
           @(negedge clk);                          // transfer cycle
           if (ctrl_busy!==1'b1)    begin $error("busy not asserted on read"); errors++; end
           if (tgt_selected!==1'b1) begin $error("target not selected on read"); errors++; end
           if (tgt_is_read!==1'b1)  begin $error("direction wrong on read"); errors++; end
           @(negedge clk);                          // completion cycle
           if (ctrl_rdata!==8'hA5) begin
               $error("controller did not receive target data (got %h)", ctrl_rdata); errors++; end
           // THE KEY CHECK: the target supplied the payload, and the controller is
           // STILL the controller -- it requested, it timed, it finished the transfer.
           if (ctrl_done!==1'b1) begin $error("controller did not terminate the read"); errors++; end

           if (errors==0) $display("PASS: role is independent of data direction; a target never initiates");
           else           $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule

5b. Verilog

Same architecture; reg/wire typing, a localparam state encoding instead of an enum, and $display reporting.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_role_model.v \u2014 the same responsibility structure in Verilog (ARCHITECTURAL MODEL)
   module i2c_role_model #(
       parameter ID_WIDTH = 3
   )(
       input  wire                clk,
       input  wire                rst_n,
       input  wire                ctrl_req,
       input  wire [ID_WIDTH-1:0] ctrl_target_id,
       input  wire                ctrl_is_read,
       input  wire [7:0]          ctrl_wdata,
       output reg                 ctrl_busy,
       output reg                 ctrl_done,
       output reg  [7:0]          ctrl_rdata,
       input  wire                tgt_req,          // DELIBERATELY IGNORED
       output reg                 tgt_selected,
       output reg                 tgt_is_read,
       output reg                 tgt_rx_valid,
       output reg  [7:0]          tgt_rx_data,
       input  wire [7:0]          tgt_tx_data
   );
       localparam IDLE = 2'd0, XFER = 2'd1, FINISH = 2'd2;
       reg [1:0]          state;
       reg [ID_WIDTH-1:0] id_q;
       reg                rd_q;
       reg [7:0]          wd_q;

       always @(posedge clk) begin
           if (!rst_n) begin
               state <= IDLE; ctrl_busy <= 1'b0; ctrl_done <= 1'b0;
               ctrl_rdata <= 8'h00; tgt_selected <= 1'b0; tgt_is_read <= 1'b0;
               tgt_rx_valid <= 1'b0; tgt_rx_data <= 8'h00;
               id_q <= {ID_WIDTH{1'b0}}; rd_q <= 1'b0; wd_q <= 8'h00;
           end else begin
               ctrl_done    <= 1'b0;
               tgt_rx_valid <= 1'b0;
               case (state)
                   IDLE: if (ctrl_req) begin        // tgt_req is not examined
                       id_q <= ctrl_target_id; rd_q <= ctrl_is_read; wd_q <= ctrl_wdata;
                       ctrl_busy <= 1'b1; tgt_selected <= 1'b1; tgt_is_read <= ctrl_is_read;
                       state <= XFER;
                   end
                   XFER: begin
                       if (rd_q) ctrl_rdata <= tgt_tx_data;
                       else begin tgt_rx_valid <= 1'b1; tgt_rx_data <= wd_q; end
                       state <= FINISH;
                   end
                   FINISH: begin
                       ctrl_busy <= 1'b0; ctrl_done <= 1'b1; tgt_selected <= 1'b0;
                       state <= IDLE;
                   end
                   default: state <= IDLE;
               endcase
           end
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_role_model_tb.v \u2014 the same checks in Verilog idiom
   module i2c_role_model_tb;
       parameter ID_WIDTH = 3;
       reg clk, rst_n, ctrl_req, ctrl_is_read, tgt_req;
       reg  [ID_WIDTH-1:0] ctrl_target_id;
       reg  [7:0] ctrl_wdata, tgt_tx_data;
       wire [7:0] ctrl_rdata, tgt_rx_data;
       wire ctrl_busy, ctrl_done, tgt_selected, tgt_is_read, tgt_rx_valid;
       integer errors, i;

       i2c_role_model #(.ID_WIDTH(ID_WIDTH)) dut (
           .clk(clk), .rst_n(rst_n), .ctrl_req(ctrl_req), .ctrl_target_id(ctrl_target_id),
           .ctrl_is_read(ctrl_is_read), .ctrl_wdata(ctrl_wdata), .ctrl_busy(ctrl_busy),
           .ctrl_done(ctrl_done), .ctrl_rdata(ctrl_rdata), .tgt_req(tgt_req),
           .tgt_selected(tgt_selected), .tgt_is_read(tgt_is_read),
           .tgt_rx_valid(tgt_rx_valid), .tgt_rx_data(tgt_rx_data), .tgt_tx_data(tgt_tx_data));

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

       task issue;
           input rd; input [7:0] d; input [ID_WIDTH-1:0] id;
           begin
               ctrl_target_id = id; ctrl_is_read = rd; ctrl_wdata = d; ctrl_req = 1'b1;
               @(negedge clk); ctrl_req = 1'b0;
           end
       endtask

       initial begin
           errors=0; rst_n=0; ctrl_req=0; tgt_req=0; ctrl_is_read=0;
           ctrl_target_id=0; ctrl_wdata=0; tgt_tx_data=8'h5A;
           repeat (2) @(negedge clk);
           if (ctrl_busy!==1'b0 || tgt_selected!==1'b0) begin
               $display("FAIL: not idle in reset"); errors=errors+1; end
           rst_n=1; @(negedge clk);

           tgt_req = 1'b1;                                  // a target cannot initiate
           for (i=0; i<4; i=i+1) begin
               @(negedge clk);
               if (ctrl_busy!==1'b0 || tgt_selected!==1'b0) begin
                   $display("FAIL: tgt_req started a transfer"); errors=errors+1; end
           end
           tgt_req = 1'b0;

           issue(1'b0, 8'h3C, 3'd2);                        // WRITE
           @(negedge clk);
           if (ctrl_busy!==1'b1)    begin $display("FAIL: busy not asserted"); errors=errors+1; end
           if (tgt_selected!==1'b1) begin $display("FAIL: target not selected"); errors=errors+1; end
           if (tgt_is_read!==1'b0)  begin $display("FAIL: direction wrong on write"); errors=errors+1; end
           if (tgt_rx_valid!==1'b1 || tgt_rx_data!==8'h3C) begin
               $display("FAIL: target did not receive the byte"); errors=errors+1; end
           @(negedge clk);
           if (ctrl_done!==1'b1) begin $display("FAIL: done not pulsed"); errors=errors+1; end
           if (ctrl_busy!==1'b0) begin $display("FAIL: busy not cleared"); errors=errors+1; end

           @(negedge clk);
           tgt_tx_data = 8'hA5;
           issue(1'b1, 8'h00, 3'd5);                        // READ
           @(negedge clk);
           if (ctrl_busy!==1'b1)    begin $display("FAIL: busy not asserted on read"); errors=errors+1; end
           if (tgt_selected!==1'b1) begin $display("FAIL: target not selected on read"); errors=errors+1; end
           if (tgt_is_read!==1'b1)  begin $display("FAIL: direction wrong on read"); errors=errors+1; end
           @(negedge clk);
           if (ctrl_rdata!==8'hA5) begin
               $display("FAIL: controller did not receive target data"); errors=errors+1; end
           if (ctrl_done!==1'b1) begin
               $display("FAIL: controller did not terminate the read"); errors=errors+1; end

           if (errors==0) $display("PASS: role is independent of data direction; a target never initiates");
           else           $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule

5c. VHDL

VHDL contributes the clearest statement of the state machine, because an enumerated type names the states rather than encoding them — type state_t is (IDLE, XFER, FINISH) is the design intent written down, and the tool chooses the encoding.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_role_model.vhd \u2014 the same structure in VHDL (ARCHITECTURAL MODEL; enumerated state type)
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_role_model is
       generic ( ID_WIDTH : positive := 3 );
       port (
           clk            : in  std_logic;
           rst_n          : in  std_logic;
           ctrl_req       : in  std_logic;
           ctrl_target_id : in  std_logic_vector(ID_WIDTH-1 downto 0);
           ctrl_is_read   : in  std_logic;
           ctrl_wdata     : in  std_logic_vector(7 downto 0);
           ctrl_busy      : out std_logic;
           ctrl_done      : out std_logic;
           ctrl_rdata     : out std_logic_vector(7 downto 0);
           tgt_req        : in  std_logic;                      -- DELIBERATELY IGNORED
           tgt_selected   : out std_logic;
           tgt_is_read    : out std_logic;
           tgt_rx_valid   : out std_logic;
           tgt_rx_data    : out std_logic_vector(7 downto 0);
           tgt_tx_data    : in  std_logic_vector(7 downto 0)
       );
   end entity;

   architecture rtl of i2c_role_model is
       type state_t is (IDLE, XFER, FINISH);
       signal state : state_t := IDLE;
       signal id_q  : std_logic_vector(ID_WIDTH-1 downto 0) := (others => '0');
       signal rd_q  : std_logic := '0';
       signal wd_q  : std_logic_vector(7 downto 0) := (others => '0');
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   state <= IDLE; ctrl_busy <= '0'; ctrl_done <= '0';
                   ctrl_rdata <= (others => '0'); tgt_selected <= '0'; tgt_is_read <= '0';
                   tgt_rx_valid <= '0'; tgt_rx_data <= (others => '0');
                   id_q <= (others => '0'); rd_q <= '0'; wd_q <= (others => '0');
               else
                   ctrl_done    <= '0';
                   tgt_rx_valid <= '0';
                   case state is
                       when IDLE =>
                           -- tgt_req is not examined: a target cannot initiate.
                           if ctrl_req = '1' then
                               id_q <= ctrl_target_id; rd_q <= ctrl_is_read; wd_q <= ctrl_wdata;
                               ctrl_busy <= '1'; tgt_selected <= '1'; tgt_is_read <= ctrl_is_read;
                               state <= XFER;
                           end if;
                       when XFER =>
                           if rd_q = '1' then
                               ctrl_rdata <= tgt_tx_data;          -- target TRANSMITS
                           else
                               tgt_rx_valid <= '1'; tgt_rx_data <= wd_q;
                           end if;
                           state <= FINISH;
                       when FINISH =>
                           ctrl_busy <= '0'; ctrl_done <= '1'; tgt_selected <= '0';
                           state <= IDLE;
                   end case;
               end if;
           end if;
       end process;
   end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_role_model_tb.vhd \u2014 the same checks with assert report severity
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_role_model_tb is
   end entity;

   architecture sim of i2c_role_model_tb is
       constant ID_WIDTH : positive := 3;
       signal clk            : std_logic := '0';
       signal rst_n          : std_logic := '0';
       signal ctrl_req       : std_logic := '0';
       signal ctrl_target_id : std_logic_vector(ID_WIDTH-1 downto 0) := (others => '0');
       signal ctrl_is_read   : std_logic := '0';
       signal ctrl_wdata     : std_logic_vector(7 downto 0) := (others => '0');
       signal ctrl_busy      : std_logic;
       signal ctrl_done      : std_logic;
       signal ctrl_rdata     : std_logic_vector(7 downto 0);
       signal tgt_req        : std_logic := '0';
       signal tgt_selected   : std_logic;
       signal tgt_is_read    : std_logic;
       signal tgt_rx_valid   : std_logic;
       signal tgt_rx_data    : std_logic_vector(7 downto 0);
       signal tgt_tx_data    : std_logic_vector(7 downto 0) := x"5A";
   begin
       dut : entity work.i2c_role_model
           generic map (ID_WIDTH => ID_WIDTH)
           port map (clk => clk, rst_n => rst_n, ctrl_req => ctrl_req,
                     ctrl_target_id => ctrl_target_id, ctrl_is_read => ctrl_is_read,
                     ctrl_wdata => ctrl_wdata, ctrl_busy => ctrl_busy, ctrl_done => ctrl_done,
                     ctrl_rdata => ctrl_rdata, tgt_req => tgt_req, tgt_selected => tgt_selected,
                     tgt_is_read => tgt_is_read, tgt_rx_valid => tgt_rx_valid,
                     tgt_rx_data => tgt_rx_data, tgt_tx_data => tgt_tx_data);

       clk <= not clk after 5 ns;

       stim : process
           procedure issue (rd : in std_logic;
                            d  : in std_logic_vector(7 downto 0);
                            id : in std_logic_vector(ID_WIDTH-1 downto 0)) is
           begin
               ctrl_target_id <= id; ctrl_is_read <= rd; ctrl_wdata <= d; ctrl_req <= '1';
               wait until falling_edge(clk);
               ctrl_req <= '0';
           end procedure;
       begin
           wait until falling_edge(clk);
           wait until falling_edge(clk);
           assert ctrl_busy = '0' and tgt_selected = '0'
               report "not idle during reset" severity error;
           rst_n <= '1';
           wait until falling_edge(clk);

           -- A TARGET CANNOT INITIATE.
           tgt_req <= '1';
           for i in 0 to 3 loop
               wait until falling_edge(clk);
               assert ctrl_busy = '0' and tgt_selected = '0'
                   report "tgt_req started a transfer -- a target must not initiate" severity error;
           end loop;
           tgt_req <= '0';

           -- WRITE: controller transmits, target receives.
           issue('0', x"3C", "010");
           wait until falling_edge(clk);
           assert ctrl_busy    = '1' report "busy not asserted"            severity error;
           assert tgt_selected = '1' report "target not selected"          severity error;
           assert tgt_is_read  = '0' report "direction wrong on write"     severity error;
           assert tgt_rx_valid = '1' and tgt_rx_data = x"3C"
               report "target did not receive the written byte" severity error;
           wait until falling_edge(clk);
           assert ctrl_done = '1' report "done not pulsed"  severity error;
           assert ctrl_busy = '0' report "busy not cleared" severity error;

           -- READ: target transmits, controller receives. ROLE IS UNCHANGED.
           wait until falling_edge(clk);
           tgt_tx_data <= x"A5";
           issue('1', x"00", "101");
           wait until falling_edge(clk);
           assert ctrl_busy    = '1' report "busy not asserted on read"   severity error;
           assert tgt_selected = '1' report "target not selected on read" severity error;
           assert tgt_is_read  = '1' report "direction wrong on read"     severity error;
           wait until falling_edge(clk);
           assert ctrl_rdata = x"A5"
               report "controller did not receive target data" severity error;
           assert ctrl_done = '1'
               report "controller did not terminate the read" severity error;

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

5d. What the Three Models Share

Ports, widths, the synchronous active-low reset, the three-state sequence, the stimulus and the expected results are identical, and all three testbenches complete at the same simulated time. The only language-level differences are the state encoding (enum / localparam / enumerated type) and the failure-reporting idiom.

What the model abstracts away, stated plainly so nothing here is mistaken for a controller: there is no SDA or SCL, no START or STOP, no address encoding, no acknowledgement, no bit-level timing, and a transfer completes in a fixed two cycles rather than taking as long as a real bus takes. The bit-level engine is deliberately absent — it is taught in Module 17, after framing, timing, acknowledgement, stretching and arbitration have been established. Copying this as a controller would produce something that compiles and cannot talk to a device.

What it does establish is worth keeping: the controller side owns a request port, the target side owns none, and both directions of payload leave the role outputs untouched.

6. Verification Connection — Roles Shape the Environment

The role split has an immediate consequence for how a testbench is organised, and it follows directly from Module 2\u0027s driver/monitor distinction.

A component that plays a controller needs to generate activity: it decides when transfers happen, which target they name and which direction they take. A component that plays a target needs to react: it waits to be named and then responds within the timing somebody else set. Those are different jobs, and an environment normally has both — one controller agent producing stimulus, and one or more target models behaving like real devices.

The asymmetry matters because it decides where interesting bugs are found. A target model is where you make a device behave badly — respond late, refuse, hold the clock, supply the wrong byte count — and a controller that only ever met well-behaved targets has not been verified. That is why a serious I\u00B2C environment invests more in its target models than the word "model" suggests. Modules 19 through 21 build them.

Nothing here requires UVM yet. The architectural point is the split itself: generate on the controller side, react on the target side, and observe the resolved bus separately from either.

7. Debugging — The Read That Was Blamed on the Wrong Device

The controller that could write and not read \u2014 role confused with direction

Pitfall \u2014 treating a read as though the target takes over the bus
Buggy Code
// A first I2C controller design. Writes work perfectly against every device on
// the board. Reads return nothing useful. The designer reasons about the read
// like this:
//
//   "On a write I am the transmitter, so I drive the data.
//    On a read the TARGET is the transmitter, so the target is driving --
//    therefore on a read the target is in charge of the transfer."
//
// and builds the read path to match: after selecting the target, the controller
// stops generating the clock and waits for the data to arrive.
//
// It is a coherent-sounding argument. Every step follows from the one before.
// The premise is wrong.
Symptom

Writes succeed against every device. Reads hang or return a fixed value. On a capture the transfer starts correctly -- the target is named, the direction is requested -- and then the bus simply stops: SCL never advances past the point where the read payload should begin. Nothing is electrically wrong. No device is holding a line. The bus is idle-but-not-free, waiting for a clock that is never coming. Because writes work, the designer concludes the controller is basically correct and starts suspecting the target: reading its datasheet again, trying a different part, suspecting the address. All of that is the wrong layer.

Root Cause

Role was confused with data direction. A read changes WHICH PARTICIPANT SUPPLIES THE PAYLOAD; it changes nothing about who is responsible for the transfer. The controller still initiates, still provides the clock for every bit including the bits the target is supplying, still chose the direction, and still terminates the exchange. A target cannot generate the clock -- that was never one of its capabilities -- so a controller that stops clocking during a read has removed the only thing advancing the transfer. The deeper error is architectural rather than a coding slip: the design has two different notions of "who is in charge" depending on payload direction, when the bus has exactly one, fixed for the whole transfer.

Fix
// Keep the roles fixed and change only the direction of the payload:
//
//   write: controller initiates, clocks, and SUPPLIES bytes
//   read:  controller initiates, clocks, and ACCEPTS bytes
//
// In both cases the controller generates every clock and terminates the transfer.
// In the architectural model in this chapter that is exactly why ctrl_busy,
// tgt_selected and the completion handshake are IDENTICAL in the write and read
// tests -- only tgt_rx_valid versus ctrl_rdata differs.
//
// The verification that catches it: run a read and a write and compare the ROLE
// outputs, not just the data. A testbench that only checks payload passes a design
// with this bug on writes and never exercises the claim that roles are invariant.
// The chapter testbench checks ctrl_busy, tgt_selected and completion in both
// directions for precisely this reason.

The engineering lesson: when a bus works in one direction and not the other, suspect a role error before a device error. Direction-specific failure is the signature of a design that has encoded "who is in charge" as a function of payload direction — and the fastest confirmation is to check whether the timing is still being generated when the payload reverses.

8. Common Misconceptions

9. Reason It Through

Work these through before reading the answers.

A temperature sensor supplies four bytes to a processor during a read transfer. A colleague says the sensor was acting as the controller for those four bytes because it was the one putting data on SDA.

Is the colleague right? No. The sensor supplied a payload; it did not initiate the transfer, did not generate the clock that moved those four bytes, did not choose the direction, and cannot end the exchange. Every responsibility that defines a controller stayed with the processor.

What would have to be true for the sensor to be a controller? It would have to start a transfer on an idle bus of its own accord and generate the timing for it. Some devices can do that — a device capable of acting as a controller — but supplying read data is not that, and no amount of data flowing outward makes it that.

A design must read a register from a target. The engineer asks: during the bytes the target is supplying, who is driving SCL?

Answer, and why it matters. The controller, for every bit, including the bits it is receiving. This is the question whose wrong answer produces the bug in §7, and it is worth being able to answer instantly: the clock belongs to the transfer, and the transfer belongs to the controller.

Sketch the interface a block needs if it must act as a controller and as a target at different times.

Reasoning. It needs both structures: a request path so it can initiate, and a selection-detection path so it can notice when somebody names it. Notice what that implies — the two are not variations of one interface, they are genuinely separate responsibilities that happen to share a physical bus. That is why devices capable of both are meaningfully more complex, and why Chapter 3.2 treats the multi-controller case as an architectural step rather than a configuration option.

10. Understanding Check

11. Summary

The bus defines two roles, and the current specification calls them controller and target where older documents say master and slave. The mapping is worth knowing because both vocabularies remain in use.

A controller initiates a transfer, generates the timing on SCL, selects which target is involved, sets the direction and terminates the exchange. A target waits, recognises when it has been named, and participates for the duration — it cannot start anything, choose a direction or end a transfer.

The distinction to carry forward is that role and payload direction are independent axes. Both roles can transmit and both can receive; only the controller initiates. A target supplying read data is a target doing its job, on a clock the controller is still generating, in a transfer the controller will end. Collapsing the two axes produces a design that writes correctly and cannot read, and the failure looks like a device problem while being an architectural one.

Two behaviours that appear to contradict this do not: a target may hold SCL to buy time inside somebody else\u0027s transfer, and a device may drive a separate interrupt pin to ask a controller to come and read it. Neither is initiation. The electrical layer is symmetric — Module 2 made every participant able to pull any line LOW — while the responsibility layer deliberately is not.

12. What Comes Next

One controller and one target is the simplest arrangement and not the interesting one. Chapter 3.2 puts many targets on the same pair of conductors and confronts the question that makes I\u00B2C what it is: if every device is physically connected to the same wires and sees everything, how does exactly one of them participate? That is the gap between physical broadcast and logical selection, and it also opens the case where more than one participant is capable of being a controller.

Browse the full path on the I\u00B2C tutorials index. For the electrical foundation this chapter assumes, see Wired-AND and SDA and SCL.

Continue learning