Coder un Tap Tempo en AVR
Quand j'ai commencé à coder en AVR, je voulais faire un bon Tap Tempo. A ce moment la, je n'ai pas trouvé de ressource en AVR pour en faire un.
Alors aujourd'hui, nous allons fair clignoter une LED en rythme que vous taperez. Cela peut parraitre un peu basique, mais c'est une étape importante dans la creation d'un Tap Tempo, et c'est aussi un exercice de code sympa.
Pourquoi l'AVR? A vrai dire, je ne sais pas vraiment. Cela m'a semblé un peu plus accessible que le PIC. Et avec la popularité d'Arduino il y a beaucoup d'aide sur internet et j'aimais l'idée d'apprendre le C.
Rappelez vous que ceci n'est pas un projet pour Arduino, même si c'est surement possible de faire marcher ce code sur Arduino. A mon avis, coder une puce AVR sans Arduino permet une meilleure compréhension du micro-controleur ainsi qu'un controle plus fin.
On a Besoin de Quoi?
- Avoir Atmel Studio installé.
- Télécharger AVRDUDE
- Avoir un "flasheur" d'AVR installé (J'utilise USBasp, il y en beaucoup de disponibles)
- Un micro-controlleur AVR, j'utiliserai l'ATtiny84A
- Un "breadboard", une LED, une resistance de 1kΩ et un interrupteur momentanné.
- Vous aurez surement besoin de la datasheet complete de votre AVR
La configuration du programmateur peut-être complexe, mais il y a beaucoup d'aide sur internet. Je recommande ces tutoriels aux anglophones.
Vous pouvez utiliser AVRDUDE directement dans Atmel Studio via l'onglet external tools, voici ma ligne de commande:
-c usbasp -p attiny84 -U flash:w:"$(ProjectDir)Debug\$(TargetName).hex":i
Configuration des Broches
Commencons par éviter toute confusion. Regardons la configuration des broches de la datasheet et notons le nom des broches auxquelles la resistance et l'interrupteur sont connectés.
Dans mon cas, l'interrupteur est en PB0 et la resistance en PB1. Indiquons cela au compilateur en utilisant la commande #define, avant même le programme.
Maintenant il nous faut indiquer que la broche de LED est une sortire en changeant le Data Direction Register. Nous devons aussi sortir Vcc (+5V) à l'interrupteur en changeant le registre de PORT (il connecte une resistance entre la broche et Vcc). De cette façon, l'AVR pourra détecter quand l'interrupteur force la broche à être au niveau bas. Nous ferons ceci dans la fonction main mais avant la boucle infinie.
Voici un petit rappel de comment changer les registres en utilisant des portes logiques. Ceci est detaillé dans ce tutoriel.
#include <avr/io.h> #define TAP_PIN PINB0 #define LED_PIN PINB1 int main(void) { while (1) { } }
#include <avr/io.h> #define TAP_PIN PINB0 #define LED_PIN PINB1 int main(void) { DDRB |= (1<<LED_PIN); // la broche LED comme sortie PORTB |= (1<<TAP_PIN); //+5V à l'interrupteur while (1) //Boucle infinie { } }
//Pour écrire 1 dans le bit: PORTA |= (1<<PINA0); //Pour ecrire 0 dans le bit: PORTA &= ~(1<<PINA0); //Pour commuter la valeur du bit: PORTA ^= (1<<PINA0);
Debouncing
Le débouncing est une partie importante d'un programme qui comprend des interrupteurs. C'est parce que les interrupteurs mécaniques ne sont pas parfait et il y a une intermittence entre on et off un court instant quand le bouton est actionné. Voici un article qui étudie en profondeur avec le debouncing.
Il y a beaucoup de façon de faire du debouncing, mais l'une des plus populaire est d'attendre un peu et puis de revérifier. Pour cela nous avons besoin de la bibliothèque "delay.h". Nous pouvons y accéder avec la commande "#include". Nous devons alors préciser la fréquence de notre horloge CPU (1MHz par défault).
Pour vérifier si le boutton est actionné nous utiliserons la fonction "bit_is_clear()". Cette fonction est incluse dans "io.h". "bit_is_clear" retourne 1 si la broche séléctionné est de niveau bas et retourne 0 sinon.
Pour vérifier l'état de la broche, créons une fonction que nous pourrons utiliser à tout moment. Cette fonction sera 1 si le bouton est appuyé et 0 si il ne l'est pas.
Nous allons aussi definir un nombre de µs seconde à attendre pour le debouncing. Pour un Tap Tempo preçis, une ms suffira.
A cette étape, notre code ressemble à ça:
#include <avr/io.h> #define F_CPU 1000000UL //indique la fréquence de l'horloge #include <util/delay.h> //inclue le header de delay
#include <avr/io.h> #define F_CPU 1000000UL //indique la fréquence de l'horloge #include <util/delay.h> //inclue la bibliothèque de delay #define TAP_PIN PINB0 #define LED_PIN PINB1 #define DEBOUNCE_TIME 1000 //définie le temps de debounce en us uint8_t debounce(void) //declaration de la fonction de debounce { if (bit_is_clear(PINB,0)) //si bouton appuyé { _delay_us(DEBOUNCE_TIME); //attendre 1ms if (bit_is_clear(PINB,0)) //si le bouton est toujours appuyé { return(1); //la fonction retourne 1 } else //si le bouton n'est plus appuyé à un moment retourne 0 { return(0); } } else { return(0); } } //fin de la fonction de debounce int main(void) { DDRB |= (1<<LED_PIN); //broche de LED comme sortie PORTB |= (1<<TAP_PIN); //+5V à l'interrupteur while (1) //boucle infinie { } }
Configuration du compteur
Maintenant que nous pouvons détecter le footswitch, commencons par compter les millisecondes entre les taps. Ceci se fait avec un timer/counter.
La pluspart des AVR ont un compteur 8-bits, souvent le Timer 0. Nous voulons que ce timer compte constemment et revienne à 0 au bout d'une milliseconde.
Notre horloge interne est calibrée à 1MHz, alors logiquement nous devrons compter 1000 cycles. Le problème est que un compteur 8-bits ne compte que jusqu'à 255. Cependant nous pouvons éviter ce problème en utilisant un prescaler de 8. Nous aurons besoin de compter que 1000/8 = 125 cycles. Pour selectionner ce prescaler nous devons ecrire CS01 dans le registre TCCR0B.
Pour avoir un compteur qui compte constemment jusqu'à 124, nous allons utiliser le mode Fast PWM avec une valeur TOP choisie (le TOP est 255 par default). Le nouveau TOP sera défini par OCR0A. Ce table de la datasheet (p.81)nous montre quel Waveform Generation Mode nous devons utiliser. Nous changerons les bits WGMXX dans TCCR0A & TCCR0B pour sélectionner le mode dont nous avons besoin.
Pour compter les ms qui sont passées nous devons incrémenter une variable quand le compteur atteint sa valeur la plus haute (TOP). Pour cela nous devons activer les interruptions. Ceci ce fait avec 3 lignes. D'abord on #include "interrupts.h". Puis on active l'interuption d'overflow du timer0, et enfin on active globalement les interruptions avec sei().
Notre compteur est maintenant configuré, il nous reste à écrire notre interruption. Nous créons une variable globale volatile qui sera incrémenté à chaque execution de l'interruption appelée "ms'. Le vexteur de notre interruption est "TIM0_OVF_vect".
Notre code ressemble maintenant à ça:
#include <avr/io.h> #define F_CPU 1000000UL //indique la fréquence d'horloge #include <util/delay.h> //inclue la bibliothèque de delay #include <avr/sfr_defs.h> //inclue les special feature registers #include <avr/interrupt.h> //iinclue les interruptions #define TAP_PIN PINB0 #define LED_PIN PINB1 //on définie le temps de débounce en µs #define DEBOUNCE_TIME 500 volatile uint16_t ms = 0; //declaring //declaration de la fonction de debounce uint8_t debounce(void) { if (bit_is_clear(PINB,0)) //si bouton appuyé { //attendre temps de debounce _delay_us(DEBOUNCE_TIME); //si bouton toujours appuyé if (bit_is_clear(PINB,0)) { return(1); //fonction donne 1 } //si bouton relaché a un moment, fonction donne 0 else { return(0); } } else { return(0); } } int main(void) { DDRB |= (1<<LED_PIN); //broche LED sortie PORTB |= (1<<TAP_PIN); //+5V broche interrupteur /*timer0 setup*/ OCR0A = 124; //max du compteur TCNT0 = 0; //Counter start //Fast PWM mode OCR1A en TOP TCCR0A |= (1<<WGM00) | (1<<WGM01); //interruption d'Overflow activée TIMSK0 |= (1<<TOIE0); //Fast PWM mode, pas de prescaler TCCR0B |= (1<<WGM02) | (1<<CS01); sei(); //active les interruptions while (1) { } } ISR(TIM0_OVF_vect) //interruption d'overflow du compteur { ms++; //incremente ms toutes les ms }
Le Tap Tempo
Nos périphériques sont configurés, codons maintenant ce Tap Tempo!
Nous devons faire attention à quelques paramètre, créons une variable pour chacun d'eux :
- Est-ce que le bouton à été relaché ?: si le programme ne prend pas cela en compte, il contabilisera des centaines de tap si on garde le bouton appuyé. Ainsi nous créons une variable uint8_t appellée buttonstate. Elle sera de 1 si le bouton est appuyé et 0 sinon. Nous l'initialiserons a 0.
- Combien de fois à t'on tapé ? Nous ne voulons pas faire la même chose pour le premier tap et les suivants. Alors autant compter le nombre de taps dans une séquence. Nous créons donc une variable uint8_t appellée nbtap. Elle est initialisée a 0.
- Quel est le tempo maximum autorisé?: Si nous ne limitons pas le tempo maximum l'AVR n'arretera pas de compter les ms, et cela posera problème entre les séquences de tap. Créons donc une variable uint16_t appellée maxtempo. Elle sera initialisée a 2000µs.
- Quel tempo je viens de taper? : La variable la plus évidente, le tempo à suivre pour la LED. Créons une variable uint16_t appelée tempo.
On déclare ces variable dans la fonction main avant la boucle infinie:
Maintenant commencons par le début. Que faire lors du premier tap? Et bien nous devons commencer à compter dans l'attente d'un éventuel second tap. Nous devons aussi indiqué que la bouton à été préssé et pas encore relaché.
Nous ferons cela dans un "if" placé dans la boucle infinie:
Notons aussi quand le bouton est relaché avec un autre "if":
Ajoutons un autre "if" pour le cas où nous depassons le tempo maximum. Nous aurons besoin de réinitialiser nbtap pour que la prochaine séquence soit normale.
Et enfin, quand le tap n'est pas le premier. Nous écrivons le nouveau tempo depuis les ms comptées entre les deux taps. Sans oublier de modifier nbtap, buttonstate et de mettre le compteur à zéro pour la prochain tap.
int main(void) { uint8_t buttonstate; //bouton relaché? uint8_t nbtap = 0; //nombre de tap en une séquence uint16_t maxtempo = 2000; //tempo max autorisé en ms uint16_t tempo; //le tempo actuel
if (debounce() == 1 && nbtap == 0 && buttonstate == 0) //premier tap { TCNT0 = 0; //commence à compter ms = 0; nbtap++; //bouton appuyé une fois buttonstate = 1; //bouton pas encore relaché }
if (debounce() == 0 && buttonstate == 1) //si le bouton est relaché { buttonstate = 0; }
if (ms >= maxtempo) // si ms dépasse tempo maximum { nbtap = 0; //réinitialise nombre de tap }
if (debounce() == 1 && nbtap != 0 && buttonstate == 0) //pas le premier tap { tempo = ms; //nouveau tempo nbtap++; buttonstate = 1; TCNT0 = 0; //réinitialise le compteur ms = 0; }
Faire clignoter la LED
Tout est fait, il ne reste plus qu'à faire clignoter la LED. Ce que je propose, c'est d'ajouter un nouveau compteur de ms pour la LED. De cette façon la LED est indépendante du processus du Tap Tempo.
Nous ajoutons une variable globale volatile "ledms" Nous la déclarons de la même façon que "ms". Cette variable est incrémentée dans l'interruption tout comme "ms".
Ajoutons maintenant un "if" dan la boucle de la fonction main. h an "if" statement in the main loop.
Avec ceci la LED est allumée sur le temp, reste allumée 8ms puis s'éteint jusqu'au prochain temps.
volatile uint16_t ms = 0; //declaring ms counting variables volatile uint16_t ledms = 0; ISR(TIM0_OVF_vect) //Timer overflow interrupt { ms++; //increment every ms ledms++; }
if (ledms >= tempo) { ledms = 0; //reinitialise le compteur LED PORTB |= (1<<LED_PIN); //Allume la LED _delay_ms(8); //attend 8ms PORTB &= ~(1<<LED_PIN); //Eteint la LED }
Le Code Final
#include <avr/io.h> #define F_CPU 1000000UL //indique la fréquence d'horloge #include <util/delay.h> //inclue la bibliothèque de delay #include <avr/interrupt.h> //inclue les interruptions #define TAP_PIN PINB0 #define LED_PIN PINB1 #define DEBOUNCE_TIME 500 //definie un temps de debounce en us volatile uint16_t ms = 0; //declare les variable qui compte les ms volatile uint16_t ledms = 0; uint8_t debounce(void) //declare la fonction de debounce { if (bit_is_clear(PINB,0)) //si la broche du boutton est basse { _delay_us(DEBOUNCE_TIME); //attendre 1ms if (bit_is_clear(PINB,0)) //si broche toujours basse (boutton appuyé) { return(1); //la fonction retourne 1 } else //si la broche n'est pas basse a un moment retourne 0 { return(0); } } else { return(0); } } int main(void) { DDRB |= (1<<LED_PIN); //LED comme sortie PORTB |= (1<<TAP_PIN); //+5v à l'interrupteur /*timer0 setup*/ OCR0A = 124; //valeur max du compteur TCNT0 = 0; //debut du compteur TCCR0A |= (1<<WGM00) | (1<<WGM01); //Fast PWM mode OCR1A est le TOP TIMSK0 |= (1<<TOIE0); //active l'interrution d'overflow TCCR0B |= (1<<WGM02) | (1<<CS01); //Fast PWM mode, prescaler de 8 uint8_t buttonstate; //bouton relaché? uint8_t nbtap; // Nombre de taps dans un séquence uint16_t maxtempo = 2000; //tempo max autorisé uint16_t tempo; sei(); //Activate interrupts while (1) { if (debounce() == 1 && nbtap == 0 && buttonstate == 0) //premier tap { TCNT0 = 0; //commence à compter ms = 0; nbtap++; buttonstate = 1; } if (debounce() == 0 && buttonstate == 1) //si bouton relaché { buttonstate = 0; } if (ms >= maxtempo) //si tempo max dépassé { nbtap = 0; //réinitialise la séquence } if (debounce() == 1 && nbtap != 0 && buttonstate == 0) //pas le premier tap { tempo = ms; //nouvelle valeur de tempo nbtap++; buttonstate = 1; TCNT0 = 0; //compteur à 0 pour la fois d'après ms = 0; } if (ledms >= tempo) //si on est sur le temps { ledms = 0; //remet compteur pour LED à zéro PORTB |= (1<<LED_PIN); //allume la led _delay_ms(8); //attend 8ms PORTB &= ~(1<<LED_PIN); //eteint la LED } } } ISR(TIM0_OVF_vect) //interruption d'overflow { ms++; //incremente les millisecondes ledms++; }
Conclusion
Ce tutoriel ne présente que le squelette d'un programme de Tap Tempo. Il y a beaucoup de possibilités d'améliorations. Je vous laisse donc cette tâche.
Rappelez vous de biens lire la datasheet pour n'importe quelle question, elles sont très bien documentées.
Bon Code ! Et venez discuter si vous rencontrez un souci.
Legal
A Propos