Atmega328 registers

De The Linux Craftsman
Aller à : navigation, rechercher

Introduction

Lorsque l'on commence à écrire des programmes qui sortent de l'ordinaire ou que l'on veut pousser un microcontrôleur à la limite de ses capacités, il est obligatoire de descendre au niveau des registres. Seulement voilà, la manipulation de registres ne s'improvise pas, nécessite de connaître le hardware que l'on utilise et, de par sa nature, est spécifique à un type de microcontrôleur !

Manipuler les registres implique donc de sacrifier la portabilité du code, offerte par l'utilisation des fonctions haut niveau comme digitalWrite(), au profit de la vitesse d'exécution et de la compacité du code.

Les fonctions comme digitalWrite(), digitalRead(), pinMode() permettent au développeur d'accomplir une action sans avoir à se soucier du type de microcontrôleur utilisé mais, in fine, elles vont elles-mêmes manipuler les registres pour accomplir ces tâches !

Le cas de l'ATmega328

L'AtMega328 est le microcontrôleur utilisé sur les Arduino Uno, pro, nano et bon nombre de circuits électroniques d'autres marques ! Ci à droite, l'association entre le nom des broches et leurs positions dans le registre.

On peut remarquer que :

  • les broches D0, D1, D2, D3, D4, D5 et D6 font partie du groupe D (PD);
  • les broches D8, D9, D10, D11, D12 et D13 font partie du groupe B (PB);
  • les broches analogiques font partie du groupe C (PC).

A chacun de ces groupes on peut associer trois registres :

  • PORT → pour positionner l'état du port (HIGH ou LOW);
  • PIN → pour lire l'état d'un port;
  • DDR (Data Direction Register) → pour positionner le sens d'un port (INPUT, OUTPUT);

On se retrouve donc avec 9 registres manipulables:

  • PORTD, PIND et DDRD pour les broches D0, D1, D2, D3, D4, D5 et D6;
  • PORTB, PINB et DDRB pour les broches D8, D9, D10, D11, D12 et D13;
  • PORTC, PINC et DDRC pour les broches analogiques.

La position du bit dans le registre est déterminé par le numéro attenant à la lettre du groupe. Par exemple, si on prend le cas de la broche D13, c'est le 5ème bit du groupe B (PB5) Mapping D13 atmega328.png .

Atmega328 pin mapping.png

Comment manipuler un registre ?

Maintenant que l'on a identifié le groupe et la position du bit des broches dans les registres, nous pouvons passer à la manipulation !

Reprenons l'exemple de la broche D13. Pour modifier son état, il suffit de modifier la valeur de PORTB:

PORTB = B00100000; // pour mettre D13 à HIGH
PORTB = B00000000; // pour mettre D13 à LOW

Seulement voilà, on sait que sa position dans les registres correspond au bit numéro 6 mais on n'a absolument aucune idée de la valeur de PORTB au moment de la modification ! La manipulation précédente va certainement modifier l'état de D13 mais va aussi modifier l'état des autre broches du groupe B. On ne peut donc pas agir de la sorte, il faut utiliser un opérateur logique pour modifier les registres et on va distinguer deux cas de figures: le passage à 1 et celui à 0.

Passage à 1

Pour modifier l'état d'un registre à 1 sans modifier l'état des autres registres nous allons utiliser l'opérateur OU.

Si on reprend l'exemple précédent, nous allons plutôt faire cela:

PORTB |= B00100000; // pour mettre seulement D13 à HIGH

Imaginons que PORTB est la valeur suivante B00010101, PORTB |= B00100000 revient à faire:

0 0 0 1 0 1 0 1
OR 0 0 1 0 0 0 0 0

0 0 1 1 0 1 0 1

On a donc réussi à modifier la valeur de D13 sans modifier l'état des autres broches !

Table de vérité de OU

A

B

A OR B

0

0

0

0

1

1

1

0

1

1

1

1

On peut également effectuer cette manipulation par décalage de bit. Dans l'exemple précédent nous avons utilisé B00100000 pour faire le OU mais on aurait pu utiliser l'écriture (1<<5) ou même la constante prévue à cet effet (1<<PB5). Le chiffre 1 est un entier qui s'écrit en binaire comme cela:

position 7 6 5 4 3 2 1 0
valeur 0 0 0 0 0 0 0 1

Il nous suffit donc de le décaler de 5 positions vers la gauche pour obtenir B00100000:

position 7 6 5 4 3 2 1 0
valeur 0 0 1 0 0 0 0 0

Les trois écritures suivantes sont donc équivalentes:

PORTB |= B00100000;
PORTB |= (1<<5);
PORTB |= (1<<PB5);

Passage à 0

Pour modifier l'état d'un registre à 1 sans modifier l'état des autres registres nous allons utiliser l'opérateur ET.

Si on reprend l'exemple précédent, nous allons plutôt faire cela:

PORTB &= ~B00100000; // pour mettre seulement D13 à LOW

L'opérateur ~ permet d'obtenir le complément A1 d'un nombre binaire: B00100000 devient alors B11011111.

Imaginons que PORTB est la valeur suivante B00110101, PORTB &= ~B00100000 revient à faire:

0 0 1 1 0 1 0 1
AND 1 1 0 1 1 1 1 1

0 0 0 1 0 1 0 1

On a donc réussi à modifier la valeur de D13 sans modifier l'état des autres broches !

Table de vérité de ET

A

B

A AND B

0

0

0

0

1

0

1

0

0

1

1

1

On peut également effectuer cette manipulation par décalage de bit. Dans l'exemple précédent nous avons utilisé ~B00100000 pour faire le ET mais on aurait pu utiliser l'écriture ~(1<<5) ou même la constante prévue à cet effet ~(1<<PB5).. Les trois écritures suivantes sont donc équivalentes:

PORTB &= ~B00100000;
PORTB &= ~(1<<5);
PORTB &= ~(1<<PB5);

Bascule d'un bit

Pour basculer l'état d'un registre de 0 à 1 ou inversement, sans modifier l'état des autres registres, nous allons utiliser l'opérateur XOR.

Si on reprend l'exemple précédent, nous allons plutôt faire cela:

PORTB ^= B00100000; // pour basculer seulement D13

Imaginons que PORTB est la valeur suivante B00110101, PORTB ^= B00100000 revient à faire:

0 0 1 1 0 1 0 1
XOR 0 0 1 0 0 0 0 0

0 0 0 0 0 1 0 1

On a donc réussi à basculer la valeur de D13 sans modifier l'état des autres broches !

Table de vérité de XOR

A

B

A XOR B

0

0

0

0

1

1

1

0

1

1

1

0

On peut également effectuer cette manipulation par décalage de bit. Dans l'exemple précédent nous avons utilisé B00100000 pour faire le XOR mais on aurait pu utiliser l'écriture (1<<5) ou même la constante prévue à cet effet (1<<PB5).. Les trois écritures suivantes sont donc équivalentes:

PORTB ^= B00100000;
PORTB ^= (1<<5);
PORTB ^= (1<<PB5);

Modification de l'état d'une broche

Dans l'exemple suivant nous allons modifier l'état de la broche D13 pour la faire clignoter.

Modification de sens

Dans un premier temps il faut la placer en sortie et pour cela nous allons manipuler le registre DDRB:

  • la valeur 1 positionne la broche en sortie (OUTPUT);
  • la valeur 0 positionne la broche en entrée (INPUT), si l'état est positionné à 1 cela active la résistance de pullup (INPUT_PULLUP).

Nous allons donc modifier le registre DDRB comme suit, les trois notations sont équivalentes:

DDRB |= B00100000; // Positionnement de D13 en OUTPUT
DDRB |= (1<<5);
DDRB |= (1<<PB5);

Modification d'état

On peut maintenant modifier son état grâce au registre PORTB:

PORTB |= B00100000; // Positionnement de D13 à HIGH
PORTB |= (1<<5);
PORTB |= (1<<PB5);

Lecture d'état

Il est possible de lire l'état de la broche D13 grâce au registre PINB. Nous allons utiliser la même technique du masquage que précédemment avec l'opérateur ET. Imaginons que PINB est la valeur suivante B00110101, PINB &= B00100000 revient à faire:

0 0 1 1 0 1 0 1
ET 0 0 1 0 0 0 0 0

0 0 1 0 0 0 0 0

On va donc retirer l'état des autres broches sans modifier pour autant celui de la broche D13 ! Il ne nous reste plus qu'à décaler ce bit de 5 positions vers la droite pour pouvoir le lire.

Les deux écritures suivantes sont équivalentes:

uint8_t state;
state = (PINB & B00100000) >> 5;
state = (PINB & B00100000) >> PB5;

Rien n’empêche d'utiliser cela pour faire des tests:

// Modification de D13 en sortie
DDRB |= B00100000;
// Passage de D13 à HIGH
PORTB |= (1 << PB5);
if ((PINB & B00100000) >> PB5) {
    Serial.println("D13 is ON");
}
// Passage de D13 à LOW
PORTB &= ~(1 << PB5);
if ((PINB & B00100000) >> PB5 == 0) {
    Serial.println("D13 is OFF");
}

Lorsque l'on exécute le code suivant on s'aperçoit que rien ne s'affiche. La raison est simple, la modification de registre est tellement rapide que l'état électrique n'a pas le temps d'être modifié avant la lecture !

Il suffit d'ajouter:

delay(1);

après les lignes modifiants les états:

...
PORTB |= (1 << PB5);
...
PORTB &= ~(1 << PB5);
...

Performances

La véritable interrogation réside dans le fait de savoir si le jeu en vaut la chandelle. Est-il intéressant de sacrifier la simplicité d'écriture et la compréhension du code pour la performance ?

Pour faire un test probant, allumons la led présente sur D13 100.000 fois avec:

  • dans un premier temps pinMode() et digitalWrite();
  • dans un second temps DDRB et PORTB;

Voila le premier test:

#define LIMIT 100000
void setup() {
  Serial.begin(9600);
  int32_t timer = millis();
  pinMode(13, OUTPUT);
  for (uint32_t i = 0; i < LIMIT; i++) {
    digitalWrite(13, HIGH);
    digitalWrite(13, LOW);
  }
  int32_t elapsed = millis()  - timer;
  Serial.print("Temps d'exécution: ");
  Serial.println(elapsed);
}
void loop() {
}

Le croquis utilise 2138 octets et le résultat est:

Temps d’exécution: 709

Voila le deuxième test:

#define LIMIT 100000
void setup() {
  Serial.begin(9600);
  int32_t timer = millis();
  DDRB |= (1<<PB5);
  for(uint32_t i = 0; i < LIMIT; i++){
    PORTB |= (1<<PB5);
    PORTB &= ~(1<<PB5);
  }
  int32_t elapsed = millis()  - timer;
  Serial.print("Temps d'exécution: ");
  Serial.println(elapsed);
}
void loop() {
}

Le croquis utilise 1844 octets et le résultat est:

Temps d’exécution: 62

On peut constater que le code utilisant les registres est non seulement plus léger (13,67%) mais également plus rapide (11.42 fois) !