Cours:TPS XR207 tpTimer : Différence entre versions

De troyesGEII
Aller à : navigation, rechercher
(Mélodie !)
(Mélodie !)
 
Ligne 380 : Ligne 380 :
  
  
ex :
+
ex :
 
<source lang=cpp>
 
<source lang=cpp>
 
  // au clair de la lune complet
 
  // au clair de la lune complet

Version actuelle datée du 6 mars 2023 à 16:30

Nous utilisons pour ce TP une carte arduino UNO, dont le µcontroleur est un aTmega328p (16MHZ). Cependant, nous n'utiliserons pas les librairies arduino.


*****************************************
*****************************************
*****************************************
*** feuille de calcul pour les TIMERS ***
*****************************************
*****************************************
*****************************************


Retour à la liste des Tps

Éléments de correction

Nous allons dans ce TP nous pencher sur l'utilisation des timers, notamment pour générer un signal, ceci en utilisant le moins possible les ressources du µcontrôleur.

ArduinoPinout.png

Matériel

Nous utiliserons :

  • 1 carte arduino UNO
  • 1 oscilloscope
  • 1 sonde de tension
  • 1 buzzer (à insérer directement sur les connecteurs arduino)
  • 2 fils M-M

Comment générer un signal périodique

Nous allons dans cette partie comparer les 2 principales méthodes pour générer un signal périodique.

Dans toute cette partie, nous considérerons une sortie périodique telle que :

  • le signal est carré (2 états '0' ou '1')
  • de fréquence 1kHz
  • de rapport cyclique 1/2
  • sortie sur la patte PB3

avec une attente .... méthode basique !

Commençons tout simplement par utiliser une pause.

Il suffit alors de configurer correctement la valeur d'attente.

Remarquons que pour plus de précision, il est possible d'utiliser la fonction _delay_us(xxx), dont le nom est suffisamment explicite pour se passer d'une description !

Question.jpg Écrire un programme générant le signal souhaité, et le visualiser avec un oscilloscope.


#include <avr/io.h>
#include <util/delay.h>

int main()
{
   // configuration e/s

   while(1)                // boucle
   {
      // modification de la sortie ... ça doit être un ou exclusif !

      // attente
      _dela.....
   }
}

Remarque : Comme expliqué précédemment, cette méthode fonctionne mais entraîne une occupation à 100% du processeur, qui ne peut donc réaliser que cette action !

l'outil adapté : le timer !

Passons aux choses sérieuses en utilisant un timer (en l’occurrence le timer2 d'un atmega328p), et donnons tout d'abord les 2 figures qui nous permettront de le configurer facilement :

Documentation simple du Timer 2 (8 bits)
La comparaison avec le Timer 2 (8 bits)

Remarquons tout de suite qu'il s'agit d'un compteur 8 bits, c'est à dire qu'il générera un débordement (overflow) lors du passage de 255 à 0.


Paramètres du timer

Comme pour toute utilisation du timer, il est nécessaire de choisir :

  • la valeur du prédiviseur, et donc la fréquence de comptage : fcpt=fcpu/p
  • le nombre de fois que le compteur s'incrémentera, n (valeur de OCR2A pour le timer2), avant d'être réinitialisé

On rappelle que sur les cartes arduino UNO, fcpu=16MHz

Au besoin, utiliser la feuille de calcul dont le lien est en début d'énoncé !!

La figure ci après doit vous permettre de trouver facilement la relation entre les différentes valeurs et ainsi déterminer les valeurs n et p adaptées à notre problème.

ChronogrammeTimer.png

Question.jpg Faire un choix pour n et p

Mode comparaison avec interruption

Nous pouvons désormais écrire le programme utilisant le timer2, qui devra utiliser le mode comparaison.

Autrement dit, on configure le timer pour que sa valeur soit réinitialisée à 0 lorsqu'il atteint la valeur OCR2A (ou si vous préférez la valeur n que vous avez calculé juste avant).

Comme indiqué sur la figure un peu plus haut, il s'agit de configurer les bits WGM2[2..0] dans le mode adéquat.

Il conviendra également de générer une interruption à chaque remise à zéro (ou plus exactement lors de chaque comparaison), et pour ce faire il suffit :

  • autoriser l'interruption de comparaison : TIMSK2|=1<<OCIE2A
  • autoriser globalement les interruptions : sei()
  • la fonction d'interruption sera définie par : ISR(TIMER2_COMPA_vect)

La valeur de sortie sera alors modifiée à chaque interruption, et le µcontrôleur pourra vaquer à d'autres activités dans le programme principal.

Question.jpg Écrire le programme utilisant le Timer2, en vous servant du squelette suivant :

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

// Fonction de traitement Timer 2 Comparaison
ISR(TIMER2_COMPA_vect)
{
 // il suffit de changer l'état de la sortie
}

int main()
{
 // variables

 // e/s

 // configuration du timer2 (prédiviseur)

 // autorisation d'interruption (cf ci dessus)

 // boucle
  while(1)
  {
    // tout se passe dans l'interruption, rien à faire ici
  }
}

Sans interruption mais sans attente passive : le matériel fait tout

La comparaison avec le Timer 2 (8 bits)

Par commodité, nous remettons (ci-contre) la documentation correspondante sous forme de schéma.

Ce mode de fonctionnement utilise le mode comparaison, et pilote également directement une sortie bien particulière.

L'idée générale est que lorsque le timer 2 (TCNT2) arrive à la même valeur que celle qui est contenue dans un registre (OCR2A ou OCR2B) une logique interne est capable de changer (ou pas) une sortie qui s'appelle OC2A ou OC2B.

Ce mode est essentiellement géré par les deux bits COM2A1 et COM2A0 comme indiqué dans le tableau ci-dessous :

Mode non PWM pour la comparaison
COM2A1 COM2A0 Description
0 0 Opération Normale PORT, OC2A déconnecté
0 1 Bascule OC2A sur la comparaison
1 0 Mise à 0 de OC2A sur la comparaison
1 1 Mise à 1 de OC2A sur la comparaison

Nous avons ajouté les informations correspondant à la carte UNO entre parenthèse. Par exemple la sortie correspondante au bit b3 du PORTB est OC2A et correspond au numéro 11 de la carte UNO. L'autre broche gérée par le comparateur B est la broche 3 et se trouve entre parenthèse avec le registre de comparaison.

Question.jpg Modifier le programme précédent en supprimant la fonction d'interruption, et en pilotant directement la sortie depuis le timer, en vous servant du tableau précédent.

Indication : l'idée est de placer le timer en mode CTC (Clear Timer on Compare match) et la génération de signaux en mode basculement de OC2A.

int main()
{
 // variables

 // e/s

 // configuration du timer2 (prédiviseur)

 // mode de génération de signaux (COM2A1 COM2A0)

 // boucle
  while(1)
  {
    // Plus aucune utilisation processeur
  }
}


Du son !!!

Attention, le paramètre lors de l'appel de la fonction _delay_ms() doit être constant. Si la durée doit varier, on pourra utiliser :

void attenteMs(uint16_t dureeMs)
{
  for (uint16_t i=0;i<dureeMs;i++) _delay_ms(1);
}


Objectif

Nous allons écrire un programme permettant de jouer de la musique sur un buzzer, et ceci en chargeant le moins possible le µcontrôleur. Pour ce faire, nous utiliserons au maximum les timers à disposition.

Le buzzer sera connecté entre la masse et la sortie PB3 du µcontrôleur, et le tableau suivant donne les fréquences à générer pour obtenir les différentes notes.


Note Do Do# Ré# Mi Fa Fa# Sol Sol# La La# Si
Fréquence (Hz) 261,63 277,18 293,66 311,13 329,63 349,23 369,99 392 415,3 440 466,16 493,88
Période (µs) 3822 3608 3405 3214 3037 2863 2703 2551 2408 2273 2145 2025

Configuration du Timer2

Le timer 2 sera bien entendu utilisé pour générer les notes sur le buzzer : en effet, c'est lui qui est connecté directement sur la patte PB3 (OC2A) !

Une seule valeur de prédiviseur devra être utilisée pour toutes les notes.

Question.jpg Choisissez une valeur de prédiviseur, et calculer alors la valeur de 'n', ceci pour chaque note

Remarque : On pourra astucieusement utiliser un tableur.

Todo.jpg Saisir alors ces valeurs dans un tableau :

enum           notes {Do, Dod, Re, Red, Mi, Fa, Fad, Sol, Sold, La, Lad, Si};
uint8_t freqNote[12]={ .... };

int main()
{
}

Tester vos notes

Commençons simplement par jouer toute la gamme. Il suffit pour cela de changer régulièrement de note.

Il convient bien évidemment de configurer correctement le timer; et de modifier régulièrement (à l'intérieur de la boucle "while(1)") la valeur de OCR2A.

Question.jpg Écrire ce programme, et tester

enum           notes {Do, Dod, Re, Red, Mi, Fa, Fad, Sol, Sold, La, Lad, Si};
const uint8_t freqNote[12]={ .... };

int main()
{
 // variables

 // e/s

 // configuration du timer2 (prédiviseur)

 // mode de génération de signaux (COM2A1 COM2A0)

 // boucle
 while(1)
 {
    // parcourir le tableau de notes et modifier la valeur de OCR2A
    OCR2A= freqNote[0];  // ou freqNote[Do]
    attenteMs(xxx);
    OCR2A = freqNote[1];
    ....
    // bon, une boucle c'est pas mal !
  }
}

Remarque : L'énumération est équivalente à dire que Do = 0, Dod = 1, Re = 2, ...

Préparons la suite

Afin de pouvoir écrire facilement une mélodie, nous allons écrire une fonction qui permet de changer la note, et également de modifier sa durée.

le prototype de la fonction sera :

void playnote(uint8_t n,uint16_t duree);

Question.jpg Modifier votre programme afin de pouvoir faire de la musique de la façon suivante :

enum           notes {Do, Dod, Re, Red, Mi, Fa, Fad, Sol, Sold, La, Lad, Si};
const uint8_t freqNote[12]={xxxxxxxxxxxx};
 
#define blanche 400
#define noire 200
#define croche 100
 
void playnote(uint8_t n,uint16_t duree)
{
   // modifier la valeur de OCR2A suivant la note n
   OCR2A = ...;
   // attendre la durée de la note
   attenteMs(duree); 
}
 
int main()
{
  // variables   // e/s   // configuration du timer2 (prédiviseur)   // mode de génération de signaux (COM2A1 COM2A0)   // boucle
  while(1)
  {
    playnote(Do,noire);
    playnote(Re,noire);
    playnote(Mi,noire);
    playnote(Re,noire);
  }
}

Petite amélioration

On pourra légèrement modifier la fonction playnote pour introduire un "blanc" (pas de son) entre 2 notes.

Pour ce faire, il suffit d'arrêter pendant un temps très court (1ms) le timer, et ensuite remettre la "bonne" valeur de prédiviseur.

Pour arrêter le timer, vous pouvez au choix :

  • Mettre le prédiviseur à 0 (CS21..CS20)
  • Déconnecter la "logique" de sortie (COM2A1-0)
void playnote(uint8_t n,uint16_t duree)
{
   // modifier la valeur de OCR2A suivant la note n
   OCR2A = ...;
   // attendre la durée de la note
   attenteMs(duree); 
   // arrêter le timer :
   TCCR2??? &= ~(...);
   // pause dans le son
   _delay_ms(1);
   // remettre le timer en route
   TCCR2??? |= (.....);
}

Mélodie !

Terminons par un peu de cosmétique en utilisant 2 tableaux qui permettront de :

  • donner la liste des notes composants la mélodie
  • donner la durée de chacune des notes

Il suffit alors de parcourir le tableau.

Question.jpg Modifier le programme en conséquence :

.....

#define nbNotes xx
enum notes melodie[nbNotes]={Fa     ,  .... }; 
const uint16_t    durees[nbNotes]={blanche,  .... }; 

....

int main()
{
  ....
 
  while(1)
  {
    for (uint8_t j=0;j<nbNotes;j++)
      playnote(....,...); 
  }
}


ex :

 // au clair de la lune complet
 #define nbNotes 33
 enum notes melodie[nbNotes]={Sol,Sol,Sol,La,Si,La,Sol,Si,La,La,Sol,La,La,La,La,Mi,Mi,La,Sol,Fad,Mi,Re,Sol,Sol,Sol,La,Si,La,Sol,Si,La,La,Sol};
 int durees[nbNotes]={croche,croche,croche,croche,noire,noire,croche,croche,croche,croche,blanche,croche,croche,croche,croche,noire,noire,croche,croche,croche,croche,blanche,croche,croche,croche,croche,noire,noire,croche,croche,croche,croche,blanche};

Tout en arrière plan :

Essayons de supprimer les "attentes passives" donnant la durée des notes.

Pour ce faire, nous utiliserons le timer1 qui permettra, grâce à une interruption, le changement de note.

Autrement dit, à chaque fois que l'on souhaite changer la note, il doit y avoir une interruption.

Configuration du timer1

Il va bien évidemment falloir choisir la valeur du prédiviseur pour ce timer, ainsi que les valeurs de n.

Le timer devra générer une interruption, au choix au bout de :

  • 100ms => durée d'une "croche"
  • 200ms => durée d'une "noire"
  • 400ms => durée d'une "blanche"

Question.jpg Faire le choix du prédiviseur et calculer les différentes valeurs de n, pour ces 3 durées.

Todo.jpg Commencer à modifier votre programme avec ces valeurs :

...
#define blanche xxx
#define noire xxx
#define croche xxx
....

Programme d'interruption

On utilisera bien évidemment le mode comparaison (CTC) du timer1, et on veillera à bien configurer l'interruption associée en :

  • autorisant cette interruption : TIMSK1 |= 1 << OCIE1A;
  • écrivant l'interruption associée : ISR(TIMER1_COMPA_vect)


Ce programme d'interruption devra :

  • incrémenter un compteur de note
  • changer la fréquence (valeur de OCR2A) sur le timer2
  • modifier la "date" (OCR1A) de prochaine interruption du timer1 en fonction de la durée de la note.


Question.jpg Écrire cette routine d'interruption en vous basant sur le squelette suivant :


ISR(TIMER1_COMPA_vect)
{
  static uint8_t i=0;         // compteur de note
  OCR2A = freqNote.....;   // changer la fréquence en fonction de la note actuelle
  OCR1A = ......;          // ainsi que la durée de la note
  i++;                     // incrémenter le compteur de note
  if (.....) ...;          // ne pas oublier de réinitialiser le compteur à un moment donné !
}

Assemblez le tout

C'est presque terminé, il vous manque juste la configuration du timer1 et votre programme doit fonctionner, le tout en n'utilisant quasiment pas de ressources !

enum           notes {Do, Dod, Re, Red, Mi, Fa, Fad, Sol, Sold, La, Lad, Si};
uint8_t freqNote[12]={xxxxxx};

#define blanche xxx
#define noire xxx
#define croche xxx

#define nbNotes 10
notes   melodie[nbNotes]={Do     ,.....};
uint16_t durees[nbNotes]={blanche,....};

ISR(TIMER1_COMPA_vect)
{
  ....
}

int main()
{
  // configuration e/s

  //configuration du timer2

  //configuration du timer1

  sei();
  while(1)
  {
  }
}