Micro Contrôleurs AVR - Le Timer 0 - Wikiversité
Micro Contrôleurs AVR - Le Timer 0 - Wikiversité
Présentation du timer 0
Le timer 0 est un compteur sur 8 bits qui peut être commandé par l'horloge du processeur
(quartz) ou par un bit externe. Nous allons présenter de manière graphique le fonctionnement du
timer 0.
Le cœur du timer 0 est un compteur sur 8 bits (c'est précisé dans le dessin) en haut à gauche de
la figure appelé TCNT0. Comme tout compteur il nécessite une horloge. Celle-ci peut être
soit l'horloge du processeur (quartz externe ou horloge interne) qui est traitée par un diviseur.
Ce diviseur est appelé prescaler en anglais et on le nommera ainsi dans la suite.
Un bit d'un registre TIFR est appelé TOV0 (Timer overflow 0 = débordement du timer 0). Ce bit
est automatiquement positionné à 1 lors du débordement du timer 0, c'est-à-dire pour un
passage de 0xFF à 0x00.
Le registre TCCR0 gère le prescaler à l'aide de trois bits CS02, CS01, CS00. La suite de ce qui est
marqué dans le dessin correspond à la suite binaire sur ces trois bits. Par exemple vous en
déduisez qu'une division par 1024 est réalisée par le nombre binaire 101...
Depuis la version de l'ATMega8, le timer 0 même s'il est resté sur 8 bits s'est progressivement
étoffé de fonctionnalités additionnelles.
Une comparaison rapide entre les deux documentations vous montre que le registre TIFR de
l'ATMega8 s'est transformé en registre TIFR0 et que le registre TCCR0 s'est transformé en
TCCR0B. Il y a eu d'autres modifications qui seront présentées au fur et à mesure.
L'optimisation d'un algorithme en vitesse (ou en taille) est très importante dans les systèmes
embarqués réalisés par des micro-contrôleurs. Une recherche d'algorithmes sur Internet vous
donnera des résultats qu’il vous faudra évaluer. Par exemple, le site : convert base (http://www.pi
clist.com/techref/language/ccpp/convertbase.htm) vous propose un algorithme de division
par 10 que voici :
unsigned int A;
unsigned int Q; /* the quotient */
Q = ((A >> 1) + A) >> 1; /* Q = A*0.11 */
Q = ((Q >> 4) + Q) ; /* Q = A*0.110011 */
Q = ((Q >> 8) + Q) >> 3; /* Q = A*0.00011001100110011 */
/* either Q = A/10 or Q+1 = A/10 for all A < 534,890 */
Exercice 1
2°) Les LEDs d'un shield maison sont couplés à l'arduino MEGA2560. Une lecture de son schéma
fait apparaître la correspondance :
Numéro f5 f4 f3 f2 f1 f0 p1 p0
Couleur r o v r o v v r
Arduino Pin 13 12 11 10 9 8 7 6
Port Arduino UNO PB5 PB4 PB3 PB2 PB1 PB0 PD7 PD6
Port Arduino LEONARDO PC7 PD6 PB7 PB6 PB5 PB4 PE6 PD7
Port Arduino MEGA2560 PB7 PB6 PB5 PB4 PH6 PH5 PH4 PH3
Écrire un sous-programme capable d'afficher un nombre sur 8 bits sur les LEDs
3°) Écrire un programme complet qui mesure le temps d'exécution du sous programme de
division par 10, puis modifier le programme pour qu’il puisse comparer avec une division par 10
normale.
Solution
1°)
2°)
int main(void){
unsigned int res,i;
unsigned char temps;
DDRH = 0x78; // 4 sorties pour H
DDRB = 0xF0; // 4 sorties pour B
L'ajout dans la boucle de "PORTC = res;" a été essentielle pour éviter la suppression de la boucle
avec juste un warning comme quoi res n'était jamais utilisé ! C'est normal c’est TCNTO qui
m'intéresse pas res !
par
Ceci est conforme à ce que j'attendais contrairement à ce qui se passait avec le PIC et mikroC.
Nous devons savoir à ce niveau, que tout débordement du timer0 (passage de 0xFF à 0x00)
entraîne le positionnement du flag TOV0, bit b0 du registre TIFR0. Vous pouvez donc utiliser ce
flag pour déterminer si vous avez eu débordement du timer0, ou, en d’autres termes, si le temps
programmé est écoulé. Cette méthode à l’inconvénient de vous faire perdre du temps inutilement
dans une boucle d'attente.
Petit exemple d'attente passive :
Le drapeau TOV0 ne se remet pas tout seul à 0... et il faut écrire un 1 pour le remettre à 0 !!!
Exercice 2
Question 1
int main(void){
// initialisation du timer division par 8
TCCR0B = 0x02; // prescaler 8 , entrée sur quartz
TCNT0 = 0x00; // tmr0 : début du comptage dans 2 cycles
// bit RB0 du PORTB en sortie
DDRB |= 0x01; //RB0 as output
while(1) {
while ((TIFR & (1<<TOV0)) == 0);
// ce qui est fait ici est fait tous les 256
comptages de TCNT0
PORTB ^= 0x01; // on bascule avec ou exclusif
TIFR0 |= 0x01; // clr TOV0 with 1 :
obligatoire !!!
// TIFR0 |= (1<<TOV0); // fait le même chose
}
return 0;
}
Solution
Le facteur 2 apparaissant dans cette formule est lié au fait que "PORTB ^= 0x01;" fait basculer le
bit et qu'ainsi il faut deux basculements pour faire une période. Il est bon de retenir que toute
technique de basculement nécessite la création d'une fréquence double. Ainsi pour créer 1 kHz il
vous faudra 2 kHz ...
Question 2
Écrire en langage C un programme qui fait la même chose que le programme ci-dessus : initialise
le timer0, efface le flag et attend à l'aide d'une boucle le positionnement de ce dernier mais 100
incrémentations seulement. On vous demande aussi d’utiliser au mieux les déclarations du
fichier d'inclusion avr/io.h :
/* TIFR0 */
#define OCF2 7
#define TOV2 6
#define ICF1 5
#define OCF1A 4
#define OCF1B 3
#define TOV1 2
/* bit 1 reserved (OCF0?) */
#define TOV0 0
//***** plein de lignes ici mais cachées *****
/* TCCR0 */
/* bits 7-3 reserved */
#define CS02 2
#define CS01 1
#define CS00 0
L'utilisation des valeurs prédéfinies permet de rendre vos programmes un peu plus lisibles.
Solution
#include <avr/io.h>
void main(void){
// initialisation du timer division par 8
TCCR0B |= _BV(CS01); // prescaler 8 , entrée sur quartz
TCCR0B &= ~(_BV(CS02) | _BV(CS00)); // pour être sûr
TCNT0 = 255-99; // tmr0 : début du comptage dans 2
cycles
// bit RB0 du PORTB en sortie
DDRB |= 0x01; //RB0 as output
while(1) {
TIFR0 |= _BV(TOV0); // clr TOV0 with 1
while (TIFR0 & _BV(TOV0)) == 0);
// ce qui est fait ici est fait tous les 256
comptages de TCNT0
PORTB ^= _BV(0); // on bascule avec ou exclusif
//TIFR0 &= ~_BV(TOV0); // reset the overflow flag
TCNT0 = 255-99; // remplace le reset de l'overflow
}
}
Question 3
Écrire le programme.
Solution
On va en fait générer 2kHz et changer sans arrêt un bit B0 du PORTB. 4 MHz / 2kHz=2000. Si l’on
divise par 8 on peut rester en 8 bits avec comme valeur 250 à compter donc une initialisation à
256-250=6. Ceci n'est qu'une valeur théorique qui dépend du compilateur. Il faut donc faire des
essais pour trouver la valeur exacte. Une autre technique pour être quasi indépendant du
compilateur c’est de remplacer :
TCNT0 = 6;
par
TCNT0 = TCNT0+6;
qui dépend moins du temps que l’on a mis pour réaliser les instructions.
#include <avr/io.h>
void main(void){
// initialisation du timer division par 8
TCCR0B |= _BV(CS01); // prescaler 8 , entrée sur quartz
TCCR0B &= ~(_BV(CS02) | _BV(CS00)); // pour être sûr
TCNT0 = 6; // tmr0 : début du comptage dans 2 cycles
// bit RB0 du PORTB en sortie
DDRB |= 0x01; //RB0 as output
while(1) {
TIFR0 |= _BV(TOV0); // reset the overflow flag
while (TIFR0 & _BV(TOV0)) == 0);
TCNT0 += 6; // remplace le reset de l'overflow
// ce qui est fait ici est fait tous les 250
comptages de TCNT0
PORTB ^= _BV(0); // on bascule avec ou exclusif
}
}
Question 4
Solution
La solution de la question 3 nous a montré qu’il fallait compter 250 pour sortie à 0 et 250 pour
sortie à 1. Ici il faut donc compter 125 pour la valeur 1 et 250+125=375 pour la valeur 0. J'espère
que tout le monde a vu le problème ! 375 ne tient plus dans 8 bits. Il faut donc encore diviser
l'horloge par 2. Mais cela n’est pas possible : on ne peut que diviser par 8 pour avoir le prescaler
à 64.
void main(void){
// initialisation du timer division par 64
TCCR0B |= (_BV(CS01) | _BV(CS00)); // prescaler 64 ,
entrée sur quartz
TCCR0B |= ~_BV(CS02); // pour être sûr
//TCNT0 = 6; // tmr0 : début du comptage dans 2 cycles
// bit RB0 du PORTB en sortie
DDRB |= 0x01; //RB0 as output
PORTB |= _BV(0);// pour être sûr de la façon dont on
démarre
while(1) {
//TIFR0 &= ~_BV(TOV0); // reset the overflow flag
TCNT0 += 240; // 256-16 : remplace le reset de
l'overflow
while (TIFR0 & _BV(TOV0)) == 0);
// ce qui est fait ici est fait tous les 256
comptages de TCNT0
PORTB ^= _BV(0); // on bascule avec ou exclusif
//TIFR0 &= ~_BV(TOV0); // reset the overflow flag
TCNT0 += 209; // 256-47remplace le reset de
l'overflow
while (TIFR0 & _BV(TOV0)) == 0);
PORTB ^= _BV(0); // on bascule avec ou exclusif
}
}
Nous testerons ce programme (un jour je n'ai pas de scope sous la main chez moi) sur une
Arduino MEGA2560 avec une horloge à 16MHz. Si l’on veut rester à 1kHz il nous faut utiliser le
programme suivant :
#include <avr/io.h>
void main(void){
// initialisation du timer division par 64
TCCR0B |= (_BV(CS01) | _BV(CS00)); // prescaler 64 ,
entrée sur quartz
TCCR0B |= ~_BV(CS02); // pour être sûr
//TCNT0 = 6; // tmr0 : début du comptage dans 2 cycles
// bit RB0 du PORTB en sortie
DDRB |= 0x01; //RB0 as output
PORTB |= _BV(0);// pour être sûr de la façon dont on
démarre
while(1) {
//TIFR0 &= ~_BV(TOV0); // reset the overflow flag
TCNT0 += 192; // 256-(16*4) : remplace le reset de
l'overflow
while (TIFR0 & _BV(TOV0)) == 0);
// ce qui est fait ici est fait tous les 256
comptages de TCNT0
PORTB ^= _BV(0); // on bascule avec ou exclusif
//TIFR0 &= ~_BV(TOV0); // reset the overflow flag
TCNT0 += 68; // 256-(47*4)remplace le reset de
l'overflow
while (TIFR0 & _BV(TOV0)) == 0);
PORTB ^= _BV(0); // on bascule avec ou exclusif
}
}
Il y a mieux à faire avec les AVRs, utiliser le module CCP (détaillé plus loin).
Exercice 3
Mettre en œuvre le timer0 sur le MEGA2560 avec un prescaler à 1024 et une attente active du bit
T0V0 du registre TIFR0. On basculera le bit b4 du PORTB qui est relié à une LED. Quelle est la
fréquence de basculement du bit b4 si la fréquence du quartz est 16 MHz ?
Solution de l'exercice 3
Timer 0 et interruption
Documentation
La mise à zéro du bit TOV0 est complètement automatique dans les ATMega contrairement aux
autres architectures connues par nous. Ceci doit être certainement lié au fait que les
interruptions sont vectorisées et qu'ainsi il est possible pour le processeur de savoir à tout
moment l'événement qui a déclenché l'interruption.
Pour comprendre cette figure, il suffit de se rappeler qu'un front montant dans l'ellipse rouge
réalisera cette-interruption. Ce front montant est réalisé par la mise à un, par le matériel, de
TOV0. En C cette interruption est désignée par "TIMER0_OVF_vect". Si vous avez compris que ce
n’est pas le logiciel qui positionnera le bit TOV0 mais le matériel, alors vous déduisez que pour
réaliser une interruption il suffit de
mettre à 1 le bit TOIE0 du registre TIMSK pour l'ATMega8. Pour l'ATMega328 ce registre
s’appelle TIMSK0.
mettre à 1 le bit I du registre SREG. Ceci se réalise par l'instruction "sei();" en C et "interrupts();"
avec l’Arduino.
Mise en œuvre
Si la fréquence de la platine MEGA2560 est de 16 MHz et que l’on utilise un préscalaire de 1024
et un basculement nous avons déjà trouvé une fréquence de 32 Hz dans un exercice précédent.
Ainsi une division par 32 doit donner 1 Hz. On le fait par exemple par interruption, toujours avec
notre MEGA2560 :
void main(){
// IT Timer0 Over Flow Active
TIMSK0=(1<<TOIE0);
// Prescaler 1024 (Clock/1024)
TCCR0B = (1<<CS02) | (1<<CS00);
//Configuration PORTB.4 en sortie
DDRB |= (1<<DDB4);
PORTB &= ~(1<<PB4); // PORTB.4 <-0
//activation des IT (SREG.7=1)
sei();
// SREG |= 0x80; // équivalent à sei()
while(1);
}
Exercice 4
Un ATMega8 est enfoui dans un FPGA (Voir Embarquer un ATMega8 dans un FPGA). Sa seule
particularité est de fonctionner à 25 MHz contre 20 MHz de fréquence maximale d'horloge pour
celui du commerce. Il exécute le programme suivant écrit pour le compilateur gcc :
/******************************************************************
***
Includes
*******************************************************************
****/
#include <avr/io.h>
#include <stdbool.h>
#include <avr/interrupt.h>
volatile unsigned char nb=0,vPORTB=1;
/******************************************************************
***
Interrupt Routine
*******************************************************************
***/
// timer0 overflow
ISR(TIMER0_OVF_vect) {
nb++;
if (!(nb % 16))
vPORTB = (vPORTB << 1);
if (vPORTB == 0x00) vPORTB = 0x01;
PORTB = vPORTB;
}
/******************************************************************
****
Main
*******************************************************************
***/
int main( void ) {
// Configure PORTB as output
DDRB = 0xFF;
PORTB = 0x01;
// enable timer overflow interrupt for both Timer0
TIMSK=(1<<TOIE0);
// set timer0 counter initial value to 0
TCNT0=0x00;
// start timer0 with 1024 prescaler
TCCR0 = (1<<CS02) | (1<<CS00);
// enable interrupts
sei();
while(true) { // grace a stdbool.h
}
return 0;
}
Remarque
1°) Calculer si le chenillard réalisé par ce programme est visible à l'œil humain (fréquence de
changement de position des LEDs inférieure à 20 Hz).
2°) Comment peut-on écrire l'instruction "if (!(nb % 16))" pour plus d'efficacité.
3°) Quelle est la suite des états (LEDs allumées) réalisée par ce programme.
Pour les deux questions suivantes ce ne sera pas la routine d'interruption qui sera chargée de
mettre le PORTB.
4°) Le programme suivant est donné et tourne dans un ATMega8 cadencé avec un quartz de 4
MHz.
#include <avr/interrupt.h>
volatile unsigned int cnt;
ISR(TIMER0_OVF_vect) {
cnt++; // increment counter
TCNT0 = 96;
}
}
int main() {
TCCR0 = (1<<CS01) | (1<<CS00); // Assign prescaler to TCNT0
DDRB = 0xFF; // PORTB is output
PORTB = 0xFF; // Initialize PORTB
TCNT0 = 96; // Timer0 initial value
TIMSK=(1<<TOIE0); // Enable TMRO interrupt
sei();
cnt = 0; // Initialize cnt
do {
if (cnt >= 400) {
PORTB = ~PORTB; // Toggle PORTB LEDs
cnt = 0; // Reset cnt
}
} while(1);
return 0;
}
5°) Modifier le programme principal pour réaliser un chenillard d'une LED se déplaçant vers les
poids faibles en gardant le traitement en dehors de l'interruption.
Solution
1°) N'oubliez pas la division par 16 qui est réalisée avec le if (!(nb % 16)) dans l'interruption.
2°) "if (!(nb % 16))" est une façon pas très efficace de calculer le reste de la division par 16.
J'ignore la technique utilisé par le compilateur, mais ce calcul est forcément long puisqu’il n'y a
pas d'instruction de division sur l'ATMega8.
Comme la division se fait par 16 qui est une puissance de deux, on peut utiliser un masque pour
faire ce calcul bien plus rapidement :
// langage C
if (!(nb & 0x0F)) // idem à if (!(nb % 16)) mais plus rapide
3°) Décalage tout simple d'une LED vers les poids forts. L'utilisation d'une variable "vPORTB"
complique un peu la lecture du programme ! Ceci est lié au fait que dans le FPGA on ne peut pas
lire PORTB (ce que l’on peut faire dans la vraie vie.
4°)
5°)
#include <avr/io.h>
#include <avr/interrupt.h>
Exercice 5
Une partie matérielle est constituée de deux afficheurs sept segments multiplexés. Les sept
segments sont commandés par le PORTC, tandis que les commandes d'affichages sont réalisée
par les bits b0 et b1 du PORTB. Un schéma de principe est donné ci-après.
Comment utiliser deux afficheurs multiplexés
1°) A l'aide de la documentation calculer les valeurs dans un tableau "unsigned char SEGMENT[]
= {0x3F,...};" pour un affichage des chiffres de 0 à 9.
3°) Réaliser le programme main() responsable de l'initialisation de l'interruption qui doit avoir lieu
toutes les 10ms (avec un quartz de 4MHz) et qui compte de 00 à 99 toutes les secondes environ
(avec un "_delay_ms(1000);")
4°) Réaliser enfin l'interruption qui affichera tantôt les dizaines, tantôt les unités.
Un exemple de réalisation d'afficheur sept segments sur deux digits peut être trouvé ICI (http://w
ww.iut-troyes.univ-reims.fr/wikigeii/index.php/Cours:Shieldinfo) . Nous allons l’utiliser pour un
exercice similaire à celui de la section précédente.
Description
Les 2 afficheurs ne peuvent pas être utilisés simultanément. L'état de la sortie mux (arduino port
4 ou PD4) permet de sélectionner l'un ou l'autre. En allumant successivement l'un puis l'autre
rapidement, on a l'illusion qu’ils sont tous 2 allumés.
Les segments des afficheurs sont câblés de façon analogue comme décrit ci-dessous :
Segment pt g f e d c b a
Arduino Pin 11 9 10 8 7 6 12 13
Port UNO PB3 PB1 PB2 PB0 PD7 PD6 PB4 PB5
Même si l’Arduino possède son propre chapitre dans ce livre, nous vous proposons ici du code le
concernant
Utiliser le langage C pour utiliser le Shield ci-dessus est pas simple. L'idée est de faire un tableau
qui considère que tout est sur un PORT et de gérer l'affichage qui gère correctement dans un
sous-programme.
#include<util/delay.h>
int main(void){
unsigned char transcod[16]=
{0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0
x5E,0x79,0x71},cmpt=0;
DDRD = 0xC0; // 2 sorties pour D
DDRB = 0x3F; // 6 sorties pour B
while(1) {
affiche7segs(transcod[cmpt]);
_delay_ms(1000);
cmpt++;
if (cmpt > 15) cmpt=0;
} // while(1)
return 0;
}
Pour utiliser les deux digits, le programme C suivant fonctionne correctement sans TIMER.
#include<util/delay.h>
int main(void){
unsigned char transcod[16]=
{0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0
x5E,0x79,0x71},cmpt=0,i;
DDRD = 0xD0; // 2 sorties pour D
DDRB = 0x3F; // 6 sorties pour B
while(1) {
for (i=0;i<50;i++) {
afficheDiz(transcod[cmpt>>4]);
_delay_ms(10);
afficheUnit(transcod[cmpt&0x0F]);
_delay_ms(10);
}
cmpt++;
} // while(1)
return 0;
}
Exercice 6
1°) Pour compliquer un peu on utilisera le MEGA2560 pour lequel la correspondance entre les
numéros Arduino et les PORTs est :
Arduino Pin 13 12 11 10 9 8 7 6
Port MEGA2560 PB7 PB6 PB5 PB4 PH6 PH5 PH4 PH3
2°) Écrire un sous-programme capable de prendre un octet et de l'afficher avec la convention "a"
poids faible.
3°) Écrire le programme qui réalise une interruption à 100 Hz et affiche tantôt sur l'afficheur des
poids faibles tantôt sur l'afficheur des poids forts.
Solution exercice 6
La question 1°) n’est pas cachée car nous ne savons pas mettre un tableau dans le bandeau
solution qui se déroule.
7 segments Pin pt g f e d c b a
Port MEGA2560 PB5 PH6 PB4 PH5 PH4 PH3 PB6 PB7
Solution
2°) Le code ci-dessous ne serait pas le même avec une carte Arduino UNO !
3°) La documentation du schield dit que pinMUX = 4. La documentation Arduino MEGA2560 (http
s://arduino.cc/en/Hacking/PinMapping2560) donne 4 en PG5. C'est donc PG5 qui permet de
commuter les afficheurs.
Pour obtenir 100 Hz à partir de 16MHz, il faut diviser par 1024 puis par 156. Cela peut se faire
avec le programme suivant :
int main(void){
DDRH = 0x78; // 4 sorties pour H
DDRB = 0xF0; // 4 sorties pour B
DDRG = 0x20; // PG5 en sortie
//** gestion du timer0
// enable timer overflow interrupt for both Timer0
TIMSK0=(1<<TOIE0);
// set timer0 counter initial value to 0
TCNT0=0x00;
// start timer0 with 1024 prescaler
TCCR0B = (1<<CS02) | (1<<CS00);
// enable interrupts
sei();
while(1) {
// réalisation d'un compteur BCD
cmpt ++;
if ((cmpt & 0x0F)>9) cmpt+=6;
if ((cmpt & 0xF0)>0x90) cmpt+=0x60;
// on attend
_delay_ms(1000);
} // while(1)
return 0;
}
Timer 0 et comparaison
Cette fonctionnalité n'existait pas dans l'ATMega8. Elle a été ajoutée pour gérer le PWM
(Modulation de largeur d'impulsion = MLI et Pulse Width Modulation en anglais). Ce PWM sert
essentiellement à commander des moteurs de Robots mais éventuellement à moduler des
éclairages.
Les fonctionnalités ajoutées sont difficiles à appréhender. En effet le mode comparaison permet
de gérer différents modes pour pouvoir réaliser toute une gamme de signaux du carré au PWM.
Ceci complique un peu la gestion pour le programmeur. La bonne nouvelle c’est que les autres
timers ont un fonctionnement un peu similaire.
Documentation de la comparaison
Les différents modes de comparaison sont choisis à l'aide des bits WGM02, WGM01 et WGM00.
Ces choix sont conformes au tableau suivant.
4 1 0 0 Reservé - - -
6 1 1 0 Reservé - - -
Remarque
* MAX = 0xFF
BOTTOM = 0x00
CTC : Clear Timer on Compare match = RAZ du timer quand
comparaison
Pour chacun des modes de ce tableau ci-dessus, les bits COM0A1 et COM0A0 auront un
fonctionnement différent. Ces bits sont destinés à gérer les formes d'onde du signal de sortie.
Comparaison simple
Nous présentons la comparaison simple ici. Elle est essentiellement utilisée avec le mode
"Bascule OC0A sur la comparaison" ci-dessous quand l'objectif est de réaliser un signal de
fréquence déterminé sans utiliser de logiciel (sauf pour tout initialiser bien sûr). Le mode de
fonctionnement le plus adapté du timer dans ce cas est appelé CTC (Clear Timer on Compare
match). Ce mode n’est pas choisi avec le tableau ci-dessous mais avec le tableau précédent.
Sans interruption, seul le mode "basculement du bit OC0A" a un intérêt mais il impose d’utiliser
ce bit (qui est le bit b6 du PORTD).
L'interruption de comparaison peut servir à utiliser un bit de sortie quelconque. Elle n’est pas
documentée mais le sera à travers un exercice (Exercice 8).
Exercice 7
1°) Donner le squelette d'un programme qui utilise le mode CTC pour réaliser un signal de
période 8 ms sur le bit OC0A. La fréquence du quartz sera de 16MHz comme sur la carte
Arduino UNO.
2°) Dessiner sur un chronogramme le comptage du timer et le signal généré sur OC0A.
3°) Quelle est la fréquence la plus basse que l’on peut réaliser sur le bit OC0A ? Quel mode du
générateur de signal utilise-t-on ?
4°) Quelle est la fréquence la plus haute que l’on peut réaliser sur le bit OC0A ? Quel mode du
générateur de signal utilise-t-on ?
Solution
1°)
#include <avr/io.h>
int main(void){
// set OC0A as output
DDRD |= 0x40;
// Set the Timer Mode to CTC
TCCR0A |= (1 << WGM01);
// Set the value that you want to count to
OCR0A = 0xF9;
// Set OC0A toggle mode
TCCR0A |= (1 << COM0A0);
// set prescaler to 256 and start the timer
TCCR0B |= (1 << CS02);
while (1)
{
//main loop
}
return 0;
}
Le premier 2 qui apparaît au dénominateur est lié à la valeur minimale de OCR0A soit 1. Ainsi,
pour faire une période il faut compter de 0 à 1 puis que l’on repasse à 0 (soit deux comptages).
Le deuxième est lié au mode basculement.
Compléter l'exemple ci-dessous trouvé sur Internet pour réaliser un clignotement d'un Hertz sur
une LED connectée sur le bit b4 du PORTB.
#include <avr/io.h>
#include <avr/interrupt.h>
int main(void){
// Set the Timer Mode to CTC
TCCR0A |= (1 << WGM01);
// Set the value that you want to count to
OCR0A = 0xF9;
TIMSK0 |= (1 << OCIE0A); //Set the ISR COMPA vect
sei(); //enable interrupts
// set prescaler to 256 and start the timer
TCCR0B |= (1 << CS02);
while (1)
{
//main loop
}
return 0;
}
int main(void){
// Set the Timer Mode to CTC
TCCR0A |= (1 << WGM01);
// Set the value that you want to count to
OCR0A = 0xF9;
TIMSK0 |= (1 << OCIE0A); //Set the ISR COMPA vect
sei(); //enable interrupts
// set prescaler to 256 and start the timer
TCCR0B |= (1 << CS02);
//Configuration PORTB.4 en sortie
DDRB |= (1<<DDB4);
PORTB &= ~(1<<PB4); // PORTB.4 <-0
while (1)
{
//main loop
}
return 0;
}
Les 4 ms annoncées sont très théoriques et ne tiennent pas compte du temps que le processeur
utilise pour réaliser l'interruption. Avant d'envisager de modifier ce code prenez un
fréquencemètre (un oscilloscope n’est pas simple à utiliser autour du Hertz).
Pour information, le mode PWM rapide est abordé dans un autre chapitre sur le robot miniQ.
Nous allons le décrire quand même ici : mieux vaut deux fois qu'une.
#include <avr/io.h>
#undef F_CPU
#define F_CPU 16000000UL
#include <util/delay.h>
int main(void){
// Set the Timer Mode to PWM fast
TCCR0A |= ((1 << WGM01) | (1<<WGM00));
// clear OC0A on compare match set OC0A at TOP
TCCR0A |= (1 << COM0A1);
//Configuration (PORTB.6 pour UNO)
DDRD |= ((1<<DDD6)|(1<<DDD5)|(1<<DDD4));
PORTD |= (1<<PD5);
PORTD &= ~(1<<PD4);
TCNT0 = 0x00;
// set prescaler to 1024 and start the timer
TCCR0B |= ((1 << CS02) | (1 << CS00));
while(1) {
// Set the value that you want to count to
OCR0A = 128;
//_delay_ms(300);
} // while(1)
return 0;
}
Ce programme connecté à une carte L298N est capable d'entraîner un moteur courant continu
d'un Robot. La carte de puissance en question nécessite de mettre un seul des deux bits (PD5 ou
PD4) à 1 pour tourner dans un sens. Pour inverser le sens, il faut inverser les bits PD5 et PD4. La
mise à 0 des deux bits PD4 et PD5 permet de freiner.
On désire changer l'intensité d'éclairage d'une LED à l'aide d'une PWM rapide. La valeur du
rapport cyclique varie entre 0 et 255 et sera systématiquement envoyée par la liaison série sous
forme de deux caractères 0,...,9,A,...,F.
1°) Un sous-programme sera donc chargé de lire ces deux caractères d’en vérifier la syntaxe.
Écrire ce sous-programme et le tester avec les afficheurs de l'exercice 6. On affichera "--" en cas
d'erreur de syntaxe.
Lisez le chapitre Les communications en tout genre pour vous aider sur la communication série.
Solution
Voici un programme qui fait l'acquisition de deux caractères sensés représenter un nombre
hexadécimal. Si un mauvais caractère est rentré un message est envoyé par la RS232 et sur les
afficheurs comme demandé !
#include <util/delay.h>
//#include <avr/interrupt.h>
void serialInit(void);
unsigned char serialCheckTxReady(void);
unsigned char serialCheckRxComplete(void);
void serialWrite(unsigned char DataOut);
unsigned char serialRead(void);
void usart_puts(char str[]);
void usart_puts_hexa(unsigned char val);
int main(void){
unsigned char ch,rapport_cyclique,erreur=0;
serialInit();
while(1) {
if (serialCheckRxComplete()) {
ch = UDR0;
rapport_cyclique = ch-'0';
if (rapport_cyclique > 9) rapport_cyclique -= 7;
if (rapport_cyclique > 15) {
usart_puts("Erreur Caractère non reconnu\n");
erreur=1;
} else {
rapport_cyclique <<= 4;
ch = serialRead();
ch -= '0';
if (ch > 9) ch -= 7;
if (ch > 15) {
usart_puts("Erreur Caractère non reconnu\n");
erreur = 1;
} else {
rapport_cyclique += ch;
usart_puts_hexa(rapport_cyclique);
} // else
} // if (!erreur) else
}
// on attend
//_delay_ms(300);
} // while(1)
return 0;
}
//*****************************************************************
*******
// function usart_puts()
// purpose: puts characters in first rs232 PORT
// arguments:
// corresponding string
// return:
// note:
//*****************************************************************
*******
void usart_puts(char str[]){
uint8_t i=0;
do {
serialWrite(str[i]);
i++;
} while(str[i]!=0);
}
//*****************************************************************
*******
// function usart_puts_hexa()
// purpose: puts number in hexadecimel in first rs232 PORT
// arguments:
// corresponding number
// return:
// note:
//*****************************************************************
*******
void usart_puts_hexa(unsigned char val){
int8_t digit=0;
char char_digit;
usart_puts("\n0X"); //debut hexadécimal du C
digit = (val >> 4) & 0x0F;
char_digit=digit+0x30;
if (char_digit>0x39) char_digit += 7;
serialWrite(char_digit);
digit = val & 0x0F;
char_digit=digit+0x30;
if (char_digit>0x39) char_digit += 7;
serialWrite(char_digit);
}
La vérification syntaxique est trop élémentaire, mais ce n’est pas sur ce point précis que l’on veut
travailler.
2°) Changer votre programme de test précédent pour qu’il réalise un rapport cyclique sur le bit
OC0A du PORTB.
Remarque
Solution
Voici un programme qui change le rapport cyclique en fonction d'une valeur entrée par la liaison
série :
#include <avr/io.h>
#undef F_CPU
#define F_CPU 16000000UL
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
#include <util/delay.h>
//#include <avr/interrupt.h>
void serialInit(void);
unsigned char serialCheckTxReady(void);
unsigned char serialCheckRxComplete(void);
void serialWrite(unsigned char DataOut);
unsigned char serialRead(void);
void usart_puts(char str[]);
void usart_puts_hexa(unsigned char val);
int main(void){
unsigned char ch,rapport_cyclique,erreur=0;
TCCR0A = 0x00;
// Set the Timer Mode to PWM fast
TCCR0A |= ((1 << WGM01) | (1<<WGM00));
// clear OC0A on compare match set OC0A at TOP
TCCR0A |= (1 << COM0A1);
// Set the value that you want to count to
OCR0A = 0x88; // rapport cyclique
// set prescaler to 1024 and start the timer
TCCR0B = 0x00;
TCCR0B |= ((1 << CS02) | (1 << CS00));
//Configuration PORTB.7 en sortie (PORTB.6 pour UNO)
DDRB |= (1<<DDB7);
TCNT0 = 0x00;
serialInit();
while(1) {
if (serialCheckRxComplete()) {
erreur=0;
ch = UDR0;
rapport_cyclique = ch-'0';
if (rapport_cyclique > 9) rapport_cyclique -= 7;
if (rapport_cyclique > 15) {
usart_puts("Erreur Caractère non reconnu\n");
erreur=1;
} else {
rapport_cyclique <<= 4;
ch = serialRead();
ch -= '0';
if (ch > 9) ch -= 7;
if (ch > 15) {
usart_puts("Erreur Caractère non reconnu\n");
erreur = 1;
} else {
rapport_cyclique += ch;
usart_puts_hexa(rapport_cyclique);
} // else
} // if (!erreur) else
//*****************************************************************
*******
// function usart_puts()
// purpose: puts characters in first rs232 PORT
// arguments:
// corresponding string
// return:
// note:
//*****************************************************************
*******
void usart_puts(char str[]){
uint8_t i=0;
do {
serialWrite(str[i]);
i++;
} while(str[i]!=0);
}
//*****************************************************************
*******
// function usart_puts_hexa()
// purpose: puts number in hexadecimel in first rs232 PORT
// arguments:
// corresponding number
// return:
// note:
//*****************************************************************
*******
void usart_puts_hexa(unsigned char val){
int8_t digit=0;
char char_digit;
usart_puts("\n0X"); //debut hexadécimal du C
digit = (val >> 4) & 0x0F;
char_digit=digit+0x30;
if (char_digit>0x39) char_digit += 7;
serialWrite(char_digit);
digit = val & 0x0F;
char_digit=digit+0x30;
if (char_digit>0x39) char_digit += 7;
serialWrite(char_digit);
}
Indications :
La fréquence de 50 Hz (période T=20 ms) n'est pas importante pour la commande des
servomoteurs.
Un calcul simple montre donc que OCR0A doit varier entre 255/10 et 255/20 ce qui donnera
donc 26 et 13.
On utilisera la fréquence minimale du Timer0.
OC0A est le bit PD6 du PORTD. Sur une carte Arduino, il s'agit de la broche -6
Solution
#include <avr/io.h>
#include <util/delay.h>
// Frequence 16MHz