Programmer des sketches orientés "Radio-Commande" en mode asynchrone à l'aide d'une collection de bibliothèques non bloquantes pour Arduino UNO, MEGA, ATtiny84, ATtiny85 et ATtiny167.


Organisation des librairies asynchrones

Introduction

Sur les différents forums de discussion "Arduino", on lit souvent:

"Je fais clignotter une LED toutes les 5 secondes avec la fonction delay() et je mesure la largeur d'une impulsion provenant d'un récepteur RC avec pulseIn(), puis après traitement, je pilote un servo à l'aide de la librairie <SoftwareServo>. Mon servo saccade. Je constate que je rate beaucoup d'impulsions et que mon servo ne reçoit pas d'impulsion pendant le délai de 5 secondes:  il est "mou". Comment puis-je faire ces 3 actions en même temps."

Voilà typiquement, un cas programmation dit "synchrone": la fonction delay() de 5 secondes est une fonction bloquante, le micro-contrôleur ne fait (quasi) rien d'autre que d'attendre que les 5 secondes se soient écoulées.

Pour pulseIn(), c'est la même chose, si aucune impulsion n'arrive, par défaut, cette fonction ne rend pas la main avant une seconde:  il s'agit d'une fonction quasi bloquante. A l'échelle des capacités de traitement des micro-contrôleurs, une seconde, c'est une éternité!


L'asynchrone: une solution élégante, efficace et abordable

L'auteur de ces lignes propose donc une solution: la programmation en mode dit "asynchrone" à l'aide d'une collection de bibliothèques non bloquantes taillées pour les applications orientées "Radio-Commande". Pour les évènements entrants  (ex: lecture impulsion) Asynchrone signifie qu'il n'est pas nécessaire d'attendre qu'une action soit finie avant de rendre la main au micro-contrôleur. Le simple test d'un indicateur via une fonction de la librairie suffit à savoir si l'action est terminée ou pas. Pour les évènements sortants (ex: génération d'une impulsion), Asynchrone signifie qu'ils sont exécutés dans la boucle principale, dès que le moment est arrivé, ou bien en interruption timer.


Note:

"Bibliothèque" est la vraie traduction française du mot anglais "library". "Librairie" est est faux-ami, car la traduction anglaise est "bookstore": l'endroit où on vend des livres ou des journaux. Rien à voir avec les bibliothèques de fonctions! Cependant, par abus de langage, "librairie" ou "lib" est très souvent employé pour désigner un bibliothèque de fonctions.

Les bibliothèques asynchrones développées par l'auteur:


  • TinyPinChange:  bibliothèque générique pour capter de manière asynchrone les changements d'état des broches utilisant les interruptions sur changement d'état des broches des AVR.
  • SoftRcPulseIn:  bibliothèque permettant de lire les largeurs d'impulsions des Radio-Commandes standards en s'appuyant sur la bibliothèque <TinyPinChange>. Il s'agit d'une version non bloquante de la fonction pulseIn() arduino.
  • SoftRcPulseOut:  bibliothèque dérivée de la librairie <SoftwareServo>, mais grandement améliorée puisqu'elle est optimisée pour limiter la gigue des servos due aux diverses interruptions.
  • RcSeq:  permet de lancer des séquences prédéfinies de servomoteurs et actions courtes depuis des signaux de Radio-Commande standard. Cette bibliothèque s'appuie sur les bibliothèques <TinyPinChange>, <SoftRcPulseIn> et <SoftRcPulseOut>.
  • SoftSerial: Similaire à la bibliothèque <SoftwareSerial>, mais permettant de partager le vecteur d'interruption "Pin Change" de l'AVR via la bibliothèque <TinyPinChange>. En effet, la bibliothèque <SoftwareSerial> monopolise le vecteur vecteur d'interruption "Pin Change" et, de ce fait, ne permet pas l'utilisation simultanée de la bibliothèque <SoftRcPulseIn>. Avec la bibliothèque <SoftSerial>, c'est possible. Cela permet d'implémenter un port série sur les ATtiny84 et ATtiny85. Dans le cas de l'utilisation de l' Arduino UNO et MEGA, privilégier le port série natif (Serial).
  • TinyCppmGen:  permet de générer, à partir d'un timer 8 bits, un train d'impulsions PPM exactement identique à celui disponible sur une prise "écolage" d'un émetteur RC. Réglage indépendant des largeurs d'impulsion de toutes les voies, modulation PPM négative ou positive. Utile, par exemple, pour tester les bibliothèques <RcTxSerial> et <RcRxSerial> sans ensemble RC.
  • TinyCppmReader:  permet de lire un train d'impulsions PPM comme celui disponible sur une prise "écolage" d'un émetteur RC ou sur la sortie CPPM ou PPM sum de certains récepteurs.
  • RcTxSerial:  permet de créer un port série unidirectionnel à 200 bit/s entre un émetteur RC et un récepteur RC. <RcTxSerial> est la partie émission utilisée côté émetteur. Cette bibliothèque s'appuie sur les bibliothèques <SoftRcPulseOut> ou <TinyPpmGen>. <RcTxSerial> s'utilise de concert avec la bibliothèque <RcRxSerial>.
  • RcRxSerial permet de créer un port série unidirectionnel à 200 bit/s entre un émetteur RC et un récepteur RC . <RcRxSerial> est la partie réception utilisée côté récepteur. Cette bibliothèque s'appuie sur les bibliothèques <SoftRcPulseIn> ou <TinyPpmReader> qui elles-mêmes s'appuient sur la bibliothèque <TinyPinChange>.  <RcRxSerial> s'utilise de concert avec la bibliothèque <RcTxSerial>.
  • TinyOscCal:  permet de calibrer très facilement l'oscillateur interne des ATtiny84 et ATtiny85 (registre OSCCAL) afin de rendre possible l'utilisation fiable des ports séries logiciels (bibliothèque <SoftSerial>).
  • TinySoftPwm: permet d'utiliser n'importe quelle broche d'un ATtiny85, ATtiny167 ou ATmega328 (UNO) pour faire du PWM de manière logicielle.

Compatibilité avec les différents arduinos

Note: suite à la disponibilité de la micro-carte arduino Digispark distribuée par Digistump basée sur un ATtiny85 avec port USB logiciel, j'ai adapté mes bibliothèques à cette nouvelle plate-forme très prometteuse qui facilite grandement le chargement des sketches.

Bibliothèque \ Cible
UNO
MEGA
ATtiny84
ATtiny85
Digispark (pro)
Notes
Sans DigiUSB
Avec DigiUSB
TinyPinChange
O
O
O
O
O
N
usbdrvasm.S ne partage pas le vecteur d'interruption  "pin change".
SoftRcPulseIn
O
O O
O
O
N
S'appuie sur TinyPinChange. Voir ci-dessus.
SoftRcPulseOut
O
O O
O
O
O
Rien à signaler.
RcSeq
O
O O
O
O
  O*
Partiellement: s'appuie sur TinyPinChange. Voir ci-dessus.
SoftSerial
O
O O
O
O
N
S'appuie sur TinyPinChange. Voir ci-dessus.
RcTxSerial
  O
O   O
  O
  O
  O
S'utilise avec SoftRcPulseOut ou TinyCppmGen.
RcRxSerial
O
O O
O
O
N
S'utilise avec SoftRcPulseIn ou TinyCppmReader.
TinyCppmGen
O
O O
O
O
O
Utilise un timer de seulement 8 bits.
TinyCppmReader O O O O O N S'appuie sur TinyPinChange. Voir ci-dessus.
TinyOscCal
N/A
N/A
O
O
O
N
DigiUSB impose l'horloge à 16.5MHz.
TinySoftPwm
O
?
?
O
O
0
Non testé sur MEGA et ATtiny84.

Code couleur

Support à 100% sans condition particulière
O*
Support partiel

Pas supporté (support à 0%)
?
Non testé
N/A
Non Applicable

La collection de bibliothèques compatibles UNO, MEGA, ATtiny84, ATtiny85 (Digispark) et ATtiny167 (Digispark pro) est disponible au téléchargement ci-dessous.

Les bibliothèques à télécharger

Pour des raison de commodité et de facilité de maintenance, mes bibliothèques sont désormais disponibles sur mon dépot GitHub.
https://github.com/RC-Navy/DigisparkArduinoIntegration/tree/master/libraries
Attention, je ne maintiens pas la totalité des bibliothèques présentes via ce lien (seulement environ 60%).
N'hésitez pas à consulter régulièrement ce lien pour y découvrir les nouveautés.

La documentation


La documentation est sous forme de fichier Readme.md (au format Markdown), ce qui rend visible l'aide sous GitHub dès que l'on se trouve dans un répertoire de bibliothèque.
Par exemple, voir le bas de la page: https://github.com/RC-Navy/DigisparkArduinoIntegration/tree/master/libraries/DigisparkSoftRcPulseIn

Les exemples d'utilisation des bibliothèques

Pour chaque bibliothèque, un ou plusieurs exemples d'utilisation est/sont fourni(s) dans le répertoire "examples" de la bibliothèque.
Par exemple: https://github.com/RC-Navy/DigisparkArduinoIntegration/tree/master/libraries/DigisparkSoftRcPulseIn/examples

Installation des bibliothèques asynchrones dans "arduino"

Téléchargez sur votre PC le contenu du dépôt GitHub via le lien suivant: (clic droit, puis "enregistrer la cible du lien sous")
https://github.com/RC-Navy/DigisparkArduinoIntegration/archive/master.zip
Une fois sur votre PC, décompressez le .zip et les bibliothèques (TinyPinChange,  SoftRcPulseIn,  SoftRcPulseOut,  RcSeq, SoftSerial, RcTxSerial, RcRxSerial, TinyPpmGen, TinyPpmReader, TinySoftPwm et TinyOscCal) proposées par l'auteur seront disponibles dans le répertoire "libraries".

Copiez les répertoires des bibliothèques qui vous intéressent dans le répertoire:
 "votre_repertoire_d_installation\arduino-1.x.y\libraries" de votre PC, ou mieux, dans le répertoire "libraries" de votre "sketchbook".
Le répertoire "skechbook" est le répertoire où tous vos sketches sont sauvegardés. Pour en connaître l'emplacement, ouvrez l'IDE "arduino" et allez dans "fichier"->"Préférences" et regardez "Emplacement du carnet de croquis". C'est à cet emplacement que le le répertoire "libraries" se trouve.
L'avantage de stocker vos bibliothèques dans le répertoire "libraries" de votre "sketchbook" est qu'en cas de mise à jour de votre IDE arduino, vous n'avez pas à réinstaller vos bibliothèques, mais juste à vous assurer que "fichier"->"Préférences" pointe bien vers votre "sketchbook".

Exemple de sketch asynchrone

L'exemple ci-dessous illustre comment résoudre très facilement le problème décrit dans l'introduction ci-dessus.
Pour bien montrer que la capacité à faire toutes les actions "en même" temps, la période de clignottement de la LED a été réduite à 500 ms (demi période=250 ms).
#include <Rcul.h>           // inclusion de la librairie Rcul  (utilisee par SoftRcPulseIn et SoftRcPulseOut)
#include <TinyPinChange.h> // inclusion de la librairie TinyPinChange (utilisee par SoftRcPulseIn)
#include <SoftRcPulseIn.h> // inclusion de la librairie SoftRcPulseIn (utilisee par ce sketch)
#include <SoftRcPulseOut.h> // inclusion de la librairie SoftRcPulseOut (utilisee par ce skecth)

#define BROCHE_SIGNAL_RECEPTEUR 2
#define BROCHE_SERVO 3

#define BROCHE_LED 13

#define NEUTRE_US 1500

#define MAINTENANT 1

#define IMPULSION_DELAI_MAX_MS 30 /* Pour rafraichir le servo si disparition des impulsions */
#define DEMI_PERIODE_LED_MS 250 /* Ajustement de la frequence de clignottement de la LED */

#define MOY_SUR_1_VALEURS 0
#define MOY_SUR_2_VALEURS 1
#define MOY_SUR_4_VALEURS 2
#define MOY_SUR_8_VALEURS 3

#define TAUX_DE_MOYENNAGE MOY_SUR_4_VALEURS /* Choisir ici le taux de moyennage parmi les valeurs precedentes possibles listees ci-dessus */
/* Plus le taux est élevé, plus le système est stable (diminution de la gigue), mais moins il est réactif */

#define MOYENNE(Valeur_A_Moyenner,DerniereValeurRecue,TauxDeMoyEnPuissanceDeDeux) Valeur_A_Moyenner=(((Valeur_A_Moyenner)*((1<<(TauxDeMoyEnPuissanceDeDeux))-1)+(DerniereValeurRecue))/(1<<(TauxDeMoyEnPuissanceDeDeux)))

SoftRcPulseIn ImpulsionRecepteur;
SoftRcPulseOut MonServo;

uint32_t DebutChronoLedMs=millis();
uint32_t DebutChronoServoMs=millis();

boolean EtatLed=LOW;

uint16_t Largeur_us_Moyennee; /* Variable pour contenir la largeur d'impulsion moyennee (pour plus de stabilité du servo) */

void setup()
{
ImpulsionRecepteur.attach(BROCHE_SIGNAL_RECEPTEUR);
pinMode(BROCHE_LED, OUTPUT);
MonServo.attach(BROCHE_SERVO);
MonServo.write_us(NEUTRE_US);
Serial.begin(9600);
}

void loop()
{
uint16_t Largeur_us;
static uint32_t DebutChrono=0;
static uint32_t Compteur=0;
if(!DebutChrono) DebutChrono=millis();
/* Acquisition du signal du Recepteur, traitement (inversion), puis mise a jour du Servo */
if(ImpulsionRecepteur.disponible())
{
Largeur_us=(NEUTRE_US*2)-ImpulsionRecepteur.largeur_us(); /* Traitement pour inverser le sens de rotation du servo */
MOYENNE(Largeur_us_Moyennee,Largeur_us,TAUX_DE_MOYENNAGE); /* Moyenne glissante pour filtrer le signal -> Stabilite */

MonServo.write_us(Largeur_us_Moyennee);
SoftRcPulseOut::refresh(MAINTENANT); /* Permet de synchroniser les impulsions sortantes avec les impulsions entrantes */
DebutChronoServoMs=millis(); /* Relance Chrono */
}
else
{
/* Test de disparition des impulsions */
if(millis()-DebutChronoServoMs>=IMPULSION_DELAI_MAX_MS)
{
/* Raffraichit le servo avec la derniere position connue pour eviter qu'il ne soit "mou" */
SoftRcPulseOut::refresh(MAINTENANT); /* Permet la mise a jour immediate des impulsions sortantes */
DebutChronoServoMs=millis(); /* Relance Chrono */
}
}
/* Mise a jour de l'etat de la LED */
if(millis()-DebutChronoLedMs>=DEMI_PERIODE_LED_MS)
{
digitalWrite(BROCHE_LED, EtatLed);
EtatLed=!EtatLed; /* Au prochain passage, l'etat de la LED sera inverse */
DebutChronoLedMs=millis();
}
Compteur++;
if(Compteur>=1000000)
{
Serial.print("Tps boucle = ");Serial.print((millis()-DebutChrono)/1000);Serial.println(" us");
Compteur=0;
DebutChrono=0;
}
}


Résultats:

Avec ce sketch, sur un Arduino UNO à 16 MHz, les mesures montrent que 1000000 tours de boucle principale s'effectuent en environ 12000 ms, soit un temps de boucle moyen de 12 µs! Le micro-contrôleur respire et peut faire encore beaucoup d'autres tâches "en parallèle" à la seule condition que ces taĉhes additionnelles ne fassent appel à aucune fonction bloquante! La programmation doit rester de type "asynchrone" jusqu'au bout!


Remarque:

Les mesures de largeur d'impulsion sont assez sensibles aux masquages d'interruption de quelques micro-secondes. Afin d'avoir une mesure stable, celle-ci est "lissée" à l'aide de la macro MOYENNE() qui effectue une moyenne glissante. cette méthode est d'une efficacité redoutable.

Conseils quant au câblage des servos

Les servomoteurs ont 3 fils de connexion :

  • le +
  • le - (ou la masse)
  • la commande du servomoteur : une impulsion modulée en largeur (PWM) qui définit la position de l'axe du servomoteur.

Dans cas de l'utilisation d'un à 2 servomoteur(s) standard(s) :

  • Le fil du +, est typiquement rouge, et doit être connecté à la broche 5V de la carte Arduino.
  • Le fil de la masse est typiquement noir ou marron et doit être connecté au 0V (= à la masse) de la carte Arduino.
  • Le fil de l'impulsion de commande est typiquement jaune, orange ou blanc et doit être connecté à une broche numérique de la carte Arduino.

Dans le cas de l'utilisation de plusieurs servomoteurs ou de servomoteurs nécessitant une tension supérieure à 5V et/ou une intensité importante :

  • Noter que les servomoteurs nécessitent parfois une tension supérieure à 5V et/ou une intensité importante. Donc si vous devez utiliser plus d'un ou deux servomoteur(s), vous aurez alors besoin d'utiliser une alimentation différente du 5V de la carte Arduino.
  • Dans le cas de l'utilisation d'une alimentation séparée de la carte Arduino, bien connecter ensemble les masses (le 0V) de la carte et de l'alimentation externe, mais surtout, ne pas relier les +5V ensemble!

L'asynchrone: une solution ouverte

Programmer les Arduinos en mode asynchrone: une solution ouverte
Les bibliothèques proposées par l'auteur étant indépendantes, elles peuvent être utilisées entre-elles, et par le sketch utilisateur: elles sont partageables.
Par exemple, le sketch utilisateur peut très bien utiliser la bibliothèque <TinyPinChange> pour un traitement "maison" sur des entrées, bien que celle-ci soit déjà utilisée pour <SoftRcPulseIn> et/ou par la bibliothèque <SoftSerial>.
Dans le croquis ci-dessus, les flèches blanches indiquent le sens du flux d'information.

Retour