Blue Pill - Compter le temps qui passe

Table des matières

TL;DR

Utiliser le composant SysTick pour faire clignoter une LED.

A suivre

Faire passer le temps

Pour mettre en place un délai, on trouve bien trop souvent ce genre de code:
for (int i = 0; i < delai; i++);
Passée l'envie de sortir le goudron et les plumes (ou le verre pilé, chacun ses goûts) on peut accorder à ce "code" une certaine simplicité. Mais bien que cette boucle ne semble rien faire, il n'en est pas de même pour le CPU: il passe son temps à comparer, incrémenter et sauter à une adresse. Pas l'idéal quand on se soucie de la consommation électrique de la carte. De plus, il est dommage de confier une tâche aussi simple à un composant aussi complexe et puissant qu'un CPU. Pour finir, il est possible que le compilateur supprime purement et simplement ce "code" (si on utilise un niveau maximal d'optimisation).

Délai

Mettre en place un délai, c'est compter le temps qui passe, un peu comme au début d'une partie de cache-cache: on compte à haute voix avant de pouvoir partir à la recherche des autres joueurs. Plus on compte vite, moins ils auront de temps pour trouver une cachette. Une Blue Pill, sans configuration particulière, est cadencée à 8 MHz. Cette fréquence signifie que le CPU exécute 8 000 000 (huit millions) de cycles (ou ticks) par seconde. Un compteur fonctionnant à cette vitesse vaudrait 8 000 000 au bout d'une seconde mais aussi: Il se trouve que le Cortex M3 qui anime la Blue Pill possède un tel compteur: SysTick. Son utilisation est simple: on définit une valeur maximale et le compteur une fois activé va décompter jusqu'à zéro, mettre à jour un bit dans un registre pour signifier qu'il a atteint zéro et recommencer depuis la valeur maximale. Et tout cela indépendament du CPU.

Compter le temps qui passe

Pour compter le temps, il faut savoir à quelle vitesse on compte. La fonction SystemCoreClockUpdate du fichier STM32-base-STM32Cube-master/CMSIS/STM32F1xx/src/system_stm32f1xx.c va justement placer cette vitesse (fréquence) dans la variable globale SystemCoreClock. Tout est en place pour définir des expressions comme:
#define    DELAY_1_MS  (SystemCoreClock / 1000)
#define   DELAY_10_MS  (SystemCoreClock /  100)
#define  DELAY_100_MS  (SystemCoreClock /   10)
La valeur maximale de SysTick est codée sur 24 bits soit 16 777 216 - 1, soit environ 2 secondes à 8 MHz. Il faudra garder à l'esprit cette limitation et ne pas faire:
#define DELAY_1_SEC  SystemCoreClock
quand on commencera à modifier la fréquence du CPU et que SystemCoreClock vaudra 72 000 000.

Faire clignoter la LED

Pour changer l'état de la PIN 13 du GPIO C tous les quarts de seconde (250 ms):
     1	#include "stm32f1xx.h"
     2	
     3	#define DELAY_1_MS  (SystemCoreClock / 1000)
     4	#define DELAY_LED   250    
     5	
     6	int
     7	main (void) {
     8	    SystemCoreClockUpdate(); // Update globale variable SystemCoreClock
     9	
    10	    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // Enable GPIO C 
    11	    GPIOC->CRH   |= GPIO_CRH_MODE13_1;  // GPIO C PIN 13 mode 0b10: Output mode, max speed 2 MHz
    12	
    13	    // https://developer.arm.com/documentation/dui0552/a/cortex-m3-peripherals/system-timer--systick/systick-control-and-status-register
    14	    SysTick->LOAD = (DELAY_LED * DELAY_1_MS) - 1;                         // Countdown from (250 * (SystemCoreClock / 1000)) - 1 to zero (== 1/4 second)
    15	    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk; // Set processor clock as clock source + Enable SysTick
    16	
    17	    while (1) {
    18	        while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0); // 0 reached
    19	        GPIOC->ODR ^= GPIO_ODR_ODR13;                              // GPIO C PIN 13 output toggle state
    20	    }
    21	
    22	    return 0;
    23	}
La définition du composant SysTick et de ses registres se trouvent dans le fichier STM32-base-STM32Cube-master/CMSIS/ARM/inc/core_cm3.h. C'est dans le registre LOAD que l'on place la valeur maximale puis tout se passe dans le registre CTRL: Pour faire clignoter une LED, on configure SysTick puis on attend indéfiniment que le compteur atteigne zéro (ligne 18) et on inverse l'état de la sortie 13 du GPIO C à l'aide du registre Output Data Register (ligne 19) qui représente l'état des sorties des PINs d'un GPIO (page 173 du manuel de référence).

Première(s) fonction(s)

Il y a plusieurs choses à prendre en compte avant d'écrire une fonction: Il parait assez logique de regrouper les fonctionalités d'un composant au sein d'un seul fichier source, ici systick.c:
$ cat systick.c
#include "stm32f1xx.h"

#define SYSTICK_DELAY_1_MS  (SystemCoreClock / 1000)

void
systick_delay_load(uint32_t load) {
    SysTick->LOAD  = load;
    SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;

    while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0);

    SysTick->CTRL = 0;
}

void
systick_delay_ms(uint32_t millis) {
    SysTick->LOAD  = SYSTICK_DELAY_1_MS - 1;
    SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;

    while (millis--) {
        while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0);
    }

    SysTick->CTRL = 0;
}
Parce que l'appel à une de ces fonctions, le passage du paramètre, la configuration de SysTick, la (ou les) boucle(s), la comparaison ... consomment du CPU donc des ticks donc du temps, j'estime que la milli-seconde est l'ordre de grandeur à privilégier pour ce composant: on compte grosso-merdo une milli-seconde et quelques dizaines de micro-secondes. Il est de la responsabilité de l'appelant de vérifier que le paramètre est "valide", une gestion d'erreur rajouterai une complexité inutile au regard de la simplicité du code.

Pourquoi 2 fonctions ? La première, plus "légère", laisse à l'appelant la responsabilité de la valeur de load. On a vu que la taille de cette valeur implique un délai maximum de 2 secondes (9 fois moins si on configure le CPU à pleine vitesse). La seconde, plus "intuitive" mais plus "lourde" (calcul de load à chaque appel, double boucle) peut traiter, quelle que soit la vitesse du CPU, un très grand nombre de milli-seconde.

En ajoutant quelques "define", il est possible d'utiliser l'une ou l'autre ou les 2 fonctions:

$ cat systick.c
#if USE_SYSTICK_DELAY_LOAD
void
systick_delay_load(uint32_t load) {
...
}
#endif /* USE_SYSTICK_DELAY_LOAD */

#if USE_SYSTICK_DELAY_MS
#define SYSTICK_DELAY_1_MS  (SystemCoreClock / 1000)
void
systick_delay_ms(uint32_t millis) {
...
}
#endif /* USE_SYSTICK_DELAY_MS */

$ cat systick.h
#pragma once

#if !USE_SYSTICK_DELAY_LOAD && !USE_SYSTICK_DELAY_MS
#error "USE_SYSTICK_DELAY not defined"
#endif 

#if USE_SYSTICK_DELAY_LOAD
void systick_delay_load(uint32_t load);
#endif

#if USE_SYSTICK_DELAY_MS
void systick_delay_ms(uint32_t millis);
#endif
Pour faire clignoter la LED toutes les 250 milli-secondes le "code" suivant peut sembler "lourd" mais c'est parce qu'il illustre l'usage d'une (ou des deux) fonctions:
#include "stm32f1xx.h"
#include "systick.h"

#if USE_SYSTICK_DELAY_LOAD
#define DELAY_1_MS  (SystemCoreClock / 1000)
#endif

#define DELAY_LED  250

int
main(void) {
#if USE_SYSTICK_DELAY_LOAD
    uint32_t load;
#endif
    SystemCoreClockUpdate(); // Update globale variable SystemCoreClock
#if USE_SYSTICK_DELAY_LOAD
    load = DELAY_LED * DELAY_1_MS; // OK @8 MHz
#endif

    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // Enable GPIO C 
    GPIOC->CRH   |= GPIO_CRH_MODE13_1;  // GPIO C PIN 13 mode 0b10: Output mode, max speed 2 MHz

    while (1) {
#if USE_SYSTICK_DELAY_LOAD
        systick_delay_load(load);
#elif USE_SYSTICK_DELAY_MS
        systick_delay_ms(DELAY_LED);
#endif
        GPIOC->ODR ^= GPIO_ODR_ODR13; // GPIO C PIN 13 output toggle state
    }

    return 0;
}
Si les délais d'allumage et d'extinction de la LED devaient être différents:
#include "stm32f1xx.h"
#include "systick.h"

#define DELAY_1_MS  (SystemCoreClock / 1000)
#define DELAY_LED   250

int
main(void) {
    uint32_t load;
    SystemCoreClockUpdate();       // Update globale variable SystemCoreClock
    load = DELAY_LED * DELAY_1_MS; // OK @8 MHz

    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // Enable GPIO C 
    GPIOC->CRH   |= GPIO_CRH_MODE13_1;  // GPIO C PIN 13 mode 0b10: Output mode, max speed 2 MHz

    // LED is on
    while (1) {
        systick_delay_load(load);        // 250 ms
        GPIOC->ODR ^= GPIO_ODR_ODR13;    // GPIO C PIN 13 output toggle state
        systick_delay_ms(DELAY_LED * 4); // 1000 ms
        GPIOC->ODR ^= GPIO_ODR_ODR13;    // GPIO C PIN 13 output toggle state
    }

    return 0;
}
Ici la LED reste allumée 250 ms et s'éteint durant 1 000 ms et, sans aucun soucis d'optimisation, on utilise les deux fonctions. En "production" les contraintes du projet décideront de la (bonne) fonction à utiliser.

Il est à noter que ce "code" est bloquant: le CPU ne fera rien d'autre que d'attendre, ce qui est rarement une bonne chose dans le domaine de l'embarqué. C'est pourquoi le composant SysTick peut aussi lever une interruption, bien plus efficace.

Le code complet: https://gitlab.com/dsx/blue-pill/-/tree/master/stm32f103c8t6/00_gpio_systick_delay

A retenir

Les ressources CPU sont précieuses, il est dommage de les gâcher pour une tâche aussi simple que de compter.