Arduino UNO R4 - FreeRTOS Time Slicing

Se si va ad esaminare il file FreeRTOSConfig. della libreria FreeRTOS™ inclusa tra le librerie del "core" Arduino UNO R4 si scopre che i parametri relativi al multi tasking sono così impostati:

#define configUSE_PREEMPTION (1)
e
#define configUSE_TIME_SLICING (0)

... quindi, FreeRTOS™ è configurato per utilizzare il "preemptive RTOS scheduler", ma NON il "prioritised preemptive scheduling with time slicing" e quindi che, per task alla stessa priorità, NON viene effettuato il task switching. :confused:

Il motivo di tale scelta risulta chiaro se si pensa che FreeRTOS™ è visto solo come una libreria aggiuntiva di un insieme che NON è stato disegnato per lavorare in multi task, time sharing. Ci sono difatti alcune librerie che prevedono, nelle loro funzioni, delle tempistiche ben precise e sono state scritte con in mente il concetto del mono task senza tenere conto che in un ambiente multi task, time sharing potrebbero venire interrotte in qualsiasi momento (cosa che invece tiene sempre in considerazione chi scrive driver per ambienti RTOS).

Se, per ragioni di efficienza, si imposta il parametro:

#define configUSE_TIME_SLICING (1)

si deve tenere ben presente che la cosa può creare grossi problemi in funzione delle librerie che si andranno ad utilizzare nel proprio programma.

Un tipico esempio è la libreria Wire che è a corredo del "core" Arduino UNO R4 (quindi specificatamente fatta per il Renesas RA4M1) che, in ben due punti ha un qualche cosa tipo:

while(((millis() - start) < timeout_ms) && bus_status == WIRE_STATUS_UNSET && err == FSP_SUCCESS) {

... è evidente che, se durante il while() avviene un task switching, si potrebbe tornare al while() (dopo il giro tra i vari task ad eguale priorità) dopo un tempo che potrebbe essere considerato come un timeout nella while(). :face_with_diagonal_mouth:

Considerate che ho fatto la prova con un programma che usa vari sensore su bus I2C e altre cose per un totale di 6 task (i vari task sono tutti alla stessa priorità) ed un timer. L'ovvia possibilità di conflitto nell'uso del bus I2C è naturalmente gestita con un "mutex", ma ... NON basta.

Dopo qualche ora di funzionamento capita che avviene il task switch al momento sbagliato e ... il bus I2C cessa di funzionare (non riesce neanche a riprendersi) e quindi, niente dati in arrivo dai vari sensore su I2C.

1 Like

Esaminata la libreria Wire di Arduino UNO R4 ho supposto che il problema potesse proprio risiedere nelle sole due funzioni che applicano un controllo di timeout.

La cosa fatta bene sarebbe stata riscrivere da capo la libreria Wire per la gestione del I2C :roll_eyes:, ma non ne avevo alcuna voglia per cui ho cercato una soluzione più veloce e facilmente applicabile anche ad altre librerie che dovessero presentare lo stesso problema.

Isolate le sole due funzioni con problemi di tempo, ho scritto io due piccole funzioni aggiuntive per sospendere temporaneamente il task switching nella parte con le tempistiche delicate, per poi riattivarlo.

Ho quindi aggiunto, in testa alla libreria Wire di Arduino UNO R4, file Wire.cpp, il seguente spezzone di codice:

/* -------------------------------------------------------------------- gpb01 */

static inline void disableScheduler (void) {
#if defined (INC_ARDUINO_FREERTOS_H) || defined (INC_FREERTOS_H)
  if ( xTaskGetSchedulerState( ) == taskSCHEDULER_RUNNING ) {
    vTaskSuspendAll();
    return;
  } else return;
#else
  return;
#endif
}

static inline void resumeScheduler (void) {
#if defined (INC_ARDUINO_FREERTOS_H) || defined (INC_FREERTOS_H)
  if ( xTaskGetSchedulerState( ) == taskSCHEDULER_SUSPENDED ) {
    if( !xTaskResumeAll() ) {
      taskYIELD ();
    }
    return;
  } else return;
#else
  return;
#endif
}

/* -------------------------------------------------------------------- gpb01 */

che, nel caso NON si usi FreeRTOS™, lascia il tutto inalterato (le due funzioni NON fanno nulla) mentre, se in testa al modulo Wire.cpp si aggiunge la riga:

#include "Arduino_FreeRTOS.h"

... indicando che si vuole usare la libreria in ambiente FreeRTOS™, abilita le dovute verifiche e le dovute azioni per fermare temporaneamente e ripristinare il task switching.

Le due funzioni sono state dichiarate static inline per velocizzarle al massimo (possiamo semplificare dicendo che le funzioni inline non vengono chiamate come le normali funzioni, ma inserite direttamente come codice).

Dopo di che ho modificato le due funzioni:

uint8_t TwoWire::write_to(uint8_t address, uint8_t* data, uint8_t length, unsigned int timeout_ms, bool sendStop)
e
uint8_t TwoWire::read_from(uint8_t address, uint8_t* data, uint8_t length, unsigned int timeout_ms, bool sendStop)

... inserendo le dovute chiamate alle mie funzioni.

Il risultato è la libreria Wire modificata che vi allego: R4_Wire.zip (8.5 KB) che contiene tutte le modifiche descritte.

Ho ricompilato lo stesso programma usato in precedenza e che dava problemi e ...
... sono ormai più di 24 ore che gira senza problemi e senza apprezzabili riduzioni della performance. :grin:

Probabilmente NON è la soluzione più elegante (... si sarebbe dovuto riscrivere tutta la libreria per immaginarla girare in un ambiente time sharing), ma è sicuramente una soluzione veloce, applicabile allo stesso modo anche ad altre librerie che dovessero presentare lo stesso problema se si mette, nel file FreeRTOSConfig.h il parametro:

#define configUSE_TIME_SLICING (1)

... attivando così il time slicing.

Se la provate e riscontrate problemi, o se invece avete una soluzione migliore, fatemelo sapere che vediamo di risolvere o metterla in piedi. :wink:

Guglielmo

Dato che le due piccole funzioni che ho scritto, nell'uso del I2C vengono chiamate praticamente in continuazione, ho cercato di ridurre ancora il numero di cicli macchina necessari eliminando la chiamata ad una funzione di FreeRTOS™ (quella che verifica se lo scheduler e sospeso), sostituendola con un semplice test di una flag, così da risparmiare quel po' di cicli macchina che occorrono per entrare ed uscire da una funzione.

Sempre per ragioni di velocità, ho sprecato, per una flag, un uint32_t dato che, sull'architettura Cortex M4, è il tipo dato che per la sua manipolazione richiede il minor numero di cicli macchina fondamentalmente per tre motivi:

  1. Allineamento all'architettura
  2. Accesso alla memoria ottimizzato
  3. Efficienza nelle operazioni matematiche

Per cui, la parte relativa alle due piccole funzioni da me scritte, diventa:

/* -------------------------------------------------------------------- gpb01 */

static uint32_t schedulerIsSupended = 0;          /* Most efficient data type */

static inline void disableScheduler (void) {
#if defined (INC_ARDUINO_FREERTOS_H) || defined (INC_FREERTOS_H)
  if ( xTaskGetSchedulerState( ) == taskSCHEDULER_RUNNING ) {
    schedulerIsSupended = 0xffffffff;
    vTaskSuspendAll();
    return;
  } else return;
#else
  return;
#endif
}

static inline void resumeScheduler (void) {
#if defined (INC_ARDUINO_FREERTOS_H) || defined (INC_FREERTOS_H)
  if ( schedulerIsSupended != 0 ) {
    schedulerIsSupended = 0;
    if( !xTaskResumeAll() ) {
      taskYIELD ();
    }
    return;
  } else return;
#else
  return;
#endif
}

/* -------------------------------------------------------------------- gpb01 */

Ovviamente il risparmio è di pochi cicli macchina, ma, dato che queste due piccole funzioni sono richiamate in continuazione quando si utilizza la libreria, alla fine comunque il tempo risparmiato non è trascurabile :wink:

Allego la versione modificata della libreria: R4_Wire.zip (8.4 KB) .

Come sempre, se riscontrate problemi, o se avete una soluzione migliore, fatemelo sapere.

Guglielmo

1 Like

Naturalmente, dato che già il "mutex", che ho previsto nel programma di test, esclude l'utilizzo simultaneo del bus I2C da parte di più task (quindi, uno solo di quelli che usano detto bus, è in stato di esecuzione, mentre gli altri sono bloccati sul "mutex"), senza modificare la libreria Wire, è possibile portare i task che usano il bus I2C ad una priorità più alta degli altri task per evitare che vengano interrotti da task a priorità più bassa.

Questo può andare bene o meno a seconda dei casi ...
...in particolare, nel programma di test che sto utilizzando, uno dei task, sfruttando la matrice di LED che è presente su Arduino UNO R4 WiFi, crea un'animazione continua su detti LED e, dato che uno dei task che usa il bus I2C aggiorna lo schermo di un display OLED (cosa che richiede un certo numero di operazioni che impiegano del tempo) ... si vede chiaramente l'animazione arrestarsi un attimo durante l'aggiornamento del OLED e poi ripartire.

Invece, con la modifica proposta alla libreria Wire, NON occorre cambiare le priorità e tutto gira in modo fluido senza interruzioni apprezzabili :wink:

Guglielmo

Ah, un altra cosa, dato che NON mi piacciono i "warning" (... ed il "core" Arduino UNO R4 se ne porta dietro già troppi :confused:) , se in testa alla Wire che ho modificato NON si include Arduino_FreeRTOS.h (perché non si deve lavorare in ambiente FreeRTOS™ e quindi NON si necessita della modifica), si ha una inutile variabile definita (schedulerIsSupended), per cui ... ho apportato una semplice piccola "condizione":

/* -------------------------------------------------------------------- gpb01 */

#if defined (INC_ARDUINO_FREERTOS_H) || defined (INC_FREERTOS_H)
static uint32_t schedulerIsSupended = 0;          /* Most efficient data type */
#endif

static inline void disableScheduler (void) {
#if defined (INC_ARDUINO_FREERTOS_H) || defined (INC_FREERTOS_H)
  if ( xTaskGetSchedulerState( ) == taskSCHEDULER_RUNNING ) {
    schedulerIsSupended = 0xffffffff;
    vTaskSuspendAll();
    return;
  } else return;
#else
  return;
#endif
}

static inline void resumeScheduler (void) {
#if defined (INC_ARDUINO_FREERTOS_H) || defined (INC_FREERTOS_H)
  if ( schedulerIsSupended != 0 ) {
    schedulerIsSupended = 0;
    if( !xTaskResumeAll() ) {
      taskYIELD ();
    }
    return;
  } else return;
#else
  return;
#endif
}

/* -------------------------------------------------------------------- gpb01 */

facendo così in modo che la definizione della variabile 'schedulerIsSupended' venga fatta solo se si è inclusa la libreria Arduino_FreeRTOS :grin:

Per chi vuole provarla, la libreria modificata ed aggiornata si trova anche su GITHUB. :wink:

Guglielmo

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.