Forum: Mikrocontroller und Digitale Elektronik STM32 Overhead Funktionsaufruf


von Walter T. (nicolas)


Lesenswert?

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.

von Chris J. (Gast)


Lesenswert?

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.

von Dr. Sommer (Gast)


Lesenswert?

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.

von Bernd K. (prof7bit)


Lesenswert?

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.

von Walter T. (nicolas)


Lesenswert?

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?

von Carl D. (jcw2)


Lesenswert?

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.

von A. (Gast)


Lesenswert?

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!

von Chris J. (Gast)


Lesenswert?

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.

von Walter T. (nicolas)


Lesenswert?

A. schrieb:
> Einen STM32

An dieser Stelle hört die Gemeinsamkeit zwischen Antwort und der 
Fragestellung leider auf.

von Jens G. (jensg)


Lesenswert?

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

von Christopher J. (christopher_j23)


Lesenswert?

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