FreeRTOS xTaskDelayUntil: task periodici senza deriva lenta del timing
I task periodici sembrano spesso banali: leggere un sensore ogni 10 ms, eseguire un controllo ogni 1 ms, pubblicare telemetria ogni secondo. Il bug è quasi sempre nascosto nella parola “ogni”. Un delay relativo introduce deriva del periodo; un delay assoluto definisce una schedulazione. FreeRTOS V11.3.0 documenta esplicitamente anche il comportamento di catch-up di xTaskDelayUntil(): un buon promemoria che un task a frequenza fissa ha comunque bisogno di una politica di overrun.
Il problema pratico
Su prodotti STM32, ESP32 e MCU simili, molti difetti di timing non sono crash netti. Sono degradazioni lente: la fase di campionamento si allontana dal trigger ADC, un task CAN concentra i messaggi dopo un intervallo occupato, un LED di stato sembra innocuo ma nasconde pressione sullo scheduler, oppure un loop di controllo esegue due iterazioni consecutive dopo essere rimasto bloccato. Sul banco si vedono poco; sul campo costano tempo.
Il pattern iniziale più comune è do_work(); vTaskDelay(period);. Il prossimo wake-up viene quindi schedulato rispetto al momento in cui il task riesce finalmente a chiamare il delay. Se do_work() dura 2 ms e il delay è 10 ms, il periodo reale è circa 12 ms, più jitter da interrupt e task a priorità superiore. Nel tempo non è un loop a 100 Hz stabile: è un loop la cui frequenza dipende dal tempo di esecuzione.
xTaskDelayUntil() risolve un problema diverso. Schedula il prossimo sblocco rispetto a un valore assoluto precedente del tick. Il task può quindi puntare a una cadenza stabile anche quando ogni iterazione varia leggermente. È la scelta corretta per housekeeping periodico, macchine a stati di acquisizione, controlli di progresso watchdog e molte basi tempi di protocollo.
Tempo assoluto, non “dormi dopo il lavoro”
L’API FreeRTOS mantiene lo stato in una variabile TickType_t di proprietà del task. Si inizializza con xTaskGetTickCount() e poi si passa per puntatore a ogni iterazione. FreeRTOS la aggiorna al prossimo istante di rilascio. Il punto architetturale è questo: il periodo si misura da rilascio a rilascio, non dalla fine di un’iterazione alla prossima chiamata di delay.
Usata come previsto, l’API gestisce correttamente anche il wrap del tick. Non conviene sostituirla con confronti unsigned scritti a mano e sparsi nell’applicazione, a meno di avere un motivo preciso e test espliciti sul wrap-around. Questa API esiste perché overflow e off-by-one sono errori ricorrenti.
Il catch-up non è una strategia di overrun
Le note di rilascio di FreeRTOS Kernel V11.3.0 citano aggiornamenti alla documentazione sul comportamento di catch-up di xTaskDelayUntil(). Il significato pratico è semplice: se il task è già in ritardo quando chiama l’API, potrebbe non bloccarsi per un periodo completo. È corretto per una schedulazione assoluta: il task sta recuperando la timeline che aveva promesso di seguire.
Ma recuperare non è sempre sicuro. Per un logger di temperatura può essere accettabile eseguire subito un’iterazione dopo una lunga scrittura flash. Per un supervisore motor-control, elaborare iterazioni vecchie può rendere il sistema meno deterministico. Per un gateway di comunicazione, una raffica di lavoro “in recupero” può affamare task a priorità più bassa e creare il prossimo problema di latenza.
xTaskDelayUntil() per eliminare la deriva, ma decidi separatamente cosa fare quando un’iterazione manca la deadline. Il lavoro in ritardo va misurato, segnalato e a volte saltato.Un pattern per task periodici bounded
Il seguente pattern C è volutamente piccolo. Mantiene la cadenza FreeRTOS stabile, registra gli overrun e impedisce un catch-up illimitato risincronizzando dopo un ritardo severo. Le soglie precise appartengono al prodotto, non all’RTOS.
#include "FreeRTOS.h"
#include "task.h"
#include <stdint.h>
#include <stdbool.h>
#define SENSOR_PERIOD_MS 10u
#define MAX_ACCEPTABLE_LATE_MS 20u
static void publish_timing_fault(uint32_t late_ms);
static bool sensor_sample_and_filter(void);
static void sensor_task(void *arg)
{
const TickType_t period = pdMS_TO_TICKS(SENSOR_PERIOD_MS);
const TickType_t max_late = pdMS_TO_TICKS(MAX_ACCEPTABLE_LATE_MS);
TickType_t next_release = xTaskGetTickCount();
for (;;) {
TickType_t now = xTaskGetTickCount();
TickType_t late = now - next_release; /* valido con aritmetica unsigned */
if (late > max_late) {
publish_timing_fault(late * portTICK_PERIOD_MS);
/* Risincronizza invece di eseguire una raffica di iterazioni vecchie. */
next_release = now;
}
if (sensor_sample_and_filter()) {
/* Qui si può salvare un breadcrumb o alimentare un task watchdog:
dopo lavoro utile, non prima di una chiamata periferica bloccante. */
}
xTaskDelayUntil(&next_release, period);
}
}
Ci sono due dettagli importanti. Primo: la metrica di ritardo viene controllata prima del lavoro. Così si capisce se il task è stato rilasciato tardi o bloccato da qualcosa fuori dal corpo normale. Secondo: la risincronizzazione è esplicita. Senza questa politica, un task ritardato da erase flash, coesistenza radio, sezione con interrupt disabilitati o inversione di priorità può rieseguire subito più volte finché la schedulazione assoluta torna nel futuro.
Esempio pratico: un prodotto di telemetria a batteria
Immagina un nodo industriale a batteria basato su STM32 o ESP32. Campiona corrente e tensione a 100 Hz, calcola contatori energia rolling, invia un pacchetto compatto via CAN o Wi‑Fi e cerca di restare il più possibile in low-power. Il task a 100 Hz non dovrebbe essere scritto come sample(); vTaskDelay(10 ms);, perché ogni conversione ADC, ramo di filtro e aggiornamento storage entra nel periodo.
Userei xTaskDelayUntil() per il task di acquisizione, salverei il peggior ritardo osservato in un blocco diagnostico retained e definirei una regola di prodotto: se il task è in ritardo di meno di due periodi, registra e continua; se è più in ritardo, marca la finestra di campionamento come non valida, risincronizza ed evita di riportare una precisione falsa. Il cliente ottiene un prodotto onesto sui problemi di timing, non un firmware che cambia silenziosamente frequenza di campionamento.
Lo stesso ragionamento vale per prodotti ESP32 con Wi‑Fi, dove radio e flash possono introdurre latenze sorprendenti. L’API RTOS dà un target stabile; l’architettura firmware deve comunque decidere quanto ritardo è tollerabile per il processo fisico misurato.
Checklist pratica
- Usa
xTaskDelayUntil()per task a frequenza fissa; usavTaskDelay()per backoff relativi o attese non critiche. - Inizializza il tick di wake precedente una sola volta prima del loop, non a ogni iterazione.
- Misura separatamente tempo di esecuzione e ritardo: indicano classi di bug diverse.
- Definisci una politica di overrun: continuare, saltare lavoro vecchio, degradare l’output, resettare un sottosistema o alzare una diagnostica.
- Non lasciare che un task periodico ad alta priorità faccia logging illimitato, retry infiniti, allocazioni dinamiche o chiamate driver bloccanti.
- Verifica tick rate e risoluzione: un periodo da 1 ms con tick a 100 Hz non è una schedulazione da 1 ms.
- Testa con ritardi iniettati: scritture flash, interrupt disabilitati, burst radio, contesa mutex e massimo carico ISR.
Come lo affronterei su un progetto cliente
Prima elencherei ogni task che dichiara una frequenza: controllo, campionamento, finestre di comunicazione, supervisione watchdog, refresh UI, flush storage. Per ciascuno distinguerei requisito hard, requisito soft o semplice comodità. Il lavoro periodico hard e soft merita diagnostica esplicita; i task di comodità non meritano priorità alta.
Poi convertirei i loop sensibili alla deriva a xTaskDelayUntil(), aggiungerei contatori di overrun leggeri e rivedrei priorità e punti bloccanti. Su STM32 questo spesso espone chiamate HAL che attendono troppo in un task che dovrebbe solo avviare DMA e tornare. Su ESP32 spesso emergono flash, logging o connettività finiti per errore su un percorso timing-critical. Infine metterei i contatori di ritardo in un report diagnostico leggibile sul campo, così il primo problema cliente produce dati e non ipotesi.
Commenti
Hai un problema concreto di timing firmware o una nota su questo articolo? Invia una breve email includendo il titolo dell’articolo.