Le multitâche est important de nos jours. Je me demande comment nous pouvons y parvenir dans les microcontrôleurs et la programmation intégrée. Je conçois un système basé sur un microcontrôleur PIC. J'ai conçu son micrologiciel dans MplabX IDE à l'aide de C, puis conçu une application pour celui-ci dans Visual Studio à l'aide de C #.
Depuis que je me suis habitué à utiliser des threads en programmation C # sur le bureau pour implémenter des tâches parallèles, existe-t-il un moyen de faire de même dans mon code de microcontrôleur? L'IDE MplabX fournit pthreads.h
mais c'est juste un stub sans implémentation. Je sais qu'il existe un support FreeRTOS mais son utilisation rend votre code plus complexe. Certains forums disent que les interruptions peuvent également être utilisées comme multitâche, mais je ne pense pas que les interruptions soient équivalentes aux threads.
Je conçois un système qui envoie des données à un UART et en même temps, il doit envoyer des données à un site Web via Ethernet (filaire). Un utilisateur peut contrôler la sortie via le site Web, mais la sortie est activée / désactivée avec un délai de 2-3 secondes. Voilà donc le problème auquel je suis confronté. Existe-t-il une solution pour le multitâche dans les microcontrôleurs?
la source
Réponses:
Il existe deux principaux types de systèmes d'exploitation multitâche, préemptifs et coopératifs. Les deux permettent de définir plusieurs tâches dans le système, la différence réside dans le fonctionnement du changement de tâche. Bien sûr, avec un seul processeur principal, une seule tâche est en cours d'exécution à la fois.
Les deux types de systèmes d'exploitation multitâche nécessitent une pile distincte pour chaque tâche. Cela implique donc deux choses: premièrement, le processeur permet de placer des piles n'importe où dans la RAM et a donc des instructions pour déplacer le pointeur de pile (SP) - c'est-à-dire qu'il n'y a pas de pile matérielle à usage spécial comme il y en a sur le bas de gamme Photos. Cela laisse de côté les séries PIC10, 12 et 16.
Vous pouvez écrire un OS presque entièrement en C, mais le sélecteur de tâches, où le SP se déplace, doit être en assembleur. À divers moments, j'ai écrit des commutateurs de tâches pour les PIC24, PIC32, 8051 et 80x86. Les tripes sont toutes assez différentes selon l'architecture du processeur.
La deuxième exigence est qu'il y ait suffisamment de RAM pour prévoir plusieurs piles. Habituellement, on voudrait au moins quelques centaines d'octets pour une pile; mais même à seulement 128 octets par tâche, huit piles vont nécessiter 1 Ko d'octets de RAM - vous n'avez pas besoin d'allouer la même pile de taille pour chaque tâche. N'oubliez pas que vous avez besoin de suffisamment de pile pour gérer la tâche en cours et tous les appels à ses sous-programmes imbriqués, mais aussi empiler de l'espace pour un appel d'interruption car vous ne savez jamais quand un va se produire.
Il existe des méthodes assez simples pour déterminer la quantité de pile que vous utilisez pour chaque tâche; par exemple, vous pouvez initialiser toutes les piles à une valeur particulière, disons 0x55, et exécuter le système pendant un certain temps, puis arrêter et examiner la mémoire.
Vous ne dites pas quel type de PIC vous souhaitez utiliser. La plupart des PIC24 et PIC32 auront amplement d'espace pour exécuter un système d'exploitation multitâche; le PIC18 (le seul PIC 8 bits à avoir des piles en RAM) a une taille de RAM maximale de 4K. C'est donc assez incertain.
Avec le multitâche coopératif (le plus simple des deux), le changement de tâche n'est effectué que lorsque la tâche "abandonne" son contrôle à l'OS. Cela se produit chaque fois que la tâche doit appeler une routine de système d'exploitation pour exécuter une fonction qu'elle attend, comme une demande d'E / S ou un appel de temporisation. Cela facilite le basculement des piles par le système d'exploitation, car il n'est pas nécessaire de sauvegarder tous les registres et les informations d'état, le SP peut simplement être basculé vers une autre tâche (s'il n'y a pas d'autres tâches prêtes à s'exécuter, une pile inactive est contrôle). Si la tâche en cours n'a pas besoin d'appeler le système d'exploitation mais s'exécute depuis un certain temps, elle doit abandonner volontairement le contrôle pour que le système reste réactif.
Le problème avec le multitâche coopératif est que si la tâche n'abandonne jamais le contrôle, elle peut monopoliser le système. Seul celui-ci et toutes les routines d'interruption qui se trouvent être contrôlées peuvent s'exécuter, de sorte que le système d'exploitation semble se bloquer. C'est l'aspect "coopératif" de ces systèmes. Si un temporisateur de surveillance est implémenté et n'est réinitialisé que lorsqu'un changement de tâche est effectué, il est possible d'attraper ces tâches errantes.
Windows 3.1 et les versions antérieures étaient des systèmes d'exploitation coopératifs, ce qui explique en partie pourquoi leurs performances n'étaient pas si bonnes.
Le multitâche préemptif est plus difficile à mettre en œuvre. Ici, les tâches ne sont pas nécessaires pour abandonner le contrôle manuellement, mais à la place, chaque tâche peut se voir attribuer un délai maximal (par exemple 10 ms), puis un changement de tâche est effectué vers la tâche exécutable suivante s'il y en a une. Cela nécessite d'arrêter arbitrairement une tâche, d'enregistrer toutes les informations d'état, puis de basculer le SP vers une autre tâche et de la démarrer. Cela rend le sélecteur de tâches plus compliqué, nécessite plus de pile et ralentit un peu le système.
Pour le multitâche coopératif et préemptif, des interruptions peuvent survenir à tout moment, ce qui préemptera temporairement la tâche en cours d'exécution.
Comme le souligne Supercat dans un commentaire, un avantage du multitâche coopératif est qu'il est plus facile de partager des ressources (par exemple du matériel comme un ADC multicanal ou un logiciel comme la modification d'une liste chaînée). Parfois, deux tâches souhaitent accéder à la même ressource en même temps. Avec une planification préemptive, il serait possible pour le système d'exploitation de commuter des tâches au milieu d'une tâche à l'aide d'une ressource. Les verrous sont donc nécessaires pour empêcher qu'une autre tâche n'entre et n'accède à la même ressource. Avec le multitâche coopératif, cela n'est pas nécessaire car la tâche contrôle quand elle se libérera automatiquement sur le système d'exploitation.
la source
void foo(void* context)
la logique du contrôleur (noyau) tire un pointeur et une paire de pointeurs de fonction de la file d'attente et l'appelle un à la fois. Cette fonction utilise le contexte pour stocker ses variables et autres et peut ensuite ajouter soumettre une continuation à la file d'attente. Ces fonctions doivent revenir rapidement pour laisser d'autres tâches leur moment dans le CPU. Il s'agit d'une méthode basée sur les événements ne nécessitant qu'une seule pile.Le filetage est fourni par un système d'exploitation. Dans le monde embarqué, nous n'avons généralement pas de système d'exploitation ("bare metal"). Cela laisse donc les options suivantes:
Je vous conseille d'utiliser le plus simple des schémas ci-dessus qui fonctionnera pour votre application. D'après ce que vous décrivez, j'aurais la boucle principale générant des paquets et les plaçant dans des tampons circulaires. Ensuite, disposez d'un pilote basé sur UART ISR qui se déclenche chaque fois que l'octet précédent est envoyé jusqu'à ce que le tampon soit envoyé, puis attend plus de contenu de tampon. Approche similaire pour l'Ethernet.
la source
Comme dans tout processeur simple cœur, le multitâche logiciel réel n'est pas possible. Vous devez donc prendre soin de basculer entre plusieurs tâches dans un sens. Les différents RTOS s'en occupent. Ils ont un planificateur et basé sur un système, ils basculeront entre différentes tâches pour vous donner une capacité multitâche.
Les concepts impliqués dans le faire (sauvegarde de contexte et restauration) sont assez compliqués, donc le faire manuellement sera probablement difficile et rendra votre code plus complexe et parce que vous ne l'avez jamais fait auparavant, il y aura des erreurs. Mon conseil ici serait d'utiliser un RTOS testé comme FreeRTOS.
Vous avez mentionné que les interruptions fournissent un niveau de multitâche. C'est en quelque sorte vrai. L'interruption interrompra votre programme actuel à tout moment et y exécutera le code, c'est comparable à un système à deux tâches où vous avez 1 tâche avec une priorité faible et une autre avec une priorité élevée qui se termine dans une tranche de temps du planificateur.
Vous pouvez donc écrire un gestionnaire d'interruption pour un temporisateur récurrent qui enverra quelques paquets sur l'UART, puis laissez le reste de votre programme s'exécuter pendant quelques millisecondes et envoyer les prochains octets. De cette façon, vous obtenez en quelque sorte une capacité multitâche limitée. Mais vous aurez également une interruption assez longue, ce qui pourrait être une mauvaise chose.
La seule vraie façon de faire plusieurs tâches en même temps sur un MCU monocœur est d'utiliser le DMA et les périphériques car ils fonctionnent indépendamment du cœur (DMA et MCU partagent le même bus, donc ils travaillent un peu plus lentement lorsque les deux sont actifs). Ainsi, pendant que le DMA mélange les octets à l'UART, votre cœur est libre d'envoyer les éléments à l'Ethernet.
la source
Les autres réponses décrivent déjà les options les plus utilisées (boucle principale, ISR, RTOS). Voici une autre option comme compromis: Protothreads . Il s'agit essentiellement d'une bibliothèque très légère pour les threads, qui utilise la boucle principale et certaines macros C, pour "émuler" un RTOS. Bien sûr, ce n'est pas un système d'exploitation complet, mais pour les threads "simples", cela peut être utile.
la source
Ma conception de base pour un RTOS à tranche de temps minimale n'a pas beaucoup changé sur plusieurs micro-familles. Il s'agit essentiellement d'une interruption de minuterie entraînant une machine d'état. La routine de service d'interruption est le noyau du système d'exploitation tandis que l'instruction switch dans la boucle principale correspond aux tâches utilisateur. Les pilotes de périphériques sont des routines de service d'interruption pour les interruptions d'E / S.
La structure de base est la suivante:
Il s'agit essentiellement d'un système coopératif multitâche. Les tâches sont écrites pour ne jamais entrer dans une boucle infinie, mais cela nous est égal car les tâches s'exécutent dans une boucle d'événements, de sorte que la boucle infinie est implicite. Il s'agit d'un style de programmation similaire aux langages orientés événements / non bloquants comme javascript ou go.
Vous pouvez voir un exemple de ce style d'architecture dans mon logiciel d'émetteur RC (oui, je l'utilise en fait pour piloter des avions RC, il est donc quelque peu critique pour la sécurité de m'empêcher de planter mes avions et potentiellement de tuer des gens): https://github.com / slebetman / pic-txmod . Il a essentiellement 3 tâches - 2 tâches en temps réel implémentées en tant que pilotes de périphérique avec état (voir les trucs ppmio) et 1 tâche d'arrière-plan implémentant la logique de mixage. Donc, fondamentalement, il est similaire à votre serveur Web en ce qu'il a 2 threads d'E / S.
la source
Bien que j'apprécie que la question porte spécifiquement sur l'utilisation d'un RTOS intégré, il me semble que la question plus large posée est "comment réaliser le multitâche sur une plate-forme embarquée".
Je vous conseille fortement d'oublier d'utiliser un RTOS intégré au moins pour le moment. Je conseille ceci parce que je pense qu'il est essentiel d'apprendre d'abord comment réaliser la «simultanéité» des tâches au moyen de techniques de programmation extrêmement simples comprenant des planificateurs de tâches simples et des machines à états.
Pour expliquer très brièvement le concept, chaque module de travail qui doit être effectué (c'est-à-dire chaque «tâche») a une fonction particulière qui doit être appelée («cochée») périodiquement pour que ce module fasse quelque chose. Le module conserve son propre état actuel. Vous disposez alors d'une boucle infinie principale (le planificateur) qui appelle les fonctions du module.
Illustration brute:
La structure de programmation à un seul thread comme celle-ci, par laquelle vous appelez périodiquement les fonctions principales de la machine à états à partir d'une boucle de programmation principale, est omniprésente dans la programmation intégrée, et c'est pourquoi j'encourage fortement l'OP à être familier et à l'aise avec lui avant de plonger directement dans l'utilisation Tâches / threads RTOS.
Je travaille sur un type d'appareil intégré doté d'une interface LCD matérielle, d'un serveur Web interne, d'un client de messagerie, d'un client DDNS, de VOIP et de nombreuses autres fonctionnalités. Bien que nous utilisions un RTOS (Keil RTX), le nombre de threads individuels (tâches) utilisés est très faible et la plupart du «multitâche» est réalisé comme décrit ci-dessus.
Pour donner quelques exemples de bibliothèques qui illustrent ce concept:
La bibliothèque de réseautage Keil. La pile TCP / IP entière peut être exécutée sur un seul thread; vous appelez périodiquement main_TcpNet (), qui itère la pile TCP / IP et toute autre option de mise en réseau que vous avez compilée depuis la bibliothèque (par exemple le serveur Web). Voir http://www.keil.com/support/man/docs/rlarm/rlarm_main_tcpnet.htm . Certes, dans certaines situations (peut-être en dehors de la portée de cette réponse), vous atteignez un point où il devient bénéfique ou nécessaire d'utiliser des threads (en particulier si vous utilisez des sockets BSD bloquants). (Plus loin: le nouveau V5 MDK-ARM génère en fait un thread Ethernet dédié - mais j'essaie juste de fournir une illustration.)
La bibliothèque Linphone VOIP. La bibliothèque linphone elle-même est monothread. Vous appelez la
iterate()
fonction à un intervalle suffisant. Voir http://www.linphone.org/docs/liblinphone-javadoc/org/linphone/core/LinphoneCore.html#iterate () . (Un mauvais exemple parce que je l'ai utilisé sur une plate-forme Linux embarquée et les bibliothèques de dépendances de Linphone engendrent sans aucun doute des threads, mais encore une fois, c'est pour illustrer un point.)Pour en revenir au problème spécifique décrit par l'OP, le problème semble être le fait que la communication UART doit avoir lieu en même temps que certains réseaux (transmission de paquets via TCP / IP). Je ne sais pas quelle bibliothèque réseau vous utilisez réellement, mais je suppose qu'elle a une fonction principale qui doit être appelée fréquemment. Vous auriez besoin d'écrire votre code qui traite de la transmission / réception de données UART pour être structuré de manière similaire, comme une machine d'état qui peut être itérée par des appels périodiques à une fonction principale.
la source