Pilotage d’une LED RVB par la liaison Série

L’objectif est ici de faire interagir une carte Arduino et une application C#. Les échanges entre l’application et la carte Arduino s’effectuent via la liaison série. Le programme C# permet de sélectionner et d’ouvrir le port série de la carte Arduino. Ensuite le déplacement des curseurs de l’application envoie des messages sur la liaison série. Messages qui sont interprétés par la carte Arduino afin de commander l’intensité lumineuse l’une Led RVB.

câblage

La plus grande broche de la LED correspond à la cathode à relier à la masse

De gauche à droite

  1. Anode Bleu (+)
  2. Anode Vert (+)
  3. Cathode commune (-)
  4. Anode Rouge (+)
1234

Programme Arduino

Le programme consiste à lire les données reçues sur la liaison série, d’en extraire les données concernant la commande des leds. Le pilotage des leds à proprement parlé s’effectue à l’aide des instructions analogWrite.

Le programme est le suivant :

#define ROUGE 3
#define GND 4
#define VERT 5
#define BLEU 6
void setup() {
  // put your setup code here, to run once:
  pinMode(GND, OUTPUT);
  digitalWrite(GND, LOW);
  Serial.begin(115200);

}

void loop() {
  if (Serial.available() > 0)
  {
    byte valRouge = Serial.parseInt();
    byte valVert = Serial.parseInt();
    byte valBleu = Serial.parseInt();
    while(Serial.available() > 0) Serial.read();
    analogWrite(ROUGE, valRouge);
    analogWrite(VERT, valVert);
    analogWrite(BLEU, valBleu);
  }

}

 

programme C#

l’application ressemble à ceci :

Le code final est le suivant :

using System;
using System.Windows.Forms;
using System.IO.Ports;

namespace pilotage_led_RVB
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (serialPort1.IsOpen)
            {
                button1.Text = "Connexion";
                serialPort1.Close();
                trackBar1.Enabled = false;
                trackBar2.Enabled = false;
                trackBar3.Enabled = false;
            }
            else {
                button1.Text = "Déconnexion";
                serialPort1.PortName = comboBox1.SelectedItem.ToString();
                serialPort1.Open();
                trackBar1.Enabled = true;
                trackBar2.Enabled = true;
                trackBar3.Enabled = true;
            }
            
        }

        private void trackBar1_Scroll(object sender, EventArgs e)
        {
            serialPort1.WriteLine(trackBar1.Value + "/" + trackBar2.Value + "/" + trackBar3.Value);
        }

        private void trackBar2_Scroll(object sender, EventArgs e)
        {
            serialPort1.WriteLine(trackBar1.Value + "/" + trackBar2.Value + "/" + trackBar3.Value);
        }

        private void trackBar3_Scroll(object sender, EventArgs e)
        {
            serialPort1.WriteLine(trackBar1.Value + "/" + trackBar2.Value + "/" + trackBar3.Value);
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            String[] ports = SerialPort.GetPortNames();
            foreach (var port in ports)
            {
                comboBox1.Items.Add(port);
            }
        }
    }
}

 

Variables

Déclaration des variables dans arduino

variables globales, locales, static

Variables globales

Ce sont des variables déclarées au début du programme, en dehors de toute fonction. Ces variables ont une portée globale, c’est à dire que chaque fonction constituant le programme peut les utiliser, les manipuler.

Avantages :

  • On déclare les variables une fois pour toute, on n’a plus à y revenir…
  • Comme les variables sont accessibles à toutes les fonctions, on peut faire l’économie de passage de paramètres.

Inconvénients :

  • Les variables mobilisent l’espace mémoire durant toute la vie du programme, on peut ainsi saturer la mémoire vive avec des variables qui n’ont servi qu’une fois (à l’initialisation par exemple)
  • Si on utilise les mêmes variables dans diverses fonctions, il se peut qu’une fonction modifie la valeur d’une manière qui n’était pas attendue par une autre fonction qui utilisait la même variable, c’est un source importante de bugs…

On évite donc le plus possible d’utiliser les variables globales et en particulier pour des compteurs de boucles.

Variables locales

Ce sont des variables déclarées au sein d’une fonction. Ces variables ont une portée locale, c’est-à-dire qu’une fois sortie de la fonction elles sont détruites libérant ainsi de l’espace mémoire. Ce sont les variables internes à une fonction, nécessaire à son fonctionnement.

Avantage :

  • Variables uniquement utilisables par la fonction à laquelle elles appartiennent, les autres fonctions ne peuvent pas interférer.
  • Des fonctions diverses peuvent avoir des variables portant le même nom sans risque d’interférence.

Inconvénients :

  • On doit utiliser un passage paramètre pour renvoyer une variable de ce genre à une autre fonction.
  • Les variables sont détruites à la sortie de la fonction, il n’y a pas de mémorisation de la dernière valeur de la variable.

Variables statiques

Ce sont des variables déclarées au sein d’une fonction, mais qui ne sont pas détruites à la sortie de cette dernière, de cette manière au prochain rappel de la fonction cette variable n’est pas recrée, mais elle est toujours là avec la dernière valeur obtenue. Ces variables associent le coté permanent des variables globales et le fait que cette variable est isolée des autres fonctions.

Variables volatiles

Ce sont des variables globales amenées à être modifiées par un processus extérieur au programme principal. Le mot volatile est une indication donnée au compilateur de ne pas simplifier son code et supprimer cette variable même si elle semble inutile.

Par exemple Routine d’interruption
Les routines d’interruptions utilisent souvent des variables testées dans le programme principal. Par exemple, l’interruption d’un port série en réception : le programme d’interruption stocke et teste les caractères reçus, signalant la fin du message par une variable globale au programme principal :

int End_Message = FALSE; // variable globale
void main ( void )
{
    ....
        while (! End_Message) 
/* mal réglé le compilateur va supprimer ce test car pour lui 
!End_Message= !FALSE= TRUE et comme While(TRUE) est toujours vrai le compilateur simplifie en supprimant ce test « inutile », or End_Message est modifié dans sous programme d’interruption ce test ne doit donc pas être simplifié */
        {
        .....
        }
}
interrupt void Isr_Receive ( void )
        {
            .....
                if ( RX_CHAR == ETX ) { End_Message = TRUE;}
                .....
                    .....
        }

 

Si l’optimisation du compilateur est dé-validée, ce programme fonctionne correctement. Par contre n’importe quel compilateur “décent” avec l’optimisation validée, va supprimer le code après la boucle while dans le main(), la condition ( ! End_Message) étant toujours vraie pour lui : en effet le compilateur n’a pas idée que la variable End_Message est modifiée par la routine d’interruption !
Dans ce cas-là et pour que tout rentre dans l’ordre il suffit de déclarer la variable End_Message avec l’attribut volatile.

bibliothèque Ethernet et Udp

Fonctions de bases nécessaires à l’utilisation du protocole UDP

L’utilisation de la carte réseau avec Arduino est subordonnée à l’utilisation de la bibliothèque Ethernet. Nous passerons en revue les fonctions nécessaire à l’utilisation du protocole UDP.

Bibliothèques nécessaires :

La communication entre la carte Ethernet et l’Arduino s’appuie sur la liaison série synchrone (SPI) , suivant la version de Arduino il peut être nécessaire d’inclure la bibliothèque SPI.h. Les autres fichiers d’entête à inclure au projet sont Ethernet.h et Udp.h

Le début de notre programme contiendra donc au minimum les entêtes suivants :

#include <SPI.h>         
#include <Ethernet.h>
#include <EthernetUdp.h>

 Bibliothèque ethernet

Un objet « Ethernet » est auto-instancié lors de l’inclusion de la bibliothèque Ethernet. Pour utiliser le protocole UDP, une seule fonction de la bibliothèque Ethernet est nécessaire :

Ethernet.begin(mac,IP,gateway,subnet);

Cette fonction initialise la bibliothèque et les paramètres du réseau. Les paramètres sont les suivants :

  • mac: l’adresse MAC (Media Access Control) pour le composant (tableau de 6 octets) : c’est l’adresse Ethernet matérielle de votre module. Pour de nombreux composants Ethernet, cette adresse est prédéfinie par le fabricant. Pour le module Ethernet Arduino, vous devez choisir votre propre adresse MAC.
  • ip: l’adresse IP du composant (tableau de 4 octets).
  • gateway: l’adresse IP de la passerelle du réseau (tableau de 4 octets). Optionnel: la valeur par défaut est l’adresse IP de la carte avec le dernier octet à 1.
  • subnet: le masque du sous-réseau du réseau (tableau de 4 octets). Optionnel : la valeur par défaut est 255.255.255.0.

pour plus d’informations et les autres fonctions exposées, voir la documentation en français ici : http://www.mon-club-elec.fr/pmwiki_reference_arduino/pmwiki.php?n=Main.BibliothèqueEthernet#toc2

bibliothèque UDP

Un objet Udp s’instancie de la manière suivante :

EthernetUDP Udp;

On peut alors utiliser les méthodes suivantes :

  • Udp.begin(localPort);cette fonction initialise le protocole UDP et démarre l’écoute sur le port désigné
  • Udp.stop(); cette fonction stoppe l’écoute du port. Utile si le traitement d’un paquet est long. Cela évite le débordement du buffer d’entrée de la carte Ethernet. (cette fonction a été rajoutée dans la bibliothèque Eiffel)
  • Udp.parsePacket(); cette fonction retourne la taille d’un paquet en octets ou 0 si aucun paquet n’est disponible
  • Udp.available(); cette fonction permet de savoir s’il y a des données dans le buffer de reception. Elle retourne un entier (int) qui vaut 0 si le buffer est vide et le nombre d’octets disponible s autrement.
  • Udp.read(unsigned char* buffer, size_t len); cette fonction permet de lire le paquet reçu et de le placer dans buffer (dont la longueur maximum est spcécifiée par buflen).
    Avant d’appeler cette fonction, il faut s’assurer que le buffer de reception n’est pas vide , il faut donc faire un test avec la fonction Udp.parsePacket()
    La fonction renvoie un entier (int) qui correspond au nombre d’octets lus. Si le nombre d’octets contenus dans le paquet est supérieur à la longueur du buffer, il y a troncature, le nombre renvoyé est alors négatif.
  • Udp.beginPacket(IPAddress ip, uint16_t port); permet d’initier l’envoi d’un paquet vers l’hôte distant dont on spécifie l’adresse IP et le port. La fonction retourne 1 en cas de succès et 0 en cas d’échec.
  • Udp.beginPacket(const char *host, uint16_t port); autre version en passant en argument le nom d’hôte au lieu de l’adresse IP.
  • Udp.endPacket(); ferme le paquet et l’envoie à l’hôte distant.
  • Udp.write(byte data); écrit un octet unique dans le paquet à envoyer.
  • Udp.write(const byte *buffer, size_t taille); Envoie « taille » octets du buffer vers le paquet.

exemple

UDPSendReceiveString.ino

#include <SPI.h>         // needed for Arduino versions later than 0018
#include <Ethernet.h>
#include <EthernetUdp.h>         // UDP library from: bjoern@cs.stanford.edu 12/30/2008


// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network:
byte mac[] = {
    0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};
IPAddress ip(192, 168, 1, 177);

unsigned int localPort = 8888;      // local port to listen on

// buffers for receiving and sending data
char packetBuffer[UDP_TX_PACKET_MAX_SIZE];  //buffer to hold incoming packet,
char  ReplyBuffer[] = "acknowledged";       // a string to send back

// An EthernetUDP instance to let us send and receive packets over UDP
EthernetUDP Udp;

void setup() {
    // start the Ethernet and UDP:
    Ethernet.begin(mac, ip);
    Udp.begin(localPort);

    Serial.begin(9600);
}

void loop() {
    // if there's data available, read a packet
    int packetSize = Udp.parsePacket();
    if (packetSize) {
        Serial.print("Received packet of size ");
        Serial.println(packetSize);
        Serial.print("From ");
        IPAddress remote = Udp.remoteIP();
        for (int i = 0; i < 4; i++) {
            Serial.print(remote[i], DEC);
            if (i < 3) {
                Serial.print(".");
            }
        }
        Serial.print(", port ");
        Serial.println(Udp.remotePort());

        // read the packet into packetBufffer
        Udp.read(packetBuffer, UDP_TX_PACKET_MAX_SIZE);
        Serial.println("Contents:");
        Serial.println(packetBuffer);

        // send a reply to the IP address and port that sent us the packet we received
        Udp.beginPacket(Udp.remoteIP(), Udp.remotePort());
        Udp.write(ReplyBuffer);
        Udp.endPacket();
    }
    delay(10);
}

 

 

Bibliothèques et débogage

bibliothèques

Définition

Une bibliothèque permet de mettre à disposition du programmeur des fonctions pré-écrites. On parle de fonctions haut-niveau. Par exemple dans la bibliothèque math.h (incluse par défaut dans tout programme Arduino) on trouvera des fonctions telles que sin() et cos() qui calculent le sinus ou le cosinus d’un angle exprimé en radiant.

La bibliothèque peut aussi être un pilote (driver) pour un composant que l’on veut interfacer avec la carte Arduino. Par exemple la bibliothèque LiquidCrystal pilote automatiquement un afficheur cristaux liquides de 2 lignes 16 caractères.

L’intérêt d’une telle bibliothèque est qu’on n’a pas à connaitre la manière dont l’afficheur est commandé pour l’utiliser. C’est la bibliothèque qui va prendre en charge la génération des signaux électriques (horloge, données..) pour piloter l’afficheur en fonction de la nature de la fonction.

Utilisation d’une bibliothèque dans un programme.

 Dans l’interface de développement (IDE) Arduino c’est on ne peut plus simple : depuis le menu « Croquis » sélectionner « Inclure une bibliothèque » puis la sélectionner dans la liste déroulante.

On voit alors apparaitre une directive de type « #include… » dans le programme

Dans l’exemple ci-dessus, on a importé  la bibliothèque LiquidCrystal.

Il faut souvent lire la documentation de la bibliothèque pour connaitre la manière de l’utiliser

Par exemple la bibliothèque LiquidCrystal expose les méthodes (ou fonctions) suivantes :

Fonctions d’initialisation

Fonctions de gestion de l’écran

Fonctions de positionnement du curseur

Fonctions modifiant l’aspect du curseur

 

Fonctions d’écriture

Fonctions de contrôle du mode d’affichage

Fonction de création de caractère personnalisé

createChar()

En général il faut initialiser la bibliothèque en instanciant un objet c’est-à-dire en créant une variable du type correspondant à l’objet.

Explication : dans le cas d’une variable simple on la crée en écrivant :

int valeur = 3;

On instancie un objet simple  de type Int (entier) auquel on affecte la valeur 3.
Pour un objet plus complexe on procède de manière analogue :

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

On créer ici un objet « lcd » de type LiquidCrystal, de plus on passe en paramètres les numéros de broches de l’Arduino permettant la connexion avec l’afficheur (voir la documentation de la bibliothèque)

Remarque : la création d’objet (que ce soit de simples variables ou des objets complexes) revient à faire des réservations d’espace mémoire et fournir des valeurs initiales.

L’appel des fonctions liées à l’objet se fait en indiquant le nom de l’objet suivi d’un « . » (point) suivi lui-même du nom de la fonction.

exemple

lcd.begin(16, 2); // initialise l’afficheur: 16 caractères, 2 lignes
lcd.print("hello, world!"); // affiche un message.

En général une bibliothèque est fournie avec des exemples d’utilisation. On y accède depuis le menu « Fichiers » puis « Exemples » enfin on choisit l’exemple dans la bibliothèque qui nous intéresse.

Installation d’une bibliothèque

Pour installer une bibliothèque il faut fermer l’IDE Arduino.

L’installation d’une bibliothèque est simple : il faut la copier dans le dossier « Libraries » contenu dans le dossier Arduino de l’utilisateur (« chemin de l’utilisateur »/documents/Arduino). Les bibliothèques « natives » c’est dire fournies d’office avec Arduino sont localisées dans « dossier d’installation d’Arduino »/libraries. Les bibliothèques natives contiennent par exemple Ethernt, Firmata,Keyboard, LiquidCrystal, Stepper, Wifi…

En général le dossier de la bibliothèque contient les éléments suivants :

  • Les fichiers codes de la bibliothèque (LiquidCrystal.h et LiquidCristal.cpp)
  • Un fichier texte keywords.txt permetaant la coloration syntaxique
  • Un dossier « examples » contenant lui-même des sous dossiers nommés avec le nom des exemples.

Le nom des exemples doit être le même que le dossier qui les contient . Par exemple le dossier  « HelloWorld » contient « HelloWorld.ino »

débogage

L’IDE Arduino ne possède pas d’outils tels qu’exécution en mode pas à pas, mode trace, visualisation des registres…

Il faut user de stratagèmes pour déboguer un programme. Le plus simple est d’utiliser le moniteur série et d’y faire afficher les valeurs de variables, les résultats de calculs…

Pour utiliser le moniteur il faut initialiser la liaison série. Puis à chaque fois qu’on désire afficher une valeur, il faut appeler la fonction « print » ou « println »

/* Blink without Delay*/
// set pin numbers:
const int ledPin =  13;      // the number of the LED pin
int ledState = LOW;             // ledState used to set the LED
long previousMillis = 0;        // will store last time LED was updated
long interval = 1000;           // interval at which to blink (milliseconds)
void setup() { 
    pinMode(ledPin, OUTPUT);       // set the digital pin as output:
    // -------------------- débogage ----------------------------//
    //  initialisation de la liaison série 
    // la ligne suivante pourra être commentée pour le passage en production
    Serial.begin(9600);
    //-------------------- fin débogage -------------------------//
}
void loop()
{
    unsigned long currentMillis = millis();
    if(currentMillis - previousMillis > interval) 
    {   
        // -------------------- débogage ----------------------------//
        // affichage de la valeur de CurrentMillis
        // la ligne suivante pourra être commentée pour le passage en production
        Serial.print("time: "); Serial.println(currentMillis,DEC); 
        //-------------------- fin débogage -------------------------//
        previousMillis = currentMillis;    // save the last time you blinked the LED 
        if (ledState == LOW)// if the LED is off turn it on and vice-versa
            ledState = HIGH;
        else
            ledState = LOW;

        // -------------------- débogage ----------------------------//
        // affichage de la valeur de ledState
        // la ligne suivante pourra être commentée pour le passage en production
        Serial.print("led state: "); Serial.println(ledState);  
        //-------------------- fin débogage -------------------------//   
        digitalWrite(ledPin, ledState); // set the LED with the ledState of the variable
    }
}

 

La fonction Serial.begin(9600); initialise la liaison série à un débit de 9600 bauds

Les fonctions Serial.print et Serial.println sont deux fonctions similaires : elles affichent des données sur la liaison série. À la différence de print, println affiche les données sur le port série suivi d’un caractère de “retour de chariot” (ASCII 13, or ‘\r’) et un caractère de “nouvelle ligne” (ASCII 10, or ‘\n’).

Serial.print(val)
Serial.print(val, format)

 

Serial.print(78); // affiche "78"
Serial.print(1.23456); // affiche "1.23"
Serial.print(byte(78)); // affiche "N" (dont la valeur ASCII est 78)
Serial.print('N'); // affiche "N"
Serial.print("Hello world."); // affiche "Hello world." 

Un second paramètre optionnel (format) spécifie :

  • pour les nombres entiers, la base à utiliser. Les valeurs autorisées sont BYTE, BIN (binaire, ou base 2), OCT (octal, ou base 8), DEC (décimal, ou base 10), HEX (hexadécimal, ou base 16),
  • pour les nombres à virgules (float), le paramètre précise le nombre de décimales après la virgule à utiliser.
Serial.print(78, BYTE); // affiche "N"
Serial.print(78, BIN) ; // affiche  "1001110"
Serial.print(78, OCT); // affiche "116"
Serial.print(78, DEC); // affiche "78"
Serial.print(78, HEX); // affiche "4E"
Serial.println(1.23456, 0); // affiche "1"
Serial.println(1.23456, 2); // affiche "1.23"

 

Lorsqu’on compile et qu’on lance le progamme, il faut appuyer sur l’icône « Serial Monitor »  dans l’IDE Arduino on voit alors une fenêtre qui s’ouvre et qui affiche les informations demandées dans le programme

À noter que le débit du moniteur doit être ajusté avec le débit de la liaison série ( 9600 baud)

En visualisant les variables et en plaçant des espions aux bons endroits du programme on se sort de quasiment toutes les situations.