Ce que nous allons faire - Prérequis
Nous allons voir dans ce cours comment utiliser le timer dans sa fonction la plus simple, déclencher des interruptions périodiquement pour servir de base de temps à l'application.
Dans notre cas, ces interruptions serviront à provoquer le changement d'état de notre LED. C'est l'équivalent de notre "blinky" du tout premier cours, mais avec un timer.
Les prérequis de ce cours sont :
Avoir suivi le cours CH32V003 : GPIO et interruptions.
Avoir suivi le cours CH32V003 : introduction aux timers.
Le code
Dans une application réelle, on utilise pratiquement jamais la fonction Delay_Ms() dont nous nous sommes servis au départ, d'une part parce que le programme a mieux à faire que de tourner en rond pour faire passer le temps, et d'autre part parce que seul un timer peut générer des intervales de temps précis - une boucle d'attente subira immanquablement des interruptions, ce qui modifiera le délai de manière imprévisible.
Dans cet exercice, nous n'utiliserons que le timer le plus simple du monde que nous avons vu au début du cours précédent, à savoir :
Le moment est venu pour vous de créer un nouveau projet, modifier system_ch32v00x.c comme vous en avez maintenant l'habitude, puis de remplacer le contenu du fichier main.c par le code suivant :
#include <ch32v00x.h> #include <stdbool.h> #define LED_GPIO_PIN GPIO_Pin_4 #define LED_GPIO_PORT GPIOC #define LED_GPIO_RCC RCC_APB2Periph_GPIOC // La variable toggleLED indique si on doit inverser l'état de la LED. volatile bool toggleLED = false; __attribute__((interrupt("WCH-Interrupt-fast"))) // TIM2_IRQHandler est le nom figurant dans startup_ch32v00x.S pour ce vecteur. void TIM2_IRQHandler() { // Est-ce que l'interruption à traiter est un débordement du compteur ? if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { // Oui, alors signaler à la super loop qu'elle a du travail. toggleLED = true; // Et ne pas oublier de remettre à 0 l'indicateur d'interruption. TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } } // Le micro-contrôleur fonctionne à 48MHz #define MCU_FREQ 48000000ul // On veut que la LED change d'état tous les 1/8 de seconde, comme lors de // notre tout premier exercice, ce qui correspond à une fréquence de 8Hz. #define LED_FREQ 8ul // Un compteur de 16 bits peut prendre 65536 valeurs différentes, c'est-à-dire // 2 à la puissance 16. En d'autres termes, le compteur peut diviser sa // fréquence d'horloge par au maximum 65536. #define COUNTER_MAX 65536ul int main() { RCC_APB2PeriphClockCmd(LED_GPIO_RCC, ENABLE); GPIO_InitTypeDef LED_GPIO_init = { 0 }; LED_GPIO_init.GPIO_Pin = LED_GPIO_PIN; LED_GPIO_init.GPIO_Mode = GPIO_Mode_Out_PP; LED_GPIO_init.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(LED_GPIO_PORT, &LED_GPIO_init); // -- Activer le timer 2. // ATTENTION, les timers 1 et 2 ne sont pas sur le même bus interne. // TIM1 est sur APB2 et TIM2 sur APB1. RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // -- Calculer les valeurs à assigner au prescaler et à l'auto-reload register. // Je détaille ici les étapes du calcul pour que ce soit bien clair. uint16_t prescalerValue = 1; uint32_t freqRatio = MCU_FREQ / LED_FREQ; if (freqRatio > COUNTER_MAX) { // Si le rapport entre la fréquence du micro-contrôleur et celle de la // LED est supérieur à la capacité de division du compteur, ça signifie // qu'on a besoin des services du prescaler. prescalerValue = freqRatio / COUNTER_MAX; // Comme c'est une division entière, on vérifie si elle tombe juste. // S'il y a un reste, on doit augmenter la valeur du prescaler de 1 // pour arrondir de façon à ne pas dépasser la capacité du compteur. if (MCU_FREQ % prescalerValue) { prescalerValue++; } // On recalcule le facteur de division après prescaling. freqRatio /= prescalerValue; } // Pour diviser par N, le compteur doit compter de 0 à (N - 1), donc on // doit ajuster les valeurs que nous avons calculé avant de les utiliser // pour configurer le timer. prescalerValue--; uint16_t autoReloadValue = freqRatio - 1; // -- Configurer le timer. TIM_TimeBaseInitTypeDef timeBaseInit = { 0 }; timeBaseInit.TIM_Period = autoReloadValue; timeBaseInit.TIM_Prescaler = prescalerValue; timeBaseInit.TIM_ClockDivision = TIM_CKD_DIV1; timeBaseInit.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &timeBaseInit); // -- Activer l'interruption du timer dans le contrôleur d'interruptions. NVIC_InitTypeDef nvicInit = { 0 }; nvicInit.NVIC_IRQChannel = TIM2_IRQn; nvicInit.NVIC_IRQChannelPreemptionPriority = 1; nvicInit.NVIC_IRQChannelSubPriority = 0; nvicInit.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvicInit); // -- Demander au timer de déclencher une interruption pour chaque // événement de mise à jour (dont débordement du compteur du timer). TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // -- Démarrer le compteur du timer. TIM_Cmd(TIM2, ENABLE); BitAction ledStatus = Bit_RESET; while (1) { if (toggleLED) { ledStatus = (ledStatus == Bit_SET) ? Bit_RESET : Bit_SET; GPIO_WriteBit(LED_GPIO_PORT, LED_GPIO_PIN, ledStatus); toggleLED = false; } } }
Comme d'habitude, des commentaires détaillés couvrent les points qui sont nouveaux pour vous. Prenez le temps de bien suivre ce que fait chaque fonction en descendant dans son source ("step into" au debugger). Lorsque vous tombez sur des noms de registres, consultez le chapitre 11 "General-purpose Timer (GPTM)" du manuel de référence pour comprendre ce qui se passe.
Ce que nous avons appris
Vous savez maintenant :
Configurer le compteur et le prescaler d'un timer.
Traiter les interruptions correspondant aux update events (événements de mise à jour) du timer.