El I2C (Inter-Integrated Circuit) es un bus síncrono que conecta dispositivos mediante dos líneas.
Con solo dos cables, nos permite conectar pantallas, sensores y expansores de puertos a nuestro microcontrolador sin montar un festival de pines.
Es el protocolo que usarás para conectar una pantalla OLED, un sensor de temperatura BME280, un acelerómetro o un expansor de puertos.
Lo genial del I2C es que cada dispositivo tiene una dirección única (como un número de portal en una calle). El microcontrolador (maestro) dice: “¡Hey, dispositivo número 0x3C!”, y solo ese dispositivo responde, ignorando a los demás.
Los dos cables: SDA y SCL
En I2C necesitamos conectar:
- SDA (Serial Data): Por donde viajan los datos.
- SCL (Serial Clock): La señal de reloj que marca el ritmo (síncrono).
- GND: Siempre, siempre, tierras comunes.
Resistencias Pull-Up El bus I2C funciona con lógica de “colector abierto”. Esto significa que necesita resistencias (normalmente 4.7kΩ) conectando SDA y SCL a 3.3V para funcionar. Afortunadamente, la mayoría de módulos y sensores comerciales ya las traen integradas. Pero si usas chips sueltos, ¡no las olvides!
I2C de hardware y SoftI2C
En MicroPython (y en la electrónica en general) hay dos formas de controlar este bus. Es crucial entender la diferencia:
machine.I2C de hardware
Utiliza los circuitos físicos dedicados dentro del chip.
- Pros: Es muy rápido (hasta 400kHz o más), no consume CPU y es muy estable.
- Contras: Estás limitado a usar pines específicos o matrices de pines concretas del microcontrolador.
machine.SoftI2C por software
El procesador “simula” la señal I2C encendiendo y apagando los pines manualmente por software.
- Pros: ¡Puedes usar cualquier par de pines GPIO! Es tremendamente flexible.
- Contras: Es más lento y consume recursos del procesador.
¿Cuál elijo?
En ESP32 y Raspberry Pi Pico, la implementación de SoftI2C es excelente y suficiente para el 90% de los casos (leer sensores, pantallas). Si necesitas máxima velocidad, busca los pines hardware en el datasheet y usa I2C.
Inicialización y escaneo del bus
Lo primero que debemos hacer siempre al conectar un dispositivo I2C es hacer un escaneo. Esto nos confirma que el cableado está bien y nos dice qué dirección tiene nuestro dispositivo.
Vamos a usar SoftI2C por su flexibilidad.
from machine import Pin, SoftI2C
import time
# Configuración para ESP32 (puedes cambiar los pines)
# sda=Pin(21), scl=Pin(22) son los típicos en placas ESP32
i2c = SoftI2C(scl=Pin(22), sda=Pin(21), freq=100000)
# Configuración para Raspberry Pi Pico
# i2c = SoftI2C(scl=Pin(1), sda=Pin(0), freq=100000)
print("Escaneando bus I2C...")
dispositivos = i2c.scan()
if len(dispositivos) == 0:
print("¡No se encontró nada! Revisa cables y pull-ups.")
else:
print("Dispositivos encontrados:", len(dispositivos))
for dispositivo in dispositivos:
print("Dirección Decimal:", dispositivo, " | Hexadecimal:", hex(dispositivo))
Si ves algo como Hexadecimal: 0x3c, ¡felicidades! Tienes conexión. (Por cierto, 0x3C suele ser una pantalla OLED).
Leer y escribir registros
Aquí es donde I2C se diferencia de UART. Los dispositivos I2C funcionan como una estantería llena de cajones numerados. Estos cajones se llaman Registros.
- Si quieres saber la temperatura, tienes que leer el “cajón” (registro) específico donde el sensor guarda ese dato.
- Si quieres configurar la sensibilidad, tienes que escribir en el “cajón” de configuración.
Para ello usamos los métodos readfrom_mem y writeto_mem (mem viene de memoria/registro).
Ejemplo conceptual: lectura de un sensor
Imaginemos un sensor ficticio que tiene la dirección 0x50 y guarda el dato de temperatura en el registro 0x10.
# Dirección del dispositivo (la que obtuvimos al escanear)
DIR_DISPOSITIVO = 0x50
# Dirección del registro que queremos leer
REG_TEMPERATURA = 0x10
# Leemos 2 bytes desde ese registro (porque el dato puede ser grande)
datos = i2c.readfrom_mem(DIR_DISPOSITIVO, REG_TEMPERATURA, 2)
# 'datos' es un objeto bytes, por ejemplo b'\x01\x45'
# Ahora tendríamos que convertir esos bytes a un número útil
valor = int.from_bytes(datos, 'big')
print("Valor crudo del sensor:", valor)
Escritura de una configuración
Ahora queremos cambiar la configuración del sensor. El manual dice que el registro de control es el 0x20 y que escribiendo un 1 activamos el modo de alta precisión.
DIR_DISPOSITIVO = 0x50
REG_CONTROL = 0x20
# Escribimos un byte con valor 1 en el registro 0x20
# Necesitamos pasar los datos como bytes, de ahí el b'\x01'
i2c.writeto_mem(DIR_DISPOSITIVO, REG_CONTROL, b'\x01')
Métodos básicos y métodos de memoria
A veces verás tutoriales usando writeto (sin _mem). La diferencia es sutil pero importante:
writeto(addr, data): Envía datos “a lo bruto” al dispositivo. Útil para pantallas OLED o dispositivos simples que procesan flujos de datos.writeto_mem(addr, reg, data): Envía primero la dirección del registro y luego el dato. Es lo que usan el 99% de los sensores.
Errores comunes
- OSError: [Errno 19] ENODEV: Significa “Error No Device”. El microcontrolador ha gritado “¡Hey!” y nadie ha respondido “¡ACK!”.
- Revisa el cableado (SDA con SDA, SCL con SCL). ¡En I2C NO se cruzan!
- Revisa si el dispositivo tiene alimentación.
- Faltan las resistencias Pull-Up.
- Direcciones equivocadas: Muchos sensores cambian su dirección si conectas un pin llamado
ADDRa GND o a VCC. Verifica el datasheet.
El I2C es la base de la robótica y la domótica moderna. Aunque al principio asusta tener que buscar direcciones de memoria y registros, la buena noticia es que casi siempre existirá una librería (driver) que haga este trabajo sucio por ti, encapsulando estos readfrom_mem en funciones fáciles como sensor.read_temperature().
Pero ahora, si esa librería falla, ¡ya sabes cómo arreglarla!