Skip to content

I2C (Inter-Integrated Circuit)

Overview

I2C is a multi-master, multi-slave half-duplex bus using two wires: SDA (data) and SCL (clock). This implementation uses the Linux i2c-dev kernel interface with I2C_RDWR ioctl for combined (atomic) read/write transactions.

Status: Implemented

Full implementation with combined messages, bus scan, 7-bit and 10-bit addressing, and register access helpers.

Protocol Specification

Transaction Format

┌───────┬─────────────┬─────┬──────────────────────┬─────┬───────┐
│ START │   Address   │ R/W │      Data Bytes      │ ACK │ STOP  │
│  (S)  │   (7 bits)  │(1b) │     (N bytes)        │(1b) │  (P)  │
└───────┴─────────────┴─────┴──────────────────────┴─────┴───────┘

START: SDA falls while SCL is high
STOP:  SDA rises while SCL is high
ACK:   Receiver pulls SDA low on 9th clock
NACK:  Receiver leaves SDA high on 9th clock

Combined Write-then-Read (Repeated START)

┌───────┬───────┬───┬────────────┬─────┬────────┬───────┬───┬──────┬─────┬──────┐
│ START │ ADDR  │ W │  Reg Addr  │ ACK │ Re-START│ ADDR  │ R │ Data │NACK │ STOP │
│       │ (7b)  │   │  (1 byte)  │     │        │ (7b)  │   │(N B) │     │      │
└───────┴───────┴───┴────────────┴─────┴────────┴───────┴───┴──────┴─────┴──────┘
  No STOP between write and read → atomic register read

Clock Speeds

Mode Frequency Use Case
Standard 100 kHz EEPROMs, basic sensors
Fast 400 kHz Most peripherals
Fast-plus 1 MHz High-speed sensors
High-speed 3.4 MHz Specialized ICs

API Reference

Data Structures

typedef struct {
    int       fd;              /* Bus file descriptor */
    char      device[32];      /* Device path */
    uint16_t  slave_addr;      /* Current slave address */
    bool      ten_bit;         /* 10-bit addressing mode */
    bool      is_open;
    uint64_t  transactions;    /* Statistics */
    uint64_t  nacks;           /* NACK errors */
} i2c_t;

Constants

#define I2C_MAX_TRANSFER    4096   /* Max bytes per transfer */

Bus Lifecycle

i2c_open

int i2c_open(i2c_t *i2c, const char *device);

Open an I2C bus device (e.g., "/dev/i2c-1").

i2c_close

void i2c_close(i2c_t *i2c);

i2c_set_slave

int i2c_set_slave(i2c_t *i2c, uint16_t addr, bool ten_bit);

Set the target slave address. Uses I2C_SLAVE_FORCE if the device is bound to a kernel driver.

Combined Messages (I2C_RDWR)

i2c_write_read

int i2c_write_read(i2c_t *i2c, uint16_t addr,
                   const void *write_buf, size_t write_len,
                   void *read_buf, size_t read_len);

Atomic write-then-read transaction using repeated START. This is the preferred access method it reads from a specific register without releasing the bus.

Parameter Type Description
addr uint16_t 7-bit slave address
write_buf const void * Data to write (register address, etc.)
write_len size_t Write length
read_buf void * Buffer for response data
read_len size_t Number of bytes to read

i2c_write / i2c_read

int i2c_write(i2c_t *i2c, uint16_t addr, const void *data, size_t len);
int i2c_read(i2c_t *i2c, uint16_t addr, void *buf, size_t len);

Simple write-only or read-only transactions.

Register Access (Convenience)

int i2c_write_reg8(i2c_t *i2c, uint16_t addr, uint8_t reg, uint8_t value);
int i2c_read_reg8(i2c_t *i2c, uint16_t addr, uint8_t reg, uint8_t *value);
int i2c_read_reg16(i2c_t *i2c, uint16_t addr, uint8_t reg, uint16_t *value);
int i2c_write_reg16(i2c_t *i2c, uint16_t addr, uint8_t reg, uint16_t value);
int i2c_read_block(i2c_t *i2c, uint16_t addr, uint8_t start_reg, void *buf, size_t len);

Byte Order

i2c_read_reg16 / i2c_write_reg16 use big-endian byte order, which is the convention for most sensor ICs (e.g., TI, Bosch, ST).

Bus Scan

i2c_scan

int i2c_scan(i2c_t *i2c, uint16_t *found_addrs, int max_count);

Scan the 7-bit address space (0x03–0x77) by sending zero-length writes. Returns the number of responding devices.

Usage Examples

Read Sensor Temperature (e.g., TMP102)

#include "i2c.h"
#include <stdio.h>

#define TMP102_ADDR    0x48
#define TMP102_TEMP_REG 0x00

int main(void)
{
    i2c_t bus;
    int rc = i2c_open(&bus, "/dev/i2c-1");
    if (rc < 0) {
        fprintf(stderr, "I2C open failed: %s\n", strerror(-rc));
        return 1;
    }

    /* Read 16-bit temperature register */
    uint16_t raw_temp;
    rc = i2c_read_reg16(&bus, TMP102_ADDR, TMP102_TEMP_REG, &raw_temp);
    if (rc < 0) {
        fprintf(stderr, "Read failed: %s\n", strerror(-rc));
        i2c_close(&bus);
        return 1;
    }

    /* TMP102: 12-bit resolution, 0.0625°C per LSB */
    float temp_c = (int16_t)(raw_temp) / 256.0f;
    printf("Temperature: %.2f °C\n", temp_c);

    i2c_close(&bus);
    return 0;
}

Bus Scan (Device Discovery)

#include "i2c.h"
#include <stdio.h>

int main(void)
{
    i2c_t bus;
    i2c_open(&bus, "/dev/i2c-1");

    uint16_t devices[128];
    int count = i2c_scan(&bus, devices, 128);

    printf("I2C Bus Scan (/dev/i2c-1):\n");
    printf("     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f\n");

    for (int row = 0; row < 8; row++) {
        printf("%02x: ", row * 16);
        for (int col = 0; col < 16; col++) {
            uint16_t addr = row * 16 + col;
            bool found = false;
            for (int i = 0; i < count; i++) {
                if (devices[i] == addr) { found = true; break; }
            }
            if (addr < 0x03 || addr > 0x77) printf("   ");
            else if (found) printf("%02x ", addr);
            else printf("-- ");
        }
        printf("\n");
    }
    printf("\n%d device(s) found\n", count);

    i2c_close(&bus);
    return 0;
}

Multi-Byte Block Read (e.g., MPU6050 Accelerometer)

#include "i2c.h"
#include <stdio.h>

#define MPU6050_ADDR     0x68
#define ACCEL_XOUT_H     0x3B

int main(void)
{
    i2c_t bus;
    i2c_open(&bus, "/dev/i2c-1");

    /* Read 6 bytes starting at ACCEL_XOUT_H (auto-increment) */
    uint8_t accel_data[6];
    i2c_read_block(&bus, MPU6050_ADDR, ACCEL_XOUT_H, accel_data, 6);

    int16_t ax = (int16_t)(accel_data[0] << 8 | accel_data[1]);
    int16_t ay = (int16_t)(accel_data[2] << 8 | accel_data[3]);
    int16_t az = (int16_t)(accel_data[4] << 8 | accel_data[5]);

    printf("Accel: X=%d Y=%d Z=%d (raw)\n", ax, ay, az);

    i2c_close(&bus);
    return 0;
}

Build & Run

# Compile
gcc -Wall -Wextra -Werror -O3 -std=c11 -o i2c_demo i2c.c

# Load kernel module
sudo modprobe i2c-dev

# Add user to i2c group
sudo usermod -aG i2c $USER

# Run
./i2c_demo

Clock Stretching

Normal clock:
SCL: ─┐ ┌─┐ ┌─┐ ┌─┐ ┌─
      └─┘ └─┘ └─┘ └─┘

Clock stretching (slave holds SCL low):
SCL: ─┐ ┌─┐     ┌─┐ ┌─┐ ┌─
      └─┘ └─────┘ └─┘ └─┘
        Slave holds SCL low
        (needs more time to process)

The Linux I2C subsystem handles clock stretching automatically via the hardware controller. If a slave stretches too long, the transaction times out with -ETIMEDOUT.

Error Handling

Error Meaning Recovery
-ENXIO No ACK from slave (wrong address) Check wiring, verify address
-EREMOTEIO NACK during data transfer Slave busy, retry
-ETIMEDOUT Clock stretching timeout Slave hung, reset bus
-EBUSY Address bound to kernel driver Use I2C_SLAVE_FORCE (automatic)

Test Output

$ ./build/i2c_demo
[i2c] Opened /dev/i2c-1
[i2c] Bus scan: 3 devices found (0x48, 0x50, 0x68)
[i2c] TMP102 (0x48): temperature = 23.50 °C
[i2c] EEPROM (0x50): read 16 bytes OK
[i2c] MPU6050 (0x68): WHO_AM_I = 0x68 ✓
[i2c] Stats: 12 transactions, 0 NACKs
[PASS] All i2c tests passed