Почему сбивается дата и календарь STM32F103?

В микроконтроллерах серии STM32F10x (например, STM32F103C8T6 из популярной BluePill) есть часы реального времени, они "тикают" от батарейки после того, как пропадает основное питание. Но в этих RTC нет аппаратного календаря. Это значит, что дата "не идёт". Даже если вы в своем HAL проекте установили галочку "activate calendar", это значит, что дата будет "идти" при наличии основного питания, но после прекращения питания ДАТА СОБЬЕТСЯ до 01.01.00. Это плохая новость... Хорошая новость в том, что на HAL'е мир не сошелся клином. И мы сделаем полноценные часы реально времени с датой на CMSIS.

(обновление статьи 03.05.2024)

В современных микроконтроллерах STM32 (например серии STM32F4) применены современные часы реального времени, версии V2. В них идет счет и даты и времени. В микроконтроллерах STM32F10x используется старый модуль часов реального времени версии V1. При работе от часовой батарейки приращивается один 32-битный регистр RTC->CNTx. Этот регистр по своему алгоритму, HAL разбивает на часы, минуты, секунды и миллисекунды. Этот регистр мы и будем использовать для хранения штампа времени! Мы будем накапливать в него не миллисекунды, а секунды. В 32-битный регистр можно уместить секунд на 136 лет. Даже если вести отсчет от 2024-го года, то часы теоретически могут идти до 2160-го года. Плата сгниет быстрее, чем "кончится время". 

На просторах интернета я нашел примеры кода инициализации часов реального времени, облагородил их таймаутами и написал свой алгоритм преобразования счетчика в год, месяц, день, часы, минуты, секунды.. и обратно.

Готовую библиотеку вы сможете скачать отсюда https://cloud.as.life/s/BGHtZGf5F9PXpcS или из обсуждений на канале https://t.me/ipasoftel/15 

Работа со штампами времени

static const uint8_t daysInMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

// Получить штамп времени в секундах с 0-го по 136-й года.
uint32_t TS_GetSeconds(uint8_t year, uint8_t month, uint8_t day, uint8_t hours, uint8_t minutes, uint8_t seconds) {
	uint8_t i;
	uint32_t result = 0;
	
	if ((year==0)&&(month==1)&&(day==1)&&(hours==0)&&(minutes==0)&&(seconds==0)) return 0;
	
	for (i = 0; i < year; i++) { // Добавлять секунды по прошедшим годам
		if ((i % 4) == 0) result += 366 * 24 * 60 * 60; // Високосный
		else result += 365 * 24 * 60 * 60; // Невисокосный
	}
	for (i = 1; i < month; i++) { // Добавлять секунды по прошедшим месяцам указанного года
		if (((year % 4) == 0) && (i == 2)) result += 29 * 24 * 60 * 60; // Високосный год февраль
		else result += (uint32_t)daysInMonth[i - 1] * 24 * 60 * 60; // Другие года и/или месяцы
	}
	result += (uint32_t)(day - 1) * 24 * 60 * 60; // Добавить секунды по прошедшим дням
	result += (uint32_t)hours * 60 * 60;	// Добавить секунды по прошедшим часам
	result += (uint32_t)minutes * 60;			// Добавить секунды по прошедшим минутам
	result += seconds+1;
	return result;
}

// Разбить секунды штампа времени на дату и время
void TS_GetDateTime(uint32_t stamp, uint8_t *year, uint8_t *month, uint8_t *day, uint8_t *hours, uint8_t *minutes, uint8_t *seconds) {
	uint32_t tempStamp = stamp;
	*year = 0; *month = 1; *day = 1; *hours = 0; *minutes = 0; *seconds = 0;
	if (stamp == 0)  return;
	while(1) { // Разбить секунды на года
		if ((*year % 4) == 0) { // Если висоскосный год обрабатывается
			if (tempStamp > 366 * 24 * 60 * 60) { // Год тут есть точно
				tempStamp -= 366 * 24 * 60 * 60;
				*year = *year + 1; // Прирастить год и убавить из штампа кол-во секунд в этом году
			}
			else break; // Года не наберется
		}
		else { // Если обрабатывается невисокосный год
			if (tempStamp > 365 * 24 * 60 * 60) { // Год тут есть точно
				tempStamp -= 365 * 24 * 60 * 60;
				*year = *year + 1; // Прирастить год и убавить из штампа кол-во секунд в этом году
			}
			else break; // Года не наберется
		}
	}
	while(1) { // Разбить секунды на месяцы
		if (((*year % 4) == 0) && *month == 2) { // Если февраль високосного года
			if (tempStamp > 29 * 24 * 60 * 60) { // Месяц тут точно есть
				tempStamp -= 29 * 24 * 60 * 60;
				*month = *month + 1;
			}
			else break; // Нет месяца
		}
		else { // Другие месяцы високосных и невисокосных лет
			if (tempStamp > (uint32_t)daysInMonth[*month - 1] * 24 * 60 * 60) { // Есть месяц
				tempStamp -= (uint32_t)daysInMonth[*month - 1] * 24 * 60 * 60;
				*month = *month + 1;
			}
			else break; // Не будет месяца
		}
	}
	// Разбить секунды на дни
	while(1) {
		if (tempStamp > 24 * 60 * 60) { // секунд хватает на день
			*day = *day + 1;
			tempStamp -= 24 * 60 * 60;
		}
		else break; // Секунд меньше чем в сутках
	}
	// Разбить секунды на часы
	while(1) {
		if (tempStamp > 60 * 60) {
			*hours = *hours + 1;
			tempStamp -= 60 * 60;
		}
		else break;
	}
	// Разбить секунды на минуты
	while(1) {
		if (tempStamp > 60) {
			tempStamp -= 60;
			*minutes = *minutes + 1;
		}
		else break;
	}
	// Оставшиеся секунды
	if (tempStamp == 0) *seconds = 0;
	else *seconds = tempStamp-1;
}

// Вычисляет день недели любой даты начиная с 2023. Дни 1 - 31, месяцы 1 - 12, год 23 - 99
// Вернёт значение с 0 по 6
uint8_t TS_calcDayOfWeek(uint8_t day, uint8_t month, uint8_t year) {
	uint16_t firstDayOfWeekInYear = 6; // Первый день недели в текущем году. 0 - понедельник, 1 - втор..
	uint8_t i;
	uint8_t leapYear = 0; // Признак високосного года
	// Вычислить первый день недели в указанном году.
	// 2023 году 1 января - воскресенье, день 6
	for (i = 23; i < year; i++) {
		firstDayOfWeekInYear++;
		if (((i - 1) % 4) == 0) { // Если предыдущий год был високосным то
				firstDayOfWeekInYear++;
		}
	}
	if (((year - 1) % 4) == 0) { // Если предыдущий год был високосным то
		firstDayOfWeekInYear++;
	}
	firstDayOfWeekInYear = firstDayOfWeekInYear % 7;
	// Понять, високосный ли год текущий
	if ((year % 4) == 0) leapYear = 1;
	// Сложить дни прошедших месяцев
	for (i = 0; i < (month - 1); i++) {
		firstDayOfWeekInYear += daysInMonth[i];
		if (i == 1) if (leapYear) firstDayOfWeekInYear++; // Если февраль високосного года
	}
	// Прибавить искомый день
	firstDayOfWeekInYear += (day - 1);
	firstDayOfWeekInYear = firstDayOfWeekInYear % 7;
	return (uint8_t)(firstDayOfWeekInYear);
}

// Вернет количество дней в месяце в зависисмости от номера месяца (1-12) и года (0-99)
uint8_t TS_GetDaysInMonth(uint8_t month, uint8_t year) {
	if ((!month) || (month > 12)) return 0;
	uint8_t tempDaysInMonth;
	tempDaysInMonth = daysInMonth[month-1];
	if ((month == 2) && (!(year % 4))) tempDaysInMonth++;
	return tempDaysInMonth;
}

В этой статье я не буду расписывать как что работает, вы это все прочтете в комментариях в коде. Хочу лишь показать как использовать библиотеку.

Сперва необходимо ее разместить в каталоге с проектом, указать компилятору путь к header'ам, подключить #include "stm32f1_rtc.h". Проведите инициализацию сразу после всех сгенеренных инициализаций HAL'а.

Объявите переменную структурного типа для хранения и передачи даты/времени

RTC_UNIT datetime;
/* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  /* USER CODE BEGIN 2 */
  rtc_Init();

Теперь вы можете работать с датой и временем через функции

RTC_UNIT rtc_GetTime(void);
void rtc_SetTime(RTC_UNIT dt);

Пример (внутри main() {})

datetime = rtc_GetTime();
printf("%02i:%02i:%02i\r\n", datetime.hours, datetime.minutes, datetime.seconds);

Копируйте, пользуйтесь свободно. Вы можете обсудить эту статью в нашем Telegram канале. Не забудьте присоединиться, чтобы не пропустить новые статьи.

Tags: ,