Hallo Leute,
ich bewege mich aktuell noch auf der toolchain-Ebene. Dabei kommen mehr
und mehr Fragen auf.
gcc übersetzen geht, Assemblerausgabe des Compilers sieht brauchbar aus.
Nun fehlt ja noch was: die Laufzeitumgebung bzw. C-Standard lib.
CMSIS von STM bringt einen Satz an .h und .c mit, auch Startupfiles. So
wie ich es verstehe, sollte das Startup + CMSIS-Dateien + eigene Quellen
ein lauffähiges Programm ergeben, halt ohne jegliche
C-Standardfunktionen.
Dass gcc bzw. ld über fehlendes _start meckert, liegt an meinem
Linkerscript?
Sooo, nun möchte ich jedoch auch mal ein wenig Komfort und grundlegende
C-Standardfunktionen verwenden. Habe dazu die newlib übersetzt, soweit
so gut. Allerdings erzeugt gcc dann durchaus üppige Programm (Mindestens
6K, und ordentlich RAM-Verbrauch). Irgend wie passt dann meiner Meinung
nach auch das CMSIS-Startupfile nicht mehr dazu. Ist es korrekt, dass
das CMSIS-Startupfile nicht mehr benötigt wird, da ja die
newlib-Startupfiles das ganze Handling übernehmen (und noch viel mehr)?
Letzte Frage: Lässt sich der newlib-Code noch optimieren, oder sollte
ich bei kleineren Programmen einfach nur CMSIS + zugehörige startup
verwenden?
Gibt es noch andere Libs außer newlib, die einen kleineren Footprint
haben. In meiner Erinnerung erzeugt die avrlibc zum Beispiel deutlich
kleinere Minimalprogramme (nützt mir halt leider nichts).
So, ich hoffe, ich konnte einigermaßen meine Probleme verdeutlichen und
freue mich über jede Antwort.
Zunaechst mal, ich hab keinen blassen Schimmer von dem Cortex Zeugs. Ich
benutze Renesas. Aber das ist kein Grund ahnungslos zu sein. :-)
> Nun fehlt ja noch was: die Laufzeitumgebung bzw. C-Standard lib.
Was dir fehlt ist ein Linkerscript und Startupcode. Eventuell auch
Libaries wenn du sie verwenden willst.
> Dass gcc bzw. ld über fehlendes _start meckert, liegt an meinem> Linkerscript?
Dein Linker erwartet eine funktion _start welche als erstes nach einem
Reset des Controllers ausgefuehrt wird. Bei PCs wird sowas normalerweise
von der Laufzeitumgebung bereitgestellt. Bei Microcontrollern ist es
eher deine Aufgabe sowas zu machen. Du koenntest z.b selber soetwas in
Assembler (oder C?) programmieren.
In dieser Funktion musst du folgendes bereitstellen:
1. Notwendigste Basisintialisierung.
Stackpointer init
Eventuell /CS fuer externes Ram bereitstellen.
Waitstates wenn dein Proz ein lahmer Hobel ist
2. initialisierte Variablen aus Flash ins Ram kopieren
3. normale Variable auf Null initialisieren
4. Vielleicht den Stackbereich mit einer Magicnumber
fuellen. (dann kann man spaeter den Stackverbrauch
pruefen)
5. main aufrufen.
> Sooo, nun möchte ich jedoch auch mal ein wenig Komfort und> grundlegende C-Standardfunktionen verwenden. Habe dazu die> newlib übersetzt, soweit so gut. Allerdings erzeugt gcc> dann durchaus üppige Programm (Mindestens 6K, und ordentlich> RAM-Verbrauch).
Da kann ich jetzt nichts zu sagen, allerdings entspricht das nicht
meiner Erfahrung. Aber natuerlich wenn du erstmal printf aufrufst
dann darfst du dich nicht wundern wenn dein Code fett wird.
Und natuerlich geht Ram fuer Stack oder gar Heap verloren.
Ausserdem solltest du Linkerscript und StartUp Code wirklich selber
erzeugen oder fuer deine Hardware anpassen. Defaultmaessig kann es sein
das der Compiler/Linker Standardfiles in seinem Dateibaum verwendet. Es
ist aber viel besser man hat lokale Kopien in seinem Project weil man
die dann genau an sein Projekt anpassen kann. (z.B Stackgroesse)
Meine LDFLAGS sehen z.B so aus:
LDFLAGS = -nostartfiles -o start.elf start.o
-Wl,-Map=mapfile.txt -T m30624fgafp.ld
Dann enthaelt mein start.o die Startfunktion und ich uebergebe mein
lokales Linkerscript. Und es schadet nicht das mapfile gruendlich zu
lesen. .-)
Olaf
High Performer schrieb:> Dass gcc bzw. ld über fehlendes _start meckert, liegt an meinem> Linkerscript?
Jein. Linkerscript und startupcode müssen zusammenpassen. Wenn das
script ein label _start erwartet, muß das im startupcode auch vorhanden
sein. Wenn das dort anders heisst, musst du das im linkerscript
anpassen.
High Performer schrieb:> Ist es korrekt, dass> das CMSIS-Startupfile nicht mehr benötigt wird, da ja die> newlib-Startupfiles das ganze Handling übernehmen (und noch viel mehr)?
Die newlib ist völlig prozesorunabhängig, die kann gar keine passenden
startupfiles für deinen oder irgend einen anderen Prozessor mitbringen.
Und das tut die, soweit ich mir erinnere, auch nicht.
Oliver
- Newlib ist de-facto-Standard für kostenlose ARM-Toolchains auf Basis
von GNU gcc, binutils. Soweit ich weiss, liegt das an der Lizenz, die
wenig restriktiv ist. Es gibt u.a. uc-libc aber die ist meines Wissens
mit restriktiverer Lizenz (die ehemals von Keil vorkompiliert angebotene
GNU Toolchain kam mit dieser libc, war vor Jahren mein erster und
einziger Kontakt mit uc-libc). Evtl. gibt es im BSD-Umfeld noch etwas.
Alles in Allem ist die newlib aber schon brauchbar. stdio mit FP-support
allerdings sehr "fett". Nur mit Ganzzahlunterstützung nicht mehr ganz so
resourcenhungrig (iprintf statt printf)
- Falls "fehlendes _start" im Kontext mit missing entry steht, dann
liegt das am Linker-script und/oder am Startup-code. Entweder ist im
Linkerscript ein entry-Eintrag mit _start als Parameter oder _start ist
ein default. Entry-Point Angabe im Linkerscript soll mit dem Symbol
übereinstimmen, bei der die Anhängigkeitskette beginnt (Oft "reset
handler"). Wichtig ist die Angabe des Entry-Points vor allem bei "unused
code removal" (-ffunction-sections), falls im Linkerscript nicht
explizit per KEEP sections davon ausgenommen werden. Im ungünstigsten
Fall findet der Linker sonst keinen Startpunkt und sieht keinen "used
code". Dem Genzen geht man aus dem Weg, in dem die Vektortabelle einer
eigenen Section zugewiesen wird und diese per KEEP im Linkerscript als
"nicht-aufräumbar" markiert wird. Muss man ohnehin tun, damit zumindest
einem Minmaltabelle an der Speicherstelle steht, an der die Hardware
nach Reset sucht). Da in dieser Tabelle bei den mir bekannten
ARM-Controllern das Symbol für den Resethandler enthalten ist, kann
nichts mehr schief gehen.
- Wenn nicht anders angegeben, wird ein default Linker-script der Newlib
verwendet, sodenn diese installiert ist (das Skript kann man sich mit
arm-*-ld --verbose anschauen). Habe es bisher nur als Inspirationsquelle
genutzt aber nie direkt verwendet. Soweit erinnert, ist das nicht für
getrennte Speicherbereiche für read-only und read-write vorgesehen.
Dieses Linkerscript ist für die Verwendung mit der crt0 aus der newlib
vorbereitet. Zu beachten ist noch, dass wenn die toolchain mit
multilib-support compiliert ist (üblich), einige Linkerscripte und
Startupdateien existieren können.
- Funktionen aus der newlib (oder anderer libc) kann man auch verwenden,
wenn man eigene oder von einem Hersteller bereitgestellte Linkerscript
und startup-code nutzt. Wurde ja bereit von Olaf(Gast) erläutert (mit
-nostartfiles verhindern, das die Standard-crt0 genutzt wird, mit -T das
eigene Linkerscript vorgeben, dann per -lc dem Linker mitteilen, dass
auch in der libc nach Abhängigkeiten gesucht werden soll. (Und immer den
Linker über das Frontend rufen, also per arm-*-gcc ... , nie den Linker
arm-*-ld direkt, sonst gibt es nur unnötigen Stress.)
- Einige Funktionen der newlib, u.a. printf/iprintf benötigen
Hilfsfunktionen, mit denen die Verbindung zur Hardware hergestellt wird:
sogen. syscalls. Beim letzten Mal nachsehen - für Target ARM7TDMI -,
waren diese in den newlib-Quellen im Bereich ARM so implementiert, dass
software-interrupt aufgerufen werden (SWI). Hat man einen passenden
SWI-Handler (oder eine Debug-Umgebung, welche die SWIs "umbiegt", kann
man auch mit der syscall Implementierung aus der newlib arbeiten - ist
aber nicht grade die resourcenschonenst Implementierung. Am Besten
eigene syscalls bereitstellen.
Martin Thomas schrieb:> Evtl. gibt es im BSD-Umfeld noch etwas.
Die newlib ist zu großen Teilen eine BSD-Bibliothek. Allerdings ist
BSD halt auf größeren Maschinen groß geworden als heutigen Controllern,
folglich konnte man sich mehr Featuritis leisten. Möglicherweise
könnte man stattdessen auch ganz alte UNIX-Bibliotheken stattdessen
anpassen, nachdem die alten UNIX-Quellen ja mittlerweile frei zugänglich
sind; bis V7 UNIX, wobei es sinnvoller sein dürfte, das auf V6/V7
aufsetzende 2BSD stattdessen zu verfolgen. Dessen letzte Inkarnation
namens 2.11BSD hat vom "Feeling" schon einiges von 4.4BSD geerbt (beide
stammen von Anfang der 1990er Jahre), ist aber halt trotzdem noch ein
System für PDP-11-Maschinen, die wohl mittlerweile performancemäßig
selbst ein AVR mit externem Speicherausbau übertreffen dürfte.
Alles in allem war das aber der Grund, warum wir bei der AVR-Toolchain
stattdessen die avr-libc separat gezimmert haben. Die hat an einigen
wenigen Stellen auch von BSD geerbt, aber eben vieles auch neu
implementiert mit besonderem Augenmerk auf sparsame Ressourcennutzung.
Leider ist das natürlich dann komplett AVR-zentrisch geschrieben, da
der Scope des Projekts nie eine Multiplattformfähigkeit beinhaltet
hat.
> Alles in allem war das aber der Grund, warum wir bei der> AVR-Toolchain stattdessen die avr-libc separat gezimmert haben.
Ist das Problem bei soetwas nicht das man bei der Entwicklung immer von
den Moeglichkeiten der neueren Prozessoren gejagt wird? Mir ist
jedenfalls noch nie der Flash ausgegangen.
Olaf
Olaf schrieb:> Mir ist> jedenfalls noch nie der Flash ausgegangen.
Dann benutzt du zu große Controller. :-)
Rein vom Code hat sich bei AVR nicht so viel in den letzten 10 Jahren
geändert, dass man deshalb unbedingt den alten Bibliothekscode groß
überarbeiten müsste. Selbst der Xmega brachte da keine großen
Einschnitte. Die reine Hardware jenseits des CPU-Kerns ist natürlich
eine ganz andere als vor 10 Jahren, aber das berührt die Bibliothek
nur wenig.
>Was dir fehlt ist ein Linkerscript und Startupcode. Eventuell auch>Libaries wenn du sie verwenden willst.
die CMSIS (ST-Version) liefert einen startupcode passend zum Prozessor.
Dieser Startupcode ist relativ einfach in Assembler gestrickt.
>Dein Linker erwartet eine funktion _start
Weil das so im Linkerscript steht? Und ist das ein Standard des Linkers,
so dass in jedem Linkerscript _start definiert sein muss?
>welche als erstes nach einem>Reset des Controllers ausgefuehrt wird. Bei PCs wird sowas normalerweise>von der Laufzeitumgebung bereitgestellt. Bei Microcontrollern ist es>eher deine Aufgabe sowas zu machen.
Das macht z.B. der Startupcode der CMSIS, der einfach die Adresse von
main in den Resetvektor einträgt (beim Überssetzen natürlich).
>1. Notwendigste Basisintialisierung.> Stackpointer init> Eventuell /CS fuer externes Ram bereitstellen.> Waitstates wenn dein Proz ein lahmer Hobel ist>>2. initialisierte Variablen aus Flash ins Ram kopieren>>3. normale Variable auf Null initialisieren>5. main aufrufen.
Genau das macht der Startupcode der CMSIS.
>Da kann ich jetzt nichts zu sagen, allerdings entspricht das nicht>meiner Erfahrung. Aber natuerlich wenn du erstmal printf aufrufst
neee, ein nacktes main()
[...]
>Dann enthaelt mein start.o die Startfunktion und ich uebergebe mein>lokales Linkerscript. Und es schadet nicht das mapfile gruendlich zu>lesen. .-)
Danke für diesen Tipp speziell und Deine Antwort im Allgemeinen!
Hallo Oliver,
>Jein. Linkerscript und startupcode müssen zusammenpassen.
Das ist kein Problem, wenn ich den starupcode der CMSIS verwende, der
nur wenige Zeilen Assembler umfasst.
>Wenn das>script ein label _start erwartet, muß das im startupcode auch vorhanden>sein. Wenn das dort anders heisst, musst du das im linkerscript>anpassen.
Das würde heißen (da ich beim gcc-Aufruf dem ld kein eigenes Script
mitgegeben habe), dass das default-script von ld ein definiertes Label
_start erwartet?
> Ist es korrekt, dass> das CMSIS-Startupfile nicht mehr benötigt wird, da ja die> newlib-Startupfiles das ganze Handling übernehmen (und noch viel mehr)?
Die newlib ist völlig prozesorunabhängig, die kann gar keine passenden
startupfiles für deinen oder irgend einen anderen Prozessor mitbringen.
Und das tut die, soweit ich mir erinnere, auch nicht.
Na ja, es gibt die crt0.o etc. Da ist jede Menge Zeug drin mit ctors,
dtors, exitprocs etc. Genau dieses Zeug bläht mein Minimalprogramm
derart auf. Lässt sich das irgend wie einschränken. Ich brauche keine
exitprocs und so'n Zeug auf einem klitzekleinen Cortex-M3.
Und genau hier liegt mein Verständnisproblem: Wie hängen der startupcode
der CMSIS (der nichts anderes tut als Speicherbereiche zu initialisieren
etc. und dann main aufzurufen) und die crt0 der newlib zusammen,
voneinander ab oder schließen sich gar aus? Mir ist einfach nicht klar,
wie der code in der crt0 überhaupt zum Zuge kommen bzw. mit dem
Startupcode der CMSIS zusammenarbeiten soll. Oder ist der Code in crt0
womöglich gar kein Startupcode sondern es handelt sich einfach um
Helperfunktionen für die restliche libc?
Da ich die Programme aktuell noch nicht praktisch testen kann, weiß ich
natürlich auch nicht, ob insgesamt was sinnvolles rauskommt.
Also ich sehe schon, ich muss das alles nochmal genauer durchforsten.
Bin über jede weitere Info dankbar und werde mich bei weiteren
Erkenntnissen wieder hier melden.
> die CMSIS (ST-Version) liefert einen startupcode passend zum Prozessor.> Dieser Startupcode ist relativ einfach in Assembler gestrickt.
Das ist schoen weil du dann schonmal einen Anfang hast. Ich empfehle dir
aber trotzdem mit einer lokalen Kopie zu arbeiten weil dieses File nicht
nur zum Prozessor passen muss sondern auch zu deiner Anwendung.
Vielleicht willst du ja mal die Stackgroesse aendern, oder die Position.
> Weil das so im Linkerscript steht? Und ist das ein Standard des> Linkers, so dass in jedem Linkerscript _start definiert sein muss?
Hm..hast du schonmal ein Linkerscript gelesen?
Bei mir steht da zum Beispiel das hier drin:
[..]
OUTPUT_FORMAT("elf32-m32c", "elf32-m32c",
"elf32-m32c")
OUTPUT_ARCH(m32c)
ENTRY(_start)
[..]
Auch da liefert dir eines der Pakete (binutils oder newlib)
normalerweise Beispiele mit. Die sind aber nicht nur Prozessorabhaengig
sondern haengen auch von deiner Zielhardware ab. Auch da wuerde ich
empfehlen mit einer lokalen Kopie zu arbeiten damit du Sonderwuensche
einbauen kannst. (z.B eine eigene Section fuer einen Bootloader oder
Dataflash)
> Das macht z.B. der Startupcode der CMSIS, der einfach die Adresse von> main in den Resetvektor einträgt (beim Überssetzen natürlich).
Das geht natuerlich auch. Man muesste auch komplett auf den
Assemblerteil verzichten koennen und alles in C machen koennen. Man
sollte nur daran denken das man keine Variablen nutzen kann und alles
inline deklarieren muss bevor man seinen Stack eingerichtet hat.
> Na ja, es gibt die crt0.o etc. Da ist jede Menge Zeug drin mit ctors,> dtors, exitprocs etc. Genau dieses Zeug bläht mein Minimalprogramm> derart auf. Lässt sich das irgend wie einschränken.
Man muss das ja nicht verwenden. Die crt0 scheint bei dir zu viel Luxus
zu bieten. (C++?) Deshalb mache ich meine Initialisierungen lieber
selber.
Bei mir ist dann ein Programm das aus einem leeren main() besteht nur
56Byte gross. Es besteht dann halt nur aus den wenigen Zeilen Assembler
um den STack zu initialisieren und Variablen zu kopieren.
Aber es gibt natuerlich ein paar Dinge die so ein einfaches Programm
nicht macht. Dein Prozessor hat irgendwo eine dicke Liste mit den
Interruptvektoren? Wo steht die? Wie gross ist die? Vielleicht bindet
dein crt0 da bereits einen default ein?
In deinem mapfile sollte eigentlich drin stehen was genau wieviel Platz
braucht und dann kannst du dir das ja getrennt anschauen und entscheiden
ob es dir wichtig ist.
Oh..und ich koennte mir auch vorstellen das du verschiedene crt0 zur
Auswahl hast. Es koennte einen Unterschied sein ob man ein fettes ARM
system hat auf dem man gleich Linux installiert oder einen kleinen
Minicontroller.
Olaf
Hallo Olaf,
zuerst einmal Dir und den anderen vielen Dank für die Antworten.
habe mir mal die xref und map angeschaut. Also ich denke, ich kann das
ganze nicht so einfach kleiner machen. Die Funktionen, die in crt0, crti
etc. vorhanden sind, werden von den newlib-Funktionen genutzt. Neben den
syscalls ist ein Haufen anderes Zeug drin. Ich denke mal, wenn ich das
alles rauslasse bzw. durch stubs ersetze, wird die newlib wohl nicht
funktionieren.
Schon alleine für malloc braucht es eine Menge overhead, und ich denke,
viele der Standardfunktionen verwenden malloc etc. Werde aber nochmal
recherchieren
Ich weiß noch immer nicht so richtig, wie ich da ran soll. Bei Interesse
sende ich mal die interessanten Auszüge aus der map bzw. xref.
Darf ich Dich mal nach Deiner Laufzeitumgebung fragen?
Gruß
> Schon alleine für malloc braucht es eine Menge overhead, und ich denke,> viele der Standardfunktionen verwenden malloc etc. Werde aber nochmal> recherchieren
Ja, nun wenn du den vollen Luxus nutzen willst dann wird dein Binary
eben ein paar Kilobyte groesser. Wo ist das Problem? Wer in einem
controller malloc nutzen will der muss sowieso etwas richtig grosses
vorhaben oder nicht?
> Darf ich Dich mal nach Deiner Laufzeitumgebung fragen?
Ich verstehe die Frage nicht.
Olaf
Hallo Olaf,
>Ja, nun wenn du den vollen Luxus nutzen willst dann wird dein Binary>eben ein paar Kilobyte groesser. Wo ist das Problem? Wer in einem>controller malloc nutzen will der muss sowieso etwas richtig grosses>vorhaben oder nicht?
Ja, das ist mir schon klar. Bei mir war es allerdings so, dass auch
ein Programm mit leeren main() schon 6 kByte groß war. Es wurden nämlich
eine Menge Funktionen, u.a. malloc etc. eingebunden.
Nun, ist bin einen riesigen Schritt weiter: gcc mit -nostartfiles
aufgerufen bewirkte Wunder! Ich habe jetzt zusätzlich das (kleine)
Startupfile der STM-CMSIS eingebunden und ein Linkerscript gebastelt.
Nun sieht vom erzeugten Code her alles prima aus, und ein einfaches
Programm mit ein paar einfachen Standardfunktionsaufrufen (z.B.
strlen()) ist nur noch wenige hundert Bytes groß. Das meiste davon
braucht die Initialisierungsfunktion der CMSIS, die man sich auch noch
sparen könnte. Also soweit alles im grünen Bereich!
>> Darf ich Dich mal nach Deiner Laufzeitumgebung fragen?>> Ich verstehe die Frage nicht.
Ganz einfach: ich wollte wissen, welche Konfiguration bei dir so kleine
Programme erzeugt, damit ich einen Ansatz habe, wie es funktionieren
könnte.
Aber das hat sich ja jetzt erledigt.
Vielen Dank nochmal für Deine/Eure Unterstützung. Mal sehen, wie's
weitergeht. Werde mich sicher nochmal hier melden ;-)
Viele Grüße
Olaf schrieb:> Wer in einem> controller malloc nutzen will der muss sowieso etwas richtig grosses> vorhaben oder nicht?
Die stdio-Implementierung dürfte standardmäßig ein malloc()
referenzieren, denn irgendwoher muss sie ja ihren Pufferspeicher
beziehen. Diese Referenz ist vermutlich selbst dann noch da, wenn
man mit setbuf/setvbuf einen expliziten statischen Puffer dann
zuweist, denn dass man das vorhat, kann das stdio-Subsystem ja zur
Compilezeit nicht ahnen.
Allerdings könnte man dafür ein dummy-malloc() mitliefern, das weiter
nichts macht, als in eine Endlosschleife zu rennen, sodass man das
ggf. mit dem Debugger schnell finden kann, ob malloc() doch noch aus
Versehen gerufen wird. In der nicht-Debug-Variante gibt die Funktion
dann immer 0 zurück (malloc failed).