I. Matériel▲
Le matériel mis en œuvre ici se compose de :
- Raspberry Pi 3+ ;
- Linux Raspbian : Linux raspberryPi 4.14.71-v7+ #1145 SMP armv7l GNU/Linux ;
- Module I2C SI7021 (mesure de température et d’humidité).
II. Préliminaires▲
J'utilise le Raspberry avec des connexions SSH depuis mon PC qui tourne sous Windows 10, Putty pour avoir une console, et VNC Viewer pour éditer mes fichiers directement avec Geany sur le Raspberry.
Geany est un éditeur fourni avec Raspbian qui est très léger et très pratique pour concevoir de petits modules de test en mode console. Il permet évidemment de concevoir des programmes plus évolués, mais devient vite limité surtout en édition de code. Cependant, pour créer du code de test, il est, je trouve, très pratique. Il gère des projets de manière très simple et ne crée pas de répertoires improbables comme Qt (que j'utilise dans la version graphique de ma station météo).
III. Liaison série I2C▲
Pourquoi l'I2C ? En me documentant sur les divers protocoles de communication existants, l'I2C me semble convenir au mieux à mon problème. À savoir, peu de connectique (le SPI demande trois fils, deux pour la communication, un pour la sélection de l'esclave ). L'I2C fonctionne en mode maître-esclave et chaque périphérique connecté sur le bus est adressable avec une adresse unique par périphérique, donc deux fils suffisent.
Le Raspberry Pi, tout comme l'Arduino d'ailleurs, fournit directement les sorties nécessaires (SCL et SDA). Le câblage est donc des plus simple. Schéma du montage :
Beaucoup de modules doivent être alimentés en 3,3 V et non en 5 V sous peine de destruction (tension de 3,3 V fournie par le Raspberry et l'Arduino).
La norme pour l’I2C conseille aussi de mettre des résistances pull-up sur les lignes SDA et SCL. Sur le module dont je dispose, des résistances de pull-up sont intégrées en surface de la carte. Si plusieurs modules sont branchés sur le même bus, il faut vérifier la présence ou l'absence de ces résistances.
IV. Configuration Raspberry Pi▲
Avant de commencer, il faut être sûr que le Raspberry est configuré pour utiliser l'I2C. On vérifie que sa configuration accepte l'I2C. Pour cela, on ouvre l’utilitaire raspi-config avec la commande :
sudo raspi-config
On choisit l'option 5 :
Puis « P5 I2C » :
On répond évidemment « Oui », et la réponse est :
On quitte l'application, on redémarre le Raspberry, et on teste la communication. Pour cela : on se procure donc ledit capteur (SI7021), la datasheet de ce capteur, on connecte le capteur sur l'I2C du Raspberry, et on teste la présence de ce capteur (ou de tout autre module I2C présent sur le bus).
Test de l'installation :
i2cdetect -y 1
Si tout est normal, on obtient donc l'adresse du capteur (fixée à 0x40 pour le SI7021). Dans l'image ci-dessus, j'ai deux modules I2C connectés sur le bus, donc, deux réponses (0x40 pour le SI7021 et 0x20 pour un MCP23017). S'il n'y a pas de module détecté à l'adresse 0x40, il faut vérifier la configuration de l’I2C sur le Raspberry et le câblage du module SI7021. Attention à l'alimentation du module qui doit être de 3,3 V.
V. Le capteur SI7021▲
Je cherchais un capteur bon marché pour ma future station météo. De mon expérience avec les capteurs d'humidité, ils ont tous tendance à dériver dans le temps. Si on paie un capteur plusieurs centaines d'euros, il vaut mieux prévoir un système de calibration. Si on le paie quelques euros, on peut le remplacer (voir garder une référence pour un contrôle de la dérive).
C'est ce dernier mode que j'ai choisi, donc il me faut communiquer avec le capteur.
Ce capteur ne fonctionne qu'en I2C, malheureusement l'adresse I2C n'est pas modifiable. Cependant, c'est le seul capteur que j'ai trouvé qui fournisse une vraie mesure de l'humidité relative proche des standards météo, il est monté sur un circuit et est protégé par une membrane qui laisse passer la vapeur d'eau, mais pas les poussières et autres polluants.
Humidité relative : précision de +/- 3 % dans la gamme de 0-80 %.
Température : précision inférieure à 0.4 °C dans la gamme de -10 °C à +85 °C.
Le protocole de communication est décrit dans la datasheet.
Les commandes sont les suivantes :
La commande pour lire le buffer de la température est 0xF3 et pour l'humidité 0xF5. Le mode de ces commandes est le mode NO_HOLD_MASTER_MODE, ce qui signifie que le capteur ne bloque pas le bus I2C pendant la mesure et la conversion en mots de 8 bits. Le capteur signale que les données ne sont pas encore disponibles et le master doit relancer la lecture pour récupérer les données quand la conversion est terminée.
Dans le mode, HOLD_MASTER_MOD, le capteur démarre une séquence de mesure de la température ou de l'humidité et réalise la conversion en mots de 8 bits. Mais durant le cycle de mesure et de conversion, l’horloge (SCL) est maintenue au niveau bas par le module pour bloquer le bus, dès que la séquence de mesure est terminée , l’horloge est libérée et les données sont récupérées dans le buffer.
VI. Séquence de lecture▲
Dans les tableaux ci-dessous, la partie blanche est le code envoyé par le Master tandis que la partie grisée est la réponse du Slave.
La datasheet nous dit :
On lit cela comme suit :
Le master envoie une condition Start ('S'), puis l'adresse du module (soit 0x40 sur 7 bits) et enfin 'W' (bit à 0) pour Write. Traduit en français, cela donne : départ de la trame, on envoie l'adresse du module qui doit écouter et on prévient que l'on va écrire sur la ligne.
Le module répond par 'A' :
qui est le bit d’acquittement (Acknowledge ou ACK, en clair, « d'accord » ou « bien reçu »).
On trouve alors la séquence suivante :
qui signifie que le master envoie la commande de lecture (température ou humidité) et que le module acquitte la commande (ACK).
On ordonne au module de passer en mode réponse avec la séquence de commande 'Sr ' qui signifie 'Start repeat', suivi de l'adresse du capteur, et enfin du passage du Master en mode écoute 'R' (Read) :
Si le module est prêt à répondre, il acquitte (ACK) et retourne le premier octet de donnée (l’octet de poids fort). Si les données ne sont pas prêtes, le maître doit boucler sur la demande jusqu'à obtention du ACK :
Si le Master reçoit bien l’octet, il acquitte (ACK) :
Le module envoie alors le second octet de donnée (octet de poids faible) :
Deux possibilités dès lors :
- On a tout reçu et le maître envoie un NACK (non-acquittement) suivi de 'P' (Stop) pour l'arrêt de la communication ;
- On veut le checksum, le maître acquitte (ACK) et reçoit un troisième octet de donnée qui est le checksum et on termine alors la communication comme au point 1.
Il apparaît donc qu'il faut lire deux octets à la suite pour obtenir une mesure. Un troisième octet serait disponible pour un checksum. Dans la version de mon logiciel, je demanderai le checksum pour l'afficher à l'écran après l'affichage des deux octets de mesure.
On reçoit d'abord le MSB (Most Significant Byte ou octet de poids fort), ensuite le LSB (Least Significant Byte ou octet de poids faible), le code mesure doit donc être calculé de la sorte,
kitxmlcodeinlinelatexdvpCode\_Measure = MSB \times 256 + LSBfinkitxmlcodeinlinelatexdvp, ensuite :
pour l'humidité (RH) : kitxmlcodeinlinelatexdvp\%\;RH= \frac{125 \times Code\_Measure}{65536}-6finkitxmlcodeinlinelatexdvp (datasheet page 21) et
pour la température (TT) : kitxmlcodeinlinelatexdvp^{o}C\;TT= \frac{175.72\times Code\_Measure}{65536}-46.85finkitxmlcodeinlinelatexdvp (datasheet page 22).
VII. Le logiciel▲
Le Raspberry configuré, le SI7021 bien compris, on peut commencer à coder.
L'idée est de faire un petit logiciel en mode console qui ira lire les données de température et d’humidité sur un capteur SI7021.
Une fois le code au point en mode console, on pourra l'intégrer dans un projet plus conséquent.
VII-A. Les bibliothèques▲
De nombreuses bibliothèques existent pour gérer la communication I2C.
Pour ma part, j'ai finalement opté pour wiringPi, car d'après mes recherches sur Internet, c'est une librairie relativement générique (I2C, SPI, GPIO…) et assez simple d'emploi. Pour l'installation, voir le site :
http://wiringpi.com/download-and-install/.
Le site est en anglais, mais assez simple à suivre, et l’auteur de la bibliothèque, Gordon Henderson, répond à vos courriers (merci à lui).
VII-B. La programmation▲
Donc la gestion du SI7021 fait partie d'un tout plus vaste. Pour faciliter mon futur, je développe chaque morceau dans des fichiers séparés et en mode console, d’où la structure suivante :
- main.cpp : qui contient l'appel aux fonctions de lecture et la gestion de l'affichage ;
- SI7021.cpp : qui contient la définition des fonctions de lecture ;
- SI7021.h : qui contient les prototypes des fonctions.
Il est nécessaire de faire les #include suivants dans le .cpp principal et dans le fichier SI7021.cpp :
#include <wiringPiI2C.h>
#include <wiringPi.h>
Les étapes de la programmation pour la lecture de données depuis un capteur sont les suivantes :
- Créer un file descriptor pour le bus I2C ;
- Envoyer la commande vers le module I2C ;
- Faire la lecture des données du module ;
- Libérer le bus I2C via le file descriptor.
Un exemple pour la lecture de la température d'un module SI7021. La programmation suit la séquence de lecture décrite au chapitre VISéquence de lecture.
-
Création du file descriptor :
Sélectionnezconst
char
SI7021addr=
0x40
;int
fd ; fd=
wiringPiI2CSetup(
SI7021addr); -
Explication : wiringPiI2CSetup (librairie wiringPiI2C.h) initialise le bus I2C ; la fonction retourne -1 si l'initialisation a échoué sinon, retourne une valeur 'int' pour le file descriptor ;
-
Envoi de la commande au capteur :
Sélectionnez#define Measure_cmd 0xF3
if
(
wiringPiI2CWrite(
fd, Measure_cmd)<
0
)return
(-
1
) ;Explication : wiringPiI2CWrite (bibliothèque wiringPiI2C.h) écrit une valeur sur le bus I2C.
Measure_cmd vaut 0xF3 pour la température et vaut 0XF5 pour l'humidité en No Hold Master Mode pour le module SI7021. Si la commande échoue, on sort de la fonction avec le code -1 ; -
Lecture des données du module.
En réponse à la commande Measure_cmd, le capteur SI7021 renvoie deux octets pour les mesures. Il faut donc dire à la fonction de lecture que l'on attend deux octets. Cela se fait avec la commande :Sélectionnezwhile
((
read(
fd, data,2
)<
0
)) delay(
10
) ;Explication : la fonction read() lit un octet sur le bus I 2C (c'est une fonction C d’Unix, de la bibliothèque unistd.h). La fonction renvoie le nombre d’octets lus et lorsqu'il n'y a plus d’octets à lire, elle retourne 0 (= EOF). En cas d'erreur, la fonction retourne -1 (cf. man read
fd est le file descriptor data est un tableau de uint8_t qui contiendra les données lues
le '2' est le nombre d’octets attendus à lire. Si on désire lire le checksum, il suffit de mettre un 3 à la place du 2, et évidemment prévoir de la place dans le tableau.
Le SI7021 renvoie deux octets, le premier est l’octet de poids fort MSB (Most Significant Byte) codé sur 8 bits, tandis que le second est l’octet de poids faible LSB (Least Significant Byte) avec la donnée codée sur 6 bits (les deux derniers sont à 00 pour la température et à 10 pour l'humidité, voir la datasheet du SI7021) ; - Il reste à libérer le bus I2C via son file descriptor :
close
(
fd);
VIII. Le code complet▲
Dans mon test, j'ai créé une temporisation de 10 secondes. Donc, toutes les 10 secondes une nouvelle mesure RH et TT est effectuée avec la lecture du checksum. L'affichage se fait par groupe de quatre données, le MSB, le LSB, les CHK et la mesure.
Je n'y ai pas mis non plus de gestion d'erreur. C'est un code de test pour comprendre le fonctionnement de l'I2C et du SI7021.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <time.h>
#include <wiringPiI2C.h>
#include <wiringPi.h>
#include "SI7021.h"
int
main
(
int
argc, char
**
argv)
{
time_t nbSec;
float
TT,RH;
long
Tps0,TpsNow;
/**
******************
* INTIALISATION
* ******************
*/
if
(
wiringPiSetup
(
) ==
-
1
)
return
(
EXIT_FAILURE);
time
(&
nbSec);
TpsNow =
(
long
)nbSec;
Tps0 =
TpsNow-
9
;
for
(
;;) {
if
(
TpsNow >
Tps0+
10
) {
TT =
MesTT
(
);
printf
(
"
Temp = %.2f
\n
"
,TT);
RH =
MesRH
(
);
printf
(
"
Hum = = %.2f
\n
"
,RH);
Tps0 =
TpsNow;
}
time
(&
nbSec);
TpsNow =
(
long
)nbSec;
}
}
#ifndef _SI7021
#define _SI7021
extern
float
MesTT
(
);
extern
float
MesRH
(
);
#endif
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <time.h>
#include <wiringPiI2C.h>
#include <wiringPi.h>
#include "SI7021.h"
#define Measure_TT 0xF3
#define Measure_RH 0xF5
const
char
SI7021addr =
0x40
; // base address
float
MesTT
(
)
{
int
fd;
float
cTemp;
uint8_t data [5
];
fd =
wiringPiI2CSetup (
SI7021addr);
if
(
wiringPiI2CWrite (
fd, Measure_TT) <
0
)
return
(-
1
);
while
((
read (
fd, data, 3
) <
0
))
delay (
10
) ;
printf
(
"
\n
MSB = %d
\n
"
,data[0
]);
printf
(
"
LSB = %d
\n
"
,data[1
]);
printf
(
"
CHK = %d
\n
"
,data[2
]);
cTemp =
(((
data [0
] *
256
+
data [1
]) *
175
.72
) /
65536
.0
) – 46
.85
;
close
(
fd);
return
(
cTemp);
}
float
MesRH
(
)
{
int
fd;
float
humidity;
uint8_t data [2
] ;
fd =
wiringPiI2CSetup
(
SI7021addr);
if
(
wiringPiI2CWrite (
fd, Measure_RH) <
0
)
return
(-
1
);
while
((
read (
fd, data, 3
) <
0
))
delay (
10
) ;
printf
(
"
\n
MSB = %d
\n
"
,data[0
]);
printf
(
"
LSB = %d
\n
"
,data[1
]);
printf
(
"
CHK = %d
\n
"
,data[2
]);
humidity =
(((
data[0
] *
256
+
data[1
]) *
125
.0
) /
65536
.0
) – 6
;
/**
*** l'humidité ne pouvant être supérieure à 100% ***
*/
if
(
humidity >=
100
.0
)
humidity =
99
.9
;
close
(
fd);
return
(
humidity);
}
IX. La compilation▲
Lors de la compilation, ne pas oublier de mettre la commande -l wiringPi dans la commande de compilation.
Comme je débute en C avec Linux, je m'intéresse aussi à make. Donc pour compiler les fichiers, j'ai créé le fichier makefile suivant :
#SI7021
MesTT:
main.o SI7021.o
gcc -Wall -o MesTT main.o SI7021.o -l wiringPi
main.o:
main.cpp SI7021.h
gcc -c -Wall -o main.o main.cpp -l wiringPi
SI7021.o :
SI7021.cpp SI7021.h
gcc -c -Wall -o SI7021.o SI7021.cpp -l wiringPi
Ce fichier fait aussi partie de mon étude de Linux, donc n'est pas optimisé, mais il me permet de comprendre mieux qu'un fichier contenant des signes cabalistiques curieux ($@, $^, $>, $() ,……) :-).
J'y viendrais sûrement, mais là, ce fichier minimal me convient très bien.
X. Le résultat▲
La compilation donne le résultat suivant :
Le code compilé donne le résultat suivant sur une console :
XI. Annexe : version Arduino▲
Pour les utilisateurs d’Arduino, voici le code basé sur la même structure.
SI7021.cpp contient trois fonctions :
- int litregistre() qui lit les deux registres utilisateurs du module SI7021 ;
- double ReadTT(int mode) qui lit la température dans le mode spécifié par la variable 'mode' ;
- double ReadRH(int mode) qui lit l'humidité dans le mode spécifié par la variable 'mode'.
#include <Wire.h>
#include "SI7021.h"
const
int
ADDR =
0x40
;
void
setup
(
) {
Serial.begin
(
115200
) ;
Wire.begin
(
) ;
delay
(
100
) ;
Wire.beginTransmission
(
0x40
) ;
Wire.endTransmission
(
) ;
pinMode
(
13
,OUTPUT) ;
litregistre
(
);
}
void
loop
(
) {
double
TT,RH;
// 0xFx demande le mode NO_HOLD_MASTER
// 0xEx demande le mode HOLD_MASTER
RH =
ReadRH
(
0xE5
);
TT=
ReadTT
(
0xF3
);
Serial.print
(
"
Temp =
"
);
Serial.print
(
TT);
Serial.print
(
"
\t
RH =
"
);
Serial.println
(
RH);
Serial.println
(
) ;
delay
(
10000
) ;
}
#include "SI7021.h"
#include "Arduino.h"
#include<Wire.h>
int
litregistre
(
)
{
uint8_t reg1,reg2;
Wire.beginTransmission
(
0x40
);// Départ I2C
Wire.write
(
0xE7
);//Sélection du registre "User reg1"
Wire.endTransmission
(
false
);// Stop I2C
Wire.requestFrom
(
0x40
, 1
);// Demande 1 octet de données
while
(
Wire.available
(
)){
reg1 =
Wire.read
(
);
}
Wire.beginTransmission
(
0x40
);// Départ I2C
Wire.write
(
0x11
);// Sélection du registre "Heater reg"
Wire.endTransmission
(
false
);// Stop I2C
Wire.requestFrom
(
0x40
, 1
);// Demande 1 octet de données
while
(
Wire.available
(
)){
reg2 =
Wire.read
(
);
}
Serial.print
(
"
reg1 =
"
);
Serial.print
(
reg1,BIN);
Serial.print
(
"
\t
Heater =
"
);
Serial.println
(
reg2,BIN);
return
(
0
);
}
double
ReadTT
(
int
mode)
{
int
data[5
];
double
cTemp;
int
i =
0
;
unsigned
long
dtr,dacq,dstart;
Wire.beginTransmission
(
0x40
);// Départ de l'I2C
Wire.write
(
mode);// Définit le mode F5 = RH NO_HOLD_MASTER E5 = HOLD_MASTER
Wire.endTransmission
(
);// Stop I2C
delay
(
10
);// Nécessaire pour la conversion en mode F3
Wire.requestFrom
(
0x40
, 3
);// Demande 3 octets de données
while
(
Wire.available
(
)) {
data[i++
] =
Wire.read
(
);// Lit 1 octet de données
}
cTemp =
((((
double
)data [0
] *
256
+
(
double
)data [1
]) *
175
.72
) /
65536
.0
) -
46
.85
;
return
(
cTemp);
}
double
ReadRH
(
int
mode)
{
uint8_t data[5
];
double
cRh;
int
i=
0
;
Wire.beginTransmission
(
0x40
);// Départ I2C
Wire.write
(
mode);// Définit le mode F5 = RH NO_HOLD_MASTER E5 = HOLD_MASTER
Wire.endTransmission
(
false
);// Stop I2C
delay
(
10
);// Nécessaire pour la conversion en mode F3
Wire.requestFrom
(
0x40
, 3
);// Demande 3 octets de données
while
(
Wire.available
(
)) {
data[i++
] =
Wire.read
(
);// lit 1 octet de données
}
cRh =
(
double
)data[0
]*
256
.0
;
cRh +=
(
double
)data[1
];
cRh *=
125
.0
;
cRh /=
65536
.0
;
cRh -=
6
.0
;
if
(
cRh >
100
.0
) cRh =
99
.9
;
if
(
cRh <
1
) cRh =
-
99
.9
;
return
(
cRh);
}
#ifndef __SI7021
#define __SI7021
double
ReadTT
(
int
);
double
ReadRH
(
int
);
int
litregistre
(
);
#endif
XII. Conclusion▲
Ce tutoriel est le fruit de nombreuses heures de recherche sur le Net, d'écriture de code, de râles et d'autres encore. J'espère qu'il vous aura aidé.
Ce tutoriel n'est sûrement pas la vérité divine, mais simplement ma solution pour résoudre un problème. Mes problèmes restent nombreux, mais je ne suis pas sûr qu'ils feront tous l'objet d'un tutoriel (quoi que…).
Merci à f-leb pour ses avis et conseils d’amélioration, ainsi qu’à jacques_jean pour sa relecture orthographique et grammaticale.
XIII. À propos de l'auteur ▲
Je suis électronicien, ma formation scolaire s'est achevée en 1982, pour laisser la place à la formation professionnelle. J'ai un (très) long parcours de réparation/conception/installation de diverses plates-formes électroniques (de 1982 à aujourd'hui).
L'informatique a toujours été une passion (j'ai commencé le C en 1985 avec un Amiga 500).
Je ne code sûrement pas de la meilleure manière (mais j'ai un lourd passé en VBA). Aussi, si vous voulez améliorer mes codes, ne vous gênez pas.
Michel