Auteur : Flyers E-mail : flyers-at-next-touch-dot-com
Ecrit le : 05/06/2008 Dernière modification : 05/06/2008

Vous pouvez copier et/ou redistribuer tout ou partie de cet article si le nom et l'email de l'auteur figurent clairement sur les copies.


Introduction à la programmation embarquée

Table des Matières

  1. Introduction
  2. Syntaxe ARM
  3. Gestionnaire d'interruption
  4. Communication série
  5. Conclusion
  6. Annexe

Introduction



Bienvenue dans le monde de l'embarqué. Un monde qui paraît à des années lumières de la programmation "classique" en raison du peu de recherches et de documentations réalisées sur ce type d'environnement. Je vais tenté de remédier à ce petit problème en vous présentant l'interface AT91M55800A. Cette carte embarque :

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 :

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 fonction 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. Prenons l'exemple de stfmd r13, {r4-r6} comparer à stfmd r13!, {r4-r6} :


On voit à gauche que le résultat sans ! ne modifie pas le pointeur de pile contrairement à celui de droite.

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 :

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 les 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 :

le registre spsr (et cpsr en mode user) 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 :



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



On voit que les bits de poids faible 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 :

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.
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 puis en mode IRQ, 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 :


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.

Communication série


Au début de l'article, nous avons vu que la carte embarque 3 modules de communication série (USART). Nous allons donc voir comment utiliser ce module de communication avec gestion des interruptions sur envoi/réception de données.

Pour l'envoi, nous allons implémenter une fonction bloquante qui attendra la fin de l'envoie (flagTX) pour passer à la suite. Lors d'une fin d'envoie, une interruption est levée ce qui nous permet de quitter la boucle d'attente.

Concernant la lecture, la fonction sera non bloquante et traitera les erreurs de transmission. Sa valeur de retour (flagRX) sera :

Le gestionnaire d'interruption nous permettra de modifier ces valeurs de retour pour savoir si la transmission est finie ou s'il y a eut une erreur.

Commençons donc par l'envoi de données :

void write (char *data, unsigned int len)
{
	// Attente si transmission déjà en cours
	while (flagTX != 1) ;
	
	// Configuration du pointeur de données
	USART1->US_TPR = (int) data ;
	
	// Lancement de la transmission avec la longueur de la chaine à transmettre
	USART1->US_TCR = (short unsigned int) len ;
	
	// Activation des interruptions sur fin d'émission
	USART1->US_IER = US_ENDTX ;
}



La fonction d'envoi prend en argument les données à envoyer ainsi que la taille. On voit clairement la boucle d'attente de fin de transmission.
Il existe deux modes de transmission sur l'USART : le mode synchrone et le mode asynchrone. Nous utilisons ce second mode, donc pour transmettre des données, il nous suffit de placer le pointeur de données dans USART1->US_TPR et l'USART s'occupe de les transmettre. En mode synchrone, l'envoie se fait caractère par caractère via le registre USART1->US_THR.

S'ensuit la réception des données :


int read (char *data, unsigned int len)
{	
	// Initialisation du flag de lecture
	flagRX = 1 ;
	
	// Configuration du pointeur de données
	USART1->US_RPR = (int) data ;
	
	// Lancement de la réception avec la longueur de la chaine à recevoir
	USART1->US_RCR = (short unsigned int) len ;
	
	// Reset du time-out
	USART1->US_CR = US_STTTO ;
	
	// Activation des interruptions sur les différentes erreurs
	USART1->US_IER = US_OVRE | US_FRAME | US_TIMEOUT ;
	
	// Paramétrage du time-out
	USART1->US_RTOR = 100 ;
	
	// Attente de la fin de la réception ou de la réception d'une erreur
	//    Tant que la lecture n'est pas finie
	//    Et qu'il n'y a pas d'erreur de transmission
	while (((USART1->US_CSR & US_ENDRX) == 0) && (flagRX == 1)) ;
	
	// Retour du flag de lecture
	return flagRX ;
}



La réception se déroule comme l'écriture, on place le pointeur de buffer de réception dans USART1->US_RPR.
La mise à 1 du flag US_STTTO permet de lancer la détection de TIMEOUT après réception d'un caractère seulement.
USART1->US_RTOR contient le temps d'attente avant la levée d'un TIMEOUT (100 * 4 * temps de transmission d'un bit)

Et enfin le traitement des interruptions :


void USART1_IRQ (void)
{
	// Récupération des flags d'états de l'USART
	int CSR = USART1->US_CSR ;
	
	// Si fin de réception
	if ((CSR & US_ENDTX) == US_ENDTX)
	{
		// Modification du flag d'émission
		flagTX = 1 ;
		
		// Désactivation des interruptions d'émission
		USART1->US_IDR = US_ENDTX ;
	}
	
	// Si time-out
	if ((CSR & US_TIMEOUT) == US_TIMEOUT)
	{
		// Modification du flag de réception
		flagRX = 2 ;
		
		// Désactivation des interruptions sur time-out
		USART1->US_IDR = US_TIMEOUT ;
	}
	
	// Si overflow
	if ((CSR & US_OVRE) == US_OVRE)
	{
		// Modification du flag de réception
		flagRX = 3 ;
		
		// Désactivation des interruptions sur overflow
		USART1->US_IDR = US_OVRE ;
	}
	
	// Si framing error
	if ((CSR & US_FRAME) == US_FRAME)
	{
		// Modification du flag de réception
		flagRX = 4 ;
		
		// Désactivation des interruptions sur framing error
		USART1->US_IDR = US_FRAME ;
	}
}



Ici, il s'agit simplement de vérifier quelle interruption a été levée et de modifier le flag en conséquence. La désactivation des interruptions n'est pas nécessaire.

Au final, un petit programme qui test le fonctionnement du TIMEOUT :

#include "stdio.h"
#include "string.h"
#include "pio.h"
#include "pioa.h"
#include "piob.h"
#include "apmc55800.h"
#include "aic.h"
#include "usart.h"

#define PIOA_BASE 0xFFFEC000
#define USART1_BASE 0xFFFC4000

StructAIC *AIC ;
StructPIO *PIOA ;
StructUSART *USART1 ;
StructAPMC *APMC ;

volatile int flagTX = 1 ;
volatile int flagRX = 1 ;

void Initialise (void) ;
void InitIRQ(void);
void write (char *data, unsigned int len) ;
int read (char *data, unsigned int len) ;
void USART1_IRQ (void) ;

extern void isr (void) ;

int main()
{	
	char tbuf[] = "test";
	char rbuf[15] = "" ;
	int retour ;

	AIC = (StructAIC*) AIC_BASE ;
	PIOA = (StructPIO*)PIOA_BASE ;
	USART1 = (StructUSART*)USART1_BASE ;
	APMC = (StructAPMC*)APMC_BASE ;

	Initialise();
	InitIRQ () ;
	
	// Envoi de la chaine de test
	write(tbuf, strlen(tbuf)) ;
		
	// Réception de cette même chaine et test du time-out
	retour = read(rbuf, strlen(tbuf)+1) ;
	
	// Affichage console du résultat
	printf("%d\n%s\n", retour, rbuf) ;
	
	return 0;
}

void Initialise (void)
{
	// Activation de l'horloge de l'USART
	APMC->APMC_PCER = 1 << 3 ;
	
	// Redirection des pins du PIO sur l'USART
	PIOA->PIO_PDR = PA18 | PA19 ;
	
	// Reset de l'USART
	USART1->US_CR = US_RSTRX | US_RSTTX ;
	
	// Configuration de la vitesse de transmission 38400
	USART1->US_BRGR = 52 ;
	
	// Configuration de l'USART (loopback 8bits sans parité avec bits de stop)
	USART1->US_MR = US_CHMODE_LOCAL_LOOPBACK | US_NBSTOP_1 | US_PAR_NO | US_CHRL_8 | US_CLKS_MCK ;
	
	// Activation de l'USART
	USART1->US_CR = US_TXEN | US_RXEN ;
}

void InitIRQ(void)
{
	// Configuration de la ligne USART de l'AIC
	AIC->AIC_SMR[3] = AIC_SRCTYPE_INT_LEVEL_SENSITIVE | 0x06 ;
	// Paramétrage du handler d'interruption
	AIC->AIC_SVR[3] = (int) isr ;
	// Activation des interruptions
	AIC->AIC_IECR = (1<<3) ;	
}

void write (char *data, unsigned int len)
{
	// Attente si transmission déjà en cours
	while (flagTX != 1) ;
	
	// Configuration du pointeur de données
	USART1->US_TPR = (int) data ;
	
	// Lancement de la transmission avec la longueur de la chaine à transmettre
	USART1->US_TCR = (short unsigned int) len ;
	
	// Activation des interruptions sur fin d'émission
	USART1->US_IER = US_ENDTX ;
}

int read (char *data, unsigned int len)
{	
	// Initialisation du flag de lecture
	flagRX = 1 ;
	
	// Configuration du pointeur de données
	USART1->US_RPR = (int) data ;
	
	// Lancement de la réception avec la longueur de la chaine à recevoir
	USART1->US_RCR = (short unsigned int) len ;
	
	// Reset du time-out
	USART1->US_CR = US_STTTO ;
	
	// Activation des interruptions sur les différentes erreurs
	USART1->US_IER = US_OVRE | US_FRAME | US_TIMEOUT ;
	
	// Paramétrage du time-out
	USART1->US_RTOR = 100 ;
	
	// Attente de la fin de la réception ou de la réception d'une erreur
	//    Tant que la lecture n'est pas finie
	//    Et qu'il n'y a pas d'erreur de transmission
	while (((USART1->US_CSR & US_ENDRX) == 0) && (flagRX == 1)) ;
	
	// Retour du flag de lecture
	return flagRX ;
}

void USART1_IRQ (void)
{
	// Récupération des flags d'états de l'USART
	int CSR = USART1->US_CSR ;
	
	// Si fin de réception
	if ((CSR & US_ENDTX) == US_ENDTX)
	{
		// Modification du flag d'émission
		flagTX = 1 ;
		
		// Désactivation des interruptions d'émission
		USART1->US_IDR = US_ENDTX ;
	}
	
	// Si time-out
	if ((CSR & US_TIMEOUT) == US_TIMEOUT)
	{
		// Modification du flag de réception
		flagRX = 2 ;
		
		// Désactivation des interruptions sur time-out
		USART1->US_IDR = US_TIMEOUT ;
	}
	
	// Si overflow
	if ((CSR & US_OVRE) == US_OVRE)
	{
		// Modification du flag de réception
		flagRX = 3 ;
		
		// Désactivation des interruptions sur overflow
		USART1->US_IDR = US_OVRE ;
	}
	
	// Si framing error
	if ((CSR & US_FRAME) == US_FRAME)
	{
		// Modification du flag de réception
		flagRX = 4 ;
		
		// Désactivation des interruptions sur framing error
		USART1->US_IDR = US_FRAME ;
	}
}



Concernant l'initialisation de l'USART (Initialise()), on désactive les pin 18 et 19 du PIOA pour relier directement l'USART à ces pins (comme on peut le voir sur le schémas en annexe).
La vitesse de transmission se calcul de la sorte : vitesse horloge / (16 * valeur dans US_BRGR). Pour les tests nous sommes rester en loopback (envoi des données simulé). Et enfin l'activation de l'envoi / réception se fait via le registre USART1->US_CR.
Au niveau de l'initialisation des interruptions (InitIRQ()), le code est le même que précédemment à la différence du type d'interruption activée. L'activation des interruptions au niveau de l'USART (USART1->US_IER) se fait sur envoi / réception

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.

Flyers <flyers-at-next-touch-dot-com>