Bare-metal RTC Driver for STM32F411CEx

Kunal Salvi
6 min readOct 18, 2021

--

Highly efficient and small footprint driver for the “watch” of the embedded world

Time, an important and inevitable fact of our lives. As we keep ourselves updated by using a wristwatch or clocks on our phones, it is equally important for a microcontroller to keep track of “real” time, in real-time. RTC or Real Time Clock is equivalent to an actual “watch” that the microcontroller reads. Why I’m using all these upper quotes will be clear very soon.

I’ve been working on an application and I needed to keep the time of the data I’m acquiring. Now, I could’ve simply gone with an I2C RTC module and just call it a day, and believe me I wanted to do that. But buying a module rather than using the one I get on-chip seemed redundant and expensive. So I buckled up and started learning about RTC already on my dear STM32F411CEx (I’ll never get tired of saying that name).

So RTC configurations for STM32F411CE depends on quite a few different registers blocks, namely the RCC, PWR and RTC itself. RTC, like any other digital peripheral, requires a steady clock to function properly. That clock can be provided either by a low speed external (LSE) clock, the low speed internal (LSI) clock or the high speed external (HSE) clock nutered down to 1 MHz. The most stable results are achieved by supplying the RTC a clock signal from LSE of 32.786 KHz. Other options are LSI that runs at 32 KHz and HSE that is divided appropriately to get 1 MHz signal. When I was writing my driver, I spent hours on getting my LSE to work, but I couldn’t. Turned out my clock was dead (RIP little guy 🙏).

So we’ll have to make do with HSE as our clock for RTC and trust me it works the same. As we have decided what clock we’ll use, let’s get this show on the road. In fig 1, you can see how the RTC clock tree is organised in STM32F411CEx. We need to set the DBP bit in the PWR_CR register. This bit disables backup domain write protection which allows us to configure the RTC and RCC_BDCR registers. Once this bit is set, we are free to write the RTC and RCC registers.

Fig 1: RTC Clock Tree

As shown in fig 1, HSE is divided by 25 to get 1 MHz clock. That is achieved by setting the appropriate RTCPRE bits in the RCC_CFGR register. We’ll put a value of 25 in the 16th place in this register. For more information, refer the reference manual. Now we divert our attention towards the RCC_BDCR register. BDCR stands for Backup Domain Control Register. After reset, these bits are write protected, hence cannot be modified. The DBP bit in the PWR_CR register needs to be set in order to modify this register. We’ll select HSE as our clock by setting the RTCSEL bits in RCC_BDCR register. After that we’ll enable the RTCEN bit in the same register.

PWR -> CR |= PWR_CR_DBP;
RCC -> CFGR |= 25 << 16;
RCC -> BDCR |= RCC_BDCR_RTCSEL;
RCC -> BDCR |= RCC_BDCR_RTCEN;

In order to write the RTC registers, we need to insert a specific sequence of data in the RCC_WPR register. The sequence is 0xCA followed by 0x53. Bytes in this sequence, unlocks the write protection on all the RTC registers. After we’ve unlocked the RTC registers, we’ll put the RTC in initialization mode by setting the INIT bit in the RTC_ISR bit and wait till INITF bit is set

RTC -> WPR = 0xCA;
RTC -> WPR = 0x53;
RTC -> ISR |= RTC_ISR_INIT;
while(!(RTC -> ISR & RTC_ISR_INITF));

Now we’ll need to get the the prescaler register configured to suit our clock selection. We’ll do that by putting the value 0x7C1F3F in the RTC_PRER register. This divides the clock and gives a 1 Hz clock to the RTC module.

RTC -> PRER = 0x00000000;
RTC -> PRER = 0x7C << 16;
RTC -> PRER |= 0x1F3F << 0;

Just like any clock, we need to put the starting time in our RTC too for it to start counting from. We do that by setting the appropriate bits in the RTC_TR register. Fig 1 shows the RTC_TR register.

Fig 2: RTC_TR Register of STM32F411xxx

To put the appropriate values in the register above, you need to do some calculations. Don’t fret, I’ve done all the heavy lifting for you. Refer the code below.

int ht,hu,mt,mu,st,su;
uint32_t t;
ht = T->hour / 10;
hu = T->hour % 10;
mt = T->min / 10;
mu = T->min % 10;
st = T->seconds / 10;
su = T->seconds % 10;
t = ht << 20 | hu << 16 | mt << 12 | mu << 8 | st << 4 | su << 0;
RTC -> TR = t ;// time

Similarly we’ll put the data for date in the RTC_DR register and the register is shown below in Fig 3.

Again, I’ve done allthe heavy lifting for you.

int yt,yu,wd,mt,mu,dt,du;
uint32_t d;
D->year = D -> year - 2000;
yt = D->year / 10;
yu = D->year % 10;
wd = D->week_day;mt = D->month / 10;
mu = D->month % 10;
dt = D->day / 10;
du = D->day % 10;
d = yt << 20 | yu << 16 | wd << 13 | mt << 12 | mu << 8 | dt << 4 | du << 0;
RTC -> DR = d ;

Now configure the time format, by setting the FMT bit in the RTC_CR register. The STM32F411CEx supports both 12 and 24 hour format. We’ll use the 12 hour format. With this, we are done with initialization. Before we wind up the setup, we’ll exit the initialization mode.

RTC -> CR |= RTC_CR_FMT | RTC_CR_TSE; 
RTC -> ISR &= ~RTC_ISR_INIT;
RTC -> WPR = 0xFF;
PWR -> CR &= ~PWR_CR_DBP;

To simplify the setting of time, we’ll use a struct for setting anf retrieving the time and date.

typedef struct RTC_Date
{
int day;
int month;
int year;
int week_day;
}RTC_Date;
typedef struct RTC_Time
{
int am_pm;
int hour;
int min;
int seconds;
int milliseconds;
}RTC_Time;

Now to get time, we’ll use the RTC_Time structure as shown below:

uint32_t t;
int hour_tens, hour_unit;
int mins_tens, mins_unit;
int second_tens, second_unit;
int seconds, mins, hour;
t = RTC -> TR;
hour_tens = (t & 0x300000)>>20;
hour_unit = (t & 0xF0000)>>16;
mins_tens = (t & 0x7000)>>12;
mins_unit = (t & 0xf00) >> 8;
second_tens = (t & 0x70) >> 4;
second_unit = (t & 0xf);
hour = (hour_tens * 10) + hour_unit;
mins = (mins_tens * 10) + mins_unit;
seconds = (second_tens * 10) + second_unit;
T->hour = hour;
T->min = mins;
T->seconds = seconds;

Similarly, we’ll use RTC_Date structure to get date as shown below:

uint32_t d;
int date, datet, dateu;
int month,montht,monthu;
int year,yeart,yearu;
int wd;
d = RTC -> DR;
yeart = (d & 0xf00000) >> 20;
yearu = (d & 0xf0000) >> 16;
year = 2000 + (yeart * 10) + yearu;
montht = (d & 0x1000) >> 12;
monthu = (d & 0xf00) >> 12;
month = (montht * 10) + monthu;
datet = (d & 0x30) >> 4;
dateu = (d & 0xf) >> 0;
date = (datet * 10) + dateu;
D -> day = date;
D -> month = month;
D -> year = year;

With this we come to the end of retrieving date and time from the RTC.

I have uploaded an easy and ready-to-use driver on my Github. Make sure you fork it and follow.

Follow me on Medium at Kunal Salvi.

Instagram at Ziran_Daruwala and Blackshield Engineering.

For complete code and the latest version, check out my Github Page.

--

--

Kunal Salvi
Kunal Salvi

Written by Kunal Salvi

Embedded Systems Engineer | Roboticist | Researcher | Innovator

No responses yet