I²C · Module 1
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.
Chapter 1.1 ended on a constraint, not an answer: giving each of N devices its own interface of W signals spends N × W pins, I/O cells and routed traces at the one package least able to afford them, and the routing cost is worse than proportional. Two levers act on that product. Narrowing W has a floor. Removing the multiplication does not.
This chapter spends both levers, deliberately, one at a time, and watches what each one costs. That matters more than the destination. Anyone can be told that I²C uses two wires; the useful thing is to arrive at two wires by a sequence of exchanges you could have made yourself, because then every later constraint — why a device must be named, why transfers take turns, why the bus behaves the way it does when something goes wrong — is already accounted for as the price of a step you took.
1. Trading Space for Time
Start with the narrower lever, because it is the one with a hard limit.
A parallel interface presents several bits at the same instant on several conductors. The width is a choice about simultaneity: eight bits at once needs eight conductors, and the information arrives in one interval. Reduce the conductor count and the same information has to be presented in more than one interval instead. The bits did not go anywhere — they were re-expressed in time rather than in space.
That is the whole of serialisation, and it is worth stating plainly because the word is often treated as a technology rather than an exchange. Serialisation buys conductors with intervals. Each conductor removed is a pin at each end, an I/O cell behind each pin, a trace, and the routing that trace consumed — all of the costs Chapter 1.1 enumerated. Each interval added is time the interface is occupied and the requester is waiting.
Taken to its limit, one conductor carries the information one bit at a time. That is the floor the previous chapter promised: an interface cannot be narrower than a single conductor, so the saving from this lever is bounded, and once you reach the bottom of it the only thing left to trade is time.
2. The Same Byte, Two Shapes
The exchange is easier to believe when you can see it.
One byte carried in parallel, then the same byte carried serially
9 cyclesTwo readings of this figure are worth separating, because only one of them is the lesson.
The obvious reading is slower: the byte took eight intervals instead of one. True, and not yet meaningful — an interval here is an arbitrary unit, and nothing in the figure says how long one is. Whether eight of them is slow depends entirely on what is waiting for the byte, which is a question about the system, not about the shape.
The reading that matters is seven conductors were released. Those seven are permanently returned to the budgets that Chapter 1.1 showed were fixed and contested. That trade is only worth making when time is the cheaper currency — which, for the configuration and status traffic on a board, it usually is, and for the data a processor is stalled on, it usually is not.
Notice also what the figure does not say. It shows eight bit-times but takes no position on which bit goes first, on how the receiver knows a byte has begun, or on what happens between bytes. Those are protocol decisions, and I²C makes each of them specifically; Module 7 settles bit order and framing for this bus. The figure is making one claim only: same information, different arrangement.
3. Building the Serialiser — From Diagram to Hardware
Figure 1 asserted that the same byte can occupy one conductor across eight intervals. That claim deserves to be built, because serialisation is the first idea in this curriculum that is genuinely a piece of hardware rather than an argument — and building it makes the cost concrete in a way no figure can.
What does a block that performs that rearrangement actually contain? Reason it out before looking at any code. It needs somewhere to hold the whole word while it is being emitted, a way to present one bit at a time, something to know when it is finished, and a way to tell the rest of the system that it is busy.
Those four parts are the whole design, and the three implementations below all contain exactly them. The behaviour they share, stated once so it does not have to be repeated three times:
- Reset is synchronous and active-low. It clears the shift register, the counter and all three outputs.
startis accepted only while the block is idle. On the accepting edge the word is captured, the most significant bit is presented onserial_out, andbusyrises.- Each subsequent edge presents the next bit down and decrements the counter. Bits leave most significant first — a choice this block makes for itself, discussed below.
- When the counter reaches zero,
busyfalls,serial_outreturns to its idle level, anddonepulses for exactly one cycle. serial_out,busyanddoneare all registered, so each is stable for a whole cycle and can be sampled without race conditions.
A WIDTH-bit word therefore occupies the output for WIDTH cycles, which is §1's exchange expressed as a state machine: the conductors saved are gone from the port list, and the intervals bought appear as busy being high.
3a. SystemVerilog
The SystemVerilog form uses always_ff to declare the intent — this is sequential logic, and a lint tool will object if the body accidentally describes something else.
module serializer #(
parameter int WIDTH = 8 // any width: a byte here, a word elsewhere
)(
input logic clk,
input logic rst_n, // synchronous, active-low
input logic start, // accepted only while idle
input logic [WIDTH-1:0] din, // the word to send
output logic serial_out, // registered: one bit, valid for a whole cycle
output logic busy, // high from the accepting edge until the last bit
output logic done // one-cycle pulse after the last bit
);
localparam int CW = $clog2(WIDTH) + 1; // wide enough to hold WIDTH itself
logic [WIDTH-1:0] sr; // holds the bits still to be sent
logic [CW-1:0] cnt; // bits remaining AFTER the one on the output
always_ff @(posedge clk) begin
if (!rst_n) begin
sr <= '0;
cnt <= '0;
serial_out <= 1'b0;
busy <= 1'b0;
done <= 1'b0;
end else begin
done <= 1'b0; // default: done is a single-cycle pulse
if (!busy) begin
if (start) begin
serial_out <= din[WIDTH-1]; // present the MSB immediately
sr <= {din[WIDTH-2:0], 1'b0}; // keep the rest, MSB-aligned
cnt <= WIDTH - 1; // that many still to go
busy <= 1'b1;
end
end else if (cnt != '0) begin
serial_out <= sr[WIDTH-1]; // next bit down
sr <= {sr[WIDTH-2:0], 1'b0}; // advance
cnt <= cnt - 1'b1;
end else begin
serial_out <= 1'b0; // back to idle level
busy <= 1'b0;
done <= 1'b1; // exactly one cycle
end
end
end
endmoduleThe testbench collects the emitted bits and compares them against the word that was loaded — a check that fails loudly if the bit order is wrong, if a bit is dropped, or if one is emitted twice. It samples on the falling edge, where a registered output is unambiguously stable, and it also verifies the two things a requester depends on: that start is ignored while the block is busy, and that done is a single-cycle pulse rather than a level.
module serializer_tb;
localparam int WIDTH = 8;
logic clk = 1'b0;
logic rst_n, start;
logic [WIDTH-1:0] din;
logic serial_out, busy, done;
logic [WIDTH-1:0] captured;
int errors = 0;
serializer #(.WIDTH(WIDTH)) dut (
.clk(clk), .rst_n(rst_n), .start(start), .din(din),
.serial_out(serial_out), .busy(busy), .done(done)
);
always #5 clk = ~clk; // rising edge every 10 time units
// Send one word and return what the DUT actually emitted, MSB first.
task automatic send(input logic [WIDTH-1:0] word);
din = word;
start = 1'b1;
@(negedge clk); // the posedge between accepted start
start = 1'b0;
captured = {captured[WIDTH-2:0], serial_out}; // bit WIDTH-1
for (int i = 1; i < WIDTH; i++) begin
@(negedge clk);
// CHECK — the block must stay busy for every bit of the word:
if (busy !== 1'b1) begin
$error("busy dropped early at bit %0d", WIDTH-1-i); errors++;
end
captured = {captured[WIDTH-2:0], serial_out};
end
// CHECK — start must be ignored while busy (a second request cannot
// corrupt the word in flight):
start = 1'b1;
@(negedge clk); // this is the done cycle
start = 1'b0;
if (busy !== 1'b0) begin $error("busy did not clear"); errors++; end
if (done !== 1'b1) begin $error("done did not pulse"); errors++; end
@(negedge clk);
if (done !== 1'b0) begin $error("done is a level, not a pulse"); errors++; end
endtask
initial begin
captured = '0; din = '0; start = 1'b0; rst_n = 1'b0;
repeat (2) @(negedge clk);
// CHECK — reset holds every output at its idle value:
if (busy !== 1'b0 || done !== 1'b0 || serial_out !== 1'b0) begin
$error("outputs not idle during reset"); errors++;
end
rst_n = 1'b1;
@(negedge clk);
send(8'hA5); // nominal: alternating pattern
if (captured !== 8'hA5) begin
$error("word 0xA5 came back as %h", captured); errors++;
end
send(8'h01); // boundary: only the LAST bit is set,
if (captured !== 8'h01) begin // so an off-by-one in the counter
$error("word 0x01 came back as %h", captured); errors++; // drops it
end
send(8'h80); // boundary: only the FIRST bit is set,
if (captured !== 8'h80) begin // so a load-order bug loses it
$error("word 0x80 came back as %h", captured); errors++;
end
if (errors == 0) $display("PASS: serialiser emitted every word MSB-first, %0d bits per word", WIDTH);
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmoduleThe two boundary words are chosen deliberately. 0x80 has only its most significant bit set, so a block that loads the shift register without presenting the MSB first loses that bit entirely. 0x01 has only its least significant bit set, so a counter that stops one cycle early never emits it. Either bug passes a test that only ever sends 0xA5, which is why a single alternating pattern is a weak stimulus.
3b. Verilog
The hardware is identical. The differences are typing and idiom: reg for anything assigned in a procedural block, wire for ports that are not, $clog2 from Verilog-2005, and explicit comparisons instead of assert in the testbench.
module serializer #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire start,
input wire [WIDTH-1:0] din,
output reg serial_out, // `reg`: assigned procedurally
output reg busy,
output reg done
);
localparam CW = $clog2(WIDTH) + 1;
reg [WIDTH-1:0] sr;
reg [CW-1:0] cnt;
always @(posedge clk) begin
if (!rst_n) begin
sr <= {WIDTH{1'b0}};
cnt <= {CW{1'b0}};
serial_out <= 1'b0;
busy <= 1'b0;
done <= 1'b0;
end else begin
done <= 1'b0;
if (!busy) begin
if (start) begin
serial_out <= din[WIDTH-1];
sr <= {din[WIDTH-2:0], 1'b0};
cnt <= WIDTH - 1;
busy <= 1'b1;
end
end else if (cnt != {CW{1'b0}}) begin
serial_out <= sr[WIDTH-1];
sr <= {sr[WIDTH-2:0], 1'b0};
cnt <= cnt - 1'b1;
end else begin
serial_out <= 1'b0;
busy <= 1'b0;
done <= 1'b1;
end
end
end
endmodule module serializer_tb;
parameter WIDTH = 8;
reg clk;
reg rst_n, start;
reg [WIDTH-1:0] din;
wire serial_out, busy, done;
reg [WIDTH-1:0] captured;
integer errors;
integer i;
serializer #(.WIDTH(WIDTH)) dut (
.clk(clk), .rst_n(rst_n), .start(start), .din(din),
.serial_out(serial_out), .busy(busy), .done(done)
);
initial clk = 1'b0;
always #5 clk = ~clk;
// Verilog-2005 has no `task automatic` with local ints in the same shape,
// so the send sequence is a plain task over module-level state.
task send;
input [WIDTH-1:0] word;
begin
din = word;
start = 1'b1;
@(negedge clk);
start = 1'b0;
captured = {captured[WIDTH-2:0], serial_out};
for (i = 1; i < WIDTH; i = i + 1) begin
@(negedge clk);
if (busy !== 1'b1) begin
$display("FAIL: busy dropped early at bit %0d", WIDTH-1-i);
errors = errors + 1;
end
captured = {captured[WIDTH-2:0], serial_out};
end
start = 1'b1; // must be ignored while busy
@(negedge clk);
start = 1'b0;
if (busy !== 1'b0) begin $display("FAIL: busy did not clear"); errors = errors + 1; end
if (done !== 1'b1) begin $display("FAIL: done did not pulse"); errors = errors + 1; end
@(negedge clk);
if (done !== 1'b0) begin $display("FAIL: done is a level"); errors = errors + 1; end
end
endtask
task check;
input [WIDTH-1:0] expected;
begin
if (captured !== expected) begin
$display("FAIL: expected %h, emitted %h", expected, captured);
errors = errors + 1;
end
end
endtask
initial begin
captured = {WIDTH{1'b0}};
din = {WIDTH{1'b0}};
start = 1'b0;
errors = 0;
rst_n = 1'b0;
repeat (2) @(negedge clk);
if (busy !== 1'b0 || done !== 1'b0 || serial_out !== 1'b0) begin
$display("FAIL: outputs not idle during reset"); errors = errors + 1;
end
rst_n = 1'b1;
@(negedge clk);
send(8'hA5); check(8'hA5);
send(8'h01); check(8'h01);
send(8'h80); check(8'h80);
if (errors == 0) $display("PASS: serialiser emitted every word MSB-first");
else $display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule3c. VHDL
Again the same hardware. VHDL contributes one genuine structural difference worth noticing rather than glossing over: a port declared out cannot be read inside the architecture in VHDL-93, and this design reads busy to decide whether start is accepted. The portable idiom is to keep the state in internal signals and drive the ports from them concurrently — which is what the three assignments at the top of the architecture do.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity serializer is
generic ( WIDTH : positive := 8 );
port (
clk : in std_logic;
rst_n : in std_logic; -- synchronous, active-low
start : in std_logic;
din : in std_logic_vector(WIDTH-1 downto 0);
serial_out : out std_logic;
busy : out std_logic;
done : out std_logic
);
end entity;
architecture rtl of serializer is
signal sr : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
signal cnt : natural range 0 to WIDTH := 0; -- bits remaining after the one out
-- Internal copies: an `out` port is not readable in VHDL-93, and the
-- control decision below needs to read the busy state.
signal sout : std_logic := '0';
signal bsy : std_logic := '0';
signal dne : std_logic := '0';
begin
serial_out <= sout;
busy <= bsy;
done <= dne;
process (clk)
begin
if rising_edge(clk) then
if rst_n = '0' then
sr <= (others => '0');
cnt <= 0;
sout <= '0';
bsy <= '0';
dne <= '0';
else
dne <= '0'; -- single-cycle pulse by default
if bsy = '0' then
if start = '1' then
sout <= din(WIDTH-1); -- MSB first
sr <= din(WIDTH-2 downto 0) & '0';
cnt <= WIDTH - 1;
bsy <= '1';
end if;
elsif cnt /= 0 then
sout <= sr(WIDTH-1);
sr <= sr(WIDTH-2 downto 0) & '0';
cnt <= cnt - 1;
else
sout <= '0';
bsy <= '0';
dne <= '1';
end if;
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity serializer_tb is
end entity;
architecture sim of serializer_tb is
constant WIDTH : positive := 8;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal start : std_logic := '0';
signal din : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
signal serial_out : std_logic;
signal busy : std_logic;
signal done : std_logic;
signal captured : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
begin
dut : entity work.serializer
generic map (WIDTH => WIDTH)
port map (clk => clk, rst_n => rst_n, start => start, din => din,
serial_out => serial_out, busy => busy, done => done);
clk <= not clk after 5 ns; -- rising edge every 10 ns
stim : process
-- Send one word and leave what was emitted in `captured`.
procedure send (word : in std_logic_vector(WIDTH-1 downto 0)) is
begin
din <= word;
start <= '1';
wait until falling_edge(clk); -- the rising edge accepted start
start <= '0';
captured <= captured(WIDTH-2 downto 0) & serial_out;
for i in 1 to WIDTH-1 loop
wait until falling_edge(clk);
assert busy = '1'
report "busy dropped before the word finished" severity error;
captured <= captured(WIDTH-2 downto 0) & serial_out;
end loop;
start <= '1'; -- must be ignored while busy
wait until falling_edge(clk); -- the done cycle
start <= '0';
assert busy = '0' report "busy did not clear" severity error;
assert done = '1' report "done did not pulse" severity error;
wait until falling_edge(clk);
assert done = '0' report "done is a level" severity error;
end procedure;
begin
wait until falling_edge(clk);
wait until falling_edge(clk);
assert busy = '0' and done = '0' and serial_out = '0'
report "outputs not idle during reset" severity error;
rst_n <= '1';
wait until falling_edge(clk);
send(x"A5");
assert captured = x"A5" report "word A5 not reconstructed" severity error;
send(x"01"); -- only the last bit set
assert captured = x"01" report "word 01 not reconstructed" severity error;
send(x"80"); -- only the first bit set
assert captured = x"80" report "word 80 not reconstructed" severity error;
report "serialiser self-check complete" severity note;
wait;
end process;
end architecture;3d. What the Three Implementations Do and Do Not Share
The hardware is the same in all three: one shift register, one counter, three registered outputs, a synchronous active-low reset, and WIDTH cycles of occupancy per word. Ports, widths, reset polarity, cycle behaviour and the test vectors are identical by construction — the same 0xA5, 0x01, 0x80 prove the same properties in each language.
Three differences are worth knowing, and none of them changes the circuit:
| Concern | SystemVerilog | Verilog | VHDL |
|---|---|---|---|
| Sequential intent | always_ff declares it, tools can check it | always @(posedge clk) by convention | clocked process with rising_edge |
| Reading own status | reads the busy output directly | reads the busy output directly | needs an internal signal — out is not readable in VHDL-93 |
| Test failure reporting | $error with a running count | explicit compare plus $display | assert ... report ... severity error |
The VHDL row is the only one that changed how the design is written rather than how it is described, which is exactly the kind of difference worth carrying: the hardware was never in question, the language's rule about port readability was.
serializer_tb — one word (0xA5) leaving on one conductor over eight cycles
10 cyclesThis is an RTL simulation trace, not a protocol figure: every signal on it is a port of the module above, and the cycle count is the module's real behaviour rather than an illustration. Read serial_out across cycles 1 to 8 and it spells 1 0 1 0 0 1 0 1, which is 0xA5 most significant bit first — the same eight bits the testbench reconstructs and compares.
What the simulation proves. That WIDTH bits can leave on one conductor in WIDTH cycles with no information lost, that the requester can tell when the block is occupied and when it has finished, and that a second request cannot corrupt a word in flight. That is §1's exchange, working, in hardware, in three languages.
What it deliberately does not implement. There is no addressing, no framing, no acknowledgement and no shared conductor. serial_out is an ordinary output that drives the wire on its own — the very assumption Module 2 has to dismantle. The MSB-first ordering is this block's own choice, made so the testbench has something definite to check; it is not an I²C rule, and the bit ordering the protocol actually specifies is established in Module 7. This is a serialisation exercise, not a transmitter.
3e. Verification Boundary — What This Testbench Is and Is Not Checking
One observation here is worth more than it looks, and it is the first appearance of a distinction that shapes every verification module later.
The testbench above checks pins. It samples serial_out on a falling edge, shifts the bit into an accumulator, and compares the accumulator against the word it loaded. That is exactly the right check for this block, because the block's contract is a pin-level contract: present these bits, in this order, on these cycles.
A protocol environment cannot work that way. Once a conductor is shared and the bits on it are grouped into structures with meaning, a testbench that only knows about levels per cycle is looking at the wrong layer. What it needs to recover is a transaction — this device was addressed, this direction was requested, these bytes moved, this was the outcome — reconstructed from the same pin activity this testbench treats as the answer.
That is the job of a monitor, and it is why verification environments for shared buses are built around transaction objects rather than around signal comparisons:
// Compare against what serializer_tb checks: a bit, on a cycle, on a pin.
// A protocol monitor observes the same pins and emits something far larger,
// because the meaningful unit of a shared bus is not a bit but a transfer.
class transfer_item extends uvm_sequence_item;
rand bit [6:0] target; // WHICH device — impossible to know at pin level alone
rand bit is_read; // direction of the transfer
rand byte payload[]; // the bytes that moved
// outcome, timing and error status also belong here
endclassThe fields matter more than the syntax. target and is_read are not observable in a single cycle of serial_out — they are reconstructed by a component that has been watching long enough to know where a transfer began and what the early bits meant. That reconstruction is the hardest component in an I²C environment, and Modules 19 to 21 build it properly.
The lesson to carry forward is the boundary itself: a pin-level check verifies a block, and a transaction-level check verifies a protocol. This chapter's testbench is correctly the former. Reaching for the former when the latter is needed is one of the most common ways an I²C testbench ends up passing while proving very little.
3f. Debugging the Serialiser
The word that came back with its bits in the wrong places — a load that forgot to present the MSB
Pitfall — loading the shift register without emitting the first bit
// The engineer loads the whole word into the shift register on start, and lets
// the normal shift path emit every bit, including the first:
if (start) begin
sr <= din; // load the WHOLE word, emit nothing yet
cnt <= WIDTH; // WIDTH bits still to go
busy <= 1'b1;
end
// ... and on each busy cycle:
serial_out <= sr[WIDTH-1];
sr <= {sr[WIDTH-2:0], 1'b0};
// This looks tidier than the version in the chapter — one place emits bits, not
// two. It compiles, it simulates, and with din = 0xA5 the output sequence is
// still eight alternating bits, so a quick eyeball sees nothing wrong.Every word arrives shifted by one bit position, and the LAST bit is lost. The receiver reconstructs 0x4A from 0xA5, 0x00 from 0x01, and 0x00 from 0x80 — but only 0x80 makes the problem obvious, because 0xA5 still looks like a plausible alternating pattern and 0x01 still looks like a plausible small number. The serialiser also occupies the output for WIDTH+1 cycles instead of WIDTH, so a downstream block counting cycles drifts by one per word and the error accumulates across a long transfer rather than staying local.
Two off-by-one errors that cancel each other in appearance and not in effect. Loading sr with the whole word means the first bit presented on the output comes from the SHIFT path on the cycle AFTER start, so serial_out spends the accepting cycle at its idle level and the word starts one cycle late. Setting cnt to WIDTH rather than WIDTH-1 then extends the transfer by one cycle to compensate, which hides the late start in the busy duration while still dropping the final bit, because the shift register has run out of real data by the last cycle. The design has two responsibilities — present the current bit, and hold the bits not yet presented — and this version gives both to one register without accounting for the bit that is already on the output.
// Present the MSB AT the accepting edge, and keep only the bits that remain:
if (start) begin
serial_out <= din[WIDTH-1]; // this bit is on the wire NOW
sr <= {din[WIDTH-2:0], 1'b0}; // sr holds only what is still to come
cnt <= WIDTH - 1; // and that is how many remain
busy <= 1'b1;
end
// The invariant to hold in your head: cnt counts the bits still to be presented
// AFTER the one currently on serial_out. Load and shift then agree, the transfer
// is exactly WIDTH cycles, and no bit is emitted twice or lost.
//
// The verification that catches it: send 0x80 (only the MSB set) and 0x01 (only
// the LSB set). The first fails the moment a load-order bug drops the leading
// bit; the second fails the moment a counter bug drops the trailing one. A test
// that only ever sends 0xA5 passes against both bugs, which is why the chapter's
// testbench sends all three.The engineering lesson, and it generalises well beyond this block: when a serialised word arrives with its bits in the wrong places, the fault is almost never the conductor. A physical problem corrupts bits unpredictably — a different bit each time, or bits that change with temperature or cable length. A state problem corrupts them systematically — the same displacement on every word, every run, on every board. That distinction is the fastest triage available on any serial link: capture two different words and compare how they failed. Identical displacement means look at the counter and the load path; scattered differences mean look at the wire. Module 2 gives the electrical half of that reasoning, and Module 23 turns it into a full debug workflow.
4. Serialisation Alone Does Not Finish the Job
Now apply the result honestly to the problem from Chapter 1.1, because this is where a lot of reasoning stops one step early.
Suppose every peripheral's interface is narrowed to its serial minimum. The processor's cost is no longer N × W; it is N × w, where w is small. That is a real improvement — and it is still a product. Eight devices still mean eight separate links landing on the same package, eight sets of pins, eight escape paths out of the same congested region. Narrowing changed a coefficient. It did not change the shape of the growth.
The multiplication survives serialisation, because serialisation is a statement about how one link is arranged and says nothing about how many links exist. To attack the multiplication you have to attack the assumption underneath it: that each device needs a link of its own.
5. Sharing the Medium
Drop that assumption. Let one set of conductors run past every device, with all of them physically attached to it.
The processor now drives one interface regardless of how many devices hang off it. Adding a peripheral becomes an act of attaching it to something that already exists, which is the structural change Chapter 1.1 identified as the only lever that removes the product. The cost stops tracking device count.
This second exchange has its own price, and it is not the same price as the first. Serialisation traded conductors for intervals on one link. Sharing trades exclusivity for coordination. On a private link a device had the conductors to itself whenever it wanted them; on a shared one it has them only when nothing else is using them, and something has to establish whose turn it is. That is a different kind of cost — not time spent transmitting, but rules that every participant must implement and obey.
6. Sharing Destroys the Addressing You Already Had
Here is the consequence that most directly shapes everything after it, and it is easy to miss because the thing being lost was never explicit.
On a dedicated link, the wire is the address. A signal on it can only concern the one device at the far end; no identity has to be transmitted because the physical connection already carried it. That addressing was free and invisible, which is exactly why its disappearance is startling.
Attach every device to the same conductors and that mechanism is gone. Every device now observes every transfer. A pattern of bits travelling down the shared wires is seen by the sensor, the memory and the regulator alike, and nothing about the medium distinguishes which of them was meant. Sharing did not merely introduce contention over turns; it destroyed the system's only way of saying who.
So the identity has to be reintroduced — and the only place left to put it is inside the information being transmitted. The transfer has to begin by naming its intended participant, every device has to watch for its own name, and the ones that do not match have to ignore what follows.
That is the third exchange: a logical identity, carried in the data, replacing a physical identity that used to be carried by the wiring. It costs transmitted bits at the start of every transfer, and it costs each device the logic to recognise itself. In return it makes one set of conductors serve an arbitrary number of participants.
This chapter deliberately goes no further with it. How wide that name is, how it is encoded, which values are reserved and how a device signals that it heard its own name are specified exactly, and they belong to Module 6 and Module 7. What Module 1 owes you is the reason the field has to exist at all — which is that you gave away the wire that used to do the job.
7. Why Two Conductors and Not One
The floor on serialisation was one conductor. I²C uses two. That is not a failure to optimise, and the reason is the most useful thing in this chapter for understanding the comparison in Chapter 1.4.
A receiver presented with a changing voltage on a single wire faces a question the sender takes for granted: when should I look? Bits are only distinguishable if the receiver samples at the right instants, and a lone data conductor carries no instruction about where those instants are. There are two ways to answer.
Agree in advance. Both ends are configured with the same bit rate, the receiver finds the start of a transmission from the data itself and then times the remaining bits from its own local reference. This works, and it is what asynchronous serial links do — but the two ends are now running independent clocks, so any difference between them accumulates across a transmission and eventually lands a sample in the wrong bit. The repository's UART material develops this precisely: see clock drift and accumulated error and baud rate, bit rate and throughput. The cost of one conductor is paid in configuration that must match and in a tolerance budget that must be met.
Send the timing. Spend one more conductor on a clock that accompanies the data, and the sender tells the receiver when each bit is valid. Nothing has to be agreed in advance, nothing drifts, and devices that run at different internal speeds still interoperate — the timing is on the wire, not in a setting. What it costs is exactly one conductor, at every device on the bus.
I²C takes the second option. The second wire is not a second data path; it is the timing. One conductor carries the information, one carries the instants at which that information is valid, and both are shared by every device on the bus. That is the two-wire result — and it is a derived result, not a slogan: it is the narrowest arrangement that still lets devices with unrelated internal clocks talk to each other without prior agreement.
8. The Progression, End to End
Read the left column downward and the right column as the invoice. Nothing in the third row is free; it is simply that its costs are paid in currencies a board has more of — rules implemented once in hardware, and time on traffic nobody is waiting for — rather than in pins and routing, which are fixed and contested.
The step between rows 2 and 3 is the one that matters architecturally. Rows 1 and 2 differ by a coefficient. Rows 2 and 3 differ by the shape of the growth.
9. What Philips Actually Specified
The derivation above is not a reconstruction of a historical debate; it is the reasoning the result embodies. But the result is a real specification with a name, and it is worth placing it precisely.
I²C — Inter-Integrated Circuit — was developed at Philips Semiconductors in the early 1980s to let the controller chips inside a piece of consumer equipment talk to the other chips on the same board without spending a dedicated interface on each one. That intent is visible in every property the derivation arrived at: two conductors rather than many, devices named logically rather than by wire, and a design point chosen for control traffic rather than for bulk transfer.
The specification is maintained today by NXP as UM10204, I²C-bus specification and user manual, and it is the authority this curriculum cites for every claim about how the bus actually behaves. Where later chapters state a rule, a limit or a timing relationship, that document is where it comes from.
10. Common Misconceptions
11. Reason It Through
Work this through before reading the answers.
A design currently gives each of its six configuration devices a narrow, serialised, point-to-point link. Someone proposes keeping the serialisation and merging all six onto one shared set of conductors. A colleague objects that "we already serialised, so the remaining saving is small."
Is the colleague right about the size of the saving? No, and the error is a category error rather than an arithmetic one. Serialisation reduced W and left N untouched: six narrow links are still six sets of pins, six escape paths and six I/O cells at the host. Merging removes the N × term entirely, so the host drops from six interfaces to one. The first change improved a coefficient; the second changes how the cost grows.
What has to be added that the six links did not need? Two things, both consequences of the shared medium. The transfers must carry a name, because the wire is no longer doing the identifying. And there must be rules that establish whose turn it is, because six devices attached to the same conductors can otherwise attempt to use them at once.
What new failure modes appear? Ones that belong to the combination rather than to any device. A participant that holds the conductors, misbehaves, or simply fails in the wrong state can now affect transfers that have nothing to do with it — a class of problem that six private links did not have, and that no single device's datasheet describes.
When would the colleague's instinct actually be right? If one of those six is not configuration traffic at all — if it is a device the system is genuinely waiting on. Merging it in would make it share intervals with five others and wait for their turns to finish. The honest answer is then a mixed design, which is what real boards look like: the shared bus for the traffic whose profile fits it, and a dedicated link for the one whose profile does not.
12. Understanding Check
13. Summary
Chapter 1.1 left a product to attack: N × W. This chapter spent both available levers and priced each one.
Serialisation re-expresses information from space into time — the same byte on one conductor across several intervals instead of several conductors in one. It buys conductors with intervals, it bottoms out at one conductor, and crucially it does not change how cost scales, because it says nothing about how many links exist.
Sharing the medium does change the scaling. One set of conductors past every device makes the host's cost independent of the device count, and it is bought with coordination — rules about whose turn it is — rather than with time.
Sharing also destroys the addressing the wiring was doing for free, because a dedicated wire is an address and a shared one is not. Identity has to be reintroduced inside the transfer, which is why a transmitted address is a structural necessity of this topology rather than a feature of this particular bus.
Finally, the serialisation floor is one conductor but the bus uses two, because a receiver needs to know when to sample. Sending the timing alongside the data removes any need for the two ends to have agreed on a rate, and costs exactly one conductor. What falls out is a shared, addressed, two-wire bus — which is what Philips specified as I²C, and what NXP maintains today as UM10204.
14. What Comes Next
The shape is derived; it is not yet located. Chapter 1.3 puts this bus into a real system — the host controller inside an SoC or FPGA, the regulators, sensors, memories and clock devices attached to it, and the kind of traffic it actually carries, which turns out to be a specific and narrow kind. Chapter 1.4 then closes Module 1 by weighing it honestly against the two other board-level options an engineer reaches for.
The derivation also left one debt outstanding, and Module 2 is where it is paid: nothing here explained how devices can share a conductor safely. Browse the full path on the I²C tutorials index.
For the same space-versus-time reasoning reached from a different direction, see Why SPI Exists, which keeps conductors and spends them on throughput and simplicity, and UART vs RS-232 and RS-485, which shows what happens when the timing is not sent and the electrical layer is a separate decision. For the on-chip form of the same exchange, see Why APB Exists.
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
SDA and SCL — The Two-Wire Contract
Name the two conductors, establish that both are bidirectional shared nodes rather than any device's output, and define what a free bus looks like. Then state precisely the question the rest of the module exists to answer: what happens when two devices attached to the same wire disagree about its level?
- Related topic
The Shared Bus — Many Targets, Sometimes Many Masters
One pair of conductors serves an entire board. Physically the bus is a broadcast medium — every device sees everything — while participation is logically selective. That gap is the central abstraction of I²C, and it also opens the multi-controller case.
- Related topic
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.
