Programmation ARM

Introduction à la programmation embarquée

Introduction

Bienvenue dans le monde de l'embarqué. J'avais écrit cet article lors de mes études en 2007 à l'époque. Ce qui est intéressant avec le monde de l'embarqué, c'est que les technologies ne peuvent pas évoluer aussi vite que sur le web car en fonctionnement autonome. Je vais tenté de vous présenter une introduction en douceur grâce à l'interface AT91M55800A. Cette carte embarque :
  • un processeur ARM7TDMI
  • 8K de SRAM
  • 1 interface JTAG
  • 2 modules d'entrées/sorties (PIOA et PIOB)
  • 6 modules d'horloge/comptage (TC Timer Counter)
  • 3 modules de communication série (USART)
  • 1 module de gestion d'énergie (APMC)
  • 1 gestionnaire d'interruptions (AIC)
  • 8 modules de convertion Analogique/Numérique (ADC)
  • 2 modules de convertion Numérique/Analogique(DAC)
  • etc...
Vous me demanderez certainement pourquoi cette carte et pas une autre ? Je vous répondrais que c'est tout simplement la seule que j'ai à disposition. De toute façon, en matière de gestion d'interruptions et de programmation bas niveau, la méthode reste toujours la même, seul change la syntaxe. Nous allons donc voir comment programmer sur cette bestiole.
Pour cela, j'utilise la suite logiciel ADS mais il existe aussi la suite GCC ARM qui est open source. Les codes fournis dans cet article ne seront pas compatible avec GCC ARM du moins la partie traitant de la gestion des interruptions. N'oubliez pas sous ADS de configurer le linker pour qu'il charge le code à l'adresse 0x0200 0000 si vous voulez le tester.
Commençons donc par les bases, l'assembleur ARM.

Syntaxe ARM

Le but de cet article n'est pas de fournir une documentation exhaustive de la syntaxe ARM, nous ne verrons ici que ce dont nous aurons besoin pour la suite du tutoriel. Tout d'abord, avant de commencer, il faut savoir que les processeurs ARM (tout comme son concurrent MIPS) sont des processeurs de type RISC contrairement aux processeurs de nos stations de travail, les CISC. C'est à dire que ces processeurs ont un set d'instructions limité du fait de leurs structures interne. L'intérêt étant qu'il est possible de programmer toutes les opérations complexes réalisées par les processeurs CISC avec ce set d'instruction limité. Du coup, on se retrouve avec des processeurs ayant une plus grande autonomie, et souvent plus petits. Par exemple, il n'existe aucune mnémonique de division en assembleur ARM. Nous avons à notre disposition 16 registres de 32 bits :
  • r0 à r12 sont des registres d'accumulation tout comme eax avec intel.
  • r13 est le registre pointant sur la pile, équivalent à esp.
  • r14 est le registre contenant l'adresse de retour d'appel d'une fonction, équivalent à ebp.
  • r15 est le registre contenant l'adresse de l'instruction en cours d'éxécution, équivalent à eip.
Ainsi qu'un ensemble de mnémoniques que je détaillerais au fur et à mesure.
Le passage de paramètres à une fonction s'effectue par le biais des registres r0 à r3 puis il faut ensuite les empiler sur la pile. C'est à la fonction de dépiler ces arguments et les sauvegarder lors d'appel récursif par exemple. La valeur de retour de la fonction est passée dans r0
Voyons tout cela avec la function dummy:
      int dummy(int a, int b, int c, int d)
      {
        return a+2*b+3*c+4*d;
      }
      
ce qui donne en assembleur ARM :
      dummy :
        ; r0 = a
        ; r1 = b
        ; r2 = c
        ; r3 = d
        stmfd r13!, {r4-r12,r14} ; sauvegarde des arguments
        add r1, r1, r1  ; r1 = r1+r1
        add r2,r2,r2,LSL #1 ; r2 = r2+r2*2
        add mov r3,r3,LSL #2 ; r3 = r3*4
        add r0, r0, r1  ; r0 = r0+r1
        add r0, r0, r2 ; r0 = r0+r2
        add r0, r0, r3 ; r0 = r0+r3
        ldmfd r13!, {r4-r12, r14} ; restauration des arguments
      
stmfd est une mnémonique permettant de pusher dans la première opérande (r13 dans notre cas) les registres entre accolade (ici on push r4 à r12 et r14). Le ! signifie que l'on veut incrémenter l'adresse de r13 d'autant de données que l'on push.
Très bien maintenant nous allons voir les tests logiques :
      if (a == b)
      {
        a += b;
      }
      else
      {
        b += a;
      } 
      
donnera :
      ; r0 = a
      ; r1 = b
        cmp r0, r1
        beq then
        b else
      then
        add r0, r0, r1 ; a += b 
        b end
      else
        add r1, r1, r0 ; b += a
      end
      
Ici le point important est la mnémonique b qui permet de sauter à un label. b sans condition est un saut inconditionnel (on s'en serait douter) exactement comme jmp. beq suit la condition "equal" et sautera à then que si a = b. Voici une liste de quelques conditions :
Flag Sens
EQ égal
NE différent
HI supérieur (non signé)
LS inférieur ou égal (non signé)
GE supérieur ou égal (signé)
LT inférieur (signé)
Enfin la dernière notion que nous devons aborder, et pas des moindre, c'est l'accès à la mémoire.
      int *ptr = var;
      ptr = ptr +4 // incrément du pointeur
      *ptr = 10 // mets 10 en valeur pointée par le pointeur
      
donnera :
      ;;; initialisation du pointeur
      ldr r0, =var  ;; récupération de l'adresse de la variable
      str r0, ptr
      ;;; modification du pointeur
      ldr r0, ptr
      add r0,r0,#4
      str r0,ptr
      ;;; modification de la valeur pointée
      ldr r0, ptr
      mov r1, #10
      str r1, [r0]
      
l'opérateur = permet de récupérer l'adresse d'une variable. la mnémonique ldr permet de charger dans le registre la seconde opérande quant à str il permet de stocker le registre dans la seconde opérande.
Nous n'avons fait que survoler très rapidement l'assembleur ARM et je ne peux que vous conseillez de lire l'ARM Assembly Language Programming pour approfondir vos connaissances. Dans la suite de l'article je reviendrais sur chaque nouvelle notion rencontrée.

Gestionnaire d'interruption

Nous rentrons maintenant dans le vif du sujet et je vous conseil de vous accrochez un peu. En effet, la gestion des interruptions est une des base de la programmation d'un micro-processeur. Il faut comprendre le principe une fois car la méthode reste similaire sur d'autres plateformes. Vous vous rappelez peut-être que je vous ai dit qu'il n'y a que 16 registres disponibles sur une architecture ARM ? Et bien ce n'est pas tout à fait vrai. En effet, il en existe en réalité 32 mais ils sont répartis sur plusieurs contextes car le processeur peut fonctionner sous différents modes :
  • User : fonctionnement normal
  • FIQ : actif lors d'une interruption de type FIQ
  • IRQ : actif lors d'une interruption de type IRQ
  • Supervisor : mode superviseur
  • Abort
  • Undefined
  • System
Le registre cpsr (et spsr en mode superviseur) permet de connaître l'état dans lequel se trouve le microprocesseur. Voici un tableau récapitulatif des registres accessibles en fonction de l'état du processeur : registres 


Et ci-après l'organisation du registre cpsr/spsr:


cpsr

On voit que les bits faibles donnent l'état courant du processeur tandis que les bits FIQ et IRQ permettent d'activé le passage en mode FIQ et IRQ. Le changement d'état du processeur s'effectue lors de la levée d'exception. Une exception est levée dans différents cas :
  • Un Reset : on passe en mode Supervisor
  • Une instruction inconnue : on passe en mode Undefined
  • Une interruption logiciel (mnémonique swi) : on passe en mode Supervisor
  • Impossibilité d'accéder à l'instruction ou à une zone mémoire : on passe en mode Abort
  • Une Interruption IRQ : on passe en mode IRQ
  • Une Interruption FIQ : on passe en mode FIQ
Nous nous intéresserons plus particulièrement à la gestion des interruptions de type IRQ. En effet les FIQ ne sont utiles que dans le cas où nous voulons gérer une interruption sans rien sauvegarder dans la pile (ce qui est impossible dans le cas de l'ordonnancement traité à la fin de l'article). Comme je l'ai dit en début d'article la carte embarque un gestionnaire d'interruption (AIC) que nous devons activer pour pouvoir gérer les interruptions. Je vous conseil vivement d'avoir la datasheet sous les yeux pour mieux comprendre les codes qui vont suivre. Bien, commençons donc par la routine de traitement d'interruption. Dans cette routine, nous passons en mode superviseur ce qui n'est faisable qu'en assembleur (inline ou non). Ce qui donne :
      EXPORT isr
        IMPORT pa9_irq_C_handler
        
        AREA EX2, CODE, READONLY
      
      AIC_BASE EQU 0xFFFFF000
      AIC_EOICR EQU 0x130
      
      isr
      
      ;/* sauvegarde de l'adresse de retour*/
        SUBS lr,lr, #4
        STMFD sp!, {r0, lr}
      ;/* sauvegarde du SPSR */
        MRS r0, SPSR
        STMFD sp!, {r0}
      ;/* on réautorise les interruptions et l'on passe en mode svc*/
      ;/* afin de bénéficier de la pile superviseur */
        MRS r0, spsr
        BIC r0, r0, #0x80 ; mise à 0 du flag Interruption IRQ
        BIC r0, r0, #0x1F ; mise à 0 des flags M4..M0
        ORR r0, r0, #0x13 ; mode superviseur activer
        MSR cpsr_c, r0
      ;/* appel de la fonction de traitement*/ 
        STMFD SP!, {r1-r3,r12,lr}
        
        BL pa9_irq_C_handler
        
        LDMFD SP!, {r1-r3,r12,lr}
      ;/* On repasse en mode irq avec interruption bloquées*/
        MRS r0, cpsr
        BIC r0, r0, #0x1F ; mise à 0 des flags M4..M0
        ORR r0, r0, #0x80 | 0x12 ; mise à 1 du flag Interruption IRQ et passage en mode IRQ
      ;/* On acquitte l'AIC pour signifier que l'interruption a été traité*/
        LDR r0, =AIC_BASE
        STR r1, [r0, #AIC_EOICR]
      ;/* on restore les registres*/
        LDMFD sp!, {r0} ; on récupère le SPSR qui avait été sauvegardé
        MSR SPSR_cxsf, r0
      ;/* retour d'interruption*/
        LDMFD sp!, {r0, pc}^
      
        END
      
Cette routine s'occupe seulement d'exécuter une fonction C (ici pa9_irq_C_handler) en mode superviseur pour traiter l'interruption.
A noter le décalage réaliser au tout début "SUBS lr,lr, #4" permettant d'exécuter l'instruction suivante après le traitement d'interruption. Le décallage dépend de l'interruption qui a été levée. En effet en modifiant le registre lr, c'est l'adresse de retour de l'interruption qui est modifiée.
Ce tableau récapitule les décalages à appliquer en fonction des différentes interruptions :
exception
Nous avons réalisé une routine de gestion d'interruption générique. Mais pour pouvoir lever des interruptions, il nous faut activer l'AIC et les ports ou interfaces sur lesquels les interruptions seront "catchées".
      #include "pio.h"
      #include "pioa.h"
      #include "piob.h"
      #include "apmc55800.h"
      #include "stdio.h"
      #include "aic.h"
      
      #define PIOA_BASE 0xFFFEC000
      #define PIOB_BASE 0xFFFF0000
      
      void InitialiseIRQ (void) ;
      
      void InitialiseBP (void) ;
      
      void InitialiseLED (void) ;
      
      void AfficheLed (unsigned char a) ;
      
      extern void isr (void) ;
      
      StructAIC *AIC ;
      StructPIO *PIOA ;
      StructPIO *PIOB ;
      StructAPMC *APMC ;
      
      unsigned char cpt = 0;
      
      int main (void)
      {
        AIC = (StructAIC*) AIC_BASE ;
        PIOA = (StructPIO*) PIOA_BASE ;
        PIOB = (StructPIO*) PIOB_BASE ;
        APMC = (StructAPMC*) APMC_BASE ;
        
        InitialiseBP () ;
        InitialiseLED () ;
        AfficheLed(0);
        InitialiseIRQ () ;
        
        
        while (1)
        {
          AfficheLed(cpt);
        }
        
        return 0;
      }
      
      void InitialiseIRQ (void)
      {
      /* interruption sur niveau et de priorité = 3 */
        AIC->AIC_SMR[13] = AIC_SRCTYPE_INT_LEVEL_SENSITIVE | 0x03 ;
      /* adresse de la routine de traitement des interruption du port A */ 
        AIC->AIC_SVR[13] = (int) isr ;
      /* activation des irq du pioa */ 
        AIC->AIC_IECR = (1<<13) ;
      /* activation des irq dues à pa9 */ 
        PIOA->PIO_IER = PA9 ;
      }
      
      void InitialiseBP (void)
      {
      /*On active les horloges de périphérique pour pioa et piob*/
        APMC->APMC_PCER = 0x6000 ;
        
      /*activation de PA9 en entrée */ 
        PIOA->PIO_PER = PA9 ;
        PIOA->PIO_ODR = PA9 ;
      }
      
      void InitialiseLED (void)
      {
      /* Les LED sont attachée au portB PB8 -> PB15 */
      /* On active les 8 bits du Port B auxquels sont attachée les led */
        PIOB->PIO_PER = 0x0000FF00 ;
      /* On met ces 8 bits en sortie */
        PIOB->PIO_OER = 0x0000FF00 ;
      /* Une led est allumée lorsque la sortie du Port B est à 0 */
      /* On les éteint toutes PB8-> PB15 = "0xFF" */
        PIOB->PIO_SODR = 0x00FF00 ;
      }
      
      void AfficheLed (unsigned char a)
      {
      /* Constitution du mot permettant de mettre le port à 0 */
      /* Les led sont allumées si la sortie est à 0 */
        PIOB->PIO_SODR = (~a << 8) & 0x00FF00 ;
      /* Constitution du mot permettant de mettre le port à 1 */
      /* On inverse les bits 8 à 15 du mot précédent */ 
        PIOB->PIO_CODR = (a << 8) & 0x00FF00 ;
      }
      
      void pa9_irq_C_handler(void)
      {
      
        /* on acquitte l'irq du périphérique par lecture du registre d'état */
        /* on vérifie par la même occasion que l'IRQ est bien due à PA9 */
        if ( PIOA->PIO_ISR == (1<< 9) )
        {
          /* test que c'est un appui */
          if ((PIOA->PIO_PDSR & (1<<9) )==0)
          /* On incrémente le compteur */
            cpt++;
        }
        else /* interruption non due à PA9 on ne fait rien de spécial*/
          {}
      }
      
Ce code est très simple, il active les interruptions sur le port 19 du PIOA (correspondant à un bouton poussoir) ainsi lors de l'appui sur le bouton, une interruption de type IRQ sera levée. Le gestionnaire d'interruption s'occupe ensuite d'afficher sur les leds de la carte une valeur s'incrémentant à chaque interruption catchée.
Ce qui nous intéresse ici c'est la fonction InitialiseIRQ(). Dans celle-ci on définit que l'interruption IRQ sera levée lors d'un changement de niveau (haut -> bas ou bas -> haut) du signal en entrée sur le port 19 du PIOA. Puis on définit quelle est la routine d'interruption à exécuter. Enfin on active les interruptions IRQ au niveau de l'AIC et au niveau du PIOA.
Voici un tableau récapitulatif des sources d'interruptions possible gérées par l'AIC. On voit que le numéro 13 correspond à l'interruption IRQ du PIOA.
arm aic interupts

Communication série

Au début de l'article, nous avons vu que la carte embarque 2 modules de communication série (USART). Nous allons donc voir comment utiliser ce module de communication. Dans un premier temps nous ne fairons qu'envoyer et recevoir des informations sans gérer d'interruptions. Puis, nous verrons comment gérer les interruptions de l'USART.

Ordonnancement

Nous allons maintenant voir comment gérer un ordonnancement basique de processus à l'aide du module TC (Timer/Counter). Le but étant de commuter la tâche en exécution toutes les millisecondes.

Conclusion

Voilà, vous savez maintenant comment gérer des interruptions sur un processeur de type ARM. Les modes de fonctionnement des processeurs ARM7 se retrouvent dans les processeurs ARM9, ce qui veut dire que la gestion des interruptions reste quasiment similaire. Vous en savez maintenant autant que moi en matière de programmation embarquée :). Si vous voulez approfondir, je vous encourage à parcourir la datasheet qui contient des informations très complémentaires et exhaustives, même si elle est quelque peu indigeste. J'espère que cet article vous aura plu et si vous avez des questions, n'hésitez pas.

Annexe

Schémas simplifié de la carte AT91M55800A.
carte

No comments:

Post a Comment

Merci de votre commentaire. Celui-ci est en cours de modération.

Most seen