Projets:Perso:2013:Module de localisation

From Electrolab
Revision as of 23:17, 10 March 2014 by Mael (Talk | contribs)

Jump to: navigation, search

Système de balisage infrarouge

Cette page présente mes travaux actuels sur la fabrication d'un système de balisage infrarouge pour mobile.
Le système contient les fonctions suivantes :

  • Emission infrarouge de 4 balises autonomes,
  • Réception infrarouge sur une tourelle,
  • Contrôle d'un moteur pas à pas d'entrainement de la tourelle.

En plus de la fonction de triangulation associée au balisage, le système intègre en permanence deux entrées codeuses, assurant ainsi l'odométrie du mobile. Les données d'odométrie sont ensuite intégrées aux données de balisage en permanence par l'intermédiaire d'un filtre de Kalman.

Un troisième volet de ce projet est le calcul en permanence d'une "void area", basée sur un réseau de capteurs ultrason déployés sur le mobile.

Dans un second temps, un autre projet viendra compléter celui-ci avec la réalisation d'un système de contrôle commande des moteurs (en DC, stepper DC puis en BLDC) avec un calcul de trajectoire basé sur les splines de Catmull-Rom.


Localisation par balisage infrarouge

Le principe de balisage infrarouge consiste à utiliser des balises émettant un signal infrarouge que va détecter un plot central, déployé sur une structure mobile roulante par exemple. Le concept est développé dans le cadre de la coupe de France de robotique, et permet de disposer facilement d’une information de localisation sur la table de 3x2m. >>Table + balisage<<

Le but est d’obtenir la meilleur estimation de x et y afin d’avoir la position du robot sur la table, ainsi que son orientation. Le principe appliqué est celui de la triangulation, basée sur une mesure d’angle (il existe le même principe avec les distances, tout n’étant au final qu’une application des équations de trigonométrie). Le choix du balisage infrarouge est lié au faible coût de la mise en œuvre (une centaine d’euros tout compris) et à la relativement grande précision du système. Il sera couplé, dans la suite de ce document, à un dispositif d’odométrie et un dispositif de sonars permettant d’affiner la position courante du robot et de créer une « void area » autour du robot indiquant les zones accessibles ou non au robot. >>Table + odométrie<<


Microcontrôleur DsPIC

Cette section est dédiée à l'utilisation des microcontrôleurs DsPic de chez Microchip. Ces chips sont majoritairement utilisés dans le système présenté ici. Il est donc nécessaire de poser un certain nombre de bases sur leur utilisation, en particulier leur mise en oeuvre et les drivers disponibles.

Configuration générale

Les DsPic sont de petits microcontrôleurs 16 bits très puissants que l'on peut utiliser pour tout ou presque :

  • Contrôle de moteur par PWM,
  • Conversion analogique numérique 16 bits (ADC),
  • Acquisition de signaux numériques (Input Capture)
  • Communication CAN (natif), i2c (natif), UART (natif), SPI (natif), etc.
  • Une foultitude de timers,
  • etc.

Il existe plusieurs familles de DsPIC : 30F, 32, 33F, 33E. Seuls les 33F sont détaillés ici.
Dans une même famille, tous les microcontrôleurs utilisent les drivers de la même façon, avec le même code, sous réserve qu'ils disposent du driver. En effet, dans la famille des 33F il va exister pléthore de chips différents, la différence se situant au niveau du nombre de drivers, d'E/S, de timers, de place mémoire, etc.
Dans la suite, les explications seront apportées sur un DsPIC33FJ128MC802 ou un DsPIC33FJ64GP802, dont la principale différence est un PWM pour le premier. La programmation peut se faire en C ou en assembleur. Les exemples proposés sont en C, je reviendrais par la suite sur la manière de les programmer.

Schéma de câblage

Tous les microcontrôleurs de la famille des DsPIC33FJ sont câblés pareils : >> Schéma de câblage chip <<

Le connecteur J1 est utilisé pour la programmation du DsPic, en utilisant un ICD2 ou un Pickit 3.

  1.  !MCLR
  2. +Vcc
  3. GND
  4. PGEC1
  5. PGED1

Les ports suivants sont nécessairement câblés pour pouvoir utiliser le chip, conformément à la figure précédente :

  • !MCLR : La patte sert à la fois à la programmation et à la mise en marche du Chip : !MCLR = 0/NC - Chip inactif, !MCLR = VDD : Chip actif.
  • PGEDi/PGECi : broches de programmation, elles peuvent aussi avoir d'autres application lors de l'usage du chip. Il y a plusieurs mappages des PGEC/D possibles, j'utilise ici le premier.
  • VSS/AVSS : ground, 0 logique. La broche "A-" est normalement la branche utilisée pour faire passer de la puissance dans le chip, mais en règle général il vaut mieux tout avoir sur la même source et utiliser ensuite des composant fait pour pour les aspects puissance.
  • OSC1 / OSC2 : entrées du quartz. Voir le chapitre horloge pour le détail d'utilisation.
  • VDD/AVDD : 3,3V (+/- 10%)
  • VCAP : Cette entrée doit être reliée à la masse par un condo de 10µF, l'idéal étant un tantale polarisé (CMS ou non d'ailleurs). Sans ce condo, le chip ne marchera pas et ne pourra même pas être programmé.

Horloge

Les DsPIC peuvent être cadencés assez haut, mais ils utilisent pour cela une PLL interne (Phase Lock Loop). Le principe de la PLL est détaillé sur internet, nous ne verrons ici que l'application. Trois types d'horloges sont utilisées :

  • Quartz,
  • Oscillateur,
  • Horloge interne.

De manière générale, il est déconseillé d'utiliser des horloges trop rapides. En effet, les signaux "carrés" sont plus mauvais à ces fréquences. Une bonne pratique est de dédier un quartz de fréquence raisonnable (8 ou 10 MHz) à chaque chip, et d'en augmenter la fréquence par une PLL.

Utilisation d'un quartz

Les quartz sont des cristaux passifs que le microcontrôleur utilise pour générer une horloge. Le quartz est relié aux broches OSC1 et OSC2 du microcontrôleur. Les quartz utilisables sont de deux types, en fonction de leur vitesse d'horloge :

  • XT : quartz jusqu'à 10 MHz,
  • HS : quartz de 10 MHz à 40 MHz.

Chaque broche OSC1 et OSC2 est également relié à la masse (GND), par l'intermédiaire d'un condo de 15 µF. A noter que ça marche aussi sans le condo, mais ils sont recommandés dans la doc du chip.
Dans : MPLAB : Configure\configuration bits...

  • Oscillator mode : Primary oscillator (XT, HS, EC) w/ PLL
  • Internal External switch over mode : Start up device with FRC, then automatically switch to bla bla bla...
  • Primary oscillator source : HS Oscillator mode (pour cause de quartz utilisé à 10 MHz)

Nota : les quartz étant passifs, ils sont dédiés à un seul chip et unique chip, on ne peut pas utiliser un quartz pour plusieurs chips.

Utilisation d'un oscillateur

ADU

Utilisation de l'horloge interne

ADU

Configuration de l'horloge

Ci-dessous un bout de code permettant de paramétrer la PLL au démarrage du chip :

void oscConfig(void){
PLLFBD=30; // M=32
CLKDIVbits.PLLPOST=0; // N1=2
CLKDIVbits.PLLPRE=0; // N2=2
OSCTUN=0; // Tune FRC oscillator, if FRC is used
RCONbits.SWDTEN=0; // Disable Watch Dog Timer
__builtin_write_OSCCONH(0x03); // Initiate Clock Switch to Primary Oscillator with PLL (NOSC=0b011)
__builtin_write_OSCCONL(0x01); // Start clock switching
while (OSCCONbits.COSC != 0b011); // Wait for Clock switch to occur
while(OSCCONbits.LOCK!=1) {}; // Wait for PLL to lock
}

Ce code est prévu pour un quartz de 10 MHz, permettant de passer la fréquence de cycle du microcontrôleur à 40 MHz (une opération toutes les 1 / [40.E6], soit 25 ns).

Configuration des ports d'entrées / sorties

La configuration des ports d’E/S est réalisée de base par les trois registres suivants :

  • AD1PCFGL' : Ports analogiques, à positionner à [0xffff] pour les désactiver,
  • _TRISBx / _TRISAx : Direction de la broche RBx / RAx :
    • 1 : broche en entrée,
    • 0 : broche en sortie.
  • _RBx / _RAx : Etat de la broche :
    • 1 : broche à VDD (+3,3V),
    • 0 : broche à VSS (GND).

Nota : Lorsque l’on passe plusieurs broches à l’état logique 1 l’une à la suite de l’autre, notamment avec des très hautes fréquences, il est possible que le condensateur qui sert à modifier l’état de la broche ne puisse pas suivre et alimenter toutes les broches. Dans ce cas, il faut séparer les opérations de changement d’état de plusieurs cycles (au moins deux ou trois) pour permettre au condo de recharger.

En plus des manipulations standards, il existe deux autres opérations associées à l’utilisation des drivers : redirection des ports d’entrée ou de sortie : _RPxR (output) et RPINRx (input).

  • _RPxR : les _RPxR sont des registres associés à **ADU**
  • RPINRx : la plupart des drivers ont leurs entrées redirigeables vers l’une ou l’autre des broches RPx du chip. Pour chaque driver, il faut trouver le bon RPINRx et indiquer dans ce registre le numéro de la broche à laquelle associer l’entrée.

Pour chaque driver, les redirections de port seront précisées au cas par cas.

Les timers

Les timers sont probablement les fonctions les plus utiles d’un microcontrôleur. Ils permettent de réaliser des opérations avec une fréquence particulière, avec des actions spécifiques à chaque révolution du timer. Le principe est qu’un registre va être incrémenté régulièrement, proportionnellement à la fréquence Fcy, jusqu’à une certaine valeur. Lorsque cette valeur est atteinte, une interruption est déclenchée qui permet de réaliser l’action voulue à la révolution du timer.
Les DsPIC disposent de plus ou moins de timer selon les séries, celui utilisé ici dispose de 5 timers.
Certains timers sont utilisés par des fonctions (comme les timers 2 et 3 avec l’Input Capture, définit plus loin), ou peuvent être combinés pour former des timers 32 bits (de révolution très largement supérieure). Les applications présentées ici utiliseront plusieurs de ces fonctions, mais sans être exhaustif.

Principe

Les timers fonctionnent selon le principe ci-dessous : <<Schéma Timer à ajouter>>

Paramétrage du timer

Le code ci-dessous permet de paramétrer un timer pour avoir une interruption cadencée à la fréquence voulue :

void init_timer1(void)
{
_T1IE=0; // interruption timer
_T1IF=0; // flag d’interruption remis à zéro
T1CON=0x0020;
/* configuration du registre de controle :
Le bit de poids fort <Bit_15> commande le démarrage du timer,
Les bits <5-4> permettent de ralentir la vitesse du timer, en divisant la fréquence de mise à jour du timer par une valeur spécifique (ce que l’on appelle un prescaler. Ainsi, au lieu d’être incrémenté à chaque coup d’horloge (Fcy), le compteur du timer est incrémenté tous les 8, 64 ou 256 coups :
  • <5-4> = 0 ; // A chaque coup d’horloge
  • <5-4> = 1 ; // tous les 8 coups
  • <5-4> = 2 ; // tous les 64 coups
  • <5-4> = 3 ; // tous les 256 coups */
PR1= <<valeur>>;
// Lorsque le compteur du timer atteint cette valeur, une interruption est générée et le timer remis à 0
T1CONbits.TON=1; // démarrage du timer
}

Pour faciliter la manipulation du timer, on définit en général la donnée suivante :

#define timer1 IEC0bits.T1IE

Avec 1 pour le timer 1, 2 pour le timer 2 et ainsi de suite. Cela permet au microcontrôleur de générer les interruptions, ce qu’il ne fait pas sinon.

Nota 1 : même si les interruptions sont désactivées, le timer fonctionne si T1CONbits.TON=1. Nota 2 : vérifier que le registre IEC0 contient bien le bit du timer associé. En particulier, pour les timer 4 et 5 il faut chercher sur le registre IEC1 :

#define timer4 IEC1bits.T4IE

A titre informatif, le tableau ci-dessous donne quelques réglages de base pour générer des interruptions aux périodes indiquées, sur la base de réglage de la PLL et du quartz utilisé :

Tableau
Config 1 Config 2 Config 3 Config 4

Horloge

10 MHz

10 MHz

10 MHz

10 MHz

M

38

30

0

10

N1

0

0

0

0

N2

0

0

0

0

FCAN

50 MHz

40 MHz

5 MHz

15 MHz

Période

20 ns

25 ns

200 ns

66 ns

Interruption timer

Une interruption timer est définie dans la routine suivante :

void __attribute__((interrupt, no_auto_psv)) _T1Interrupt (void)
{
TMR1=0; // remise à 0 du compteur
Ajouter le code ici
IFS0bits.T1IF=0; // remise à 0 du drapeau d’interruption du timer
}

Nota 1 : le code ne doit pas être trop long, sinon son exécution peut masquer la survenue d’autres interruptions (voir priorité des interruptions), Nota 2 : TMR4 et TMR5 sont sur IFS1.

Cas d'utilisations des timers

Les timers sont utilisés dans les applications qui suivent dans plusieurs cas :

  • Génération de la commande d’un servomoteur (pulse d’une milliseconde sur une période de 20ms),
  • Génération d’un signal pour LED infrarouge conforme protocole IRDa,
  • Commande d’un moteur pas à pas avec une carte de commande,
  • Etc.

Ces différents cas seront détaillés dans la description complète des fonctions, au niveau des applications elles-mêmes.

Input Capture et interruptions externes

Il existe deux fonctions assez similaires qui réagissent à un changement d’état d’une entrée du microcontrôleur en générant une interruption. Ces fonctions sont :

  • Input Capture, permettant via un couplage aux timer de mesurer précisément le temps écoulé entre deux changements d’état,
  • Interruptions externes : principalement utilisées pour du comptage d’occurrence de changement d’état.

Paramétrage de l'Input Capture

La fonction d’Input Capture (IC) est très utile pour la mesure précise d’une période entre deux changements d’états d’une entrée, sur le même front ou sur des fronts différents. Il est également possible d’utiliser, comme pour les timers, un prescaler générant une interruption au bout de la 4eme, 16eme, etc. survenue du changement d’état attendu.

void init_input_capture(void)
{
IC1CON = 0x0001;
IC7CON = 0x0001;
RPINR7 = 0x000B;
RPINR10 = 0x000A;
_IC1IF = 0;
_IC1IE = 1;
_IC7IF = 0;
_IC7IE = 1;
}

Le registre ICxCON permet le paramétrage du module d’input capture. Dans notre cas, seuls les paramètres suivants sont intéressants :

  • Bit 7 – ICTMR : lorsque l’interruption survient, le contenu d’un timer est recopié dans un registre associé à l’input capture. Si ICTMR = 1, c’est le contenu du timer 2 (ie TMR2) qui est recopié dans le registre, sinon c’est le contenu du timer 3 (ie TMR3),
  • Bit <6 – 5> - ICI<1:0> : décale la génération de l’interruption de 1 à 4 survenue de l’évènement scruté. Par exemple, si ce paramètre vaut 3, et que l’IC est programmée pour détecter tous les fronts montants sur une broche, l’interruption sera générée au troisième front montant et non à chaque fois,
  • Bit <2 - 0> - ICM<2:0> : précise l’évènement scruté. Un paramètre à 0 désactive l’IC. Un paramètre à 1 génèrera une interruption à chaque front montant ou descendant. Dans ce mode le décalage de l’ICI<1:0> ne fonctionne pas. Un paramètre à 2 ou plus entrainera une interruption, après décalage de ICI<1:0>, sur chaque front montant, chaque front descendant, tous les 4, 16 front montants, etc. se reporter au datasheet du composant.

Dans le paramétrage de l’exemple, l’IC va générer une interruption à chaque front montant ou descendant.
Il faut ensuite associer à chaque IC une broche, et on utilise pour ça le registre RPINR vu précédemment. Dans l’exemple, le RPINRx de l’IC1 est RPINR7 octet de poids faible, auquel on a associé la valeur 11 (0xB). Ainsi, l’entrée de l’IC1 est la broche RP11 (ie RB11).
Enfin, de la même manière que pour les timers, il est nécessaire d’activer les interruptions sur l’IC :

  • _IC1IE = 1;

Après s’être assuré au préalable que l’interruption n’est pas en cours, sans quoi le code ne sera jamais appelé :

  • _IC1IF = 0;

Interruption Input Capture

Une interruption d’input capture est définie dans la routine suivante :

void __attribute__((interrupt, no_auto_psv)) _IC1Interrupt(void)
{
Value = IC1BUF;
// Ajouter TMR3 = 0 ou TMR2 = 0 si il est nécessaire de recommencer à compter depuis 0 (et ainsi éviter les rollovers)
_IC1IF = 0;
}

La valeur du timer 2 ou 3 (selon réglage) est copiée dans le registre ICxBUF, mais pas remise à 0. Si besoin, il est possible de la remettre à 0 en utilisant directement les registres de comptage TMRx. Noter qu’il est possible, pour by-passer une limitation du chip, d’utiliser le TMR d’un autre timer, sous réserve que celui-ci soit remis à 0 juste après et qu’aucun autre soft ne s’en serve.

Paramétrage de l'interruption externe

Les interruptions externes sont en réalité une extension des fonctions d’entrée / sortie, mais avec la génération d’une interruption à chaque front montant. Si la fonction ressemble beaucoup à l’IC, elle est cependant limitée sans possibilité de prescaler. A noter enfin qu’il ne vaut mieux pas utiliser l’INT0 qui est très haute dans la liste des priorité d’interruption, et qui peut donc rapidement poser problème pour l’exécution du code.

void init_interrupts(void)
{
RPINR0 = 0x0900; // RPINR0 => octet de poids fort pour le choix de la broche associée à l’interruption 1 (INT1), donc ici sur la broche RP9 (RB9)
_INT1EP = 0;
_INT1IF = 0; // vérification que l’interruption n’est pas en cours
_INT1IE = 1; // Activation de l’interruption
}

Le bit _INT1EP contrôle le sens de génération de l’interruption :

  • 1 – interruption sur le front descendant,
  • 0 – interruption sur le front montant.

Interruption externe

Une interruption externe est définie dans la routine suivante :

void __attribute__((interrupt,no_auto_psv))_INT1Interrupt(void)
{
code
_INT1IF = 0;
}

Les communications

Les DsPIC33FJ possèdent plusieurs modes de communication gérés automatiquement par le chip, qu’il ne reste qu’à paramétrer :

  • I2C,
  • UART,
  • ECAN.

Dans le projet décrit par la suite, seule I2C et ECAN seront utilisés, l’I2C en maitre et l’ECAN en émission / réception. Les drivers de communication sont définis dans des fichiers indépendants :

  • ecan.c / ecan.h, disponibles sur le site : ****
  • i2c_Func.c / i2c.h, disponibles sur le site : ****

Les fichiers ne sont pas disponibles ici car ils appartiennent à leurs créateurs (qui n’est pas moi, simple utilisateur), et donc restent disponibles sous leur contrôle uniquement.

Utilisation de l'I2C

Dans les applications du projet, le chip est utilisé comme maitre I2C pour lire des télémètres à ultrason.
Il est possible d’utiliser l’i2C pour la communication globale avec le chip, par exemple pour la transmission d’info avec le blackfin. Cependant, il faut penser que l’I2C n’est pas fait pour la communication à distance (de l’ordre du mètre et plus), en particulier dans un environnement CEM sévère (par exemple avec de puissants moteurs brushless à proximité).
C’est pourquoi dans les applications du projet, les communications internes seront réalisées en ECAN (plus robuste), et l’I2C limité à l’interface avec les composants qui ne comprennent que ça (typiquement les sonars à ultrason).

Câblage de l'I2C

Le principe de l’i2c est qu’il est possible d’utiliser des composants sur plusieurs niveaux de tension, par exemple entre les DsPIC33F (en 3,3V) et les capteurs à ultrason (en 5V).
Pour cela, on utilise sur chaque ligne des résistances de pull-up, portant la tension de la ligne à la tension nécessaire pour que tout le monde comprenne (c’est un bus, donc il peut y avoir plusieurs composants). Tous les sda (serial data) sont à relier ensemble, tous les scl (serial clock) aussi. <<Ajouter figure>>

Paramétrage de l'I2C

La fonction I2C est initialisée facilement (en mode maitre) à partir de la fonction donnée dans le fichier i2c_Func.c : InitI2C().
Cette fonction peut cependant être (légèrement) modifiée :

  • I2C1BRG : ce registre permet de modifier la fréquence de communication de l’I2C. L’I2C fonctionne principalement en 140 KHz et 400 KHz. A noter, puisque c’est le maître qui donne l’horloge, il est normalement possible de mettre la valeur que l’on souhaite dans la limite des 400 KHz max.

Il n’y a pas de redirection (à ma connaissance) sur les broches de l’I2C, il faut nécessairement utiliser les broches RB8 / RB9 ou RB5 / RB6 (I2C complémentaire, utilisable en modifiant les bits de configuration (Configure\ Configuration Bits… \ ALTI2C (Alternate I2C Pins)).

Interruption sur l'I2C

En mode maitre, c’est le chip qui décide d’initier la communication au moment opportun. Les esclaves peuvent également initier la communication, mais cette fonction ne sera pas étudiée ici.
Il existe deux fonctions pour la communication I2C :

  • Write : écrit un certain nombre de données dans le chip à l’adresse indiquée. La définition des données écrites pour les deux composants doit faire l’objet d’une ICD .
  • Read : Un premier cycle d’écriture va positionner l’esclave en lecture (en général, l’envoi de l’adresse du composant esclave « +1 », par exemple 0xE2 : adresse de l’esclave en écriture, 0xE3 : adresse de l’esclave en lecture), puis un redémarrage permet à l’esclave de transmettre ses données.

La fonction standard d’écriture du fichier i2c_Func.c est utilisable en l’état (rien trouvé à redire).
On l’appelle ainsi :

LDByteWriteI2C(0xE2, 0, 0x51);

Avec :

  • 0xE2 : adresse de l’esclave,
  • 0 : adresse de démarrage pour l’écriture des données,
  • 0x51 : la (les) valeur(s) à écrire.

Pour la fonction de lecture, j’ai modifié légèrement la fonction donnée dans le fichier.
On l’appelle ainsi :

LDByteReadI2C(0xE2, 0x02, i2c_buffer_out_1, 4);

Avec :

  • 0xE2 : adresse de l’esclave,
  • 0 x02: adresse de démarrage de la lecture des données,
  • I2c_buffer_out_1 : une structure de type [unsigned char i2c_buffer_out_1[20]] permettant de récupérer les données en lecture,
  • 4 : le nombre de bits à lire.

  La fonction est modifiée comme suit :

unsigned int LDByteReadI2C(unsigned char ControlByte, unsigned char Address, unsigned char *Data, unsigned char Length)
{
IdleI2C();
//wait for bus Idle
StartI2C();
//Generate Start Condition
WriteI2C(ControlByte);
//Write Control Byte
IdleI2C();
//wait for bus Idle
WriteI2C(Address);
//Write start address
IdleI2C();
//wait for bus Idle
RestartI2C();
//Generate restart condition
WriteI2C(ControlByte | 0x01);
//Write control byte for read
IdleI2C();
//wait for bus Idle
getsI2C(Data, Length);
//read Length number of bytes
NotAckI2C();
//Send Not Ack
StopI2C();
//Generate Stop

}

De plus, avant de démarrer une communication, il faut forcer les esclaves à revenir en idle mode :

  • StartI2C();
  • StopI2C();

En règle générale ça marche…

Utilisation du CAN