Outil pédagogique rapide pour Arduino

Une carte Arduino dispose d’un port série. Ce port série peut être utilisé pour piloter un petit interprète de commande interactif permettant d’illustrer les différentes possibilités offertes à un développeur pour l’usage des différents types d’entrées/sortie disponible dans un micro-contrôleur simple typique.

On dispose, en général :

  • d’entrée TOR (Tout Ou Rien) quelque fois nommées entrées digitales
  • de sortie TOR (Tout Ou Rien) quelque fois nommées sorties digitales
  • d’entrée Analogiques utilisées pour la mesures de tensions électriques variables
  • de sorties Analogiques utilisées pour produire des tensions électriques variables

Entrées digitales

Elles permettent de distinguer deux niveaux de tension électrique. Elles sont souvent utilisées pour superviser des détecteurs dont la sortie est fournie sous la forme d’un contact ouvert ou fermé. De tels détecteurs sont très courant dans les automatismes et la domotiques :

  • détecteur de présence,
  • détecteur de mouvement,
  • détecteur d’ouverture de fenêtre ou de porte,
  • détecteur de température,
  • détecteur de niveau,
  • détecteur de fin de course,
  • boutons poussoir,
  • interrupteurs,
  • etc.

Les entrées TOR des micro-contrôleurs possèdent une résistance de tirage interne configurable permettant de transformer la sortie contact d’un détecteur en deux niveaux de tension.

Sorties digitales

Elles permettent typiquement de piloter des relais, des voyants, des moteurs pas à pas à travers l’utilisation d’interfaces de puissances (relais électromécaniques ou électroniques).

Entrées analogiques

Elles permettent la numérisation d’un signal représenté par une tension électrique variable. Cette numérisation permet le traitement numérique de ce signal.

Cette numérisation est réalisée par un circuit électronique spécial : un Convertisseur Analogique Numérique (Analog Digital Converter — ADC — en anglais). Le cas le plus répandu d’usage d’un tel convertisseur est le traitement du signal sonore fourni par un microphone (le microphone d’un PC).

Sorties analogiques

Elles permettent de transformer un signal numérique en signal analogique sous la forme d’une tension électrique variable. Cette transformation utilise un circuit électronique spécial : un Convertisseur Numérique Analogique (Digital Analog Converter — DAC — en anglais). Le cas le plus répandu d’usage d’un tel convertisseur est la synthèse du signal sonore fourni aux haut-parleurs d’un PC).

On trouve également de “fausses” sorties analogiques (sans connotation péjoratives). Elles sont apparues il y a fort longtemps lorsqu’on a eu besoin de faire varier la tension électrique de commande des moteurs électriques à courant continu puis, plus tard, lorsqu’on a en besoin de faire varier l’intensité lumineuse fournie par les ampoules électriques et, beaucoup plus tard, les Leds.

La réalisation des amplificateurs électroniques de puissance est très complexe et ne permet pas d’atteindre les puissances nécessaires. Commuter une tension élevées n’est pas trop compliqué et en tout cas plus facile que réaliser un amplificateur.

Les ingénieurs ont alors remarqué que la rotation d’un moteur électrique, surtout s’il est puissant, est caractérisée par une inertie importante. Si la tension de commande du moteur est commutée à une fréquence assez élevée, le moteur ne “verra” que la valeur moyenne de la tension fournie. La commande du moteur va utiliser une technique connue sous le nom de Modulation en Largeur d’Impulsion (Pulse Width Modulation en anglais).

La plupart des micro-contrôleurs disposent de sorties numériques qui peuvent être pilotées par une modulation en largeur d’impulsion. On appellera, par abus de langage, de telles sorties : “sorties analogiques”.

Entrées/Sorties d’une carte Arduino

Une carte Arduino dispose des périphériques suivants:

  • 12 entrées/sorties numériques: pins 2 à 13
  • 6 entrées analogiques: pins A0 à A5
  • 6 sorties “analogiques” PWM: pins 3, 5, 6, 9, 10, 11

… et de ports de communications : I2C, SPI, série qui ne sont pas décrits ici.

Dans un but pédagogique ou de démonstration, il est intéressant de montrer très rapidement et sans programmation le rôle que peut jouer chaque entrées/sortie. C’est de cette remarque qu’est venue l’idée de réaliser une commande interactive de ces entrées/sorties. Cette commande n’est pas une simulation graphique, elle doit permettre de commander une carte réelle.

Un petit langage de commande

La première étape est la définition d’un langage de commande et de sa syntaxe:

  • commande d’une sortie: “set type numéro valeur”
  • lecture d’une entrée: “get type numéro”

Le “type” peut être “analog” ou “digital”.

Un interprète de ce langage va être développé et implanté dans une carte Arduino. Un terminal va être connecté au port série de cette carte et va être utilisé “en ligne de commande”.

Une session sur l’écran du terminal ressemblera à :

***** Starting dialog
  usage:
    set analog/digital (2..13)/(3,5,6,9,10,11) value(CR)
    get analog/digital (0..5)/2..13)(CR)
    help: this information 
> set analog 3 200
  OK
> set digital 3 1
  OK
> set digital 3 0
  OK
> get analog 0
  369
> get digital 3
  0
> get analog 3
  176
> set analog 3 180
  OK
> get analog 3
  195
> get digital 2
  0
> get analog 0
  173
> get analog 1
  162
> get analog 2
  159
> get analog 3
  173
> 

pc-arduino-oscillo
En guise de terminal, on utilise un PC, le logiciel GtkTerm (émulateur de terminal) et un convertisseur USB-serial pour émuler un port série de PC connecté aux pins 0-Rx et 1-Tx de la carte Arduino. Un oscilloscope permet de visualiser l’effet des différentes commandes.

pc

La commande “set analog 3 127” programme une modulation de largeur d’impulsion sur la sortie n°3 de la carte Arduino avec un rapport cyclique de 0,5 (127/255). C’est bien ce que l’on peut voir sur l’affichage de l’oscilloscope ci-dessous.

oscillo

Interprète de ce langage

Un gros défaut de la plupart des exemples Arduino est qu’ils sont bloquant, on ne peut pas faire autre chose que ce pourquoi ils ont été développé. Ce défaut provient du fait que lorsqu’une boucle d’attente est nécessaire, elle est implémentée de façon active, on ne peut donc rien faire pendant qu’on attend !

La fonction delay() est un parfait exemple de cette façon de procéder. Qui a cependant le mérite de la simplicité.

Cet interprète va être réalisé de telle sorte qu’il ne soit pas bloquant.

Tous les interprètes informatiques interactifs ont la même structure : une boucle Read-Eval-Print:

  • Read : lire de la commande à exécuter,
  • Eval: analyser la commande et exécuter l’action demandée,
  • Print: afficher le résultat obtenu.

La structure standard est bloquante à cause de read():

void loop() {
  read();    // wait for a full command (a plain line)
  eval();    // parse the command and execute the action
  print();   // display the result
}

La structure suivante ne l’est pas:

void loop() {
  if(read()) { // a non-blocking read()
               // a plain command has been typed (a full line)
    eval();    // parse the command and execute the action
    print();   // display the result
  }
  .... 
}

Read

Quelques remarques pour expliquer la fonction read():

  • la fonction read() rend 0 si aucune commande n’est à traiter et 1 sinon. Elle n’est donc pas bloquante.
  • la méthode Serial.available() rend 0 si aucun caractères n’a été tapé. Elle permet d’échapper immédiatement de la fonction read()
  • la fin de ligne n’est pas signalée de la même façon selon les différents environnements d’exécution de l’émulateur de terminal (Linux: LF(\n), Windows: CR(\r)+LF(\n), ou Mac OS: CR(\r)). Il faut donc prévoir qu’une ligne peut se terminer par CR ou LF.
  • le tampon inpbuf[] est remplis au fur et à mesure que des caractères sont détectés. Le contenu de ce tampon va constituer la commande à analyser et à exécuter par la fonction eval()
int read(void) {
  if (Serial.available() > 0) {         // a non-blocking read !
    static int i = 0;
    ib = Serial.read();
    Serial.write(ib);                  // echoing the received character
    if ((ib != '\r')&&(ib != '\n')) {
      inpbuf[i++] = ib;
      inpbuf[i]   = '\0';
      return 0;                       // no new input line ready
    } else {
      ib = '\0';                      // to reset the character input buffer
      i  = 0;                         // to rewind the input line buffer
      return 1;                       // a full new line is ready
    }
  }
  return 0;
}

Eval

void eval(void) {
  opCode[0] = '\0';
  type[0]   = '\0';
  
  var = 0;
  val = 0;
  sscanf(inpbuf,"%s %s %d %d", opCode, type, &var, &val);
  if(!strcmp(opCode, "set")) {
    if(!strcmp(type, "digital")) {
      if((var >=2)&&(var <=13)) { digitalWrite(var,val); strcpy(outbuf, "OK"); } else { strcpy(outbuf, "PORT not writable"); } } else if (!strcmp(type, "analog")) { if((var == 3)||(var == 5)||(var == 6)||(var == 9)||(var == 10)||(var == 11)) { analogWrite(var,val); strcpy(outbuf, "OK"); } else { strcpy(outbuf, "PORT not writable"); } } else { strcpy(outbuf, "Type unknown"); } } else if (!strcmp(opCode, "get")) { if(!strcmp(type, "digital")) { if ((var >=2)&&(var <=13)) {
        sprintf(outbuf, "%d", digitalRead(var));
      } else {
        strcpy(outbuf,"digital input number out of range.");
      }
    } else if (!strcmp(type, "analog")) {
      if (var < 6) {
        sprintf(outbuf, "%d", analogRead(var));
      } else {
        strcpy(outbuf,"analog input number out of range.");
      }
    }
  } else if (!strcmp(opCode, "help")) {
    Serial.print("\n  usage:\n");
    Serial.print("    set analog/digital (3,5,6,9,10,11)/(2..13) (0..255)/(0,1)(CR)\n");
    Serial.print("    get analog/digital (0..5)/2..13)(CR)\n");
    Serial.print("    help: this information");
  }
}

Print

void print(void) {
  Serial.print("\n  ");
  Serial.println(outbuf); // display the output result
  Serial.print("> ");     // display the prompt
}

Le code complet

/***
 * ReadEvalPrint loop.
 *
 *  author:    Jean DEMARTINI
 *  date:      2016.01.08
 *  copyright: SoFAB - Telecom Valley
 *
 *  This piece of software is open source and distributed under the GPL licence.
 *
 *  a very simple command-line demonstration tool for Arduino.
 * 
 *  syntax of commands:
 * 
 *  set type num value(CR)
 *  
 *  with:
 *    type:   digital / analog
 *    num:    2..13 for digital / 3,5,6,9,10,11 for analog (PWM)
 *    value:  0,1 for digital / 0..255 for analog (PWM)
 *    
 *  get type num(CR)
 *  
 *  with:
 *    type:   digital / analog
 *    num:    2..13 for digital / 0..5 for analog
 *    
 *  help
 *    
 *  WARNING:
 *  this sketch is designed to operate via the serial port
 *  of the Arduino only (pin 0/TX and pin 1/RX).
 *  
 *  This port is active only if the USB cable is disconnected.
 *  Keeping USB cable and Serial port connexion disturbs all
 *  the functionnalities linked to the USB link (no upload possible
 *  for instance). It seems that the serial port is used for many
 *  conflictual functionnalities.
 *  
 *  The monitor provided by the IDE Arduino is NOT a terminal, its 
 *  mode of operations split the usage of the Tx line and the usage
 *  of the Rx line. Then this application is not designed to function
 *  with the arduino monitor but with a "true" terminal:
 *    - Linux: gtkterm
 *    - Windows: TeraTerm (for instance)
 *    
 *  WARNING:
 *    PWM analog outputs are not readable.
 *    
 *    There is no real edition possibilities when typing a command. In case of
 *    mistyping return and retype the command.
 *    
 *    To use this application it is necessary:
 *      - to flash the arduino
 *      - then to disconnect the USB cable
 *      - then to connect the serial port
 *      - then to reset the arduino board
 */
 
char ib;              // incoming serial character
char inpbuf[64];      // incoming buffer
char outbuf[64];      // displaying buffer

char opCode[10];      // command code
char type[10];        // type of input/output : digital/analog
int  var;             // input/output number
int  val;             // value affected to the output or read from the input

int read(void) {
  if (Serial.available() > 0) {         // a non-blocking read !
    static int i = 0;
    ib = Serial.read();
    Serial.write(ib);                  // echoing the received character
    if ((ib != '\r')&&(ib != '\n')) {
      inpbuf[i++] = ib;
      inpbuf[i]   = '\0';
      return 0;                       // no new input line ready
    } else {
      ib = '\0';                      // to reset the character input buffer
      i  = 0;                         // to rewind the input line buffer
      return 1;                       // a full new line is ready
    }
  }
  return 0;
}

void eval(void) {
  opCode[0] = '\0';
  type[0]   = '\0';
  
  var = 0;
  val = 0;
  sscanf(inpbuf,"%s %s %d %d", opCode, type, &var, &val);
  if(!strcmp(opCode, "set")) {
    if(!strcmp(type, "digital")) {
      if((var >=2)&&(var <=13)) { digitalWrite(var,val); strcpy(outbuf, "OK"); } else { strcpy(outbuf, "PORT not writable"); } } else if (!strcmp(type, "analog")) { if((var == 3)||(var == 5)||(var == 6)||(var == 9)||(var == 10)||(var == 11)) { analogWrite(var,val); strcpy(outbuf, "OK"); } else { strcpy(outbuf, "PORT not writable"); } } else { strcpy(outbuf, "Type unknown"); } } else if (!strcmp(opCode, "get")) { if(!strcmp(type, "digital")) { if ((var >=2)&&(var <=13)) {
        sprintf(outbuf, "%d", digitalRead(var));
      } else {
        strcpy(outbuf,"digital input number out of range.");
      }
    } else if (!strcmp(type, "analog")) {
      if (var < 6) { sprintf(outbuf, "%d", analogRead(var)); } else { strcpy(outbuf,"analog input number out of range."); } } } else if (!strcmp(opCode, "help")) { Serial.print("\n usage:\n"); Serial.print(" set analog/digital (3,5,6,9,10,11)/(2..13) (0..255)/(0,1)(CR)\n"); Serial.print(" get analog/digital (0..5)/2..13)(CR)\n"); Serial.print(" help: this information"); } } void print(void) { Serial.print("\n "); Serial.println(outbuf); // display the output result Serial.print("> ");     // display the prompt
}

void setup() {
  Serial.begin(115200);     // opens serial port, sets data rate to 115200 bps
  //  display the invite and the prompt
  Serial.print("\n***** Starting dialog\n");
  Serial.print("  usage:\n");
  Serial.print("    set analog/digital (2..13)/(3,5,6,9,10,11) value(CR)\n");
  Serial.print("    get analog/digital (0..5)/2..13)(CR)\n");
  Serial.print("    help: this information\n> ");
  strcpy(outbuf,"");

  int i;
  for ( i=0 ; i