Kampis Elektroecke

AVR-GCC unter die Haube geschaut…

In den letzten Tagen habe ich mich intensiv mit meiner AVR-Bibliothek beschäftigt und einige Treiber für die XMega- und die ATMega32-Peripherie weiter geschrieben bzw. in einer ersten Version als statische Bibliothek veröffentlicht.

Während der Codeentwicklung für das Batteriebackup-Systems, bzw. des Clocksystems des XMegas bin ich allerdings auf ein Hindernis gestoßen. Bei beiden Komponenten sind einzelne Bits durch das Configuration Change Protection-Register geschützt. Dies hat zur Folge, dass bestimmte Bits nur nach dem Setzen einer bestimmten Signatur im CCP-Register (hier 0xD8) geändert werden können.

  • Reset im CTRL-Register des Batteriebackup-Systems
  • Auswahl der Taktquelle im CTRL-Register der Clock

Erschwerend kommt hinzu, dass man nach dem Setzen der Signatur nur 4 Taktzyklen Zeit hat um eines der geschützten Register zu beschreiben. Danach wird es wieder gesperrt.

Gut, dachte ich. Also schreibe ich einfach folgende Funktion um die Taktquelle zu wechseln:

Der Atomic-Block soll dabei verhindern das diese Befehlsfolge durch einen Interrupt gestört und somit das Taktlimit überschritten wird (für den Reset des Batteriebackup-Systems gibt es eine Funktion mit einem identischen Aufbau).

Doch der Code funktionierte nicht. Eine Fehlersuche führte schnell zu der Erkenntnis, dass die Optimierung daran schuld ist. Damit dieser Code funktioniert ist mind. das Optimierungslevel -O1 erforderlich. Eine Optimierung ist aber für ein Debuggen sehr hinderlich, weil gewissen Programmblöcke nicht angesprungen werden können, da diese zu stark optimiert werden. Für eine Softwareentwicklung ist das also alles andere als optimal. 

Aber was macht der Compiler aus diesem (für den Menschen) offensichtlichen Code? Es kann doch nicht sein, dass die Zuweisung

mehr als vier Taktzyklen benötigt. Also habe ich mir das ganze Problem mal im Assembly angeschaut. Für eine einfachere Betrachtung habe ich den Code ausgelagert und mal geschaut was der Compiler aus dem Code macht:

In diesem Beispiel habe ich einfach eine beliebige Taktquelle genommen. Der Wert, der in das CLK.CTRL-Register geschrieben wird ist nicht von Interesse. Das Disassembly zeigt Interessantes:

Nun ist das AVR Instruction Set gefragt, in dem die einzelnen Befehle nachgeschlagen werden können. Für eine genauere Betrachtung reicht der Block nach dem Setzen der Signatur:

In dieser Codesequenz wird zuerst die die Registeradresse des CLK.CTRL-Registers geladen in die Register R24 und R25 geladen. Jeder LDI-Befehl dauert 1 Taktzyklus. Direkt im Anschluss daran wird der Wert 0x01 (die Einstellung für die Taktquelle) in das Register R18 kopiert. Dieser Vorgang dauert ebenfalls 1 Taktzyklus.

Anschließend wird der Wert aus R24 (also die Zieladresse) in das Register R30 kopiert. Das Register R30 stellt ein Register zur indirekten Adressierung dar, genauer das unterste Byte des Z-Registers. Hier wird also die Adresse des Zielregisters (0x40) gespeichert, damit dieses Zielregister mit der nächsten Instruktion adressiert werden kann. Dieser Vorgang dauert ebenfalls 1 Taktzyklus.

Über den STD-Befehl werden dann die Daten aus einem Register (hier R18) in ein adressiertes Register geschrieben. Durch den Zusatz Z+0 wird das Low-Byte des Z-Registers angesprochen. Damit werden also die Daten des Registers R18 indirekt über die Adresse in R30 in die Speicherstelle 0x40 kopiert. Der STD-Befehl benötigt mind. 1 Taktzyklus. 

Insgesamt benötigt das Kopieren des Wertes in das CLK.CTRL-Register, nachdem das Signaturbyte gesetzt worden ist, also 5 Taktzyklen und ist damit 1 Taktzyklus zu lang. Als Kontrast dazu der Code mit eingeschalteter Optimierung:

Die entstandene Codesequenz ist deutlich kürzer und auch das Beschreiben des CLK.CTRL-Registers dauert jetzt nur noch 3 Zyklen:

  • 1 Zyklus um den Wert für die Clocksource in das Register R24 zu laden
  • 2 Zyklen um den Wert aus R24 mittels des STS-Befehls in das CLK.CTRL-Register zu schreiben

Damit der Code auch ohne eingeschaltete Optimierung funktionieren (weil man z .B. den Debugger nutzen möchte) kann, sollte er direkt in Assembler verfasst werden. Wenn eine Kombination aus C und Assembler verwendet wird muss unbedingt das entsprechende ABI, hier also AVR-GCC-ABI, berücksichtigt werden. 

Kurz gesagt: Ein ABI definiert wie bestimmte Schnittstellen und Datentypen in Maschinencode umgewandelt werden. Bei einer Schnittstelle können z. B. Funktionsaufrufe betrachtet werden.

Der Übergabeparameter der Funktion steht in Register R24. Die Berechnung erfolgt durch den, im ABI beschriebenen, Weg:

  • R26 ist die Ausgangsbasis
  • Es wird ein uin8_t übergeben. Dieser ist 1 Byte groß und damit ungerade → Aufrunden auf 2
  • R26 – 2 = R24

Bei weiteren Argumenten wird analog vorgegangen, wobei die Ausgangsbasis immer die berechnete Registeradresse ist. Die resultierende Adresse beschreibt immer die Position des LSB. Alle anderen Teile des Übergabewertes werden dann in der jeweils um eins inkrementierten Adresse abgelegt.

Mit diesem Wissen kann die Funktion nun angepasst werden:

  • Zuerst kopiert die Funktion die Adresse des CLK.CTRL-Registers in das Register R30 (Low-Byte des Z-Registers).
  • Anschließend wird die Schutzsignatur 0x0D aus dem Speicher in das Register R16 geladen
  • Direkt danach wird der Inhalt aus Register R16 mittels OUT-Befehl in das CCP-Register kopiert
  • Zu guter letzt kopiert der ST-Befehl den Übergabeparameter für die Taktquelle in das, durch das Z-Register adressierte, Register

Mit dieser Lösung lässt sich die Taktquelle unabhängig von dem eingestellten Optimierungslevel ändern. Allerdings ist dieser Code noch nicht ganz optimal, wie das Disassembly zeigt:

Wie man erkennt, dauert es trotzdem noch 3 Taktzyklen, bis das CLK.CTRL-Register beschrieben wurde, da vor dem Beschreiben des Registers noch die Werte geladen werden:

Wünschenswert wäre es, wenn die Werte geladen werden bevor die Schutzsignatur in das CCP-Register geschrieben wird. Das mehrfache Aufrufen von asm()-Befehlen ist zudem unschön und äußerst fehleranfällig, da sich der erzeugte Code ändern kann, bzw. die einzelnen asm()-Befehle nicht als ganzes betrachtet werden und der Compiler diese dadurch falsch interpretieren könnte. Es empfiehlt sich daher alle Assembler-Befehle in eine einzige asm()-Anweisung zu schreiben:

Zusätzlich wird noch eine Clobber-Liste verwendet um den Compiler über sich ändernde Register zu informieren. Damit ergibt sich das gewünschte Verhalten und der Code ist sauber geschrieben:

Das Register R24 wird mit der Adresse für das CLK.CTRL-Register geladen, welche dann in das Register R30 geschrieben wird. Nun wird der Wert für die Clocksource in das Register R24 geladen und der Wert der Schutzsignatur wird mittels OUT-Befehl in das CCP-Register geschrieben. Direkt mit dem nächsten Takt wird dann der Wert aus Register R24, also die 0x01, in das durch R30 adressierte Register geschrieben.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.