I²C
Two-wire communication between controllers and peripherals
I²C Fundamentals
I²C (Inter-Integrated Circuit) is a synchronous, serial bus developed by Philips (now NXP) that lets many devices share just two wires: SDA (data) and SCL (clock). Instead of giving every peripheral its own select line like SPI does, I²C gives every device an address — the master names who it wants to talk to, and every device listens on the same two wires and only responds when its own address is called.
Only two shared signal lines, regardless of how many devices are on the bus
Address-based device selection — no extra select pin per device
Built-in per-byte acknowledgment (ACK/NACK) Supports multiple masters with arbitration
A slave can pause a transaction via clock stretching Lower throughput and more protocol overhead than SPI
Half-duplex — data moves in only one direction at a time Needs pull-up resistors to function at all
Bus capacitance and rise time cap practical speed and wire length Where you'll meet it
Sensors, EEPROM, real-time clocks, GPIO expanders, ADC/DAC, PMICs, display controllers and battery-management ICs are classic I²C peripherals — anywhere pin count matters more than raw speed, and several low-rate devices can comfortably share one bus.
The Electrical Layer
Everything distinctive about I²C — its wired-AND behavior, ACK, arbitration, clock stretching — comes from one electrical choice: devices use open-drain outputs. A device can pull a line LOW, but it never actively drives it HIGH. HIGH only happens because a pull-up resistor lifts the line once every device on it has released.
The rule this creates If even one device on the bus pulls a line LOW, the whole bus reads LOW — no matter how many other devices are trying to release it HIGH. This single wired-AND rule is what makes ACK, arbitration and clock stretching possible without any dedicated hardware for them.
Pull-up resistor sizing Too large → slow rising edge, may violate timing at higher speeds
Too small → more current drawn whenever a device pulls the line LOW
Correct value is a trade-off against bus capacitance and target speed Bus capacitance & rise time
The HIGH transition isn't actively driven — it's an RC charging curve set by the pull-up resistor and the total bus capacitance (traces, pins, connectors). More devices or longer traces mean more capacitance, a slower rise, and a lower practical maximum clock speed. This is also why I²C suits short, board-level wiring rather than long cable runs.
A 3.3 V MCU talking to a 1.8 V sensor cannot simply share a line — a bidirectional, open-drain- aware level translator is needed. A plain unidirectional logic-level converter is not automatically suitable, because both SDA and SCL must remain able to be pulled LOW from either side.
START and STOP Conditions
Every I²C transaction is framed by two special SDA transitions that only happen while SCL is HIGH — everywhere else on the bus, SDA is only allowed to change while SCL is LOW.
Why this rule matters Because normal data only ever changes while SCL is LOW, a receiver can unambiguously recognize a START or STOP the instant it sees SDA move while SCL is HIGH — there's no other legal reason for that to happen. This is the entire mechanism that lets devices detect the beginning and end of a transaction without a separate framing signal.
Bit Transfer and Byte Format
One bit moves per SCL clock cycle. Bytes are grouped as 8 data bits followed by a 9th acknowledgment bit — every single byte on an I²C bus, address or data, carries this same 9-clock shape.
After 8 bits, the receiver pulls SDA LOW during the 9th clock. That LOW is the acknowledgment — "I got the byte."
The receiver simply leaves SDA HIGH (releases it) during the 9th clock. NACK doesn't automatically mean failure — a master deliberately NACKs the final byte of a read to tell the slave "stop, I don't need more."
Who generates ACK? (frequent interview trap) ACK is always generated by whichever side received that byte — not always the same device. During a write, the master sends and the slave ACKs. During a read, the slave sends and the master ACKs (or NACKs the last byte). Ownership of ACK flips with the direction of the byte, not with who's "in charge" of the bus.
I²C Addressing
7-bit vs 10-bit 7-bit addressing is by far the most common, giving 112 usable addresses after reserved ranges. 10-bit addressing exists for larger systems needing more address space, using a two-byte addressing scheme most devices don't implement.
Two devices sharing the same fixed address on one bus is a real problem — both may respond to the same call. Fixes include address-select pins on the part, an I²C multiplexer/switch to put each device on its own sub-bus, or simply using a separate bus.
I²C reserves a special "General Call" address that can address multiple listening devices at once — but support for it is entirely implementation-dependent. Never assume a given slave responds to it without checking its datasheet.
I²C Transactions
Why repeated START?
It lets the master switch from writing (telling the slave which register to read) to reading, without ever releasing the bus with a full STOP in between. That guarantees no other master can slip in and change the slave's internal register pointer between the two phases.
For multiple bytes, the master ACKs every byte except the last, which it NACKs to signal "stop sending" right before the STOP condition — e.g. receiving 0xAB (ACK) then 0xCD (NACK) as a two-byte sensor read.
Clock Stretching
SCL is normally driven by the master — but because it's open-drain like SDA, a slave that needs more time before it can respond is allowed to hold SCL LOW itself. The master, which can only release SCL and watch for it to actually go HIGH, is forced to wait.
A sensor asked for fresh data may need real conversion time before it has anything valid to send. Rather than requiring every master to know each slave's exact timing budget in advance, the slave can simply stretch the clock until it's ready — then release SCL and let the transaction continue exactly where it left off. Support and limits on this vary by MCU and device, so both datasheets need checking.
Multi-Master & Arbitration
Because SDA and SCL are open-drain and wired-AND, more than one master can share the same bus safely. If two masters start transmitting at the same time, arbitration — built directly into the electrical behavior of the bus — decides who continues.
LOW always wins over HIGH. A master sending 1 (releasing) while another sends 0 (pulling LOW) sees the bus read back 0 — a mismatch against its own bit — and immediately stops driving.
Because SCL is shared too, a master that tries to release SCL but sees it held LOW (by another master or a stretching slave) simply waits — this naturally synchronizes every master's clock to the slowest one active at that moment.
I²C arbitration vs CAN arbitration Both rely on a wired signaling trick where LOW/dominant beats HIGH/recessive, but CAN builds arbitration into an explicit identifier field as part of every frame, while I²C arbitration only becomes relevant on the rare occasion two masters happen to transmit at once.
I²C vs SPI vs UART vs CAN
Four protocols, four different jobs. The right choice usually comes down to pin budget, required speed, how many devices share the link, and whether the link needs to survive an entire vehicle harness rather than just a few centimeters of PCB.
The short version I²C — many low-speed devices, minimal pins. SPI — one fast device, extra CS pin acceptable. UART — a simple point-to-point link with no bus-sharing concept at all. CAN — not a local peripheral bus at all, but a robust network connecting separate ECUs.
MCU I²C Peripheral
SDA/SCL pins are usually shared with GPIO through a mux. A perfectly configured I²C controller still produces nothing on the bus if the pins were never switched into their I²C alternate function.
FIFO & interrupts Many controllers buffer TX/RX bytes and raise interrupts for address-match, TX-ready, RX-ready, transfer-complete, ACK-error and arbitration-lost conditions, so software isn't required to babysit every clock.
Typical CONTROL/STATUS fields (illustrative) Exact bit names and behavior are always vendor-specific — treat this as a mental model, not a register map to copy.
Driver Development
START → wait → send address → check ACK → send register → check ACK → send/receive data → check ACK → STOP. Simple and easy to reason about, but the CPU is busy the whole time.
Kick off the transfer, return immediately, and let a state machine advance on each interrupt (address sent, byte sent, ACK received, transfer complete) before invoking a completion callback.
A typical state machine Blocking vs non-blocking A blocking I2C_Read() waits until data arrives before returning — simple, but ties up the calling task. A non-blocking I2C_ReadAsync() returns immediately and completes later via interrupt or DMA callback, which suits real-time systems better at the cost of more state to manage.
I²C in an RTOS
It's common for several tasks to each own a different device on the same physical I²C bus — one task reading a sensor, another writing EEPROM, a third checking an RTC. All of them share one peripheral, so access must be serialized.
Why a mutex is mandatory here A full I²C transaction — START through STOP — must be treated as atomic. If Task A's START and address byte are followed by Task B's START before Task A finishes, the two transactions interleave on the wire and corrupt both. A mutex held for the entire select → transfer → STOP sequence is the standard fix.
A robust driver assumes any of these can happen: SCL never returns HIGH, SDA stays stuck LOW, a slave never ACKs, or the bus reports busy indefinitely. Without a timeout, a hung transfer can block its calling task forever — always pair a wait with a timeout and a defined recovery path.
Errors and Bus Recovery
Bus recovery sequence The exact recovery procedure should follow the specific MCU's reference manual and the peripheral's own datasheet guidance.
Practical I²C Debugging
Reading a logic analyzer capture Decode every byte against the datasheet — don't just eyeball hex. Confirm each ACK/NACK explicitly rather than assuming it's there.
I²C address scanner A simple, very practical tool: sweep candidate addresses, sending each with the write bit set, and note which ones ACK. It confirms a device is present and responding at that address — it cannot, by itself, tell you which specific device that is.