понеділок, 26 грудня 2016 р.

STM32: Годинник реального часу на stm32f1xx. Проблеми, рішення (частина перша)



За допомоги способу реалізації RTC, які висвітлені в цій статті, так і не вдалось враховувати дні, протягом яких MCU був у вимкненому стані, або режимі сну за допомоги HAL драйверу. Реалізація за допомоги CMSIS працює коректно. В другій частині цієї статті (продовженні) описано як вірішити проблему з датою, за допомоги невеличкої бібліотеки, де змінено деякі HAL функції роботи з RTC.

Передмова

В деяких проектах, виконаних на мікроконтролерах, є потреба в годиннику реального часу - RTC (real time clock). На мікроконтролерах STM32F1xx теж є RTC. Хоч і куций, але є і з ним можна працювати. Чому куций? Тому що час, який зберігається, а точніше продовжує йти - це звичайний 32-бітний лічильник секунд. І це все! Добра новина що, цей 32-бітний, лічильник знаходиться в домені BACKUP і продовжує додавати секунди навіть без основного живлення, якщо під'єднано до ніжки мікроконтролера VBAT - батарейку на 3 вольта. Розгляньмо підключення резервного живлення та способи реалізації RTC. Піддослідною платою розробника буде miniboard на чипові STM32F103C8T6. З підключеним дисплеєм WH1602 (HD44780). Бібліотека роботи з цим дисплеєм та його підключення у статті:
"STM32: Бібліотека для роботи з LCD WH1602(4) або іншим сумісним дисплеєм з контролером HD44780"

miniboard STM32F103C8T6

Схема підключення резервного живлення до VBAT

update 25.02.2019: Як виявилось, батарейку 3V, достатньо під'єднати до шпильки VBAT мікроконтролеру через діод D1, щоб основне живлення не потрапляло на батарейку. І це все, діод D2 можна не встановлювати, за бажанням. Коли мікроконтролер увімкнений, то і RTC живиться від загального живлення. Коли з мікроконтролеру зняти живлення, то RTC живиться від батарейки.

Щоб основне живлення не потрапляло на батарейку, а батарейка не живила всю схему, слід підключати, наприклад, по такій схемі:
Схема підключення резервного живлення VBAT

RTC за допомоги драйверу HAL

Реалізація RTC на HAL драйвері неповноцінна. А саме, лічильник RTC зберігає виключно секунди в межах доби. Дата зберігається просто в енергозалежній ОЗП і при вимкненні основного живлення значення дня, місяця і року скинуться до 01.01.00. З-за цього будильник можна встановити в межах доби. Звернемось до документації. В пункті 33.2.2 на сторінці 427 зазначено:
WARNING: Drivers RestrictionsRTC version used on STM32F1 families is version V1. All the features supported by V2
(other families) will be not supported on F1.
As on V2, main RTC features are managed by HW. But on F1, date feature is completely
managed by SW.


Then, there are some restrictions compared to other families:

  • Only format 24 hours supported in HAL (format 12 hours not supported)
  • Date is saved in SRAM. Then, when MCU is in STOP or STANDBY mode, date will be lost. User should implement a way to save date before entering in low power mode (an example is provided with firmware package based on backup registers)
  • Date is automatically updated each time a HAL_RTC_GetTime or HAL_RTC_GetDate is called. 
  • Alarm detection is limited to 1 day. It will expire only 1 time (no alarm repetition, need to program a new alarm) 
Програмна підтримка колодару. Та ще й так кострубато. З-за цього накладені обмеження в порівнянні з іншими серіями мікроконтролерів. А саме:
  • Тільки 24-годинний формат часу
  • Дата зберігається в ОЗП і втрачається при перезавантаженні, втраті живлення чи переході в режим очікування. Треба самому потурбуватись про збереження дати в backup регістри.
  • Сигнал тривоги обмежений однією добою. І потрібно наново встановлювати при потребі.
Зрозуміло. Так і зробимо. Потурбуємось про збереження дати в backup регістри самі.
Скористаємось нашим помічником CubeMX for STM32.

Налаштування і запуск RTC в CubeMX

Особливо налаштовувати нічого і не потрібно. Просто в пункті "Pinout" в гілці "RCC" пункт "Low Speed Clock (LSE)" виставте в "Crystal/Ceramic Resonator", а в гілці "RTC" активуйте пункт "Activate Clock Source". Активувати пункт "Activate Calendar" не потрібно. Бо тоді, при кожному новому увімкнені чи перезавантаженні мікроконтролера, буде встановлюватись дата, яку задали в налаштуваннях кожного разу. Зробіть як показано на малюнку:
RTC PinOut
Далі переходимо до вкладки "Clock Configuration" і обираємо зовнішній кварцовий резонатор як на малюнку:
RTC Clock Configuration
В пункті RTC Configuration нічого не змінював, залишив як є:
RTC Configuration
У вкладці "NVIC Settings" за потреби оберіть переривання по RTC.

Код ініціалізації HAL

Генеруємо проект з кодом ініціалізації CubeMX. Відкривайте свій засіб розробки. Для ініціалізації годинника реального часу CubeMX генерує такі рядки до файлу main.c:
Структура з налаштуваннями RTC.
/* Private variables ---------------------------------------------------------*/
RTC_HandleTypeDef hrtc;
Прототип функції ініціалізації RTC.
/* Private function prototypes -----------------------------------------------*/
static void MX_RTC_Init(void);
В головну функцію main вставляє рядок виклику функції ініціалізації RTC.
 /* Initialize all configured peripherals */
  MX_RTC_Init();
І саму функцію ініціалізації RTC вже після main.
/* RTC init function */

static void MX_RTC_Init(void)
{

    /*Initialize RTC Only*/

  hrtc.Instance = RTC;
  hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;
  hrtc.Init.OutPut = RTC_OUTPUTSOURCE_ALARM;
  if (HAL_RTC_Init(&hrtc) != HAL_OK)
  {
    Error_Handler();
  }

}
Простіше не буває.
Якщо вам потрібно, щоб були переривання раз на секунду від RTC і ви зробили відповідну позначку в CubeMX у вкладці "NVIC Settings", то в файл stm32f1xx_it.c буде розміщено обробник переривань:
/**
* @brief This function handles RTC global interrupt.
*/
void RTC_IRQHandler(void)
{
  /* USER CODE BEGIN RTC_IRQn 0 */

  /* USER CODE END RTC_IRQn 0 */
  HAL_RTCEx_RTCIRQHandler(&hrtc);
  /* USER CODE BEGIN RTC_IRQn 1 */
  
  /* USER CODE END RTC_IRQn 1 */
}
Але переривання відбуватись не будуть поки в основному файлі main.c не дозволити ці переривання функцією HAL_RTCEx_SetSecond_IT(&hrtc); в якості параметру вказівник на структуру RTC. Наприклад, розташуйте цей рядок в користувацькому блоці 2.
 /* USER CODE BEGIN 2 */

  HAL_RTCEx_SetSecond_IT(&hrtc);

 /* USER CODE END 2 */
Після виконання цього рядка мікроконтролером, кожної секунди мікроконтролер буде звертатись до функції обробника переривань RTC "RTC_IRQHandler", який розташований в файлі stm32f1xx_it.c.
Якщо потрібна корекція часу то можна скористатись функцією HAL_RTCEx_SetSmoothCalib, детально про цю функцію дивись у документації, про корректировку докладно тут.

Це все. Після цих маніпуляцій в 32-бітний регістр RTC кожну секунду буде додаватись одиниця. А при вимкнені основного живлення домен RTC буде живитись від резервного живлення, якщо ви його під'єднали. І годинник буде продовжувати працювати.
Але ж треба задати поточну дату і час, та показати дату і час на дисплей. А також не забуваймо, що нам самим треба потурбуватись про збереження дати в backup регістри. Ну що ж - почнемо!

Функція встановлення дати і часу

Щоб оперувати датою і часом, потрібно спочатку оголосити структури дати та часу:
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;

Тепер можна встановлювати дату і час. Вхідні данні функції це рік, місяць, день, день тижня, години, хвилини, секунди та формат часу. Функція нічого не повертає:
static void setDataAndTime(uint8_t year, uint8_t month, uint8_t day, uint8_t weekday, 
             uint8_t hour, uint8_t min,  uint8_t sec, uint32_t format)
{
    /**Initialize RTC and set the Time and Date**/
  sTime.Hours = hour;
  sTime.Minutes = min;
  sTime.Seconds = sec;

  if (HAL_RTC_SetTime(&hrtc, &sTime, format) != HAL_OK)
  {
    Error_Handler();
  }

  sDate.WeekDay = weekday;
  sDate.Month = month;
  sDate.Date = day;
  sDate.Year = year;

  if (HAL_RTC_SetDate(&hrtc, &sDate, format) != HAL_OK)
  {
    Error_Handler();
  }

}

Функція збереження дати до backup регістрів

Так як для збереження дати потрібно 32 біти, а backup регістри мають по 16 біт, то нам потрібно для збереження дати аж два backup регістри. На вхід функція отримує в які саме регістри потрібно зберегти дату. Нічого не повертає:
static void writeDateToBackup(uint32_t bkp_reg1, uint32_t bkp_reg2)
{
  HAL_RTC_GetDate(&hrtc, &sDate, FORMAT_BCD);
  HAL_RTCEx_BKUPWrite(&hrtc, bkp_reg2, (sDate.WeekDay << 8) | (sDate.Year));
  HAL_RTCEx_BKUPWrite(&hrtc, bkp_reg1, (sDate.Date << 8) | (sDate.Month));
}

Функція відновлення дати з backup регістрів

І ще нам потрібна функція яка буде читати записану дату з backup регістрів, заповнювати структуру дати і встановлювати її як поточну. На вхід функція отримує з яких саме регістрів потрібно відновити дату. Нічого не повертає:
static void readBackupToDate(uint32_t bkp_reg1, uint32_t bkp_reg2)
{
 sDate.Month = HAL_RTCEx_BKUPRead(&hrtc,bkp_reg1) & 0x00ff;
 sDate.Date = HAL_RTCEx_BKUPRead(&hrtc,bkp_reg1) >> 8;
 sDate.Year = HAL_RTCEx_BKUPRead(&hrtc,bkp_reg2) & 0x00ff;
 sDate.WeekDay =HAL_RTCEx_BKUPRead(&hrtc,bkp_reg2) >> 8;
 HAL_RTC_SetDate(&hrtc, &sDate, FORMAT_BCD);
}

Функція перевірки нового дня

Ця функція потрібна щоб перевірити чи настав новий день. Як настав, то повертає "false", як той самий день, то повертає "true". 
static bool checkNewDay(uint32_t bkp_reg1)
{
 HAL_RTC_GetDate(&hrtc, &sDate, FORMAT_BCD);
 if(sDate.Date != HAL_RTCEx_BKUPRead(&hrtc,bkp_reg1) >> 8)
 {
  return false;
 } else {
  return true;
 }
}
За допомоги цієї функції можна оновити при зміні дня данні про дату в backup регістри.

Як користуватись

Всю необхідну роботу по ініціалізації та запуску нашого RTC виконав CubeMX, залишилось всім цим скористатись.
Алгоритм буде приблизно такий:
  • Після ініціалізації RTC перевіряємо чи в backup регістрах, відведених для зберігання дати, щось є крім нуля? Як нуль, то можна встановити якусь початкову дату.
  • Як не нуль, то запам'ятовуємо значення RTC лічильника в тимчасову змінну, тому що виклик функції запису збереженої дати зменшить значення RTC лічильника в межах доби. І час, коли MCU знаходився у відключеному стані - втратиться. Структуру дати заповнюємо даними з backup регістрів де зберігається дата перед вимкненням пристрою. Відновлюємо значення RTC лічильника з тимчасової змінної та викликаємо функцію HAL_RTC_GetTime або HAL_RTC_GetDate які приведуть час до стану "в межах доби", а якщо MCU був відключений більше ніж добу, то "пропущений" час додасця до дати автоматично;
  • Далі вже можна показувати час та дату на дисплей, спочатку викликавши функцію HAL_RTC_GetTime та функцію HAL_RTC_GetDate, а вже потім організувати вивід на дисплей отриманих даних;
  • Не забуваємо хоч раз на початку нової доби викликати функцію checkNewDay, щоб перевірити чи не настав новий день і зберігати до backup регістрів нове значення дати.
Ось приблизний приклад як це може виглядати програмно:
/* USER CODE BEGIN 2 */
  lcdInit(); // ініціалізація дисплею
  uint8_t aShowTime[16] = {0};

// Після увімкнення, пробудження, скидання, потрібно викликати цю функцію  
HAL_RTC_WaitForSynchro(&hrtc);  
// Перевіряємо чи дата вже була збережена чи ні
  if (HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR1) == 0)
  {
   // як ні, то задамо якусь початкову дату і час
   setDataAndTime(0x17,RTC_MONTH_JANUARY,0x14,RTC_WEEKDAY_SATURDAY,0x21,0x48,0x00,FORMAT_BCD);
   // і запишемо до backup регістрів дату
   writeDateToBackup(RTC_BKP_DR1,RTC_BKP_DR2);
  } else {
 // якщо вже годинник встановлено, то...
 // запам'ятовуємо кількість секунд з лічильника RTC
 uint32_t timeCounter = (RTC->CNTH << 16) | RTC->CNTL;
 // відновимо з backup регістрів дату. Від лічильника RTC віднімиться час що виходить за межі доби
 readBackupToDate(RTC_BKP_DR1,RTC_BKP_DR2);
 // відновимо попереднє, збережене значення лічильника RTC
 RTC->CNTH = timeCounter>>16;
 RTC->CNTL = timeCounter;
 // Після запису до регістру CNT потрібно викликати цю функцію
 HAL_RTC_WaitForSynchro(&hrtc);
 // виклик цієї функції врахує пропущені дні що був MCU у вимкненому стані
 HAL_RTC_GetTime(&hrtc, &sTime, FORMAT_BIN);
  }
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   // отримуємо поточні час і дату
   HAL_RTC_GetTime(&hrtc, &sTime, FORMAT_BIN);
   HAL_RTC_GetDate(&hrtc, &sDate, FORMAT_BIN);

   // виводимо дату і час на дисплей
   sprintf((char*)aShowTime,"\t%02d:%02d:%02d",sTime.Hours, sTime.Minutes, sTime.Seconds);
   lcdGoto(LCD_2nd_LINE,0);
   lcdPuts((char*)aShowTime);

   sprintf((char*)aShowTime,"\t%02d/%02d/%02d",sDate.Date,sDate.Month,sDate.Year);
   lcdGoto(LCD_1st_LINE,0);
   lcdPuts((char*)aShowTime);

// перевіримо чи настав інший день
   if(!checkNewDay(RTC_BKP_DR1))
   {
    // як так, то запишемо до backup регістрів нову дату
    writeDateToBackup(RTC_BKP_DR1,RTC_BKP_DR2);
   }
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */
В прикладі все враховується, час не втрачається при вимкнені пристрою, якщо під'єднано резервне живлення, як показано на схемі на початку статті. А дата залишається тією що була на момент відключення живлення, хоча і мають додатись пропущені дні. Поки не зміг "побороти" проблему.

Це тільки приклад для демонстрації роботи RTC. Функції можна вдосконалити чи переробити на свій лад. Показав сам принцип.

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

Налаштування і запуск RTC за допомоги CMSIS драйверу

Щоб обійтись без HAL для RTC можна скористатись стандартним драйвером CMSIS. Це стандартна бібліотека, у ній визначено/оголошено всі імена регістрів мікроконтролера. Тому немає потреби задавати їх адреси в пам'яті, постійно зазираючи до технічної документації по мікроконтролеру. Не містить програмного коду як такого.
Ось варіант ініціалізації на CMSIS:
static void mRTCInit(void)
{
 if ((RCC->BDCR & RCC_BDCR_RTCEN) != RCC_BDCR_RTCEN)        // Перевірка роботи годинника, якщо не увімкнені, то ініціалізувати
   {
    RCC->APB1ENR |= RCC_APB1ENR_PWREN | RCC_APB1ENR_BKPEN;  // Увімкнути тактування PWR та Backup
    PWR->CR |= PWR_CR_DBP;                                  // Дозволити доступ до Backup області
    RCC->BDCR |= RCC_BDCR_BDRST;                            // Скинути Backup область
    RCC->BDCR &= ~RCC_BDCR_BDRST;
    RCC->BDCR |= RCC_BDCR_RTCEN | RCC_BDCR_RTCSEL_LSE;      // Обрати LSE джерело (кварц 32768) і подати тактування
    RCC->BDCR |= RCC_BDCR_LSEON;                            // Увімкнути LSE
    while ((RCC->BDCR & RCC_BDCR_LSEON) != RCC_BDCR_LSEON){}// Дочекатись увімкнення
    BKP->RTCCR |= 0;                                        // Калібрування RTC значення від 0 до 0x7F
    while (!(RTC->CRL & RTC_CRL_RTOFF));                    // Перевірити чи закінчились зміни регістрів RTC
    RTC->CRL  |=  RTC_CRL_CNF;                              // Дозволити запис до RTC
    RTC->PRLL  = 0x7FFF;                                    // Налаштувати дільник на 32768 (32767+1)
    RTC->CRL  &=  ~RTC_CRL_CNF;                             // Заборонити запис до RTC
    while (!(RTC->CRL & RTC_CRL_RTOFF));                    // Дочекатись кінця запису
    RTC->CRL &= (uint16_t)~RTC_CRL_RSF;                     // Синхронизувати RTC
    while((RTC->CRL & RTC_CRL_RSF) != RTC_CRL_RSF){}        // Дочекатись синхронізації
    PWR->CR &= ~PWR_CR_DBP;                                 // Заборонити доступ до Backup області
   }
}
Текст функції докладно доповнений коментарями, додаткового пояснення не потребує.
Після виклику функції mRTCInit, яку ми самі створили, годинник реального часу почне додавати одиницю в 32-бітний регістр RTC раз на секунду.

Реалізація колодару

Напишемо всі необхідні функції для реалізації колодару.

Задавання і отримання значення 32-бітного лічильника RTC

Функція отримання значення 32-бітного лічильника RTC:
static uint32_t mRTCGetCounter(void)                           // Отримати значення лічильника
{
          return  (uint32_t)((RTC->CNTH << 16) | RTC->CNTL);
}
Параметрів немає. Повертає значення 32-бітного лічильника.

Функція встановлення нового значення 32-бітного лічильника RTC:
static void mRTCSetCounter(uint32_t count)                     // Записати нове значення лічильника
{
  RCC->APB1ENR |= RCC_APB1ENR_PWREN | RCC_APB1ENR_BKPEN;   // Увімкнути тактування PWR і Backup
  PWR->CR |= PWR_CR_DBP;                                    // Дозволити доступ до Backup області
  while (!(RTC->CRL & RTC_CRL_RTOFF));                      // Перевірити чи закінчились зміни регистрів RTC
  RTC->CRL |= RTC_CRL_CNF;                                  // Дозволити запис в регістри RTC
  RTC->CNTH = count>>16;                                    // Записати нове значення годинникового лічильника
  RTC->CNTL = count;
  RTC->CRL &= ~RTC_CRL_CNF;                                 // Заборонити запис в регістри RTC
  while (!(RTC->CRL & RTC_CRL_RTOFF));                      // Дочекатись кінця запису
  PWR->CR &= ~PWR_CR_DBP;                                   // Заборонити доступ до Backup області
}
Параметри: нове значення лічильника. Нічого не повертає.

Отримувати і задавати значення RTC лічильника ми вже можемо, залишилось ще перетворити значення лічильника в дату і час, та перетворення з дати і часу в значення лічильника.
Тут є два варіанти. Скористатись Unix Time, початок відліку часу ведеться від 01.01.1970 року і 32-бітного лічильника вистачить аж до 19 січня 2038 року, 03:14:07 UTC. Або вести початок відліку з 01.01.2001 року, що значно відтермінує час до виникнення переповнення лічильника.
Далі покажу дві реалізації перетворення значення лічильника в дату і час та навпаки. Одна реалізація колодару буде рахувати секунди від 01.01.1970. Інша реалізація колодару початок відліку почне від 01.01.2001. Обидва колодари реалізуємо у вигляді окремих бібліотек.

Рахуємо від 1 січня 1970 року

Взяв тут цю готову реалізацію, але алгоритми перетворення загальновідомі, на основі них і реалізовані перетворення.

Файл "UnixTime.h" де оголошена структура зберігання дати і часу та оголошені прототипи функцій:
/***************************************************************************
 * Ця бібліотека призначена для роботи з RTC STM32F1xx за допомоги UnixTime*
 * дозволяє перетворювати з лічильника в колодар (день, місяць, рік)    *
 * і час (години, хвилини, секунди), і навпаки       *
 ***************************************************************************/
#ifndef __UNIXTIME_H
#define __UNIXTIME_H
 
 #define SECOND_A_DAY 86400

typedef struct
 {
   int year;
   char mon;
   char mday;
   char hour;
   char min;
   char sec;
   char wday;
 } unixColodar;

void counterToColodar (unsigned long counter, unixColodar * unixTime);
unsigned long colodarToCounter (unixColodar * unixTime);

#endif

Файл "UnixTime.c" де розташовані самі функції перетворення:
#include "UnixTime.h"

void counterToColodar (unsigned long counter, unixColodar * unixTime)
{
 unsigned long a;
 char b;
 char c;
 char d;
 unsigned long time;

 time = counter%SECOND_A_DAY;
 a = ((counter+43200)/(SECOND_A_DAY>>1)) + (2440587<<1) + 1;
 a>>=1;
 unixTime->wday = a%7;
 a+=32044;
 b=(4*a+3)/146097;
 a=a-(146097*b)/4;
 c=(4*a+3)/1461;
 a=a-(1461*c)/4;
 d=(5*a+2)/153;
 unixTime->mday=a-(153*d+2)/5+1;
 unixTime->mon=d+3-12*(d/10);
 unixTime->year=100*b+c-4800+(d/10);
 unixTime->hour=time/3600;
 unixTime->min=(time%3600)/60;
 unixTime->sec=(time%3600)%60;
}

unsigned long colodarToCounter (unixColodar * unixTime)
{
 char a;
 int y;
 char m;
 unsigned long Uday;
 unsigned long time;

 a=((14-unixTime->mon)/12);
 y=unixTime->year+4800-a;
 m=unixTime->mon+(12*a)-3;
 Uday=(unixTime->mday+((153*m+2)/5)+365*y+(y/4)-(y/100)+(y/400)-32045)-2440588;
 time=Uday*SECOND_A_DAY;
 time+=unixTime->sec+unixTime->min*60+unixTime->hour*3600;
 return time;
}
Функція counterToColodar, перетворює значення лічильника в дату і час. На вхід параметри: значення лічильника і вказівник на структуру дати і часу. Нічого не повертає.
Функція colodarToCounter, перетворює дату і час в значення лічильника. На вхід параметри:
вказівник на структуру дати і часу. Повертає значення лічильника.
Завантажити архів бібліотеки UnixTime.rar

Як користуватись

Додайте файли "UnixTime.h" та "UnixTime.c" до свого проекту. На початку головного файлу "main.c" приєднайте бібліотеку:
#include "UnixTime.h"
Оголосіть змінну типу unixColodar:
unixColodar unixTime;
Та додамо до головної програми "main.c" функцію зручного встановлення часу і дати:
static uint32_t colodarSetting(int year, char month, char day, char hour, char min, char sec)
{
 unixTime.year=year;
 unixTime.mon=month;
 unixTime.mday=day;
 unixTime.hour=hour;
 unixTime.min=min;
 unixTime.sec=sec;
 return colodarToCounter(&unixTime);
}
Щоб встановити дату і час, потрібно викликати цю функцію з параметрами часу і дати, а поверне функція вже готове значення 32-бітного лічильника RTC. Отриманий результат можна внести до лічильника RTC викликавши функцію mRTCSetCounter.

Наприклад, щоб встановити дату 23 грудня 2016 року, а час 21 годину 15 хвилин 00 секунд, треба зробити так:
mRTCSetCounter(colodarSetting(2016,12,23,21,15,0));
Цей рядок можна додати до функції ініціалізації RTC mRTCInit і тоді лічильник часу встановиться початковими часом і датою тільки при першому запуску RTC.

Щоб отримати поточну дату і час треба викликати функцію counterToColodar:
counterToColodar(mRTCGetCounter(), &unixTime);
Параметри: значення лічильника RTC, які повертає функція mRTCGetCounter, та вказівник на структуру дати і часу, яку заповнить поточними датою і часом функція counterToColodar.
Структура даних дати і часу заповнені, тепер можна отриману дату і час показати на дисплей. Це вже залежить на який дисплей ви будете виводити інформацію. Наприклад, якщо на дисплей типу WH1602 з бібліотекою hd44780. Підключення і робота з бібліотекою в статті: "STM32: Бібліотека для роботи з LCD WH1602(4) або іншим сумісним дисплеєм з контролером HD44780", то вивід дати і часу на дисплей можуть виглядати так:
counterToColodar(mRTCGetCounter(), &unixTime);
lcdGoto(LCD_1st_LINE,0);
lcdNtos((uint32_t) unixTime.mday, 2);
lcdGoto(LCD_1st_LINE,3);
lcdNtos((uint32_t) unixTime.mon, 2);
lcdGoto(LCD_1st_LINE,6);
lcdNtos((uint32_t) unixTime.year, 4);
lcdGoto(LCD_2nd_LINE,0);
lcdNtos((uint32_t) unixTime.hour, 2);
lcdGoto(LCD_2nd_LINE,3);
lcdNtos((uint32_t) unixTime.min, 2);
lcdGoto(LCD_2nd_LINE,6);
lcdNtos((uint32_t) unixTime.sec, 2);

Рахуємо від 1 січня 2001 року

Це просто ще одна реалізація колодару, як приклад. Взято тут. В цьому варіанті ще додано віднімання днів, що минули з 1 січня 1970 року по 1 січня 2001 року.
Файл "fTimeJD.h" де оголошено константу, структуру і прототипи функцій.
#ifndef FTIMEJD_H_
#define FTIMEJD_H_

#include "stm32f1xx.h"

#define JD0 2451911 // днів до 01 січня 2001 ПН

typedef struct{
  uint16_t year;
  uint8_t month;
  uint8_t day;
  uint8_t hour;
  uint8_t minute;
  uint8_t second;
} ftime_t;

// функція перетворення григоріанської дати і часу в значення лічильника
uint32_t FtimeToCounter(ftime_t * ftime);
// функція перетворення значення лічильника в григоріанську дату і час
void CounterToFtime(uint32_t counter,ftime_t * ftime);

#endif /* FTIMEJD_H_ */

Файл "fTimeJD.c" з функціями бібліотеки.
#include "fTimeJD.h"

// функція перетворення григоріанської дати і часу в значення лічильника
uint32_t FtimeToCounter(ftime_t * ftime)
{
  uint8_t a;
  uint16_t y;
  uint8_t m;
  uint32_t JDN;

  // Обчислення необхідних коефіцієнтів
  a=(14-ftime->month)/12;
  y=ftime->year+4800-a;
  m=ftime->month+(12*a)-3;
  // Обчислюємо значення поточного Юліанського дня
  JDN=ftime->day;
  JDN+=(153*m+2)/5;
  JDN+=365*y;
  JDN+=y/4;
  JDN+=-y/100;
  JDN+=y/400;
  JDN+=-32045;
  JDN+=-JD0;    // приберемо дні які минули до 1 січня 2001 року
  JDN*=86400;        // перетворимо дні в секунди
  JDN+=(ftime->hour*3600);  // і додамо секунди поточного дня
  JDN+=(ftime->minute*60);
  JDN+=(ftime->second);
  // повертаємо кількість секунд з опівночі 1 січня 2001 року
  return JDN;
}

// функція перетворення значення лічильника в григоріанску дату і час
void CounterToFtime(uint32_t counter,ftime_t * ftime)
{
  uint32_t ace;
  uint8_t b;
  uint8_t d;
  uint8_t m;

  ace=(counter/86400)+32044+JD0;
  b=(4*ace+3)/146097;
  ace=ace-((146097*b)/4);
  d=(4*ace+3)/1461;
  ace=ace-((1461*d)/4);
  m=(5*ace+2)/153;
  ftime->day=ace-((153*m+2)/5)+1;
  ftime->month=m+3-(12*(m/10));
  ftime->year=100*b+d-4800+(m/10);
  ftime->hour=(counter/3600)%24;
  ftime->minute=(counter/60)%60;
  ftime->second=(counter%60);
}
Функція CounterToFtime, перетворює значення лічильника в дату і час. На вхід параметри: значення лічильника і вказівник на структуру дати і часу. Нічого не повертає.
Функція FtimeToCounter, перетворює дату і час в значення лічильника. На вхід параметри:
вказівник на структуру дати і часу. Повертає значення лічильника.
Завантажити архів бібліотеки fTimeJD.rar

Як користуватись

Додайте файли "fTimeJD.h" та "fTimeJD.c" до свого проекту. На початку головного файлу "main.c" приєднайте бібліотеку:
#include "fTimeJD.h"
Оголосіть змінну типу ftime_t:
ftime_t jdTime;
Та додамо до головної програми "main.c" функцію зручного встановлення часу і дати:
static uint32_t colodarSetting(int year, char month, char day, char hour, char min, char sec)
{
  jdTime.year = year;
  jdTime.month = month;
  jdTime.day = day;
  jdTime.hour = hour;
  jdTime.minute = min;
  jdTime.second = sec;
  return FtimeToCounter(&jdTime);
}
Щоб встановити дату і час, потрібно викликати цю функцію з параметрами часу і дати, а поверне функція вже готове значення 32-бітного лічильника RTC. Отриманий результат можна внести до лічильника RTC викликавши функцію mRTCSetCounter.

Наприклад, щоб встановити дату 23 грудня 2016 року, а час 21 годину 15 хвилин 00 секунд, треба зробити так:
mRTCSetCounter(colodarSetting(2016,12,23,21,15,0));
Цей рядок можна додати до функції ініціалізації RTC mRTCInit і тоді лічильник часу встановиться початковими часом і датою тільки при першому запуску RTC.

Щоб отримати поточну дату і час треба викликати функцію CounterToFtime:
CounterToFtime(mRTCGetCounter(), &jdTime);
Параметри: значення лічильника RTC, які повертає функція mRTCGetCounter, та вказівник на структуру дати і часу, яку заповнить поточними датою і часом функція CounterToFtime.
Структура даних дати і часу заповнені, тепер можна отриману дату і час показати на дисплей. З дисплеєм і бібліотекою, як в попередньому прикладі, вивід дати і часу на дисплей можуть виглядати ще й так:
uint8_t aShowTime[20] = {0}; 

CounterToFtime(mRTCGetCounter(), &jdTime);

sprintf((char*)aShowTime,"%02d:%02d:%02d",jdTime.hour, jdTime.minute, jdTime.second);
lcdGoto(LCD_2nd_LINE,0);
lcdPuts((char*)aShowTime);

sprintf((char*)aShowTime,"%02d/%02d/%02d",jdTime.day,jdTime.month,jdTime.year);
lcdGoto(LCD_1st_LINE,0);
lcdPuts((char*)aShowTime); 

Деякі зауваження

  • Перший запуск годинника реального часу може тривати досить довго, аж до 1 секунди. Подальші увімкнення, як годинник запущений, будуть швидкими.
  • Хоч і в функціях перетворення часу і дати в секунди та навпаки, багато операцій ділення і множення, та на STM32 ці операції хардварні і виконуються дуже швидко. Перетворення з лічильника секунд в дату і час триває менш ніж одна мілісекунда.
  • Інформацію про будильник не надавав. Поки не було необхідності. Можливо згодом додам.
  • плата розробника mini board STM32F103C8T6 продається з роз'ємами які не впаяні до плати. Треба їх самостійно впаяти. Будьте обережні  і добре промийте спиртом плату після паяння. Часто-густо по причині забрудненості плати годинник реального часу RTC просто не запускається, або плин секунд мікроконтролера не відповідає плину секунд в реальності. Похибка може бути дуже значною. Можливо буде потреба пропаяти годинникові кварц і конденсатори. Або навіть підібрати конденсатори що в обв'язці годинникового кварцу.
  • щодо вживання слова Колодар замість Календар. Слово календар походить від латинського calendarium - боргова книжка. Я з вами не боргову книжку налаштовував і запускав, а відлік часу. Коло-дар  - це коло, яке дарує нам наше Сонце і по ньому ведеться відлік часу. Так буде правильно.

10 коментарів:

  1. Позвольте не согласиться с Вашим комментарием от ноября 2018 года об отсутствии необходимости дополнительных схемных решений при подключении батарейки. При напряжении Vb ниже Vdd она будет заряжаться, а для нее это противопоказано. На мой взгляд Ваша "Схема підключення резервного живлення VBAT" - абсолютно верное решение

    ВідповістиВидалити
    Відповіді
    1. Так, згоден. Ввели мене в оману. Повівся на авторитет. На перевірку виявилось, що батарейку таки треба підключати через діод. Дякую за зауваження. Зараз виправлю.

      Видалити
  2. Как быть с МК STM32L100crt6? На нем нету CNTH & CNTL регистров. Есть какие-то альтернативы?

    ВідповістиВидалити
    Відповіді
    1. я не знаю. якщо ви використовуєте HAL бібліотеки, то може підійде невеличка альтернативна бібліотека https://stm32withoutfear.blogspot.com/2018/05/rtc-for-stm32f1xx-part-Two.html

      Видалити
  3. Дякую за роботу! Окрема подяка)

    ВідповістиВидалити
    Відповіді
    1. Будь ласка. Позитивні відгуки - надихають. Дякую за цікавість до моєї роботи.

      Видалити
  4. страшный язык украинский, даже переводчик его плохо понимает

    ВідповістиВидалити
  5. Братіку! Щиро вам вдячний за вашу роботу! Все описано доступно, все зрозуміло!

    ВідповістиВидалити