2025-12-17 2:27 AM - last edited on 2025-12-17 2:38 AM by Andrew Neil
I'm working on a project with the STM32H503CBU7 which requires USB. I have a 32MHz external crystal on my board that appears to be stable. The chip works and can be programmed and such. I also troubleshot out other physical issues like cold joints and shorts because that has burned me before when the USB device fails to enumerate.
My intent was to use the PLL2Q source with the USB peripheral so I could also specify a 200MHz sys clock for everything else via PLL1. When I use the PLL2Q source however, I get no interrupts from the `USB_DRD_FS_IRQHandler` handler and so my USB stack does not operate.
To make things more confusing, it also does not work even when both PLL's have the same values set for all parameters, as shown in the image below.
It is important to note that this works with the PLL1Q source, as it is shown in the image, and I'm currently compromising a little on the sys clock speed. It will work for my purposes, but I'm more interested to know whether there's some "magic bullet" setting I'm missing, or if this could be a known configuration limitation.
The generated code in my `main.c` file that configures the peripheral clocks seemed to be okay, so I don't think it is a code auto-gen issue, but I could also be wrong about that.
I have already tried this on two chips because I thought there could be something wrong with the first, but they both exhibit this behavior.
Solved! Go to Solution.
2025-12-17 5:09 PM - edited 2025-12-17 5:31 PM
Thanks to both @gbm and @TDK for the recommendations to check the registers while debugging!
I found that the PLL2QEN bit was not getting set in the RCC->RCC_PLL2CFGR register due to a code auto-gen bug in CubeMX (6.16.1).
This is the default line of code generated when PLL2Q is selected in the clock config UI:
PeriphClkInitStruct.PLL2.PLL2ClockOut = RCC_PLL2_DIVP|RCC_PLL2_DIVR;It should actually be
PeriphClkInitStruct.PLL2.PLL2ClockOut = RCC_PLL2_DIVP|RCC_PLL2_DIVR|RCC_PLL2_DIVQBecause all three outputs are in use.
Manually adding this register-set between the peripheral clock init and the rest of the init functions works for me and won't get clobbered when the auto-gen re-runs. I should probably know better than to rely so heavily on CubeMX code generation, but I'm not to the point with where I can remove the convenience of it for handling other things.
/* USER CODE BEGIN SysInit */
// auto-gen code does not enable the PLL2Q output which is required for USB clock
RCC->PLL2CFGR |= RCC_PLL2CFGR_PLL2QEN;
/* USER CODE END SysInit */I also tried moving all my other clock sources back to PLL1 but keeping just USB on PLL2, and it's as if CubeMX doesn't "think" that PLL2Q exists because it removes the entire auto-gen function for the peripheral clock initialization at that point. I'll have to make use of @gbm's suggestion in the meantime to properly configure PLL2.
I ended up just copying the auto-generated code using the init struct because it's easier for me to read and comprehend:
void PeriphClk_Config(void)
{
RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0};
/** Initializes the peripherals clock
*/
PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_USB;
PeriphClkInitStruct.PLL2.PLL2Source = RCC_PLL2_SOURCE_HSE;
PeriphClkInitStruct.PLL2.PLL2M = 2;
PeriphClkInitStruct.PLL2.PLL2N = 12;
PeriphClkInitStruct.PLL2.PLL2P = 2;
PeriphClkInitStruct.PLL2.PLL2Q = 4;
PeriphClkInitStruct.PLL2.PLL2R = 1;
PeriphClkInitStruct.PLL2.PLL2RGE = RCC_PLL2_VCIRANGE_3;
PeriphClkInitStruct.PLL2.PLL2VCOSEL = RCC_PLL2_VCORANGE_WIDE;
PeriphClkInitStruct.PLL2.PLL2FRACN = 0;
PeriphClkInitStruct.PLL2.PLL2ClockOut = RCC_PLL2CFGR_PLL2QEN;
PeriphClkInitStruct.UsbClockSelection = RCC_USBCLKSOURCE_PLL2Q;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK)
{
Error_Handler();
}
}I call this function after the sys clock init and before the other peripheral inits.
2025-12-17 7:03 AM
For clarity, you said the configuration in the screenshot works, yes?
But if you change the radio button from PLL1Q to PLL2Q without changing anything else, it no longer works?
Shouldn't be a magic setting anywhere. Does it fail to enumerate? If you debug your code, does it make it past initialization and through MX_USB_Init okay?
2025-12-17 7:35 AM
Check what happens if you set the USB clock to HSI48 synchronized to USB SOF - this configuration must work regardless of main oscillator failure/malfunction. Once this works, verify the HSE. It may oscillate at some sub-harmonic of its nominal frequency.
2025-12-17 9:45 AM
Yes, that's the confusing part: both PLLs based off the HSE with the same MHz configured, but with PLL2Q no interrupts are triggered whereas PLL1Q they are.
By "fails to enumerate" are you referring to the USB device on the host machine, or -- pardon my lack of knowledge, I'm pretty green in embedded dev -- some other type of internal enumeration like a register that contains information about whether that PLL is operating correctly?
To answer the first case if so: the host can enumerate it with PLL1Q selected, and not with PLL2Q selected. To also answer part of @gbm's question, it also enumerates on the host with the HSI48 source.
I've ordered some different caps to try tuning my oscillator, which is a 9pf. I've assumed a 4 pf stray capacitance so I've used 10pf load caps. But I'll check with 9pf load caps and 8pf load caps as well.
2025-12-17 10:28 AM
By "fails to enumerate" I mean does the host system register an unknown device in the Device Manager when you connect it? This is what happens when the USB pullup is enabled but the device doesn't respond correctly.
If not, it may not even be getting to the USB initialization code. You should debug your code and find out where execution stops.
2025-12-17 11:39 AM
I do get the "Device not recognized" in the PLL2Q case. I've checked with a probe on the D+ line and the pull-up is active to +3v3, so the PHY is definitely doing *something* and there are no faults throughout the init steps and into the main while(1) loop.
It is also worth mentioning that I'm using all of the CubeMX auto-generated code for a USB peripheral initialization, but my USB stack is tinyusb. Keep in mind that I'm migrating a working codebase from a previous project based on the STM32F103C8 so I'm confident it's not the stack itself that's causing problems -- and also because the PLL1Q and HSI48 cases fully work on this chip.
I'm able to manually control the behavior of the PHY by setting that pullup register myself, e.g after the auto-gen USB init function is called.
I don't have a probe that can decode USB protocol, so I can't tell how far along the PHY gets in the communication process before it mysteriously doesn't trigger my interrupt handlers to send the requisite descriptors back to the host.
I'll need some advice as to where to check for "execution to stop." When I went poking around the codebase to see where the `USB_DRD_FS_IRQHandler` is referenced it seemed to be straight into the linker. All of the rest of the initialization proceeds nominally, but there's a chicken-egg problem with trying to break in the interrupt handler if it is never called.
To me it doesn't seem like the clock config is making it in, or the clock mux is not switching correctly for this case.
2025-12-17 11:50 AM
Probably a bug somewhere in the clock configuration.
Maybe route PLL2Q out to a timer and verify the speed is as expected there. Also verify at the register level that PLL2Q is set as the USB clock.
2025-12-17 12:25 PM
Roger that. I'll hunt around and see what I can find, and try the timer approach.
If you happen to remember which register(s) to check off the top of your head that would save me some time hunting around in the manual, otherwise feel free not to respond.
2025-12-17 1:19 PM
I did some experiments to check the case described on Nucleo-H503. USB is operating correctly with HSI48 and HSE/PLL2Q. I attach the minimal code for HSE/PLL2 setup. It mighty be useful for checking the RCC register content in a debug session. In my code, both PLLs are configured in the same way. USB clock source is selected by setting RCC->CCIPR4;
#define HCLK_FREQ 240000000u
static inline void ClockSetup2(void)
{
RCC->PLL2DIVR = (2 - 1) << RCC_PLL2DIVR_PLL2P_Pos | (HCLK_FREQ * 2 / 48000000 - 1) << RCC_PLL2DIVR_PLL2Q_Pos | (2 - 1) << RCC_PLL2DIVR_PLL2R_Pos
| (HCLK_FREQ / 1000000u - 1) << RCC_PLL2DIVR_PLL2N_Pos;
RCC->CR |= RCC_CR_HSEON | RCC_CR_HSI48ON;
while ((RCC->CR & (RCC_CR_HSERDY | RCC_CR_HSI48RDY)) != (RCC_CR_HSERDY | RCC_CR_HSI48RDY)) ;
// 2 MHz from 24 MHz HSE -> div by 12
RCC->PLL2CFGR = RCC_PLLxCFGR_PLLxSRC_HSE | (HSE_VALUE / 2000000u) << RCC_PLL2CFGR_PLL2M_Pos | RCC_PLL2CFGR_PLL2PEN | RCC_PLL2CFGR_PLL2QEN;
RCC->CR |= RCC_CR_PLL2ON;
// wait for PLL ready
while (~RCC->CR & RCC_CR_PLL2RDY) ;
// select USB clock
RCC->CCIPR4 = 2u << RCC_CCIPR4_USBSEL_Pos; // PLL2Q as USB clock
}
2025-12-17 5:09 PM - edited 2025-12-17 5:31 PM
Thanks to both @gbm and @TDK for the recommendations to check the registers while debugging!
I found that the PLL2QEN bit was not getting set in the RCC->RCC_PLL2CFGR register due to a code auto-gen bug in CubeMX (6.16.1).
This is the default line of code generated when PLL2Q is selected in the clock config UI:
PeriphClkInitStruct.PLL2.PLL2ClockOut = RCC_PLL2_DIVP|RCC_PLL2_DIVR;It should actually be
PeriphClkInitStruct.PLL2.PLL2ClockOut = RCC_PLL2_DIVP|RCC_PLL2_DIVR|RCC_PLL2_DIVQBecause all three outputs are in use.
Manually adding this register-set between the peripheral clock init and the rest of the init functions works for me and won't get clobbered when the auto-gen re-runs. I should probably know better than to rely so heavily on CubeMX code generation, but I'm not to the point with where I can remove the convenience of it for handling other things.
/* USER CODE BEGIN SysInit */
// auto-gen code does not enable the PLL2Q output which is required for USB clock
RCC->PLL2CFGR |= RCC_PLL2CFGR_PLL2QEN;
/* USER CODE END SysInit */I also tried moving all my other clock sources back to PLL1 but keeping just USB on PLL2, and it's as if CubeMX doesn't "think" that PLL2Q exists because it removes the entire auto-gen function for the peripheral clock initialization at that point. I'll have to make use of @gbm's suggestion in the meantime to properly configure PLL2.
I ended up just copying the auto-generated code using the init struct because it's easier for me to read and comprehend:
void PeriphClk_Config(void)
{
RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0};
/** Initializes the peripherals clock
*/
PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_USB;
PeriphClkInitStruct.PLL2.PLL2Source = RCC_PLL2_SOURCE_HSE;
PeriphClkInitStruct.PLL2.PLL2M = 2;
PeriphClkInitStruct.PLL2.PLL2N = 12;
PeriphClkInitStruct.PLL2.PLL2P = 2;
PeriphClkInitStruct.PLL2.PLL2Q = 4;
PeriphClkInitStruct.PLL2.PLL2R = 1;
PeriphClkInitStruct.PLL2.PLL2RGE = RCC_PLL2_VCIRANGE_3;
PeriphClkInitStruct.PLL2.PLL2VCOSEL = RCC_PLL2_VCORANGE_WIDE;
PeriphClkInitStruct.PLL2.PLL2FRACN = 0;
PeriphClkInitStruct.PLL2.PLL2ClockOut = RCC_PLL2CFGR_PLL2QEN;
PeriphClkInitStruct.UsbClockSelection = RCC_USBCLKSOURCE_PLL2Q;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK)
{
Error_Handler();
}
}I call this function after the sys clock init and before the other peripheral inits.