Pour un microcontrôleur métallique nu, par rapport au code maison avec boucle d'arrière-plan et architecture d'interruption du minuteur, quels sont les avantages d'un système d'exploitation non préemptif? Quels sont ces avantages suffisamment intéressants pour qu'un projet adopte un système d'exploitation non préemptif, plutôt que d'utiliser du code maison avec une architecture de boucle d'arrière-plan?
.
Explication de la question:
J'apprécie vraiment tous ceux qui ont répondu à ma question. Je sens que la réponse est presque là. J'ajoute cette explication à ma question ici qui montre ma propre considération et peut aider à affiner la question ou à la rendre plus précise.
Ce que j'essaie de faire, c'est de comprendre comment choisir le RTOS le plus approprié pour un projet en général.
Pour y parvenir, une meilleure compréhension des concepts de base et des avantages les plus attractifs des différents types de RTOS et du prix correspondant aidera, car il n'y a pas de meilleur RTOS pour toutes les applications.
J'ai lu des livres sur OS il y a quelques années mais je ne les ai plus avec moi. J'ai cherché sur Internet avant de poster ma question ici et j'ai trouvé cette information très utile: http://www.ustudy.in/node/5456 .
Il existe de nombreuses autres informations utiles telles que les introductions sur le site Web de différents RTOS, des articles comparant la planification préventive et la planification non préemptive, etc.
Mais je n'ai trouvé aucun sujet mentionné quand choisir un RTOS non préemptif et quand il est préférable d'écrire votre propre code en utilisant une interruption de minuterie et une boucle d'arrière-plan.
J'ai certaines réponses, mais je n'en suis pas assez satisfait.
J'aimerais vraiment connaître la réponse ou le point de vue de personnes plus expérimentées, en particulier dans la pratique de l'industrie.
Jusqu'à présent, ma compréhension est la suivante:
peu importe que vous utilisiez ou non un système d'exploitation, certains types de codes de planification sont toujours nécessaires, même sous forme de code comme:
in the timer interrupt which occurs every 10ms
if(it's 10ms)
{
call function A / execute task A;
}
if(it's 50ms)
{
call function B / execute task B;
}
Avantage 1:
un système d'exploitation non préemptif désigne le chemin / style de programmation pour le code de planification, afin que les ingénieurs puissent partager la même vue même s'ils n'étaient pas dans le même projet auparavant. Ensuite, avec la même vue sur la tâche conceptuelle, les ingénieurs peuvent travailler sur différentes tâches et les tester, les profiler de manière indépendante autant que possible.
Mais combien pouvons-nous vraiment gagner de cela? Si les ingénieurs travaillent dans le même projet, ils peuvent trouver un moyen de bien partager la même vue sans utiliser un système d'exploitation non préemptif.
Si un ingénieur appartient à un autre projet ou entreprise, il en bénéficiera s'il connaissait le système d'exploitation auparavant. Mais s'il ne l'a pas fait, encore une fois, cela ne semble pas faire une grande différence pour lui d'apprendre un nouveau système d'exploitation ou un nouveau morceau de code.
Avantage 2:
si le code du système d'exploitation a été bien testé, cela permet de gagner du temps du débogage. C'est vraiment un bon avantage.
Mais si l'application n'a que 5 tâches, je pense que ce n'est pas vraiment compliqué d'écrire votre propre code en utilisant une interruption de minuterie et une boucle d'arrière-plan.
Un système d'exploitation non préemptif est ici référencé à un système d'exploitation commercial / gratuit / hérité avec un planificateur non préemptif.
Lorsque j'ai posté cette question, je pense principalement à certains systèmes d'exploitation comme:
(1) KISS Kernel (A Small NonPreemptive RTOS - revendiqué par son site Web)
http://www.frontiernet.net/~rhode/kisskern.html
(2) uSmartX (RTOS léger - revendiqué par son site Web)
(3) FreeRTOS (C'est un RTOS préemptif, mais si je comprends bien, il peut également être configuré comme un RTOS non préemptif)
(4) uC / OS (similaire à FreeRTOS)
(5 ) code OS / planificateur hérité dans certaines entreprises (généralement créé et géré par l'entreprise en interne)
(Impossible d'ajouter plus de liens en raison de la limitation du nouveau compte StackOverflow)
Si je comprends bien, un système d'exploitation non préemptif est une collection de ces codes:
(1) un planificateur utilisant une stratégie non préemptive.
(2) des installations pour la communication entre les tâches, le mutex, la synchronisation et le contrôle du temps.
(3) gestion de la mémoire.
(4) autres installations / bibliothèques utiles comme système de fichiers, la pile réseau, l' interface graphique et etc. (FreeRTOS et uC / OS offre ces, mais je ne sais pas s'ils encore du travail lorsque le planificateur est configuré comme non préemptif)
Certains ils ne sont pas toujours là. Mais l'ordonnanceur est un incontournable.
la source
Réponses:
Cela sent un peu hors sujet, mais je vais essayer de le ramener sur la bonne voie.
Le multitâche préventif signifie que le système d'exploitation ou le noyau peut suspendre le thread en cours d'exécution et passer à un autre en fonction de l'heuristique de planification qu'il a en place. La plupart du temps, les threads en cours d'exécution n'ont aucune idée qu'il se passe d'autres choses sur le système, et ce que cela signifie pour votre code, c'est que vous devez faire attention à le concevoir de sorte que si le noyau décide de suspendre un thread au milieu d'un opération en plusieurs étapes (par exemple, changer une sortie PWM, sélectionner un nouveau canal ADC, lire l'état d'un périphérique I2C, etc.) et laisser un autre thread s'exécuter pendant un certain temps, afin que ces deux threads n'interfèrent pas l'un avec l'autre.
Un exemple arbitraire: disons que vous êtes nouveau dans les systèmes embarqués multithread et que vous avez un petit système avec un ADC I2C, un écran LCD SPI et une EEPROM I2C. Vous avez décidé que ce serait une bonne idée d'avoir deux threads: un qui lit à partir de l'ADC et écrit des échantillons dans l'EEPROM, et un qui lit les 10 derniers échantillons, les fait la moyenne et les affiche sur l'écran LCD SPI. Le design inexpérimenté ressemblerait à quelque chose comme ça (grossièrement simplifié):
Ceci est un exemple très grossier et rapide. Ne codez pas comme ça!
Rappelez-vous maintenant, le système d'exploitation multitâche préventif peut suspendre l'un de ces threads à n'importe quelle ligne du code (en fait à n'importe quelle instruction d'assemblage) et donner à l'autre thread le temps de s'exécuter.
Pensez-y. Imaginez ce qui se passerait si le système d'exploitation décidait de suspendre
adc_thread()
entre le réglage de l'adresse EE pour écrire et l'écriture des données réelles.lcd_thread()
s'exécutait, se promenait avec le périphérique I2C pour lire les données dont il avait besoin, et lorsqueadc_thread()
son tour recommençait, l'EEPROM ne serait pas dans le même état qu'elle était laissée. Les choses ne fonctionneraient pas très bien du tout. Pire, cela pourrait même fonctionner la plupart du temps, mais pas tout le temps, et vous deviendriez fou en essayant de comprendre pourquoi votre code ne fonctionne pas quand il A l'air comme il le devrait!C'est un meilleur exemple; le système d'exploitation pourrait décider de préempter
i2c_write()
duadc_thread()
contexte de et recommencer à l'exécuter à partirlcd_thread()
du contexte de! Les choses peuvent devenir vraiment désordonnées très rapidement.Lorsque vous écrivez du code pour travailler dans un environnement multitâche préventif, vous devez utiliser des mécanismes de verrouillage pour vous assurer que si votre code est suspendu à un moment inopportun, tout l'enfer ne se déchaîne pas.
Le multitâche coopératif, d'autre part, signifie que chaque thread contrôle quand il abandonne son temps d'exécution. Le codage est plus simple, mais le code doit être conçu avec soin pour s'assurer que tous les threads ont suffisamment de temps pour s'exécuter. Un autre exemple artificiel:
Ce code ne fonctionnera pas comme vous le pensez, ou même s'il semble fonctionner, il ne fonctionnera pas lorsque le débit de données du fil d'écho augmente. Encore une fois, prenons une minute pour l'examiner.
echo_thread()
attend qu'un octet apparaisse dans un UART, puis le récupère et attend qu'il y ait de la place pour l'écrire, puis l'écrit. Après cela, cela donne un tour aux autres threads.seconds_counter()
incrémentera un nombre, attendra 1000 ms, puis donnera aux autres threads une chance de s'exécuter. Si deux octets entrent dans l'UART pendant ce tempssleep()
, vous risquez de ne pas les voir car notre hypothétique UART n'a pas de FIFO pour stocker des caractères pendant que le CPU est occupé à faire d'autres choses.La bonne façon de mettre en œuvre ce très mauvais exemple serait de mettre
yield_cpu()
où que vous ayez une boucle occupée. Cela aidera les choses à avancer, mais pourrait causer d'autres problèmes. Par exemple, si le timing est critique et que vous cédez le CPU à un autre thread qui prend plus de temps que prévu, vous pourriez avoir votre timing désactivé. Un système d'exploitation multitâche préventif n'aurait pas ce problème car il suspend de force les threads pour s'assurer que tous les threads sont correctement planifiés.Maintenant, qu'est-ce que cela a à voir avec une boucle de minuterie et d'arrière-plan? Le minuteur et la boucle d'arrière-plan sont très similaires à l'exemple multitâche coopératif ci-dessus:
Cela ressemble assez à l'exemple de filetage coopératif; vous avez une minuterie qui configure les événements et une boucle principale qui les recherche et agit sur eux de manière atomique. Vous n'avez pas à vous soucier des «fils» ADC et LCD qui interfèrent l'un avec l'autre car l'un n'interrompra jamais l'autre. Vous devez toujours vous soucier d'un "fil" trop long; par exemple ce qui se passe si
get_adc_data()
prend 30 ms? vous manquerez trois occasions de vérifier un personnage et de le faire écho.L'implémentation loop + timer est souvent beaucoup plus simple à implémenter qu'un micro-noyau multitâche coopératif car votre code peut être conçu plus spécifiquement pour la tâche à accomplir. Vous n'êtes pas vraiment multitâche autant que de concevoir un système fixe où vous donnez à chaque sous-système un certain temps pour effectuer ses tâches d'une manière très spécifique et prévisible. Même un système multitâche coopératif doit avoir une structure de tâches générique pour chaque thread et le prochain thread à exécuter est déterminé par une fonction de planification qui peut devenir assez complexe.
Les mécanismes de verrouillage pour les trois systèmes sont les mêmes, mais les frais généraux requis pour chacun sont assez différents.
Personnellement, je code presque toujours sur cette dernière norme, l'implémentation loop + timer. Je trouve que le filetage est quelque chose qui devrait être utilisé avec parcimonie. Non seulement il est plus complexe à écrire et à déboguer, mais il nécessite également plus de surcharge (un micro-noyau multitâche préemptif sera toujours plus gros qu'un minuteur stupidement simple et un suiveur d'événements de boucle principale).
Il y a aussi un dicton que toute personne travaillant sur des threads appréciera:
:-)
la source
poll()
vient immédiatement à l'esprit).Le multitâche peut être une abstraction utile dans de nombreux projets de microcontrôleurs, bien qu'un véritable planificateur préemptif soit trop lourd et inutile dans la plupart des cas. J'ai réalisé plus de 100 projets de microcontrôleurs. J'ai utilisé des tâches coopératives un certain nombre de fois, mais le changement de tâche préventif avec ses bagages associés n'a jusqu'à présent pas été approprié.
Les problèmes liés à la tâche préventive par rapport à la tâche coopérative sont les suivants:
En général, dédier une tâche à un travail particulier est logique lorsque le processeur peut prendre en charge cela et que le travail est suffisamment compliqué avec suffisamment d'opérations dépendantes de l'historique pour que le diviser en quelques événements individuels distincts soit lourd. C'est généralement le cas lors de la gestion d'un flux d'entrée de communication. Ces choses sont généralement fortement pilotées par l'état en fonction de certaines entrées précédentes. Par exemple, il peut y avoir des octets d'opcode suivis par des octets de données uniques pour chaque opcode. Ensuite, il y a le problème de ces octets qui vous viennent à l'esprit lorsque quelque chose d'autre a envie de les envoyer. Avec une tâche distincte gérant le flux d'entrée, vous pouvez le faire apparaître dans le code de tâche comme si vous sortiez et obteniez l'octet suivant.
Dans l'ensemble, les tâches sont utiles lorsqu'il y a beaucoup de contexte d'état. Les tâches sont essentiellement des machines à états, le PC étant la variable d'état.
Beaucoup de choses qu'un micro doit faire peuvent être exprimées comme répondant à un ensemble d'événements. En conséquence, j'ai généralement une boucle d'événement principale. Cela vérifie chaque événement possible dans l'ordre, puis revient en haut et recommence. Lorsque la gestion d'un événement prend plus que quelques cycles, je reviens généralement au début de la boucle d'événement après avoir géré l'événement. Cela signifie que les événements ont une priorité implicite en fonction de l'endroit où ils sont vérifiés dans la liste. Sur de nombreux systèmes simples, cela suffit.
Parfois, vous obtenez des tâches un peu plus compliquées. Ceux-ci peuvent souvent être décomposés en une séquence d'un petit nombre de choses distinctes à faire. Vous pouvez utiliser des indicateurs internes comme événements dans ces cas. J'ai fait ce genre de choses plusieurs fois sur des PIC bas de gamme.
Si vous avez la structure d'événement de base comme ci-dessus, mais devez également répondre à un flux de commandes via l'UART, par exemple, il est utile d'avoir une tâche distincte pour gérer le flux UART reçu. Certains microcontrôleurs ont des ressources matérielles limitées pour le multitâche, comme un PIC 16 qui ne peut pas lire ou écrire sa propre pile d'appels. Dans de tels cas, j'utilise ce que j'appelle une pseudo-tâche pour le processeur de commandes UART. La boucle d'événements principale gère toujours tout le reste, mais l'un de ses événements à gérer est qu'un nouvel octet a été reçu par l'UART. Dans ce cas, il passe à une routine qui exécute cette pseudo-tâche. Le module de commande UART contient le code de tâche, et l'adresse d'exécution et quelques valeurs de registre de la tâche sont enregistrées dans la RAM de ce module. Le code sauté par la boucle d'événement enregistre les registres actuels, charge les registres de tâches enregistrés, et passe à l'adresse de redémarrage de la tâche. Le code de tâche appelle une macro YIELD qui fait l'inverse, qui revient ensuite au début de la boucle d'événement principale. Dans certains cas, la boucle d'événement principale exécute la pseudo-tâche une fois par passage, généralement en bas pour en faire un événement de faible priorité.
Sur un PIC 18 et supérieur, j'utilise un véritable système de tâches coopératif car la pile d'appels est lisible et inscriptible par le firmware. Sur ces systèmes, l'adresse de redémarrage, quelques autres éléments d'état et le pointeur de pile de données sont conservés dans une mémoire tampon pour chaque tâche. Pour laisser toutes les autres tâches s'exécuter une fois, une tâche appelle TASK_YIELD. Cela enregistre l'état actuel de la tâche, recherche dans la liste la prochaine tâche disponible, charge son état, puis l'exécute.
Dans cette architecture, la boucle d'événement principale n'est qu'une autre tâche, avec un appel à TASK_YIELD en haut de la boucle.
Tout mon code multitâche pour les PIC est disponible gratuitement. Pour le voir, installez la version des outils de développement PIC à l' adresse http://www.embedinc.com/pic/dload.htm . Recherchez les fichiers avec «tâche» dans leurs noms dans le répertoire SOURCE> PIC pour les PIC 8 bits et dans le répertoire SOURCE> DSPIC pour les PIC 16 bits.
la source
Edit: (Je vais laisser mon précédent post ci-dessous; peut-être que cela aidera quelqu'un un jour.)
Les systèmes d'exploitation multitâche de tout type et les routines de service d'interruption ne sont pas - ou ne devraient pas être - des architectures système concurrentes. Ils sont destinés à différents emplois à différents niveaux du système. Les interruptions sont vraiment destinées à de brèves séquences de code pour gérer des tâches immédiates comme le redémarrage d'un périphérique, éventuellement l'interrogation de périphériques sans interruption, le chronométrage dans le logiciel, etc. On suppose généralement que l'arrière-plan effectuera tout traitement supplémentaire qui n'est plus critique en temps après la les besoins immédiats ont été satisfaits. Si tout ce que vous avez à faire est de redémarrer une minuterie et de basculer une LED ou d'impulser un autre appareil, l'ISR peut généralement tout faire au premier plan en toute sécurité. Sinon, il doit informer l'arrière-plan (en définissant un indicateur ou en mettant un message en file d'attente) que quelque chose doit être fait et libérer le processeur.
J'ai vu des structures de programme très simples dont la boucle arrière - plan est juste une boucle de repos:
for(;;){ ; }
. Tout le travail a été effectué dans le temporisateur ISR. Cela peut fonctionner lorsque le programme doit répéter une opération constante qui est garantie de se terminer en moins d'une période de temporisation; certains types limités de traitement du signal me viennent à l'esprit.Personnellement, j'écris des ISR qui nettoient une sortie, et laisse l'arrière-plan prendre le dessus sur tout ce qui doit être fait, même si c'est aussi simple qu'une multiplication et un ajout qui pourrait être fait en une fraction de temps. Pourquoi? Un jour, j'aurai l'idée brillante d'ajouter une autre fonction "simple" à mon programme, et "diable, il faudra juste un court ISR pour le faire" et soudain, mon architecture précédemment simple développe des interactions que je n'avais pas prévues sur et arriver de manière incohérente. Ce n'est pas très amusant à déboguer.
(Comparaison précédemment publiée de deux types de tâches multiples)
Changement de tâche: Pre-emptive MT s'occupe du changement de tâche pour vous, notamment en veillant à ce qu'aucun thread ne soit affamé par le processeur et que les threads de haute priorité s'exécutent dès qu'ils sont prêts. Cooperative MT nécessite que le programmeur s'assure qu'aucun thread ne garde le processeur trop longtemps à la fois. Vous devrez également décider combien de temps est trop long. Cela signifie également que chaque fois que vous modifiez le code, vous devez savoir si un segment de code dépasse maintenant ce quantum temporel.
Protection des opérations non atomiques: avec un PMT, vous devrez vous assurer que les échanges de threads ne se produisent pas au milieu des opérations qui ne doivent pas être divisées. Lecture / écriture de certaines paires de registres de périphériques qui doivent être traitées dans un ordre particulier ou dans un délai maximum, par exemple. Avec CMT, c'est assez facile - ne cédez pas le processeur au milieu d'une telle opération.
Débogage: généralement plus facile avec CMT, car vous planifiez quand / où les commutations de thread se produiront. Les conditions de concurrence entre les threads et les bogues liés aux opérations non thread-safe avec un PMT sont particulièrement difficiles à déboguer car les changements de thread sont probabilistes, donc non répétables.
Comprendre le code: les threads écrits pour un PMT sont à peu près écrits comme s'ils pouvaient être autonomes. Les threads écrits pour un CMT sont écrits sous forme de segments et selon la structure du programme que vous choisissez, il peut être plus difficile à suivre pour un lecteur.
Utilisation d'un code de bibliothèque non thread-safe: vous devrez vérifier que chaque fonction de bibliothèque que vous appelez sous un thread-safe PMT. printf () et scanf () et leurs variantes ne sont presque toujours pas thread-safe. Avec un CMT, vous saurez qu'aucun changement de thread ne se produira, sauf lorsque vous cédez spécifiquement le processeur.
Un système piloté par machine à états finis pour contrôler un appareil mécanique et / ou suivre des événements externes est souvent un bon candidat pour la CMT, car à chaque événement, il n'y a pas grand-chose à faire - démarrer ou arrêter un moteur, définir un drapeau, choisir l'état suivant , etc. Ainsi, les fonctions de changement d'état sont intrinsèquement brèves.
Une approche hybride peut très bien fonctionner dans ces types de systèmes: CMT pour gérer la machine d'état (et donc, la plupart du matériel) fonctionnant comme un thread, et un ou deux threads supplémentaires pour effectuer des calculs plus longs lancés par un état changement.
la source