Skip to main content
Associate II
August 4, 2026
Question

STM32WL5MOC integrated RTC/LSE: consistent ~100-120 ppm drift, confirmed after ruling out software causes

  • August 4, 2026
  • 7 replies
  • 112 views

Summary

On an STM32WL5MOC-based LoRaWAN end node, we've measured a consistent ~100 ppm RTC drift on the module's integrated LSE (32.768 kHz) crystal — noticeably outside the commonly quoted ~±20 ppm room-temperature tolerance. We isolated this down to the RTC hardware itself through several independent tests before concluding it's a genuine crystal characteristic (or specific to this device/batch) rather than a software or configuration issue. Posting to ask whether ~100 ppm is within expected variance for this SiP's integrated crystal, or worth escalating further.

Configuration

  • MCU: STM32WL5MOC (Cortex-M4 side)
  • RTC: RTC_BINARY_MIX mode, PREDIV_A=31PREDIV_S=1023 (10-bit synchronous prescaler)
  • RTC clock source: LSE — confirmed selected and ready (RCC_FLAG_LSERDY__HAL_RCC_GET_RTC_SOURCE()) before proceeding; HAL_RTC_MspInit() itself calls Error_Handler() if LSE isn't ready/selected, so normal boot already implies this passed.
  • Application: LoRaWAN Class A end node (US915), periodic uplinks every few minutes, DeviceTimeReq/DeviceTimeAns used for network time sync.

Configuration

  • MCU: STM32WL5MOC (Cortex-M4 side)
  • RTC: RTC_BINARY_MIX mode, PREDIV_A=31PREDIV_S=1023 (10-bit synchronous prescaler)
  • RTC clock source: LSE — confirmed selected and ready (RCC_FLAG_LSERDY__HAL_RCC_GET_RTC_SOURCE()) before proceeding; HAL_RTC_MspInit() itself calls Error_Handler() if LSE isn't ready/selected, so normal boot already implies this passed.
  • Application: LoRaWAN Class A end node (US915), periodic uplinks every few minutes, DeviceTimeReq/DeviceTimeAns used for network time sync.

Result

Consistently ~100-120 ppm (averaging close to 100 ppm) RTC drift on the STM32WL5MOC's integrated LSE crystal, confirmed via three independent methods (multi-day network timestamp analysis, an isolated RTC-only firmware build with everything else stripped out, and on-device DeviceTimeAns interval measurement), with every plausible software-side cause checked and ruled out.

Question

Is ~100 ppm within the expected range for this SiP's integrated 32.768 kHz crystal (batch variance, temperature, aging), or would this be worth flagging as a potential hardware/board-level issue? Any guidance on typical tolerance for the STM32WL5MOC's integrated LSE, or additional steps to further isolate crystal vs. board-level causes (e.g., load capacitance mismatch), would be appreciated.

 

Alex

7 replies

waclawek.jan
Super User
August 4, 2026

Which LSE drive level do you use?

JW

Associate II
August 4, 2026

Hi JW, 

I’m using the high drive as per the documentation RCC_LSEDRIVE_HIGH.

See below my SystemClock_Config() and MX_RTC_Init() functions.

void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

/** Configure LSE Drive Capability
* High drive capability (LSEDRV[1:0] = 11) per ST guidance, given the
* on-board SubGHz radio's RF activity is a plausible noise source for a
* low-drive 32.768kHz oscillator sharing the same board.
*/
HAL_PWR_EnableBkUpAccess();
__HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_HIGH);

/** Configure the main internal regulator output voltage
*/
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE2);

/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE|RCC_OSCILLATORTYPE_MSI;
RCC_OscInitStruct.LSEState = RCC_LSE_ON;
RCC_OscInitStruct.MSIState = RCC_MSI_ON;
RCC_OscInitStruct.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT;
RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_6;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler(ERROR_RCC_OSC_CONFIG);
}

/** Configure the SYSCLKSource, HCLK, PCLK1 and PCLK2 clocks dividers
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK3|RCC_CLOCKTYPE_HCLK
|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1
|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_MSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.AHBCLK3Divider = RCC_SYSCLK_DIV1;

if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
{
Error_Handler(ERROR_RCC_CLOCK_CONFIG);
}
}

void MX_RTC_Init(void)
{

/* USER CODE BEGIN RTC_Init 0 */

/* USER CODE END RTC_Init 0 */

RTC_AlarmTypeDef sAlarm = {0};

/* USER CODE BEGIN RTC_Init 1 */

/* USER CODE END RTC_Init 1 */

/** Initialize RTC Only
*/
hrtc.Instance = RTC;
hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
hrtc.Init.AsynchPrediv = RTC_PREDIV_A;
hrtc.Init.SynchPrediv = RTC_PREDIV_S;
hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
hrtc.Init.OutPutRemap = RTC_OUTPUT_REMAP_NONE;
hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
hrtc.Init.OutPutPullUp = RTC_OUTPUT_PULLUP_NONE;
hrtc.Init.BinMode = RTC_BINARY_MIX;
// Match BCD update to 10-bit synch prediv (0..1023) for 1 Hz calendar tick
hrtc.Init.BinMixBcdU = RTC_BINARY_MIX_BCDU_2;
if (HAL_RTC_Init(&hrtc) != HAL_OK)
{
Error_Handler(ERROR_RTC_INIT);
}

/* USER CODE BEGIN Check_RTC_BKUP */

/* USER CODE END Check_RTC_BKUP */

/** Initialize RTC and set the Time and Date
*/
// Binary mix mode doesn't need SSRU for subsecond underflow
// if (HAL_RTCEx_SetSSRU_IT(&hrtc) != HAL_OK)
// {
// Error_Handler(ERROR_RTC_SET_ALARM);
// }

/** Enable the Alarm A
*/
sAlarm.BinaryAutoClr = RTC_ALARMSUBSECONDBIN_AUTOCLR_NO;
sAlarm.AlarmTime.SubSeconds = 0x0;
sAlarm.AlarmMask = RTC_ALARMMASK_NONE;
sAlarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDBINMASK_NONE;
sAlarm.Alarm = RTC_ALARM_A;
if (HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, 0) != HAL_OK)
{
Error_Handler(ERROR_RTC_SET_ALARM);
}
/* USER CODE BEGIN RTC_Init 2 */
// Capture backup register state for debug (UART not ready yet)
g_rtc_bkup_dr0 = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
g_rtc_bkup_dr1 = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
g_rtc_inits = (READ_BIT(RTC->ICSR, RTC_ICSR_INITS) != 0U) ? 1UL : 0UL;
g_rtc_wut_restored = 0;

// Post-init snapshot: HAL_RTC_MspInit() (already run via HAL_RTC_Init()
// above) verifies LSE is ready and selected before returning, calling
// Error_Handler() itself otherwise - reaching here already implies success.
g_rtc_bdcr_after_init = RCC->BDCR;

// DON'T restore WUT here - it starts counting immediately!
// WUT should only be set right before entering shutdown mode
// If sentinel matches and interval is valid, restore WUT
// if (g_rtc_bkup_dr0 == 0xDEADBEEF && g_rtc_bkup_dr1 > 0 && g_rtc_bkup_dr1 <= 65535)
// {
// if (HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, g_rtc_bkup_dr1, RTC_WAKEUPCLOCK_CK_SPRE_16BITS, 0) == HAL_OK)
// {
// g_rtc_wut_restored = 1;
// }
// }
/* USER CODE END RTC_Init 2 */

}

 

 

Thanks,

ST Technical Moderator
August 6, 2026

Hello alexemdesgagne,

 

you can refer at 

for more information on LSE ppm and the RTC smooth digital calibration feature embedded in the STM32WL55JC.

BR,
Filippo

In order to give better visibility on the answered topics, please click on 'Best answer' on the reply which solved your issue or answered your question. Thanks
Associate II
August 7, 2026

Hi Filippo, 

 

I already tried the calibration and this gave me good results.

According to the specs, I should have ~20ppm, not ~100ppm.

 

Is the 20ppm achievable just with calibration?

 

Thanks,

Alex

ST Technical Moderator
August 10, 2026

Hi alexemdesgagne,

how do you measure the LSE drift?

BR,
Filippo

In order to give better visibility on the answered topics, please click on 'Best answer' on the reply which solved your issue or answered your question. Thanks
waclawek.jan
Super User
August 10, 2026

Which specs?

JW