A guide to improving your GIGA R1 internal RTC accuracy

Updated to include a faster calibration method curtesy of @JoeHuber

With a little bit of work, you too could have the internal RTC accurate to 1 ppm - that's a second lost or gained every 12 days or so.

step 1: The RTC needs to be sourced from the Low Speed External (LSE) 32.768 KHz oscillator. This is the default source. However, on what appears to be most, if not all boards, the default driving level of LOW is insufficient. This results in the RTC falling back to the Low Speed Internal (LSI) oscillator - this is bad.

Solution: change the drive level of the LSE crystal (as directed by Arduino team in post #11 here Can I access the MEMS_32.768 to run RTC in LSE mode?)

by adding this to your sketch:

class FixRTC {
public:
    FixRTC() {
        LL_RCC_LSE_Disable();
        LL_RCC_LSE_SetDriveCapability(LL_RCC_LSEDRIVE_HIGH);
        LL_RCC_LSE_Enable();
    }
};

FixRTC fixrtc __attribute__ ((init_priority (101)));

No need to call it, the constructor does the bizz. A git issue has been raised that may get this value changed in the core and this override could be removed.

step 2: Having run with this drive level override for a day or two, you should now find that your RTC is (probably) gaining low double-digit seconds per day. It now needs calibrating.

According to the STM reference manual, the RTC can be made to output a 1 Hz signal. This can be measured over a 32 second period and the result used to calculate a calibration value. I couldn't get this to work reliably or accurately Unable to calibrate LSE RTC on GIGA R1 WiFi and not everyone has a logic analyser anyway.

So, another way (others are available) is to measure the drift in seconds having sync'd the RTC with an external source (like ntp). In post #55 below @JoeHuber provides a sketch that can be used to calculate the adjustment required. Alternatively, you can take the long road and measure your drift over a 12 day, 3hr and 16min period. Using this period, it is 1 calibration point per second of drift.

For shorter measurement periods (with less accuracy):

  • 12 calib points for each second over a 24hr period,
  • 24 points per sec over 12 hrs etc

(FYI, the drifts on my 3 boards after 12 days were +175, +264 and +193)

Step 3: Apply the calibration. The function below can be used to calibrate your RTC. Pass it your calibration points value (negative if you're gaining time, positive if loosing). It will need to be called whenever the RTC battery is removed whilst the board is powered down.

extern RTC_HandleTypeDef RTCHandle;

void LSEcalibration(int16_t LSEcalibrationvalue) {
  if (LSEcalibrationvalue > 0) {
    if (HAL_RTCEx_SetSmoothCalib(&RTCHandle, RTC_SMOOTHCALIB_PERIOD_32SEC, RTC_SMOOTHCALIB_PLUSPULSES_SET, (uint32_t)(512-LSEcalibrationvalue)) != HAL_OK) {
      // handle error;
    }
  }
  else {
    if (HAL_RTCEx_SetSmoothCalib(&RTCHandle, RTC_SMOOTHCALIB_PERIOD_32SEC, RTC_SMOOTHCALIB_PLUSPULSES_RESET, (uint32_t)abs(LSEcalibrationvalue)) != HAL_OK) {
      // handle error;    
    }
  }
}

You should now have an RTC that's acceptable to most.

If you are interested in monitoring the voltage of your RTC backup battery, you can use this:

mbed::AnalogIn mcuADCVref(ADC_VREF);
mbed::AnalogIn rtcADCVbat(ADC_VBAT);
uint32_t mcuVref, rtcVbat;

void setup() {
  mcuVref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(mcuADCVref.read_u16(), ADC_RESOLUTION_16B);
...
}
void loop() {
  rtcVbat = __HAL_ADC_CALC_DATA_TO_VOLTAGE(mcuVref, rtcADCVbat.read_u16(), ADC_RESOLUTION_14B);
...
}

rtcVbat will contain the RTC battery voltage in mV.

FYI, adding:

mbed::AnalogIn mcuADCTemp(ADC_TEMP);
uint32_t mcuTemp;
...
mcuTemp = __HAL_ADC_CALC_TEMPERATURE(mcuVref, mcuADCTemp.read_u16(), ADC_RESOLUTION_16B);

will give you the mcu junction temp in degrees C. Mine average low 40s

Note: If you want to use a rechargeable battery be mindful of the voltage, some coin cells (e.g. LIR2032) are over 4V fully charged (3.6V is max permissible).

easter egg: If you are planning on using a rechargeable, why not have the MCU recharge it for you while it's running (like the BIOS/CMOS on your PC)

The MCU charges through one of two internal resistors, 1K5 or 5K ohms. Use:

LL_PWR_SetBattChargResistor(LL_PWR_BATT_CHARGRESISTOR_1_5K)
or
LL_PWR_SetBattChargResistor(LL_PWR_BATT_CHARG_RESISTOR_5K);

depending on the specs of your battery. For me, charging @ 1K5 until just below nominal and then @ 5K for a while after works.
(5K is the default so the above call can be omitted for that value)

Then:

LL_PWR_EnableBatteryCharging();

and to disable:

LL_PWR_DisableBatteryCharging();

(getters are also available)

The board defaults back to not charging when main power is removed, so monitor and set charging as needed in your sketch.

Half way down I was wondering if you were going to figure how to charge rtc vbat...and there you have! Nice work!
So happy my initial feeble attempt at bringing this rtc in line has profited us all.
Thank you!

Anything but feeble. Good team work.

Steve Wow, you are a veritable fountain of exceedingly useful info. (I now have a Prefs class working due to your prior advice)

I've had an item on my ToDo list to figure out how to coax a VBat reading from the MCU and today that task poped to the top of my action items. So I did a last minute forum search before starting off on my own exploration and this thread came up with the exact answer including actual code. I have a stub waiting in my project for reading VBat which I'm going to fill in momentarily.

THANKS!!!

PS An Update to say it works perfectly.

Glad my info was so timely for you.

Steve, might I ask how/where you found that info for reading VBat? I've mostly been reading the STM data sheet and working things up from the very barest metal if there's not a standard Arduino function for it. But it seems there may be several platform layers in Arduino and mbed that I'm not yet taking advantage of.

For instance one of my upcoming tasks is to move a data acquisition function from the M7 to the M4 so it doesn't block the rest of the app while waiting for input. In simple terms I monitor 38 digital inputs and note a timer value when there's a transition on each input. I need to calculate the speed and acceleration of those input transitions. This function waits up to 20 seconds for the pulses to begin and the sequence takes less than 0.5 seconds to complete.

I'd like to learn about the minimum OS overhead that I can achieve on the M4. Any variable system overhead can skew the edge detection and thus the time captured from the hardware timer. Any thoughts on where to learn about mbed on M4 beyond just the simple RPC demo from Arduino?

Hi Joe, I found ADC_VBAT by chance. I was researching MCU temp (having seen some reports of boards running hot) and found an article using ADC_TEMP. A search of the arduinoCore-mbed repo turned up:

    ADC_TEMP = 0xF0, // Internal pin virtual value
    ADC_VREF = 0xF1, // Internal pin virtual value
    ADC_VBAT = 0xF2, // Internal pin virtual value

in the GIGA variants PinNames.h. I used the same funcs for VBAT that were used for TEMP.

My primary sources are the rm0399 reference manual
https://www.st.com/en/microcontrollers-microprocessors/stm32h745-755/documentation.html

mbed os API reference
https://os.mbed.com/docs/mbed-os/v6.16/apis/index.html

and the repos
ArduinoCore-mbed
ARMmbed/mbed-os: Arm Mbed OS is a platform operating system designed for the internet of things

I found the overhead introduced by many of the higher level functions unacceptable in performance critical areas. For example, I don't use micros() or millis(), or the low power timer API they wrapper. Instead I use us_ticker_read() which reads a register. A lot of effort has gone into making the mbed os thread safe, but this has added a lot of overhead.

I would start by using Arduino functions and building in some low cost instrumentation. Set budgets for key functions to focus efforts. If RPC is too expensive I'd recommend using your own shared memory area. Probably goes without saying, but avoid things like String. Generally, if it makes life easier it will come at a cost.

Using interrupts for this should make it impervious

Not really. Just treat it the same as the M7 and try not to share pins.

Hi Steve

Thanks for taking the time to share so much high quality info and wisdom.

Using interrupts for this should make it impervious

That was my original thought since I’d used interrupts in a previous version of this device that used a MEGA and had only 8 inputs that needed to be captured. But since this newer sensor has 38 outputs I moved up to a GIGA and my early investigations found evidence and complaints about using more than 16 interrupts causing mbed to crash. So since the GIGA has such a high speed processor I reverted to using software based edge detection. I was able to choose input pins that consolidated the 38 input signals into 7 input registers. So for the 0.5 sec sample interval I turn off interrupts, scan those 7 registers as fast as possible, mask out the irrelevant bits, look for transitions and note the value from a hardware timer clocked at 0.1us resolution. This works well enough on the M7 except it blocks my main loop for up to 20 sec waiting for the sample period to start, and there’s a minimal but still noticeable jitter in only some of the captured times. I’m guessing that might be from higher priority interrupts that mbed or Arduino may be using.

So moving this function to the currently unused M4 seems appealing as long as the lower clock rate (and lack of cache??) don’t slow things down too much. Ideally I could completely turn off all M4 interrupts for that 0.5 sec to minimize measurement jitter.

Or conversely can you suggest a way to use interrupts to detect edges on a maskable set of 38 pins on those 7 registers?

Steve thanks again for all of your really insightful comments and prior suggestions.

Cheers, Joe

Ahh, yes, forgot about the EXTI limitation.

If you are not already planning to access the pin registers directly I would consider it. Kurt did some good work on speeding up digitalWrite which also included a fast read

I would try your project out on the M4 with the stock mbed build first, but if you cannot eliminate the interference then a bare metal build of mbed is an option
https://os.mbed.com/docs/mbed-os/v6.16/bare-metal/index.html
I've not used it myself but it looks interesting and there is some doco.

Steve
Yes I saw Kurt's work. I originally selected the 38 input pins to coalesce them into as few input registers as possible (seven) so that I can do a word access of the input registers and gather all 38 input bits in just 7 reads. The input speed seems fine but I do still see some small jitter (1%) which I'd like to avoid if possible.

I really appreciate the link to the bare metal mbed info.

I hope someday I can reciprocate even a small portion of the very helpful assistance you've given me. I've done some work in double buffering graphics and text display to avoid flicker and learned of a way to avoid showing garbage in the frame buffer as the GIGA and GIGA Display boots. Let me know if either of these topics are of interest.

Already done :slight_smile: I hadn't looked in-depth at the NVIC but your posts triggered (intended) my curiosity. I've long since worked out everything I need to understand about the GIGA for my own project. Anything new I learn now, about this board and mbed, comes from trying to assist fellow makers. Cheers for that :+1:

Ah, if you're looking for things to investigate about the GIGA I can certainly ask more questions. :wink:

Steve Your RTC info made me curious so I took a closer look. I added some code to my app which sets the RTC from NTP during startup and then periodically logs the difference between the current NTP time and the current RTC time. The difference grows to as much as 70 seconds in less than a day but then it quickly diminishes to less than 10 seconds. This happens repeatedly, and it's consistent across three different GIGA boards.

I was a bit surprised to see some sort of adaptive correction mechanism with that coarse behavior.

I'm going to adjust my logging so I can see the overall periodicity better.

Hi @JoeHuber The RTC smooth calibration inserts or masks a number of LSE pulses spread over a 32 second period. If you are seeing big shifts like that then something is going wrong. Please report back when you've adjusted your logging.

OK that sounds more like the gentle nudge I was expecting to see. When or how often are these 32 second calibration periods triggered? Or is it constantly applying them every 32 seconds? Does it have its own algorithm for determining the calibration amount needed or is it just implementing a fixed value that we can set?

Based on the drift rate I'm currently seeing across my 3 test units it looks like I'd accumulate an error of 15 to 22 seconds per day. (3 seconds of drift over a 12,000 second test run so far)

My test units do have WiFi active so some underlying code could be calling NTP if it wanted to. I wonder if it's that clever???

It takes the calibration value you supply to, for example, my LSEcalibration function in post #1 and applies it over a 32s period. So if you specified -150 to the function it would mask 150 of the expected 1,048,576 pulses spread evenly over 32s. If you specified +150 it would insert 150 additional pulses spread evenly over the period.

Sounding like you've not calibrated yet :wink:

Right, I haven't done either the LSE drive nor calibration functions yet. I first wanted to establish a measurement baseline and then see how each of those might improve things. I thought I was on the right track since I had accumulated a 70 second drift on two of my boards overnight, while another one had only about 15 sec. But then I was shocked to see the two boards also go back down to only 15 sec. I couldn't (still can't) imagine what magical force caused that to happen since all three were sitting on my desk running the same logging code.

Are those 32 second adjustment periods run consecutively and constantly? If I haven't specified any calibration yet am I to assume that there is no automatic adjustment occurring and that only straight clock dividers are at play?

Indeed, that is very odd. 15s of drift over 24hrs without calibration is in the window of what I've seen.

Once set, the calibration is always-on. If you specify an adjustment of -128, for example, then (crudely speaking) 4 of the 32,768 pulses every second will be ignored. If you loose vbat then the calibration is lost.

correct

My overall thought was to automatically calculate a calibration factor using NTP to measure the clock drift over several/many days.

Even with the GIGA powered down (but with a battery to supply Vbat to keep the RTC running) it should work even the next time the GIGA is booted up as long as I prevent NTP from setting the RTC until after calculating the calibrating factor.

I could keep the calibration factor in flash and restore it after every boot. And with long term monitoring I could tweak the calibration factor even more finely if warranted.

But first I need to better understand the default behavior I'm seeing. I've got more detailed and persistent logging set up now so we'll see what it captures overnight.

This is my code:

void setup(void) {
    BootNTPTime = GetNTPEpoch();

This is in loop() and runs every minute

if (DebugStatusTimer > 60000) {
    DebugStatusTimer = 0;
    long CurrentNTPTime = GetNTPEpoch();
    if (CurrentNTPTime > 0) {
      long CurrentRTCTime = getCurrentTime() - (prefs.GMTOffset * 3600);
      long Uptime = CurrentNTPTime - BootNTPTime;
      long Drift = CurrentNTPTime - CurrentRTCTime;
      double DriftRate = Drift * 24.0 * 60.0 * 60.0 / Uptime;

      char buf[100];
      int ret = snprintf(buf, 100, "UT:%7d DT:%4d DTRt:%5.1f", Uptime, Drift, DriftRate);
      LogMessage(buf);
    }
  }

And this is a graph of 1185 samples taken 1 minute apart thus spanning almost 71,000 seconds.