web-dev-qa-db-fra.com

Comment puis-je faire TDD sur des appareils incorporés?

Je ne suis pas nouveau pour la programmation et j'ai même travaillé avec des basses de niveau C et ASM sur AVR, mais je ne peux vraiment pas avoir la tête autour d'un projet C intégré à plus grande échelle.

Être dégénéré par la philosophie de Ruby de TDD/BDD, je suis incapable de comprendre comment les gens écrivent et testent le 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 obtenir plus dans une programmation de bas niveau, mais je n'ai vraiment aucune idée de la façon de m'approcher, car cela ressemble à un état d'esprit complètement différent que je suis habitué. Je n'ai pas de difficulté à comprendre les arithmétiques du pointeur, ni comment allouer la mémoire fonctionne, mais lorsque je vois à quel point le code C/C++ complexe a l'air comparé à Ruby, cela semble incroyablement difficile.

Depuis que je me suis déjà commandé un conseil d'Arduino, j'aimerais avoir plus de bas de niveau C et bien comprendre comment faire les choses correctement, mais il semble que rien des règles des langues de haut niveau ne s'appliquent.

Est-il même possible de faire TDD sur des périphériques incorporés ou lors du développement de pilotes ou de choses telles que le chargeur de démarrage personnalisé, etc.?

17
Jakub Arnold

Tout d'abord, vous devez savoir que tenter 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 c en lisant le code de production, mais cela va prendre beaucoup plus de temps que d'apprendre en faisant.

Être dégénéré par la philosophie de Ruby de TDD/BDD, je suis incapable de comprendre comment les gens écrivent et testent le 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 en êtes meilleur. La plupart des programmeurs C comprennent pas comment les gens utilisent Ruby, mais cela ne signifie pas qu'ils ne peuvent pas.

Est-il même possible de faire TDD sur des périphériques incorporés ou lors du développement de pilotes ou de choses telles que le chargeur de démarrage personnalisé, etc.?

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

enter image description hereSi un bourdon peut le faire, vous pouvez aussi!

N'oubliez pas que l'application de pratiques d'autres langues ne fonctionne généralement pas. TDD est plutôt universel cependant.

18
Pubby

Une grande variété de réponses ici ... surtout en abordant la question de différentes manières.

J'ai écrit des logiciels et un micrologiciel embarqué de bas niveau et un micrologiciel depuis plus de 25 ans dans une variété de langues - principalement C (mais avec des détournements dans ADA, Occam2, PL/M et une variété d'assembleurs en cours de route).

Après une longue période de pensée 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 à créer des emballages et des harnais de test (où ils ajoutent de la valeur!)

La méthode va quelque chose comme ceci:

  1. Écrivez une unité de code d'abstraction de pilote ou de matériel pour chaque périphérique majeur que vous souhaitez utiliser. Écrivez également un pour initialiser le processeur et obtenez-vous tout mis en place (cela rend l'environnement amical). Généralement sur de petits processeurs embarqués - votre AVR étant un exemple - il pourrait y avoir 10 à 20 unités de ce type. Ceux-ci pourraient être des unités pour l'initialisation, la conversion A/D sur des tampons de mémoire non calé, une sortie bitwise, une entrée de bouton-poussoir (sans débutage uniquement échantillonnées), des pilotes de modulation de largeur d'impulsion, UART/simples pilotes série Les interruptions d'utilisation et les petites/O tampons. Il peut y avoir quelques-uns de plus - par exemple I2C ou SPI Pilotes pour EEPROM, EPROM ou d'autres périphériques I2C/SPI.

  2. Pour chacune des unités d'abstraction matérielle (HAL)/pilotes, j'écris ensuite un programme de test. Ceci s'appuie sur un port série (UART) et le processeur init - afin que le premier programme de test utilise uniquement ces 2 unités et effectue simplement une entrée et une sortie de base. Cela me permet de tester que je peux démarrer le processeur et que j'ai un support de base de débogage de base en série de l'E/S. Une fois que cela fonctionne (et seulement puis), je développe ensuite les autres programmes de test HAL, construisez-les sur le bien connu UART et INIT. Donc, je pourrais avoir des programmes de test pour lire les entrées bitwises et les afficher sous une forme agréable (hexagonale, 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 pilotés afin que je puisse sélectionner un test à exécuter, exécuter et voir le résultat. Je ne peux pas le scripter, mais généralement, je n'ai pas besoin de - le menu conduit est assez bon.

  3. Une fois que j'ai toutes mes halines en cours d'exécution, je trouve ensuite un moyen d'obtenir une tickette régulière. Ceci est typiquement à une vitesse de 4 à 20 ms. Cela doit être régulier, généré dans une interruption. Le renversement/débordement des compteurs est généralement comment cela peut être fait. Le gestionnaire d'interruptions augmente alors une taille d'octet "sémaphore". À 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 dirige la boucle principale. Il suffit à peu près que le sémaphore devienne non-0 (le je résumé cette détail). À ce stade, vous pouvez jouer avec des compteurs pour compter ces tiques (CO Vous connaissez le taux de tick) et vous pouvez ainsi définir des indicateurs indiquant si la coche exécutive actuelle est pour un intervalle de 1 seconde, 1 minute et d'autres intervalles courants pourrait vouloir utiliser. Une fois que l'exécutif sait que le sémaphore est> 0, il exécute une seule passe à travers toutes les fonctions de "mise à jour" de l'application ".

  5. Les processus d'application s'assoient efficacement les uns avec les autres et se déroulent régulièrement par une tique "Mise à jour". Ceci est juste une fonction appelée par l'exécutif. C'est effectivement pauvre-homme multi-tâches avec une très simple cultivée maison RTOS qui repose sur toutes les applications entrant, faisant un peu de travail et sortant. Les applications doivent conserver leurs propres variables d'état et ne peuvent pas faire de longs calculs car il n'y a pas de système d'exploitation préventif pour forcer l'équité. Évidemment, le temps de fonctionnement des applications (cumulativement) devrait être inférieur à la période principale.

L'approche ci-dessus est facilement étendue afin que vous puissiez avoir des choses comme des piles de communication ajoutées qui fonctionnent de manière asynchrone et des messages de communication peuvent ensuite être livrées aux applications (vous ajoutez une nouvelle fonction à chacun qui correspond à "RX_Message_Handler" et vous écrivez un répartiteur de message qui sur quelle application d'expédition à).

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

Il présente également l'avantage d'être construit en pièces modulaires avec des interfaces bien définies. Vous pouvez tirer des morceaux à tout moment, remplacez différentes pièces. À chaque point de la manière dont vous pouvez ajouter du faisceau de test ou des gestionnaires qui construisent sur les bonnes pièces de couche inférieures connues (les trucs ci-dessous). J'ai constaté qu'environ 30% à 50% d'une conception pouvant profiter d'ajouter des tests d'unités spécialement écrits qui sont généralement facilement faciles à ajouter.

J'ai suivi tout cela une étape supplémentaire (une idée que j'ai nickée de quelqu'un d'autre qui l'a fait) et remplacez la couche HAL avec un équivalent pour PC. Ainsi, par exemple, vous pouvez utiliser C/C++ et Winforms ou similaire sur un PC et en écrivant soigneusement le code, vous pouvez imiter chaque interface (par exemple, EEPROM = un fichier de disque lu dans la mémoire PC), puis exécutez l'ensemble de l'application embarquée complète sur un PC. La capacité d'utiliser un environnement de débogage amical peut économiser une grande quantité de temps et d'efforts. Seuls les grands projets peuvent généralement justifier ce montant d'effort.

La description ci-dessus est quelque chose qui n'est pas unique à la manière dont je fais des choses sur des plates-formes intégrées - j'ai rencontré de nombreuses organisations commerciales qui font semblables. La façon dont elle est faite est généralement très différente de 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 une saveur ... Cette approche fonctionne pour de petits systèmes embarqués qui se déroulent dans quelques ko avec une gestion agressive de la batterie à des monstres de 100k ou plus de lignes sources fonctionnant en permanence. Si vous exécutez "Embedded" sur un gros système d'exploitation comme Windows CE ou ainsi de suite, tout ce qui précède est complètement immatériel. Mais ce n'est pas une véritable programmation intégrée, de toute façon.

16
quickly_now

Ce que j'ai fait est séparer le code dépendant du périphérique à partir du code indépendant des périphériques, puis testez le code indépendant du périphérique. Avec une bonne modularité et une bonne discipline, vous ferez la liquidation avec un principalement Code bien testé.

3
Paul Nathan

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

La chose à propos de C est qu'il est en fait capable de couvrir des plates-formes sur une gamme massive de richesse de l'API et de performances matérielles (et de son absence). MacVIM a répondu de manière réactive sur des machines avec plus de 1 000 fois moins de performances de mémoire et de processeur qu'un smartphone typique aujourd'hui. Pouvez-vous Ruby====? C'est l'une des raisons pour lesquelles il pourrait sembler plus simple que les exemples de mature C que vous avez choisis.

2
hotpaw2

Je suis en position inverse d'avoir passé la plupart des 9 dernières années en tant que programmeur C et travaillant récemment sur certains Ruby sur Rails Front-sets .

Les trucs que je travaille dans C est 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 livres, jusqu'à quelques millions). Exemple de fonctionnalité est une base de données personnalisée dans la mémoire, interfaçant vers des machines avec des exigences de temps de réponse courtes et une gestion de niveau supérieur du flux de travail d'entrepôt.

Je peux tout d'abord dire, nous ne faisons aucun TDD. J'ai essayé à plusieurs reprises introduisant des tests d'unité, mais en C, il s'agit de plus de problèmes que ce qu'il vaut la peine - au moins lors du développement de logiciels personnalisés. Mais je dirais que TDD est beaucoup moins nécessaire en C que de rubis. Principalement, c'est-à-dire parce que C est compilé, et s'il compile sans avertissements, vous avez déjà effectué une quantité assez similaire de tests aux tests d'échafaudages générés automatiquement RSPEC dans les rails. Ruby sans tests d'unité n'est pas réalisable.

Mais ce que je dirais, c'est que c ne doit pas être aussi difficile que certaines personnes le font. Une grande partie de la bibliothèque standard C est un gâchis de noms de fonction incompréhensibles et de nombreux programmes C suivent cette convention. Je suis heureux de dire que nous ne le faisons pas, et d'avoir beaucoup d'emballages pour la fonctionnalité de la 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_clusion, etc.). Notre code C interne se lit mieux à moi que la plupart des autres choses que j'ai lues.

Mais lorsque vous dites que les règles d'autres langues de niveau supérieur ne semblent pas appliquer, je ne voudrais pas être en désaccord. Beaucoup de bon code C code "ressent" objet. Vous voyez souvent un modèle d'initialisation d'une poignée à une ressource, appelant certaines fonctions qui transmettent la poignée comme un argument et éventuellement libérer la ressource. En effet, les principes de conception de la programmation orientée objet proviennent en grande partie des bonnes choses que les gens faisaient dans des langues procédurales.

Les moments où C devient vraiment compliqué sont souvent lorsque vous faites des choses comme des pilotes de périphérique et des noyaux OS qui ne sont que fondamentalement de 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.

Le code source de Ruby est une chose très intéressante. Dans le Ruby API Docs (http://www.ruby-doc.org/core-1.9.3/) Vous pouvez cliquer sur le code source des différentes méthodes. La chose intéressante Ce code est-il assez agréable et élégant - il n'a pas l'air aussi complexe que vous pourriez imaginer.

2
asc99c

Il n'y a aucune raison pour que vous ne puissiez pas. Le problème est qu'il n'ya peut-être pas de belles cadres de test unitaires "hors-la-chaussée" telles que vous avez dans d'autres types de développement. C'est bon. Cela signifie simplement que vous devez adopter une approche "roll-votre propre" des tests.

Par exemple, vous devrez peut-être avoir à programmer une instrumentation pour produire des "fausses entrées" pour vos convertisseurs A/D ou que vous devrez peut-être générer un flux de "fausses données" pour votre appareil embarqué pour répondre.

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

2
Angelo

Est-il même possible de faire TDD sur des périphériques incorporés ou lors du développement de pilotes ou de choses telles que le chargeur de démarrage personnalisé, etc.?

Il y a quelque temps 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 des gars qui vendent cette CPU. Et nous avons utilisé un schéma où leur chargeur de démarrage boit notre chargeur de démarrage. Mais c'était lent, car nous avions besoin de clignoter deux fichiers dans NOR Flash au lieu d'un, nous devions construire la taille de notre chargeur de démarrage dans le premier chargeur de démarrage et la reconstruire à chaque fois que nous avons changé notre chargeur de démarrage et bientôt.

J'ai donc décidé d'intégrer des fonctions de leur chargeur de démarrage dans la nôtre. Parce que c'était un code de commerce, je devais m'assurer que toutes les choses fonctionnaient comme prévu. Donc j'ai modifié [~ # ~] qemu [~ # ~] Pour imiter les blocs IP de cette CPU (pas tous, seuls ceux qui touchent le chargeur de démarrage) et ajouter du code à QEMU à "Printf "Tous lisent/écriture aux registres qui contrôlent des trucs comme le contrôleur PLL, UART, SRAM et ainsi de suite. Ensuite, j'ai mis à niveau notre chargeur de démarrage pour prendre en charge cette CPU, puis comparé la sortie qui donne à notre chargeur de démarrage et à leur émulateur, cela vous aide à attraper plusieurs bugs. Il a été écrit en partie dans l'assembleur ARM, en partie en C. Aussi, après cette modification, QEMU m'a aidé à attraper un virus, que je ne pouvais pas attraper à l'aide de JTAG et un véritable cpu ARM.

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

0
Evgeniy