Un risultato plausibile, ottenuto da dati che non esistono
Un piccolo dispositivo osserva le vibrazioni di una macchina. Ogni dieci millisecondi raccoglie un blocco di misure e lo consegna a un modello di intelligenza artificiale. Il modello è abbastanza veloce, almeno guardando la media. Eppure alcune risposte sono inspiegabili. Il problema può precedere qualsiasi errore statistico: mentre il modello legge le misure, il circuito di acquisizione sta già sostituendole con quelle successive. Il modello riceve così pezzi di due istanti diversi, senza che nessuna istruzione debba necessariamente generare un errore.
Abstract. Studiamo quando una zona di memoria può essere riutilizzata senza alterare un ingresso ancora in lettura. Costruiamo una cronologia sintetica, deriviamo una condizione sulla durata dei dati, confrontiamo due e tre buffer e poi introduciamo una gestione esplicita della loro proprietà. Il risultato centrale è preciso: aggiungere memoria può assorbire un ritardo, ma solo un protocollo di utilizzo impedisce di sovrascrivere un blocco occupato; se mancano risorse, bisogna rendere esplicita la perdita. Tutti i numeri sono calcolati in una simulazione Python, non misurati su una scheda o su un prodotto EL-AI.
Due attività che procedono insieme
Un buffer è una regione di memoria usata per trattenere temporaneamente dati. Il produttore la riempie; il consumatore la legge. Qui il produttore è un’acquisizione assistita da DMA, cioè accesso diretto alla memoria: un dispositivo trasferisce dati senza chiedere alla CPU di copiare ogni singolo campione. La CPU, il processore che esegue il nostro calcolo, può intanto svolgere l’inferenza, ossia applicare il modello ai dati. La contemporaneità è utile, ma significa che i due attori possono accedere alla stessa memoria.
Immaginiamo due vassoi: mentre uno viene riempito, l’altro è letto. È l’intuizione del doppio buffer, spesso chiamato ping-pong. L’analogia ha un limite importante: in memoria non c’è una mano che segnala automaticamente che il vassoio è ancora occupato. Un indirizzo valido resta valido anche dopo che il contenuto è cambiato. Conservare un puntatore significa ricordare dove leggere, non congelare ciò che vi si trova. La proprietà che ci serve è la stabilità dell’intero blocco durante tutte le letture che ne dipendono.
L’esperimento: otto blocchi e un ritardo isolato
Il nostro sensore ipotetico produce un canale a 16.000 campioni al secondo. Raggruppiamo 160 campioni, ciascuno rappresentato con due byte. Un blocco contiene quindi 320 byte e richiede 160/16.000 secondi, cioè 10 ms, per essere acquisito. Questi valori sono parametri scelti per rendere leggibile il caso, non specifiche di un hardware provato. Numeriamo i blocchi da zero: il blocco 0 viene scritto tra 0 e 10 ms, il blocco 1 tra 10 e 20 ms e così via.
Il consumatore elabora un solo blocco alla volta, nell’ordine di arrivo. I tempi assegnati agli otto blocchi sono 6, 6, 14, 6, 6, 6, 6 e 6 ms: la media è 7 ms. Assumiamo che l’ingresso debba restare disponibile per tutta l’elaborazione, inclusa l’eventuale attesa prima che la CPU inizi. Niente interruzioni aggiuntive, copie, cache o costi di sincronizzazione. Non stiamo stimando quanto velocemente gira una rete: isoliamo il rapporto fra la durata del suo ingresso e il riuso della memoria.
Con due buffer ciclici, il primo ospita i blocchi pari e il secondo quelli dispari. Il blocco 2 finisce di arrivare a 30 ms; la CPU lo legge da 30 a 44 ms. Ma il blocco 4 comincia a riempire lo stesso buffer a 40 ms. Per quattro millisecondi lettura e riscrittura si sovrappongono. Il tempo medio inferiore ai 10 ms non impedisce questo evento: la correttezza riguarda ogni blocco, non il comportamento medio della sequenza.
Quanto tempo abbiamo davvero prima del riuso?
Per calcolare questo intervallo definiamo T come periodo di acquisizione, B come numero di buffer e k come indice del blocco. Il blocco k viene completato a rₖ=(k+1)T. Il suo buffer verrà riscritto quando inizia il blocco k+B, cioè a dₖ=(k+B)T. La lettera d indica qui la scadenza della disponibilità dei dati, non una scadenza clinica o di controllo. La differenza fra questi istanti è il tempo utile dopo l’acquisizione:
Cₖ è il tempo durante il quale elaboriamo il blocco e Wₖ è l’attesa prima di iniziare; entrambi sono in millisecondi, come T. La seconda riga dice che attesa più elaborazione devono finire prima del riuso. Con B=2 rimangono soltanto 10 ms: il buffer in acquisizione non è uno spazio gratuito per aspettare. Per il blocco 2 otteniamo 0+14≤10, una disuguaglianza falsa. Con B=3 diventerebbe 14≤20: quel ritardo rientra nella finestra disponibile.
Per includere l’effetto sui blocchi successivi calcoliamo l’inizio sₖ come il maggiore fra l’arrivo del blocco e la fine fₖ₋₁ del precedente. Poi sommiamo Cₖ. Questo è un processore seriale senza preemption, cioè senza interrompere un blocco per eseguirne un altro:
Il margine mₖ è positivo se i dati non servono più prima del riuso, negativo se le durate si sovrappongono. Il blocco 3 arriva a 40 ms, ma aspetta fino a 44: finisce a 50, esattamente quando lo stesso buffer deve accogliere il blocco 5. Il suo margine è zero. La simulazione tratta questa uguaglianza con un ordine ideale degli eventi; un sistema reale non dovrebbe trasformarla in una garanzia, perché mancano margine per jitter, sincronizzazione e ultimo accesso effettivo.
| Blocco | Arrivo ms | Inizio ms | Fine ms | Riuso con B=2 ms | Margine ms |
|---|---|---|---|---|---|
| 0 | 10 | 10 | 16 | 20 | 4 |
| 1 | 20 | 20 | 26 | 30 | 4 |
| 2 | 30 | 30 | 44 | 40 | -4 |
| 3 | 40 | 44 | 50 | 50 | 0 |
| 4 | 50 | 50 | 56 | 60 | 4 |

Leggere la figura da sinistra a destra: grigio significa acquisizione, blu lettura entro la finestra e rosso lettura che oltrepassa il riuso. Nei due pannelli i tempi della CPU sono identici. Cambia soltanto quando il produttore torna sulla stessa memoria. La tabella rende visibile anche l’attesa del blocco 3: limitarsi a confrontare ogni Cₖ con T avrebbe nascosto l’effetto del lavoro rimasto in coda.
Che cosa può leggere davvero il modello
Una sovrapposizione non dimostra automaticamente quanto cambierà la predizione: dipende dall’ordine delle letture del modello. Possiamo però costruire un controesempio minimo alla coerenza dei dati. Consideriamo quattro posizioni rappresentative del blocco, agli indici 0, 40, 80 e 120. Il nuovo blocco 4 comincia a sovrascriverle rispettivamente a 40, 42,5, 45 e 47,5 ms. A 44 ms, una fotografia ideale di quelle posizioni contiene le etichette di generazione [4, 4, 2, 2]. Le etichette indicano da quale blocco proviene il valore; non sono ampiezze misurate dal sensore.
Il dispositivo potrebbe quindi costruire caratteristiche da un segnale mai acquisito come tale. Non abbiamo eseguito una rete neurale e non attribuiamo a questo esempio un tasso di errore diagnostico. Abbiamo dimostrato qualcosa di più ristretto ma fondamentale: il contratto “questa inferenza usa il blocco 2” non è rispettato. Aumentare l’accuratezza del modello su un dataset pulito non corregge quel contratto.
Dare a ogni buffer un proprietario
Cambiamo ora la regola: il produttore non sceglie più automaticamente il prossimo indirizzo ciclico, ma può usare soltanto un buffer libero. Distinguiamo quattro stati. FREE significa disponibile; FILLING riservato all’acquisizione; READY completo e in attesa; READING riservato al consumatore. Il percorso normale è FREE → FILLING → READY → READING → FREE. “Pronto” non significa “libero”: i dati in attesa appartengono già a un lavoro che deve ancora leggerli.
La seconda simulazione implementa questi stati con una coda di eventi. A 40 ms, con due buffer, il blocco 2 è ancora in lettura e il blocco 3 è appena diventato pronto. Nessun buffer è libero per il blocco 4. La politica scelta scarta interamente il nuovo blocco, registrandone l’indice. L’esecuzione termina con i blocchi 0, 1, 2, 3, 5, 6 e 7 elaborati: nessuna sovrascrittura ammessa, ma una perdita esplicita. Con tre buffer, sulla stessa traccia finita, non si perde nessun blocco.
Questo risultato separa due requisiti spesso confusi: integrità dei dati consegnati e completezza dell’acquisizione. Possiamo soddisfare il primo perdendo il secondo. Un contatore progressivo dei blocchi aiuta il destinatario a vedere il salto da 3 a 5; non ricostruisce la misura mancante. In un’analisi temporale quel vuoto deve mantenere il proprio timestamp: concatenare silenziosamente i blocchi restanti inventerebbe una continuità che il sensore non ha fornito.
Perché tre buffer non risolvono ogni ritardo
Una riserva assorbe una perturbazione transitoria, non uno squilibrio permanente. Ripetiamo il calcolo ciclico con ogni elaborazione lunga 12 ms e T sempre pari a 10 ms. Dopo il primo blocco si accumulano 2 ms di attesa per ogni arrivo: Wₖ=2k ms. Con tre buffer la finestra è 20 ms, quindi il margine diventa 20−(2k+12)=8−2k ms. Il blocco 4 raggiunge margine zero; il blocco 5, sesto della sequenza, oltrepassa il riuso di 2 ms. Il programma verifica esattamente questo primo superamento.
Nessun numero finito di buffer conserva indefinitamente tutti i dati di questo flusso se un solo consumatore resta più lento del produttore. La proprietà esplicita protegge i blocchi occupati, ma obbliga prima o poi a scartare, rallentare la sorgente se possibile oppure aumentare la capacità di elaborazione. Qui “scartare un blocco” è una decisione astratta: su una periferica reale occorre verificare come fermare, deviare o ignorare l’acquisizione senza che il DMA continui comunque a scrivere nel buffer occupato.
Copiare, trattenere o ridurre il lavoro
Copiare subito l’ingresso in una memoria privata può accorciare il tempo durante il quale il buffer del sensore deve restare intatto. In quel caso Cₖ, nella condizione di riuso, va sostituito con la durata fino all’ultima lettura del buffer originale, non automaticamente con tutta l’inferenza. Ma la copia deve finire prima della riscrittura, la destinazione deve restare occupata finché serve e il trasferimento costa tempo e banda di memoria. Non abbiamo misurato questi costi e non presumiamo che la copia sia sempre la soluzione migliore.
Passare da due a tre buffer nel nostro caso porta la sola memoria dei blocchi da 640 a 960 byte: 320 byte in più. Non è il picco RAM dell’applicazione. Restano pesi, attivazioni del modello, stack, code, allineamento e altre strutture. Anche “zero-copy”, cioè leggere direttamente la memoria acquisita, non significa “zero attesa”: elimina una copia, ma può prolungare l’occupazione del buffer. La scelta corretta dipende da quale risorsa limita il sistema e da quali perdite sono accettabili nel compito.
Dalla simulazione al firmware: ciò che resta da verificare
La documentazione primaria di Zephyr sui ring buffer illustra la separazione fra accesso alla memoria, trasferimento e conferma dei dati prodotti o consumati. La sezione sulla concorrenza precisa che non esiste un controllo interno generale degli accessi concorrenti; distingue un produttore e un consumatore da casi multipli e discute la visibilità delle scritture su più CPU. È un riferimento per capire il contratto, non una prova che il nostro simulatore implementi quel kernel. Abbiamo letto le sezioni Concepts, Instantiation and Usage, Concurrency e Internal Operation; le API dipendono dalla versione del firmware scelta.
Nel programma didattico gli eventi allo stesso istante sono ordinati così: prima fine CPU, poi fine acquisizione, infine inizio della nuova acquisizione. Gli stati vengono cambiati in un unico processo Python. Questo rende il caso deterministico, ma non dimostra atomicità, coerenza delle cache o ordine delle operazioni su un microcontrollore. Per una prova fisica servirebbero scheda e periferica identificate, versione del runtime, configurazione DMA, collocazione dei buffer, strategia di sincronizzazione e una traccia temporale degli accessi. Occorrerebbe inoltre provare il comportamento in saturazione, non soltanto un funzionamento regolare.
Risposta e riproducibilità
Il sensore può davvero riscrivere dati che il modello sta ancora leggendo, anche quando l’elaborazione media sembra veloce. Il punto da controllare è per quanto tempo ciascun blocco resta necessario e chi può riutilizzarne la memoria. Nel nostro esempio il terzo buffer assorbe un ritardo isolato; la proprietà esplicita evita la corruzione, ma con due buffer richiede uno scarto. Nessuna delle due osservazioni autorizza una garanzia generale di tempo reale. La conseguenza pratica è progettare insieme modello, acquisizione e politica di perdita: una risposta AI ha significato soltanto se sappiamo quali dati l’hanno prodotta.
Il listato breve seguente riproduce la cronologia ciclica: max conserva l’eventuale attesa, mentre reuse−end misura il margine. L’archivio contiene anche la simulazione a eventi con proprietà, la traccia completa, il controesempio delle generazioni miste e il caso di sovraccarico persistente. Gli assert verificano il blocco sovrapposto, quello scartato e il primo superamento con 12 ms costanti. Non c’è casualità, quindi non serve un seed. La scansione della cronologia è lineare nel numero di blocchi; la coda di eventi usa un heap e costa O(n log n + nB), con memoria O(nB+n+B): n è il numero di blocchi, B quello dei buffer. Cerchiamo i buffer liberi e registriamo tutti i B stati a ogni evento. Per B fisso questi costi diventano O(n log n) e O(n).
Fonte primaria e codice
Zephyr Project Documentation — Ring Buffers.
T = 10 # milliseconds
costs = [6, 6, 14, 6, 6, 6, 6, 6]
for B in (2, 3):
end = 0
for k, C in enumerate(costs):
release = (k + 1) * T
start = max(release, end)
end = start + C
reuse = (k + B) * T
print(B, k, start, end, reuse - end)
Codice, dati e istruzioni · JSON. Calcoli didattici eseguiti con Python 3.14.0; figure con Matplotlib 3.11.2. Analisi con assistenza AI, senza dichiarare peer review o revisione umana. Copertina originale ImageGen, illustrativa: non documenta persone, sedi o installazioni EL-AI. Fonti consultate il 3 ottobre 2026.

