Comment créer plusieurs threads en cours d'exécution?


60

Est-il possible de faire fonctionner plusieurs parties du programme sans faire plusieurs choses dans le même bloc de code?

Un thread attend un périphérique externe tout en faisant clignoter une LED dans un autre thread.


3
Vous devriez probablement d'abord vous demander si vous avez vraiment besoin de threads. Les minuteurs peuvent déjà convenir à vos besoins et ils sont supportés nativement sur Arduino.
jfpoilpret

1
Vous voudrez peut-être aussi consulter la Uzebox. C'est une console de jeu vidéo homebrew à deux puces. Ainsi, bien que ce ne soit pas exactement un Arduino, le système entier est construit sur des interruptions. Ainsi, l'audio, la vidéo, les commandes, etc. sont tous alimentés par des interruptions, tandis que le programme principal n'a pas à s'en préoccuper. Peut être une bonne référence.
cbmeeks

Réponses:


50

Il n’existe pas de support multi-processus, ni multi-threading sur l’Arduino. Vous pouvez cependant faire quelque chose de proche de plusieurs threads avec certains logiciels.

Vous voulez regarder Protothreads :

Les protothreads sont des threads sans pile extrêmement légers conçus pour les systèmes à contraintes de mémoire sévères, tels que les petits systèmes intégrés ou les nœuds de réseau de capteurs sans fil. Protothreads fournit une exécution linéaire du code pour les systèmes événementiels implémentés en C. Protothreads peut être utilisé avec ou sans système d'exploitation sous-jacent pour fournir des gestionnaires d'événements bloquants. Les protothreads fournissent un flux de contrôle séquentiel sans machines à états complexes ni multi-threading.

Bien sûr, il est un exemple Arduino ici avec un exemple de code . Cette question SO pourrait également être utile.

ArduinoThread est un bon aussi.


Notez que le Arduino DUE a une exception à cela, avec plusieurs boucles de contrôle: arduino.cc/en/Tutorial/MultipleBlinks
tuskiomi

18

Les Arduino basés sur AVR ne supportent pas le threading (matériel), je ne suis pas familier avec les Arduino basés sur ARM. L’utilisation des interruptions, en particulier des interruptions chronométrées, est un moyen de contourner cette limitation. Vous pouvez programmer un minuteur pour interrompre la routine principale toutes les microsecondes, pour exécuter une autre routine spécifique.

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


15

Il est possible de faire du multi-thread côté logiciel sur Uno. Le threading de niveau matériel n'est pas pris en charge.

Pour réaliser le multithreading, il faudra implémenter un planificateur de base et gérer un processus ou une liste de tâches afin de suivre les différentes tâches à exécuter.

La structure d'un ordonnanceur non-préemptif très simple serait la suivante:

//Pseudocode
void loop()
{

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

}

Ici, tasklistpeut être un tableau de pointeurs de fonction.

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

Avec chaque fonction du formulaire:

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

Chaque fonction peut effectuer une tâche distincte, par exemple function1effectuer des manipulations de DEL et function2effectuer des calculs flottants. Il sera de la responsabilité de chaque tâche (fonction) de respecter le temps qui lui est imparti.

Espérons que cela devrait suffire à vous aider à démarrer.


2
Je ne suis pas sûr que je parlerais de "threads" en utilisant un planificateur non préemptif. Soit dit en passant, un tel ordonnanceur existe déjà sous forme de bibliothèque arduino
jfpoilpret

5
@jfpoilpret - Le multithreading coopératif est une réalité.
Connor Wolf

Oui tu as raison! Mon erreur; Cela faisait si longtemps que je n'avais pas été confronté au multithreading coopératif et que, dans mon esprit, le multithreading devait être préventif.
Jfpoilpret

9

Selon la description de vos besoins:

  • un thread en attente d'un périphérique externe
  • un fil clignote une LED

Il semble que vous pourriez utiliser une interruption Arduino pour le premier "fil" (je préférerais l'appeler "tâche" en fait).

Les interruptions Arduino peuvent appeler une fonction (votre code) en fonction d'un événement externe (niveau de tension ou changement de niveau sur une broche d'entrée numérique), ce qui déclenchera immédiatement votre fonction.

Cependant, un point important à garder à l'esprit avec les interruptions est que la fonction appelée doit être aussi rapide que possible (en règle générale, il ne devrait exister aucun delay()appel ni aucune autre API susceptible de dépendre de cette dernière delay()).

Si vous avez une longue tâche à activer lors du déclenchement d'un événement externe, vous pouvez éventuellement utiliser un planificateur coopératif et lui ajouter une nouvelle tâche à partir de votre fonction d'interruption.

Un deuxième point important concernant les interruptions est que leur nombre est limité (par exemple, seulement 2 sur l'ONU). Donc, si vous commencez à avoir plus d'événements externes, vous devrez implémenter une sorte de multiplexage de toutes les entrées en une seule et laisser votre fonction d'interruption déterminer quel invas multiplexé était le déclencheur réel.


6

Une solution simple consiste à utiliser un planificateur . Il y a plusieurs implémentations. Ceci décrit brièvement celui qui est disponible pour les cartes basées sur AVR et SAM. Fondamentalement, un seul appel lancera une tâche. "croquis dans un croquis".

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

Scheduler.start () ajoutera une nouvelle tâche qui exécutera la tâche taskSetup une fois, puis appellera plusieurs fois taskLoop au moment même où l'esquisse Arduino fonctionne. La tâche a sa propre pile. La taille de la pile est un paramètre facultatif. La taille de pile par défaut est de 128 octets.

Pour permettre le changement de contexte, les tâches doivent appeler rendement () ou délai () . Il existe également une macro de support pour l'attente d'une condition.

await(Serial.available());

La macro est un sucre syntaxique pour:

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

Await peut également être utilisé pour synchroniser des tâches. Ci-dessous un exemple d'extrait de code:

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);
}

Pour plus de détails, voir les exemples . Il y a des exemples de plusieurs voyants clignotant sur le bouton anti-rebond et d'un shell simple avec une lecture de ligne de commande non bloquante. Les modèles et les espaces de noms peuvent être utilisés pour aider à structurer et réduire le code source. L' esquisse ci-dessous montre comment utiliser les fonctions de modèle pour le clignotement multiple. C'est suffisant avec 64 octets pour la pile.

#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();
}

Il existe également un repère pour donner une idée de la performance, par exemple le temps nécessaire pour démarrer la tâche, le changement de contexte, etc.

Enfin, il existe quelques classes de support pour la synchronisation et la communication au niveau des tâches; File d'attente et sémaphore .


3

D'une incantation précédente de ce forum, la question / réponse suivante a été déplacée vers génie électrique. Il contient un exemple de code arduino permettant de faire clignoter une LED à l'aide d'une interruption de minuterie lors de l'utilisation de la boucle principale pour effectuer des entrées / sorties en série.

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

Republier:

Les interruptions sont un moyen courant de faire avancer les choses pendant que quelque chose d'autre se passe. Dans l'exemple ci-dessous, le voyant clignote sans être utilisé delay(). À chaque Timer1déclenchement, la routine de service d'interruption (ISR) isrBlinker()est appelée. Il allume / éteint la LED.

Pour indiquer que d’autres événements peuvent se produire simultanément, loop()écrit foo / bar à plusieurs reprises sur le port série, indépendamment du clignotement de la 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);
}

Ceci est une démo très simple. Les ISR peuvent être beaucoup plus complexes et peuvent être déclenchés par des minuteries et des événements externes (broches). La plupart des bibliothèques communes sont implémentées à l’aide d’ISR.


3

Je suis également venu à ce sujet lors de la mise en œuvre d'un écran LED matriciel.

En un mot, vous pouvez créer un planificateur d’interrogation en utilisant la fonction millis () et une interruption de minuterie dans Arduino.

Je suggère les articles suivants de 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

Vous pouvez également essayer ma bibliothèque ThreadHandler

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

Il utilise un planificateur d'interruption pour permettre la commutation de contexte sans relayer sur return () ou delay ().

J'ai créé la bibliothèque parce que j'avais besoin de trois threads et que j'avais besoin de deux d'entre eux pour fonctionner à une heure précise, peu importe ce que faisaient les autres. Le premier thread a géré la communication série. La seconde consistait à utiliser un filtre de Kalman utilisant la multiplication à matrice flottante avec la bibliothèque Eigen. Et le troisième était un fil de boucle de contrôle de courant rapide qui devait pouvoir interrompre les calculs de la matrice.

Comment ça fonctionne

Chaque thread cyclique a une priorité et une période. Si un thread ayant une priorité plus élevée que le thread en cours d'exécution atteint sa prochaine heure d'exécution, le planificateur mettra en pause le thread en cours et basculera vers le processus de priorité supérieure. Une fois que le thread hautement prioritaire a terminé son exécution, le planificateur revient au thread précédent.

Règles de planification

Le schéma de planification de la bibliothèque ThreadHandler est le suivant:

  1. La plus haute priorité en premier.
  2. Si la priorité est la même, le thread avec l'échéance la plus ancienne est exécuté en premier.
  3. Si deux threads ont le même délai, le premier thread créé sera exécuté en premier.
  4. Un thread ne peut être interrompu que par des threads avec une priorité plus élevée.
  5. Une fois qu'un thread est en cours d'exécution, il bloque l'exécution pour tous les threads ayant une priorité inférieure jusqu'à ce que la fonction d'exécution revienne.
  6. La fonction de boucle a la priorité -128 par rapport aux threads ThreadHandler.

Comment utiliser

Les threads peuvent être créés via l'héritage c ++

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

    virtual ~MyThread(){}

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

MyThread* threadObj = new MyThread();

Ou via createThread et une fonction lambda

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

Les objets de thread se connectent automatiquement au ThreadHandler lors de leur création.

Pour lancer l'exécution des objets de thread créés, appelez:

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

1

Et voici encore une autre bibliothèque multitâche coopérative à microprocesseur - PQRST: file d'attente prioritaire pour l'exécution de tâches simples.

Dans ce modèle, un thread est implémenté en tant que sous-classe d'un Task, qui est planifié pour un temps futur (et éventuellement reprogrammé à intervalles réguliers, si, comme cela est courant, il est LoopTaskremplacé par des sous-classes ). La run()méthode de l'objet est appelée lorsque la tâche devient due. La run()méthode effectue un certain travail, puis retourne (il s’agit du bit coopératif); il va généralement maintenir une sorte de machine à états pour gérer ses actions sur des invocations successives (un exemple trivial est la light_on_p_variable dans l'exemple ci-dessous). Cela nécessite de repenser légèrement la façon dont vous organisez votre code, mais il s'est avéré très flexible et robuste pour une utilisation assez intensive.

Les unités de temps sont agnostiques, il est donc aussi agréable de fonctionner en unités de millis()as micros(), ou en tout autre tick qui convient.

Voici le programme 'blink' implémenté à l'aide de cette bibliothèque. Cela montre qu'une seule tâche est en cours d'exécution: d'autres tâches seraient généralement créées et démarrées à l'intérieur 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());
}

Ce sont des tâches «d'exécution à terme», non?
Edgar Bonet le

@ EdgarBonet Je ne suis pas trop sûr de ce que vous voulez dire. Une fois que la run()méthode est appelée, elle n’est pas interrompue et a donc la responsabilité de terminer assez rapidement. Cependant, en règle générale, il fera son travail, puis se planifiera lui-même (éventuellement automatiquement, dans le cas d'une sous-classe de LoopTask) pour une date ultérieure. Un schéma courant consiste pour la tâche à conserver une machine à états interne (un exemple trivial est l' light_on_p_état ci-dessus) afin qu'elle se comporte de manière appropriée à la prochaine échéance.
Norman Gray

Donc, oui, il s’agit de tâches exécutées jusqu’à l’achèvement (RtC): aucune tâche ne peut être exécutée avant que celle en cours ne termine son exécution en revenant de run(). Ceci est en contraste avec les threads coopératifs, qui peuvent donner le processeur par exemple en appelant yield()ou delay(). Ou des threads préemptifs, qui peuvent être planifiés à tout moment. Je pense que la distinction est importante, car j'ai vu que beaucoup de gens qui viennent ici à la recherche de threads le font parce qu'ils préfèrent écrire du code bloquant plutôt que des machines à états. Bloquer les vrais threads qui génèrent le processeur est correct. Le blocage des tâches RtC ne l’est pas.
Edgar Bonet le

@ EdgarBonet C'est une distinction utile, oui. Je considérerais à la fois ce style et les threads de style de rendement, simplement comme des styles différents de thread coopératif, par opposition aux threads de préemption, mais il est vrai qu'ils nécessitent une approche différente pour les coder. Il serait intéressant de voir une comparaison réfléchie et approfondie des différentes approches mentionnées ici; une belle bibliothèque non mentionnée ci-dessus est protothreads . Je trouve des choses à critiquer dans les deux cas, mais aussi à louer. Bien sûr, je préfère mon approche, car elle semble la plus explicite et ne nécessite aucune pile supplémentaire.
Norman Gray

(correction: la protothreads a été mentionnée, dans la réponse de @ sachleen )
Norman Gray
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.