Forum: Mikrocontroller und Digitale Elektronik ARM Cortex M0+ Startupcode Minimal


von doll (Gast)


Lesenswert?

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.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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.

von PittyJ (Gast)


Lesenswert?

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.

von Mampf F. (mampf) Benutzerseite


Lesenswert?

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.

von Vincent H. (vinci)


Lesenswert?

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
.syntax unified
2
.thumb
3
.arch armv6-m
4
5
.section .isr_vector
6
.word _estack
7
.word startup
8
9
.section .text
10
startup:
11
  b main


Das beinhaltet aber natürlich nicht die sonst notwendigen Schritte:
- .data kopieren
- .bss räumen
- .preinit/init array Funktionen aufrufen


/edit
typo

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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".

von Stefan F. (Gast)


Angehängte Dateien:

Lesenswert?

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.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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 :)

von doll (Gast)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

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.

von Stefan F. (Gast)


Lesenswert?

Ich denke schon, das Startup-Code in Asembler durchaus üblich ist, 
jedenfalls bei ST, denn deren Code Templates machen das alle, 
ausnahmslos.

von Jim M. (turboj)


Lesenswert?

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...

von Nop (Gast)


Lesenswert?

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.

von Stefan F. (Gast)


Lesenswert?

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.

von Bernd K. (prof7bit)


Lesenswert?

Hier ist ein Minimal-Blinky für eins dieser Nucleo-Boards.

https://github.com/prof7bit/bare_metal_stm32f401xe

Und hier ist noch eine Variante davon für das 411er Board:

https://github.com/ChristianRinn/bare_metal_stm32f411xe

von Nop (Gast)


Lesenswert?

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.

von Stefan F. (Gast)


Lesenswert?

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.

von Johannes S. (Gast)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

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.

von Stefan F. (Gast)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

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.

von Niklas Gürtler (Gast)


Lesenswert?

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.

von Johannes S. (Gast)


Lesenswert?

Niklas Gürtler schrieb:
> Bei ARM7TDMI weiß ich's jetzt nicht.

Die meinte ich aber, hatte das Wort Vorgänger unterschlagen.

von Bernd K. (prof7bit)


Lesenswert?

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.

von Axel S. (a-za-z0-9)


Lesenswert?

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.

von Stefan F. (Gast)


Lesenswert?

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.

von nfet (Gast)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

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.

von .data (Gast)


Lesenswert?

nfet schrieb:
> 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.

Und da wartet so einiges:
Beitrag "Re: [C] Böse Falle: Datentyp korrekt angegeben, falscher verwendet"

von .data (Gast)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

.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.

von Hermann K. (r2d2)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

.. 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.

von .data (Gast)


Lesenswert?

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.

von .data (Gast)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

.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.

von .data (Gast)


Lesenswert?

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.
1
SECT_VECTABLE const vector_table_t __vector_table = {

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.

von Harry L. (mysth)


Lesenswert?

Danke an data und Nop!

Hab gerade was gelernt.

von Christopher J. (christopher_j23)


Lesenswert?

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.

von Bernd K. (prof7bit)


Lesenswert?

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.

von Bernd K. (prof7bit)


Lesenswert?

.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.

von doll (Gast)


Lesenswert?

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.

von Stefan F. (Gast)


Lesenswert?

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.

von .data (Gast)


Lesenswert?

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
4
 * into it.

von Bernd K. (prof7bit)


Lesenswert?

.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.

von .data (Gast)


Lesenswert?

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 :-)

von .data (Gast)


Lesenswert?

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.

von doll (Gast)


Lesenswert?

Stefanus F. schrieb:
> 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

Nach sowas habe ich gesucht, top danke.

von Bernd K. (prof7bit)


Lesenswert?

.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.

von Nop (Gast)


Lesenswert?

.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.

von M.K. B. (mkbit)


Lesenswert?

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.

von Bernd K. (prof7bit)


Lesenswert?

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.

von Nop (Gast)


Lesenswert?

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.

von Bernd K. (prof7bit)


Lesenswert?

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.

von Bernd K. (prof7bit)


Lesenswert?

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.

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
Noch kein Account? Hier anmelden.