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.
