TIMERS - Kasimashi/Systemes-embarques GitHub Wiki

Les Timers

Les timers sont des composants hardwares spéciaux qui fournissent des timestamps très précis, permettent de mesurer des intervals de temps, et d'effectuer des événements périodiquements pour de l'hardware ou du software. Dans la suite nous utiliserons les timers pour mesurer la longueur du pulse sur des signaux d'entrées (input capture) ou encore la génération de waveform de sortie (output compare et PWM)

Timer Organization and Counting Mode

Un timer est un composant hardware indépendant qui permet d'incrémenter ou de décrémenter un compteur à chaques cycle d'horloge. Le compteur tourne continuellement jusqu'à sa déactivation. Le compteur recommence son cycle dès qu'il atteint la valeur 0 lorsqu'il est en mode décrémentation. Il peut aussi recommencer son cycle s'il atteint une valeur défini lorsque le compteur s'incrémente.

Le softwware permet de selectionner la fréquence du timer, donc le timer est configurable pour tourner à une période désirée.

Si un timer fonctionne comme "output compare", le comparateur compare systématiquement la valeur du compteur avec une constante donnée par le software. Il génère en fonction une sortie ou une interruption si les deux valeurs sont égales. Le software permet de configurer la valeur de la constante pour controler le timing des sorties ou des interruptions.

Si un timer fonctionne comme "input compare", l'hardware log la valeur du compteur dans un registre spécial appelé (CCR) et génère une interruption quand l'événement voulu se passe. Typiquement, l'interrupt handler a besoin de copier le registre CCR vers un buffer utilisateur pour enregistrer les événements passé. Ensuite le software calcule la différence entre 2 valeurs loggé et trouve le temps passé entre deux événements.

Un compteur d'un timer hardware a 3 modes de comptages : le mode "up-counting", le mode "down-counting", et le mode "center-aligned counting" .

  • Le mode "up-counting" commence à compter à partir de 0 jusqu'à une valeur d'une constante puis recommence à partir de 0. Le software permet de setter la valeur de la constante et le stock dans un registre spécial appelé (ARR) pour auto-reload register. Par exemple si le registre ARR est à 4, le compteur va prendre les valeurs 0,1,2,3,4,0,1,2,3,4,(...) jusqu'à la déactivation du timer.
  • Le mode "down-counting", le compteur commence à la valeur de l'ARR et compte jusqu'à 0. Et recommence à la valeur de l'ARR. Par exemple si l'ARR vaut 4 on aura : 4,3,2,1,0,4,3,2,1,0,(...) jusqu'à la déactivation du timer.
  • Le mode "center-aligned counting", qui lui fait un "up-counting" puis un "down-counting" alternativement. Par exemple si l'ARR vaut 4 on aura : 0,1,2,3,4,3,2,1,0 (...) jusqu'à déactivation du timer.

Le compteur du timer forme en faites un signal triangulaire ou un signal à dent de scie. La période était controllé par la fréquence de l'horloge du compteur et de la valeur stocké dans le registre ARR. Pour un compteur "up-counting" et "down-counting", la période du signal à dent de scie est donnée par la formule :

$Countingperiod = (1+ARR) \times \frac{1}{f_{clkcnt}}$

Pour un compteur "center-aligned counting" , la période du signal triangulaire est donnée par la formule :

$Countingperiod = 2 * ARR * \frac{1}{f_{clkcnt}}`

Le compteur d'un timer possède deux événements : overflow et underflow. Dans le mode "up-counting", l'overflow est atteint lorsque le compteur est remis à 0. Dans le mode "down-counting", l'overflow est atteint lorsque le compteur atteint la valeur de ARR. Dans le mode "center-aligned counting", l'overflow et l'underflow est atteint alternativement 0 et ARR.

Quand on utilise un timer pour mesurer une grande différence de temps entre deux événements, le soft doit considérer que le underflow et l'overflow pour éviter de sous estimer la différence de temps. Le timer handler peut vérifier en vérifiant les flag du timer status register pour compter le nombre d'overflow et de underflow.

Input Capture

Le mode input capture d'un timer permet la mesure des signaux digitaux en terme de fréquence, largeur d'impulsion. On va par ici compter à partir d'un front et réarmer le timer à chaques front montant par exemple pour y mesurer la fréquence.

Passons à l'exemple : Nous utiliserons la capture d'entrée pour mesurer la fréquence et la largeur du signal d'entrée. Ici, je vais utiliser deux minuteries, c'est-à-dire Timer 1 pour la sortie PWM et Timer 2 pour la capture d'entrée.

  • Le timer 1 est connecté à l'horloge APB2, qui fonctionne à 180 MHz
  • Le Timer 2 est connecté à l'horloge APB1, qui fonctionne à 90 MHz

Cette partie est très importante. La fréquence minimale que l'appareil peut lire en dépend. Ci-dessous la configuration du TIMER 2

image

  • J'ai activé le mode direct de capture d'entrée pour le canal 1
  • Le Prescaler est réglé sur 90, ce qui diviserait l'horloge APB2 par 90, ce qui rendrait l'horloge du Timer 2 = 1 MHz
  • Je laisse l'ARR à 0xffffffff (Max pour Timer 32 bits)
  • La fréquence minimale que le Timer peut lire est égale à (TIMx CLOCK/ARR). Dans notre cas, ce sera (1MHz/0xffffffff) Hz.

Le TIMER1 est configuré pour donner un signal de sortie PWM.

image

Mesure de la fréquence

Afin de mesurer la fréquence du signal d'entrée, nous devons mesurer le temps entre les 2 fronts montants, ou entre 2 fronts descendants.

Ci-dessous la figure expliquant la même chose image

  • Lorsque le premier front montant se produit, la valeur du compteur est enregistrée.
  • Une autre valeur du compteur est enregistrée après l'apparition du deuxième front montant.
  • Maintenant la différence entre ces 2 valeurs de compteur est calculée.
  • La différence entre les valeurs du compteur nous donnera la fréquence.
  • L'ensemble de ce processus est illustré ci-dessous
#define TIMCLOCK   90000000
#define PRESCALAR  90

uint32_t IC_Val1 = 0;
uint32_t IC_Val2 = 0;
uint32_t Difference = 0;
int Is_First_Captured = 0;

/* Measure Frequency */
float frequency = 0;

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
	if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
	{
		if (Is_First_Captured==0) // if the first rising edge is not captured
		{
			IC_Val1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // read the first value
			Is_First_Captured = 1;  // set the first captured as true
		}

		else   // If the first rising edge is captured, now we will capture the second edge
		{
			IC_Val2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);  // read second value

			if (IC_Val2 > IC_Val1)
			{
				Difference = IC_Val2-IC_Val1;
			}

			else if (IC_Val1 > IC_Val2)
			{
				Difference = (0xffffffff - IC_Val1) + IC_Val2;
			}

			float refClock = TIMCLOCK/(PRESCALAR);

			frequency = refClock/Difference;

			__HAL_TIM_SET_COUNTER(htim, 0);  // reset the counter
			Is_First_Captured = 0; // set it back to false
		}
	}
}
  • La fonction de rappel ci-dessus est appelée chaque fois qu'un front montant est détecté.
  • Lors de son premier appel, Is_First_Captured était 0, donc IC_Val1 sera enregistré.
  • Lorsqu'il est appelé après le deuxième front montant, Is_First_Captured est désormais 1 donc IC_Val2 sera enregistré.
  • Nous calculerons ensuite la Différence entre les 2 valeurs.
  • L'horloge de référence est calculée en fonction de la configuration que nous avons effectuée pour notre minuterie.
  • La fréquence est égale à la (Horloge de référence / Différence).

Dans la fonction principale, nous devons démarrer le TIMER en mode interruption de capture d'entrée. Je démarre également le Timer 1 en mode PWM, afin de fournir le signal pour le Timer2.

  TIM1->CCR1 = 50;
  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);

  HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);

image

Mesure de la taille d'un pulse

Afin de mesurer la largeur d'impulsion, nous voulons que l'interruption se déclenche sur les deux fronts du signal entrant. image

Comme vous pouvez le voir sur la figure ci-dessus, j'ai changé la polarité sur les deux bords. Cela déclenchera l'interruption sur les deux fronts du signal entrant.

image

  • Lorsque le premier front montant se produit, la valeur du compteur est stockée dans l'ICVal 1
  • La prochaine interruption se produira sur le front descendant et la valeur du compteur est stockée dans l'IC val 2
  • La largeur d'impulsion peut être calculée à l'aide de cette valeur de compteur.
  • Le processus pour cela est indiqué ci-dessous
/* Measure Width */
uint32_t usWidth = 0;

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
	if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)  // if the interrupt source is channel1
	{
		if (Is_First_Captured==0) // if the first value is not captured
		{
			IC_Val1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // read the first value
			Is_First_Captured = 1;  // set the first captured as true
		}

		else   // if the first is already captured
		{
			IC_Val2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);  // read second value

			if (IC_Val2 > IC_Val1)
			{
				Difference = IC_Val2-IC_Val1;
			}

			else if (IC_Val1 > IC_Val2)
			{
				Difference = (0xffffffff - IC_Val1) + IC_Val2;
			}

			float refClock = TIMCLOCK/(PRESCALAR);
			float mFactor = 1000000/refClock;

			usWidth = Difference*mFactor;

			__HAL_TIM_SET_COUNTER(htim, 0);  // reset the counter
			Is_First_Captured = 0; // set it back to false
		}
	}
}
  • La fonction de rappel ci-dessus est appelée lorsque le front montant ou descendant est détecté.
  • Lorsqu'il est appelé pour le front montant, Is_First_Captured était 0, donc IC_Val1 sera enregistré.
  • Lorsqu'il est appelé pour le front descendant, Is_First_Captured est maintenant 1 donc IC_Val2 sera enregistré.
  • Nous calculerons ensuite la Différence entre les 2 valeurs.
  • Cette différence est le temps pendant lequel le Pulse est haut, et ce temps dépendra de la configuration du timer.
  • Pour mesurer ce temps en microsecondes, il faut utiliser l'horloge de référence.
  • L'horloge de référence est calculée en fonction de la configuration que nous avons effectuée pour notre minuterie.
  • Et enfin, la usWidth sera calculée.

Compare Output

Le compteur du timer (CNT) est une valeur encodée sur 16 bits. Le registre de capture/compare (CCR) garde la valeur qui est comparé avec la valeur du compteur. Dans le STM32L, 4 canaux de sorties partagent le même compteur de timer. Malgrès que le timer compares la valeur du compteur avec 4 registres CCR simultanément et génères 4 sorties indépendantes basés sur cette comparaison. L'horloge permet de comparer le compteur du timer. (CLOCK_CNT), ce dernier peut être ralenti par la valeur d'une constante appelé prescaler pour générer une sortie qui s'étend sur une longue période.

$fclockcnt = \frac{fclockpsc}{(Prescaler + 1)}$

Une valeur importante de Prescaler permet de réduire la résolution du timer, mais réduit les occurences d'overflow et l'underflow ce qui améliorre les performances énergétiques. Plusieurs horloges peuvent controler les timers : ces horloges inclus des mécanismes internes que sont le processeur, l'external crytal oscillators et certain signaux internes comme la sortie d'un autre timer. Les horloges externes sont préférés aux horloges internes car les horloges externes sont plus précises.

Timer_block_diagram

Setting Output Mode

Quand la valeur du timer counter (CNT) est égale au compare value register (CCR), le canal de sortie (OCREF) est programmable. La sortie peut avoir différences valeurs, cela dépend de la sortie du compare mode (OCM)

Output Compare Mode (OCM) Timer Reference Output (OCREF)
Timing mode (0000) Gelé
Active mode (0001) Logique haute si CNT = CCR
Inactive Mode (0010) Logique basse si CNT = CCR
Toggle Mode (0011) Toggle si CNT = CCR
Force Inactive Mode (0100) Forcer la logique à bas (toujours à bas)
Force Active mode (0101) Forcer la logique à haut (toujours à haut)
PWM output mode 1 (0110) Si le mode est upcounting : Logique à haute si CNT < CCR sinon logique à bas , Si le mode est downcounting, Logique haute si CNT <= CCR , sinon logique à bas
PWM output mode 2 (0111) Si le mode est upcounting : Logique à haut si CNT >= CCR sinon logique à bas , Si le mode est downcounting, Logique haute si CNT > CCR, sinon logique à bas

La sortie du mode active ou le mode inactive produisent respectivement une logique haute ou une logique basse quand le compteur atteint la valeur du CNT. Le mode Toggle inverse la sortie quand CNT vaut CCR. Ce qui produit une sortie haute puis basse, basse puis haute alternativement. Le mode Force inactive mode et active mode permet de garder respectivements les niveaux à bas et à haut.

Dans les circuits numériques, il y a deux logiques possible : haut et bas. Le software permet de changer la représentation de la logique pour la sortie de chaques timer indépendaments par programmation de la polarité sur le output polarity bit in the control register CCER. Quand la polarité est mise à 0, le mode actif correspond à une sortie qui est en "high voltage".

Active High signal Active Low Signal
Logique haute(1) High Voltage Low Voltage
Logique Basse(0) Low Voltage High Voltage

Le canal du timer peut avoir deux sortie, la sortie principale OC et son complémentaire OCN qui est un ou-exclusif du canal de référence. Qui correspond au bit de polarité du registre CCER. Le bit CCP et CCNP dans le registre CCER sont respectivements les bits de polarité de OC et OCN.

Si seulement OC et OCN sont activés :

OC = OCREF + polarity bit for OC
OCN = OCREF + polarity bit for OCN

Si les deux OC et OCN sont activés :

OC = OCREF + Polarity bit for OC
OCN (not OCREF) + Polarity bit for OCN

La logique "high active" est toujours utilisé pour OCREF. Cependant, OC et OCN ne peuvent pas être active en même temps sur active high ou active low. Cela dépend du bit de polarité. Si le but de polarité est à 0, la sortie du canal correspondant est en active high. Sinon elle est en active low.

Si les interruptions des timers sont activés, l'interrption est appelée quand le CNT = CCR, ou quand le CNT a un overflow ou un underflow. L'ISR doit vérifier quel est le status du timer status register pour savoir quel est l'événement qui vient d'arriver. L'update interrupt flag (UIF) est setté en cas d'overflow ou d'underflow, et le flag compare interrupt flag (CCIF) est setté quand CNT = CCR.

Une interruption DMA peux aussi être généré pour charger la valeur stocké dans la mémoire dans les registres ARR et CRR automatiquement. (Se référer au chapitre DMA).

Timer_Output_compare

Example of Toggling LED with Output Compare

Dans cette section nous allons utiliser le mode output compare mode pour faire le clignotement d'une LED. Attaché au PIN PB13.

Chaques broches GPIO peux faires plusieurs fonctionalités hardware. Les fonctions différes entre les cartes et les manufactureurs, mais aussi entre les broches elle même. Dû à la complexité de la carte et au cout de fabrication, c'est impossible qu'une broche supporte toutes les fonctions. Dans le cas de notre broche nous pouvons trouver les alternatives fonctions. Dont le TIM2_CH1 qui va nous permettre de faire un toggle de la led.

Alternate_function

Supposons que la clock du système soit à 80 MHz, et que cette dernière soit notre clock système qui pilote le TIMER 2. Le calcul suivant nous permet donc de calculer le prescaler qui va nous permettre de déscendre le timer à 2 KHz :

$fclk_cnt = \frac{fclkpsc}{(Prescaler + 1)}` donc

$Prescaler = \frac{fclkpsc}{fclock_cnt} - 1 = 80 MHz / 2KHz - 1 = 40000 - 1 = 39999`

Ainsi pour allumer et éteindre la LED toutes les secondes nous devons placer notre ARR à 1999 car la fréquence d'horloge est à 2KHz. Le timer compte ainsi de 0 à 1999, donc 2000 cycles pour chaques périodes. Le registre CCR peut être alors placé entre 0 et 1999.

L'implémentation C suivante permet par software de configurer le TIM1_CH1N (La sortie complémentaire du canal 1 du timer 1) comme compare-output et de faire un toggle de la LED connecté sur PE8 toutes les secondes.

int main()
{

	System_Clock_Init(); // System clock = 80MHz
	
	RCC-> AHB2ENR |= RCC_AHB2ENR_GPIOEEN; // Enable GPIOE clock
	
	// Set mode of pin 8 as alternate function
	// 00 = Input 01 = Output, 10 = Alternate function, 11 = Analog
	GPIOE->MODER &= ~(3UL << 16); // Clear bit 17 et 16
	GPIOE->MODER |= 2UL << 16 // Set mode as 10
	
	// Select alternative function (TIM1_CH1N)
	GPIOE->AFR[1] &= ~(0xF); // ARF[0] for pin 0-7 , ARF[1] for pin 8-15
	GPIOE->AFR[1] | 1UL; //TIM1_CH1N defined as 01
	
	// Set I/O output speed value at low
	// 00 : Low, 01 Medium, 10 Fast, 11 High
	GPIOE->OSPEEDR &= ~(3UL<<16);
	
	// Set pin PE8 as no pull-up/pull-down
	// 00 for no pull up pull down
	GPIOE->PUPDR &= ~(3UL<<16);
	
	// Enable Timer1 Clock
	RCC->APN2ENT |= RCC_APB2ENR_TIM1EN;
	
	// Counting direction : 0 upcounting, 1 downcounting
	TIM1-> CR1 &= ~TIM_CR1_DIR;
	
	// Clock prescaler
	TIM1->PSC = 39999;
	
	// Auto reload register
	TIM1->ARR = 2000 -1;

	// CCR can be any value between 0 and 1999
	TIM1->CCR1 = 500;
	
	// Main output enable (MOE) 0 Disable 1 Enable
	TIM1->BDTR |= TIM_BDTR_MOE;
	
	// Clear output compare mode bits for channel 1
	TIM1->CCMR1 &= ~TIM_CCRM1_OC1M;
	
	/// Select Toggle Mode (0011)
	TIM1->CCMR1 |= TIM1_CCMR1_OC1M_0 | TIM_CCMR1_OC1M_1;
	
	// Select output for channel 1 complementary output
	TIM1-> CCER &= ~TIM_CCER_CC1NP; // Select active high
	// Enable output for channel 1 complementary output
	TIM1->CCER |= TIM_CCER_CC1NE;
	
	// Enable Timer 1
	TIM1->CR1 | TIM_CR1_CEN;
	
	while(1); //Dead loop
}

Un autre exemple de output compare est disponible dans le dossier STM32.

Timer Update Events

Un Update Event (UEV) est généré à chàque fois qu'on atteint l'overflow dans le mode upcounting, et à chaques underflow dans le downcounting. Sur l'overflow et le underflow pour le center-counting.

Ainsi la période de l'UEV est :

$UEVPeriod = (1+ARR) \times (1+Prescaler) \times \frac{1}{fclkcnt}$

Les événements UEV ont 3 objectifs :

  • Généré une trigger output (TRGO) ou sortie de déclenchement pour d'autres modules internes que sont les timers DMA, ADC et DAC.
  • Permettre l'update des registres ARR, PSR, et CCR en prenant effet immédiatement, si le buffering (aussi appelé preload) mécanisme est activé. Si le channel preload enable bit (OCPE) dans le registre CCMR1 et que l'auto-reload enable bit (ARPE) dans le registre CR1 est setté, le mécanisme de preload est activé.
  • Générer une interruptions timer si l'update interrupt flag bit (UIF) du control register CR1 est setté. L'interruption est envoyé au NVIC. En réponse, le processeur execute la routine d'interruption (ISR).

Le Software peut déactiver l'UEV en mettant à jour l'udpate disable bit à 0(UDIS) dans le registre CR1. Dans ce cas les updates events ne sont pas générés.

Dans l'exemple ci dessous le software utilise le timer update event pour atteindre une précision hardware sur le délais. A 30°C, le MSI et le HSI peut atteindre une précision de +-0.6%. Supposons que le MSI est setté à 4MHz et qu'il pilote le timer. Le prescaler est setté à 3999 et le compteur est en mode upcounting. Donc le timer counter est incrémenté chaques ms. On selectionne le timer7 car ce dernier est un timer basic sans les fonctions avancés. Dans ce cas on réserve les timers avancés pour des usages plus compliqués.

void delay(uint16_t ms)
{
	if (ms == 0) // Sanity check
	{
	}
	
	//Enable timer7 clock
	RCC->APB1ENR1 |= RCC_APB1ENR1_TIM7EN;
	
	TIM7-> CR1 &= ~TIM_CR1_EN; // Disable Timer7
	TIM7-> SR = 0; // Clear status register
	TIM7-> CNT = 0; // Clear counter
	TIM7-> PSC = 3999;	//4MHz/(1+3999) = 1Khz
	TIM7 -> ARR = ms - 1; // ARR + 1 cycle
	TIM7-> CR1 |= TIM_CR1_EN; // Enable Timer7
	
	while( (TIM7->SR & TIM_SR_UIF) == 0); // Loop until UIF is set
}

L'exemple ci dessous compte le nombre d'overflow atteint dans le mode upcounting. Si le flag UIF dans le timer status register (SR) est setté, l'interruption timer incrément le compteur d'overflow and nettoie le flag UIF pour prevenir une nouvelle execution de l'ISR.

volatile uint32_t overflow = 0;

int main(void) {
	//Init
	...
	
	// Select counting direction : 0 upcounting
	TIM1->CR1 &= ~TIM1_CR1_DIR
	
	// Enable update interrupt
	TIM1->DIER |= TIM_DIER_UIE;
	
	// Enable Timer4 interrupt on NVIC
	NVIC_EnableIRQ(TIM1_IRQn);
	
	//Enable the counter
	TIM1->CR1 | TIM_CR1_CEN;
	
	while(1);
}

void TIM1_IRQHandler(void)
{

	// Check whether an overflow event has taken place
	if ((TIM1->SR & TIM_SR_UIF) != 0) {
		overflow++;
		TIM1-> SR &= ~TIM_SR_UIF;
	}
}

Dans cette exemple la variable overflow est déclarée en volatile. Pour informer le compilateur qu'aucune optimisation ne doit être faites sur cette variable. Sans l'optimisation il est possible que le compilateur réutilise incorrectement la variables. Car l'IRQ n'est pas appelé par le software et donc pense que la variable ne change jamais.

⚠️ **GitHub.com Fallback** ⚠️