Understanding I²C From a Engineer's Perspective

Embedded Systems Fundamentals #3
If SPI is the protocol of speed, Inter-Integrated Circuit (I²C )is the protocol of simplicity.
You'll find I²C everywhere:
Temperature sensors
Accelerometers
PMICs
EEPROMs
Touch controllers
Camera sensors
Real-time clocks
Most introductions stop after explaining:
SDA
SCL
Address
ACK
But that leaves out the most interesting parts.
Why does I²C use open-drain signaling?
How can multiple controllers share the same bus?
How does arbitration work?
What happens when a peripheral slows down communication?
How do drivers recover from a stuck bus?
This article explores what really happens inside an I²C transaction.
Why I²C Exists
Imagine a board with:
Temperature sensor
Accelerometer
EEPROM
RTC
Using UART:
MCU ---- Sensor
MCU ---- EEPROM
MCU ---- RTC
MCU ---- Accelerometer
Pins quickly become a problem.
I²C solves this.
SDA ----------------
SCL ----------------
| | |
Sensor EEPROM RTC
Only two signals. Multiple devices. Minimal pin count.
Application Layer
A modern software stack typically looks like:
sensor_read_temperature();
or
hal_sensor_read();
rather than:
i2c_write(...);
A hardware abstraction layer decouples application logic from the communication protocol.
Benefits:
Easier portability
Improved testability
Reduced hardware coupling
The application should care about "reading temperature", not whether the transport uses I²C, SPI, UART, or something else.
The Software Stack
A typical I²C transaction path:
Application
↓
Sensor HAL
↓
I²C Driver
↓
I²C Controller
↓
SDA / SCL
↓
Peripheral
Each layer has distinct responsibilities.
The Most Important I²C Concept: Open-Drain Signaling
Unlike SPI and UART:
I²C devices do not actively drive logic high.
Instead:
0 = Drive Low
1 = Release Line
Pull-up resistors generate the high state.
Why Open Drain?
Because multiple devices share the bus.
If one device drives:
HIGH
while another drives:
LOW
a short circuit can occur. Open-drain avoids this. Any device can safely pull the line low.
Pull-Up Resistors
Without pull-ups:
SDA
SCL
would float.
Typical values:
2.2kΩ
4.7kΩ
10kΩ
depending on:
Bus capacitance
Voltage level
Speed
Pull-up selection directly affects signal quality.
START Condition
Data transfers begin with START.
Generated when:
SDA: HIGH → LOW
while
SCL: HIGH
This condition cannot appear during normal data transfer. Every device on the bus recognizes it.
STOP Condition
Generated when:
SDA: LOW → HIGH
while
SCL: HIGH
This marks the end of the transaction.
Addressing
After START:
START
ADDRESS
R/W
ACK
Example:
0x48
for a temperature sensor. Addressing allows multiple peripherals to share the same bus.
ACK and NACK
Every byte transferred is followed by:
ACK
or
NACK
ACK:
0
NACK:
1
This provides a basic handshake mechanism.
Examples:
Device not present
Invalid register
Transaction complete
Unlike UART parity bits, ACK/NACK provides transaction-level confirmation rather than bit-level error detection.
I²C Controller Architecture
A modern controller contains:
Control Registers
Clock Generator
TX FIFO
RX FIFO
Shift Register
Interrupt Logic
DMA Interface
Status Engine
Many concepts are similar to SPI. The protocol handling is significantly more complex.
Clock Generation
The controller generates:
SCL
Clock frequency examples:
100 kHz
400 kHz
1 MHz
3.4 MHz
Different peripherals support different speed grades.
Arbitration
One of the most elegant features of I²C.
Consider:
Controller A
Controller B
Both start transmitting. Each controller monitors SDA while transmitting.
If a controller transmits:
1
but observes:
0
it immediately stops. The winning controller continues. No corruption occurs. No retransmission collision occurs. This is why I²C supports multi-controller systems.
Clock Stretching
Sometimes a peripheral is not ready.
Instead of losing data:
The peripheral holds:
SCL = LOW
This pauses communication. The controller waits until the peripheral releases the line. Not all modern systems enable clock stretching. Many high-performance systems disable it for predictability.
Interrupt Driven I²C
For small transfers:
START
Address
Data
STOP
may complete quickly.
For larger workloads:
FIFO Threshold
↓
Interrupt
↓
ISR
↓
Continue Transfer
Interrupts reduce CPU overhead.
DMA Driven I²C
Some controllers support DMA.
Flow:
Memory
↓
DMA
↓
I²C FIFO
↓
Bus
Useful for:
EEPROM operations
Sensor logging
Large register transfers
Common Real-World Problems
Missing Pull-Ups - The symptoms are Bus stuck low No communication
Wrong Address - The symptoms are NACK after address phase
Bus Contention - The symptoms are Random communication failures
Stuck SDA - Common after peripheral reset issues. Bus recovery may require manually toggling SCL.
Clock Stretching Timeout - Peripheral never releases SCL. Driver must recover.
Debugging With a Logic Analyzer
A logic analyzer is often the fastest path to diagnosis.
Check:
✓ START condition
✓ Address
✓ ACK/NACK
✓ Register address
✓ Data bytes
✓ STOP condition
Many I²C issues become obvious immediately once the waveform is visible.
Putting Everything Together
The next time an application calls:
temperature = sensor_read_temperature();
The actual flow is:
Application
↓
Sensor HAL
↓
I²C Driver
↓
Controller Registers
↓
START
↓
Address
↓
ACK
↓
Data Transfer
↓
STOP
↓
Application
What appears to be a simple sensor read is actually a coordinated interaction between software layers, hardware state machines, open-drain signaling, arbitration logic, clock generation, and bus timing.
Understanding these mechanisms is essential for building reliable embedded systems.
What's Next?
In the next article we'll explore:
Why I3C Exists: The Evolution Beyond I²C:
Limitations of I²C
Why MIPI created I3C
Dynamic addressing
In-band interrupts
HDR modes
Backward compatibility
Driver architecture changes
Real-world migration challenges
And most importantly:
Why modern sensors are increasingly moving toward I3C.


