El i.MX RT1062 que llevan las placas Teensy 4.x trae un periférico llamado FlexIO con el que se pueden armar interfaces que el chip no incluye de fábrica. Paul Stoffregen, de PJRC, le dedicó su columna del 27 de agosto en SparkFun News, a partir de una discusión larga del foro de PJRC que había empezado con la idea de una biblioteca de bus paralelo de propósito general.

El punto de partida es corriente. Si se necesita SPI se usa SPI, si se necesita I2C se usa Wire, y si hace falta un puerto serie se elige uno. El problema aparece cuando la interfaz del otro lado no entra en ninguna de esas cajas. Un LCD paralelo de 16 bits. Una cámara que entrega una docena de bits por cada pulso de reloj de píxel. Un ADC con varias líneas de datos que se desplazan a la vez. O algo que técnicamente es SPI, pero donde el camino eléctrico mete tanto retardo que muestrear MISO a 50 MHz se vuelve el verdadero problema.

Tres cosas distintas que se llaman SPI

La primera trampa es el vocabulario. El RT1062 tiene varios periféricos con nombres parecidos que resuelven problemas muy diferentes.

LPSPI es lo que casi todos entienden por SPI. La biblioteca SPI de Teensy usa ese hardware, y en el Teensy 4.1 hay tres puertos expuestos. Para sensores, pantallas, conversores y demás dispositivos SPI comunes, ahí hay que partir.

FlexSPI es otra cosa. Está pensado para memorias de alta velocidad. Teensy usa una interfaz FlexSPI para su flash de programa, y FlexSPI2 va conectada a los pads de expansión de memoria de la cara inferior del Teensy 4.1. Tiene capacidades de temporización sofisticadas, entre ellas soporte de DQS o data strobe. Programarlo para un dispositivo poco habitual, eso sí, obliga a meterse con su maquinaria de comandos de bajo nivel y con las instrucciones de la tabla de consulta, la LUT.

FlexIO no entrega un controlador SPI terminado. Entrega piezas más básicas. Cada periférico FlexIO aporta shifters y temporizadores, más la lógica para enrutar y controlar señales. La biblioteca FlexIO_t4 expone ese hardware en Teensy y ya lo usa para implementar puertos serie y SPI adicionales. PJRC lo describe como un periférico para construirse los puertos propios, capaz de implementar UART, I2C, SPI, I2S, PWM y otras interfaces más específicas.

PeriféricoPara qué está pensadoDónde aparece en Teensy
LPSPIel SPI común de sensores y pantallastres puertos expuestos en el Teensy 4.1
FlexSPImemorias de alta velocidad, con soporte DQSflash de programa, y FlexSPI2 a los pads de expansión
FlexIObloques configurables para armarse la interfazbiblioteca FlexIO_t4, puertos serie y SPI extra

Emular SPI con FlexIO es, en realidad, el uso aburrido. La pregunta interesante aparece cuando se deja de recrear periféricos que ya existen.

De la pantalla al bus programable

La discusión del foro arrancó con una propuesta de biblioteca de interfaz paralela de propósito general, y la lista de deseos era larga. Buses de 4, 8, 16 y potencialmente más bits. Orden de pines flexible. Velocidades sostenidas de decenas de megahertz. Un reloj generado por hardware opcional. Operación no bloqueante. Y la posibilidad de reordenar bytes, nibbles y bits sueltos mientras los datos se mueven por el sistema.

La aplicación obvia eran las pantallas. Varios controladores LCD aceptan interfaces paralelas estilo 8080, y mover un píxel de 16 bits en una sola operación de bus, en vez de serializarlo por SPI, cambia bastante las cosas.

La conversación se fue rápido más allá de eso. Aparecieron ADC y DAC de alta velocidad, cámaras, buses clásicos de la familia 6502 y varias salidas de ADC muestreadas al mismo tiempo. Un usuario quería capturar 12 bits paralelos de un ADC a unos 50 MHz. A esa altura ya no se hablaba de una biblioteca de pantallas, sino de una interfaz digital programable.

¿Por qué no golpear los registros GPIO y listo?

Es una pregunta razonable, sobre todo en un Cortex-M7 a 600 MHz. El GPIO directo puede ser muy rápido, y varios participantes de la discusión ya tenían controladores de pantalla paralela funcionando así. Una implementación sobre Teensy 3.6 hacía un blit de pantalla completa en unos 7,7 ms, y otra sobre Teensy 4.1 actualizaba una pantalla de 320×480 de 16 bits en unos 5 ms.

O sea que la velocidad bruta no es el argumento para usar FlexIO.

La ventaja real es otra. FlexIO mueve al hardware la parte del protocolo que depende del tiempo. Una escritura paralela hecha a mano se parece a esto.

Código
DATA_PORT = value;

WR_LOW();
delayNanoseconds(...);
WR_HIGH();

Eso puede ser rápido, pero el procesador participa en cada transferencia. Con FlexIO los datos entran a los buffers de los shifters mientras un temporizador de hardware genera el reloj o los pulsos de escritura. Una vez configurado, el conjunto avanza sin que el Cortex-M7 tenga que conmutar la señal en cada pulso.

La diferencia pesa cuando el resto de la aplicación también tiene trabajo. Rezo, miembro del foro de PJRC que desarrollaba un controlador para pantallas ILI948x basado en FlexIO y DMA, tenía un motivo concreto para preferir transferencias no bloqueantes. El mismo Teensy corría una interfaz LVGL, atendía tráfico CAN y registraba datos en una tarjeta SD. Bloquear la CPU durante una transferencia de pantalla de 5 ms suena inofensivo hasta que esos 5 ms caen justo encima de otra cosa que sí tiene requisitos de latencia.

FlexIO no hace que una transición de GPIO sea más rápida. Hace que la CPU deje de ser responsable de cada transición de GPIO.

Shifters, temporizadores y ráfagas

El modelo mental de FlexIO no se parece al de un periférico convencional. En vez de configurar modo 0 de SPI a 20 MHz, se configuran recursos. Shifters que sostienen y serializan o paralelizan datos, temporizadores que deciden cuándo avanzan esos shifters, y pines que llevan las señales resultantes.

La introducción de PJRC a FlexIO_t4 describe cada puerto FlexIO con ocho registros de desplazamiento de 32 bits y ocho temporizadores. Eso permite dejar varias palabras preparadas antes de que el software tenga que rellenar nada.

Una versión simplificada de la configuración del lado de transmisión, sacada del experimento del foro, se ve así.

Código
p->SHIFTCFG[i] =
    FLEXIO_SHIFTCFG_INSRC(1U)
  | FLEXIO_SHIFTCFG_SSTOP(0U)
  | FLEXIO_SHIFTCFG_SSTART(0U)
  | FLEXIO_SHIFTCFG_PWIDTH(shiftWidth - 1U);

p->SHIFTCTL[0] =
    FLEXIO_SHIFTCTL_TIMSEL(timerIndex)
  | FLEXIO_SHIFTCTL_PINCFG(3U)
  | FLEXIO_SHIFTCTL_PINSEL(shifterPin)
  | FLEXIO_SHIFTCTL_SMOD(2U);

Esto ya no es código estilo Arduino. Se está configurando el hardware del shifter. El temporizador define después la temporización de la transferencia.

Código
p->TIMCMP[timerIndex] =
    ((beats * 2U - 1) << 8)
  | (SHIFT_CLOCK_DIVIDER / 2U - 1U);

Lo importante no es memorizar esos registros. Es entender el reparto del trabajo. El software carga los datos y el hardware los pone en los pines en instantes controlados con precisión. Ahí se abre la puerta a rellenos por DMA o por interrupción mientras la interfaz sigue operando.

El DMA ayuda, pero el ancho de banda no sale gratis

El DMA es el acompañante evidente de FlexIO. En vez de interrumpir a la CPU cada vez que un shifter necesita más datos, mueve bloques desde la memoria hacia los registros de FlexIO. En una actualización grande de pantalla eso deja la CPU disponible para el código de la aplicación mientras la transferencia continúa.

La discusión del foro trae un recordatorio incómodo. El DMA no crea un segundo sistema de memoria. El DMA y la CPU siguen compitiendo por los buses y por la memoria. Si los datos que entran terminan teniendo que ir a PSRAM externa, el cuello de botella pasa a ser el ancho de banda de memoria y no FlexIO.

miciwan, otro miembro del foro, aportó experiencia real con una interfaz de cámara de 12 bits. Ahí el límite práctico rondaba un reloj de píxel de 18 a 20 MHz mientras además se movían los datos capturados a memoria externa, y el tráfico DMA adicional empezaba a interferir con la transferencia crítica desde FlexIO.

La solución que mejor funcionó fue pragmática. Usar DMA para la transferencia crítica desde FlexIO hacia memoria local rápida, y después dejar que la CPU haga memcpy() de los buffers ya completos hacia otra parte. Es una lección de diseño embebido que se repite. El diagrama de bloques más elegante no siempre es la implementación más rápida.

Los pines son parte del periférico

Después aparece una limitación que no se ve desde el software. El enrutado de pines.

Un shifter de FlexIO no se conecta automáticamente a cualquier pin del Teensy. Pines concretos del Teensy mapean a pines concretos de FlexIO, y las interfaces paralelas anchas funcionan de forma más natural cuando las señales FlexIO necesarias son contiguas.

Eso puso restricciones fuertes a la idea de una biblioteca paralela genérica. La discusión identificó 20 pines contiguos de FlexIO3 en el Teensy 4.1, lo que vuelve atractivo ese bloque para una interfaz ancha. El detalle es que FlexIO3 no tiene la misma capacidad de DMA que los otros bloques FlexIO.

Queda entonces un compromiso clásico de hardware. El periférico con la mejor distribución de pines no coincide con el que tiene el mejor camino de transferencia de datos.

El rodeo evidente sería repartir el bus entre varios periféricos FlexIO. El problema es conseguir que se comporten como una sola interfaz.

¿Qué pasa si tres FlexIO fingen ser uno?

easone, el miembro del foro que había abierto la discusión original con la idea de la biblioteca paralela de propósito general, publicó una prueba de concepto que sincroniza FlexIO1, FlexIO2 y FlexIO3 para formar una sola interfaz paralela de 8 bits, con los bits de datos repartidos entre los tres periféricos.

El mapeo de la prueba quedó así.

Código
Salida   FlexIO   Pin Teensy

D0       2:0      10
D1       2:1      12
D2       2:2      11

D3       1:4       2
D4       1:5       3
D5       1:6       4

D6       3:16      8
D7       3:17      7

CLK      3:2      14

Ya es interesante porque esos no son ocho pines contiguos cómodos de un solo bloque FlexIO. Lo ingenioso está en la sincronización.

El código arranca primero ráfagas de relleno en FlexIO1 y FlexIO2. Usa retardos medidos en ciclos de CPU para escalonar la partida, y después dispara FlexIO3 en el instante correcto. Con los tres temporizadores de hardware alineados, las ráfagas siguientes se alimentan por interrupción.

Código
FlexIO1:  [relleno] [relleno] [ DATOS ][ DATOS ][ DATOS ]...
FlexIO2:            [relleno] [ DATOS ][ DATOS ][ DATOS ]...
FlexIO3:                      [ DATOS ][ DATOS ][ DATOS ]...
                                       |
                                     RELOJ

Las transferencias de relleno no llevan datos útiles. Son la forma de poner en fase máquinas de estado de hardware independientes.

Una captura con analizador lógico a 24 MHz de muestreo mostró las señales sincronizadas lo bastante bien para la prueba de concepto. El propio autor fue cauto sobre si esa sincronización seguiría siendo confiable a velocidades bastante más altas. La implicancia de arquitectura, eso sí, es más grande que la prueba de ocho bits.