Systèmes informatiques
Fermer ×

Arduino

Histoire

Le concept Arduino a été créé en Italie au début des années 2000 par des étudiants de l'IDII (Interaction Design Institute Ivrea). Ces étudiants ont choisi ce nom en référence au bar du roi Arduin (Arduino en Italien). Ce concept est inspiré du wiring, lui même inspirée du processing. L'histoire de ce projet est également racontée par le concepteur de wiring.

Aujourd'hui l'Arduino est très utilisé dans les écoles et universités.

Le concept Arduino

Architecture

Le concept Arduino est un ensemble matériel et logiciel qui peut être défini par

Les outils

Notez que je décline toutes responsabilités quant aux conséquences que pourraient avoir l'utilisation des programmes présentés. Ceux-ci pourraient être erronés ou obsolètes.

Le développement d'application est facilité par l'IDE disponible pour toutes les plateformes de développement (windows, macOS, linux). Cet IDE est fourni avec un ensemble de librairies de base accompagnées d'exemples d'utilisation. L'IDE se décline en plusieurs versions 1.8 et 2.0.

La carte Arduino uno

Pour la présentation de l'utilisation du système Arduino, on va utiliser une des premières cartes : la carte uno articulée autour d'un processeur AVR atmega328p au format DIP28 monté sur support, ce qui permet de le remplacer facilement.

Les types de données

Elles sont identiques à celles utilisées en C, avec une différence pour les réels où le type double est un alias sur le type float, ce qui fait qu'il n'y a que des réels simple précision.

Il existe un type booléen : boolean.

Les principales fonctions

L'ensemble des fonctions est disponible sur la page référence de l'Arduino.

Au niveau des fonctions mathématiques, on trouve, en plus, les fonctions :

Les fonctions de gestion du temps sont également très utilisées

Il est possible d'afficher des messages et résultats sur le terminal de l'IDE via la liaison série. Pour cela on utilise Serial

  1. La liaison série est initialisée dans la fonction setup avec Serial.begin(vitesse) qui est la vitesse de transmission en bauds (19200 par exempel)
  2. Ensuite on peut afficher des données et message avec les fonctions Serial.print(donnee) et Serial.println(donnee). One peut pas transmettre plusieurs données, mais une seule par appel à la fonction, donnée qui peut être une chaîne de caractères, un variable. println ajoute un retour à la ligne suivante.

Gestion des périphériques

Gestion des GPIOs

La cartographie des GPIOs est accessible depuis la page de la carte uno et disponible au format PDF. Les identifiants des GPIOs vont de 0 à 19. Sur cet intervalle, on trouve des GPIOs qui possèdent plusieurs fonctions :

Image issue du site arduino.cc

Les broches RX et TX, respectivement GPIO 0 et 1 sont utilisées par l'interface USB pour la programmation et lors de l'utilisation du terminal série avec l'objet Serial. Ceci rend délicat l'utilisation de ces broches en mode numérique tout ou rien. En effet lors de chaque reset ce sont les fonctions TX et RX qui sont utilisés. Lors de la connexion du terminal série, un reset est effectué, car à chaque reset le bootloader est exécuté. De plus les broches TX et RX du connecteur sont respectivement reliées aux broches TX et RX de l'atmega328 via une résistance de 1kΩ. Ce broches alimentent déjà les LEDS TX et RX.

La led nommée LED_BUILTIN est connectée au GPIO 13.

Les broches MOSI et SS correspondent aux broches des timers 2 et 1. ce qui fait qu'il n'est pas possible d'utiliser ces timers en interruptions en même temps que le bus SPI.

Les broches PWMnA et PWMnB (n=0,1,2) correspondent aux sorties A et B des timers 0,1 et 2. Ce qui fait que ces sorties ne sont pas disponibles en cas d'utilisation des timers correspondants en interruption. En conséquence, il n'est pas possible d'utiliser les sorties analogWrite qui correspondent aux timers déjà utilisés par ailleurs.

Les broches analogiques A4 et A5 correspondent respectivement aux broches numériques 18 et 19 ainsi qu'aux signaux SDA et SCL du bus I2C. Même si elles apparaissent en deux endroits sur la carte uno, il ne faut pas oublier que ce sont les mêmes broches de l'atmega328.

Principe de précaution
  • Ne jamais utiliser les broches 0 (RX) et 1(TX)
  • En cas d'utilisation du bus I2C, il ne faut pas utiliser les entrées analogiques A4 et A5 ni les entrées numériques 18 et 19
  • En cas d'utilisation du bus SPI, il na faut pas utiliser les timers 1 et 2 en interruptions, et éviter les sorties PWM1A, PWM1B PWM2A et PWM2B
  • Lors de l'utilisation d'un ou plusieurs timers en interruption, il ne faut pas utiliser les sorties PWM correspondant à ces timers.

La gestion des GPIOs se fait en deux étapes :

  1. Choisir la direction, en général dans la fonction setup avec la fonction : pinMode(ID,direction)ID est l'identifiant de GPIO et direction vaut OUTPUT, INPUT ou encore INPUT_PULLUP. Cette dernière valeur utilise la résistance interne de pull-up (connexion au +Vcc) afin de ne pas laisser l'entrée dans un état indéterminé, lorsque celle si n'est pas connecté.
  2. Ecrire une valeur avec la fonction digitalWrite(ID,valeur) ou valeur vaut 0 ou 1 ou bien une des deux constantes LOW ou HIGH
    Lire une valeur avec la fonction digitalRead(ID) qui la valeur de la broche correspondant à l'ID spécifié.

On propose de réaliser un chenillard en version Arduino en utilisant les broches d'ID de 2 à 9.

On écrit un programme qui utilise des fonctions, mais pas de classes C++ supplémentaires.

Voir l'exemple du codage du chenillard

Code source uno_chenillard.ino


const int leds[8] = {2,3,4,5,6,7,8,9};

void initport(uint8_t masque) {
  int pindir;
  for(int i=0;i<8;i+=1) {
    pindir = (masque >> i) & 1;
    pinMode(leds[i], pindir);
  }  
}

void writeport(uint8_t valeur) {
  int pinvalue;
  for(int i=0;i<8;i+=1) {
    pinvalue = (valeur>> i) & 1;
    digitalWrite(leds[i], pinvalue);
  } 
}

int valeur;

void setup() {
  initport(0xff);
  valeur = 1;
}

void loop() {
  writeport(valeur);
  delay(200);
  valeur = (valeur << 1) % 255; 
}
					

La liste des ID des broches utilisées est stockée dans un tableau de 8 entiers.

L"initialisation en sortie se fait avec la fonction initport, avec le paramètre qui permet de choisir la direction de chaque broche du port. Ici cette valeur est 0xff car toutes les huit broches sont en sorties.

L'écriture d'un octet sur le port s'effectue avec la fonction writeport qui permet d'écrire la valeur de l'octet sur le port avec le bit de poids faible qui correspond à l'indice 0 et donc à l'ID 2.

Bien que on travaille sur un octet, la valeur doit être de taille supérieure pour le calcul.

La fonction setup initialise le port en sortie et initialise la valeur à 1 (bit de poids faible à 1) qui correspond au démarrage du chenillard.

La fonction loop commence par afficher la valeur sur le port, attend 200ms puis calcule la valeur suivante. Le décalage à gauche donne les valeurs 1,2,4,8,16,32,64,128 ce qui correspond bien à notre chenillard. Après la valeur 128, il faut revenir à 1. C'est ce que fait le modulo 255, en effet après 128 la valeur est 256 qui, divisée par 255, donne un reste de 1. Si on avait choisi un type 8 bits pour la valeur, ce calcul ne fonctionnerait pas car après 128, on aurait obtenu 0 au lieu de 1.

On va s'intéresser maintenant à la taille mémoire utilisée par ce programme en utilisant simplement les informations affichées par l'IDE qui sont 1182 octets pour la mémoire de programme et 27 octets pour la mémoire RAM.

On va maintenant céer une classe C++ de gestion du port 8 bits nommée PortIO. Pour comprendre le C++, on pourra visiter le chapitre sur le langage objet C++.

Voir l'exemple du codage de la librairie C++

Code source de PortIO.h


#ifndef __PortIO_h
#define __PortIO_h

#define Nbits 8

class PortIO {

  private:
    int pins[Nbits];
    
  public:
    PortIO(void);
    PortIO(const int *,int );
 
    void setpins(unsigned int ); 
    int getpins(void);
};

#endif
					

Code source de PortIO.cpp


#include <Arduino.h>
#include "PortIO.h"

PortIO::PortIO() { }

PortIO::PortIO(const int *p,int dir) {
  int pindir;
  for(int i=0;i<Nbits;i+=1) {
    pins[i] = p[i];
    pindir = (dir >> i) & 1;
    pinMode(pins[i], pindir);
  }
}

void PortIO::setpins(unsigned int value) {
  int pinvalue;
  for(int i=0;i<Nbits;i+=1) {
    pinvalue = (value >> i) & 1;
    digitalWrite(pins[i], pinvalue);
  } 
}

int PortIO::getpins() {
  unsigned int pinvalue,portvalue=0;
  for(int i=0;i<Nbits;i+=1) {
    pinvalue = digitalRead(pins[i]);
    portvalue |= (pinvalue & 1) << i;
  } 
}
					

Le constructeur par défaut ne fait rien. Il faut utiliser la constructeur avec les deux paramètres.

Voir l'exemple du codage du programme de test de cette librairie

Code source uno_chenillard.ino


#include "PortIO.h"

const int leds[8] = {2,3,4,5,6,7,8,9};
int valeur;

PortIO portleds(leds,0xff);

void setup() {
  valeur = 1;  
}

void loop() {
  portleds.setpins(valeur);
  delay(200);
  valeur = (valeur << 1) % 255;
}
					

La liste des broches est également définie dans un tableau de 8 entiers.

Ensuite on construit l'objet portleds à partir de la classe PortIO en initialisant toutes les broches en sortie (0xff).

La fonction setup se résume à l'initialisation de la valeur à 1.

Ensuite on utilise la méthode setpins de la classe PortIO pour écrire la valeur de l'octet sur le port. La période reste définie par la fonction delay et la valeur suivante est toujours calculée avec la même expression.

Avec cette structure, la taille mémoire utilisée est de 1250 octets pour la mémoire de programme et 43 octets pour la mémoire RAM.

Mémoire programme (octets)Mémoire RAM (octets)
avec fonctions118227
avec classe125043

Utilisation des Timers

La sortie PWM

La sortie PWM est considérée comme une écriture analogique avec la fonction analogWrite(valeur) où valeur est le rapport cyclique qui est compris entre 0 et 255.

Du point de vue matériel cette sortie utilise un timer matériel. Les timers sont affectés aux sorties de la façon suivante :

Ces sorties correspondent aux sorties de comparaisons OCxA et OCxB de chaque timer du circuit atmega328p.

Voir l'exemple du codage d'une sortie PWM

Code source uno_pwm.ino


#define LED 10
#define PERIODE 200
#define DELAI_MS 5

unsigned int n;
unsigned int y;

void setup() {
  pinMode(LED,OUTPUT);
  y=0;
}

void loop() {
  analogWrite(LED,y);
  delay(DELAI_MS);
  y=127*(1+sin(2*PI*n/PERIODE));
  n = (n + 1) % PERIODE;
}
					

Ce programme fait varier la luminosité d'une LED connectée à la broche 10 avec une période de 5*200ms=1s. L'amplitude du rapport cyclique varie entre 0 et 254.

On va s'intéresser au calcul de la période avec l'utilisation du modulo, en analysant ce qui se passe sans le modulo.

Dans notre cas la période est de 200, la valeur de l'entier n est comprise entre 0 et 65535. la valeur maximale de n n'est pas un multiple de la période. Le plus grand multiple de 200 dans l'intervalle 0-65535 est k=65535200=327 qui donne une valeur de n=200 × 327 = 65400. la dernière sinusoïde ne sera pas complète avant le passage à 0 de n. On aura donc une sinusoïde incomplète toutes les 200 sinusoïdes. Ce qui n'est pas important pour la luminosité d'une LED mais qui peut l'être dans d'autres cas. Le modulo avec la période permet d'annuler cet effet indésirable quelque soit la période et le type d'entier dont la valeur maximale est bien évidement supérieure à la période.

Le timer en interruption

Pour ce faire, on utilise une libraire timer en interruption spécifique à certains processeurs atmega.

L'installation se fait à l'aide de l'IDE.

Pour utiliser ce timer en interruption, il faut

  1. Choisir un certain nombre de paramètres dont le ou les timers à utiliser avant d'inclure le fichier de définition TimerInterrupt.h
  2. Dans la fonction setup :
    1. Initialiser le timer avec la fonction ITimer1.init()
    2. Associer une fonction avec le vecteur d'interruption du timer avec la fonction ITimer1.attachInterruptInterval(periode, fonction)periode est exprimé en ms et fonction est le nom de la fonction qui sera exécutée à chaque interruption

Il n'y a rien à faire dans la fonction loop car la fonction associée au timer s'exécute à intervalles réguliers. Tout se passe comme la fonction loop et la fonction attachée au timer s'exécutaient en parallèle.

Voir l'exemple de codage du chenillard utilisant un timer matériel en interruption

Code source uno_chenillard_timer.ino


#define TIMER_INTERRUPT_DEBUG         0
#define _TIMERINTERRUPT_LOGLEVEL_     0
#define USE_TIMER_1     true
#include "TimerInterrupt.h"

#define PERIODE_MS     200
#define LED 10
const int leds[8] = {2,3,4,5,6,7,8,9};
uint8_t compteur;

void initport() {
  for(int i=0;i<8;i+=1) {
    pinMode(leds[i],OUTPUT);
    digitalWrite(leds[i],LOW);
  }  
}

void TimerChenillard(void) {
  if (compteur == 0) {
    digitalWrite(leds[7],LOW);
  }
  else {
    digitalWrite(leds[compteur-1],LOW);
  }
  digitalWrite(leds[compteur],HIGH);
  compteur = (compteur + 1) % 8; 
}

int bascule=0;

void setup() {
  pinMode(LED,OUTPUT);
  digitalWrite(LED,bascule);
  initport();
  compteur = 0 ;
  ITimer1.init();
  ITimer1.attachInterruptInterval(PERIODE_MS, TimerChenillard);
}

void loop() {
  delay(500);
  bascule = (~bascule & 1);
  digitalWrite(LED,bascule);
}
					

L'initialisation de la librairie nécessite de définir TIMER_INTERRUPT_DEBUG et _TIMERINTERRUPT_LOGLEVEL_ à 0 pour ne pas avoir de mode debug. puis de définir le timer utilisé, ici le timer 1 avec USE_TIMER1 à true.

La fonction setup initialise les broches utilisées par le port et la LED en sortie et positionnées à 0. Puis initialise le timer pour ensuite le démarrer en associant la fonction de gestion du chenillard. afin d'éviter une boucle pour chaque broche du port, on utilise un compteur de position qui limite le nombre d'accès aux broches à 2 au lieu de 8 à chaque appel de la fonction.

La fonction loop gère, de son côté, le clignotement de la LED toutes les 500ms.

Ce système montre comment implémenter deux "processus très simplifiés" sur un Arduino.

Gestion de l'interface SPI

On utilise ici le circuit LSM303D utilisé avec la liaison SPI de la raspberry pi, on utilisera également le fichier de définition des constantes LSM303D_defs.h de ce même chapitre.

On utilise la librairie SPI de l'arduino, qui est déjà installée, en utilisant la documentation de la librairie SPI :

  1. Dans la fonction setup, on initialise le l'interface SPI avec SPI.begin(), si nécessaire on initialise les paramètres de l'interface SPI qui sont, la fréquence de l'horloge, l'ordre des bits et le mode de fonctionnement avec la classe SPISettings et la méthode beginTransaction.
  2. Ensuite on transfert une donnée en écriture ou en lecture avec la fonction SPI.transfer sans oublier de positionner la broche SS à 0 avant puis à 1 après le ou les transferts.
Voir l'exemple de codage du programme de gestion de l'accéléromètre et magnétomètre LSM303D

Code source uno_accelmag_spi.ino


#include <SPI.h>
#include "LSM303D_defs.h"

SPISettings LSM303Dsettings(1000000UL, MSBFIRST, SPI_MODE0);

// calibre +-2
#define DIV_2 16384.0

void LSM303DEcrireReg(unsigned char registre,unsigned char valeur) {
  digitalWrite(SS, LOW); 
  SPI.transfer(registre);
  SPI.transfer(valeur);
  digitalWrite(SS, HIGH); 
  delayMicroseconds(1);
}

unsigned char LSM303DLireReg(unsigned char registre) {
  unsigned char resultat = 0;
  digitalWrite(SS, LOW); 
  SPI.transfer(registre | 0x80);
  resultat = SPI.transfer(0);
  digitalWrite(SS, HIGH); 
  delayMicroseconds(1);
  return resultat;
}

void LSM303DInit(void) {
  LSM303DEcrireReg(LSM303D_CTRL_1,LSM303D_CTRL_1_AZEN|LSM303D_CTRL_1_AYEN|LSM303D_CTRL_1_AXEN | LSM303D_CTRL_1_AODR_0);
  LSM303DEcrireReg(LSM303D_CTRL_2,0);
  LSM303DEcrireReg(LSM303D_CTRL_3,0);
  LSM303DEcrireReg(LSM303D_CTRL_4,0);
  LSM303DEcrireReg(LSM303D_CTRL_5,0);
  LSM303DEcrireReg(LSM303D_CTRL_6,0);
  LSM303DEcrireReg(LSM303D_CTRL_7,0);
}

bool LSM303DAccPret() {
  unsigned char valeur = LSM303DLireReg(LSM303D_STATUS_A);
  return ((valeur & LSM303D_STATUS_A_ZYXADA) == LSM303D_STATUS_A_ZYXADA);
}

short LSM303DLireAxe(unsigned int regbase) {
  short resultat =0;
  unsigned char lsb,msb;
  lsb=LSM303DLireReg(regbase);
  msb=LSM303DLireReg(regbase+1);
  resultat = (short)lsb | (((short)msb) << 8) ; 
  return resultat;
}

unsigned char id;
int ax,ay,az;
double rax,ray,raz;
int mx,my,mz;
double rmx,rmy,rmz,mm;

void printData(char *nom,double valeur) {
  Serial.print(nom);
  Serial.print(valeur);
}

void setup() {
  Serial.begin(19200);
  pinMode(SS, OUTPUT); 
  digitalWrite(SS, HIGH); 
  SPI.begin();
  SPI.beginTransaction(LSM303Dsettings);
  LSM303DInit();
  id = LSM303DLireReg(LSM303D_WHO_AM_I);
  Serial.print("ID = ");
  Serial.println(id,HEX);
  delay(100);
}

void loop() {
    while (!LSM303DAccPret());
    ax = LSM303DLireAxe(LSM303D_OUT_X_L_A);
    ay = LSM303DLireAxe(LSM303D_OUT_Y_L_A);
    az = LSM303DLireAxe(LSM303D_OUT_Z_L_A);
    rax = (double)ax / DIV_2 ;
    ray = (double)ay / DIV_2 ;
    raz = (double)az / DIV_2 ;
    mx = LSM303DLireAxe(LSM303D_OUT_X_L_M);
    my = LSM303DLireAxe(LSM303D_OUT_Y_L_M);
    mz = LSM303DLireAxe(LSM303D_OUT_Z_L_M);
    rmx = (double)mx / DIV_2 ;
    rmy = (double)my / DIV_2 ;
    rmz = (double)mz / DIV_2 ;
    mm = sqrt(rmx*rmx+rmy*rmy+rmz*rmz);
    printData("ax=",rax);
    printData(" ay=",ray);
    printData(" az=",raz);
    printData(", mx=",rmx);
    printData(" my=",rmy);
    printData(" mz=",rmz);
    printData(", M=",mm);
    Serial.println("");
}
					

La fonction LSM303DEcrireReg permet d'écrire une valeur dans un registre, elle positionne SS à 0, transfert le registre puis la valeur du registre avant de positionner SS à 1.

La fonction LSM303DLireReg permet de lire le contenu d'un registre, elle positionne SS à 0, transfert le registre en lecture (0x80), puis transfert 0, la réponse est fournie par la valeur de retour de la deuxième fonction transfer, puis elle positionne SS à 1.

La fonction LSM303DInit est une suite d'écriture dans les registres de contrôle du capteur en utilisant la fonction LSM303DEcrire.

La fonction LSM303DPret retourne un booléen qui vaut true si les données sont prêtes.

La fonction LSM303DLireAxe permet de lire les deux registres qui contiennent les données d'une axe.

La fonction setup initialise le port série (Serial.begin) pour pouvoir afficher les résultats sur le terminal inclus dans l'IDE.

Ici il n'y a plus de boucle de lecture, car cela est assuré par la fonction loop. A chaque début de la fonction on attend que les données soient prêtes. Cette méthode est bloquante, mais cela n'est pas un problème ici car on ne gère que le capteur. Dans d'autres cas, il faudrait utiliser une alternative if qui permettrait d'effectuer d'autres traitements si les données ne sont pas prêtes.

Gestion de l'interface I2C

On utilise ici le circuit bh1745 utilisé avec la liaison I2C de la raspberry pi, on utilise également le fichier de définitions bh1745_defs.h de ce même chapitre.

On utilise la librairie Wire, qui est déjà installée, en utilisant la documentation de la librairie Wire :

  1. Dans la fonction setup , on initialise l'interface I2C avec Wire.begin.
  2. On effectue une transmission en écriture en plusieurs étapes :
    1. On démarre la transmission I2C avec la fonction beginTransmission(Adresse) où Adresse est l'adresse du capteur sur le bus I2C
    2. On effectue une écriture avec la fonction Wire.write(valeur) où valeur est l'octet à transmettre
    3. On termine la transmission avec la fonction Wire.endTransmission().
    On effectue une transmission en lecture en suivant les étapes
    1. La lecture se fait avec la fonction Wire.requestFrom(adresse,taille) où adresse est l'adresse du capteur et taille est le nombre d'octets demandé.
    2. On vérifie que les données sont bien reçues avec Wire.available() qui retourne le nombre d'octets disponibles.
    3. On peut lire chaque octet reçu avec la fonction Wire.read().
Voir l'exemple de codage du programme de gestion du capteur de luminosité bh1745

Code source uno_bh1745.ino


#include <Wire.h>
#include "bh1745_defs.h"

int ecrireRegistre(byte registre, byte valeur) {
  Wire.beginTransmission(BH1745_I2CADR);
  Wire.write(registre);
  Wire.write(valeur);
  Wire.endTransmission();
}

int lireRegistre(byte registre) {
  uint8_t recu;
  Wire.beginTransmission(BH1745_I2CADR);
  Wire.write(registre);
  Wire.endTransmission();
  Wire.requestFrom(BH1745_I2CADR, 1);
  if (Wire.available() == 1) {
    recu = Wire.read();
    return recu;
  } 
  else {
    return -1;
  }
}

void bh1745Init() {
  ecrireRegistre(BH1745_SYSTEM_CTRL,BH1745_SYSCTRL_SW_RST);
  ecrireRegistre(BH1745_SYSTEM_CTRL,0);
  ecrireRegistre(BH1745_CTRL1,BH1745_CTRL1_320ms);
  ecrireRegistre(BH1745_CTRL2,BH1745_CTRL2_RGBC_EN);
  ecrireRegistre(BH1745_CTRL3,BH1745_CTRL3_VALUE);
  ecrireRegistre(BH1745_TH_LSB,0);
  ecrireRegistre(BH1745_TH_MSB,0);
  ecrireRegistre(BH1745_TL_LSB,0xff);
  ecrireRegistre(BH1745_TL_MSB,0xff);
  ecrireRegistre(BH1745_INTERRUPT,0);
}

unsigned char bh1745manutactureId() {
  unsigned char mid;
  mid = lireRegistre(BH1745_ID);
  return mid;
}

void bh1745Mesure() {
  ecrireRegistre(BH1745_CTRL3,BH1745_CTRL3_VALUE);
}

boolean bh1745Pret() {
  int valide = lireRegistre(BH1745_CTRL2);
  return ((valide & BH1745_CTRL2_VALID) == BH1745_CTRL2_VALID);
}

unsigned short bh1745lire(unsigned int LSBReg) {
  unsigned short valeur=0;
  unsigned short lsb,msb;
  lsb = lireRegistre(LSBReg);
  msb = lireRegistre(LSBReg + 1);
  valeur = lsb + (msb << 8);
  return valeur;
}

void bh1745Led(int etat) {
  if (etat) {
    ecrireRegistre(BH1745_INTERRUPT,BH1745_INTERRUPT_ENABLE);
  }
  else {
    ecrireRegistre(BH1745_INTERRUPT,0);
  }
}

unsigned short red,green,blue,luminosite;
boolean pret;

void setup() {
  Serial.begin(19200);
  Wire.begin();
  bh1745Init();
  unsigned char manufacture = bh1745manutactureId();
  Serial.print("id=");
  Serial.println(manufacture,HEX);
}

void loop() {
  pret = bh1745Pret();
  if (pret) {
    red = bh1745lire(BH1745_RED_LSB);
    green = bh1745lire(BH1745_GREEN_LSB);
    blue = bh1745lire(BH1745_BLUE_LSB);
    luminosite = bh1745lire(BH1745_CLEAR_LSB);
    Serial.print("rouge=");
    Serial.print(red);
    Serial.print(",vert=");
    Serial.print(green);
    Serial.print(",bleu=");
    Serial.print(blue);
    Serial.print(",clear=");
    Serial.println(luminosite);
  }
  delay(200);
}
					

La fonction ecrireRegistre utilise deux fonctions write pour écrire le registre suivi de la valeur

La fonction lireRegistre utilise la transmission en écriture en écrivant la valeur du registre, puis la transmission en lectrure pour lire l'octet de réponse.

La fonction bh1745Init est une suite d'écriture dans les registres de contrôle du capteur en utilisant la fonction ecrireRegistre.

La fonction bh1745Pret retourne un booléen qui vaut true si les données sont prêtes.

La fonction bh1745Mesure demande une nouvel mesure en écrivant dans un registre.

La fonction bh1745lire effectue deux lecture de registres successifs pour obtenir la valeur d=sur 16 bits.

La fonction loop lit et affiche les données si celles-ci sont disponibles. Le delai de 200ms permet de ralentir l'affichage.

Voir l'exemple du codage de la librairie C++

Code source de bh1745.h


#ifndef __BH1745_H
#define __BH1745_H

#include "bh1745_defs.h"

class BH1745 {

  private :
    int i2cadresse;

    void setAdresse(int);
    void ecrireRegistre(byte , byte );
    int lireRegistre(int );

  public:
    BH1745(void);
    BH1745(int );

    byte getID(void);

    void Initialise(void);
    void Initialise(int );
    unsigned short lireValeur(byte );
    void DemandeMesure(void);
    boolean MesurePrete(void);
    void commandeLed(byte);

};

#endif
					

Code source de bh1745.cpp


#include <Arduino.h>
#include <Wire.h>
#include "bh1745.h"

void BH1745::setAdresse(int adresse) {
  this->i2cadresse = adresse ;
}

void BH1745::ecrireRegistre(byte registre , byte valeur) {
  Wire.beginTransmission(i2cadresse);
  Wire.write(registre);
  Wire.write(valeur);
  Wire.endTransmission();
}

int BH1745::lireRegistre(int registre){
  int recu;
  Wire.beginTransmission(i2cadresse);
  Wire.write(registre);
  Wire.endTransmission();
  Wire.requestFrom(i2cadresse, 1);
  if (Wire.available() == 1) {
    recu = Wire.read();
    return recu;
  } 
  else {
    return -1;
  }  
}

BH1745::BH1745() {
  setAdresse(BH1745_I2CADR);
}

BH1745::BH1745(int i2caddresse) {
  setAdresse(i2caddresse);
}


byte BH1745::getID() {
  return lireRegistre(BH1745_ID);
}

void BH1745::Initialise() {
  Initialise(BH1745_CTRL1_320ms);
}

void BH1745::Initialise(int periode) {
  Wire.begin();
  ecrireRegistre(BH1745_SYSTEM_CTRL,BH1745_SYSCTRL_SW_RST);
  ecrireRegistre(BH1745_SYSTEM_CTRL,0);
  ecrireRegistre(BH1745_CTRL1,periode);
  ecrireRegistre(BH1745_CTRL2,BH1745_CTRL2_RGBC_EN);
  ecrireRegistre(BH1745_CTRL3,BH1745_CTRL3_VALUE);
  ecrireRegistre(BH1745_TH_LSB,0);
  ecrireRegistre(BH1745_TH_MSB,0);
  ecrireRegistre(BH1745_TL_LSB,0xff);
  ecrireRegistre(BH1745_TL_MSB,0xff);
  ecrireRegistre(BH1745_INTERRUPT,0);
}

unsigned short  BH1745::lireValeur(byte LSBReg ) {
  unsigned short valeur=0;
  unsigned short lsb,msb;
  lsb = lireRegistre(LSBReg);
  msb = lireRegistre(LSBReg + 1);
  valeur = lsb + (msb << 8);
  return valeur;      
}

void  BH1745::DemandeMesure(void) {
  ecrireRegistre(BH1745_CTRL3,BH1745_CTRL3_VALUE);  
}


boolean  BH1745::MesurePrete(void) {
  int valide = lireRegistre(BH1745_CTRL2);
  return ((valide & BH1745_CTRL2_VALID) == BH1745_CTRL2_VALID);  
}


void  BH1745::commandeLed(byte etat) {
  if (etat) {
    ecrireRegistre(BH1745_INTERRUPT,BH1745_INTERRUPT_ENABLE);
  }
  else {
    ecrireRegistre(BH1745_INTERRUPT,0);
  }  
}
					
Voir l'exemple du codage du programme de test de cette librairie

Code source uno_bh1745.ino



#include "bh1745.h"

BH1745 bh1745;
unsigned short red,green,blue,luminosite;
boolean pret;

void setup() {
  Serial.begin(19200);
  bh1745.Initialise();
  unsigned char manufacture = bh1745.getID();
  Serial.print("id=");
  Serial.println(manufacture,HEX);
}

void loop() {
  pret = bh1745.MesurePrete();
  if (pret) {
    red = bh1745.lireValeur(BH1745_RED_LSB);
    green = bh1745.lireValeur(BH1745_GREEN_LSB);
    blue = bh1745.lireValeur(BH1745_BLUE_LSB);
    luminosite = bh1745.lireValeur(BH1745_CLEAR_LSB);
    Serial.print("rouge=");
    Serial.print(red);
    Serial.print(",vert=");
    Serial.print(green);
    Serial.print(",bleu=");
    Serial.print(blue);
    Serial.print(",clear=");
    Serial.println(luminosite);
  }
  delay(200);
}
					

Visualisation des signaux

Méthode

Pour visualiser les signaux échangés entre l'Arduino et le périphérique, on propose d'utiliser une raspberry pi en oscilloscope pour signaux numériques avec le système de gestion des GPIOS pigpiod et l'application piscope.

Attention : les sorties de l'arduino fonctionnent avec une tension de 5v, les entrées de la raspberry pi supportent une tension maximale de 3.3v. Il est donc nécessaire de faire une adaptation en tension de chaque entrée avec un pont diviseur de tension.

Visualisation des signaux du chenillard

On choisit 8 GPIOs sur la raspberry, que l'on va configurer en entrée, ces entrées sont connectées aux sorties de la carte Arduino.

Le résultat affiché avec piscope donne :

Sur la vidéo de la maquette, on peut observer, entre le processeur et les leds, 8 diviseurs de tension pour adapter le niveau entre les sorties arduino de 5V et les entrées de la raspberry pi de 3.3v.

Visualisation des signaux de la communication SPI

On relie 4 GPIos aux signaux SPI (SCK,MOSI,MISO,SS) de l'Arduino. Pour permettre l'échantillonnage correct des signaux on utilise la vitesse minimale sur l'Arduino, et on paramètre pigpiod avec la période d'échantillonnage la plus faible qui est 1us.

Le résultat affiché avec piscope donne :

Les signaux correspondent à l'attente de données prêtes, l'Arduino envoie 0x27 avec le bit de poids fort à 1 pour la lecture, et reçoit 0x00 qui correspond au fait que les données ne sont pas prêtes.

Le résultat analysé par le logiciel pulseview permet d'obtenir directement les valeurs du bus SPI. Ce logiciel analyse des données au format VCD enregistrées par piscope.

Sur ce graphe on peut mesurer la fréquence d'acquisition maximale d'environ 7kHz avec une fréquence d'horloge de 125kHz. On trouve également la valeur 0x27 avec le bit de poids fort à 1 qui donne 0xA7.

Fonctionnement en programmateur

Cartes équipées de processeur AVR

Il est possible d'utiliser un Arduino pour programmer un autre Arduino, ou encore pour programmer un atmega sur une platine de test pour en faire un microcontrôleur arduino ou encore pour le programmer comme un microcontrôleur classique.

Dans tous les cas, il faut faire très attention aux tensions de fonctionnement des cartes et microcontrôleurs qui peuvent être 5v ou bien 3.3v pour ne pas risquer de détruire les composants.

Programmation d'un atmega328 sans intégrer le système Arduino avec une carte uno

IL est vivement conseillé de programmer un autre atmega328 avec la structure Arduino, puis de remplacer le microcontrôleur original de la carte uno par ce nouveau microcontrôleur. On fait une sauvegarde de microcontrôleur.

On peut maintenant utiliser cette carte uno en programmateur pour atmega328. Pour cela il suffit d'installer l'exemple ArduinoISP sur la carte uno. Ensuite après avoir connecté l'atmega328 à la carte uno via le bus SPI comme cela est expliqué sur les pages web citées auparavant. On peut utiliser avrdude avec l'option -c avrisp pour définir la carte uno avec ArduinoISP comme programmateur

Famille Arduino MKR

Description de la carte MKRZero

Image issue du site arduino.cc

La carte MKRZero fait partie de la famille MKR.

Elle est équipée d'un processeur SAMD21 Cortex®-M0+ 32bits. L'utilisation de cette carte nécessite d'installer les cartes MKR à partir de l'IDE.

Cette possède 22 broches numériques (0 à 21), dont certaines sont partagées avec les 7 entrées analogiques (A0 à A6), une liaison série (RX et TX), le bus SPI ainsi que le bus I2C qui est également disponible sur l'autre connecteur

Cette carte intègre un lecteur de carte mémoire Flash SD connecté sur le bus SPI avec la broche PA27 (définie avec la constante SDCARD_SS_PIN dans l'IDE arduino qui vaut 28) qui corrrespond au chipselect de la carte mémoire.

La LED LED_BUILTIN correspond au numéro 32.

Cette carte, comme la pluspart des cartes de la famille MKR intégre un connecteur pour batterie LIPO ainsi q'un chargeur de batterie.

Avec la famille MKR pour que les premiers messages de la fonction setup() soient affichés sur le terminal, il est important d'attendre la fin de la configuration de la liaison série avec Serial.begin() en la faisant suivre par la boucle : while (!Serial);.

Utilisation du bus SPI avec une manette de jeu

Caractéristiques de la manette de jeu

On utilise une manette de jeu DualShock compatible avec la console Playstation. Le connexion avec cette manette se fait en réalité en SPI. La manette fonctionne en 3.3V et peut donc être connectée avec une carte MKR en utilisant la documentation qui présente la connexion ainsi que le contenu des trames SPI.

ConnecteurArduino
1dataMISO10
2commandMOSI8
3alimentation vibration moteur
4GNDGND
53.3V+3.3V
6AttentionCS11 (par exemple)
7ClockSCK9
8Unknown
9Acknoledge

Le bus SPI, qui fonctionne en mode full-duplex, comprend une entête de trois octets suivi de plusieurs octets de données.

sensoctet 1octet 2octet 3
command0x01CMD0x00
data0xFF0xIL0x5A
avec
  • CMD : 0x42 pour une lecture des boutons, 0x43 pour entrer/sortir du mode configuration, 0x44 pour passer en mode analogique
  • I : I qui vaut 4 pour le mode numérique, 7 pour le mode analogique et F pour le mode configuration
  • L : qui représente le nombre de mots de 16 bits pour les données qui suivent

Exemples de trames SPI pour configurer le mode analogique et pour lire les données des boutons

Lecture des boutons (3 mots de données)

octets123456789
command0x010x420x000x000x000x000x000x000x00
data0xFF0x730x5AboutonsboutonsX droitY droitX gaucheY gauche
Les boutons sont répartis de la façon suivante :
bits76543210
octet 4gauchebasdroithautstartR3L3select
octet 5carrécroixcercletriangleR1L1R2L2
Le bit vaut 0 lorsque le bouton est activé, 1 au repos. Les valeurs analogiques vont de 0 à 255, avec 0 pour la position gauche ou haut.

Entrée et sortie du mode configuration

octets12345
command0x010x430x00Entrée/sortie0x00
data0xFF0x41 ou 0x71 ou 0xF10x5A0xFF0xFF
Avec Entrée/sortie qui vaut 0x01 pour entrer en mode configuration et 0x00 pour en sortir.
Le type de la valeur de retour dépend de l'état de la manette 0x41 en mode numérique, 0x71 en mode analogique, et 0xF1 pour la commande de sortie du mode configuration.

Configuration du mode analogique

octets123456789
command0x010x440x000x010x030x000x000x000x00
data0xFF0xF10x5A0x000x00

Programmation avec l'arduino

Il existe plusieurs librairies de gestion de la manette de jeu, ici on utilise la libraire PsxNewLib qui peut être installée à partir du gestionnaire de bibliothèque de l'IDE.

Dans un premier temps, on peut tester le bon fonctionnement avec l'exemple DumpButtonsHwSpi. Puis on peut écrire son propre code de test.

Voir l'exemple du codage du programme d'utilisation de la librairie PsxNewLib

Code source mkrZero_ps.ino


#include <PsxControllerHwSpi.h>

const byte PIN_PS2_ATT = 11;
PsxControllerHwSpi<PIN_PS2_ATT> psx;

void setup() {
  Serial.begin (115200);
  while (!Serial);
  psx.begin ();
  psx.enterConfigMode();
  psx.enableAnalogSticks();   
  psx.exitConfigMode();
  delay(50);
}

byte lx, ly, rx , ry;
uint16_t boutons;

void loop() {
  if (psx.read ()) {
      boutons = psx.getButtonWord ();
      Serial.print("tous les boutons : ");
      Serial.println(boutons,HEX);
      psx.getLeftAnalog (lx, ly);
      psx.getRightAnalog (rx, ry);
      Serial.print("analog gauche ");
      Serial.print(lx);
      Serial.print(" ");
      Serial.println(ly);
      Serial.print("analog droit ");
      Serial.print(rx);
      Serial.print(" ");
      Serial.println(ry);    
  }
  delay(100);
}
					

La classe PsxControllerHwSpi est une classe générique qui est configurée avec le numéro de la broche utilisée pour le signal CS.

On configure la manette en analogique.

A chaque lecture valide, on lit les valeurs des boutons sous la forme d'un entier de 16 bits ainsi que les valeurs des deux sticks, puis on affiche les valeurs à l'écran.

Les boutons sont répartis sur les 16 bits de l'entier :

bitbouton
15carré
14croix
13cercle
12triangle
11R1
10L1
9R2
8L2
7gauche
6bas
5droit
4haut
3start
2R3
1L3
0select
Voir l'exemple du codage du programme sans utiliser de librairie

Code source mkrZero_ps_2.ino


#include <SPI.h>

#define CS 11
#define DELAI_INTER_OCTETS_US 50
#define REPONSE_OK 0x5A
#define TAILLECMDLECTURE 9
#define TAILLECMDCONFIG 5
#define TAILLECMDANALOG 9

const byte cmdlecture[TAILLECMDLECTURE] = { 0x01 , 0x42 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 } ;
const byte cmdentremodeconfig[TAILLECMDCONFIG] = {0x01, 0x43, 0x00, 0x01, 0x00 } ;
const byte cmdsortmodeconfig[TAILLECMDCONFIG] = {0x01, 0x43, 0x00, 0x00, 0x00 } ;
const byte cmdanalogstick[TAILLECMDANALOG] = { 0x01, 0x44, 0x00, 0x01, 0x03 , 0x00 , 0x00 , 0x00 , 0x00 } ;

SPISettings PS2settings(250000UL, LSBFIRST, SPI_MODE3);
byte recus[21];

inline int calcultaille (const byte octet) {
  return (octet & 0x0f) * 2 + 3;
}

boolean PS2EchangeDonnees(const byte *commande,int taille,byte *octets) {
  boolean reponseOK = false ;
  SPI.beginTransaction(PS2settings); 
  digitalWrite(CS, LOW); 
  for(int i=0;i<taille;i+=1) {
    octets[i] = SPI.transfer(commande[i]);
    delayMicroseconds (DELAI_INTER_OCTETS_US); // tres important
  }
  digitalWrite(CS, HIGH); 
  SPI.endTransaction(); 
  reponseOK = (octets[2] == REPONSE_OK) ;
  return reponseOK ;
}

void affichereponse(char *message,byte *octets,int taille) {
  Serial.print(message);
  for(int i=0;i<taille;i+=1) {
    Serial.print(octets[i],HEX);
    Serial.print(" ");
  }
  Serial.println();
}

boolean PS2ConfigModeAnalog() {
  byte reponse[21];
  int taille;
  boolean configanalogOK;
  configanalogOK = PS2EchangeDonnees(cmdentremodeconfig,TAILLECMDCONFIG,reponse);
  if (!configanalogOK) {
    return false;
  }
  configanalogOK = PS2EchangeDonnees(cmdanalogstick,TAILLECMDANALOG,reponse);
  if (!configanalogOK) {
    return false;
  }
  configanalogOK = PS2EchangeDonnees(cmdsortmodeconfig,TAILLECMDCONFIG,reponse);
  if (!configanalogOK) {
    return false;
  }
  return configanalogOK;
}

void setup() {
  Serial.begin (115200);
  while (!Serial);
  pinMode(CS, OUTPUT); 
  digitalWrite(CS, HIGH); 
  SPI.begin();
  delay(50);
  PS2ConfigModeAnalog();
  delay(50);
}

void loop() {
 boolean fonctionnementOK = PS2EchangeDonnees(cmdlecture,TAILLECMDLECTURE,recus);
 if (fonctionnementOK) {   
    int nbrecus = calcultaille(recus[1]) ;
    affichereponse("boutons ",recus,nbrecus);
  }
  delay(100);
}
					

Les différentes trames SPI utilisées sont définies comme constantes. Il ne faut pas oublier de configurer le bus SPI avec le bit de poids faible en premier (LSBFIRST), une fréquence de 250kHz et le mode 3.

La transmission d'une trame de fait à partir du tableau des valeurs sans oublier le délai entre chaque octet qui est très important, si le délai est trop court, l'échange ne se fait pas. Cette valeur est inspirée de la librairie utilisée auparavant.

Le nombre d'octets total est calculé à partir du poids faible de l'octet 1 de la réponse, la valeur reçue indique le nombre de mots de 16 bits de données, il faut donc multiplier par 2 cette valeur et ajouter les 3 octets de l'entête pour avoir la taille totale de la trame reçue en octets.

L'intégrité de la trame reçue est vérifiée avec la présence de la valeur 0x5A qui est commune à toutes les trames reçues.

Le buffer de réception est défini avec 21 octets, car il semble que c'est le nombre maximal d'octet envoyé par la manette.

La boucle réalise l'échange de données, si la réponse est valide, elle affiche les valeurs des boutons et des stiks.

Utilisation du GPS et de la carte micro SD

Coordonnées GPS

Le GPS fournit les coordonnées géographiques de la forme M(λ,φ,h)λ représente la longitude, φ la latitude et h l'altitude.

M0 est la projection orthogonale du point M à l'altitude 0 (niveau de la mer). L'arc rouge représente l'équateur c'est à dire la latitude 0, l'arc vert représente la longitude du point M, l'arc bleu représente la latitude du point M.

Il existe plusieurs systèmes de coordonnées, dont le système de coordonnées cartésiennes nommées coordonnées géocentriques avec M(X,Y,Z) où les composantes (X,Y,Z) représentent les coordonnées du point M dans le repère orthonormé O,x,y,z avec O centre de la terre. Le plan O,x,y se situe dans le même plan que l'équateur.

Ces systèmes de coordonnées nécessitent un modèle de représentation de la terre qui n'est pas une sphère parfaite. Il existe plusieurs modèles de représentation dont le système WSG84 qui correspond à une ellipsoïde aplatie avec un demi grand axe a=6 378 137 m et un facteur d'aplatissement f=1/298,257 223 563.

{ X = ( N + h ) cos ( ϕ ) cos ( λ ) Y = ( N + h ) cos ( ϕ ) sin ( λ ) Z = ( N ( 1 - e 2 ) + h ) sin ( ϕ )
avec
N = a 1 - e 2 sin 2 ( ϕ )
et
e 2 = f ( 2 - f )

Les valeurs x,y,et z sont très grandes. Ce qui fait que pour calculer les distances entre points à la surface de la terre, on utilise un système cartésien local à partir d'un point de référence sur la surface de la terre comme, par exemple, le point M0, en effectuant également une rotation du repère pour qu'il soit mieux adapté à la représentation locale comme, par exemple, le plan M0,x,y qui est tangent la surface de la terre en M0 et z qui représente l'altitude.

Carte MKRGPS

La carte MKRGPS est équipée d'un composant GPS u-blox SAM-M8Q avec une interface I2C et série. Une bibliothèque Arduino_MKRGPS peut être installée avec l'IDE. Elle offre l'essentiel des fonctionnalités nécessaires à l'acquisition de données GPS.

Pour obtenir des données supplémentaires, il est possible d'utiliser directement les données NMEA décrites dans la documentation u-blox.

Gestion de la carte SD

La gestion de la carte SD est assurée en natif par la bibliothèque SD, qui comprend les classes Sd2Card, SdVolume, SdFile ou File et l'objet SD de la classe SDClass.

On peut déjà vérifier la présence ainsi que les caractéristiques de la carte SD avec l'exemple cardinfo. Il faut juste adapter la valeur de chipSelect = SDCARD_SS_PIN pour la carte MKRZero.

Voir l'affichage du résultat de cardinfo

cardinfo affiche

Initializing SD card...Wiring is correct and a card is present.

Card type:         SDHC
Clusters:          243712
Blocks x Cluster:  64
Total Blocks:      15597568

Volume type is:    FAT32
Volume size (Kb):  7798784
Volume size (Mb):  7616
Volume size (Gb):  7.44

Files found on the card (name, date and size in bytes): 
DOCUME~1/     2024-01-10 16:49:42
  TENSIO~1.TXT  2023-12-09 12:28:18 228
CONFIG/       2024-01-10 16:46:48
  SITE~1.CON    2024-01-10 16:46:48 18
					

La carte SD est une carte de 8GB qui contient

.
+-- config
|   +-- site.conf
+-- documents
    +-- tensions_leds.txt						
					

L'affichage précise qu'il s'agit d'une carte de type SDHC de 243712 cluster de 64 blocs soit 15597568 blocs au total.

La carte est formatée en FAT32.

La taille peut être calculée en prenant en compte que l'on a des blocs de 512 octets, ce qui fait 512 × 15597568 = 7985954816 octets = 7798784 ko = 7616 Mo = 7,4375 Go

Pour les noms de fichiers la bibliothèque ne gère pas les noms long "vfat" mais seulement les noms courts avec 8 caractères pour le nom du fichier et 3 caractères pour l'extension. C'est le format MSDOS qui est utilisé par windows, mais qui est totalement transparent avec vfat.

L'objet SD de la classe SDClass offre l'essentiel des fonctionnalités de gestion de la carte SD qui permet de gérer les répertoires et fichiers.

La classe File gère l'accès aux fichiers :

Lors de l'écriture d'une donnée dans un fichier, il faut toujours ouvrir, écrire et fermer. Seule la fermeture termine l'écriture des données. En cas d'arrêt du processeur, si le fichier n'a pas été fermé, les données sont perdues.

Enregistrement de coordonnées GPS

On propose d'écrire un programme qui enregistre les données GPS dans un fichier csv, les champs seront la latitude, la longitude, l'altitude et le nombre de satellites. Toutes les données enregistrées seront faites à partir d'une point fixe.

Dispersion autour d'un point fixe

Les données géographiques sont converties en données cartésiennes locales à l'aide de l'outil CartConvert de la librairie libre geographiclib. L'origine des coordonnées locales est la valeur moyenne des valeurs. Cette valeur ne correspond absolument pas à la position réelle exacte, elle est uniquement utilisée pour afficher la dispersion des données, qui montre la précision de ce type de GPS.

Toutes les données sont incluses dans un cercle de rayon 10m qui est une valeur de précision classique. Ces résultats sont obtenus avec un nombre de satellites compris entre 5 et 7.

Pour faire des mesures de dispersion autour d'un point fixe réel, il est possible d'utiliser une borne géodésique identifiable sur cette carte où chaque point donne accès à une fiche qui fournit toutes les informations relative à cette borne.

Voir l'exemple du codage du programme d'enregistrement de données GPS

Code source mkrZero_gps.ino


#include <SD.h> 
#include <Arduino_MKRGPS.h>
 
#define SD_CS SDCARD_SS_PIN
#define GPSERR 0
#define SDERR 1 

int EcritDonnees(char *ligne) {
  File fd = SD.open("donnees.csv", FILE_WRITE);
  if (fd) {
    fd.println(ligne);
    fd.close();
  }
}

String sLongitude, sLatitude, sAltitude;
char message_donnees[100]; 

void setup() {
  pinMode(GPSERR,OUTPUT);
  pinMode(SDERR,OUTPUT);
  digitalWrite(GPSERR,LOW);
  digitalWrite(SDERR,LOW);
  if (!GPS.begin()) {
    digitalWrite(GPSERR,HIGH);
    while (1);
  }
  if (!SD.begin(SD_CS)) {
    digitalWrite(SDERR,HIGH);
    while (1);
  }  
}

void loop() {
  if (GPS.available()) {
    float latitude   = GPS.latitude();
    float longitude  = GPS.longitude();
    float altitude   = GPS.altitude();
    int satellites = GPS.satellites(); 
    sLatitude = String(latitude,4);
    sLatitude = String(latitude,9);
    sLongitude = String(longitude,9);
    sprintf(message_donnees,"%s;%s;%s;%d",sLatitude.c_str(),
           sLongitude.c_str(),
           sAltitude.c_str(),satellites);
    EcritDonnees(message_donnees);
  }
}
					

Fonctionnement du programme

Dans la fonction setup en cas d'erreur d'initialisation du GPS ou de la carte SD, on allume une LED et termine le programme avec la boucle while infinie. Ce qui a pour effet de ne jamais exécuter la fonction loop. Dans la boucle loop, on convertit les nombres réels en chaîne de caractère en précisant le nombre de chiffres, puis on construit la ligne de données avec la fonction sprintf. C'est cette ligne qui est enregistrée dans le fichier.

Programmation d'une carte de la famille MKR

Si une carte MKR n'est plus accessible via le port USB, il est possible de la reprogrammer via son interface SWD en utilisant la librairie Adafruit DAP. Il faut respecter la procédure décrite.