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.
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.
The behaviour all three implementations below share, stated once:
- Synchronous, active-low reset clears
CONFIGto zero and the sticky fault to zero. - Address 0 —
CONFIG: a write withwr_enstoreswdata; reads return the stored value. - Address 1 —
STATUS: bit 0 is a sticky fault.fault_insets 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_inasserts 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. rdatais a combinational read mux, so a read presents its value in the same cycle the address is applied.
7a. SystemVerilog
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
endmoduleThe 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.
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
endmodule7b. 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.
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 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
endmodule7c. 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.
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; 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:
| Layer | Expressed as | Knows about | Does not know about |
|---|---|---|---|
| Test intent | a sequence — "configure, then check the fault latched" | register names and meanings | conductors, bit order, framing |
| Bus activity | a transfer item — target, direction, bytes, outcome | protocol structure | what the bytes mean |
| Observation | a monitor reconstructing transfers from pins | pin timing and protocol rules | the test's intent |
| Expectation | a register model plus a scoreboard | what each register should now hold | how 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
Related tutorials
- Related topic
Why Chips on a Board Need a Bus
A connection between two chips is not a wire. It is a pin on each package, a routed trace, the board area and layers that trace consumes, and an I/O cell driving it — and all of that is paid for again for every device added. This is the cost structure that makes dedicating an interface per peripheral stop scaling, and that forces a board to share one set of wires instead.
- Related topic
From Parallel Buses to Two Wires
Derive the bus rather than meet it. Trading wires for time gives serialisation; trading exclusivity for coordination gives a shared medium; losing the wire as an implicit address forces a logical one. Each step is a deliberate exchange, and what falls out is a two-wire addressed bus — which is what Philips specified as I²C.
- Related topic
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.
- Related topic
SCL Generation and the Bit Period
A legal I²C clock is not a frequency. LOW and HIGH are separately constrained phases, the controller pulls SCL low and releases it rather than driving it high, and a naive integer divider satisfies none of that. Build a parameterised phase generator in three languages and measure it.
