Rustre's Corner

Les interruptions : la base

Nous avons vu dans le cours précédent comment allumer une led et la faire clignoter à intervalles réguliers par le biais de délais. Cette méthode marche (heureusement ;), mais elle a un inconvénient majeur : un délai est une fonction qui demande au PIC d'attendre... pendant ce temps le PIC ne fait strictement rien d'autre. Illustrons ca dans un petit diagramme.

délais

Le pic démarre, et arrive tout de suite dans la boucle infinie du "while(1)". Dans cette boucle la led est allumée, puis le PIC attends, puis elle est éteinte, le PIC attends à nouveau, puis la led est allumée... et le PIC ne peux rien faire d'autre que ça. Si on voulait par exemple faire un long calcul (un calcul qui prendrai plus d'une seconde par exemple), le temps de faire ce calcul nous ne pourrions pas allumer et/ou éteindre la led. En effet le diagramme ressemblerait alors à ça :

délais et calcul

Mais alors comment faire? Sommes-nous perdu? Le père fouettard va-t-il venir nous punir et formatter notre disque dur? Non rassurez-vous, car il existe quelque chose de magique sur les PIC (et sur pas mal d'autres environnements, comme par exemple sur nos processeurs d'ordinateur) : les interruptions ! Mais késako les interruptions? C'est un mécanisme qui permet au processeur d'exécuter un morceau de programme qui n'est pas dans la boucle principale. En quelques sortes, ce sont quelques lignes de code qui seront exécutées à un moment précis, géré par le processeur en réponse à un évenement, quelle que soit la position dans le code ou nous étions. Le diagramme ressemble alors à ceci :

interruption

Les interruptions sont, comme décrit plus haut, une réaction suite à un évènement. Il y a plusieurs évènements possible pour lever une interruption, comme par exemple les timers, les fin de conversion analogique/numérique etc... Il y a par ailleurs sur les PICs deux niveaux d'interruptions : deux priorités, haute et basse. Ainsi une interruption de priorité basse va "interrompre" une exécution de code, mais pourra elle-même être interrompue par une interruption de priorité haute.

interruption haute et interruption basse

Dans notre cas la solution serait donc de faire le calcul dans la boucle infinie, et d'avoir une interruption toutes les 0,256s pour allumer ou éteindre la LED. Ainsi le calcul se fera, et nous pourrons continuer à allumer et éteindre la LED toutes les 0,256 secondes. Ne vous avais-je pas dit que c'était magique? :-D Il ne nous reste plus qu'à trouver l'évènement qui nous permettra d'interrompre le "long calcul" quand nous le voulons : nous allons utiliser un timer. Un timer incrémente un compteur de 1 chaque cycle d'horloge. Une fois qu'il arrive au maximum (65535 pour un timer sur 16 bits), il se remet à 0, et déclenche un évènement. Il nous suffira donc, pour déclencher l'évènement (et donc l'interruption) au moment que l'on veut, d'initialiser le compteur à la bonne valeur : dans notre cas, pour un timer qui se déclenche au bout de 0,25s il nous faut 2 500 000 cycles (vu qu'il y a un cycle toutes les 0,0000001 seconde). Il va donc falloir aller de 0 à 65535, et celà 38 fois, puis de 55865 à 65535 (2 500 000 - (65535 * 38) = 9670, et 65535 - 9670 = 55865).

Récapitulons : nous allons créer un timer qui se déclenchera 38 fois au bout du temps maximal (65535 cycles), puis la 39ème fois, au bout de 9670 cycles (pour avoir un total de 2 500 000 cycles d'horloge = 0,25s). Nous allons mettre cela en pratique dans le programme suivant.

Pour simplifier le code, dans le programme suivant nous n'allons pas nous compliquer la vie, et nous allons arrondir les 2 500 000 cycles à 65535 * 38 = 2 490 330 cycles.


Les interruptions : application pratique avec une led

Voici le programme :

/* PIC 18F252 quartz 10Mhz + PLL = 40Mhz */
/* Un timer allume puis éteint la 
 * led à intervalles réguliers */

#include <p18f252.h> // déclarations pour le PIC18F252
#include <timers.h> 	// fonctions pour les timers

// pin 0 du port A
#define IO_LED	PORTAbits.RA0
// registre de controle du port A
#define IO_LED_TRIS TRISAbits.TRISA0

// LED allumée
#define LED_ON 1
// LED éteinte
#define LED_OFF 0

// stock l'état de la LED
volatile short led_status;
// variable utilisée pour compter les interruptions
volatile short temporisation;

/* fonction interruption */
void MyInterrupt(void);

// on déclare que lors d'une interruption
#pragma code highVector=0x008
void atInterrupthigh(void)
{
// on doit éxecuter le code de la fonction MyHighInterrupt
_asm GOTO MyInterrupt _endasm		
}
#pragma code // retour à la zone de code

// ************************
// ****  Interruptions ****
// ************************
#pragma interrupt MyInterrupt 
void MyInterrupt(void)
{
	unsigned char sauv1;
	unsigned char sauv2;

	// on sauvegarde le contenu des registres de calcul
	sauv1 = PRODL;
	sauv2 = PRODH;	

	// c'est le Timer1 qui a levé l'interruption
	if (PIR1bits.TMR1IF) 
	{
		// on va réautoriser l'interruption
		PIR1bits.TMR1IF = 0; 
		
		// ca y est on a attendu 2 500 000 cycles (0,25s)
		if (temporisation >= 38) 
		{
			// on remet à 0 pour le prochain timer
			temporisation = 0;  

			// si la LED était allumée
			if (led_status == LED_ON) 
			{
				// on l'éteint 
				IO_LED = 0;
				// stock le nouvel état de la LED
				led_status = LED_OFF; 
			}
			else // sinon la LED était éteinte
			{
				// on l'allume
				IO_LED = 1;
				// stock le nouvel état de la LED
				led_status = LED_ON;
			}
		}
		else // on n'a pas encore attendu 2 500 000 cycles
		{
			// on incrémente notre variable compteur
			temporisation++;
		}	
	}

	// on restaure les registres de calcul
	PRODL = sauv1;
	PRODH = sauv2;		
}

/* fonction principale */
void main (void)
{
	// on initialise le compteur
	temporisation = 0;

	// on configure le pin 0 du port A en écriture
	IO_LED_TRIS = 0;	
	// et on vérifie que la LED est éteinte
	IO_LED = 0;

	// et on stock le nouvel état de la LED
	led_status = LED_OFF;

	// on va créer le timer
	OpenTimer1(TIMER_INT_ON & T1_16BIT_RW & T1_SOURCE_INT 
	   & T1_PS_1_1 & T1_OSC1EN_OFF & T1_SYNC_EXT_OFF);

	// On autorise toutes les interruptions
	INTCONbits.GIE = 1;
	INTCONbits.PEIE = 1;

	while (1) {
		// traitement principal
	}
}

/* configuration du PIC */
#pragma romdata CONFIG
_CONFIG_DECL (
_CONFIG1H_DEFAULT & _OSC_HSPLL_1H,
_CONFIG2L_DEFAULT & _PWRT_ON_2L,
_CONFIG2H_DEFAULT & _WDT_OFF_2H,
_CONFIG3H_DEFAULT,
_CONFIG4L_DEFAULT & _STVR_OFF_4L & _LVP_OFF_4L 
   & _DEBUG_OFF_4L,
_CONFIG5L_DEFAULT & _CP0_OFF_5L & _CP1_OFF_5L 
   & _CP2_OFF_5L & _CP3_OFF_5L,
_CONFIG5H_DEFAULT & _CPB_OFF_5H & _CPD_OFF_5H,
_CONFIG6L_DEFAULT & _WRT0_OFF_6L & _WRT1_OFF_6L 
   & _WRT2_OFF_6L & _WRT3_OFF_6L,
_CONFIG6H_DEFAULT & _WPC_OFF_6H & _WPB_OFF_6H 
   & _WPD_OFF_6H,
_CONFIG7L_DEFAULT & _EBTR0_OFF_7L & _EBTR1_OFF_7L 
   & _EBTR2_OFF_7L & _EBTR3_OFF_7L,
_CONFIG7H_DEFAULT & _EBTRB_OFF_7H
); 
#pragma romdata

On retrouve comme dans le programme précédent la partie en fin de programme qui va "configurer" le PIC. Nous ne nous étendrons pas sur ce domaine, veuillez consulter le datasheet du PIC pour en apprendre plus! Détaillons maintenant le programme :

/* PIC 18F252 quartz 10Mhz + PLL = 40Mhz */
/* Un timer allume puis éteint la 
 * led à intervalles réguliers */

#include <p18f252.h> // déclarations pour le PIC18F252
#include <timers.h> 	// fonctions pour les timers

// pin 0 du port A
#define IO_LED	PORTAbits.RA0
// registre de controle du port A
#define IO_LED_TRIS TRISAbits.TRISA0

// LED allumée
#define LED_ON 1
// LED éteinte
#define LED_OFF 0

Les deux premières lignes incluent les définitions correspondant à notre PIC, et la librairie pour les timers. Les deux lignes suivantes débutant par #define" sont des définitions (qui s'en serait douté :P). Elles vont servir à indiquer au préprocesseur que nous utiliserons le nom IO_LED pour parler du pin 0 du port A (PORTAbits.RA0), et de même pour le registre de controle du port A (qui va configurer le pin 0 du port A soit en écriture, soit en lecture). Ce #define nous permet d'utiliser un nom plus pratique, et surtout de pouvoir modifier le pin (ou même le port) si nécéssaire, sans avoir à modifier toutes les occurences dans le programme (imaginez un programme de 10000 lignes, avec 1000 occurences de ce pin et de ce port, si il fallait toutes les changer, cela deviendrait vite problématique!). De la même manière, on définit que le chiffre 1 représente une LED allumée, et le chiffre 0 une LED éteinte. Ce choix n'est pas innocent, en écrivant un 1 sur le pin, cela va mettre le pin à l'état haut (5V : LED allumée) et vice versa.

// stock l'état de la LED
volatile short led_status;
// variable utilisée pour compter les interruptions
volatile short temporisation;

Voilà la déclaration de deux variables volatiles : une qui va retenir l'état de la LED (allumée ou éteinte), et l'autre qui va nous permettre de compter 38 itérations de timer. Volatile permet d'accéder à ces variables même dans le code d'interruption.

/* fonction interruption */
void MyInterrupt(void);

// on déclare que lors d'une interruption
#pragma code highVector=0x008
void atInterrupthigh(void)
{
// on doit éxecuter le code de la fonction MyHighInterrupt
_asm GOTO MyInterrupt _endasm
}
#pragma code // retour à la zone de code

Voilà un morceau de code un peu complexe : tout ce qu'il fait, c'est indiquer au préprocesseur que le vecteur d'interruption de priorité élevé doit "pointer" sur la fonction MyInterrupt. L'instruction _asm sert à coder directement en langage assembleur (langage de bas niveau). Le code assembleur dit juste : "aller à la fonction MyInterrupt".

// ************************
// ****  Interruptions ****
// ************************
#pragma interrupt MyInterrupt 
void MyInterrupt(void)
{
	unsigned char sauv1;
	unsigned char sauv2;

	// on sauvegarde le contenu des registres de calcul
	sauv1 = PRODL;
	sauv2 = PRODH;	

	// c'est le Timer1 qui a levé l'interruption
	if (PIR1bits.TMR1IF) 
	{
		// on va réautoriser l'interruption
		PIR1bits.TMR1IF = 0; 
		
		// ca y est on a attendu 2 500 000 cycles (0,25s)
		if (temporisation >= 38) 
		{
			// on remet à 0 pour le prochain timer
			temporisation = 0;  

			// si la LED était allumée
			if (led_status == LED_ON) 
			{
				// on l'éteint
				IO_LED = 0;
				// stock le nouvel état de la LED
				led_status = LED_OFF; 
			}
			else // sinon la LED était éteinte
			{
				// on l'allume
				IO_LED = 1;
				// stock le nouvel état de la LED
				led_status = LED_ON;
			}
		}
		else // on n'a pas encore attendu 2 500 000 cycles
		{
			// on incrémente notre variable compteur
			temporisation++;
		}	
	}

	// on restaure les registres de calcul
	PRODL = sauv1;
	PRODH = sauv2;		
}

Et voilà la fonction d'interruption en question : on sauvegarde les registres de calcul (registres qui peuvent être utilisés par le code du programme principal pour stocker des résultats de calculs), puis on vérifie que l'évènement a bien été généré par notre timer (car il pourrait être généré par d'autres timers, ou une conversion analogique/numérique, par de la communication série...). Ensuite on réactive l'interruption (on réautorise le timer à générer un évènement). Ensuite rien de bien malin, on compte le nombre de fois que le timer s'est déclenché, si c'est plus de 38 fois, on allume ou on éteint la LED, selon son état précédent, sinon on augmente notre compteur.

/* fonction principale */
void main (void)
{
	// on initialise le compteur
	temporisation = 0;
	// on configure le pin 0 du port A en écriture
	IO_LED_TRIS = 0;
	// et on vérifie que la LED est éteinte
	IO_LED = 0;

	// et on stock le nouvel état de la LED
	led_status = LED_OFF; 

Et pour finir le programme principal : on initialise nos variables (temporisation et led_status), on configure le pin 0 du port A en écriture, on éteint la LED (qui l'était vu que par défaut les pattes du PIC sont à 0 lors du démarrage).

// on va créer le timer
	OpenTimer1(TIMER_INT_ON & T1_16BIT_RW & T1_SOURCE_INT 
	   & T1_PS_1_1 & T1_OSC1EN_OFF & T1_SYNC_EXT_OFF);

Voilà le passage "compliqué" : la création du timer. Reportez-vous au fichier PDF des librairies de C18 pour connaitre l'utilisation exacte de cette fonction. Ici on on demande au timer de générer un évènement, d'utiliser le mode 16bits, l'horloge T1 comme source d'interruption, un prescaler de 1 (pas de prescaler), pas d'oscillation interne, et pas de synchronisation externe. Le prescaler permet de ne déclencher l'interruption qu'une fois sur deux, ou une fois sur quatre, ou une fois sur 8, ou ... (reportez-vous au fichier pdf sur les libc). Dans notre cas, nous aurions pu nous servir d'un prescaler pour ne pas avoir besoin de compter avec la variable temporisation (ou en tout cas moins ;).

// On autorise toutes les interruptions
	INTCONbits.GIE = 1;
	INTCONbits.PEIE = 1;

	while (1) {
		// traitement principal
	}
}

Nous arrivons à la fin du programme : les deux premières lignes mettent les bits à 1 dans les registres gérant les interruptions. Cela les active, les autorise, permettant aux timers (entre autres) de lever des interruptions lorsqu'ils génèrent des évènements. Et finalement, la boucle infinie "while(1)" que nous connaissons déjà. Elle est vide pour le moment, mais elle pourra contenir à l'avenir des traitements divers (comme par exemple une scrutation de pins de ports en lecture, pour détecter l'appui sur un bouton, ou de calculs complexes, gestion de capteurs, traitements, conversions analogique/numérique...).

Voilà le fichier source ainsi que le fichier .hex compilé. Comme indiqué dans le cours précédent, il vous faut cliquer avec le bouton droit sur le lien, puis sélectionner "enregistrer sous", indiquer l'extension nécéssaire (.c ou .hex), et pour le type, sélectionner "tous les fichiers".

Dans le cours suivant, application des timers et des interruptions au pilotage d'un servo-moteur !


Schéma de montage avec LED

Le schéma est exactement le même que pour le cours précédent, et vous retrouvez le schéma eagle :

Montage du PIC18F252 avec la led


Valid XHTML 1.0! Valid CSS! Copyright 2004 © M. AGOPIAN. Ce document et tous ceux de ce site sont sous licence Creative Commons License Creative Commons License.