Hallo zusammen, ich spiele gerade mit einem -zugegeben sehr extremen - kleinen Beispiel am STM32F446 bei 168 MHz. Der Systick-Timer wird mit 1 MHz aufgerufen. Viel hat er aber auch nicht zu tun: Ein kleines Schieberegister implementieren und ein wenig an Pins wackeln. Das Oszilloskop sagt eine Auslastung um 40% beim Debug-Build. Wie so viele Menschen, habe ich am Samstagmorgen angefangen aufzuräumen, und die Schieberegisterfunktion in eine Funktion gepackt. Schwupps- die Auslastung ist beim Debug-Build so hoch, daß die MCU überhaupt nichts anderes mehr macht, als den SysTick-Timer aufzurufen. Beim Release-Build wird die Funktion natürlich geinlined und alles ist wieder gut. Die ganze Aktion drängt natürlich die Frage auf: Wie aufwendig ist so ein Funktionsaufruf bei einem STM32 überhaupt? Mit wieviel Overhead kann ich da rechnen? Wo finde ich zu diesem Thema Informationen? Viele Grüße W.T.
Walter T. schrieb: > Funktionsaufruf bei einem STM32 überhaupt? Mit wieviel Overhead kann > ich da rechnen? Wo finde ich zu diesem Thema Informationen? Der ist genauso aufwendig wie alle Register auf dem Stack zu sicher und wieder zurück zu holen. Beim Debug Build habe ich nie einen Performance Verlust feststellen können.
Am besten ist immer, sich die disassembly anzusehen und mithilfe des Cortex-M4 technical reference manual die Zyklenzahlen anzusehen. Dabei sollte natürlich die Flash Latenz und der ggf. genutzte ART/Prefetch Buffer nicht vergessen werden.
Walter T. schrieb: > Die ganze Aktion drängt natürlich die Frage auf: Wie aufwendig ist so > ein Funktionsaufruf bei einem STM32 überhaupt? Das ist ein Interrupt und kein Funktionsaufruf. Ein Funktionsaufruf könnte unter günstigen Umständen sogar vom Compiler komplett eliminiert werden (inlining) aber bei einem Interrupt muss immer der Kontext gesichert und vor Beendigung wieder hergestellt werden, das ist sogar teurer als ein simpler Funktionsaufruf. Daß bei 1MHz Interruptfrequenz ruck zuck so viel CPU draufgeht wie Du gemessen hast liegt durchaus im Bereich des zu Erwartenden. Genauere Zahlen findest Du wenn Du die Doku von ARM zu diesem Thema studierst. Vielleicht lässt sich das was Du erreichen willst auch durch geschickte Verwendung und Zusammenschaltung von Timern, DMA, PWM, SPI erreichen und die Interruptfrequenz reduzieren. Versuch so viel wie möglich von der existierenden Hardware automatisch erledigen zu lassen, zum Beispiel statt 8 Bits einzeln an einem Pin rauszuschieben kannst Du das SPI nehmen und hast nur noch 1/8 Interruptfrequenz. Oder wenn SPI nicht mehr frei ist dann bereite Dein zu sendendes Bitmuster im RAM vor und lass die timergesteuert im Hintergrund per DMA auf dem GPIO rausklopfen. Im Finden solcher Lösungen liegt die wahre Kunst.
Chris J. schrieb: > Beim Debug Build habe ich nie einen Performance > Verlust feststellen können. Normalerweise ist bei mir ein Debug-Build -O0 und ein Release-Build -Os oder -O2. Bei mir sind die Performance-Unterschiede, allein durch Inlining, deutlich. Bernd K. schrieb: > Das ist ein Interrupt und kein Funktionsaufruf. Der Punkt der Fragestellung ist nicht der Aufruf der ISR, sondern der Aufruf einer Funktion (die innerhalb der ISR oder sonstwann aufgerufen wird). Konkret sieht mein Beispiel so aus:
1 | #include "stm32f4xx_conf.h" |
2 | #include "pins_main.h" |
3 | #include "delay.h" |
4 | #include <stdio.h> |
5 | #include "intmath.h" |
6 | |
7 | |
8 | volatile int32_t v = 0; |
9 | |
10 | |
11 | int main(void) |
12 | {
|
13 | SystemInit(); |
14 | SysTick_Config(SystemCoreClock/1000000); // 1ns |
15 | |
16 | RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); |
17 | RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); |
18 | RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE); |
19 | RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE); |
20 | RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE); |
21 | |
22 | |
23 | io_setOutput(LED2_GPIO, LED2_Pin); |
24 | |
25 | io_setOutput(SPARE0_GPIO, SPARE0_Pin); |
26 | io_setOutput(SPARE1_GPIO, SPARE1_Pin); |
27 | |
28 | |
29 | while(1) |
30 | {
|
31 | io_toggleBit(LED2_GPIO, LED2_Pin); |
32 | |
33 | v++; |
34 | if( v > 100) v = -100; |
35 | delay_ms(30); |
36 | }
|
37 | }
|
38 | |
39 | |
40 | |
41 | |
42 | /* Schieberegister
|
43 | * v: Geschwindigkeit
|
44 | * l: Zaehlerlaenge
|
45 | * return: [0, 1, -1] Ueberlauf (mit Richtung) */
|
46 | typedef int32_t shifterstate_t; |
47 | |
48 | int_fast8_t shifter(int32_t v, int32_t l, shifterstate_t *akku ) |
49 | {
|
50 | int_fast8_t ovrflw = 0; |
51 | #if 0
|
52 | assert(v < 1<<30 );
|
53 | assert(v > -(1<<30) );
|
54 | assert(l < 1<<30 );
|
55 | assert(l > -(1<<30) );
|
56 | assert( v <= l );
|
57 | assert( -v <= l );
|
58 | #endif
|
59 | |
60 | *akku += v; |
61 | |
62 | if( *akku >= l && v > 0 ) { |
63 | *akku -= l; |
64 | ovrflw = 1; |
65 | }
|
66 | else if( *akku <= 0 && v < 0 ) { |
67 | *akku += l; |
68 | ovrflw = -1; |
69 | }
|
70 | |
71 | return ovrflw; |
72 | }
|
73 | |
74 | |
75 | void SysTick_Handler(void) |
76 | {
|
77 | static shifterstate_t shifterstate = 0; |
78 | int32_t l = 1000; |
79 | static uint32_t cnt = 0; |
80 | |
81 | // Auslastung anzeigen
|
82 | io_setBit(SPARE0_GPIO, SPARE0_Pin); |
83 | |
84 | // Puls ausgeben
|
85 | if( cnt == 0 ) { |
86 | io_clearBit( SPARE1_GPIO, SPARE1_Pin ); |
87 | }
|
88 | else { |
89 | io_setBit( SPARE1_GPIO, SPARE1_Pin ); |
90 | }
|
91 | |
92 | |
93 | step = shifter(v, l, &shifterstate ); |
94 | |
95 | |
96 | if( step !=0 ) { |
97 | // Im naechsten Durchgang Schritt ausgeben
|
98 | cnt = 2; |
99 | }
|
100 | |
101 | if( cnt ) cnt--; |
102 | io_clearBit(SPARE0_GPIO, SPARE0_Pin); |
103 | |
104 | }
|
Die funktion shifter() war vorher explizit in der ISR und hat ihre eigene Funktion bekommen. Vor der Auslagerung lag die Auslastung durch die ISR bei unter 40% (Debug-Build), unmittelbar danach bei über 100%. Das geht so weit, daß die Main-Funktion nicht einmal dazu kommt, die beiden Pins auf Output zu stellen. Wie ich das Verhalten wegbekomme, ist klar: Optimierung von -O0 auf -O2 stellen. Aber das ist nicht die Frage. Es ist nur der Anlaß, sich mit der Frage zu beschäftigen. Die Frage lautet: Wie kann ich den Overhead, der durch einen Funktionsaufruf entsteht, abschätzen?
Das Problem ist, daß -O0 diese Funktion niemal selber inlined. Zudem werden die Variablen nicht in Registern gehalten, alles damit der Debugger schön mit kommt. Und das bedeutet, jede Menge teuere Speicher-Register-Transfers. BTW, Performance-Nessungen mit -O0 kann man eigentlich gar nicht freundlich kommentieren.
Einen STM32 auf Registerebene zu programmieren ist nun wirklich keine große Sache. Warum ich hier auf irgendwelche Bibliotheken zurückgreifen muss, ist mir ein Rätsel? Ich schmeisse das grundsätzlich über Bord und mache nur Registerprogrammierung. Der Vorteil ist auch, dass man dann einen µcC erst so richtig kennenlernt und ausschöpfen kann (meiner Meinung nach). Vieles ist genauso simpel wie auf z.B. 8Bittern - bloß haben einige (! nicht alle..) Peripherie Module etliche Zusatzmodule, die aber erst aktiviert werden müssen und auch nicht so komplex sind, dass man die Registerbeschreibung nicht versteht. Z.B. PLL Systemclock über HSE: 1. HSE aktivieren 2. Warten bis HSE bereit ist 3. PLL parametrieren 4. PLL aktivieren 5. Warten bis PLL bereit ist 6. CLock Teiler einstellen 7. PLL als Systemtakt zuweisen 8. Warten bis Systemtakt zugewiesen ist 9. HSI deaktivieren Das sind fast alles nur einzelne Bits setzen. Wozu brauche ich hier FUnktionsaufrufe? Oder einen Timer: 1. Teiler einstellen 2. Maximalen Zählwert angeben 3. Interrupt aktivieren 4. Timer aktivieren Schon tickert der Timer und bei einem Interrupt wird ein Interrupt generiert. Damit dieser dann auch wirklich gerufen wird, muss noch im Interrupt-Controller ein Bit gesetzt werden. Das Datenblatt deines Controllers ist eh dein täglich Brot!
A. schrieb: > Einen STM32 auf Registerebene zu programmieren ist nun wirklich keine > große Sache. Warum ich hier auf irgendwelche Bibliotheken zurückgreifen > muss, ist mir ein Rätsel? Kannste privat ja auch machen, gewerblich wird dieser unleserliche Murks nicht akzeptiert. Bei uns im Konzern auch nicht, wo weltweit einheitliche Coderungs-Standards gelten. Und der Standard heisst jetzt HAL oder ähnlich und fertig.
A. schrieb: > Einen STM32 An dieser Stelle hört die Gemeinsamkeit zwischen Antwort und der Fragestellung leider auf.
Chris J. schrieb: > A. schrieb: >> Einen STM32 auf Registerebene zu programmieren ist nun wirklich keine >> große Sache. Warum ich hier auf irgendwelche Bibliotheken zurückgreifen >> muss, ist mir ein Rätsel? > > Kannste privat ja auch machen, gewerblich wird dieser unleserliche Murks > nicht akzeptiert. Bei uns im Konzern auch nicht, wo weltweit > einheitliche Coderungs-Standards gelten. Und der Standard heisst jetzt > HAL oder ähnlich und fertig. Vielleicht ist Arduino als HAL Ansatz hilfreich - https://github.com/rogerclarkmelbourne/Arduino_STM32
Walter T. schrieb: > Die ganze Aktion drängt natürlich die Frage auf: Wie aufwendig ist so > ein Funktionsaufruf bei einem STM32 überhaupt? Mit wieviel Overhead kann > ich da rechnen? Was den zeitlichen Overhead angeht man das so pauschal nicht beantworten, da dass von der Anzahl der zu sichernden Register abhängt. Konkrete Antworten findest du im Assembly-Listing. Mit __attribute__((always_inline)) kannst du den Compiler zwingen eine Funktion zu inlinen. Dann macht er das immer, also auch mit -O0. Ich würde für Debug-Builds jedoch standardmäßig -Og verwenden. Da werden dann schon kleinere Optimierungen vorgenommen aber der generierte Programmablauf entspricht wie bei -O0 noch dem des C-Programms. Der generierte Assembler ist aber meiner Meinung nach deutlich besser lesbar.
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.