TP 7 — Le bus I²C : température, accéléromètre et gyroscope
Objectifs
comprendre le bus I²C : deux fils, des adresses, des registres ;
trouver les composants présents sur un bus, et vérifier leur identité ;
lire le capteur de température de la carte et convertir sa mesure ;
configurer et lire une centrale inertielle (accéléromètre et gyroscope).
Durée indicative : deux séances de 3 h. Prérequis : TP 1 — Allumer une LED, lire un bouton.
Ce qu’il faut savoir
Le bus I²C
L’I²C relie un maître (le microcontrôleur) à plusieurs esclaves (les capteurs) par deux fils seulement : SCL, l’horloge, que le maître impose, et SDA, les données, dans les deux sens. Les deux lignes sont tirées au +3,3 V par des résistances, et chaque composant ne sait que les mettre à 0 : c’est ce qui permet à plusieurs composants de partager les mêmes fils.

Le bus I²C2 : deux fils partagés, tirés au +3,3 V.
Chaque esclave a une adresse sur 7 bits. Une transaction commence par l’adresse : seul le composant qui la reconnaît répond, en acquittant (ACK). La plupart des capteurs se lisent comme une petite mémoire : on écrit le numéro d’un registre, puis on lit un ou plusieurs octets à partir de lui. La HAL fournit les deux opérations :
HAL_I2C_Mem_Read (&hi2c2, adresse << 1, registre, I2C_MEMADD_SIZE_8BIT,
tampon, nombre_d_octets, delai_ms);
HAL_I2C_Mem_Write(&hi2c2, adresse << 1, registre, I2C_MEMADD_SIZE_8BIT,
tampon, nombre_d_octets, delai_ms);

Une lecture de registre (HAL_I2C_Mem_Read) : écrire le numéro du registre, puis lire.
Important
La HAL attend l’adresse décalée d’un bit à gauche : l’adresse 7 bits
0x48 se passe 0x48 << 1, soit 0x90. Oublier le décalage est l’erreur la
plus fréquente avec l’I²C.
Les capteurs de la carte
Composant |
Bus et broches |
Adresse 7 bits |
Rôle |
|---|---|---|---|
TMP102 (U3) |
I2C2 : SCL PB10, SDA PB11 |
0x48 |
capteur de température, soudé sur la carte |
LSM6DSOX |
I2C3 : SCL PA8, SDA PC9 |
0x6A ou 0x6B |
centrale inertielle (accéléromètre et gyroscope), sur un petit module relié au connecteur J9 |
Sur un robot complet, d’autres composants partagent le bus I2C2, comme le capteur de distance avant. Seul le TMP102 est toujours présent.
Étape 1 — Configurer l’I²C2 et chercher les composants
Créez un projet
tp07_i2ccomme au TP 1 (STM32H750VBTx, SYS → Debug : Serial Wire, horloge par défaut).Dans Connectivity → I2C2, choisissez I2C. Vérifiez que CubeMX a placé SCL et SDA sur PB10 et PB11 (sinon, cliquez sur ces broches et choisissez
I2C2_SCLetI2C2_SDA). Gardez la vitesse Standard Mode (100 kHz).Générez le code.
Écrivez un balayage du bus : pour chaque adresse de 1 à 127, demandez si un composant répond, et gardez la liste de ceux qui répondent :
if (HAL_I2C_IsDeviceReady(&hi2c2, adresse << 1, 2, 5) == HAL_OK) { /* un composant a acquitté à cette adresse */ }
Affichez le nombre de composants trouvés sur les LED de façade, ou lisez la liste dans le débogueur.
Questions
Quelles adresses répondent ? Retrouvez-les dans le tableau ci-dessus.
Que se passe-t-il si l’on oublie le décalage
<< 1?
Solution de l’étape 1
#define I2C_TIMEOUT_MS 5u
volatile uint8_t g_found[16]; /* adresses qui répondent */
volatile uint32_t g_found_count;
static void i2c2_scan(void)
{
g_found_count = 0u;
for (uint16_t a = 1u; a < 128u; a++)
{
if (HAL_I2C_IsDeviceReady(&hi2c2, (uint16_t)(a << 1), 2u, I2C_TIMEOUT_MS) == HAL_OK)
{
if (g_found_count < 16u) { g_found[g_found_count] = (uint8_t)a; }
g_found_count++;
}
}
}
appelée une fois dans USER CODE BEGIN 2. Le tableau se lit dans
Live Expressions du débogueur.
Ce qui circule sur le bus. Pour chaque adresse, le contrôleur I²C émet
une condition de départ (SDA descend pendant que SCL est haut), puis
l’octet d’adresse : les 7 bits de l’adresse et un 8e bit, 0 pour
une écriture. Au 9e coup d’horloge, il relâche SDA : si un
composant reconnaît son adresse, il tire SDA à 0, c’est l”acquittement
(ACK). Sinon, SDA reste à 1, tirée par les résistances de rappel :
c’est un NACK. Le contrôleur termine par une condition d”arrêt.
HAL_I2C_IsDeviceReady fait exactement cela (deux fois, d’après son
troisième argument), et renvoie HAL_OK en cas d’acquittement.
Le délai de 5 ms évite de rester bloqué si le bus est en défaut (une ligne collée à 0 par exemple). Le balayage complet prend 127 × 2 tentatives × environ 0,1 ms à 100 kHz : quelques dizaines de millisecondes.
Réponses aux questions 1 et 2
1. Les adresses. Sur une carte seule : 0x48, le TMP102 (sa broche d’adresse ADD0 est reliée à la masse ; reliée à V+, il répondrait en 0x49). Sur un robot complet, on voit aussi 0x29, le capteur de distance avant VL53L5CX, branché sur le même bus I2C2. La centrale inertielle, elle, est sur un autre bus (I2C3) : invisible ici.
2. Sans le décalage. Sur le bus, l’adresse occupe les 7 bits de poids fort de l’octet, le bit de poids faible indiquant le sens (lecture ou écriture). La HAL attend l’octet tel qu’il part sur le fil, donc l’adresse déjà décalée : 0x48 << 1 = 0x90. Sans le décalage, on demande 0x48 lui-même, que le contrôleur envoie comme l’adresse 0x24 : personne ne répond… ou un autre composant, par hasard, et on lit n’importe quoi. C’est l’erreur la plus fréquente avec l’I²C sur STM32 : les fiches techniques donnent l’adresse sur 7 bits, la HAL la veut sur 8.
Étape 2 — Vérifier qu’on parle bien au TMP102
Qu’un composant acquitte à l’adresse 0x48 ne prouve pas que ce soit un TMP102. Pour le vérifier, lisez un registre dont la valeur est connue : le registre de configuration (numéro 0x01), qui vaut 0x60A0 au démarrage, d’après la fiche technique du TMP102.
Lisez 2 octets à partir du registre 0x01, et reconstituez la valeur sur 16 bits : le premier octet reçu est l’octet de poids fort.
Comparez-la à 0x60A0.
Solution de l’étape 2
#define TMP102_ADDR (0x48u << 1)
#define TMP102_TEMP 0x00u
#define TMP102_CONFIG 0x01u
/* Lit un registre 16 bits du TMP102, octet de poids fort en premier. */
static int tmp102_read16(uint8_t reg, uint16_t *out)
{
uint8_t b[2];
if (HAL_I2C_Mem_Read(&hi2c2, TMP102_ADDR, reg, I2C_MEMADD_SIZE_8BIT,
b, 2u, I2C_TIMEOUT_MS) != HAL_OK)
{
return 0;
}
*out = (uint16_t)(((uint16_t)b[0] << 8) | b[1]);
return 1;
}
puis, dans USER CODE BEGIN 2 :
uint16_t c;
if (tmp102_read16(TMP102_CONFIG, &c)) { g_tmp_config = c; } /* attendu : 0x60A0 */
HAL_I2C_Mem_Read enchaîne deux transactions, comme le demande le
TMP102 : une écriture du numéro de registre (le « pointeur » du
composant), puis, après un nouveau départ (repeated start), une
lecture de deux octets. Le TMP102 envoie l’octet de poids fort en
premier (big-endian) : b[0] << 8 | b[1] reconstitue la valeur.
Pourquoi 0x60A0 prouve l’identité. Ce motif précis (12 bits de
résolution, mode de comparaison, 4 Hz de conversion) est la valeur
documentée au démarrage. Un autre composant à la même adresse n’aurait
aucune raison de la donner. On parle de vérification d’identité : la
plupart des capteurs ont un registre fait pour cela (WHO_AM_I pour la
centrale, à l’étape 4).
Étape 3 — Lire la température
Le registre de température (numéro 0x00) contient une mesure sur 12 bits, cadrée à gauche dans deux octets, en complément à deux (les températures négatives sont des nombres négatifs). Un pas vaut 0,0625 °C.
Lisez 2 octets à partir du registre 0x00.
Reconstituez la mesure : assemblez les deux octets, décalez de 4 bits à droite, puis étendez le signe si le bit 11 est à 1.
Convertissez en millièmes de degré, avec des entiers : un pas vaut 62,5 m°C.
Lisez la température toutes les secondes. Posez le doigt sur le TMP102 (repère U3, près du microcontrôleur) et regardez-la monter.
Questions
Quelle température lisez-vous ? Est-ce la température de la pièce ? Pourquoi ?
Le doigt fait-il monter la mesure tout de suite ? De combien, et en combien de temps ?
Pourquoi faut-il étendre le signe ? Que lirait-on à −10 °C sans le faire ?
Solution de l’étape 3
/* 12 bits cadrés à gauche, complément à deux, 62,5 m°C par pas. */
static int tmp102_temp_mC(int32_t *out)
{
uint16_t raw16;
int32_t raw;
if (!tmp102_read16(TMP102_TEMP, &raw16)) { return 0; }
raw = (int32_t)(raw16 >> 4); /* les 12 bits utiles */
if (raw & 0x800) { raw -= 4096; } /* extension de signe */
*out = (raw * 625) / 10; /* × 62,5 */
return 1;
}
et dans la boucle :
int32_t t;
if (tmp102_temp_mC(&t)) { g_temp_mC = t; }
HAL_Delay(1000);
Les trois opérations, sur un exemple. À 25 °C, le TMP102 renvoie
0x19 0x00 :
assemblage : 0x1900 = 0001 1001 0000 0000
décalage >>4 : 0x190 = 400 (les 4 bits de droite sont toujours 0)
bit 11 : 0 → positif, rien à faire
conversion : 400 × 62,5 = 25 000 m°C = 25,000 °C
Le × 625 / 10. Multiplier par 62,5 sans virgule : on multiplie par 625, puis on divise par 10. Le résultat est exact, car 400 × 625 = 250 000 est divisible par 10 ; dans le cas général, l’erreur d’arrondi est de 0,1 m°C, bien en dessous de la résolution du capteur.
Réponses aux questions 3 à 5
3. La température lue. Vers 28 °C au repos sur la carte de référence (28,6 °C à la mise en service), soit plusieurs degrés au-dessus de la pièce. Le TMP102 mesure sa propre température, c’est-à-dire celle du circuit imprimé sur lequel il est soudé : ses pattes sont reliées thermiquement au cuivre de la carte, que le microcontrôleur, les régulateurs et les LED réchauffent. C’est une température de carte, très utile pour surveiller l’électronique (et piloter un ventilateur, TP 4), pas une mesure de l’air ambiant.
4. Le doigt. La mesure monte de quelques dixièmes de degré en quelques secondes (+0,5 °C relevé), pas tout de suite : la chaleur doit traverser le boîtier, et le doigt chauffe aussi le cuivre autour, qui en absorbe une partie. C’est l”inertie thermique : un capteur de température mesure toujours avec retard. La mesure redescend ensuite lentement quand on retire le doigt.
5. L’extension de signe. La mesure est un nombre signé sur 12 bits,
rangé dans un int32_t de 32 bits. Pour un nombre négatif, le bit 11
(le signe sur 12 bits) vaut 1, mais les bits 12 à 31 du int32_t valent
0 : le processeur voit un grand nombre positif. À −10 °C, la mesure
vaut −160, codée 0xF60 sur 12 bits : sans extension, on lit 3 936 pas, soit
+246 °C. Retrancher 4 096 (212) quand le bit 11 est à 1 recopie
le signe sur les bits de gauche : 3 936 − 4 096 = −160.
Étape 4 — La centrale inertielle
Cette étape demande le module de centrale inertielle branché sur J9.
Activez I2C3 dans CubeMX, sur PA8 (SCL) et PC9 (SDA), en Standard Mode. Régénérez le code.
L’adresse. Selon le câblage de sa broche SA0, le LSM6DSOX répond en 0x6A ou en 0x6B. Essayez les deux avec
HAL_I2C_IsDeviceReady.L’identité. Lisez le registre WHO_AM_I (numéro 0x0F) : il doit valoir 0x6C.
La configuration. Au démarrage, les capteurs sont éteints. Écrivez :
Registre
Numéro
Valeur
Effet
CTRL3_C
0x12
0x44
mise à jour des données par blocs (BDU), adresse qui s’incrémente à chaque octet lu
CTRL1_XL
0x10
0x48
accéléromètre à 104 Hz, pleine échelle ±4 g
CTRL2_G
0x11
0x40
gyroscope à 104 Hz, pleine échelle ±250 °/s
La lecture. Lisez 6 octets à partir de OUTX_L_A (0x28) pour l’accéléromètre, et 6 octets à partir de OUTX_L_G (0x22) pour le gyroscope : trois axes X, Y, Z de 16 bits chacun, octet de poids faible en premier, en complément à deux.
Les unités. D’après la fiche technique : 0,122 mg par pas pour l’accéléromètre à ±4 g, 8,75 m°/s par pas pour le gyroscope à ±250 °/s.
Questions
Carte posée à plat et immobile, que vaut l’accélération sur chaque axe ? Lequel mesure la gravité ? Avec quel signe ? Qu’en déduire sur l’orientation du module ?
Immobile, le gyroscope devrait lire zéro. Que lit-il ? Comment corriger ce décalage, appelé biais ?
Pourquoi le réglage BDU est-il important quand on lit les deux octets d’une mesure en deux fois ?
Solution de l’étape 4
#define IMU_WHO_AM_I 0x0Fu
#define IMU_CTRL1_XL 0x10u
#define IMU_CTRL2_G 0x11u
#define IMU_CTRL3_C 0x12u
#define IMU_OUTX_L_G 0x22u
#define IMU_OUTX_L_A 0x28u
static int imu_init(void)
{
static const uint16_t addrs[2] = { 0x6Au << 1, 0x6Bu << 1 };
uint8_t v;
g_imu_addr = 0u;
for (uint32_t k = 0u; k < 2u; k++) /* SA0 à 0 ou à 1 */
{
if (HAL_I2C_IsDeviceReady(&hi2c3, addrs[k], 2u, I2C_TIMEOUT_MS) == HAL_OK)
{
g_imu_addr = addrs[k];
break;
}
}
if (g_imu_addr == 0u) { return 0; }
if (HAL_I2C_Mem_Read(&hi2c3, g_imu_addr, IMU_WHO_AM_I, I2C_MEMADD_SIZE_8BIT,
&v, 1u, I2C_TIMEOUT_MS) != HAL_OK) { return 0; }
g_who_am_i = v;
if (v != 0x6Cu) { return 0; } /* ce n'est pas un LSM6DSOX */
v = 0x44u; /* CTRL3_C : BDU + IF_INC, en premier */
HAL_I2C_Mem_Write(&hi2c3, g_imu_addr, IMU_CTRL3_C, I2C_MEMADD_SIZE_8BIT, &v, 1u, I2C_TIMEOUT_MS);
v = 0x48u; /* CTRL1_XL : 104 Hz, ±4 g */
HAL_I2C_Mem_Write(&hi2c3, g_imu_addr, IMU_CTRL1_XL, I2C_MEMADD_SIZE_8BIT, &v, 1u, I2C_TIMEOUT_MS);
v = 0x40u; /* CTRL2_G : 104 Hz, ±250 °/s */
HAL_I2C_Mem_Write(&hi2c3, g_imu_addr, IMU_CTRL2_G, I2C_MEMADD_SIZE_8BIT, &v, 1u, I2C_TIMEOUT_MS);
return 1;
}
/* Trois axes de 16 bits, octet de poids faible en premier. */
static int imu_read3(uint8_t reg, int16_t out[3])
{
uint8_t b[6];
if (HAL_I2C_Mem_Read(&hi2c3, g_imu_addr, reg, I2C_MEMADD_SIZE_8BIT,
b, 6u, I2C_TIMEOUT_MS) != HAL_OK) { return 0; }
for (uint32_t k = 0u; k < 3u; k++)
{
out[k] = (int16_t)(((uint16_t)b[2u * k + 1u] << 8) | b[2u * k]);
}
return 1;
}
et dans la boucle, dix fois par seconde :
int16_t a[3], g[3];
if ((g_imu_addr != 0u) && imu_read3(IMU_OUTX_L_A, a) && imu_read3(IMU_OUTX_L_G, g))
{
for (uint32_t k = 0u; k < 3u; k++)
{
g_acc_mg[k] = ((int32_t)a[k] * 122) / 1000; /* 0,122 mg / pas */
g_gyro_mdps[k] = ((int32_t)g[k] * 875) / 100; /* 8,75 m°/s / pas */
}
}
HAL_Delay(100);
Décoder 0x48 (CTRL1_XL). En binaire, 0100 1000 : les 4 bits de gauche
(0100) choisissent la fréquence, 104 Hz ; les deux suivants (10) la
pleine échelle, ±4 g. La fiche technique du LSM6DSOX donne ces tableaux de
bits, registre par registre.
Lire 6 octets d’un coup marche grâce au bit IF_INC de CTRL3_C : après
chaque octet lu, le capteur passe tout seul au registre suivant (OUTX_L,
OUTX_H, OUTY_L…). Une seule transaction I²C pour trois axes : plus rapide,
et les trois valeurs viennent du même échantillon.
Ici, l’ordre des octets est inversé par rapport au TMP102 : poids faible en premier (little-endian). Chaque composant a sa convention : on la lit dans sa fiche technique, on ne la devine jamais.
Le transtypage en int16_t fait de lui-même l’extension de signe : les 16 bits assemblés sont interprétés comme un nombre signé, ce qui est exactement le format du capteur.
Réponses aux questions 6 à 8
6. La gravité. Un axe lit environ ±1 000 mg, les deux autres presque 0. Un accéléromètre au repos ne mesure pas zéro : il mesure la réaction du support, qui l’empêche de tomber, dirigée vers le haut. L’axe qui pointe vers le haut lit +1 g, celui qui pointe vers le bas −1 g. Sur RatBot, le module est monté à l’envers : son axe Z pointe vers le bas, et lit environ −1 000 mg. C’est ainsi qu’on vérifie le montage d’une centrale, et le firmware du robot (comme son modèle 3D) en tient compte.
7. Le biais. Immobile, le gyroscope lit quelques dizaines à quelques centaines de m°/s, différents sur chaque axe : c’est son biais (offset), propre à chaque puce, et qui varie avec la température. Il paraît faible, mais il s”intègre : 0,1 °/s de biais donne 6° d’erreur de cap par minute. On le corrige en faisant la moyenne des mesures pendant une ou deux secondes, carte immobile, puis en la soustrayant de chaque mesure. Le firmware du robot le fait à chaque démarrage, et le remesure dès que le robot est à l’arrêt.
8. BDU. Une mesure de 16 bits est dans deux registres de 8 bits, lus l’un après l’autre. Le capteur produit 104 échantillons par seconde : si un nouvel échantillon arrive entre la lecture de l’octet bas et celle de l’octet haut, on assemble deux moitiés d’échantillons différents. Près d’un passage d’octet (de 0x00FF à 0x0100, par exemple), l’erreur est énorme : 0x01FF, ou 0x0000 — un faux pic. Avec BDU (Block Data Update), le capteur ne met à jour les deux registres qu’une fois les deux octets lus. Le défaut est rare, donc difficile à trouver : il vaut mieux ne jamais l’avoir.
Tout le code de la correction
main_user_code.c
— compilé sans avertissement contre la HAL du STM32H7.
Ce n’est pas un main.c complet : ce sont les extraits à placer dans
les blocs USER CODE du main.c généré par CubeMX, chacun sous le
commentaire qui indique son bloc. La boucle while (1) contient une
variante par étape, encadrée par #if 0 / #if 1 : n’en activer
qu’une à la fois. La configuration CubeMX attendue est rappelée en tête
du fichier, et les réponses aux questions à la fin.
Exercices supplémentaires
Ces exercices n’ont pas de correction.
Alerte de température : le TMP102 a une sortie
ALERTreliée à PE15. Réglez ses seuils (registres 0x02 et 0x03) pour qu’elle s’active au-dessus de 30 °C, et allumez une LED quand elle passe à 0.Niveau à bulle : les quatre LED indiquent de quel côté la carte penche, d’après l’accéléromètre.
Détecteur de choc : une LED s’allume (et le buzzer sonne) quand l’accélération dépasse 2 g sur un axe horizontal.
Cap : intégrez la vitesse de rotation du gyroscope autour de l’axe vertical pour obtenir un angle, et observez sa dérive en quelques minutes.
Après le TP
Pour rendre la carte au robot, voir Remettre la carte en état RatBot.