TP 10 — Des tâches pour les capteurs I²C
Objectifs
organiser la lecture de capteurs en tâches périodiques, chacune à son rythme ;
partager les dernières mesures entre tâches, proprement ;
distinguer un état (la dernière valeur, qu’on lit quand on veut) d’un événement (quelque chose qui vient d’arriver, et qu’on ne doit pas manquer) ;
savoir quand un bus partagé a besoin d’être protégé.
Durée indicative : deux séances de 3 h. Prérequis : TP 4 — Partager une ressource et TP 7 — Le bus I²C : température, accéléromètre et gyroscope.
Ce qu’il faut savoir
Deux capteurs, deux bus, deux rythmes
Le capteur de température TMP102 est sur le bus I2C2, la centrale inertielle LSM6DSOX sur le bus I2C3 (partie 1, TP 7). La température change lentement : une mesure par seconde suffit. Le gyroscope, lui, sert à suivre le cap du robot en intégrant sa vitesse de rotation : il faut le lire souvent, et à intervalle régulier.
Une tâche par rythme est la solution naturelle : une tâche à 1 Hz pour la température, une tâche à 50 Hz pour le gyroscope.
État ou événement ?
La température courante est un état : une tâche qui l’affiche veut la dernière valeur, pas l’historique. On la range dans une structure partagée, protégée par un mutex (TP 4).
Le dépassement d’un seuil est un événement : la tâche qui déclenche l’alarme ne doit pas le manquer, même si elle était occupée à ce moment. On l’envoie dans une file (TP 3).
Un bus, un propriétaire
Si deux tâches utilisent le même bus I²C, leurs transactions ne doivent pas s’entremêler : il faut un mutex autour de chaque transaction, ou une seule tâche propriétaire du bus, qui fait toutes les lectures. Le firmware du robot a choisi la seconde solution pour I2C2, où se trouvent le TMP102 et le capteur de distance : une seule tâche possède le bus.

L’organisation du TP : deux tâches de mesure, un état partagé sous mutex, une file d’événements.
Étape 1 — La tâche température
Créez un projet
rtos_tp10_capteurscomme au TP 2 (FreeRTOS CMSIS_V2, Timebase TIM6, UART5 etprintf, LED_1 à LED_4), avec I2C2 sur PB10/PB11 et I2C3 sur PA8/PC9, en mode standard (100 kHz), comme au TP 7 de la partie 1.Déclarez une structure d’état partagée, et un mutex
sensorsMutex:typedef struct { int32_t temp_mC; /* derniere temperature, en milliemes de degre */ float heading_deg; /* cap integre, en degres */ uint32_t i2c_errors; /* echecs de lecture, tous capteurs */ } sensors_t; static sensors_t g_sensors;
tempTask(priorité normale) lit le TMP102 toutes les secondes, avecosDelayUntil, et range la valeur dansg_sensorssous le mutex. Une lecture qui échoue incrémentei2c_errors.logTask(priorité basse) affiche toutes les 2 s une copie deg_sensors, faite sous le mutex :sensors_t s; osMutexAcquire(sensorsMutexHandle, osWaitForever); s = g_sensors; osMutexRelease(sensorsMutexHandle); printf("T = %ld mC ...\r\n", (long)s.temp_mC);
Questions
Pourquoi faire une copie, plutôt que d’appeler
printfen tenant le mutex ?Le
HAL_I2C_Mem_Readdure environ 0,4 ms à 100 kHz. Pendant ce temps,tempTasklaisse-t-elle tourner les autres tâches ? (Indice : comment la HAL attend-elle la fin de la transaction ?)
Solution de l’étape 1
static sensors_t g_sensors;
void StartTemp(void *argument) /* osPriorityNormal, 1 Hz */
{
uint32_t next = osKernelGetTickCount();
(void)argument;
for (;;)
{
int32_t t;
if (tmp102_temp_mC(&t)) /* lecture du TP 7, partie 1 */
{
osMutexAcquire(sensorsMutexHandle, osWaitForever);
g_sensors.temp_mC = t;
osMutexRelease(sensorsMutexHandle);
}
else
{
osMutexAcquire(sensorsMutexHandle, osWaitForever);
g_sensors.i2c_errors++;
osMutexRelease(sensorsMutexHandle);
}
next += 1000u;
osDelayUntil(next);
}
}
void StartLog(void *argument) /* osPriorityBelowNormal */
{
(void)argument;
for (;;)
{
sensors_t s;
int32_t h;
osMutexAcquire(sensorsMutexHandle, osWaitForever);
s = g_sensors; /* copie : quelques octets */
osMutexRelease(sensorsMutexHandle);
h = (int32_t)(s.heading_deg * 10.0f); /* dixièmes de degré */
printf("T = %ld mC cap = %ld.%ld deg erreurs I2C = %lu\r\n",
(long)s.temp_mC, (long)(h / 10), (long)((h < 0 ? -h : h) % 10),
(unsigned long)s.i2c_errors);
osDelay(2000);
}
}
La lecture se fait hors du mutex. La transaction I²C dure 0,4 ms : on la fait d’abord, dans une variable locale, et on ne prend le mutex que pour ranger le résultat. La section critique se réduit à une affectation.
Afficher un nombre à virgule sans %f. La bibliothèque C réduite de
CubeIDE (newlib-nano) n’affiche pas les float par défaut. On convertit
en dixièmes de degré (un entier), puis on affiche la partie entière et le
chiffre des dixièmes. La valeur absolue évite d’afficher « -12.-3 ».
Réponses aux questions 1 et 2
1. Une copie plutôt que printf sous mutex. printf dure plusieurs
millisecondes (il attend l’UART). Pendant tout ce temps, le mutex serait
tenu, et imuTask, qui veut écrire le cap 50 fois par seconde, serait
bloquée : sa période ne serait plus régulière, et l’intégration du cap
fausse (question 4). La copie, elle, prend une fraction de microseconde.
C’est la façon standard de partager un état : copier sous le verrou,
travailler sur la copie. La copie garantit aussi la cohérence : la
température, le cap et le compteur affichés datent du même instant.
2. Pendant HAL_I2C_Mem_Read. Non, tempTask ne libère pas le
processeur. En mode bloquant, la HAL attend chaque étape de la transaction
(départ, adresse, acquittement, données) en relisant en boucle les
drapeaux du périphérique I²C. La tâche reste prête et consomme le
processeur pendant 0,4 ms. Seules les tâches plus prioritaires (ici
imuTask) peuvent la préempter ; les tâches de même priorité ne
l’interrompent qu’au tic. Pour vraiment libérer le processeur, il faut la
version par interruption (HAL_I2C_Mem_Read_IT) ou par DMA, et attendre
la fin sur un sémaphore (dernier exercice).
Étape 2 — La tâche gyroscope
Configurez la centrale comme au TP 7 de la partie 1 (
CTRL3_C= 0x44,CTRL2_G= 0x40 : 104 Hz, ±250 °/s, soit 8,75 m°/s par unité).imuTask(priorité au-dessus de la normale) lit toutes les 20 ms la vitesse de rotation autour de l’axe Z (registresOUTZ_L_GetOUTZ_H_G, 0x26 et 0x27), et l’intègre :cap = cap + vitesse (°/s) × 0,020 s
Au démarrage, robot immobile, mesurez le zéro du gyroscope : la moyenne de 100 lectures, à retrancher ensuite de chaque mesure.
Rangez le cap dans
g_sensors, sous le mutex, et affichez-le danslogTask.
Tournez la carte d’un quart de tour à la main, et vérifiez le cap affiché.
Questions
Que devient le cap, carte immobile, sans correction du zéro ? Et avec ?
Pourquoi
osDelayUntilest-il indispensable ici ? Que se passerait-il avecosDelay(20)?tempTasketimuTaskfont toutes deux de l’I²C : faut-il un mutex entre elles ? Et si l’on ajoutait une troisième tâche qui lise, sur I2C2, un autre capteur ?La centrale est montée à l’envers sur le robot (axe Z vers le bas). Quel signe faut-il appliquer pour que le cap augmente quand le robot tourne à gauche ?
Solution de l’étape 2
#define GYRO_MDPS_LSB 8.75f /* ±250 °/s : 8,75 m°/s par pas */
#define IMU_PERIOD_MS 20u
#define GYRO_Z_SIGN (-1.0f) /* centrale à l'envers sur le robot */
static int gyro_z_raw(int16_t *out)
{
uint8_t b[2];
if (HAL_I2C_Mem_Read(&hi2c3, g_imu_addr, 0x26u, I2C_MEMADD_SIZE_8BIT,
b, 2u, 5u) != HAL_OK) { return 0; }
*out = (int16_t)(((uint16_t)b[1] << 8) | b[0]); /* poids faible d'abord */
return 1;
}
void StartImu(void *argument) /* osPriorityAboveNormal, 50 Hz */
{
float bias = 0.0f, heading = 0.0f;
uint32_t next;
(void)argument;
if (!imu_init()) { osThreadExit(); } /* pas de centrale */
osDelay(100); /* premières mesures prêtes */
{ /* le zéro : moyenne de 100 lectures, carte immobile */
int32_t sum = 0;
uint32_t n = 0u;
for (uint32_t k = 0u; k < 100u; k++)
{
int16_t r;
if (gyro_z_raw(&r)) { sum += r; n++; }
osDelay(IMU_PERIOD_MS);
}
if (n > 0u) { bias = (float)sum / (float)n; }
}
next = osKernelGetTickCount();
for (;;)
{
int16_t r;
if (gyro_z_raw(&r))
{
float rate_dps = ((float)r - bias) * GYRO_MDPS_LSB / 1000.0f;
heading += GYRO_Z_SIGN * rate_dps * ((float)IMU_PERIOD_MS / 1000.0f);
osMutexAcquire(sensorsMutexHandle, osWaitForever);
g_sensors.heading_deg = heading;
osMutexRelease(sensorsMutexHandle);
}
next += IMU_PERIOD_MS;
osDelayUntil(next);
}
}
(imu_init reprend l’initialisation du TP 7 de la partie 1, avec
CTRL3_C = 0x44 et CTRL2_G = 0x40.)
L’intégration. La vitesse de rotation ω (en °/s), multipliée par la durée Δt de la période (0,020 s), donne l’angle tourné pendant la période. On les additionne : c’est l”intégration numérique (méthode des rectangles), qui fait passer d’une vitesse à une position.
Le flottant sur ce processeur. Le Cortex-M7 du H750 a une unité de
calcul flottant (FPU) en double précision : une multiplication en
float coûte un cycle, comme en entier. Le suffixe f (1000.0f)
garde les calculs en simple précision ; sans lui, 1000.0 est un
double, plus lent et inutile ici.
osThreadExit termine proprement la tâche si la centrale est absente, au lieu de la laisser tourner sur des lectures en échec.
Réponses aux questions 3 à 6
3. Le cap, carte immobile. Sans correction, il dérive régulièrement : quelques degrés par minute, ou plus, selon le biais du composant (TP 7 de la partie 1). Le biais est une vitesse fausse, constante : intégrée, elle devient un angle qui grandit sans fin. Avec la correction, la dérive devient faible, mais pas nulle : le biais varie avec la température, et la moyenne de départ n’est pas parfaite. Toute position obtenue en intégrant une vitesse finit par dériver : c’est pour cela que le robot recale sa position avec le lidar (partie 3, TP 5).
4. osDelayUntil indispensable. L’intégration suppose que chaque mesure
représente exactement 20 ms de rotation. Avec osDelay(20), la
période réelle vaut 20 ms plus la durée de la lecture (0,4 ms) plus
les éventuelles préemptions par d’autres tâches ou interruptions : 21 ms
ou davantage, et variable. On compterait 20 ms de rotation pour 21 ms
réelles : le cap serait sous-estimé d’environ 5 %, d’autant plus que la
carte est chargée. Mieux encore que osDelayUntil : mesurer le temps
réellement écoulé entre deux lectures, et l’utiliser comme Δt.
5. Un mutex entre les deux bus ? Non. I2C2 et I2C3 sont deux
périphériques distincts, avec leurs registres, leurs broches et leurs
structures HAL (hi2c2, hi2c3). Chacun n’est utilisé que par une
tâche : aucune ressource n’est partagée.
Une troisième tâche sur I2C2 changerait tout : deux tâches qui lancent
des transactions en même temps sur le même contrôleur les mélangeraient
(la HAL renverrait HAL_BUSY, ou pire). Il faudrait alors un mutex
autour de chaque transaction sur I2C2, ou bien confier toutes les
lectures d’I2C2 à une seule tâche propriétaire du bus. Le firmware du
robot a choisi la seconde solution.
6. Le signe. Vu de dessus, une rotation à gauche (sens trigonométrique) est une rotation positive autour d’un axe Z dirigé vers le haut : c’est la convention de ROS, et celle du cap. La centrale du robot a son axe Z dirigé vers le bas : elle mesure la même rotation avec le signe opposé. On multiplie donc par −1. Carte seule sur la table, composants vers le haut, le signe serait +1 : d’où la constante, qu’on change selon le montage.
Étape 3 — Un événement : l’alarme de température
Créez une file
alarmQueuede 4 élémentsuint8_t.Dans
tempTask, détectez le franchissement de 35 °C à la montée, et de 32 °C à la descente (hystérésis, TP 4 de la partie 1). À chaque franchissement — et seulement à ce moment — envoyez 1 (alarme) ou 0 (fin d’alarme) dans la file.alarmTaskattend les messages : en alarme, elle fait clignoter LED_4 rapidement ; sinon, elle l’éteint.
Chauffez le TMP102 du bout du doigt.
Questions
Pourquoi ne pas faire relire
g_sensors.temp_mCen boucle àalarmTask?alarmTaskdoit clignoter et attendre un message. Comment faire avec un seul appel ? (Indice : le délai d’attente deosMessageQueueGet.)
Solution de l’étape 3
Dans StartTemp, après avoir rangé la température :
#define ALARM_ON_mC 35000
#define ALARM_OFF_mC 32000
static uint8_t alarm = 0u; /* état de l'alarme, propre à la tâche */
if (!alarm && (t >= ALARM_ON_mC))
{
alarm = 1u;
osMessageQueuePut(alarmQueueHandle, &alarm, 0u, 0u); /* événement : début */
}
else if (alarm && (t <= ALARM_OFF_mC))
{
alarm = 0u;
osMessageQueuePut(alarmQueueHandle, &alarm, 0u, 0u); /* événement : fin */
}
et la tâche d’alarme :
void StartAlarm(void *argument)
{
uint8_t alarm = 0u;
(void)argument;
for (;;)
{
uint8_t msg;
if (osMessageQueueGet(alarmQueueHandle, &msg, NULL,
alarm ? 100u : osWaitForever) == osOK)
{
alarm = msg; /* un événement */
if (!alarm)
{
HAL_GPIO_WritePin(LED_4_GPIO_Port, LED_4_Pin, GPIO_PIN_RESET);
}
}
else
{
HAL_GPIO_TogglePin(LED_4_GPIO_Port, LED_4_Pin); /* 100 ms écoulées */
}
}
}
On n’envoie un message qu’au franchissement d’un seuil, pas à chaque mesure : la file ne transporte que des événements, et ne déborde jamais.
Réponses aux questions 7 et 8
7. Pourquoi pas relire g_sensors.temp_mC en boucle. D’abord, la tâche consommerait du processeur à relire sans cesse une valeur qui change une fois par seconde. Ensuite, et surtout, elle pourrait manquer un événement : si la température passe brièvement au-dessus du seuil entre deux relectures, puis redescend, l’alarme ne se déclenche jamais. Un message dans une file, lui, attend d’être lu : l’événement n’est jamais perdu (tant que la file n’est pas pleine). La distinction état / événement est l’une des plus utiles en conception de systèmes : un état se lit quand on veut, un événement se transmet et se consomme.
8. Clignoter et attendre en un seul appel. On utilise le délai
d’attente de osMessageQueueGet comme horloge : en alarme, on attend un
message au plus 100 ms. Si un message arrive, on le traite ; si le délai
expire (la fonction renvoie osErrorTimeout), c’est qu’il faut faire
basculer la LED. Hors alarme, on attend indéfiniment : la tâche ne
consomme rien. C’est un schéma très courant : une seule attente, deux
raisons de se réveiller.
Tout le code de la correction
taches.c
— compilé sans avertissement contre la HAL et le FreeRTOS 10.6.2 du
firmware RatBot.
Le fichier contient les fonctions des tâches, qui remplacent les
squelettes générés par CubeMX (dans main.c ou freertos.c selon la
version), et les extraits à placer dans les blocs USER CODE signalés en
commentaire. 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.
Le ventilateur : une tâche pilote le ventilateur (TP 4 de la partie 1) à partir des événements d’alarme.
L’accéléromètre :
imuTasklit aussi l’accélération et détecte une carte retournée (Z dans le mauvais sens pendant plus d’une seconde) : un nouvel événement.La qualité de service : comptez, dans
imuTask, les tours où la lecture a pris plus de 2 ms, ou a échoué ; affichez ce compteur.Lecture par interruption : remplacez
HAL_I2C_Mem_ReadparHAL_I2C_Mem_Read_ITet un sémaphore levé parHAL_I2C_MemRxCpltCallback. Que gagne-t-on ?
Après le TP
Pour rendre la carte au robot, voir Remettre la carte en état RatBot.