Hallo, ich würde gerne den Arm Cortex M0+ von Grund auf verstehen, es
gibt ja das Datenblatt, aber ein Tutorial würde es mir schon leichter
machen, hat jemand schonmal dasselbe gemacht und kann mir etwas
empfehlen?
Grundaätzlich will ich natürlich nicht in Assembler programmieren, beim
Startupcode geht es aber wohl nicht ohne, das ist ok.
doll schrieb:> Grundaätzlich will ich natürlich nicht in Assembler programmieren, beim> Startupcode geht es aber wohl nicht ohne, das ist ok.
Den kann man auch in C schreiben, ist aber eher unüblich. Fange am
Besten mit dem Beispielcode des Herstellers an, in welchem sich
Startup-Code, Linkerscript, Takt-Konfiguration usw. befinden. Komplett
bei 0 anzufangen ist zäh.
Ich habe ein NXP Devboard hier. Da ist der komplette Startup Code in C
bzw eingebundenem Assembler verfügbar.
Wird mit bei MCUXpresso Development Paket geliefert. Ist kostenlos, man
muß sich nur registrieren.
Letztendlich ist der Startup Code nur 2 Seiten lang, darin wird dann
schon main() aufgerufen.
Die Hello-World (oder LED-Blinky) Beispiele des GnuARM-Plugins für
Eclipse verwenden auch kein Assembler für den Startup-Code.
M0 wie zB STM32F07 wird unterstützt.
Mein Startup-Code ist in C++ geschrieben... keine Ahnung was daran nicht
gehn soll. std::copy ist aussagekräftiger als jeder Loop das je sein
kann. Ansonsten muss man auch noch definieren was "minimal" heißen soll.
Minimal heißt für mich nämlich sowas hier
1
.syntaxunified
2
.thumb
3
.archarmv6-m
4
5
.section.isr_vector
6
.word_estack
7
.wordstartup
8
9
.section.text
10
startup:
11
bmain
Das beinhaltet aber natürlich nicht die sonst notwendigen Schritte:
- .data kopieren
- .bss räumen
- .preinit/init array Funktionen aufrufen
/edit
typo
Vincent H. schrieb:> Mein Startup-Code ist in C++ geschrieben...
Theoretisch könnte man sagen, dass während der Ausführung des
Startup-Codes noch keine korrekte Laufzeit-Umgebung existiert
(.data/.bss nicht initialisiert usw) und daher nicht garantiert ist,
dass "richtiger" C(++) Code funktioniert. Da allerdings std::copy (oder
memcpy) höchstwahrscheinlich nicht auf irgendwelche globalen Daten
zugreift, dürfte das keine Rolle spielen und "einfach funktionieren".
Die System Workbench for STM32 generiert recht übersichtlichen
Startup-Codee für C Projekt. Ich habe ihn mal angehängt.
Nachtrag: ich glaube die sysmem.c ist für deine Frage irrelevant.
Stefanus F. schrieb:> Die System Workbench for STM32 generiert recht übersichtlichen> Startup-Codee für C Projekt. Ich habe ihn mal angehängt.
Was mich an diesem Code schon lange juckt: Er ist für ARMv6M geschrieben
und damit für den Cortex-M0(+) geeignet, wird aber von ST auch für die
ARMv7M-Controller (Cortex-M3/4) verwendet. Da diese aber Post-Indexing
unterstützen, geht das so effizienter:
1
ldr r0, =_sdata
2
ldr r1, =(_edata-4)
3
ldr r2, =_sidata
4
5
cmp r0, r1
6
bhi 2f
7
1:
8
9
cmp r0, r1
10
ldr r3, [r2], #4
11
str r3, [r0], #4
12
bne 1b
13
14
2:
15
ldr r0, =_sbss
16
ldr r1, =(_ebss-4)
17
movs r2, #0
18
19
cmp r0, r1
20
bhi 2f
21
1:
22
cmp r0, r1
23
str r2, [r0], #4
24
bne 1b
25
2:
Lokale Labels, weil die "richtigen" Labels im ST-Code das Disassembly
komisch aussehen lassen. Die "cmp" Instruktion wird jeweils einmal zu
oft ausgeführt, aber das ist schneller als ein zusätzlicher Sprung.
Indem man das "cmp" weit vor den bedingten Sprung schiebt, wird die
Pipeline ggf. besser ausgenutzt. Durch Nutzung des Post-Index spart man
Instruktionen. Es wird genau wie bei ST vorausgesetzt dass die
Linker-Symbole Vielfache von 4 sind. Könnte man annehmen, dass das
Alignment noch größer ist, könnte man LDM/STM nutzen, was dann noch
schneller wäre.
Natürlich wird dieser Code nur 1x beim Hochfahren ausgeführt, aber je
nach Anwendung kann es wichtig sein dass der Controller schnell startet.
Ggf. könnte man dafür die PLL vorher schon starten, aber das ist ein
anderes Thema :)
Hm vielleicht sollte ich doch lieber mit avr beginnen, lese gerade das
Make AVR Buch, ich habe selten was gelesen wo so gut und ausführlich
erklärt wird. Sorry dass ich grade so umschwenke, ist voll am
Threadthema vorbei.
Niklas G. schrieb:> Den kann man auch in C schreiben, ist aber eher unüblich.
Das war mal so, aber bei Cortex-M ist Assembler dafür weder nötig noch
üblich. Mit einer Ausnahme: wenn man dabei auch einen Speichertest des
onchip-RAM haben will, das geht nicht in C. Aber so ein Test ist auch
nicht üblich.
doll schrieb:> ese gerade das> Make AVR Buch, ich habe selten was gelesen wo so gut und ausführlich> erklärt wird.
Auch für Cortex M0 gäbe es gute Bücher, z.B:
"The Definitive Guide to ARM® Cortex®-M0 and Cortex-M0+ Processors" von
Joseph Yiu
Ich finde dass die beim Programmieren in C wegen der 32 Bit deutlich
näher am PC dran sind als die alten 8-Bitter.
Aber dass muss der OP letztlich selbst entscheiden...
Stefanus F. schrieb:> Ich denke schon, das Startup-Code in Asembler durchaus üblich ist,> jedenfalls bei ST, denn deren Code Templates machen das alle,> ausnahmslos.
Und was will man denn da unbedingt in Assembler machen? Der Stackpointer
ist automatisch aufgesetzt, weil der mit dem Inhalt von ROM-Adresse 0
initialisiert wird. Dafür kann man einfach ein statisches Array
deklarieren.
Die Vektortabelle ist auch bloß ein Array mit entsprechender Option für
den Linker, wo die hinsoll. Der Resetvektor ist eine Funktionsadresse,
die in dieses Array gelegt wird. Ach ja und die Adresse des Stack-Arrays
ist der erste Eintrag in diesem Zeiger-Array.
Tja und schon geht's in der Resetfunktion los. BSS nullen geht mit lokal
angelegten Zeigern, vorinitialisierte Variablen rüberkopieren ebenfalls,
beides mit Infos aus dem Linkerscript. Den Stackbereich muß man nicht
nullen. Dann wird auch schon main() aufgerufen.
Nop schrieb:> Und was will man denn da unbedingt in Assembler machen?
Was will man da unbedingt in C machen? Startup Code ist nun einmal
traditionell in Assembler geschrieben und bei diesen wenigen Zeilen
bricht sich auch niemand einen Zacken aus der Krone.
Dass es auch in C geht, stellt hier niemand in Frage.
Du kannst dich ja gerne bei ST als Consulter bewerben, mit deinem
grandiosen Verbesserungsvorschlag.
Stefanus F. schrieb:> Was will man da unbedingt in C machen? Startup Code ist nun einmal> traditionell in Assembler geschrieben
Ich fragte, was genau man da in Assembler macht. Was man in C macht,
habe ich beschrieben. Macht man also genau dasselbe in Assembler? Eine
Kopierschleife, eine Nullungsschleife, dann branch nach main?
Ein Grund, wieso man das nicht in Assembler macht, ist in
professionellem Umfeld z.B. die Anforderung, daß sowenig wie möglich
Code in Assembler geschrieben werden darf. Also das, was in C nicht
möglich ist oder nicht performant genug. Hintergrund ist, daß
Assemblercode ganz generell schlechter lesbar und schlechter wartbar ist
als C-Code und man sich daher auf das Unumgängliche beschränkt.
Nop schrieb:> Hintergrund ist, dass> Assemblercode ganz generell schlechter lesbar und schlechter wartbar ist> als C-Code und man sich daher auf das Unumgängliche beschränkt.
Verstehe ich ja, aber wer vor diesen wenigen Zeilen Angst hat, der hat
noch ganz andere Probleme.
Das der Code bei einigen noch asm ist liegt wohl eher in der Historie
der Arm. Das hat man für die thumb/thumb2 Befehlssätze machen müssen.
NXP hat schon lange mit dieser ‚Tradition’ gebrochen und definiert ihn
in C.
Stefanus F. schrieb:> aber wer vor diesen wenigen Zeilen Angst hat
Nein, Du verstehst nicht. Genau gar nicht.
Nur mal so als Randbemerkung, ich habe schon ganze Projekte in Assembler
abgewickelt, aber damals ging das auch nicht anders.
Nop schrieb:> Nein, Du verstehst nicht. Genau gar nicht.
Ich denke doch.
Nur weil Assembler Code schlecht lesbar sein kann und weil man damit
viele Fehler machen kann soltle man es nicht so hart verbannen, dass
man sogar diese wenigen Zeilen vorgegebenen Startup-Code verbietet.
Wenn das wirklich so wahnsinnig wichtig ist, dass ist C ebenso die
falsche Programmiersprache.
Stefanus F. schrieb:> Nur weil Assembler Code schlecht lesbar sein kann und weil man damit> viele Fehler machen kann soltle man es nicht so hart verbannen
Doch, das sollte man, weil sie unnötig sind. Da, wo Assembler auch heute
noch Mehrwert bietet, ist es ja OK, muß aber technisch gerechtfertigt
werden. Andernfalls sind Coding-Richtlinien zahn- und damit sinnlos.
Ansonsten kommt sowas eben nicht durch ein Codereview, wenn die
technische Rechtfertigung so wie bei Dir in "Tradition" besteht.
Eine mögliche technische Rechtfertigung bestünde z.B. darin, daß nach
dem Powerup bestimmte Ausgangs-Pins schnellstmöglich auf einen
bestimmten Wert gesetzt werden müssen und man das deswegen vor der
Kopier- und Nullungsschleife machen muß. Das kann in C Ärger geben, weil
die Umgebung dann noch nicht so aussieht, weil ein C-Compiler das
voraussetzt.
Johannes S. schrieb:> Das hat man für die thumb/thumb2 Befehlssätze machen müssen
Na eben nicht, bei Cortex-M (die haben alle ausschließlich Thumb1/2)
geht das komplett in C. Bei Cortex-A mit "ARM" Instruction Set braucht's
Assembler.
Bei ARM7TDMI weiß ich's jetzt nicht.
Assemblercode ist auch schlechter portierbar. Wenn ein Hersteller den
Startup in Keil-Assembler liefert kann so manch einer damit herzlich
wenig anfangen, außer ihn zu nehmen und nach was anderem zu portieren.
Und wenn dann kann man ihn auch gleich nach C portieren.
Spätestens wenn mans einmal gemacht hat wirds beim nächstem mal
wesentlich leichter, meist unterscheidet sich nur noch die Vektortabelle
und die kann man auch aus nem Keil-Assembler rauskratzen und
umformatieren und im anderen Startupcode implantieren.
Stefanus F. schrieb:> Nop schrieb:>> Und was will man denn da unbedingt in Assembler machen?>> Was will man da unbedingt in C machen?
Wenn jeglicher anderer Code im Projekt C ist, warum soll ich mich dann
für den Startup Code mit Assembler quälen?
> Startup Code ist nun einmal> traditionell in Assembler geschrieben
Klar. Und die Ägypter machten Geschichtsschreibung traditionell mit
Hieroglyphen, die sie in Sandstein meißelten. Tradition ist gar kein
Argument. Für gar nichts.
> Du kannst dich ja gerne bei ST als Consulter bewerben, mit deinem> grandiosen Verbesserungsvorschlag.
ST hat - was Software angeht - schon immer (oder sollte ich sagen:
traditionell?) einen schlechten Geschmack.
Aber gut, in 99% der Fälle kommt der Startup-Code entweder aus einer
einschlägigen Library (ich mag opencm3) oder wird von einem anderen
Projekt kopiert. Genau wie Linkerskript und Makefile. Da ist dann
weitgehend egal, wie das im Detail implementiert ist.
Axel S. schrieb:> Wenn jeglicher anderer Code im Projekt C ist, warum soll ich mich dann> für den Startup Code mit Assembler quälen?
Du musst dich nicht quälen, der Code wird fix und fertig von ST (bzw.
der IDE) bereit gestellt. Warum sollte ich mit damit abmühen, ihn neu zu
schreiben?
Wenn ich wirklich alles neu programmieren will, dann ist C an dieser
Stelle natürlich attraktiver, da bin ich voll bei dir.
> Tradition ist gar kein Argument.
War auch nicht so gemeint. Ich wollte damit nur erklären, warum es
womöglich immer noch Assembler ist.
Woher weißt du, dass dein C Code korrekt funktioniert, bevor du die
passende Umgebung geschaffen hast? Damit bist du doch zu 100% in
undefined behavior. Den Nachweis zu erbringen, dass es dort trotzdem tut
erscheint mir Recht aufwendig. Ich kann dir mit Leichtigkeit eine memset
Funktion schreiben, die vor der Initialisierung der C Umgebung sehr
zufällige Dinge macht.
nfet schrieb:> Woher weißt du, dass dein C Code korrekt funktioniert, bevor du> die passende Umgebung geschaffen hast?
Weil die Funktion nur lokal deklarierte Pointer braucht und der
Stackpointer bereits aufgesetzt ist.
Was undefiniert ist, das ist nur der Zustand aller globaler Variablen
inkl. derer mit function-static, und somit potentiell auch die gesamte
C-Standardbibliothek.
Deswegen auch das Beispiel mit dem raschen Setzen von IO-Pins - da würde
ich mich nicht darauf verlassen, daß die Funktion dazu wirklich nirgends
globale Variablen einbezieht UND daß das auch in alle Zukunft des
Projektes so bleiben wird.
Nop schrieb:> Doch, das sollte man, weil sie unnötig sind. Da, wo Assembler auch heute> noch Mehrwert bietet, ist es ja OK, muß aber technisch gerechtfertigt> werden. Andernfalls sind Coding-Richtlinien zahn- und damit sinnlos.
Das sind sie meistens auch, wenn sie von Dilettanten geschrieben wurden.
.data schrieb:> Und da wartet so einiges:> Beitrag "Re: [C] Böse Falle: Datentyp korrekt angegeben, falscher> verwendet"
Die Lehre daraus ist nicht "Assembler im Startupcode", sondern "verwende
keine vermurksten Deklarationen". Sowas macht man mit Pointern statt
fehldeklarierter Arrays, und das nicht bloß im Startupcode.
Abgesehen davon haben Funktionen der Standardbibliothek (hier konkret:
memcpy) nichts im Startupcode verloren.
Ich finde Startup-Code in Assembler auch gut. Bei C muss ich mir immer
überlegen, was dabei raus kommt bzw. raus kommen darf. Der Optimizer hat
relativ viele Freiheiten und es ist nicht immer ganz trivial die Effekte
zu sehen. So lange man sich im normalen Bereich von C bewegt (d.h. die
Initialisierung ist abgeschlossen) gibt es da normal keine Probleme.
Aber alles was den C-Compiler missbraucht um irgendwas ungewöhnliches zu
tun kann schon mit der nächsten Compilerversion kaputt sein.
Nehmen wir den oben verlinkten Code:
https://github.com/prof7bit/bare_metal_stm32f401xe/blob/master/src/STM32F401XE/gcc_startup_system.c
- Die Vektortabelle muss mittels Attribute an die richtige Stelle
geschoben werden und explizit als benutzt markiert werden. Das ist nicht
portabel.
- Weiterhin darf der Compiler in structs Padding einfügen, d.h. zwischen
initial_stack und vectors könnten noch ein paar Bytes reinkommen. Wird
zwar vermutlich nicht passieren, dürfte aber.
- Die Schleife zum Initialisieren von BSS und Data dürfte vermutlich
wegoptimiert werden. Der Compiler sieht, dass es keine weiteren Zeiger
auf die Arrays gibt (z.B. bei aktiviertem LTO) und entfernt dann die
Schleifen. Ich bin mir aber nicht 100% sicher, wann er das genau darf.
Wenn es keinen weiteren long-Zeiger gibt sollte er es auf Grund der
Aliasing-Regeln sicher entfernen dürfen, wenn es einen gibt evtl. nicht.
Ich will mich aber 100%ig auf meinen Startup-Code verlassen können.
Wenn der Code in ASM geschrieben ist kenne ich genau das exakte
Verhalten.
----
Ein weiteres Beispiel: Ich hab einen Bootloader für Cortex M4
geschrieben. Im ersten Versuch auch komplett in C. Den Sprung zu
Applikation hab ich mittels Funktionspointer umgesetzt, so wie man es
oft sieht (z.B. hier:
https://github.com/akospasztor/stm32-bootloader/blob/master/Src/bootloader.c#L357)
allerdings ohne anschließendes while(1), weil ich weiß, dass ich nie aus
der Applikation zurück komme.
Das funktioniert auch erst mal. Nur hat der Compiler nach einer Änderung
beschlossen in der Funktion ein paar Sachen auf den Stack zu pushen. Und
die wollte er dann auch wieder popen. Und zwar dummerweise genau
zwischen dem ändern des Stackpointers (_set_MSP) und dem Jump(). Danach
war der Stackpointer natürlich nicht mehr da wo er sein soll und ich
bekomm einen Hardfault.
Mit dem anschließenden while(1) ändert der Optimizer sein Verhalten
etwas und es geht wieder. Garantiert ist es natürlich nicht und kann in
Abhängigkeit von Compilerparametern und -version mal funktionieren und
mal nicht. Also hab ich die zwei Befehlen in ASM umgesetzt und jetzt
läuft es zuverlässig.
Also so viel C wie möglich, aber nichts außerhalb des Bereichs für den C
definiert ist.
.. und abgesehen davon zeigt das Vorgehen, daß jemand die Unterschiede
zwischen Arrays und Pointern damals nicht verstanden hatte, sondern nur
die Gemeinsamkeit in der Benutzung als indexierte Variante gesehen hat.
Auch hier wäre die Lösung, sich damit zu beschäftigen, denn so ein
Fehlverständnis schlägt auch außerhalb des Startups zu.
Nop schrieb:> Abgesehen davon haben Funktionen der Standardbibliothek (hier konkret:> memcpy) nichts im Startupcode verloren.
Tja, das mußt du Freescale sagen, das Beispiel ist aus den Untiefen der
CodeWarrior Umgebung. Das ist denen und sonst niemand nur nie
aufgefallen, weil mit CodeWarrior ein historisch wertvoller Uralt-GCC
verbundled ist. Löst man das ganze aus CodeWarrior raus um den Mist
loszuwerden, compiliert es mit einem aktuellen GCC schon freut man sich
über besagte Erkenntnis. (Wie unschwer zu erkennen war ich der autor
dieses Posts).
> Die Lehre daraus ist nicht "Assembler im Startupcode", sondern "verwende> keine vermurksten Deklarationen". Sowas macht man mit Pointern statt> fehldeklarierter Arrays, und das nicht bloß im Startupcode.>
Auf gehts: Im Linker Script seien die Symbole "__bss_start" und
"__bss_end". Bitte Code in C dazu, der kein UB ist und macht was er
soll.
Nop schrieb:> .. und abgesehen davon zeigt das Vorgehen, daß jemand die> Unterschiede> zwischen Arrays und Pointern damals nicht verstanden hatte, sondern nur> die Gemeinsamkeit in der Benutzung als indexierte Variante gesehen hat.> Auch hier wäre die Lösung, sich damit zu beschäftigen, denn so ein> Fehlverständnis schlägt auch außerhalb des Startups zu.
Dies ist hier etwas spezielles. Es soll ja Startup Code in C geschrieben
werden mit Symbolen aus dem Linker Skript. Es gibt m.W. keine valide
Möglichkeit dies in C zu tun ohne auf UB zu setzen. Deswegen ist die
ganze Idee das unbedingt in C tun zu wollen auch so verkorkst.
Die Symbole holt man sich mit Adressbildung einer globalen Variable:
1
extern char __bss_start;
2
extern char __bss_end;
3
// &__bss_start
4
// &__bss_end
Pointer Arithmetik bringt da nichts, da es undefiniert ist mit einem
Pointer über das Ende des zu Grunde legenden Variable zu
lesen/schreiben. Desweiteren wird der Compiler sowas gerne
wegoptimieren, da UB. Array geht nicht, da Größe nicht fest. Die Größe
müßte ja die Länge der .bss Section sein. Kann man in C also gar nicht
anlegen. Array jeglicher Notation [1] oder [0] oder [] bringen alle
nichts. UB beim Zugriff über das Ende. Am ehesten rettet einen ein
volatile Pointer um dem Compiler die Optimierung zu untersagen, aber
streng genommen auch das UB.
Hermann K. schrieb:> - Die Vektortabelle muss mittels Attribute an die richtige Stelle> geschoben werden und explizit als benutzt markiert werden. Das ist nicht> portabel.
Chip-spezifische Sachen sind nie portabel, zumal man das ohnehin an die
richtige Stelle schieben muß. Das über das Linkerscript zu machen ist
eine ziemlich saubere Lösung.
> - Weiterhin darf der Compiler in structs Padding einfügen
Deswegen würde ich da ein Array aus Funktionszeigern vom Typ void-void
nehmen und den Stackpointer als void-Pointer reincasten. Setzt natürlich
voraus, daß beide dieselbe Breite haben, aber eine Vektortabelle ist eh
nie portabel.
> - Die Schleife zum Initialisieren von BSS und Data dürfte vermutlich> wegoptimiert werden.
Deswegen deklariert man derlei Pointer als volatile*, dann weiß man, daß
nichts wegoptimiert wird.
> Der Compiler sieht, dass es keine weiteren Zeiger> auf die Arrays gibt
Die dort verwendete Ram-Init-Routine hat keine Arrays, sie nutzt
Pointer.
> Das funktioniert auch erst mal. Nur hat der Compiler nach einer Änderung> beschlossen in der Funktion ein paar Sachen auf den Stack zu pushen. Und> die wollte er dann auch wieder popen. Und zwar dummerweise genau> zwischen dem ändern des Stackpointers (_set_MSP) und dem Jump(). Danach> war der Stackpointer natürlich nicht mehr da wo er sein soll und ich> bekomm einen Hardfault.
Das ist in der Tat ein interessantes Beispiel, das eine gewisse
Gemeinsamkeit mit einem Task-Scheduler hat. Sowas würde ich auch eher
nicht in C umsetzen wollen. Aber das ist auch ein anderes Kaliber als
eine Kopier- und Nullschleife.
Die Sachen, wo ich Assembler verwende, sind übrigens oftmals gerade die
Fälle, wo ich den Stack manipuliere. Beispielsweise bei einem
Speichertest im Startup muß ich garantieren, daß alles in Registern
abläuft, weil auch der Stackbereich getestet wird, und das geht nicht in
C, weswegen ich sowas in Assembler mache. Das register-keyword in C ist
ja bloß ein unverbindlicher Vorschlag an den Compiler.
Oder wenn man sich im Hardfaulthandler die Absturzadresse aus dem
Stackframe zieht, da ist auch Assembler fällig.
.data schrieb:> Am ehesten rettet einen ein> volatile Pointer um dem Compiler die Optimierung zu untersagen
Weswegen ich auch genau das mache. Wobei ich die Variable nicht als char
wähle, sondern als uint32_t, damit ich die Kopierschleife auf diesen
Datentyp machen kann, was viermal schneller geht als char. Dazu muß man
im Linkerscript natürlich ein align(4) vorsehen.
> streng genommen auch das UB.
Ein volatile-Pointer darf schreiben, wohin er will (wenn man mit den
Konsequenzen zurechtkommt). Man kann erwägen, ob man hier wohl
pointer-aliasing hat - ist aber nicht der Fall, weil der Compiler davon
ausgeht, daß main() aus dem Nichts heraus aufgerufen wird und zu dem
Zeitpunkt die Initialisierung vorhanden ist.
Andererseits verhindern die volatile-Pointer, daß der Compiler das
wegoptimiert. Sicherheitshalber deklariere ich Interrupt-Funktionen
(auch den Resethandler) sowieso immer als "used", wenn der Compiler auf
der jeweiligen Plattformen kein spezielles Attribut für IRQs verlangt.
Hermann K. schrieb:> - Die Schleife zum Initialisieren von BSS und Data dürfte vermutlich> wegoptimiert werden. Der Compiler sieht, dass es keine weiteren Zeiger> auf die Arrays gibt (z.B. bei aktiviertem LTO) und entfernt dann die> Schleifen. Ich bin mir aber nicht 100% sicher, wann er das genau darf.
1
extern long __bss_start__[];
2
extern long __bss_end__[];
3
4
//...
5
6
static void ram_init(void) {
7
for (long* dest = __bss_start__; dest < __bss_end__; ++dest) {
8
*dest = 0;
9
}
10
11
long* src = __etext;
12
for (long* dest = __data_start__; dest < __data_end__; ++dest) {
13
*dest = *src++;
14
}
15
}
Zero size Array Deklarationen sind als Spezialfall eigentlich nur als
letzter Member in einem struct möglich. M.W. behandelt der GCC eine
Deklaration wie diese als:
1
extern long __bss_start__[1];
2
extern long __bss_end__[1];
Entsprechend ist der Zugriff über das Ende des Array UB. Der Compiler
darf also entsprechende Optimierungen ausführen.
Zum Glück ist die Vektortabelle als globale Variable realisiert. static
machen und sie löst sich u.U. direkt in Luft auf. Wenn schon alles
voller Compiler attributes, hätte man __attribute__((used)) unbedingt
noch hinzufügen müssen.
Hermann K. schrieb:> Die Vektortabelle muss mittels Attribute an die richtige Stelle> geschoben werden und explizit als benutzt markiert werden. Das ist nicht> portabel.
Man kann natürlich durch Makros eine gewisse Portabilität zwischen
Compilern herstellen aber im Prinzip hast du recht. Nichtsdestotrotz ist
das immer noch portabler als Assembler.
Hermann K. schrieb:> Weiterhin darf der Compiler in structs Padding einfügen, d.h. zwischen> initial_stack und vectors könnten noch ein paar Bytes reinkommen.
Das wäre mir neu. Wenn der Struct von sich aus perfektes Alignment hat,
wie in diesem Fall, d.h. alle Member sind uint32_t, dann gibt es auch
überhaupt keinen Grund für Padding.
Hermann K. schrieb:> Die Schleife zum Initialisieren von BSS und Data dürfte vermutlich> wegoptimiert werden. Der Compiler sieht, dass es keine weiteren Zeiger> auf die Arrays gibt (z.B. bei aktiviertem LTO) und entfernt dann die> Schleifen.
Wenn es keine Zeiger auf diese Bereiche gibt, dann können sie doch auch
sowieso nicht genutzt werden, oder nicht?
Bitte nicht falsch verstehen. Ich habe absolut nichts gegen ASM in so
low-level Anwendungen wie dem Startup-Code aber ich denke, dass eine
C-Implementierung auch ihre Vorteile bietet, vor allem was das
Verständnis für Leute angeht, die kein ASM verstehen und dennoch (wie
der TO) verstehen wollen, was der Startup-Code macht. Man muss
allerdings ein sehr umfangreiches Verständnis von C haben, inkl. der
Garantien, etwa dem genullten BSS, welche der Compiler stillschweigend
annimmt und selbst dann ist es ein Tanz auf Messers Schneide, wenn man
das fehlerfrei und ohne unerwünschte Nebeneffekte ("geht nach
Compilerupdate nicht mehr") hinbekommen will. Ich spreche da aus eigener
leidlicher Erfahrung. Eine Implementierung in Assembler ist da unter
Umständen durchaus "schmerzfreier" für den Autor aber für den Leser ist
die C-Implementierung durchaus besser verständlich.
Hermann K. schrieb:> Weiterhin darf der Compiler in structs Padding einfügen, d.h. zwischen> initial_stack und vectors könnten noch ein paar Bytes reinkommen.
Nein. Im ABI ist haarklein definiert wie ein struct auf der jeweiligen
Plattform auszusehen hat, da hat der Compiler keinen einzigen Millimeter
Spielraum.
.data schrieb:> Wenn schon alles> voller Compiler attributes, hätte man __attribute__((used)) unbedingt> noch hinzufügen müssen.
USED wird sehr wohl benutzt in dem Beispiel auf das Du Dich beziehst.
Jim M. schrieb:> Auch für Cortex M0 gäbe es gute Bücher, z.B:> "The Definitive Guide to ARM® Cortex®-M0 and Cortex-M0+ Processors" von> Joseph Yiu
Gibt es denn auch Literatur die einem den ARM anhand von
Projektbeispielen beibringt? So wird das in Make Avr gemacht.
doll schrieb:> Gibt es denn auch Literatur die einem den ARM anhand von> Projektbeispielen beibringt? So wird das in Make Avr gemacht.
Wenn du AVR bereits kennst, wird Dir vieles bekannt vorkommen.
Für den Cortex-M0+ kann ich kein Buch bieten, aber ich habe was für dich
mit Cortex-M3: http://stefanfrings.de/mikrocontroller_buch2/index.html
Dort werden die absoluten Basics vermittelt. Danach würde ich Dir
empfehlen, die Programmiersprache auf einem PC (ohne µC) zu erlernen
(falls nicht bereits geschehen) und danach schau Dir mal meine STM32
Seiten an.
Ich habe da schon ein bisschen was für den STM32L0 (das ist ein
Cortex-M0+) zusammen geschrieben. Du wirst sehen, dass die Unterschiede
zwischen Cortex-M0+ und Cortex-M3 aus Sicht des C-Programmierers sehr
gering sind. Viel größer sind die Unterschiede in der Peripherie, die ST
drumherum gebaut hat.
Bernd K. schrieb:> USED wird sehr wohl benutzt in dem Beispiel auf das Du Dich beziehst.
Tatsache, übersehen. Man hat's beim section Attribut mit rangetackert.
Lustig dass das Beispiel trotzdem nicht ohne (inline) Assembler
auskommt. Ich dachte das sei der große Vorteil der ganzen Qualen kein
Assembler zu benötigen.
1
NAKED void Reset_Handler(void) {
2
asm("bl ram_init" :: "i" (ram_init));
3
asm("bl SystemInit" :: "i" (SystemInit));
4
asm("bl main" :: "i" (main));
5
while(1);
6
}
Der Kommentar dazu ist aufschlussreich:
1
* It is implemented in asm because this is the only reliable way
2
* to have a naked function without a stack frame and at the same
3
* time make sure the optimizer will not try to inline anything
.data schrieb:> Ich dachte das sei der große Vorteil der ganzen Qualen kein> Assembler zu benötigen.
Naja, die größten Qualen sind die ganzen Pseudo-Opcodes, Makros und
andere syntaktische Feinheiten des jeweils anderen Assemblers die man
braucht um die ganze WEAK-Geschichte und Default-Handler und
Linkersymbole und Section-Zuordnungen mit dem eigenen Assembler zu
implementieren wo das alles komplett anders notiert wird wenn man sowas
portieren wollte. Drei kleine Inline-Assembleranweisungen sind dagegen
vergleichsweise harmlos.
Wenn man drüber nachdenkt, auf diese Fehlerursache bin ich noch gar
nicht gekommen: Der Optimizer könnte erst inlinen und dann Code vor die
Initialisierung der .bss und .data section ziehen. Das wird in diesem
Beispiel auch der Grund sein.
Ihm dürfte wegen UB nicht klar sein, dass z.B.
1
uint32_t SystemCoreClock;
2
3
WEAK void SystemInit(void) {
4
// ...
5
SystemCoreClock = 84000000UL;
6
}
durch ram_init() genullt wird. Wenn ihm der Sinn danach stünde, könnte
er ohne den Inline Assembler Hack die Zuweisung vor den Aufruf von
ram_init() ziehen. Und dann wundert sich jemand warum SystemCoreClock
immer null ist :-)
Bernd K. schrieb:> Drei kleine Inline-Assembleranweisungen sind dagegen> vergleichsweise harmlos.
Der Grund warum man all solche Dinge benötigt ist aber nicht harmlos.
Man kann nicht auf der einen Seite sagen, Assembler kann ich nicht
lesen, aber C kenne ich alle Compiler Feinheiten und die Wege des
Optimizers auswendig. Dann kann man nämlich Assembler, wenn man das
alles weiß. Klingt für mich an den Haaren herbeigezogen.
.data schrieb:> Der Optimizer könnte erst inlinen und dann Code vor die> Initialisierung der .bss und .data section ziehen. Das wird in diesem> Beispiel auch der Grund sein.
Der Optimizer macht wilde Sachen wenn LTO verwendet wird. Das
ursprüngliche Problem mit dem Inlinen war daß der Code ursprünglich auf
einem Freescale Kinetis laufen sollte und dort zwischen der
Vektortabelle und den Beginn von .text nochmal kurz vor 0x400 ein
kleines "Loch" existiert wo sich die Flash option bytes befinden. Ich
wollte den ganzen Startup und Systeminit in der unteren Section haben,
der LTO vom gcc möchte dann aber gern die ganze main da noch mit
reinziehen und dann knallts weil die untere Section voll wird.
Es gibt noch nen Workaround mit purem C und indirektem Aufruf mittels
Funktionspointer aber der Zirkus war dann doch zu hässlich und der
WTF???-Faktor war zu hoch. Wenn man die kritischen Sachen mit asm
festnagelt kann nichts mehr verrutschen.
.data schrieb:> Der Kommentar dazu ist aufschlussreich:
Und verwirrend. naked bewirkt nämlich lediglich, daß die Funktion im
Prolog nicht die Register ab R4 auf dem Stack sichert, was sie auch
nicht braucht, weil es keinen Aufrufer gibt. Aber selbst wenn sie nicht
naked wäre, dann wäre der einzige Effekt, daß ein paar Bytes auf dem
Stack verlorengehen würden.
Der Stackpointer selber ist zu dem Zeitpunkt nämlich ohnehin schon
aufgesetzt, weil das der erste Eintrag in der Vektortabelle ist.
Inlining kann man verhindern, indem man der aufgerufenen Funktion ein
noinline-Attribut mitgibt. Das kann man speziell dann gut gebrauchen,
wenn man mehrere Funktionen mit großem Stackbedarf hat, die aber nie in
derselben Callchain liegen. Nicht daß die Funktionen hier überhaupt
großen Stackverbrauch hätten.
Vorteil der verlinkten Implementierung ist, daß beim Aufrufen von main()
nicht die Register ab R4 auf den Stack geschoben werden und man sich
damit ein paar Bytes auf dem Stack spart.
Und das mit den Optionbytes - ja also wenn man irgendwelche "Lücken" im
Speicherbereich hat, dann fände ich es ja logischer, das dem GCC im
Linkerscript mitzuteilen, wo das Speicherlayout beschrieben ist, und
nicht im Assembler.
Das Verwenden der globalen Variable für die Systemclock ist kein
Problem, weil die Initpointer erst nullen müssen. Kommt daher, daß beide
von einem kompatiblen Typ sind und der Compiler deswegen in der
aliasing-Analyse davon ausgehen muß, daß die Nullung die Systemclock
überschreiben kann. Deswegen sind die Zugriffe nicht unabhängig, und die
Zuweisung an die Systemclock kann nicht vor die Nullung verschoben
werden.
Ich hab als Startpunkt für meinen Startup Code (allerdings für einen
Infineon M4), den Code vom Hersteller genommen und daran angepasst, was
ich anders machen wollte. Evtl. ist auch das Linkerscript wichtig, weil
es angibt, wo die Daten liegen.
Meiner setzte sich dann aus folgenden Schritte zusammen:
1) Stackpointer initialisieren
2) System initialisieren (externen Speichercontroller einstellen, Traps
für Fehler aktivieren)
3) .data in den RAM kopieren (Linker gibt an was man kopieren muss)
4) .bss in den RAM kopieren (Linker...)
5) System Clock auf Frequenz einstellen und Subsystemen mit Clock
versorgen (Handbuch)
6) Heap initialisieren
7) Aufruf der Initialisierung und Konstruktoren (C++) für Globale
Objekte (__libc_init_array)
Einzelne Schritte können natürlich übersprungen werden, wenn man diese
nicht braucht oder der Chip das schon selbst macht. Die Reihenfolge kann
teilweise auch anders gewählt werden.
Mit __libc_init_array muss man aufpassen, weil vor dem Aufruf globale
Variablen und Objekte noch undefiniert sein können. Bei mir musste
außerdem der Heap vorher initialisiert werden, weil Konstruktoren diesen
benutzen.
Nop schrieb:> dann fände ich es ja logischer, das dem GCC im> Linkerscript mitzuteilen, wo das Speicherlayout beschrieben ist, und> nicht im Assembler.
In dem obigen Assembler ist kein Speicherlayout beschrieben. Nur ein
erzwungener Funktionsaufruf damit es ihm unmöglich wird es zu inlinen.
Alles landet dort wo es laut Linkerscript hin soll.
Der gcc will sich jedoch beim Inlinen per LTO nicht an das Linkerscript
halten oder es ist ein Bug. Der Linker wird bei LTO mit Gewalt versuchen
die aufgerufene Funktion in die falsche Section (die des Aufrufers)
hineinzuziehen und dort zu inlinen ohne sich um das Linkerscript zu
scheren, obwohl ich sie nach .text haben will und obwohl er sehen könnte
(wenn er die Augen aufmachen und im Linkerscript nachschauen würde) daß
es gar nicht in diese andere falsche Section hinein passt. Er endet dann
mit einer Fehlermeldung.
NOINLINE hülfe zwar, ist aber in diesem Fall Käse, denn das müsste man
dann direkt an die main() drankleben, ich will aber nicht daß der
Benutzer die main() irgendwie speziell dekorieren muß damit der Startup
funktioniert, es soll einfach so funktionieren.
Bernd K. schrieb:> Der gcc will sich jedoch beim Inlinen per LTO nicht an das Linkerscript> halten oder es ist ein Bug.
LTO arbeitet doch auf den Objektdateien, d.h. zu diesem Zeitpunkt ist es
egal, ob Du die Funktionen nun im C-Stil oder über asm-bl aufrufst. Oder
nicht?
> und obwohl er sehen könnte> (wenn er die Augen aufmachen und im Linkerscript nachschauen würde) daß> es gar nicht in diese andere falsche Section hinein passt.
Das ist IMO ein Bug. Aber LTO ist zumindest für ARM bare metal sowieso
unbrauchbar, weil mit LTO die Stackanalyse nicht mehr funktioniert. Man
kriegt überhaupt keine Daten mehr raus. Und das alles für so in etwa 2%
mehr Performance, größenordnungsmäßig.
Noch übler ist, daß zu dem Zeitpunkt die noinline-Attribute weg sind und
man damit rechnen muß, daß munter inlined wird und sich die Stackgrößen
dann addieren. Da man ja auch keine Stackdaten rausbekommt, siehe der
Bug im vorigen Absatz, ist das nochmal böser.
Also ich halte LTO für so unausgereift, daß ich es bare metal nicht
produktiv einsetze. Auf PCs und Smartphones ist es aber OK.
> NOINLINE hülfe zwar, ist aber in diesem Fall Käse, denn das müsste man> dann direkt an die main() drankleben
Na an die init-Funktionen würde ja schonmal helfen, daß die nicht
inlined werden.
> ich will aber nicht daß der> Benutzer die main() irgendwie speziell dekorieren muß
Verständlich, würde ich auch nicht wollen.
Nop schrieb:> Noch übler ist, daß zu dem Zeitpunkt die noinline-Attribute weg sind und> man damit rechnen muß, daß munter inlined wird
Nach meiner Beobachtung wird NOINLINE in jedem Fall respektiert. Ich
hatte mal das Vergnügen einen Bootloader auf 2 Sections aufzuteilen (1k
vor dem "Kinetis-Loch" und 1k danach) Mit section-Attributen und
gezieltem Setzen von NOINLINE konnte ich ihn trotz LTO zwingen es genau
so aufzuteilen wie ich wollte, hab mir den erzeugten Code dabei
angesehen, jedes NOINLINE hat gewirkt.
Leider kann der gcc Linker nicht selbstständig den Code auf mehrere
Sections aufteilen, das wäre wirklich ein nettes Feature bei
Flash-Geizkragen wie mir manchmal, hätt ich schonmal gebrauchen können.
Nop schrieb:> LTO arbeitet doch auf den Objektdateien, d.h. zu diesem Zeitpunkt ist es> egal, ob Du die Funktionen nun im C-Stil oder über asm-bl aufrufst. Oder> nicht?
Also es hat zumindest gewirkt. Ich nehme an daß LTO (und auch die
anderen Optimierungen) von Assemblerdateien und von inline asm
generierten Code grundsätzlich die Finger lassen muss um sich nicht den
Zorn der Assemblerprogrammierer zuzuziehen. In nennenswertem Umfang hab
ich das aber auch nicht ausprobiert.