micropython-i2c-hardware-softi2c

Comunicación I2C en MicroPython: I2C y SoftI2C

  • 5 min

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:

  1. SDA (Serial Data): Por donde viajan los datos.
  2. SCL (Serial Clock): La señal de reloj que marca el ritmo (síncrono).
  3. 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))
Copied!

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)
Copied!

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')
Copied!

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

  1. 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.
  1. Direcciones equivocadas: Muchos sensores cambian su dirección si conectas un pin llamado ADDR a 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!