Das Titelbild dieses Beitrags ist von Albert Guillaume – Gils Blas, 24 décembre 1895, Public Domain, Link
Wenn man ein Werkzeug für ein Protokoll entwickelt, das undokumentiert ist, ist es nicht verwunderlich, dass man auf überraschende Situationen stößt. Und genau das habe ich bei der Entwicklung des Hardware-Debuggers dw-link erlebt, der debugWIRE-MCUs mit dem GDB-Debugger verbindet. Obwohl ein wesentlicher Teil des debugWIRE-Protokolls rekonstruiert wurde, habe ich einige überraschende Phänomene beobachtet: MCUs mit gespaltener Persönlichkeit, Stuck-at-One-Bits in Programmzählern, halblegale Opcodes und mehr.
Das debugWIRE-Protokoll ermöglicht den Zugriff auf die On-Chip-Debugging-Funktionen (OCD) einiger AVR-MCUs. Es verwendet die RESET-Leitung als asynchrone Eindraht-Verbindung. Um die MCU in den debugWIRE-Modus zu bringen, muss man eine bestimmte Fuse (DWEN) mit dem ISP-Protokoll programmieren und danach die MCU kurz stromlos machen. Um die MCU wieder in den Normalzustand zu versetzen, muss man den debugWIRE-Status deaktivieren und die DWEN-Fuse deaktivieren.
MCUs mit gespaltener Persönlichkeit
Der Typ einer MCU kann durch Abfragen der Gerätesignatur ermittelt werden. Es gibt einen Signaturabfragebefehl im ISP-Modus und einen im debugWIRE-Modus. Man würde erwarten, dass diese Abfragen das gleiche Register auslesen und das gleiche Ergebnis zurückgeben. Meistens tun sie das auch.
Die ATmega48A, ATmega88A, ATmega168A und ATmega328 MCUs, die ich besitze, haben jedoch gespaltene Persönlichkeiten. Sie identifizieren sich korrekt, wenn sie im ISP-Modus abgefragt werden. Im debugWIRE-Modus geben sie sich jedoch als P-Typen aus, d. h. als ATmega48PA, ATmega88PA, ATmega16PA bzw. ATmega328P. Natürlich macht es für den Debugger keinen Unterschied, ob wir einen 328 oder 328P haben. Es ist jedoch etwas verwirrend. Und es verwirrt tatsächlich MPLAB X beim Debuggen einer solchen MCU.
MCUs mit festen Eins-Bits im Programmzähler
Beim Ausführen von Unittests für meinen Debugger auf allen debugWIRE-MCUs, die ich in meiner Grabbelkiste habe, stieß ich auf einige ATmega48 und ATmega88 MCUs (ohne A-Suffix), die vor mehr als 10 Jahren produziert wurden und ein sehr seltsames (und undokumentiertes) Verhalten zeigen. Während alle anderen mehr als 30 MCU-Typen die Tests bestanden, stellte sich heraus, dass der Programmzähler der genannten MCUs einige unbenutzte Bits hat, die immer eine logische Eins sind.
Atmel-ICE/MPLAB X kommt damit problemlos zurecht. GDB hat jedoch u.a. Probleme, einen Stack-Backtrace durchzuführen. Schlimmer noch, beim Single-Stepping über eine Funktion hinweg inspiziert GDB den Stack, um herauszufinden, wo ein temporärer Haltepunkt platziert werden soll. Da die vom Stack gelesene Adresse jedoch auf Bereiche verweist, die nicht existieren, gibt GDB an dieser Stelle auf. Aus diesen Gründen scheint das Debuggen dieser MCUs mit GDB von begrenztem Wert zu sein und dw-link gibt eine Fehlermeldung aus, wenn man versucht, solch eine MCU zu debuggen.
Interessanterweise haben die genannten MCUs den gleichen Gerätesignaturcode wie die mit einem A-Suffix, und die Migrationsanleitung von Microchip zur Migration von ATmegaX8 zu ATmegaX8A sagen nichts über die Verhaltensauffälligkeit der nicht-A-Typen aus. Übrigens hatten einige ältere MCUs die gleiche Besonderheit, z.B. der ATmega16. In diesem Fall enthielt das Datenblatt jedoch einen Hinweis darauf, dass ungenutzte Bits in auf dem Stack abgelegten Rücksprungadressen zu ignorieren sind.
Halblegale Opcodes
Es gibt einige Opcodes, die laut offizieller Dokumentation keine Bedeutung haben. Darüber hinaus werden manche Opcodes nur auf einigen MCU-Architekturen unterstützt. Beispielsweise wird die Hardwaremultiplikation nur auf den ATmegas unterstützt. Ebenso werden laut offizieller Dokumentation 32-Bit-jmp
– und call
-Anweisungen nur auf MCUs unterstützt, die über mehr als 8 kB Flash-Speicher verfügen. Auf kleinen MCUs reichen die relativen 16-Bit-Jump
– und Call
-Anweisungen aus, um jede Position im Flash-Speicher zu erreichen.
Wie sich herausstellt, funktionieren 32-Bit-Jump
– und Call
-Befehlscodes auch auf kleinen MCUs, im Gegensatz zu dem, was die offizielle Dokumentation sagt. Sie sind nicht sehr nützlich, da die kürzeren und schnelleren relativen Sprung- und Aufrufanweisungen jeden Flash-Standort erreichen können, aber die 32-Bit-Anweisungen funktionieren trotzdem. Es hätte vermutlich mehr Arbeit erfordert, diese Anweisungen auf kleinen MCUs zu „deaktivieren“, als die gleiche Hardwarelogik wie bei MCUs mit größerem Flash-Speicher zu verwenden. Warum in der offiziellen Dokumentation behauptet wird, dass diese Anweisungen nicht unterstützt werden, anstatt zu schreiben, dass sie nicht nützlich sind, ist mir schleierhaft.
Ich hatte die völlig unbegründete Hoffnung, dass vielleicht auch die Hardware-Multiplikation (oder einige Teile davon) auf ATtiny-MCUs funktionieren. Wie Experimente jedoch gezeigt haben, sind diese Opcodes auf ATtinys einfach nop
-Operationen.
Zwei-Wort-Anweisungen an Breakpoints
Breakpoints werden implementiert, indem eine BREAK
-Anweisung in den Programmcode gesetzt wird. Beim Neustart von einem Breakpoint aus könnte man die BREAK
-Anweisung durch die ursprüngliche Anweisung ersetzen, einen Einzelschritt durchführen, die ursprüngliche Anweisung erneut durch das BREAK
ersetzen und dann fortfahren. Um den Flash-Speicherverschleiß zu minimieren, kann man stattdessen jedoch die ursprüngliche Anweisung „offline“ in dem speziellen debugWIRE-Befehlsregister ausführen.
Das funktioniert mit den gewöhnlichen Ein-Wort-Anweisungen. Für Zwei-Wort-Anweisungen besagt die offizielle Microchip-Dokumentation, dass man an diesen Stellen möglichst keine Haltepunkte einfügen sollte, was darauf hindeutet, dass dies zu Problemen führen könnte. In der Tat bemerkte RikusW in seinen Reverse-Engineering-Notizen zu debugWIRE:
Seems that its not possible to execute a 32 bit instruction this way. The Dragon reflash the page to remove the SW BP, SS and then reflash again with the SW BP!!!
Ich habe mir diese Situation kürzlich angeschaut und festgestellt, dass MPLAB X in Verbindung mit Atmel-ICE immer noch die Flash-Seite zweimal umprogrammiert, wenn ein Breakpoint an einer Zwei-Wort-Anweisung erreicht wird.
Was passiert nun, wenn man den ersten Teil der Zwei-Wort-Anweisung in das Befehlsregister lädt, den Programmzähler auf die Adresse setzt, an dem sich die ursprüngliche Instruktion befand, und dann die Instruktion „offline“ ausführt? Es stellt sich heraus, dass die MCU das einzig Sinnvolle tut, nämlich das zweite Wort zu laden und die Zwei-Wort-Anweisung auszuführen, als ob das erste Wort am ursprünglichen Ort gestanden hätte. Dies ist offensichtlich die eleganteste Lösung für die Ausführung von Zwei-Wort-Instruktionen, und ich war versucht, sie im Hardware-Debugger zu implementieren. Da sich Atmel und Microchip nicht für diese Lösung entschieden haben, vermute ich, dass es Spezialfälle oder spezielle MCUs geben könnte, unter denen die Offline-Ausführung von Zwei-Wort-Anweisungen möglicherweise nicht funktioniert. Aus diesem Grund habe ich diese Lösung dann doch verworfen.
Stattdessen habe ich eine andere Lösung gewählt, die auch den Verschleiß des Flash-Speichers in Grenzen hält. Die Ausführung der Zwei-Wort-Anweisungen (LDS
, STS
, JMP
und CALL
) wird im Hardware-Debugger simuliert, was wenig aufwändig und schnell genug ist, und dabei zwei Umprogrammiervorgänge erspart.
Das Ändern der DWEN-Fuse ist nicht immer erfolgreich
Um in den debugWIRE-Zustand zu wechseln, muss man die DWEN-Fuse im High-Fuse-Byte programmieren. Laut den Datenblättern geschieht dies, indem die ISP-Programmierung aktiviert und dann das Fusebyte programmiert wird. Dies funktioniert in der Tat für fast alle Fälle. Ich besitze jedoch einen ATmega48 und einen ATmega168 (kein A-Suffix), die beide ein äußerst seltsames Verhalten zeigen.
Diese beiden MCUs akzeptieren die Fuse-Programmierbefehle, aber das Fusebyte ist danach unverändert. Interessanterweise ist nach der Programmierung des Low-Fuse-Bytes die Programmierung des High-Fuse-Byte immer erfolgreich.
Um mit dieser Situation zurechtzukommen, programmiert dw-link immer das Low-Fuse-Byte vor dem High-Fuse-Byte.
debugWIRE Kommunikationsgeschwindigkeit
Wenn eine debugWIRE-Sitzung gestartet wird, ist die Kommunikationsgeschwindigkeit die MCU-Taktfrequenz geteilt durch 128. D.h. ein 16-MHz-MCU-Takt resultiert in einer Kommunikationsgeschwindigkeit von 125 kbps. Läuft die MCU mit 1-MHz-Takt, ist die Kommunikationsgeschwindigkeit rund 8 kbps. Während 125 kbps relativ schnell sind, führen 8 kbps zu einem trägen Verhalten des Debuggers, insbesondere beim Single-Stepping oder Laden einer Binärdatei.
Es ist möglich, die Kommunikationsgeschwindigkeit zu ändern, wie von RikusW dokumentiert. Es ist jedoch etwas kompliziert, da nach jeder BREAK-Bedingung auf der debugWIRE-Leitung, die vom Debugger gesendet oder vom Target-Chip generiert wird, die Kommunikationsgeschwindigkeit auf ihre ursprüngliche Geschwindigkeit zurückgesetzt wird. Um die Sache noch komplizierter zu machen, wird es auf die Hälfte der ursprünglichen Geschwindigkeit eingestellt, wenn das laufende Programm asynchron gestoppt wird. Und das passiert nur mit meinem Debugger, nicht mit Atmel-ICE. Ich werde versuchen herauszufinden, welche Art von Magie Atmel-ICE verwendet, um das zu vermeiden.
Die E/A-Adresse des DWDR
Das Debug Wire Data Register (DWDR) wird für die Kommunikation mit der Umgebung verwendet. Es wird im Datenblatt jeder MCU erwähnt, die die debugWIRE-Schnittstelle unterstützt. Doch nur in wenigen Fällen erfährt man in den Datenblättern, an welcher I/O-Adresse man das DWDR findet. Manchmal hat es einen anderen Namen, wie z.B. im Datenblatt des AT90PWM1, in dem es MONDR (kurz für Monitor Data Register) genannt wird. Manchmal findet man es in den AVR IO-Include-Dateien oder in den ATDF-
Beschreibungsdateien, die zusammen mit Microchip Studio und MPLAB X ausgeliefert werden. Für den ATtiny2313 und den ATtiny4313 konnte ich allerdings nichts finden. Zum Glück findet man im Programmcode von dwire-debug und DebugWireDebuggerProgrammer die E/A-Adresse des ATtiny2313-DWDR: 0x1F.
Normalerweise verwendet eine MCU-Familie die gleiche E/A-Adresse für den DWDR, z. B. hat die ATmegaX8-Familie den DWDR an der E/A-Adresse 0x31 und die ATtinyX5-Familie verwendet 0x22. Aus diesem Grund dachte ich, nachdem ich die E/A-Adresse des DWDR für ATtiny2313 ermittelt hatte, dass der ATtiny4313 dieselbe E/A-Adresse verwenden würde. Wie sich herausstellt, ist der ATtiny4314 die einzige Ausnahme unter allen AVR-MCUs. Er verwendet die Adresse 0x27, die ich durch Herumprobieren gefunden habe.
Was ich gelernt habe
Eine wichtige Lehre war für mich, dass man nicht davon ausgehen kann, dass ein Stück Hardware/Software auf einer AVR-MCU funktioniert, nur weil es auf einer ähnlichen MCU funktioniert. Man muss schon die spezielle MCU kaufen und dann in der Realität testen. Außerdem darf man nicht glauben, dass sich zwei MCUs identisch verhalten, nur weil sie beide den gleichen Gerätesignaturcode haben.
Darüber hinaus ist es natürlich eine spannende Herausforderung, mit undokumentierten Merkmalen einer MCU-Familie umzugehen. Was mir unklar geblieben ist, ist, warum Atmel (und jetzt Microchip) das debugWIRE-Protokoll geheim gehalten haben.
Schreibe einen Kommentar