CH32V003 : timer et interruptions

par Vincent DEFERT, dernière mise à jour le 2025-12-27

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 :

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)) {
		// 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);
	}
}

// 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);

	// -- Récupérer les fréquences d'horloge des différents bus internes.
	RCC_ClocksTypeDef clocks;
	RCC_GetClocksFreq(&clocks);
	uint32_t timerFreq = clocks.HCLK_Frequency;
	
	// Si, plus bas, nous assignons une autre valeur que TIM_CKD_DIV1 au
	// paramètre timeBaseInit.TIM_ClockDivision, il faudra ajuster timerFreq
	// En conséquence. Si timeBaseInit.TIM_ClockDivision contient déjà la
	// bonne valeur (ce qui n'est pas le cas ici), on peut utiliser le code 
	// générique suivant :
	// timerFreq >>= timeBaseInit.TIM_ClockDivision > 8;
	
	// Dans la pratique, on utilisera autre chose que TIM_CKD_DIV1 pour 
	// obtenir des durées plus longues. Les facteurs de division possibles 
	// sont 1, 2 et 4.
	
	// -- 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 = timerFreq / 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 (clocks.PCLK1_Frequency % 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;

	// -- Activer le timer 2.
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

	// -- 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;
			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.

Remarques importantes

Dans notre code, nous avons utilisé clocks.HCLK_Frequency pour déterminer la fréquence d'horloge de notre timer parce que dans le manuel de référence du CH32V003, sur le diagramme de la section 3.3.1 System Clock Structure, nous voyons que SYSCLK, le signal d'horloge qui cadence notre processeur, passe par un prescaler pour devenir HCLK, le signal d'horloge utilisé par les périphériques, dont notre timer. Si notre CPU fonctionne à 48MHz, les périphériques peuvent donc fonctionner à une fréquence inférieure selon le diviseur appliqué (de 1 à 256).

Cette règle est valable pour tous les CH32V00x, CH32Mxxx et CH32X03x. Elle est par contre un peu différente pour les CH32L103, CH32V103, CH32V20x, CH32V3xx, CH32F103 et CH32F20x, dans lesquels les périphériques sont répartis sur 2 autres bus appelés APB1 et APB2, eux-mêmes reliés au bus AHB (celui fonctionnant à HCLK). Avec ces micro-contrôleurs, les bus APB1 et APB2 ont eux aussi leur propres prescalers qui divisent encore HCLK. Enfin, les timers de ces micro-contrôleurs fonctionnent soit à la fréquence de leur APB si son prescaler vaut 1 (pas de division), soit 2 fois cette fréquence si le prescaler est supérieur à 1.

En résumé, on a les 3 cas suivants :

	// Cas des CH32V00x, CH32Mxxx et CH32X03x (tous timers confondus).
	// ===============================================================
	RCC_ClocksTypeDef clocks;
	RCC_GetClocksFreq(&clocks);
	uint32_t timerFreq = clocks.HCLK_Frequency;
	
	// Puis corriger au besoin timerFreq selon timeBaseInit.TIM_ClockDivision.
	// Cas des CH32L103, CH32V103, CH32V20x, CH32V3xx, CH32F103 et CH32F20x
	// pour un timer connecté à APB1 (ex. TIM2).
	// ====================================================================
	RCC_ClocksTypeDef clocks;
	RCC_GetClocksFreq(&clocks);
	uint32_t timerFreq = clocks.PCLK1_Frequency;
	
	// Les bits 10..8 du registre RCC->CFGR0 indiquent le facteur de division 
	// du prescaler de APB1.
	if ((RCC->CFGR0 & 0x0700) != RCC_HCLK_Div1) {
		// Si le facteur de division est supérieur à 1, on multiplie la fréquence par 2.
		timClockFreq <<= 1;
	}
	
	// Puis corriger au besoin timerFreq selon timeBaseInit.TIM_ClockDivision.
	// Cas des CH32L103, CH32V103, CH32V20x, CH32V3xx, CH32F103 et CH32F20x
	// pour un timer connecté à APB2 (ex. TIM1).
	// ====================================================================
	RCC_ClocksTypeDef clocks;
	RCC_GetClocksFreq(&clocks);
	uint32_t timerFreq = clocks.PCLK2_Frequency;
	
	// Les bits 13..11 du registre RCC->CFGR0 indiquent le facteur de division 
	// du prescaler de APB2.
	if (((RCC->CFGR0 >> 3) & 0x0700) != RCC_HCLK_Div1) {
		// Si le facteur de division est supérieur à 1, on multiplie la fréquence par 2.
		timClockFreq <<= 1;
	}
	
	// Puis corriger au besoin timerFreq selon timeBaseInit.TIM_ClockDivision.

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.


Copyright (c) 2024 Vincent DEFERT - Tous droits réservés