Forum: Mikrocontroller und Digitale Elektronik Cortex M3/M4: Compiler reordering und memory barriers bei delay Funktion


von Jan K. (jan_k)


Lesenswert?

Hallo zusammen,

ich lese im Moment ein paar Sachen über Compiler Optimierungen nach und 
werde langsam paranoid. In meinem konkreten Fall geht es um eine 
blockierende Warteschleife während der Initialisierung eines MEMS 
Sensors, der seine beiden Spannungen VDD und VDDIO in bestimmter 
Reihenfolge eingeschaltet bekommen möchte. Welcher Sensor genau kann ich 
leider nicht sagen.

Der Busyloop sieht in etwa so aus und verwendet den CycleCounter vom 
Cortex M3:

timer.c
1
...
2
3
static bool _isInitialized = false;
4
static RCC_ClocksTypeDef _clocks;
5
static uint32_t _cyclesPerUs;
6
7
void TIMER_Initialize(void)
8
{
9
    if (!_isInitialized)
10
    {
11
        CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
12
13
        // DWT->CYCCNT = 0; // todo: Überlauf diskutieren
14
15
        DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // enable cycle counter
16
17
        RCC_GetClocksFreq(&_clocks); // read out current clock speeds to pre-calculate ticks to us conversion
18
        cyclesPerUs = _clocks.SYSCLK_Frequency / 1E6;
19
20
        _isInitialized = true;
21
    }
22
}
23
24
...
25
26
// todo: Interrupts sperren?
27
// todo: Opt full/none diskutieren
28
// todo: add correction for call offset
29
void __attribute__((optnone)) // oder optfull? vermutlich eher, um overhead zu verringern.
30
TIMER_BusyWait(uint32_t waitTimeMs)
31
{
32
    TIMER_BusyWaitUs(waitTimeMs * 1000);
33
}
34
35
void __attribute__((optnone))
36
TIMER_BusyWaitUs(uint32_t waitTimeUs)
37
{
38
    if (!_isInitialized)
39
    {
40
        while (1) // nur exemplarisch. Fehlerbehandlung halt
41
            ;
42
    }
43
    volatile uint32_t cyclesToWait = cyclesPerUs * waitTimeUs; // volatile to suppress optimization
44
    volatile uint32_t startCycles = DWT->CYCCNT;
45
    while ((DWT->CYCCNT - startCycles) < cyclesToWait)
46
    {
47
        __ASM("nop");
48
    }
49
}
50
51
...

In sensor.c wird der Delay verwendet, um die Sensor Versorungsspannung 
zu schalten. Nehmt bitte an, dass der "Timer" initialisiert wurde und 
dass die Logik invertiert ist!
1
...
2
static void SENSOR_EnableSensorVdd()
3
{
4
5
    // die benötigten GPIOS initialisieren
6
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
7
    
8
    GPIO_InitTypeDef gpioInit;
9
    GPIO_StructInit(&gpioInit);
10
    gpioInit.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_12;
11
    gpioInit.GPIO_Mode = GPIO_Mode_Out_PP;
12
13
    // right after reset, pins are input floating
14
    // Pin 11 and Pin 12 are connected to a PMOS Mosfet -> inverted logic
15
    GPIO_SetBits(GPIOA, GPIO_Pin_11 | GPIO_Pin_12); // *disable* sensor 3v3. Ensure state before pin is set to output PP. Default is analog in. Pullup ensures 3v3 level -> vdd & vddio is off
16
17
    GPIO_Init(GPIOA, &gpioInit);
18
    
19
    /*
20
    dmb http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0473m/dom1361289870356.html
21
    dsb http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0473m/dom1361289870725.html
22
    barrier http://www.keil.com/support/man/docs/armcc/armcc_chr1359124212159.htm
23
    */
24
    
25
   // __DMB();
26
   TIMER_BusyWait(500); // 500 msec to unload all caps and give a proper trigger signal for scope
27
   // __DMB();
28
    
29
    GPIO_ResetBits(GPIOA, GPIO_Pin_11); // VDD *enabled*
30
31
   // __DMB();
32
   TIMER_BusyWait(5);  
33
   // __DMB();
34
    GPIO_ResetBits(GPIOA, GPIO_Pin_11); // VDDIO *enabled*
35
36
}

Das ist der grundsätzliche Aufbau. Ich habe grundsätzlich den CYCCNT aus 
dem DWT Modul 
(http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0337h/BIIFBHIF.html) 
genommen, weil von sonstigen Warteschleifen als Delay loop abgeraten 
wird. Den Aufbau habe ich aus dem Datenblatt und von hier: 
https://www.carminenoviello.com/2015/09/04/precisely-measure-microseconds-stm32/

Jetzt ist es aber ja so, dass der Compiler optimieren darf. Darunter 
fällt auch ein reordering der Instruktionen sowie Speicherzugriffen. 
Daher habe ich nach compile-time memory barriers gesucht und bin z.B. 
auf das gestoßen:
https://en.m.wikipedia.org/wiki/Memory_ordering#Compile-time_memory_ordering#Compile-time_memory_ordering
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dai0321a/BIHGIEDG.html
https://github.com/ARM-software/CMSIS_5/pull/510 bzw 
https://github.com/ARM-software/CMSIS_5/commit/72210c6831f48fc739c3ec18e01358adc172375b
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0473m/dom1361289870356.html

Insbesondere in dem CMSIS_5 Link ist ein ähnlicher Fall, da wird die 
Funktion zum Deaktivieren bestimmter Interrupts diskutiert und gesagt, 
dass im Prinzip nichts den Compiler an einem reordering hindert.

Daher habe ich überlegt, die __DMB() memory barrier vor und hinter den 
TIMER_BusyWait() call zu packen.

Meine Fragen:
- Brauche ich die DMB?
- Wann kann der Compiler reordern? Gibt es da Regeln, wie z.B. "nicht 
über function calls hinweg" oder über Modulgrenzen (compilation unit), 
was ist mit LTO?
- Was ist der Zusammenhang zu volatile (ich weiß, dass das im Prinzip 
nur heißt, dass die Variable bei jedem Mal neu geladen werden muss - 
mehr nicht oder?)?
- Müssen die Interrupts in der Busy wait gesperrt werden? Wurde in einem 
Thread hier empfohlen. Kann ich aber nicht nachvollziehen
- Sollte der busy wait loop optimiert werden oder nicht? Der eigentliche 
Zeitgeber ist ja der Cyccnt und der ist volatile. Muss daher z.B. 
cyclesToWait gar nicht volatile sein?

Vielen Dank!

von Jim M. (turboj)


Lesenswert?

Jan K. schrieb:
> Jetzt ist es aber ja so, dass der Compiler optimieren darf. Darunter
> fällt auch ein reordering der Instruktionen sowie Speicherzugriffen.

Macht er in der Praxis nicht wenn was "volatile" gekennzeichnet ist.

Das ist in Deinem Falle der DWT->CYCCNT Registerzugriff, schau Dir mal 
die Deklaration an.

Die ganzen "volatile" Zugriffe müssen alle so stattfinden wie sie im 
Code stehen. D.h. sie dürfen weder umsortiert noch weggelassen werden - 
selbst wenn dabei Zwischenergebnisse verworfen werden.


IIRC ist das nicht explizit so spezifiziert im C-Standard, aber ein 
gäniges Verhalten in C Compilern.

von (Gast)


Lesenswert?

was genau funktioniert denn nicht?

Jan K. schrieb:
> - Sollte der busy wait loop optimiert werden oder nicht? Der eigentliche
> Zeitgeber ist ja der Cyccnt und der ist volatile. Muss daher z.B.
> cyclesToWait gar nicht volatile sein?

Optimierung oder nicht darf keinen Unterschied machen. Zugriffe auf 
volatile-deklarierte Typen darf der Compiler auch nicht reordern.

Die cyclesToWait und startCycles müssen nicht volatile sein und DMB 
sollte auch nicht notwendig sein.

Verdächtig ist das __ASM, wie wird das expandiert, bzw. wie sieht asm 
von TIMER_BusyWaitUs aus?

von Markus F. (mfro)


Lesenswert?

rµ schrieb:
> Verdächtig ist das __ASM, wie wird das expandiert, bzw. wie sieht asm
> von TIMER_BusyWaitUs aus?

Das kann man - denke ich - bedenkenlos weglassen. Oder wozu soll das NOP 
gut sein?

Im Übrigen kann's sein (zumindest beim M4, soweit ich weiß), daß der 
Prozessor höchstselbst es einfach weglässt bzw. rausschmeißt:

NOP does nothing. NOP is not necessarily a time-consuming NOP. The 
processor might remove it from the pipeline before it reaches the 
execution stage.

von Jim M. (turboj)


Lesenswert?

Markus F. schrieb:
> Verdächtig ist das __ASM, wie wird das expandiert,

Kennt Deine IDE keine Suchfunktion analog zu "grep"?

Das Macro steht in core_cm4.h (oder core_cm3.h für Cortex-M3).

von (Gast)


Lesenswert?

Markus F. schrieb:
> Das kann man - denke ich - bedenkenlos weglassen. Oder wozu soll das NOP
> gut sein?

Stimmt, das kann man komplett weglassen.

Wär trotzdem interessant, was der Compiler daraus macht.

Jan K. schrieb:
> Insbesondere in dem CMSIS_5 Link ist ein ähnlicher Fall, da wird die
> Funktion zum Deaktivieren bestimmter Interrupts diskutiert und gesagt,
> dass im Prinzip nichts den Compiler an einem reordering hindert.

Ich denke im Link meint man den Prozessor, nichts hindert einen 
entsprechend ausgestatteten Prozessor daran, den devx-Zugriff erst nach 
dem enableIRQ auszuführen.

Dem Compiler wird eine Memory Barrier herzlich wurscht sein.

von Markus F. (mfro)


Lesenswert?

Jim M. schrieb:
> Markus F. schrieb:
>> Verdächtig ist das __ASM, wie wird das expandiert,
>
> Kennt Deine IDE keine Suchfunktion analog zu "grep"?

Alarm! Zitatfälscher!

von (Gast)


Lesenswert?

Jim M. schrieb:
> Markus F. schrieb:
>> Verdächtig ist das __ASM, wie wird das expandiert,

Das hab ich geschrieben, nicht Markus F.

> Kennt Deine IDE keine Suchfunktion analog zu "grep"?
>
> Das Macro steht in core_cm4.h (oder core_cm3.h für Cortex-M3).

Ja schon, aber erstens hab ich das Projekt nicht da, der Compiler ist 
unbekannt, und und und.

Da das eigentliche Problem eh nicht bekannt ist wirds müßig...

von (Gast)


Lesenswert?

rµ schrieb:
> Jan K. schrieb:
>> Insbesondere in dem CMSIS_5 Link ist ein ähnlicher Fall, da wird die
>> Funktion zum Deaktivieren bestimmter Interrupts diskutiert und gesagt,
>> dass im Prinzip nichts den Compiler an einem reordering hindert.
>
> Ich denke im Link meint man den Prozessor, nichts hindert einen
> entsprechend ausgestatteten Prozessor daran, den devx-Zugriff erst nach
> dem enableIRQ auszuführen.
>
> Dem Compiler wird eine Memory Barrier herzlich wurscht sein.

nah, es ist komplizierter, und in dem fix wird auch keine memory barrier 
eingefügt sondern eine compiler barrier.

von Random .. (thorstendb) Benutzerseite


Lesenswert?

warum so kompliziert, man könnte doch ein RTOS nehmen? Für delays im ms 
Bereich allemal, soll es kürzer und genauer sein zieht man einen Timer 
auf.

von W.S. (Gast)


Lesenswert?

Jan K. schrieb:
> ich lese im Moment ein paar Sachen über Compiler Optimierungen nach und
> werde langsam paranoid. In meinem konkreten Fall geht es um eine
> blockierende Warteschleife während der Initialisierung eines MEMS
> Sensors, der seine beiden Spannungen VDD und VDDIO in bestimmter
> Reihenfolge eingeschaltet bekommen möchte.

Mache es einfacher. Das ganze Zeugs gilt ja wohl nur für die 
Einschalt-Phase des ganzen Konstrukts, also ganz am Anfang und es 
scheinen auch bloß ein paar Millisekunden zu sein. Sowas macht man ganz 
simpel mit einer popligen Trampelschleife - ohne ein RTOS zu bemühen und 
ohne einen Timer dafür zu benützen. Schließlich passiert das Ganze ja 
nur ein einziges Mal beim Einschalten.

Was die Compiler-Optimierungen betrifft, so habe ich hier schon 
Schauergeschichten über den GCC gelesen. Danach benimmt der sich 
gelegentlich glatt daneben und "optimiert" angeblich ganze Programmteile 
hinweg. Kann ich nicht nachvollziehen, da ich den nicht nehme. Der Keil 
hingegen erlaubt sich nach meiner Erfahrung keine derartigen 
Extravaganzen.

Die Optimierungen beim Keil sehen häufig so aus, daß Zugriffe auf lokale 
Variablen innerhalb von Funktionen durchaus auch mal umgestellt werden. 
Aber die Zugriffe auf globale Variablen oder gar auf HW-Register finden 
grundsätzlich GENAU SO statt, wie geschrieben. Allerdings macht der Keil 
gelegentlich etwas dahingehend, daß er sich einen Zeiger kreiert, mit 
dem die HW-Register oder globalen Variablen mit platzsparenderen 
Maschinenbefehlen erreichbar sind, als wenn deren konkrete Adresse 
jedesmal separat gebildet werden müßte. Das ändert jedoch am 
tatsächlichen Zugriff nichts.

Nochwas:
Falls du in deiner Firmware selbst am blutigen Anfang keine 
blockierenden Trampelschleifen haben willst, dann arbeite mit Events. 
Sowas hab ich hier schon x-mal gepostet.
Also:
1. Den SysTick in Gang setzen und die Systemuhr damit einrichten
2. Event-System initialisieren
3. je nach Gusto dann eben einen odeer mehrere delayed Events starten.
4. Die Event-Queue in der Grundschleife in main pollen. Wenn dann nach 
der Delay-Zeit der Event in de Queue erscheint, ihn eben abarbeiten.

W.S.

von Markus F. (mfro)


Lesenswert?

W.S. schrieb:
> Kann ich nicht nachvollziehen, da ich den nicht nehme.

Kann ich auch nicht nachvollziehen, obwohl ich den nehme.

von Bernd K. (prof7bit)


Lesenswert?

W.S. schrieb:
> Was die Compiler-Optimierungen betrifft, so habe ich hier schon
> Schauergeschichten über den GCC gelesen.

Ja, da werden gerne Schauermärchen erzählt, am Ende stellt sich jedesmal 
raus daß derjenige den Code so geschrieben hat daß er überflüssige 
Anweisungen ohne Wirkung enthielt oder beabsichtigte Wirkungen von 
ansonsten offensichtlich wirkungslosem Code dem Compiler nicht bekannt 
gemacht wurden, solche Teile dürfen natürlich selbstverständlich 
komplett eliminiert werden (sollen sogar, warum sonst sollte man die 
Optimierung einschalten wenn man das nicht wollte?).

> Danach benimmt der sich
> gelegentlich glatt daneben und "optimiert" angeblich ganze Programmteile
> hinweg.

Gehört bis zum Beweis des Gegenteils ins Reich der Märchen.

: Bearbeitet durch User
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.