Electronics Design AU
STM32Solved

STM32L4 never wakes from Stop mode — button EXTI interrupt just doesn't fire

5 min read4 replies
Original Question

Asked by stale_biscuit_03 ·

Working on a battery-powered sensor node, STM32L476RG (Nucleo board for now). Idea is: device sits in Stop 2 mode most of the time, wakes when someone presses an external button wired to PB13, takes a reading, goes back to sleep.

In Run mode the button works perfectly — I can trigger HAL_GPIO_EXTI_Callback() all day from the debugger. The problem is only after HAL_PWR_EnterSTOPMode(). The MCU goes to sleep (current drops on my bench supply, so I know it's actually in Stop), but pressing the button does nothing. No callback, no wake, current stays low forever. Have to power-cycle to get it back.

Things I've checked:

  • EXTI13 is configured in CubeMX as "External Interrupt Mode with Rising edge trigger" on PB13, pull-down enabled
  • NVIC has EXTI15_10_IRQn enabled, I can see it in the generated stm32l4xx_it.c
  • Multimeter on PB13 confirms it's reading 3.3V when I press the button, so the signal is definitely getting to the pin

This is the exact same GPIO/EXTI config that works fine in Run mode, just doesn't wake the device from Stop. What am I missing?

From the knowledge baseHow Do You Configure STM32 Low-Power Modes (Sleep, Stop, Standby)?

4 Replies

hal_is_lying
Accepted Answer

Almost certainly a line-sharing conflict, and PB13 is exactly the pin that makes this bite people. Is anything else in the project using PC13? On a Nucleo board that's the onboard User button (B1), and it's very common to have it wired up for something else in the same project — even just as a secondary debug input, or left over from an earlier version of the firmware.

Here's the mechanism: STM32's EXTI lines are numbered 0–15, and each number corresponds to "pin N on whichever port you've routed it to" — but only one port at a time can be routed to a given line number. PB13 and PC13 are both "pin 13," so they both compete for EXTI13. The routing lives in SYSCFG->EXTICR[3] (EXTICR4 covers lines 12–15), and HAL_GPIO_Init() overwrites the relevant EXTICR bits every time you initialise a pin in EXTI mode — with no warning that it's stealing the line from whatever was there before.

So if MX_GPIO_Init() configures PC13 as EXTI first (because CubeMX generates pins in port/pin order, and it's auto-added as the User Button), then your PB13 init runs after it and reassigns EXTI13 to port B — that part is actually fine, PB13 wins in that case. But if you've got PC13 configured anywhere after your PB13 init (a library, a later manual call, a HAL_GPIO_Init() you added for a different feature), it silently steals EXTI13 back to port C. Your button's rising edge on PB13 then does nothing at all, because the EXTI13 line is now watching PC13 instead — which explains why it works fine when you trigger it from the debugger (you're calling the ISR/callback directly or the GPIO state happens to line up during your test) but never actually fires from a real button press in Stop mode, where the last SYSCFG_EXTICR write before you enter Stop is what determines which port actually wakes you.

Fastest way to confirm: drop a breakpoint after all your MX_xxx_Init() calls and read SYSCFG->EXTICR[3] directly in the debugger's watch window. Bits [7:4] of that register hold the EXTI13 port selector — 0000 = PA, 0001 = PB, 0010 = PC. If it's not 0001, that's your bug, and you'll find the last HAL_GPIO_Init() call touching pin 13 on some other port somewhere in your init order.

Two ways to fix it: reorder your inits so the PB13 config genuinely runs last (fragile — breaks again the next time someone touches init order), or just move your wake button off pin 13 entirely onto a pin number nothing else in the project uses. That second option is the one I'd actually ship.

cube_mx_curses

Can confirm this one, lost half a day to it on a G4 project. Worth adding: CubeMX's Pinout view will flag it if you look — right-click a pin that's fighting for a shared EXTI line and there's a small warning in the pin config panel about the line already being assigned, but it's easy to miss because it doesn't stop you generating code, and it doesn't show up as a build error, it's purely a GUI hint you have to go looking for.

If reassigning the pin isn't an option (locked into a board revision, say), CubeMX does let you pick a genuinely free line number instead — go through your Pinout view and check which EXTI lines 0–15 are actually free across every port you're using, not just the one you're currently wiring. On an L4 with a handful of buttons, an accelerometer interrupt, and a radio IRQ pin, it's surprisingly easy to run out of unique low-numbered lines and not notice until this exact symptom shows up.

clock_tree_chaos

Once you get the EXTI routing sorted and the wake actually fires, don't be surprised if the next symptom is your UART or SPI transactions immediately after wake coming out garbled, or a delay-based timing routine running way too fast or slow. Stop mode always resumes on HSI regardless of what was clocking the core before entry — worth calling SystemClock_Config() again as literally the first thing in your wake-up handling, before touching any peripheral whose timing depends on the system clock. Covered in more detail in the STM32 low-power modes writeup if you haven't hit that one yet — you will the moment the EXTI fix above starts working.

stale_biscuit_03

That was it exactly. Had HAL_GPIO_Init() for PC13 sitting in a debug helper module I added weeks ago for a "hold button at boot to force DFU" feature, completely forgot it was still in there and it runs after the main GPIO init. Watched SYSCFG->EXTICR[3] like you said — sure enough, 0010 instead of 0001.

Moved my wake button to PA0 instead since nothing else in the project touches it, works first try now. Also added the SystemClock_Config() call in the wake handler before I even got to test it, so thanks for the heads up on that one too — would've been a fun second bug to chase.

Related Discussions