Ein Programm unterteile ich in Module, wie man das eben so macht. Zu jedem Modul gibt es (statische) Daten. Diese Daten lege ich nicht einzeln an als Bytes oder Worte, sondern in Strukturen. Damit ist jedes Modul grob so was wie ein Objekt in einer OO Sprache (natürlich viel schwächer).
Soweit ist das ein alter Hut.
Nehmen wir mal so eine Objekt:
Funktionen lesen und schreiben diese Daten. Ein Zugriff auf z.B.
ist dabei 4 Byte lang (lds, sts):
Man kann nun auf die Idee kommen, in Bereichen, die viel auf foo rumnudeln, indirekt auf foo zuzugreifen, denn ein indirekter Zugriff mit ld(d) oder st(d) kostet nur 2 Bytes und ist gleichschnell:
Code:
foo_t * pfoo = &foo;
pfoo->bar = blah;
avr-gcc macht daraus:
Shit! GCC kann zur Compilezeit die Adresse von foo bestimmen und greift direkt nach foo, aber nicht indirekt, wie wir gerne hätten. Meistens ist das auch ok, Adressregister sind begehrt, wie du vom Assembler her weisst. Zudem muss für nen indirekten Zugriff das Adress-Register erst mit der Adresse vorgeladen werden, und das kostet. Für wenige Instruktionen trifft avr-gcc also die beste Entscheidung, zumal wenn zwischen den Zugriffen noch Funktionsaufrufe passieren. Da müsste als Register eines der call-saved genommen werden, und das kann nur Y sein (ABI von avr-gcc, Link von oben), wo dann im Prolog/Epilog noch Stack-Zugriffe hinzukommen.
Wir brauchen also einen Weg, um zu erzwingen, daß die Adresse von foo im Register bleibt. Das geht, indem wir die Information, daß es sich dabei um die Adresse von foo handelt, wieder vernichten! Dazu habe ich ein Inline-Asm geschrieben (Geht für alle gcc, nicht nur für avr-gcc):
Code:
#define RELOAD(reg,var) \
__asm volatile (";RELOAD " reg " with " #var : "=" reg (var) : "0" (var))
Nachdem der Präprozessor darüber gerauscht ist, etwa über
Code:
RELOAD ("z", pfoo);
sieht's nicht mehr ganz so kryptisch aus. Im wesentlichen bleibt davon übrig
Code:
__asm volatile (";RELOAD z with pfoo" : "=z" (pfoo) : "0" (pfoo));
was sagt, daß pfoo in ein Register der Klasse "z" (also Z) geladen werden soll. Dann wird das asm in den Code eingefügt (Kommentar) und Z bekommt für GCC einen nicht nachvollziehbaren Wert, der aber der gleiche ist wie vorher, weil ja nix passiert im asm.
Code:
foo_t * pfoo = &foo;
RELOAD ("z", pfoo);
pfoo->bar = blah;
gibt dann
Code:
ldi r30,lo8(foo)
ldi r31,hi8(foo)
/* #APP */
;RELOAD z with pfoo
/* #NOAPP */
std Z+6, r24
Aus Systemsicht segmentiert man das RAM und benutzt Z bzw. Y als Data Segment Pointer. Zum Glück hat man die Wahl, ich würd nicht freiwillig auf ner segmentierten Arechitektur proggen. Für AVR war ursprünglich sogar ein segmentiertes Layout geplant, aber das erwies sich als zu schwer handhabbar für Hochsprachen.
Hier mal ein Beispiel aus dem wahren Leben
Code:
dcf_error_t dcf_check_time()
{
// Eine neue Minute hat angefangen:
// DCF-Zeit auf Fehler prüfen und
// in time_dcf-Struktur kopieren, wenn ok.
dcf_t *z = &dcf;
RELOAD ("z", z);
if (parity_even_bit (z->time_bits.minute ^ z->time_bits.minute_parity))
return BAD_PARITY_M;
if (parity_even_bit (z->time_bits.hour ^ z->time_bits.hour_parity))
return BAD_PARITY_H;
if (parity_even_bit (z->time_bits.date_parity
^ z->time_bits.day ^ z->time_bits.day_of_week
^ z->time_bits.month ^ z->time_bits.year
))
return BAD_PARITY_D;
z->time.second = 0;
z->time.minute = z->time_bits.minute;
z->time.hour = z->time_bits.hour;
z->time.year = z->time_bits.year;
z->time.day = z->time_bits.day;
z->time.day_of_week = z->time_bits.day_of_week;
z->time.month = z->time_bits.month;
return DCF_OK;
}
time und time_bits sind unterschiedlich aufgebaut und können daher nicht mit z->time = z->time_bits kopiert werden, ausserdem befinden sich schon viele Werte in Registern.
Mit RELOAD hat das 130 Byte, ohne RELOAD sind es 158 Byte, das sind über 20% mehr!
Natürlich hätte man auch die Adresse von foo als Parameter übergeben können, aber das hilft auch nicht immer, weil GCC einfach zu schlau ist...
Bei den meisten Funktionen schreibe ich die Zugriffe also mit -> , das ist nur eine Zeile Code mehr, um die Adresse zu besorgen. Der generierte Code sieht aber genauso aus, wie mit direktem Zugriff wie oben erklärt, und man vergibt sich nix dabei. Ein Blick ins asm erklärt schnell, ob sich ein explizites RELOAD lohnt.
Hab eben mal die Statistik zu meinem momentanen Projekt geschaut: Es hat über 3900 Zeilen spartanisch kommentierten C-Code, braucht aber nur knapp 7kByte Flash des ATmega8. Und da kommt noch einiges mehr rein! (ein grösserer µC kommt nicht in Frage).
Lesezeichen