Ce document montre comment générer des impulsions périodiques à rapport cyclique variable, au moyen d'un chronomètre-compteur (Timer-Counter). On verra aussi comment générer des interruptions périodiques.
Les exemples fonctionnent sur l'Arduino Mega (ATmega 2560) avec les 4 Timers 16 bits (Timers 1,3,4,5), ou bien sur l'Arduino Uno (ATmega 328) avec le Timer 16 bits (Timer 1), ou bien sur l'Arduino Yun (ATmega 32u4) avec les deux Timers 16 bits (Timers 1 et 3). Ils pourront facilement être adaptés aux Timers 8 bits de l'ATmega 328.
Le microcontrôleur ATMega 2560 comporte 4 chronomètre-compteurs 16 bits (Timer-Counter) numérotés 1,3,4 et 5. On note n le numéro du Timer utilisé. Il se configure avec deux registres de contrôle 8 bits TCCRnA et TCCRnB (par exemple TCCR3A et TCCR3B pour le Timer 3). Le compteur proprement dit est un registre 16 bits, noté TCNTn, qui est incrémenté (ou décrémenté) d'une unité à chaque top d'horloge. L'horloge utilisée est l'horloge principale de l'Arduino, à laquelle un facteur de division de fréquence est éventuellement appliqué. Le choix de l'horloge se fait sur les bits 2,1 et 0 du registre TCCRnB, comme suit :
L'horloge principale de l'Arduino Mega a une fréquence f=16 MHz. Si l'on choisit par exemple l'horloge 010, sa fréquence est f/8=2 MHz, et le compteur parcourt un cycle complet en 32,768 ms.
Le compteur comporte aussi trois registres 16 bits OCRnA, OCRnB et OCRnC (Output Compare Register) qui sont comparés au registre TCNT pour déclencher différentes actions.
Pour générer un signal ou des interruptions périodiques, il faut utiliser le Timer en mode générateur d'onde (Waveform generation). Il y a plusieurs modes générateur (16). Le choix se fait avec les 4 bits WGMn3, WGMn2, WGMn1 et WGMn0 (Waveform Generation Mode). Les deux premiers sont les bits 4 et 3 du registre TCCRnB; les deux derniers sont les bits 1 et 0 du registre TCCRnA.
Nous allons utiliser un mode qui permet de générer des impulsions en ajustant finement la fréquence : le mode PWM, Phase and Frequency Correct, avec contrôle de la valeur maximale du compteur par le registre ICRn. Ce mode est sélectionné par les bits WGMn3=1, WGMn2=0, WGMn3=0, WGMn4=0. Le registre ICRn (Input Control Register) est utilisé ici pour fixer la valeur maximale que prend le compteur (registre TCNTn). Lorsqu'il atteint cette valeur maximale, il est décrémenté jusqu'à revenir à 0. Il y a donc une phase croissante (up-counting) suivi d'une phase décroissante (down-counting).
Chaque Timer comporte 3 sorties COnA, COnB et COnC (Compare Output). Pour savoir à quelle borne de l'Arduino ces sorties sont reliées, il faut consulter la table de correspondance de l'Arduino Mega. Supposons que l'on souhaite générer un signal PWM sur la sortie 5 de l'arduino. La table nous indique que cette sortie est reliée à la sortie OC3A, c'est-à-dire la sortie A du Timer 3. Nous allons donc utiliser le Timer 3. Pour configurer la sortie A, il faut agir sur les bits 7 et 6 du registre TCCR1A, nommés COM3A1, COM3A0. Nous choisissons la configuration COM3A1=1, COM3A0=1, qui conduit au comportement suivant : set OC3A on compare match when up-counting, clear OC3A on compare match when down-counting. Le registre du compteur TCNT3 est comparé au registre OCR3A (Output Compare Register). Lorsque TCNT3=OCR3A dans la phase croissante, la sortie OC3A est mise à 1. Lorsque TCNT3=OCR3A dans la phase décroissante, la sortie OC3A est mise à 0. Ce fonctionnement est montré sur la figure suivante :
Figure pleine pageOn voit que la période T du signal obtenu est le double de la période de l'horloge multipliée par la valeur de ICR3. Par exemple, si ICR3=160 et si l'on choisit la fréquence d'horloge f=16 MHz, la fréquence du signal est 50 kHz. La valeur de OCR3A, comprise entre 0 et ICR3, permet d'ajuster le rapport cyclique du signal PWM (Pulse Width Modulation).
Le code suivant permet de configurer et de déclencher le Timer pour obtenir ce résultat :
uint32_t period = 100; // période en microsecondes float rapport = 0.5; // rapport cyclique uint8_t clockBits = 1 << CS10; // fréquence de l'horloge du Timer à 16 MHz DDRE |= 1 << PORTE3; // Configuration de l'entrée/sortie 3 du port E (borne 5) comme sortie TCCR3A = (1 << COM3A1) | (1 << COM3A0); TCCR3B = 1 << WGM33; uint32_t icr = (F_CPU/1000000*period/2); uint16_t ocra = icr*(1.0-rapport); ICR3 = icr; OCR3A = ocra; TCCR3B |= clockBits; // choix de l'horloge et déclenchement du Timer
Ce code ne fonctionne que si la valeur de icr est inférieure à 0xFFFF. Dans le cas contraire, il faut abaisser la fréquence de l'horloge en appliquant un facteur diviseur.
Pour stopper le Timer, on met à zéro les bits 0,1,2 du registre TCCR3B :
TCCR3B &= ~0x7;
Le mode générateur d'onde permet aussi de générer des interruptions périodiques. Pour cela, il faut agir sur le registre de configuration TIMSK3 (Timer Interrupt Mask Register). Dans le cas présent, on souhaite générer une interruption parallèlement à la génération du signal PWM. Il faut pour cela activer le bit 0 de ce registre, TOIE3 (Timer Overflow Interrupt Enable), qui permet d'obtenir une interruption à chaque fois que le compteur atteint le débordement, c'est-à-dire à chaque fois qu'il atteint la valeur de ICR3. On obtient ainsi des interruptions périodiques à la période T. Voici comment faire la configuration et activer les interruptions :
TIMSK3 = 1 << TOIE3; sei(); // set interruptions
La fonction appelée lors de l'interruption est définie de la manière suivante :
ISR(TIMER3_OVF_vect) { // Overflow Interrupt // code à exécuter lors de l'interruption }
On souhaite générer un train d'impulsions périodiques, c'est-à-dire N impulsions espacées d'une durée T. Cet exemple pourrait servir à piloter un moteur pas-à-pas en contrôlant la vitesse de rotation et le nombre de pas.
Pour générer le train à N impulsions, on incrémente un compteur d'impulsion dans la fonction d'interruption définie précédemment. Lorsque ce compteur atteint la valeur N, on stoppe le Timer.
On commence par définir les constantes et les variables globales :
#include "Arduino.h" char pwmPin = 5; // OC3A (timer 3, Output compare A), PE3 (port E sortie 3) char clockBits; volatile uint32_t count; int32_t max_count; uint16_t diviseur[6] = {0,1,8,64,256,1024};
La fonction suivante configure et déclenche le Timer 3. Le premier argument est la période en microsecondes sous forme d'un entier 32 bits. Le deuxième argument est le nombre d'impulsions à générer (maximum 2,4 109), éventuellement négatif si l'on veut un nombre infini d'impulsions. Le dernier argument est le rapport cyclique, compris entre 0 et 1. Le diviseur de fréquence de l'horloge est déterminé pour que la valeur de ICR soit inférieure à 0xFFFF, puisqu'il sagit d'un registre 16 bits. Pour le plus grand diviseur, la période de l'horloge est 1024/16=64 microsecondes. La période maximale est alors 64 10-6*0xFFFF=4,19 s. Pour des périodes plus longues, l'utilisation d'un Timer peut être remplacé par une temporisation avec la fonction delay.
void pulse_train(uint32_t period, int32_t nombre, float rapport) { TCCR3A = 0; TCCR3A |= (1 << COM3A1) | (1 << COM3A0); TCCR3B = 1 << WGM13; // phase and frequency correct pwm mode, top = ICR3 uint32_t icr = (F_CPU/1000000*period/2); int d = 1; while ((icr>0xFFFF)&&(d<5)) { d++; icr = (F_CPU/1000000*period/2/diviseur[d]); } clockBits = d; uint16_t ocra = icr * (1.0-rapport); ICR3 = icr; OCR3A = ocra; TIMSK3 = 1 << TOIE3; // overflow interrupt enable sei(); count = 0; max_count = nombre+1; TCNT3 = 0; // mise à zéro du compteur TCCR3B |= clockBits; }
Voici la fonction exécutée lors de l'interruption, à chaque fois que ITCN3=ICR3 (Overflow interrupt). Elle stoppe le Timer en mettant à 0 les 3 bits de l'horloge, et annule la sortie. Si le nombre d'impulsions est négatif, on obtient un signal PWM sans fin.
ISR(TIMER3_OVF_vect) { // Overflow interrupt count++; if (count==max_count) { TCCR3B &= ~0x7; TCCR3A = 0; PORTE &= ~(1 << PORTE3); TCNT3 = 0; } }
La fonction setup configure la borne 5 comme sortie et l'initialise à 0 (on pourrait faire la même chose avec pinMode et digitalWrite).
void setup() { DDRE |= 1 << PORTE3; // data direction register PORTE &= ~(1 << PORTE3); }
La fonction loop ci-dessous montre un exemple d'utilisation. Un train de 10 impulsions de période 100 μs est généré toutes les secondes.
void loop() { delay(1000); pulse_train(100,10,0.2); }
Voici l'oscillographe de la sortie 5 :
La modulation FSK (Frequency Shift Key) consiste à transmettre un signal numérique binaire en modulant la fréquence d'une porteuse. On se propose de générer un signal PWM avec deux périodes T et 2T. On utilise le Timer 3 pour générer le signal. Le Timer 1 est utilisé pour générer des interruptions. À chaque interruption, on lit un bit d'un tampon de mémoire comportant 256 octets. On fonction de la valeur de ce bit, on choisit l'une ou l'autre des deux périodes.
On commence par définir les constantes et les variables globales :
#include "Arduino.h" #define BUF_SIZE 256 uint8_t buf[BUF_SIZE]; uint8_t pwmPin = 5; // OC3A (timer 3, Output compare A), PE3 (port E sortie 3) uint8_t clockBits; uint16_t top_0, ocra_0; uint16_t top_1, ocra_1; volatile uint8_t buf_index; volatile uint8_t bit_index;
La fonction suivante configure et déclenche les Timer 3 et 1. Le premier argument est la période T donnée en microsecondes. Le deuxième argument est le nombre de périodes T dans un train représentant un bit de donnée. Le troisième argument est le rapport cyclique, compris entre 0 et 1. Le Timer 1 génère aussi un signal périodique sur la sortie OC1A (borne 11). L'interruption est déclenchée lorsque TCNT1=OCR1A (compare A interrupt), ce qui permet d'obtenir un signal dont les fronts coïncident avec les changements de fréquence.
void fsk_pulse_train(uint32_t period, int32_t nombre, float rapport) { // periode en microsecondes, nombre d'impulsions cli(); // on désactive les interruptions TCCR3A = 0; TCCR3A |= (1 << COM3A1) | (1 << COM3A0); TCCR3B = 1 << WGM13; // phase and frequency correct pwm mode, top = ICR3 clockBits = 1 << CS10; top_0 = (F_CPU/1000000*period/2); ocra_0 = top_0 * (1.0-rapport); top_1 = top_0 << 1; ocra_1 = ocra_0 << 1; ICR3 = top_0; OCR3A = ocra_0; bit_index = 0; buf_index = 0; TCCR1A = 0; TCCR1A |= (1 << COM1A1); TCCR1B = 1 << WGM13; ICR1 = top_0*nombre ; OCR1A = ocra_0*nombre; TIMSK1 = 1 << OCIE3A; // output compare A match interrupt sei(); // activation des interruptions TCCR3B |= clockBits; // déclenchement des Timers TCCR1B |= clockBits; }
La fonction appelée par l'interruption du Timer 1 lit un bit dans le tampon et modifie les registres ICR3 et OCR3A en fonction de la valeur du bit.
ISR(TIMER1_COMPA_vect) { uint8_t b; b = buf[buf_index] & (1 << bit_index); bit_index++; if (bit_index==8) { bit_index = 0; buf_index++; } if (b==0) { ICR3 = top_1; OCR3A = ocra_1; TCNT3 = 0; } else { ICR3 = top_0; OCR3A = ocra_0; TCNT3 = 0; } }
La fonction setup suivante configure les deux bornes utilisées en sortie, initialise le tampon avec une alternance de 0 et de 1, puis déclenche l'émission du signal FSK avec une période de base T=10 μs.
void setup() { int i; DDRE |= 1 << PORTE3; // borne 5 en sortie DDRB |= 1 << PORTB5; // borne 11 en sortie for (i=0; i<BUF_SIZE; i++) { buf[i] = 0xAA; } fsk_pulse_train(10,30,0.5); }
La fonction loop est vide :
void loop() { }
Voici les oscillographes de la sortie 5 (bleue) et 11 (rouge) :