El SPI es un bus serie síncrono y dúplex completo orientado a transferencias rápidas entre un controlador y uno o varios periféricos.
Cuando necesitamos mover datos rápido —por ejemplo, pintar píxeles en una pantalla TFT, leer una tarjeta SD o comunicar con un chip de radio LoRa— el I2C se queda corto. El SPI puede alcanzar fácilmente velocidades de 8, 20 o incluso 80 MHz.
La API SPI de Zephyr tiene algo más de ceremonia que la de I2C. Requiere manejar buffers y la configuración de chip select (CS) de forma explícita; vamos a verla por partes.
Diferencias entre I2C y SPI
Antes de escribir código, hay que entender cómo ve Zephyr estos buses:
- Full Duplex: El SPI envía y recibe a la vez. Mientras sale un byte por MOSI, entra otro por MISO.
- Chip Select (CS): En I2C seleccionamos por dirección (software). En SPI seleccionamos bajando un pin físico a tierra. Zephyr gestiona este pin automáticamente si lo configuramos bien.
- Buffers: La API de SPI está pensada para DMA. No enviamos “un byte”, enviamos “bloques de memoria”.
Configurar el hardware en el overlay
Cuando el chip select se controla mediante GPIO, la propiedad cs-gpios se define en el controlador SPI y el reg del periférico selecciona su índice.
Supongamos que tenemos una Pantalla LCD conectada al puerto spi1. Queremos usar el pin 10 del puerto 0 como CS y ir a 8MHz.
/* app.overlay */
#include <zephyr/dt-bindings/gpio/gpio.h>
&spi1 {
status = "okay";
/* Definimos los pines CS gestionados por software (GPIO) */
/* <&controlador pin flags> */
cs-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>;
/* Nuestro dispositivo (ej. Pantalla) */
my_display: display@0 {
compatible = "mi-empresa,mi-pantalla"; /* Compatible de ejemplo */
reg = <0>; /* Índice dentro del array cs-gpios (0 = el pin 10) */
/* Configuración SPI */
spi-max-frequency = <8000000>; /* 8 MHz */
};
};El compatible es un marcador para el ejemplo. En un proyecto real debes usar el del driver existente o crear un binding propio que herede las propiedades comunes de un periférico SPI.
Usar cs-gpios es más flexible que usar el “Hardware CS” nativo del microcontrolador. Permite usar cualquier pin como Chip Select, y Zephyr se encarga de bajarlo y subirlo justo antes y después de la transmisión.
La API de SPI: buffers y conjuntos
Para enviar datos, Zephyr utiliza dos estructuras: spi_buf y spi_buf_set.
Esto parece mucha burocracia para enviar un dato, pero tiene una razón: permite enviar datos que están dispersos en la memoria RAM como si fueran una sola trama continua, sin tener que copiarlos a un buffer intermedio. Es eficiencia pura.
Ejemplo: escribir en una pantalla
Supongamos que quieres enviar un comando de un byte seguido de cien bytes de imagen.
#include <zephyr/kernel.h>
#include <zephyr/drivers/spi.h>
#include <errno.h>
/* Obtenemos el nodo */
#define SPI_OP SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_LINES_SINGLE
static const struct spi_dt_spec dev_spi = SPI_DT_SPEC_GET(DT_NODELABEL(my_display), SPI_OP, 0);
int main(void) {
if (!spi_is_ready_dt(&dev_spi)) {
printk("SPI no listo\n");
return -ENODEV;
}
/* Datos a enviar */
uint8_t comando = 0x2C; // Write RAM
uint8_t datos[] = {0xFF, 0x00, 0xFF, 0x00}; // Píxeles...
/* 1. Definimos los buffers individuales */
struct spi_buf tx_bufs[] = {
{ .buf = &comando, .len = 1 },
{ .buf = datos, .len = sizeof(datos) }
};
/* 2. Empaquetamos en un Set */
/* Le decimos: "Envía todo esto seguido, sin levantar el CS entre medias" */
struct spi_buf_set tx = {
.buffers = tx_bufs,
.count = 2
};
/* 3. Enviamos (Write) */
/* Al ser solo escritura, pasamos NULL en el set de recepción */
int ret = spi_write_dt(&dev_spi, &tx);
if (ret != 0) {
printk("Error SPI: %d\n", ret);
}
return ret;
}Fíjate en la potencia de esto: Hemos enviado un comando y un array de datos en una sola transacción. El Chip Select bajó al principio y subió al final. Si hubiéramos hecho dos llamadas a spi_write_dt, el CS habría subido entre medias, rompiendo la comunicación con la pantalla.
Enviar y recibir a la vez con transceive
Si estás leyendo un sensor SPI, necesitas enviar (normalmente una dirección) y recibir a la vez.
uint8_t tx_data[] = {0x80 | 0x10, 0x00}; /* Comando + byte dummy */
uint8_t rx_data[2];
struct spi_buf tx_b = { .buf = tx_data, .len = sizeof(tx_data) };
struct spi_buf_set tx = { .buffers = &tx_b, .count = 1 };
struct spi_buf rx_b = { .buf = rx_data, .len = sizeof(rx_data) };
struct spi_buf_set rx = { .buffers = &rx_b, .count = 1 };
/* Transacción Full Duplex */
/* Nota: En SPI, para leer N bytes, normalmente tienes que enviar N bytes "basura" (dummy) para generar el reloj */
spi_transceive_dt(&dev_spi, &tx, &rx);
/* En este protocolo de ejemplo, rx_data[0] coincide con el comando
y el dato útil llega durante el byte dummy en rx_data[1]. */