Ce que nous allons faire - Prérequis
Nous allons voir dans ce cours comment utiliser un encodeur rotatif ou codeur incrémental. Nous verrons 2 manières de le faire, en utilisant le mode ad hoc du timer, ou en utilisant des interruptions externes.
Les prérequis de ce cours sont :
Avoir suivi le cours CH32V003 : timer et interruptions.
Disposer d'un module encodeur rotatif tel que mentionné dans les prérequis.
Qu'est-ce que c'est ? Comment ça marche ?
Ce terme peut désigner plusieurs choses différentes par leur usage mais ayant un principe de fonctionnement identique. On utilisera plutôt le terme de codeur incrémental pour un dispositif de mesure de déplacements angulaires ou linéaires, et le terme d'encodeur rotatif pour un dispositif d'interface homme-machine.
Les codeurs incrémentaux sont utilisés avec des moteurs pour connaître la position angulaire de leur axe. Un exemple typique est l'antenne d'un radar de bateau, qui tourne en permanence, mais dont on a besoin de connaître l'angle par rapport à l'axe du bateau pour pouvoir positionner les obstacles.
Les encodeurs rotatifs sont par exemple utilisés par certains systèmes de billetterie automatique (vous tournez le bouton pour choisir la destination, vous appuyez dessus pour valider votre choix, etc.), ou encore pour le réglage de volume de certains amplificateurs audio (vous sentez qu'il y a des "crans" quand vous tournez le bouton, alors que le mouvement d'un potentiomètre est "lisse").
Physiquement, votre module encodeur rotatif se présente comme ceci :
Electriquement, voici son schéma ainsi que la manière de le relier à la carte de développement :
Son principe de fonctionnement est simple : il dispose de 2 sorties marquées A et B sur son symbole. Les marquages correspondants sur le circuit imprimé sont respectivement CLK et DT. Pour que vous vous y retrouviez plus facilement dans votre câblage, nous utiliserons ces noms. Le marquage SW correspond au poussoir intégré à l'encodeur, manœuvré en appuyant sur l'axe au lieu de le faire tourner.
Lorsque vous tournez l'axe de l'encodeur, une impulsion est émise sur CLK et sur DT à chaque "cran" de rotation, et ces impulsions sont en quadrature, c'est-à-dire décalées d'un quart de période. Le sens du décalage (CLK en premier ou DT en premier) dépend du sens de rotation. En analysant les impulsions de l'encodeur, vous pouvez donc à la fois connaître l'amplitude du mouvement, sa direction et sa vitesse.
Les chronogrammes ci-dessous vous permettront de visualiser le fonctionnement de l'encodeur dans les 2 sens de rotation. Vous aurez sans doute remarqué les résistances pull-up sur le schéma, ce qui signifie que les sorties sont actives à l'état bas.
On peut résumer le fonctionnement de l'encodeur en disant que si vous échantillonnez l'état de DT sur un front descendant de CLK, vous aurez DT = 1 si vous tournez dans le sens anti-horaire et DT = 0 dans le sens horaire, et chaque front descendant de CLK correspond à un nouveau "cran" de rotation.
Comme le fonctionnement de notre encodeur rotatif est identique à celui des codeurs incrémentaux, tout ce que vous verrez dans ce cours est valable pour les deux, mais avec l'avantage que les manipulation seront beaucoup plus simples avec notre petit module, sans compter que son coût est inférieur d'au moins 2 ordres de grandeur à celui de ses grands frères
Vous trouverez quelques exemples ici de codeurs incrémentaux industriels. Regardez les data sheets de ceux de Omron, par exemple. Vous remarquerez qu'en plus des sorties A et B, ils disposent d'une sortie Z, encore appelée "index". Une impulsion est produite sur cette sortie lorsque le codeur se trouve dans une position de référence. Selon la construction de la machine qui l'utilise, on sait alors où un se trouve.
Utilisation d'un timer en mode encodeur
Quand on utilise un timer en mode encodeur, les sorties de l'encodeur servent d'horloge au compteur, ce qui exclut tout autre utilisation des canaux. On voit d'autre part sur le diagramme général dans le manuel de référence que seuls les canaux 1 et 2 sont utilisés en mode encodeur.
En mode encodeur, la configuration de la base de temps est donc réduite à sa plus simple expression. Les canaux peuvent être configurés de la même façon qu'en mode input capture.
Lorsqu'on fait tourner l'encodeur rotatif, le compteur du timer va générer des événements de mise à jour qu'on traitera dans la routine d'interruption du timer (une interruption par "cran" de rotation).
Pour savoir dans quel sens on tourne, il faut examiner le bit DIR du registre TIMn_CTLR1. Normalement, ce bit est mis à jour par programme pour déterminer le sens de comptage, mais en mode encodeur, ce sens étant défini par les signaux d'entrée, le programme lira ce bit au lieu de l'écrire.
On notera enfin que l'encodeur rotatif est sujet au rebond. On utilisera donc le filtre des canaux pour l'éliminer, comme on l'a déjà fait dans le cours CH32V003 : mesure de durées.
Le moment est venu pour vous de créer un nouveau projet, modifier system_ch32v00x.c, puis remplacer le contenu du fichier main.c par le code suivant :
#include <ch32v00x.h> #include <stdbool.h> #include <debug.h> volatile bool dataReady; volatile int dirFlag; __attribute__((interrupt("WCH-Interrupt-fast"))) void TIM2_IRQHandler() { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { dirFlag = (TIM2->CTLR1 & (1 << 4)) >> 4; dataReady = true; TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } } int main() { USART_Printf_Init(115200); // -- On configure les entrées du timer. // PD4 = T2CH1 = CLK // PD3 = T2CH2 = DT RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE); GPIO_InitTypeDef gpioInit = { 0 }; gpioInit.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_3; gpioInit.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOD, &gpioInit); // -- On active le timer. RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // -- On configure la base de temps. TIM_TimeBaseInitTypeDef timeBaseInit = { 0 }; timeBaseInit.TIM_Period = 1; timeBaseInit.TIM_Prescaler = 0; // On met CKD à 2, donc la fréquence d'échantillonage des filtres est 48MHz / 4 = 12MHz. timeBaseInit.TIM_ClockDivision = TIM_CKD_DIV4; timeBaseInit.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &timeBaseInit); // -- On configure le timer en mode encodeur. TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI2, TIM_ICPolarity_Falling, TIM_ICPolarity_Falling); // -- On utilise le filtre d'entrée pour éliminer les rebonds. TIM_ICInitTypeDef icInit; icInit.TIM_Channel = TIM_Channel_1; icInit.TIM_ICPolarity = TIM_ICPolarity_Falling; icInit.TIM_ICSelection = TIM_ICSelection_DirectTI; icInit.TIM_ICPrescaler = TIM_ICPSC_DIV1; icInit.TIM_ICFilter = 12; TIM_ICInit(TIM2, &icInit); // -- On active les interruptions de mise à jour. NVIC_InitTypeDef nvicInit = { 0 }; nvicInit.NVIC_IRQChannel = TIM2_IRQn; nvicInit.NVIC_IRQChannelPreemptionPriority = 0; nvicInit.NVIC_IRQChannelSubPriority = 1; nvicInit.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvicInit); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // -- On active le compteur. TIM_Cmd(TIM2, ENABLE); dataReady = false; while (1) { if (dataReady) { dataReady = false; printf("dir = %d\r\n", dirFlag); } } }
Le premier paramètre de la fonction TIM_EncoderInterfaceConfig() peut être TIM_EncoderMode_TI1, TIM_EncoderMode_TI2 ou TIM_EncoderMode_TI12. Essayez les différentes valeurs et voyez ce que ça change, en vous référant aux chronogrammes plus haut pour suivre ce qui se passe. Supprimez le filtre pour voir son impact. Aussi, jouez avec les valeurs de TIM_ClockDivision et de TIM_ICFilter pour voir leur influence.
Utilisation d'interruptions externes
L'utilisation d'interruptions externes ne devrait vous poser aucune difficulté puisque c'est une application directe de ce que nous avons vu dans le cours CH32V003 : GPIO et interruptions. Il est cependant utile de se livrer à l'exercice car vous pouvez vous trouver dans une situation où vous utilisez déjà les 2 timers pour autre chose et devez en plus utiliser un encodeur rotatif.
Je vous propose de refermer cette page et d'essayer d'écrire le code correspondant par vous-même. Si vous rencontrez des difficultés, vous pourrez utiliser le code ci-dessous pour vous aider.
#include <ch32v00x.h> #include <stdbool.h> #include <debug.h> // On a plus facile à suivre avec des noms qu'avec des numéros. #define CLK_PIN GPIO_Pin_4 #define CLK_INT EXTI_Line4 #define DT_PIN GPIO_Pin_3 volatile bool dataReady; volatile int dirFlag; __attribute__((interrupt("WCH-Interrupt-fast"))) void EXTI7_0_IRQHandler() { if (EXTI_GetITStatus(CLK_INT) != RESET) { // Quand on a un front descendant sur CLK, le sens de rotation est // donné par DT : 1 = anti-horaire, 0 = horaire. // Pour utiliser la même convention qu'avec la version avec timer, // il faut donc complémenter la valeur. dirFlag = !GPIO_ReadInputDataBit(GPIOD, DT_PIN); dataReady = true; EXTI_ClearITPendingBit(CLK_INT); } } int main() { USART_Printf_Init(115200); // -- On configure les entrées du GPIO. // PD4 = CLK // PD3 = DT RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE); GPIO_InitTypeDef gpioInit = { 0 }; gpioInit.GPIO_Pin = CLK_PIN | DT_PIN; gpioInit.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOD, &gpioInit); // On active les interruptions externes sur CLK RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_EXTILineConfig(GPIO_PortSourceGPIOD, GPIO_PinSource4); EXTI_InitTypeDef extiInit = { 0 }; extiInit.EXTI_Line = CLK_INT; extiInit.EXTI_Mode = EXTI_Mode_Interrupt; extiInit.EXTI_Trigger = EXTI_Trigger_Falling; extiInit.EXTI_LineCmd = ENABLE; EXTI_Init(&extiInit); NVIC_InitTypeDef nvicInit = { 0 }; nvicInit.NVIC_IRQChannel = EXTI7_0_IRQn; nvicInit.NVIC_IRQChannelPreemptionPriority = 0; nvicInit.NVIC_IRQChannelSubPriority = 1; nvicInit.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvicInit); dataReady = false; while (1) { if (dataReady) { dataReady = false; printf("dir = %d\r\n", dirFlag); } } }
Vous aurez sans doute remarqué qu'il y a beaucoup de rebond, au point qu'on ne sait plus dans quel sens on tourne ! La solution est ici de mettre un condensateur sur chaque sortie du module encodeur rotatif comme ci-dessous.
Ce que nous avons appris
Vous savez maintenant :
Ce qu'est un encodeur rotatif et comment ça fonctionne.
Comment configurer un timer du CH32V003 pour gérer un encodeur.
Comment gérer un encodeur avec des interruptions externes.