STM32G484 USB CDC Code läuft nur wenn mit <= -O1 kompiliert

OP #8090403
Lesenswert?

Hallo,

habe ein Board (Eigenentwicklung) mit STM32G484 und würde gerne den virtuellen Comport über USB verwenden. Habe dazu den Code von Stefan Frings Webseite (https://stefanfrings.de/stm32/stm32g4.html#vcpnohal) verwendet: https://stefanfrings.de/stm32/STM32G431CB_usb_test.zip

Im ersten Schritt habe ich das Projekt "so wie es aus dem Zip-Archiv kommt" kompiliert (Die Optimierung war dabei auf -O1 eingestellt) und auf den Mikrocontroller geflasht. Es hat alles direkt funktioniert, ohne Anpassungen. Das Gerät wird damit bei jedem Anstecken sauber erkannt und sendet auch den String, welchen ich mit einem Terminal-Programm am PC empfangen kann.

Dann habe ich die Optimierung auf -O2 (und später auch auf -O3) gestellt. Damit Enumeriert das Device nicht mehr, bzw. nur noch in sehr seltenen Fällen (gefühlt 1 von 100 Ansteckversuchen klappt).

Woran kann das liegen? Ist da irgendwo ein Bug im Code?

dmesg unter Ubuntu liefert:

1
[ 7273.530542] usb 3-1: new full-speed USB device number 22 using xhci_hcd
2
[ 7273.646576] usb 3-1: device descriptor read/64, error -32
3
[ 7273.868572] usb 3-1: device descriptor read/64, error -32
4
[ 7273.976592] usb usb3-port1: attempt power cycle
5
[ 7274.356538] usb 3-1: new full-speed USB device number 23 using xhci_hcd
6
[ 7274.356872] usb 3-1: Device not responding to setup address.
7
[ 7274.560871] usb 3-1: Device not responding to setup address.
8
[ 7274.768530] usb 3-1: device not accepting address 23, error -71
9
[ 7274.768619] usb 3-1: WARN: invalid context state for evaluate context command.

Habe leider keine Ahnung, wie ich das weiter debuggen kann. Habt ihr da Tipps für mich?

Vielen Dank und viele Grüße, Georg

#8090463
Lesenswert?

Solche Fehler kenne ich auch.

Was ich bisher schon hatte:

Bug im Code: Undefined Behavior in C oder C++. Der Code verlässt sich auf etwas, worauf er sich nicht verlassen kann. Bei höherer Optimierung verändert der Compiler dann das verhalten.

Den Vorschlag von Anton kannst Du versuchen. Kann aber auch sein dass das Problem erst in der Linker-Stufe ist, dann helfen Änderungen im Compile-Teil nichts.

Du kannst den Code aber auch mal von einer auf Coding spezialisierten AI auf Undefined Behavior untersuchen lassen. Die Regeln dafür sind ziemlich komplex, das alles im Detail im Kopf zu haben ist nicht so einfach. Da geht die AI oft systematischer und genauer vor. Rechne aber auch damit dass Fehlerkennungen rauskommen.

Bug im Optimizer des Compilers: Der Compiler denkt er dürfte was optimieren, das ist aber falsch und geht dann kaputt. Hatte ich schon bei einer älteren Version des gcc, trat nur bei Code für Cortex-M0 auf. Du hast ja Cortex-M4, ist also nicht dieser Bug den ich damals hatte. Könnte aber etwas ähnliches sein. Schau also zumindest mal ob Du eine einigermaßen aktuelle Version des GCC hast.

OP #8090466
Lesenswert?

Hatte noch kurz Zeit und habe folgendes ausprobiert:

  1. O3 im makefile eingestellt
  2. #pragma GCC optimize ("O1") vor die erste Funktion im usb.c File gestellt
  3. #pragma GCC optimize ("O3") vor die erste Funktion für den "Application-Layer" im usb.c File gestellt

--> damit funktioniert es dann. Werde heute Abend dann den Bereich eingrenzen und es so hoffentlich finden.

OP #8090693
Lesenswert?

Guten Abend,

habe herausgefunden, dass wenn ich die Funktion InitEndpoints(void) in die pragmas packe, also so:

1
#pragma GCC optimize ("O1")
2
InitEndpoints(void)
3
{
4
    trace("InitEndpoints\n");
5
    USB_CNTR = 1;          // disable reset and int
6
    CMD.Configuration = 0; // nothing before CONFIGURED
7
    CMD.TransferLen = 0;   // nothing to transfer
8
    CMD.PacketLen = 0;     // nothing to transfer
9
    CMD.TransferPtr = 0;
10
    USB_CNTR = 0;          // all int off
11

12
    suspended = false;
13
    configurationSet = false;
14
    transmitting = false;
15
    receiving = false;
16

17
    // EP0 = Control, IN and OUT
18
    EpTable[0].TxOffset = Ep0TxOffset;
19
    EpTable[0].TxCount = 0;
20
    EpTable[0].RxOffset = Ep0RxOffset;
21
    EpTable[0].RxCount = EpCtrlLenId;
22

23
    // EP1 = Bulk IN (only IN)
24
    EpTable[1].TxOffset = Ep1TxAOffset;
25
    EpTable[1].TxCount = 0;
26
    EpTable[1].RxOffset = Ep1TxBOffset; // here 2nd tx buffer
27
    EpTable[1].RxCount = EpBulkLenId;
28

29
    // EP2 = Bulk OUT (only OUT)
30
    EpTable[2].TxOffset = Ep2RxAOffset;
31
    EpTable[2].TxCount = EpBulkLenId;
32
    EpTable[2].RxOffset = Ep2RxBOffset;
33
    EpTable[2].RxCount = EpBulkLenId;
34

35
    // EP3 = Int, IN and OUT
36
    EpTable[3].TxOffset = Ep3TxOffset;
37
    EpTable[3].TxCount = EpIntLenId;
38
    EpTable[3].RxOffset = Ep3RxOffset;
39
    EpTable[3].RxCount = EpIntLenId;
40

41
    USB_BTABLE = EpTableOffset;
42

43
    USB_EP0R =
44
        (3 << 12) |              // STAT_RX = 3, rx enabled
45
        (2 << 4) |               // STAT_TX = 2, send nak
46
        (1 << 9) |               // EP_TYPE = 1, control
47
        logEpCtrl;
48

49
    USB_EP1R =
50
        (0 << 12) |              // STAT_RX = 0, rx disabled
51
        (2 << 4) |               // STAT_TX = 2, send nak
52
        (0 << 9) |               // EP_TYPE = 0, bulk
53
        logEpBulkIn;
54

55
    USB_EP2R =
56
        (3 << 12) |              // STAT_RX = 3, rx enabled
57
        (0 << 4) |               // STAT_TX = 0, tx disabled
58
        (0 << 9) |               // EP_TYPE = 0, bulk
59
        logEpBulkOut;
60

61
    USB_EP3R =
62
        (3 << 12) |              // STAT_RX = 3, rx enabled
63
        (2 << 4) |               // STAT_TX = 2, send nak
64
        (3 << 9) |               // EP_TYPE = 0, bulk
65
        logEpInt;
66

67
    USB_ISTR = 0;                // remove pending interrupts
68
    USB_CNTR =
69
        CTRM |                   // Int after ACK packages in or out
70
        RESETM |                 // Int after reset
71
        SUSPM | WKUPM | ESOFM |
72
        SOFM;                    // Int every 1ms frame
73
    USB_SetAddress(0);
74
}
75
#pragma GCC optimize ("O3")

Dass dann das Programm funktioniert.

Hat jemand einen Tipp für mich, wie ich weiter machen kann? Wo liegt genau die Ursache?

OP #8090700
Lesenswert?

Ok, habe wohl was rausgefunden:

In der oben identifizierten Funktion wird ja EpTable verwendet. Das ist so definiert:

1
#define EpTable   ((struct TEpTableEntry *) (USB_RAM + (EpTableOffset<<UMEM_SHIFT)))

habe das zu

1
#define EpTable   ((volatile struct TEpTableEntry *) (USB_RAM + (EpTableOffset<<UMEM_SHIFT)))

geändert und jetzt geht alles auch mit -O3 und ohne irgendwelche pragmas. Fehlt da echt einfach das volatile? Ist das ein Bug? Kann das jemand bestätigen?

#8090712
Lesenswert?

Ich kann dein Problem mit einem STM32G431CBT6 reproduzieren. Der Code funktioniert auch bei mir nicht mit -O2 und -O3. Und ich stimme dir auch zu, dass der Zugriff auf EpTable[] der Knackpunkt ist, nicht die delays.

Allerdings kann ich deine Lösung nicht nachvollziehen. Trotz eingefügtem volatile funktioniert es bei mir immer noch nicht mit -O2 und -O3. Also ist das wohl noch nicht die korrekte Lösung.

: Bearbeitet durch User
OP #8090950
Lesenswert?

Hallo Hans,

Danke fürs Ausprobieren. Ich konnte Deine Beobachtungen bei mir jetzt auch nachvollziehen und habe jetzt insgesamt 3 Änderungen gemacht:

1
#define EpTable   ((struct TEpTableEntry *) (USB_RAM + (EpTableOffset<<UMEM_SHIFT)))
2
#define EpTable   ((volatile struct TEpTableEntry *) (USB_RAM + (EpTableOffset<<UMEM_SHIFT)))
1
struct TCommand CMD;
2
volatile struct TCommand CMD;
1
int ReadControlBlock(uint8_t* PBuffer, int maxlen)
2
int ReadControlBlock(volatile uint8_t* PBuffer, int maxlen)

Könntest Du das bitte bei Dir auch ausprobieren, ob es dann geht? Anbei auch die modifizierte usb.c Datei. Es kommt jetzt zwar eine Compiler-Warning (wenn O3 aktiv ist), aber ich glaube, die kann man ignorieren, bzw. später noch fixen.

Vielen Dank für die Unterstützung!

Angehängte Dateien:
#8091047
Lesenswert?

Alternative: Unmittelbar vor die Zeile

1
USB_BTABLE = EpTableOffset;

eine Memory Barrier einbauen. Die gibt es oft bereits als Macro definiert.

Ansonsten für gcc:

1
asm volatile ("" ::: "memory");
2
USB_BTABLE = EpTableOffset;

Die Idee ist das der optimierende Compiler ansonsten das Setzen der Struktur ans Ende der Funktion verschieben kann, d.h. die Funktion wird mit einem Sprung zu "memset" verlassen.

Mit der Memory Barrier darf er das dann nicht mehr.

Achtung: Die __DMB() und __DSB() CMSIS Macros haben keinen Memory Clobber, das könnte hier schief gehen.

#8091211
Lesenswert?

Jim M. schrieb:

asm volatile ("" ::: "memory");

Hat leider nicht geklappt, ebenso wenig __DMB() und __DSB(), die hatte ich am Samstag schon versucht.

Georg M. schrieb:

habe jetzt insgesamt 3 Änderungen gemacht

1
#define EpTable   ((volatile struct TEpTableEntry *) (USB_RAM + (EpTableOffset<<UMEM_SHIFT)))
2
volatile struct TCommand CMD;
3
int ReadControlBlock(volatile uint8_t* PBuffer, int maxlen)

Damit funktioniert es bei mir nicht.

Johann L. schrieb:

Bringt denn -fno-strict-aliasing etwas?

Nein

-Wstrict-aliasing=3

Hilft auch nicht.

Bislang hilft nur, das:

1
#pragma GCC push_options
2
#pragma GCC optimize ("O1")
3
void InitEndpoints(void) {
4
   ...
5
}
6
#pragma GCC pop_options

Ich hatte die Funktion am Samstag in drei einzelne Funktionen aufgeteilt, die dann direkt nacheinander aufgerufen werden.

  • Teil1: Die Zeilen davor
  • Teil2: Die Zeilen mit EpTable[]
  • Teil3: Die Zeilen danach

Nur für Teil2 musste ich die Optimierung auf O1 herab setzen.

: Bearbeitet durch User
#8091229
Lesenswert?

Georg M. schrieb:

USB_CNTR = 1; // disable reset and int

[schnipp]

USB_CNTR = 0; // all int off

Nicht, dass ich die geringste Ahnung von STMx hätte. Aber das erscheint mir widersprüchlich.

Das USB darf erst dann irgendwas auslösen, wenn es komplett konfiguriert ist. Ist das sichergestellt? Deine mehr oder weniger sporadischen Fehler hören sich nicht so an.

Beim Versuch rauszufinden was das USB_CNTR macht, bin ich auf das gestoßen:
https://community.st.com/stm32-mcus-embedded-software-32/lowlevel-usb-programming-86152

Wenn das Unsinn war, nicht mal ignorieren. ;-)

#8091290
Lesenswert?

Nick schrieb:

USB_CNTR

Ich kann deine Frage nicht beantworten, allerdings bin ich sehr sicher, dass dies nicht die Problemursache ist. Denn

a) ich habe den Code in vielen Projekten auf Blue-Pill Boards (STM32F103) eingesetzt, wo er bisher stets problemlos lief. Die höheren Optimierungsstufen habe ich bisher nicht benutzt, um genau solche Probleme wie hier zu vermeiden. Leider habe ich habe kein Blue-Pill Board mehr vorrätig, womit ich -O3 testen könnte.

b) er läuft ja auch auf dem STM32G4, mit -O1.

Am Samstag hatte ich versucht, den erzeugten Assembler-Code von -O1 mit -O3 zu vergleichen. Aber was der Compiler in der hohen Stufe fabriziert durchblicke ich nicht mehr.

: Bearbeitet durch User
#8091642
Lesenswert?

Ich habe noch ein Board mit STM32F303CCT6 gefunden, damit kann ich das Problem ebenfalls nachvollziehen. Die Funktion InitEndpoints() Funktioniert mit O1 aber nicht mit O2 und O3.

Fehlermeldung vom Linux PC:

1
[  412.227447] usb 1-10.3: Device not responding to setup address.
2
[  412.431098] usb 1-10.3: device not accepting address 22, error -71
3
[  412.431466] usb 1-10.3: WARN: invalid context state for evaluate context command.
4
[  412.503131] usb 1-10.3: new full-speed USB device number 23 using xhci_hcd
5
[  412.503320] usb 1-10.3: Device not responding to setup address.
6
[  412.707261] usb 1-10.3: Device not responding to setup address.
7
[  412.911215] usb 1-10.3: device not accepting address 23, error -71
8
[  412.911585] usb 1-10.3: WARN: invalid context state for evaluate context command.
9
[  412.911654] usb 1-10-port3: unable to enumerate USB device

Hinzufügen von 3x volatile

1
#define EpTable   ((volatile struct TEpTableEntry *) (USB_RAM + (EpTableOffset<<UMEM_SHIFT)))
2
volatile struct TCommand CMD;
3
int ReadControlBlock(volatile uint8_t* PBuffer, int maxlen)

und die memory Barrieren

1
asm volatile ("" ::: "memory");
2
USB_BTABLE = EpTableOffset;
3
asm volatile ("" ::: "memory");

und die Compiler Optionen

1
-Wstrict-aliasing=3 -fno-strict-aliasing

hilft leider nicht.

Aber diese Zeilen helfen:

1
#pragma GCC push_options
2
#pragma GCC optimize ("O1")
3
void InitEndpoints(void) {
4
   ...
5
}
6
#pragma GCC pop_options

Also exakt das gleiche Problem auch bei diesem Board.

: Bearbeitet durch User
#8091649
Lesenswert?

Hans W. schrieb:

Falls sich jemand mit Durchblick den Assembler Code (für den STM32G431CBT6) anschauen möchte, siehe Anhang.

Ich hab ja von Anfang an vorgeschlagen dass mal in eine AI zur Analyse zu werfen.

Hier qwen3.8-2.4t-a95b:

Analysis: InitEndpoints() only works with -O1

Symptom

source.c was compiled for an ARM Cortex-M4 with GCC at three optimization levels. Only the -O1 build works reliably; -O2 and -O3 fail.

What the code touches

EpTable lives in the USB packet memory / buffer descriptor table (BTABLE):

  • USB register base: 0x40005C00 (EP0R=+0x00, CNTR=+0x40, ISTR=+0x44, DADDR=+0x4C, BTABLE=+0x50) → the STM32-style USB FS device peripheral.
  • Packet memory base: 0x40006000, with USB_BTABLE = EpTableOffset = 0x190, so the descriptors live at 0x40006190 … 0x400061AF.

Per the reference manual, the BTABLE descriptor registers inside packet memory are 16-bit registers that may only be accessed with 16-bit (halfword) transfers.

The difference between the builds

-O1 (works) — every descriptor field is a separate halfword store

1
8000474: strh.w r2, [r3, #144]   @ 0x40006190  EpTable[0].TxOffset
2
8000478: strh.w r2, [r3, #146]   @ 0x40006192  EpTable[0].TxCount
3
800047e: strh.w r0, [r3, #148]   @ 0x40006194  EpTable[0].RxOffset
4
8000486: strh.w r0, [r3, #150]   @ 0x40006196  EpTable[0].RxCount
5
... all EpTable fields written one 16-bit store at a time

-O2 / -O3 (fail) — adjacent 16-bit fields merged into 32-bit stores

GCC's store-merging pass (-fstore-merging, enabled only from -O2) has combined the adjacent uint16_t fields into word / doubleword stores:

1
8000480: strd r2, r4, [r1, #400] @ 0x40006190  TxOffset|TxCount  as one 32-bit word
2
                                  @ 0x40006194  RxOffset|RxCount  as one 32-bit word
3
8000486: strd r0, r4, [r1, #408] @ entry 1, again 2 x 32-bit
4
800048c: str.w r4, [r1, #416]    @ entry 2 TxOffset|TxCount as one 32-bit word
5
8000492: str.w r4, [r1, #420]    @ entry 2 RxOffset|RxCount as one 32-bit word
6
8000498: str.w r4, [r1, #424]    @ entry 3 ...
7
800049e: str.w r4, [r1, #428]

Root cause

The descriptor struct is declared with plain (non-volatile) uint16_t fields. Because the fields are not volatile, the compiler may reorder and merge the stores — and at -O2/-O3 it does exactly that, fusing adjacent halfword writes into 32-bit stores.

But the USB packet memory only supports 16-bit accesses. The 32-bit writes do not land as two consecutive halfwords, so the buffer descriptor table at 0x40006190 gets corrupted (wrong/missing buffer offsets and counts). EP0 then cannot receive SETUP packets, enumeration never completes → the device fails.

At -O1 the store-merging pass is not run, all writes stay strh, and it works.

Notes:

  • The USB registers themselves (USB_EP0R, USB_CNTR, …) are unaffected — they are evidently declared as volatile 32-bit, so they are always emitted as single word stores in all three builds.
  • A second, latent problem visible at -O2/-O3: because the descriptors and CMD are not volatile, GCC also reorders their writes around the USB_CNTR writes.

Fix

Make the fields of the BTABLE struct volatile (like ST's headers do for the EP registers):

1
typedef struct {
2
    volatile uint16_t TxOffset;
3
    volatile uint16_t TxCount;
4
    volatile uint16_t RxOffset;
5
    volatile uint16_t RxCount;
6
} EpEntry;
7

8
#define EpTable ((volatile EpEntry *) (0x40006000 + EpTableOffset))

This forces one strh per field and preserves the write order, so the code works at any optimization level.

A quick-and-dirty alternative is compiling with -fno-store-merging, but that only papers over the missing volatile — fix the types instead, and audit any other structs mapped into the 0x40006000 region, since everything written there must be accessed as 16-bit.

: Bearbeitet durch User
Persönliche Seite #8091658
Lesenswert?

Hans W. schrieb:

So weit waren wir doch schon: es hilft nicht!

Sorgen die Änderungen denn dafür dass die Zugriffe nicht mehr zusammengefasst werden? Wenn Nein, sollte man dieses Problem angehen, wenn ja, gibt's noch ein anderes Problem - zeig dann das neue Disassembler-Listing.

Man könnte auch einfach mal im Debugger am Ende der Funktion einen Breakpoint setzen und schauen, ob die Tabelle und Register die korrekten Werte haben.

: Bearbeitet durch User
#8091660
Lesenswert?

Niklas G. schrieb:

Sorgen die Änderungen denn dafür dass die Zugriffe nicht mehr zusammengefasst werden?

Ich bin nicht sicher. Habe den Assembler Code mal angehängt.

Man könnte auch einfach mal im Debugger am Ende der Funktion einen Breakpoint setzen und schauen, ob die Tabelle und Register die korrekten Werte haben.

Haben sie offensichtlich nicht, sonst würde der Code ja funktionieren.

Angehängte Dateien:
: Bearbeitet durch User
#8091669
Lesenswert?

Ich tue mich mit dem Debuggen schwer. Wenn ich InitEndpoints() mit O1 compiliere, dann funktionieren die Breakpoints. Wenn ich sie hingegen mit O2 compiliere, dann werden die Breakpoints ignoriert, egal wo ich sie hin setze. Ich vermute mal, dass der Debugger mit der hohen Optimierungsstufe nicht zurecht kommt.

Ich habe deswegen ganz am Ende die Zeile

1
 __asm__ volatile ("NOP");

eingefügt und darauf den Breakpoint gesetzt. Klappt aber auch nicht.

Ich mache für heute mal Schluss. Vielleicht mag Georg das Debugging fortsetzen.

: Bearbeitet durch User
#8091670
Lesenswert?

Niklas G. schrieb:

Also qwen auf das neue Ergebnis ansetzen

Result: Why -O2 -fno-store-merging still fails — root cause #2: store reordering

Status

Build Width of BTABLE stores Order of BTABLE stores Works?
-O1 16-bit (strh) source order ✅ yes
-O2 / -O3 merged to 32-bit (str/strd) reordered ❌ no — root cause #1 (result.md)
-O2 -fno-store-merging 16-bit (strh) — fixed still reordered ❌ no — root cause #2 (this document)
any -O + volatile uint16_t fields 16-bit (strh) source order ✅ yes — the real fix

What the new disassembly shows

In assembler_O2_no-store-merging.txt the width problem is gone: all 16 descriptor fields of the BTABLE at 0x40006190 … 0x400061AE are written with individual strh.w instructions and the correct values.

But the -O2 instruction scheduler has reordered the stores of the non-volatile EpTable fields around the volatile USB register writes:

address instruction problem
8000556 USB_BTABLE = 0x190 written while 0x9c, 0xa0, 0xa4, 0xac are still unwritten
800056c USB_EP2R = 0x3002 (STAT_RX=3 → RX enabled) its RxOffset descriptor (0xa4) still stale
8000574 USB_EP3R = 0x3623 (STAT_RX=3 → RX enabled) its RxOffset descriptor (0xac) still stale
8000578 USB_ISTR = 0
8000584 USB_CNTR = 0x9f00 (all USB interrupts enabled)
8000588 strh EpTable[2].TxOffset (0xa0) descriptor write after CNTR
8000592 strh EpTable[1].RxOffset (0x9c, 2nd TX buffer) descriptor write after CNTR
8000596 strh EpTable[2].RxOffset (0xa4) descriptor write after CNTR
800059e strh EpTable[3].RxOffset (0xac) descriptor write after CNTR
80005a2 USB_DADDR = 0x80 (EF=1, device goes live) only now everything is consistent

Contrast with the working -O1 build (assembler_O1.txt): all 16 descriptor stores (8000474 … 80004d0) complete before USB_BTABLE, USB_EPxR, USB_ISTR, USB_CNTR (80004d8 onwards). No window, no bug.

Why GCC is allowed to do this

  • The C standard (and GCC) guarantee program order only among volatile accesses. The USB registers (USB_CNTR, USB_EPxR, …) are volatile and keep their mutual order — but the EpTable fields are plain (non-volatile) memory, so the compiler may schedule those stores anywhere.
  • -fno-store-merging only disables the store-width-merging pass (pass_store_merging). It does nothing about instruction scheduling / store reordering — a completely separate pass. That is why the flag alone cannot fix the problem.

Why it breaks the hardware

The moment USB_CNTR = 0x9f00 at 8000584 is written, the USB IRQ can fire on any subsequent instruction boundary:

  • ISTR latches SOF / ESOF / RESET / SUSP / WKUP events regardless of the interrupt mask; the mask only controls whether an IRQ is raised.
  • On an attached bus, SOF arrives every 1 ms, and reset/suspend/wakeup events occur constantly during enumeration.

Any ISR entering inside the window 8000584 … 800059e operates on a half-written BTABLE while EP2 and EP3 are already RX-enabled via EP2R/EP3R: the RX descriptors of EP2/EP3 still contain stale/garbage buffer addresses, and EP1's second TX buffer offset is missing. The ISR then computes wrong buffer addresses/counts → corrupted EP0 SETUP reception → enumeration never completes.

Verification (host x86-64 GCC 14, same middle-end passes)

No ARM cross-compiler was available, so the code was reconstructed in repro.c (declarations taken from the disassembly literals) and compiled on the host. Same phenomena, same fix:

Version / flags Generated stores for EpTable
plain fields, -O2 merged into 64-bit movq stores and reordered: USB_BTABLE, USB_EP0R, USB_EP1R emitted before the last descriptor quad
plain fields, -O2 -fno-store-merging all 16-bit movw, but still reordered across BTABLE/EPxR/ISTR/CNTR
volatile uint16_t fields, -O2 16 in-order movw stores, all before BTABLE → EPxR → ISTR → CNTR → DADDR
volatile uint16_t fields, -O3 identical, still correct ✅

Reproduce with:

1
gcc -O2 -S repro.c -o repro_O2.s
2
gcc -O2 -fno-store-merging -S repro.c -o repro_O2_nosm.s
3
gcc -O3 -S repro.c -o repro_O3.s

The fix

volatile uint16_t fields force both properties at once — the exact 16-bit access width and source-order placement relative to the volatile USB registers. One change cures root cause #1 and #2 at every optimization level:

1
typedef struct {
2
    volatile uint16_t TxOffset;
3
    volatile uint16_t TxCount;
4
    volatile uint16_t RxOffset;
5
    volatile uint16_t RxCount;
6
} EpEntry;
7

8
#define EpTable ((volatile EpEntry *)(0x40006000 + EpTableOffset))

Apply this in the header that declares EpTable, then:

  1. Remove -fno-store-merging from the build flags — no longer needed.
  2. Remove the #pragma GCC optimize ("O1") stopgap around InitEndpoints() in source.c — no longer needed.
  3. Make CMD volatile as well: it is shared with the ISR, and at -O2 GCC merges its clears into one 8-byte store (vstr d7) and moves them across the USB_CNTR writes (harmless for SRAM here, but the same latent class of problem).

Important caveats

  • The #pragma GCC optimize ("O1") currently in source.c rescues only this one function. The USB ISR and any send/receive routines that also touch EpTable or the packet memory are still compiled at -O2 and suffer the same merging/reordering. Only the type fix protects all code.
  • Audit every access to the 0x40006000 packet-memory region (BTABLE and buffer data): each access must be volatile and 16-bit wide. This peripheral's packet memory does not support 32/64-bit transfers.
Persönliche Seite #8091671
Lesenswert?

Hans W. schrieb:

Ich vermute mal, dass der Debugger mit der hohen Optimierungsstufe nicht zurecht kommt.

Kompilierst du denn bei -O2 auch mit -g3 ? So manche besonders schlaue IDE schaltet das -g3 im Release-Modus ab (VS Code STM32 Extension...).

Hans W. schrieb:

. Klappt aber auch nicht.

Holzhammermethode ist

1
__BKPT()

bzw.

1
__asm__ volatile ("BKPT");

Wenn der Prozessor da stehen geblieben ist geht's aber nicht mehr weiter - zum drübersteppen kann man den Program Counter (PC) manuell im Debugger um 2 erhöhen.

: Bearbeitet durch User
#8091676
Lesenswert?

Niklas G. schrieb:

Kompilierst du denn bei -O2 auch mit -g3

Ich benutze -g

Letzter Versuch für heute, obwohl wir das doch schon versucht hatten:

  • Volatile hinzufügen:
1
#define EpTable   ((volatile struct TEpTableEntry *) (USB_RAM + (EpTableOffset<<UMEM_SHIFT)))
2
volatile struct TCommand CMD;
  • Entferne -fno-store-merging
  • Compiliere mit -O2 -> läuft
  • Compiliere mit -O3 -> läuft

Das verstehe ich jetzt nicht. Denn das hatte ich doch bereits vorher versucht (siehe Beitrag "Re: STM32G484 USB CDC Code läuft nur wenn mit <= -O1 kompiliert"), und da klappte es nicht.

Ist das denn jetzt eine saubere Lösung, oder soll man doch besser bei -O1 (für diese eine Funktion) bleiben? Georg, teste du das bitte auch nochmal.

Persönliche Seite #8091677
Lesenswert?

Hans W. schrieb:

Ist das denn jetzt eine saubere Lösung

Ja, das "volatile" ist da schon richtig.

Hans W. schrieb:

oder soll man doch besser bei -O1 (für diese eine Funktion) bleiben?

Ne, künstlich niedrige Optimierungslevel zu nutzen verschleiert den Fehler nur, vielleicht wird eine zukünftige Compiler-Version das auch bei -O1 kaputtoptimieren. Einzelne Funktionen unterschiedlich zu optimieren ist auch chaotisch.

Wenn's dann funktioniert kannst du ja bei mir einen Pull-Request einreichen, macht ja Sinn das zentral "für alle" zu fixen:

https://github.com/Erlkoenig90/WSusb

#8091680
Lesenswert?

Da ist noch was: Nach dem Hinzufügen des volatile kommt diese Warnung:

1
../Src/usb.c: In function 'DoGetStatus':
2
../Src/usb.c:1004:21: warning: storing the address of local variable 'Buf' in 'CMD.TransferPtr' [-Wdangling-pointer=]
3
 1004 |     CMD.TransferPtr = Buf;
4
      |     ~~~~~~~~~~~~~~~~^~~~~
5
../Src/usb.c:927:13: note: 'Buf' declared here
6
  927 |     uint8_t Buf[4];

Wie kriege ich die weg?

OP #8091691
Lesenswert?

Hallo zusammen,

oh, da hat sich ja viel getan. Bei mir funktioniert es mit den oben genannten 3 volatile dann halt zuverlässig, somit tue ich mir schwer, noch was zu debuggen. Aber wie ich sehe geht es jetzt bei hanswieland ja auch? @hanswieland: Wieso geht es jetzt auf einmal und davor hat es mit den 3 volatile nicht funktioniert?

Lasst gerne wissen, wenn ich noch etwas testen soll.

#8091700
Lesenswert?

Georg M. schrieb:

Wieso geht es jetzt auf einmal?

Das wüsste ich auch gerne. Keine Ahnung! Mir ist das richtig unangenehm, denn wir hätten das Thema viel schneller abschließen können.

Zusammenfassung der finalen Änderungen:

Falschen Offset (292) korrigieren:

1
#define Ep3RxOffset   (392)    /* 8 Bytes ab 392 */

2x volatile hinzufügen:

1
#define EpTable   ((volatile struct TEpTableEntry *) (USB_RAM + (EpTableOffset<<UMEM_SHIFT)))
2
volatile struct TCommand CMD;

Weil:

  • der Compiler 16 Bit Zugriffe paarweise zu 32 Bit zusammen fasst
  • der Compiler die Reihenfolge der Zugriffe sonst verändert

In Folge dessen sind dann noch folgende Anpassungen nötig, um Compiler-Warnungen los zu werden:

1x volatile hinzufügen:

1
int ReadControlBlock(volatile uint8_t* PBuffer, int maxlen)

Ganz am Ende in Funktion DoGetStatus() dies einfügen:

1
CMD.TransferPtr = 0;
: Bearbeitet durch User
#8091770
Lesenswert?

Nochmal vielen Dank an Gerd und Niklas für eure Geduld.

Nick schrieb:

Gegen so eine Art von Logik hab ich schon vor langer Zeit aufgegeben zu argumentieren.

Du bist halt Profi, ich nicht. Die höheren Optimierungen des Compilers sind mir teilweise zu komplex, dann blicke ich nicht mehr durch.

Trotzdem auch dir ein herzliches Dankeschön für's Helfen.

: Bearbeitet durch User

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren