Comment faire TDD sur des appareils embarqués?

17

Je ne suis pas nouveau dans la programmation et j'ai même travaillé avec du C et ASM de bas niveau sur AVR, mais je ne peux vraiment pas me lancer dans un projet C intégré à plus grande échelle.

Étant dégénéré par la philosophie Ruby du TDD / BDD, je ne peux pas comprendre comment les gens écrivent et testent du code comme celui-ci. Je ne dis pas que c'est un mauvais code, je ne comprends tout simplement pas comment cela peut fonctionner.

Je voulais entrer davantage dans la programmation de bas niveau, mais je n'ai vraiment aucune idée de la façon d'aborder cela, car cela ressemble à un état d'esprit complètement différent auquel je suis habitué. Je n'ai aucune difficulté à comprendre l'arithmétique des pointeurs, ni comment fonctionne l'allocation de mémoire, mais quand je vois à quel point le code C / C ++ est complexe par rapport à Ruby, cela semble incroyablement difficile.

Étant donné que je me suis déjà commandé une carte Arduino, j'adorerais en savoir plus sur le bas niveau C et vraiment comprendre comment faire les choses correctement, mais il semble qu'aucune des règles des langages de haut niveau ne s'applique.

Est-il même possible de faire du TDD sur des appareils intégrés ou lors du développement de pilotes ou de choses comme un chargeur de démarrage personnalisé, etc.?

Jakub Arnold
la source
3
Salut Darth, nous ne pouvons vraiment pas vous aider à surmonter votre peur du C, mais la question sur le TDD sur les appareils embarqués est sur le sujet ici: j'ai révisé votre question pour la présenter à la place.

Réponses:

18

Tout d'abord, vous devez savoir qu'essayer de comprendre le code que vous n'avez pas écrit est 5 fois plus difficile que de l'écrire vous-même. Vous pouvez apprendre le C en lisant le code de production, mais cela va prendre beaucoup plus de temps que d'apprendre en faisant.

Étant dégénéré par la philosophie Ruby du TDD / BDD, je ne peux pas comprendre comment les gens écrivent et testent du code comme celui-ci. Je ne dis pas que c'est un mauvais code, je ne comprends tout simplement pas comment cela peut fonctionner.

C'est une compétence; vous vous améliorez. La plupart des programmeurs C ne comprennent pas comment les gens utilisent Ruby, mais cela ne signifie pas qu'ils ne le peuvent pas.

Est-il même possible de faire du TDD sur des appareils intégrés ou lors du développement de pilotes ou de choses comme un chargeur de démarrage personnalisé, etc.?

Eh bien, il y a des livres sur le sujet:

entrez la description de l'image ici Si un bourdon peut le faire, vous le pouvez aussi!

Gardez à l'esprit que l'application de pratiques dans d'autres langues ne fonctionne généralement pas. TDD est cependant assez universel.

Pubby
la source
2
Chaque TDD que j'ai vu pour mes systèmes embarqués n'a trouvé que des erreurs dans les systèmes qui avaient facilement résolu des erreurs que j'aurais trouvées facilement par moi-même. Ils n'auraient jamais trouvé ce dont j'avais besoin d'aide, les interactions en fonction du temps avec d'autres puces et les interactions d'interruption.
Kortuk
3
Cela dépend du type de système sur lequel vous travaillez. J'ai trouvé que l'utilisation de TDD pour tester le logiciel, associée à une bonne abstraction matérielle, me permet en fait de simuler beaucoup plus facilement ces interactions dépendantes du temps. L'autre avantage que les gens regardent souvent est que les tests, étant automatisés, peuvent être exécutés à tout moment et ne nécessitent pas que quelqu'un s'assoie sur l'appareil avec un analyseur logique pour s'assurer que le logiciel fonctionne. TDD m'a sauvé des semaines de débogage dans mon seul projet actuel. Souvent, ce sont les erreurs que nous pensons faciles à repérer qui provoquent des erreurs auxquelles nous ne nous attendrions pas.
Nick Pascucci
De plus, il permet le développement et les tests hors cible.
cp.engr
Puis-je suivre ce livre pour comprendre TDD pour C non intégré? Pour toute programmation en espace utilisateur C?
suréchange
15

Une grande variété de réponses ici ... abordant principalement le problème de diverses manières.

J'écris des logiciels et micrologiciels de bas niveau intégrés depuis plus de 25 ans dans une variété de langues - principalement C (mais avec des détournements vers Ada, Occam2, PL / M et une variété d'assembleurs en cours de route).

Après une longue période de réflexion et d'essais et d'erreurs, je me suis installé dans une méthode qui obtient des résultats assez rapidement et est assez facile de créer des enveloppes de test et des harnais (où ils ajoutent de la valeur!)

La méthode va quelque chose comme ceci:

  1. Écrivez un pilote ou une unité de code d'abstraction matérielle pour chaque périphérique principal que vous souhaitez utiliser. Écrivez-en également un pour initialiser le processeur et tout configurer (cela rend l'environnement convivial). Généralement, sur les petits processeurs intégrés - votre AVR en est un exemple - il peut y avoir 10 à 20 unités de ce type, toutes petites. Il peut s'agir d'unités d'initialisation, de conversion A / N en tampons de mémoire non mis à l'échelle, de sortie au niveau du bit, d'entrée de bouton-poussoir (pas de rebond juste échantillonné), de pilotes de modulation de largeur d'impulsion, de pilotes UART / série simples utilisant des interruptions et de petits tampons d'E / S. Il pourrait y en avoir quelques autres, par exemple des pilotes I2C ou SPI pour EEPROM, EPROM ou d'autres périphériques I2C / SPI.

  2. Pour chacune des unités d'abstraction matérielle (HAL) / pilote, j'écris ensuite un programme de test. Cela repose sur un port série (UART) et un processeur init - donc le premier programme de test utilise uniquement ces 2 unités et ne fait que quelques entrées et sorties de base. Cela me permet de tester que je peux démarrer le processeur et que j'ai un support de débogage de base fonctionnant avec les E / S série. Une fois que cela fonctionne (et alors seulement), je développe les autres programmes de test HAL, en les construisant au-dessus des bonnes unités UART et INIT connues. Donc, je pourrais avoir des programmes de test pour lire les entrées au niveau du bit et les afficher sous une forme agréable (hexadécimale, décimale, peu importe) sur mon terminal de débogage série. Je peux ensuite passer à des choses plus grandes et plus complexes comme les programmes de test EEPROM ou EPROM - je fais la plupart de ces menus pour que je puisse sélectionner un test à exécuter, l'exécuter et voir le résultat. Je ne peux pas le SCRAPTER mais en général je ne le fais pas

  3. Une fois que j'ai tout mon HAL en cours d'exécution, je trouve alors un moyen d'obtenir un tick de minuterie régulier. Ceci est typiquement à un taux quelque part entre 4 et 20 ms. Cela doit être régulier, généré lors d'une interruption. Le roulement / débordement des compteurs est généralement la façon dont cela peut être fait. Le gestionnaire d'interruption INCRÉMENT ensuite un "sémaphore" de taille octet. À ce stade, vous pouvez également jouer avec la gestion de l'alimentation si vous en avez besoin. L'idée du sémaphore est que si sa valeur est> 0, vous devez exécuter la "boucle principale".

  4. L'EXÉCUTIF exécute la boucle principale. Il attend à peu près ce sémaphore pour devenir non-0 (je résume ce détail). À ce stade, vous pouvez jouer avec les compteurs pour compter ces ticks (car vous connaissez le taux de ticks) et vous pouvez donc définir des indicateurs indiquant si le tick exécutif actuel est pour un intervalle de 1 seconde, 1 minute et d'autres intervalles communs que vous pourrait vouloir utiliser. Une fois que l'exécutif sait que le sémaphore est> 0, il exécute un seul passage à travers chaque fonction "mise à jour" des processus "d'application".

  5. Les processus de demande sont effectivement placés côte à côte et exécutés régulièrement par une coche de «mise à jour». Ce n'est qu'une fonction appelée par l'exécutif. Il s'agit en fait d'un multitâche pour les pauvres avec un RTOS local très simple qui repose sur toutes les applications entrant, effectuant un petit travail et sortant. Les applications doivent conserver leurs propres variables d'état et ne peuvent pas effectuer de calculs longs car il n'y a pas de système d'exploitation préventif pour forcer l'équité. Évidemment, le temps d'exécution des applications (cumulativement) doit être inférieur à la période de tick principale.

L'approche ci-dessus est facilement étendue afin que vous puissiez ajouter des éléments comme des piles de communication qui s'exécutent de manière asynchrone et des messages de communication peuvent ensuite être envoyés aux applications (vous ajoutez une nouvelle fonction à chacune qui est le "rx_message_handler" et vous écrivez un répartiteur de messages qui figure vers quelle demande envoyer).

Cette approche fonctionne pour à peu près tous les systèmes de communication que vous souhaitez nommer - elle peut (et a déjà fonctionné) pour de nombreux systèmes propriétaires, des systèmes de communication à normes ouvertes, elle fonctionne même pour les piles TCP / IP.

Il a également l'avantage d'être construit en pièces modulaires avec des interfaces bien définies. Vous pouvez retirer et retirer des pièces à tout moment, substituer différentes pièces. À chaque point le long du chemin, vous pouvez ajouter un faisceau de test ou des gestionnaires qui s'appuient sur les bonnes parties connues de la couche inférieure (les éléments ci-dessous). J'ai constaté qu'environ 30% à 50% d'une conception peuvent bénéficier de l'ajout de tests unitaires spécialement écrits qui sont généralement assez facilement ajoutés.

J'ai pris tout cela un peu plus loin (une idée que j'ai reprise de quelqu'un d'autre qui l'a fait) et j'ai remplacé la couche HAL par un équivalent pour PC. Ainsi, par exemple, vous pouvez utiliser C / C ++ et winforms ou similaire sur un PC et en écrivant le code ATTENTIVEMENT, vous pouvez émuler chaque interface (par exemple EEPROM = un fichier disque lu dans la mémoire du PC), puis exécuter l'intégralité de l'application intégrée sur un PC. La possibilité d'utiliser un environnement de débogage convivial peut économiser beaucoup de temps et d'efforts. Seuls de très gros projets peuvent généralement justifier cette quantité d'efforts.

La description ci-dessus n'est pas unique à la façon dont je fais les choses sur les plates-formes intégrées - j'ai rencontré de nombreuses organisations commerciales qui font de même. La façon dont cela se fait est généralement très différente dans la mise en œuvre, mais les principes sont souvent à peu près les mêmes.

J'espère que ce qui précède donne un peu de saveur ... cette approche fonctionne pour les petits systèmes embarqués qui fonctionnent dans quelques ko avec une gestion agressive de la batterie jusqu'aux monstres de 100K ou plus de lignes sources qui fonctionnent en permanence. Si vous exécutez "embarqué" sur un gros système d'exploitation comme Windows CE, etc., tout ce qui précède est complètement sans importance. Mais ce n'est pas vraiment de la programmation embarquée.

vite_maintenant
la source
2
La plupart des périphériques matériels que vous ne pouvez pas tester via un UART, car très souvent, vous êtes principalement intéressé par les caractéristiques de synchronisation. Si vous souhaitez vérifier un taux d'échantillonnage ADC, un cycle de service PWM, le comportement d'un autre périphérique série (SPI, CAN, etc.), ou simplement le temps d'exécution d'une partie de votre programme, vous ne pouvez pas le faire via un UART. Tout test de micrologiciel intégré sérieux comprend un oscilloscope - vous ne pouvez pas programmer de systèmes intégrés sans un.
1
Oh oui, absolument. J'ai juste oublié de mentionner celui-là. Mais une fois que vous avez votre UART opérationnel, il est très facile de configurer des tests ou des cas de test (ce qui était la question), de stimuler les choses, de permettre aux utilisateurs de saisir, d'obtenir des résultats et d'afficher de manière conviviale. Cela + votre CRO rend la vie très facile.
quick_now
2

Le code qui a une longue histoire de développement incrémentiel et d'optimisations pour plusieurs plates-formes, comme les exemples que vous avez choisis, est généralement plus difficile à lire.

Le problème avec C est qu'il est en fait capable de s'étendre sur des plates-formes sur une vaste gamme de richesse API et de performances matérielles (et leur absence). MacVim a fonctionné de manière réactive sur des machines avec plus de 1000 fois moins de mémoire et de performances de processeur qu'un smartphone classique d'aujourd'hui. Peut votre code Ruby? C'est l'une des raisons pour lesquelles cela pourrait sembler plus simple que les exemples C matures que vous avez choisis.

hotpaw2
la source
2

Je suis dans la position inverse d'avoir passé la plupart des 9 dernières années en tant que programmeur C et récemment travaillé sur certains frontaux Ruby on Rails.

Les choses sur lesquelles je travaille en C sont principalement des systèmes personnalisés de taille moyenne pour contrôler les entrepôts automatisés (coût typique de quelques centaines de milliers de livres, jusqu'à quelques millions). Un exemple de fonctionnalité est une base de données en mémoire personnalisée, s'interfaçant aux machines avec des exigences de temps de réponse courtes et une gestion de niveau supérieur du flux de travail de l'entrepôt.

Je peux dire d'abord que nous ne faisons pas de TDD. J'ai essayé à plusieurs reprises d'introduire des tests unitaires, mais en C, c'est plus de problèmes que cela ne vaut - au moins lors du développement de logiciels personnalisés. Mais je dirais que TDD est beaucoup moins nécessaire en C que Ruby. Principalement, c'est simplement parce que C est compilé, et s'il compile sans avertissements, vous avez déjà effectué une quantité de tests assez similaire aux tests d'échafaudage générés automatiquement par rspec dans Rails. Ruby sans tests unitaires n'est pas réalisable.

Mais ce que je dirais, c'est que le C ne doit pas être aussi dur que certaines personnes le font. Une grande partie de la bibliothèque standard C est un gâchis de noms de fonctions incompréhensibles et de nombreux programmes C suivent cette convention. Je suis heureux de dire que nous ne le faisons pas, et en fait, nous avons beaucoup d'encapsuleurs pour les fonctionnalités de bibliothèque standard (ST_Copy au lieu de strncpy, ST_PatternMatch au lieu de regcomp / regexec, CHARSET_Convert au lieu de iconv_open / iconv / iconv_close et ainsi de suite). Notre code C interne me lit mieux que la plupart des autres trucs que j'ai lus.

Mais quand vous dites que les règles d'autres langues de niveau supérieur ne semblent pas s'appliquer, je ne suis pas d'accord. Beaucoup de bon code C «sent» orienté objet. Vous voyez souvent un modèle d'initialisation d'un descripteur sur une ressource, d'appeler certaines fonctions en passant le descripteur comme argument, puis de libérer la ressource. En effet, les principes de conception de la programmation orientée objet provenaient en grande partie des bonnes choses que les gens faisaient dans les langages procéduraux.

Les moments où C devient vraiment compliqué sont souvent liés à des choses comme les pilotes de périphériques et les noyaux de système d'exploitation qui sont fondamentalement très bas niveau. Lorsque vous écrivez un système de niveau supérieur, vous pouvez également utiliser les fonctionnalités de niveau supérieur de C et éviter la complexité de bas niveau.

Une chose très intéressante que vous voudrez peut-être regarder est le code source C pour Ruby. Dans les documents de l'API Ruby (http://www.ruby-doc.org/core-1.9.3/), vous pouvez cliquer et voir le code source des différentes méthodes. Ce qui est intéressant, c'est que ce code semble assez agréable et élégant - il n'a pas l'air aussi complexe que vous pourriez l'imaginer.

asc99c
la source
" ... vous pouvez également utiliser les fonctionnalités de niveau supérieur de C ... ", comme il en existe? ;-)
alk
Je veux dire un niveau plus élevé que la manipulation de bits et la magie de pointeur à pointeur que vous avez tendance à voir dans le code de type de pilote de périphérique! Et si vous ne vous inquiétez pas de la surcharge de quelques appels de fonction, vous pouvez créer un code C qui semble vraiment de haut niveau.
asc99c
" ... vous pouvez créer un code C qui semble vraiment assez élevé. ", absolument, je suis entièrement d'accord avec cela. Mais bien que " ... les fonctionnalités de niveau supérieur ... " ne soient pas des C, mais dans votre tête, n'est-ce pas?
alk
2

Ce que j'ai fait, c'est séparer le code dépendant de l'appareil du code indépendant de l'appareil, puis tester le code indépendant de l'appareil. Avec une bonne modularité et discipline, vous vous retrouverez avec une base de code pour la plupart bien testée.

Paul Nathan
la source
2

Il n'y a aucune raison pour que vous ne puissiez pas. Le problème est qu'il n'y a peut-être pas de bons frameworks de test unitaire "standard" comme vous en avez dans d'autres types de développement. C'est bon. Cela signifie simplement que vous devez adopter une approche «roll-your-own» pour les tests.

Par exemple, vous devrez peut-être programmer l'instrumentation pour produire de «fausses entrées» pour vos convertisseurs A / N ou peut-être devrez-vous générer un flux de «fausses données» pour que votre appareil intégré puisse y répondre.

Si vous rencontrez une résistance à l'utilisation du mot "TDD" appelez-le "DVT" (test de vérification de conception) qui rendra l'EE plus à l'aise avec l'idée.

Angelo
la source
0

Est-il même possible de faire du TDD sur des appareils intégrés ou lors du développement de pilotes ou de choses comme un chargeur de démarrage personnalisé, etc.?

Il y a quelque temps, j'avais besoin d'écrire un chargeur de démarrage de premier niveau pour un processeur ARM. En fait, il y en a un parmi les gars qui vendent ce CPU. Et nous avons utilisé un schéma où leur chargeur de démarrage démarre notre chargeur de démarrage. Mais cela a été lent, car nous devions flasher deux fichiers dans NOR flash au lieu d'un, nous avons dû construire la taille de notre chargeur de démarrage dans le premier chargeur de démarrage, et le reconstruire à chaque fois que nous avons changé notre chargeur de démarrage et ainsi de suite.

J'ai donc décidé d'intégrer les fonctions de leur chargeur de démarrage dans les nôtres. Parce que c'était du code commercial, je devais m'assurer que tout fonctionnait comme prévu. J'ai donc modifié QEMU pour émuler les blocs IP de ce CPU (pas tous, seulement ceux qui touchent le chargeur de démarrage), et ajouter du code à QEMU pour "printf" tout lire / écrire dans des registres qui contrôlent des choses comme le PLL, UART, le contrôleur SRAM et bientôt. Ensuite, j'ai mis à jour notre chargeur de démarrage pour prendre en charge ce processeur, et après avoir comparé la sortie qui donne notre chargeur de démarrage et leur émulateur, cela m'aide à attraper plusieurs bogues. Il a été écrit en partie dans l'assembleur ARM, en partie en C. Aussi après que QEMU modifié m'a aidé à attraper un bug, que je ne pouvais pas attraper en utilisant JTAG et un vrai processeur ARM.

Donc, même avec C et assembleur, vous pouvez utiliser des tests.

Evgeniy
la source
-2

Oui, il est possible de faire TDD sur un logiciel embarqué. Les personnes qui disent que ce n'est pas possible, non pertinent ou non applicable ne sont pas correctes. Il y a une valeur sérieuse à tirer du TDD dans l'embarqué comme avec n'importe quel logiciel.

Cependant, la meilleure façon de le faire n'est pas d'exécuter vos tests sur la cible mais d'abstraire vos dépendances matérielles et de les compiler et les exécuter sur votre PC hôte.

Lorsque vous effectuez TDD, vous allez créer et exécuter de nombreux tests. Vous avez besoin d'un logiciel pour vous y aider. Vous voulez un framework de test qui le rend rapide et facile à faire, avec la découverte automatique de test et la génération de simulation.

La meilleure option pour C en ce moment est Ceedling. Voici un article à propos de ce que j'ai écrit à ce sujet:

http://www.electronvector.com/blog/try-embedded-test-driven-development-right-now-with-ceedling

Et il est construit en Ruby! Vous n'avez cependant pas besoin de connaître de Ruby pour l'utiliser.

cherno
la source
les réponses devraient être autonomes. Forcer les lecteurs à accéder à des ressources externes pour découvrir la substance est mal vu à Stack Exchange ("lire l'article ou consulter Ceedling"). Pensez à éditer pour l'adapter aux normes de qualité du site
gnat
Ceedling dispose-t-il de mécanismes pour prendre en charge les événements asynchrones? L'un des aspects les plus difficiles des applications embarquées en temps réel est qu'elles traitent la réception des entrées de systèmes très complexes qui sont eux-mêmes difficiles à modéliser ...
Jay Elston
@Jay Il n'a rien de spécifiquement pour soutenir cela. Cependant, j'ai réussi à tester ce genre de chose avec la simulation et en mettant en place une architecture pour la prendre en charge. Par exemple, j'ai récemment travaillé sur un projet où des événements déclenchés par interruption étaient placés dans une file d'attente puis consommés dans une machine d'état "gestionnaire d'événements". Il s'agissait essentiellement d'une fonction qui était appelée chaque fois qu'un événement se produisait. Lors du test de cette fonction, je pouvais simuler l'appel de fonction qui a extrait des événements de la file d'attente, et ainsi simuler tout événement survenant dans le système. L'essai de conduite aide aussi ici.
cherno