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?
Ich würde mir die wenigen delay Schleifen (nop) anschauen. Die werden in den höheren Stufen vielleicht weg optimiert oder laufen schneller als geplant (ist nur geraten).
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.
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?
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.
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.
eine Memory Barrier einbauen. Die gibt es oft bereits als Macro definiert.
Ansonsten für gcc:
1
asmvolatile("":::"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.
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.
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.
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.
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
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
Gegen so eine Art von Logik hab ich schon vor langer Zeit aufgegeben zu argumentieren.
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
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):
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.
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.
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.
Sieht besser aus, kein "strd" mehr drin. Wegen dem "USB_SetAddress" würde ich Entwarnung geben, das sieht in allen Fällen gut aus. Also qwen auf das neue Ergebnis ansetzen oder den Debugger probieren.
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.
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 beforeUSB_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 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:
Apply this in the header that declares EpTable, then:
Remove -fno-store-merging from the build flags — no longer needed.
Remove the #pragma GCC optimize ("O1") stopgap around InitEndpoints()
in source.c — no longer needed.
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 volatileand 16-bit wide. This
peripheral's packet memory does not support 32/64-bit transfers.
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.
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:
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.