| PICOS18 > Tutorial > Preemption |
|
L'application mono-tâche que vous venez de réaliser n'a que très peu de raison d'être... En ajoutant à votre application une seconde tâche vous découvrierez les mécanismes de préemption d'une tâche sur l'autre. |
![]() |
| > Réveil périodique d'une tâche |
Il est encore difficile de se rendre compte de l'interêt d'un noyau lorsqu'on
utilise qu'une seule tâche pour ... boucler à l'infini !
Nous allons donc commencer à rendre un peu plus intéressante notre
tâche en lui faisant clignoter une LED à une certaine fréquence.
Modifier le code de la tâche comme suit :
TASK(TASK0)
{
TRISBbits.TRISB4 = 0;
LATBbits.LATB4 = 0;
SetRelAlarm(ALARM_TSK0, 1000, 200);
while(1)
{
WaitEvent(ALARM_EVENT);
ClearEvent(ALARM_EVENT);
LATBbits.LATB4 = ~LATBbits.LATB4;
}
}
Notre tâche est ici composée de 2 séquences : une qui précède le "while(1)" qui correspond à la phase d'initialisation de la tâche, et une autre qui se trouve dans le corps du while.
Les 2 premières instructions vous permettent de manipuler les ports
d'entrées/sorties du PIC18. Le mot clef "TRISx" permet de mettre
un port en entrée ou en sortie, alors que le mot clef "TRISxbits"
qui est une structure permet de modifier l'état d'un bit de port.
Ici nous avons donc mis le port RB4 en sortie (d'où le "0",
un "1" signifiant "entrée") et sa valeur de sortie
à "0" (mot clef LATxbits).
Ensuite la fonction SetRelAlarm est utilisée pour programmer l'alarme de la tâche. Dans le châpitre précédent, nous avons vu ce qu'était une alarme : un object TIMER basé sur une horloge logicielle à 1ms. Dans le fichier "taskdesc.c" nous avons vu que l'alarme dont l'ID vaut 0 (ALARM_TSK0) a été associée à la tâche TASK0 :
AlarmObject Alarm_list[] =
{
/*******************************************************************
* -------------------------- First task ---------------------------
*******************************************************************/
{
OFF, /* State */
0, /* AlarmValue */
0, /* Cycle */
&Counter_kernel, /* ptrCounter */
TASK0_ID, /* TaskID2Activate */
ALARM_EVENT, /* EventToPost */
0 /* CallBack */
},
};
La fonction SetRelAlarm possède 3 champs :
Dans notre cas cela signifie que l'alarme 0 va attendre 1000ms avant de poster l'événement ALARM_EVENT, puis le postera périodiquement toutes les 200 ms.
Une fois l'alarme programmée, le code de la tâche peut continuer à s'exécuter. Dans le "while(1)", la première fonction appelée est la fonction WaitEvent, qui permet de mettre en sommeil la tâche jusqu'à l'arrivée de l'événement attendu, ici ALARM_EVENT.
Vous pouvez mettre votre tâche en attente sur n'importe quel événement,
l'important étant de le définir comme une puissance de 2 dans
le fichier "define.h". Les valeurs autorisées sont donc
: 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02 et 0x01.
| > Simulation de la tâche 0 |
Pour vérifier que nous avons bel et bien réalisé une tâche de clignotement à une période de 200ms, il est possible de simuler l'application en plaçant un breakpoint en face de la ligne de pilotage du port RB4 :

De plus vous pouvez espionner les périphériques du PIC18 ou bien les variables globales de PICos18, en particulier "global_counter" qui est un compteur décimal du nombre de millisecondes écoulées. Pour obtenir une affichage en décimal de la valeur clicker à l'aide du bouton droit de la souris sur la variable de votre choix et choisissez un affichage "Dec".
En faisant des RUN successif (F9), vous verrez le PORTB passer de 0x00 à 0x10, et la valeur de global_counter s'incrémenter de 200 en 200, en commençant par la valeur 1001 (le noyau met 1ms à booter, d'où la valeur de 1001 au lieu de 1000 attendue au premier abord).
Cela signifie que le port RB4 passe à 1 au bout de 1 seconde puis oscille
périodiquement toutes les 200ms.
Modifiez à présent les valeurs des paramètres de temps
de la fonction SetRelAlarm pour connaître leur effet. Mettez par exemple
0 dans le dernier champ, vous verrez alors que le port RB4 reste à 1
et qu'on ne rentre plus jamais dans la tâche.
Consultez le PDF relatif à l'API du noyau PICos18 pour tout connaître des fonctions utilisées par la tâche.
| > Création d'une seconde tâche |
Maintenant que nous avons réalisé une tâche périodique, nous allons créer une seconde tâche et les synchroniser.
1) Sous MPLAB®, enregistrez le fichier "tsk_task0.c" sous le nom "tsk_task1.c"
2) Modifiez les références à la nouvelle tâche dans le fichier taskdesc.c, comme suit :
#define DEFAULT_STACK_SIZE 128
DeclareTask(TASK0);
DeclareTask(TASK1);
volatile unsigned char stack0[DEFAULT_STACK_SIZE];
volatile unsigned char stack1[DEFAULT_STACK_SIZE];
/**********************************************************************
* ---------------------- TASK DESCRIPTOR SECTION ---------------------
**********************************************************************/
#pragma romdata DESC_ROM
const rom unsigned int descromarea;
/**********************************************************************
* ----------------------------- task 0 ------------------------------
**********************************************************************/
rom_desc_tsk rom_desc_task0 = {
TASK0_PRIO, /* prioinit from 0 to 15 */
stack0, /* stack address (16 bits) */
TASK0, /* start address (16 bits) */
READY, /* state at init phase */
TASK0_ID, /* id_tsk from 1 to 15 */
sizeof(stack0) /* stack size (16 bits) */
};
/**********************************************************************
* ----------------------------- task 1 ------------------------------
**********************************************************************/
rom_desc_tsk rom_desc_task1 = {
TASK1_PRIO, /* prioinit from 0 to 15 */
stack1, /* stack address (16 bits) */
TASK1, /* start address (16 bits) */
READY, /* state at init phase */
TASK1_ID, /* id_tsk from 1 to 15 */
sizeof(stack1) /* stack size (16 bits) */
};
3) Puis changez le corps de la fonction de la tâche 1 par le code suivant :
unsigned char hour, min, sec;
/**********************************************************************
* ------------------------------ TASK1 -------------------------------
*
* Second task of the tutorial.
*
**********************************************************************/
TASK(TASK1)
{
hour = min = sec = 0;
while(1)
{
WaitEvent(TASK1_EVENT);
ClearEvent(TASK1_EVENT);
sec++;
if (sec == 60)
{
sec = 0;
min++;
if (min == 60)
{
min = 0;
hour++;
}
}
}
}
Comme vous l'avez très certainement compris cette tâche permet
de créer un chronomètre en heure/minute/seconde : à chaque
fois que la tâche est réveillée, la variable des secondes
est incrémentée de 1, et les variables min et hour sont mises
à jour en conséquence.
4) Il paraît donc évident que la tâche doit être appellée toutes les 1000ms. Modifiez la tâche 0 pour quelle poste un événement toutes les secondes à notre nouvelle tâche :
SetRelAlarm(ALARM_TSK0, 1000, 1000);
while(1)
{
WaitEvent(ALARM_EVENT);
ClearEvent(ALARM_EVENT);
LATBbits.LATB4 = ~LATBbits.LATB4;
SetEvent(TASK1_ID, TASK1_EVENT);
}
5) Il faut renseigner le compilateur sur les nouveau symboles que nous avons ajoutés à l'aide du fichier "define.h".
/*********************************************************************** * ----------------------------- Events -------------------------------- **********************************************************************/ #define ALARM_EVENT 0x80 #define TASK1_EVENT 0x10 /*********************************************************************** * ----------------------------- Task ID ------------------------------- **********************************************************************/ #define TASK0_ID 1 #define TASK1_ID 2 #define TASK0_PRIO 7 #define TASK1_PRIO 6
6) Enfin il faut ajouter la nouvelle tâche au projet ("Project/Add files to Project...").
Vous pouvez désormais recompiler le projet (F10).
| > Simulation de la tâche 1 |
Afin de vérifier que les variables hour, min et sec sont bien mise à jour sur une base de 1 seconde, lancez la simulation avec les breakpoints suivants :

Une simulation de 1 seconde demande beaucoup de temps à MPLAB® donc
si vous souhaitez tester les variables min et hour lorsqu'elles atteignent 60,
il vous faudrait modifier l'ALARM_TASK0 pour qu'elle soit plus rapide, au moins
pour le temps de la simulation.
| > La préemption |
Votre application est désormais composée de 2 tâches, elle est donc à proprement parler multi-tâches. Pourtant cela ne veut pas dire que les 2 tâches fonctionnent en même temps, c'est parfaitement impossible pour le processeur du PIC18 qui ne peut exécuter qu'un seul code à la fois. Tout au plus les tâches fonctionnent en parallèle, c'est-à-dire l'une après l'autre en fonction d'un certain nombre de régles définis par le noyau, notamment les priorités.
Pour bien comprendre les notions de préemption, de multi-tâches et de temps-réel, placez des breakpoints comme ci-dessous et lancer la simulation :

Le pointeur parcourt les breakpoints dans l'ordre suivant :
En fait cela s'explique par le fait que la tâche 0 est plus prioritaire que la tâche 1, donc lorsqu'elle aura posté un événement à la tâche 1, elle continuera à s'exécuter jusqu'à ce qu'elle soit mise en sommeil par le WaitEvent. Du coup la tâche 1 à le champ libre pour pouvoir s'exécuter et le pointeur s'arrête sur le breakpoint du ClearEvent.
Maintenant modifiez la priorité de la tâche 1 dans le fichier "define.h"
pour qu'elle soit la plus prioritaire :
/*********************************************************************** * ----------------------------- Task ID ------------------------------- **********************************************************************/ #define TASK0_ID 1 #define TASK1_ID 2 #define TASK0_PRIO 7 #define TASK1_PRIO 10
Recompilez et relancez la simulation, désormais l'ordre de passage des breakpoints sera le suivant :
Comme la tâche 1 est désormais plus prioritaire, lorsqu'un événement lui est assigné, la tâche en cours est immédiatement suspendue pour permettre à la tâche de plus forte priorité de s'exécuter : on dit qu'il y a eu préemption (qui signifie prendre la main).
La préemption est une des caractéristiques fondamentales des systèmes
multi-tâches temps réels. En effet dans un système non préemptif,
la tâche 0 aurait continué de s'exécuter jusqu'à
se mettre en sommeil pour permettre à la tâche 1 de fonctionner.
On dit alors qu'un tel système est coopératif, c'est-à-dire
que c'est à la tâche en cours de fonctionnement de décider
quand rendre la main au reste de l'application. Le système SALVO
qui existe pour PIC18 est un bon exemple de système coopératif.
Certes dans notre cas, celà n'a pas trop de conséquence, l'application
semble fonctionner de façon identique. Toutefois lorsqu'il s'agit de
gérer des protocoles de communication complexes ou bien lorsque l'application
comporte de nombreuses tâches, un système préemptif garantit
le bon fonctionnement de l'application, quelque soit la taille du code.
Pourtant la préemption n'est un critère suffisant pour qualifier
un système temps réel. Il est nécessaire d'y ajouter le
déterminisme, c'est-à-dire
la capacité à garantir que le temps nécessaire pour passer
d'une tâche à l'autre est une constante. Dans PICos18 elle vaut
50 µs, ce qui veut dire qu'une fois que la tâche 0 poste l'événement
à la tâche 1, le passage de la tâche 0 à la tâche
1 prend 50 µs.
PICos18 est donc un système préemptif, multi-tâches et temps réel qui garantit qu'une tâche de plus forte priorité s'exécutera immédiatement si un événement lui est associé, et ceci dans un délai de 50 µs quelque soit l'application. Vous comprendrez donc qu'une boucle infinie "while(1);" dans la tâche la plus prioritaire de votre application blockera toute votre application. Essayez en simulation !