Multitasking in Arduino.
Il microcontrollore può eseguire solo un'attività alla volta, poiché ha un core di elaborazione (alcuni ne hanno di più, come ESP32), quindi non esiste un vero "multitasking" e non può esserlo. Ma a causa dell'elevata velocità di esecuzione, il core può eseguire compiti a sua volta e per una persona sembrerà multitasking: dopotutto, cos'è un secondo per noi, per un microcontrollore - decine di milioni di operazioni.
In generale, ci sono due approcci per organizzare un programma complesso: un super ciclo con interruzioni e usando il sistema operativo RTOS. Non analizzeremo i sistemi operativi, ci concentreremo sul primo approccio.
Super ciclo.
Un super ciclo è il ciclo principale di un programma che va dall'alto verso il basso e inizia dall'inizio quando raggiunge la fine. Nell'IDE di Arduino, il nostro super ciclo è loop(). Nel ciclo principale possiamo interrogare sensori, controllare dispositivi esterni, visualizzare dati, eseguire calcoli e così via, ma in ogni caso queste azioni si verificheranno una dopo l'altra, in sequenza. La maggior parte delle azioni non richiede un'esecuzione costante: ad esempio, non è necessario aggiornare il display un milione di volte al secondo, pulsanti o sensori di polling: è sufficiente farlo più volte al secondo utilizzando un timer software (ne parleremo più avanti) . Alcune azioni richiedono un tempo del processore relativamente lungo, come calcoli complessi o invio di un'immagine al display, e durante questo periodo possiamo perdere alcuni eventi importanti (impulso dal sensore di velocità, rotazione dell'encoder, dati in arrivo tramite l'interfaccia di comunicazione).
Oltre al ciclo principale, abbiamo degli interrupt che, quando si verifica un determinato evento, ci consentono di interrompere l'esecuzione del codice nel ciclo principale e procedere all'esecuzione del codice nel gestore degli interrupt e, una volta completato, tornare al ciclo principale e continua a lavorare. Alcuni compiti possono essere risolti solo con gli interrupt, senza scrivere una singola riga nel ciclo loop().
- Interrupt hardware esterni che consentono di interrompere da un segnale esterno (pressione di un pulsante, rotazione di un encoder, un impulso da un contagiri).
- Inoltre, il microcontrollore ha degli interrupt interni che vengono chiamati dalla sua periferia. Ci possono essere diverse dozzine di interruzioni di questo tipo! Uno di questi interrupt è il timer interrupt: in base al periodo configurato, il programma verrà interrotto ed eseguirà il codice specificato.
- Nell'implementazione di Arduino, uno dei timer è configurato per il tempo reale, grazie al quale le funzioni millis() e micros() funzionano per noi (funzioni del tempo). Sono queste funzioni che sono uno strumento pronto per la gestione del tempo del nostro codice e ci consentono di creare il lavoro "in programma" (ne parleremo più avanti).
- Un altro esempio sono gli interrupt UART. Vi siete mai chiesti dove e come il microcontrollore riceve i dati dal monitor della porta? Non lo interroghiamo manualmente. Nell'implementazione di Arduino, i dati inseriti tramite UART provocano un interrupt, in cui vengono aggiunti a un buffer, e da questo buffer li leggiamo nel programma in qualsiasi momento per noi conveniente.
Considera alcune opzioni per implementare il multitasking in Arduino.
Multitasking con yield().
Nell'argomento della funzione tempo, abbiamo toccato la funzione yield(), che consente di eseguire il codice all'interno dei ritardi delay(). Questo ti consente di implementare molto rapidamente l'esecuzione "parallela" di due attività: una in ritardo e la seconda - costantemente. Considera un esempio in cui un LED lampeggia e un pulsante viene interrogato:
void setup()
{
pinMode(13, OUTPUT);
}
void loop()
{
digitalWrite(13, 1);
delay(1000);
digitalWrite(13, 0);
delay(1000);
}
void yield()
{
// e qui puoi eseguire il polling del pulsante e non perdere la pressione a causa del delay().
}
Allo stesso modo, puoi eseguire il polling dell'encoder o di altri moduli che richiedono il polling più frequente. Non meno vitale sarebbe un esempio con uno scenario di movimento di un motore passo-passo o un movimento servo regolare, che richiedono frequenti chiamate a "funzioni di movimento".
Considera un esempio di un motore che si muove lungo diversi punti dati, la funzione di rotazione del motore dovrebbe essere chiamata il più spesso possibile (questo viene fatto in quasi tutte le librerie per motori passo-passo):
void setup()
{
}
void loop()
{
// impostare l'angolo di destinazione №1
delay(1000);
// impostare l'angolo di destinazione №2
delay(120);
// impostare l'angolo di destinazione №3
delay(2000);
// impostare l'angolo di destinazione №4
delay(250);
// impostare l'angolo di destinazione №5
delay(600);
}
void yield()
{
// ruotare il motore
}
Pertanto, abbiamo dipinto in modo rapido e semplice la "traiettoria" del movimento per un motore passo-passo nel tempo, senza utilizzare timer e librerie. Per programmi più complessi, ad esempio, con il movimento di due motori, un trucco del genere potrebbe non funzionare più ed è più facile lavorare con un timer.
Multitasking con millis().
La maggior parte degli esempi per vari moduli/sensori utilizza delay() come una "frenata" del programma, ad esempio, per inviare i dati dal sensore alla porta seriale. Sono questi esempi che rovinano la percezione del principiante e inizia anche a usare i delay(). E non andrai lontano con i delay()!
Queste funzioni restituiscono il tempo trascorso dall'avvio del programma, il cosiddetto uptime. Abbiamo due di queste funzioni:
L'implementazione del classico timer si presenta così:
- millis() - millisecondi, tipo unsigned long, da 1 a 4.294.967.295 ms (~50 giorni), risoluzione 1 ms. Dopo "overflow", il conteggio riparte da zero.
- micros() - microsecondi, tipo unsigned long, da 4 a 4294967295 µs (~70 minuti), risoluzione 4 µs. Dopo "overflow", il conteggio riparte da zero.
- Imposta una variabile per il timer di tipo unsigned long (uint32_t): questo è il tipo restituito da millis().
- Cerca la differenza tra il tempo di esecuzione corrente del programma e la variabile timer.
- Se la differenza è maggiore del periodo richiesto, eseguiamo il codice necessario e resettiamo il timer.
L'implementazione del classico timer si presenta così:
#define MY_PERIOD 500 // periodo
uint32_t timer1; // variabile
void setup()
{
}
void loop()
{
if (millis() - timer1 >= MY_PERIOD) // cerca la differenza
{
timer1 = millis(); // reset timer // eseguire un'azione
}
}
- Questa costruzione "lascia" il periodo se ci sono ritardi e altre sezioni di blocco nel codice, durante i quali millis() ha il tempo di aumentare di un tempo maggiore del periodo del timer. Questo può essere critico, ad esempio, per il conteggio del tempo e altre situazioni simili in cui il periodo di funzionamento del timer non deve essere spostato.
- Allo stesso tempo, se l'esecuzione del codice è bloccata per più di un periodo, l'algoritmo correggerà semplicemente questa differenza, poiché la reimpostiamo sul valore millis() corrente.
Timer multipli.
Vogliamo eseguire un'azione due volte al secondo, la seconda - tre e la terza - 10. Abbiamo bisogno di 3 variabili timer e 3 costrutti con una condizione:
uint32_t myTimer1, myTimer2, myTimer3; // variabili
void setup()
{
}
void loop()
{
if (millis() - myTimer1 >= 500) // timer per 500 ms (2 volte al secondo)
{
myTimer1 = millis(); // reset timer
// eseguire azione 1
}
if (millis() - myTimer2 >= 333) // timer per 333 ms (3 volte al secondo)
{
myTimer2 = millis(); // reset timer
// eseguire azione 2
}
if (millis() - myTimer3 >= 100) // timer per 100 ms (10 volte al secondo)
{
myTimer3 = millis();
// reset timer // eseguire azione 3
}
}
Ed è così che possiamo, ad esempio: interrogare il sensore 10 volte al secondo, filtrare i valori e visualizzare le letture due volte al secondo. E lampeggia la luce tre volte al secondo.
Ed è così che possiamo, ad esempio: interrogare il sensore 10 volte al secondo, filtrare i valori e visualizzare le letture due volte al secondo. E lampeggia la luce tre volte al secondo.
Altre implementazioni.
Considera alcune altre opzioni per implementare e reimpostare il timer:
Overflow.
Tutte le costruzioni di timer discusse sopra sopravvivono in modo sicuro alla transizione attraverso 0 e continuano a funzionare senza modificare e spostare il periodo, perché stiamo usando un tipo di dati senza segno.
Multitasking con timer interrupt (per AVR).
Per attività particolarmente critiche dal punto di vista temporale, è possibile utilizzare l'esecuzione su un interrupt timer. Quali compiti potrebbero essere?
- Indicazione dinamica.
- Generazione di uno specifico protocollo di segnale/comunicazione.
- Software PWM.
- Motori passo-passo.
- Qualsiasi altro esempio di esecuzione oltre il tempo specificato o solo esecuzione periodica secondo un periodo rigoroso.
Impostare il timer sulla frequenza e sulla modalità di funzionamento desiderate è un compito impossibile per un principiante, sebbene possa essere risolto in 2-3 righe di codice, quindi suggerisco di utilizzare le librerie. Per configurare gli interrupt per i timer 1 e 2, sono disponibili le librerie TimerOne e TimerTwo.
I timer interrupt sono uno strumento molto potente, ma non abbiamo molti timer e dovremmo usarli solo quando ne abbiamo davvero bisogno. Il 99% dei problemi può essere risolto senza interruzioni del timer scrivendo un ciclo principale ottimale e utilizzando millis() correttamente.
Libreria per millis().
In un grande progetto con un sacco di compiti, sarà piuttosto spiacevole descrivere la stessa costruzione del timer, in questo caso è molto più redditizio usare le librerie (o avvolgerlo in una classe, come negli esempi sopra). Per esempio:
TimerMs questo è il "Millis Timer" con funzioni e impostazioni aggiuntive.
- Temporizzatore e modalità di esecuzione periodica.
- Collegamento di una funzione di gestione.
- Ripristina/Avvia/Riavvia/Interrompi/Pausa/Riprendi.
- Possibilità di forzare l'overflow del timer.
- Restituisce il tempo rimanente in ms, nonché unità arbitrarie di 8 e 16 bit.
- Diverse funzioni per ottenere lo stato attuale del timer.
- L'algoritmo mantiene un periodo stabile e non teme l'overflow di millis().
Compatibile con tutte le piattaforme Arduino (utilizzando le funzioni Arduino).