Micro contrôleurs AVR/La conversion analogique numérique
Nous allons présenter dans ce chapitre la conversion analogique numérique. Même si nous nous contentons de l’application aux AVR, un certain nombre de principes généraux sont étudiés. Des applications simples sont aussi présentées.
La conversion analogique numérique dans le monde Arduino
modifierL'environnement Arduino possède une primitive simple d'utilisation : analogRead. Elle retourne un nombre sur 10 bits puisque les convertisseurs sont des convertisseurs 10 bits. Cela veut dire qu’ils sont capables de retourner une valeur entre 0 et 1023, valeur représentant une tension entre 0 et 5V. Par exemple le programme
// programme d'exemple lecture conversion et envoi sur liaison
void setup()
{
Serial.begin(9600);
}
void loop()
{
Serial.print("Photocoupleur : ");
Serial.println(analogRead(A2),DEC);
delay(500);
}
enverra dans la liaison série la valeur lue sur l'entrée repérée par A2.
Malgré la simplicité de cet exemple, il est nécessaire de se poser des questions importantes lorsqu'on utilise la conversion, en particulier sur la tension de référence.
Documentation de la conversion analogique numérique
modifierLa documentation est donnée sous forme de schéma un peu plus loin. Commençons donc par des généralités.
Généralités
modifierLa compréhension de cette partie nécessite d’avoir à l'esprit des idées générales sur le fonctionnement d'une conversion analogique numérique. Nous les rappelons maintenant :
- Une conversion n’est pas instantanée
- elle se fait avec un "algorithme" qu’il faut "dérouler" donc nécessite une horloge
- Une tension de référence est nécessaire pour réaliser une conversion car celle-ci est essentiellement basée sur une comparaison.
- En interne, elle est de 1,1 V dans les versions ATMagaX8/XX8 mais de 2,56 V dans les ATMega8/16/32.
- En externe elle peut utiliser les broches AREF ou VREF. VREF permet d’utiliser Vcc comme référence.
La formule magique qui permet de calculer la conversion est :
où ADC est un nombre entier sur 10 bits. Ve est la tension d'entrée présente sur le convertisseur tandis que, comme son nom l'indique, Vref est la tension de référence.
On trouve parfois dans les documentations officielles la valeur de 1024 au lieu du 1023 ici. Nous ne sommes pas d'accord avec cette valeur de 1024 qui est une valeur sur 11 bits et non sur 10 bits !!!
Comment choisit-on la tension de référence avec la librairie Arduino
modifierPar exemple si l’on désire utiliser la tension de référence interne (discutée plus loin) on doit écrire
void setup() {
//permet de choisir une tension de référence de 1.1V
analogReference(INTERNAL);
}
tandis que l’utilisation d'une référence externe (sur le broche AREF) se fait par :
void setup() {
//permet de choisir une tension de référence externe à la carte
analogReference(EXTERNAL);
}
Exercice 1
modifierUne tension de référence de 2,56 V est utilisée comme référence pour une convertisseur d'un ATMega8 sur 10 bits.
1°) Quelle est la résolution en tension de ce convertisseur ?
2°) A quelle tension correspond le nombre 0x12F ?
3°) Une tension de 2V est présente sur l'entrée. Quelle sera la valeur de la conversion ?
4°) Un thermomètre LM35C peut mesurer une température entre -40 °C et +110 °C avec une précision de 1,5 °C et une résolution de 10 mV/°C. Quel nombre retournera le convertisseur pour une température de 45 °C ?
1°) 2,56 / 1024 = 2,5 mV (nombre exact).
2°) 0x12F = 303 donc tension 303 * 2,5 mV = 757,5 mV = 0,76 V
3°) 2 / 2,56 * 1023 = 799,21 arrondie à 799 = 0x31F
4°) Entre -40° et +110°, il y a 150 intervalles. 150 x 10 mV cela fait une tension max de 1,5 V. On suppose encore une tension de référence de 2,56 V.
+45° donne 85 intervalles de 10 mV soit 0,85 V soit un nombre de 1023/2,56 * 0,85 = 339,66 arrondi à 0x154
Il est grand temps d'expliquer le fonctionnement de la conversion.
Des registres pour la conversion analogique numérique
modifierPour le registre ADMUX, le dessin est autosuffisant. Notez quand même les différentes possibilités sur les choix de la référence et de l'entrée convertie. Les entrées AREF et AVCC existent et doivent être connectées à la masse par l'intermédiaire d'une capacité de 100nF. AVCC est en plus connectée à VCC.
Les quatre bits de sélection MUX3:0 laissent penser qu’il y a 16 entrées possibles à sélectionner, mais ce n’est pas vraiment le cas comme le montre le dessin. Huit sont clairement des convertisseurs tandis que d'autres sont reliés en interne et d'autres encore pas utilisées.
Pour le registre ADCSRA, ADEN autorise la conversion tandis que ADSC la fait démarrer (SC=Start Conversion). Ce bit est à 1 pendant toute la durée de conversion et repasse à 0 lorsque celle-ci est terminée.
- ADATE relève du déclenchement automatique. On le mettra systématiquement à 0. Il est lié au registre ADCSRB que nous n'étudierons pas.
- ADIF est le bit qui est positionné à 1 quand la conversion est terminée. Il peut être lié à une interruption en positionnant ADIE à 1. L'interruption effacera le bit ADIF. Si aucune interruption est utilisée, il faut effacer ADIF en écrivant un 1 dedans.
La division d'horloge doit être choisie pour être réalisée à 200kHz au maximum.
Exercice 2
modifierDonner le morceau de programme qui lance la conversion et attend la fin de conversion. Deux techniques sont à explorer :
- une avec le bit ADSC
- une avec le bit ADIF (qui est un flag et nécessite une attention particulière)
// on lance la conversion
ADCSRA |= (1 << ADSC);
// on attend qu'elle soit finie
while ((ADCSRA & (1 << ADSC))==(1 << ADSC));
// ici le bit ADSC vient de passer à 0
Autre technique :
// on lance la conversion
ADCSRA |= (1 << ADSC);
// on attend qu'elle soit finie
while (ADCSRA & (1 << ADIF) != (1 << ADIF));
// ici le bit ADIF vient de passer à 1
// c’est un flag qu’il faut remettre à 0 !!!!
ADCSRA |= (1 << ADIF);
Exercice 3
modifierPouvez-vous expliquer le result=ADCH du programme ci-dessous ainsi que chacun des commentaires. Y a-t-il erreur dans un commentaire ?
#include <avr/io.h>
int main() {
unsigned char result;
// Choose AREF pin for the comparison voltage
// (it is assumed AREF is connected to the +5V supply)
// Choose channel 3 in the multiplexer
// Left align the result
ADMUX = (1 << REFS0) | (1 << ADLAR) | (3);
// Start the ADC unit,
// set the conversion cycle 16 times slower than the duty cycle
ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADSC);
// Wait for the measuring process to finish
while (ADCSRA & (1 << ADSC)) continue;
// Read the 8-bit value
result = ADCH;
}
ADMUX = (1 << REFS0) montre que c’est VREF qui est pris comme tension de référence. Nous pensons donc qu’il faut remplacer AREF par VREF dans le commentaire puisque AREF n’est pas pris en compte.
Le result =ADCH; veut dire que l’on ne garde que les poids fort. A priori il n'y a que 2 bits de poids fort puisque ADCL en prend 8 ! Mais c’est sans compter sur le bit ADLAR qui positionné à 1 qui met les 8 bits de poids fort dans ADCH et deux bits de poids faible dans ADCL... qui sont donc perdus. En général ce n’est pas très grave de perdre les poids faibles.
Exemple d'utilisation avec interruption
modifierVoici un exemple d'utilisation de la conversion analogique/numérique et son interruption associée.
#include <avr/io.h>
#include <avr/interrupt.h>
ISR(ADC_vect) {
PORTD = ADCL;
PORTB = ADCH;
ADCSRA |= (1<<ADSC);
}
int main() {
unsigned char result;
DDRB = 0xFF;
DDRD = 0xFF;
DDRA = 0; // make port A an input for ADC
ADCSRA = 0x8F; //enable interrupt select clk/128
ADMUX = 0xC0; //{{Unité|2.56|{{Abréviation|V|volt}}}}ref and ADC
sei();
ADCSRA |= (1 << ADSC); //start conversion
while (1); // wait forever
return 0;
}
Nous ne donnerons pas plus d'information sur ce morceau de code et laissons le lecteur le lire attentivement.
Applications : lecture de plusieurs interrupteurs
modifierL'utilisation d'un bit de PORT par interrupteur devient vite très consommatrice de broches du composant. Nous allons étudier un moyen simple d’éviter cette augmentation.
Voici un premier schéma d'exemple ci-contre
Exercice 4
modifier1°) Trouver les valeurs des tensions en fonction de l'appui sur les boutons poussoirs. On supposera pour ce calcul que Vcc = 5V.
2°) Calculer les ADC correspondants sur 10 bits si VREF = 5V.
3°) Écrire un programme complet capable d'afficher complètement l'état des 4 interrupteurs sous forme binaire.
Exercice 5
modifierOn peut faire encore plus que dans le cas de l'exercice 4 en décodant tout un clavier avec une seule entrée.
1°) Calculer dans un tableau les tensions et valeurs 10 bits correspondantes pour l'appui d'une touche du clavier si la tension Vcc est fixée à 5V (ainsi que VREF).
2°) Peut-on prendre en compte l'appui de plusieurs touches ?
3°) Écrire un sous programme de lecture du clavier qui retourne la touche appuyée. On utilisera obligatoirement un switch.
Détection avec interruption
modifierEn robotique mobile, des interrupteurs peuvent être utilisés pour détecter des obstacles. Il faut alors réagir de toute urgence. Une interruption peut être adaptée à cette situation.
L'utilisation d'interruption dans ce contexte est cependant assez subtil dans la mesure où l'interruption CAN est seulement déclenchée quand la conversion est terminée. Il faut donc essayer de coupler tout cela avec ce que l’on appelle une interruption externe.
Préalable sur l'interruption externe
modifierCette partie détaille l’utilisation des interruptions INT0 et INT1, attachées aux pin PD2 et PD3 pour l'AVR ATMega328.
Registre EICRA
modifierLe registre EICRA permet de choisir le mode de déclenchement de l'interruption avec deux bits de réglage par interruption (soit 4 pour l'ATMega328).
EICRA bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Fonction | ----- | ----- | ----- | ----- | ISC11 | ISC10 | ISC01 | ISC00 |
Valeur initiale | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Le tableau suivant donne la valeur des bits ISCx0 et ISCx1 pour configurer le mode de déclenchement associé à l'interruption INTx :
ISCx1 | ISCx0 | Déclenchement de l'interruption sur : |
---|---|---|
0 | 0 | Un niveau bas sur l'entrée INTx |
0 | 1 | Un changement d'état sur l'entrée INTx |
1 | 0 | Un front descendant sur l'entrée INTx |
1 | 1 | Un front montant sur l'entrée INTx |
Registre EIMSK
modifierLe registre EIMSK permet d'autoriser ou non les interruptions INT1 et INT0.
EIMSK bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Fonction | ----- | ----- | ----- | ----- | ----- | ----- | INT1 | INT0 |
Valeur initiale | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Une mise à '1' du bit INTx permet d'autoriser l'interruption associée.
Registre EIFR Exernal Interrupt Flag Register
modifierLe registre EIFR permet d'observer l'état des interruptions INT1 et INT0.
EIFR bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Fonction | ----- | ----- | ----- | ----- | ----- | ----- | INTF1 | INTF0 |
Valeur initiale | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Le bit INTFx passe à '1' lors du déclenchement de l'interruption.
Exemple
modifierPour déclencher une interruption à chaque changement d'état de la patte PD2 (donc sur les fronts montant et descendant), on pourra utiliser le code suivant :
ISR(INT0_vect) // programme d'interruption : le programme principal est interrompu,
{ // l'interruption exécutée et ensuite le programme principal continu normalement son exécution
PORTB^=0x01; // modification de la sortie PB0
}
void setup() {
DDRB=0x0F; // configuration de PB0 en sortie
cli(); // arrêt des interruptions
EICRA=0x01; // mode de déclenchement de l'interruption
EIMSK=0x01; // choix des interruptions actives
sei(); // autorisation des interruptions
}
void loop() {
}
Application au robot Asuro
modifierVoici un exemple concernant le robot Asuro qui utilise un ATMega8 présenté dans un autre chapitre. Sur l'ATMega8 une interruption liée à la conversion analogique numérique existe mais elle n'est déclenchée que lors de la terminaison de la conversion et non lors d'un changement sur une entrée. C'est pour cela que les concepteurs d'Asuro ont utilisé le schéma ci-contre. L'appui sur un interrupteur sera aussi détecté sur l'entrée PD3 qui peut elle déclencher une interruption.
Exercice 6
modifier1°) Calculer les changements de tensions lors des appuis d'interrupteurs si la tension d'alimentation est 5V.
2°) On vous donne un extrait d'un morceau de programme pour le Robot ASURO. Pouvez-vous déduire de ce programme les valeurs manquantes de ce morceau de programme ?
…
i = ADCL + (ADCH << 8);
taste = ((1024.0/(float)i - 1.0) * 63.0 + 0.5);
…
// K1 ou K2 (ou non exclusif)
if (taste == || taste == || taste == ) { //links kollidiert
…
// K5 ou K6 (ou non exclusif)
if (taste == || taste == || taste == ) { //rechts kollidiert
…
Indications : links en allemand signifie gauche tandis que rechts signifie droite. Les interrupteurs sont montrés dans la photo ci-contre. Ils sont disposés dans l’ordre de K1 à K6 avec donc K1 en haut et K6 en bas. Cette information vous permet de trouver lesquels sont activés pour la gauche et pour la droite.
3°) Donner le squelette d'un programme qui arrête le robot quand un des interrupteurs est fermé.
Solution de l'exercice 6
modifier1°) Si nous utilisons un tableur comme ci-dessous,
A | B | C | D | E | |
---|---|---|---|---|---|
1 | Vcc | R23 | VADC | ADC | |
2 | 5 | 1000000 | |||
3 | R30 | 68000 | =A2*B3/(B2+B3) | =(C3*1023/A2) | K6 |
qui est à continuer vers le bas pour chacun des interrupteurs, nous obtenons les résultats :
A | B | C | D | E | F | |
---|---|---|---|---|---|---|
1 | Vcc | R23 | VADC | ADC | ((( 1024.0/(float)i - 1.0)) * 63.0 + 0.5) | |
2 | 5 | 1000000 | ||||
3 | R30 | 68000 | 0,3183520599 | 65 | K6 | 1,4889598068 |
4 | R29 | 33000 | 0,1597289448 | 32 | K5 | 927,9378126617 |
5 | R28 | 16000 | 0,0787401575 | 16 | K4 | 1911,5186616902 |
6 | R27 | 8200 | 0,0406665344 | 8 | K3 | 7690,99860525 |
7 | R26 | 4000 | 0,0199203187 | 4 | K2 | 15765,9574780059 |
8 | R25 | 2000 | 0,0099800399 | 2 | K1 | 31531,353372434 |
9 | R25//R26 | 0,0066577896 | 1 | 584,5714285714 | K1 & K2 | 47296,7492668623 |
10 | R29//R30 | 0,1086745961 | 22 | 978,9390191898 | K6 & K5 | 2838,8948907742 |
Nous avons supposé la tension de référence à 5V dans ce calculs. Cette tension ne semble pas très adaptées pour certaines variations. Bien sûr il est possible de changer les valeurs des résistances pour avoir un domaine de variation plus grand, mais n'oubliez pas qu'il faut détecter un front descendant sur l'entrée qui est branchée sur INT1. C'est à cause de cela que l'on est obligé d'opérer avec des résistances qui réalisent une grosse variation quand un interrupteur est appuyé.
Il est possible de changer la tension de référence pour avoir une dynamique plus grande sur ADC. Pour mémoire, la tension de référence est de 2,56 V pour l'ATMega8 de l'Asuro et celle de l'ATMega328 est de 1,1 V (pour l'Arduino UNO).
2°) Le calcul présenté dans l'énoncé ne nous convainc pas vraiment. Nous n'avons d'ailleurs pas réussi à retrouver plusieurs années après l'écriture de cet énoncé un programme original qui proposait ce calcul de la variable "taste" qui de notre point de vue ne sert pas à grand chose, à part à faire un calcul en flottant qui consomme beaucoup de ressources d'un microcontrôleur 8 bits. En effet nous pensons au vue des calculs présentés en question 1 qu'il faudrait mieux ne pas faire ce calcul et plutôt faire un test de comparaison directement sur i du genre :
…
i = ADCL + (ADCH << 8); // avec nos calculs ADCL suffit
…
// K1 ou K2 ou K3 (ou non exclusif)
if (i<=12) { //links kollidiert
…
// K5 ou K6 ou K4 (ou non exclusif)
if (i> 12 ) { //rechts kollidiert
…
3°) La documentation schématique de l'Asuro montre que c’est INT1 qu’il faut utiliser. On n'a pas le choix, ce sont les concepteurs qui ont choisi avec leur Circuit Imprimé.
La seule documentation dont vous disposez un peu plus haut dans ce cours est celle de l'ATMega328. Nous allons faire comme si c'était ce processeur qui équipait l'Asuro.
ISR(INT1_vect) // programme d'interruption : le programme principal est interrompu,
{ // l'interruption exécutée et ensuite le programme principal continu normalement son exécution
.... // arret des moteurs par exemple
}
void setup() {
... // configuration des moteurs, des CANs et autres...
cli(); // arrêt des interruptions
EICRA = (1 << ISC11) ; // mode de déclenchement de l'interruption sur front descendant
EIMSK = (1<< INT1); // choix de interruption active
sei(); // autorisation des interruptions
}
void loop() {
}
Exercice final de synthèse
modifierIl s'agit tout simplement dans cet exercice de brancher un potentiomètre sur l'entée ADC0 d'un ATMega328 (entrée A0 d'un Arduino UNO) et de commander un servomoteur à l'aide du timer1. Évidemment la position du servomoteur dépend directement de la position du potentiomètre.
Commande du servomoteur
modifierCette partie a déjà été étudiée dans un chapitre précédent. Le code correspondant se trouve dans l'exercice 3 de l'étude du Timer 1.
Programme complet
modifierRéaliser une conversion analogique numérique est l'objet de ce chapitre. Si vous êtes arrivés ici, vous en maîtrisez certains aspects. En particulier vous savez que le résultat de la conversion analogique numérique vous donnera un résultat entre 0 et 1023 suivant la position du potentiomètre.
Si vous relisez la partie sur le timer 1 et la commande du servomoteur vous savez que la valeur du registre du PWM pour le moteur devra lui varier entre 32 et 156.
Les valeurs numériques données ci-dessous peuvent dépendre légèrement de vos servomoteurs. Ce que vous devez vérifier avant de les utiliser est qu'elles ne positionne pas le servomoteur en butée. C'est tout. |
En résumé nous avons des valeurs entre 0-1023 que nous devons transformer en valeurs entre 32-156. Ce n'est pas un problème difficile à priori mais si vous écrivez bêtement la formule en C :
commandeServo = ADC * (156-32)/1023 + 32;
(qui est la bonne formule) et bien vous obtiendrez toujours 32 dans commandeServo ! Si vous ne comprenez pas pourquoi c'est parce que vous avez oublié que la division en C est entière et donc que (156-32)/1023 donnera toujours 0. Pour obtenir le bon résultat il faut forcer la division en division flottante ou tout simplement précalculer le nombre (156-32)/1023 qui vaut 0.1212....
Nous avons conçu un circuit pour nos essais à l'aide de Tinkercad. Ce circuit est présenté dans la vignette ci-contre.
Écrire le code complet qui résout ce problème.
#include <avr/io.h>
#include <util/delay.h>
// Frequence 16MHz
//******************* Pour platine UNO **************
int main(){
//********* Configuration du CAN **************
uint16_t result;
// Choose AVCC pin for the comparison voltage
// Choose channel 0 in the multiplexer
ADMUX = (1 << REFS0) ;
// Start the ADC unit,
// set the conversion cycle 16 times slower than the duty cycle
ADCSRA = (1 << ADEN) | (1 << ADPS2);
//*********** partie Timer 1 **************
// initialisation pour comparaison
DDRB |= (1<<DDB1); // 1 = sortie=OC1A (15)
ICR1 = 1250; // MLI à 50 Hz
//TIFR0 |= 0x04; // clr TOV1 with 1
// Prescaler 256 (Clock/256)
TCCR1B = (1<<CS12);
//ICR1 = 1250; // MLI à 50 Hz
// Timer 1 en mode 14
TCCR1B |= (1<<WGM12);
TCCR1A |= (1<<WGM11)|(1<<WGM10);
TCCR1A |= (1<<COM1A1); //bascule sortie a chaque comparaison
OCR1A = 32;
while(1){
// start conversion
ADCSRA |= (1 << ADSC);
// Wait for the measuring process to finish
while (ADCSRA & (1 << ADSC));
// Read the 16-bit value
result = ADC;
//OCR1A = (uint8_t)ADC * (156-32) /1023 +32;
OCR1A = (uint8_t)(ADC * 0.1212 +32);
_delay_ms(1000);
}
}
Voir aussi
modifierArticles
modifier- Les entrées analogiques de l'Arduino
- Premiers Pas en Informatique Embarquée document de cours (de Simon Landrault et al.)
Livres
modifierL'utilisation d'un convertisseur analogique numérique pour lire l'état de plusieurs interrupteurs est décrite pour une autre architecture dans :
- ARM Microcontroller Interfacing Harware and Software, Warwick A. Smith, Elektor 2010