Bad jitter on LSE over ~0.1s time scales
In our product, we would like to use the LSE crystal to accurately time a measurement of roughly 0.1 seconds. In this case, we are using an STM32G474 MCU. Using a Nucleo-G474 board for proof-of-concept work, we have found that there is very bad jitter on the LSE clock over time scales of 0.1 seconds. Our procedure to test the LSE stability, using the Nucleo board, is as follows:
- Configure TIM5.1 to input LSE pulses on TI1.
- Configure TIM5.2 to input PA1 pulses on TI2.
- Enable the LSE.
- Feed a 32.768 kHz signal from a very stable frequency generator into PA1.
- Enable the HSE.
- Configure the PLL for 170 MHz.
- Switch SYSCLK to run off the 170 MHz PLL.
- Repeatedly measure the number of 170 MHz ticks in 3272 pulses of TIM5.1 and TIM5.2 (both channels measured simultaneously).
By comparing the number of 170 MHz HSE-derived ticks that elapse in 3272 counts of the LSE vs. the number of ticks that elapse in 3272 of our reference generator, we can see that the tick counts on the reference generator channel are very stable, while the tick counts on the LSE channel vary significantly.
The fact that the reference generator measurement is very stable implies that the 170 MHz timebase must be very stable which implies that the HSE must be very stable. Everything is so stable that the tick counts on this channel fall into 2-3 bins. This is very good and the kind of accuracy we are looking for and is what we typically see when measuring other types of stable oscillators (our product is based on precisely measuring a number of different type of crystal oscillators).
The LSE measurement, on the other hand, is very bad and over short time frames falls into a range of 25 or more bins.
To try and understand the problem, we modified one of our own (non-Nucleo) boards from a different design so that we could feed a second signal from the frequency generator into the LSE OSC_IN pin and configure it for bypass mode (LSEBYP=1). In this mode, using the same technique to measure the LSE we achieve an accuracy of 2-3 bins which matches the accuracy measured on channel TIM5.2. So, it doesn't appear to be some sort of resynchronization problem between the backup power domain and the main power domain but actually the STM LSE crystal drive circuit being very jittery.
To illustrate the problem, here are 50 samples taken over an approximately 5-second period on the Nucleo board:
LSE TIM5.2
---------- ----------
1: 0x01030389 0x01030413
2: 0x01030382 0x01030412
3: 0x0103038F 0x01030412
4: 0x0103038D 0x01030412
5: 0x0103038E 0x01030413
6: 0x0103038C 0x01030413
7: 0x0103038B 0x01030412
8: 0x01030391 0x01030412
9: 0x0103038C 0x01030412
10: 0x01030392 0x01030411
11: 0x01030394 0x01030412
12: 0x01030387 0x01030411
13: 0x01030388 0x01030412
14: 0x01030397 0x01030412
15: 0x0103038B 0x01030413
16: 0x0103038E 0x01030412
17: 0x01030388 0x01030412
18: 0x0103038D 0x01030412
19: 0x0103038A 0x01030412
20: 0x0103038D 0x01030411
21: 0x01030390 0x01030412
22: 0x0103038B 0x01030412
23: 0x01030388 0x01030413
24: 0x0103038B 0x01030413
25: 0x01030387 0x01030412
26: 0x0103038A 0x01030412
27: 0x0103038A 0x01030411
28: 0x01030390 0x01030412
29: 0x01030389 0x01030412
30: 0x0103038C 0x01030411
31: 0x01030389 0x01030412
32: 0x01030384 0x01030413
33: 0x0103038A 0x01030412
34: 0x01030389 0x01030412
35: 0x01030386 0x01030412
36: 0x0103038F 0x01030411
37: 0x01030383 0x01030412
38: 0x0103038F 0x01030412
39: 0x0103038A 0x01030412
40: 0x01030396 0x01030411
41: 0x0103038F 0x01030412
42: 0x0103038F 0x01030413
43: 0x01030395 0x01030412
44: 0x0103038F 0x01030412
45: 0x0103038C 0x01030411
46: 0x0103038F 0x01030411
47: 0x0103038E 0x01030412
48: 0x01030393 0x01030412
49: 0x0103038B 0x01030412
50: 0x01030390 0x01030412We can see that the reference generator is very consistent with counts between 0x01030411 - 0x01030413, while the LSE is all over the place. Our understanding is that the LSE is supposed to be extremely accurate, so these results are very disappointing to us.
As a further test, we configured the board to echo the LSE clock on RCC_MCO (PA8) and just eyeballing the second rising edge there seems to be a lot of jitter visible on the oscilloscope, although we didn't try to characterize it other than by eye.
I am attaching the code we used to perform this measurement in case there may be any issues here. Note that this code uses our internal libraries, but it should be pretty obvious what it is doing. Because the measurement on TIM5_CH2 is very stable and because the measurement on the LSE channel in bypass mode is also very stable, we think that we are performing the measurement correctly and that the problem lies with the behaviour of the LSE crystal or drive circuit. We have also tried varying the LSEDRV values but this has not helped the jitter in any way and it actually changes the LSE frequency when we do this.
Has anyone else attempted to characterize the LSE performance over short time scales? What is the expectation here - is it only accurate over scales on the order of 1 second used by the RTC so that the jitter averages out? Is there anything we can do to improve this performance? We are currently considering using an external crystal oscillator in bypass mode if there's no way to make the STM drive circuit perform adequately.
#define LSE_MODE_XTAL 1
#define LSE_MODE_BYPASS 2
#define LSE_MODE LSE_MODE_XTAL
#if LSE_MODE == LSE_MODE_XTAL
#define LSE_DRV_VAL 1
#endif
// The number of prescaled edges to count on each TIM5 channel. We set the
// prescaler to 8, and want to perform measurements of roughly 0.1 seconds on
// the LSE and TIM5.2 timer inputs which are both running at 32.768 kHz, so an
// edge count of 409 is appropriate.
#define TIM_EDGE_MEASURE_COUNT 409
// We discard the first N edges counted on each TIM5 channel to ensure we are
// aligned to the rising edge of a pulse.
#define TIM_EDGE_DISCARD_COUNT 2
// The total number of edges we should count.
#define TIM_EDGE_TOTAL_COUNT \
(TIM_EDGE_MEASURE_COUNT + TIM_EDGE_DISCARD_COUNT)
int
main()
{
// Initialize trace logging.
trace_init();
// Reset the backup domain.
RCC.enable_rcc_device(PWR);
PWR.unlock_backup_domain();
RCC.reset_backup_domain();
// Start the LSE.
#if LSE_MODE == LSE_MODE_XTAL
RCC->bdcr = (LSE_DRV_VAL << 3);
RCC->bdcr = (LSE_DRV_VAL << 3) | 1;
#elif LSE_MODE == LSE_MODE_BYPASS
RCC->bdcr = 0x00000004;
RCC->bdcr = 0x00000005;
#endif
// Configure PLL for 170 MHz.
PWR.set_vcore_voltage(stm32g4::VCORE_1V28);
RCC.enable_hse();
RCC.select_hsclk(stm32g4::HSCLK_SOURCE_HSE);
RCC.configure_pllr(6,85,2);
FLASH.set_latency(4);
RCC.set_hpre(2);
RCC.select_sysclk(stm32g4::SYSCLK_SOURCE_PLLRCLK);
__spin_delay_m4(85);
RCC.set_hpre(1);
// Configure pin PA1 to act as TIM5_CH2.
RCC.enable_rcc_device(GPIOA);
GPIOA.configure(1,2,stm32::GPIO_AF);
// Configure TIM5 to measure LSE pulses on channel 1 and TIM5_CH2 pulses
// on channel 2, both with a prescaler value of 8. Channels 3 and 4 are
// put into input mode and disabed.
RCC.enable_rcc_device(TIM5);
TIM5->ccer = 0;
TIM5->cr1 = 0x00000000;
TIM5->tisel = 0x00000002;
TIM5->ccmr[0] = 0x00003D3D;
TIM5->ccmr[1] = 0x00000101;
TIM5->arr = 0xFFFFFFFF;
TIM5->psc = 0;
// Repeatedly count edges on TIM5.1 and TIM5.2, discarding the first 2
// edges from each channel to ensure we are aligned to the start of a pulse.
for (;;)
{
// Reset TIM5 for the new measurement.
TIM5->cnt = 0;
TIM5->sr = 0;
TIM5->cr1 = 0x00000001;
TIM5->ccer = 0x00000011;
// Wait for the desired number of edges.
uint32_t lse_edges = 0;
uint32_t ch2_edges = 0;
uint32_t lse_start_cnt = 0;
uint32_t ch2_start_cnt = 0;
uint32_t lse_end_cnt = 0;
uint32_t ch2_end_cnt = 0;
while (lse_edges < TIM_EDGE_TOTAL_COUNT ||
ch2_edges < TIM_EDGE_TOTAL_COUNT)
{
uint32_t sr = TIM5->sr;
if (sr & (1 << 1))
{
uint32_t ccr1 = TIM5->ccr[0];
if (lse_edges < TIM_EDGE_TOTAL_COUNT)
{
++lse_edges;
if (lse_edges == TIM_EDGE_DISCARD_COUNT)
lse_start_cnt = ccr1;
else if (lse_edges == TIM_EDGE_TOTAL_COUNT)
lse_end_cnt = ccr1;
}
}
if (sr & (1 << 2))
{
uint32_t ccr2 = TIM5->ccr[1];
if (ch2_edges < TIM_EDGE_TOTAL_COUNT)
{
++ch2_edges;
if (ch2_edges == TIM_EDGE_DISCARD_COUNT)
ch2_start_cnt = ccr2;
else if (ch2_edges == TIM_EDGE_TOTAL_COUNT)
ch2_end_cnt = ccr2;
}
}
}
// Stop TIM5.
TIM5->ccer = 0;
TIM5->cr1 = 0x00000000;
// Transmit/record the number of elapsed TIM ticks on each channel.
uint32_t lse_ticks = (lse_end_cnt - lse_start_cnt);
uint32_t ch2_ticks = (ch2_end_cnt - ch2_start_cnt);
TRACE("(nedges) (lse_ticks) (ch2_ticks)",
TIM_EDGE_MEASURE_COUNT,lse_ticks,ch2_ticks);
}
}Thanks for any insights others may have, and for taking any time to look at this!
