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:

static inline void SysClock_SetClockSource(uint8_t Source)
{
   ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
   {
       CCP = CCP_IOREG_gc;
       CLK.CTRL = Source;
   }
}

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

CLK.CTRL = Source;

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:

int main(void)
{
    CCP = CCP_IOREG_gc;
    CLK.CTRL = CLK_SCLKSEL_RC32M_gc;
    return 0;
}

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:

--- C:\Users\Kampi\Desktop\Assembler\Assembler\Debug/.././main.c ---------------
{
0000010C  PUSH R28		Push register on stack 
0000010D  PUSH R29		Push register on stack 
0000010E  IN R28,0x3D		In from I/O location 
0000010F  IN R29,0x3E		In from I/O location 
	CCP = CCP_IOREG_gc;
00000110  LDI R24,0x34		Load immediate 
00000111  LDI R25,0x00		Load immediate 
00000112  LDI R18,0xD8		Load immediate 
00000113  MOVW R30,R24		Copy register pair 
00000114  STD Z+0,R18		Store indirect with displacement 
	CLK.CTRL = CLK_SCLKSEL_RC32M_gc;
00000115  LDI R24,0x40		Load immediate 
00000116  LDI R25,0x00		Load immediate 
00000117  LDI R18,0x01		Load immediate 
00000118  MOVW R30,R24		Copy register pair 
00000119  STD Z+0,R18		Store indirect with displacement 
	return 0;
0000011A  LDI R24,0x00		Load immediate 
0000011B  LDI R25,0x00		Load immediate 
}
0000011C  POP R29		Pop register from stack 
0000011D  POP R28		Pop register from stack 
0000011E  RET 		Subroutine return 
--- No source file -------------------------------------------------------------

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:

	CLK.CTRL = CLK_SCLKSEL_RC32M_gc;
00000115  LDI R24,0x40		Load immediate 
00000116  LDI R25,0x00		Load immediate 
00000117  LDI R18,0x01		Load immediate 
00000118  MOVW R30,R24		Copy register pair 
00000119  STD Z+0,R18		Store indirect with displacement 
	return 0;

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:

--- C:\Users\Kampi\Desktop\Assembler\Assembler\Debug/.././main.c ---------------
{
	CCP = CCP_IOREG_gc;
0000010C  LDI R24,0xD8		Load immediate 
0000010D  OUT 0x34,R24		Out to I/O location 
	CLK.CTRL = CLK_SCLKSEL_RC32M_gc;
0000010E  LDI R24,0x01		Load immediate 
0000010F  STS 0x0040,R24		Store direct to data space 
}
00000111  LDI R24,0x00		Load immediate 
00000112  LDI R25,0x00		Load immediate 
00000113  RET 		Subroutine return 
--- No source file -------------------------------------------------------------

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:

static inline void SysClock_SetClockSource(ClockSource Source)
{
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
        asm volatile("movw r30,  %0" :: "r" (&CLK.CTRL));
        asm volatile("ldi  r16,  %0" :: "M" (CCP_IOREG_gc));
        asm volatile("out   %0, r16" :: "i" (&CCP));
        asm volatile("st     Z,  %0" :: "r" (Source));
    }
}
  • 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:

--- C:\Users\Kampi\Desktop\Assembler\Assembler\Debug/.././main.c ---------------
{
0000010C  PUSH R28		Push register on stack 
0000010D  PUSH R29		Push register on stack 
0000010E  IN R28,0x3D		In from I/O location 
0000010F  IN R29,0x3E		In from I/O location 
	asm volatile("movw r30,  %0" :: "r" (&CLK.CTRL));
00000110  LDI R24,0x40		Load immediate 
00000111  LDI R25,0x00		Load immediate 
00000112  MOVW R30,R24		Copy register pair 
	asm volatile("ldi  r16,  %0" :: "M" (CCP_IOREG_gc));
00000113  LDI R16,0xD8		Load immediate 
	asm volatile("out   %0, r16" :: "i" (&CCP));
00000114  OUT 0x34,R16		Out to I/O location 
	asm volatile("st     Z,  %0" :: "r" (0x02));
00000115  LDI R24,0x02		Load immediate 
00000116  LDI R25,0x00		Load immediate 
00000117  STD Z+0,R24		Store indirect with displacement 
00000118  LDI R24,0x00		Load immediate 
00000119  LDI R25,0x00		Load immediate 
}
0000011A  POP R29		Pop register from stack 
0000011B  POP R28		Pop register from stack 
0000011C  RET 		Subroutine return 
--- No source file -------------------------------------------------------------

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:

	asm volatile("st     Z,  %0" :: "r" (0x02));
00000115  LDI R24,0x02		Load immediate 
00000116  LDI R25,0x00		Load immediate 
00000117  STD Z+0,R24		Store indirect with displacement

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:

asm volatile(	"movw r30,  %0"        "\n\t"
                "ldi  r16,  %2"        "\n\t"
                "out   %3, r16"        "\n\t"
                "st     Z,  %1"       "\n\t"
                :: "r" (&CLK.CTRL), "r" (Source), "M" (CCP_IOREG_gc), "i" (&CCP) : "r16", "r30", "r31");

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:

00000CDC  LDI R24,0x40		Load immediate 
00000CDD  LDI R25,0x00		Load immediate 
00000CDE  MOVW R30,R24		Copy register pair 
00000CDF  LDD R18,Z+0		Load indirect with displacement 
00000114  LDI R24,0x01		Load immediate 
00000115  LDI R25,0x00		Load immediate 
00000116  MOVW R30,R18		Copy register pair 
00000117  LDI R16,0xD8		Load immediate 
00000118  OUT 0x34,R16		Out to I/O location 
00000119  STD Z+0,R24		Store indirect with displacement

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.

3 Kommentare

  1. Wenn ich:
    asm volatile( „movw r30, %0“ „\n\t“
    „ldi r16, %2“ „\n\t“
    „out %3, r16“ „\n\t“
    „st Z, %1“ „\n\t“
    :: „r“ (&CLK.CTRL), „r“ (Source), „M“ (CCP_IOREG_gc), „i“ (&CCP) : „r16“, „r30“, „r31“);
    So in meinen Quellcode einfüge, erhalte ich eine Fehlermeldung:
    > ‚Source‘ undeclared (first use in this function)
    Also Source ist nicht definert. Wie komme ich da raus?

  2. Ich wollte es nicht als Funktion realsisieren.
    Daber habe ich den asm Teil einfach in meinen restliche C-Code kopiert.
    Jetzt habe ich ‚Source‘ durch das gewünschte Bitmuster (0b001)
    ersetzt und jetzt geht es.
    Fehlt nur noch ATOMIC?
    Vielen Dank!

Schreibe einen Kommentar

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