Les microcontrôleurs modernes ont des performances assez élevées et cela donne à de nombreux programmeurs la possibilité de penser approximativement dans la veine suivante: - «Ce n'est pas grave si 1 à 5% des performances vont à la maintenance du système d'exploitation. Mais mon code sera facilement débogué et explicite! » Ces réflexions sont soutenues par une grande quantité de mémoire non volatile (flash) pour stocker le code du système d'exploitation et une mémoire opérationnelle (RAM / SRAM) pour allouer sa propre pile à chaque tâche. Cependant, dans la plupart des cas, cette idée est erronée. Et dans cet article, je vais vous expliquer pourquoi.
À propos des projets avec lesquels je travaille
Dans ma pratique, je dois souvent travailler avec un «designer». J'ai décrit cette approche en détail dans mon précédent article sur l'
utilisation de C ++ dans les microcontrôleurs . Ensuite, je n'ai pas dit la chose la plus importante. La plupart des «blocs» de ce «constructeur» sont en quelque sorte liés à un système d'exploitation en temps réel. La plupart des «blocs» ont leur propre flux (tâche, en termes de système d'exploitation en temps réel FreeRTOS utilisé). Et que, en moyenne, le projet a environ 10-15 tâches. Parfois, cette valeur atteint 35-40.
O so tant?
Voici une courte liste des tâches rencontrées
dans chaque projet:
- Maintenance ADC (chaque module est desservi par son propre flux);
- maintenance wdt (si le système d'exploitation tombe en panne, la tâche ne le réinitialisera pas et l'appareil redémarrera);
- travailler avec des pages de paramètres (un flux séparé contrôle le travail avec la mémoire flash);
- maintenance du protocole d'interaction avec le monde extérieur (en aval de l'interface. Par exemple, uart);
Ensuite, il y a déjà des choses spécifiques pour chaque appareil, comme un flux pour l'entretien des thermistances (recevoir des données du flux de mesure ADC et convertir ces données en température), interroger les périphériques externes, etc.
Simplicité apparente
Malgré le fait qu'il existe de nombreuses tâches dans le projet, chacune d'entre elles est «cachée» à l'intérieur d'un objet de la classe correspondante (rappelez-vous que le constructeur est en C ++, mais cela peut également être imité en C en utilisant «la programmation en C dans un style orienté objet». Mais
il vaut mieux ne pas nécessaire ). Étant donné que les objets de ce «constructeur» sont globaux et FreeRTOS 9 est utilisé dans les projets, qui prend en charge la création de leurs propres entités dans des tampons alloués par l'utilisateur, l'utilisation de la mémoire peut être contrôlée même au stade de la construction. Donc, du point de vue de la surveillance des fuites de mémoire - tout est plus ou moins normal. Mais il y a les nuances suivantes:
- il faut bien comprendre combien une pile est nécessaire pour chaque thread. Dans ce cas:
- les cas critiques doivent être pris en compte (par exemple, l'imbrication avec un certain comportement);
- si des fonctions de bibliothèques standard sont utilisées, sachez également comment elles sont organisées, ou du moins ayez une idée de la quantité qu'elles utiliseront la pile;
En dehors de ce fait, il semble que l'utilisation du système d'exploitation ne fera qu'améliorer la logique du code et le rendre plus clair.
Utilisation abusive des fonctionnalités du système d'exploitation
Les principaux problèmes commencent au moment où vous commencez à oublier ce que vous écrivez spécifiquement pour le microcontrôleur. Le système d'exploitation impose ses coûts de travail avec ses propres entités (telles que les sémaphores, les mutex, les files d'attente). Voici un exemple de
classe UART pour implémenter une fonction de terminal . Dans l'interruption, un octet est reçu, après quoi, s'il dépasse la plage par des caractères d'entrée valides, il est ajouté à la file d'attente avec les remplacements correspondants (par exemple, '\ n' change la séquence "\ n \ r"). Cela a été fait afin de sécuriser le port pour l'envoi (car le port peut fonctionner non seulement comme un terminal. Les données du journal peuvent également être envoyées via celui-ci). D'une part, cela garantit que la réponse sera envoyée dès que possible et n'interférera pas avec l'envoi de données de priorité plus élevée (en outre, tandis que des données de priorité plus élevée sont envoyées, elles s'accumulent dans le tampon, ce qui permet d'utiliser le DMA pour envoyer la réponse). Cependant, à partir de ce moment, vous arrivez sur une piste glissante. Au lieu d'écrire un groupe dans la file d'attente, on pourrait simplement configurer correctement l'interruption sur un tampon non vide qui ne fonctionne pas actuellement sur l'UART et à la fin du DMA. Cette approche nécessite une compréhension claire du fonctionnement des périphériques. Cependant, cela réduit les coûts à un minimum absolu, rendant la nécessité d'une telle solution nulle.
Ignorer la fonctionnalité matérielle du microcontrôleur
Dans ma pratique, j'ai rencontré un projet avec 18 minuteries logicielles de système d'exploitation réglées sur la même fréquence. Dans le même temps, il y avait environ 10 temporisateurs dans le microcontrôleur, dont seul le systic était utilisé. Pour synchroniser le planificateur du système d'exploitation. Cette décision s'explique par le manque de volonté de "jouer avec le matériel" du microcontrôleur. En même temps, environ 10 kb ont été alloués à la pile pour la fonction appelée par le temporisateur logiciel. En fait, environ 1 ko a été utilisé (court). Cela était dû à «l'ambiguïté de ce qui se passe à l'intérieur des bibliothèques appelées».
Dans ce cas, il était possible de sélectionner en toute sécurité TIM6 (dans le cas de l'utilisation de stm32f4), ce qui générerait une interruption avec une fréquence donnée et à l'intérieur, il appellerait simplement les fonctions requises.
Utiliser une boucle infinie au lieu d'une machine à états
En tant que colonne distincte, je soulignerais l'incapacité de certains programmeurs à écrire des machines compactes à états finis, et à la place créer un flux dans lequel il y a une boucle infinie qui commence son travail en obtenant quelque chose de la file d'attente. Fait intéressant, comment créer des machines à états finis compactes au moyen du langage lui-même est écrit dans
cet article .
Ignorer le "planificateur matériel"
De nombreux microcontrôleurs 32 bits ont un contrôleur d'interruption bien pensé avec un système de priorité personnalisable. Dans le cas de stm32f4, il porte le nom NVIC et a la possibilité de définir des priorités d'interruption avec 16 niveaux (sans tenir compte des sous-niveaux).
La plupart des applications sous FreeRTOS que j'ai dû traiter pouvaient être écrites comme des machines à états appelées en interruptions avec des priorités correctement configurées. Et au cas où le processeur reviendrait à "exécution normale" - passez en "veille". Dans ce cas, il ne serait pas nécessaire de bloquer l'accès à la plupart des ressources (variables et autres). Les applications perdraient un niveau d'abstraction supplémentaire. Et dans ce cas - loin d'être gratuit. Cependant, cette approche nécessite une planification architecturale réfléchie pour chaque projet. Dans les projets, les «concepteurs» - toutes les interruptions ont une priorité et, en fait, sont nécessaires pour «filtrer» les données. Ensuite, placez les restes dans la file d'attente, d'où le flux d'objets de la classe correspondante les récupérera.
Résumé
Dans cet article, j'ai parlé des problèmes de base auxquels vous devez faire face lors de l'utilisation du système d'exploitation dans des projets de microcontrôleurs, et j'ai également examiné les cas courants d'utilisation du système d'exploitation lorsque cela aurait pu être évité sans perdre la lisibilité et la logique du code.