In the previous post I started a mini-series dedicated to the built-in RTC in STM32 chips. I began with probably the most popular microcontroller SMT32F103 found among others in the cheap BluePill board. I got to the point where after resetting the microcontroller the time was still correct, but the date started from zero.

Why is the date being reset?

Alright, I’m only resetting the MCU, so the RTC counter keeps running. So why does the time match after reset, but the date does not?

It comes from the way ST uses the built-in RTC counter. Let me remind you that it’s used in such a way that when a day rolls over, the counter is reset. It simply does not hold a complete date like it does in Linux.

That’s why the time is preserved after reset. The date is stored in an ordinary variable in RAM, which is reset when the microcontroller starts. And that is the cause of the “problem”. How do we fight it?

Let’s finally use the Backup Registers!

I’ve mentioned backup registers a few times already. Let me remind you that these are a dozen/several dozen battery-backed registers. Additionally, clearing these registers requires a special procedure, which is not included in a normal MCU reset.

Writing to these registers also requires unlocking. In HAL, this lock is removed by default. This is done in the HAL_RTC_MspInit function

HAL_PWR_EnableBkUpAccess();

Since the lock is removed, we can write to these registers. It’s very simple. Just use the HAL function from the extended RTC function set.

void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister, uint32_t Data);

You pass the RTC handle, the backup register number, and the data. Unfortunately, the argument is misleading, because it points to a 32-bit variable that you can put into the register. BKP holds ONLY 16 bits.

By the way, these ubiquitous 32-bit arguments are a plague of HAL, which opponents use to start flame wars on electronics forums. I partly agree with them, but come on—it doesn’t completely eliminate HAL 🙂

Ok, so how do you write the current date into these registers? Since they fit 2 bytes each, and each date variable fits in one byte, two backup registers are enough to store the complete date. I made this simple function in main.

void BackupDateToBR(void)
{
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, ((RtcDate.Date << 8) | (RtcDate.Month)));
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR3, ((RtcDate.Year << 8) | (RtcDate.WeekDay)));
}

I write day and month to BKP_DR2, and year and weekday to BKP_DR3. Weekday is not mandatory – it is always recalculated in ST’s library.

When should you back up the date? There’s no point doing it every second. Only when the date changes.

if(RtcDate.Date != CompareDate)
{
    BackupDateToBR();
    CompareDate = RtcDate.Date;
}

Restoring the date at startup

Now it’s enough to read this data at startup and set it using the function we already know, HAL_RTC_SetDate. Let’s try it!

In the /* USER CODE BEGIN Check_RTC_BKUP */ section in the RTC initialization—where last time I inserted a return so as not to set the clock with the data entered in Cube—I’ll add some code. I’ll read the saved date from the BKP regs and set the calendar.

/* USER CODE BEGIN Check_RTC_BKUP */
RtcDate.Date = (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2) >> 8);
RtcDate.Month = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
RtcDate.Year = (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR3) >> 8);
RtcDate.WeekDay = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR3);

HAL_RTC_SetDate(&hrtc, &RtcDate, RTC_FORMAT_BIN);

//
//  You have to do not let Init code to reinitialize Time and Date in RTC.
//  It's "a bug" in HAL widely described and complain on forums.
//  That causes every MCU restart the time and date will be same as you configured in CubeMX.
//  All you have to do is just return before init time/date in this user section.
//
  return;
/* USER CODE END Check_RTC_BKUP */

This code will cause the date to be remembered after a microcontroller reset. Success! …until…

Date rollover vs. backup

What happens if, during battery backup power, the date rolls over? By one, two, or 20 days? Before we move on to battery backup power, let’s test this by simulating such a situation in the debugger. How? By changing the values in the RTC counter, simulating a date rollover “in the absence” of the core.

To change the register, start debugging and pause code execution. Now in the upper-right corner you have several sections. Go into SFRs. These are Special Function Registers, i.e., simplified, you can say these are peripheral registers. Expand the tree for RTC.

The three registers we care about are – CRL, CNTH and CNTL.

CRL – this is the control register for RTC. We’ll need it so that RTC allows us to write our value into the counter. This is done with bit 4, i.e., CNF.

CNTH and CNTL – the upper and lower RTC counters. Both 16-bit. This is where RTC counts seconds.

Now, to change the counter value you need to:

  1. Set the CNF bit of the CRL register to 0x1
  2. Modify the CNTH and CNTL values – in Eclipse they will “return” to their values after you enter them
  3. Clear the CNF bit of the CRL register (0x0) to latch the introduced changes in the counter.

By how much should we increase the RTC counter? We want to check changes by a day or a few. One day is 86400 seconds, so that’s how much you need to add to the counter for each day to simulate a day change.

I’ll add 2 days and a few seconds – I’ll set the counter to 180000. CNTH = 0x2, CNTL = 0xBF20. 

Before the change, during pause I have the date set to 17.05.2020 and the time 16:58:17 (I jumped into the future). Now I’ll change the counter and reset the MCU. This is exactly the MCU power coming back while the RTC is on battery backup power.

What happened?

Damn, something is wrong! The time changed, but the day was not updated. How is that possible? We remembered the date in the backup register.

This is where the weakness of ST’s solution shows up… The problem is in the HAL_RTC_SetDate function. Inside this function, the RTC counter is corrected, which in short consists of CUTTING OFF THE ELAPSED DAYS. This causes the number of elapsed days to disappear from the RTC counter and it is not transferred to the date in any way. SCANDAL!

This is a terrible problem, because even using backup registers we are still not able to easily control the date. There are two solutions:

  1. Write your own RTC handling, which will be better thought out.
  2. Do a bit of code gymnastics and recover the date without modifying ST’s code (HAL and the RTC lib).

Let me do the second option. Just for practice and to show that you can still use the library generated by Cube.

Full date recovery

Oh, HAL opponents will hate me, because I’m going to make a workaround now involving adding many clock cycles. But maybe they’ll praise me for ingenuity 🙂 Feel free to discuss.

I will need two RTC_DateTypeDef variables:

  1. RtcDate, which I use everywhere in the later code. This is the target variable that should contain the date.
  2. BackupDate – a temporary variable for calculations.

Everything will happen in RTC initialization. After MCU startup, I collect the backup registers into the target variable.

/* USER CODE BEGIN Check_RTC_BKUP */

RTC_DateTypeDef BackupDate;

RtcDate.Date = (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2) >> 8);
RtcDate.Month = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
RtcDate.Year = (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR3) >> 8);
RtcDate.WeekDay = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR3);

In the next step I collect the time, which is correct, into the target time variable. At this moment a new date is also calculated based on the RTC counter. By adding 2 days, it will be January 3rd of year 00, because ST’s software calendar counts from January 1st of year 00.

Now I collect that date into the temporary backup variable.

HAL_RTC_GetTime(&hrtc, &RtcTime, RTC_FORMAT_BIN); // There is also an internal date update based on HW RTC time elapsed!!
HAL_RTC_GetDate(&hrtc, &BackupDate, RTC_FORMAT_BIN); // Days elapsed since MCU power down

Now it’s enough to add these 2 days to our target calendar variable and everything is great? Yes! Only… what if it’s so many days that in the new date the month or year rolls over. The calculations become complicated…

Operations on the number of elapsed days

I found two useful algorithms operating on integers. They do have some issues with year zero, but that can be worked around.

The first function, CalculateDayNumber , is supposed to return the day number from the beginning of the calendar based on the date.

The second returns the date based on the day number.

To avoid strange errors around year zero, I added and subtracted 20 years in both functions. 20 because, like year zero, it is a leap year. So there won’t be errors because of that.

/* USER CODE BEGIN 1 */
uint32_t CalculateDayNumber(uint8_t Date, uint8_t Month, uint8_t Year)
{
	// Format:
	// DD.MM.YY
	uint32_t _Year = Year + 20;
	Month = (Month + 9) % 12;
	_Year = _Year - (Month / 10);
	return ((365 * _Year) + (_Year / 4) - (_Year / 100) + (_Year / 400) + (((Month * 306) + 5) / 10) + (Date - 1));
}

void CalculateDateFromDayNumber(uint32_t DayNumber, uint8_t *Date, uint8_t *Month, uint8_t *Year)
{
	uint32_t _Date, _Month, _Year;
	_Year = ((10000 * DayNumber) + 14780) / 3652425;
	int32_t ddd = DayNumber - ((365 * _Year) + (_Year / 4) - (_Year / 100) + (_Year / 400));
	if (ddd < 0)
	{
		_Year -= 1;
		ddd = DayNumber - ((365 * _Year) + (_Year / 4) - (_Year / 100) + (_Year / 400));
	}
	int32_t mi = ((100 * ddd) + 52) / 3060;
	_Month = (mi + 2) % 12 + 1;
	_Year = _Year + (mi + 2)/12;
	_Date = ddd - ((mi * 306) + 5)/10 + 1;
	*Date = _Date;
	*Month = _Month;
	*Year = _Year - 20;
}
/* USER CODE END 1 */

Now, having the backup date and the date that elapsed while power was absent, I can calculate their corresponding day numbers.

  uint32_t BackupDateDays = CalculateDayNumber(BackupDate.Date, BackupDate.Month, BackupDate.Year);
  uint32_t RtcDateDays = CalculateDayNumber(RtcDate.Date, RtcDate.Month, RtcDate.Year);

To the target date, it’s enough to add the days that elapsed. However, you need to subtract January 1st due to the fact that inside the algorithm we are actually counting the day 20 years ahead (due to errors for year 0000).

RtcDateDays += (BackupDateDays - CalculateDayNumber(1, 1, 0));

Now the only thing left is to transform the day number into a date using the second algorithm.

CalculateDateFromDayNumber(RtcDateDays, &RtcDate.Date, &RtcDate.Month, &RtcDate.Year);

Finally, write this date into the software calendar.

HAL_RTC_SetDate(&hrtc, &RtcDate, RTC_FORMAT_BIN);

That’s it 🙂

Now the date will always (in 99%) be correct. I’ll test again by entering 180000 into RTC.

Does it always work? I won’t bet my hand on it. Buuuut let’s try adding… 700 days, shall we?

19.05.20 + 700 days = 19.04.22 according to the calculator.

700 days * 86400 seconds = 60480000 seconds (0x39ADA00). Plus 2 hours gives 60847200 (0x39AF629) and I’ll enter that value into RTC.

So what? It works!

Summary

Using ST’s library is quite a challenge. The creators of our favorite STMs pulled a nasty trick on us by delivering such a hard-to-use library. However, contrary to what the smartest guys say – it can be used.

Also remember that this is the case for the poorest RTC, where there is only a seconds counter. In STM32F4 there is already a hardware calendar. I will definitely test it in the next posts.

The article grew again. There are still two issues left. Battery backup, which is not as trivial as one might expect, and driving RTC with an external crystal, which on BluePill will also cause a few problems. That’s for the next post!

In the meantime, I invite you to the comments section. Let me know if you liked this post and whether it makes sense to create articles like this. Every comment is valuable to me.

The full project along with the library can be found as usual on my GitHub: LINK

If you noticed any mistake, disagree with something, would like to add something important, or simply feel you’d like to discuss this topic, write a comment. Remember that the discussion should be polite and in accordance with the rules of the Polish language.

Podobne artykuły

.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *