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 est un ensemble matériel et logiciel qui peut être défini par
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.
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.
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.
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
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 :
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.
La gestion des GPIOs se fait en deux étapes :
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.
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++.
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.
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 fonctions | 1182 | 27 |
avec classe | 1250 | 43 |
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.
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 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.
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
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.
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.
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 :
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.
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 :
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.
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);
}
}
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);
}
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.
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.
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.
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.
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
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);.
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.
Connecteur | Arduino | ||
---|---|---|---|
1 | data | MISO | 10 |
2 | command | MOSI | 8 |
3 | alimentation vibration moteur | ||
4 | GND | GND | |
5 | 3.3V | +3.3V | |
6 | Attention | CS | 11 (par exemple) |
7 | Clock | SCK | 9 |
8 | Unknown | ||
9 | Acknoledge |
Le bus SPI, qui fonctionne en mode full-duplex, comprend une entête de trois octets suivi de plusieurs octets de données.
sens | octet 1 | octet 2 | octet 3 |
---|---|---|---|
command | 0x01 | CMD | 0x00 |
data | 0xFF | 0xIL | 0x5A |
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)
octets | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
command | 0x01 | 0x42 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 |
data | 0xFF | 0x73 | 0x5A | boutons | boutons | X droit | Y droit | X gauche | Y gauche |
bits | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
octet 4 | gauche | bas | droit | haut | start | R3 | L3 | select |
octet 5 | carré | croix | cercle | triangle | R1 | L1 | R2 | L2 |
Entrée et sortie du mode configuration
octets | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
command | 0x01 | 0x43 | 0x00 | Entrée/sortie | 0x00 |
data | 0xFF | 0x41 ou 0x71 ou 0xF1 | 0x5A | 0xFF | 0xFF |
Configuration du mode analogique
octets | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
command | 0x01 | 0x44 | 0x00 | 0x01 | 0x03 | 0x00 | 0x00 | 0x00 | 0x00 |
data | 0xFF | 0xF1 | 0x5A | 0x00 | 0x00 |
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.
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 :
bit | bouton |
---|---|
15 | carré |
14 | croix |
13 | cercle |
12 | triangle |
11 | R1 |
10 | L1 |
9 | R2 |
8 | L2 |
7 | gauche |
6 | bas |
5 | droit |
4 | haut |
3 | start |
2 | R3 |
1 | L3 |
0 | select |
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.
Le GPS fournit les coordonnées géographiques de la forme M(λ,φ,h) où λ 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.
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.
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.
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.
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.
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.
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.
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.