/*
* ================================================================
* Projet FreeRTOS — Système d'arrosage automatique intelligent
* Réalisé par : Aziz Guidara - Zeineb Karoui
* Carte : STM32 NUCLEO-C031C6
* ================================================================
*/
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "stm32c0xx_hal.h"
#include <stdio.h>
#include <string.h>
/* ================================================================
* SECTION 1 — Broches GPIO
* ================================================================ */
#define PIN_LED_ZONE1 GPIO_PIN_5
#define PIN_LED_ZONE2 GPIO_PIN_6
#define PIN_LED_URGENCE GPIO_PIN_7
#define PIN_BTN_URGENCE GPIO_PIN_4
#define PORT_A GPIOA
#define PORT_B GPIOB
/* ================================================================
* SECTION 2 — LCD I2C (PCF8574 backpack, adresse 0x27)
*
* Le LCD 1602 sur Wokwi utilise un expandeur I2C PCF8574.
* Les bits du registre PCF8574 sont câblés ainsi :
* bit 7 (D7) → bit 6 (D6) → bit 5 (D5) → bit 4 (D4)
* bit 3 = BL (backlight)
* bit 2 = EN (enable)
* bit 1 = RW (toujours 0 = write)
* bit 0 = RS (0=commande, 1=donnée)
* ================================================================ */
#define LCD_ADDR 0x27 << 1 // adresse I2C décalée (HAL)
#define LCD_BL 0x08 // backlight ON
#define LCD_EN 0x04
#define LCD_RW 0x02
#define LCD_RS 0x01
I2C_HandleTypeDef hi2c1;
/* ================================================================
* SECTION 3 — Fonctions bas niveau LCD
* ================================================================ */
// Envoyer 1 octet sur le bus I2C vers le PCF8574
void LCD_I2C_Write(uint8_t data) {
HAL_I2C_Master_Transmit(&hi2c1, LCD_ADDR, &data, 1, 10);
}
// Générer une impulsion Enable pour valider un nibble
void LCD_Pulse(uint8_t data) {
LCD_I2C_Write(data | LCD_EN); // EN = 1
HAL_Delay(1);
LCD_I2C_Write(data & ~LCD_EN); // EN = 0
HAL_Delay(1);
}
// Envoyer un nibble (4 bits) au LCD
void LCD_SendNibble(uint8_t nibble, uint8_t rs) {
// Les 4 bits de données occupent les bits 7-4 du PCF8574
uint8_t data = (nibble << 4) | LCD_BL | rs;
LCD_Pulse(data);
}
// Envoyer un octet complet (commande ou donnée) en mode 4 bits
// Le LCD reçoit d'abord le nibble haut, puis le nibble bas
void LCD_Send(uint8_t byte, uint8_t rs) {
LCD_SendNibble(byte >> 4, rs); // nibble haut en premier
LCD_SendNibble(byte & 0x0F, rs); // nibble bas ensuite
}
// Envoyer une commande (RS=0)
void LCD_Cmd(uint8_t cmd) {
LCD_Send(cmd, 0);
HAL_Delay(2);
}
// Envoyer un caractère (RS=1)
void LCD_Char(char c) {
LCD_Send((uint8_t)c, LCD_RS);
}
// Afficher une chaîne de caractères
void LCD_String(const char *str) {
while (*str) {
LCD_Char(*str++);
}
}
// Positionner le curseur (col=0..15, row=0..1)
void LCD_SetCursor(uint8_t col, uint8_t row) {
uint8_t addr = (row == 0) ? (0x80 + col) : (0xC0 + col);
LCD_Cmd(addr);
}
// Effacer une ligne entière (16 espaces)
void LCD_ClearLine(uint8_t row) {
LCD_SetCursor(0, row);
for (uint8_t i = 0; i < 16; i++) {
LCD_Char(' ');
}
LCD_SetCursor(0, row);
}
// Initialisation complète du LCD en mode 4 bits
void LCD_Init(void) {
HAL_Delay(50); // attendre alimentation stable
// Séquence d'initialisation spéciale en 4 bits
// (3 fois nibble 0x3, puis passage en mode 4 bits)
LCD_SendNibble(0x03, 0); HAL_Delay(5);
LCD_SendNibble(0x03, 0); HAL_Delay(1);
LCD_SendNibble(0x03, 0); HAL_Delay(1);
LCD_SendNibble(0x02, 0); HAL_Delay(1); // passage 4 bits
// Configuration : 4 bits, 2 lignes, caractères 5x8
LCD_Cmd(0x28);
// Éteindre le display
LCD_Cmd(0x08);
// Effacer l'écran
LCD_Cmd(0x01); HAL_Delay(2);
// Mode entrée : curseur se déplace à droite, pas de shift
LCD_Cmd(0x06);
// Allumer display, curseur OFF, pas de clignotement
LCD_Cmd(0x0C);
}
/* ================================================================
* SECTION 4 — Initialisation I2C (PB6=SCL, PB7=SDA)
* ================================================================ */
void MX_I2C_Init(void) {
__HAL_RCC_I2C1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef cfg = {0};
cfg.Pin = GPIO_PIN_6 | GPIO_PIN_7;
cfg.Mode = GPIO_MODE_AF_OD; // open-drain obligatoire pour I2C
cfg.Pull = GPIO_PULLUP;
cfg.Speed = GPIO_SPEED_FREQ_LOW;
cfg.Alternate = GPIO_AF6_I2C1;
HAL_GPIO_Init(GPIOB, &cfg);
hi2c1.Instance = I2C1;
hi2c1.Init.Timing = 0x00303D5B; // ~100kHz sur HSI 16MHz
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
HAL_I2C_Init(&hi2c1);
}
/* ================================================================
* SECTION 5 — Structure message queue
* ================================================================ */
typedef struct {
uint8_t zone;
uint16_t humidite;
} HumiditeMsg_t;
/* ================================================================
* SECTION 6 — Handles FreeRTOS et variables partagées
* ================================================================ */
QueueHandle_t xHumidityQueue;
SemaphoreHandle_t xEmergencySem;
SemaphoreHandle_t xThresholdMutex;
SemaphoreHandle_t xLCDMutex;
volatile uint16_t gSeuilHumidite = 2000;
volatile uint8_t gUrgenceActive = 0;
volatile uint16_t gHum1 = 0;
volatile uint16_t gHum2 = 0;
/* ================================================================
* SECTION 7 — Initialisation GPIO (LEDs + bouton)
* ================================================================ */
void MX_GPIO_Init(void) {
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef cfg = {0};
// LEDs sortie
cfg.Pin = PIN_LED_ZONE1 | PIN_LED_ZONE2 | PIN_LED_URGENCE;
cfg.Mode = GPIO_MODE_OUTPUT_PP;
cfg.Pull = GPIO_NOPULL;
cfg.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(PORT_A, &cfg);
// Bouton entrée pull-up (actif bas)
cfg.Pin = PIN_BTN_URGENCE;
cfg.Mode = GPIO_MODE_INPUT;
cfg.Pull = GPIO_PULLUP;
HAL_GPIO_Init(PORT_B, &cfg);
HAL_GPIO_WritePin(PORT_A,
PIN_LED_ZONE1 | PIN_LED_ZONE2 | PIN_LED_URGENCE,
GPIO_PIN_RESET);
}
/* ================================================================
* SECTION 8 — Initialisation ADC
* ================================================================ */
ADC_HandleTypeDef hadc;
void MX_ADC_Init(void) {
__HAL_RCC_ADC_CLK_ENABLE();
hadc.Instance = ADC1;
hadc.Init.Resolution = ADC_RESOLUTION_12B;
hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
hadc.Init.LowPowerAutoWait = DISABLE;
hadc.Init.ContinuousConvMode = DISABLE;
hadc.Init.NbrOfConversion = 1;
hadc.Init.DiscontinuousConvMode = DISABLE;
hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
hadc.Init.DMAContinuousRequests = DISABLE;
hadc.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
HAL_ADC_Init(&hadc);
}
uint16_t LireADC(uint32_t channel) {
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = channel;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_39CYCLES_5;
HAL_ADC_ConfigChannel(&hadc, &sConfig);
HAL_ADC_Start(&hadc);
HAL_ADC_PollForConversion(&hadc, 100);
return HAL_ADC_GetValue(&hadc);
}
/* ================================================================
* SECTION 9 — LCD_Print thread-safe
*
* Utilise xLCDMutex pour éviter que deux tâches FreeRTOS
* n'écrivent en même temps sur le LCD (affichage corrompu).
* ================================================================ */
void LCD_Print(uint8_t ligne, const char *texte) {
if (xSemaphoreTake(xLCDMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
LCD_ClearLine(ligne);
LCD_SetCursor(0, ligne);
LCD_String(texte);
xSemaphoreGive(xLCDMutex);
}
}
/* ================================================================
* TÂCHE 1 CORRIGÉE : Surveillance humidité
* - Période : 5s | Priorité : 2
* - Vider la queue AVANT d'envoyer de nouveaux messages
* → évite l'accumulation de vieux ordres d'arrosage
* - Ne pas envoyer si la zone est déjà en cours d'arrosage
* ================================================================ */
// Flags globaux : 1 si la vanne de cette zone est déjà ouverte
// Évite d'envoyer des messages en double dans la queue
volatile uint8_t gVanne1Ouverte = 0;
volatile uint8_t gVanne2Ouverte = 0;
void vTaskSurveillance(void *pvParameters) {
HumiditeMsg_t msg;
uint16_t seuilLocal;
char ligne0[17];
for (;;) {
// Lire le seuil protégé par mutex
if (xSemaphoreTake(xThresholdMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
seuilLocal = gSeuilHumidite;
xSemaphoreGive(xThresholdMutex);
} else {
seuilLocal = 2000;
}
// Lire les capteurs
gHum1 = LireADC(ADC_CHANNEL_0);
gHum2 = LireADC(ADC_CHANNEL_1);
// Afficher sur LCD ligne 0
snprintf(ligne0, sizeof(ligne0), "Z1:%-4d Z2:%-4d", gHum1, gHum2);
LCD_Print(0, ligne0);
if (!gUrgenceActive) {
/* ------------------------------------------------
* CORRECTION CLEF : envoyer un message SEULEMENT si
* 1. humidité < seuil (sol vraiment sec)
* 2. la vanne n'est PAS déjà ouverte pour cette zone
* (évite les doublons dans la queue)
* ------------------------------------------------ */
if (gHum1 < seuilLocal && !gVanne1Ouverte) {
msg.zone = 1;
msg.humidite = gHum1;
xQueueSend(xHumidityQueue, &msg, 0);
}
if (gHum2 < seuilLocal && !gVanne2Ouverte) {
msg.zone = 2;
msg.humidite = gHum2;
xQueueSend(xHumidityQueue, &msg, 0);
}
/* ------------------------------------------------
* Si une zone EST au-dessus du seuil mais que sa
* vanne est ouverte → vider la queue de ses messages
* en attente (cas où pot tourné pendant arrosage)
* ------------------------------------------------ */
if (gHum1 >= seuilLocal && gVanne1Ouverte == 0) {
// Zone 1 OK : s'assurer qu'aucun message zone1
// ne traîne dans la queue
HumiditeMsg_t tmp;
// Parcourir la queue pour vider les msg zone1
UBaseType_t count = uxQueueMessagesWaiting(xHumidityQueue);
for (UBaseType_t i = 0; i < count; i++) {
if (xQueueReceive(xHumidityQueue, &tmp, 0) == pdTRUE) {
// Remettre dans la queue seulement si
// c'est une autre zone ET toujours sec
if (tmp.zone != 1) {
xQueueSend(xHumidityQueue, &tmp, 0);
}
}
}
}
if (gHum2 >= seuilLocal && gVanne2Ouverte == 0) {
HumiditeMsg_t tmp;
UBaseType_t count = uxQueueMessagesWaiting(xHumidityQueue);
for (UBaseType_t i = 0; i < count; i++) {
if (xQueueReceive(xHumidityQueue, &tmp, 0) == pdTRUE) {
if (tmp.zone != 2) {
xQueueSend(xHumidityQueue, &tmp, 0);
}
}
}
}
}
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
/* ================================================================
* TÂCHE 2 CORRIGÉE : Activation des vannes
* - Priorité : 3 | Vanne ouverte 10s
* - Vérifier ENCORE une fois l'humidité avant d'ouvrir
* (la valeur peut avoir changé depuis l'envoi du message)
* - Mettre gVanne1Ouverte/gVanne2Ouverte à jour
* - Vérifier toutes les 500ms pendant l'arrosage
* ================================================================ */
void vTaskVannes(void *pvParameters) {
HumiditeMsg_t msg;
char ligne1[17];
for (;;) {
if (xQueueReceive(xHumidityQueue, &msg, portMAX_DELAY) == pdTRUE) {
if (gUrgenceActive) continue;
/* ------------------------------------------------
* VÉRIFICATION IMMÉDIATE avant d'ouvrir la vanne :
* Re-lire l'humidité ET le seuil au moment présent.
* Le message dans la queue peut dater de plusieurs
* secondes → la situation a peut-être changé.
* ------------------------------------------------ */
uint16_t humMaintenant;
if (msg.zone == 1) {
humMaintenant = LireADC(ADC_CHANNEL_0);
gHum1 = humMaintenant;
} else {
humMaintenant = LireADC(ADC_CHANNEL_1);
gHum2 = humMaintenant;
}
uint16_t seuilMaintenant = 2000;
if (xSemaphoreTake(xThresholdMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
seuilMaintenant = gSeuilHumidite;
xSemaphoreGive(xThresholdMutex);
}
/* ------------------------------------------------
* Ne pas ouvrir si l'humidité est déjà OK !
* C'est ici que le bug précédent était corrigé :
* même si un message est dans la queue, on vérifie
* que le sol est ENCORE sec avant d'arroser.
* ------------------------------------------------ */
if (humMaintenant >= seuilMaintenant) {
// Sol OK → ignorer ce message, pas d'arrosage
snprintf(ligne1, sizeof(ligne1),
"Z%d deja OK:%-4d", msg.zone, humMaintenant);
LCD_Print(1, ligne1);
vTaskDelay(pdMS_TO_TICKS(1500));
LCD_Print(1, "");
continue; // retourner attendre prochain message
}
// Sol confirmé sec → ouvrir la vanne
if (msg.zone == 1) {
gVanne1Ouverte = 1;
HAL_GPIO_WritePin(PORT_A, PIN_LED_ZONE1, GPIO_PIN_SET);
snprintf(ligne1, sizeof(ligne1), "Arrosage Z1 ON");
} else {
gVanne2Ouverte = 1;
HAL_GPIO_WritePin(PORT_A, PIN_LED_ZONE2, GPIO_PIN_SET);
snprintf(ligne1, sizeof(ligne1), "Arrosage Z2 ON");
}
LCD_Print(1, ligne1);
// Boucle d'arrosage : vérifier toutes les 500ms
uint32_t tempsDebut = xTaskGetTickCount();
uint8_t continuer = 1;
while (continuer) {
vTaskDelay(pdMS_TO_TICKS(500));
// Stop si urgence
if (gUrgenceActive) {
continuer = 0;
break;
}
// Re-lire humidité et seuil
uint16_t humCourante;
if (msg.zone == 1) {
humCourante = LireADC(ADC_CHANNEL_0);
gHum1 = humCourante;
} else {
humCourante = LireADC(ADC_CHANNEL_1);
gHum2 = humCourante;
}
uint16_t seuilCourant = 2000;
if (xSemaphoreTake(xThresholdMutex,
pdMS_TO_TICKS(50)) == pdTRUE) {
seuilCourant = gSeuilHumidite;
xSemaphoreGive(xThresholdMutex);
}
// Mettre à jour LCD ligne 0
char lcdL0[17];
snprintf(lcdL0, sizeof(lcdL0),
"Z1:%-4d Z2:%-4d", gHum1, gHum2);
LCD_Print(0, lcdL0);
// Sol redevenu humide → arrêter l'arrosage
if (humCourante >= seuilCourant) {
continuer = 0;
snprintf(ligne1, sizeof(ligne1),
"Z%d hum OK:%-4d", msg.zone, humCourante);
LCD_Print(1, ligne1);
vTaskDelay(pdMS_TO_TICKS(1000));
break;
}
// Timeout 10s atteint
if ((xTaskGetTickCount() - tempsDebut) >=
pdMS_TO_TICKS(10000)) {
continuer = 0;
break;
}
}
// Fermer la vanne et réinitialiser le flag
if (!gUrgenceActive) {
if (msg.zone == 1) {
HAL_GPIO_WritePin(PORT_A, PIN_LED_ZONE1, GPIO_PIN_RESET);
gVanne1Ouverte = 0;
snprintf(ligne1, sizeof(ligne1), "Z1 fermee");
} else {
HAL_GPIO_WritePin(PORT_A, PIN_LED_ZONE2, GPIO_PIN_RESET);
gVanne2Ouverte = 0;
snprintf(ligne1, sizeof(ligne1), "Z2 fermee");
}
LCD_Print(1, ligne1);
vTaskDelay(pdMS_TO_TICKS(2000));
LCD_Print(1, "");
}
}
}
}
/* ================================================================
* TÂCHE 3: Arrêt d'urgence
* Priorité : 4 (la plus haute, préempte tout)
* réinitialiser les flags de vannes lors de l'urgence
* ================================================================ */
void vTaskUrgence(void *pvParameters) {
for (;;) {
if (HAL_GPIO_ReadPin(PORT_B, PIN_BTN_URGENCE) == GPIO_PIN_RESET) {
vTaskDelay(pdMS_TO_TICKS(50));
if (HAL_GPIO_ReadPin(PORT_B, PIN_BTN_URGENCE) == GPIO_PIN_RESET) {
if (!gUrgenceActive) {
gUrgenceActive = 1;
HAL_GPIO_WritePin(PORT_A,
PIN_LED_ZONE1 | PIN_LED_ZONE2, GPIO_PIN_RESET);
HAL_GPIO_WritePin(PORT_A,
PIN_LED_URGENCE, GPIO_PIN_SET);
// Réinitialiser les flags de vannes
gVanne1Ouverte = 0;
gVanne2Ouverte = 0;
xQueueReset(xHumidityQueue);
xSemaphoreGive(xEmergencySem);
LCD_Print(0, "!!! URGENCE !!!");
LCD_Print(1, "Systeme arrete");
}
}
} else {
if (gUrgenceActive) {
gUrgenceActive = 0;
HAL_GPIO_WritePin(PORT_A, PIN_LED_URGENCE, GPIO_PIN_RESET);
LCD_Print(0, "Systeme OK");
LCD_Print(1, "Reprise...");
vTaskDelay(pdMS_TO_TICKS(2000));
LCD_Print(1, "");
}
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
/* ================================================================
* SECTION 13 — TÂCHE 4 : Ajustement seuil
* Période : 2s | Priorité : 2
* LCD ligne 1 : "Seuil:XXXX"
* ================================================================ */
void vTaskSeuil(void *pvParameters) {
char ligne1[17];
for (;;) {
uint16_t valPot = LireADC(ADC_CHANNEL_2);
if (xSemaphoreTake(xThresholdMutex, pdMS_TO_TICKS(200)) == pdTRUE) {
gSeuilHumidite = valPot;
xSemaphoreGive(xThresholdMutex);
}
if (!gUrgenceActive) {
snprintf(ligne1, sizeof(ligne1), "Seuil:%-4d", valPot);
LCD_Print(1, ligne1);
}
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
/* ================================================================
* SECTION 14 — TÂCHE 5 : Heartbeat
* Priorité : 1 (fond, s'exécute quand CPU libre)
* Clignote '*' en position (15,0) → prouve que FreeRTOS tourne
* ================================================================ */
void vTaskHeartbeat(void *pvParameters) {
uint8_t dot = 0;
for (;;) {
if (!gUrgenceActive) {
if (xSemaphoreTake(xLCDMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
LCD_SetCursor(15, 0);
LCD_Char(dot ? '*' : ' ');
xSemaphoreGive(xLCDMutex);
}
dot = !dot;
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* ================================================================
* SECTION 15 — Main
* ================================================================ */
int main(void) {
HAL_Init();
MX_GPIO_Init();
MX_ADC_Init();
MX_I2C_Init(); // I2C avant LCD !
// Initialiser le LCD via HAL (pas LiquidCrystal_I2C)
LCD_Init();
LCD_SetCursor(0, 0);
LCD_String("Arrosage Auto");
LCD_SetCursor(0, 1);
LCD_String("Demarrage...");
HAL_Delay(2000);
LCD_Cmd(0x01); // clear screen
HAL_Delay(2);
// Créer objets FreeRTOS
xHumidityQueue = xQueueCreate(5, sizeof(HumiditeMsg_t));
xEmergencySem = xSemaphoreCreateBinary();
xThresholdMutex = xSemaphoreCreateMutex();
xLCDMutex = xSemaphoreCreateMutex();
// Créer les tâches
xTaskCreate(vTaskSurveillance, "Surveillance", 256, NULL, 2, NULL);
xTaskCreate(vTaskVannes, "Vannes", 256, NULL, 3, NULL);
xTaskCreate(vTaskUrgence, "Urgence", 256, NULL, 4, NULL);
xTaskCreate(vTaskSeuil, "Seuil", 256, NULL, 2, NULL);
xTaskCreate(vTaskHeartbeat, "Heartbeat", 128, NULL, 1, NULL);
vTaskStartScheduler();
while (1);
}
void loop() {}
/* ================================================================
* GUIDE DU PROFESSEUR
* ================================================================
*
* POURQUOI LiquidCrystal_I2C NE FONCTIONNAIT PAS ?
* ──────────────────────────────────────────────────
* LiquidCrystal_I2C est une bibliothèque Arduino qui utilise
* Wire.h (API Arduino I2C). Sur STM32 avec HAL, Wire.h n'est
* pas disponible → le LCD restait muet. La solution est de
* piloter le PCF8574 (expandeur I2C du LCD) directement
* avec HAL_I2C_Master_Transmit(), ce que fait ce code.
*
* BROCHAGE I2C SUR NUCLEO-C031C6 :
* PB6 = SCL (horloge I2C)
* PB7 = SDA (données I2C)
* Ces broches utilisent la fonction alternate AF6_I2C1.
*
* COMMENT FONCTIONNE LE LCD EN MODE 4 BITS :
* ────────────────────────────────────────────
* Le LCD reçoit chaque octet en 2 fois (nibble haut puis bas).
* Le PCF8574 est un expandeur GPIO I2C : écrire 1 octet sur
* I2C pilote simultanément 8 broches GPIO qui vont vers le LCD.
* Bits du PCF8574 :
* [7..4] = D7..D4 (données/nibble)
* [3] = BL (backlight, toujours 1 pour allumer)
* [2] = EN (impulsion pour valider la donnée)
* [1] = RW (0 = écriture)
* [0] = RS (0 = commande, 1 = donnée/caractère)
*
* CE QUE VOUS VOYEZ SUR LE LCD :
* ────────────────────────────────
* Ligne 0 : "Z1:XXXX Z2:XXXX*"
* humidité zone1 humidité zone2 heartbeat
* Ligne 1 : change selon l'état du système :
* "Seuil:XXXX" → état normal
* "Arrosage Z1 ON" → vanne 1 ouverte
* "Arrosage Z2 ON" → vanne 2 ouverte
* "!!! URGENCE !!!"→ bouton urgence pressé
*
* PROCÉDURE DE TEST :
* ────────────────────
* TEST 1 — Arrosage zone 1 :
* → Tourner pot1 (Capteur Zone 1) à GAUCHE
* → LCD ligne 1 : "Arrosage Z1 ON"
* → LED bleue (PA5) s'allume 10 secondes
*
* TEST 2 — Arrosage zone 2 :
* → Tourner pot2 (Capteur Zone 2) à GAUCHE
* → LCD ligne 1 : "Arrosage Z2 ON"
* → LED verte (PA6) s'allume 10 secondes
*
* TEST 3 — Modifier le seuil :
* → Tourner pot_seuil à DROITE (seuil haut)
* → LCD ligne 1 : "Seuil:3500" → arrosage plus facile
* → Tourner pot_seuil à GAUCHE (seuil bas)
* → LCD ligne 1 : "Seuil:0200" → arrosage quasi impossible
*
* TEST 4 — Arrêt d'urgence :
* → Pendant un arrosage, cliquer le bouton ROUGE
* → LED bleue/verte s'éteignent IMMÉDIATEMENT
* → LED rouge (PA7) s'allume
* → LCD : "!!! URGENCE !!!" / "Systeme arrete"
* → Le heartbeat '*' disparaît
*
* TEST 5 — Reprise après urgence :
* → Relâcher le bouton rouge
* → LCD : "Systeme OK" puis retour normal
* → LED rouge s'éteint, système redémarre
*
* POURQUOI UN MUTEX POUR LE LCD (xLCDMutex) ?
* ─────────────────────────────────────────────
* Plusieurs tâches veulent écrire sur le LCD simultanément :
* T2 veut écrire "Arrosage Z1 ON" pendant que T4 veut écrire
* "Seuil:1500". Sans mutex, les deux écritures s'entremêlent
* et on voit des caractères aléatoires sur l'écran.
* Le xLCDMutex garantit qu'une seule tâche accède au LCD
* à la fois → affichage toujours cohérent.
* ================================================================
*/