Dieser Artikel handelt von meinem ersten Open Source-Projekt "repl" (Link zum Repository unten). Die Idee dieses Projekts ist es, dem Mikrocontroller-Programmierer zu ermöglichen, das Programm im Mikrocontroller über eine seiner Schnittstellen zu debuggen, während sich das Debuggen nicht wesentlich vom Debuggen über die jtag-Schnittstelle unterscheidet. Es war möglich, das Programm zu stoppen, Haltepunkte zu setzen, Register und Speicher anzuzeigen, indem das Programm instruktiv debuggt wurde.
Das erste, was mir in den Sinn kommt, ist das Erstellen einer 2x-Anwendung. Einer der Threads ist für die Debugging-Oberfläche verantwortlich, der andere für das Benutzerprogramm, das ich erstellt habe. Das Umschalten zwischen Threads erfolgt über einen Timer, jeder Thread hat seinen eigenen Stack. Ich habe mich entschieden, keine Menge zum Schreiben der Debugging-Oberfläche zu verwenden. Sie müssen 2 verschiedene verwendet werden, oder wenn Sie mit einem Heap arbeiten, müssen Sie ständig zu einem Thread wechseln.
Die erste Idee für die Implementierung für das Debuggen von Anweisungen bestand darin, die Zeit zwischen Timer-Interrupts gerade so weit zu reduzieren, dass nur 1 Befehl ausgeführt werden konnte. Diese Option hat ihre ideale Arbeit am Atmega328p-Mikrocontroller gezeigt. Tatsache ist, dass die Mindestzeit zwischen den Interrupts für Atmega 1 Prozessorzyklus beträgt. Jeder Befehl, unabhängig von der Anzahl der für seine Ausführung erforderlichen Zyklen, endet immer, wenn seine Ausführung begonnen hat.
Als ich zu stm32 wechselte, funktionierte diese Option leider nicht, da der Cortex-M-Kern die Ausführung eines Befehls in der Mitte unterbrechen und ihn nach der Rückkehr vom Interrupt erneut ausführen kann. Dann habe ich mich gut entschieden, keine Probleme, ich werde nur die Anzahl der Taktzyklen zwischen den Interrupts erhöhen, bis der Befehl ausgeführt wird, dann bin ich auf ein anderes Problem gestoßen, einige Befehle werden nur mit dem darauf folgenden Befehl ausgeführt oder überhaupt nicht ausgeführt, leider kann dieses Problem nicht gelöst werden, also habe ich mich entschieden den Ansatz radikal ändern.
Ich habe beschlossen, diese Anweisungen zu emulieren. Auf den ersten Blick sieht die Idee, einen Cortex M-Core-Emulator zu schreiben, und sogar einen, der in den Speicher des Mikrocontrollers passt, fantastisch aus. Aber mir wurde klar, dass ich nicht alle Anweisungen emulieren musste, sondern nur diejenigen, die dem Programmzähler zugeordnet sind. Es gab nicht so viele davon. Den Rest kann ich einfach an einen anderen Speicherort verschieben und dort ausführen. Damit nur ein Befehl ausgeführt wird, füge danach den Sprungbefehl „b“ hinzu.
Und dann kamen die Haltepunkte, für deren Implementierung entschied ich mich, eine Anweisung zu verwenden, die die while (1); -Logik implementiert. Wir ersetzen die Anweisung an der Adresse, an der wir den Haltepunkt setzen möchten, und warten auf den Timer-Interrupt. Ich verstehe, dass so etwas wie eine Anweisung, die eine Ausnahme auslösen würde, besser wäre, aber ich wollte eine universelle Version erstellen. Bei der Ausführung ersetzen wir diese Anweisung zurück. Eine gute Option ist es, das Programm im RAM des Mikrocontrollers auszuführen, da sonst der Flash-Speicher des Mikrocontrollers nicht lange hält. Aber zu diesem Zeitpunkt hatte ich bereits einen Anweisungsemulator für stm32 fertig geschrieben und beschlossen, warum nicht denselben für Atmega328 zu schreiben, und schrieb ihn. Jetzt müssen Sie die Anweisungen nicht mehr ersetzen, sie können emuliert werden.
Um all diese Freunde mit der Laufzeitumgebung zu finden, wollte ich zuerst meinen eigenen GDB-Client schreiben. Leider unterstützt es zwei Schnittstellen für die Arbeit mit ide. Es liegt an ihr, zu entscheiden, welche Idee sie verwenden soll. Beide zu implementieren (der erste schien mir recht einfach, der zweite nicht sehr), und ich müsste die Quellen mit der Firmware kombinieren, was mir keine sehr gute Idee schien. Also habe ich beschlossen, meinen eigenen gdbserver zu schreiben, zum Glück gab es nur ein Protokoll und es war ziemlich einfach.
Meine erste Schnittstelle, die ich implementieren wollte, war GSM. Als Transceiver habe ich mich für SIM800 als Server entschieden, einen Server mit PHP-Unterstützung. Die Hauptidee war, nach dem Eintreffen einer Post-Anfrage vom Mikrocontroller die Verbindung zum Mikrocontroller 30 Sekunden lang aufrechtzuerhalten und alle 100 ms die Datenbank zu kontaktieren. Wenn die Daten angezeigt wurden, senden Sie sie als Antwort auf die Anfrage und warten Sie auf die nächste Anfrage vom Mikrocontroller.
Die erste Verbindung des GDB-Clients mit dem Server hat gezeigt, dass mit dem Befehl pause oder step zu viele Anforderungen vom GDB-Client an den Server eingegangen sind. Daher wurde beschlossen, alle diese Anforderungen zu einer großen für den Mikrocontroller zusammenzufassen. Dazu verstand ich die Logik dieser Anforderungen und lernte, sie vorherzusagen. Jetzt wurden diese Befehle nicht so schnell ausgeführt, ich möchte schneller, aber erträglich.
Die nächste Schnittstelle war USB. Für den Atmega328-Mikrocontroller habe ich mich für die V-USB-Bibliothek entschieden. Um mit USB zu arbeiten, habe ich die Run-Befehlslogik neu geschrieben. Nachdem der Mikrocontroller nach diesem Befehl das auf den Pausenbefehl wartende Programm nicht gestartet hat, hat er es 1 Sekunde lang gestartet, dann wurde ein neuer Ausführungsbefehl an ihn gesendet usw. Diese Logik ist notwendig, weil Ich deaktiviere die Schnittstelle, während das Programm läuft. Um mir die Mühe zu ersparen, einen Treiber zu schreiben, habe ich mich für den Standard-Hid-Treiber entschieden. Wenn Kommunikationsfunktionen den Funktionsbericht abrufen, wird der Funktionsbericht ausgeblendet.
Für das Flashen von Mikrocontrollern habe ich entschieden, dass dies für die USB-Schnittstelle überflüssig ist. Für die erste Firmware benötigen Sie also noch einen Programmierer. Aber für die GSM-Schnittstelle ist es am meisten. Normalerweise wird zu diesem Zweck ein separates Programm geschrieben, aber ich habe beschlossen, es anders zu machen. Um ein separates Programm zu schreiben, habe ich beschlossen, das Programm vollständig in den Flash-Speicher des Mikrocontrollers zu laden. Nachdem der Download abgeschlossen ist, kopieren Sie dieses Programm an den Anfang des Speichers. Dann dachte ich mir, warum ich das gesamte Programm senden soll, kann ich nur den Unterschied zwischen der aktuellen und der vorherigen Binärdatei der Firmware senden.
Um diesen Unterschied zu minimieren, habe ich beschlossen, die Array-Abschnitte .text, .data, .bss und .contructors für einen Teil des Benutzerprogramms umzubenennen (der verallgemeinerte Name ist für verschiedene Mikrocontroller unterschiedlich) und sie direkt nach dem Hauptprogramm im Speicher abzulegen.
Ich musste auch meine eigenen Funktionen schreiben, um diese Abschnitte zu initialisieren. In den meisten Fällen entspricht eine kleine Programmänderung einer kleinen binären Änderung, die einer kleinen übertragenen Datenmenge entspricht. Infolgedessen blinkt der Mikrocontroller häufig schneller als die Befehle RUN, STEP, PAUSE.
Und zum Schluss ein Video der Arbeit:
Stm32-Debugging über die USB-Schnittstelle.
Stm32-Debugging über die GSM-Schnittstelle.
Atmega328-Debugging über USB-Schnittstelle.
Atmega328-Debugging über die GSM-Schnittstelle.
Git-Repository