Skip to content

← Tornar a la llista

El firmware: Arduino i timing crític

L'Arduino Uno té 2 KB de RAM. Una tira de 300 LEDs són 900 bytes només de pixels. Cada decisió és una qüestió d'espai.

3 min de lectura

Un Arduino Uno té dos kilobytes de RAM. Una tira de tres-cents LEDs pintats en RGB ocupa nou-cents bytes només pel buffer de pixels. La meitat de la memòria, només per “què pinto”. Cada decisió del firmware és, en realitat, una decisió d’espai.

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

El context

El paper del firmware (lumware-firmware/lumware-firmware.ino) és intencionalment estret: rebre bytes per serial, identificar frames, pintar la tira amb el timing exacte que demana WS281x i contestar amb un ACK. Res més. Cap animació guardada, cap paleta, cap “mode demo”. Tot això viu al host.

Aquesta separació no és neta per estètica. És forçada: l’Uno no té memòria per a un atlas d’imatges, ni té com llegir un disc, ni té rellotge precís per portar animacions complexes amb timing fiable. El que sí té és un pin de sortida ràpid i una biblioteca (Adafruit_NeoPixel) que respecta el protocol elèctric WS281x. Aprofitem just això.

La decisió

El firmware és una state machine de quatre estats:

HUNT_SOF → READ_HEADER → READ_PAYLOAD → READ_CRC → DISPATCH
  • HUNT_SOF: descarta tot byte que no sigui 0xA5. Quan el troba, ancora un frame nou i reseteja l’estat intern.
  • READ_HEADER: acumula 4 bytes (VERSION, TYPE, LEN_HI, LEN_LO). Si la versió no és 0x01, si el tipus no és conegut, o si LEN > 1024, envia un ACK d’error i torna a HUNT_SOF.
  • READ_PAYLOAD: llegeix LEN bytes. Cap buffer sencer. Si és un frame DATA, cada tres bytes es pinten directament amb setPixelColor(led_idx, r, g, b) i el firmware passa al següent. Mai s’acumulen 900 bytes a memòria.
  • READ_CRC: 1 byte final. Es compara amb el CRC8 running que s’ha anat calculant des de VERSION. Si no coincideix, ACK CRC_FAIL i fora.
  • DISPATCH: si tot ha anat bé, leds_strip->show() (només per DATA), i ACK OK.

Per què streaming i no buffer sencer. Si bufferejés el payload DATA per validar el CRC abans de pintar, hauria de reservar fins a 1024 bytes — la meitat de la RAM de l’Uno, només per a un frame que potser és invàlid. Així, el “preu” és haver de revertir el frame si el CRC falla: els pixels ja escrits queden pintats fins al pròxim show(). La pràctica diu que és tolerable — els CRC fallen poc i la pèrdua màxima és el contingut d’un sol frame, no un crash. El show() només s’invoca si el CRC és OK, així que en cas d’error el frame no es projecta a la tira encara que els bytes ja estiguin escrits al buffer intern.

Hi ha cinc codis d’error d’ACK ben tipats:

  • 0x00 OK
  • 0x01 CRC_FAIL
  • 0x02 VERSION (frame d’una altra versió de wire)
  • 0x03 UNKNOWN_TYPE (TYPE no reconegut)
  • 0x04 OVERFLOW (LEN > MAX_PAYLOAD)

Per què cinc i no un? Perquè la causa importa: un CRC_FAIL justifica retransmissió immediata; un VERSION justifica abortar i renegociar; un OVERFLOW és un bug del host que cal arreglar, no retransmetre. Distingir-los al firmware permet decidir al host.

El que ve

Tornem al host i mirem per què el protocol té aquesta forma i no JSON. Per què 6 bytes d’overhead, per què big-endian, per què CRC8 i no CRC16 ni res. Era una decisió o una conseqüència inevitable?