Supprimer les delay() sans utiliser millis(), avec easyRun

Voici un tuto qui montre une méthode simple pour supprimer les delay() d'un programme sans utiliser la fonction millis(), grâce à la bibliothèque easyRun.h

Prenons l'exemple d'un bête feu tricolore

const int pinVert = 4;
const int pinOrange = 5;
const int pinRouge = 6;

void setup()
{
  Serial.begin(115200);
  pinMode(pinVert, OUTPUT);
  pinMode(pinOrange, OUTPUT);
  pinMode(pinRouge, OUTPUT); 
}

void loop()
{
  Serial.println("Feu vert");
  digitalWrite(pinVert, HIGH);
  digitalWrite(pinOrange, LOW);
  digitalWrite(pinRouge, LOW);
  delay(4000);
 
  Serial.println("Feu orange");
  digitalWrite(pinVert, LOW);
  digitalWrite(pinOrange, HIGH);
  digitalWrite(pinRouge, LOW);
  delay(1000);

  Serial.println("Feu rouge");
  digitalWrite(pinVert, LOW);
  digitalWrite(pinOrange, LOW);
  digitalWrite(pinRouge, HIGH);
  delay(5000);
}

Ce programme a un vilain défaut : il utilise la fonction delay()

Chaque exécution de la fonction loop() dure 10 secondes.
L'arduino passe l'essentiel de son temps dans la fonction delay().
Il est donc impossible de lui demander de faire autre chose en parallèle de la gestion du feu tricolore.

Dans un premier temps, il convient de restructurer le programme, sans changer son principe de fonctionnement.

Les lignes de code qui doivent être exécutées immédiatement les unes à la suite des autres, sont isolées dans des fonctions dédiées.

Rien de bien méchant, cela donne ceci :

const int pinVert = 4;
const int pinOrange = 5;
const int pinRouge = 6;

void feuVert()
{
  Serial.println("Feu vert");
  digitalWrite(pinVert, HIGH);
  digitalWrite(pinOrange, LOW);
  digitalWrite(pinRouge, LOW);
  delay(4000);
}

void feuOrange()
{
  Serial.println("Feu orange");
  digitalWrite(pinVert, LOW);
  digitalWrite(pinOrange, HIGH);
  digitalWrite(pinRouge, LOW);
  delay(1000);
}

void feuRouge()
{
  Serial.println("Feu rouge");
  digitalWrite(pinVert, LOW);
  digitalWrite(pinOrange, LOW);
  digitalWrite(pinRouge, HIGH);
  delay(5000);
}

void setup()
{
  Serial.begin(115200);
  pinMode(pinVert, OUTPUT);
  pinMode(pinOrange, OUTPUT);
  pinMode(pinRouge, OUTPUT); 
}

void loop()
{
  feuVert();
  feuOrange();
  feuRouge();
}

La fonction loop() a été éclatée en trois fonctions distinctes, chacune se terminant par un delay().
A ce stade, le programme a toujours le même fonctionnement, et le même défaut.

Il est important de tester cette version intermédiaire, pour s'assurer de n'avoir rien cassé.
Si besoin, certaines variables locales à la fonction loop() peuvent être remontées en variables globales, afin d'être connues de partout.

Ouf le plus dur est fait :slight_smile:

A présent, pour supprimer les delay(), une méthode simple consiste à ajouter une nouvelle variable globale, de type "tâche asynchrone".
Cette bestiole est définie par la bibliothèque easyRun.h
Elle permet d'appeler une fonction "pas tout de suite mais dans un certain temps"

Le code définitif est alors :

#include "easyRun.h"

const int pinVert = 4;
const int pinOrange = 5;
const int pinRouge = 6;

asyncTask lanceur; //lanceur est un objet de type "tâche asynchrone"

void feuVert()
{
  Serial.println("Feu vert");
  digitalWrite(pinVert, HIGH);
  digitalWrite(pinOrange, LOW);
  digitalWrite(pinRouge, LOW);
  lanceur.set(feuOrange, 4000); //lanceur devra appeler la fonction feuOrange dans 4000 ms
}

void feuOrange()
{
  Serial.println("Feu orange");
  digitalWrite(pinVert, LOW);
  digitalWrite(pinOrange, HIGH);
  digitalWrite(pinRouge, LOW);
  lanceur.set(feuRouge, 1000);
}

void feuRouge()
{
  Serial.println("Feu rouge");
  digitalWrite(pinVert, LOW);
  digitalWrite(pinOrange, LOW);
  digitalWrite(pinRouge, HIGH);
  lanceur.set(feuVert, 5000);
}

void setup()
{
  Serial.begin(115200);
  pinMode(pinVert, OUTPUT);
  pinMode(pinOrange, OUTPUT);
  pinMode(pinRouge, OUTPUT);
  lanceur.set(feuVert, 0); //Pour appeler la fonction feuVert dès que possible, et ainsi amorcer le cycle
}

void loop()
{
  easyRun(); //actualisation des objets easyRun, dont lanceur.
}

Et voilà !
La fonction loop() ne contient plus que l'appel à la fonction easyRun(), qui se charge d'actualiser l'objet lanceur.

A présent, le programme exécute la fonction loop() à haute fréquence, des milliers de fois par secondes.
Et une fois de temps en temps, cela déclenche l'appel à une des trois fonctions de commutation des feux.

L'arduino est donc disponible à 99,99% de son temps pour faire autre chose.

Complément :

Lors de la phase de mise au point, je recommande d'ajouter systématiquement une variable globale de type loopMeter.
C'est un autre type de bestiole définie par la bibliothèque easyRun.h

En tout début de programme :

#include "easyRun.h"
loopMeter lm;
...

Juste une ligne à ajouter, rien d'autre à faire.

Littéralement, cela ajoute un loop-mètre (pardon pour ce mot-valise) au programme.
Le loop-mètre génère des stats sur la durée de la fonction loop(), affichées dans le terminal série toutes les 10 secondes.

Ainsi, avec le programme ci-dessus sur une arduino uno, on obtient

loopMeter: min=0ms avg=0ms max=2ms => 42062 loops per sec (2 easyRun items)

On voit que la fonction loop() est exécutée 42.000 fois par seconde.

Quel que soit votre programme, si vous voulez que l'arduino fasse "plusieurs choses à la fois", vous devez conserver une fonction loop() très courte, afin qu'elle puisse être exécutée un grand nombre de fois par seconde.
Le loop-mètre vous y aidera.

Ouaaaa...

Merci je vais tester.

Bravo.

En fait, je voudrais faire quelque chose de semblable mais sans avoir à mettre quelque chose dans loop. Ce qui permettrait d'utiliser du code bloquant dans loop. J'ai une gestion similaire pour un écran tactile, jusqu'à présent je n'avais pas eu de problèmes, mais j'ai eu une fonction bloquante et je n'avais donc plus accès au touchpad. En fait les fonctions de la gestion d'un écran sont bloquantes, notamment pour effacer un écran, afficher un BMP venant de la carte SD, les fonctions de remplissage... C'est très compliqué de faire autrement.

Sachant copier ce qui se fait, je me dis qu'il devrait être possible de faire ce qui se fait quand j'utilise du C++ sous PC, on a des évènements (timer, boutons...) qui passent par une gestion d'évènements, et qui agissent même si on a des fonctions bloquantes.

Salut

Je te soumets mon avis sur la question :slight_smile:

easyRun apporte avant tout des facilités d'exécution de fonctions basées sur un ordonnanceur coopératif, c'est-à-dire des fonctions qui ne s'interrompent pas entre elles.
Sur ESP il y a un scheduler intégré au core arduino, mais la peinture n'a pas l'air totalement sèche.
J'ai préféré implanter mon propre scheduler qui est compatible avec tous les core.
C'est très adapté à certains besoins (la plupart des miens) autour d'un arduino, mais pas à tous.

Le résultat est bon tant que l'on n'a pas de grosse nécessité de réactivité sur événement, et tant que l'on n'a pas de grosses portions de code bloquant.

Ne rien mettre dans le loop, revient à sortir du framework arduino.
On pourrait essayer de hacker la fonction delay() mais là encore c'est sortir du framework.
Pour aller vers un vrai système multi-tâches, il faut lorgner vers RTOS.
Mais ce n'est pas forcément la solution miracle. Là encore, cela dépend du besoin spécifique à chaque montage.

En intermédiaire, on peut obtenir ponctuellement des améliorations intéressantes et exploitant les dispositifs hardware à disposition (timers, interruptions). Cela permet avant tout d'avoir une précision temporelle sur les événements, mais ne résoud pas le problème de répartir intelligemment le temps CPU sur les diverses tâches à accomplir.

Pour les fichiers sur SD, perso j'utilise plutôt des fichiers chargés directement dans la flash en progmem.

Pour les écran graphiques : effectivement j'ai le cas d'un écran 800x600 qui, même en SPI, même piloté par une machine à état qui sépare tous les petits delays prévus dans le protocole de communication avec le périphérique, a encore une fonction bloquante qui dure 200 à 300 ms pour actualiser 100% des pixels.
De quoi pénaliser la réactivité sur d'autres tâches.
Là je n'ai pas de solution miracle, à part éventuellement remplacer le rafraichissement full screen par des rafraîchissements partiels, en rendant la main entre chaque.

En tout cas je ne vais pas râler après easyRun, j'ai fait pareil pour la gestion du touchpad; Et étant plutôt sur AVR,je n'ai pas tellement le choix.

Ne rien mettre dans le loop, revient à sortir du framework arduino.

Quand je dis ne rien mettre, c'est l'ppel à easyRun() ou à mon scanEvent(). En gros, avoir le même programme 1_button_demo mais sans le easyRun. Que celui-ci soit géré autrement, par exemple par un timer "invisible". Un utilisateur lamba veut un bouton, il le déclare et la bibliothèque le gère en masqué, sans avoir besoin d'appeler le gestionnaire.
J'ai eu besoin de faire tourner un moteur pas à pas. Avec AccelStepper leur easyRun s'appelle stepper.runSpeed();
mais il faut le titiller sans arrêt pour la rotation. En passant par un timer, je n'ai plus rien à faire que de lancer mon programme.

C'est comme la fonction millis() qui se met à jour toute seule même si on a du code qui n'appelle pas le système.

Pour les fichiers sur SD, perso j'utilise plutôt des fichiers chargés directement dans la flash en progmem.

J'aime bien la Uno, et quand il y a un code plus la gestion de l'écran plus la gestion évènementielle du touchpad, il ne me reste plus rien en flash. D'ailleurs pour un écran de 320x240 en 65000 couleurs, cela fait 150ko et je n'en ai que 32. Et en passant à des formats compressés, j'ai plus la place de mettre le gestionnaire.

Là je n'ai pas de solution miracle, à part éventuellement remplacer le rafraichissement full screen par des rafraîchissements partiels, en rendant la main entre chaque.

Je vois surtout les personnes que l'on aiguillent sur ta bibliothèque qui ne sont pas tous chevronnés et qui ont des difficultés à faire un code qui nous paraît simple. Difficile de leur demander de scinder le code!

En intermédiaire, on peut obtenir ponctuellement des améliorations intéressantes et exploitant les dispositifs hardware à disposition (timers, interruptions). Cela permet avant tout d'avoir une précision temporelle sur les événements, mais ne résoud pas le problème de répartir intelligemment le temps CPU sur les diverses tâches à accomplir.

C'est vers cela que je vais me tourner éventuellement. C'est ce que j'ai fait pour mes pas à pas, c'est ce qui est fait pour Servo. Là le problème de la répartition ne se pose pas, le moteur prend tout le temps qui lui est utile et le reste c'est pour loop().

Bonsoir
Je découvre le paramétrage dans Arduino, et j'ai réussi à faire fonctionner un relais suivant un seuil de T°, mais je me heurte à pouvoir faire vérifier (par exemple) la mesure d'une T° une fois par 30mn sans que le relais soit actionné pendant 30 mn
Je compte sur vos compétences pour me sortir de cette impasse
Cdlt
BobArdui

Il faut supprimer le delay() qu'il y a à le ligne 43.

(ou alors poster au bon endroit en donnant des éléments pouvant permettre de donner une réponse.)

Remarque :
Le sujet original est un tutoriel.
Les échanges qui se rapportent directement à ce tutoriel ont bien évidement leur place ici.

Les échanges qui portent sur la mise en oeuvre de cas particulier à partir de ce tutoriel n'ont pas leur place ici et doivent être traités comme n'importe quel autre sujet c'est à dire dans le forum principal.
Voir "Faire un nouveau sujet avec les balises".

Merci.

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