Arduino, horloge DS3231, mémoire 24C32, et communication 433MHz avec STX882: planification et SLEEP mode

Arduino,  horloge DS3231, mémoire 24C32, et communication 433MHz avec STX882: planification et SLEEP mode

En avant vers un projet un peu complexe, il a fallu que je m’intéresse à la possibilité d’émettre et recevoir des informations à distance entre deux modules Arduino pour communiquer un état. Très vite s’est posée la question du mode de fonctionnement: allumer tout le temps le récepteur pour ne rien manquer ? Impossible, une aberration en terme d’autonomie. Mais alors comment faire ?

Le circuit DS3231 (avec mémoire intégrée 24C32)

Il est très facile de trouver des circuits d’horloge de ce type:

En vert se trouve un module d’horloge. Il fonctionne avec une simple pile bouton (ou via une alimentation extérieure). Il conserve ainsi son horloge, et maintient une heure exacte avec une dérive inférieure à 2 minutes par an, grâce à oscillateur à quartz compensé en température. On peut programmer deux alarmes sur ce module et ainsi s’en servir pour réveiller l’Arduino via une interruption dans un mode économie d’énergie.

De plus, il possède (entouré en rouge) un module 24C32, qui est une EEPROM de 4Ko, qui supporte 1 millions de cycles d’écritures et persiste la donnée même en cas de coupure de courant. C’est donc un module très pratique pour persister quelques informations pendant que l’Arduino est éteint et réveiller cet Arduino de manière programmée.

Le principe

On a donc maintenant une solution pour faire communiquer deux Arduinos sans les laisser allumer tout le temps:!

  • le récepteur est allumé et attend une émission
  • l’émetteur est alors allumé et réalise une émission. Il programme ensuite son alarme pour un temps +XX minutes
  • le récepteur reçoit l’émission. Il programme alors son alarme pour un temps +XX minutes moins quelques secondes.

Ainsi, même si les deux arduinos ne sont pas synchronisés en temps, ils reprogramment leur réveil pour un temps relatif identiques. Il ne peuvent pas se rater:

  • le récepteur se réveille quelques secondes avant l’émetteur et attend une émission
  • l’émetteur se réveille et réalise une émission. Il programme ensuite son alarme pour un temps +XX minutes
  • le récepteur reçoit l’émission. Il programme alors son alarme pour un temps +XX minutes moins quelques secondes

A chaque réveil, l’émission sert également de synchro temps.

Une expérience pour valider le principe

Pour valider cette approche, on réalise l’expérience suivante. L’émetteur envoie un statut en alternance: ROUGE ou VERT. Et il se rendort après chaque émission. Ce statut est stocké dans l’EEPROM, de manière à pouvoir le relire et assurer l’alternance au prochain réveil.

Le récepteur de son côté assure ses réveils réguliers et attend le message. Selon le message, il allume une LED rouge ou une LED verte pendant quelques secondes avant de s’endormir.

Les problèmes à résoudre

Pour réaliser cette expérience sur Arduino, il a fallu résoudre une myriade de problèmes:

  • la librairie pour l’émission sans fil Radiohead utilise déjà le mécanisme d’interruption et cela provoque quelques petits conflits lors de l’usage de cette interruption par l’horloge.
  • Acquitter l’alarme (remettre à zéro son drapeau de signalement) avant de programmer la suivante, avec un délai pour laisser le temps au circuit d’enregistrer l’écriture
  • vérifier via lecture après chaque écriture sur l’horloge (alarme, front) est une précaution supplémentaire pour robustifier les endormissements/réveils des Arduinos
  • Et un peu d’hygiène sur le bus I2C pour le débloquer en cas de problème de communication via le bus.

Avec toutes ces précautions, j’ai pu tester mon expérience toute une nuit, et le matin, les deux arduino se réveillaient encore régulièrement, ensemble et capable d’échanger un message. Bien mieux que mes premiers tests au début qui ne duraient que quelques heures avant de voir un ou deux arduinos définitivement endormis et aucune alarme capable de les réveiller.

Une fois que les codes sont chargés sur chaque Arduino, on appuie sur le bouton reset de chaque carte en même temps et on les laisse se synchroniser. Ensuite, le cycle de réveil a lieu toutes les 30 secondes selon la séquence suivante:

  • réveil du récepteur, avec la led du statut précédent allumée
  • réveil de l’émetteur, bref flash jaune lié à l’émission
  • l’émetteur se rendort
  • le récepteur change la couleur de la led de statut et se rendort

Avec ce circuit, on a une bonne situation pratique pour tester l’ensemble de ces composants et leur distance de fonctionnement. Mais attention, à alimenter avec une batterie externe. Car même une pile 9V ne tient pas longtemps: en cause, les leds de statut de l’horloge et de l’arduino et les convertisseurs de tension.

Côté émetteur: circuit et code

Voici le circuit émetteur, avec une led jaune qui s’allume brièvement pour rendre visible la communication.

// =============================================================================
// ÉMETTEUR RADIO - Arduino Uno + STX882 + DS3231/24C32
// =============================================================================
//
// DESCRIPTION :
//   Au réveil (déclenché par l'alarme DS3231 sur INT0/pin2), lit l'état
//   courant (ROUGE ou VERT) en EEPROM (via 24C32), l'alterne, l'émet en
//   radio (RadioHead RH_ASK via STX882), enregistre le nouvel état en
//   EEPROM, programme l'alarme suivante dans WAKE_INTERVAL_TX secondes,
//   puis se rendort en SLEEP_MODE_PWR_DOWN.
//
// ROBUSTESSE I2C — 4 défenses cumulées :
//   1. Reset du flag A1F en premier     → évite la condition de course sur SQW
//   2. Délai entre opérations I2C       → laisse le DS3231 propager ses écritures
//   3. Acknowledge (relire après écrire)→ détecte les corruptions silencieuses
//   4. Reset bus I2C (9 pulses clock)   → débloque un esclave qui tient SDA bas
//
// =============================================================================
// CÂBLAGE
// =============================================================================
//
//  ┌─────────────────────────────────────────────────────────────────┐
//  │  DS3231 (I2C)                                                   │
//  │    VCC  → 3.3V ou 5V Arduino                                    │
//  │    GND  → GND Arduino                                           │
//  │    SDA  → A4 (Arduino Uno)                                      │
//  │    SCL  → A5 (Arduino Uno)                                      │
//  │    SQW  → D2 (INT0) — signal d'alarme (LOW actif)              │
//  │                                                                 │
//  │  24C32 (EEPROM sur même module, même bus I2C)                   │
//  │    Adresse I2C par défaut : 0x57                                │
//  │    (partagé automatiquement avec DS3231 à 0x68)                 │
//  │                                                                 │
//  │  STX882 (ASK/OOK 433 MHz, émission)                             │
//  │    VCC  → 5V Arduino                                            │
//  │    GND  → GND Arduino                                           │
//  │    DATA → D12 (pin TX RH_ASK par défaut)                        │
//  │    (antenne : fil 1/4 d'onde ≈ 17 cm sur la broche ANT)         │
//  │                                                                 │
//  │  LED JAUNE (signal d'émission)                                  │
//  │    Anode  → D7 (via résistance 220 Ω)                           │
//  │    Cathode → GND                                                │
//  └─────────────────────────────────────────────────────────────────┘
//
// =============================================================================

#include <avr/sleep.h>
#include <avr/power.h>
#include <Wire.h>
#include <RH_ASK.h>
#include <SPI.h>          // requis par RadioHead même en ASK

// =============================================================================
// PARAMÈTRES PRINCIPAUX — modifier ici
// =============================================================================

// Mettre à 0 pour désactiver tous les messages Serial (production / économie RAM)
#define DEBUG 1

// Vitesse du port série (utilisée uniquement si DEBUG=1)
const uint32_t SERIAL_BAUD            = 9600;

// Intervalle entre deux émissions (secondes).
// L'alarme 1 du DS3231 supporte hh:mm:ss → pas de limite pratique (< 86400 s).
const uint8_t WAKE_INTERVAL_TX        = 30;

// Durée d'allumage de la LED jaune pendant l'émission (ms)
const uint16_t LED_TX_DURATION        = 300;

// Broche de la LED jaune
const uint8_t PIN_LED_JAUNE           = 7;

// Broche de l'alarme DS3231 (INT0 = D2 sur Uno)
const uint8_t PIN_ALARM               = 2;

// Adresse I2C du DS3231
const uint8_t DS3231_ADDR             = 0x68;

// Adresse I2C du 24C32 (A0/A1/A2 à GND → 0x57)
const uint8_t EEPROM_I2C_ADDR        = 0x57;

// Adresse mémoire dans le 24C32 où l'on stocke l'état
const uint16_t EEPROM_STATE_ADDR      = 0x0000;

// Valeurs de l'état
const uint8_t ETAT_ROUGE              = 0x01;
const uint8_t ETAT_VERT               = 0x02;

// Nombre maximum de tentatives pour une écriture I2C avec acknowledge
const uint8_t I2C_MAX_RETRIES         = 3;

// Délai entre deux opérations I2C consécutives (ms) — défense n°2
const uint8_t I2C_INTER_OP_DELAY_MS  = 5;

// =============================================================================
// MACRO DE DEBUG — compilées à zéro si DEBUG=0
// =============================================================================

#if DEBUG
  #define DBG(msg)        Serial.print(F(msg))
  #define DBGLN(msg)      Serial.println(F(msg))
  #define DBGVAL(msg, v)  do { Serial.print(F(msg)); Serial.println(v); } while(0)
  #define DBGSTR(v)       Serial.println(v)
  #define DBG_FLUSH()     Serial.flush()
#else
  #define DBG(msg)
  #define DBGLN(msg)
  #define DBGVAL(msg, v)
  #define DBGSTR(v)
  #define DBG_FLUSH()
#endif

// =============================================================================
// VARIABLES GLOBALES
// =============================================================================

volatile bool alarmeFired = false;

// PTT = 255 → désactivé, évite que RadioHead tire pin 10 ou 13 HIGH en permanence
RH_ASK radio(2000, 11, 12, 255);

// =============================================================================
// FONCTIONS UTILITAIRES — I2C robuste
// =============================================================================

uint8_t decToBcd(uint8_t val) { return ((val / 10) << 4) | (val % 10); }
uint8_t bcdToDec(uint8_t val) { return ((val >> 4) * 10) + (val & 0x0F); }

// Défense n°4 — Reset bus I2C par 9 impulsions clock manuelles.
// Débloque un esclave qui tient SDA bas suite à une transaction interrompue.
// Recommandé par NXP UM10204 §3.1.16.
void i2cReset() {
  DBGLN("[I2C] Reset bus (9 pulses)...");
  Wire.end();
  delay(10);
  pinMode(A4, OUTPUT); pinMode(A5, OUTPUT);  // SDA=A4, SCL=A5
  digitalWrite(A4, HIGH);
  for (uint8_t i = 0; i < 9; i++) {
    digitalWrite(A5, LOW);  delayMicroseconds(5);
    digitalWrite(A5, HIGH); delayMicroseconds(5);
  }
  // Condition STOP : SDA monte pendant que SCL est HIGH
  digitalWrite(A4, LOW);  delayMicroseconds(5);
  digitalWrite(A5, HIGH); delayMicroseconds(5);
  digitalWrite(A4, HIGH);
  Wire.begin();
  delay(10);
  DBGLN("[I2C] Bus reinitialise.");
}

// Écrit un registre DS3231 et vérifie en relisant (acknowledge).
// Défenses n°2 (délai), n°3 (relecture), n°4 (reset si erreur persistante).
// Retourne true si l'écriture est confirmée.
bool i2cEcrireRegistreDS3231(uint8_t reg, uint8_t valeur) {
  for (uint8_t tentative = 1; tentative <= I2C_MAX_RETRIES; tentative++) {

    // Défense n°2 : délai avant chaque opération
    delay(I2C_INTER_OP_DELAY_MS);

    // Écriture
    Wire.beginTransmission(DS3231_ADDR);
    Wire.write(reg);
    Wire.write(valeur);
    uint8_t err = Wire.endTransmission();

    if (err != 0) {
      DBGVAL("[I2C] Erreur transmission reg=0x", reg);
      DBGVAL("[I2C]   I2C err=", err);
      DBGVAL("[I2C]   Tentative ", tentative);
      i2cReset();  // défense n°4
      continue;
    }

    // Défense n°2 : délai entre écriture et relecture
    delay(I2C_INTER_OP_DELAY_MS);

    // Défense n°3 : acknowledge — relire et comparer
    Wire.beginTransmission(DS3231_ADDR);
    Wire.write(reg);
    Wire.endTransmission();
    Wire.requestFrom(DS3231_ADDR, (uint8_t)1);
    uint8_t lu = Wire.read();

    if (lu == valeur) {
      // Écriture confirmée
      return true;
    }

    DBGVAL("[I2C] Acknowledge KO reg=0x", reg);
    DBG("[I2C]   ecrit=0x"); Serial.print(valeur, HEX);
    DBG(" lu=0x"); Serial.println(lu, HEX);
    DBGVAL("[I2C]   Tentative ", tentative);
    i2cReset();  // défense n°4
  }

  DBGVAL("[I2C] ECHEC definitif apres ", I2C_MAX_RETRIES);
  DBGLN("[I2C]   tentatives.");
  return false;
}

// =============================================================================
// FONCTIONS UTILITAIRES — DS3231
// =============================================================================

// Lit l'heure complète (sec, min, heure) depuis le DS3231
void ds3231LireHeure(uint8_t &sec, uint8_t &min, uint8_t &heur) {
  Wire.beginTransmission(DS3231_ADDR);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(DS3231_ADDR, (uint8_t)3);
  sec  = bcdToDec(Wire.read() & 0x7F);
  min  = bcdToDec(Wire.read() & 0x7F);
  heur = bcdToDec(Wire.read() & 0x3F);
}

// Programme l'alarme 1 du DS3231 à "maintenant + deltaSec".
//
// ORDRE DES OPÉRATIONS (critique) :
//   1. Effacer A1F EN PREMIER → SQW remonte HIGH avant qu'on arme l'IRQ
//   2. Lire l'heure courante
//   3. Écrire les registres de l'alarme (avec acknowledge)
//   4. Activer l'interruption (avec acknowledge)
//
// Chaque écriture est vérifiée par relecture (acknowledge).
// En cas d'échec I2C, le bus est resetté et on retente (I2C_MAX_RETRIES fois).
void ds3231ProgrammerAlarme1(uint8_t deltaSec) {

  // --- ÉTAPE 1 : effacer A1F et A2F AVANT tout le reste (défense n°1) ---
  // Le registre de statut (0x0F) contient des bits read-only (OSF, EN32kHz).
  // On ne peut pas faire un acknowledge bit-à-bit sur A1F seul — on écrit 0x00
  // ce qui efface aussi OSF (acceptable) et EN32kHz (=0 par défaut).
  // La vérification porte uniquement sur les bits A1F (bit0) et A2F (bit1).
  DBGLN("[DS3231] Effacement flag A1F...");
  for (uint8_t t = 1; t <= I2C_MAX_RETRIES; t++) {
    delay(I2C_INTER_OP_DELAY_MS);
    Wire.beginTransmission(DS3231_ADDR);
    Wire.write(0x0F);
    Wire.write(0x00);
    Wire.endTransmission();

    delay(I2C_INTER_OP_DELAY_MS);

    // Vérifier que A1F et A2F sont bien à 0
    Wire.beginTransmission(DS3231_ADDR);
    Wire.write(0x0F);
    Wire.endTransmission();
    Wire.requestFrom(DS3231_ADDR, (uint8_t)1);
    uint8_t statut = Wire.read();

    if ((statut & 0x03) == 0x00) {
      DBGLN("[DS3231] Flag A1F efface et confirme.");
      break;
    }
    DBGVAL("[DS3231] Flag A1F non efface (statut=0x", statut);
    DBGLN("), retry...");
    i2cReset();
  }

  // SQW est maintenant HIGH — délai de stabilisation avant d'armer l'IRQ
  delay(10);  // défense n°2 : laisser SQW se stabiliser

  // --- ÉTAPE 2 : lire l'heure courante ---
  uint8_t sec, min, heur;
  ds3231LireHeure(sec, min, heur);

  uint8_t cibleSec = (sec  + deltaSec) % 60;
  uint8_t retenue1 = (sec  + deltaSec) / 60;
  uint8_t cibleMin = (min  + retenue1) % 60;
  uint8_t retenue2 = (min  + retenue1) / 60;
  uint8_t cibleHeur= (heur + retenue2) % 24;

  DBG("[DS3231] Heure actuelle : ");
  Serial.print(heur); DBG(":"); Serial.print(min); DBG(":"); Serial.println(sec);
  DBG("[DS3231] Alarme cible   : ");
  Serial.print(cibleHeur); DBG(":"); Serial.print(cibleMin); DBG(":"); Serial.println(cibleSec);

  // --- ÉTAPE 3 : écrire les 4 registres de l'alarme 1 (0x07 à 0x0A) ---
  // A1M1=0, A1M2=0, A1M3=0, A1M4=1 → alarme quand hh:mm:ss correspondent (ignore date)
  // Chaque registre est écrit et vérifié individuellement.
  bool ok = true;
  ok &= i2cEcrireRegistreDS3231(0x07, decToBcd(cibleSec));           // A1 secondes
  ok &= i2cEcrireRegistreDS3231(0x08, decToBcd(cibleMin));           // A1 minutes
  ok &= i2cEcrireRegistreDS3231(0x09, decToBcd(cibleHeur));          // A1 heures
  ok &= i2cEcrireRegistreDS3231(0x0A, 0b10000000);                   // A1M4=1

  // --- ÉTAPE 4 : activer l'interruption (INTCN=1, A1IE=1, A2IE=0) ---
  ok &= i2cEcrireRegistreDS3231(0x0E, 0b00000101);

  if (ok) {
    DBGLN("[DS3231] Alarme 1 configuree et confirmee.");
  } else {
    DBGLN("[DS3231] ATTENTION : alarme 1 configuree avec des erreurs.");
  }
}

// =============================================================================
// FONCTIONS UTILITAIRES — EEPROM I2C (24C32)
// =============================================================================

// Écrit un octet dans le 24C32 et vérifie par relecture.
void eepromEcrire(uint16_t adresse, uint8_t valeur) {
  for (uint8_t t = 1; t <= I2C_MAX_RETRIES; t++) {
    delay(I2C_INTER_OP_DELAY_MS);  // défense n°2

    Wire.beginTransmission(EEPROM_I2C_ADDR);
    Wire.write((uint8_t)(adresse >> 8));
    Wire.write((uint8_t)(adresse & 0xFF));
    Wire.write(valeur);
    uint8_t err = Wire.endTransmission();
    delay(5);  // temps d'écriture interne du 24C32 (tWR ≤ 5ms)

    if (err != 0) {
      DBGVAL("[EEPROM] Erreur transmission (I2C err=", err);
      DBGVAL(") tentative ", t);
      i2cReset();
      continue;
    }

    // Défense n°3 : relire et comparer
    delay(I2C_INTER_OP_DELAY_MS);
    Wire.beginTransmission(EEPROM_I2C_ADDR);
    Wire.write((uint8_t)(adresse >> 8));
    Wire.write((uint8_t)(adresse & 0xFF));
    Wire.endTransmission();
    Wire.requestFrom(EEPROM_I2C_ADDR, (uint8_t)1);
    uint8_t lu = Wire.read();

    if (lu == valeur) {
      DBG("[EEPROM] Ecriture confirmee addr=0x"); Serial.print(adresse, HEX);
      DBG(" val=0x"); Serial.println(valeur, HEX);
      return;
    }

    DBGVAL("[EEPROM] Acknowledge KO tentative ", t);
    DBG("[EEPROM]   ecrit=0x"); Serial.print(valeur, HEX);
    DBG(" lu=0x"); Serial.println(lu, HEX);
    i2cReset();
  }

  DBGLN("[EEPROM] ECHEC definitif ecriture.");
}

// Lit un octet depuis le 24C32.
uint8_t eepromLire(uint16_t adresse) {
  delay(I2C_INTER_OP_DELAY_MS);  // défense n°2

  Wire.beginTransmission(EEPROM_I2C_ADDR);
  Wire.write((uint8_t)(adresse >> 8));
  Wire.write((uint8_t)(adresse & 0xFF));
  uint8_t err = Wire.endTransmission();
  Wire.requestFrom(EEPROM_I2C_ADDR, (uint8_t)1);
  uint8_t val = Wire.read();

  if (err == 0) {
    DBG("[EEPROM] Lecture addr=0x"); Serial.print(adresse, HEX);
    DBG(" val=0x"); Serial.println(val, HEX);
  } else {
    DBGVAL("[EEPROM] Erreur lecture (I2C err=", err);
    DBGLN(")");
  }
  return val;
}

// =============================================================================
// FONCTIONS UTILITAIRES — RADIO
// =============================================================================

// Émet un octet d'état via RadioHead RH_ASK, avec signal LED jaune.
void emettreEtat(uint8_t etat) {
  const char* nom = (etat == ETAT_ROUGE) ? "ROUGE" : "VERT";
  DBG("[RADIO] Emission de l'etat : "); DBGSTR(nom);

  digitalWrite(PIN_LED_JAUNE, HIGH);
  bool ok = radio.send(&etat, 1);
  radio.waitPacketSent();
  delay(LED_TX_DURATION);
  digitalWrite(PIN_LED_JAUNE, LOW);

  if (ok) {
    DBGLN("[RADIO] Paquet envoye avec succes.");
  } else {
    DBGLN("[RADIO] ECHEC envoi paquet.");
  }
}

// =============================================================================
// FONCTIONS UTILITAIRES — SOMMEIL
// =============================================================================

void isrReveil() {
  alarmeFired = true;
}

// Endort le MCU en SLEEP_MODE_PWR_DOWN.
// Réveil sur niveau bas de INT0 (SQW du DS3231, open-drain).
// PRÉCONDITION : le flag A1F doit être effacé avant d'appeler cette fonction,
//               sinon SQW est encore LOW et attachInterrupt déclenche immédiatement.
void dormirJusquAlarme() {
  DBGLN("[SLEEP] Mise en veille (SLEEP_MODE_PWR_DOWN)...");
  DBG_FLUSH();

  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
  power_adc_disable();

  // Armer l'interruption seulement maintenant (SQW est HIGH après effacement A1F)
  attachInterrupt(digitalPinToInterrupt(PIN_ALARM), isrReveil, LOW);
  sleep_cpu();  // ← s'endort ici, reprend à la ligne suivante après l'IRQ

  sleep_disable();
  detachInterrupt(digitalPinToInterrupt(PIN_ALARM));
  power_adc_enable();

  DBGLN("[SLEEP] Reveil !");
}

// =============================================================================
// SETUP
// =============================================================================

void setup() {
#if DEBUG
  Serial.begin(SERIAL_BAUD);
  while (!Serial) {}
  DBGLN("=== EMETTEUR — demarrage ===");
  DBGVAL("[CONFIG] WAKE_INTERVAL_TX    = ", WAKE_INTERVAL_TX);
  DBGVAL("[CONFIG] LED_TX_DURATION     = ", LED_TX_DURATION);
  DBGVAL("[CONFIG] I2C_MAX_RETRIES     = ", I2C_MAX_RETRIES);
  DBGVAL("[CONFIG] I2C_INTER_OP_DELAY  = ", I2C_INTER_OP_DELAY_MS);
#endif

  pinMode(PIN_LED_JAUNE, OUTPUT);
  digitalWrite(PIN_LED_JAUNE, LOW);

  // SQW est open-drain : le pull-up interne est indispensable
  pinMode(PIN_ALARM, INPUT_PULLUP);

  Wire.begin();
  DBGLN("[INIT] Bus I2C initialise.");

  // RadioHead doit être initialisé EN PREMIER — il appelle attachInterrupt() en interne
  if (radio.init()) {
    DBGLN("[INIT] RadioHead RH_ASK OK.");
  } else {
    DBGLN("[INIT] ERREUR init RadioHead !");
  }

  // RH_ASK inclut SPI.h qui peut laisser SCK (pin 13 = LED L) HIGH après init.
  // On force pin 13 LOW explicitement pour éteindre la LED L.
  pinMode(13, OUTPUT);
  digitalWrite(13, LOW);
  DBGLN("[INIT] Pin 13 forcee LOW (LED L eteinte).");

  DBGVAL("[INIT] Premiere alarme dans ", WAKE_INTERVAL_TX);
  DBGLN("[INIT]   secondes.");
  ds3231ProgrammerAlarme1(WAKE_INTERVAL_TX);

  DBGLN("[INIT] Setup termine.");
}

// =============================================================================
// LOOP — déroulé principal
// =============================================================================

void loop() {
  // 1. Sommeil jusqu'à l'alarme DS3231
  //    (A1F est effacé dans ds3231ProgrammerAlarme1, SQW est donc HIGH ici)
  alarmeFired = false;
  dormirJusquAlarme();

  DBGLN("--- Cycle emetteur ---");

  // 2. Lire l'état précédent en EEPROM
  DBGLN("[LOOP] Lecture etat precedent...");
  uint8_t etatActuel = eepromLire(EEPROM_STATE_ADDR);

  // 3. Alterner l'état (initialiser sur ROUGE si valeur inconnue)
  uint8_t nouvelEtat;
  if (etatActuel == ETAT_ROUGE) {
    nouvelEtat = ETAT_VERT;
    DBGLN("[LOOP] ROUGE -> VERT");
  } else {
    nouvelEtat = ETAT_ROUGE;
    if (etatActuel == ETAT_VERT) {
      DBGLN("[LOOP] VERT -> ROUGE");
    } else {
      DBG("[LOOP] Etat inconnu (0x"); Serial.print(etatActuel, HEX);
      DBGLN(") -> ROUGE (init)");
    }
  }

  // 4. Émettre par radio
  emettreEtat(nouvelEtat);

  // 5. Sauvegarder en EEPROM (avec acknowledge)
  DBGLN("[LOOP] Sauvegarde EEPROM...");
  eepromEcrire(EEPROM_STATE_ADDR, nouvelEtat);

  // 6. Programmer l'alarme suivante (efface A1F en premier, acknowledge sur chaque registre)
  DBGVAL("[LOOP] Prochaine alarme dans ", WAKE_INTERVAL_TX);
  DBGLN("[LOOP]   secondes.");
  ds3231ProgrammerAlarme1(WAKE_INTERVAL_TX);

  DBGLN("[LOOP] Retour en sommeil.");
  // → dormirJusquAlarme() en tête de loop
}

Côté récepteur: circuit et code

Voici le circuit côté récepteur.

// =============================================================================
// RÉCEPTEUR RADIO - Arduino Uno + SRX882S + DS3231/24C32 + LED rouge + LED verte
// =============================================================================
//
// DESCRIPTION :
//   Se réveille WAKE_INTERVAL_RX secondes après chaque échange (soit
//   quelques secondes avant l'émetteur), allume la LED correspondant au
//   DERNIER état connu (lu en EEPROM), puis écoute la radio pendant
//   LISTEN_TIMEOUT_MS ms. Dès réception, allume la LED du NOUVEL état,
//   sauvegarde en EEPROM, programme l'alarme suivante dans
//   WAKE_INTERVAL_RX secondes, et se rendort.
//   Si aucun paquet n'est reçu dans le délai, il se rendort quand même.
//
// ROBUSTESSE I2C — 4 défenses cumulées :
//   1. Reset du flag A1F en premier     → évite la condition de course sur SQW
//   2. Délai entre opérations I2C       → laisse le DS3231 propager ses écritures
//   3. Acknowledge (relire après écrire)→ détecte les corruptions silencieuses
//   4. Reset bus I2C (9 pulses clock)   → débloque un esclave qui tient SDA bas
//
// =============================================================================
// CÂBLAGE
// =============================================================================
//
//  ┌─────────────────────────────────────────────────────────────────┐
//  │  DS3231 (I2C)                                                   │
//  │    VCC  → 3.3V ou 5V Arduino                                    │
//  │    GND  → GND Arduino                                           │
//  │    SDA  → A4 (Arduino Uno)                                      │
//  │    SCL  → A5 (Arduino Uno)                                      │
//  │    SQW  → D2 (INT0) — signal d'alarme (LOW actif)              │
//  │                                                                 │
//  │  24C32 (EEPROM sur même module, même bus I2C)                   │
//  │    Adresse I2C par défaut : 0x57                                │
//  │    (partagé automatiquement avec DS3231 à 0x68)                 │
//  │                                                                 │
//  │  SRX882S (récepteur ASK/OOK 433 MHz)                            │
//  │    VCC  → 5V Arduino                                            │
//  │    GND  → GND Arduino                                           │
//  │    DATA → D11 (pin RX RH_ASK par défaut)                        │
//  │    CS   → 5V (activer la réception en continu)                  │
//  │    (antenne : fil 1/4 d'onde ≈ 17 cm sur la broche ANT)         │
//  │                                                                 │
//  │  LED ROUGE                                                      │
//  │    Anode  → D5 (via résistance 220 Ω)                           │
//  │    Cathode → GND                                                │
//  │                                                                 │
//  │  LED VERTE                                                      │
//  │    Anode  → D6 (via résistance 220 Ω)                           │
//  │    Cathode → GND                                                │
//  └─────────────────────────────────────────────────────────────────┘
//
// =============================================================================

#include <avr/sleep.h>
#include <avr/power.h>
#include <Wire.h>
#include <RH_ASK.h>
#include <SPI.h>

// =============================================================================
// PARAMÈTRES PRINCIPAUX — modifier ici
// =============================================================================

#define DEBUG 1

const uint32_t SERIAL_BAUD            = 9600;

// Intervalle de réveil du récepteur (< WAKE_INTERVAL_TX de l'émetteur).
// Ex : émetteur à T+30s, récepteur à T+25s → marge 5s pour se mettre en écoute.
const uint8_t WAKE_INTERVAL_RX        = 25;

// Durée d'écoute radio après réveil (ms). Doit couvrir le délai de réveil TX.
const uint16_t LISTEN_TIMEOUT_MS      = 8000;

// Durée d'affichage de l'ancien état (LED allumée pendant l'attente radio)
const uint16_t LED_ANCIEN_DURATION    = 2000;

// Durée d'affichage du nouvel état avant le sleep
const uint16_t LED_NOUVEL_DURATION    = 2000;

const uint8_t PIN_LED_ROUGE           = 5;
const uint8_t PIN_LED_VERTE           = 6;
const uint8_t PIN_ALARM               = 2;

const uint8_t DS3231_ADDR             = 0x68;
const uint8_t EEPROM_I2C_ADDR        = 0x57;
const uint16_t EEPROM_STATE_ADDR      = 0x0000;

const uint8_t ETAT_ROUGE              = 0x01;
const uint8_t ETAT_VERT               = 0x02;

const uint8_t I2C_MAX_RETRIES         = 3;
const uint8_t I2C_INTER_OP_DELAY_MS  = 5;

// =============================================================================
// MACRO DE DEBUG
// =============================================================================

#if DEBUG
  #define DBG(msg)        Serial.print(F(msg))
  #define DBGLN(msg)      Serial.println(F(msg))
  #define DBGVAL(msg, v)  do { Serial.print(F(msg)); Serial.println(v); } while(0)
  #define DBGSTR(v)       Serial.println(v)
  #define DBG_FLUSH()     Serial.flush()
#else
  #define DBG(msg)
  #define DBGLN(msg)
  #define DBGVAL(msg, v)
  #define DBGSTR(v)
  #define DBG_FLUSH()
#endif

// =============================================================================
// VARIABLES GLOBALES
// =============================================================================

volatile bool alarmeFired = false;
uint16_t cycleCount = 0;

// PTT = 255 → désactivé
RH_ASK radio(2000, 11, 12, 255);

// =============================================================================
// FONCTIONS UTILITAIRES — I2C robuste
// =============================================================================

uint8_t decToBcd(uint8_t val) { return ((val / 10) << 4) | (val % 10); }
uint8_t bcdToDec(uint8_t val) { return ((val >> 4) * 10) + (val & 0x0F); }

// Défense n°4 — Reset bus I2C par 9 impulsions clock manuelles.
void i2cReset() {
  DBGLN("[I2C] Reset bus (9 pulses)...");
  Wire.end();
  delay(10);
  pinMode(A4, OUTPUT); pinMode(A5, OUTPUT);
  digitalWrite(A4, HIGH);
  for (uint8_t i = 0; i < 9; i++) {
    digitalWrite(A5, LOW);  delayMicroseconds(5);
    digitalWrite(A5, HIGH); delayMicroseconds(5);
  }
  digitalWrite(A4, LOW);  delayMicroseconds(5);
  digitalWrite(A5, HIGH); delayMicroseconds(5);
  digitalWrite(A4, HIGH);
  Wire.begin();
  delay(10);
  DBGLN("[I2C] Bus reinitialise.");
}

// Écrit un registre DS3231 et vérifie par relecture.
// Défenses n°2 (délai), n°3 (relecture), n°4 (reset si erreur).
bool i2cEcrireRegistreDS3231(uint8_t reg, uint8_t valeur) {
  for (uint8_t t = 1; t <= I2C_MAX_RETRIES; t++) {
    delay(I2C_INTER_OP_DELAY_MS);  // défense n°2

    Wire.beginTransmission(DS3231_ADDR);
    Wire.write(reg);
    Wire.write(valeur);
    uint8_t err = Wire.endTransmission();

    if (err != 0) {
      DBGVAL("[I2C] Erreur transmission reg=0x", reg);
      DBGVAL("[I2C]   err=", err); DBGVAL(" tentative ", t);
      i2cReset();
      continue;
    }

    delay(I2C_INTER_OP_DELAY_MS);  // défense n°2 entre écriture et relecture

    // Défense n°3 : acknowledge
    Wire.beginTransmission(DS3231_ADDR);
    Wire.write(reg);
    Wire.endTransmission();
    Wire.requestFrom(DS3231_ADDR, (uint8_t)1);
    uint8_t lu = Wire.read();

    if (lu == valeur) return true;

    DBGVAL("[I2C] Acknowledge KO reg=0x", reg);
    DBG("[I2C]   ecrit=0x"); Serial.print(valeur, HEX);
    DBG(" lu=0x"); Serial.println(lu, HEX);
    i2cReset();
  }

  DBGVAL("[I2C] ECHEC definitif apres ", I2C_MAX_RETRIES);
  DBGLN(" tentatives.");
  return false;
}

// =============================================================================
// FONCTIONS UTILITAIRES — DS3231
// =============================================================================

void ds3231LireHeure(uint8_t &sec, uint8_t &min, uint8_t &heur) {
  Wire.beginTransmission(DS3231_ADDR);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(DS3231_ADDR, (uint8_t)3);
  sec  = bcdToDec(Wire.read() & 0x7F);
  min  = bcdToDec(Wire.read() & 0x7F);
  heur = bcdToDec(Wire.read() & 0x3F);
}

// Programme l'alarme 1 à "maintenant + deltaSec".
// Ordre : 1) effacer A1F → 2) lire heure → 3) écrire alarme → 4) activer IRQ
void ds3231ProgrammerAlarme1(uint8_t deltaSec) {

  // --- ÉTAPE 1 : effacer A1F et A2F EN PREMIER (défense n°1) ---
  DBGLN("[DS3231] Effacement flag A1F...");
  for (uint8_t t = 1; t <= I2C_MAX_RETRIES; t++) {
    delay(I2C_INTER_OP_DELAY_MS);
    Wire.beginTransmission(DS3231_ADDR);
    Wire.write(0x0F);
    Wire.write(0x00);
    Wire.endTransmission();

    delay(I2C_INTER_OP_DELAY_MS);

    Wire.beginTransmission(DS3231_ADDR);
    Wire.write(0x0F);
    Wire.endTransmission();
    Wire.requestFrom(DS3231_ADDR, (uint8_t)1);
    uint8_t statut = Wire.read();

    if ((statut & 0x03) == 0x00) {
      DBGLN("[DS3231] Flag A1F efface et confirme.");
      break;
    }
    DBGVAL("[DS3231] Flag non efface (statut=0x", statut);
    DBGLN("), retry...");
    i2cReset();
  }

  // Délai de stabilisation SQW avant d'armer l'IRQ (défense n°2)
  delay(10);

  // --- ÉTAPE 2 : lire l'heure courante ---
  uint8_t sec, min, heur;
  ds3231LireHeure(sec, min, heur);

  uint8_t cibleSec  = (sec  + deltaSec) % 60;
  uint8_t retenue1  = (sec  + deltaSec) / 60;
  uint8_t cibleMin  = (min  + retenue1) % 60;
  uint8_t retenue2  = (min  + retenue1) / 60;
  uint8_t cibleHeur = (heur + retenue2) % 24;

  DBG("[DS3231] Heure actuelle : ");
  Serial.print(heur); DBG(":"); Serial.print(min); DBG(":"); Serial.println(sec);
  DBG("[DS3231] Alarme cible   : ");
  Serial.print(cibleHeur); DBG(":"); Serial.print(cibleMin); DBG(":"); Serial.println(cibleSec);

  // --- ÉTAPE 3 : écrire les registres alarme (avec acknowledge) ---
  bool ok = true;
  ok &= i2cEcrireRegistreDS3231(0x07, decToBcd(cibleSec));
  ok &= i2cEcrireRegistreDS3231(0x08, decToBcd(cibleMin));
  ok &= i2cEcrireRegistreDS3231(0x09, decToBcd(cibleHeur));
  ok &= i2cEcrireRegistreDS3231(0x0A, 0b10000000);   // A1M4=1

  // --- ÉTAPE 4 : activer l'interruption ---
  ok &= i2cEcrireRegistreDS3231(0x0E, 0b00000101);   // INTCN=1, A1IE=1

  if (ok) {
    DBGLN("[DS3231] Alarme 1 configuree et confirmee.");
  } else {
    DBGLN("[DS3231] ATTENTION : alarme configuree avec des erreurs.");
  }
}

// =============================================================================
// FONCTIONS UTILITAIRES — EEPROM I2C (24C32)
// =============================================================================

void eepromEcrire(uint16_t adresse, uint8_t valeur) {
  for (uint8_t t = 1; t <= I2C_MAX_RETRIES; t++) {
    delay(I2C_INTER_OP_DELAY_MS);

    Wire.beginTransmission(EEPROM_I2C_ADDR);
    Wire.write((uint8_t)(adresse >> 8));
    Wire.write((uint8_t)(adresse & 0xFF));
    Wire.write(valeur);
    uint8_t err = Wire.endTransmission();
    delay(5);  // tWR interne 24C32

    if (err != 0) {
      DBGVAL("[EEPROM] Erreur (I2C err=", err);
      DBGVAL(") tentative ", t);
      i2cReset();
      continue;
    }

    delay(I2C_INTER_OP_DELAY_MS);
    Wire.beginTransmission(EEPROM_I2C_ADDR);
    Wire.write((uint8_t)(adresse >> 8));
    Wire.write((uint8_t)(adresse & 0xFF));
    Wire.endTransmission();
    Wire.requestFrom(EEPROM_I2C_ADDR, (uint8_t)1);
    uint8_t lu = Wire.read();

    if (lu == valeur) {
      DBG("[EEPROM] Ecriture confirmee addr=0x"); Serial.print(adresse, HEX);
      DBG(" val=0x"); Serial.println(valeur, HEX);
      return;
    }

    DBGVAL("[EEPROM] Acknowledge KO tentative ", t);
    i2cReset();
  }
  DBGLN("[EEPROM] ECHEC definitif ecriture.");
}

uint8_t eepromLire(uint16_t adresse) {
  delay(I2C_INTER_OP_DELAY_MS);

  Wire.beginTransmission(EEPROM_I2C_ADDR);
  Wire.write((uint8_t)(adresse >> 8));
  Wire.write((uint8_t)(adresse & 0xFF));
  uint8_t err = Wire.endTransmission();
  Wire.requestFrom(EEPROM_I2C_ADDR, (uint8_t)1);
  uint8_t val = Wire.read();

  if (err == 0) {
    DBG("[EEPROM] Lecture addr=0x"); Serial.print(adresse, HEX);
    DBG(" val=0x"); Serial.println(val, HEX);
  } else {
    DBGVAL("[EEPROM] Erreur lecture (I2C err=", err); DBGLN(")");
  }
  return val;
}

// =============================================================================
// FONCTIONS UTILITAIRES — LEDs
// =============================================================================

void ledsEteindre() {
  digitalWrite(PIN_LED_ROUGE, LOW);
  digitalWrite(PIN_LED_VERTE, LOW);
}

void ledAfficherEtat(uint8_t etat, const char* contexte) {
  ledsEteindre();
  if (etat == ETAT_ROUGE) {
    digitalWrite(PIN_LED_ROUGE, HIGH);
    DBG("[LED] "); DBGSTR(contexte); DBGLN(" -> ROUGE allumee.");
  } else if (etat == ETAT_VERT) {
    digitalWrite(PIN_LED_VERTE, HIGH);
    DBG("[LED] "); DBGSTR(contexte); DBGLN(" -> VERTE allumee.");
  } else {
    DBG("[LED] "); DBGSTR(contexte);
    DBG(" -> etat inconnu (0x"); Serial.print(etat, HEX); DBGLN("), LEDs eteintes.");
  }
}

// =============================================================================
// FONCTIONS UTILITAIRES — RADIO
// =============================================================================

bool recevoirEtat(uint8_t &etatRecu, uint16_t timeoutMs) {
  DBGVAL("[RADIO] Ecoute pendant (ms) : ", timeoutMs);

  uint8_t buf[1];
  uint8_t buflen;
  unsigned long debut = millis();

  while (millis() - debut < timeoutMs) {
    buflen = sizeof(buf);
    if (radio.recv(buf, &buflen) && buflen == 1) {
      DBG("[RADIO] Paquet brut : 0x"); Serial.println(buf[0], HEX);
      if (buf[0] == ETAT_ROUGE || buf[0] == ETAT_VERT) {
        etatRecu = buf[0];
        unsigned long elapsed = millis() - debut;
        DBG("[RADIO] Valide apres "); Serial.print(elapsed); DBGLN(" ms.");
        return true;
      } else {
        DBG("[RADIO] Invalide ignore (0x"); Serial.print(buf[0], HEX); DBGLN(")");
      }
    }
  }

  DBGLN("[RADIO] Timeout — aucun paquet valide.");
  return false;
}

// =============================================================================
// FONCTIONS UTILITAIRES — SOMMEIL
// =============================================================================

void isrReveil() {
  alarmeFired = true;
}

void dormirJusquAlarme() {
  DBGLN("[SLEEP] Mise en veille (SLEEP_MODE_PWR_DOWN)...");
  DBG_FLUSH();

  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
  power_adc_disable();

  attachInterrupt(digitalPinToInterrupt(PIN_ALARM), isrReveil, LOW);
  sleep_cpu();  // ← s'endort ici

  sleep_disable();
  detachInterrupt(digitalPinToInterrupt(PIN_ALARM));
  power_adc_enable();

  DBGLN("[SLEEP] Reveil !");
}

// =============================================================================
// SETUP
// =============================================================================

void setup() {
#if DEBUG
  Serial.begin(SERIAL_BAUD);
  while (!Serial) {}
  DBGLN("=== RECEPTEUR — demarrage ===");
  DBGVAL("[CONFIG] WAKE_INTERVAL_RX    = ", WAKE_INTERVAL_RX);
  DBGVAL("[CONFIG] LISTEN_TIMEOUT_MS   = ", LISTEN_TIMEOUT_MS);
  DBGVAL("[CONFIG] I2C_MAX_RETRIES     = ", I2C_MAX_RETRIES);
  DBGVAL("[CONFIG] I2C_INTER_OP_DELAY  = ", I2C_INTER_OP_DELAY_MS);
#endif

  pinMode(PIN_LED_ROUGE, OUTPUT);
  pinMode(PIN_LED_VERTE, OUTPUT);
  ledsEteindre();

  pinMode(PIN_ALARM, INPUT_PULLUP);

  Wire.begin();
  DBGLN("[INIT] Bus I2C initialise.");

  if (radio.init()) {
    DBGLN("[INIT] RadioHead RH_ASK OK.");
  } else {
    DBGLN("[INIT] ERREUR init RadioHead !");
  }

  DBGVAL("[INIT] Premiere alarme dans ", WAKE_INTERVAL_RX);
  DBGLN("[INIT]   secondes.");
  ds3231ProgrammerAlarme1(WAKE_INTERVAL_RX);

  DBGLN("[INIT] Setup termine.");
}

// =============================================================================
// LOOP — déroulé principal
// =============================================================================

void loop() {
  // 1. Sommeil jusqu'à l'alarme DS3231
  alarmeFired = false;
  ledsEteindre();
  dormirJusquAlarme();

  cycleCount++;
  DBG("--- Cycle recepteur #"); Serial.print(cycleCount); DBGLN(" ---");

  // 2. Afficher l'ancien état connu pendant l'attente radio
  DBGLN("[LOOP] Lecture ancien etat EEPROM...");
  uint8_t etatConnu = eepromLire(EEPROM_STATE_ADDR);
  ledAfficherEtat(etatConnu, "Ancien etat");
  delay(LED_ANCIEN_DURATION);

  // 3. Écouter la radio
  uint8_t etatRecu = 0;
  bool receptionOk = recevoirEtat(etatRecu, LISTEN_TIMEOUT_MS);

  if (receptionOk) {
    // 4. Afficher et sauvegarder le nouvel état
    ledAfficherEtat(etatRecu, "Nouvel etat");
    DBGLN("[LOOP] Sauvegarde EEPROM...");
    eepromEcrire(EEPROM_STATE_ADDR, etatRecu);
  } else {
    DBGLN("[LOOP] Pas de reception — etat conserve.");
    ledAfficherEtat(etatConnu, "Etat conserve");
  }

  // 5. Programmer l'alarme suivante (efface A1F en premier, acknowledge sur chaque registre)
  DBGVAL("[LOOP] Prochaine alarme dans ", WAKE_INTERVAL_RX);
  DBGLN("[LOOP]   secondes.");
  ds3231ProgrammerAlarme1(WAKE_INTERVAL_RX);

  // 6. Maintenir la LED visible avant le sleep
  delay(LED_NOUVEL_DURATION);

  DBGLN("[LOOP] Retour en sommeil.");
}

Comments

No comments yet. Why don’t you start the discussion?

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *