Toute la vérité sur RTOS. Article # 30. Procédures d'initialisation et de démarrage de Nucleus SE



Tout système d'exploitation possède un mécanisme de démarrage spécifique. Le principe de fonctionnement de ce mécanisme pour chaque système est différent. Habituellement, ils disent que le système démarre (Eng. Boot), il s'agit d'une abréviation de "bootstrap", qui se réfère à l'expression "tirer soi-même sur une clôture par ses bootstraps" (pour franchir la clôture, se tirer par les sangles des chaussures), qui décrit grossièrement comment le système passe indépendamment d'un état dans lequel la mémoire est pleine de vide ( note du traducteur: si absolument précis, alors ordure ) à l'exécution stable du programme. Traditionnellement, une petite partie du programme est chargée en mémoire; elle peut être stockée dans la ROM. Dans le passé, il pouvait être saisi à l'aide des commutateurs situés à l'avant de l'ordinateur. Ce chargeur de démarrage lit un programme de démarrage plus complexe qui a déjà chargé le système d'exploitation. Aujourd'hui, l'ordinateur de bureau démarre comme suit: le code BIOS recherche les périphériques (disques durs, CD-ROM, clés USB) à partir desquels vous pouvez démarrer, puis le système d'exploitation démarre.

Le système d'exploitation pour les systèmes embarqués peut également être initialisé de la même manière. Et en fait, les systèmes d'exploitation embarqués développés sur la base des systèmes d'exploitation de bureau sont chargés. Mais dans la plupart des RTOS "classiques", une méthode beaucoup plus simple (et donc plus rapide) est utilisée.

Le système d'exploitation fait partie du logiciel. Si ce logiciel est déjà en mémoire (par exemple, sous une forme ou une autre de ROM), il vous suffit de vous assurer que la séquence de commandes CPU après la réinitialisation se termine avec l'exécution du code d'initialisation du système d'exploitation. C'est ainsi que la plupart des RTOS fonctionnent, y compris Nucleus SE ( note du traducteur: cela s'applique également à notre RTOS MAX ).

La plupart des outils de développement de logiciels intégrés incluent le code de démarrage nécessaire pour gérer la réinitialisation du processeur et transférer le contrôle à la fonction Point d'entrée dans la fonction main () . Le code redistribuable Nucleus SE ne traite pas ce processus, car il doit être aussi portable que possible. Au lieu de cela, il contient la fonction main () , qui prend le contrôle du CPU et initialise et démarre le système d'exploitation. Cette fonctionnalité sera discutée en détail ci-dessous.

Articles précédents de la série:
Article # 29. Interruptions dans Nucleus SE
Article # 28. Minuteries logicielles
Article # 27. Heure système
Article # 26. Canaux: services auxiliaires et structures de données
Article # 25. Canaux de données: introduction et services de base
Article # 24. Files d'attente: services auxiliaires et structures de données
Article # 23. Files d'attente: introduction et services de base
Article # 22. Boîtes aux lettres: services auxiliaires et structures de données
Article # 21. Boîtes aux lettres: introduction et services de base
Article # 20. Sémaphores: services auxiliaires et structures de données
Article # 19. Sémaphores: introduction et services de base
Article # 18. Groupes d'indicateurs d'événements: services d'assistance et structures de données
Article # 17. Groupes de drapeaux d'événements: introduction et services de base
Article # 16. Signaux
Article # 15. Partitions de mémoire: services et structures de données
Article # 14. Sections de mémoire: introduction et services de base
Article # 13. Structures de données de tâche et appels d'API non pris en charge
Article # 12. Services pour travailler avec des tâches
Article # 11. Tâches: configuration et introduction à l'API
Article # 10. Scheduler: fonctionnalités avancées et préservation du contexte
Article # 9. Scheduler: implémentation
Article # 8. Nucleus SE: conception interne et déploiement
Article # 7. Nucleus SE: Introduction
Article # 6. Autres services RTOS
Article # 5. Interaction et synchronisation des tâches
Article # 4. Tâches, changement de contexte et interruptions
Article # 3. Tâches et planification
Article # 2. RTOS: Structure et mode temps réel
Article # 1. RTOS: introduction.

Initialisation de la mémoire


Les déclarations de toutes les variables statiques dans le code Nucleus SE commencent par le préfixe ROM ou RAM pour indiquer où elles doivent être situées. Ces deux directives #define sont définies dans le fichier nuse_types.h et doivent être configurées en tenant compte des spécificités de l'ensemble d'outils de développement utilisé (compilateur et éditeur de liens). En règle générale, la ROM doit être de type const ( note du traducteur: d'après mon expérience, const n'est pas toujours suffisant, statique est meilleur ), et RAM est une valeur vide.

Toutes les variables ROM sont initialisées statiquement, ce qui est logique. Les variables RAM ne sont pas initialisées statiquement (car cela ne fonctionne qu'avec certaines boîtes à outils configurées pour copier automatiquement de la ROM vers la RAM); un code d'initialisation explicite est inclus dans l'application et sera décrit en détail ci-dessous.

Nucleus SE ne stocke pas de données «constantes» dans la RAM, qui est généralement insuffisante dans les petits systèmes. Au lieu d'utiliser des structures de données complexes pour décrire les objets du noyau, des ensembles de tables (tableaux) sont utilisés, qui sont facilement placés dans la ROM ou la RAM, selon les besoins.

Fonction Main ()


Voici le code complet de la fonction main () de Nucleus SE:

void main(void) { NUSE_Init(); /* initialize kernel data */ /* user initialization code here */ NUSE_Scheduler(); /* start tasks */ } 

La séquence des opérations est assez simple:

  • Tout d'abord, la fonction NUSE_Init () est appelée . Il initialise toutes les structures de données Nucleus SE et sera décrit en détail ci-dessous.
  • L'utilisateur peut ensuite insérer tout code d'initialisation d'application qui sera exécuté avant le démarrage du planificateur de tâches. Pour plus d'informations sur ce qui peut être réalisé avec ce code, voir plus loin dans cet article.
  • Enfin, le planificateur Nucleus SE ( NUSE_Scheduler () ) démarre. Cela sera également discuté en détail plus loin dans cet article.

Fonction NUSE_Init ()


Cette fonction initialise toutes les variables du noyau Nucleus SE et les structures de données.

Voici le code de fonction complet:
 void NUSE_Init(void) { U8 index; /* global data */ NUSE_Task_Active = 0; NUSE_Task_State = NUSE_STARTUP_CONTEXT; #if NUSE_SYSTEM_TIME_SUPPORT NUSE_Tick_Clock = 0; #endif #if NUSE_SCHEDULER_TYPE == NUSE_TIME_SLICE_SCHEDULER NUSE_Time_Slice_Ticks = NUSE_TIME_SLICE_TICKS; #endif /* tasks */ #if ((NUSE_SCHEDULER_TYPE != NUSE_RUN_TO_COMPLETION_SCHEDULER) || NUSE_SIGNAL_SUPPORT || NUSE_TASK_SLEEP || NUSE_SUSPEND_ENABLE || NUSE_SCHEDULE_COUNT_SUPPORT) for (index=0; index<NUSE_TASK_NUMBER; index++) { NUSE_Init_Task(index); } #endif /* partition pools */ #if NUSE_PARTITION_POOL_NUMBER != 0 for (index=0; index<NUSE_PARTITION_POOL_NUMBER; index++) { NUSE_Init_Partition_Pool(index); } #endif /* mailboxes */ #if NUSE_MAILBOX_NUMBER != 0 for (index=0; index<NUSE_MAILBOX_NUMBER; index++) { NUSE_Init_Mailbox(index); } #endif /* queues */ #if NUSE_QUEUE_NUMBER != 0 for (index=0; index<NUSE_QUEUE_NUMBER; index++) { NUSE_Init_Queue(index); } #endif /* pipes */ #if NUSE_PIPE_NUMBER != 0 for (index=0; index<NUSE_PIPE_NUMBER; index++) { NUSE_Init_Pipe(index); } #endif /* semaphores */ #if NUSE_SEMAPHORE_NUMBER != 0 for (index=0; index<NUSE_SEMAPHORE_NUMBER; index++) { NUSE_Init_Semaphore(index); } #endif /* event groups */ #if NUSE_EVENT_GROUP_NUMBER != 0 for (index=0; index<NUSE_EVENT_GROUP_NUMBER; index++) { NUSE_Init_Event_Group(index); } #endif /* timers */ #if NUSE_TIMER_NUMBER != 0 for (index=0; index<NUSE_TIMER_NUMBER; index++) { NUSE_Init_Timer(index); } #endif } 


Tout d'abord, les variables globales sont initialisées:
  • NUSE_Task_Active - index de la tâche active, initialisé à zéro; plus tard, cela peut changer le planificateur.
  • NUSE_Task_State - initialisé avec la valeur NUSE_STARTUP_CONTEXT , ce qui limite la fonctionnalité de l'API pour tout code d'initialisation d'application ultérieur.
  • Si la prise en charge de l'heure système est activée, NUSE_Tick_Clock est défini sur zéro.
  • Si le planificateur de tranche horaire est activé, NUSE_Time_Slice_Ticks reçoit la valeur configurée NUSE_TIME_SLICE_TICKS .

Ensuite, les fonctions sont appelées pour initialiser les objets du noyau:

  • NUSE_Init_Task () est appelée pour initialiser les structures de données de chaque tâche. Cet appel n'est ignoré que si le planificateur Run to Completion est utilisé et que les signaux, la pause de tâche et le compteur de planification ne sont pas configurés (car cette combinaison de fonctions entraînera l'absence de ces structures de tâches dans la RAM, par conséquent, l'initialisation ne sera pas effectuée).
  • NUSE_Init_Partition_Pool () est appelée pour initialiser chaque objet de pool de partitions. Ces appels sont ignorés si aucun pool de partitions n'est configuré.
  • NUSE_Init_Mailbox () est appelée pour initialiser chaque objet de boîte aux lettres. Ces appels sont ignorés s'il n'y a pas de boîtes aux lettres configurées.
  • NUSE_Init_Queue () est appelée pour initialiser chaque objet de file d'attente. Ces appels sont ignorés si aucune file d'attente n'est configurée.
  • NUSE_Init_Pipe () est appelé pour initialiser chaque objet de canal. Ces appels sont ignorés si aucun canal n'est configuré.
  • NUSE_Init_Semaphore () est appelé pour initialiser chaque objet sémaphore. Ces appels sont ignorés si aucun sémaphores n'est configuré.
  • NUSE_Init_Event_Group () est appelé pour initialiser chaque objet de groupe d'événements. Ces appels sont ignorés s'il n'y a aucun groupe d'événements configuré.
  • NUSE_Init_Timer () est appelé pour initialiser chaque objet timer. Ces appels sont ignorés si aucun temporisateur n'est configuré.

Initialisation de la tâche


Voici le code complet de la fonction NUSE_Init_Task ():
 void NUSE_Init_Task(NUSE_TASK task) { #if NUSE_SCHEDULER_TYPE != NUSE_RUN_TO_COMPLETION_SCHEDULER NUSE_Task_Context[task][15] = /* SR */ NUSE_STATUS_REGISTER; NUSE_Task_Context[task][16] = /* PC */ NUSE_Task_Start_Address[task]; NUSE_Task_Context[task][17] = /* SP */ (U32 *)NUSE_Task_Stack_Base[task] + NUSE_Task_Stack_Size[task]; #endif #if NUSE_SIGNAL_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Signal_Flags[task] = 0; #endif #if NUSE_TASK_SLEEP || NUSE_INCLUDE_EVERYTHING NUSE_Task_Timeout_Counter[task] = 0; #endif #if NUSE_SUSPEND_ENABLE || NUSE_INCLUDE_EVERYTHING #if NUSE_INITIAL_TASK_STATE_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Status[task] = NUSE_Task_Initial_State[task]; #else NUSE_Task_Status[task] = NUSE_READY; #endif #endif #if NUSE_SCHEDULE_COUNT_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Schedule_Count[task] = 0; #endif } 


Si le planificateur Run to Completion n'a pas été configuré, le bloc de contexte pour la tâche NUSE_Task_Context [task] [] est initialisé. La plupart des éléments ne sont pas affectés de valeurs, car ils représentent des registres de machine communs, qui devraient avoir une valeur intermédiaire lors du démarrage d'une tâche. Dans l'exemple (Freescale ColdFire) de l'implémentation de Nucleus SE (mais pour les autres processeurs, le mécanisme sera similaire), les trois dernières entrées sont définies explicitement:

  • NUSE_Task_Context [tâche] [15] contient le registre d'état ( SR , registre d'état) et a la valeur de la directive #define NUSE_STATUS_REGISTER .
  • NUSE_Task_Context [tâche] [16] contient le compteur de programme ( PC , compteur de programme) et a la valeur d'adresse du point d'entrée du code de tâche: NUSE_Task_Start_Address [tâche] .
  • NUSE_Task_Context [tâche] [17] contient le pointeur de pile ( SP , pointeur de pile) et est initialisé avec la valeur calculée comme la somme de l'adresse de la pile de tâches ( NUSE_Task_Stack_Base [tâche] ) et la taille de la pile de tâches ( NUSE_Task_Stack_Size [tâche] ).

Si la prise en charge du signal est activée, les indicateurs de signal de tâche ( NUSE_Task_Signal_Flags [tâche] ) sont mis à zéro.

Si la suspension de tâche est activée (c'est-à-dire l'appel de service API NUSE_Task_Sleep () ), le compteur de délai d'expiration de tâche ( NUSE_Task_Timeout_Counter [tâche] ) est défini sur zéro.

Si l'état de suspension de tâche est activé, l'état de la tâche ( NUSE_Task_Status [tâche] ) est initialisé. Cette valeur initiale est définie par l'utilisateur (dans NUSE_Task_Initial_State [tâche] ) si la prise en charge de l'état initial de la tâche est activée. Sinon, l'état est affecté à NUSE_READY .

Si le compteur de planification est activé, le compteur de tâches ( NUSE_Task_Schedule_Count [tâche] ) est mis à zéro.

Initialisation des pools de partitions


Voici le code complet de la fonction NUSE_Init_Partition_Pool () :

 void NUSE_Init_Partition_Pool(NUSE_PARTITION_POOL pool) { NUSE_Partition_Pool_Partition_Used[pool] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Partition_Pool_Blocking_Count[pool] = 0; #endif } 

Le compteur du pool de partitions "utilisé" ( NUSE_Partition_Pool__Partition_Used [pool] ) est défini sur zéro.

Si le verrouillage des tâches est activé, le compteur de tâches bloquées des pools de partitions ( NUSE_Partition_Pool_Blocking_Count [pool]) est défini sur zéro.

Initialisation des boîtes aux lettres


Ci-dessous, le code complet NUSE_Init_Mailbox () :

 void NUSE_Init_Mailbox(NUSE_MAILBOX mailbox) { NUSE_Mailbox_Data[mailbox] = 0; NUSE_Mailbox_Status[mailbox] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Mailbox_Blocking_Count[mailbox] = 0; #endif } 

La banque de données de boîte aux lettres ( NUSE_Mailbox_Data [boîte aux lettres] ) est définie sur zéro et l'état ( NUSE_Mailbox_Status [boîte aux lettres] ) devient "inutilisé" (c'est-à-dire zéro).

Si le verrouillage des tâches est activé, le compteur des tâches de boîte aux lettres bloquées ( NUSE_Mailbox_Blocking_Count [boîte aux lettres] ) est défini sur zéro.

Initialisation de la file d'attente


Voici le code complet de la fonction NUSE_Init_Queue () :

 void NUSE_Init_Queue(NUSE_QUEUE queue) { NUSE_Queue_Head[queue] = 0; NUSE_Queue_Tail[queue] = 0; NUSE_Queue_Items[queue] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Queue_Blocking_Count[queue] = 0; #endif } 

Les pointeurs vers le début et la fin de la file d'attente (en fait, ce sont les index NUSE_Queue_Head [queue ] et NUSE_Queue_Tail [file d'attente) ) reçoivent des valeurs qui indiquent le début de la zone de données des files d'attente (c'est-à-dire qu'ils prennent une valeur nulle). Le compteur dans la file d'attente ( NUSE_Queue_Items [file d'attente] ) est également mis à zéro.

Si le verrouillage des tâches est activé, le compteur des tâches de file d'attente bloquées ( NUSE_Queue_Blocking_Count [file d'attente) ) est mis à zéro.

Initialisation de canal


Voici le code complet de la fonction NUSE_Init_Pipe () :

 void NUSE_Init_Pipe(NUSE_PIPE pipe) { NUSE_Pipe_Head[pipe] = 0; NUSE_Pipe_Tail[pipe] = 0; NUSE_Pipe_Items[pipe] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Pipe_Blocking_Count[pipe] = 0; #endif } 

Les pointeurs vers le début et la fin du canal (en fait, ce sont les index - NUSE_Pipe_Head [pipe] et NUSE_Pipe_Tail [pipe] ) reçoivent une valeur qui indique le début de la zone de données du canal (c'est-à-dire qu'ils prennent une valeur nulle). Le compteur de canaux ( NUSE_Pipe_Items [pipe] ) est également défini sur zéro.

Si le verrouillage des tâches est activé, le compteur de tâches bloquées du canal ( NUSE_Pipe_Blocking_Count [pipe] ) est défini sur zéro.

Initialisation du sémaphore


Voici le code complet de la fonction NUSE_Init_Semaphore () :

 void NUSE_Init_Semaphore(NUSE_SEMAPHORE semaphore) { NUSE_Semaphore_Counter[semaphore] = NUSE_Semaphore_Initial_Value[semaphore]; #if NUSE_BLOCKING_ENABLE NUSE_Semaphore_Blocking_Count[semaphore] = 0; #endif } 

Le compteur de sémaphore ( NUSE_Semaphore_Counter [sémaphore] ) est initialisé avec la valeur définie par l'utilisateur ( NUSE_Semaphore_Initial_Value [sémaphore] ).

Si le verrouillage de tâche est activé, le compteur de tâches de sémaphore verrouillé ( NUSE_Semaphore_Blocking_Count [sémaphore] ) est défini sur zéro.

Initialisation des groupes d'événements


Voici le code complet de la fonction NUSE_Init_Event_Group () :

 void NUSE_Init_Event_Group(NUSE_EVENT_GROUP group) { NUSE_Event_Group_Data[group] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Event_Group_Blocking_Count[group] = 0; #endif } 

Les drapeaux du groupe d'événements sont réinitialisés, c'est-à-dire NUSE_Event_Group_Data [group] reçoit une valeur nulle.

Si le verrouillage des tâches est activé, le compteur de tâches bloquées du groupe d'indicateurs d'événements ( NUSE_Event_Group_Blocking_Count [groupe] ) est défini sur zéro.

Initialisation de la minuterie


Ci-dessous, le code complet de NUSE_Init_Timer () ;

 void NUSE_Init_Timer(NUSE_TIMER timer) { NUSE_Timer_Status[timer] = FALSE; NUSE_Timer_Value[timer] = NUSE_Timer_Initial_Time[timer]; NUSE_Timer_Expirations_Counter[timer] = 0; } 

L'état du temporisateur ( NUSE_Timer_Status [timer] ) est réglé sur "inutilisé", c'est-à-dire FAUX

La valeur du compte à rebours ( NUSE_Timer_Value [timer ]) est initialisée par la valeur définie par l'utilisateur ( NUSE_Timer_Initial_Time [timer] ).

Le compteur d'achèvement ( NUSE_Timer_Expirations_Counter [timer] ) est mis à zéro.

Initialisation du code d'application


Une fois les structures de données Nucleus SE initialisées, il devient possible d'exécuter le code responsable de l'initialisation de l'application avant de démarrer la tâche. Cette fonctionnalité peut être utile pour les tâches suivantes:

  • Initialisation des structures de données d'application. Le remplissage explicite des structures de données est plus facile à comprendre et à déboguer que l'initialisation automatique des variables statiques.
  • Affectation des objets du noyau. Étant donné que tous les objets du noyau sont créés statiquement au stade de la construction et sont identifiés à l'aide de valeurs d'index, il peut être utile d'attribuer un «propriétaire» ou de déterminer l'utilisation de ces objets. Cela peut être fait à l'aide de la directive #define, cependant, s'il existe plusieurs instances de tâches, il est préférable d'affecter des index d'objets via des tableaux globaux (indexés par ID de tâche).
  • Initialisation de l'appareil. Cela peut être utile pour l'installation initiale des périphériques.

Évidemment, bon nombre de ces objectifs peuvent être atteints avant l'initialisation de Nucleus SE, mais l'avantage de l'emplacement du code d'application ici est que vous pouvez désormais utiliser les services du noyau (appels d'API). Par exemple, une file d'attente ou une boîte aux lettres peut être préremplie avec des données qui devront être traitées au démarrage de la tâche.

Les appels d'API ont une restriction: toutes les actions qui conduisent généralement à l'activation du planificateur sont interdites (par exemple, suspendre / bloquer des tâches). La variable globale NUSE_Task_State a été définie sur NUSE_STARTUP_CONTEXT pour indiquer cette limitation.

Lancer le planificateur


Une fois l'initialisation terminée, il ne reste plus qu'à exécuter le planificateur pour commencer à exécuter le code d'application - tâches. La configuration de l'ordonnanceur et le travail de divers types d'ordonnanceurs ont été décrits en détail dans l'un des articles précédents ( # 9 ), donc seul un bref résumé est requis ici.
La séquence des étapes clés est la suivante:

  • Définition de la variable globale NUSE_Task_State sur NUSE_TASK_CONTEXT .
  • Sélectionnez l'index de la première tâche à exécuter. Si la prise en charge de la tâche initiale est activée, la recherche de la première tâche terminée est effectuée, sinon, une valeur nulle est utilisée.
  • L'ordonnanceur est appelé - NUSE_Scheduler () .

Ce qui se passe exactement à la dernière étape dépend du planificateur sélectionné. Lorsque vous utilisez le planificateur Run to Completion, un cycle de planification démarre et les tâches sont appelées séquentiellement. Lorsque vous utilisez d'autres planificateurs, le contexte de la première tâche est chargé et le contrôle est transféré à la tâche.

L'article suivant traite des diagnostics et de la vérification des erreurs.

À propos de l'auteur: Colin Walls travaille dans l'industrie électronique depuis plus de trente ans, consacrant la majeure partie de son temps au micrologiciel. Il est maintenant ingénieur firmware chez Mentor Embedded (une division de Mentor Graphics). Colin Walls intervient souvent lors de conférences et séminaires, auteur de nombreux articles techniques et de deux livres sur le firmware. Vit au Royaume-Uni. Blog professionnel de Colin , e-mail: colin_walls@mentor.com.

Source: https://habr.com/ru/post/fr460617/


All Articles