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?
quelle
Antworten:
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.
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.
Nun, es gibt Bücher zu diesem Thema:
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.
quelle
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:
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.
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
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.
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.
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.
quelle
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.
quelle
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.
quelle
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.
quelle
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).
quelle
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.
quelle
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.
quelle