Wie mache ich TDD auf eingebetteten Geräten?

17

Ich bin nicht neu in der Programmierung und habe sogar mit C und ASM auf niedriger Ebene an AVR gearbeitet, aber ich kann mich wirklich nicht mit einem größeren Embedded-C-Projekt beschäftigen.

Da ich von der Ruby-Philosophie von TDD / BDD entartet bin, kann ich nicht verstehen, wie Leute Code wie diesen schreiben und testen. Ich sage nicht, dass es ein schlechter Code ist, ich verstehe nur nicht, wie das funktionieren kann.

Ich wollte mich mehr mit Low-Level-Programmierung befassen, aber ich habe wirklich keine Ahnung, wie ich das angehen soll, da es wie eine völlig andere Denkweise aussieht, die ich gewohnt bin. Ich habe keine Probleme damit, Zeigerarithmetik zu verstehen oder wie die Speicherzuweisung funktioniert, aber wenn ich sehe, wie komplex C / C ++ - Code im Vergleich zu Ruby aussieht, scheint es einfach unglaublich schwierig.

Da ich mir bereits ein Arduino-Board bestellt habe, würde ich gerne etwas tiefer in C einsteigen und wirklich verstehen, wie man Dinge richtig macht, aber es scheint, als gäbe es keine der Regeln für Hochsprachen.

Ist es überhaupt möglich, TDD auf eingebetteten Geräten oder bei der Entwicklung von Treibern oder Dingen wie benutzerdefiniertem Bootloader usw. durchzuführen?

Jakub Arnold
quelle
3
Hallo Darth, wir können Ihnen wirklich nicht helfen, Ihre Angst vor C zu überwinden, aber die Frage zu TDD auf eingebetteten Geräten ist hier ein aktuelles Thema: Ich habe Ihre Frage überarbeitet, um dies stattdessen zu ermöglichen.

Antworten:

18

Zuallererst sollten Sie wissen, dass der Versuch, Code zu verstehen, den Sie nicht geschrieben haben, 5x schwerer ist als der Versuch, ihn selbst zu schreiben. Sie können C lernen, indem Sie den Seriencode lesen, aber es wird viel länger dauern als das Lernen, indem Sie es tun.

Da ich von der Ruby-Philosophie von TDD / BDD entartet bin, kann ich nicht verstehen, wie Leute Code wie diesen schreiben und testen. Ich sage nicht, dass es ein schlechter Code ist, ich verstehe nur nicht, wie das funktionieren kann.

Es ist eine Fähigkeit; du wirst besser darin. Die meisten C-Programmierer verstehen nicht, wie Leute Ruby verwenden, aber das heißt nicht, dass sie es nicht können.

Ist es überhaupt möglich, TDD auf eingebetteten Geräten oder bei der Entwicklung von Treibern oder Dingen wie benutzerdefiniertem Bootloader usw. durchzuführen?

Nun, es gibt Bücher zu diesem Thema:

Bildbeschreibung hier eingeben Wenn eine Hummel das kann, kannst du das auch!

Beachten Sie, dass das Anwenden von Methoden aus anderen Sprachen normalerweise nicht funktioniert. TDD ist jedoch ziemlich universell.

Pubby
quelle
2
Jedes TDD, das ich für meine eingebetteten Systeme gesehen habe, hat nur Fehler in den Systemen gefunden, die leicht zu beheben waren, die ich selbst leicht gefunden hätte. Sie hätten nie gefunden, wofür ich Hilfe brauche, die zeitabhängigen Interaktionen mit anderen Chips und Interaktionen unterbrechen.
Kortuk
3
Dies hängt davon ab, an welchem ​​System Sie arbeiten. Ich habe festgestellt, dass ich mit TDD zum Testen der Software in Verbindung mit einer guten Hardwareabstraktion diese zeitabhängigen Interaktionen viel einfacher nachahmen kann. Der andere Vorteil, auf den die Leute oft schauen, ist, dass die automatisierten Tests jederzeit ausgeführt werden können und dass niemand mit einem Logikanalysator auf dem Gerät sitzen muss, um sicherzustellen, dass die Software funktioniert. TDD hat mir allein in meinem aktuellen Projekt Wochen des Debuggens erspart. Oft sind es die Fehler, die wir für leicht erkennbar halten und die Fehler verursachen, die wir nicht erwarten würden.
Nick Pascucci
Außerdem ermöglicht es die Entwicklung und das Testen außerhalb des Ziels.
cp.engr
Kann ich diesem Buch folgen, um TDD für nicht eingebettetes C zu verstehen? Für jede User Space C Programmierung?
Überaustausch
15

Hier gibt es eine Vielzahl von Antworten, die sich hauptsächlich auf verschiedene Arten mit dem Problem befassen.

Ich schreibe seit über 25 Jahren eingebettete Low-Level-Software und -Firmware in einer Vielzahl von Sprachen - hauptsächlich in C (aber mit Umleitungen zu Ada, Occam2, PL / M und einer Vielzahl von Assemblern auf dem Weg).

Nach langen Überlegungen und Versuchen habe ich mich für eine Methode entschieden, die ziemlich schnell Ergebnisse liefert und es ziemlich einfach macht, Testverpackungen und -geschirre zu erstellen (wo sie WERT HINZUFÜGEN!).

Die Methode sieht ungefähr so ​​aus:

  1. Schreiben Sie einen Treiber oder eine Hardware-Abstraktionscode-Einheit für jedes wichtige Peripheriegerät, das Sie verwenden möchten. Schreiben Sie auch eine, um den Prozessor zu initialisieren und alles einzurichten (dies macht die freundliche Umgebung). In der Regel gibt es auf kleinen Embedded-Prozessoren - Ihr AVR ist ein Beispiel - 10 bis 20 solcher Einheiten, die alle klein sind. Dies können Einheiten für die Initialisierung, A / D-Konvertierung in nicht skalierte Speicherpuffer, bitweise Ausgabe, Tastereingang (keine Entprellung, nur abgetastet), Pulsweitenmodulationstreiber, UART / einfache serielle Treiber, die Interrupts verwenden, und kleine E / A-Puffer sein. Möglicherweise gibt es einige weitere - z. B. I2C- oder SPI-Treiber für EEPROM, EPROM oder andere I2C / SPI-Geräte.

  2. Für jede der Hardware-Abstraktions- (HAL) / Treibereinheiten schreibe ich dann ein Testprogramm. Dies setzt eine serielle Schnittstelle (UART) und Prozessorinitialisierung voraus. Das erste Testprogramm verwendet also nur diese beiden Einheiten und führt lediglich einige grundlegende Ein- und Ausgaben durch. Auf diese Weise kann ich testen, ob ich den Prozessor starten kann und ob die grundlegende Debug-Unterstützung für serielle E / A funktioniert. Sobald dies funktioniert (und nur dann), entwickle ich die anderen HAL-Testprogramme und baue diese auf den bekanntermaßen guten UART- und INIT-Einheiten auf. Vielleicht habe ich Testprogramme zum Lesen der bitweisen Eingaben und zum Anzeigen dieser in einer netten Form (hex, dezimal, was auch immer) auf meinem seriellen Debug-Terminal. Ich kann mich dann größeren und komplexeren Dingen wie EEPROM- oder EPROM-Testprogrammen zuwenden. Die meisten dieser Menüs werden von mir gesteuert, damit ich einen Test zum Ausführen, Ausführen und Anzeigen des Ergebnisses auswählen kann. Ich kann es nicht SCHREIBEN, aber normalerweise nicht

  3. Sobald ich alle meine HAL laufen habe, finde ich einen Weg, um einen regelmäßigen Timer-Tick zu bekommen. Dies liegt normalerweise zwischen 4 und 20 ms. Dies muss regelmäßig in einem Interrupt generiert werden. Der Rollover / Overflow von Zählern erfolgt normalerweise so. Der Interrupt-Handler ERHÖHT dann eine "Semaphor" -Byte-Größe. An dieser Stelle können Sie bei Bedarf auch mit der Energieverwaltung experimentieren. Die Idee des Semaphors ist, dass, wenn sein Wert> 0 ist, Sie die "Hauptschleife" ausführen müssen.

  4. Das EXECUTIVE führt die Hauptschleife aus. Es wartet so ziemlich nur darauf, dass dieses Semaphor ungleich 0 wird (das Ich abstrahiere dieses Detail weg). An diesem Punkt können Sie mit Zählern spielen, um diese Ticks zu zählen (da Sie die Tick-Rate kennen) und so Markierungen setzen, die anzeigen, ob das aktuelle Executive-Tick für ein Intervall von 1 Sekunde, 1 Minute und andere von Ihnen gewohnte Intervalle gilt Vielleicht möchten Sie verwenden. Sobald die Führungskraft weiß, dass das Semaphor> 0 ist, führt sie einen einzelnen Durchlauf durch jede "Anwendungs" -Prozess "Aktualisierungs" -Funktion aus.

  5. Die Bewerbungsprozesse sitzen effektiv nebeneinander und werden regelmäßig von einem "Update" -Tick ausgeführt. Dies ist nur eine Funktion, die von der Exekutive aufgerufen wird. Dies ist praktisch ein schlechtes Multitasking-System mit einem sehr einfachen RTOS-System für den Eigenbau, bei dem alle Anwendungen eingegeben, ein wenig bearbeitet und beendet werden müssen. Anwendungen müssen ihre eigenen Statusvariablen beibehalten und können keine Berechnungen mit langer Laufzeit durchführen, da es kein vorbeugendes Betriebssystem gibt, das Fairness erzwingt. Offensichtlich sollte die Laufzeit der Anwendungen (kumuliert) kleiner sein als die Haupt-Tick-Periode.

Der oben beschriebene Ansatz lässt sich leicht erweitern, sodass beispielsweise Kommunikationsstacks hinzugefügt werden können, die asynchron ausgeführt werden und Kommunikationsnachrichten an die Anwendungen übermittelt werden können. Dazu fügen Sie jeweils eine neue Funktion hinzu, nämlich den "rx_message_handler" heraus welcher Antrag zu versenden ist).

Dieser Ansatz funktioniert für so ziemlich jedes Kommunikationssystem, das Sie benennen möchten - er kann (und hat es getan) für viele proprietäre Systeme, offene Kommunikationssysteme und sogar für TCP / IP-Stacks.

Es hat auch den Vorteil, dass es modular mit genau definierten Schnittstellen aufgebaut ist. Sie können jederzeit Teile ein- und ausziehen und durch andere Teile ersetzen. An jedem Punkt auf dem Weg können Sie Testgeschirre oder -handler hinzufügen, die auf den bekannten guten Teilen der unteren Schicht aufbauen (das unten stehende Zeug). Ich habe festgestellt, dass ungefähr 30% bis 50% eines Designs davon profitieren können, speziell geschriebene Komponententests hinzuzufügen, die normalerweise relativ einfach hinzugefügt werden können.

Ich bin noch einen Schritt weiter gegangen (eine Idee, die ich von jemandem geklaut habe, der dies getan hat) und habe die HAL-Ebene durch eine Entsprechung für den PC ersetzt. So können Sie beispielsweise C / C ++ und Winforms oder ähnliches auf einem PC verwenden und durch sorgfältiges Schreiben des Codes jede Schnittstelle emulieren (z. B. EEPROM = eine in den PC-Speicher eingelesene Festplattendatei) und anschließend die gesamte eingebettete Anwendung auf einem PC ausführen. Die Verwendung einer benutzerfreundlichen Debug-Umgebung kann viel Zeit und Mühe sparen. Nur wirklich große Projekte können diesen Aufwand in der Regel rechtfertigen.

Die obige Beschreibung ist etwas, das sich nicht nur auf eingebettete Plattformen bezieht. Ich bin auf zahlreiche kommerzielle Organisationen gestoßen, die ähnliche Aufgaben ausführen. Die Art und Weise der Umsetzung ist normalerweise sehr unterschiedlich, aber die Prinzipien sind häufig sehr ähnlich.

Ich hoffe, das obige gibt ein bisschen Geschmack ... Dieser Ansatz funktioniert für kleine eingebettete Systeme, die in wenigen kB mit aggressivem Batteriemanagement laufen, bis hin zu Monstern mit 100K oder mehr Quellleitungen, die permanent mit Strom versorgt werden. Wenn Sie "eingebettet" auf einem großen Betriebssystem wie Windows CE oder so weiter ausführen, ist das alles völlig unerheblich. Aber das ist sowieso keine ECHTE Embedded-Programmierung.

schnell_nun
quelle
2
Die meisten Hardware-Peripheriegeräte können Sie nicht über einen UART testen, da Sie häufig hauptsächlich an den Timing-Eigenschaften interessiert sind. Wenn Sie eine ADC-Abtastrate, ein PWM-Tastverhältnis, das Verhalten eines anderen seriellen Peripheriegeräts (SPI, CAN usw.) oder einfach die Ausführungszeit eines Teils Ihres Programms überprüfen möchten, können Sie dies nicht über a UART. Zu jedem ernsthaften Test der eingebetteten Firmware gehört ein Oszilloskop - ohne dieses können Sie keine eingebetteten Systeme programmieren.
1
Oh ja, absolut. Ich habe nur vergessen, das zu erwähnen. Sobald Sie Ihren UART eingerichtet und in Betrieb genommen haben, ist es sehr einfach, Test- oder Testfälle einzurichten (worum es bei der Frage ging), Dinge zu stimulieren, Benutzereingaben zuzulassen, Ergebnisse zu erhalten und auf benutzerfreundliche Weise anzuzeigen. Dies + Ihr CRO macht das Leben sehr einfach.
quick_now
2

Code mit einer langen Tradition inkrementeller Entwicklungen und Optimierungen für mehrere Plattformen, wie die von Ihnen ausgewählten Beispiele, ist in der Regel schwerer zu lesen.

Die Sache mit C ist, dass es tatsächlich in der Lage ist, Plattformen über einen riesigen Bereich von API-Reichhaltigkeit und Hardware-Leistung (und deren Fehlen) hinweg zu überspannen. MacVim lief reaktionsschnell auf Computern mit über 1000-fach geringerer Speicher- und Prozessorleistung als ein typisches heutiges Smartphone. Kann Ihr Ruby-Code? Das ist einer der Gründe, warum es einfacher aussieht als die ausgereiften C-Beispiele, die Sie ausgewählt haben.

hotpaw2
quelle
2

Ich bin in der umgekehrten Position, als ich die meisten der letzten 9 Jahre als C-Programmierer verbracht habe und vor kurzem an einigen Ruby on Rails-Frontends gearbeitet habe.

Die Dinge, an denen ich in C arbeite, sind meistens mittelgroße kundenspezifische Systeme zur Steuerung von automatisierten Lagern (typische Kosten von ein paar hunderttausend Pfund, bis zu ein paar Millionen). Beispielfunktionalität ist eine benutzerdefinierte speicherinterne Datenbank, die an Maschinen mit kurzen Antwortzeiten und einer übergeordneten Verwaltung des Warehouse-Workflows angeschlossen ist.

Ich kann zuallererst sagen, wir machen keine TDD. Ich habe wiederholt versucht, Komponententests einzuführen, aber in C ist es mehr Mühe als Mühe wert - zumindest bei der Entwicklung von kundenspezifischer Software. Aber ich würde sagen, dass TDD in C weitaus weniger benötigt wird als in Ruby. Dies liegt hauptsächlich daran, dass C kompiliert wird. Wenn es ohne Warnungen kompiliert wird, haben Sie bereits eine vergleichbare Anzahl von Tests durchgeführt wie die von rspec automatisch generierten Gerüsttests in Rails. Ruby ohne Unit-Tests ist nicht machbar.

Aber ich würde sagen, dass C nicht so schwer sein muss, wie manche Leute es schaffen. Ein Großteil der C-Standardbibliothek besteht aus unverständlichen Funktionsnamen, und viele C-Programme folgen dieser Konvention. Ich bin froh, sagen zu können, dass wir dies nicht tun und tatsächlich viele Wrapper für die Standardbibliotheksfunktionalität haben (ST_Copy anstelle von strncpy, ST_PatternMatch anstelle von regcomp / regexec, CHARSET_Convert anstelle von iconv_open / iconv / iconv_close usw.). Unser hauseigener C-Code liest sich für mich besser als die meisten anderen Dinge, die ich gelesen habe.

Aber wenn Sie sagen, dass Regeln aus anderen höheren Sprachen nicht zutreffen, würde ich dem nicht zustimmen. Viel guter C-Code fühlt sich objektorientiert an. Häufig wird ein Muster zum Initialisieren eines Handles für eine Ressource angezeigt, zum Aufrufen einiger Funktionen, die das Handle als Argument übergeben, und zum Freigeben der Ressource. Tatsächlich beruhten die Gestaltungsprinzipien der objektorientierten Programmierung im Wesentlichen auf den guten Dingen, die Menschen in prozeduralen Sprachen taten.

Die Zeiten, in denen C wirklich kompliziert wird, sind häufig, wenn Sie Gerätetreiber und Betriebssystem-Kernel ausführen, die im Grunde genommen nur sehr niedrig sind. Wenn Sie ein System auf höherer Ebene schreiben, können Sie auch die Funktionen auf höherer Ebene von C verwenden und die Komplexität auf niedriger Ebene vermeiden.

Eine sehr interessante Sache, die Sie vielleicht durchsehen möchten, ist der C-Quellcode für Ruby. In den Ruby API-Dokumenten (http://www.ruby-doc.org/core-1.9.3/) können Sie auf klicken und den Quellcode für die verschiedenen Methoden anzeigen. Das Interessante ist, dass dieser Code sehr schön und elegant aussieht - er sieht nicht so komplex aus, wie Sie es sich vorstellen können.

asc99c
quelle
" ... können Sie auch die übergeordneten Funktionen von C ... verwenden ", wie gibt es das? ;-)
alk
Ich meine eine höhere Ebene als die Bit-Manipulation und die Zeiger-auf-Zeiger-Zauberei, die Sie im Gerätetreiber-Typcode sehen! Und wenn Sie sich keine Sorgen über den Aufwand einiger Funktionsaufrufe machen, können Sie C-Code erstellen, der einigermaßen hochwertig aussieht.
asc99c
" ... Sie können C-Code erstellen, der wirklich einigermaßen hochwertig aussieht. ", absolut, dem stimme ich voll und ganz zu. Aber obwohl " ... die Features der höheren Ebene ... " nicht von C sind, sondern in deinem Kopf, nicht wahr?
alk
2

Ich habe den geräteabhängigen Code vom geräteabhängigen Code getrennt und dann den geräteabhängigen Code getestet. Mit guter Modularität und Disziplin erhalten Sie eine meist gut getestete Codebasis.

Paul Nathan
quelle
2

Es gibt keinen Grund, warum Sie nicht können. Das Problem ist, dass es möglicherweise keine netten Standard-Frameworks für Komponententests gibt, wie Sie sie bei anderen Entwicklungstypen haben. Das ist okay. Es bedeutet nur, dass Sie einen "Roll-Your-Own" -Ansatz zum Testen wählen müssen.

Beispielsweise müssen Sie möglicherweise Instrumente programmieren, um "Fake-Eingänge" für Ihre A / D-Wandler zu erzeugen, oder Sie müssen möglicherweise einen Stream von "Fake-Daten" generieren, auf die Ihr eingebettetes Gerät reagieren kann.

Wenn Sie auf Widerstand stoßen, das Wort "TDD" zu verwenden, nennen Sie es "DVT" (Design Verification Test).

Angelo
quelle
0

Ist es überhaupt möglich, TDD auf eingebetteten Geräten oder bei der Entwicklung von Treibern oder Dingen wie benutzerdefiniertem Bootloader usw. durchzuführen?

Vor einiger Zeit musste ich einen First-Level-Bootloader für eine ARM-CPU schreiben. Eigentlich gibt es einen von den Leuten, die diese CPU verkaufen. Und wir haben ein Schema verwendet, bei dem ihr Bootloader unseren Bootloader bootet. Dies war jedoch langsam, da wir zwei Dateien in NOR-Flash anstelle von einer flashen mussten, mussten wir die Größe unseres Bootloaders in den ersten Bootloader integrieren und ihn jedes Mal neu erstellen, wenn wir unseren Bootloader änderten und so weiter.

Deshalb habe ich mich entschlossen, Funktionen ihres Bootloaders in unseren zu integrieren. Da es sich um kommerziellen Code handelte, musste ich sicherstellen, dass alles wie erwartet funktionierte. Also habe ich QEMU modifiziert , um IP-Blöcke dieser CPU zu emulieren (nicht alle, nur diejenigen, die den Bootloader berühren), und Code zu QEMU hinzugefügt, um alle Lese- / Schreibvorgänge in Registern zu "drucken", die Dinge wie PLL, UART, SRAM-Controller und zu steuern bald. Dann habe ich unseren Bootloader aktualisiert, um diese CPU zu unterstützen, und nachdem ich die Ausgabe verglichen habe, die unseren Bootloader und seinen On-Emulator enthält, kann ich einige Fehler aufspüren. Es wurde teilweise in ARM Assembler, teilweise in C geschrieben. Auch nach dieser Änderung half mir QEMU, einen Fehler zu finden, den ich mit JTAG und einer echten ARM-CPU nicht finden konnte.

Selbst mit C und Assembler können Sie also Tests verwenden.

Evgeniy
quelle
-2

Ja, TDD kann mit eingebetteter Software durchgeführt werden. Die Leute, die sagen, dass es nicht möglich, nicht relevant oder nicht zutreffend ist, sind nicht korrekt. Wie bei jeder Software ist TDD auch bei Embedded von großem Wert.

Der beste Weg, dies zu tun, ist jedoch nicht, Ihre Tests auf dem Ziel auszuführen, sondern Ihre Hardware-Abhängigkeiten zu abstrahieren und auf Ihrem Host-PC zu kompilieren und auszuführen.

Wenn Sie TDD durchführen, werden Sie viele Tests erstellen und ausführen. Sie benötigen Software, um dies zu tun. Sie möchten ein Test-Framework, mit dem dies schnell und einfach möglich ist, mit automatischer Testerkennung und Scheingenerierung.

Die beste Option für C ist derzeit Ceedling. Hier ist ein Beitrag darüber, den ich geschrieben habe:

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

Und es ist in Ruby gebaut! Sie müssen keinen Ruby kennen, um es zu benutzen.

cherno
quelle
Von den Antworten wird erwartet, dass sie für sich selbst stehen. Die Leser dazu zu zwingen , auf externe Ressourcen zuzugreifen, um die Substanz herauszufinden, ist bei Stack Exchange verpönt ("Artikel lesen oder Ceedling lesen"). Betrachten Sie bearbeiten ing es fit Website Qualitätsnormen zu machen
gnat
Verfügt Ceedling über Mechanismen zur Unterstützung asynchroner Ereignisse? Einer der schwierigeren Aspekte von eingebetteten Echtzeitanwendungen ist, dass sie Eingaben von sehr komplexen Systemen empfangen, die selbst schwer zu modellieren sind ...
Jay Elston
@Jay Es gibt nichts spezielles, um das zu unterstützen. Ich hatte jedoch Erfolg damit, solche Dinge mit Spott zu testen und eine Architektur einzurichten, die dies unterstützt. Ich habe zum Beispiel kürzlich an einem Projekt gearbeitet, bei dem interruptgesteuerte Ereignisse in eine Warteschlange gestellt und dann in einem "Event-Handler" -Zustandsautomaten konsumiert wurden. Dies war im Wesentlichen nur eine Funktion, die immer dann aufgerufen wurde, wenn ein Ereignis auftrat. Beim Testen dieser Funktion konnte ich den Funktionsaufruf verspotten, der Ereignisse aus der Warteschlange gezogen hat, und so jedes im System auftretende Ereignis simulieren. Auch hier hilft Probefahren.
Tscherno