3.11) I2C — Introduction, Specifications, Bus Signals, Master-Slave, Error Handling, Addressing

posted by Hamid Sayyed • November 12, 2025 0 Comments

I2C (pronounced eye-two-see) is a widely used two-wire serial bus standard originally developed by Philips to enable simple, low-speed communication between chips on a PCB. It is especially popular in sensors, RTCs, EEPROMs, ADCs, DACs, and small peripheral modules because it requires only two lines (SDA and SCL) plus pull-ups and supports multiple devices on the same bus. The bus is designed for short distances on a board and for modest speeds — it trades raw throughput for simplicity, small pin count and the ability to share a single pair of wires among many masters and slaves. I2C uses open-drain/open-collector drivers and external pull-up resistors so that any device can pull the line low while release returns it to high; this design supports wired-AND logic useful for arbitration and multi-master operation. A typical I2C transfer uses a START condition, a 7- or 10-bit address, a read/write bit, ACK/NACK bits, and a STOP condition; devices can perform clock stretching and arbitration if multiple masters are active. For embedded programmers the essential skill is to understand bus timing and conditions, how to choose pull-ups and clock speeds for reliable signalling, how to implement master/slave state machines, and how to detect and recover from common errors like NACK, bus stuck low, or arbitration loss. This article explains I2C technical specifications, physical signals, master-slave configuration, addressing schemes, error handling strategies and gives practical tips for implementing robust I2C on AVR and similar microcontrollers.

Specifications and Modes

I2C defines several speed modes and typical electrical levels. Modern derivatives support faster modes but understanding the classic modes is enough for most embedded projects:

Mode Clock (SCL) Description / Typical Use
Standard mode up to 100 kHz Original I2C speed for many sensors and EEPROMs; simple and reliable.
Fast mode up to 400 kHz Common for faster peripherals like some ADCs and displays.
Fast mode plus up to 1 MHz Used when higher throughput is required and bus capacitance is controlled.
High speed mode up to 3.4 MHz Rare for simple micros; needs special signalling and electrical design.

Electrical characteristics include open-drain outputs and pull-up resistors. Typical logic levels follow the MCU supply voltage (for example 3.3 V or 5 V). Bus capacitance, pull-up value, and line length determine rise time and set a practical limit for speed and reliability. Use smaller pull-up resistances for low capacitance and higher speeds; for long lines higher capacitance, use lower speed and/or stronger pull-ups.

Bus Signals and Basic Waveforms

The two active signals are SDA for data and SCL for the clock. Lines are held high by pull-ups. When any device needs to pull a line low it drives it to ground. Important conditions: START is a high→low transition on SDA while SCL is high; STOP is a low→high transition on SDA while SCL is high. Data bits must be stable while SCL is high and may change only while SCL is low. Each byte transferred is followed by an acknowledge bit where the receiver pulls SDA low during the ninth clock to signal success.

Addressing: 7-bit, 10-bit and General Call

I2C addressing usually uses 7 bits giving 128 possible addresses, some reserved by the specification. Devices are addressed by a master sending the 7-bit address followed by a single read/write bit. There is also a 10-bit addressing extension used for large bus environments; it requires two address frames and special handling. A general call address (0000000) lets the master broadcast to all devices for common commands or initialisation. Careful selection of addresses and avoiding collisions is key when multiple modules share the bus.

Address TypeFrame on WireNotes
7-bit[A6 A5 A4 A3 A2 A1 A0] [R/W]Most common; combine these bits into U8 value (address<<1 | R/W).
10-bitFirst: 11110XX0 [R/W=0] then second: lower 8 bits; next byte contains R/W.Rare, used when 7-bit address space insufficient.
General call0000000 [R/W]Broadcast to all devices that support general call.

Master and Slave Roles, Multi-master and Arbitration

A master initiates transfers and generates clock pulses. A slave responds when addressed. Many systems have a single master; however I2C supports multi-master where more than one master can attempt bus control. Arbitration is automatic: if two masters drive different levels, the one that releases SDA earlier loses arbitration and must cease transmission. This wired-AND behaviour makes multi-master safe if implemented correctly. When a master wants to read data, it transmits the address with R/W=1 and then releases SDA for the slave to drive data during subsequent clocks.

Clock Stretching and Flow Control

Clock stretching allows a slave to pause the master by holding SCL low after pulling SDA. This gives the slave time to prepare data or service an internal operation. Well-designed masters respect stretches and treat long stretches as a sign of heavy slave load or a problematic device. Some simple masters or bit-banging implementations must explicitly implement stretch detection; otherwise they risk data corruption.

Data Frame Sequence (Byte Transfer)

Typical sequence: START, address + R/W, ACK from slave, data byte(s) with ACK after each byte, and STOP. On read operations the slave drives data and the master issues ACK (to continue) or NACK (to terminate read) followed by STOP. Repeated START allows a master to change direction (write then read) without releasing the bus — commonly used for register reads from sensors.

Error Conditions and Handling Strategies

Common errors include NACK after address (no device responded), NACK after data (slave cannot accept more data), bus stuck low (device holding SDA or SCL), arbitration lost, and timeout/clock stretching beyond acceptable limit. Robust firmware detects NACK and retries transfers with backoff. For bus stuck low, firmware can attempt clock pulses on SCL while monitoring SDA to coax devices to release the line, or toggle reset lines if present. Arbitration loss is not an error per se; the losing master must stop and retry later. Always implement timeouts and a limited retry policy to avoid infinite blocking in masters.

Practical error handling checklist: check ACK/NACK bits after address and each data byte, implement retry with a small delay (few ms), detect and recover from stuck bus by issuing up to 9 clock pulses on SCL to release a stuck SDA, and if required reset or power cycle the misbehaving device.

Electrical Design, Pull-ups and Bus Capacitance

Pull-up resistors set the line rise time and must be chosen to match bus capacitance and speed. For small boards and standard mode, 4.7k is common at 5 V. For higher speeds or larger capacitance use lower values (2.2k, 1k) but mind power consumption and device drive limitations. Excessive pull-up strength will increase static current and stress devices; too weak pull-ups produce slow rising edges and communication errors. Keep I2C traces short, avoid long stubs, and if routing across connectors use series termination or buffering chips designed for I2C.

Common SFRs and Registers (AVR Two-Wire Interface / TWI)

On AVR chips the TWI (Two-Wire Interface) peripheral implements I2C-like features. The common registers are TWSR (status), TWBR (bit rate), TWAR (own address), TWDR (data), TWCR (control), and TWAMR (address mask on some AVRs). Status codes in TWSR indicate current bus state (start transmitted, address acked, data transmitted, etc.). Use the status register to build a state machine and handle errors precisely.

RegisterPurpose
TWBRBit rate register that, with prescaler in TWSR, determines SCL frequency using SCLfreq = CPUfreq / (16 + 2×TWBR×4^TWPS).
TWSRStatus register (top 5 bits) and prescaler bits TWPS (bottom two bits).
TWAROwn slave address (7-bit) and General Call recognition bit.
TWDRData register for read/write; writing TWDR loads data to transmit, reading TWDR gives received byte.
TWCRControl register: start/stop, enable TWI, enable interrupt, clear flag, ACK control.

Status Codes — Useful Examples

TWSR gives status codes; firmware should check these. Here are a few commonly used codes and meaning:

Code (hex)Meaning
0x08START transmitted
0x18SLA+W transmitted and ACK received
0x28Data transmitted and ACK received
0x40SLA+R transmitted and ACK received
0x50Data received and ACK returned
0x58Data received and NACK returned (end of read)
0x20, 0x30SLA+W or data transmitted and NACK received (handle as error)

Example: AVR Master Write and Read (Polled TWI)

/* Polled TWI (I2C) master write/read example for AVR
   - Uses TWBR/TWSR/TWCR/TWDR/TWAR registers
   - Includes simple timeout handling to avoid lockups
   - Example demonstrates: write a register, then read N bytes
   - Tested conceptually on ATmega series; adapt CPU_FREQ and addresses as required
*/

#include <avr/io.h>
#include <util/delay.h>
#include <stdint.h>
#include <stdbool.h>>

/* Configure these for your platform */
#define CPU_FREQ_HZ 16000000UL
#define SCL_FREQ_HZ 100000UL   /* 100 kHz standard mode */

/* Timeouts (in TWI operation loops) to avoid infinite blocking */
#define TWI_TIMEOUT 30000UL

/* Helper macro to get TWI status (mask prescaler bits) */
#define TWI_STATUS  (TWSR & 0xF8)

/* Initialize TWI (master) with prescaler = 1 */
void twi_init(uint32_t cpu_hz, uint32_t scl_hz)
{
    /* TWBR = ((F_CPU / SCL) - 16) / 2  when prescaler = 1 */
    uint32_t twbr_val = ((cpu_hz / scl_hz) - 16UL) / 2UL;
    if (twbr_val > 255) twbr_val = 255; /* limit for 8-bit TWBR */
    TWBR = (uint8_t)twbr_val;
    /* Clear prescaler bits (TWPS1:0 = 0) */
    TWSR &= ~((1 << TWPS1) | (1 << TWPS0));
    /* Enable TWI */
    TWCR = (1 << TWEN);
}

/* Send START condition - returns true on success */
bool twi_start(void)
{
    uint32_t timeout = TWI_TIMEOUT;
    TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN); /* send START */
    while (!(TWCR & (1 << TWINT)) && --timeout);
    if (timeout == 0) return false;
    if ((TWI_STATUS != 0x08) && (TWI_STATUS != 0x10)) return false; /* START or repeated START */
    return true;
}

/* Send STOP condition */
void twi_stop(void)
{
    TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN);
    /* TWSTO is cleared by hardware when stop is executed; no need to wait here */
}

/* Write a single byte to bus (data or address). Returns true if ACK received */
bool twi_write_byte(uint8_t data)
{
    uint32_t timeout = TWI_TIMEOUT;
    TWDR = data;
    TWCR = (1 << TWINT) | (1 << TWEN); /* start transmission of data */
    while (!(TWCR & (1 << TWINT)) && --timeout);
    if (timeout == 0) return false;
    uint8_t status = TWI_STATUS;
    /* Status 0x18 = SLA+W transmitted, ACK received
       Status 0x28 = data transmitted, ACK received */
    if ((status == 0x18) || (status == 0x28) || (status == 0x40) || (status == 0x50)) {
        return true;
    }
    return false;
}

/* Write SLA+W (address + write bit) explicitly and check for ACK */
bool twi_send_slave_address_write(uint8_t sla7)
{
    uint8_t addr = (sla7 << 1) | 0; /* R/W = 0 */
    uint32_t timeout = TWI_TIMEOUT;
    TWDR = addr;
    TWCR = (1 << TWINT) | (1 << TWEN);
    while (!(TWCR & (1 << TWINT)) && --timeout);
    if (timeout == 0) return false;
    uint8_t status = TWI_STATUS;
    return (status == 0x18); /* SLA+W transmitted, ACK received */
}

/* Write SLA+R (address + read bit) explicitly and check for ACK */
bool twi_send_slave_address_read(uint8_t sla7)
{
    uint8_t addr = (sla7 << 1) | 1; /* R/W = 1 */
    uint32_t timeout = TWI_TIMEOUT;
    TWDR = addr;
    TWCR = (1 << TWINT) | (1 << TWEN);
    while (!(TWCR & (1 << TWINT)) && --timeout);
    if (timeout == 0) return false;
    uint8_t status = TWI_STATUS;
    return (status == 0x40); /* SLA+R transmitted, ACK received */
}

/* Read a byte and send ACK afterwards (to continue reading) */
uint8_t twi_read_byte_ack(void)
{
    uint32_t timeout = TWI_TIMEOUT;
    TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWEA); /* enable ACK after reception */
    while (!(TWCR & (1 << TWINT)) && --timeout);
    /* If timeout occurs, return 0 (caller should handle) */
    return TWDR;
}

/* Read a byte and send NACK afterwards (to end reading) */
uint8_t twi_read_byte_nack(void)
{
    uint32_t timeout = TWI_TIMEOUT;
    TWCR = (1 << TWINT) | (1 << TWEN); /* NACK (TWEA = 0) */
    while (!(TWCR & (1 << TWINT)) && --timeout);
    return TWDR;
}

/* High-level write: write data buffer to slave register (register addressing common) */
/* sla7 : 7-bit slave address
   reg  : register/address inside slave to write to
   buf  : pointer to data bytes to write
   len  : length of data bytes
   returns true on success */
bool twi_write_reg(uint8_t sla7, uint8_t reg, const uint8_t *buf, uint8_t len)
{
    if (!twi_start()) return false;
    if (!twi_send_slave_address_write(sla7)) { twi_stop(); return false; }
    if (!twi_write_byte(reg)) { twi_stop(); return false; }

    for (uint8_t i = 0; i < len; ++i) {
        if (!twi_write_byte(buf[i])) { twi_stop(); return false; }
    }
    twi_stop();
    return true;
}

/* High-level read: read len bytes from slave starting at register 'reg' */
/* Uses repeated START (write reg then repeated START + read) */
bool twi_read_reg(uint8_t sla7, uint8_t reg, uint8_t *out_buf, uint8_t len)
{
    if (!twi_start()) return false;
    if (!twi_send_slave_address_write(sla7)) { twi_stop(); return false; }
    if (!twi_write_byte(reg)) { twi_stop(); return false; }

    /* Repeated START to switch to read mode */
    if (!twi_start()) { twi_stop(); return false; }
    if (!twi_send_slave_address_read(sla7)) { twi_stop(); return false; }

    for (uint8_t i = 0; i < len; ++i) {
        if (i < (len - 1)) {
            out_buf[i] = twi_read_byte_ack();  /* request ACK to continue */
        } else {
            out_buf[i] = twi_read_byte_nack(); /* last byte, send NACK */
        }
    }
    twi_stop();
    return true;
}

/* Example usage: write one byte to register 0x10, then read 2 bytes back */
int main(void)
{
    uint8_t device_addr = 0x50;   /* change to your slave 7-bit address */
    uint8_t write_reg = 0x10;
    uint8_t write_data = 0xAB;
    uint8_t read_buf[2];

    /* Initialize TWI for 100 kHz using CPU frequency macro */
    twi_init(CPU_FREQ_HZ, SCL_FREQ_HZ);

    /* Small delay to stabilise devices after power-up */
    _delay_ms(10);

    /* Write single byte to device register */
    if (twi_write_reg(device_addr, write_reg, &write_data, 1)) {
        /* optional: success indication (toggle LED or similar) */
    } else {
        /* handle write error: retry, log, or fallback */
    }

    _delay_ms(5); /* short settling time before read */

    /* Read 2 bytes from same register (common for 16-bit sensors) */
    if (twi_read_reg(device_addr, write_reg, read_buf, 2)) {
        /* read_buf[0] and read_buf[1] contain received bytes */
    } else {
        /* handle read error */
    }

    while (1) {
        /* main loop: your application code */
    }
    return 0;
}

Note: above is concise pseudo-code to show the flow: initialise TWBR/TWSR, issue START, send address+R/W, check status codes, write data, read with ACK/NACK, and STOP. In real code always check for NACK and implement retry/timeout logic.

Error Recovery Techniques and Robust Design

Implement retries with exponential backoff when a transfer fails with NACK. Limit retries to avoid lockups. For bus stuck low, the common technique is to toggle SCL up to 9 times while monitoring SDA; this clocks through a stuck slave to release SDA. If the bus remains stuck, attempt a STOP condition, reinitialise the TWI peripheral, and if available, reset the peripheral device. Use timeouts in blocking loops so that an unexpected hung bus does not stall the system. If using interrupts, ensure your ISR is short and that any shared buffers use volatile and atomic access or disable interrupts briefly while manipulating multi-byte variables.

Practical Tips for Embedded Engineers

  • Choose pull-ups that match bus capacitance and speed; test with an oscilloscope if possible.
  • Avoid long stubs and keep topology simple — star topologies are not recommended for I2C.
  • Use repeated START instead of STOP+START when performing combined register-addressed reads for devices that require it.
  • Reserve a couple of debug I2C addresses for development boards to avoid address collisions in prototypes.
  • Consider hardware I2C buffers or level translators for mixed-voltage systems or long lines.

Conclusion

I2C is a compact, flexible bus ideal for board-level communication with sensors and small peripherals. Its two-wire simplicity and built-in features such as ACK/NACK, arbitration and clock stretching make it suitable for many embedded applications, but reliable operation requires careful attention to bus timing, pull-ups, and error handling. Using the AVR TWI peripheral or software bit-banging, designers can implement robust master and slave code by checking TWSR status, handling NACKs, respecting clock stretching, and providing recovery for stuck buses. With the techniques explained above, students and engineers can design dependable I2C systems for a wide range of projects.

Comments

Post a Comment

Subscribe to Post Comments [Atom]