Partons dans les entrailles du microcontrôleur en allant à la rencontre d’une mémoire un peu particulière : les registres. Dans notre cas nous allons parler des ports, un registre servant à gérer les entrées/sorties de la carte.
Sommaire
Utiliser les ports
Dans cette partie nous allons voir une technique avancée de programmation de microcontrôleur. Bien qu’elle soit sans danger pour votre matériel, une mauvaise manipulation concernant la liaison série peut arriver et apporter des complications au niveau du chargement du programme (nous allons voir pourquoi). Soyez donc prudent et codez en connaissance de cause. Bien entendu, je vais tout vous expliquer pour que vous réussissiez sans problèmes !
Retour aux sources
Vous vous souvenez, lorsque nous avons introduit la notion de "microcontrôleur" ? On a parlé de différentes choses qui se trouvaient à l’intérieur de ce composant et notamment des registres, en expliquant brièvement que ce sont des sortes de mémoires spécifiques… eh bien en voici un exemple complet : les ports . Ces derniers sont en effet des bits au sens le plus strict qui soit puisqu’ils traduisent directement l’état et le comportement des sorties. Ces bits servent donc à configurer vos entrées/sorties et à lire/écrire dessus. Ils sont accessibles directement et ont un effet immédiat sur l’électronique de votre microcontrôleur. En clair, c’est passer au travers de fonctions déjà toutes prêtes pour appliquer directement ce que vous voulez faire avec votre microcontrôleur. Cela revient à rentrer dans le détail et à coder façon "bas niveau". Par exemple, lorsque vous direz "mets ce bit à 1 (ou 0)" vous irez en fait activer (ou désactiver) un transistor à l’intérieur du microcontrôleur, ce qui aura pour effet de basculer la sortie à l’état haut ou bas. Lorsque vous utilisez les fonctions pinMode() ou digitalRead() ou digitalWrite(), sans le savoir vous utilisez en fait les registres en question. La seule différence est que certaines opérations de contrôle sont faites pour s’assurer que vous ne fassiez pas trop de bêtises.
Des bêtises ? des vérifications ? je vais casser quelque chose ?
Non rassurez-vous .
Les avantages d’utiliser les ports
Lorsque l’on décide d’utiliser les ports plutôt que les fonctions d’abstractions digitalWrite(), on le fait souvent avec une bonne raison (et vous comprendrez pourquoi en lisant les inconvénients). Voici deux grandes raisons qui pourraient vous inciter, dans des cas précis, à vouloir utiliser les ports :
-
Dans le cas où vous rédigez une application qui possède un côté critique au niveau du temps (par exemple la broche 3 et la
broche 4 doivent basculer de manière synchrone sinon le message ne passe pas), l’utilisation des ports est faite
pour vous ! En effet, comme dit plus haut contrairement à
digitalWrite()
, ici vous agissez directement sur le matériel, sans abstraction. Du coup ça va plus vite ! (là ou il pourrait s’écouler quelques précieuses micro-secondes entre deuxdigitalWrite()
consécutifs) -
Si votre code rentre au chausse pied dans le microcontrôleur et que gagner quelques octets de mémoire programme vous arrangerait
bien, là encore jouer avec les registres est une solution. En effet, chaque appel à
digitalWrite()
représente plus d’instructions machines qu’un appel direct au port. Au bout d’un code assez conséquent, il peut donc s’avérer utile de passer par les ports pour sauver quelques octets à chaque fois (en particulier si les fonctionsdigitalWrite()
sont très souvent utilisées à plein d’endroits différents).
Les risques et inconvénients à connaître
Vous avez décidé que finalement utiliser les ports c’était pas si mal, et en valait la peine ? Voyons pourquoi vous feriez-mieux d’y réfléchir une deuxième fois…
-
Inconvénient n°1 :
Votre code va devenir difficile à relire. En effet, utiliser les ports nécessite d’écrire
des lignes du genre
PORTD |= B00000100;
là où un simpledigitalWrite(2)
aurait fait l’affaire. -
Inconvénient n°2 :
Votre code devient moins portable. Les registres utilisés peuvent changer (de nom,
d’adresse, etc…) entre les gammes de microcontrôleur. D’ordinaire, c’est la fonction
digitalWrite()
qui justement s’assure que tout est bien fait. En écrivant directement dans les registres, vous court-circuitez les vérifications et donc vous devez vous assurer vous-même de bien écrire pour activer les bonnes entrées/sorties (sous peine d’activer la mauvaise I/O ou même de ne pas compiler tout court ) - Inconvénient n°3 : Vous allez vous arracher les cheveux ! Lorsqu’on joue avec les ports, on doit faire des opérations en binaire avec des masques. Il est très rapide de faire une erreur et de ne plus rien comprendre. La plus triste d’entre elles serait de bidouiller les broches utilisées par la liaison série et de se retrouver sans liaison série (ce qui d’ailleurs pourrait rendre difficile la reprogrammation de la carte…)
Mais, si vous ne manquez pas de rigueur, vous trouverez toutes les informations nécessaires relatives au microcontrôleur que vous utilisez. Donc le risque est amoindri et vous savez ce que vous faites.
OK j’ai bien compris tout ça et c’est bien gentil, mais concrètement on fait quoi ?
Concrètement, vous allez agir sur 3 données particulières qui s’appellent DDR, PORT ou encore PIN. Voyons maintenant à quoi elles servent et comment nous allons nous en servir…
Utilisation des ports
Vous êtes encore là ? Vous n’avez pas froid aux yeux, j’aime ça ! (ou simplement curieux ou inconscient, ça marche aussi ). Puisque vous êtes là, continuons, voyons voir les étapes nécessaires à cette utilisation mystique des broches… Tout d’abord, sachez qu’il y a trois registres à manipuler pour réussir à se servir des broches. Un premier registre, DDR , servira "d’aiguillage". Ensuite deux autres serviront à lire ou écrire sur une broche, ce sont PORT et PIN . Dans une Arduino Uno, ces trois registres existent en trois exemplaires, le B, le C et le D. B sert pour les broches numériques 8 à 13, C pour les broches analogiques et D pour les broches 0 à 7.
Le registre DDRx
Comme je viens de le dire, DDR sert d’aiguillage. C’est lui qui permet de définir l’utilisation d’une broche en entrée, ou
en sortie, tout comme le faisait la fonction pinMode(). Un 0 signifie "entrée" et un 1 signifie "sortie". Chaque broche
d’un même port étant commandée par ce registre. Au début de votre programme, vous avez la fonction
pinMode()
pour définir l’utilisation des broches. Admettons que vous souhaitiez utiliser les broches 4, 5 et 6 en sortie et aussi
13 et 11 ainsi que les broches analogiques 2 à 4. Pour cela, vous auriez d’ordinaire écrit :
void setup()
{
//définition des broches suivantes utilisées en sortie logique
pinMode(4, OUTPUT);
pinMode(5, OUTPUT);
pinMode(6, OUTPUT);
pinMode(11, OUTPUT);
pinMode(13, OUTPUT);
pinMode(A2, OUTPUT);
pinMode(A3, OUTPUT);
pinMode(A4, OUTPUT);
}
Plutôt long et fastidieux non ? Avec les ports tout cela tiendra en 3 lignes, une par port. Pour cela, imaginez que les entrées/sorties soient représentées chacune par un bit dans un octet. Par exemple pour le registre D qui représente les entrées/sorties de 0 à 7, on pourrait avoir 00110010. Les numéros des broches sont ensuite les mêmes que ceux des bits, le poids faible sera la broche 0 et le poids fort la broche 7. Donc pour mettre les broches 4, 5 et 6 en sortie (donc les autres en entrée) on fera :
PORTD = B01110100;
Le B devant la suite de 0 et 1 signifie que l’on donne un nombre binaire. On pourrait également donner un nombre hexadécimal
en mettant un
0x
suivi de deux caractères hexadécimaux. Dans la même lignée, nous pouvons donc déclarer l’utilisation de toutes les entrées/sorties
précédentes :
DDRD = B01110100; // 0 (poids faible) vers 7 (poids fort)
DDRC = B00011100; // A0 (poids faible) vers A5 (poids fort)
DDRB = B00101000; // 8 (poids faible) vers 13 (poids fort)
Le registre D possède l’accès aux broches de la transmission série. Je vous recommande très fortement de toujours laisser ces derniers à 1 (pour le Tx) et 0 (pour le Rx), pour faire DDRD = xxxxxx10 si vous ne voulez pas avoir de mauvaises surprises…
Le registre PORTx
C’est maintenant que l’on s’amuse . En effet, c’est ici que l’utilisation des ports va prendre tout son sens… Imaginons que vous vouliez basculer à l’état haut toutes les sorties 2, 3, 5 et 7 en même temps . Vous pourriez utiliser plusieurs fois digitalWrite(x, HIGH) mais il risque de se passer quelques microsecondes entre chaque action. Nous allons donc utiliser le registre PORT pour faire cette opération. Tout d’abord, on définit les broches en sortie (encore une fois, attention aux deux derniers bits pour la liaison série) :
void setup()
{
DDRD = B11111110; // Tout le monde (de 1 à 7) en sortie !
}
Ensuite, au moment d’exécuter notre programme nous allons juste avoir à utiliser PORTD pour actionner les sorties. Par exemple en faisant :
PORTD |= B10101100; //2, 3, 5 et 7 à l'état haut
Avez-vous remarqué ? J’ai utilisé l’opérateur OU (|) pour actionner uniquement les sorties qui m’intéressaient tout en ignorant les autres. Cela doit vous rappeler des opérations de masquage que l’on avait vu un peu plus tôt, dans un chapitre précédent. Maintenant, je vais au contraire utiliser l’opérateur antagoniste ET (&) pour mettre à l’état bas les sorties précédentes.
PORTD &= B01010011; //2, 3, 5 et 7 à l'état bas
Je mets ainsi à zéro les broches qui m’intéressent en ignorant l’état des autres. C’est en tout point l’inverse du précédent ! Avec les ports (et les registres en général) les opérateurs de masquage ET et OU sont très importants à connaître et à maîtriser.
Le registre PINx
Bon c’est chouette, vous savez modifier toutes les broches en une seule fois, mais savez-vous les lire en une seule fois ? Pour cela, il faut d’abord mettre quelques broches en entrée pour qu’elles puissent être lues. Je vous propose de reprendre celles de tout à l’heure (2, 3, 5 et 7) :
void setup()
{
DDRD = B00000010; // Tout le monde (de 2 à 7) en entrée !
}
Ensuite, pour lire les données présentes sur ce port, il faut utiliser le dernier registre de cette leçon qui s’appelle PIN. Par exemple, pour stocker l’état des sorties dans un octet on fera :
char lePort = PIND;
C’est cool mais moi je ne veux qu’une certaine broche dans ma variable, comme avec digitalRead() !
Et à votre avis ? on fait quoi ? Et bien oui, on masque ! Pour obtenir la broche 3 uniquement par exemple, on fera :
char monEntree = PIND & B00001000; //sélectionne le bit 3 uniquement
Et c’est tout !
En fait c’est tout c’est vite dit. Le monde des registres est assez immense et important dans les microcontrôleurs. Et ceux que l’on vient de voir font partie des choses les plus simples à utiliser. Libre à vous avec vos nouvelles connaissances de continuer à en découvrir toujours plus.