Come posso creare più thread in esecuzione?


60

C'è un modo in cui posso far funzionare più parti del programma senza fare più cose nello stesso blocco di codice?

Un thread in attesa di un dispositivo esterno mentre lampeggia anche un LED in un altro thread.


3
Probabilmente dovresti prima chiederti se hai davvero bisogno di discussioni. I timer potrebbero essere già OK per le tue esigenze e sono supportati nativamente su Arduino.
jfpoilpret,

1
Potresti voler dare un'occhiata anche a Uzebox. È una console per videogiochi homebrew a due chip. Quindi, sebbene non sia esattamente un Arduino, l'intero sistema è basato sugli interrupt. Quindi audio, video, controlli, ecc. Sono tutti guidati dall'interruzione mentre il programma principale non deve preoccuparsene. Potrebbe essere un buon riferimento.
cbmeeks,

Risposte:


50

Non esiste supporto multi-processo, né multi-threading su Arduino. Puoi fare qualcosa vicino a più thread con alcuni software però.

Vuoi dare un'occhiata a Protothreads :

I prototipi sono thread stackless estremamente leggeri progettati per sistemi con gravi limiti di memoria, come piccoli sistemi integrati o nodi di rete di sensori wireless. I prototipi forniscono l'esecuzione di codice lineare per i sistemi guidati da eventi implementati in C. I prototipi possono essere utilizzati con o senza un sistema operativo sottostante per fornire gestori di eventi bloccanti. I prototipi offrono un flusso sequenziale di controllo senza macchine a stati complessi o multi-threading completo.

Naturalmente, c'è un esempio di Arduino qui con un codice di esempio . Anche questa domanda SO potrebbe essere utile.

Anche ArduinoThread è buono.


Si noti che Arduino DUE ha un'eccezione a questo, con più loop di controllo: arduino.cc/en/Tutorial/MultipleBlinks
tuskiomi

18

Gli Arduino basati su AVR non supportano il threading (hardware), non ho familiarità con gli Arduino basati su ARM. Un modo per aggirare questa limitazione è l'uso di interruzioni, in particolare interruzioni a tempo. È possibile programmare un timer per interrompere la routine principale ogni tanti microsecondi, per eseguire un'altra routine specifica.

http://arduino.cc/en/Reference/Interrupts


15

È possibile eseguire il multi-threading lato software su Uno. Il threading a livello hardware non è supportato.

Per ottenere il multithreading, sarà necessario implementare un programmatore di base e mantenere un processo o un elenco di attività per tenere traccia delle diverse attività che devono essere eseguite.

La struttura di un pianificatore non preventivo molto semplice sarebbe come:

//Pseudocode
void loop()
{

for(i=o; i<n; i++) 
run(tasklist[i] for timelimit):

}

Qui, tasklistpuò essere una matrice di puntatori a funzione.

tasklist [] = {function1, function2, function3, ...}

Con ogni funzione del modulo:

int function1(long time_available)
{
   top:
   //Do short task
   if (run_time<time_available)
   goto top;
}

Ogni funzione può eseguire un'attività separata come l' function1esecuzione di manipolazioni dei LED e l' function2esecuzione di calcoli float. Sarà responsabilità di ciascun compito (funzione) aderire al tempo assegnato ad esso.

Spero che questo sia sufficiente per iniziare.


2
Non sono sicuro che parlerei di "thread" quando si utilizza uno scheduler non preventivo. A proposito, un tale programmatore esiste già come libreria arduino: arduino.cc/en/Reference/Scheduler
jfpoilpret

5
@jfpoilpret - Il multithreading cooperativo è una cosa reale.
Connor Wolf,

Sì hai ragione! Errore mio; era stato tanto tempo fa che non avevo affrontato il multithreading cooperativo che nella mia mente il multithreading doveva essere preventivo.
jfpoilpret,

9

Secondo la descrizione delle tue esigenze:

  • un thread in attesa di un dispositivo esterno
  • un filo lampeggia un LED

Sembra che potresti usare un interrupt Arduino per il primo "thread" (preferirei chiamarlo "compito" in effetti).

Gli interrupt di Arduino possono chiamare una funzione (il tuo codice) in base a un evento esterno (livello di tensione o variazione di livello su un pin di ingresso digitale), che attiverà immediatamente la tua funzione.

Tuttavia, un punto importante da tenere presente con gli interrupt è che la funzione chiamata dovrebbe essere il più veloce possibile (in genere, non dovrebbero esserci delay()chiamate o altre API da cui dipenderebbero delay()).

Se hai una lunga attività da attivare al trigger di un evento esterno, allora potresti potenzialmente utilizzare uno scheduler cooperativo e aggiungere una nuova attività dalla tua funzione di interruzione.

Un secondo punto importante sugli interrupt è che il loro numero è limitato (ad es. Solo 2 su UNO). Quindi, se inizi ad avere più eventi esterni, dovresti implementare un qualche tipo di multiplexing di tutti gli input in uno e la tua funzione di interruzione determinerà quale inutile multiplex era il trigger effettivo.


6

Una soluzione semplice è utilizzare un Scheduler . Esistono diverse implementazioni. Questo ne descrive brevemente uno disponibile per schede basate su AVR e SAM. Fondamentalmente una singola chiamata avvierà un'attività; "schizzo all'interno di uno schizzo".

#include <Scheduler.h>
....
void setup()
{
  ...
  Scheduler.start(taskSetup, taskLoop);
}

Scheduler.start () aggiungerà una nuova attività che eseguirà taskSetup una volta, quindi chiamerà ripetutamente taskLoop proprio mentre lo schizzo di Arduino funziona. L'attività ha il proprio stack. La dimensione dello stack è un parametro facoltativo. La dimensione predefinita dello stack è di 128 byte.

Per consentire il cambio di contesto, le attività devono chiamare yield () o delay () . Esiste anche una macro di supporto per l'attesa di una condizione.

await(Serial.available());

La macro è zucchero sintattico per quanto segue:

while (!(Serial.available())) yield();

In attesa può anche essere utilizzato per sincronizzare le attività. Di seguito è riportato un frammento di esempio:

volatile int taskEvent = 0;
#define signal(evt) do { await(taskEvent == 0); taskEvent = evt; } while (0)
...
void taskLoop()
{
  await(taskEvent);
  switch (taskEvent) {
  case 1: 
  ...
  }
  taskEvent = 0;
}
...
void loop()
{
  ...
  signal(1);
}

Per ulteriori dettagli vedere gli esempi . Ci sono esempi da più lampeggi LED per pulsante di rimbalzo e una semplice shell con lettura della riga di comando non bloccante. Modelli e spazi dei nomi possono essere utilizzati per aiutare a strutturare e ridurre il codice sorgente. Lo schizzo seguente mostra come utilizzare le funzioni del modello per il multi-lampeggiamento. È sufficiente con 64 byte per lo stack.

#include <Scheduler.h>

template<int pin> void setupBlink()
{
  pinMode(pin, OUTPUT);
}

template<int pin, unsigned int ms> void loopBlink()
{
  digitalWrite(pin, HIGH);
  delay(ms);
  digitalWrite(pin, LOW);
  delay(ms);
}

void setup()
{
  Scheduler.start(setupBlink<11>, loopBlink<11,500>, 64);
  Scheduler.start(setupBlink<12>, loopBlink<12,250>, 64);
  Scheduler.start(setupBlink<13>, loopBlink<13,1000>, 64);
}

void loop()
{
  yield();
}

C'è anche un benchmark per dare un'idea delle prestazioni, ovvero il tempo di iniziare l'attività, il cambio di contesto, ecc.

Infine, ci sono alcune classi di supporto per la sincronizzazione e la comunicazione a livello di attività; Coda e semaforo .


3

Da un precedente incantesimo di questo forum, la seguente domanda / risposta è stata spostata in Ingegneria Elettrica. Ha un codice arduino di esempio per far lampeggiare un LED utilizzando un interrupt del timer mentre si utilizza il loop principale per eseguire l'IO seriale.

https://electronics.stackexchange.com/questions/67089/how-can-i-control-things-without-using-delay/67091#67091

Repost:

Gli interrupt sono un modo comune per fare le cose mentre sta succedendo qualcos'altro. Nell'esempio seguente, il LED lampeggia senza utilizzare delay(). Ogni volta che si Timer1attiva, isrBlinker()viene chiamata la routine di servizio di interruzione (ISR) . Accende / spegne il LED.

Per mostrare che possono accadere simultaneamente altre cose, loop()scrive ripetutamente foo / bar sulla porta seriale indipendentemente dal lampeggio del LED.

#include "TimerOne.h"

int led = 13;

void isrBlinker()
{
  static bool on = false;
  digitalWrite( led, on ? HIGH : LOW );
  on = !on;
}

void setup() {                
  Serial.begin(9600);
  Serial.flush();
  Serial.println("Serial initialized");

  pinMode(led, OUTPUT);

  // initialize the ISR blinker
  Timer1.initialize(1000000);
  Timer1.attachInterrupt( isrBlinker );
}

void loop() {
  Serial.println("foo");
  delay(1000);
  Serial.println("bar");
  delay(1000);
}

Questa è una demo molto semplice. Gli ISR ​​possono essere molto più complessi e possono essere attivati ​​da timer ed eventi esterni (pin). Molte delle librerie comuni sono implementate utilizzando gli ISR.


3

Sono arrivato anche a questo argomento durante l'implementazione di un display a LED a matrice.

In una parola, è possibile creare uno scheduler di polling utilizzando la funzione millis () e l'interruzione del timer in Arduino.

Suggerisco i seguenti articoli di Bill Earl:

https://learn.adafruit.com/multi-tasking-the-arduino-part-1/overview

https://learn.adafruit.com/multi-tasking-the-arduino-part-2/overview

https://learn.adafruit.com/multi-tasking-the-arduino-part-3/overview


2

Puoi anche provare la mia libreria ThreadHandler

https://bitbucket.org/adamb3_14/threadhandler/src/master/

Utilizza uno scheduler di interruzione per consentire il cambio di contesto senza inoltro su yield () o delay ().

Ho creato la libreria perché avevo bisogno di tre thread e ne avevo bisogno di due per essere eseguiti in un momento preciso, indipendentemente da cosa facessero gli altri. Il primo thread ha gestito la comunicazione seriale. Il secondo stava eseguendo un filtro Kalman usando la moltiplicazione della matrice float con la libreria Eigen. E il terzo era un thread loop di controllo corrente veloce che doveva essere in grado di interrompere i calcoli della matrice.

Come funziona

Ogni thread ciclico ha una priorità e un punto. Se un thread, con priorità più alta rispetto al thread in esecuzione corrente, raggiunge il tempo di esecuzione successivo, lo scheduler mette in pausa il thread corrente e passa a quello con priorità più alta. Una volta completata l'esecuzione del thread ad alta priorità, lo scheduler ritorna al thread precedente.

Regole di pianificazione

Lo schema di pianificazione della libreria ThreadHandler è il seguente:

  1. Prima la massima priorità.
  2. Se la priorità è la stessa, viene eseguito per primo il thread con la prima scadenza.
  3. Se due thread hanno la stessa scadenza, il primo thread creato verrà eseguito per primo.
  4. Un thread può essere interrotto solo da thread con priorità più alta.
  5. Una volta che un thread è in esecuzione, bloccherà l'esecuzione per tutti i thread con priorità inferiore fino al ritorno della funzione di esecuzione.
  6. La funzione loop ha priorità -128 rispetto ai thread ThreadHandler.

Come usare

I thread possono essere creati tramite ereditarietà c ++

class MyThread : public Thread
{
public:
    MyThread() : Thread(priority, period, offset){}

    virtual ~MyThread(){}

    virtual void run()
    {
        //code to run
    }
};

MyThread* threadObj = new MyThread();

O tramite createThread e una funzione lambda

Thread* myThread = createThread(priority, period, offset,
    []()
    {
        //code to run
    });

Gli oggetti thread si connettono automaticamente a ThreadHandler quando vengono creati.

Per avviare l'esecuzione degli oggetti thread creati, chiamare:

ThreadHandler::getInstance()->enableThreadExecution();

1

Ed ecco un'altra libreria multitasking cooperativa a microprocessore: PQRST: una coda di priorità per l'esecuzione di attività semplici.

In questo modello, un thread viene implementato come sottoclasse di a Task, che è programmato per un certo periodo di tempo futuro (e possibilmente riprogrammato a intervalli regolari, se, come è comune, LoopTaskinvece subclasse ). Il run()metodo dell'oggetto viene chiamato quando l'attività diventa scaduta. Il run()metodo fa il dovuto e poi ritorna (questo è il bit cooperativo); manterrà in genere una sorta di macchina a stati per gestire le sue azioni su invocazioni successive (un esempio banale è la light_on_p_variabile nell'esempio seguente). Richiede un leggero ripensamento su come organizzare il codice, ma si è dimostrato molto flessibile e robusto in un uso abbastanza intenso.

È agnostico riguardo alle unità di tempo, quindi è felice correre in unità di millis()come micros(), o qualsiasi altro segno di spunta che è conveniente.

Ecco il programma "lampeggiante" implementato usando questa libreria. Questo mostra solo una singola attività in esecuzione: altre attività sarebbero in genere create e avviate all'interno setup().

#include "pqrst.h"

class BlinkTask : public LoopTask {
private:
    int my_pin_;
    bool light_on_p_;
public:
    BlinkTask(int pin, ms_t cadence);
    void run(ms_t) override;
};

BlinkTask::BlinkTask(int pin, ms_t cadence)
    : LoopTask(cadence),
      my_pin_(pin),
      light_on_p_(false)
{
    // empty
}
void BlinkTask::run(ms_t t)
{
    // toggle the LED state every time we are called
    light_on_p_ = !light_on_p_;
    digitalWrite(my_pin_, light_on_p_);
}

// flash the built-in LED at a 500ms cadence
BlinkTask flasher(LED_BUILTIN, 500);

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
    flasher.start(2000);  // start after 2000ms (=2s)
}

void loop()
{
    Queue.run_ready(millis());
}

Queste sono attività "run-to-completamento", giusto?
Edgar Bonet,

@EdgarBonet Non sono sicuro di cosa tu voglia dire. Dopo che il run()metodo è stato chiamato, non viene interrotto, quindi ha la responsabilità di terminare ragionevolmente tempestivamente. In genere, tuttavia, farà il suo lavoro, quindi riprogrammare se stesso (possibilmente automaticamente, nel caso di una sottoclasse di LoopTask) per qualche tempo futuro. Un modello comune è che l'attività mantenga una macchina a stati interna (un esempio banale è lo light_on_p_stato sopra) in modo che si comporti adeguatamente alla prossima scadenza.
Norman Gray,

Quindi sì, queste sono attività da eseguire a completamento (RtC): nessuna attività può essere eseguita prima che quella corrente completi la sua esecuzione tornando da run(). Ciò è in contrasto con i thread cooperativi, che possono produrre la CPU, ad esempio, chiamando yield()o delay(). O discussioni preventive, che possono essere programmate in qualsiasi momento. Ritengo che la distinzione sia importante, poiché ho visto che molte persone che vengono qui in cerca di thread lo fanno perché preferiscono scrivere codice di blocco piuttosto che macchine a stati. Il blocco di thread reali che producono la CPU va bene. Il blocco delle attività RtC non lo è.
Edgar Bonet,

@EdgarBonet È una distinzione utile, sì. Considererei sia questo stile, sia i thread di stile, semplicemente come stili diversi di thread cooperativo, al contrario di thread preventivi, ma è vero che richiedono un approccio diverso per codificarli. Sarebbe interessante vedere un confronto approfondito e ponderato dei vari approcci qui menzionati; una bella biblioteca non menzionata sopra è il protothreads . Trovo cose da criticare in entrambi, ma anche da elogiare. Preferisco (ovviamente) il mio approccio, perché sembra più esplicito e non ha bisogno di pile extra.
Norman Gray,

(correzione: protothreads è stato menzionato, nella risposta di @ sachleen )
Norman Gray il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.