Understanding I2C Communication

I2C (Inter-Integrated Circuit, also written as I²C) is one of the most popular communication protocols used by microcontrollers to talk to sensors, displays, and other chips. It's a serial, half-duplex, and synchronous interface.

What Makes I2C Special?

Serial: Data is transferred one bit at a time over a single data line, like cars passing through a one-lane bridge one after another.
Half-duplex: Data travels in only one direction at a time, similar to using a walkie-talkie where only one person can talk while the other listens.
Synchronous: Both devices rely on a shared clock signal to coordinate communication, ensuring perfect timing.

The I2C Bus

The I2C bus uses just two wires shared by all connected devices:

Controller and Target Model

I2C uses a controller-target model (formerly master-slave). The controller initiates communication and provides the clock signal. The target responds to the controller's commands. In typical embedded projects, the microcontroller (Pico) acts as the controller, and connected devices like displays or sensors act as targets.

I2C Addresses

Each I2C target device has a 7-bit or 10-bit address (7-bit is most common). This allows up to 128 possible addresses. Many devices have a fixed address defined by the manufacturer, but others allow configuring the lower bits using pins or jumpers.

Speed Modes

ModeSpeed
StandardUp to 100 kbps
FastUp to 400 kbps
Fast Mode PlusUp to 1 Mbps
High-SpeedUp to 3.4 Mbps

I2C on the Raspberry Pi Pico 2

The RP2350 has two separate I2C controllers (I2C0 and I2C1) that can operate simultaneously. Both controllers support multiple pin options for SDA and SCL:

ControllerGPIO Pins
I2C0 - SDAGP0, GP4, GP8, GP12, GP16, GP20
I2C0 - SCLGP1, GP5, GP9, GP13, GP17, GP21
I2C1 - SDAGP2, GP6, GP10, GP14, GP18, GP26
I2C1 - SCLGP3, GP7, GP11, GP15, GP19, GP27

Using I2C with Embassy

Setting up I2C in Embassy is straightforward. Here's a basic example:

// Bind I2C interrupt
bind_interrupts!(struct Irqs {
    I2C0_IRQ => i2c::InterruptHandler<I2C0>;
});

// Initialize I2C
let sda = p.PIN_16;
let scl = p.PIN_17;
let mut config = I2cConfig::default();
config.frequency = 400_000; // 400kHz

let i2c = I2c::new_async(
    p.I2C0,
    scl,
    sda,
    Irqs,
    config,
);

Why Use I2C?

I2C is ideal when you want to connect multiple devices using just two wires. It's well-suited for applications where speed is not critical but wiring simplicity is important. The embedded-hal crate provides standard I2C traits, and platform HALs handle the low-level details.