micropython-spi-comunicacion-alta-velocidad

Comunicación SPI en MicroPython

  • 5 min

El SPI (Serial Peripheral Interface) es un bus síncrono de alta velocidad para comunicar dispositivos.

Mientras que el I2C suele trabajar a 100kHz o 400kHz, el SPI puede alcanzar frecuencias de varios MHz (según la placa, el cableado y el periférico). Por eso, es el protocolo que usaremos casi obligatoriamente para componentes que manejan grandes volúmenes de datos, como:

  • Pantallas TFT/LCD (dibujar píxeles requiere millones de bits).
  • Tarjetas SD (lectura/escritura de archivos).
  • Lectores RFID (como el RC522).
  • Chips Ethernet (como el W5500).

La arquitectura de 4 hilos

A diferencia del I2C, que elige a quién habla mediante una dirección numérica, el SPI es un protocolo más “físico”. Si quieres hablar con un chip, tienes que “tocarle el hombro” activando un cable específico.

Esto implica que necesitamos más cables (normalmente 4):

  1. SCK (Serial Clock): El reloj que marca el ritmo. Lo controla el Maestro.
  2. MOSI (Master Out Slave In): Datos que salen del microcontrolador hacia el dispositivo.
  3. MISO (Master In Slave Out): Datos que entran al microcontrolador desde el dispositivo.
  4. CS / SS (Chip Select / Slave Select): El cable “selector”.

Full Duplex Una gran ventaja de SPI es que es full duplex: puede enviar y recibir datos al mismo tiempo. Puedes imaginarlo como una carretera de doble sentido, mientras que en I2C los dispositivos se turnan para transmitir.

Nomenclatura de las señales

En los dispositivos modernos, a veces los nombres cambian para ser más claros (o para evitar terminología amo/esclavo):

  • MOSI SDO (Serial Data Out) en el Maestro / SDI en el Esclavo.
  • MISO SDI (Serial Data In) en el Maestro / SDO en el Esclavo.
  • CS CE (Chip Enable).

SPI de hardware y SoftSPI

Al igual que en I2C, en MicroPython tenemos dos clases:

  • machine.SPI (Hardware): Usa el bloque de silicio dedicado. Es la opción recomendada. Alcanza velocidades de vértigo (40-80MHz) sin cargar la CPU.
  • machine.SoftSPI (Software): Simulación por bit-banging. Es útil si te quedas sin pines específicos, pero mucho más lento. En pantallas TFT, usar SoftSPI hará que el refresco de imagen se vea lento.

Inicialización del bus

Vamos a configurar un bus SPI por hardware.

En ESP32

El ESP32 tiene dos buses SPI libres para el usuario: HSPI (ID 1) y VSPI (ID 2).

from machine import Pin, SPI

# Configuración usando VSPI (ID 2)
# sck=18, mosi=23, miso=19 son los pines estándar VSPI del ESP32
spi = SPI(2, baudrate=10000000, polarity=0, phase=0, sck=Pin(18), mosi=Pin(23), miso=Pin(19))

# Nota: El pin CS lo gestionamos nosotros manualmente como un Pin normal
cs = Pin(5, Pin.OUT)
cs.value(1) # Desactivado (Activo en bajo)
Copied!

En Raspberry Pi Pico

La Pico tiene SPI0 y SPI1.

# Configuración usando SPI0
# sck=Pin(18), mosi=Pin(19), miso=Pin(16)
spi = SPI(0, baudrate=10000000, polarity=0, phase=0, sck=Pin(18), mosi=Pin(19), miso=Pin(16))
cs = Pin(17, Pin.OUT)
cs.value(1)
Copied!

Modos SPI: polaridad y fase

Aquí es donde el SPI se complica un poco. No todos los chips escuchan igual. Hay 4 combinaciones posibles de cómo funciona el reloj, definidas por:

  • Polaridad (CPOL): ¿El reloj está en 0 o en 1 cuando no se usa?
  • Fase (CPHA): ¿Leemos el dato en el flanco de subida o en el de bajada?

La mayoría de dispositivos usan el Modo 0 (CPOL=0, CPHA=0), pero si un dispositivo no te responde, revisa su datasheet para ver qué modo espera.

Cómo enviar y recibir datos

En SPI, la comunicación es un intercambio. Imagina dos engranajes girando: por cada bit que empujas fuera (MOSI), un bit entra (MISO). Siempre.

Aunque solo quieras enviar, el hardware recibirá algo (basura o 0s). Aunque solo quieras recibir, tienes que enviar algo (normalmente bytes vacíos 0x00 o 0xFF) para “empujar” al reloj.

Control de Chip Select (CS)

El protocolo SPI funciona casi siempre con lógica Active Low.

  1. Ponemos CS en LOW (0V) El dispositivo se despierta y escucha.
  2. Transmitimos datos.
  3. Ponemos CS en HIGH (3.3V) El dispositivo deja de escuchar.
import time

# Datos a enviar
mensaje = b'Hola'

# 1. Seleccionamos el esclavo
cs.value(0)

# 2. Escribimos
spi.write(mensaje)

# 3. Liberamos el esclavo
cs.value(1)
Copied!

Leyendo datos (Intercambio)

Si queremos leer 5 bytes de un sensor:

# Buffer para guardar lo recibido (lleno de ceros)
buffer_recepcion = bytearray(5)

cs.value(0)

# Leemos 5 bytes. 
# Internamente, MicroPython está enviando 5 bytes de 0x00 para generar el reloj
spi.readinto(buffer_recepcion)

cs.value(1)

print("Recibido:", buffer_recepcion)
Copied!

Transacción completa de escritura y lectura

A veces necesitamos enviar un comando y leer la respuesta simultáneamente.

comando = b'\xAB' # Comando de ejemplo
respuesta = bytearray(1)

cs.value(0)
# Escribe el comando Y guarda lo que entra en 'respuesta' al mismo tiempo
spi.write_readinto(comando, respuesta)
cs.value(1)

print("Respuesta al comando:", respuesta)
Copied!

Ejemplo práctico de loopback SPI

Al igual que con la UART, podemos probar el SPI sin sensores conectando el cable MOSI directamente al MISO.

from machine import Pin, SPI
import time

# Ajusta los pines según tu placa
spi = SPI(1, baudrate=1000000, polarity=0, phase=0, sck=Pin(10), mosi=Pin(11), miso=Pin(12))

# Mensaje de prueba
tx_data = b'MicroPython SPI'
rx_data = bytearray(len(tx_data))

print("Enviando:", tx_data)

# Al estar unidos MOSI y MISO, lo que sale vuelve a entrar
spi.write_readinto(tx_data, rx_data)

print("Recibido:", rx_data)

if tx_data == rx_data:
    print("¡Test Loopback Exitoso!")
else:
    print("Fallo en la transmisión.")
Copied!

Qué implementación elegir

  • Usa I2C si tienes muchos sensores lentos y quieres ahorrar cables.
  • Usa SPI si necesitas mover datos rápido (Pantallas, SD) o si el dispositivo lo requiere específicamente.
  • Recuerda siempre gestionar manualmente el pin CS antes y después de cada transacción.

Con UART, I2C y SPI en tu caja de herramientas, ya no hay chip que se te resista. Ahora sí, podemos pasar a conectar dispositivos reales y complejos.