Serveur d'impression

Couverture de code embarquée incroyablement astucieuse – EEJournal – Bien choisir son serveur d impression

Le 5 mai 2020 - 24 minutes de lecture

Je viens d'entendre parler d'un nouvel outil de couverture de code de Microchip Technology qui vise les conceptions intégrées, qui est rapide et facile à utiliser, et qui fait des choses qu'aucun autre outil de couverture de code n'a fait auparavant. Juste l'écriture qui m'a fait un flashback sur le spiel d'ouverture de Star Trek: la série originale, où le narrateur a hésité sur "Aller audacieusement derrière l'au-delà, derrière lequel aucun homme n'a hardiment passé derrière, au-delà, avant", ou des mots à cet effet. Mais nous nous éloignons…

Oh cher!

Je l'ai déjà dit et je le répéterai – en fait, je dirai à tous ceux qui n'arrivent pas à s'écarter assez rapidement – je suis ingénieur en conception de matériel par métier et je suis fier de celui-ci. Quand j’étais étudiant à l’université, une de mes rêveries était d’être à une fête après avoir obtenu mon diplôme, pour que quelqu'un me demande: «Que fais-tu?» et pour moi de répondre d'une voix grave de James Earl Jones: "Je suis ingénieur! ” Mais, encore une fois, nous nous éloignons…

Au fil des ans, j'ai travaillé avec une variété de langages d'assemblage, ainsi que certains langages supérieurs comme BASIC, Forth, FORTRAN, Pascal et Python, et je capture actuellement la plupart de mon code de projet de loisir en C avec un soupçon de reniflement d'un dribble de C ++. Dans l'ensemble, cependant, tout ce que je sais sur le côté logiciel de la clôture a été durement combattu, et tout code que je crée doit être traité avec suspicion («coupable jusqu'à preuve du contraire» serait une bonne règle).

Le truc c'est que, côté matériel, je suis au fait avec la grande variété d'outils et de technologies de test et de vérification que nous utilisons, y compris la simulation, l'émulation et la vérification formelle. Cependant, je crains d'être beaucoup moins familier avec la façon dont ces choses sont abordées dans le domaine logiciel.

Dans le cas de mes propres programmes, qui sont principalement créés en C / C ++ en utilisant l'IDE Arduino, un cycle de développement typique implique ma capture de quelques lignes de code, la compilation du code (qui lui-même nécessite quelques rotations pour éliminer les erreurs évidentes J'ai inséré juste pour des rires et des sourires), en exécutant le code et en regardant les résultats (ou leur absence) dans la perplexité.

La plupart du temps, rien ne se passe ou ne semble se produire. À l'occasion, juste pour briser la monotonie, quelque chose de complètement inattendu se produit. Si je suis chanceux, cela n'implique rien qui commence à fondre ou à fumer, mais je ne placerais pas d'argent dans ce cas.

C'est le point où nous entrons dans la phase deux (ou phase deux, comme le voudraient les Français). Cela implique que j'insère des instructions Serial.print (), en les dispersant dans mon code comme des confettis. L'une des choses vraiment frustrantes est lorsque l'acte d'insertion d'une instruction d'impression fait que le code commence à fonctionner, et l'acte de suppression de cette instruction entraîne le retour du code à son état de non-fonctionnement. «Oh mon Dieu», je pourrais me dire (ou des mots à cet effet).

Quand vous arrivez à une fourchette dans le Code…

Un autre problème que je rencontre souvent est que je lance mon programme et me tape dans le dos, mais je n'ai pas testé tous les modèles d'utilisation possibles. Je découvre généralement ce fait lorsque je démontre fièrement ma création à un ami, au cours de laquelle nous revenons à (a) rien ne se passe (ennuyeux) ou (b) quelque chose d'inattendu (déroutant) ou (c) quelque chose d'horrible (excitant, mais pas dans le bon sens).

Le problème ici est que mes tests d'origine n'avaient pas réellement vérifié tout le code. Dans certains cas, il se peut que je n’ait exercé que ce qui se passerait si la condition associée à une instruction if () renvoyait true ou false, mais pas les deux. Dans d'autres cas, j'ai peut-être négligé d'exercer des fonctions entières.

Penser aux branches dans des déclarations conditionnelles me rappelle la citation qui est communément attribuée au légendaire Yogi Berra: «Lorsque vous arrivez à une fourche sur la route, prenez-la!» Si seulement il était possible d'évaluer simultanément les deux branches du code. Mais, vous l'avez deviné, nous nous éloignons…

Je me souviens il y a environ 40 ans, lorsque des amis développeurs de logiciels utilisaient des instructions printf () pour tenter de s'assurer que chaque fonction avait été exécutée et que chaque branche conditionnelle avait été exercée, mais à mesure que le code grossit de plus en plus, cette technique devient plus difficile et plus difficile à exécuter à la main. En outre, c'est une chose d'utiliser cette technique sur un ordinateur de classe poste de travail avec beaucoup de mémoire et un lecteur de disque; c'est une proposition très différente lorsque vous travaillez avec une unité de microcontrôleur intégrée (MCU) avec une mémoire limitée et d'autres ressources.

Tout cela a conduit à la création d'outils de couverture de code à l'usage des développeurs de logiciels. Triste à raconter, jusqu'à présent, je n'avais qu'une idée duveteuse sur la façon dont ces outils effectuent leur magie. Ma compréhension de haut niveau était que, lorsque vous exécutez votre application (je suppose que sous le contrôle d'un script de test afin d'assurer la répétabilité), l'outil de couverture de code rapporterait la quantité de votre code qui avait été testée et quelle proportion avait été laissé intact.

Bien sûr, sachant que x% de votre code a été exercé est d'une utilité limitée – vous devez également savoir quelles fonctions ont été exécutées et quelles branches conditionnelles ont été testées, ou non, selon le cas.

Idéalement, votre environnement de développement intégré (IDE) aura un éditeur contextuel qui peut afficher la couverture de code de manière appropriée, indiquant quelles parties du code ont été testées ou non, et capable de travailler au niveau de la fonction ou de la ligne de code source niveau.

En fait, vous avez vraiment besoin d'une résolution plus fine que le niveau de ligne de code source, car chaque ligne en C peut potentiellement contenir plusieurs instructions, et ne me fait même pas commencer à parler de l'opérateur conditionnel, ou ternaire,?: En C / C ++ .

En fait, si la vérité est connue, lorsque nous parlons d'applications intégrées, il peut même être souhaitable de pouvoir visualiser les choses au niveau de l'instruction d'assemblage d'abstraction, car chaque instruction de haut niveau se développera généralement en plusieurs instructions au niveau de l'assemblage.

Ne présumez pas

Il y a quelques jours, en écrivant ces mots, je discutais avec David Otten, responsable du développement d'outils, Technologie de micropuce. D'après ce que vous avez lu jusqu'à présent, il n'est peut-être pas surprenant d'apprendre que le sujet de notre conversation était la couverture du code. David me parlait du nouveau Microchip Couverture du code MPLAB outil, qu'il croit être la plus grande chose depuis l'invention du pain tranché.

Dans le cadre de notre conversation, David a mentionné qu'un nombre surprenant de développeurs de logiciels embarqués professionnels n'utilisent pas du tout la couverture de code. Pour être honnête, j'ai trouvé cela un peu difficile à croire – si vous me l'aviez demandé il y a quelques jours, j'aurais supposé que la grande majorité des développeurs de logiciels embarqués utilisaient de tels outils – j'ai donc envoyé quelques e-mails à certains développeurs Je sais leur demander quels outils ils utilisent.

Vous ne pouvez qu'imaginer ma surprise de découvrir que le seul de mes amis que j'ai interrogé qui utilise réellement la couverture de code est Jacob Beningo, fondateur du Beningo Embedded Group, qui dit qu'il utilise Segger Ozone. Maintenant, je ne veux pas mettre des mots dans la bouche de Jacob, mais nous avons bavardé au téléphone, et je crois comprendre que l'utilisation de cet outil vous oblige à instrumenter le code et à lui permettre de diffuser les données de couverture de code à partir d'un port de test pendant l'exécution temps. Bien que Jacob n'ait eu que des choses positives à dire sur l'outil, je dois croire que son utilisation a un impact sur les performances du système.

Un autre de mes amis est Jonny Doin, fondateur et PDG de GridVortex Systems au Brésil. En plus des systèmes critiques et critiques pour la sécurité, tels que ceux trouvés dans les centrales nucléaires, GridVortex se concentre sur des projets d'infrastructure urbaine à grande échelle impliquant des systèmes intelligents hautement évolutifs.

En réponse à mon e-mail d'origine, Jonny a déclaré qu'ils utilisaient auparavant la couverture de code chez GridVortex. Plus récemment, cependant, ils sont passés à un examen approfondi par les pairs et à des tests unitaires. Après avoir été interrogé sur quel outil ils ont utilisé pour effectuer des tests unitaires; Jonny a répondu comme suit:

Nous n'utilisons pas d'outil externe. Permettez-moi de vous présenter brièvement comment nous effectuons les tests unitaires:

  • Notre framework est écrit en C, en utilisant la même approche système que vous trouveriez dans les premiers Unix, mais avec une implémentation radicalement différente.
  • Les systèmes que nous concevons sont des systèmes durs en temps réel, généralement avec un chemin de signal DSP critique, pour les systèmes industriels à haute disponibilité.
  • Nous adoptons une conception orientée vers les aspects et effectuons explicitement une évaluation continue des préoccupations transversales, de la séparation des aspects, de l'équilibrage des classes, de la limitation des connaissances et de la réduction de la complexité locale.
  • L'architecture du système est fortement modélisée sur le chemin de données / chemin de contrôle, avec une orientation de flux. Chaque flux de données est un flux, et le code est généralement constitué de petits threads avec une machine à états finis qui se trouve entre les FIFO de données.
  • Nos niveaux inférieurs sont très similaires aux processus VHDL, avec les protocoles d'implémentation FSM et logique et la synchronisation inter-thread. Les couches de niveau supérieur, comme la classe d'analyseur JSON, sont implémentées en tant que FSM avec un temps de blocage système minimal.
  • Nos niveaux supérieurs sont fortement modélisés sur l'abstraction MVC, avec des modèles, des contrôleurs, des ViewControllers et des vues. Parfois, l'abstraction MVC englobe les threads exécutant des modèles et des contrôleurs côté serveur intégré et Javascript exécutant ViewControllers dans les navigateurs.
  • Notre noyau multitâche implémente un modèle de thread multimode, où tout processus peut être planifié comme un programme à tour de rôle, ou comme un programme dur préemptif déclenché par le temps, et plusieurs interruptions de chemin de signal.
  • En règle générale, un thread se bloque pendant quelques microsecondes, avec un multitâche à très haute fréquence.
  • Le code est très compartimenté, avec des espaces de noms, des classes d'interface et des classes traditionnelles. Nous n'implémentons PAS l'héritage multiple ou RTTI, mais nous utilisons généreusement des littéraux composés anonymes et des interfaces polymorphes.
  • Le système est absolument asynchrone; c'est-à-dire que chaque fonction est thread-safe et toutes les interfaces prennent en charge les rappels d'achèvement. Ainsi, nous avons des versions asynchrones de toutes les fonctions NetBSD, tous les appels TCP et le fileio (fread / fwrite) ont tous des interfaces asynchrones. Cela permet de bloquer zéro appels sur l'ensemble du système.
  • L'un des composants de niveau inférieur du système est une classe de serveur de terminaux, qui prend en charge plusieurs sessions de terminal d'une CLI (interface de ligne de commande).

Dans cette architecture, chaque classe de domaine ou couche de code dans une chaîne d'API donnée a une très petite zone exposée. C'est là que les tests unitaires entrent dans le flux.

Chaque sous-système, implémentant généralement une classe d'interface, possède un module de commande CLI qui exerce tous les aspects de l'interface, avec une ligne de commande exposée brute pour tester complètement l'interface.

Certaines classes ont des exigences normatives pour que la logique de test en ligne fasse partie de l'exécution normale des objets. C'est le cas de toutes les primitives cryptographiques et de certaines constructions cryptographiques. Par exemple, les DRBG et les pools d'entropie sont tenus de mettre en œuvre des tests statistiques continus en ligne pour mesurer et garantir une distribution d'entropie élevée, certains protocoles de bus de terrain doivent avoir des observateurs sémantiques et certaines fonctions de contrôle doivent avoir des vérificateurs d'intégrité de boucle de contrôle. Les primitives cryptographiques sont nécessaires pour disposer de suites de tests accessibles à l'utilisateur afin de tester la conformité à tous les vecteurs de test approuvés pour cette primitive.

Cependant, pour garantir la conformité aux objectifs de conception, chaque interface ou classe doit avoir une interface de commande de test unitaire, généralement composée d'un ensemble manuel de commandes et de modèles de test pilotés par commande, pour tester les cas d'angle et les aspects. Le système dispose d'un ensemble d'outils système qui effectuent une introspection de pile, une lecture de la mémoire et des observateurs de synchronisation, pour aider à caractériser tout objet en cours d'exécution. Grâce aux modules de commande Unit Test, vous pouvez isoler et tester complètement une classe ou un sous-système complet, avant de l'intégrer dans un flux fonctionnel sur le système cible.

Une partie du cadre de test unitaire consiste en des mesures matérielles. Nous avons au moins 16 lignes parallèles à grande vitesse de GPIO qui sont raccordées à un analyseur logique externe, et nous avons des DAC connectés à un oscilloscope. Nous utilisons un Tektronix MSO (16 numériques + 4 analogiques) comme outil de test d'unité logicielle.

Le code testé est instrumenté avec des bits de débogage GPIO activés dans des lieux d'intérêt (par exemple, les états de la machine d'état) et peut être observé avec 1 ns de précision de synchronisation. La perte d'insertion de ces GPIO est vraiment négligeable (~ 4 à ~ 8 ns). De ces 16 GPIO, nous consacrons 8 bits au bus Process ID (PID). Le noyau place le PID de tout processus en cours d'exécution sur le bus PID, permettant une trace détaillée en temps réel du système en cours d'exécution. Les DAC analogiques peuvent être raccordés à n'importe quel nœud de chemin de signal pour observer le signal avant et après un filtre FIR, en temps réel, pour effectuer un test unitaire de la fonction de filtre, par exemple.

Ces lignes GPIO font partie du système final et sont utilisées pour vérifier le système sur des tâches de contrôle réelles.

Bien que notre classe Kernel soit vraiment minimale (le commutateur de tâches prend 13 instructions), le noyau capture la télémétrie des threads et peut afficher en temps réel les temps de processus de tous les threads en cours d'exécution.

Ce n'est qu'après que le sous-système a réussi les tests unitaires, fonctionnant sur du matériel réel, qu'il est fusionné sur la branche git traditionnelle.

Cette approche est extrêmement efficace, et nous l'avons utilisée pour tout écrire et tester des chemins de signaux ADC en temps réel durs à du code réseau complexe, en plus de la surveillance de bas niveau de Wireshark. L'identification des erreurs d'interface et des journaux de débogage, des charges système importantes et la détection des défauts de conception sont facilitées par les outils de commande de test unitaire.

Cela contraste fortement avec les outils de test unitaires commerciaux, qui implémentent généralement une couche d'instrumentation sur les interfaces des fonctions testées pour capturer, consigner et injecter le flux de données de paramètres à travers le système en cours d'exécution. Le problème avec ces outils est qu'ils imposent généralement une charge de traitement sur chaque interface de fonction instrumentée, et cela interfère avec le temps de traitement normal des fonctions. Dans un système en temps réel dur, cette perte d'insertion de temps de traitement peut être pertinente, et elle change le comportement dynamique des boucles de contrôle, par exemple.

Une autre mise en garde de l'instrumentation de code de test unitaire traditionnel est que le système de test se comporte différemment du système de terrain. Certains défauts de conception très durs peuvent être liés au timing et lorsque vous instrumentez le système, vous «résolvez» le problème.

Nous implémentons maintenant la prise en charge ARM CoreSight dans le système pour les circuits sur puce afin d'aider à détecter les erreurs et d'effectuer des mesures non invasives dans le cadre du système en cours d'exécution pour permettre les tests unitaires et la surveillance du code.

Ainsi, notre approche des tests unitaires consiste à concevoir les structures de test dans le cadre du système final, de manière à ce que l'observation et la mesure des unités de code soient une fonction de base standard du système.

Phew! Je n'aurais pas pu mieux le dire moi-même. Je dirais que je regrette d'avoir demandé, sauf que j'ai trouvé l'explication de Jonny fascinante.

La simple lecture de ce qui précède a mon pauvre vieille cabane bourdonnante de pensées, les principales pensées étant (a) je suis content de ne pas être développeur de logiciels et (b) je suis content de ne pas avoir à implémenter le test unitaire GridVortex procédure dans mon propre code. Cela dit, si je devais confier ma vie à un système embarqué, je serais plus heureux s'il s'agissait d'un système conçu et mis en œuvre par l'équipe de Jonny en utilisant le processus décrit ci-dessus. Mais, encore une fois, nous nous éloignons…

Présentation de la couverture de code MPLAB

Avant de commencer, rappelons-nous que, si un développeur de logiciels intégrés utilise une couverture de code, il s'agit très probablement d'un outil qui a vu le jour dans le monde des applications de haut niveau, c'est-à-dire des applications logicielles conçues pour fonctionner sur des ordinateurs personnels, des postes de travail, et les serveurs – et qui a ensuite été "évolué" (chausse-pied) pour servir le monde embarqué.

Si vous ne faites pas attention, assurer une couverture de test élevée des logiciels intégrés à l'aide des outils de couverture de code traditionnels nécessite souvent une certaine quantité de modifications matérielles, des logiciels coûteux et des efforts importants pour rechercher des informations pertinentes dans des fichiers de données volumineux. Pire encore, mesurer la couverture du code dans un système embarqué nécessite souvent l'utilisation d'un système de test sensiblement différent de la conception d'origine.

Ces problèmes ne sont exacerbés que par le fait que la couverture du code se produit souvent à la fin du processus de développement, date à laquelle le matériel et les logiciels ont été optimisés en fonction des coûts et le système répond étroitement aux exigences spécifiées. Le démarrage de l'analyse de la couverture du code à ce stade nécessite souvent d'ajouter des instructions «printf» ou un certain type de code de transmission série dans chaque fonction afin que le microcontrôleur puisse dire «Program Counter = X». Une fois ces segments de code ajoutés tout au long du projet, les besoins en ressources de code dépassent souvent les limites du contrôleur à coût optimisé. Une alternative consiste à remplacer le microcontrôleur par une variante de mémoire plus grande (avec un peu de chance, elle est disponible dans le même package).

L'appareil a également besoin d'un canal de communication pour produire des informations, comme un UART. Il est regrettable que toutes les chaînes disponibles soient déjà utilisées par l'application, car cela nécessitera un jeu de jambes astucieux. En outre, étant donné que la vitesse d'exécution a probablement été enlisée avec le code supplémentaire, en particulier dans le cas d'une application en temps réel, il peut être nécessaire d'augmenter la vitesse d'horloge du MCU pour améliorer les performances du système.

Ce que je vais dire ensuite ressemble presque à un anticlimax, mais je vais le dire quand même. Aucun outil ne connaît mieux votre code que votre compilateur. Si tu as Couverture du code MPLAB, tout ce que vous avez à faire est d'activer la fonction «Couverture de code» dans le compilateur MPLAB avant de compiler votre code. Les compilateurs de Microchip peuvent déterminer le nombre optimal de points d’instrumentation nécessaires et où les placer pour minimiser l’impact sur la taille et les performances du code (l’analyse montre que l’outil MPLAB Code Coverage utilise seulement de la moitié au tiers des points d’instrumentation des offres concurrentes).

Mais cela s'améliore encore, car l'impact de la couverture de code MPLAB est généralement inférieur à 1% de la mémoire du programme, ce qui signifie que vous pouvez effectuer votre analyse de couverture de code en utilisant le même matériel que le système réel (si votre application utilise déjà 99 % de l'espace de programme disponible, alors vous avez d'autres problèmes, y compris le fait que quelqu'un est sur le point de coller sa tête autour de la porte en disant: «Nous devons ajouter une fonctionnalité supplémentaire – ne vous inquiétez pas, ce n'est qu'une petite, mais J'ai promis au patron que tu le ferais travailler demain). »

Les choses peuvent-elles s'améliorer? Pourquoi, oui, ils le peuvent, car toutes les données de couverture de code sont stockées sur puce à l'aide d'une quantité infime de mémoire de données, ce qui signifie (a) vous n'avez pas à bloquer l'un de vos canaux de communication pendant le test et (b) l'impact sur les performances de l'application est minime.

Une fois le test d'application effectué, vous pouvez récupérer les données de couverture de code de la puce et les afficher dans MPLAB X IDE, qui est étroitement intégré à l'outil de couverture de code MPLAB. Chaque ligne de code source et chaque instruction d'assemblage est marquée comme entièrement ou partiellement exécutée ou pas exécutée du tout. De plus, vous pouvez facilement basculer entre les vues pour voir la couverture du code au niveau des instructions d'assemblage, au niveau de la ligne source, au niveau de la fonction, au niveau du fichier et au niveau du projet.

Oui, bien sûr, vous pouvez générer des rapports personnalisés aux formats HTML et CSV avec un contenu sélectionnable pour répondre à vos besoins spécifiques de certification de qualité (j'ai pensé que cela irait de soi, mais j'ai décidé de le dire quand même).

Comme David l'a noté dans son Mesurer la couverture de code dans un système embarqué blog: «MPLAB Code Coverage est l'outil de couverture de code le plus simple du marché, il suffit de l'activer dans MPLAB X IDE, puis de créer, programmer, exercer et lire votre appareil.» Une chose que David a omis de mentionner est que la couverture de code MPLAB fonctionne avec tout des microcontrôleurs Microchip, y compris les versions 8 bits.

Et nous arrivons donc au «vif du sujet» sous la forme de prix, qui peuvent être résumés comme suit: MPLAB X IDE est toujours gratuit; Les compilateurs MPLAB XC ont des versions pro et gratuites; et la couverture de code MPLAB – qui se présente sous la forme d'une licence de poste de travail pour 799 $ sans frais de maintenance – fonctionne avec les versions gratuite et pro des compilateurs MPLAB XC.

Rappelant que je crée la plupart de mes projets de loisirs à l'aide de processeurs qui peuvent être programmés à l'aide de l'IDE Arduino – y compris Arduinos (bien sûr), les appareils Teensy de PJRC.com et l'impressionnant ShieldBuddy de HITEX.com (voir aussi Tout le monde a besoin d'un bouclier) – Je suis ravi de signaler que David vient de m'informer d'une fonctionnalité intéressante MPLAB X IDE dont je n'étais pas au courant auparavant, à savoir qu'il peut importer des projets Arduino, ce qui signifie que la couverture de code MPLAB peut également être utilisée avec eux.

À la fin de la journée, je me pose une question cruciale: "Y a-t-il une chance que nous puissions persuader Microchip de créer un outil de couverture de code Arduino IDE équivalent?" Peut-être quelque chose qui pourrait être téléchargé gratuitement avec des capacités limitées de telle sorte qu'il pourrait signaler la couverture uniquement au niveau de la fonction et de la ligne source. Tout ce que je peux dire, c'est que si une telle journée frabjeuse venait à se produire, je connais beaucoup de gens qui danseraient dans les rues, et votre humble narrateur se retrouverait à la tête du défilé.

Commentaires

Laisser un commentaire

Votre commentaire sera révisé par les administrateurs si besoin.