Hors-série. Du temps. Partie 3.

Partie 3 de ce hors-série, qui sera à nouveau un peu technique, et viendra en complément des deux autres, sur la gestion basique du temps et la fluctuation du signal avec un Arduino.

Il existe un phénomène de fluctuation temporelle, bien connu des amateurs électroniciens qui cherchent à générer des ondes propres : en effet, le temps d’exécution des différentes fonctions biaise notre perception initiale. Pour prendre la suite des articles précédents, nous avons l’impression qu’en inscrivant des délais comme 500ms sur les deux évènements (allumage et extinction), l’exécution nous enverra une onde parfaite de 1Hz. Après tout, c’est logique, non ? Nous demandons un temps d’allumage d’exactement 500 millisecondes, et un temps d’extinction d’exactement 500 millisecondes.

Ceci n’est pas le cas en pratique.

Pour plusieurs raisons, dont la plus évidente est que l’exécution de nos fonctions elles-même prend un temps non-nul. Ceci a avoir avec l’implémentation des fonctions dont nous nous servons, avec la manière dont elles ont été codées. digitalWrite() est par exemple connue pour sa « lenteur », malgré sa grande facilité d’emploi. Elle prend deux paramètres très simples en entrée : un numéro de broche, en pratique un nombre entier (ou une variable préalablement déclarée), et un état haut ou bas… et c’est tout ! Elle nous épargne du travail, en contrepartie, elle prend pas mal de cycles d’horloges pour fonctionner.

On peut toutefois réduire le nombre de cycles d’horloge nécessaires pour « flipper » l’état d’une broche, ce qu’on verra plus loin. « Flipper » signifie ici passer du +5V au 0V et inversement.

Avant même de réfléchir aux corrections (une vraie correction précise demande évidemment un immense travail et surtout des pros), je cherchais plus simple : comment mettre en évidence ce phénomène ? Comment le voir, même grossièrement, sans un oscilloscope ?

En fait, avoir des premiers résultats, c’est assez simple. Il faut communiquer avec l’Arduino (par le port série), et lui demander de nous renseigner sur le temps d’exécution de notre fonction. En pratique, on commence par placer un Serial.begin(9600) dans le setup. Le 9600 est une vitesse, ce sont des « bauds », des bits par secondes. Puis on se sert du temps.

Pour cela, on se sert de la fonction micros(). Cette fonction est un « time-stamp », c’est à dire un marqueur temporel. En pratique, c’est une mesure de temps calée sur l’horloge interne de l’Arduino. Cette horloge, c’est souvent un cristal de quartz : il vibre par l’effet piézoélectrique. Au passage, sur un Arduino Uno, il est calé sur 16 millions de Hz.

C’est considérable, non ? Seize millions par seconde…

Et pourtant, rien du tout comparé aux milliards de Hz d’un ordinateur même basique… Question de point de vue, comme toujours… !

En pratique, dans notre cas, micros() va nous donner en microsecondes le temps écoulé depuis le début de notre programme, avec une résolution de 4 microsecondes – elle compte de 4 en 4, ceci est du à la cadence du quartz et à sa méthode de calcul (1) . La valeur qu’elle retourne sera un multiple de 4, dans notre cas, cette résolution est largement suffisante.

Si on assigne ce nombre à un jeu de variable correctement choisi, on peut, par une simple soustraction, avoir la durée de l’exécution de notre fonction.

En la comparant avec le délai théorique que nous visons, nous avons une première idée du décalage.

J’ai donc commencé par écrire un premier code très simple. Voyons ça plus en détail, avec l’exemple du 1Hz en onde carrée symétrique avant d’aller plus loin.

float delai_1 = 500; /* On déclare nos délais théoriques souhaités, ici 500ms – puisqu’ils sont sur la fonction delay() qui prend des ms en paramètre – une demi-seconde.*/
float delai_2 = 500;

float delai_theorique = delai_1*1000 + delai_2*1000; /* On déclare une variable qui correspondra à notre délai théorique, somme des deux délais précédents. On multiplie chacune des valeurs par 1000 pour obtenir des microsecondes, ce qui nous servira à comparer la valeur du micros() qui donne, comme son nom l’indique, des microsecondes. */

 

void setup()

{

Serial.begin(9600); /* On initialise la communication avec le port série (USB) à une vitesse de 9600 bauds */

[…]

}

void loop()

{

unsigned long marqueur_fin; // On déclare la variable de fin

unsigned long marqueur_debut=micros(); /* On assigne à la variable de début la valeur de l’horloge mesurée à ce moment */

digitalWrite(13, HIGH);
digitalWrite(12, HIGH);
digitalWrite(11, HIGH);
digitalWrite(10, HIGH);
digitalWrite(9, HIGH);
digitalWrite(8, HIGH);
digitalWrite(7, HIGH);
digitalWrite(6, HIGH);
delay(delai_1);

digitalWrite(13, LOW);
digitalWrite(12, LOW);
digitalWrite(11, LOW);
digitalWrite(10, LOW);
digitalWrite(9, LOW);
digitalWrite(8, LOW);
digitalWrite(7, LOW);
digitalWrite(6, LOW);
delay(delai_2);

marqueur_fin=micros(); /* On récupère la valeur de l’horloge à la fin du programme dans notre variable marqueur_fin que nous avons déclaré en tout début de boucle */

float delai_reel = marqueur_fin-marqueur_debut; /* On calcule notre soustraction, qui correspond au temps écoulé depuis sa déclaration initiale.*/

Serial.print(delai_reel – delai_theorique); // On l’affiche sur la console
Serial.println( » microsecondes d’écart »);

Et dans mon cas je me retrouve avec… 1 000 084 microsecondes en moyenne. Au lieu des 1 000 000 de microsecondes (1 seconde) théoriques. On peut se dire, oui, 84… 88 microsecondes d’écart, ce n’est pas grand chose ? Et bien, il faut voir que nous n’avons testé ici qu’une seule fonction… !

Capture d_écran 2017-11-12 à 17.03.47

L’onde n’est pas celle que l’on attend, n’est pas celle que l’on avait prévue, et ça peut devenir un problème, en pratique, expérimentalement, ça le devient. Si le délai est plus important, la fréquence est moins grande que celle qu’on attend. La fréquence, ce n’est rien d’autre qu’un « quelque chose » par seconde. Si ce « quelquechose » prend plus temps qu’un autre, sa fréquence est moins grande. L’image mathématique pourtant parfaite que l’on avait de notre fonction n’est pas si parfaite en sortie.

Pourquoi y porter notre attention ? Vous n’avez que mes mots teintés de subjectivité, mais après quelques corrections que je vais aborder un peu plus loin, et bien, l’expérience n’est pas tout à fait la même avec les différentes fréquences qui s’enchainent. Elle me paraît, et je ne suis pas le seul à penser ainsi, en effet plus « nette ». Même à des fréquences basses.

Y compris dans le cas où cela serait entièrement subjectif (ce que je ne crois pas, mais on est à peu près toujours d’accord avec soi-même, n’est-ce pas ?), je trouve la démarche intéressante, et le code gagne en qualité.

Voici mon raisonnement.

digitalWrite() prend du temps. Trop de temps. delay() génère de plus une interruption (delayMicroseconds non, par exemple).

1) Soit je persiste à utiliser ce duo parce qu’il est simple, alors je dois savoir comment digitalWrite() est implémentée, pour contourner si possible certains cycles de son exécution, ou changer (voire réécrire!) de fonction.
2) Soit je pense le flicker différemment, et j’utilise par exemple un time-stamp et un booléen qui va « flipper » l’état du pin (à nous de voir comment s’y prendre pour gagner encore du temps là-dessus) avec le délai (mais sans la fonction delay())que j’ai choisi : mais comment transposer une fonction comme desync_full avec cette méthode ? Je n’ai pas encore creusé loin de ce côté là. On peut toutefois aller très loin en précision et en rapidité avec cette méthode, en manipulant correctement les registres de la puce, en désactivant les interruptions, et en ayant une très bonne connaissance des timers de l’Arduino (un timer est un registre incrémenté à chaque signal de l’horloge interne du contrôleur : il y a moyen de trouver des configurations particulièrement rapides en creusant de ce côté là). Pour les intéressés, voir en référence (5).

Je suis arrivé à un résultat que je peux qualifier de satisfaisant pour l’instant avec la première.

Après quelques recherches, je me suis aperçu que si digitalWrite() est si lente, c’est qu’elle doit exécuter pas mal de choses, et qu’elle doit en plus convertir le numéro pin inséré en paramètre (un simple entier) pour que ce numéro soit « compréhensible » par la puce interne, l’ATmega. Mais on peut by-passer (2). On peut aller assez loin de ce côté, en manipulant directement les registres de la puce.

Âmes sensibles, s’abstenir… !

En effet, un inoffensif :

digitalWrite(13, HIGH);

digitalWrite(13, LOW);

Peut vite devenir un :

PORTB |= _BV(PB5);
PORTB &= ~_BV(PB5);

Et ça se corse encore. Pas impossible et très efficace, mais il faut se pencher sérieusement sur l’architecture interne des puces. Je trouve ça personnellement passionnant, mais on va encore dire que je suis bizarre…

Autre solution, se servir d’une librairie déjà écrite. J’ai trouvé la digitalWriteFast (2), (3), qui fonctionne très bien. Ainsi qu’une méthode alternative, voir en référence (4).

En pratique : j’ai utilisé la digitalWriteFast, facile d’emploi, pour commencer mes tests : sans optimisation particulière, j’obtiens 16 us de retard, ce qui est acceptable, en l’état (à comparer avec le résultat ci-dessus).

J’ai uploadé un fichier dans le dossier Arduino : Prog1 + Prog2 + Buttons. Comme son nom l’indique, il contient un unique code source contenant deux programmes d’environ 5 minutes chacun, qui incluent la librairie digitalWriteFast pour ceux qui voudraient tester tel quel. Ces deux programmes sont commandés par deux boutons physiques dans mon montage actuel (en pull-up, c’est pour cela qu’on lit un LOW quand le bouton est appuyé), mais on peut tout à fait faire à sa guise en effet.

Il suffit par exemple de remplacer le void loop par un tout simple

void loop()

{

programme_1();

}

Le programme 1 se lancera bien sûr seul, sans nécessiter de boutons physiques.


 

Annexe

On peut aussi explorer en pensant différemment le flicker, avec ce genre de portions de code, le flicker est généré par un booléen (toujours avec la digitalWrite ici) ! (avec un très bon timing, d’ailleurs)

unsigned long marqueur;
boolean etat_led=0;
const int led=13;

void setup()
{

pinMode(led, OUTPUT);

digitalWrite(led, etat_led);

marqueur = micros();

}

void loop()
{

if((micros() – marqueur) >= 1000000)
{
etat_led = !etat_led;
digitalWrite(led, etat_led);
marqueur = micros();
}

}

Mais je ne suis pas allé plus loin pour l’instant de ce côté. A suivre donc, peut-être !


 

A terme, si je poursuis plus loin, je pense que je scinderais mon code avec des headers maison pour y voir plus clair. Je basculerais peut-être sur un Arduino Mega. Une copie se trouve pas loin des 10 euros, et possède beaucoup plus de pins qu’un Uno. Utile pour implémenter facilement d’autres features sur le proto, comme un écran LCD, des boutons etc…

Tout peut s’imaginer. C’est une question de temps, de créativité, et d’envie.

(et d’infinies prises de têtes sur le code)

🙂


 

Références

(1) Code source officiel de l’Arduino. Voir la fonction micros(). https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/wiring.c

(2) La librairie digitalWriteFast (anglais) : https://code.google.com/archive/p/digitalwritefast/downloads

(3) Le forum qui parle de la digitalWriteFast, ses auteurs (anglais) : http://forum.arduino.cc/index.php?topic=46896.0

(4) Une amélioration, domaine public, de la digitalWriteFast (français) qui by-pass la traduction vers l’ATmega https://forum.arduino.cc/index.php?topic=458624.0

(5) Très bon article sur les timers de l’Arduino. http://www.locoduino.org/spip.php?article84

(6) Arduino is Slow and How to Fix it http://www.instructables.com/id/Arduino-is-Slow-and-how-to-fix-it/

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion /  Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion /  Changer )

Connexion à %s