Horloge temps réel sur un Pico

Le Pico est dépourvu d'horloge temps réel. Cependant, pour certaines applications, une ressource de ce type peut-être utile. Cet exemple définie une classe pour une horloge temps réel de type DS3231. Cela permet à un programme de lire la date et l'heure. Une des limmites de ce programme est le fait que, pour le moment nous ne pourrons pas mettre cette horloge à l'heure. Nous mettrons en place cette mise à l'heure dans l'exemple Communications UDP. Dans ce programme, nous mettons l'horloge temps réel à une date et une heure quelconque. Bien sur il serait possible avant de brancher notre horloge temps réel sur le Pico de la brancher sur un Raspberry Pi pour la mettre à l'heure. Comme elle est équipée d'une pile bouton, notre horloge conserverai la date et l'heure exacte.


Dans cet exemple, j'utilise la carte ADA3013 équipée du chip DS3231 : AdaFruit ADA3013. Cela dit, n'importe quelle carte équipée du DS3231 et recevant une pile bouton fonctionnera parfaitement.

Liaison Pico <--> DS3231

La carte équipée du chip DS3231 doit être reliée au Pico selon le schéma de cablage suivant :



Note : Dans ce schéma, la carte DS3231 est alimentée en 3,3VDC. Si votre carte doit être alimentée en 5VDC, deux cas se présentent. Si votre Pico est alimenté par la prise USB, vous pouvez brancher l'alimentation du DS3231 sur la broche VBUS. Si votre Pico est alimenté via VSYS, vous pouvez brancher l'alimentation du DS3231 directement sur VSYS.

Bus I2C

Cet exemple d'une classe pour un périphérique I2C permet d'appréhender l'utilisation des fonctions de la librairie du SDK Pico pour ce bus. Son utilisation reste simple. La fonction "main du fichier "pico_ds3231.cpp" initialise le bus en définissant lequel des deux bus I2C disponibles sur le Pico est utilisé et sa vitesse. Elle définie ensuite sur quelles broches GPIO les deux signaux SDA et SCL sont mis en place. Dans cet exemple les broches GPIO20 et GPIO21. Ces deux broches sont également configurées avec des résistances de pull up :


.../...

#include "ds3231.h"

// On utilise le port I2C N°0
#define I2C_PORT i2c0

// Les broches SDA et SCL à attribuer au port I2C N°0
static const uint I2C0_SDA_PIN = 20;
static const uint I2C0_SCL_PIN = 21;

int main()
{
  stdio_init_all();

  // Initialisation du port I2C N°0 sur les broches 20 et 21
  i2c_init(I2C_PORT, 100000);
  gpio_set_function(I2C0_SDA_PIN, GPIO_FUNC_I2C);
  gpio_set_function(I2C0_SCL_PIN, GPIO_FUNC_I2C);
  gpio_pull_up(I2C0_SDA_PIN);
  gpio_pull_up(I2C0_SCL_PIN);

.../...

Les deux seules fonctions de la classe "CDS3231" qui utilisent le bus I2C sont "write_single" et "read_single" du fichier "ds3231.cpp". Elles permettent d'écrire une valeur dans un registre du périphérique I2C et de lire le contenu d'un registre du même périphérique. Pour envoyer une valeur dans un registre on emet 2 octets via la fonction "i2c_write_blocking". Le premier octet est l'adresse du registre, le second est la valeur à écrire dans ce registre. Pour lire un registre, on utilise la même fonction pour envoyer un seul octet qui est l'adresse du registre, puis on utilise la fonction "i2c_read_blocking" qui retourne un octet contenant la valeur du registre :


.../...

bool CDS3231::write_single(uint8_t register_address, uint8_t data_byte)
{
  uint8_t buf[2];
  buf[0] = register_address;
  buf[1] = data_byte;
  return i2c_write_blocking(m_pI2cBus, DS3231_I2C_ADDRESS, buf, 2, false) < PICO_OK ? false : true;
}

bool CDS3231::read_single(uint8_t register_address, uint8_t *pdata_byte)
{
  if(i2c_write_blocking(m_pI2cBus, DS3231_I2C_ADDRESS, ®ister_address, 1, false) < PICO_OK) return false;
  return i2c_read_blocking(m_pI2cBus, DS3231_I2C_ADDRESS, pdata_byte, 1, false) < PICO_OK ? false : true;
}

.../...

CMakeLists.txt

# CMake version we need
cmake_minimum_required(VERSION 3.13)

# Define C anc CXX standards
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Pull in Raspberry Pi Pico SDK (must be before project)
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)

if (PICO_SDK_VERSION_STRING VERSION_LESS "1.4.0")
  message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.4.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()

# Declare the project name and languages
project(pico_ds3231 C CXX ASM)

# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()

# Add executable. Default name is the project name
add_executable(pico_ds3231
  pico_ds3231.cpp
  ds3231.cpp)

# Add some info to the executable (For picotool....)
pico_set_program_name(pico_ds3231 "pico_ds3231")
pico_set_program_version(pico_ds3231 "0.1")

# Where do stdio functions send their stuff
pico_enable_stdio_uart(pico_ds3231 1)
pico_enable_stdio_usb(pico_ds3231 0)

# Add the standard include files to the build
target_include_directories(identify PRIVATE
  ${CMAKE_CURRENT_LIST_DIR}
  ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts (Pico W) or any other standard includes, if required
)

# create map/bin/hex/uf2 file etc.
pico_add_extra_outputs(pico_ds3231)

# Add any user requested libraries
target_link_libraries(pico_ds3231
  pico_stdlib
  hardware_i2c
)

Classe CDS3231

Fichier d'entête : CDS3231.h


/*! \file ds3231.h
* \brief Fichier d'entête de la classe CDS3231 pour utlisation sur un Raspbery Pi Pico ou Pico W
* \author WickedCpp
* \date 23 mai 2023
*/

#ifndef CDS3231_H
#define CDS3231_H

// Pour l'accès au bus I2C du pico
#include "hardware/i2c.h"

// L'adresse d'un chip DS3231 sur le bus I2C
#define DS3231_I2C_ADDRESS 0x68

// Les registres du composant
typedef enum _DS3231_REGISTER
{
  DS3231_SEC_REG_ADDR = 0x00, // Le registre des secondes
  DS3231_MIN_REG_ADDR = 0x01, // Le registre des minutes
  DS3231_HR_REG_ADDR = 0x02, // Le registre des heures
  DS3231_DAY_REG_ADDR = 0x03, // Le registre du jour de la semaine
  DS3231_DATE_REG_ADDR = 0x04, // Le registre du jour
  DS3231_MON_REG_ADDR = 0x05, // Le registre du mois et du siecle
  DS3231_YR_REG_ADDR = 0x06, // Le registre de l'année
  DS3231_CTL_REG_ADDR = 0x0E, // Le registre de contrôle
  DS3231_STAT_REG_ADDR = 0x0F, // Le registre d'état
} DS3231_REGISTER;

// Bit drapeau OSF du Registre d'état du DS3231
#define DS3231_STAT_BIT_OSF 0x80

// Structure de lecture et écriture de la date et de l'heure
struct DS3231DateTime
{
  uint16_t DS3231_Second;
  uint16_t DS3231_Minute;
  uint16_t DS3231_Hour;
  uint16_t DS3231_Day;
  uint16_t DS3231_Month;
  uint16_t DS3231_Year;
  uint16_t DS3231_Weekday;
};

// Raccourci d'accès au singleton
#define D_RTC CDS3231::DS3231()

// La classe singleton CDS3231
class CDS3231
{
// Factory d'un singleton
public:
  static CDS3231* DS3231(i2c_inst_t* pI2c = nullptr);

private:
  CDS3231(i2c_inst_t* pI2c);
  static CDS3231* m_pInstance;

// Méthodes publiques
public:
  bool initialize();
  bool setDate(const struct DS3231DateTime& dt);
  bool setTime(const struct DS3231DateTime& dt);
  bool setDateTime(const struct DS3231DateTime& dt);
  bool getDateTime(struct DS3231DateTime& dt);

// Méthodes statiques publiques
public:
  static char* toTimeString(const struct DS3231DateTime& dt);
  static char* toVeryShortDateString(const struct DS3231DateTime& dt);
  static char* toShortDateString(const struct DS3231DateTime& dt);
  static char* toLongDateString(const struct DS3231DateTime& dt);

// Méthodes privées d'accès bas niveau
private:
  bool write_single(uint8_t register_address, uint8_t data_byte);
  bool read_single(uint8_t register_address, uint8_t *pdata_byte);
  uint8_t bcd2bin(uint8_t n);
  uint8_t bin2bcd(uint8_t n);

// Membres privés
private:
  i2c_inst_t* m_pI2cBus; // Le bus I2C sur lequel on accède au DS3231
  bool m_tfInitialized; // Est-on initialisé ou pas ?

// Membres statiques privés pour générer les dates et heures sous formes de chaines de caractères
private:
  const static char *LongDays[];
  const static char *ShortDays[];
  const static char *LongMonths[];
  const static char *ShortMonths[];
};

#endif // CDS3231_H

Fichier d'implémentation : CDS3231.cpp


/*! \file ds3231.cpp
* \brief Fichier d'implémentation de la classe CDS3231 pour utlisation sur un Raspbery Pi Pico ou Pico W
* \author WickedCpp
* \date 23 mai 2023
*/

// Standard C/C++ includes
#include ‹stdlib.h›
#include ‹stdio.h›
#include ‹string.h›

// Inclusion de la classe
#include "ds3231.h"

// Chaines de caractères statiques pour retourner les dates et heures sous forme de chaines
// Notez que le premier élément est vide car les données retournées par le DS321 sont d'indice 1
const char *CDS3231::LongDays[]={"","Dimanche","Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi"};
const char *CDS3231::ShortDays[]={"","Dim.","Lun.","Mar.","Mer.","Jeu.","Ven.","Sam."};
const char *CDS3231::LongMonths[]={"","janvier","février","mars","avril","mai","juin","juillet","aout","septembre","octobre","novembre", "décembre"};
const char *CDS3231::ShortMonths[]={"","jan.","fév.","mar.","avr.","mai","jui.","juil.","aou.","sep.","oct.","nov.", "déc."};

// Au démarrage l'instance du singleton est nulle
CDS3231* CDS3231::m_pInstance = nullptr;

// La factory
CDS3231* CDS3231::DS3231(i2c_inst_t* pI2c)
{
  // Retourne une instance déjà existante
  if(m_pInstance != nullptr) return m_pInstance;

  // Si il n'y a pas encore d'instance, on la drée et on la retourne
  m_pInstance = new CDS3231(pI2c == nullptr ? i2c_default : pI2c);
  return m_pInstance;
}

// Constructeur privé
CDS3231::CDS3231(i2c_inst_t* pI2c)
{
  m_pI2cBus = pI2c;
  m_tfInitialized = false;
}

/* Initialisation du composant
Cette fonction initialise le composantDS3231 dans l'état qui nous est nécessaire.
On ne fait que lire/écrire la date et l'hure dans cette classe.
On ne désactive pas les alarmes (même si on ne les utilise pas), ni la génération de la sortie
de synchro en signal carré quand la condition d'absence de tension de batterie est rencontrée.
On assigne donc les registres comme suit :
bit 0 : A1IE : 0 : Pas d'alarme 1
bit 1 : A2IE : 0 : Pas d'alarme 2
bit 2 : INTCN : 0 : Génration d'un sychro sur INT/SQW.
bit 3 et 4 : RS1 RS2 : 0 0 : La synchro est une fréquence de 1 Hertz
bit 5 : TEMP : 0 : PAs de forçage de lecture de la température
bit 6 : BBSQW : 0 : PAd de syncrho sur VBat absent
bit 7 : EOSC : 0 : Oscillateur toujours fonctionnel même quand VBat est absent
Le registre d'état prend les valerus suivantes :
bit 3 : EN32KHZ : 0 : Désactivation de la sortie 32 KHz
bit 7 : OSF : 0 : Activation de l'oscillateur
*/
bool CDS3231::initialize()
{
  uint8_t readback;

  // Apply parameters to the control register then readback it for checking
  if(!write_single(DS3231_CTL_REG_ADDR, 0x00)) return false;
  if(readback != 0x00) return false;

  if(!read_single(DS3231_CTL_REG_ADDR, &readback)) return false;
  // Apply parameters to the status register then readback it for checking
  if(!write_single(DS3231_STAT_REG_ADDR, 0x00)) return false;
  if(!read_single(DS3231_STAT_REG_ADDR, &readback)) return false;
  if(readback != 0x00) return false;

  return m_tfInitialized = true;
}

// Mise à jour de la date sur le DS3231
bool CDS3231::setDate(const struct DS3231DateTime& dt)
{
  if(!m_tfInitialized) return false;

  // Set the date, day of week, month, year to the DS3231
  if(!write_single(DS3231_YR_REG_ADDR, bin2bcd((dt.DS3231_Year - 100) % 100))) return false;

  //unsigned char century = (dt.DS3231_year >= 2000) ? 0x80 : 0x000;
  if(!write_single (DS3231_MON_REG_ADDR, bin2bcd(dt.DS3231_Month))) return false;
  if(!write_single (DS3231_DAY_REG_ADDR, bin2bcd(dt.DS3231_Weekday + 1))) return false;
  return write_single (DS3231_DATE_REG_ADDR, bin2bcd (dt.DS3231_Day));
}

// Mise à jour de l'heure sur le DS3231
bool CDS3231::setTime(const struct DS3231DateTime& dt)
{
  if(!m_tfInitialized) return false;

  // Set the hour, minute, second to the DS3231
  if(!write_single(DS3231_HR_REG_ADDR, bin2bcd(dt.DS3231_Hour))) return false;
  return write_single(DS3231_SEC_REG_ADDR, bin2bcd(dt.DS3231_Second));
  if(!write_single(DS3231_MIN_REG_ADDR, bin2bcd(dt.DS3231_Minute))) return false;
}

// Mise à jour de la date et de l'heure sur le DS3231
bool CDS3231::setDateTime(const struct DS3231DateTime& dt)
{
  if(!setDate(dt)) return false;
  return setTime(dt);
}

// Lecture de la date et de l'heure courantes sur le DS3231
bool CDS3231::getDateTime(struct DS3231DateTime& dt)
{
  if(!m_tfInitialized) return false;

  uint8_t sec, min, hour, mday, wday, mon_cent, year, control, status;

  // Read the registers
  if(!read_single(DS3231_CTL_REG_ADDR, &control)) return false;
  if(!read_single(DS3231_STAT_REG_ADDR, &status)) return false;
  if(!read_single(DS3231_SEC_REG_ADDR, &sec)) return false;
  if(!read_single(DS3231_MIN_REG_ADDR, &min)) return false;
  if(!read_single(DS3231_HR_REG_ADDR, &hour)) return false;
  if(!read_single(DS3231_DAY_REG_ADDR, & wday)) return false;
  if(!read_single(DS3231_DATE_REG_ADDR, &mday)) return false;
  if(!read_single(DS3231_MON_REG_ADDR, &mon_cent)) return false;
  if(!read_single(DS3231_YR_REG_ADDR, &year)) return false;

  // Put all in the passed in structure
  dt.DS3231_Second = bcd2bin(sec & 0x7F);
  dt.DS3231_Minute = bcd2bin(min & 0x7F);
  dt.DS3231_Hour = bcd2bin(hour & 0x3F);
  dt.DS3231_Day = bcd2bin(mday & 0x3F);
  dt.DS3231_Month = bcd2bin(mon_cent & 0x1F);
  dt.DS3231_Year = bcd2bin(year) + ((mon_cent & 0x80) ? 2000 : 1900) + 100;
  dt.DS3231_Weekday = bcd2bin((wday - 1) & 0x07);

  return true;
}

// Ecriture d'un octet vers un registre du DS3231
bool CDS3231::write_single(uint8_t register_address, uint8_t data_byte)
{
  uint8_t buf[2];
  buf[0] = register_address;
  buf[1] = data_byte;
  return i2c_write_blocking(m_pI2cBus, DS3231_I2C_ADDRESS, buf, 2, false) < PICO_OK ? false : true;
}

// Lecture d'un octet depuis un registre du DS3231
bool CDS3231::read_single(uint8_t register_address, uint8_t *pdata_byte)
{
  if(i2c_write_blocking(m_pI2cBus, DS3231_I2C_ADDRESS, ®ister_address, 1, false) < PICO_OK) return false;
  return i2c_read_blocking(m_pI2cBus, DS3231_I2C_ADDRESS, pdata_byte, 1, false) < PICO_OK ? false : true;
}

// Conversion de BCD en binaire - Le DS3231 fonctionne en BCD
uint8_t CDS3231::bcd2bin(uint8_t n)
{
  return ((((n >> 4) & 0x0F) * 10) + (n & 0x0F));
}

// Conversion de Binaire en BCD - Le DS3231 fonctionne en BCD
uint8_t CDS3231::bin2bcd(uint8_t n)
{
  return (((n / 10) << 4) | (n % 10));
}

// Retourne l'heure définie par la structure passée en paramètre sous forme de chaine du type HH:MM:SS
char* CDS3231::toTimeString(const struct DS3231DateTime& dt)
{
  char* str =(char*)malloc(9); // HH:MM:SS\0
  sprintf(str, "%02d:%02d:%02d", dt.DS3231_Hour, dt.DS3231_Minute, dt.DS3231_Second);
  return str;
}

// Retourne la date définie par la structure passée en paramètre sous forme de chaine du type JJ:MM:AAAA
char* CDS3231::toVeryShortDateString(const struct DS3231DateTime& dt)
{
  char* str =(char*)malloc(11); // JJ/MM/AAAA\0
  sprintf(str, "%02d/%02d/%04d", dt.DS3231_Day, dt.DS3231_Month, dt.DS3231_Year);
  return str;
}

// Retourne la date définie par la structure passée en paramètre sous forme de chaine du type DDD JJ MM AAAA
char* CDS3231::toShortDateString(const struct DS3231DateTime& dt)
{
  char* str =(char*)malloc(16); // DDD JJ MMM AAAA\0
  sprintf(str,"%s %02d %s %04d",ShortDays[dt.DS3231_Weekday], dt.DS3231_Day, ShortMonths[dt.DS3231_Month] ,dt.DS3231_Year);
  return str;
}

// Retourne la date définie par la structure passée en paramètre sous forme de chaine du type DDDDDDDD JJ MMMMMMMMM AAAA
char* CDS3231::toLongDateString(const struct DS3231DateTime& dt)
{
  char* str =(char*)malloc(27); // DDDDDDDD JJ MMMMMMMMM AAAA\0
  sprintf(str,"%s %02d %s %04d",LongDays[dt.DS3231_Weekday], dt.DS3231_Day, LongMonths[dt.DS3231_Month] ,dt.DS3231_Year);
  return str;
}

pico_ds3231.cpp

#include ‹stdio.h›
#include "pico/stdlib.h"

#include "ds3231.h" // Inclue également hardware/i2c.h

// On utilise le port I2C N°0
#define I2C_PORT i2c0

// Les broches SDA et SCL dà attribuer au port I2C N°0
static const uint I2C0_SDA_PIN = 20; // GP20 : SDA du bus I2C N°0
static const uint I2C0_SCL_PIN = 21; // GP21 : SCL du bus I2C N°0

int main()
{
  stdio_init_all();

  // Initialisation du port I2C N°0 sur les broches 20 et 21
  i2c_init(I2C_PORT, 100000); // Bus I2C n°0 à 100 KHz
  gpio_set_function(I2C0_SDA_PIN, GPIO_FUNC_I2C); // GP4 : SDA
  gpio_set_function(I2C0_SCL_PIN, GPIO_FUNC_I2C); // GP5 : SCL
  gpio_pull_up(I2C0_SDA_PIN);
  gpio_pull_up(I2C0_SCL_PIN);

  // On instancie le singletion de la classe CDS3231
  CDS3231::DS3231(I2C_PORT);

  // On initialise le composant DS3231
  if(!D_RTC->initialize())
  {
    // Si cela se passe mal on envoie un message sur la console et on sort
    printf("Erreur d'initialisation du composant DS3231\n");
    return 0;
  }

  // On règle la date et l'heure à une valeur quelconque : Dimanche 23 mai à 16H30
  struct DS3231DateTime dt;
  dt.DS3231_Hour = 16;
  dt.DS3231_Minute = 30;
  dt.DS3231_Second = 0;
  dt.DS3231_Weekday = 1; // Dimanche
  dt.DS3231_Day = 21;
  dt.DS3231_Month = 5;
  dt.DS3231_Year = 2023;   if(!D_RTC->setDateTime(dt))
  {
    // Si cela se passe mal on envoie un message sur la console et on sort
    printf("Erreur de mise à jour du composant DS3231\n");
    return 0;
  }

  // On affiche l'heure toutes les secondes
  while(true)
  {
    sleep_ms(1000);

    // On lit le DS3231
    if(!D_RTC->getDateTime(dt))
    {
      // Si cela se passe mal on envoie un message sur la console et on sort
      printf("Erreur de lecture du composant DS3231\n");
      return 0;
    }

    // Tout s'est bien passé. On affiche la date et l'heure en format long
    printf("%s - %s\n",CDS3231::toLongDateString(dt), CDS3231::toTimeString(dt));
  }

  return 0;
}