Petites expériences multitâches dans un microcontrôleur

Dans l'une des notes précédentes, l'auteur a tenté de faire valoir que lors de la programmation du microcontrôleur, un simple changement de tâche sera utile dans les situations où l'utilisation du système d'exploitation en temps réel est trop importante et la boucle complète pour toutes les actions requises est trop petite ( Il a dit, tout comme le comte de La Fer). Plus précisément, pas trop peu, mais trop confus.


Dans une note ultérieure, il était prévu de rationaliser l'accès aux ressources partagées par plusieurs tâches en utilisant des files d'attente basées sur des tampons en anneau (FIFO) et une tâche distincte spécialement affectée à cela. Ayant dispersé pour différentes tâches ces actions qui ne sont pas liées les unes aux autres, nous sommes en droit d'attendre un code plus visible. Et si en même temps nous obtenons une certaine commodité et simplicité, alors pourquoi ne pas l'essayer?


De toute évidence, le microcontrôleur n'est pas conçu pour résoudre une tâche imaginable de l'utilisateur. Alors, peut-être, un tel sélecteur de tâches s'avérera tout à fait suffisant dans de nombreuses situations. En bref, une petite expérience ne risque pas de faire mal. Par conséquent, afin de ne pas être infondé, votre humble serviteur a décidé d'écrire quelque chose et de tester ses gribouillis.


Dans les microcontrôleurs, je dois dire que l'exigence de considérer le temps comme quelque chose d'important et de dur est plus courante que dans les ordinateurs à usage général. Aller au-delà du cadre dans le premier cas équivaut à une inopérabilité, et dans le second cas, cela ne conduit qu'à une augmentation du temps d'attente, ce qui est tout à fait acceptable si les nerfs sont en ordre. Il existe même deux termes «temps réel doux» et «temps réel dur».


Permettez-moi de vous rappeler que nous parlions de contrôleurs avec le noyau Cortex-M3,4,7. Aujourd'hui, c'est une famille très commune. Dans les exemples ci-dessous, nous avons utilisé le microcontrôleur STM32F303, qui fait partie de la carte STM32F3DISCOVERY.


Le commutateur est un fichier assembleur unique.
L'assembleur n'a pas peur de l'auteur, mais au contraire inspire l'espoir que la vitesse maximale sera atteinte.


Initialement, la logique la plus simple de l'opération de commutation était prévue, qui est présentée dans la figure 1 pour huit tâches.



Dans ce schéma, les tâches prennent leur partie du temps une par une et ne peuvent donner que le reste de leur tick et, si nécessaire, sauter quelques-unes de leurs ticks. Cette logique s'est avérée bonne, car la taille quantique peut être réduite. Et c'est précisément ce qui est nécessaire pour ne pas soulever d'urgence une tâche pour laquelle une interruption vient de se produire, mais aussi pour augmenter puis diminuer sa priorité. Le paquet qui vient d'être reçu attendra tranquillement 200-300 microsecondes jusqu'à ce que sa tâche reçoive son tick. Et si nous avons un Cortex-M7 fonctionnant à une fréquence de 216 MHz, alors 20 microsecondes pour un tick sont tout à fait raisonnables, car il faudra moins d'une demi-microseconde pour basculer. Et toute tâche de l'exemple ci-dessus ne sera jamais en retard de plus de 140 microsecondes.


Cependant, avec une augmentation du nombre de tâches, même avec une taille extrêmement petite du quantum temporel, le retard dans le début de l'activité de la tâche requise peut cesser d'être agréable. Sur cette base, et en tenant également compte du fait que seule une petite partie des tâches nécessite vraiment du temps réel dur, il a été décidé de modifier légèrement la logique du commutateur. Il est illustré à la figure 2.



Maintenant, nous sélectionnons seulement une partie des tâches qui reçoivent un quantum entier, et sélectionnons seulement un tick pour le reste, dans lequel elles se relaient dans le jeu. Dans ce cas, le sous-programme d'initialisation reçoit un paramètre d'entrée, à savoir le numéro de position, à partir duquel toutes les tâches seront affectées en droits et partageront un tick. Dans le même temps, l'ancien schéma restait disponible, pour cela il suffit de mettre la valeur du paramètre à zéro ou le nombre total de tâches. Les coûts de commutation ont augmenté de quelques instructions d'assembleur.


Deux schémas similaires sont utilisés pour permettre l'accès aux ressources partagées. Le premier, mentionné dans une note précédente, utilise plusieurs FIFO (ou tampons circulaires selon le nombre de producteurs de messages) et une tâche de correspondance distincte. Il est conçu pour communiquer avec le monde extérieur et ne nécessite pas d'attentes de tâches qui génèrent des messages. Il suffit de s'assurer que les files d'attente ne sont pas bondées.


Le deuxième schéma utilise également une tâche distincte pour autoriser l'accès, mais introduit des attentes car il gère la ressource interne dans les deux sens. Ces actions ne peuvent pas être liées au temps. La figure 3 montre les composants du deuxième circuit.



Les principaux éléments sont un tampon de requêtes, en fonction du nombre de tâches souhaitées, et un indicateur d'accès. Le fonctionnement de cette conception est assez simple. La tâche de gauche envoie une demande d'accès à un endroit spécialement alloué pour elle (par exemple, la tâche 2 écrit 1 dans la demande 2). Tâche - le répartiteur sélectionne qui autoriser et écrit le numéro de la tâche sélectionnée dans l'indicateur de résolution. La tâche qui a reçu l'autorisation effectue ses actions et écrit le signe de fin d'accès à la demande, la valeur 0xFF. Le planificateur, voyant que la demande est effacée, réinitialise l'indicateur d'autorisation, réinitialise la demande précédente et réinitialise la demande d'une autre tâche.


Deux projets de test sous IAR et une description de la carte STM32F3DISCOVERY utilisée peuvent être consultés ici . Dans le premier projet, l'ATS303 a simplement vérifié ses performances et l'a débogué. Toutes les LED installées sur cette carte sont utiles. Personne n'a été blessé.


Le deuxième projet de BTS303 a testé les deux options d'allocation des ressources mentionnées. Dans ce document, les tâches 1 et 2 génèrent des messages de test reçus par l'opérateur. Pour communiquer avec l'opérateur, j'ai dû ajouter un foulard avec un port COM TTL, comme indiqué sur la photo ci-dessous.



L'opérateur utilise un émulateur de terminal. Je pense que le lecteur excusera l'auteur pour la couleur du tube doux. Cela ressemble à ceci.



Pour démarrer le fonctionnement de l'ensemble du système, avant de résoudre les interruptions, des étapes préliminaires sont nécessaires dans le corps de la tâche zéro principale (), qui sont présentées ci-dessous.


void main_start_task_switcher(U8 border); U8 task_run_and_return_task_number((U32)t1_task); U8 task_run_and_return_task_number((U32)t2_task); U8 task_run_and_return_task_number((U32)t3_human_link); U8 task_run_and_return_task_number((U32)t4_human_answer); U8 task_run_and_return_task_number((U32)task_5); U8 task_run_and_return_task_number((U32)task_6); U8 task_run_and_return_task_number((U32)task_7); 

Dans ces lignes, le commutateur démarre d'abord, puis, à son tour, les sept tâches restantes.


Voici l'ensemble minimal d'appels requis pour le travail.


  void task_wake_up_action(U8 taskNumber); 

Cet appel est utilisé dans une interruption à partir d'un minuteur matériel utilisateur. Les défis des tâches elles-mêmes parlent d'eux-mêmes.


  void release_me_and_set_sleep_steps(U32 ticks); U8 get_my_number(void); 

Toutes ces fonctions se trouvent dans le fichier de commutateur d'assembleur. Il existe plusieurs autres fonctions utiles pour les tests, mais non obligatoires.


Dans le projet BTS303, la tâche 3 reçoit les commandes de l'opérateur de l'extérieur et lui envoie les réponses provenant de la tâche 4. La tâche 4 reçoit les commandes de l'opérateur de la tâche 3 et les exécute avec les réponses possibles. La tâche 3 reçoit également des messages des tâches 1 et 2 et les envoie via UART à l'émulateur de terminal (par exemple, mastic).


La tâche 0 (principale) effectue un travail auxiliaire, par exemple, vérifie le nombre de mots non affectés dans la zone empilée de chaque tâche. Ces informations peuvent être demandées par l'opérateur et se faire une idée de l'utilisation de la pile. Initialement, pour chaque tâche, une zone de pile de 512 octets (128 mots) est allouée et il est nécessaire de surveiller (au moins au stade du débogage) que ces zones ne se rapprochent pas du débordement.


Les tâches 5 et 6 effectuent des calculs sur une variable à virgule flottante commune. Pour ce faire, ils en demandent l'accès à la tâche 7.


Il existe une autre fonctionnalité supplémentaire qui peut être vue dans les projets de test. Il est conçu pour que vous puissiez réveiller la tâche non pas après l'expiration du nombre de ticks, mais après une durée spécifiée, et cela ressemble à ceci.


  void wake_me_up_after_milliSeconds(U32 timeMS); 

Pour sa mise en œuvre, un minuteur matériel supplémentaire est également requis, qui est également implémenté dans les cas de test.


Comme vous pouvez le voir, la liste de tous les appels nécessaires tient sur une seule page.

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


All Articles