Mon désamour pour la gestion de l'horloge système d'Arduino

Sommaire:
      Je n'aime pas cette gestion
      Comment fonctionne-t-elle?
      J'aurais préféré
      Le programme mystère

Je n'aime pas cette gestion

Vrai, je n'aime pas du tout cette gestion. Définitivement. Sauf si un jour on me convainc que c'est une bonne solution. L'horloge système de l'Arduino Uno, Nano et Mega (je ne parle que de celles que je connais) monopolisent trop de temps par rapport aux besoins, utilisent trop de code et trop de variables. Je la voudrait plus discrète. Si leur mise à jour durait 1µs de moins, comme elles s'activent toutes les 1024µs, on gagnerait environ 1ms toutes les secondes, 8 heures tous les ans, et comme nous sommes nombreux à utiliser l'Arduino, le temps de calcul gâché est énorme. C'est sûr, on se déteste.

Quand j'écris un programme qui ne sert pas à grand chose, que je n'utilise pas souvent, je me fiche pas mal des optimisations. Mais si j'écris une bibliothèque que je vais réutiliser plusieurs fois, ou si d'autres vont l'utiliser, soigner le code devient primordial. Et pour l'horloge système, quasiment tout le monde de l'Arduino l'utilise, dans beaucoup de cas sans le savoir!

 

Comment fonctionne-t-elle?

Elle utilise le timer 0 et le travail se fait en grande partie par interruptions. Si vous pensez ne pas l'utiliser, votre programme sera interrompu régulièrement par mademoiselle la routine de remise à l'heure.

C'est un choix qui a été fait d'utiliser le timer 0 qui est un 8 bits, cela laisse un timer 16 bits accessible pour des tâches plus gourmandes. C'est bien. Cela aurait été sympa qu'il compte les microsecondes, mais avec un quartz à 16MHz, ce n'est pas possible (on peut compter toutes les 0,5µs, toutes les 4µs, mais pas entre les deux). A été choisi de compter toutes les 4µs, c'est bien suffisant. C'est pour cela que l'on dit que la résolution de delayMicroseconds() est de 4µs.

Le timer 0 est limité à 8 bits et ne peut compter que jusqu'à 255, ce qui est insuffisant pour les chronométrages, et la gestion du temps. C'est comme vendre des montres qu n'auraient que l'aiguille des secondes; elle donnerait bien l'heure à la seconde près, mais ce ne serait pas très utile. On a rajouté des aiguilles pour la plupart des gens et on compte sur 12 heures. Il existe des modèles qui comptent plus loin, en donnant le jour et le mois... Comme on a besoin avec l'horloge système de compter les microsecondes, les secondes, les heures, on a donc fait pareil, on a étendu dans des variables le comptage.

Le timer 0 s'incrémente toutes les 4µs, il arrive au maximum toutes les 1024µs. Et c'est là qu'un choix a été fait (je le démolis le mec si je le vois): c'est d'avoir un comptage des microsecondes différent du comptage des millisecondes.

Pour les microsecondes, chaque fois que le registre TCNT0 du timer 0 passe de 255 à 0 (qu'il déborde) on incrémente une variable timer0_overflow_count. Ainsi cette variable est les poids forts du nombre de microsecondes. Le nombre de microsecondes depuis le démarrage du programme (la variable est mise à 0 au départ) est timer0_overflow_count * 1024 + TCNT0 * 2. Le calcul est simple, il n'y a que des additions et des décalages. timer0_overflow_count est une variable 32 bits, elle va déborder toutes les heures environ (la référence arduino dit à propos de micros(): "This number will overflow (go back to zero), after approximately 70 minutes".

Pour le comptage des millisecondes, cela se complique. On va les compter dans une deuxième variable 32 bits timer0_millis. Cette dernière devrait s'incrémenter de 1 toutes les millisecondes. Mais le timer 0 ne peut le faire que toutes les 1,024ms ce n'est pas assez souvent. Le rattrapage de la bêtise du choix se fait ainsi: on incrémente de 1 la variable timer0_millis toutes les 1,024ms, et comme cela ne suffit pas, on va même l'incrémenter de 2 au lieu de 1 de temps en temps (24 fois sur les 1024). Il va y avoir des millisecondes qui n'existent pas, comme mon horloge de tout à l'heure qui sautait les positions 14, 18, 39 et 45!

Une preuve? C'est "facile", on compte les restes de millis() par 1024; on compte chaque fois que ce reste fait 0, chaque fois que ce reste fait 1,... Et on s'aperçoit qu'il y a 24 restes qui n'existent pas. millis()/1024 ne peut jamais donner 42 par exemple. Voici un programme qui fait ce comptage (ne tourne que sur Mega car on utilise un tableau de 1024 valeurs qui contient le nombre de fois que chacun des restes est trouvé, et cela dépasse la taille de la RAM d'une UNO ou d'une Nano). S'affiche régulièrement sur la console (réglée sur 115200 bauds) le nombre de restes par 1024 de millis() trouvés. Ce programme peut afficher à un certain moment ce tableau (colonne de gauche: le premier reste, 32 colonnes de droite: le nombre de fois que le reste est sorti). On voit alors que toutes les valeurs sont sorties environ 10.000 fois sauf les 24 valeurs qui sont sautées.

La précision de millis() est deux fois moins bonne que ce qu'on pense normalement;

 

J'aurais préféré

J'aurais nettement préféré, parce c'est plus simple, que le timer 0 compte jusqu'à 250 et non pas 256. Du coup le débordement aurait eu lieu toutes les millisecondes, il n'y aurait eu qu'un seul compteur externe timer0_millis.
- la routine d'interruption n'aurait qu'incrémenter une variable 32 bits au lieu de deux avec en plus un rattrapage toutes les 42 ou 43 appels
- la gestion de millis() aurait été inchangée
- cela aurait compliqué un peu micros() car il aurait fallu multiplier timer0_millis par 250, l'ajouter à TCNT0 puis multiplier le tout par 4. Mais le nombre d'appels à micros() est très faible devant l'appel de la routine d'interruption. La multiplication par 250 pourrait être faite par la multiplication ou sachant que 250, c'est 256-4-2 ce qui fait 2 décalages et des additions.
- il y aurait une correction eu à faire pour le PWM éventuel (gamme 0-250 au lieu de 0-256). Pour le PWM, on pourrait prendre la gamme 0-250 pour tous les PWM ou faire une correction comme elle est faire actuellement avec timer0_millis.

Actuellement j'estime que la routine d'interruption dure 6,2µs (8,063µs-1.88µs). c'est une approximation du temps manquant quand on envoie un train d'impulsions de 1,88µs fait avec le timer 1. Avec le programme Impulsions.ino, on obtient le chronogramme:

Pour chaque milliseconde, on a un état bas plus long pendant que le timer 0 met l'horloge à l'heure. Il dure toujours 8,188µs. C'est le cas si deux routines d'interruptions du timer 1 encadrent la routine du timer 0. Si le timer 0 déclenche pendant le temps libre entre deux routines du timer 1, le temps à l'état bas serait plus long. Mais avec des impulsions timer 1 de 1,88µs, la probabilité est faible.

Pour savoir combien de temps durerait une routine d'interruption qui ne ferait qu'incrémenter un long, il suffit de faire un programme passant son temps dans une fonction d'interruption qui incrémente un long (plus une impulsion pour voir et mesurer). Je propose le programme TempsIncrementationLong.ino. On obtient alors le chronogramme:

La durée de l'interruption est de 4,25µs auquel on peut soustraire 125ns pour les deux digitalWriteFast. La durée d'une interruption qui ne ferait qu'incrémenter un long est donc de 4,125µs. Quasiment la moitié!

 

Le programme mystère

Si vous avez tout compris, vous comprendrez sans doute pourquoi mon programme mystère n'affiche rien:

// Programme mystère
void setup()
{
  Serial.begin(115200);
}

void loop()
{
  // Sachant que Serial.println("tic tac"); prend environ 1/8ms
  // Qu'affiche ce programme?
  if (millis()%1024 == 42) Serial.println("tic tac");
  if (millis()%1024 == 42+43) Serial.println("tic toc");
  if (millis()%1024 == 42+43+42) Serial.println("tac toc");
}

 


dansetrad.fr Contactez-moi