14. STM32. Програмування STM32F103. RTC


20.09.2016

Годинник реального часу (RTC)

STM32 мають вбудований годинник реального часу. Він може працювати незалежно від основного живлення мікроконтролера. Для роботи вбудованого годинника до спеціального виводу треба підключити живлення напругою 3В. Наприклад, батарейку CR2032. Такі батарейки використовують у годинниках, у комп`ютерах на материнських платах, та в інших приладах. Годинник споживає дуже мало енергії, тому батарейки вистачає на тривалий час. Також годинник може працювати як будильник - формувати сигнал на одному з виходів або виводити мікроконтролер з режиму енергозбереження. Спочатку ми запустимо годинник і налаштуємо таким чином, щоб він рахував секунди і продовжував працювати після відключення основного живлення мікроконтролера. Будильник розглянемо пізніше.

stm32_rtc_bat

Перш ніж перейти до використання годинника, слід знати, що у STM32 є так званий Battery backup domain. Це частина мікроконтролера, яка може живитися від батарейки. До нього належать наш годинник та регістри налаштування, LSE генератор, тобто схема обслуговування зовнішнього низькочастотного (32768 Гц) кварцу, виводи PC13..PC15. Крім того, ця зона у STM32F103C8 містить 42 16-бітних регістри, які зберігають свої значення, доки на вхід, до якого підключається батарейка, подається напруга. Їх можна використовувати для своїх задач. Використання цих регістрів теж розглянемо пізніше.

stm32_rtc

Отже, нам треба запустити RTC. Робити це потрібно тільки за умови, якщо RTC ще не ввімкнутий. Після того, як ми налаштуємо RTC, його налаштування будуть зберігатися у регістрах, поки не відключимо батарейку. Копирсатися у налаштуваннях, коли годинник вже "на ходу" не слід. Тому функція ініціалізації годинника має такий вигляд:


unsigned char RTC_Init(void)
{
	// Дозволити тактування модулів управління живленням і управлінням резервної областю
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
	// Дозволити доступ до області резервних даних
	PWR_BackupAccessCmd(ENABLE);
	// Якщо годинник вимкнений - ініціалізувати
	if ((RCC->BDCR & RCC_BDCR_RTCEN) != RCC_BDCR_RTCEN)
	{
		// Виконати скидання області резервних даних
		RCC_BackupResetCmd(ENABLE);
		RCC_BackupResetCmd(DISABLE);

		// Вибрати джерелом тактових імпульсів зовнішній кварц 32768 і подати тактування
		RCC_LSEConfig(RCC_LSE_ON);
		while ((RCC->BDCR & RCC_BDCR_LSERDY) != RCC_BDCR_LSERDY) {}
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);

		RTC_SetPrescaler(0x7FFF); // Встановити поділювач, щоб годинник рахував секунди

		// Вмикаємо годинник
		RCC_RTCCLKCmd(ENABLE);

		// Чекаємо на синхронізацію
		RTC_WaitForSynchro();

		return 1;
	}
	return 0;
}

Запишемо у мікроконтролер програму, текст якої наведено у кінці статті. Підключимо батарейку. Вмикаємо живлення мікроконтролера. Після першого включення мікроконтролер запустить функцію ініціалізації та ввімкне RTC. Під час наступних запусків, функція буде вмикати лише тактування потрібних модулів і вмикати доступ до області резервних даних (PWR_BackupAccessCmd(ENABLE);). Це нам потрібно, щоб отримати доступ до лічильника RTC. Перша ініціалізація RTC - досить тривалий процес. Тому, хай Вас не лякає затримка при старті мікроконтролера, яка може тривати близько секунди. У подальшому мікроконтролер стартує без затримок. Якщо відключити живлення мікроконтролера і живлення від батарейки, налаштування RTC будуть скинуті.

Лічильник RTC зчитується функцією RTC_GetCounter(). Ми отримаємо час у секундах. Лишається тільки перевести секунди у звичайний формат часу. Встановити лічильник на конкретну дату можна за допомогою функції RTC_SetCounter(). Функції слід передати час у секундах.

Я дуже засмутився через те, що у цьому контролері такий куций RTC. У інших серіях контролерів ініціалізація RTC відбувається через структуру, де можна задати дату, день тижня та час у звичному форматі. Та зчитувати дату та час у структури. Тобто, у тих контролерах реальний годинник, який сам враховує високосні роки, кількість днів у місяці та таке інше. У нашому випадку він хіба що тягне на назву "таймер, який може працювати від батарейки". Але це не біда. Треба добавити трохи коду. Ось Вам структура і дві функції. Перша функція переводить секунди у звичайну дату і час. Друга функція вираховує секунди по заданій у структурі даті та часу. З методикою розрахунків можна ознайомитися тут.

Приклад програми, яка ініціалізує RTC та встановлює початковий час тільки під час першої ініціалізації. Дані про дату і час виводяться у порт USART.


#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rtc.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_pwr.h"
#include "stm32f10x_usart.h"
#include "stdio.h"
#include "misc.h"

#include "tim2_delay.h"

void SetSysClockToHSE(void)
{
	ErrorStatus HSEStartUpStatus;

	/* SYSCLK, HCLK, PCLK2 and PCLK1 configuration -----------------------------*/
    /* RCC system reset(for debug purpose) */
    RCC_DeInit();

    /* Enable HSE */
    RCC_HSEConfig( RCC_HSE_ON);

    /* Wait till HSE is ready */
    HSEStartUpStatus = RCC_WaitForHSEStartUp();

    if (HSEStartUpStatus == SUCCESS)
    {
        /* Enable Prefetch Buffer */
        //FLASH_PrefetchBufferCmd( FLASH_PrefetchBuffer_Enable);

        /* Flash 0 wait state */
        //FLASH_SetLatency( FLASH_Latency_0);

        /* HCLK = SYSCLK */
        RCC_HCLKConfig( RCC_SYSCLK_Div1);

        /* PCLK2 = HCLK */
        RCC_PCLK2Config( RCC_HCLK_Div1);

        /* PCLK1 = HCLK */
        RCC_PCLK1Config(RCC_HCLK_Div1);

        /* Select HSE as system clock source */
        RCC_SYSCLKConfig( RCC_SYSCLKSource_HSE);

        /* Wait till PLL is used as system clock source */
        while (RCC_GetSYSCLKSource() != 0x04)
        {
        }
    }
    else
    { /* If HSE fails to start-up, the application will have wrong clock configuration.
     User can add here some code to deal with this error */

        /* Go to infinite loop */
        while (1)
        {
        }
    }
}



void usart_init(void)
{
	    /* Enable USART1 and GPIOA clock */
	    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

	    /* Configure the GPIOs */
	    GPIO_InitTypeDef GPIO_InitStructure;

	    /* Configure USART1 Tx (PA.09) as alternate function push-pull */
	    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	    GPIO_Init(GPIOA, &GPIO_InitStructure);

	    /* Configure USART1 Rx (PA.10) as input floating */
	    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	    GPIO_Init(GPIOA, &GPIO_InitStructure);

	    /* Configure the USART1 */
	    USART_InitTypeDef USART_InitStructure;

	  /* USART1 configuration ------------------------------------------------------*/
	    /* USART1 configured as follow:
	          - BaudRate = 115200 baud
	          - Word Length = 8 Bits
	          - One Stop Bit
	          - No parity
	          - Hardware flow control disabled (RTS and CTS signals)
	          - Receive and transmit enabled
	          - USART Clock disabled
	          - USART CPOL: Clock is active low
	          - USART CPHA: Data is captured on the middle
	          - USART LastBit: The clock pulse of the last data bit is not output to
	                           the SCLK pin
	    */
	    USART_InitStructure.USART_BaudRate = 115200;
	    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	    USART_InitStructure.USART_StopBits = USART_StopBits_1;
	    USART_InitStructure.USART_Parity = USART_Parity_No;
	    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;

	    USART_Init(USART1, &USART_InitStructure);

	    /* Enable USART1 */
	    USART_Cmd(USART1, ENABLE);
}

void USARTSend(char *pucBuffer)
{
    while (*pucBuffer)
    {
        USART_SendData(USART1, *pucBuffer++);
        while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET)
        {
        }
    }
}

//========================================================================================
unsigned char RTC_Init(void)
{
	// Дозволити тактування модулів управління живленням і управлінням резервної областю
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
	// Дозволити доступ до області резервних даних
	PWR_BackupAccessCmd(ENABLE);
	// Якщо годинник вимкнений - ініціалізувати
	if ((RCC->BDCR & RCC_BDCR_RTCEN) != RCC_BDCR_RTCEN)
	{
		// Виконати скидання області резервних даних
		RCC_BackupResetCmd(ENABLE);
		RCC_BackupResetCmd(DISABLE);

		// Вибрати джерелом тактових імпульсів зовнішній кварц 32768 і подати тактування
		RCC_LSEConfig(RCC_LSE_ON);
		while ((RCC->BDCR & RCC_BDCR_LSERDY) != RCC_BDCR_LSERDY) {}
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);

		RTC_SetPrescaler(0x7FFF); // Встановити поділювач, щоб годинник рахував секунди

		// Вмикаємо годинник
		RCC_RTCCLKCmd(ENABLE);

		// Чекаємо на синхронізацію
		RTC_WaitForSynchro();

		return 1;
	}
	return 0;
}

/*
* Як конвертувати дату можна прочитати тут:
* https://ru.m.wikipedia.org/wiki/%D0%AE%D0%BB%D0%B8%D0%B0%D0%BD%D1%81%D0%BA%D0%B0%D1%8F_%D0%B4%D0%B0%D1%82%D0%B0
*/

// (UnixTime = 00:00:00 01.01.1970 = JD0 = 2440588)
#define JULIAN_DATE_BASE	2440588

typedef struct
{
	uint8_t RTC_Hours;
	uint8_t RTC_Minutes;
	uint8_t RTC_Seconds;
	uint8_t RTC_Date;
	uint8_t RTC_Wday;
	uint8_t RTC_Month;
	uint16_t RTC_Year;
} RTC_DateTimeTypeDef;

// Get current date
void RTC_GetDateTime(uint32_t RTC_Counter, RTC_DateTimeTypeDef* RTC_DateTimeStruct) {
	unsigned long time;
	unsigned long t1, a, b, c, d, e, m;
	int year = 0;
	int mon = 0;
	int wday = 0;
	int mday = 0;
	int hour = 0;
	int min = 0;
	int sec = 0;
	uint64_t jd = 0;;
	uint64_t jdn = 0;

	jd = ((RTC_Counter+43200)/(86400>>1)) + (2440587<<1) + 1;
	jdn = jd>>1;

	time = RTC_Counter;
	t1 = time/60;
	sec = time - t1*60;

	time = t1;
	t1 = time/60;
	min = time - t1*60;

	time = t1;
	t1 = time/24;
	hour = time - t1*24;

	wday = jdn%7;

	a = jdn + 32044;
	b = (4*a+3)/146097;
	c = a - (146097*b)/4;
	d = (4*c+3)/1461;
	e = c - (1461*d)/4;
	m = (5*e+2)/153;
	mday = e - (153*m+2)/5 + 1;
	mon = m + 3 - 12*(m/10);
	year = 100*b + d - 4800 + (m/10);

	RTC_DateTimeStruct->RTC_Year = year;
	RTC_DateTimeStruct->RTC_Month = mon;
	RTC_DateTimeStruct->RTC_Date = mday;
	RTC_DateTimeStruct->RTC_Hours = hour;
	RTC_DateTimeStruct->RTC_Minutes = min;
	RTC_DateTimeStruct->RTC_Seconds = sec;
	RTC_DateTimeStruct->RTC_Wday = wday;
}

// Convert Date to Counter
uint32_t RTC_GetRTC_Counter(RTC_DateTimeTypeDef* RTC_DateTimeStruct) {
	uint8_t a;
	uint16_t y;
	uint8_t m;
	uint32_t JDN;

	a=(14-RTC_DateTimeStruct->RTC_Month)/12;
	y=RTC_DateTimeStruct->RTC_Year+4800-a;
	m=RTC_DateTimeStruct->RTC_Month+(12*a)-3;

	JDN=RTC_DateTimeStruct->RTC_Date;
	JDN+=(153*m+2)/5;
	JDN+=365*y;
	JDN+=y/4;
	JDN+=-y/100;
	JDN+=y/400;
	JDN = JDN -32045;
	JDN = JDN - JULIAN_DATE_BASE;
	JDN*=86400;
	JDN+=(RTC_DateTimeStruct->RTC_Hours*3600);
	JDN+=(RTC_DateTimeStruct->RTC_Minutes*60);
	JDN+=(RTC_DateTimeStruct->RTC_Seconds);

	return JDN;
}

void RTC_GetMyFormat(RTC_DateTimeTypeDef* RTC_DateTimeStruct, char * buffer) {
	const char WDAY0[] = "Monday";
	const char WDAY1[] = "Tuesday";
	const char WDAY2[] = "Wednesday";
	const char WDAY3[] = "Thursday";
	const char WDAY4[] = "Friday";
	const char WDAY5[] = "Saturday";
	const char WDAY6[] = "Sunday";
	const char * WDAY[7]={WDAY0, WDAY1, WDAY2, WDAY3, WDAY4, WDAY5, WDAY6};

	const char MONTH1[] = "January";
	const char MONTH2[] = "February";
	const char MONTH3[] = "March";
	const char MONTH4[] = "April";
	const char MONTH5[] = "May";
	const char MONTH6[] = "June";
	const char MONTH7[] = "July";
	const char MONTH8[] = "August";
	const char MONTH9[] = "September";
	const char MONTH10[] = "October";
	const char MONTH11[] = "November";
	const char MONTH12[] = "December";
	const char * MONTH[12]={MONTH1, MONTH2, MONTH3, MONTH4, MONTH5, MONTH6, MONTH7, MONTH8, MONTH9, MONTH10, MONTH11, MONTH12};

	sprintf(buffer, "%s %d %s %04d",
			WDAY[RTC_DateTimeStruct->RTC_Wday],
			RTC_DateTimeStruct->RTC_Date,
			MONTH[RTC_DateTimeStruct->RTC_Month -1],
			RTC_DateTimeStruct->RTC_Year);
}
//========================================================================================

int main(void)
{
	char buffer[80] = {`\0`};
	uint32_t RTC_Counter = 0;
	RTC_DateTimeTypeDef	RTC_DateTime;

	SetSysClockToHSE();
	TIM2_init();

	usart_init();

	if (RTC_Init() == 1) {
		// Якщо перша ініціалізація RTC Встановлюємо початкову дату, наприклад 22.09.2016 14:30:00
		RTC_DateTime.RTC_Date = 22;
		RTC_DateTime.RTC_Month = 9;
		RTC_DateTime.RTC_Year = 2016;

		RTC_DateTime.RTC_Hours = 14;
		RTC_DateTime.RTC_Minutes = 30;
		RTC_DateTime.RTC_Seconds = 00;

		// Після ініціалізації потрібна затримка. Без неї час не встановлюється.
		delay_ms(500);
		RTC_SetCounter(RTC_GetRTC_Counter(&RTC_DateTime));
	}

	while(1)
	{
		RTC_Counter = RTC_GetCounter();
		sprintf(buffer, "\r\n\r\nCOUNTER: %d\r\n", (int)RTC_Counter);
		USARTSend(buffer);
 
		RTC_GetDateTime(RTC_Counter, &RTC_DateTime);
		sprintf(buffer, "%02d.%02d.%04d  %02d:%02d:%02d\r\n",
			RTC_DateTime.RTC_Date, RTC_DateTime.RTC_Month, RTC_DateTime.RTC_Year,
			RTC_DateTime.RTC_Hours, RTC_DateTime.RTC_Minutes, RTC_DateTime.RTC_Seconds);
		USARTSend(buffer);

		// Функція генерує у буфері дату власного формату
		RTC_GetMyFormat(&RTC_DateTime, buffer);
		USARTSend(buffer);

		/* delay */
		while (RTC_Counter == RTC_GetCounter()) {

		}

    }
}

Бажаю успіхів!

Дивись також:

STM32
Коментарі:
Микола говорить:
21.09.2016 01:39
Привіт славному місту Кременчук! Андрію, дуже вдячний вам за цикл по програмуванню STM32.
Але - чому не HAL?
На HAL є готові функції для роботи з RTC.
Питання по темі.
Не можу допетрити як отримати дату з назвою дня неділі та назвою місяця (наприклад: Середа 21 Вересня 2016) .
У файлі stm32f1xx_hal_rtc.h є дефайни днів та місяців, співставлення до номеру дня та місяця. А як можна це використати навпаки?
Вельми вдячний за увагу.

P.S. І все ж таки - чи плануєте перехід на HAL?

andre говорить:
21.09.2016 08:13
Доброго дня.
HAL і FreeRTOS планується розглянути пізніше. Ще буде з десяток статей з використанням SPL.
Що до дати у текстовому форматі, я дороблю приклад де буде видно як це можна зробити.

Микола говорить:
21.09.2016 15:51
Дякую. Чекаю з нетерпінням. 
Можлива у вас є якісь проекти де використовуються кнопки для корекції часу і дати.
Або де почитати.
Контролер STM32F103C8.

andre говорить:
22.09.2016 12:34
Трохи доробив приклад. Зверніть увагу на функцію RTC_GetMyFormat. Вона формує час у форматі типу Thursday 22 October 2016.

Микола говорить:
01.10.2016 02:34
Дякую за увагу. Буду розбиратись. Чекаю HAL.

Андрій говорить:
01.02.2017 10:18
пан Микола, ось тут http://stm32withoutfear.blogspot.com/2016/12/RTC-stm32f1xx.html перегляньте варіант RTC за допомоги HAL драйверу

Михайло говорить:
05.07.2017 10:44
#include "tim2_delay.h"
Поясніть, будь ласка, де взяти цей файлик і куди покласти...

andre говорить:
05.07.2017 14:09
Прямуєте на сторінку «STM32. Скачати приклади» http://www.avislab.com/blog/stm32-examples/

Знайдіть потрібний приклад, у даному випадку https://github.com/avislab/STM32F103/tree/master/Example_RTC
І там є усі файли проекту.

Також можна скачати всі приклади одним ZIP-архівом:
https://github.com/avislab/STM32F103/archive/master.zip

Михайло говорить:
05.07.2017 15:53
Там проект для CooCox.
А як для Keil ? Тут же для Keil чи ні ?

andre говорить:
06.07.2017 23:51
Немає різниці Keil, CooCox, IAR. Кругом "C" і однакові бібліотеки. Просто додайте *.h та *.c файли у свій проект.

Володимир говорить:
20.09.2020 07:22
Доброго дня!
Підкажіть, будь ласка, в чому може бути проблема. Підключаю батарейку до відповідного входу, RTC працює при виключеному живлені, проте напруга батерйки з'являється на живленні.

Додати коментар
Code
* - обов'язкові поля

Архіви