Micro contrôleurs AVR/AVR et robotique : mini-Q 2WD
Nous allons étudier une nouvelle platine robotique dans ce chapitre. Encore une nouvelle, dirons certains, alors que l'étude des deux autres n’est pas complète !
Et bien oui et nous assumons ce choix.
La platine Asuro a un rapport qualité/prix intéressant. C'est la seule des trois platines qui se programme à distance. Cela devrait être toujours le cas car la programmation d'un robot consiste à faire un programme a le mettre dans le robot et à l'essayer. Pour cette dernière étape il faut déconnecter le fil de programmation, ce que nous n'avons pas à faire avec Asuro. Elle a cependant des inconvénients. Parmi ceux-ci la mécanique avec la demi balle de ping-pong n’est pas exceptionnelle. Les batteries sont de type AAA.
La platine Pololu a été abandonnée en enseignement à cause essentiellement du fait qu’il n'y a aucun moyen de savoir de combien a tourné chaque roue... ou plutôt que pour le faire il faut acheter un ensemble roue plus codeur assez coûteux. Les batteries sont elles aussi de type AAA. Les exemples fournis sont très intéressants et peut-être trop intéressants : on a quasiment tout.
La platine mini-Q possède, quant à elle, des codeurs sur ses roues et utilisent des batteries de type AA. Cela a été suffisant pour la choisir comme base de notre enseignement. Signalons que le petit circuit de programmation vendu avec cette platine nous semble trop fragile pour une utilisation intensive avec des étudiants. D'ailleurs la version 2 commercialisée à partir de 2015, utilise maintenant une platine compatible "Leonardo" avec un connecteur USB micro ce qui nous semble plus adapté à une utilisation répétée.
Introduction
modifierSchéma du miniQ 2WD vous donne son schéma interne (qui comporte une erreur d'ailleurs).
- La carte Arduino équivalente est la : Arduino nano /w ATMega328
- Le programmateur d'origine est : AVR ISP
- Le port série est en général /dev/ACM0 sous Linux
Nous allons présenter dans ce chapitre la programmation du robot miniQ en langage C pur. Nous rappelons que les platine compatible Arduino sont programmables en C pur à partir de l'environnement Arduino. Il suffit d’utiliser un "main()" et l'éditeur de lien se débrouille. Évidemment les primitives Arduino ne sont alors plus disponibles ou alors ont un fonctionnement non prévisible.
Une version plus orientée "langage Arduino" de ce chapitre est en cours de construction aussi. Elle est disponible ICI dans le Wiki de l'IUT de Troyes.
Boutons poussoirs et Conversion Analogique Numérique (Robot version 1)
modifierL'entrée de conversion analogique numérique numéro 5 est reliée comme le montre la figure ci-dessous.
Utilisation du timer 0 pour la MLI
modifierUn chapitre entier a été consacré au timer 0 dans ce livre. Vous avez intérêt à le relire avant d'aller plus loin.
Certains se demandent probablement s'il ne serait pas mieux d’utiliser le timer 1 pour la réalisation de la MLI (Modulation de Largeur d'Impulsion). C'est probablement vrai, mais c’est impossible à réaliser sans interruption. Nous allons essayer d'expliquer pourquoi maintenant.
Commande des deux moteurs
modifierLe timer 0 comme les autres timers permet une commande simultanée de deux moteurs. Puisqu’il s'agit du timer 0, les deux bits associés sont OC0A en PD6 (bit 6 du PORTD) et OC0B en PD5. Si le concepteur du robot a câblé ces deux bits comme commande des moteurs il est naturel d’utiliser le timer 0. C'est le cas comme on peut le voir sur le schéma de principe. Vous voyez bien PD5 connecté à EN1... mais par contre PD6 connecté à IN2. C'est forcément une erreur : PD6 doit être connecté à EN2. L'erreur est naturellement sur le schéma et non sur la carte, heureusement pour nous !
Côté puissance, la commande nécessite deux signaux :
- ENi qui reçoit donc la MLI
- INi qui détermine le sens de rotation
avec i=1 ou i=2 pour chacun des moteurs.
Le code d'initialisation de la PWM (ou MLI) est examiné maintenant.
Code pour initialiser la MLI
modifierRappelons avant de commencer comment est câblé le module de commande des moteurs avec le processeur à l'aide d'une figure ci-contre)
Voici le code fourni avec la platine est le suivant :
void pwm_init(void)
{
TCCR0A = 0XA3;
TCCR0B = 0X03;//clk/64
TCNT0 = 0X00; //initialisation timer 0
TIMSK0 = 0X00;//Aucune interruption timer
}
Ce que fait la ligne TCCR0B=0X03 est décrit dans le commentaire. On rappelle que la division de l'horloge est choisie par les trois bits de poids faibles de TCCR0B
- (000) arrêt
- (001) division par 1
- (010) division par 8
- (011) division par 64
- (100) division par 256
- (101) division par 1024
- (110) front descendant de T0
- (111) front montant de T0
T0 est un bit qui n’est pas placé au même endroit suivant la famille.
Ce que fait la ligne TCCR0A = 0XA3 est un peu plus complexe et nécessite la lecture de deux tableaux (qui sont rappelés). Voici le premier pour le poids fort de TCCR0A.
- Mode PWM rapide et comparaison
COM0A1 | COM0A0 | Description |
---|---|---|
0 | 0 | Opération Normale PORT, OC0A déconnecté |
0 | 1 | WGM02=0 Opération Normale PORT, OC0A déconnecté |
0 | 1 | WGM02=1 Basculement de OC0A sur la comparaison |
1 | 0 | Mise à 0 de OC0A sur la comparaison et à 1 à BOTTOM |
1 | 1 | Mise à 1 de OC0A sur la comparaison et à 0 à BOTTOM |
qui est identique pour COM0B1 et COM0B0. Les deux bits de poids fort du 0xA3 choisissent donc le troisième mode, et idem pour B. On utilise donc pour les deux moteurs la mise à zéro de OC0A (et aussi OC0B) qui sont les deux sorties reliées à EN1 et EN2.
L'autre tableau à lire est :
- Description des bits pour la génération de forme d'onde
Mode | WGM02 | WGM01 | WGM00 | Mode de fonctionnement | Bas si | Mise à jour de OCRAx si | Drapeau TOV0 positionné si |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | Normal | 0XFF | immédiatement | MAX |
1 | 0 | 0 | 1 | PWM à phase correct | OXFF | TOP | BOTTOM |
2 | 0 | 1 | 0 | CTC | OCR0A | immédiatement | MAX |
3 | 0 | 1 | 1 | PWM rapide | 0XFF | BOTTOM | MAX |
4 | 1 | 0 | 0 | Reservé | - | - | - |
5 | 1 | 0 | 1 | PWM à phase correct | OCR0A | TOP | BOTTOM |
6 | 1 | 1 | 0 | Reservé | - | - | - |
7 | 1 | 1 | 1 | PWM rapide | OCR0A | BOTTOM | TOP |
Même si WGM02 est dans un autre registre, il n'y a aucune difficulté de remarquer que c’est la ligne 3 (PWM rapide) qui est choisie. Ceci est facilement trouvé avec la figure qui est rappelée :
Code pour utiliser les moteurs
modifierIl est possible de trouver des exemples de programmation du robot dans cette page commerciale par exemple. À partir de ces exemples nous présentons un code source légèrement modifié pour être conforme au schéma de principe. Nous avons aussi renommé les paramètres pour savoir qui est moteur gauche et droit et diminué aussi leur taille.
//*************** Motor_Control ***********************
//* utilise le timer0 pour commander les deux moteurs *
//* nécessite un appel de pwm_init() avant 1er appel *
//*****************************************************
void Motor_Control(char MG_DIR,char MG_EN,char MD_DIR,char MD_EN)
{
//////////MGauche////////////////////////
if(MG_DIR==FORW)//si vers l'avant
PORTD |=(1<<7);//IN2 passé à 1
else
PORTD &=~(1<<7);//IN2 passé à 0
OCR0A = MG_EN;//Rapport cyclique
///////////MDroit//////////////////////
if(MD_DIR==FORW)//si vers l'avant
PORTD |=(1<<4);//IN1 passé à 1
else
PORTD &=~(1<<4);//IN1 passé à 0
OCR0B = MD_EN;//Rapport cyclique
}
Les deux directions sont définies avec les paramètres MG_DIR et MD_DIR avec G pour Gauche et D pour Droite). Les vitesses des moteurs sont réglées avec MG_EN et MD_EN variant de 0 (arrêt) à 255 (pleine vitesse).
Travail à faire : Exercice 1
modifierVous devez réaliser une commande du robot pour qu’il réalise un huit (deux cercles connectés par un point). Vous choisirez le rayon de vos cercles en jouant sur les rapports cycliques. Puis vous jouerez sur les temporisations pour gérer tout cela. Bien entendu, le fait qu’il n'y ait pas d'information de retour sur l'endroit où vous êtes complique un peu la mise au point.
1°) Voici un code complet d'initialisation et d'utilisation des moteurs :
//******************** Ce code sera modifié pour devenir du C pur
//******************** Revenez voir de temps en temps
//******************** Ce code n'a pas encore été essayé
#include <avr/io.h>
#undef F_CPU
#define F_CPU 16000000UL
#include <util/delay.h>
#define FORW 1
#define BACK 0
//***** declaration des prototypes
void setup();
void pwm_init(void);
void Motor_Control(char MG_DIR,char MG_EN,char MD_DIR,char MD_EN);
int main(){
setup();
Motor_Control(1,100,1,100);
_delay_ms(5000);
Motor_Control(0,0,0,0);
_delay_ms(1000);
Motor_Control(0,100,0,100);
_delay_ms(5000);
Motor_Control(0,0,0,0);
while(1);
return 0;
}
void setup() {
DDRD = 0XF2;//PORTD
DDRB = 0XFE;//PORTB en sortie sauf b0
pwm_init();
}
void pwm_init(void) {
TCCR0A = 0XA3;
TCCR0B = 0X03;//clk/64
TCNT0 = 0X00; //initialisation timer 0
TIMSK0 = 0X00;//Aucune interruption timer
}
void Motor_Control(char MG_DIR,char MG_EN,char MD_DIR,char MD_EN) {
//////////MGauche////////////////////////
if(MG_DIR==FORW)//si vers l'avant
PORTD |=(1<<7);//IN2 passé à 1
else
PORTD &=~(1<<7);//IN2 passé à 0
OCR0A = MG_EN;//Rapport cyclique
///////////MDroit//////////////////////
if(MD_DIR==FORW)//si vers l'avant
PORTD |=(1<<4);//IN1 passé à 1
else
PORTD &=~(1<<4);//IN1 passé à 0
OCR0B = MD_EN;//Rapport cyclique
}
Modifier ce code pour réaliser un simple cercle. Seule la partie main() est à modifier !
Vos essais seront faits sur la table de travail.
|
2°) Réaliser maintenant votre huit (deux cercles se touchant par un point). On pourra réaliser tout cela dans le noir avec une prise de trajectoire par appareil photo à pose longue.
3°) Étude du rayon du cercle. Nous avons vu en cours que Essayez d'étudier si le rayon peut s'écrire où et désignent le rapport cyclique des moteurs gauche et droit (qui varient entre 0 et 255) et K est une constante à déterminer.
Utiliser les détecteurs infra-rouge frontaux
modifierLe robot dispose de cinq détecteurs infra-rouge pour le suivi de lignes. Ce n’est pas ces détecteurs que nous allons étudier mais un détecteur et deux émetteurs dirigés respectivement vers la droite et vers la gauche. Cela devrait nous permettre de détecter des obstacles. Le détecteur est relié à INT0/PB0 et peut donc être géré par une interruption.
L'idée générale est d'envoyer un train d'impulsions en émission et de compter ce que l’on reçoit.
Code d'émission
modifierVous pouvez utiliser le code suivant :
// train d'impulsion Gauche
void L_Send(void) {
int i;
for(i=0;i<24;i++) {
PORTB &=~(1<<1);//b1 à 0
_delay_us(12);//attente
PORTB |=(1<<1);//b1 à 1
_delay_us(12);//attente
}
}
//train d'impulsion Droit
void R_Send(void) {
int i;
for(i=0;i<24;i++) {
PORTB &=~(1<<2);//b2 à 0
_delay_us(12);
PORTB |=(1<<2);//b2 à 1
_delay_us(12);
}
}
Si vous regardez attentivement le schéma de principe vous déduisez facilement que l'émission se fait à l'aide d'un zéro.
Réception par interruption
modifierL'interruption est d’abord initialisée à l'aide du code :
void pcint0_init(void) {
PCICR = 0X01;
PCMSK0 = 0X01;//Autorisation de l'interruption INT0
}
Le code de l'interruption est tout simple :
ISR(PCINT0_vect) {
count++;//on incrémente le compteur pour chaque impulsion reçue
}
setup() associé
modifierLa terminologie "setup()" employée dans le titre de cette section est naturellement issue de l'environnement Arduino. Même si nous programmons en C, l’idée d'imposer une architecture de programme divisée en deux parties (setup() et loop()) nous semble pertinente. C'est pourquoi nous la reprenons ici.
Les lignes de code associé à la détection infra-rouge à mettre dans la partie setup() du programme sont :
void setup() {
DDRD = 0XF2;//PORTD
DDRB = 0XFE;//PORTB en sortie sauf b0
pcint0_init();
sei();
}
Un code pour éviter les obstacles
modifierTout ce qui précède peut être utilisé pour détecter et donc éviter les obstacles. L’idée générale est très simple :
- on initialise le compteur "count" à 0
- On émet à droite 24 impulsions plusieurs fois, on compte ensuite ce que l’on reçoit (cela est réalisé automatiquement à l'aide d'une interruption).
- si c’est supérieur à 20, on recule suffisamment on tourne du bon côté et on repart en marche avant.
- on initialise le compteur à 0
- On émet à gauche 24 impulsions plusieurs fois, on compte ce que l’on reçoit.
- si c’est supérieur à 20, on recule suffisamment on tourne du bon côté et on repart en marche avant.
La mise au point de ce code peut prendre beaucoup de temps.
Indications : le code pour émettre plusieurs fois peut être par exemple :
count = 0;
for(i=0;i<20;i++) { //left transmitter sends 20 pulses
L_Send40KHZ();
delayMicroseconds(600);
}
if(count>DISTANCE_IR)//if recieved a lot pulse , it means there's a obstacle
Ce code émet 20 fois 24 impulsions.
Le DISTANCE_IR du "if(count>DISTANCE_IR)" peut être défini par une constante. Divers essais chez moi on montré que la valeur 35 est un bon point de départ. Si vous augmentez cette valeur vous détecterez les obstacles plus près et si vous la diminuez ce sera le contraire. Il est défini comme une constante DISTANCE_IR dans le code de la section suivante et nous vous encourageons très fortement d’utiliser cette technique pour la mise au point.
Travail à faire : exercice 2
modifierVous devez mettre au point un code qui permette de ne jamais percuter des obstacles, c'est-à-dire qui évite les obstacles qu’il détecte à droite comme à gauche.
Indication : à ce stade vous n'avez pas de documentation sur l’utilisation complète des PORTs B et D. On vous demande d’utiliser le code :
#define SPEED 60
#define DISTANCE_IR 35
int count;//count the motor speed pulse
void setup() {
pcint0_init();
sei(); //enable the interrupt
Motor_Control(FORW,SPEED,FORW,SPEED);//run motor
}
int main() {
setup();
while(1) {
Eviter_Obstacle();//obstacle avoidance
}
}
Ce code doit être naturellement complété par le code donné dans la section Utiliser les détecteurs infra-rouge frontaux ainsi que par l'écriture du sous-programme "Eviter_Obstacle()" qui est le cœur même de ce que l’on cherche à faire. Vous pouvez noter qu'une constante SPEED est définie à 60 ce qui évite de faire de la casse. Une constante DISTANCE_IR est définie à 35 et devra être utilisée dans le test après réception.
Tout ce qui précède peut être utilisé pour détecter et donc éviter les obstacles. L’idée générale est très simple :
- on initialise le compteur "count" à 0
- On émet à droite 24 impulsions plusieurs fois, on compte ensuite ce que l’on reçoit (cela est réalisé automatiquement à l'aide d'une interruption).
- si c’est supérieur à 20, on recule suffisamment on tourne du bon côté et on repart en marche avant.
- on initialise le compteur à 0
- On émet à gauche 24 impulsions plusieurs fois, on compte ce que l’on reçoit.
- si c’est supérieur à 20, on recule suffisamment on tourne du bon côté et on repart en marche avant.
La mise au point de ce code peut prendre beaucoup de temps.
Indications : le code pour émettre plusieurs fois peut être par exemple :
count = 0;
for(i=0;i<20;i++) { //left transmitter sends 20 pulses
L_Send40KHZ();
delayMicroseconds(600);
}
if(count>DISTANCE_IR)//if recieved a lot pulse , it means there's a obstacle
Ce code émet 20 fois 24 impulsions.
Le DISTANCE_IR du "if(count>DISTANCE_IR)" peut être défini par une constante. Divers essais chez moi on montré que la valeur 35 est un bon point de départ. Si vous augmentez cette valeur vous détecterez les obstacles plus près eet si vous la diminuez ce sera le contraire. Il est défini comme une constante DISTANCE_IR dans le code de la section suivante et nous vous encourageons très fortement d’utiliser cette technique pour la mise au point.
Travail à faire : exercice 3
modifierOn vous donne un labyrinthe dont vous ne connaissez pas la forme et on vous demande de le parcourir, si possible sans heurter les bords.
Utilisation de l'infra-rouge pour suivre une ligne
modifierLe robot mini-Q est équipé de cinq couples leds/photo-transistors (infra-rouge) dirigés vers le bas. Ils sont destinés à mesurer la réflexion du support sur lequel roule le robot. Si vous prenez un support blanc et que vous fixez un scotch noir vous arriverez à détecter le support noir qui n'a pas la même réflectance infra-rouge que le support blanc.
La mise au point sera difficile car sur le robot que nous avons essayé les sensibilités des capteurs ne sont pas identiques. La calibrage de chacun des capteurs peut se faire par le programme simple :
//*********** Code Arduino à transformer en C pur pour ce document
int data[5]={0X00,0X00,0X00,0X00,0x00};//save the analog value
void setup(){
Serial.begin(9600);
}
void Read_IRLine(void){ //read the analog value
// les capteurs A0, A1 sont peu sensibles sur le robot essayé !!!
data[0]=analogRead(A0); // gauche
data[1]=analogRead(A1);
data[2]=analogRead(A2);
data[3]=analogRead(A3);
data[4]=analogRead(A7); // droite
}
void loop(){
Read_IRLine();
for (char i=0;i<5;i++){
Serial.print(data[i]);
Serial.print(" ");
}
Serial.println(" ");
delay(1000);
}
qui affichera toutes les secondes les valeurs des 5 photo-transistors infra-rouges. On ne désire pas utiliser ce programme puisque nous avons comme objectif de faire du C pur. Vous allez alors constater que le ce simple programme est bien plus long en C pur, mais nous allons le réaliser en plusieurs étapes.
Travail à réaliser : Exercice 4
modifierLe travail à réaliser se décompose en plusieurs questions.
Ce travail consiste à utiliser la liaison série pour calibrer vos 5 capteurs. Calibrer veut dire ici, trouver les seuils en-dessous desquels on considère que l’on a du noir. Ces seuils ne sont pas tous identiques pour tous les robots et pour un même robot pour tous les capteurs. Pour les trouver, vous positionnez une fois le capteur sur du blanc, une fois sur du noir et le seuil sera à mi-chemin.
Un moyen de faire cela de manière assez facile à ajuster consiste à définir des constantes comme ci-dessous :
#define SEUILGG 985
#define SEUILG 970
#define SEUIL 750
#define SEUILD 700
#define SEUILDD 750
GG veut dire le plus à gauche, DD le plus à droite.
Les seuils se trouvent en mettant le robot en marche pour que son alimentation se fasse avec les accumulateurs et non l'USB !
Les seuils donnés ici sont dépendants du robot dont vous disposez. Il vous faudra bien sûr les adapter à votre robot.
Question 1
modifierOn désire calibrer les cinq capteurs avec une plaque blanche de test comprenant une piste en chatterton noir. Pour cela on va d’abord régler les problèmes de la liaison série. On vous demande de partir de la librairie ci-dessous (voir le chapitre correspondant) :
#define F_CPU 16000000 // {{unité|16|MHz}} oscillator.
#define BaudRate 9600
#define MYUBRR (F_CPU / 16 / BaudRate ) - 1
unsigned char serialCheckRxComplete(void)
{
return( UCSR0A & _BV(RXC0)) ; // nonzero if serial data is available to read.
}
unsigned char serialCheckTxReady(void)
{
return( UCSR0A & _BV(UDRE0) ) ; // nonzero if transmit register is ready to receive new data.
}
unsigned char serialRead(void)
{
while (serialCheckRxComplete() == 0) // While data is NOT available to read
{;;}
return UDR0;
}
void serialWrite(unsigned char DataOut)
{
while (serialCheckTxReady() == 0) // while NOT ready to transmit
{;;}
UDR0 = DataOut;
}
void serialInit(void) {
//Serial Initialization
/*Set baud rate 9600 */
UBRR0H = (unsigned char)(MYUBRR>>8);
UBRR0L = (unsigned char) MYUBRR;
/* Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Frame format: 8data, No parity, 1stop bit */
UCSR0C = (3<<UCSZ00);
}
On vous demande de réaliser un sous-programme "void serialWrite16(uint16_t can)" capable d'afficher la variable 16 bits "can" en clair et en décimal sur votre liaison série.
Indication : voici un code qui affiche en hexadécimal :
void serialPutsHexa(uint16_t can){
int8_t i=0,digit=0;
char char_digit;
serialWrite('0');serialWrite('X');
for (i=12;i>-1;i-=4) {// only four digits
digit = (can >> i) & 0x0F;
char_digit=digit+0x30;
if (char_digit>0x39) char_digit += 7;
serialWrite(char_digit);
}
}
Il suffit donc de le modifier en calculant les milliers, les centaines, les dizaines et les unités et en les transformant en caractères.
Question 2
modifierIl va nous falloir maintenant lire les convertisseurs analogique numérique. Le prototype de ce que l’on cherche à faire est :
uint16_t readAnalog(uint8_t mux);
Indication :
void initAnalog() {
ADMUX = (1 << REFS0);//reference Vcc
// Start the ADC unit,
// set the conversion cycle 16 times slower than the duty cycle
ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADSC);
}
uint16_t readAnalog(uint8_t mux) {
uint16_t result;
ADMUX |= (mux & 0x0F); // mettre les 1 sans modifier
ADMUX &= ~(mux & 0x0F); // mettre les 0 sans modifier
ADCSRA |= (1 << ADSC); // start
while (ADCSRA & (1 << ADSC));
result = ADCH;
result <<= 8;
result += ADCL;
return result;
}
Question 3
modifierUne fois les seuils de chaque capteur trouvé, convertir l’ensemble des données en un nombre binaire sur 5 bits. Le bit de poids faible sera à 1 si une ligne noire est présente sous le capteur A0 et ainsi de suite jusqu'au bit b4. Tester de nouveau avec un affichage binaire.
Indication : vous pourrez vous inspirer du code Arduino d'un un travail dévaluation donné à l'IUT de Troyes et en particulier du code suivant :
int data[5]={0X00,0X00,0X00,0X00,0x00};//save the analog value
#define SEUILGG 941
#define SEUILG 500
#define SEUIL 400
#define SEUILD 880
#define SEUILDD 600
char Read_IRLine(void){ //read the analog value
char result=0;
data[0]=readAnalog(0); // gauche
data[1]=readAnalog(1);
data[2]=readAnalog(2);
data[3]=readAnalog(3);
data[4]=readAnalog(7); // droite
if (data[0] < SEUILGG) result |= 0x01; else result &= 0xFE;
if (data[1] < SEUILG) result |= 0x02; else result &= 0xFD;
if (data[2] < SEUIL) result |= 0x04; else result &= 0xFB;
if (data[3] < SEUILD) result |= 0x08; else result &= 0xF7;
if (data[4] < SEUILDD) result |= 0x10; else result &= 0xEF;
return result;
}
qui convertit les 5 données analogiques des capteurs infra-rouge en un nombre binaire sur 5 bits. Ce nombre peut ainsi être utilisé dans un "switch". Cherchez les valeurs autorisées de Read_IRLine.
Question 4
modifierVous commencerez ensuite à essayer de réaliser un programme qui suit une ligne fermée en avançant. Le principe est d'accélérer le bon moteur quand on dévie de la ligne.
La valeur 0x00 sera traitée comme valeur aberrante dans un premier temps : on a perdu la piste donc on s'arrête. Maisdans un deuxième temps, vous pouvez aussi faire ce que nous a fait un étudiant : faire marche arrière dans ce cas précis... et c’est pas mal du tout comme idée.
Capteurs infra-rouge sur les roues pour se repérer
modifierLes roues sont réalisées de telle façon qu'un photo-coupleur infrarouge permette de compter des ticks. Il y en a environ 8 par tours, cela donne une résolution assez faible. Nous ne chercherons donc pas à réaliser des calculs de trajectoires précis.
Le travail de calcul de trajectoire à partir de données internes s’appelle Navigation à l'estime et cela réalise une intégration (d'un point de vie mathématique). Le meilleur moyen de s'en persuader est d'imaginer les conséquences de petites erreurs sur des points de mesure. L'incertitude de la position augmente au fur et à mesure du déplacement.
Pourquoi le problème de se repérer est difficile
modifierAvant de se poser des questions il faut essayer de bien comprendre ce que veut dire connaître exactement l'état du robot. Celui-ci est donné par trois paramètres même si l’on est en deux dimensions : . x et y sont naturellement le repérage de la position sur les deux axes x et y et est l'orientation du robot par rapport à l'axe des x. Regardez la figure ci-contre pour vous convaincre de la pertinence de ces trois données.
Imaginons que vous ayez fait déplacer votre robot pendant un temps T quelconque. Vous relevez les compteurs de chacune des roues et convertissez en centimètres parcourus et trouvez deux nombres A et B. Est-il possible d’en déduire la nouvelle position (et orientation) sachant que l’on connaît exactement la position de départ ? Autre manière de poser le problème : je connais parfaitement ainsi que la distance parcourue par chacune des roues est-il possible d’en déduire ? Malheureusement la réponse à cette question est partiellement négative ! Il n'est possible que d’avoir mais pas le reste.
Tout cela est résumé simplement par la figure ci-contre. Dans cette figure, les deux longueurs (en pointillés) rouges sont identiques (et égales à A) et les deux longueurs (en pointillés) bleues sont elles aussi identiques (et égales à B) et pourtant on obtient deux points d'arrivées différents. Seule l'orientation finale est identique. Cela montre qu'aucune formule ne permet le calcul de xf à partir de xd, A et B.
Doit-on abandonner l’idée de trouver le point final à partir du point de départ ? Non, heureusement. En fait le problème présenté devient possible si les deux distances parcourues A et B deviennent différentielles (c'est-à-dire toutes petites). C'est quand même une mauvaise nouvelle malgré tout. Cela veut dire qu’il nous faut mémoriser les distances parcourues par chacune des roues à des instants précis et assez fréquemment pour se rapprocher de l’idée mathématique de différentielle. Nous essaierons dans un premier temps dix mesures par seconde. Nous utiliserons des compteurs sur 8 bits en complément à deux... et dix fois par seconde nous mémoriserons donc les valeurs des deux compteurs (droit et gauche) dans une mémoire.
Code de départ
modifierVoici le code permettant d’utiliser les capteurs. Il commence par une initialisation :
//encoder value
void interrupt01_init(void)
{
EICRA = 0X0F;
EIMSK = 0X03;
}
Puis vient lesinterruptions proprement dites qui incrémentent chacune son compteur.
ISR(INT0_vect)//motor encoder interrupt
{
if(++count_r==120)
count_r=0;
}
ISR(INT1_vect)
{
if(++count_l==120)
count_l=0;
}
Travail à faire : exercice 5
modifierOn vous demande d'exécuter votre code de suivi de ligne sur une forme inconnue d'avance et d’utiliser les photo-transistors infra-rouge des roues pour reconstruire la trajectoire suivie.
- l'évaluation du programme sera fait sans référence particulière : vous ne savez pas quand vous revenez au point de départ (aucune marque sur le circuit)
- l'évaluation du programme est fait avec une référence particulière. Une barre perpendiculaire à la trajectoire (donc détectable avec les photo-transistors) est présente au point de départ et au point d'arrivée. Cette contrainte forte devrait permettre de diminuer les erreurs sur la trajectoire.
Travail à faire : exercice final
modifierVotre robot sera enfermée dans une pièce/labyrinthe (maquette de taille en rapport avec le robot) ayant une forme non connue à l'avance. Il devra se déplacer en évitant les murs et essayer en même temps de déduire la forme de la pièce.
MiniQ version 2
modifierVoici un site commercial qui propose de la documentation et une présentation de la version 2 (compatible Arduino Leonardo) du Robot mobile miniQ. Contrairement à la version 1, tous les codes proposés comme exemples sont fournis en langage Arduino. Ce qui est présenté ici est, par contre, en code C standard.
On rappelle que l’utilisation du code C nécessite une entête. Elle sera de la forme :
#include <avr/io.h>
//#include <avr/interrupt.h>
#undef F_CPU
#define F_CPU 16000000UL
#include <util/delay.h>
Boutons poussoirs et Conversion Analogique Numérique (Robot version 2)
modifierUne nouvelle version du robot MiniQ est disponible depuis 2015. La grande différence est qu’il est architecturé autour du 32U4 au lieu de l'ATMega328. Cela peut paraître anecdotique, mais les périphériques du 32U4 sont relativement différents de ceux du 328 en ce qui concerne la conversion analogique numérique et la MLI.
Le robot MiniQ (version 2) possède trois interrupteurs montés comme dans la figure ci-contre. Le point important est le numéro du convertisseur (AN8). Notez aussi le nombre d'interrupteurs : 3 (qui sont appelés de KEY1 à KEY3. KEY1 est du côté gauche (avec la convention usuelle du regard dirigé vers l'avant).
Trouver l'interrupteur appuyé
modifierExercice 1 :
1°) Montrer que vous savez faire un programme Arduino qui utilise A6 (Conversion Analogique Numérique) et affiche dans un moniteur série les différentes valeurs suivant le/les boutons appuyés. Remplir alors le tableau ci-dessous.
Key3 | Key2 | Key1 | ADC8 (V et N) |
---|---|---|---|
0 | 0 | 0 | ? |
0 | 0 | 1 | ? |
0 | 1 | 0 | ? |
0 | 1 | 1 | ? |
1 | 0 | 0 | ? |
1 | 0 | 1 | ? |
1 | 1 | 0 | ? |
1 | 1 | 1 | ? |
Faire un programme qui affiche correctement (en clair) la touche appuyée. Pour cela on cherchera les points milieux des valeurs trouvées dans le tableau.
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
}
void loop() {
// put your main code here, to run repeatedly:
Serial.println(analogRead(A6));
delay(1000);
}
Les valeurs trouvées expérimentalement sont :
- KEY1 (sw3) appuyé : CAN8 = 135
- KEY2 (sw4) appuyé : CAN8 = 575
- KEY3 (sw5) appuyé : CAN8 = 722
2°) Écrire un sous-programme en C pur d'initialisation de la conversion analogique numérique.
void initCAN8() {
DIDR2 |= (1<<ADC8D); // disable digital input
//ADMUX |= (1<<MUX2)|(1<<MUX1);//ADC6 pour KEYx NON!!!!
ADCSRB |= (1<<MUX5);//ADC8 pour KEYx
ADMUX |= (1<<REFS0); //reference à 5V
ADCSRA |= (1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0); // division par 128 : pas pressé
ADCSRA |= (1<<ADEN);//|(1<<ADATE); // demarrage logique de conversion
ADCSRA |= (1<< ADSC); // start first conversion
while((ADCSRA & (1<< ADSC)) != 0); // attente fin conversion
//_delay_ms(10);
}
Conversion en binaire
modifierContinuer l'exercice précédent en réalisant un sous-programme qui retournera un nombre binaire représentant l'interrupteur appuyé.
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | key3 | key2 | key1 |
Ce tableau indique que quand key1 est appuyé on a un 1 dans b0 tandis que si c’est key3 le 1 correspondant est dans b2....
Le prototype de la fonction demandée sera :
unsigned char conversion(int can);
Touches incrémentation/décrémentation
modifierRéaliser un sous-programme capable d'incrémenter/décrémenter une variable (de type unsigned char donc variant de 0 à 255) en fonction de l'appui sur les boutons.
Utilisation du timer 4 pour commander les moteurs
modifierLes moteurs du miniQ version 2 sont commandés par le timer4. Il s'agit d'un timer assez complexe, qui peut fonctionner sur 10 bits et permet la commande de transistors de puissance FET (commandes inversées avec temps morts). Heureusement, nous nous contenterons du fonctionnement simple sur 8 bits.
Un exemple pour le moteur gauche
modifierLe moteur gauche est commandé par la broche PC6(=/OC4A) du PORTC.
void initMoteurG() {
DDRC |=0x40; // PC6 en sortie
TCCR4B |= 0x06; // division par 64
TCCR4A |= 0x42; // PWM en et /OC4A
TCCR4E |= 0x01; // 0C4OE0 pour /OC4A
}
void setVitesseG(unsigned char speedG) {
OCR4A = 255-speedG; // rapport cyclique
}
main() {
// put your setup code here, to run once:
initMoteurG();
setVitesseG(155);
while(1);
}
La sélection marche avant marche arrière n’est pas réalisée. Passons maintenant à l'autre moteur.
Un exemple pour le moteur droit
modifierLe moteur droit est connecté à la sortie PD7/OC4D du PORTD. Voici le code permettant de gérer correctement le moteur droit.
void initMoteurD() {
DDRD |= 0x80; // PD7 en sortie
TCCR4B |= 0x06; // division par 64
TCCR4C |= 0x09; // PWM en et OC4D et sans /OC4D
TCCR4E |= 0x20; // 0C4OE5 pour OC4D
}
void setVitesseD(unsigned char speedD) {
OCR4D = speedD; // rapport cyclique
}
Travail à faire
modifierExercice 2 : Reprendre les exemples de code et refaire une initialisation commune pour les deux moteurs. On prendra alors soin de remplacer l'hexadécimal par le nom des bits.
void initpwm() {
DDRC |= (1<<PC6); // PC6 en sortie : moteur gauche
DDRD |= (1<<PD7); // PD7 en sortie : moteur droit
TCCR4B |= (1<<CS42)|(1<<CS41); // division par 64
// moteur gauche
TCCR4A |= (1<<COM4A0)|(1<<PWM4A); // PWM en et /OC4A
TCCR4E |= (1<<OC4OE0); // 0C4OE0 pour /OC4A
// moteur droit
TCCR4C |= (1<<COM4D1)|(1<<PWM4D); // PWM en et OC4D
TCCR4E |= (1<<OC4OE5); // 0C4OE5 pour OC4D
}
Remarquez que l’on n'a pas géré les directions de rotations des deux moteurs.
Exercice 3 : On désire reprendre le travail de lecture des interrupteurs en C (voir plus haut). Écrire un programme C capable de lire ADC8 (ATTENTION c’est A6 pour l'Arduino). L'appui sur le bouton gauche KEY1 fera tourner le moteur gauche, tandis que l'appui sur le bouton droit fera tourner le moteur droit.
void setVitesseG(unsigned char speedG) {
OCR4A = 255-speedG; // rapport cyclique
}
void setVitesseD(unsigned char speedD) {
OCR4D = speedD; // rapport cyclique
}
void initpwm() {
DDRC |= (1<<PC6); // PC6 en sortie : moteur gauche
DDRD |= (1<<PD7); // PD7 en sortie : moteur droit
TCCR4B |= (1<<CS42); // division par 8
// moteur gauche
TCCR4A |= (1<<COM4A0)|(1<<PWM4A); // PWM en et /OC4A
TCCR4E |= (1<<OC4OE0); // 0C4OE0 pour /OC4A
// moteur droit
TCCR4C |= (1<<COM4D1)|(1<<PWM4D); // PWM en et OC4D
TCCR4E |= (1<<OC4OE5); // 0C4OE5 pour OC4D
}
void initCAN8() {
DIDR2 |= (1<<ADC8D); // disable digital input
//ADMUX |= (1<<MUX2)|(1<<MUX1);//ADC6 pour KEYx NON!!!!
ADCSRB |= (1<<MUX5);//ADC8 pour KEYx
ADMUX |= (1<<REFS0); //reference à 5V
ADCSRA |= (1<<ADPS1)|(1<<ADPS0); // division par 8 : pas pressé
ADCSRA |= (1<<ADEN);//|(1<<ADATE); // demarrage logique de conversion
ADCSRA |= (1<< ADSC); // start first conversion
while((ADCSRA & (1<< ADSC)) != 0); // attente fin conversion
//_delay_ms(10);
}
main() {
uint16_t can8;
// put your setup code here, to run once:
initpwm();
initCAN8();
while(1){
ADCSRA |= (1<< ADSC); // start conversion
while((ADCSRA & (1<< ADSC)) != 0);// attente fin conversion
can8 = ADC;
if (can8 > 800) {
setVitesseG(0);
setVitesseD(0);
} else if (can8 > 600) {
setVitesseD(155);
setVitesseG(0);
} else if (can8 > 200) {
setVitesseD(0);
setVitesseG(0);
} else {
setVitesseD(0);
setVitesseG(155);
}
_delay_ms(100);
}
Remarquez que l’on n'a pas géré les directions de rotations des deux moteurs.
Exercice 4 : refaire un sous-programme qui gère les deux moteurs à la fois et le sens. On a déjà utilisé ce genre de sous-programme, il avait le prototype :
void Motor_Control(char MG_DIR,char MG_EN,char MD_DIR,char MD_EN);
Indication : les commandes de sens pour respectivement le moteur droit et le moteur gauche sont les broches PE6 et PD6.
#define FORW 1
#define BACK 0
//...........
void initpwm() {
DDRC |= (1<<PC6); // PC6 en sortie : moteur gauche
DDRD |= (1<<PD7); // PD7 en sortie : moteur droit
TCCR4B |= (1<<CS42)|(1<<CS41); // division par 64
// moteur gauche
TCCR4A |= (1<<COM4A0)|(1<<PWM4A); // PWM en et /OC4A
TCCR4E |= (1<<OC4OE0); // 0C4OE0 pour /OC4A
// moteur droit
TCCR4C |= (1<<COM4D1)|(1<<PWM4D); // PWM en et OC4D
TCCR4E |= (1<<OC4OE5); // 0C4OE5 pour OC4D
// pendant qu'on y est on prépare tout pour pouvoir changer de sens
DDRE |= (1<<PE6); // sens moteur droit;
DDRD |= (1<<PD6); // sens moteur gauche
}
void Motor_Control(char MG_DIR,char MG_EN,char MD_DIR,char MD_EN) {
// moteur gauche
if(MG_DIR==FORW)//si vers l'avant
PORTD &=~(1<<PD6);//IN2 passé à 0
else
PORTD |=(1<<PD6);//IN2 passé à 1
OCR4A = 255-MG_EN; // rapport cyclique ou setVitesseG(MG_EN);
// puis moteur droit
if(MD_DIR==FORW)//si vers l'avant
PORTE &=~(1<<PE6);//IN1 passé à 0
else
PORTE |=(1<<PE6);//IN1 passé à 1
OCR4D = MD_EN; // rapport cyclique ou setVitesseD(MD_EN);
}
Suivi de lumière en C
modifierDans cette section nous désirons réaliser un programme permettant au robot de suivre une source de lumière.
Programme de départ (version Arduino)
modifiervoid setup() {
pinMode(5,OUTPUT);//EN1 init the pins for motor
pinMode(12,OUTPUT);//IN1=M1_DIR
pinMode(6,OUTPUT);//EN2
pinMode(7,OUTPUT);//IN2=M2_DIR
}
void MotorG(unsigned char M1_DIR,unsigned char M1_EN)//control the motor
{
//////////M1-->left motor////////////////////////
if(M1_DIR==0)//en avant
digitalWrite(12,0);
else
digitalWrite(12,1);
if(M1_EN==0)
analogWrite(5,LOW);
else
analogWrite(5,M1_EN);
}
void MotorD(unsigned char M2_DIR,unsigned char M2_EN)//control the motor
{
//////////M2-->right motor////////////////////////
if(M2_DIR==0)//en avant
digitalWrite(7,0);
else
digitalWrite(7,1);
if(M2_EN==0)
analogWrite(6,LOW);
else
analogWrite(6,M2_EN);
}
void loop() {
// put your main code here, to run repeatedly:
int n;
unsigned char keys;
n=analogRead(A5);
if (n > 800) {
MotorG(0,75);
MotorD(1,75);
} else if (n > 600) {
MotorG(0,50);
MotorD(1,50);
} else if (n > 350) {
MotorG(0,0);
MotorD(0,0);
} else if (n > 350) {
MotorG(1,50);
MotorD(0,50);
} else {
MotorG(1,75);
MotorD(0,75);
}
delay(50);
}
Exercice 5
modifierÉliminer toute la partie non nécessaire pour calibrer les capteurs de lumière en utilisant la liaison série.
Exercice 6
modifierReprendre le code donné et utiliser toute la partie d'utilisation des moteurs en C déjà réalisé auparavant.
Exercice 7
modifierRéaliser l'initialisation convertisseur analogique numérique CAN0. On est prêt maintenant à retirer le setup() et le loop() pour ne faire qu'un main.
Suivi de ligne en C
modifierLe premier problème à résoudre est de calibrer les cinq seuils des cinq capteurs infrarouges. L'expérience nous a montré qu'ils peuvent avoir des réponses assez différentes et donc que cette étape est nécessaire.
Comment calibrer correctement les capteurs pour trouver les seuils
modifierCeci peut être réalisé par un simple programme Arduino qui nous permet de garder la liaison série :
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
}
void loop() {
int data[5];
uint8_t i;
// put your main code here, to run repeatedly:
for (i=0;i<5;i++)
{
data[i]=analogRead(i);//store the value read from the sensors
Serial.print(data[i]);Serial.print(" - ");
}
Serial.println();
delay(500);
}
mais nécessite de graver la séquence d'initialisation (bootloader) si vous utilisez un programmateur. Cela prend environ 1mn.
Un essai avec un Robot m'a donné les résultats suivants.
Position du capteur | extrême gauche | gauche | centre | droite | extrême droite |
broche µc | 36 | 37 | 38 | 39 | 40 |
entrée CAN | ADC7 | ADC6 | ADC5 | ADC4 | ADC1 |
entrée Arduino | A0 | A1 | A2 | A3 | A4 |
BLANC | 836 | 982 | 980 | 974 | 970 |
NOIR | 210 | 390 | 338 | 305 | 305 |
Seuils | 523 | 686 | 659 | 639 | 637 |
Abandon de la gestion des convertisseurs Analogique sous Arduino
modifierNous voulons maintenant faire la même chose que le programme précédent mais avec une gestion des Convertisseurs Analogiques Numériques en C.
void initCANs() {
ADMUX |= (1<<REFS0); //reference à 5V
ADCSRA |= (1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0); // division par 128 : pas pressé
ADCSRA |= (1<<ADEN);//|(1<<ADATE); // demarrage logique de conversion
ADCSRA |= (1<< ADSC); // start first conversion
while((ADCSRA & (1<< ADSC)) != 0); // attente fin conversion
}
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
initCANs();
}
void loop() {
int data[5];
uint8_t muxs[]={7,6,5,4,1};
uint8_t i;
// put your main code here, to run repeatedly:
for (i=0;i<5;i++){
// choix des CANx avec MUX2,MUX1,MUX0
ADMUX &= 0xF8; // mise à 0 de MUX2 MUX1 MUX0
ADMUX |= muxs[i]; //choix CANx
delay(1); // au cas où un peu de temps est nécessaire
ADCSRA |= (1<< ADSC); // start conversion
while((ADCSRA & (1<< ADSC)) != 0); // attente fin conversion
data[i]=ADC;//store the value read from the sensors
Serial.print(data[i]);Serial.print(" - ");
}
Serial.println();
delay(500);
}
Gestion du suivi de ligne en C
modifierNous disposons d'à peu près de tout le code nécessaire à résoudre le problème avec tout ce que l'on vient de faire. Mais il nous faut de la remise en forme.
Exercice 8
modifierLe premier sous-programme que nous allons réaliser est un sous programme qui s'appelle "sense5IR()". Son objectif est de retourner une valeur pondérée. L'idée est d'allouer un poids aux 5 capteurs infrarouges :
Position du capteur | extrême gauche | gauche | centre | droite | extrême droite |
entrée CAN | ADC7 | ADC6 | ADC5 | ADC4 | ADC1 |
Poids | +24 | +12 | 0 | -12 | -24 |
Si chaque fois que vous avez un capteur qui détecte du noir vous ajoutez son poids et que vous divisez par le nombre de capteurs qui détectent du noir, vous aurez une valeur discrète entre +24 et -24. C'est cette valeur discrète que devra nous retourner "sense5IR()".
Écrire la fonction "int16_t sense5IR()" en lui ajoutant "void initCANs()".
int16_t sense5IR() {
uint8_t muxs[]={7,6,5,4,1};
uint8_t i,nbCapteurs;
int16_t data[5],ponderation[5]={24,12,0,-12,-24};
int16_t seuils[5]={523,686,659,639,637}; // dépendent du robot
int16_t result;
for (i=0;i<5;i++){
// choix des CANx avec MUX2,MUX1,MUX0
ADMUX &= 0xF8; // mise à 0 de MUX2 MUX1 MUX0
ADMUX |= muxs[i]; //choix CANx
_delay_ms(1); // au cas où un peu de temps est nécessaire
ADCSRA |= (1<< ADSC); // start conversion
while((ADCSRA & (1<< ADSC)) != 0); // attente fin conversion
data[i]=ADC;//store the value read from the sensors
} // for
// calcul pondéré : mise en forme
result=0;nbCapteurs=0;
for (i=0;i<5;i++){
if (data[i]<seuils[i]) {// si noir
result += ponderation[i];
nbCapteurs++;
}
}
return result/nbCapteurs;
}
void initCANs() {
ADMUX |= (1<<REFS0); //reference à 5V
ADCSRA |= (1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0); // division par 128 : pas pressé
ADCSRA |= (1<<ADEN);//|(1<<ADATE); // demarrage logique de conversion
ADCSRA |= (1<< ADSC); // start first conversion
while((ADCSRA & (1<< ADSC)) != 0); // attente fin conversion
}
Exercice 9
modifierLa boucle d'exécution du programme d'un robot est en générale caractérisée par le paradigme sense plan act :
- lire des capteurs (sense en anglais)
- planifier (plan en anglais)
- agir (act en anglais)
Écrire la partie planification et le programme principal associé.
Indication : La planification consiste à calculer les vitesses des moteurs droit et gauche à partir du résultat de la fonction "sense5IR()". L'idée générale est d'augmenter et diminuer les vitesses des moteurs pour rattraper la ligne suivie. Une relation de proportionnalité peut être choisie, mais il est possible de réaliser ici un régulateur PID.
#include <avr/io.h>
#undef F_CPU
#define F_CPU 16000000UL
#include <util/delay.h>
//********** Leonardo ATMega32U4
#define FORW 1
#define BACK 0
//...........
void initpwm();
void initCANs();
void Motor_Control(char MG_DIR,char MG_EN,char MD_DIR,char MD_EN);
int16_t sense5IR();
int main() {
int16_t SenseResult;
uint8_t speedL=60,speedR=60,w=0;
// setup()
initpwm();
initCANs();
//loop()
while(1) {
// sense
SenseResult=sense5IR();
//plan : rester sur la ligne
speedL=60;
speedR=60;
// P : Gain adjustment
w=2*SenseResult;
speedR += w;
speedL -= w;
// act
Motor_Control(FORW,speedL,FORW,speedR);
} // while(1)
return 0;
}
Nous présentons une version préliminaire avec PID, enfin plutôt PD. La partie intégrale d'un PID doit saturer si l'on ne veut pas que ce terme devienne prépondérant.
Contrairement au code précédent, nous avons réussi à le faire fonctionner avec une vitesse de 80.
Tout ceci doit être amélioré.
int main() {
int16_t SenseResult;
uint8_t speedL=60,speedR=60,w=0;
int16_t P,I,D,kP=3,kI=2,kD=2,Integral=0,Last=0;
// setup()
initpwm();
initCANs();
//loop()
while(1) {
// sense
SenseResult=sense5IR();
//plane : rester sur la ligne
speedL=80;
speedR=80;
P = SenseResult * kP;
Integral = Integral + SenseResult; // A saturer !!!
//I = Integral*kI;
I=0;
D = (Last-SenseResult)*kD;
w = P + I + D;
speedR += w;
speedL -= w;
// save current value for next time
Last = SenseResult;
// act
Motor_Control(FORW,speedL,FORW,speedR);
} // while(1)
return 0;
}
À ce stade vous disposez de tout le nécessaire pour réaliser un suivi de ligne.
Détection d'obstacle en C
modifierPrincipe
modifierSont présents sur le robot, un détecteur infrarouge et deux émetteurs dirigés respectivement vers la droite et vers la gauche. Ils permettent de savoir si le nombre d'impulsions reçu (par écho) peut laisser penser à la présence d'un obstacle. Le détecteur est relié à INT0/PB0 et peut donc être géré par une interruption qui ne fera que compter le nombre d'impulsions reçu. À noter que cette interruption est incapable, par elle-même de savoir qui a émis, la droite ou la gauche.
L'idée générale est donc d'envoyer un train d'impulsions en émission et de compter ce que l'on reçoit.
Récepteur
modifierNous ne nous attarderons pas sur la partie récepteur. Il suffit de configurer l'interruption associée à la broche PB0 et d'incrémenter une variable :
volatile uint8_t count=0;
ISR(PCINT0_vect) {
count++;//on incrémente le compteur pour chaque impulsion reçue}
}
// autorisation interruption PCINT0
void setPCINT0() {
PCICR = (1<<PCIE0);
PCMSK0 = (1<<PCINT0); //Autorisation de l'interruption INT0
}
Ajouter ce code à votre programme.
Émetteur
modifierLe robot dispose de 2 émetteurs dont le tableau suivant donne les informations importantes :
Position | Broche | Timer |
---|---|---|
Droite | PB4 | ---- |
Gauche | PC7 | OC4A |
Si vous suivez, il va falloir de nouveau configurer un timer ! Le tableau ci-dessus pourrait laisser penser qu'il faut prendre le timer 4, mais on rappelle que l'on utilise déjà ce timer pour la MLI des moteurs. On décide de prendre le TIMER0.
On rappelle que l'on aura besoin de code à travers une interruption pour basculer PB4 et PC7 puisqu'ils ne sont reliés à aucun OC0X.
Émetteurs et Timer0
modifierNous allons utiliser le mode comparaison du timer0 afin d'obtenir une fréquence très précise d'émission de 38 kHz.
Exercice 10
modifier- Choisir des valeurs de prédiviseur et de comparaison permettant d'obtenir une fréquence de comparaison de 76 kHz.
- Ajouter la configuration du timer a votre programme avec un sous-programme :
void setTIMER0_interrupt_ctc() {
// set PB4 and PC7 as output
// choix du prédiviseur
// mode remise à zéro sur comparaison (mode CTC = (010) contrairement à la figure !!!
// valeur de OCR0A
// autorisation de l'interruption de comparaison (bit OCIE0A du registre TIMSK0)
}
- Ajouter l'interruption de comparaison associée a votre programme. N'oubliez pas que cette interruption doit émettre soit du côté droit soit du côté gauche. Il faudra donc lui associer une variable globale "volatile uint8_t left;" qui conditionnera si l'émission se fait à droite ou à gauche. Par exemple :
volatile uint8_t left=0;
ISR(TIMER0_COMPA_vect)
{
if (left)
PORTC ^= (1<<PC7);
else
//.....?????
}
À ce point, il faut bien comprendre ce que l'on est capable de faire :
- un sei() dans le programme principal déclenchera automatiquement une émission à droite ou à gauche (suivant la valeur de la variable "left").
- par conséquent l'interruption "ISR(PCINT0_vect)" comptera toujours les éventuels échos.
- si vous voulez arrêter d'émettre il vous faudra soit réaliser un cli() (déconseillé) soit désactiver l'interruption ISR(TIMER0_COMPA_vect) avec le bit OCIE0A du registre TIMSK0.
Émission droite/gauche
modifierPour commencer on utilisera les primitives "_delay_us()" pour attendre quelques microsecondes.
- left <- 0
- count <- 0
- Répéter 20 fois :
- Émettre pendant 300us sur l'émetteur droit en autorisant l'interruption de comparaison
- Arrêter d'émettre
- On coupe l'émission pendant 600 us
- Lire valeur du compteur de réception. S'il est plus grand que 20 on prend les mesures appropriées (recul puis rotation appropriée puis marche avant)
On doit faire la même chose avec l'émission à gauche.
- left <- 1
- count <- 0
- Répéter 20 fois :
- Émettre pendant 300us sur l'émetteur gauche en autorisant l'interruption de comparaison
- Arrêter d'émettre
- On coupe l'émission pendant 600 us
- Lire valeur du compteur de réception. S'il est plus grand que 20 on prend les mesures appropriées (recul puis rotation appropriée puis marche avant)
À vous de jouer, vous disposer maintenant d'un détecteur d'obstacles. Vous devez l'associer aux solutions de l'exercice 4 qui sont gestion complète des commandes des moteurs.
Exercice 11
modifier- Écrire un programme qui répond au cahier des charges suivant :
- le robot avance en ligne droite
- s'il y a un obstacle à droite (count >20 par ex) alors
- reculer
- tourner sur place vers la gauche
- on repart en marche avant
- s'il y a un obstacle à gauche (count >20 par ex) alors
- reculer
- tourner sur place vers la droite
- on repart en marche avant
Code à compléter avec les exercices précédents :
/*
* miniQ1.c
*
* Created on: 25 mai 2016
* Author: moutou01
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#undef F_CPU
#define F_CPU 16000000UL
#include <util/delay.h>
#define FORW 1
#define BACK 0
volatile uint8_t count=0,left=1;
ISR(PCINT0_vect)
{
count++;//on incrémente le compteur pour chaque impulsion reçue}
}
void setPCINT0()
{
PCICR = (1<<PCIE0);
PCMSK0 = (1<<PCINT0); //Autorisation de l'interruption INT0
}
ISR(TIMER0_COMPA_vect)
{
if (left)
PORTC ^= (1<<PC7);
else
PORTB ^= (1<<PB4);
}
void setTIMER0_interrupt_ctc() {
// mode CTC (010)
TCCR0B &= ~((1<<WGM02)|(WGM00));
TCCR0A |= (1<<WGM01);
//prediviseur par 1
TCCR0B |= (1<<CS00);
// maxi CTC
OCR0A = 210;
//Autorisation interruption comparaison
TIMSK0 |= (1<<OCIE0A);
}
main() {
uint16_t can8;
// put your setup code here, to run once:
initpwm();
//initCAN8();
setPCINT0();
setTIMER0_interrupt_ctc();
sei();
Motor_Control(FORW,75,FORW,75);//avanti
while(1){
left = 1;
count = 0;
for (uint8_t i=0;i<20;i++) {
_delay_us(300);
// on coupe l'émission en bloquant interruptions
TIMSK0 &= ~(1<<OCIE0A);
_delay_us(600);
// on rememt l'émission en autorisant les interruptions
TIMSK0 |= (1<<OCIE0A);
}// for
if (count > 20) {
Motor_Control(BACK,0,BACK,0);
_delay_ms(1);
Motor_Control(BACK,75,BACK,75);
_delay_ms(400);
Motor_Control(FORW,0,BACK,0);
_delay_ms(1);
Motor_Control(FORW,75,BACK,75);
_delay_ms(200);
}
Motor_Control(FORW,75,FORW,75);//avanti
left = 0;
count = 0;
for (uint8_t i=0;i<20;i++) {
_delay_us(300);
// on coupe l'émission en bloquant interruptions
TIMSK0 &= ~(1<<OCIE0A);
_delay_us(600);
// on rememt l'émission en autorisant les interruptions
TIMSK0 |= (1<<OCIE0A);
}// for
if (count > 20) {
Motor_Control(BACK,0,BACK,0);
_delay_ms(1);
Motor_Control(BACK,75,BACK,75);
_delay_ms(400);
Motor_Control(BACK,0,FORW,0);
_delay_ms(1);
Motor_Control(BACK,75,FORW,75);
_delay_ms(200);
}
Motor_Control(FORW,75,FORW,75);//avanti
}
}
Voir aussi
modifier- Cours et TP mini-Q à l'IUT de Troyes plutôt orienté Arduino.
- Cours et TP mini-Q (version 2) à l'IUT de Troyes plutôt orienté C.
- Site commercial qui propose de la documentation et une présentation de la version 2 (compatible Arduino Leonardo).