Pr. Baghouri Mostafa · ENSA Tétouan · ATmega328 (Arduino)
Ce cours fait partie du cursus Génie en Cybersécurité et Systèmes Embarqués (Semestre 2) dispensé à l'Université Abdelmalek Essaadi – ENSA Tétouan, Département Intelligence Artificielle et Digitalisation.
L'enseignant responsable est Pr. Baghouri Mostafa (Mostafa.baghouri@ensam-casa.ma).
Cette diapositive annonce le Chapitre 6 : Timers AVR en Assembleur et en C. Il s'agit du chapitre central consacré aux mécanismes de temporisation et de comptage sur la famille AVR.
Le microcontrôleur ciblé est l'ATmega328 (base de l'Arduino Uno). Les programmes seront écrits en assembleur AVR et en C (avr-gcc).
À l'issue de ce chapitre, l'étudiant doit être capable de :
Un compteur est basé sur un registre compteur (Counter Register). Ce registre peut être :
Ce modèle simplifié est la brique fondamentale de tout timer/compteur dans un microcontrôleur.
Premier exemple concret : un compteur de personnes à l'entrée d'un bâtiment. Un capteur (sensor) envoie une impulsion à chaque passage.
UP → le registre s'incrémente.Pour générer un délai temporel avec un compteur, on exploite la fréquence régulière de l'horloge système :
UP du compteur.$FC = 252) dans le registre.$FC (252), le timer effectue 4 cycles → délai = 4 µs. En chargeant $00, il effectue 256 cycles → délai maximal = 256 µs.
Le timer/compteur d'un microcontrôleur est un module matériel autonome qui compte les fronts d'horloge. Il joue plusieurs rôles :
delay(), millis() en Arduino)Chaque timer possède un flag d'overflow (TOVn) qui est automatiquement mis à 1 lorsque le compteur déborde (passe de 0xFF à 0x00 pour 8 bits).
TCNT0 = $FC (252 en décimal).FC → FD → FE → FF → (overflow) → 00.TOV0 = 1.SBIS TIFR0, TOV0), puis l'efface par logiciel en écrivant 1 dans le bit correspondant de TIFR0.Le timer/compteur peut recevoir son signal d'horloge depuis deux sources, sélectionnables via les bits CS (Clock Select) du registre TCCRn :
Le registre TIFR contient le flag TOVn. Quand le compteur déborde, TOVn = 1. Le programme peut soit lire ce flag en polling, soit configurer une interruption.
L'ATmega328 (Arduino Uno) possède 3 timers :
| Timer | Résolution | Caractéristiques |
|---|---|---|
| Timer0 | 8 bits | Utilisé par Arduino pour millis(), delay(), PWM pins 5 et 6 |
| Timer1 | 16 bits | Haute précision, capture d'entrée, PWM pins 9 et 10 |
| Timer2 | 8 bits | Prescalers différents de Timer0, PWM pins 3 et 11, mode asynchrone |
Chaque timer n dispose d'un ensemble de registres 8 bits accessibles par adresse :
| Registre | Nom complet | Rôle |
|---|---|---|
| TCNTn | Timer/Counter Register | Valeur courante du compteur (lecture/écriture) |
| TOVn | Timer Overflow Flag | Mis à 1 quand le compteur déborde (dans TIFRn) |
| TCCRn | Timer Counter Control Register | Configure le mode (Normal, CTC, PWM) et la source d'horloge |
| OCRn | Output Compare Register | Valeur de comparaison avec TCNTn |
| OCFn | Output Compare Flag | Mis à 1 quand TCNTn = OCRn (dans TIFRn) |
SBI/CBI pour certains).
OUT et IN (accès direct au port I/O) ne fonctionnent que pour les adresses ≤ 0x3F. Pour Timer1 (adresses > 0x3F), il faut utiliser STS / LDS.
Le Timer 0 est un timer 8 bits, le premier et le plus simple de l'ATmega328. Il compte de 0x00 à 0xFF (0 à 255), puis déborde.
millis(), micros(), delay(). Si vous modifiez ses registres, ces fonctions seront perturbées. Dans ce cours, nous configurons le timer manuellement.
Le Timer 0 expose les registres suivants :
| Registre | Adresse | Rôle |
|---|---|---|
| TCCR0A | 0x44 | Contrôle A : mode WGM, sorties OC0A/B |
| TCCR0B | 0x45 | Contrôle B : mode WGM, prescaler CS |
| TCNT0 | 0x46 | Registre compteur (0–255) |
| OCR0A | 0x47 | Valeur de comparaison A |
| OCR0B | 0x48 | Valeur de comparaison B |
| TIFR0 | 0x35 | Flags : TOV0, OCF0A, OCF0B |
Le registre TIFR0 (Timer Interrupt Flag Register 0) contient 3 bits de flag :
| WGM02 | WGM01 | WGM00 | Mode |
|---|---|---|---|
| 0 | 0 | 0 | Normal |
| 0 | 0 | 1 | Phase correct PWM |
| 0 | 1 | 0 | CTC (Clear Timer on Compare Match) |
| 0 | 1 | 1 | Fast PWM |
| 1 | 0 | 1 | Phase correct PWM (TOP=OCR0A) |
| 1 | 1 | 1 | Fast PWM (TOP=OCR0A) |
| CS02 | CS01 | CS00 | Source d'horloge |
|---|---|---|---|
| 0 | 0 | 0 | Aucune (timer arrêté) |
| 0 | 0 | 1 | clk (pas de prescaler) |
| 0 | 1 | 0 | clk / 8 |
| 0 | 1 | 1 | clk / 64 |
| 1 | 0 | 0 | clk / 256 |
| 1 | 0 | 1 | clk / 1024 |
| 1 | 1 | 0 | Horloge ext. (T0, front descendant) |
| 1 | 1 | 1 | Horloge ext. (T0, front montant) |
En mode Normal (WGM = 000), le timer compte librement de 0x00 à 0xFF, puis repart de 0x00 (overflow). À chaque overflow, le flag TOV0 passe à 1.
TCNT0 avec une valeur initiale.TOV0 soit mis à 1 (polling ou interruption).TOV0 en écrivant 1 dans le bit 0 de TIFR0.Objectif : écrire un programme qui génère un délai de 14 cycles machine en utilisant le Timer0 en mode Normal.
0xF2.Configuration des registres :
; Mode Normal (WGM02:00 = 000), prescaler = 1 (CS = 001)
TCCR0A = 0x00; // WGM01=0, WGM00=0
TCCR0B = 0x01; // CS02=0, CS01=0, CS00=1 → clk/1
Le prescaler à 1 (CS = 001) signifie que le timer s'incrémente à chaque cycle d'horloge. La chaîne de valeurs est :
; Assembleur AVR
TCCR0A = 0x00; // Mode Normal
TCCR0B = 0x01; // Prescaler = 1 (clk/1)
Programme complet : génère un signal carré sur PB5 en utilisant le Timer0 (mode Normal, TCNT0 = 0xF2).
LDI R16, (1<<5) ; masque pour PB5
SBI DDRB, 5 ; PB5 = sortie
LDI R17, 0
OUT PORTB, R17 ; PB5 = 0
BEGIN:
LDI R20, 0xF2
OUT TCNT0, R20 ; charger timer
LDI R20, 0x00
OUT TCCR0A, R20 ; mode normal
LDI R20, 0x01
OUT TCCR0B, R20 ; démarrer timer (clk/1)
AGAIN:
SBIS TIFR0, TOV0 ; sauter si TOV0 = 1
RJMP AGAIN ; sinon boucler
LDI R20, 0x00
OUT TCCR0B, R20 ; arrêter Timer0
LDI R20, (1<<TOV0)
OUT TIFR0, R20 ; effacer TOV0
EOR R17, R16 ; basculer bit 5
OUT PORTB, R17 ; appliquer sur PB5
RJMP BEGIN
DDRB = 1<<5;
PORTB &= ~(1<<5);
while (1) {
TCNT0 = 0xF2;
TCCR0A = 0x00;
TCCR0B = 0x01;
while((TIFR0 & (1<<TOV0)) == 0) {}
TCCR0B = 0;
TIFR0 = (1<<TOV0);
PORTB ^= (1<<5);
}
On reprend l'exemple 1 avec XTAL = 10 MHz. Combien de temps dure exactement le délai généré par le timer ?
Cette valeur ne compte que le temps passé dans le timer. Le programme a aussi des instructions à exécuter, qui prennent du temps supplémentaire (voir page suivante).
Chaque instruction assembleur prend 1 ou 2 cycles machine. Pour un calcul précis du délai total, il faut compter tous les cycles :
| Instruction | Cycles | Description |
|---|---|---|
LDI R20,0xF2 | 1 | Charger valeur |
OUT TCNT0,R20 | 1 | Initialiser timer |
LDI R20,0x00 | 1 | |
OUT TCCR0A,R20 | 1 | Mode normal |
LDI R20,0x01 | 1 | |
OUT TCCR0B,R20 | 1 | Démarrer timer |
SBIS TIFR0,TOV0 | 1/2 | Test flag (1 si non sauté, 2 si sauté) |
RJMP AGAIN | 2 | Branchement relatif |
LDI/OUT ×4 | 4 | Stop timer + clear flag |
EOR R17,R16 | 1 | Toggle bit |
OUT PORTB,R17 | 1 | Sortie |
RJMP BEGIN | 2 | |
| Total instructions | 18 |
Procédure générale pour calculer la valeur à charger dans TCNT0 :
T = 1 / fn = délai_voulu / TTCNT0 = 256 − nGénérer un signal carré de période 10 µs sur PORTB.3 avec XTAL = 10 MHz.
; Assembleur
BEGIN:
LDI R20, 206
OUT TCNT0, R20
LDI R20, 0x01
OUT TCCR0B, R20 ; démarrer, clk/1
AGAIN:
SBIS TIFR0, TOV0
RJMP AGAIN
; arrêter, clear flag, toggle PB3
RJMP BEGIN
// C
while (1) {
TCNT0 = 206;
TCCR0A = 0x00;
TCCR0B = 0x01;
while((TIFR0 & 0x01) == 0) {}
TCCR0B = 0;
TIFR0 = 1<<TOV0;
PORTB = PORTB ^ (1<<3);
}
Modifier TCNT0 pour obtenir le plus grand délai possible sans prescaler (XTAL = 10 MHz).
TCNT0 = 0x00. Le timer compte de 0 à 255 (256 états), puis déborde.Au-delà de cette limite, il faut soit utiliser un prescaler, soit utiliser le Timer1 (16 bits).
TCNT0 = 0x00; // Départ à zéro = délai max
TCCR0A = 0x00;
TCCR0B = 0x01;
while((TIFR0 & (1<<TOV0)) == 0) {}
TCCR0B = 0;
TIFR0 = 0x01;
PORTB ^= (1<<3);
Quand le délai souhaité dépasse la capacité du timer, trois techniques sont disponibles :
| Prescaler | T_tick | Délai max Timer0 (8 bits) | Délai max Timer1 (16 bits) |
|---|---|---|---|
| 1 | 62,5 ns | 16 µs | 4 ms |
| 8 | 500 ns | 128 µs | 32 ms |
| 64 | 4 µs | 1,024 ms | 262 ms |
| 256 | 16 µs | 4,096 ms | 1,048 s |
| 1024 | 64 µs | 16,384 ms | 4,194 s |
Le préscaler 10 bits de l'ATmega328 est partagé entre Timer0 et Timer1. Il divise clk_IO par un facteur configurable avant de l'envoyer au timer :
Les bits CS02:CS00 du registre TCCR0B sélectionnent quelle sortie du prescaler alimente le timer. La broche externe T0 (PD4) peut également être utilisée comme source (CS = 110 ou 111).
Programme analysé : TCNT0 = 0x10, prescaler ÷64 (CS = 011), XTAL = 8 MHz.
Le code utilise TCCR0B = 0x03 pour sélectionner CS = 011 (÷64), et appelle une fonction DELAY en subroutine (RCALL/RET).
XTAL = 8 MHz, prescaler 1024. Calculer la période d'horloge et le délai maximal.
XTAL = 8 MHz, prescaler ÷256, signal carré 125 Hz sur PORTB.3.
; Assembleur — sous-routine DELAY
DELAY:
LDI R20, 0x83 ; TCNT0 = 131
OUT TCNT0, R20
LDI R20, 0x04 ; CS = 100 (÷256)
OUT TCCR0, R20
AGAIN:
IN R20, TIFR
SBRS R20, TOV0
RJMP AGAIN
LDI R20, 0
OUT TCCR0, R20
LDI R20, (1<<TOV0)
OUT TIFR, R20
RET
En mode CTC (WGM = 010), le timer ne déborde plus à 255 mais se remet à zéro automatiquement lorsque TCNT0 == OCR0A. Le flag OCF0A est alors mis à 1.
Pour activer le mode CTC sur Timer0, configurer les bits WGM dans TCCR0A et TCCR0B :
| WGM02 (TCCR0B) | WGM01 (TCCR0A) | WGM00 (TCCR0A) | Mode |
|---|---|---|---|
| 0 | 1 | 0 | CTC |
// Activer mode CTC
TCCR0A = (1<<WGM01); // WGM01=1, WGM00=0 → CTC
TCCR0B = 0x01; // prescaler = 1
OCR0A = 49; // valeur de comparaison
Même objectif que l'exemple 3 : signal carré période 10 µs sur PORTB.3, XTAL = 10 MHz. Cette fois en mode CTC.
; Assembleur
LDI R20, 49
OUT OCR0A, R20 ; TOP = 49
BEGIN:
LDI R20, (1<<WGM01)
OUT TCCR0A, R20 ; CTC mode
LDI R20, 1
OUT TCCR0B, R20 ; prescaler = 1
AGAIN:
SBIS TIFR0, OCF0A ; attendre OCF0A
RJMP AGAIN
; stop, clear, toggle PB3
RJMP BEGIN
// C
OCR0A = 49;
TCCR0A = (1<<WGM01);
TCCR0B = 1;
while((TIFR0 & (1<<OCF0A)) == 0) {}
TCCR0B = 0;
TIFR0 = 1<<OCF0A;
PORTB ^= 1<<3;
Le Timer2 est également un timer 8 bits, structurellement très similaire au Timer0, avec les mêmes registres :
| Timer0 | Timer2 | Rôle |
|---|---|---|
| TCCR0A/B | TCCR2A/B | Contrôle |
| TCNT0 | TCNT2 | Compteur |
| OCR0A/B | OCR2A/B | Comparaison |
| TIFR0 | TIFR2 | Flags TOV, OCF |
| CS22:CS20 | Timer0 | Timer2 |
|---|---|---|
| 011 | ÷64 | ÷32 |
| 100 | ÷256 | ÷64 |
| 101 | ÷1024 | ÷128 |
| 110 | Ext. falling | ÷256 |
| 111 | Ext. rising | ÷1024 |
La différence principale réside dans les options de prescaler. Timer2 permet des granularités intermédiaires (÷32, ÷128) utiles pour certaines applications audio ou RTC.
millis()/delay()).En dehors des prescalers, la programmation de Timer2 est identique à Timer0 : mêmes modes, même logique de flag, mêmes formules de calcul.
Le Timer1 est le seul timer 16 bits de l'ATmega328. Il compte de 0 à 65535 (0xFFFF).
Il possède 3 registres de contrôle (TCCR1A, TCCR1B, TCCR1C) et des registres 16 bits divisés en deux octets H/L :
| Registre | Composantes | Rôle |
|---|---|---|
| TCNT1 | TCNT1H + TCNT1L | Compteur 16 bits |
| OCR1A | OCR1AH + OCR1AL | Comparaison A |
| OCR1B | OCR1BH + OCR1BL | Comparaison B |
| ICR1 | ICR1H + ICR1L | Capture d'entrée (ICP1) |
| TIFR1 | bit 0=TOV1, bit 1=OCF1A, bit 2=OCF1B, bit 5=ICF1 | Flags |
STS (store) et LDS (load) au lieu de OUT / IN.
Timer1 supporte 16 modes de fonctionnement définis par WGM13:WGM10 (4 bits répartis sur TCCR1A et TCCR1B).
| Mode | WGM13 | WGM12 | WGM11 | WGM10 | Description |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | Normal (TOP=0xFFFF) |
| 4 | 0 | 1 | 0 | 0 | CTC (TOP=OCR1A) |
| 8 | 1 | 0 | 0 | 0 | Phase&Freq correct PWM |
| 12 | 1 | 1 | 0 | 0 | CTC (TOP=ICR1) |
| 14 | 1 | 1 | 1 | 0 | Fast PWM (TOP=ICR1) |
| 15 | 1 | 1 | 1 | 1 | Fast PWM (TOP=OCR1A) |
TCCR1B = 0x09 = WGM12 | CS10 (mode CTC, prescaler 1).
L'ATmega328 ne dispose que d'un bus de données 8 bits. Pour lire/écrire un registre 16 bits (TCNT1, OCR1A…), le matériel utilise un registre TEMP interne transparent au programmeur.
// Écriture TCNT1 = 0xF353
TCNT1H = 0xF3; // → TEMP = 0xF3
TCNT1L = 0x53; // → écriture atomique
// Lecture TCNT1
uint8_t lo = TCNT1L; // → TEMP = TCNT1H gelé
uint8_t hi = TCNT1H; // → lu depuis TEMP
Générer un signal carré de période 2 ms (toggle toutes les 1 ms) sur PB5 avec Timer1 en mode Normal.
; TCNT1H doit être écrit avant TCNT1L
LDI R20, 0xD8
STS TCNT1H, R20 ; TEMP = 0xD8
LDI R20, 0xF0
STS TCNT1L, R20 ; écriture atomique
LDI R20, 0x00
STS TCCR1A, R20 ; mode normal
LDI R20, 0x01
STS TCCR1B, R20 ; CS = 1 (clk/1)
Alternative C plus élégante : TCNT1 = -10000; (le compilateur gère les octets H et L automatiquement).
Même délai de 1 ms, mais avec le mode CTC du Timer1.
// C — sous-routine delay1ms
void delay1ms() {
TCNT1 = 0;
OCR1A = 10000 - 1;
TCCR1A = 0;
TCCR1B = 0x09; // CTC, clk/1
while((TIFR1 & (1<<OCF1A)) == 0) {}
TCCR1B = 0;
TIFR1 = 1<<OCF1A;
}
| # | Question | Réponse |
|---|---|---|
| 1 | Combien de timers dans l'ATmega32 ? | 3 |
| 2 | Timer0 est-il 16 bits ? | Faux — 8 bits |
| 3 | Timer1 est-il 16 bits ? | Vrai |
| 4 | TCCR0 adressable au bit ? | Faux — adressable par octet |
| 5 | Mode Normal : débordement de __ à __ ? | $FFFF/$FF → $0000/$00 |
| 6 | Mode CTC : débordement quand ? | Quand TCNT = OCR1A |
| 7 | TCNT1 pour 5 ms, XTAL = 8 MHz ? | 65536 − 40000 = 25536 = 0x63C0 TCNT1H=0x63, TCNT1L=0xC0 |
| 8 | TCNT0 pour 20 µs, XTAL = 1 MHz ? | 256 − 20 = 236 = 0xEC |
En mode compteur, le timer reçoit son signal d'horloge non plus de l'oscillateur interne mais d'une source externe : la broche T0 (PD4) pour Timer0 ou T1 (PD5) pour Timer1.
Sur l'ATmega328 (28 broches), les entrées d'horloge externe se trouvent sur :
| Timer | Broche | Numéro port | Broche Arduino |
|---|---|---|---|
| Timer0 | T0 = PD4 | Pin 6 | D4 |
| Timer1 | T1 = PD5 | Pin 11 | D5 |
DDRx.bit = 0) avant de les utiliser comme source d'horloge. Le timer accepte des signaux de fréquence inférieure à f_XTAL/2 (critère de Nyquist).
Le prescaler peut également être utilisé en mode compteur : T0 → ÷8 → Timer0. Cela permettrait de ne compter qu'un événement sur 8.
Programme : compter les impulsions sur T0 (front descendant) et afficher TCNT0 sur PORTC.
TCCR0B = 0x06 → CS02=1, CS01=1, CS00=0 → horloge externe, front descendant.
; Assembleur
CBI DDRD, 4 ; T0 (PD4) = entrée
LDI R20, 0xFF
OUT DDRC, R20 ; PORTC = sortie
LDI R20, 0x00
OUT TCCR0A, R20 ; mode normal
LDI R20, 0x06
OUT TCCR0B, R20 ; ext. clock, falling edge
AGAIN:
IN R20, TCNT0
OUT PORTC, R20 ; afficher la valeur
RJMP AGAIN
// C
DDRC = 0xFF;
TCCR0A = 0;
TCCR0B = 0x06; // Count on falling edge
while (1) { PORTC = TCNT0; }
Programme : compteur1 en mode CTC, broche T1 (PD5), mettre PC0 à 1 toutes les 100 impulsions.
; Assembleur
CBI DDRD, 5 ; T1 = entrée
SBI DDRC, 0 ; PC0 = sortie
LDI R20, 0
STS OCR1AH, R20 ; OCR1A high = 0
LDI R20, 99
STS OCR1AL, R20 ; OCR1A = 99
LDI R20, 0x00
STS TCCR1A, R20
LDI R20, 0x0E
STS TCCR1B, R20 ; CTC + ext falling
// C
TCCR1A = 0;
TCCR1B = 0x0E;
OCR1A = 99;
while (1) {
while((TIFR1 & (1<<OCF1A)) == 0);
TIFR1 = (1<<OCF1A);
PORTC |= (1<<0);
PORTC &= ~(1<<0);
}
| # | Question | Réponse |
|---|---|---|
| 1 | Source d'horloge si CS02:00 = 6 ? | Oscillateur externe, front descendant (broche Tn) |
| 2 | Broche d'entrée pour Compteur0 ? | PD4 (T0) — pin 6 du 28-pin |
| 3 | Pour utiliser PB1 comme horloge de Timer1 ? | Configurer DDRB.0 = 0 (entrée) — T1 est sur PD5, pas PB1 ! |
| 4 | Peut-on choisir front montant ou descendant ? | Oui — CS=110 (falling) ou CS=111 (rising) |
| Critère | Mode Normal | Mode CTC |
|---|---|---|
| Délai répétable | Manuel (recharger TCNT) | Automatique |
| Précision | Légère dérive possible | Très précis |
| Complexité | Simple | Légèrement plus complexe |
| PWM possible | Non | Oui (avec pin OC0A) |
| Besoin | Timer recommandé |
|---|---|
| Délai court (< 25 µs à 10 MHz) | Timer0 ou Timer2 (8 bits) |
| Délai moyen (ms) | Timer1 (16 bits) ou Timer0 + prescaler |
| Délai long (dizaines de ms) | Timer1 + prescaler 1024 |
| Comptage d'événements (T0) | Timer0 en mode compteur |
| Comptage d'événements (T1) | Timer1 en mode compteur |
| Application RTC | Timer2 asynchrone |