TP 9 — L'horloge temps réel =========================== Objectifs --------- - utiliser l'**horloge temps réel** (RTC) du microcontrôleur : régler et lire la date et l'heure ; - comprendre le **domaine de sauvegarde**, qui garde l'heure quand le reste du microcontrôleur redémarre ou s'éteint ; - éviter de remettre l'heure à zéro à chaque démarrage, grâce à un **registre de sauvegarde**. Durée indicative : une séance de 3 h. Prérequis : :doc:`tp05_uart`, pour afficher l'heure au terminal. Ce qu'il faut savoir -------------------- La RTC et son quartz ~~~~~~~~~~~~~~~~~~~~ La RTC est un compteur de secondes, de minutes, d'heures et de jours, cadencé par un quartz dédié de **32,768 kHz** (l'oscillateur **LSE**). 32 768 = 2\ :sup:`15` : deux diviseurs (127 + 1 puis 255 + 1) en tirent exactement **un battement par seconde**. La RTC appartient au **domaine de sauvegarde** (*backup domain*) : une petite partie du microcontrôleur qui reste alimentée quand le reste redémarre. Si la carte est entièrement coupée, une **pile bouton** branchée sur la broche VBAT prend le relais et la RTC continue de compter. .. tikz:: La RTC : le quartz de 32 768 Hz, divisé jusqu'à 1 Hz, dans le domaine de sauvegarde. \tikzset{b/.style={draw, rounded corners, fill=blue!6, align=center, font=\small, minimum height=11mm, inner sep=4pt}, ar/.style={-{Stealth}, thick}, lb/.style={font=\scriptsize, align=center}} \node[b, fill=orange!12] (q) at (0,0) {quartz LSE\\[-2pt]\scriptsize 32 768 Hz}; \node[b] (a) at (3.6,0) {diviseur\\asynchrone\\[-2pt]\scriptsize ÷ (127 + 1)}; \node[b] (s) at (7.2,0) {diviseur\\synchrone\\[-2pt]\scriptsize ÷ (255 + 1)}; \node[b, fill=green!8] (c) at (10.8,0) {calendrier\\[-2pt]\scriptsize h, min, s, date}; \draw[ar] (q) -- (a); \draw[ar] (a) -- node[above, lb]{256 Hz} (s); \draw[ar] (s) -- node[above, lb]{1 Hz} (c); \node[b, fill=yellow!20, minimum width=26mm] (k) at (10.8,-1.8) {registres BKP0…\\[-2pt]\scriptsize gardés au reset}; \begin{scope}[on background layer] \node[draw, dashed, rounded corners, fill=black!3, fit=(q)(c)(k), inner sep=6mm, label={[font=\scriptsize]below:domaine de sauvegarde : alimenté par V\textsubscript{DD}, ou par la pile sur VBAT}] {}; \end{scope} Ce domaine contient aussi des **registres de sauvegarde** (BKP0, BKP1…), qui gardent leur valeur à travers un redémarrage : on peut y laisser une « signature » pour savoir, au démarrage, si l'heure est déjà réglée. .. warning:: **N'activez jamais la surveillance du quartz LSE** (*LSE Clock Security System*, case **LSE CSS** dans CubeMX). Sur ce microcontrôleur, un défaut connu, décrit dans la note d'errata de ST, la fait déclarer le quartz en panne lors d'une coupure d'alimentation, alors qu'il fonctionne : le domaine de sauvegarde est alors remis à zéro, et l'heure est perdue. C'est ce qui faisait perdre l'heure aux cartes RatBot à chaque coupure. Étape 1 — Configurer la RTC --------------------------- #. Créez un projet ``tp09_rtc`` comme au TP 5, avec l'**UART5** en asynchrone à 115 200 bit/s et ``printf`` redirigé. #. Dans **System Core → RCC**, réglez **Low Speed Clock (LSE)** sur **Crystal/Ceramic Resonator**. Ne touchez pas à la HSI : le reste du programme garde son horloge. #. Dans **Timers → RTC**, cochez **Activate Clock Source** seulement. Ne cochez **pas** **Activate Calendar** : CubeMX écrirait alors dans ``MX_RTC_Init()`` un réglage de l'heure, exécuté à chaque démarrage, qui effacerait l'heure conservée (voir l'étape 3). Le calendrier de la RTC fonctionne sans cette case : c'est votre programme qui réglera l'heure. #. Dans **Clock Configuration**, réglez **RTC Clock Mux** sur **LSE**. #. Vérifiez dans **Parameter Settings** de la RTC : **Asynchronous Predivider** 127, **Synchronous Predivider** 255. #. Générez le code. .. Capture d'écran à insérer : CubeMX, RTC : Activate Clock Source cochée, Activate Calendar décochée. Déposez _static/captures/sans_os/tp09-rtc.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/sans_os/tp09-rtc.png .. :align: center .. :width: 90% .. Capture d'écran à insérer : CubeMX, RCC : Low Speed Clock (LSE) sur Crystal/Ceramic Resonator. Déposez _static/captures/sans_os/tp09-rcc-lse.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/sans_os/tp09-rcc-lse.png .. :align: center .. :width: 90% .. admonition:: Question 1. Vérifiez que 32 768 / (127 + 1) / (255 + 1) donne bien 1 Hz. .. dropdown:: Réponse à la question 1 :icon: light-bulb :color: info **1. Les deux diviseurs.** .. code-block:: text 32 768 Hz / (127 + 1) = 256 Hz (diviseur asynchrone) 256 Hz / (255 + 1) = 1 Hz (diviseur synchrone) Pourquoi deux étages, et pourquoi ce partage ? Le premier diviseur, **asynchrone**, est le plus gros consommateur : il tourne à la fréquence du quartz. Le placer en premier, avec la plus grande division possible, réduit la fréquence de tout le reste, et donc la consommation de la RTC sur pile. Le second, **synchrone**, compte les **fractions de seconde** : son compteur (registre ``SSR``) donne l'heure au 1/256 de seconde près, utile pour horodater finement. Le choix de 32 768 Hz n'est pas un hasard : c'est 2\ :sup:`15`, divisible par deux jusqu'à 1 Hz exactement ; et un quartz de cette fréquence est petit, bon marché et consomme très peu. Toutes les montres à quartz l'utilisent. Étape 2 — Régler et lire l'heure -------------------------------- #. Dans ``USER CODE BEGIN 2``, réglez une date et une heure avec ``HAL_RTC_SetTime`` et ``HAL_RTC_SetDate``, format binaire (``RTC_FORMAT_BIN``). #. Dans la boucle, une fois par seconde, lisez-les et affichez-les au terminal sous la forme ``2026-10-28 14:05:09``. .. Capture d'écran à insérer : le terminal qui affiche la date et l'heure chaque seconde. Déposez _static/captures/sans_os/tp09-terminal-heure.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/sans_os/tp09-terminal-heure.png .. :align: center .. :width: 90% .. important:: Appelez toujours ``HAL_RTC_GetDate`` **juste après** ``HAL_RTC_GetTime``, même si la date ne vous intéresse pas. La RTC fige ses registres de date à la lecture de l'heure, et ne les libère qu'à la lecture de la date : sans cet appel, l'heure lue ne change plus. .. dropdown:: Solution de l'étape 2 :icon: code :color: secondary .. code-block:: c static void rtc_set(uint16_t year, uint8_t mon, uint8_t day, uint8_t h, uint8_t m, uint8_t s) { RTC_TimeTypeDef t = {0}; RTC_DateTypeDef d = {0}; t.Hours = h; t.Minutes = m; t.Seconds = s; t.DayLightSaving = RTC_DAYLIGHTSAVING_NONE; t.StoreOperation = RTC_STOREOPERATION_RESET; d.Year = (uint8_t)(year - 2000u); /* la RTC compte de 00 à 99 */ d.Month = mon; d.Date = day; d.WeekDay = RTC_WEEKDAY_MONDAY; /* non utilisé ici */ HAL_RTC_SetTime(&hrtc, &t, RTC_FORMAT_BIN); HAL_RTC_SetDate(&hrtc, &d, RTC_FORMAT_BIN); } static void rtc_print(void) { RTC_TimeTypeDef t; RTC_DateTypeDef d; HAL_RTC_GetTime(&hrtc, &t, RTC_FORMAT_BIN); HAL_RTC_GetDate(&hrtc, &d, RTC_FORMAT_BIN); /* obligatoire, juste après */ printf("%04u-%02u-%02u %02u:%02u:%02u\r\n", 2000u + d.Year, d.Month, d.Date, t.Hours, t.Minutes, t.Seconds); } ``rtc_set(2026u, 10u, 28u, 14u, 0u, 0u);`` dans ``USER CODE BEGIN 2``, et ``rtc_print(); HAL_Delay(1000);`` dans la boucle. **RTC_FORMAT_BIN.** La RTC range l'heure en **BCD** (*binary-coded decimal*) : chaque chiffre décimal sur 4 bits, 14 h s'écrit 0x14. C'est commode pour un afficheur, moins pour un calcul. Avec ``RTC_FORMAT_BIN``, la HAL convertit dans les deux sens, et on manipule des nombres ordinaires. **Pourquoi GetDate juste après GetTime.** Lire l'heure et la date en deux fois pose un problème à minuit : on pourrait lire 23:59:59, puis une date qui vient de passer au lendemain. Pour l'éviter, la RTC **fige** ses registres d'heure et de date (registres « ombres ») à la lecture de l'heure, et ne les libère qu'à la lecture de la date. Les deux valeurs sont alors cohérentes. Sans la lecture de la date, les registres restent figés : l'heure ne change plus. **%02u** affiche le nombre sur deux chiffres, complété par un zéro : ``14:05:09``, et non ``14:5:9``. Étape 3 — Ne pas perdre l'heure ------------------------------- Appuyez sur le bouton de reset, ou relancez le programme : l'heure repart de la valeur réglée à l'étape 2. Elle n'a pourtant pas été perdue : c'est le programme qui la réécrase à chaque démarrage. #. Au démarrage, lisez le registre de sauvegarde **BKP0** : .. code-block:: c uint32_t sig = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0); #. Ne réglez l'heure **que si** ``sig`` ne vaut pas une signature de votre choix, par exemple ``0x52544331``. Après le réglage, écrivez cette signature avec ``HAL_RTCEx_BKUPWrite``. #. Vérifiez qu'après un reset, l'heure continue de compter là où elle en était. .. note:: Si vous avez coché **Activate Calendar** à l'étape 1, ``MX_RTC_Init()`` règle l'heure lui-même à chaque démarrage, avant votre code. CubeMX prévoit pour ce cas un bloc ``USER CODE BEGIN Check_RTC_BKUP`` dans ``MX_RTC_Init()`` : y tester la signature et sortir de la fonction (``return;``) si elle est présente évite le réglage. .. admonition:: Questions 2. Pourquoi l'heure survit-elle au reset, mais pas forcément à une coupure de l'alimentation ? Que faut-il sur la carte pour qu'elle y survive ? 3. Pourquoi tester une signature plutôt que simplement « l'heure est-elle à zéro ? » ? .. dropdown:: Solution de l'étape 3 :icon: code :color: secondary .. code-block:: c #define RTC_SIGNATURE 0x52544331u /* « RTC1 » en ASCII */ static void rtc_init_once(void) { if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) != RTC_SIGNATURE) { rtc_set(2026u, 10u, 28u, 14u, 0u, 0u); HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, RTC_SIGNATURE); printf("heure reglee (premier demarrage)\r\n"); } else { printf("heure conservee\r\n"); } } appelée dans ``USER CODE BEGIN 2`` à la place de ``rtc_set``. **L'ordre : régler, puis signer.** Si la carte est coupée entre les deux, la signature manque, et l'heure sera simplement réglée de nouveau au démarrage suivant. Dans l'ordre inverse, une coupure au mauvais moment laisserait une signature sur une heure jamais réglée. **0x52544331** : les octets 0x52, 0x54, 0x43, 0x31 sont les codes ASCII de « R », « T », « C », « 1 ». N'importe quelle valeur peu probable convient ; une valeur lisible se reconnaît dans le débogueur, et le « 1 » permet de changer de signature si l'on change un jour le format des registres. .. dropdown:: Réponses aux questions 2 et 3 :icon: light-bulb :color: info **2. Reset ou coupure.** Le microcontrôleur a plusieurs **domaines d'alimentation**. Le domaine de sauvegarde (la RTC, le LSE et ses registres BKP) est alimenté par V\ :sub:`DD` quand elle est présente, et **n'est pas touché par un reset** : le bouton reset ou un redémarrage logiciel ne remettent à zéro que le cœur et les périphériques. La RTC continue de compter pendant le reset. Une **coupure** de V\ :sub:`DD`, en revanche, éteint aussi ce domaine… sauf si la broche **VBAT** est alimentée : un commutateur interne bascule alors le domaine de sauvegarde sur VBAT. Il faut donc une **pile bouton** (CR2032, par exemple) ou un **supercondensateur** sur VBAT. La RTC consomme environ un microampère : une pile bouton tient des années. **3. Une signature plutôt que zéro.** Après une perte d'alimentation, le contenu des registres de la RTC n'est pas garanti « à zéro » ; et surtout, **minuit est une heure valide** : une RTC qui affiche 00:00:00 peut très bien être réglée. Une valeur de 32 bits choisie au hasard a une chance sur quatre milliards de se trouver là par accident : sa présence prouve que **notre programme** a réglé l'heure. Étape 4 — Régler l'heure depuis le terminal ------------------------------------------- Ajoutez une commande au terminal (TP 5) : taper ``T`` suivi de ``2026-10-28 14:05:00`` et Entrée règle la date et l'heure. .. dropdown:: Solution de l'étape 4 :icon: code :color: secondary On accumule les caractères reçus dans une **ligne**, jusqu'à la touche Entrée, puis on l'analyse avec ``sscanf`` (il faut ``#include ``) : .. code-block:: c static char line[40]; static uint32_t len = 0u; uint8_t c; if (HAL_UART_Receive(&huart5, &c, 1u, 10u) == HAL_OK) { HAL_UART_Transmit(&huart5, &c, 1u, 10u); /* écho */ if ((c == '\r') || (c == '\n')) /* fin de ligne */ { unsigned y, mo, d, h, mi, s; line[len] = '\0'; if ((sscanf(line, "T %u-%u-%u %u:%u:%u", &y, &mo, &d, &h, &mi, &s) == 6) && (y >= 2000u) && (y <= 2099u) && (mo >= 1u) && (mo <= 12u) && (d >= 1u) && (d <= 31u) && (h < 24u) && (mi < 60u) && (s < 60u)) { rtc_set((uint16_t)y, (uint8_t)mo, (uint8_t)d, (uint8_t)h, (uint8_t)mi, (uint8_t)s); HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, RTC_SIGNATURE); printf("\r\nheure reglee\r\n"); } else if (len > 0u) { printf("\r\nformat : T AAAA-MM-JJ HH:MM:SS\r\n"); } len = 0u; } else if (len < sizeof(line) - 1u) { line[len++] = (char)c; /* garde la place du '\0' */ } } Et l'affichage de l'heure passe sur un rythme non bloquant (``HAL_GetTick``, TP 1), pour que la boucle reste à l'écoute du terminal. **sscanf** analyse le texte selon un modèle, et renvoie le **nombre de champs reconnus** : 6 si tout est là. Tester ce nombre est indispensable : un texte mal formé laisserait des variables non initialisées. **Vérifier les plages.** ``sscanf`` accepte « T 2026-13-45 99:99:99 ». Écrire de telles valeurs dans la RTC donnerait une heure incohérente : toute donnée venue de l'extérieur se **valide** avant usage. (Un contrôle complet vérifierait aussi le nombre de jours du mois, le 30 février…) **La taille de la ligne est bornée** : un texte trop long est tronqué au lieu d'écrire au-delà du tableau. C'est l'erreur de **débordement de tampon**, à l'origine de nombreuses failles de sécurité. **En réglant l'heure, on écrit aussi la signature** : sans elle, sur une carte qui n'avait jamais été réglée, le démarrage suivant remplacerait l'heure saisie au terminal par l'heure fixée dans le programme. .. dropdown:: Tout le code de la correction :icon: download :color: secondary :download:`main_user_code.c <../../corrections/sans_os/tp09/main_user_code.c>` — compilé sans avertissement contre la HAL du STM32H7. Ce n'est **pas** un ``main.c`` complet : ce sont les extraits à placer dans les blocs ``USER CODE`` du ``main.c`` généré par CubeMX, chacun sous le commentaire qui indique son bloc. La boucle ``while (1)`` contient une variante par étape, encadrée par ``#if 0`` / ``#if 1`` : n'en activer **qu'une à la fois**. La configuration CubeMX attendue est rappelée en tête du fichier, et les réponses aux questions à la fin. Exercices supplémentaires ------------------------- Ces exercices n'ont pas de correction. - **Alarme** : la RTC peut déclencher une interruption à une heure donnée (*Alarm A*). Faites sonner le buzzer actif (:doc:`tp03_son`) à une heure réglée au terminal. - **Mesure de dérive** : réglez l'heure sur celle du PC, laissez tourner plusieurs heures **sans brancher le débogueur**, puis comparez. Calculez la dérive en ppm (millionièmes). Pourquoi ne faut-il pas lire l'heure au débogueur pendant la mesure ? - **Horodatage** : préfixez chaque message envoyé au terminal par l'heure. - **Horloge sur l'écran** : affichez l'heure en grands chiffres sur l'écran OLED (:doc:`tp08_spi_ecran`). Après le TP ----------- Pour rendre la carte au robot, voir :ref:`etat-ratbot`.