Skip to content

← Volver al listado

El firmware: Arduino y timing crítico

El Arduino Uno tiene 2 KB de RAM. Una tira de 300 LEDs son 900 bytes solo de píxeles. Cada decisión es una cuestión de espacio.

3 min de lectura

Un Arduino Uno tiene dos kilobytes de RAM. Una tira de trescientos LEDs en RGB ocupa novecientos bytes solo de buffer de píxeles. La mitad de la memoria, solo para “qué pinto”. Cada decisión del firmware es, en realidad, una decisión de espacio.

Diagrama de la state machine del parser del firmware: 5 estados (HUNT_SOF, READ_HEADER, READ_PAYLOAD, READ_CRC, DISPATCH).

El contexto

El papel del firmware (lumware-firmware/lumware-firmware.ino) es intencionadamente estrecho: recibir bytes por serie, identificar frames, pintar la tira con el timing exacto que exige WS281x y contestar con un ACK. Nada más. Ninguna animación guardada, ninguna paleta, ningún “modo demo”. Todo eso vive en el host.

Esta separación no es limpia por estética. Es forzada: el Uno no tiene memoria para un atlas de imágenes, ni puede leer un disco, ni tiene reloj preciso para llevar animaciones complejas con timing fiable. Lo que sí tiene es un pin de salida rápido y una biblioteca (Adafruit_NeoPixel) que respeta el protocolo eléctrico WS281x. Aprovechamos justo eso.

La decisión

El firmware es una state machine de cuatro estados:

HUNT_SOF → READ_HEADER → READ_PAYLOAD → READ_CRC → DISPATCH
  • HUNT_SOF: descarta todo byte que no sea 0xA5. Cuando lo encuentra, ancla un frame nuevo y resetea el estado interno.
  • READ_HEADER: acumula 4 bytes (VERSION, TYPE, LEN_HI, LEN_LO). Si la versión no es 0x01, si el tipo no es conocido, o si LEN > 1024, envía un ACK de error y vuelve a HUNT_SOF.
  • READ_PAYLOAD: lee LEN bytes. Sin buffer entero. Si es un frame DATA, cada tres bytes se pintan directamente con setPixelColor(led_idx, r, g, b) y el firmware pasa al siguiente. Nunca se acumulan 900 bytes en memoria.
  • READ_CRC: 1 byte final. Se compara con el CRC8 running que se ha ido calculando desde VERSION. Si no coincide, ACK CRC_FAIL y fuera.
  • DISPATCH: si todo ha ido bien, leds_strip->show() (solo para DATA), y ACK OK.

Por qué streaming y no buffer entero. Si bufferease el payload DATA para validar el CRC antes de pintar, tendría que reservar hasta 1024 bytes — la mitad de la RAM del Uno, solo para un frame que quizá sea inválido. Así, el “precio” es tener que revertir el frame si el CRC falla: los píxeles ya escritos quedan pintados hasta el próximo show(). La práctica dice que es tolerable — los CRC fallan poco y la pérdida máxima es el contenido de un solo frame, no un crash. show() solo se invoca si el CRC es OK, así que en caso de error el frame no se proyecta a la tira aunque los bytes ya estén escritos en el buffer interno.

Hay cinco códigos de error de ACK bien tipados:

  • 0x00 OK
  • 0x01 CRC_FAIL
  • 0x02 VERSION (frame de otra versión de wire)
  • 0x03 UNKNOWN_TYPE (TYPE no reconocido)
  • 0x04 OVERFLOW (LEN > MAX_PAYLOAD)

¿Por qué cinco y no uno? Porque la causa importa: un CRC_FAIL justifica retransmisión inmediata; un VERSION justifica abortar y renegociar; un OVERFLOW es un bug del host que hay que arreglar, no retransmitir. Distinguirlos en el firmware permite decidir en el host.

Lo que viene

Volvemos al host y miramos por qué el protocolo tiene esta forma y no JSON. Por qué 6 bytes de overhead, por qué big-endian, por qué CRC8 y no CRC16 ni nada. ¿Era una decisión o una consecuencia inevitable?