[ Arduino annexe D ] Les ports

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.

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 deux digitalWrite() 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 fonctions digitalWrite() 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…

  1. 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 simple digitalWrite(2) aurait fait l’affaire.
  2. 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 😀 )
  3. 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 :

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 :

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 :

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) :

Ensuite, au moment d’exécuter notre programme nous allons juste avoir à utiliser PORTD pour actionner les sorties. Par exemple en faisant :

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.

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) :

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 :

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 :

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. 😉

Une expérience (bonne ou mauvaise) à partager ? Des questions à poser ? Des conseils à donner ? Ne soyez pas timide !

17 commentaires

  1. on en arrive à de l’assembleur.
    il est possible d’inclure de l’assembleur dans le C++ D’arduino sans déclarer asm, puis de sauver les registres ?
    quelles sont les instructions assembleur concernées ?

  2. Salut ESKIMON,

    Je suis super débutant, et j’essaie de lire en utilisant le registre PINx les huit bit d’un port, mais impossible.

    En fait, j’aimerais recevoir sur 4 bit les codes envoyés par une télécommande.
    Je m’étais dit que lire d’un seul coup le port, puis masquer et comparer à mes codes de commande afin d’exécuter les actions correspondantes.
    Mais je n’arrive pas à lire avec PINx, il y a surement quelque chose qui m’échappe ?

    Lorsque j’essaie de faire afficher la valeur du port C avec PINC, j’obtiens toujours 0
    pourtant, si je lit mes entrées une par une avec DigitalRead, j’ai bien les bonnes valeurs.

    Voici mon code pour tester, je met à 1 certaines entrées en les reliant au 5V

    void setup() {
    Serial.begin(9600);
    DDRC = B00000000; //configure le port C en entrée
    }

    void loop() {
    Serial.print(PINC, BIN);
    delay(5000);
    }

    Merci de m’aiguiller, et désolé pour mon niveau …

    • C’est une bonne question… à laquelle je ne saurais pas répondre pour le moment. Je suis en congé, loin de mon matériel et comme j’aime bien faire des tests pour ne pas répondre à coté de la plaque je ne vais pas prendre de risque 😀 J’essaierai de revenir sur cette question quand je reviendrais chez moi 🙂

      • Pas de problème, pour l’instant, je m’en suis sorti avec çà :

        byte Code; // octet qui contient le code de la dernière commande reçue

        const byte BrocheData[] = {A0, A1, A2, A3}; // tableau qui contient les numéros de broches des entrées de télécommande

        void LireCode() { // cette fonction lit les 4 bits de data envoyés par la télécommande et les place dans Code
        Code = 0b00000000; // initialise à 0000 0000
        static byte i;
        for (i = 0; i < 4; i ++) {
        if (digitalRead(BrocheData[i])) {
        Code |= 1 << i;
        } // fin if
        else {
        Code &= ~(1 << i);
        } // fin else
        } // fin for
        } // fin LireCode

        Mais par souci d'efficacité, j'aurais quand même aimé lire ces 4 bits d'un seul coup, d'où mes essais malheureusement infructueux avec le registre PINC. Donc profite bien de tes vacances, et si en revenant, tu as une idée …

        • Bonjour denis,
          visiblement ton code est ok.
          as tu mis les resistanses de charges au +5v sur les entées?
          Si tu ne les mets pas, tu auras ce resultat.
          Attention tous les bits commancant par 0 ne sont pas afficher pour faire un essais positionnes une resistance de 10 kohms entre le + et A5 et la tous tes bits vont s’afficher

          • Merci Philippe, je teste çà dès que j’ai un peu de temps et je reviens pour les infos…

  3. Bonjour
    Super tuto mais je crois qu il y a une petite erreur dans le tuto juste au dessus du cadre mauve du DDRD ( attention liaison serie ) la fin devrait etre xxxxxx10 au lieu de xxxxx100 .

  4. Bonjour,
    Merci pour ce programme. Cela signifierait-il qu’on peut lire plusieurs entrées analogiques en même temps?
    Actuellement je lis trois entrées analogiques (retours capteurs) en env.300 µs et j’aurais aimé acquérir plus rapidement ces informations. Me conseilleriez-vous cela ?
    Bien cordialement

    • hello
      Isabelle
      non ce n’est pas possible
      le µ ne possede qu’un seul convertisseur analogique/numérique ( CAN). et un multiplexeur interne dirige une des 6 entrées analogiques d’un UNO vers la seule entrée du CAN
      donc, c’est chacune son tour. de plus la 1ere conversion est plus longue et risque d’etre faussée vers le haut.
      voir page 242 et 254 de la data sheet

Laisser un commentaire