Top.Mail.Ru

Часть 16. Таймер реального времени RTC на C++.

Сегодня рассмотрим таймер реального времени (RTC) или по-простому часы. Это такая штука, которая много чего умеет:

  1. Считать реальное время, учитывая високосный год, разное количество дней в месяцах и летнее время;
  2. Имеет два будильника, от которых можно выводить МК из спячки;
  3. Блок автоматической побудки с периодическим флагом и прерыванием;
  4. Специальные входы, к которым можно подключить концевые выключатели, защищающие устройство от несанкционированного доступа;
  5. Корректировка хода часов - от внешнего источника 50/60 Гц или с помощью выхода для замера частоты;
  6. До 84-х регистров на 32 бита, доступ к которым производится через специальную процедуру. Эти регистры не сбрасываются при отключении питания, если батарейка есть в наличии. Сбросить их можно или полным отключением всего питания или сбросом домена резервирования.

Более подробно об этих режимах можно почитать (для STM32F4xx) в RM0090 - Reference manual и в AN3371 - Using the hardware real-time clock (RTC), который изначально содержит ошибки в описании.

Всё, что будет описано здесь, относится к следующим МК:

Тип

Применимые продукты

Микроконтроллеры

  • STM32 F0
  • STM32 F2
  • STM32 F3 (STM32F30x, STM32F31x, STM32F37x, STM32F38x)
  • STM32 F4 (STM32F405xx, STM32F407xx, STM32F415xx, STM32F417xx)
  • STM32 L1

В данной статье рассмотрим только инициализацию RTC и прерывание от блока автоматической побудки с периодическим флагом и прерыванием.

Тактирование RTC.

Тактирование часов можно завести от трёх источников:

  1. LSI - RC-генератор на 32 КГц для STM32F2xx и STM32F4xx, 37 КГц для STM32L1xx;
  2. LSE - генератор на 32768 Гц, внешний кварцевый резонатор;
  3. HSE - высокочастотный генератор, частота задаётся делителем.

У LSI и HSE генераторов есть особенность. При снятии питания они останавливаются, даже при наличии батарейки. Таким образом, проинициализированные часы начнут идти с того времени, которое было при отключении питания. Тактовые сигналы приходят на цепочку делителей, ассинхронный и синхронный:

RTC_Clock - это входной сигнал от источников тактирования. Делители настраиваются таким образом, чтобы на выходе Ck_Spre была частота в 1 Гц, которая подаётся на счётчик календаря и часов. Расчёт делителей производится по формуле:

Рекомендуемые делители для разных источников тактирования:

(1) - Для STM32L1xx - LSI = 37 КГц. Точность хода часов не гарантирована.

(2) - Для STM32F2xx и STM32F4xx - LSI = 32 КГц. Точность хода часов не гарантирована.

Тактовый генератор для часов собран по следующей схеме:

Для других МК указанных ранее серий, схема может незначительно изменяться. Как видим, LSE и LSI такты имеют фиксированную величину частоты. С HSE немного по-другому. Там нужно выставлять деление частоты основного тактового генератора HSE таким, чтобы на выходе мультиплексора RTC_Clock_Mux частота должна быть не выше 1 МГц. В данной статье рассмотрим программирование только самих часов.

Сами часы имеют два основных регистра: даты - RTC->DR и времени - RTC->TR. Эти регистры нельзя считать или записать напрямую, они имеют теневые регистры, при записи в которые перезапись в сам регистр происходит через некоторое время, необходимое для синхронизации таймера, аналогично происходит и при чтении. Сначала происходит синхронизация регистров счёта и его теневого регистра. Сделано это для того, чтобы мы не могли получить неверное время или дату. Примерно работает так... Мы читаем в определённый момент дату и время, а часы-то тикают. При переходе любого счётчика через ноль мы можем в начале цикла чтения считать его предыдущее значение, а в конце уже следующее. Таким образом, допустим мы получим время предыдущего дня или даже предыдущего года, а дату уже следующего. Теневые регистры перезаписываются только после того, как часы "тикнули". Режим теневых регистров можно отключить с помощью записи в CR->BYPSHAD единицы. Но нам это как бы не нужно.

Сами регистры выглядят так:

Получается, что часы и календарь этого типа МК хранят время и дату не в unix формате, а в BCD, что иногда упрощает обработку данных, иногда усложняет.

Сами регистры часов находятся в так называемом Backup domain (Резервный Домен). Его особенность состоит в том, что ни по какому сбросу регистры, находящиеся в Резервном Домене, не сбрасываются. Сбросить их можно только после полного обесточивания или с помощью специального бита RCC_BDCR_BDRST. Кроме этого, большая часть регистров часов защищена от записи, и без некоторых манипуляций их невозможно изменить.

Типичный алгоритм инициализации тактирования:

Шаг

Что делать

Как это сделать

Комментарии

1

Включить тактирование Backup и PWR

Записать биты RCC_APB1ENR_PWREN и RCC_AHB1ENR_BKPSRAMEN

2

Разрешить доступ к Backup-области

Записать бит PWR_CR_DBP

3

Сбросить Backup-область

Записать и сбросить бит RCC_BDCR_BDRST

4

Выбрать источник тактирования

Записать бит RCC_BDCR_RTCSEL_0

В случае выбора LSE в качестве источника тактирования

5

Включить тактирование

Записать бит RCC_BDCR_RTCEN

6

Включить источник тактирования предделителей

Записать бит RCC_BDCR_LSEON

Для источника LSE

7

Дождаться запуска часов

Проверять бит RCC_BDCR_LSERDY

Как только бит равен 1, часы запустились

8

Отключить защиту регистров RTC от записи

Записать "0xCA", а затем "0x53" в регистр RTC_WPR

Регистры RTC могут быть изменены

9

Войти в режим инициализации

Установить бит INIT равным ‘1’ в регистре RTC_ISR

Счетчик календаря останавливается, чтобы разрешить обновление

10

Дождаться подтверждения режима инициализации (синхронизации часов)

Опрашивать бит INITF в RTC_ISR до тех пор, пока он не будет установлен

Для устройств средней плотности требуется примерно 2 такта RTCCLK

11

Программируем регистр прескалеров

Регистр RTC_PRER: сначала записать синхронное значение, а затем асинхронное

По умолчанию регистр прескалеров RTC_PRER инициализируется для предоставления календарному блоку 1 Гц, когда RTCCLK = 32768 Гц

12

Выход из режима инициализации

Очистить бит INIT в регистре RTC_ISR

Текущий счетчик календаря автоматически загружается, и подсчет возобновляется после 4 тактов RTCCLK

13

Включить защиту регистров RTC от записи

Записать "0xFF" в регистр RTC_WPR

Регистры RTC больше не могут быть изменены

14

Запретить доступ к Backup-области

Сбросить бит PWR_CR_DBP

Этим у нас занимается функция Init:

if(!(RCC->BDCR & RCC_BDCR_RTCEN))                                     // Проверка RTC, если не включено, то инициализировать
{
    RCC->APB1ENR  |= RCC_APB1ENR_PWREN;                               // Включить тактирование PWR и Backup
    RCC->AHB1ENR  |= RCC_AHB1ENR_BKPSRAMEN;
    PWR->CR       |= PWR_CR_DBP;                                      // Разрешить доступ к Backup-области
    RCC->BDCR     |= RCC_BDCR_BDRST;                                  // Сбросить Backup-область
    RCC->BDCR     &= ~RCC_BDCR_BDRST;

    switch (RTC_Tact)
    {
      // ---------- LSI ------------ //
      case RTC_LSI:                                                   // Внутренний генератор 32000 Гц
        break;
      // ----------- LSE ----------- //
      case RTC_LSE:                                                   // Кварцевый генератор 32768 Гц
        _Prediv_A = 0x7F;                                             // Делитель для кварца
        _Prediv_S = 0xFF;
        RCC->BDCR |= RCC_BDCR_RTCSEL_0;                               // Выбрать LSE источник (кварц 32768)
        RCC->BDCR |= RCC_BDCR_RTCEN;                                  // Подать тактирование
        RCC->BDCR |= RCC_BDCR_LSEON;                                  // Включить LSE
        for (StartUpCounter = 0;; StartUpCounter++)                   // В цикле ждём определённое время, ожидая запуска
        {
          if(RCC->BDCR & RCC_BDCR_LSERDY) break;                      // Запуск есть, выходим из цикла
          if(StartUpCounter > 0x500000)                               // Время истекло
          {
            RCC->CR &= ~RCC_BDCR_LSEON;                               // Отключаем LSE
            RCC->BDCR &= 0x00 << RCC_BDCR_RTCSEL_Pos;                 // Сбросить выбор тактирования
            RCC->BDCR &= ~RCC_BDCR_RTCEN;                             // Отключить тактирование
            return false;                                             // Запуска нет, выходим из функции
          }
        }
        break;
      // ----------- HSE ----------- //
      case RTC_HSE:                                                   // Основной кварцевый резонатор
        break;
      default:
        break;
    }
    RTC->WPR = 0xCA;                                                  // Выключаем защиту от записи
    RTC->WPR = 0x53;
    RTC->ISR |= RTC_ISR_INIT;                                         // Входим в режим редактирования
    for (StartUpCounter = 0;; StartUpCounter++)                       // В цикле ждём определённое время, ожидая запуска
    {
      if(RTC->ISR & RTC_ISR_INITF) break;                             // Инициализация разрешена, выходим из цикла
      if(StartUpCounter > 0x500000)                                   // Время истекло
      {
        RCC->CR &= ~RCC_CSR_LSION;                                    // Отключаем LSI
        RCC->BDCR &= ~RCC_BDCR_LSEON;                                 // Отключаем LSE
        RCC->BDCR &= 0x00 << RCC_BDCR_RTCSEL_Pos;                     // Отключаем выбор источника
        PWR->CR &= ~PWR_CR_DBP;                                       // Запретить доступ к Backup-области
        RTC->WPR = 0xFF;                                              // Включаем защиту от записи
        RCC->BDCR &= ~RCC_BDCR_RTCEN;                                 // Отключить тактирование
        return false;                                                 // Запуска нет, выходим из функции
      }
    }
    RTC->PRER &= ~(0x7F << RTC_PRER_PREDIV_A_Pos);
    RTC->PRER |= _Prediv_A << RTC_PRER_PREDIV_A_Pos;                  // Настроить делитель
    RTC->PRER &= ~(0x7FFF << RTC_PRER_PREDIV_S_Pos);
    RTC->PRER |= _Prediv_S << RTC_PRER_PREDIV_S_Pos;                  
    RTC->ISR &= ~(RTC_ISR_INIT);                                      // Выходим из режима редактирования
    RTC->WPR = 0xFF;                                                  // Включаем защиту от записи
    PWR->CR &= ~PWR_CR_DBP;                                           // Запретить доступ к Backup-области
    _RTC_FreqSource = RTC_Tact;
  }
  return _RTC_FreqSource;
}

В примере пропущена инициализация LSI и HSE, но в самом коде она присутствует. Типичный алгоритм инициализации часов:

Шаг

Что делать

Как это сделать

Комментарии

1

Разрешить доступ к Backup-области

Записать бит PWR_CR_DBP

2

Отключить защиту регистров RTC от записи

Записать "0xCA", а затем "0x53" в регистр RTC_WPR

Регистры RTC могут быть изменены

3

Войти в режим инициализации

Установить бит INIT равным "1" в регистре RTC_ISR

Счетчик календаря останавливается, чтобы разрешить обновление

4

Дождаться подтверждения режима инициализации (синхронизации часов).

Опрашивать бит INITF в RTC_ISR до тех пор, пока он не будет установлен

Для устройств средней плотности требуется примерно 2 такта RTCCLK

5

При необходимости запрограммировать регистр прескалеров

Регистр RTC_PRER: сначала записать синхронное значение, а затем асинхронное

По умолчанию регистр прескалеров RTC_PRER инициализируется для предоставления календарному блоку 1 Гц, когда RTCCLK = 32768 Гц

6

Загрузка значений времени и даты в теневые регистры

Установить регистры RTC_TR и RTC_DR

7

Настройка формата времени (12 часов или 24 часа)

Установить бит FMT в регистре RTC_CR

FMT = 0: 24 часовой/дневной формат, FMT = 1: AM / PM часовой формат

8

Выход из режима инициализации

Очистить бит INIT в регистре RTC_ISR

Текущий счетчик календаря автоматически загружается, и подсчет возобновляется после 4 тактов RTCCLK

9

Включить защиту регистров RTC

от записи

Записать "0xFF" в регистр RTC_WPR

Регистры RTC больше не могут быть изменены

10

Запретить доступ к Backup-области

Сбросить бит PWR_CR_DBP

Инициализацией у нас занимаются две функции setDate(uint8_t Date, uint8_t Month, uint16_t Year, uint8_t Week) для установки даты и setTime(uint8_t Hour, uint8_t Minute, uint8_t Sec) для установки времени. Код одной из них:

void RTC_main::setTime(uint8_t Hour, uint8_t Minute, uint8_t Sec)
{
  PWR->CR |= PWR_CR_DBP;                                              // Разрешить доступ к Backup-области
  RTC->WPR = 0xCA;                                                    // Выключаем защиту от записи
  RTC->WPR = 0x53;
  RTC->ISR |= RTC_ISR_INIT;                                           // Входим в режим редактирования календаря
  while (!(RTC->ISR & RTC_ISR_INITF)){};                              // Ждем подтверждения входа в режим редактирования
  RTC->TR = ((Hour/10) << RTC_TR_HT_Pos)    |
            ((Hour%10) << RTC_TR_HU_Pos)    |
            ((Minute/10) << RTC_TR_MNT_Pos) |
            ((Minute%10) << RTC_TR_MNU_Pos) |
            ((Sec/10) << RTC_TR_ST_Pos)     |
            ((Sec%10) << RTC_TR_SU_Pos);
  RTC->ISR &= ~(RTC_ISR_INIT);                                        // Выходим из режима редактирования
  RTC->WPR = 0xFF;                                                    // Включаем защиту от записи
  PWR->CR &= ~PWR_CR_DBP;                                             // Запретить доступ к Backup-области
}

Для чтения даты и времени есть два типа функций:

uint16_t      getYear(void);                                          // Читаем год
uint8_t       getWeek(void);                                          // День недели
uint8_t       getMonth(void);                                         // Месяц
uint8_t       getDate(void);                                          // Дату
uint8_t       getHour(void);                                          // Час
uint8_t       getMinute(void);                                        // Минуты
uint8_t       getSeconds(void);                                       // Секунды

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

void          getTime(void);
void          getDates(void);

Эти функции не возвращают ничего, но они заполняют две структуры:

typedef struct
{
  uint8_t Hour;
  uint8_t Minute;
  uint8_t Sec;
} sRTC_Time;

typedef struct
{
  uint8_t   Date;
  uint8_t   Month;
  uint16_t  Year;
  uint8_t   Week;
} sRTC_Date;

Данные из которых после вызова этих функций можно считать:

myRTC.Dates.Date
myRTC.Dates.Month
myRTC.Dates.Year
myRTC.Dates.Week
myRTC.Time.Hour
myRTC.Time.Minute
myRTC.Time.Sec

Кроме регистров даты и времени в некоторых МК есть регистры субсекунд, но применения я им пока не нашёл. Подкидывайте идеи.

У некоторых МК есть ещё плюшки. При тактировании от LSE бывает такое, что кварц не желает работать. В специальном регистре можно покрутить биты, которые увеличивают время ожидания запуска или изменяют параметры выводов, к которым подключен кварцевый резонатор, для изменения условий запуска.

Есть ещё одна функция Reset(). Данная функция просто сбрасывает Backup-область, использовалась при отладке кода, для сброса часов в исходное состояние. Она больше не нужна, но я решил оставить на всякий пожарный. Так как после инициализации код в функции Init() больше не будет выполняться. Также использование этой функции может быть полезно, если после инициализации тактирования на какой-либо источник, допустим LSI, вы позже захотите запитать его от LSE. В этом случае приходится вызывать её, а потом уже функцию Init(). Но следует учесть, что при этом сбросится и календарь, и часы, и будильники.

Больше ничего особенного здесь нет. Таймер побудки, будильники и защиту от несанкционированного доступа рассмотрим чуть позже. Также можно вводить коррекцию хода часов, но там материала ещё на статью. По крайней мере то, что описано здесь, уже позволяет выводить текущее время на экран или в какой либо порт, SPI или UART.

Пример в проекте WorkDevel\Developer\Tests MK\F407\F407_RTC_IT_Example, ссылка на Яндекс диск, и WorkDevel. Из-за увеличения размеров архива, я не стал паковать весь каталог с примерами. В архиве только новый пример и все библиотеки. Если нужно старые примеры, скачивайте или с Яндекс диска, или из статьи №15. Это последняя статья, где пакет примеров был полным. Далее будет только частичным.

Подписаться
Уведомить о
guest

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x