Hallo Ich benutze beruflich wie auch privat den CodeVision C-Compiler. Prozesoren: ATtiny26, ATMega32, ATMega128. Nun möchte ich auf Eclipse mit WinAVR/AVR-GCC als Compiler umsteigen. Dabei interessiert mich hauptsächlich die bessere IDE von Eclipse und die möglichkeiten Plugins einzubinden wie z.B. CVS oder ähnliches. Zu Testzwecken habe ich ein bestehendes Project, welches mit CV geschrieben wurde auf WINAVR angepasst (Änderung von Interruptaufrufe, Includes, Bitvarialen, Flashkonstanten, EEPromzugriffe). Nach dem Compilieren mußte ich aber feststellen, das der Code um ~18% größer geworden ist. CodeVision, Ooptimierung max.Size, 1805 Bytes WINAVR mit -Os, 2132 Bytes Auch die Optionen -O1 bis -O3 (ohne -Os) haben keine Verringerung der Codegröße gebracht. Da das Projekt auf einem Tiny26 mit 2k Flash laufen soll, hätte ich schon ein Problem. Im privaten Sector würde ich einfach einen größeren Prozessor nehmen. Aber im beruflichen Sector, wo auch Stückzahlen eine Rolle spielen, erzeugt jeder Cent der für einen größeren Prozessor ausgegeben werden muß entsprechende Mehrkosten. Ist der Compiler beim optimieren einfach schlechter oder warum kommt es zu diesem Ergebnis? Gibt es weitere Compiler die mit Eclipse zusammen arbeiten, aber besser optimieren? Gruß Steffen
Steffen Graap wrote: > Ist der Compiler beim optimieren einfach schlechter oder warum kommt es > zu diesem Ergebnis? Kann man, ohne den Code zu sehen, nichts dazu sagen.
Hallo Jörg Danke für die schnelle Antwort Anbei das gezipte Eclipse-Projekt Gruß Steffen
Kannst du mal die Codegrößen der einzelnen Funktionen von Codevision aufschreiben? Die größte Funktion ist INT0_vect, aber wenn ich mir den generierten Assemblercode ansehe, finde ich da kaum noch Optimierungspotenzial. Gerade in Bereichen wie dem Aufblähen von 8-bit-Zahlen auf 16 bits (integer promotion) ist der GCC hier besser, als ich erwartet hätte (das ist sonst seine schwache Seite). Wenn man es mit -mint8 compiliert, passt es gerade so in die 2 KiB rein. Das könnte für eine derartige Applikation sogar legal sein, aber schick ist es nicht. Mich würde mal interessieren, wo CV hier so viel besser ist, dann guck ich nochmal weiter. Achte bitte auf eine einheitliche Groß-/Kleinschreibung. Wenn die Datei boot.h heißt, sollte dann nicht #include "Boot.h" stehen. Zum Glück war's nur eine Stelle.
Es könnte hilfreich sein, in der ISR einige der "static" Vars vorneweg zu laden und hinten wieder wegzuspeichern. Ziemlich viel Platz wird mit den vielen LDS/STS verbraten, Register sind billiger. Oder: Die statischen und externen Variablen alle miteinander in eine struct packen und per Pointer adressieren. LDD ist nämlich kürzer als LDS.
Im Anhang habe ich mal das Projekt in CodeVision-Style angehängt. In der bm8.lst kann man gut die Zuornung von assembler zu C-Code sehen. Die Größe der Funktion ist INT0_vect in den beiden Compiler wie folgt CV: 410 Byte (von 0x131 - 0x2CB) WINAVR: 634 (608) laut *.s Datei Ich denke eine wichtige Sache ist, das in CV Bitvariablen, soweit wie möglich (bis 104 Bit), in die Prozessorregister gelegt werden. Das hat den Vorteil, das man mit SET/CLT & BLD ein Bit setzen/löschen kann (3Byte) und mit SBIC/SBIS die Abfrage machen kann ohne vorhergehenden Ladebefehl. Das zweite was ich gefunden habe ist so etwas: lds r24,u8Adresse cpi r24,lo8(4) brsh .L57 lds r24,u8Adresse (nicht nötig, kostet 4 Byte) clr r25 subi r24,lo8(-(au8Prog)) sbci r25,hi8(-(au8Prog)) Die option -mint8 ist ein guter Vorschlag. Ich hatte mich schon gewundert warum bei 8bit Variablen immer mit 16bit gerechnet wurde. @ Andreas Kaiser Danke für deine Vorschläge, werde sie berücksichtigen Gruß Steffen
sorry das Laden der Variablen in Register funktioniert aber nur in einer Interrupt-Routine. Hier wird die Abarbeitung ja nicht durch andere Interrupts unterbrochen, sodas die Register überschriben werden könnten. Welche Register sollte man für soetwas benutzen? Hat der Compiler bevorzugte Register? Sollte ein Compiler so etwas nicht von alleine machen (Optimierung)? Gruß Steffen
Richtig, es ging mir nur um die ISR. Aber das ist der mit Abstand grösste Brocken. Er sollte es alleine machen, ja, aber manchmal hilft es, dem Compiler entgegenzukommen. Ausprobieren. Mit Registern meinte ich eher, Variablen die von der Arbeitsweise her "static" sein müssen, vorneweg in "auto" Variablen zu laden, nicht in explizite Register. Wäre es nicht ausgerechnet ein schon angestaubter Tiny26, liessen sich auch die bei neueren AVRs vorhandenen 3 GIOR Bytes im I/O-Space platzsparend einsetzen. Atmel dürfte die als Analogon zum Bitspace der 8051 verstanden haben, für überwiegend bitadressierten Kram sind Bytes im I/O-Bereich ideal.
Was die Optimierung von in Speicher plazierten Variablen sehr effektiv aushebeln kann ist, ist die Gefahr von Aliasing. Vielleicht ist etwas gewonnen, wenn du GCC davon überzeugen kann, dass diesbezüglich von pu8Bytezeiger keine Gefahr ausgeht.
Grund für den bei CV erheblich kürzeren Code ist eine Optimierungstechnik, die im GCC mangels Sinn (Speed) auf Workstations/Servern nicht enthalten ist: Auslagerung kurzer Codesequenzen in kleinste Unterprogramme (bm8.lst: OPTIMIZER ADDED SUBROUTINE). Im Zusammenhang mit GCC ist mir dieser Schritt beim Microchip C30 begegnet (Procedural Abstraction), dort aber als proprietäre Erweiterung in einem separaten Compiler-Pass realisiert. Beispiel: ... variable += 1; // 5 Worte ... variable += 1; ... variable += 1; ... variable += 1; ... ist kürzer, wenn realisert als void incr(void) { variable += 1; } ... incr(); // 1 Wort ... incr(); ... incr(); ... incr(); ...
Steffen Graap wrote: > Das zweite was ich gefunden habe ist so etwas: > > lds r24,u8Adresse > cpi r24,lo8(4) > brsh .L57 > lds r24,u8Adresse (nicht nötig, kostet 4 Byte) > clr r25 > subi r24,lo8(-(au8Prog)) > sbci r25,hi8(-(au8Prog)) Das hast du ihm aber extra so gesagt, indem du einen ganzen Haufen Variablen als volatile deklariert hast. Damit zwingst du den Compiler, sie immer aus dem Speicher zu lesen und sie jedesmal wieder zurück zu schreiben. Ich überblicke die ganze state machine da nicht, aber ich vermute mal ganz stark, dass keine einzige davon volatile sein muss. Wenn überhaupt, dann würde wahrscheinlich ein Synchronisationsflag als volatile genügen. Leider ist das alles recht unübersichtlich, wenn man nur mal so draufguckt. Ich sehe gerade, dass der CV einige der Variablen in globale Register gepackt hat. Das kann er, da er ja immer alle Dateien zusammen als eine einzige C-Datei compiliert und daher nie von externen Moduln ausgehen muss. Das könnte man dem GCC natürlich auch noch nahelegen. Allerdings sind deine Headerdateien ziemlich durcheinander und nicht sauber nach Definition und Deklaration getrennt, daher gelingt mir das jetzt nicht auf Anhieb. Deine EEPROM-Zugriffe stimmen für den GCC übrigens so noch nicht, da musst du explizit eeprom_write_...() aufrufen.
Yep, die volatiles hatte ich glatt übersehen. Aber da passt der Trick mit dem pointer-to-struct rein. Denn damit lässt sich diese optimierungsunfreundliche Eigenschaft recht elegant innerhalb der ISR umgehen, und da ist das ja ohnehin nicht relevant. D.h. wenn der Pointer auf die Daten nicht volatile ist, kann optimiert werden. Und ausserhalb der ISR ist es weiterhin volatile.
Das die ganzen Variablen als volatile deklariert sind, hat den Grund, das im Tutorial für WINAVR steht, das Variablen die innerhalb und außerhalb von ISR benutzt werden, so deklariert werden sollten. In CV brauchte ich dies nicht machen. Mit der Statemachine wird einfach ein Bus (Selectrix) ausgewertet, mit jedem Bustakt wird auf das nächste Bit geschaltet. Das GCC mit den Prozessorregistern recht sparsam umgeht ist mir auch schon aufgefellen. In CV wird R0 und R1 für die Assemblerbefehler, die Register R2 - R15 für Bitvariabeln (Anzahl einstellbar), alle übrigen (außer X, Y und Z, für die globalen Variablen (automatisch oder manuel zugewiesen) genutzt. Das mit den EEPromzugriffen ist mir bekannt, habe ich aber der Einfachheit halber weg gelassen, da der Compiler nicht gemeckert hat. Dies würde den Code aber nochals aufblähen. Gruß Steffen
Steffen Graap wrote: > In CV brauchte ich dies nicht machen. Mag sein, aber GCC ist nicht für Microcontroller optimiert, sondern für High-End Maschinen in denen zeilen/blockübergreifende Optimierung wichtig ist. Und es ist auch nicht GCC oder Jörg, der "volatile" fordert, sondern der C Standard seit 20 Jahren. > Das GCC mit den Prozessorregistern recht sparsam umgeht Er hat bei "volatile" einfach keine Chance, denn das ist letztlich nichts anderes also die Aussage "führe bitte keine Registeroptimierung durch". Er tut damit nur was du ihm sagst. Also: volatile struct S { int var1; int var2; } globale_variablen; und dann in der ISR: struct S *gvars = (struct S *) &globale_variablen; ... gvars->var ... und der Spuk dürfte vorbei sein. In der ISR selbst besteht für "volatile" kein Grund, solange du auf nested interrupts verzichtest.
Naja, ist ziemlich unsauber. Ich würde eher dafür plädieren, volatile nur dort zu benutzen, wo es wirklich notwendig ist. Allerdings ist diese Abwägung zuweilen ziemlich schwierig. Normalerweise sollte es komplett genügen, eine Art Semaphore volatile zu haben und den Rest nicht.
> Naja, ist ziemlich unsauber. Es ist geringfügig umständlich in der Handhabung und vielleicht nicht allzu schön, aber m.E. durchaus sauber, was den Standard angeht. > Normalerweise sollte es komplett genügen, eine Art Semaphore volatile > zu haben und den Rest nicht. Hilft in diesem Fall rein garnicht. Denn an der Optimierung von Variablen ändert keine Semaphore irgendwas. Nur die Problematik bei nicht-atomarem Zugriff lässt sich per Sema erledigen. Und was ISRs angeht, hilft sie auch da nicht, denn was machst du in der ISR wenn die Sema grad blockiert ist?
Andreas Kaiser wrote: >> Naja, ist ziemlich unsauber. > Es ist geringfügig umständlich in der Handhabung und vielleicht nicht > allzu schön, aber m.E. durchaus sauber, was den Standard angeht. Meine Lesart des Standards ist, dass das Ergebnis des ,,Wegcastens'' eines type qualifiers nicht definiert ist. >> Normalerweise sollte es komplett genügen, eine Art Semaphore volatile >> zu haben und den Rest nicht. > Und was ISRs angeht, hilft sie auch da nicht, denn was machst du in der > ISR wenn die Sema grad blockiert ist? Ich meinte mehr so eine Art Flag, das im main context gepollt werden kann und sicherstellt, dass man auf konsistente Daten zugreift. Eigentlich braucht man volatile doch nur für Dinge, bei denen kein sequence point dazwischen liegt, also das typische Flag, auf das man in main() pollt. U. U. ist gerade für so eine einfache Applikation eine "memory barrier" sinnvoller zu handhaben als eine Unmenge an volatile-Variablen. Die "memory barrier" wird implementiert mit:
1 | asm volatile(:::"memory"); |
Sie zwingt den Compiler, danach alle Werte, die aus dem Speicher gelesen worden sind, neu zu evaluieren. Leider kann man sie meiner Meinung nach nicht auf Register anwenden (falls man globale Registervariablen benutzt). Ich bin mir nicht ganz sicher, ob ein vergleichbarer Trick für solche Variablen benutzbar wäre:
1 | asm volatile(:::"r2"); |
>
1 | asm volatile(:::"memory"); |
Das ist nun aber sehr compilerspezifisch. Und für Ersatz von "volatile" Vars einigermassen umständlich und fehlerträchtig, denn das muss man dann im gesamten Hauptprogramm an den Zugriffen auf solche Variablen unterbringen und es setzt gewissen Vorstellungen von der Arbeitsweise von Compilern voraus. Zudem wirft es alle speicherbezogenen Optimierungen über Bord, also auch die harmlosen Variablen. Wenn wegcasten von qualifiern nicht definiert ist, wie gibt man dann einen String aus (d.h. übergibt ihn an eine Funktion, die "char *" erwartet)? Immer erst in eine lokale Variable umkopieren? Ist mir noch nirgends begegnet. Dafür aber jede Menge casts, um "const" loszuwerden weil die aufgerufene Funktion irgendwann in grauer Vorzeit ohne "const" deklariert wurde.
Andreas Kaiser wrote: > Wenn wegcasten von qualifiern nicht definiert ist, wie gibt man dann > einen String aus (d.h. übergibt ihn an eine Funktion, die "char *" > erwartet)? Immer erst in eine lokale Variable umkopieren? Ja, das ist der einzig saubere Weg, den ich im Standard sehe. Das ist auch nicht verwunderlich, ist ja schließlich ein Standard, und der soll die Eckpunkte markieren. Letztlich deutet die Situation, die du beschreibst, ja auf einen Designfehler hin (gerufene Funktion ändert zwar die Daten nicht, wurde aber nicht sauber dahingehend deklariert, sodass man mit einem Hack arbeiten muss). Dein Cast tötet den Compilertest aber auch für die Fälle, in denen es wirklich kracht, d. h. die gerufene Funktion ist als non-const char * deklariert, wird mittels typecast aus einem const char * gefüttert, hackt aber anschließend tatsächlich auf dem String herum (wozu sie gemäß ihrer Deklaration berechtigt ist) -> peng! Da das ,,Wegcasten'' des qualifiers aber ein Designfehler ist, würde ich dies niemandem für ein Neudesign empfehlen.
Danke für eure Antworten. Ich habe die volatile Deklaration wieder entfernt, da ich sie nicht benötige. Da ich meine C Kenntnisse mit CodeVision entwickelt habe, bin ich nicht so fit in ANSI-C. Aufgrund des Hinnweis im Tutorial habe ich die deklaration einfach eingefügt ohne weiter darüber nach zu Denken. Mittlerer Weile bin ich bei einer Codegröße angelangt, die 10% größer als bei CodeVision ist. Für mich eine vertretbare Größe. Über die Firma in der ich arbeite werden wir mal versuchen ob Pavel (Entwickler von CodeVison) eine Komandozeilen-Version seines Compilers mitanbietet, die dann in die Eclipseumgebeung eingebunden werden kann. Das währe für mich das Optimum (kleiner Code, max. Flexibilität der IDE). Gruß Steffen
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.