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¶
Bus Lifecycle¶
i2c_open¶
Open an I2C bus device (e.g., "/dev/i2c-1").
i2c_close¶
i2c_set_slave¶
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¶
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) |