La fonction « blink » revisitée

Ou comment programmer directement les registres du microcontrôleur

Patrick BRIAND

Toutes les personnes qui se sont intéressées de près ou de loin à la programmation sur la carte Arduino ont commencé par regarder le code de l’exemple « blink ». Le code qui est fourni pour cet exemple parait simple, mais en réalité, il cache une certaine complexité derrière les appels aux fonctions fournies par l’environnement Arduino.

Commentez

- Titre : La fonction « blink » revisitée

- Auteur : Patrick BRIAND

- Parution : 19 juin 2024

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2024 Patrick BRIAND. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Le « blink » classique - avantages et inconvénients

- II-A. LED_BUILTIN

- II-B. Les fonctions pinMode() et digitalWrite()

- II-C. La fonction delay()

- II-D. Moralité

- III. Piloter directement la LED en utilisant les registres d’entrées-sorties digitales

- III-A. Généralités sur les ports de l’ATmega2560

- III-A-1. Le registre DDRB

- III-A-2. Le registre PORTB

- III-B. Fini les pinMode() et digitalWrite()

- III-C. Et si l’on simplifiait encore le code

- III-D. Conclusion

- IV. Créer une base de temps avec le TIMER0

- IV-A. Description du TIMER0

- IV-A-1. Le registre TCCR0A

- IV-A-2. Le registre TCCR0B

- IV-A-3. Le registre OCR0A

- IV-A-4. Le registre TIMSK0

- IV-B. Le nouveau code du « Blink »

- IV-C. Résultat

- V. Conclusion

I - Introduction

L’objectif de ce tutoriel est de réécrire cet exemple d’application en mettant en œuvre directement un port digital et en activant le périphérique Timer 8 bits pour créer une base de temps.

Les codes fournis dans ce tutoriel se basent sur les périphériques du microcontrôleur ATmega2560 équipant la carte Arduino Mega 2560.

Si vous souhaitez suivre les exemples fournis dans ce tutoriel pour d’autres types de cartes Arduino, vous devrez alors modifier le code qui est donné ici.

II - Le « blink » classique - avantages et inconvénients

Le code fourni en exemple est simple, mais pas dénué de multiples inconvénients. On retrouve dans ce code des appels aux interfaces logicielles pinMode() pour donner la direction de la broche LED_BUILTIN, l’interface digitalWrite() pour placer cette broche à l’état haut ou bas et une fonction delay() qui permet de temporiser. Pour mémoire, je vous redonne le code de cette application.

blink.ino

#include <Arduino.h>

// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
delay(1000);                       // wait for a second
digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
delay(1000);                       // wait for a second
}

II-A - LED_BUILTIN

Il est assez facile de savoir que la LED_BUILTIN est le connecteur 13 sur la carte Arduino. Mais il est un peu plus difficile de voir que ce connecteur 13 est en fait la broche 26 du microcontrôleur et que cette broche est définie comme la PIN 7 du Port B comme on peut le voir sur la partie de schéma ci-dessous :

Le petit problème que l’on voit aussi, c’est que cette broche a l’air d’avoir plusieurs fonctions différentes, et donc il est important de savoir que l’on ne pourra pas utiliser les fonctions alternatives à cette broche.

Pour votre information, le schéma de la carte Mega est téléchargeable ici.

II-B - Les fonctions pinMode() et digitalWrite()

Ma première interrogation arrive quand je me dis : « Mais que se cache-t-il derrière la fonction pinMode() ? » Heureusement pour moi, je peux aller directement voir le code de pinMode() dans l’environnement Arduino.

void pinMode(uint8_t pin, uint8_t mode)
{
uint8_t bit = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *reg, *out;

if (port == NOT_A_PIN) return;

// JWS: can I let the optimizer do this?
reg = portModeRegister(port);
out = portOutputRegister(port);

if (mode == INPUT) {
uint8_t oldSREG = SREG;
cli();
*reg &= ~bit;
*out &= ~bit;
SREG = oldSREG;
} else if (mode == INPUT_PULLUP) {
uint8_t oldSREG = SREG;
cli();
*reg &= ~bit;
*out |= bit;
SREG = oldSREG;
} else {
uint8_t oldSREG = SREG;
cli();
*reg |= bit;
SREG = oldSREG;
}
}

Comme je pouvais m’en douter, cette fonction est commune à toutes les broches de tous les ports digitaux du microcontrôleur, et donc il y a du boulot à faire, comme déterminer à quel port et à quelle broche du microcontrôleur correspond le connecteur 13 de la carte Arduino. Bien évidemment, ces calculs prennent du temps et ont une capacité de nuisance sur la performance de votre application.

Pour mesurer le temps d’exécution de cette fonction, j’utilise la PIN 10 de la carte Arduino que je positionne à 1 juste avant l’appel de la fonction et que je repositionne à 0 juste après son appel comme le montre le code suivant :

// Accès direct aux registres pilotant la PIN 10

// Positionne la pin 10 en sortie
#define SET_PIN10_DIRECTION()     (DDRB  |= _BV(PINB4))
// Positionne la sortie 10 à l’état haut
#define SET_PIN10_HIGH()          (PORTB |= _BV(PINB4))
// Positionne la sortie 10 à l’état bas
#define SET_PIN10_LOW()           (PORTB &= ~_BV(PINB4))

// Fonction d'initialisation
void setup() {

// Initialisation du port de mesure
SET_PIN10_DIRECTION();
SET_PIN10_LOW();

SET_PIN10_HIGH();
pinMode(LED_BUILTIN, OUTPUT);
SET_PIN10_LOW();
}

void loop() {
}

Dans ce code, j’utilise des macros pour piloter la PIN 10. Pour cela, les macros font des accès directs aux registres pilotant le PORT B. Je vous expliquerai plus en détail comment fonctionnent ces macros dans le chapitre suivant.

Avec un oscilloscope, je peux mesurer le temps que dure cette fonction. J’obtiens alors la mesure du temps d’exécution comme le montre cette capture d’écran :

En instrumentant le code, j’ai pu mesurer que cette fonction prenait 2,7 µs (microsecondes).

Vu la fonction pinMode(), je me dis que digitalWrite() doit avoir aussi la même complexité, et toujours grâce à mon environnement de développement préféré, je retrouve le code de cette fonction.

void digitalWrite(uint8_t pin, uint8_t val)
{
uint8_t timer = digitalPinToTimer(pin);
uint8_t bit = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *out;

if (port == NOT_A_PIN) return;

// If the pin that support PWM output, we need to turn it off
// before doing a digital write.
if (timer != NOT_ON_TIMER) turnOffPWM(timer);

out = portOutputRegister(port);

uint8_t oldSREG = SREG;
cli();

if (val == LOW) {
*out &= ~bit;
} else {
*out |= bit;
}

SREG = oldSREG;
}

On se retrouve dans la même situation, avec un code complexe et consommateur en temps CPU.

Toujours avec mon oscilloscope préféré et en utilisant le même principe de mesure, je peux constater que le temps d’exécution de cette fonction est de 5,58 µs.

II-C - La fonction delay()

La fonction delay(1000) permet d’attendre 1 seconde, et je me dis que pendant ce temps, le microcontrôleur ne va pas pouvoir faire grand-chose d’autre que d’attendre. Le code de cette fonction confirme mes doutes avec la magnifique boucle while.

void delay(unsigned long ms)
{
uint32_t start = micros();

while (ms > 0) {
yield();
while ( ms > 0 && (micros() - start) >= 1000) {
ms--;
start += 1000;
}
}
}

Comme je suis curieux de nature, j’ai jeté un œil dans le code qui se cache derrière la fonction micros().

unsigned long micros() {
unsigned long m;
uint8_t oldSREG = SREG, t;

cli();
m = timer0_overflow_count;
#if defined(TCNT0)
t = TCNT0;
#elif defined(TCNT0L)
t = TCNT0L;
#else
#error TIMER 0 not defined
#endif

#ifdef TIFR0
if ((TIFR0 & _BV(TOV0)) && (t < 255))
m++;
#else
if ((TIFR & _BV(TOV0)) && (t < 255))
m++;
#endif

SREG = oldSREG;

return ((m << 8) + t) * (64 / clockCyclesPerMicrosecond());
}

Je peux donc en déduire que cette fonction se base sur le Timer 8 bits (TIMER0) du microcontrôleur Atmel. Au moins, maintenant je sais que ce Timer n’est plus utilisable pour autre chose, comme la fonction Fast PWM qui va être décrite un peu plus loin dans ce tutoriel.

II-D - Moralité

L’exemple « Blink » fourni avec le package Arduino est un bon exemple pour débuter et prendre en main l’environnement de développement, mais je préfère l’oublier si je veux faire une application plus complexe et plus performante en termes de ressource CPU.

Si l’on souhaite comprendre l’impact de ces instructions sur l’occupation mémoire de ce code, je construis le code de cette application et j’obtiens le message suivant dans l’environnement Arduino :

Le croquis utilise 1540 octets (0%) de l'espace de stockage de programmes. Le maximum est de 253952 octets.
Les variables globales utilisent 9 octets (0%) de mémoire dynamique, ce qui laisse 8183 octets pour les variables locales. Le maximum est de 8192 octets.

Si on compile un croquis vide (fonction setup() et loop() vide), on obtient le résultat suivant :

Le croquis utilise 662 octets (0%) de l'espace de stockage de programmes. Le maximum est de 253952 octets.
Les variables globales utilisent 9 octets (0%) de mémoire dynamique, ce qui laisse 8183 octets pour les variables locales. Le maximum est de 8192 octets.

Donc, on peut en déduire que le code du blink a coûté 878 Octets de mémoire Flash et 0 octet de RAM. Les 662 octets de Flash d’un code vide correspondent au code nécessaire pour initialiser le microcontrôleur et gérer les deux fonctions setup() et loop().

Maintenant, dans le chapitre suivant, on va regarder comment se passer des fonctions pinMode() et digitalWrite().

III - Piloter directement la LED en utilisant les registres d’entrées-sorties digitales

Comme on va utiliser directement les périphériques de l’Atmel ATmega2560, la première chose à faire est de télécharger la datasheet que l’on peut trouver ici.

Il ne faut pas avoir peur des 435 pages, on ne va utiliser qu’une toute petite partie de ce microcontrôleur.

III-A - Généralités sur les ports de l’ATmega2560

La structure de commande d’une broche sur ce microcontrôleur est la suivante :

On voit beaucoup de choses sur ce graphique, mais voici ce qui est important pour nous dans ce tutoriel :

1. Un signal « PUD » vient activer ou désactiver une résistance de pull-up qui permet de forcer à l’état haut cette broche si elle n’est pas câblée. Ce bit PUD est piloté par le bit 4 du registre d’usage général MCUCR (MCU Control register).

2. On voit aussi un signal nommé « SLEEP » qui est utilisé lorsque l’on souhaite mettre le microcontrôleur en mode basse consommation. Ce signal est piloté par le bit 0 du registre SMCR (MCU Sleep Controller).

3. Pour finir, une broche est commandée par trois bits : le bit DDxn, le bit PORTxn et le bit PINxn. Tous les trois sont en fait des bits contenus dans les registres DDRx, PORTx et PINx ou x est le nom du port. Dans notre cas, nous utilisons la LED connectée sur le connecteur 13 de la carte Mega, ce qui correspond à la broche 7 du port B. Cela implique que x = B et que n = 7, et donc que l’on va devoir programmer les registres DDRB, PORTB et PINB.

Maintenant, on va regarder la description des registres utiles pour piloter notre sortie digitale.

III-A-1 - Le registre DDRB

Ce registre permet de configurer la direction des broches sur le port B et chaque broche du port peut être configurée indépendamment l’une de l’autre. Le registre DDRB est défini comme ceci :

Le bit DDB0 permet de configurer la broche 0 du port B, et ainsi de suite jusqu’à la broche 7.

Positionner le bit à 0 définit une broche en entrée, le mettre à 1 permet de la définir comme une sortie.

En regardant la valeur initiale du registre (ligne initial value), on peut voir que toutes les broches du PORT B sont en entrée par défaut.

Dans notre exemple, la fonction pinMode() va être remplacée par l’écriture d’un 1 dans le bit DDB7.

III-A-2 - Le registre PORTB

Le registre PORTB permet de lire ou d’écrire des états logiques sur les broches du PORTB. Comme pour la direction, le registre permet de lire ou écrire chaque broche indépendamment l’une de l’autre.

Comme dans le cas du registre DDRB, si l’on veut modifier l’état du connecteur 13 de la carte Mega, on va devoir écrire la valeur souhaitée dans le bit PORTB7.

III-B - Fini les pinMode() et digitalWrite()

En programmant directement le registre DDRB et PORTB, on va pouvoir se passer des fonctions pinMode() et digitalWrite(). Voici à nouveau le nouveau code modifié :

// Do not remove the include below
#include <Arduino.h>

#define SET_PIN13_DIRECTION()     (DDRB |= _BV(PINB7))
#define SET_PIN13_HIGH()          (PORTB |= _BV(PINB7))
#define SET_PIN13_LOW()           (PORTB &= ~_BV(PINB7))

#define WAITING_TIME                   1000  // 1 seconde

//The setup function is called once at startup of the sketch
void setup()
{
SET_PIN13_DIRECTION();  // La broche 7 du port B est en sortie ce qui est équivalent à pinMode(LED_BUILTIN, OUTPUT);
}

// The loop function is called in an endless loop
void loop()
{
// Mettre à 1 la broche 7 du port B = digitalWrite(LED_BUILTIN, HIGH);
SET_PIN13_HIGH();
delay(WAITING_TIME);

// Mettre à 0 la broche 7 du port B = digitalWrite(LED_BUILTIN, LOW);
SET_PIN13_LOW();
delay(WAITING_TIME);
}

Pour commencer, j’ai remplacé pinMode(LED_BUILTIN, OUTPUT) par une macro SET_PIN13_DIRECTION().

Cette macro permet la mise à jour du registre DDRB en mettant le Bit7 de ce registre à 1 afin que la broche 7 du port B soit une sortie. Dans l’exemple, on fait un OU logique avec 0x80 afin d’éviter de modifier les autres valeurs de direction du port. La valeur 0x80 est générée par la macro _BV(PINB7). La macro _BV(PINB7) correspond au code 1<<PINB7, et PINB7 valant 7, on obtient 1<<7 soit le bit 1 décalé sept fois vers la gauche, ce qui est équivalent à l’octet 0x80.

L’utilisation des masques est inutile dans cet exemple, mais c’est une bonne habitude à prendre pour éviter de casser la configuration du registre lorsque d’autres broches sont utilisées.

J’ai écrit aussi deux autres macros SET_PIN13_HIGH et SET_PIN13_LOW pour piloter la LED, ces macros vont donc remplacer l’appel des fonctions digitalWrite(). Rien qu’en lisant ce code, on voit déjà que le changement d’état de la LED va être beaucoup plus rapide.

J’ai aussi ajouté une constante nommée WAITING_TIME, mais ça, c’est juste pour mes yeux, car les magic numbers me piquent les yeux dans du code C (merci encore à Wikipédia pour sa définition du magic number).

Toujours en utilisant le même principe de mesure, je vais pouvoir mesurer le temps d’exécution de l’initialisation du connecteur 13 et son temps de commutation.

J’obtiens ces deux mesures, la première étant celle de l’initialisation, et la seconde celle de la commutation.

On voit ici que le temps d’exécution est de 254 ns, ce qui est bien inférieur à la méthode standard.

Je pense qu’à ce moment, certains lecteurs vont me dire : « On a utilisé le registre DDRB et PORTB, mais à quoi peut bien servir le registre PINB ? »

III-C - Et si l’on simplifiait encore le code

On va pouvoir simplifier le code en utilisant la fonctionnalité fournie par le registre PINB. Dans la datasheet du microcontrôleur, on peut lire : écrire un 1 logique dans PINBx effectue la commutation de la valeur de PORTBx, et ceci indépendamment de l’état de DDRBx.

Ce registre se configure comme les deux registres précédents et sa définition est :

Cela revient à dire que l’on peut faire commuter la sortie du connecteur 13 en écrivant un 1 dans le bit PINB7.

Le nouveau code va donc devenir :

// Do not remove the include below
#include <Arduino.h>

#define SET_PIN13_DIRECTION()       (DDRB |= _BV(PINB7))
#define TOGGLE_PIN13()              (PINB = _BV(PINB7))

#define WAITING_TIME                   1000  // 1 seconde
00
//The setup function is called once at startup of the sketch
void setup()
{
// La broche 7 du port B est en sortie ce qui est équivalent
// à pinMode(LED_BUILTIN, OUTPUT);
SET_PIN13_DIRECTION();
}

// The loop function is called in an endless loop
void loop()
{
// Change l'état de la broche 7 du port B
TOGGLE_PIN13();
delay(WAITING_TIME);
}

On a pu supprimer deux lignes de codes, c’est déjà bien.

Dans ce code, on voit que le Toggle se fait directement sans utiliser un masque. Le masque est inutile dans ce cas, parce que le registre ne prend en compte que l’écriture d’un 1, l’écriture d’un 0 étant sans effet.

III-D - Conclusion

Regardons maintenant l’impact de ces modifications sur l’occupation mémoire de notre microcontrôleur en compilant ce code.

Le croquis utilise 814 octets (0%) de l'espace de stockage de programmes. Le maximum est de 253952 octets.
Les variables globales utilisent 9 octets (0%) de mémoire dynamique, ce qui laisse 8183 octets pour les variables locales. Le maximum est de 8192 octets.

Par rapport au code classique du Blink (1 540 octets de Flash), on a gagné 726 octets de code. Ce nouveau code coûte donc 152 octets de mémoire Flash.

Et si maintenant on pouvait se passer de la fonction delay(WAITING_TIME).

IV - Créer une base de temps avec le TIMER0

Notre base de temps va se baser sur le TIMER0 de l’Atmel2560. C’est ce Timer qui est utilisé pour les fonctions delay() et millis(). Donc, si l’on reprogramme ce Timer pour notre utilisation, ces deux fonctions ne seront plus utilisables, mais c’est l’un des objectifs de ce tutoriel.

IV-A - Description du TIMER0

Ce Timer est un Timer 8 bits, comportant deux unités de comparaison. C’est un Timer d’usage général et il permet la génération de signaux de type PWM (Pulse Width Modulation). Son block diagram est le suivant :

Si l’on souhaite détailler un peu plus ce Timer, on peut distinguer plusieurs blocs dans ce diagramme.

- Le premier nommé « clock select » permet de gérer l’horloge qui va être utilisée par le Timer. Ce Timer peut utiliser l’horloge interne du microcontrôleur ou se baser sur une horloge externe. L’horloge interne est issue d’un prescaler qui permet de choisir la fréquence que l’on souhaite injecter.

- Le bloc « control logic » permet de choisir le mode de fonctionnement de ce Timer. Ces modes vont être décrits plus loin dans ce tutoriel.

- Le « Timer/counter » est le compteur principal, il compte ou décompte en fonction de sa configuration.

- On trouve ensuite deux registres de comparaison qui ont pour objectif de comparer le compteur avec les valeurs que l’on souhaite.

Ce Timer à quatre modes de fonctionnement :

1. Le mode normal : le compteur 8 bits compte tout simplement de 0 à 255, et lorsqu’il déborde (il doit prendre la valeur 256 qui ne rentre pas sur 8 bits), il revient à 0 et met à 1 le bit TOV. Ce bit peut être utilisé pour générer une interruption et il sera remis à zéro lorsque l’interruption nommée ‘TIMER0_OVF’ sera déclenchée ;

2. Le mode « CTC » (« Clear Timer On Compare ») : ce mode ressemble au mode normal, excepté que la valeur maximale du compteur est celle qui est contenue dans le registre de comparaison OCR0A. Une interruption « TIMER0_COMPA » pourra être déclenchée lorsque le compteur aura la même valeur que le registre de comparaison. Lorsqu’il y a égalité entre le compteur et le registre de comparaison, le compteur repasse à 0 (BOTTOM) ;

3. Le mode « Fast PWM » : ce mode de fonctionnement permet de générer des PWM avec des fréquences plus rapides que le mode « Phase Correct PWM » qui suit. Cette rapidité est due au fait que le compteur ne compte que dans un seul sens (incrémente ou décrémente au choix par configuration) tandis que dans le prochain mode, le compteur compte et décompte ce qui va diviser par deux la fréquence du PWM ;

4. Le mode « Phase Correct PWM » : dans ce mode, le compteur compte jusqu’à 255 (TOP) puis décompte jusqu’à 0 (BOTTOM) et recommence ce cycle. Ce mode de fonctionnement diminue la fréquence du PWM, mais augmente sa précision. L’égalité entre le compteur et le registre de comparaison va faire changer l’état de la sortie OCnX comme le montre le chronogramme suivant :

Pour notre exemple, on va utiliser le mode CTC pour générer une interruption toutes les 10 ms.

Pour configurer notre Timer, on va devoir configurer les registres de contrôle TCCR0A, TCCR0B, le registre de comparaison OCR0A, et pour finir, le registre TIMSK0.

IV-A-1 - Le registre TCCR0A

Ce registre ne contient que 6 bits à configurer, mais ce n’est pas aussi simple qu’on pourrait le croire. Notre chance, on n’utilise pas la fonction PWM et on n’utilise pas le comparateur B.

Le registre est défini comme ceci :

Les bits COM0A1 et COM0A2 possèdent un comportement différent pour chaque mode de fonctionnement du Timer. Comme on n’a pas de fonction PWM, on est dans le cas suivant :

Pour notre Timer, nous sommes dans le mode « Normal port operation, OC0A disconnected », ces deux bits prennent tous les deux la valeur 0 : facile !

Le comparateur B n’étant pas utilisé, c’est pareil, on trouve les valeurs 0 et 0 pour chacun des deux bits.

Il reste maintenant à régler les deux bits WGM01 et WGM02.

Le mode d’opération choisi est le CTC, et vu le tableau ci-dessus, on en déduit que WGM1 = 1 et WGM0 = 0.

Pour en finir avec ce registre, on va y écrire la valeur 0x02.

IV-A-2 - Le registre TCCR0B

Au premier coup d’œil, la programmation de ce registre ne parait pas simple non plus.

Les bits FOC0A et FOC0B servent à forcer la comparaison avec les comparateurs A et B, donc inutiles dans notre cas, ils restent à 0.

Le bit WGM02 est le troisième bit pour définir le mode d’opération du Timer comme on l’a vu dans le chapitre précédent. Pour le mode CTC, ce bit est à 0.

Reste maintenant le cas des bits CS02, CS01 et CS00. Ils servent à définir l’horloge qui va alimenter notre compteur (hé oui, il faut bien qu’il compte quelque chose ce compteur). Et c’est ici que l’on va devoir sortir notre calculatrice.

Notre objectif, obtenir une base de temps à 10 ms. Vous allez me dire pourquoi 10 ms ? Hé bien parce que ce compteur pourrait me servir dans une autre fonction comme ma fonction délais() à moi. Plus sérieusement, l’objectif est de déclencher une interruption, et une périodicité d’interruption trop courte peut poser de gros problèmes, comme une interruption qui retombe avant qu’elle ne soit traitée. Mais je vous rassure, ce ne sera pas le cas dans cet exemple.

Premier choix, on doit se baser sur la CLKio, car c’est la seule source d’horloge à notre disposition. Il faut maintenant choisir le prescaler (ou diviseur de fréquence). On sait que l’horloge est à 16 MHz, donc un tic d’horloge (ou cycle d’horloge, c.-à-d. le temps entre deux impulsions successives de l’horloge) vaut 62,5 ns (= 1/16.10^6, hé oui ça ne fait pas beaucoup, surtout quand c’est un Timer 8 bits). On peut faire un tableau avec les différents prescalers.

Prescaler

Valeur du tic

Valeur maximum de comparaison (8 bits)

1

0,0000625 ms

0,016 ms (256 x 0,0000625)

8

0,0005 ms

0,128 ms

64

0,004 ms

1,024ms

256

0,016 ms

4,096 ms

1024

0,064 ms

16,384 ms

On voit qu’un seul prescaler convient pour notre base de temps : 1024. Donc CS02=1, CS01=0 et CS00=1.

Ce registre prendra ainsi la valeur 0x05.

IV-A-3 - Le registre OCR0A

Ce registre va contenir la valeur de comparaison qui déclenchera l’interruption au bout de 10 ms, la question est : combien de tics d’horloge faut-il pour attendre 10 ms ? Le calcul se fait par la formule suivante :

- 10 ms / 0,064 ms = 156,25

Pas facile de faire rentrer le 0,25 dans le registre, on va devoir choisir entre 156 et 157.

- OCR0A = 156, soit une interruption toutes les 9,984 ms.

- OCR0A = 157, soit une interruption toutes les 10,048 ms.

Le plus précis étant 156, c’est la valeur que je vais retenir, et on aura un décalage de 0,016 ms sur les 10 ms souhaitées. Si l’on compte 100 interruptions successives pour attendre 100 x 10 ms = 1 seconde, la temporisation ne durera en fait « que » 998,4 ms, et l’état de la LED aura basculé 1,6 ms trop tôt en théorie, un écart que l’on pourra évidemment négliger ici. Mais voilà ce qui arrive quand on calcule avec des puissances de 2.

Remarque : si vous regardez par exemple la note sur la fonction millis() (millis() - Arduino Reference), il y est mentionné que millis() s’incrémente en fait toutes les 1,024 ms, et que tous les 41-42 tics d’horloge la valeur de millis() est compensée pour rattraper le décalage. L’écart n’est pas toujours négligeable sur les grandes durées.

IV-A-4 - Le registre TIMSK0

C’est notre dernier registre à prendre en compte, celui qui autorise les interruptions quand le comparateur est égal au compteur.

Comme on utilise le comparateur A, on n’autorise les interruptions que sur ce comparateur, et donc on place le bit OCIE0A à 1.

Le registre prendra ainsi la valeur 0x02.

IV-B - Le nouveau code du « Blink »

Voici le nouveau code de cette formidable application.

// Do not remove the include below
#include "Arduino.h"

#define SET_PIN13_DIRECTION()       (DDRB |= _BV(PINB7))
#define TOGGLE_PIN13()              (PINB = _BV(PINB7))

#define WAITING_TIME                100  // 1 seconde

// Configuration en mode CTC
#define TCCROA_CONFIGURATION        _BV(WGM01)

// Configuration en mode CTC
#define TCCROB_CONFIGURATION        _BV(CS00)|_BV(CS02)

// autorise les interruptions sur output compare A
#define OUTPUT_COMPARE_MATCH_A      _BV(OCIE0A)

#define OCR0A_LIMIT                 156

static uint16_t topCounter;

//The setup function is called once at startup of the sketch
void setup()
{
// PIN13 en sortie
SET_PIN13_DIRECTION();

/* TCCR0A – Timer/Counter Control Register A */
TCCR0A = TCCROA_CONFIGURATION;

/* TCCR0B – Timer/Counter Control Register B */
TCCR0B = TCCROB_CONFIGURATION;

/* OCR0A – Output Compare Register A */
OCR0A = 156;  // give a time of 9.984ms oups ! un Magic number

/* TIMSK0 – Timer/Counter Interrupt Mask Register */
TIMSK0 |= OUTPUT_COMPARE_MATCH_A;
TCNT0 = 0;

topCounter = 0;

}

// The loop function is called in an endless loop
void loop()
{
}

/*
* Interruption routine
*
*/
ISR(TIMER0_COMPA_vect)
{
topCounter++;
if (topCounter == WAITING_TIME) {
topCounter = 0;
TOGGLE_PIN13();
}
}

Le code a bien changé. La première remarque que l’on peut faire, c’est que la fonction loop() ne fait plus rien. Mais il ne faut pas la retirer sous peine d’avoir un message d’erreur provenant du linker Arduino.

La deuxième remarque, une nouvelle fonction vient de faire son apparition, celle qui commence par ISR (Interrupt Service Routine). Cette nouvelle fonction est celle d’interruption qui est appelée quand le compteur correspond avec le comparateur A.

IV-C - Résultat

Regardons maintenant l’impact de ces modifications sur l’occupation mémoire de notre microcontrôleur en compilant ce code.

Le croquis utilise 760 octets (0%) de l'espace de stockage de programmes. Le maximum est de 253952 octets.
Les variables globales utilisent 11 octets (0%) de mémoire dynamique, ce qui laisse 8181 octets pour les variables locales. Le maximum est de 8192 octets.

Par rapport au code classique du Blink (1 540 octets de Flash), on a gagné 780 octets de code. Ce nouveau code coûte donc 98 octets de mémoire Flash.

V - Conclusion

Pour finir ce tutoriel, je vais pouvoir faire une comparaison des différentes méthodes que j’ai utilisées. Le tableau récapitule les différentes méthodes et leurs impacts sur le temps d’exécution et d’occupation mémoire :

Temps d’exécution :

Méthode

Initialisation

Commutation

Mémoire Flash utilisée

Librairie Standard

2,7 µs

5,58 µs

878 octets

Accès direct aux registres

254 ns

254 ns

152 octets

Accès direct au Timer

254 ns

254 ns

98 octets

On peut en déduire que l’on peut diviser la taille du code et le temps d’exécution des appels par 10.

Dans le cas d’une application simple comme le Blink, on peut se permettre d’utiliser la librairie standard d’Arduino. Par contre, si vous souhaitez exploiter pleinement les capacités du microcontrôleur de cette carte, il est préférable de ne pas utiliser la librairie Arduino et de programmer directement les registres de ce microcontrôleur.

Dans le dialecte des programmeurs de code embarqués, la librairie Arduino est ce que l’on appelle un « Basic Software », c'est-à-dire une couche logicielle qui fait abstraction du matériel. Le codeur s’appuie donc dessus pour développer son application en faisant appel à ces fonctions de base. Cette méthode à l’avantage de simplifier le codage d’une application, par contre cette simplification se paye cash en taille mémoire et temps d’exécution.

Dans ce tutoriel, nous avons vu le fonctionnement des ports digitaux et en partie le fonctionnement du Timer 8 bits de ce microcontrôleur. Mais il existe bien d’autres sujets à explorer comme les PWM, convertisseur Analogique-Numérique, la SPI, l’UART, les Timers 16 bits et d’autres fonctionnalités encore.

Je tiens a remercier tout particulièrement f-leb pour son support, sa relecture et ses remarques judicieuses au sujet de ce tutoriel. Je n’oublie pas Delias pour ses conseils sur la macro _BV, Vincent PETIT pour ses remarques et escartefigue pour la relecture orthographique.