wiki:SujetTP6-2018

Programmation Arduino

Objectifs de la séance

Cette séance de TME est assez chargée puisqu'elle va avoir 4 objectifs

  1. Découverte de l'arduino et premier programme
  2. Programmation multitâches par composants
  3. Utilisation de l'écosystème Arduino pour la programmation des périphériques
  4. Communication point-à-point NRF24L01

Préambule

Vous devez utiliser la version d'arduino qui se trouve (je vous conseille d'ajouter le chemin /opt/arduino-1.6.8 dans la variable PATH dans le .bashrc.

> /opt/arduino-1.6.8/arduino

Les documents nécessaires se trouvent :

Lors de cette séance, nous allons programmer sur Arduino en utilisant :

  • La LED présente sur le module.
  • L'écran OLED
  • Le capteur de lumière (cherchez-le :-)
  • Le port série qui relie le module et le PC.
  • Le port SPI qui relie le module au NRF

Démarrage

La première chose est d'exécuter l'exemple blink`

  • Brancher le module Arduino avec le câble USB
  • lancer : /opt/arduino-1.6.8/arduino &
  • Sélectionner : Tools -> Boards -> Arduino Nano
  • Sélectionner : Tools -> Processor -> ATmega328
  • Sélectionner : Tools -> Ports -> /dev/ttyUSB0 ou /dev/ttyUSB1
  • Charger le programme Blink : File -> Exemples -> 0.1 Basics -> Blink
  • Cliquer sur l'icône en forme de V pour Compiler
  • Cliquer sur l'icône en forme de -> pour uploader
  • En bas de la fenêtre un message vous indique la réussite de la compilation et de l'upload.
  • La led doit clignoter sur le module

Changer la fréquence et tester.

Exécution multi-tâches

Il est possible de programmer des applications multi-tâches coopératives dans l'environnement Arduino sans pour autant dispose des services d'un OS. Le code a été volontairement simplifié à l'extrême afin de bien comprendre le principe. ICI, toute l'application sera dans un seul fichier et nous n'allons pas utiliser la programmation objet pour ne pas complexifier.

Chaque tâche est représentée par une fonction loop_Tache() qui code son comportement. Dans l'environnement Arduino, la fonction loop() s'exécute en boucle, c'est elle qui va séquencer l'exécution des fonction loop_Tache(). La fonction loop() demande donc l'exécution des fonction loop_Tache() à tour de rôle. Les tâches n'ont pas le droit de conserver le processeur sinon cela crée un blocage du système. Cela signifie qu'il est interdit de faire des boucles d'attente d'un événement/ La structure générale d'une tâche est la suivante :

void loop_Tache(arguments) {
   // test de la condition d'exécution, si absent on SORT
   if (evement_attendu_absent) return;
   // code de la tache
   ....
}

Dans le cas plus général, les tâches ont un contexte d'exécution représenté par une variable globale sous forme d'une structure. Cette structure est passée en argument de la tâche. Cette structure doit le plus souvent être initialisée. L'initialisation est faite par une seconde fonction setup_Tache(), laquelle doit être appelée par la fonction setup().

Pour les tâches périodiques (elles sont fréquentes), nous pouvons écrire une fonction qui exploite un timer interne du processeur qui s'incrémente chaque microseconde. Cette fonction nommée waitFor(int timer, unsigned long period) prend deux paramètres timer et period. Le premier un numéro de timer (il en faudra autant que de tâches périodiques). Le second est une période en microsecondes.

wairFor() peut être appelée aussi souvent que nécessaire, elle rend 1 une seule fois par période (second paramètre). Si elle n'est pas appelée pendant longtemps alors elle rend le nombre de périodes qui se sont écoulées.

Dans l'application suivante nous avons deux tâches périodiques Led et Mess. La première fait clignoter une led dont le numéro est passé en paramètre à 5Hz. La seconde affiche bonjour à une fois par seconde.

// --------------------------------------------------------------------------------------------------------------------
// Multi-tâches cooperatives : solution basique
// --------------------------------------------------------------------------------------------------------------------

// --------------------------------------------------------------------------------------------------------------------
// unsigned int waitFor(timer, period) 
// Timer pour taches périodiques 
// configuration :
//  - MAX_WAIT_FOR_TIMER : nombre maximum de timers utilisés
// arguments :
//  - timer  : numéro de timer entre 0 et MAX_WAIT_FOR_TIMER-1
//  - period : période souhaitée
// retour :
//  - nombre de période écoulée depuis le dernier appel
// --------------------------------------------------------------------------------------------------------------------
#define MAX_WAIT_FOR_TIMER 2
unsigned int waitFor(int timer, unsigned long period){
  static unsigned long waitForTimer[MAX_WAIT_FOR_TIMER];
  unsigned long newTime = micros() / period;              // numéro de la période modulo 2^32 
  int delta = newTime - waitForTimer[timer];              // delta entre la période courante et celle enregistrée
  if ( delta < 0 ) delta += 1 + (0xFFFFFFFF / period);    // en cas de dépassement du nombre de périodes possibles sur 2^32 
  if ( delta ) waitForTimer[timer] = newTime;             // enregistrement du nouveau numéro de période
  return delta;
}

//--------- définition de la tache Led

struct Led_st {
  int timer;                                              // numéro de timer utilisé par WaitFor
  unsigned long period;                                             // periode de clignotement
  int pin;                                                // numéro de la broche sur laquelle est la LED
  int etat;                                               // etat interne de la led
}; 

void setup_Led( struct Led_st * ctx, int timer, unsigned long period, byte pin) {
  ctx->timer = timer;
  ctx->period = period;
  ctx->pin = pin;
  ctx->etat = 0;
  pinMode(pin,OUTPUT);
  digitalWrite(pin, ctx->etat);
}

void loop_Led(struct Led_st * ctx) {
  if (!waitFor(ctx->timer, ctx->period)) return;          // sort s'il y a moins d'une période écoulée
  digitalWrite(ctx->pin,ctx->etat);                       // ecriture
  ctx->etat = 1 - ctx->etat;                              // changement d'état
}

//--------- definition de la tache Mess

struct Mess_st {
  int timer;                                              // numéro de timer utilisé par WaitFor
  unsigned long period;                                             // periode d'affichage
  char mess[20];
} Mess_t ; 

void setup_Mess(struct Mess_st * ctx, int timer, unsigned long period, const char * mess) {
  ctx->timer = timer;
  ctx->period = period;
  strcpy(ctx->mess, mess);
  Serial.begin(9600);                                     // initialisation du débit de la liaison série
}

void loop_Mess(struct Mess_st *ctx) {
  if (!(waitFor(ctx->timer,ctx->period))) return;         // sort s'il y a moins d'une période écoulée
  Serial.println(ctx->mess);                              // affichage du message
}

//--------- Déclaration des tâches

struct Led_st Led1;
struct Mess_st Mess1;

//--------- Setup et Loop

void setup() {
  setup_Led(&Led1, 0, 100000, 13);                        // Led est exécutée toutes les 100ms 
  setup_Mess(&Mess1, 1, 1000000, "bonjour");              // Mess est exécutée toutes les secondes 
}

void loop() {
  loop_Led(&Led1);                                        
  loop_Mess(&Mess1); 
}
                                     

Questions

  • Que contient le tableau waitForTimer[]` ?
  • Dans quel cas la fonction waitFor() peut rendre 2 ?
  • Modifier le programme initial pour afficher "Salut" en plus de "bonjour" toutes les 1.5 secondes sans changer le comportement existant.

Utilisation de l'écran

Nous allons utiliser un écran OLED connecté en I2C, 128x32 ssd1306

  • La bibliothèque de l'écran se trouve en tapant la requête ssd1306 arduino
    à l'adresse https://github.com/adafruit/Adafruit_SSD1306. Vous devrez prendre également la bibliothèque GFX à l'adresse https://github.com/adafruit/Adafruit-GFX-Library qui est la bibliothèque graphique.
  • Vous pouvez exécuter l'exemple proposé dans la bibliothèque. Cette bibliothèque fonctionne pour plusieurs types modèles. Vous allez choisir le bon exemple : 128x32 I2C.
  • Pour ajouter une bibliothèque Arduino, vous devez simplement télécharger le .zip et importer directement le .zip en sélectionnant le menu Sketch -> include Library -> Add ZIP Library
  • Pour tester la librairie rendez-vous dans File -> Exemples -> Adafruit SSD1306 -> ssd1306_128x32_i2c. Il s'agit d'un programme qui teste les fonctionnalité de l'écran et de la bibliothèque graphique.

Questions

  • Extraire de ce code, ce qui est nécessaire pour juste afficher un compteur qui s'incrémente toutes des 1 seconde sur l'écran OLED.

Communications inter-tâches

Lorsqu'on écrit un programme multi-tâches, il est intéressant de les faire communiquer. Pour ce faire, nous allons simplement créer variables globales et les donner en arguments aux taches communicantes.

Supposons que nous voulions que la tâche T1 envoie un message à la tâche T2. Nous allons utiliser une boite à lettre. Le code suivant explique le principe qui est basé sur une variable d'état à 2 valeur indiquant l'état de la boite. La boite peut être vide ou pleine. l'écrivain T1 ne peut écrire que lorsque la boite est vide. Lorsqu'elle est vide, il y écrit et il change l'état. Inversement, le lecteur attend qu'elle soit pleine. Lorsqu'elle est pleine, il la lit et change l'état.

Il s'agit d'une communication sans perte. Si T1 ne testait pas l'état de la boite, on pourrait avoir des pertes, c'est parfois nécessaire, si T2 n'a pas eu le temps d'utiliser la boite mais que T1 a une nouvelle valeur, il peut écraser la valeur présente.

struct mailbox {
  enum {EMPTY, FULL} state;
  int val;
} mb0 = {.state = EMPTY};

void loop_T1(&mb) {
  if (mb->state != EMPTY) return; // attend que la mailbox soit vide
  mb->val = 42;
  mb->state = FULL;
}

void loop_T2(&mb) {
  if (mb->state != FULL) return; // attend que la mailbox soit pleine
  // usage de mb->val
  mb->state = EMPTY;
}

Questions

  • Ajouter une tâche qui lit toutes les 0,5 seconde le port analogique 15 (par analogRead()) sur lequel se trouve la photo-résistance et qui sort sa valeur dans une boite à lettre.
  • Mofifier la tâche Led pour que la fréquence de clignotement soit inversement proportionnel à la lumière reçue (moins il y a de lumière plus elle clignote vite). La tâche Led devra donc se brancher sur la boite à lettre.

Utilisation du module NRF24L01

Récupération de la bibliothèque du NRF24L01

Si nous voulons continuer à cross compiler, il faut installer la librairie qui va permettre de contrôler le module NRF24L01. Il existe plusieurs librairies. Celle choisie à le mérite d'être disponible dans l'environnement RaspberryPi et Arduino. C'est-à-dire que lorsque vous aurez compris comment l'utiliser avec la RaspberryPi, le passage sur Arduino sera facile.

  • Aller sur le site https://github.com/tmrh20/RF24
  • Récupérer le .zip de la branche master (bouton clone and download -> Download ZIP)
    $ unzip RF24-master.zip
    $ cd RF24-master
    $ mkdir $HOME/rf24
    $ ./configure --prefix=$HOME/rf24 --soc=BCM2835 --c_compiler=bcm2708hardfp-gcc --cxx_compiler=bcm2708hardfp-g++ --driver=SPIDEV --ldconfig=''
    $ make
    $ make install
    
  • Vérification que la library est installée.
    $ ls $HOME/rf24
      include  lib
    

Cette même bibliothèque a été installée sur les cartes RaspberryPi car la bibliothèque est dynamique et non pas statique, donc il faut la bibliothèque sur la RaspberryPi.

Cette compilation vous sera utile la semaine prochaine. Aujourd'hui, j'ai mis sur une RaspberryPi, un émetteur qui envoi toute les secondes un message. Vous allez l'écouter avec votre Arduino et l'afficher sur l'écran OLED.

Documents de référence du module NRF24L01

Premier usage : lecture d'un capteur

La documentation de la bibliothèque est ici dont voici un résumé :

  • RF24 (uint8_t _cepin, uint8_t _cspin)
    Configuration du module radio et du SPI, reçoit les numéros de broche cepin (radio) cspin (SPI Slave Select)
  • bool begin (void) Démarrage du module radio
  • void startListening (void)
  • void stopListening (void)
  • bool available (void)]
  • void read (void *buf, uint8_t len)
  • bool write (const void *buf, uint8_t len)
  • void openWritingPipe (const uint8_t *address)
  • void openReadingPipe (uint8_t number, const uint8_t *address)
  • emet (émetteur sur une RaspberryPi)
    Ce code est donné à titre indicatif, il ne sera pas utilisé aujourd'hui par vous. Il s'agit du code qui tourne sur l'une des RaspberryPi.
    #include <cstdlib>
    #include <iostream>
    #include <sstream>
    #include <string>
    #include <unistd.h>
    #include <RF24/RF24.h>
    #include <ctime>
    
    typedef uint8_t byte;
    
    using namespace std;
    
    RF24 radio(15,8); // radio(CE,CS)
    
    byte addresses[][6] = {"0XXXX"};
    
    void setup() {
      radio.begin();
      radio.setPALevel(RF24_PA_LOW);
      radio.openWritingPipe(addresses[0]);
      radio.printDetails();
    }
    
    char mess[16];
    char buffer[32];
    unsigned long timer;
    
    void loop() {
      cout << "." << flush;
      sprintf(buffer,"%s %d", mess, timer);
      radio.write( buffer, sizeof(buffer) );
      timer++;  
      sleep(1);
    }
    
    int main(int argc, char **argv)
    {
        if (argc > 1) strcpy(mess, argv[1]);
        setup();
        while (1)
            loop();
        return 0;
    }
    
  • Sensor (sur l'Arduino)
    #include <SPI.h>
    #include "RF24.h"
    #include "printf.h"
    
    RF24 radio(9,10);
    
    byte addresses[][6] = {"0XXXX"};
    
    void setup() {
      radio.begin();
      radio.setRetries(15,15);
      radio.setPALevel(RF24_PA_LOW);
      radio.openReadingPipe(1,addresses[0]);
      radio.printDetails();
      radio.startListening();
      Serial.begin(9600);
    }
    
    void loop() {
      char buffer[32];
    
      if( radio.available()){
         radio.read( buffer, sizeof(buffer) );             // Get the payload
         Serial.println(buffer);
       }
    }
    
  • Makefile sur la RaspberryPi
    Ce Makefile est donné à titre indicatif, il ne sera pas utilisé aujourd'hui.
    RPI?=20
    SRC=src
    APP=NRF24L01_base
    DST=nom1_nom2
    
    CROSSDIR        = /users/enseig/franck/peri
    CROSS_COMPILE   = $(CROSSDIR)/arm-bcm2708hardfp-linux-gnueabi/bin/bcm2708hardfp-
    
    INC=$(HOME)/rf24/include
    LIB=$(HOME)/rf24/lib
    CFLAGS=-Wall -Wfatal-errors -O2 -I$(INC)
    LDFLAGS=-L$(LIB) -lrf24
    
    all:  $(APP).x
    
    $(APP).x: $(APP).cpp
        $(CROSS_COMPILE)g++ -o $@ -I$(INC) $<  -O2 $(LDFLAGS)
    
    upload: 
        scp -P50$(RPI) $(APP).x pi@peri:$(DST)
    
    clean:
        rm -f *.o *.x *~
    

Travail demandé

Commencer par compiler sur l'Arduino le code du recepteur seul, et ajouter une tâche qui lit le message envoyé et l'affche sur l'écran Oled.

Last modified 6 years ago Last modified on Apr 6, 2018, 12:08:25 PM