Wie kann ich Mikrocontroller-Code während der Laufzeit debuggen?

Ich verwende den Nuc240-Mikrocontroller, dessen UART0 für „printf“ konfiguriert ist. Ich verwende einen UART-zu-USB-Controller, um die Ausgabe auf dem Bildschirm anzuzeigen. Dies funktioniert auch während der Laufzeit, sodass ich die Dinge während der Laufzeit nach dem Beenden des Debug-Modus überwachen konnte. Aber ist dies die beste Methode oder gibt es andere alternative Methoden? Außerdem verwende ich IAR Embedded Workbench und das Terminal I/0 zeigt die Ausgabe in ‚printf‘ nicht an, was könnte der Grund dafür sein? Als Debugger verwende ich NuLink-Pro.

Antworten (3)

Sie haben nach der besten Methode gefragt, aber keine Kriterien für die Beurteilung der "besten" angegeben.

Die Verwendung eines UART zur Laufzeit zum Überwachen und Debuggen ist sicherlich eine Methode, die ihren Nutzen haben kann. Ich mache das oft, wenn der Prozessor einen Ersatz-UART hat oder der UART bereits für die Kommunikation mit einem Host verwendet wird.

Es ist jedoch normalerweise keine gute Idee, das monströse printf zu verwenden und eine Binär-zu-ASCII-Konvertierung im Mikro durchzuführen. Es zieht eine Menge Code mit sich, kann erhebliche Laufzeitzyklen verwenden und führt zu einem hohen Kommunikationsaufwand.

Es ist normalerweise besser, die Dinge so einfach wie möglich und im Kommunikationsprotokoll zu halten. Schieben Sie die Belastungen der Datenkonvertierung, der Präsentation für den Benutzer, der Handhabung von Gleitkommazahlen und dergleichen so weit wie möglich auf den Host. Der Host hat im Wesentlichen unbegrenzten Speicher und Zyklen für diese Aufgaben, während der Mikro dies nicht tut.

Ein Schema, das ich die meiste Zeit verwende, ist die Verwendung eines Opcode-basierten Binärprotokolls in beide Richtungen. Alles, was gesendet wird, beginnt mit einem Opcode-Byte. Darauf folgen dann alle Datenbytes, die für diesen Opcode definiert sind.

Aus Gründen der Klarheit und Konsistenz der Dokumentation nenne ich die an das eingebettete System gesendeten Pakete "Befehle" und die vom eingebetteten System an den Host gesendeten "Antworten". Antworten werden nicht unbedingt nur als Antwort auf Befehle gesendet, aber die Namensunterscheidung ist nützlich. Befehle und Antworten haben jeweils ihren eigenen Opcode-Bereich.

Ich habe ein paar Standardbefehle und -antworten, die ich immer verwende. Befehl und Antwort 0 ist immer NULL, was bedeutet, dass es ein akzeptabler Opcode ist, aber nichts tut und keine Datenparameter akzeptiert. Befehl 1 ist PING, der keine Parameter entgegennimmt, aber die PONG-Antwort sendet, die ebenfalls 1 ohne Parameter ist. Befehl 2 ist FWINFO. Das sendet die FWINFO-Antwort (ebenfalls 2), die Typ-ID, Version und Sequenznummer der Firmware angibt.

Im Mikro habe ich eine Aufgabe, die Bytes aus dem UART liest und den Befehlsstrom verarbeitet. Es beginnt mit dem Lesen des nächsten Bytes als Opcode. Eine Dispatch-Tabelle wird verwendet, um zu der Routine für diesen Befehl zu springen. Diese Routine liest alle Datenbytes, die der Befehl haben mag, führt den Befehl aus und kehrt zur Hauptschleife zurück, die das nächste Opcode-Byte erhält.

Antworten werden von jeder Aufgabe gesendet, die etwas zu senden hat, wann immer sie etwas zu senden hat. Damit mehrere Aufgaben unabhängig voneinander Antworten senden können, habe ich einen Mutex für den Antwortstrom. Um eine Antwort zu senden, müssen Sie den Mutex abrufen, die Antwortbytes senden und den Mutex freigeben. Dies ermöglicht es verschiedenen Teilen des Systems, Antworten nach Bedarf asynchron zu senden, wobei jedoch garantiert wird, dass jede Antwort als Ganzes gesendet wird, ohne dass Bytes von anderen Antworten damit verschachtelt sind.

Auf der Hostseite wird der Antwortstrom in einem separaten Thread behandelt. Es erhält jedes Opcode-Byte, verzweigt zu der Routine, um diese Antwort zu verarbeiten usw. Dies ist normalerweise in ein Testprogramm integriert, das dem entfernten System eine Befehlszeilenschnittstelle darstellt. Der Benutzer gibt Befehle an einer Eingabeaufforderung ein. Jede Befehlsroutine parst die Befehlsparameter und sendet jegliche binäre Antwort an das entfernte System, die durch diesen Befehl impliziert sein könnte. Da sowohl die Befehls- als auch die Antwortverarbeitungsteile des Programms möglicherweise Dinge an den Benutzer schreiben müssen, verwende ich einen Mutex, um in die Standardausgabe zu schreiben.

Als Beispiel sehen Sie hier, was passiert, wenn der Benutzer am Host den Befehl eingibt, um die Versionsinformationen der Remote-System-Firmware abzurufen. Der Benutzer tippt „FWINFO“ ein und drückt die Eingabetaste. Der Befehlsprozessor parst FWINFO, schlägt dieses Schlüsselwort in einer Tabelle nach und verzweigt zu der Routine, um diesen Befehl zu handhaben. Diese Routine stellt sicher, dass sich nichts anderes auf der Befehlszeile befindet (gibt einen Fehler aus, wenn dies der Fall ist) und sendet das einzelne Byte 2 an das Remote-System. Der Befehlsprozessor ist nun mit diesem Befehl fertig, schreibt also eine neue Zeile und Eingabeaufforderung und wartet darauf, dass der Benutzer den nächsten Befehl eingibt.

Der Befehlsprozessor im eingebetteten System empfängt das Opcode-Byte 2. Dies bewirkt, dass die Ausführung zu der FWINFO-Befehlsverarbeitungsroutine vektorisiert. Diese Routine erfasst den Mutex des Antwortstroms und sendet die Bytes 2, Typ-ID, Version und Sequenz. Dann gibt er den Mutex frei und kehrt zur Hauptaufgabenschleife zurück, die nach dem nächsten Befehls-Opcode sucht.

Zurück auf dem Host empfängt der Antwortbehandlungs-Thread das 2-Antwort-Opcode-Byte. Dadurch wird zum Antwortcode FWINFO verzweigt. Dieser Code liest die nächsten drei Bytes und speichert sie als Firmware-Typ-ID, Version und Sequenznummer. Er erfasst dann den Standardausgabe-Mutex, schreibt "Firmware ist Typ xx Version yy Sequenz zz", gibt den Mutex frei und kehrt zur Hauptschleife zurück, um nach dem nächsten Antwort-Opcode-Byte zu suchen.

Aus Sicht des Benutzers gibt er „FWINFO“ ein und das Programm antwortet sofort mit „Firmware is type xx version yy sequence zz“.

Beachten Sie einige wichtige Dinge, die sich aus dieser Architektur ergeben:

  1. Die Anzahl der über die Verbindung mit begrenzter Bandbreite gesendeten Bytes ist minimal. Beispielsweise wurde nur ein einzelnes Byte gesendet, um die Firmware-Versionsinformationen anzufordern, und nur 4 Bytes wurden zurückgesendet.

  2. Das Parsen der Befehle ist im eingebetteten System einfach. Es schlägt jeden Opcode in einer Tabelle nach und springt zu der Routine, um den spezifischen Befehl zu behandeln. Es gibt kein String-Parsing, ASCII-zu-Binär-Konvertierung usw.

  3. Die Datenkodierung ist im bequemsten Format für das eingebettete System. Werte werden als binäre Bytes gesendet, so wie sie bereits im Speicher sind.

  4. Große Bibliotheksroutinen zum Konvertieren zwischen Binär- und ASCII-Dateien wie printf werden nicht in den Build gezogen.

  5. Antworten können asynchron gesendet werden. Dies kann zum Debuggen nützlich sein, wie Sie gefragt haben. Sie können einige Antworten definieren, die gesendet werden, wenn ein bestimmtes Ereignis eintritt. Diese werden vom Hostprogramm auf der Standardausgabe angezeigt, sobald sie empfangen werden.

Ich habe manchmal Befehle erstellt, um bestimmte Debug-Antworten spontan zu aktivieren oder zu deaktivieren. Sie können sogar einen Befehl haben, der ein Flag setzt, sodass die Hauptereignisschleife beispielsweise alle 10 ms eine bestimmte Antwort sendet. Ich habe so etwas oft verwendet, um periodisch alle gemessenen Analogwerte zu erhalten. Auf der Hostseite können diese in eine CSV-Datei geschrieben und dann als Diagramm angezeigt werden, wenn der Benutzer einen bestimmten Befehl eingibt.

Es gibt viele Dinge, die getan werden können. Indem Sie mit einfachen binären Sequenzen in jeder Richtung beginnen, die asynchron zueinander sind, können Sie alle Arten von Telemetrie- und Debugging-Funktionen hinzufügen, während die normalen Interaktionen mit dem System weiterhin intakt bleiben.

Cortex M0 unterstützt nicht die dedizierte Hardware, die für das Debuggen im UART-Stil optimiert ist - dies ist die ITM- und SWO-Funktion, die in M3 und höher vorhanden ist. Es unterstützt auch kein Instruction Trace (ETM).

Das einzige dedizierte Runtime-Trace-Feature, das in einem M0-Gerät vorhanden sein könnte, ist MTB . Wenn Sie ein Gerät haben, das dies enthält, kann es zur Laufzeit einige sehr begrenzte Ablaufverfolgungsinformationen in den System-RAM schreiben. Das bedeutet, dass Sie beim Eintritt in den Debug-Zustand die jüngste Historie beobachten können (zumindest die Zweige).

Andernfalls können Sie möglicherweise Ihre eigene Zustandsverfolgung im RAM erfassen, sodass Sie sie später manuell analysieren können.

Aber ich habe eine UART-Port-Option zum Debuggen in meinem Cortex M0 verfügbar!
UART ist ein funktionaler Port, der möglicherweise von Ihrer Anwendung benötigt wird. Sie haben gefragt, ob es andere Methoden gibt. UART ist auch relativ langsam. Wenn Sie fragen möchten, warum der UART nicht in Ihrer Toolchain angezeigt wird, sollten Sie eine neue Frage zu dieser bestimmten Sache stellen - es ist einfacher, auf dieser Website jeweils eine Frage zu beantworten.

Die Antwort hängt weitgehend davon ab, welche Ressourcen Sie auf Ihrer MCU und in Ihrem System zur Verfügung haben. Das Herauswerfen von Daten aus dem UART ist ein guter Weg, aber nicht der einzige Feedback-Mechanismus. Die Verwendung von 1 oder mehreren LEDs kann nützlich sein. Wenn Ihr System über ein Display verfügt, können Sie auf das Display schreiben. Sie können andere Kommunikationsperipheriegeräte verwenden, wenn Sie die Ausgabe decodieren können. Sie können PWM- oder DAC-Ausgänge und ein Oszilloskop/DMM verwenden, um Daten herauszubekommen. Wenn Ihr System über eine Art Audioausgabe verfügt, können Sie Pieptöne, Töne und Sequenzen verwenden. Im Grunde möchten Sie einfach alle ungenutzten Ausgabemechanismen nutzen, die Sie haben, die die Hauptaufgabe des Systems nicht beeinträchtigen.