Multitâche sur microcontrôleurs PIC

17

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.hmais 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?

Avion
la source
Les threads ne peuvent être utilisés que sur des processeurs qui exécutent un système d'exploitation, car les threads font partie du processus et les processus ne sont utilisés que dans les systèmes d'exploitation.
TicTacToe
@Zola oui, vous avez raison. Mais qu'en est-il des contrôleurs?
Avion
2
Copie possible de RTOS pour les systèmes embarqués
Roger Rowland
1
Pouvez-vous expliquer pourquoi vous avez besoin d'un véritable multitâche et ne pouvez pas raisonnablement implémenter votre logiciel basé sur une approche de tâche à tour de rôle ou une boucle select () ou similaire?
whatsisname
2
Eh bien, comme je l'ai déjà dit, j'envoie et reçois des données sur uart et en même temps j'envoie et reçois des données sur ethernet. En dehors de cela, je dois également enregistrer les données sur la carte SD avec le temps, donc oui DS1307 RTC est impliqué et EEPROM est également impliqué. Jusqu'à présent, je n'ai que 1 UART, mais peut-être après quelques jours, j'enverrai et recevrai des données de 3 modules UART. Le site Web recevra également des données de 5 systèmes différents installés à distance. Tout cela doit être parallèle mais juste pas son pas parallèle mais avec un retard de quelques secondes. !
Avion du

Réponses:

20

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.

tcrosley
la source
3
Un avantage du multitâche coopératif est qu'il n'est généralement pas nécessaire d'utiliser des verrous pour coordonner l'accès aux ressources. Il suffira de garantir que les tâches laissent toujours les ressources dans un état partageable chaque fois qu'elles abandonnent le contrôle. Le multitâche préemptif est beaucoup plus compliqué si une tâche peut être désactivée alors qu'elle détient un verrou sur une ressource nécessaire à une autre tâche. Dans certains cas, la deuxième tâche peut finir par être bloquée plus longtemps qu'elle ne l'aurait été dans un système coopératif, car la tâche qui détient le verrou aurait consacré le système ...
supercat
1
... toutes les ressources nécessaires pour terminer l'action qui (sur le système de préemption) aurait nécessité le verrouillage, rendant ainsi l'objet protégé disponible pour la deuxième tâche.
supercat
1
Alors que les multitâches coopératives nécessitent de la discipline, il peut parfois être plus facile de garantir que les exigences de temps seront respectées dans le cas d'un multitâche coopératif que dans le cas d'un préemptif. Étant donné que très peu de verrous devront être maintenus sur un commutateur de tâches, un système de commutateur de tâches à tour de rôle à cinq tâches où les tâches ne doivent pas dépasser 10 ms sans céder, combiné avec un peu de logique qui dit "Si la tâche X est urgente doit être exécutée, exécutez-la ensuite ", garantira que la tâche X n'aura jamais à attendre plus de 10 ms une fois qu'elle signale avant de s'exécuter. En revanche, si une tâche nécessite un verrou quelle tâche X ...
supercat
1
... va avoir besoin, mais est désactivé par un commutateur préemptif avant de le libérer, X pourrait ne rien faire d'utile jusqu'à ce que le planificateur du processeur se déplace pour exécuter la première tâche. À moins que le planificateur n'inclue une logique pour reconnaître et gérer l'inversion de priorité, cela peut prendre un certain temps avant de laisser la première tâche terminer son activité et libérer le verrou. De tels problèmes ne sont pas insolubles, mais les résoudre nécessite une grande complexité qui aurait pu être évitée dans un système coopératif. Les systèmes coopératifs fonctionnent très bien, sauf pour un problème: ...
supercat
3
vous n'avez pas besoin de plusieurs piles en coopérative si vous codez en continuations. En substance, votre code est divisé en fonctions, 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.
Ratchet Freak
16

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:

  • La boucle d'interrogation principale classique. Votre fonction principale a un certain temps (1) qui effectue la tâche 1 puis la tâche 2 ...
  • Boucle principale + drapeaux ISR: vous disposez d'un ISR qui effectue la fonction critique et alerte ensuite la boucle principale via une variable de drapeau que la tâche a besoin de service. Peut-être que l'ISR place un nouveau caractère dans un tampon circulaire, puis indique à la boucle principale de gérer les données lorsqu'il est prêt à le faire.
  • Tous les ISR: une grande partie de la logique ici est exécutée à partir des ISR. Sur un contrôleur moderne comme un ARM qui a plusieurs niveaux de priorité. Cela peut fournir un puissant schéma "semblable à un thread", mais peut également être source de confusion pour le débogage, il ne doit donc être réservé qu'aux contraintes de synchronisation critiques.
  • RTOS: un noyau RTOS (facilité par un temporisateur ISR) peut permettre de basculer entre plusieurs threads d'exécution. Vous avez mentionné FreeRTOS.

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.

Houston Fortney
la source
3
C'est une réponse très utile car elle traite de la racine du problème (comment effectuer plusieurs tâches sur un petit système embarqué, plutôt que des threads comme solution). Un paragraphe sur la façon dont il pourrait s'appliquer à la question d'origine serait superbe, y compris peut-être les avantages et les inconvénients de chacun pour le scénario.
David
8

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.

Arsenal
la source
2
merci, DMA semble intéressant. Je vais certainement le chercher.!
Avion du
Toutes les séries de PIC n'ont pas de DMA.
Matt Young
1
J'utilise PIC32;)
Avion
6

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.

erebos
la source
d'où puis-je télécharger son code source pour Windows? Je pense que ce n'est disponible que pour Linux.!
Avion
@CZAbhinav Il devrait être indépendant du système d'exploitation et vous pouvez obtenir le dernier téléchargement ici .
erebos
Je suis actuellement dans Windows et j'utilise MplabX, je ne pense pas que ce soit utile ici. Quoi qu'il en soit merci.!
Avion
Je n'ai pas entendu parler des protothreads, cela ressemble à une technique intéressante.
Arsenal
@CZAbhinav De quoi parlez-vous? C'est du code C et n'a rien à voir avec votre système d'exploitation.
Matt Young
3

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:

unsigned char tick;

void interrupt HANDLER(void) {
    device_driver_A();
    device_driver_B();
    if(T0IF)
    {
        TMR0 = TICK_1MS;
        T0IF = 0;   // reset timer interrupt
        tick ++;
    }
}

void main(void)
{
    init();

    while (1) {
        // periodic tasks:
        if (tick % 10 == 0) { // roughly every 10 ms
            task_A();
            task_B();    
        }
        if (tick % 55 == 0) { // roughly every 55 ms
            task_C();
            task_D();    
        }

        // tasks that need to run every loop:
        task_E();
        task_F();
    }
}

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.

Slebetman
la source
1
Je n'appellerais pas vraiment cela «multitâche coopératif», car ce n'est vraiment pas très différent de tout autre programme de microcontrôleur qui doit faire plusieurs choses.
whatsisname
2

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:

for(;;)
{
    main_lcd_ui_tick();
    networking_tick();
}


...

// In your LCD UI module:
void main_lcd_ui_tick(void)
{
    check_for_key_presses();
    update_lcd();
}

...

// In your networking module:
void networking_tick(void)
{
    //'Tick' the TCP/IP library. In this example, I'm periodically
    //calling the main function for Keil's TCP/IP library.
    main_TcpNet();
}

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:

  1. 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.)

  2. 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.

Page Trevor
la source
2
Merci pour cette belle explication, j'utilise la bibliothèque TCP / IP fournie par microchip et c'est un code complexe très énorme. J'ai réussi à le diviser en plusieurs parties et à le rendre utilisable selon mes besoins. Je vais certainement essayer une de vos approches.!
Avion du
Amusez-vous :) L'utilisation d'un RTOS vous facilite la vie dans de nombreuses situations. À mon avis, l'utilisation d'un thread (tâche) facilite beaucoup l'effort de programmation dans un sens, car vous pouvez éviter d'avoir à diviser votre tâche en une machine à états. Au lieu de cela, vous écrivez simplement votre code de tâche comme vous le feriez dans vos programmes C #, avec votre code de tâche créé comme si c'était la seule chose qui existe. Il est essentiel d'explorer les deux approches, et à mesure que vous progressez dans la programmation intégrée, vous commencez à avoir une idée de l'approche qui convient le mieux à chaque situation.
Trevor Page
Je préfère également utiliser l'option de filetage. :)
Avion