Tutoriel C pour programmation embarquée.
Répondre à la discussion
Affichage des résultats 1 à 6 sur 6

Tutoriel C pour programmation embarquée.



  1. #1
    Murayama

    Tutoriel C pour programmation embarquée.


    ------

    Bonjour!

    Ça fait quelques années que je traîne ici de façon intermittente, alors histoire
    de participer un peu, voici un tutoriel pour programmation embarquée en C. Je viens
    de télécharger un environnement de développement qui n'est pas vraiment nouveau en
    soi, mais qui vient d'être porté sur MacOS. Comme c'est le même que sur PC, on peut
    penser qu'il n'y aura aucune différence si vous voulez reproduire ces expériences
    sur PC. Les programmes utilisés ici existent déjà sur le net. À tout hasard, je
    précise qu'il n'y a aucun problème de copyright puisque c'est moi qui les ai écrits,
    je ne fais ici que les traduire et les adapter à la carte utilisée, tout en vérifiant
    qu'ils fonctionnent sur MacOS.
    Et puis je vais essayer de faire en sorte que même quelqu'un qui n'a que de vagues
    notions de C puisse comprendre. Ce sera donc aussi un genre de tutoriel C. Je sais
    bien qu'en essayant de tout faire en même temps, il y a un risque de ne rien faire
    de bon, mais en tout cas ce sera reproductible par copier-coller.

    La carte que j'ai choisie, c'est une des cartes "launchpad" de TI, avec un MSP430F5529.
    À noter que cette carte est moins chère qu'une Arduino (11,80 Euros chez Digikey), et a
    beaucoup plus de mémoire que la UNO. 10 k vs 2k, si je me souviens bien. La flash fait
    128k, ce qui est suffisant pour bien s'amuser. L'environnement gratuit permet de
    compiler jusqu'à 16k d'exécutable. Tous les programmes que je me propose de faire
    tiendront dans ces 16k.

    Voilà, j'ai installé Code Composer, alors je la branche. Hop! NB: Pour télécharger
    conde composer, il faut s'inscrire et donner un minimum de renseignements, mais t'as
    rien sans rien.

    Je démarre Code Composer. Il me demande où est mon code. Si vous n'avez jamais
    utilisé Code Composer, vous pouvez créer un nouveau répertoire pour votre code.
    Le mien, c'est ~/Develop/CodeWerk430

    Maintenant, nous allons créer un nouveau projet. Aller dans le menu file->new->project.
    À ce moment là, faire attention d'aller dans le menu Code Composer Studio et choisir
    CCS project et non General->Project.

    Ensuite, il va falloir choisir le processeur. Comme c'est un 5529, entrez "5529" dans
    le filtre, et il ne restera qu'un seul chip. Mettez un nom au projet. J'ai choisi
    C-01BlinkLed (programme en C, no 1 et titre). Et choisissez "Empty project (with main.c)"
    dans "templates and examples". Finish. Voilà, nous sommes en piste.

    On peut immédiatement changer int main(void) en void main(void). Une valeur de retour
    n'a aucun sens si le programme n'est pas lancé par un autre programme (i.e. dans un OS).
    On peut aussi immédiatement renommer le programme "main.c" en "C01BlinkLed.c". Sinon,
    le "workspace" devient vite un foutoir absolu avec tous les programmes qui s'appellent
    main.c.

    Voila! Il reste uniquement un morceau de code qui ne fait rien. Nous allons ajouter
    le code pour faire clignoter une LED. La carte LaunchPad 5529 a une LED rouge connectée
    au port 1, pin 0.

    Pour utiliser un GPIO en sortie, il faut mettre à 1 le registre de configuration
    correspondant. Ce registre est PxDIR (Dir = direction, 0 = entrée, 1 = sortie).
    Ensuite, pour allumer ou éteindre la LED, il faut mettre la valeur du port à 1 ou 0.
    Exemples:
    P1OUT |= 0x01; // Allumer la LED
    P1OUT &= ~0x01; // Éteindre la LED
    P1OUT ^= 0x01; // Changer l'état de la LED

    Du point de vue fréquence de clignotement, le processeur a par défaut (i.e. quand on
    ne configure rien) une fréquence approximative de 1 MHz. Par ailleurs, il existe une
    fonction __delay_cycles qui permet de mettre un délai à 1 cycle d'horloge près, ce
    qui est impossible avec une boucle. Donc si vous mettez un délai de 1 million de cycles,
    le processeur attendra 1 seconde.

    Le programme est le suivant:
    Code:
    #include <msp430.h>
    
    #define    RED_LED_DIR        P1DIR
    #define    RED_LED_OUT        P1OUT
    #define    RED_LED            0x01
    
    #define    DELAY_VAL        1000000
    
    void main(void) {
        WDTCTL = WDTPW | WDTHOLD;    // Stop watchdog timer
        RED_LED_DIR |= RED_LED;
        while(1) {
            RED_LED_OUT |= RED_LED;
            __delay_cycles(DELAY_VAL);
            RED_LED_OUT &= ~RED_LED;
            __delay_cycles(DELAY_VAL);
        }
    }
    Voilà, votre premier programme sur MSP430 fonctionne.
    Il utilise 234 bytes de flash. Il est assez difficile de juger de l'efficacité sur un
    programme aussi petit, alors nous allons faire un peu mieux dans le prochain.

    Pascal

    -----

  2. #2
    Murayama

    Re : Tutoriel C pour programmation embarquée.

    Re...!

    Dans le premier tutoriel, nous avons vu comment faire clignoter une LED. Par contre,
    le gros inconvénient de ce programme est qu'il tourne à fond et ne fait quasiment rien
    d'autre qu'attendre.

    Alors cette fois, nous allons introduire les timers et les interruptions. D'abord un
    timer, c'est quoi? C'est simplement un compteur avec certaines fonctions dont nous
    parlerons plus tard dans d'autres programmes. Ce compteur est externe au coeur du
    processeur, ce qui veut dire que pendant que le timer compte, le processeur peut dormir
    ou faire autre chose.

    Un timer est programmable. On peut lui donner la valeur jusqu'à laquelle il doit
    compter. Par exemple 50000. C'est un peu comme un réveil. On lui dit "compte jusqu'à
    50 000 et réveille moi quand t'as fini". C'est comme tout, quand on ne fait faire le
    boulot par quelqu'un d'autre, c'est plus relax que quand on fait tout soi-même.
    On peut lui dire aussi "compte jusqu'à 50 000, et à chaque fois que t'as fini, tu
    me réveilles pendant que tu recommences à compter".

    Dans tous les cas (réveil unique ou multiple) on va avoir besoin de ce qui s'appelle
    une routine d'interruption (ISR = interrupt service routine) qui est appelée
    automatiquement par le processeur.

    Le code est le suivant (il vaut mieux faire un nouveau programme pour pouvoir revenir
    sur le premier. La méthode est expliquée au chapitre précédent).

    Code:
    #include <msp430F5659.h>
    
    #define    RED_LED_DIR        P1DIR
    #define    RED_LED_OUT        P1OUT
    #define    RED_LED            0x01
    
    #define    BUSY_DIR            P1DIR
    #define    BUSY_OUT            P1OUT
    #define    BUSY_SIG            0x04
    
    #define    TIMER_VAL        50000
    
    int main(void) {
        WDTCTL = WDTPW + WDTHOLD;                 //    Stop WDT
        RED_LED_DIR |= RED_LED;                    //    Red LED to output
        RED_LED_OUT &= ~RED_LED;                    //    Red led off at start
        BUSY_DIR |= BUSY_SIG;                        //    Busy signal in output mode
        BUSY_OUT &= ~BUSY_SIG;                        //    Busy signal off at start
        TA0CCTL0 = CCIE;                            //    Timer interrupt enabled
        TA0CCR0 = 50000;                            //    Timer upper value
        TA0CTL = TASSEL_2 + MC_1 + TACLR;        //    Use SMCLK, upmode, clear TAR
        __bis_SR_register(LPM0_bits + GIE);        //    Enter low power mode, wait for interrupts
    }
    
    // Timer0 A0 interrupt service routine
    #pragma vector=TIMER0_A0_VECTOR
    __interrupt void TIMER0_A0_ISR(void) {
        BUSY_OUT |= BUSY_SIG;
        RED_LED_OUT ^= RED_LED;                          // Toggle P1.0
        BUSY_OUT &= ~BUSY_SIG;
    }
    Ce qu'il est intéressant de remarquer, c'est que le main ne fait que configurer,
    et ensuite ne fait plus rien. Il n'y a pas besoin de boucle while dans le main, parce
    que le MSP430 a un mécanisme qui gère les interruptions avec une boucle implicite. Et
    quand il n'y a pas d'interruption, il n'y a pas besoin de travailler.
    Vous pouvez aussi remarquer l'introduction d'un signal BUSY. Le programme met se signal
    à 1 à chaque fois qu'il travaille, puis à 0 en quittant. En vérifiant le taux d'occupation
    à l'oscilloscope, on voit bien que le processeur ne fait quasiment rien.
    Nom : Hima.png
Affichages : 393
Taille : 11,8 Ko

    Occupation mémoire:
    Flash = 260
    Data = 0
    RAM = 206

    À propos de ces données. On notera que bien que la RAM n'est pas du tout utilisée,
    le résultat affiché est de 206 bytes, ce qui correspond probablement à la configuration
    de la pile et non à son utilisation.

    C'est tout pour ce 2 ème épisode. À partir de ce qui suit, nous allons essayer de
    voir comment faire (dès le début) pour que le code reste clair et facilement
    compréhensible. Au lieu de tout coder d'un bloc, on peut utiliser des fonctions
    pour bien séparer ce qui est séparable

    Le code devient:
    Code:
    #include <msp430F5659.h>
    
    #define    RED_LED_DIR        P1DIR
    #define    RED_LED_OUT        P1OUT
    #define    RED_LED            0x01
    
    #define    BUSY_DIR            P1DIR
    #define    BUSY_OUT            P1OUT
    #define    BUSY_SIG            0x04
    
    #define    TIMER_VAL        50000
    
    //    Function prototypes
    void ConfigGPIO(void);
    void ConfigTimer(void);
    
    int main(void) {
        WDTCTL = WDTPW + WDTHOLD;                 //    Stop WDT
        ConfigGPIO();
        ConfigTimer();
        __bis_SR_register(LPM0_bits + GIE);        //    Enter low power mode, wait for interrupts
    }
    
    void ConfigGPIO(void) {
        RED_LED_DIR |= RED_LED;                    //    Red LED to output
        RED_LED_OUT &= ~RED_LED;                    //    Red led off at start
        BUSY_DIR |= BUSY_SIG;                        //    Busy signal in output mode
        BUSY_OUT &= ~BUSY_SIG;                        //    Busy signal off at start
    }
    void ConfigTimer(void) {
        TA0CCTL0 = CCIE;                            //    Timer interrupt enabled
        TA0CCR0 = 50000;                            //    Timer upper value
        TA0CTL = TASSEL_2 + MC_1 + TACLR;        //    Use SMCLK, upmode, clear TAR
    }
    
    // Timer0 A0 interrupt service routine
    #pragma vector=TIMER0_A0_VECTOR
    __interrupt void TIMER0_A0_ISR(void) {
        BUSY_OUT |= BUSY_SIG;
        RED_LED_OUT ^= RED_LED;                          // Toggle P1.0
        BUSY_OUT &= ~BUSY_SIG;
    }
    Occupation mémoire:
    Flash = 272
    Data = 0
    RAM = 206

    Avantages d'utiliser des fonctions:
    - Le code est plus clair, autodocumenté
    - Les problèmes possibles sont isolables facilement
    - Quand le code grossit, on peut toujours garder une fonction dans un seul
    écran, c'est plus facile pour chercher les erreurs.
    Inconvénients:
    - Chaque appel de fonction prend un peu de place en mémoire programme et aussi
    en "stack". Pour les curieux, vous pouvez chercher de la doc sur la "stack" (pile)
    d'un processeur. Le processeur est donc légèrement ralenti. Par contre, comme il
    s'agit ici d'une approche par interruption, le clignotement arrivera EXACTEMENT
    au même rythme puisque c'est le timer qui bat la mesure.
    Ceci dit, 12 bytes de plus pour un code plus clair, ce n'est pas cher payer.



    Pascal
    Dernière modification par Murayama ; 22/09/2015 à 07h42.

  3. #3
    Murayama

    Re : Tutoriel C pour programmation embarquée.

    Et de trois!

    Dans cette étape, nous allons uniquement montrer comment structurer un peu le
    programme en plusieurs fichiers. Ce n'est pas vraiment utile pour celui-là,
    mais c'est juste pour montrer la méthode.

    L'environnement CCS permet de gérer le programme sans trop se fatiguer à faire des
    makefiles, etc... Pour la plupart des usages quotidiens, cela suffit amplement.

    Nous allons mettre les configurations du programme dans un fichier séparé. J'appelle
    toujours ce fichier HardwareConfig.h. Je mets dedans tout ce qui est relatif au
    hardware. En reproduisant exactement le même programme que précédemment, nous pouvons
    écrire HardwareConfig.h comme suit (en pratique: file->new->header file):

    Code:
    #ifndef _HARDWARE_CONFIG_H_
    #define _HARDWARE_CONFIG_H_
    
    #include "MSP430F5529.h"
    
    #define    RED_LED_DIR        P1DIR
    #define    RED_LED_OUT        P1OUT
    #define    RED_LED            0x01
    
    #define    BUSY_DIR            P1DIR
    #define    BUSY_OUT            P1OUT
    #define    BUSY_SIG            0x04
    
    #define    TIMER_VAL        50000
    
    #endif
    Le programme principal devient:

    Code:
    #include "HardwareConfig.h"
    
    //    Function prototypes
    void ConfigGPIO(void);
    void ConfigTimer(void);
    
    int main(void) {
        WDTCTL = WDTPW + WDTHOLD;                 //    Stop WDT
        ConfigGPIO();
        ConfigTimer();
        __bis_SR_register(LPM0_bits + GIE);        //    Enter low power mode, wait for interrupts
    }
    
    void ConfigGPIO(void) {
        RED_LED_DIR |= RED_LED;                    //    Red LED to output
        RED_LED_OUT &= ~RED_LED;                    //    Red led off at start
        BUSY_DIR |= BUSY_SIG;                        //    Busy signal in output mode
        BUSY_OUT &= ~BUSY_SIG;                        //    Busy signal off at start
    }
    void ConfigTimer(void) {
        TA0CCTL0 = CCIE;                            //    Timer interrupt enabled
        TA0CCR0 = 50000;                            //    Timer upper value
        TA0CTL = TASSEL_2 + MC_1 + TACLR;        //    Use SMCLK, upmode, clear TAR
    }
    
    // Timer0 A0 interrupt service routine
    #pragma vector=TIMER0_A0_VECTOR
    __interrupt void TIMER0_A0_ISR(void) {
        BUSY_OUT |= BUSY_SIG;
        RED_LED_OUT ^= RED_LED;                          // Toggle P1.0
        BUSY_OUT &= ~BUSY_SIG;
    }
    Compilation: aucune différence en ce qui concerne la taille du code et de la RAM.

    Maintenant comme je parlais de rangement, on peut aller plus loin. On pourrait mettre
    ce qui concerne le timer dans un fichier séparé et ce qui concerne le port d'entrée
    sortie dans un autre. Essayons.
    Je crée une paire de fichiers timer.h et timer.c. file->new->header file, puis
    file->source fule, pour Timer et GPIO respectivement.
    Pour éviter les sections code répétées, je mets tout dans la même, avec des séparations
    de ce type: //-----8<----- explication -----8<-----
    Code:
    //-----8<----- HardwareConfig.h -----8<-----
    #ifndef _HARDWARE_CONFIG_H_
    #define _HARDWARE_CONFIG_H_
    
    #include "MSP430F5529.h"
    #include "Timer.h"
    #include "GPIO.h"
    
    #define    RED_LED_DIR        P1DIR
    #define    RED_LED_OUT        P1OUT
    #define    RED_LED            0x01
    
    #define    BUSY_DIR            P1DIR
    #define    BUSY_OUT            P1OUT
    #define    BUSY_SIG            0x04
    
    #define    TIMER_VAL        50000
    
    #endif
    //-----8<----- TimerTest.h -----8<-----
    #include "HardwareConfig.h"
    
    int main(void) {
        WDTCTL = WDTPW + WDTHOLD;                 //    Stop WDT
        ConfigGPIO();
        ConfigTimer();
        __bis_SR_register(LPM0_bits + GIE);        //    Enter low power mode, wait for interrupts
    }
    
    
    // Timer0 A0 interrupt service routine
    #pragma vector=TIMER0_A0_VECTOR
    __interrupt void TIMER0_A0_ISR(void) {
        BUSY_OUT |= BUSY_SIG;
        RED_LED_OUT ^= RED_LED;                          // Toggle P1.0
        BUSY_OUT &= ~BUSY_SIG;
    }
    //-----8<----- Timer.h -----8<-----
    #ifndef _TIMER_H_
    #define _TIMER_H_
    
    void ConfigTimer(void);
    
    #endif
    //-----8<----- Timer.c -----8<-----
    #include "HardwareConfig.h"
    
    void ConfigTimer(void) {
        TA0CCTL0 = CCIE;                            //    Timer interrupt enabled
        TA0CCR0 = 50000;                            //    Timer upper value
        TA0CTL = TASSEL_2 + MC_1 + TACLR;        //    Use SMCLK, upmode, clear TAR
    }
    //-----8<----- GPIO.h -----8<-----
    #ifndef GPIO_H_
    #define GPIO_H_
    
    void ConfigGPIO(void);
    
    #endif
    //-----8<----- GPIO.c -----8<-----
    #include "HardwareConfig.h"
    
    void ConfigGPIO(void) {
        RED_LED_DIR |= RED_LED;                    //    Red LED to output
        RED_LED_OUT &= ~RED_LED;                    //    Red led off at start
        BUSY_DIR |= BUSY_SIG;                        //    Busy signal in output mode
        BUSY_OUT &= ~BUSY_SIG;                        //    Busy signal off at start
    }
    Compilation: ce résultat est aussi important: le programme n'a pas augmenté de
    volume, c'est la même taille au byte près (272).

    Conclusion
    On peut voir que le programme principal est devenu extrêmement simple.
    On pourra arguer que sur un programme aussi simple, c'est un peu exagéré, et c'est
    vrai. Par contre, même si le programme évolue, on garde la même simplicité en séparant
    ce qui est séparable.
    Notons que si je veux porter ce programme sur un autre processeur de la même famille,
    il me suffit de changer la ligne #include "MSP430F5529.h" dans HardwareConfig.h
    Si vous voulez mettre la LED rouge sur le port 7.4, par exemple, il vous suffit de
    changer comme suit dans HardwareConfig.h:

    #define RED_LED_DIR P7DIR
    #define RED_LED_OUT P7OUT
    #define RED_LED 0x10 // Port 7.4

    Voilà. On a vu les timers et leurs interruptions. Le prochain consistera à montrer comment
    on utilise un timer en PWM. Et avec des horloges un peu plus rapides...

    Pascal

  4. #4
    Murayama

    Re : Tutoriel C pour programmation embarquée.

    Bonjour!

    Voici le 4ème épisode.
    Dès le 2ème tutoriel, nous avons vu comment utiliser un timer pour créer un événement
    répétitif. Pour créer un signal PWM, il serait évidemment possible de le faire par
    interruption par la méthode suivante (pseudo code)

    1. Confiurer le temps pour la partie "on" du signal
    2. Attendre l'interruption
    3. À l'interruption, configurer le temps pour la partie "off"
    4. À l'interruption retour à 1.

    Mais les timers ont une propriété intéressante avec leurs registres de comparaison.
    Dans notre cas (timer A0), le registre de la période est TACCR0, mais il y a d'autres
    registres de comparaison pour définir un rapport cyclique.
    En utilisant par exemple TA0CCR1, et en dirigeant la sortie du comparateur sur une
    patte du processeur, nous pouvons avoir la configuration pwm "gratuitement", c'est à dire
    sans jamais réveiller le processeur, sans jamais utiliser une interruption.

    Le graphique suivant montre le principe:
    TACCR0 indique la période du signal PWM
    TACCR1 indique la une durée inférieure à TA0CCR0. L'effet dépend du mode.

    TimerA.png

    Le compteur incrémente en permanence. La sortie dépend du mode. Pour générer du PWM,
    on voit qu'il y a soit le mode 3 (set/reset), soit le mode 7 (reset/set). On peut noter
    que les modes 2 et 6 donnent le même résultat à partir du 1 er retour à 0 du compteur.
    Mais pour tout dire, comme l'état de départ est indéfini, je me demande bien à quoi
    ces modes peuvent servir...

    Bon, les choses sérieuses, voici le code.
    Explication:
    1. La fréquence est mise à 25 MHz. On met d'abord la tension du processeur à 1.9V.
    2. La période du timer est mise à 25000 (dans les faits, le compteur est mis à 25000-1 pour
    compter de 0 à 24999, ce qui fait une période de 25000. La période est donc de 1kHz comme
    on peut le voir sur l'oscillogramme.
    3. La période secondaire (PWM_ON) est mise à 2500. De cette façon, on a un rapport cyclique
    de 1/10.
    4. Une fois le timer configurer, il n'y a plus rien à faire, le signal PWM est établi.


    Code:
    #include "HardwareConfig.h"
    
    #define    PWM_PERIOD    CLOCK_KHZ            //    Will result in 1000 Hz signal
    #define    PWM_ON        (CLOCK_KHZ/10)        //    Will result in 1/10 duty
    #define    PWM_SEL        P1SEL
    #define    PWM_DIR        P1DIR
    #define    PWM_BIT        0x04
    
    void EnablePWM(void);
    
    int main(void) {
        WDTCTL = WDTPW + WDTHOLD;            //    Stop WDT
        SetCoreVoltage(VCORE19);                //    Set the core to 1.9V
        SetFLL(CLOCK_MHZ);                    //    Set SMCLK to 25 MHz
        EnablePWM();                            //    Start PWM
        __bis_SR_register(LPM0_bits);        //    Enter LPM0(low power mode 0)
    }
    
    void EnablePWM(void) {
        PWM_DIR |= PWM_BIT;                    //    P1.2 output
        PWM_SEL |= PWM_BIT;                    //    P1.2 as timer output
        TA0CCR0 = PWM_PERIOD-1;                //    PWM Period
        TA0CCTL1 = OUTMOD_7;                    //    CCR1 reset/set
        TA0CCR1 = PWM_ON;                    //    CCR1 PWM duty cycle
        TA0CTL = TASSEL_2 + MC_1 + TACLR;    //    SMCLK, up mode, clear TAR
    }
    Voilà. On compile. Le code est nettement plus gros que dans les premiers exemples, (640
    bytes) mais ceci vient de la boucle d'asservissement de fréquence (FLL).
    On peut noter une chose : le programme ne fait que configurer le signal et ensuite
    ne fait plus rien. Pas d'interruption, silence complet, mais le PWM, lui, fonctionne. On voit
    que la fréquence et le rapport cyclique sont bien ce qui a été configuré.

    tek00001.png

    Il y a quelques détails que j'ai passés sous silence:
    - J'ai modifié la fréquence d'horloge pour avoir 25 MHz au lieu de 1.
    - J'utilise des typedefs (void fichier Types.h) pour les principaux types.

    Contenu des différents fichiers.
    NB: je n'ai pas mis le .h de F5xUtils, mais uniquement l'implémentation. J'imagine que
    ce ne sera pas difficile de compléter.


    Code:
    //-----8<----- Dans HardwareConfig.h -----8<-----
    #include "MSP430F5529.h"
    #include "Types.h"
    #include "F5xUtils.h"
    
    #define    CLOCK        25000000
    #define    CLOCK_KHZ    25000
    #define    CLOCK_MHZ    25
    //-----8<----- Types.h -----8<-----
    typedef unsigned char        uint8;
    typedef unsigned int        uint16;
    typedef unsigned long int    uint32;
    
    typedef signed char            int8;
    typedef signed int            int16;
    typedef signed long int        int32;
    //-----8<----- F5XUtils.c -----8<-----
    void SetCoreVoltage(uint8 vc) {
        unsigned int currentVCore;
        // Get actual VCore
        currentVCore = PMMCTL0 & PMMCOREV_3;
        // Correct accordingly
        while (vc != currentVCore) {
            if (vc > currentVCore) increase_vcore(++currentVCore);
            else decrease_vcore(--currentVCore);
         }
    }
    
    void increase_vcore(uint8 vc) {
        // Open PMM module registers for write access
        PMMCTL0_H = 0xA5;
        // Set VCore to new VCore
        PMMCTL0 = 0xA500 + (vc * PMMCOREV0);
        // Set SVM new Level
        SVSMLCTL = (SVSMLCTL & ~(3 * SVSMLRRL0)) + SVMLE + (vc * SVSMLRRL0);
        // Wait till SVM is settled (Delay)
        while ((PMMIFG & SVSMLDLYIFG) == 0);
        // Clear already set flags
        PMMIFG &= ~(SVMLVLRIFG + SVMLIFG);
        if ((PMMIFG & SVMLIFG)) {
            // Wait till level is reached
            while ((PMMIFG & SVMLVLRIFG) == 0);
        }
        // Disable Low side SVM
        SVSMLCTL &= ~SVMLE;
        // Lock PMM module registers for write access
        PMMCTL0_H = 0x00;
    }
    
    void decrease_vcore(uint8 vc) {
        // Open PMM module registers for write access
        PMMCTL0_H = 0xA5;
        // Set SVM new Level
        SVSMLCTL = (SVSMLCTL & ~(3 * SVSMLRRL0)) + SVMLE + (vc * SVSMLRRL0);
        // Wait till SVM is settled (Delay)
        while ((PMMIFG & SVSMLDLYIFG) == 0);
        // Set VCore to 1.85 V for Max Speed.
        PMMCTL0 = 0xA500 + (vc * PMMCOREV0);
        // Disable Low side SVM
        SVSMLCTL &= ~SVMLE;
        // Lock PMM module registers for write access
        PMMCTL0_H = 0x00;
    }
    
    //*****************************************************************************
    //    Delay
    //*****************************************************************************
    //    Sets a delay in milliseconds. The limit is 32... seconds
    //-----------------------------------------------------------------------------
    void Delay(uint16 d) {
        volatile uint16 a;
        while(d-- != 0) {
            __delay_cycles(CLOCK_KHZ);
        }
    }
    
    //    Sets a delay in microseconds
    void DelayUS(uint16 d) {
        volatile uint16 a;
        while(d-- != 0) {
            __delay_cycles(CLOCK_MHZ);
        }
    }
    
    //*****************************************************************************
    //    Sets the frequency locked loop
    //*****************************************************************************
    //    Accurately tune the clock frequency. Modulation not used here.
    //-----------------------------------------------------------------------------
    void SetFLL(uint8 mhz) {
        uint32 freq = mhz * 1000000;
        freq /= 32768;
        P5SEL |= 0x30;        //    Enable XT1
        UCSCTL6 &= ~XT1OFF;    //    Set XT1 ON
        UCSCTL6 |= XCAP_3;    //    Capacitors
        // Loop until XT1 fault flag is cleared
        do {
            UCSCTL7 &= ~XT1LFOFFG;    //    Clear XT1 fault flags
        } while(UCSCTL7&XT1LFOFFG);    //    Test XT1 fault flag
        __bis_SR_register(SCG0);    //    Disable FLL control loop
        UCSCTL0 = 0;
        UCSCTL1 = DCORSEL_7;
        UCSCTL2 = (uint16)freq;
        __bic_SR_register(SCG0);
        __delay_cycles(781250);
        do {
            // Clear XT2,XT1,DCO fault flags
            UCSCTL7 &= ~(XT2OFFG + XT1LFOFFG + DCOFFG);
            SFRIFG1 &= ~OFIFG;    // Clear fault flags
        } while (SFRIFG1&OFIFG);    // Test oscillator fault flag
    }
    Voilà. Dans le prochain épisode, je vais brancher un servomoteur histoire de ne pas parler dans
    le vide et de faire quelque chose de concret.

    ... à suivre ...

    Pascal

  5. A voir en vidéo sur Futura
  6. #5
    Murayama

    Re : Tutoriel C pour programmation embarquée.

    Bonjour!

    Comme je le disais à la fin de mon dernier message, nous allons cette fois piloter
    un servomoteur. Un servo, ça se pilote avec un genre de PWM particulier. La période
    est de 20 ms, et la partie "ON" est entre 1 et 2 ms. Donc il faut régler le PWM sur
    50Hz. Par contre, pour passer de 25 MHz à 50 Hz, il faut faut diviser par 500 000.
    Mais les registres des timers sont en 16 bits, soit 65525 maximum.

    Pour y arriver, il y a au moins 2 solutions:
    1. utiliser une fréquence plus faible. Par exemple, si nous utilisons non pas 25 MHz
    mais 1 MHz, il suffira de diviser par 20 000 pour avoir 50 Hz. Et le gros avantage
    serait d'avoir une période de 20 000 qui correspond à 20 ms. Donc en utilisant un
    TACCR1 entre 1000 et 2000, on aurait exactement le profil nécessaire. Et cerise sur
    le gâteau, mettre un TACCR1 de 1000, cela signifierait 1000µs, ce qui est bien
    pratique.
    Inconvénient: on a un processeur rapide qui peut aller jusqu'à 25MHz, et on l'utilise
    à 1 MHz. C'est dommage.

    2. Une deuxième solution est de garder une fréquence assez élevée, mais utiliser un
    diviseur pour générer la fréquence du timer. Donc on peut probablement générer une
    fréquence proche de 1 MHz.
    Les timers du MSP430 ont 2 diviseurs.
    L'un peut diviser par 1, 2, 3, 4, 5, 6, 7, 8, l'autre peut diviser par 1, 2, 4, 8
    En les utilisant en chaîne, on a de nombreuses solutions de diviseurs, mais
    malheureusement pas 25 MHz. On a une solution proche avec 24, qui est le maximum
    dans les limites garanties par le constructeur. Alors un bon compromis serait
    de configurer l'horloge principale à 24 MHz et utiliser les diviseurs 6 et 4, ou
    3 et 8. En résumant les avantages:
    - La fréquence du processeur est assez élevée, pas loin du maximum garanti
    - Le PWM sera exactement à 50Hz, et la valeur du pulse pourra être mise directement en µs.
    NB: j'imagine que les servos sont relativement tolèrants, donc en faire un à peine
    plus rapide ou à peine plus lent ne changerait pas grand chose dans la mesure où ce qui
    est important, c'est la largeur du pulse.

    NB: il est évidemment possible de garder 25 MHz. Dans ce cas là, en mettant une
    divisant par 24, on aurait une fréquence de timer de 1.0416 MHz. En mettant une
    période de 20833 cycles, on retomberait sur 50Hz exactement. Mais il faudrait faire
    des corrections de 25 / 24 pour les µs, ce qui serait moins pratique. Donc allons-y
    pour 24 MHz.

    Maintenant que le problème de la fréquence est résolu, il va falloir s'occuper du
    rapport cyclique et faire pour qu'il change, sinon le programme ne sera pas vraiment
    convaincant.

    Ce qu'il est possible de faire, c'est de changer le rapport cyclique à chaque fois
    que la période de PWM est écoulée. On peut par exemple faire évoluer le rapport
    linéairement de 1000 à 2000, puis de 2000 à 1000 alternativement. Le servo va
    tourner lentement dans un sens, puis revenir alternavitement.
    Dans la configuration du PWM, il faut simplement ajouter un "flag" pour dire qu'on
    a besoin d'interruptions, et il faut configurer l'interruption timer comme
    précédemment.
    Maintenant, de combien doit-on augmenter ou diminuer? La valeur minimale est de 1000µs
    et la valeur maximale est de 2000. On a donc une amplitude de 1000. En incrémentant
    de 1 à chaque fois, il faudra 1000 périodes. Mais comme les changements auront
    une fréquence de 50 Hz, il faudra 20 secondes pour aller de 1000 à 2000 et 20 secondes
    dans le sens opposé. Il est donc préférable d'aller un peu plus vite, par exemple
    en ajoutant 10 µs à chaque fois. On aura une montée / descente en 2 secondes, ce qui
    est mieux pour l'observation.

    Voilà le code. Je ne remets pas les détails pour changer l'horloge, il suffira
    de changer 25 en 24 dans HardwareConfig.h.

    Code:
    #include "HardwareConfig.h"
    #include "F5xUtils.h"
    
    #define    PWM_PERIOD    20000                //    Will result in 1000 Hz signal
    #define    PWM_SEL        P1SEL
    #define    PWM_DIR        P1DIR
    #define    PWM_BIT        0x04
    
    #define    STEP            10                    //    Results in a 2 second alternative
    
    void EnablePWM(void);
    uint16    angle;                            //    Current angle
    int8 step = STEP;                        //    Direction
    
    void ChangeAngle();
    
    int main(void) {
        WDTCTL = WDTPW + WDTHOLD;            //    Stop WDT
        angle = 1500;
        SetCoreVoltage(VCORE19);                //    Set the core to 1.9V
        SetFLL(CLOCK_MHZ);                    //    Set SMCLK to 25 MHz
        EnablePWM();                            //    Start PWM
        __bis_SR_register(LPM0_bits + GIE);    //    Enter LPM0(low power mode 0)
    }
    
    void EnablePWM(void) {
        PWM_DIR |= PWM_BIT;                    //    P1.2 output
        PWM_SEL |= PWM_BIT;                    //    P1.2 as timer output
        TA0CCR0 = PWM_PERIOD-1;                //    PWM Period
        TA0CCTL0 = CCIE;                        //    Timer interrupt enabled
        TA0CCTL1 = OUTMOD_7;                    //    CCR1 reset/set
        TA0CCR1 = angle;                        //    CCR1 PWM duty cycle
        TA0EX0 = 2;                            //    Divider by 3
        TA0CTL = TASSEL_2 + MC_1 + TACLR + ID_3;    //    SMCLK, up mode, clear TAR
    }
    
    void ChangeAngle() {
        angle += step;
        if(angle < 1000) {
            step = STEP;
            angle += step;
        }
        else if(angle > 2000) {
            step = -STEP;
            angle += step;
        }
        TA0CCR1 = angle;
    }
    
    // Timer0 A0 interrupt service routine
    #pragma vector=TIMER0_A0_VECTOR
    __interrupt void TIMER0_A0_ISR(void) {
        ChangeAngle();
    }
    L'oscillogramme ci-dessous montre la période de 20 ms et la largeur du pulse.

    Nom : ServoMin.png
Affichages : 332
Taille : 10,2 Ko

    Dans la prochaîne "leçon", je vais ajouter un potentiomètre et asservir le servo
    sur sa valeur.

    ... à suivre ...

    Pascal

  7. #6
    Murayama

    Re : Tutoriel C pour programmation embarquée.

    Bonjour!

    J'ai eu la tête dans le guidon ces derniers temps, ce qui explique le délai...
    Aux modérateurs: Je viens de voir qu'il y a une section "projets" sur ce site. Il serait
    peut-être intéressant de déplacer ceci dans les projets bien qu'il ne s'agisse pas à
    proprement parler d'un projet précis... Je vous laisse juger ce qu'il convient de faire.

    Bon, revenons au sujet. Dans le derneier message, j'ai montré comment piloter un
    servo avec une carte très bon marché (TI launchpad 5529, voir message 1).
    Le programme réalisé ne sert pas à grand chose, sauf peut-être si vous avez un servo
    à tester. Note à propse des servos: ce programme pilote des servos classiques, avec
    PWM, comme il en existe depuis des années. Les servos récents sont pilotés en digital,
    avec un protocole basé sur RS485 qui permet de mettre plusieurs servos sur le même bus.

    Aujourd'hui je vais ajouter un potentiomètre, et ce sera donc le premier asservissement
    complet sensor <-> µC <-> actuator.
    Il va falloir mesurer le potentiomètre à intervalles réguliers et changer en conséquence
    le rapport cyclique du signal PWM du servo. Il faut donc résoudre les problème suivants:

    1. À quel rythme mesurer
    2. Résolution de la mesure
    3. Comment lier la mesure à la commande.

    Pour le point 1, nous savons que le signal PWM a une période de 20 ms. Il serait donc
    absolument inutile de mesurer plus vite que 20 ms (donc 50 Hz). Alors disons que nous
    allons utiliser la même période que le PWM, ce qui sera finalement plus simple.

    Pour la résolution de la mesure, nous avons vu que le timer a une résolution de 1000.
    C'est à dire que si nous mettons le registre à 1000, nous aurons une largeur d'impulsion
    de 1000 µs, et si nous le mettons à 2000, nous aurons 2000 µs.
    La résolution de l'ADC est en 12 bits, c'est à dire 4096. Il nous faut donc faire
    correspondre linéairement:
    pottimer
    0-------->1000
    4095--->2000

    Une manière de faire cela est de diviser l'entrée simplement par 4 et de l'ajouter à
    1000. Cela nous donnera:

    pot/4-------1000 + pot/4
    0----------->1000
    1023------>2023

    Par contre, les fabricants de servo ont parfois des normes qui diffèrent. Par exemple,
    chez Futaba, le point milieu est 1520µs. Alors on va encore modifier un peu en prenant
    pot/4-512 qui donnera une valeur presque symétrique. et en l'ajoutant au point milieu
    qui est, selon le fabricant, 1500 ou 1520.

    pot/4-512 ------- Middle+(pot/4-512)
    -512--------------> 988
    511--------------->2011

    L'avantage, c'est que c'est symétrique. On peut aussi tronquer pour ne pas descendre
    sous 1000µs ni monter au dessus de 2000.

    Le programme. Il suffira maintenant de mesurer la tension du potentiomètre. Sans
    interruption suppléementaire puisque nous en avons déjà une synchrone avec le PWM.
    Il faut d'abord configurer le convertisseur:

    Code:
    void EnableADC() {
        ADC12CTL0 = ADC12ON+ADC12SHT0_2;        // Turn on ADC12, set sampling time
        ADC12CTL1 = ADC12SHP;                    // Use sampling timer
        ADC12CTL0 |= ADC12ENC;                    // Enable conversions
    }
    Et il faut aussi une fonction pour lire la valeur de l'ADC. On notera qu'il est possible
    d'écrire directement return ADC12MEM0. Mais il est assez pratique d'avoir une variable
    locale pour vérifier que tout fonctionne bien, c'est la raison d'être de la valeur de
    retour retval.

    Code:
    uint16 ReadADCVal(void) {
        uint16 retval;                            //    Value for debugging purpose
        ADC12CTL0 |= ADC12SC;                   //    Start conversion-software trigger
        while (!(ADC12IFG & BIT0));                //    Wait for the ADC
        retval = ADC12MEM0;                        //    Set breakpoint here to verify value
        return retval;
    }
    Ensuite, pour relier la mesure à la commande, on utilise l'interruption du timer utilisé
    pour la modulation PWM. Je vais aussi m'autoriser une petite fonction pour mettre
    directement l'angle du servo. Comme dit plus haut, nous avons en gros une amplitude de
    +/- 500 pour aller de - angle max à + angle max. Il se trouve que les servos que j'ai
    sur ma table ont la bonne idée d'avoir une amplitude min / max de +/- 50 degres. On
    va donc pouvoir mettre l'angle en dixièmes de degrés. La fonction est donc comme suit:

    Code:
    void SetAngle(int16 angle) {
        if(angle <= -500) TA0CCR0 = 1000;
        else if(angle >= 500) TA0CCR0 = 2000;
        else TA0CCR0 = MID_VAL + angle;
    }
    Puis, pour utiliser directement la valeur ADC, on peut créer une autre fonction:

    Code:
    Void UseADCVal(uint16 adcval) {
        int16 angleval;
        angleval = adcval;
        angleval >>= 2;        //    Scale [0 .. 4096[ to [0 .. 1024[
        angleval -= 512;    //    Shift [0 .. 1024[ to [-512 .. 512[
        SetAngle(angleval);
    }
    Voilà. Notez qu'il est parfaitement possible de grouper ces 2 fonctions en une. Mais
    je l'ai implémenté de cette façon parce que j'utilise aussi des messages venant de
    BlueTooth qui sont déjà au format [-512 .. 512[

    Bon, maintenant, on peut essayer. Voici le code complet, à l'exception des routines
    pour configurer la tension du core, et la fréquence du processeur. Mais ça, c'est déjà
    expliqué dans les messages précédents.

    On notera que le programme principale ne fait rien d'autre que configurer. Tout le
    reste se passe dans les fonctions appelées par les interruptions.

    Code:
    #include "HardwareConfig.h"
    #include "F5xUtils.h"
    
    #define    PWM_PERIOD    20000                //    Will result in 1000 Hz signal
    #define    PWM_SEL        P1SEL
    #define    PWM_DIR        P1DIR
    #define    PWM_BIT        0x04
    
    #define    MID_POINT    1500
    
    #define    STEP            1
    
    void EnablePWM(void);
    void EnableADC(void);
    
    uint16    angle;                                //    Current angle
    int8 step = STEP;                            //    Direction
    
    void ChangeAngle();
    
    int main(void) {
        WDTCTL = WDTPW + WDTHOLD;                //    Stop WDT
        angle = MID_POINT;
        SetCoreVoltage(VCORE19);                    //    Set the core to 1.9V
        SetFLL(CLOCK_MHZ);                        //    Set SMCLK to 25 MHz
        EnablePWM();                                //    Start PWM
        EnableADC();                                //    Does what it means.
        __bis_SR_register(LPM0_bits + GIE);        //    Enter LPM0(low power mode 0)
    }
    
    void EnablePWM(void) {
        PWM_DIR |= PWM_BIT;                        //    P1.2 output
        PWM_SEL |= PWM_BIT;                        //    P1.2 as timer output
        TA0CCR0 = PWM_PERIOD-1;                    //    PWM Period
        TA0CCTL0 = CCIE;                            //    Timer interrupt enabled
        TA0CCTL1 = OUTMOD_7;                        //    Reset when reaching CCR1 set when going back to 0.
        TA0CCR1 = angle;                            //    CCR1 PWM duty cycle
        TA0EX0 = 2;                                //    Divider by 3
        TA0CTL = TASSEL_2 +                        //    SMCLK (24 MHz)
                 MC_1 +                            //    Counting upwards
                 TACLR +                            //    Clear whet TACCR0 reached
                 ID_3;                            //    Divide by 8 (therefore by 24 with the above 3 divider)
    }
    
    void EnableADC() {
        ADC12CTL0 = ADC12ON+ADC12SHT0_2;            // Turn on ADC12, set sampling time
        ADC12CTL1 = ADC12SHP;                    // Use sampling timer
        ADC12CTL0 |= ADC12ENC;                    // Enable conversions
    }
    
    uint16 ReadADCVal(void) {
        uint16 retval;                            //    Value for debugging purpose
        ADC12CTL0 |= ADC12SC;                    //    Start conversion-software trigger
        while (!(ADC12IFG & BIT0));                //    Wait for the ADC
        retval = ADC12MEM0;                        //    Set breakpoint here to verify value
        return retval;
    }
    
    void SetAngle(int16 angle) {
        if(angle <= -500) TA0CCR1 = 1000;
        else if(angle >= 500) TA0CCR1 = 2000;
        else TA0CCR1 = MID_POINT + angle;
    }
    
    void UseADCVal(uint16 adcval) {
        int16 angleval;
        angleval = adcval;
        angleval >>= 2;                            //    Scale [0 .. 4096[ to [0 .. 1024[
        angleval -= 512;                            //    Shift [0 .. 1024[ to [-512 .. 512[
        SetAngle(angleval);
    }
    
    // Timer0 A0 interrupt service routine
    #pragma vector=TIMER0_A0_VECTOR
    __interrupt void TIMER0_A0_ISR(void) {
        uint16 adcval = ReadADCVal();
        UseADCVal(adcval);
    }
    ... à suivre ...

    Pascal
    Dernière modification par Murayama ; 09/10/2015 à 08h52. Motif: réarrangement

Discussions similaires

  1. Tutoriel pour webservice
    Par lordgodgiven dans le forum Internet - Réseau - Sécurité générale
    Réponses: 0
    Dernier message: 27/07/2013, 13h48
  2. Quel DUT (ou BTS) pour informatique embarquée
    Par invite48525701 dans le forum Orientation après le BAC
    Réponses: 10
    Dernier message: 12/10/2012, 11h45
  3. Recherche images CP pour tutoriel Nébulosity
    Par astro31 dans le forum Matériel astronomique et photos d'amateurs
    Réponses: 0
    Dernier message: 27/11/2010, 13h38
  4. Un petit tutoriel pour les utilisateurs de Mac
    Par inviteacb6b292 dans le forum Matériel astronomique et photos d'amateurs
    Réponses: 9
    Dernier message: 16/07/2009, 10h55
  5. SVP-Cherche tutoriel pour NOD32
    Par invite23950fd4 dans le forum Internet - Réseau - Sécurité générale
    Réponses: 9
    Dernier message: 23/08/2006, 13h51
Dans la rubrique Tech de Futura, découvrez nos comparatifs produits sur l'informatique et les technologies : imprimantes laser couleur, casques audio, chaises gamer...