Skip to content
VLSI Mentor

I²C · Module 1

Where I²C Lives — Boards, SoCs and Real Devices

Place the derived bus in a real system: the host controller inside an SoC or FPGA, the regulators, sensors, memories and clock devices attached to it, and what each one is actually doing. The traffic turns out to have a specific shape — control plane, not data plane — and that shape is why the bus remains useful.

Chapter 1.2 derived a shape: a shared pair of conductors, one carrying serialised information and one carrying the timing that makes it readable, with participants named logically because the wiring no longer names them. A derived shape is not yet an engineering fact. It becomes one when you can point at a board and say which part is the host, which parts are attached, what each is being asked to do, and why the traffic between them fits a bus built on those particular trades.

That is this chapter's job. It is deliberately not a tour of devices. The devices are here because their jobs have a common profile, and recognising that profile is the thing worth carrying forward — it is what lets you look at an unfamiliar peripheral and predict, before reading its datasheet, that it will probably be on this bus.

1. A Representative Board

Consider a system built around an SoC, an MCU or an FPGA. Whatever the host is, the same situation recurs: a handful of supporting devices exist that the host must configure at start-up and consult occasionally afterwards, none of which the system is waiting on.

A host SoC, MCU or FPGA contains an I²C controller block, which connects to a single shared two-wire bus. Attached to that bus are a power-management IC, a temperature sensor, an EEPROM, a real-time clock and a clock generator. Every device shares the same two conductors and is distinguished by the address it answers to.Host SoC / MCU / FPGAI²C controller block + driversoftwareShared two-wire busone pair of conductors, manydevicesPMICrail setup, sequencing,telemetryTemp sensorperiodic readings, thermallimitsEEPROMboard ID, calibration, serialnumberRTCwall-clock time across powercyclesClock generatoroutput frequencies, enablesone interface12
Figure 1 — a representative arrangement. One controller inside the host drives one shared pair of conductors; every supporting device attaches to the same pair and is distinguished by the address it answers to.

The figure is the third row of Chapter 1.2's progression, populated. What it adds is the observation that the five attached devices have nothing in common as components — a regulator, a thermometer, a memory, a timekeeper and a frequency source are unrelated parts — and yet they are all here. Something other than device category put them on the same bus.

2. What Each Device Is Actually Doing

The common factor is the nature of the exchange, so it is worth being specific about what the host wants from each one. None of these descriptions is a device tutorial; each is one sentence about a job and one about the traffic that job produces.

The PMIC generates and sequences the supply rails the rest of the board needs. The host sets output voltages, enables rails in a required order, and later reads back status such as whether a rail is in regulation or a fault has latched. The traffic is a burst of writes at start-up and occasional reads afterwards.

The temperature sensor reports a physical quantity that changes slowly. The host configures limits once and then reads a value periodically — periodically meaning at a rate set by how fast a board can actually heat up, which is not fast.

The EEPROM holds small persistent facts about this particular board: an identifier, a hardware revision, a serial number, calibration constants measured during manufacturing test. It is read during boot and written rarely, often only in the factory.

The RTC keeps wall-clock time across power cycles. The host reads it at start-up to learn the time and writes it when the time is set. In between it is idle.

The clock generator produces the frequencies other parts of the board need. The host programs its outputs during initialisation and generally leaves them alone.

Read those five together and a pattern is unmistakable. Each exchange is small, each is infrequent, and in no case is the processor's work blocked while waiting for it. Whether a temperature reading takes ten microseconds or a thousand changes nothing about the system, because nothing downstream is stalled on it.

3. The Traffic Has a Shape

That pattern is worth naming precisely, because it is the property the bus was designed against.

The traffic on this bus is bursty at start-up and sparse afterwards. Bringing a board up means programming the regulator, configuring the clock generator, reading the board's identity out of the memory and setting up the sensor — a concentrated sequence of small transfers, all of which happen once. After that the bus is nearly idle, waking up for a periodic sensor reading or an occasional status check.

The transfers themselves are small in a specific way: they are register-sized. Almost every device here presents itself to the host as a set of numbered locations that can be written and read — set this location to that value; tell me what this location contains. A transfer therefore tends to move a handful of bytes, not a block.

Chapter 1.2 showed that this bus buys conductors with intervals and buys scale with coordination. Traffic that is small, sparse and unhurried is exactly the traffic for which both of those are nearly free. The bus is not merely adequate for this profile; it was designed for it, and that is the whole reason a deliberately narrow, deliberately unhurried interface has survived decades of everything else getting faster.

4. Control Plane and Data Plane

Generalise the observation, because this is the distinction that makes the rest of a board's architecture legible.

A system's interfaces divide into two rough categories by what they carry. Data-plane interfaces carry the payload the system exists to move or process — the samples, the packets, the pixels, the memory traffic a processor is stalled on. Control-plane interfaces carry the information that makes the data plane work correctly: configuration written before anything runs, status read to confirm it is running, identification, calibration, and low-rate measurements used to supervise rather than to process.

I²C is a control-plane interface, and essentially every property derived in Chapter 1.2 follows from that: sharing is affordable because control traffic is sparse; serialisation is affordable because nothing is waiting; a modest signalling rate is sufficient because the demand is small; and spending a conductor on timing rather than on throughput is the right call because interoperability across unrelated devices matters more than transfer rate.

The distinction also explains a fact that otherwise looks odd: a modern SoC with an extremely fast memory interface still talks to its power regulator over a slow shared bus. Those are not competing choices. They are different jobs, correctly assigned.

5. Where the Bus Sits Inside the Host

Everything so far has been outside the package. Look inward, because that is where an RTL or verification engineer meets this.

The host does not manipulate the two conductors directly from software. Inside the chip there is a controller block — a piece of hardware whose job is to turn requests into activity on the bus and report what came back. Software interacts with that block through registers: it describes the transfer it wants, starts it, and later learns how it ended. The block handles the signalling.

This produces a useful two-level picture. Outside the package, devices are distinguished by the address they answer to. Inside the package, the controller block is itself just another addressable peripheral of the host's internal bus — which is the same control-plane pattern one level down, and why the on-chip control buses this curriculum cross-references exist for the same reasons.

The bus also has roles: one participant initiates transfers and supplies the timing, the others respond when named. Chapter 3.1 defines those roles properly, including how the specification's terminology has evolved. Module 1 needs only the asymmetry — somebody starts transfers and everybody else answers — because that asymmetry is what makes the comparison in Chapter 1.4 meaningful.

6. Where RTL, DV and FPGA Engineers Meet This Bus

The placement above is not background. Each of the three audiences this curriculum serves encounters the bus at a specific, predictable point.

An RTL designer either implements one of the two sides or integrates somebody else's. Implementing the host-side controller means building the block described in §5 — a register interface facing the internal bus, and logic facing the two conductors. Implementing the device side means the opposite: recognising your own address on a shared medium and exposing a register file to whoever asked. Integration is more common than implementation, and it is not trivial either: the block has to be connected to the host's internal control bus, its pins routed to the package, and its behaviour under reset defined. Modules 17 and 18 build both sides.

A verification engineer meets it as a problem with an unusual amount of environment in it. Verifying a host controller means modelling devices that respond correctly — and, more interestingly, devices that respond incorrectly, since a controller that only works against well-behaved peripherals has not been verified. The register programming sequences a driver would perform become stimulus; the responses become checks. Because the medium is shared, correctness claims involve the interaction of participants rather than a single endpoint, which is a materially larger problem than verifying a point-to-point link. Modules 20 through 22 develop it.

An FPGA engineer meets it first, and usually during bring-up. A board frequently cannot do anything until its regulator has been programmed and its clock generator configured — both over this bus — so it is on the critical path to the first sign of life. It is also where an identity EEPROM is read to discover which board variant the design is running on. When something does not work, this bus is often both the thing being debugged and the instrument used to debug other things. Module 19 covers the implementation realities, and Module 23 the debugging.

None of that is career framing. It is a statement about where in a real workflow the material in the following twenty-three modules gets used.

7. RTL Connection — The Kind of State a Control Bus Reaches

Every device in §2 was described by the state the host wants to reach: a voltage setting, a fault flag, an identifier, a frequency selection. That description is not a metaphor. Inside each of those parts is a small block of addressable registers, and the bus exists to read and write them.

It is worth building one, because the shape is remarkably consistent across the whole device class, and because seeing it makes the control-plane argument concrete: this is all the state a management bus typically touches, and it explains why the traffic is as small as §3 claimed.

A representative block has three kinds of register, and the differences between them are the lesson:

  • A configuration register — read/write. The host sets it and it stays set. Reset must put it in a safe state, because the board comes up before anything has configured it.
  • A status register — the host reads it; the hardware writes it. Its bits report conditions the host did not cause, so it needs a way to be cleared once the host has seen a condition, and a way to not lose a new condition while being cleared.
  • An identification register — read-only, constant. It lets software discover what it is talking to before it configures anything, which is the foundation of every board-variant and calibration scheme.
A control and status register block. A write enable, address and write data arrive from the host side. An address decode routes writes to a configuration register or to the clear path of a status register, and selects which register drives the read data output. A hardware fault input sets the sticky status bit. A constant device identifier is also readable.Host sidewr_en, addr, wdata, rdataAddress decodeselects one registerCONFIGread/write, reset to a safevalueSTATUShardware sets, host clearsDEVICE_IDread-only constantHardware eventsets the sticky bitwriteclearset12
Figure 2 — a representative control/status block. A small address decode selects one of three registers: a writable configuration register, a status register whose sticky bit is set by hardware and cleared by the host writing a one, and a constant identifier. This is the shape almost every device on a control bus presents.

The behaviour all three implementations below share, stated once:

  • Synchronous, active-low reset clears CONFIG to zero and the sticky fault to zero.
  • Address 0 — CONFIG: a write with wr_en stores wdata; reads return the stored value.
  • Address 1 — STATUS: bit 0 is a sticky fault. fault_in sets it. The host clears it by writing a one to it — the write-one-to-clear convention. Reads return the current value.
  • Address 2 — DEVICE_ID: returns a constant. Writes to it are ignored, not an error.
  • Address 3: unimplemented; reads return zero.
  • Set beats clear. If fault_in asserts in the same cycle the host writes the clear, the bit stays set. This is deliberate and it is the most important line in the design — see the debug section below.
  • rdata is a combinational read mux, so a read presents its value in the same cycle the address is applied.

7a. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
csr_block.sv — configuration, sticky status and a constant identifier
   module csr_block #(
       parameter logic [7:0] DEVICE_ID = 8'h5A     // what software reads to identify this part
   )(
       input  logic       clk,
       input  logic       rst_n,                   // synchronous, active-low
       input  logic       wr_en,                   // write strobe from the host side
       input  logic [1:0] addr,
       input  logic [7:0] wdata,
       output logic [7:0] rdata,                   // combinational read
       input  logic       fault_in                 // a hardware condition, not a host action
   );
       localparam logic [1:0] A_CONFIG = 2'd0;
       localparam logic [1:0] A_STATUS = 2'd1;
       localparam logic [1:0] A_ID     = 2'd2;

       logic [7:0] cfg;
       logic       fault_sticky;

       always_ff @(posedge clk) begin
           if (!rst_n) begin
               cfg          <= 8'h00;              // safe value: the board boots unconfigured
               fault_sticky <= 1'b0;
           end else begin
               if (wr_en && addr == A_CONFIG)
                   cfg <= wdata;

               // SET BEATS CLEAR. Evaluate the host's write-one-to-clear first,
               // then let a same-cycle hardware event override it. Ordering it the
               // other way silently loses faults — see the debug section.
               if (wr_en && addr == A_STATUS && wdata[0])
                   fault_sticky <= 1'b0;
               if (fault_in)
                   fault_sticky <= 1'b1;
           end
       end

       // Read mux. Writes to DEVICE_ID and to the unimplemented address are
       // ignored above, so reads here are the only thing those addresses do.
       always_comb begin
           unique case (addr)
               A_CONFIG: rdata = cfg;
               A_STATUS: rdata = {7'b0, fault_sticky};
               A_ID:     rdata = DEVICE_ID;
               default:  rdata = 8'h00;
           endcase
       end
   endmodule

The testbench walks the properties a driver actually depends on, and its last check is the one that matters: a fault arriving in the same cycle as a clear must not be lost.

Azvya Education Pvt. Ltd.VLSI Mentor
csr_block_tb.sv — self-checking: reset values, read-back, read-only ID, sticky set/clear, and the set-beats-clear race
   module csr_block_tb;
       localparam logic [7:0] DEVICE_ID = 8'h5A;

       logic       clk = 1'b0;
       logic       rst_n, wr_en, fault_in;
       logic [1:0] addr;
       logic [7:0] wdata, rdata;
       int         errors = 0;

       csr_block #(.DEVICE_ID(DEVICE_ID)) dut (
           .clk(clk), .rst_n(rst_n), .wr_en(wr_en), .addr(addr),
           .wdata(wdata), .rdata(rdata), .fault_in(fault_in)
       );

       always #5 clk = ~clk;

       task automatic wr(input logic [1:0] a, input logic [7:0] d);
           addr = a; wdata = d; wr_en = 1'b1;
           @(negedge clk);
           wr_en = 1'b0;
       endtask

       task automatic expect_rd(input logic [1:0] a, input logic [7:0] exp, input string what);
           addr = a;
           #1;                                      // let the read mux settle
           if (rdata !== exp) begin
               $error("%s: addr %0d read %h, expected %h", what, a, rdata, exp);
               errors++;
           end
       endtask

       initial begin
           rst_n = 1'b0; wr_en = 1'b0; fault_in = 1'b0; addr = 2'd0; wdata = 8'h00;
           repeat (2) @(negedge clk);

           // 1 — RESET: config cleared, status clear, identifier already readable.
           expect_rd(2'd0, 8'h00,     "reset config");
           expect_rd(2'd1, 8'h00,     "reset status");
           expect_rd(2'd2, DEVICE_ID, "identifier during reset");
           rst_n = 1'b1;
           @(negedge clk);

           // 2 — NOMINAL: a configuration write reads back.
           wr(2'd0, 8'h3C);
           expect_rd(2'd0, 8'h3C, "config read-back");

           // 3 — READ-ONLY: writing the identifier must be ignored, not fatal.
           wr(2'd2, 8'hFF);
           expect_rd(2'd2, DEVICE_ID, "identifier after write attempt");

           // 4 — UNIMPLEMENTED: the spare address reads as zero.
           expect_rd(2'd3, 8'h00, "unimplemented address");

           // 5 — STICKY: hardware sets the bit and it STAYS set after the event ends.
           fault_in = 1'b1;  @(negedge clk);
           fault_in = 1'b0;  @(negedge clk);
           expect_rd(2'd1, 8'h01, "fault latched");
           @(negedge clk);
           expect_rd(2'd1, 8'h01, "fault still latched one cycle later");

           // 6 — WRITE-ONE-TO-CLEAR: writing a zero must NOT clear it; a one must.
           wr(2'd1, 8'h00);
           expect_rd(2'd1, 8'h01, "write-zero must not clear");
           wr(2'd1, 8'h01);
           expect_rd(2'd1, 8'h00, "write-one clears");

           // 7 — THE RACE: a new fault in the same cycle as the clear must survive.
           //     This is the check that fails on the common implementation bug.
           fault_in = 1'b1;
           wr(2'd1, 8'h01);                         // clear and set collide here
           fault_in = 1'b0;
           expect_rd(2'd1, 8'h01, "fault arriving during a clear must not be lost");

           if (errors == 0) $display("PASS: csr_block — reset, read-back, read-only ID, sticky W1C, set-beats-clear");
           else             $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule

7b. Verilog

Same hardware, Verilog typing and idiom. always @(*) replaces always_comb, and a plain case with a default replaces unique case — the unique qualifier was a tool-checkable assertion about the case being full and non-overlapping, not part of the circuit.

Azvya Education Pvt. Ltd.VLSI Mentor
csr_block.v — the same register block in Verilog
   module csr_block #(
       parameter [7:0] DEVICE_ID = 8'h5A
   )(
       input  wire       clk,
       input  wire       rst_n,
       input  wire       wr_en,
       input  wire [1:0] addr,
       input  wire [7:0] wdata,
       output reg  [7:0] rdata,
       input  wire       fault_in
   );
       localparam [1:0] A_CONFIG = 2'd0;
       localparam [1:0] A_STATUS = 2'd1;
       localparam [1:0] A_ID     = 2'd2;

       reg [7:0] cfg;
       reg       fault_sticky;

       always @(posedge clk) begin
           if (!rst_n) begin
               cfg          <= 8'h00;
               fault_sticky <= 1'b0;
           end else begin
               if (wr_en && addr == A_CONFIG)
                   cfg <= wdata;

               // SET BEATS CLEAR — the later assignment wins for the same signal
               // in the same procedural block, so the fault is never lost.
               if (wr_en && addr == A_STATUS && wdata[0])
                   fault_sticky <= 1'b0;
               if (fault_in)
                   fault_sticky <= 1'b1;
           end
       end

       always @(*) begin
           case (addr)
               A_CONFIG: rdata = cfg;
               A_STATUS: rdata = {7'b0, fault_sticky};
               A_ID:     rdata = DEVICE_ID;
               default:  rdata = 8'h00;
           endcase
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
csr_block_tb.v — the same seven checks in Verilog idiom
   module csr_block_tb;
       parameter [7:0] DEVICE_ID = 8'h5A;

       reg        clk;
       reg        rst_n, wr_en, fault_in;
       reg  [1:0] addr;
       reg  [7:0] wdata;
       wire [7:0] rdata;
       integer    errors;

       csr_block #(.DEVICE_ID(DEVICE_ID)) dut (
           .clk(clk), .rst_n(rst_n), .wr_en(wr_en), .addr(addr),
           .wdata(wdata), .rdata(rdata), .fault_in(fault_in)
       );

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

       task wr;
           input [1:0] a;
           input [7:0] d;
           begin
               addr = a; wdata = d; wr_en = 1'b1;
               @(negedge clk);
               wr_en = 1'b0;
           end
       endtask

       task expect_rd;
           input [1:0] a;
           input [7:0] exp;
           begin
               addr = a;
               #1;
               if (rdata !== exp) begin
                   $display("FAIL: addr %0d read %h, expected %h", a, rdata, exp);
                   errors = errors + 1;
               end
           end
       endtask

       initial begin
           errors = 0;
           rst_n = 1'b0; wr_en = 1'b0; fault_in = 1'b0; addr = 2'd0; wdata = 8'h00;
           repeat (2) @(negedge clk);

           expect_rd(2'd0, 8'h00);                  // reset config
           expect_rd(2'd1, 8'h00);                  // reset status
           expect_rd(2'd2, DEVICE_ID);              // identifier readable in reset
           rst_n = 1'b1;
           @(negedge clk);

           wr(2'd0, 8'h3C);  expect_rd(2'd0, 8'h3C);    // nominal read-back
           wr(2'd2, 8'hFF);  expect_rd(2'd2, DEVICE_ID); // identifier is read-only
           expect_rd(2'd3, 8'h00);                       // unimplemented reads zero

           fault_in = 1'b1;  @(negedge clk);
           fault_in = 1'b0;  @(negedge clk);
           expect_rd(2'd1, 8'h01);                       // sticky after the event ended
           @(negedge clk);
           expect_rd(2'd1, 8'h01);                       // still sticky

           wr(2'd1, 8'h00);  expect_rd(2'd1, 8'h01);     // write-zero does not clear
           wr(2'd1, 8'h01);  expect_rd(2'd1, 8'h00);     // write-one clears

           fault_in = 1'b1;                              // the race
           wr(2'd1, 8'h01);
           fault_in = 1'b0;
           expect_rd(2'd1, 8'h01);                       // must NOT be lost

           if (errors == 0) $display("PASS: csr_block behaves correctly in all seven checks");
           else             $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule

7c. VHDL

VHDL contributes one difference worth naming: the set-beats-clear ordering that SystemVerilog and Verilog get from "the last assignment in the process wins" is expressed here as an explicit if/elsif priority chain. The VHDL is arguably clearer about the intent, because the priority is written down rather than implied by statement order.

Azvya Education Pvt. Ltd.VLSI Mentor
csr_block.vhd — the same register block in VHDL (explicit set-over-clear priority)
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity csr_block is
       generic ( DEVICE_ID : std_logic_vector(7 downto 0) := x"5A" );
       port (
           clk      : in  std_logic;
           rst_n    : in  std_logic;                            -- synchronous, active-low
           wr_en    : in  std_logic;
           addr     : in  std_logic_vector(1 downto 0);
           wdata    : in  std_logic_vector(7 downto 0);
           rdata    : out std_logic_vector(7 downto 0);
           fault_in : in  std_logic
       );
   end entity;

   architecture rtl of csr_block is
       constant A_CONFIG : std_logic_vector(1 downto 0) := "00";
       constant A_STATUS : std_logic_vector(1 downto 0) := "01";
       constant A_ID     : std_logic_vector(1 downto 0) := "10";

       signal cfg          : std_logic_vector(7 downto 0) := (others => '0');
       signal fault_sticky : std_logic := '0';
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   cfg          <= (others => '0');
                   fault_sticky <= '0';
               else
                   if wr_en = '1' and addr = A_CONFIG then
                       cfg <= wdata;
                   end if;

                   -- SET BEATS CLEAR, written as an explicit priority: the hardware
                   -- event is tested FIRST, so a fault arriving during a clear wins.
                   if fault_in = '1' then
                       fault_sticky <= '1';
                   elsif wr_en = '1' and addr = A_STATUS and wdata(0) = '1' then
                       fault_sticky <= '0';
                   end if;
               end if;
           end if;
       end process;

       -- Combinational read mux, concurrent rather than a process.
       rdata <= cfg                       when addr = A_CONFIG else
                "0000000" & fault_sticky  when addr = A_STATUS else
                DEVICE_ID                 when addr = A_ID     else
                (others => '0');
   end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
csr_block_tb.vhd — the same seven checks with assert report severity
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity csr_block_tb is
   end entity;

   architecture sim of csr_block_tb is
       constant DEVICE_ID : std_logic_vector(7 downto 0) := x"5A";

       signal clk      : std_logic := '0';
       signal rst_n    : std_logic := '0';
       signal wr_en    : std_logic := '0';
       signal fault_in : std_logic := '0';
       signal addr     : std_logic_vector(1 downto 0) := "00";
       signal wdata    : std_logic_vector(7 downto 0) := (others => '0');
       signal rdata    : std_logic_vector(7 downto 0);
   begin
       dut : entity work.csr_block
           generic map (DEVICE_ID => DEVICE_ID)
           port map (clk => clk, rst_n => rst_n, wr_en => wr_en, addr => addr,
                     wdata => wdata, rdata => rdata, fault_in => fault_in);

       clk <= not clk after 5 ns;

       stim : process
           procedure wr (a : in std_logic_vector(1 downto 0);
                         d : in std_logic_vector(7 downto 0)) is
           begin
               addr  <= a;
               wdata <= d;
               wr_en <= '1';
               wait until falling_edge(clk);
               wr_en <= '0';
           end procedure;

           procedure expect_rd (a   : in std_logic_vector(1 downto 0);
                                exp : in std_logic_vector(7 downto 0);
                                what : in string) is
           begin
               addr <= a;
               wait for 1 ns;
               assert rdata = exp report "csr_block: " & what severity error;
           end procedure;
       begin
           wait until falling_edge(clk);
           wait until falling_edge(clk);

           expect_rd("00", x"00",     "config not cleared by reset");
           expect_rd("01", x"00",     "status not cleared by reset");
           expect_rd("10", DEVICE_ID, "identifier not readable during reset");
           rst_n <= '1';
           wait until falling_edge(clk);

           wr("00", x"3C");  expect_rd("00", x"3C",     "config did not read back");
           wr("10", x"FF");  expect_rd("10", DEVICE_ID, "identifier was writable");
           expect_rd("11", x"00",                        "unimplemented address not zero");

           fault_in <= '1';  wait until falling_edge(clk);
           fault_in <= '0';  wait until falling_edge(clk);
           expect_rd("01", x"01", "fault did not latch");
           wait until falling_edge(clk);
           expect_rd("01", x"01", "fault did not stay latched");

           wr("01", x"00");  expect_rd("01", x"01", "write-zero wrongly cleared the fault");
           wr("01", x"01");  expect_rd("01", x"00", "write-one did not clear the fault");

           fault_in <= '1';                              -- the race
           wr("01", x"01");
           fault_in <= '0';
           expect_rd("01", x"01", "fault arriving during a clear was lost");

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

7d. What This Block Is, and What It Is Not

Ports, widths, reset polarity and behaviour are identical across the three implementations, and all three testbenches run the same seven checks with the same vectors. The only language-level difference worth remembering is how set-beats-clear is expressed: SystemVerilog and Verilog rely on the last assignment in the process winning, while VHDL writes the priority explicitly with if/elsif. Both describe the same flip-flop; the VHDL states the intent and the others imply it, which is a genuine argument for reviewing the intent in a comment wherever the implied form is used.

This is not an I²C device. The host-side interface here is a plain synchronous wr_en/addr/wdata/rdata bundle — the kind of interface an on-chip bus presents, and deliberately the simplest thing that lets us look at the registers. There is no serial link, no addressing of the device itself, no framing and no acknowledgement. What a real I²C target adds is a front end that recognises its own address on a shared pair of conductors and turns serial traffic into exactly these wr_en/addr/wdata operations — which is Module 18's subject, with the device-side register interface specifically in Module 18.9.

What the block does establish is the point §3 and §4 were making. Look at how little state there is: one configuration byte, one sticky bit, one constant. A host that wants all of it moves a handful of bytes and then has nothing left to ask for. That is why control-plane traffic is small — not by convention, but because the state being addressed is genuinely tiny.

8. Verification Connection — Bus Transaction Versus Register Intent

The register block in §7 exposes a distinction that shapes every serious verification environment for this class of device, and it is worth seeing now rather than in Module 19, because it changes how you read the rest of the curriculum.

A test that wants to exercise the device has an intent: set the configuration to 0x3C, then confirm the fault bit latches. That intent is about registers. It says nothing about conductors, bit order, addressing or framing, and it should not — the same intent is valid whether the device is reached over I²C, over SPI, or over the on-chip bundle used above.

The bus, meanwhile, carries a transfer: a device was addressed, a direction was chosen, some bytes moved, and an outcome occurred. That is about conductors and protocol.

Good environments keep those layers separate, and the separation maps onto components:

LayerExpressed asKnows aboutDoes not know about
Test intenta sequence — "configure, then check the fault latched"register names and meaningsconductors, bit order, framing
Bus activitya transfer item — target, direction, bytes, outcomeprotocol structurewhat the bytes mean
Observationa monitor reconstructing transfers from pinspin timing and protocol rulesthe test's intent
Expectationa register model plus a scoreboardwhat each register should now holdhow the bytes got there

The reason this matters is reuse in both directions. A test written against register intent survives a change of bus, which is why the same configuration sequence can be retargeted. And a monitor written against protocol structure survives a change of test, which is why one monitor serves an entire environment.

It also tells you where the sticky bit from §7 belongs. fault_sticky is not something the bus can predict — no transfer causes it, a hardware event does. So a scoreboard comparing "what the host wrote" against "what the device holds" will be wrong about that bit unless the register model knows it is hardware-writable and write-one-to-clear. Register models carry exactly that kind of per-field access information, and getting it wrong is one of the most common sources of false failures in register verification. Module 20.8 builds the model and scoreboard properly.

Nothing above needs to be implemented yet. The point is that the boundary exists, that it is visible in a block this small, and that the tiny register set in §7 is what the whole verification stack eventually exists to check.

9. Debugging — Locating a Device in the Control Path

A peripheral that does not respond is the single most common bring-up complaint on a control bus, and the instinct is to suspect the protocol. §5 and §7 supply a better first question, because the device sits at the end of a chain and most of the chain is not protocol at all.

Walk it in order, because each step is cheaper to check than the next and each failure looks identical from software:

Is the device powered and out of reset? Many of these parts are supplied by rails that another device on the same bus configures. A regulator that has not been programmed yet means a sensor that is not alive yet — and the bus is fine. This ordering problem is invisible from a protocol trace, because the silent device produces no trace at all.

Does the host's controller block think it did anything? The registers in §5 report it. A transfer that never left the block, because the block was not enabled or was left mid-operation by an earlier failure, presents exactly like a device that did not answer.

Is the device electrically on the bus? A part whose conductors are not connected, or whose supply domain differs from the bus's, is present on the schematic and absent from the bus.

Only then, is it being addressed correctly? This is the layer everyone checks first and it is the fourth most likely cause.

10. Common Misconceptions

11. Reason It Through

Work this through before reading the answers.

You are handed an unfamiliar board and a parts list. The board has an SoC, a power regulator, two sensors, a small serial memory, a high-speed analogue-to-digital converter and a gigabit network interface. Before opening a single datasheet, predict which parts are likely to share a control bus and which are not.

Which parts probably share it, and on what evidence? The regulator, both sensors and the small memory. The evidence is the exchange each one implies: rails configured once and checked occasionally, physical quantities that change slowly, a small persistent fact read at boot. All are small, sparse and non-blocking, which is precisely the profile that makes sharing and serialisation nearly free.

Which parts almost certainly do not, and why? The converter and the network interface. Both produce a continuous payload the system is processing, which makes them data-plane: the system is waiting on them, so their transfers cannot take turns with a regulator's status poll, and the conductors they need are worth spending. Note the qualification, because it is the interesting part — this says nothing about their control interfaces. A converter frequently has its mode and gain programmed over exactly the shared control bus while its samples leave on a dedicated one. The same part can sit on both planes.

What would change your prediction? A sensor whose output the system consumes continuously rather than samples occasionally — a high-rate inertial sensor in a control loop, say — has a data-plane profile despite being a sensor, and would justify a dedicated interface. The category of the part predicts nothing; the profile of the traffic predicts almost everything.

What does this tell you about where to look first when the board does not boot? At the control plane, and early in it. If the regulator has not been programmed correctly, nothing downstream has a chance to work, and the failure will present as something far less specific than "the bus is wrong". Bring-up order follows the control plane's dependency order, which is why this bus is usually the first thing brought to life and the first thing suspected.

12. Understanding Check

13. Summary

The shape derived in Chapter 1.2 lands in a specific place. A host — SoC, MCU or FPGA — contains a controller block that software drives through registers, and that block drives one shared pair of conductors past every attached device.

The attached devices are unrelated as components and identical in the one respect that matters: the host writes a little configuration, reads a little status or data, does it infrequently, and is never blocked waiting. A regulator's rails, a sensor's slow physical quantity, a board's identity in a small memory, the time of day, a clock generator's outputs — all small, all sparse, all register-shaped.

That profile has a name. This is a control-plane interface, not a data-plane one, and the distinction is the durable idea in this chapter: ask of any interface whether the system is waiting on it. If it is, the interface deserves width and dedicated conductors. If it is not, paying for width there takes resources from traffic with a stronger claim. A board running a very fast memory interface and a very slow configuration bus is two traffic classes priced correctly.

The bus is also where the three audiences of this curriculum actually work: RTL engineers implement or integrate the controller and the device side; verification engineers model well-behaved and badly-behaved participants on a shared medium; FPGA engineers meet it during bring-up, often before the board can do anything else.

14. What Comes Next

You can now recognise where this bus belongs. What remains for Module 1 is the harder judgement: recognising when it does not. Chapter 1.4 closes the module by putting I²C beside the two other interfaces an engineer reaches for at board level, and by working through decisions where the right answer is not this bus.

Module 2 then returns to the debt Chapter 1.2 left open — how several devices can share one conductor without harming each other — which is the electrical foundation everything after it stands on. Browse the full path on the I²C tutorials index.

For the control-plane pattern one level down, inside the chip rather than on the board, see Why APB Exists — a deliberately simple bus for exactly this class of traffic — and Why AXI Exists for the data-plane counterpart. For a worked example of the distinction at the opposite extreme of bandwidth, see The Memory Hierarchy.

Continue learning