Un peu sur le multitâche dans les microcontrôleurs

Un peu sur le multitâche


Quiconque, jour après jour, ou au cas par cas, est engagé dans la programmation de microcontrôleurs, sera tôt ou tard confronté à la question: dois-je utiliser un système d'exploitation multitâche? Il y en a beaucoup sur le réseau, et beaucoup d'entre eux sont gratuits (ou presque gratuits). Choisissez simplement.


Des doutes similaires surgissent lorsque vous rencontrez un projet dans lequel le microcontrôleur doit effectuer simultanément plusieurs actions différentes. Certains d'entre eux ne sont pas liés aux autres, tandis que les autres, au contraire, ne peuvent pas se passer les uns des autres. De plus, il peut y avoir trop des deux. Ce qui est «trop» dépend de qui évaluera ou qui réalisera le développement. Eh bien, si c'est la même personne.


Il ne s'agit pas plutôt d'une question de quantité, mais d'une différence qualitative des tâches par rapport à la vitesse d'exécution ou à d'autres exigences. De telles pensées peuvent survenir, par exemple, lorsque le projet doit surveiller régulièrement la tension d'alimentation (est-elle manquante?), Assez souvent lire et enregistrer les valeurs des quantités d'entrée (elles ne donnent pas de repos), surveiller occasionnellement la température et contrôler le ventilateur (il n'y a rien à respirer), vérifiez votre regarde avec quelqu'un en qui vous avez confiance (c'est bon pour vous de le commander), restez en contact avec l'opérateur (essayez de ne pas l'irriter), vérifiez la somme de contrôle de la mémoire permanente du programme pour la démence (lorsqu'il est activé, ou une fois par semaine, ou le matin).


De telles tâches hétérogènes peuvent être programmées avec beaucoup de sens et avec succès, en s'appuyant sur une seule tâche d'arrière-plan et des interruptions de temporisation. Dans le gestionnaire de ces interruptions, à chaque fois une des «pièces» de la tâche suivante est exécutée. Selon l'importance, l'urgence ou des considérations similaires, ces défis sont souvent répétés pour certaines tâches, mais rarement pour d'autres. Et pourtant, nous devons nous assurer que chaque tâche fait une partie de courte durée du travail, puis se prépare pour la prochaine petite partie du travail, et ainsi de suite. Cette approche, si vous vous y habituez, ne semble pas trop compliquée. Un inconvénient survient lorsque vous souhaitez créer un projet. Ou, par exemple, transférez soudainement à un autre. Il convient de noter que la seconde est souvent plus difficile et sans aucune pseudo-tâche multiple.


Mais que faire si vous utilisez un système d'exploitation prêt à l'emploi pour les microcontrôleurs? Bien sûr, beaucoup le font. C'est une bonne option. Mais l'auteur de ces lignes, jusqu'à présent, a été et continue d'être arrêté par l'idée qu'il sera nécessaire de comprendre cela, après avoir passé beaucoup de temps, à choisir parmi ce que nous avons réussi à obtenir et à utiliser uniquement ce qui est vraiment nécessaire. Et faites tout cela, faites attention à vous plonger dans le code de quelqu'un d'autre! Et il n'y a aucune certitude que dans six mois cela ne devra pas être répété, car il sera oublié.


En d'autres termes, pourquoi avez-vous besoin d'un garage complet d'outils et d'accessoires si un vélo y est entreposé et utilisé?


Par conséquent, il y avait un désir de faire un simple «changement» de tâches uniquement pour Cortex-M4 (enfin, peut-être même pour M3 et M7). Mais l'ancien, bon désir de ne pas trop tendre n'a pas disparu.


Donc, nous faisons le plus simple. Un petit nombre de tâches partagent également le temps d'exécution. Comme dans la figure 1 ci-dessous, quatre tâches le font. Soit le principal zéro, car il est difficile d'en imaginer un autre.



En travaillant de cette façon, ils sont assurés d'obtenir leur créneau horaire ou durée (tick) et ne sont pas tenus de connaître l'existence d'autres tâches. Chaque tâche exactement après 3 ticks aura à nouveau l'occasion de faire quelque chose.


Mais, d'autre part, si l'une des tâches est nécessaire pour attendre un événement externe, par exemple, en appuyant sur un bouton, cela passera stupidement le temps précieux de notre microcontrôleur. Nous ne pouvons pas être d'accord avec cela. Et notre crapaud (conscience) - aussi. Il faut faire quelque chose.


Et laissez la tâche, si elle n'a rien à faire jusqu'à présent, donner le temps qui reste de la tique à ses camarades, qui, très probablement, labourent de toutes leurs forces.


En d'autres termes, le partage est nécessaire. Laissez la tâche 2 faire exactement cela, comme dans la figure 2.



Et pourquoi notre tâche principale ne devrait-elle pas abandonner le reste du temps, si vous devez encore attendre? Permettons-le. Comme le montre la figure 3.



Et si vous savez que certaines tâches ne vous obligeront pas bientôt à vérifier à nouveau quelque chose ou simplement à travailler? Et elle pouvait se permettre un peu de sommeil, et au lieu de cela, elle perdrait du temps et se mettrait sous ses pieds. Pas une commande, elle doit être corrigée. Laissez la tâche 3 manquer un morceau de son temps (ou mille). Comme le montre la figure 4.



Eh bien, comme nous le voyons, nous avons décrit une coexistence équitable des tâches ou quelque chose comme ça. Nous devons maintenant faire en sorte que nos tâches individuelles se comportent comme prévu. Et si nous essayons de valoriser le temps, alors il vaut la peine de se souvenir d'un langage de bas niveau (je n'ai pas peur du mot assembleur) et de ne pas faire entièrement confiance au compilateur d'aucune langue, de haut niveau ou très haut. En effet, au fond de nos cœurs, nous sommes résolument contre toute dépendance. De plus, le fait que nous n'ayons besoin d'aucun assembleur, mais uniquement de Cortex-M4, nous simplifie la vie.


Pour la pile, nous sélectionnons une zone commune de RAM qui se remplira, c'est-à-dire dans le sens de la diminution des adresses mémoire. Pourquoi? Tout simplement parce que cela ne fonctionne pas différemment. Nous diviserons mentalement cet important domaine en sections égales en fonction du nombre du nombre maximum déclaré de nos tâches. La figure 5 montre cela pour quatre tâches.



Ensuite, nous sélectionnons l'endroit où nous allons stocker des copies des pointeurs de pile pour chaque tâche. Maintenant, en interrompant la minuterie, que nous prenons comme minuterie système, nous enregistrons tous les registres de la tâche en cours dans sa zone de pile (le registre SP pointe maintenant là), puis nous enregistrons son pointeur de pile dans un endroit spécial (nous enregistrons sa valeur), nous obtenons le pointeur de pile de la tâche suivante ( écrire une nouvelle valeur dans le registre SP) à partir de notre emplacement spécial et restaurer tous ses registres. Leurs copies sont désormais indiquées par le registre SP de notre prochaine tâche. Eh bien, nous quittons l'interruption, bien sûr. De plus, tout le contexte de la tâche suivante dans la liste apparaît dans les registres.


Il sera probablement superflu de dire que la prochaine tâche suivante3 dans la file d'attente sera principale. Et il n'est pas superflu, bien sûr, de rappeler que le Cortex-M4 possède déjà une minuterie SysTick et une interruption spéciale, et de nombreux fabricants de microcontrôleurs le savent. Nous allons l'utiliser et cette interruption comme prévu.


Pour démarrer cette minuterie système et effectuer toutes les préparations et vérifications nécessaires, vous devez utiliser la procédure prévue à cet effet.


U8 main_start_task_switcher(void); 

Cette routine renvoie 0 si toutes les vérifications ont réussi, ou un code d'erreur si quelque chose s'est mal passé. Il est vérifié, fondamentalement, si la pile est correctement alignée et s'il y a suffisamment d'espace pour elle, et tous nos emplacements spéciaux sont également remplis de valeurs initiales. Bref, l'ennui.


Si quelqu'un veut regarder le texte du programme, à la fin de la narration, il pourra le faire facilement, par exemple, par courrier personnel.


Oui, j'ai complètement oublié quand nous sortons les registres de la prochaine tâche du stockage pour la première fois de sa vie, il est nécessaire qu'ils obtiennent des valeurs originales significatives. Et depuis, elle les ramassera dans sa section de la pile, vous devez les y placer à l'avance et déplacer son pointeur de pile afin qu'il soit pratique à prendre. Pour cela, nous avons besoin d'une procédure


  U8 task_run_and_return_task_number(U32 taskAddress); 

À ce sous-programme, nous rapportons l'adresse 32 bits du début de notre tâche que nous voulons exécuter. Et elle (le sous-programme) nous indique le numéro de la tâche, qui s'est avéré dans une table générale spéciale, ou 0 s'il n'y avait pas d'espace dans la table. Ensuite, nous pouvons exécuter une autre tâche, puis une autre et ainsi de suite, même si les trois s'ajoutent à notre tâche principale sans fin. Elle ne donnera jamais son numéro zéro à personne.


Quelques mots sur les priorités. La principale priorité était et reste de ne pas surcharger le lecteur de détails inutiles.


Mais sérieusement, nous devons nous rappeler qu'il y a des interruptions des ports série, de plusieurs connexions SPI, d'un convertisseur analogique-numérique, d'un autre temporisateur, après tout. Et que se passera-t-il si nous allons passer à une autre tâche (changer de contexte) lorsque nous sommes dans le gestionnaire d'une sorte d'interruption. Après tout, ce ne sera pas une tâche légitime, mais une opacification temporaire du programme. Et nous garderons cet étrange contexte comme une sorte de tâche. Il y aura une confusion: le col ne se ferme pas, la casquette ne rentre pas. Arrêtez, non, cela vient d'une histoire différente.


Dans notre cas, cela ne peut tout simplement pas être autorisé. Nous ne devons pas nous autoriser à changer de contexte lors du traitement d'une interruption non planifiée. Voici les priorités pour cela. Il suffit d'attendre un peu, et c'est seulement à ce moment-là, lorsque cette audace sans précédent se termine, de passer calmement à une autre tâche. En bref, la priorité de l'interruption de notre commutateur de tâches doit être plus faible que la priorité de l'une des autres interruptions utilisées. Soit dit en passant, cela se fait également dans notre procédure de démarrage, et c'est là qu'il est installé, le plus non prioritaire possible.


Je ne voulais pas parler, mais je devais le faire. Notre processeur a deux modes de fonctionnement: privilégié et non privilégié. Et aussi deux registres pour le pointeur de pile:
SP principal et processus SP. Donc, nous n'échangerons pas pour des bagatelles, nous n'utiliserons que le mode privilégié et uniquement le pointeur de pile principal. De plus, tout cela a déjà été donné au début du contrôleur. Donc, nous ne compliquerons tout simplement pas nos vies.


Reste à rappeler que chaque tâche, à coup sûr, voudrait pouvoir tout jeter en enfer et comment se détendre. Et cela peut arriver à tout moment pendant la journée de travail, c'est-à-dire pendant notre tique. Cortex-M4 prévoit pour de tels cas une commande d'assembleur spéciale SVC, que nous adapterons à notre situation. Cela conduit à une interruption qui nous mènera au but. Et nous autoriserons la tâche non seulement à quitter le lieu de travail après le déjeuner, mais pas à venir demain. Laissez-le venir après les vacances. Et si nécessaire, laissez-le venir une fois la réparation terminée ou ne vient pas du tout. Pour ce faire, il existe une procédure que la tâche elle-même peut provoquer.


  void release_me_and_set_sleep_period(U32 ticks); 

Cette routine n'a qu'à indiquer combien de tiques sont prévues pour se reposer. Si 0, alors vous ne pouvez reposer que le reste de la coche actuelle. Si 0xFFFFFFFF, la tâche se mettra en veille jusqu'à ce que quelqu'un se réveille. Tous les autres chiffres indiquent le nombre de tiques pendant lesquelles la tâche sera en état de sommeil.


Pour que quelqu'un d'autre puisse se réveiller de côté ou le faire dormir, j'ai dû ajouter de telles procédures.


  void task_wake_up_action(U8 taskNumber); void set_task_sleep_period(U8 taskNumber, U32 ticks); 

Et, juste au cas où, même un tel sous-programme.


  void task_remove_action(U8 taskNumber); 

En gros, elle raye une tâche de la liste des employés. Honnêtement, je ne sais pas encore pourquoi je l'ai écrit. Vous êtes soudainement utile?


Il est temps de montrer à quoi ressemble l'endroit où une tâche est remplacée par une autre, c'est-à-dire le commutateur lui-même.


Juste au cas où, rappelons que certains des registres, lors de la saisie de l'interruption, sont enregistrés sur la pile sans notre participation, automatiquement (comme c'est la coutume dans Cortex-M4). Par conséquent, nous avons seulement besoin de sauver le reste. Cela peut être vu ci-dessous. Ne vous inquiétez pas de ce que vous voyez, ce sont les instructions de l'assembleur Cortex-M4 (M3, M7), comme indiqué par l'IAR Embedded Workbench.


Ceux qui n'ont pas encore rencontré les instructions de montage, croyez-moi, ils ressemblent vraiment à ça. Ce sont les molécules qui composent tout programme sous l'ARM Cortex-M4.


 SysTick_Handler STMDB SP!,{R4-R11} ;   LDR R0,=timersTable ;    LDR R1,=stacksTable ;    LDR R2,[R0] ;R2   ()  STR SP,[R1,R2,LSL #2] ;   SP (R2 * 4) __st_next_check ADD R2,R2,#1 ;   CMP R2,#TASKS_LIMIT ;R2-TASKS_LIMIT  BLO __st_no_border_yet ;   MOV R2,#0 ;    (main) LDR R3,[R1] ; main SP MOV SP,R3 B __st_timer_ok __st_no_border_yet ;; LDR SP,[R1,R2,LSL #2] ;    (errata Cortex M4) ;; CMP SP,#0 ; LDR R3,[R1,R2,LSL #2] ;  SP      CMP R3,#0 ; =0     BEQ __st_next_check MOV SP,R3 LDR R3,[R0,R2,LSL #2] ;  suspend timer CBZ R3,__st_timer_ok ; 0    ,   ; CMP R3,#0xFFFFFFFF ; ,   BEQ __st_next_check SUB R3,R3,#1 ;  1 STR R3,[R0,R2,LSL #2] ;  suspend timer B __st_next_check __st_timer_ok STR R2,[R0] ;     LDMIA SP!,{R4-R11} ;  R4-R11 BX LR 

La gestion de l'interruption ordonnée par la tâche elle-même lorsqu'elle renvoie le reste de la tique semble similaire. La seule différence est que vous devez toujours vous soucier de dormir un peu plus tard (ou de vous endormir complètement). Il y a une subtilité. Deux actions doivent être effectuées, écrire le nombre souhaité dans la minuterie de mise en veille et provoquer l'interruption du SVC. Le fait que ces deux actions ne se produisent pas atomiquement (c'est-à-dire pas les deux en même temps) m'inquiète un peu. Imaginez pendant une milliseconde que nous venions d'armer la minuterie et à ce moment-là, il était temps de travailler sur une autre tâche. L'autre a commencé à dépenser sa tique, alors que notre tâche sera de dormir les tiques suivantes, comme prévu (car sa minuterie n'est pas nulle). Puis, le moment venu, notre tâche recevra sa coche et la donnera immédiatement pour interrompre SVC, en raison des deux actions que celle-ci reste à faire. À mon avis, rien de terrible ne se produira, mais les sédiments resteront. Nous allons donc le faire. La future minuterie de sommeil est placée à un endroit préliminaire. Elle est prise à partir de là par la routine d'interruption elle-même de SVC. L'atomicité, pour ainsi dire, est atteinte. Ceci est illustré ci-dessous.


 SVC_Handler LDR R0,__sysTickAddr ; SysTick  MOV R1,#6 ;   CSR ,   STR R1,[R0] ;Stop SysTimer MOV R1,#7 ; ,   STR R1,[R0] ;Start SysTimer ; STMDB SP!,{R4-R11} ;   LDR R0,=timersTable ;    LDR R1,=stacksTable ;    LDR R2,[R0] ;R2   ()  STR SP,[R1,R2,LSL #2] ;   SP (R2 * 4) LDR R3,=tmpTimersTable ;   tmpTimers LDR R3,[R3,R2,LSL #2] ;tmpTimer    STR R3,[R0,R2,LSL #2] ; timer  __svc_next_check ADD R2,R2,#1 ;   CMP R2,#TASKS_LIMIT ;R2-TASKS_LIMIT  BLO __svc_no_border_yet ;   MOV R2,#0 ;    (main) LDR R3,[R1] ; main SP MOV SP,R3 B __svc_timer_ok __svc_no_border_yet ;; LDR SP,[R1,R2,LSL #2] ;Restore SP does not work (errata Cortex M4) ;; CMP SP,#0 ; LDR R3,[R1,R2,LSL #2] ;  SP      CMP R3,#0 ; =0     BEQ __svc_next_check MOV SP,R3 LDR R3,[R0,R2,LSL #2] ;  suspend timer CBZ R3,__svc_timer_ok ; 0    ,   B __svc_next_check __svc_timer_ok STR R2,[R0] ;     LDMIA SP!,{R4-R11} ; R4-R11 BX LR 

Il convient de rappeler que tous ces sous-programmes et gestionnaires d'interruptions se réfèrent à une certaine zone de données, qui semble effectuée par l'auteur comme le montre la figure 7.


  DATA SECTION .taskSwitcher:CODE:ROOT(2) __topStack DCD sfe(CSTACK) __botStack DCD sfb(CSTACK) __dimStack DCD sizeof(CSTACK) __sysAIRCRaddr DCD 0xE000ED0C __sysTickAddr DCD 0xE000E010 __sysSHPRaddr DCD 0xE000ED18 __sysTickReload DCD RELOAD ;******************************************************************************* ; Task table for concurrent tasks (main is number 0). ;******************************************************************************* SECTION TABLE:DATA:ROOT(2) DS32 1 ;stack shift due to FPU mainCopyCONTROL DS32 1 ;Needed to determine if FPU is used mainPSRvalue DS32 1 ;Copy from main ;******************************************************************************* 

Pour s'assurer que tout ce qui précède est du bon sens, l'auteur a dû écrire un petit projet dans le cadre de l'IAR Embedded Workbench, où il a réussi à examiner et à tout toucher en détail. Tout a été testé sur le contrôleur STM32F303VCT6 (ARM Cortex-M4). Ou plutôt, en utilisant la carte STM32F3DISCOVERY. Il y a suffisamment de LED pour donner à chaque tâche beaucoup de clignotements avec sa propre LED séparément.


Il y a quelques autres fonctionnalités que j'ai trouvées utiles. Par exemple, un sous-programme qui compte dans chaque zone de pile le nombre de mots non affectés, c'est-à-dire restant égal à zéro. Cela peut être utile lors du débogage, lorsque vous devez vérifier si le remplissage de la pile avec une tâche ou une autre est trop proche du niveau limite.


  U32 get_task_stack_empty_space(U8 taskNum); 

Je voudrais mentionner une autre fonction. C'est l'occasion pour la tâche elle-même de découvrir votre numéro dans la liste. Vous pourrez en parler à quelqu'un plus tard.


 ;******************************************************************************* ; Example: U8 get_my_number(void); ;     (). ..    . ;******************************************************************************* get_my_number LDR R0,=timersTable ;    (currentTaskNumber) LDR R0,[R0] ;  BX LR ;============================================================== 

C'est probablement tout pour le moment.

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


All Articles