Gérer la communication I2C entre un Raspberry Pi et un capteur grâce à Wiring Pi

Exemple d’application avec un capteur de température et d’humidité SI7021

michel.semal

J'ai décidé de créer ce tutoriel, suite à mes recherches pour faire communiquer mon Raspberry Pi 3 et un module SI7021 en I^2C. Ne trouvant rien de probant, j'ai dû rassembler des morceaux d'informations et de code sur le Net. J'ai ensuite mis le tout ensemble et je vous livre le fruit de mes recherches.

Commentez

- Titre : Gérer la communication I2C entre un Raspberry Pi et un capteur grâce à Wiring Pi

- Auteur : michel.semal

- Parution : 23 janvier 2019

- Licence : Copyright ® 2019 Michel Semal. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- À la découverte de la Raspberry Pi

- Raspberry Pi 3 - Découverte et mise en route

- Raspberry Pi 3 - Préparation du système

- Raspberry Pi 3 - Choisir son système d'exploitation

Sommaire

- I. Matériel

- II. Préliminaires

- III. Liaison série I2C

- IV. Configuration Raspberry Pi

- V. Le capteur SI7021

- VI. Séquence de lecture

- VII. Le logiciel

- VII-A. Les bibliothèques

- VII-B. La programmation

- VIII. Le code complet

- IX. La compilation

- X. Le résultat

- XI. Annexe : version Arduino

- XII. Conclusion

- XIII. À propos de l'auteur

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 I^2C 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'I^2C ? En me documentant sur les divers protocoles de communication existants, l'I^2C 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'I^2C 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’I^2C 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'I^2C. On vérifie que sa configuration accepte l'I^2C. 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'I^2C du Raspberry, et on teste la présence de ce capteur (ou de tout autre module I^2C 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 I^2C 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’I^2C 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 I^2C, malheureusement l'adresse I^2C 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 :

Page 18 de la datasheet – description des commandes

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 I^2C 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 :

1. 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 ;

2. 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,

, ensuite :

pour l'humidité (RH) : (datasheet page 21) et
pour la température (TT) : (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 I^2C.

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 (I^2C, 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 :

1. Créer un file descriptor pour le bus I^2C ;

2. Envoyer la commande vers le module I^2C ;

3. Faire la lecture des données du module ;

4. Libérer le bus I^2C 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 VI.

1. Création du file descriptor :

const char SI7021addr = 0x40 ;
int fd ;
fd = wiringPiI2CSetup (SI7021addr);

2. Explication : wiringPiI2CSetup (librairie wiringPiI2C.h) initialise le bus I^2C ; la fonction retourne -1 si l'initialisation a échoué sinon, retourne une valeur 'int' pour le file descriptor ;

3. Envoi de la commande au capteur :

#define Measure_cmd 0xF3
if (wiringPiI2CWrite (fd, Measure_cmd) < 0)
return (-1) ;

Explication : wiringPiI2CWrite (bibliothèque wiringPiI2C.h) écrit une valeur sur le bus I^2C.
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 ;

4. 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 :

while ((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) ;

5. Il reste à libérer le bus I^2C 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'I^2C et du SI7021.

main.cpp

#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;
}
}

SI7021.h

#ifndef _SI7021
#define _SI7021
extern float MesTT();
extern float MesRH();
#endif

SI7021.cpp

#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("\nMSB  = %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("\nMSB  = %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'.

SI7021.ino

#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("\tRH =");
Serial.println(RH);

Serial.println() ;
delay(10000) ;
}

SI7021.cpp

#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("\tHeater = ");
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);
}

SI7021.h

#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