Embedded HandbookПриложения
Все разделы › II. Ядро и системные механизмы
STM32G474, STM32G431 · уровень средний · 25 мин чтения
Открыть в интерактивном справочнике

12а. RTC нового образца: доли секунды, калибровка, пробуждение, TAMP

Раздел 12.6 разобрал часы F1 — счётчик секунд — и календарь F4 с двумя будильниками. Но у часов F4 и более новых серий есть и то, что там осталось за кадром: доли секунды, сдвиг хода на долю секунды, подстройка частоты кварца в миллионных долях (ppm) и таймер пробуждения. У G4 к этому добавился отдельный блок TAMP: входы вмешательства, внутренние источники тревоги и 32 резервных регистра. Здесь — всё это на G474: формулы, расчёт по ним и код по регистрам.

Опоры: справочное руководство RM0440 (G4), главы 35 «RTC» и 36 «TAMP», регистр RCC_BDCR и раздел PWR о пробуждении; заголовки stm32g474xx.h, stm32g431xx.h и stm32f407xx.h и библиотека HAL для G4 из паков Keil. Расчёты сделаны моделью на ПК и сверены с формулами RM0440; код собран armclang для G474 и G431. QEMU часы G4 не моделирует — на железе не проверено.

Как это называется в документации

Оригинал Перевод
sub second register (SSR) регистр долей секунды: счётчик синхронного делителя
asynchronous, synchronous prescaler (PREDIV_A, PREDIV_S) асинхронный и синхронный делители
shift control (SHIFTR), ADD1S, SUBFS сдвиг часов на долю секунды назад или вперёд
smooth digital calibration (CALR), CALP, CALM гладкая цифровая калибровка: вставить или выбросить импульсы
wakeup timer (WUT), WUCKSEL таймер пробуждения и выбор его такта
alarm sub second mask (MASKSS) сколько младших бит SS сравнивает будильник
shadow registers, BYPSHAD теневые копии календаря; BYPSHAD — читать мимо них
timestamp (TSE, ITSE, TAMPTS) метка времени: календарь, записанный в момент события
tamper, internal tamper вмешательство: внешний вход или внутренний источник
backup registers (TAMP_BKPxR) резервные регистры
no erase (TAMPxNOER) не стирать резервные регистры при вмешательстве

12а.1 Что нового по сравнению с F4

Доли секунды, сдвиг, гладкая калибровка и таймер пробуждения есть уже у F407: в его заголовке — регистры SSR, SHIFTR, CALR и WUTR. У G474 тот же набор, но блок перестроен:

F407 G474
режим настройки и флаги в одном ISR (0x0C) ICSR (0x0C) — настройка, SR (0x50) — флаги
сброс флага нуль в ISR, сохранив INIT (раздел 12.6) единица в SCR (0x5C)
калибровка грубая CALIBR (0x18) и гладкая CALR (0x3C) только гладкая CALR (0x28)
вмешательство TAFCR (0x40) внутри RTC отдельный блок TAMP: 3 входа и 4 внутренних источника
резервные регистры RTC_BKP0R…BKP19R: 20 штук, 80 байт TAMP_BKP0R…BKP31R: 32 штуки, 128 байт

У G431 резервных регистров 16 — 64 байта: RM0440 даёт 32 устройствам категорий 3 и 4 и 16 — категории 2, заголовки пака это подтверждают. Код с F4 для резервных регистров на G4 не соберётся: RTC->BKP0R там нет, есть TAMP->BKP0R.

RTC у G4: часовой кварц 32 768 Гц идёт через гладкую калибровку и делитель PREDIV_A, получается 256 Гц; счётчик SS считает вниз от PREDIV_S до нуля и выдаёт 1 Гц календарю; доля секунды равна PREDIV_S минус SS, делённому на PREDIV_S плюс 1; SHIFTR прибавляет SUBFS к SS; будильники сравнивают и биты SS; таймер пробуждения WUT берёт такт от кварца через делитель на 2–16 или 1 Гц и будит через линию EXTI 20; блок TAMP с 32 резервными регистрами пишет метку времени и стирает регистры при вмешательстве

Включение по регистрам. У G4 у RTC свой такт шины — бит RTCAPBEN, а источник такта выбирается в RCC_BDCR один раз: сменить RTCSEL потом можно только сбросом резервного домена или после отказа LSE:

void rtc_lse_on(void)
{
    RCC->APB1ENR1 |= RCC_APB1ENR1_PWREN | RCC_APB1ENR1_RTCAPBEN;   /* у G4 у RTC свой такт шины */
    PWR->CR1 |= PWR_CR1_DBP;                /* доступ к резервному домену */
    RCC->BDCR |= RCC_BDCR_LSEON;
    while (!(RCC->BDCR & RCC_BDCR_LSERDY)) { }
    RCC->BDCR |= RCC_BDCR_RTCSEL_0 | RCC_BDCR_RTCEN;   /* 01 — LSE; сменить потом — только сбросом домена */
}

Ключи защиты записи в RTC_WPR те же, что у F4: 0xCA и 0x53. В примерах ниже их пишет rtc_unlock, а rtc_lock возвращает защиту записью 0xFF.


12а.2 Два делителя и доли секунды

Секунду из 32 768 Гц делают два делителя подряд (RM0440, 35.3.4):

ck_apre = 32 768 / (PREDIV_A + 1), 1 Гц = ck_apre / (PREDIV_S + 1).

Асинхронный PREDIV_A — 7 бит, синхронный PREDIV_S — 15 бит; общий делитель — от 1 до 2²² = 4 194 304. Синхронный — это и есть счётчик SS в регистре RTC_SSR: он считает вниз от PREDIV_S до нуля с тактом ck_apre. Отсюда доля секунды:

доля = (PREDIV_S − SS) / (PREDIV_S + 1).

При делителях по умолчанию (127 и 255) SS = 191 — это 0,25 с, SS = 127 — 0,5 с, SS = 0 — 0,99609 с. Шаг — 1 / (PREDIV_S + 1), и его можно измельчить, перенеся деление в синхронный делитель:

PREDIV_A PREDIV_S ck_apre Шаг долей CALP
127 255 256 Гц 3906,25 мкс можно
31 1023 1024 Гц 976,56 мкс можно
7 4095 4096 Гц 244,14 мкс можно
3 8191 8192 Гц 122,07 мкс можно
0 32767 32768 Гц 30,52 мкс нельзя

Цена мелкого шага — ток: чем быстрее ck_apre, тем выше может быть потребление часов, и RM0440 советует держать PREDIV_A большим (цифр тока руководство не даёт). Второе ограничение — калибровка: при PREDIV_A меньше 3 CALP не действует (12а.3). Поэтому 3 и 8191 — самый мелкий шаг, 122 мкс, при полном диапазоне калибровки. Ещё два условия RM0440: будильник по секундам требует PREDIV_S не меньше 3, а подстройка по сети 50 или 60 Гц (REFCKON) — делителей по умолчанию.

Делители пишутся в режиме настройки, и RM0440 требует записать PRER двумя отдельными обращениями (HAL для G4 пишет одним):

void rtc_prescalers(uint32_t a, uint32_t s)
{
    rtc_unlock();
    RTC->ICSR |= RTC_ICSR_INIT;             /* календарь стоит */
    while (!(RTC->ICSR & RTC_ICSR_INITF)) { }
    RTC->PRER = s;                          /* RM0440: две раздельные записи */
    RTC->PRER = s | (a << RTC_PRER_PREDIV_A_Pos);
    RTC->ICSR &= ~RTC_ICSR_INIT;            /* пошёл через 4 такта RTCCLK */
    rtc_lock();
}

INITF поднимается примерно через 2 такта RTCCLK — 61,0 мкс, а после снятия INIT календарь трогается через 4 такта — 122,1 мкс.

Сдвиг на долю секунды

Если часы надо подогнать под точное время — от GPS, сервера времени, другого прибора, — останавливать календарь не нужно. Запись в RTC_SHIFTR прибавляет SUBFS к счётчику SS, а он считает вниз — часы отстают на SUBFS / (PREDIV_S + 1) с. С битом ADD1S к этому прибавляется целая секунда, и часы уходят вперёд на 1 − SUBFS / (PREDIV_S + 1) с:

Нужно PREDIV_S SHIFTR Вышло
назад на 0,1 с 255 0x0000001A −0,101562 с
назад на 0,1 с 8191 0x00000333 −0,099976 с
вперёд на 0,25 с 255 0x800000C0 +0,250000 с
вперёд на 0,25 с 8191 0x80001800 +0,250000 с

Модель счётчика на ПК — SS идёт вниз, сдвиг прибавляет к нему SUBFS — через три секунды хода даёт ту же разницу, что и формула. Точность сдвига — шаг долей: при 255 назад на 0,1 с не выходит, ошибка 1,56 мс.

int rtc_shift(uint32_t add1s, uint32_t subfs)
{
    if (RTC->SSR & 0x8000u)                 /* SS[15] = 1 — новый сдвиг переполнит SS */
        return -1;
    rtc_unlock();
    while (RTC->ICSR & RTC_ICSR_SHPF) { }
    RTC->SHIFTR = (add1s ? RTC_SHIFTR_ADD1S : 0u) | subfs;
    rtc_lock();
    return 0;
}

После сдвига SS бывает больше PREDIV_S, и тогда, по RM0440, верное время на секунду меньше, чем в TR. Формула доли при этом просто даёт отрицательное число — считайте её со знаком:

int32_t rtc_ms(uint32_t ss, uint32_t prediv_s)
{
    return ((int32_t)prediv_s - (int32_t)ss) * 1000 / ((int32_t)prediv_s + 1);
}

12а.3 Гладкая калибровка: ppm вместо подстроечного конденсатора

Часовой кварц редко даёт ровно 32 768 Гц. Ошибка в +20 ppm — это 1,728 с в сутки и 51,8 с за 30 суток, в −35 ppm — 3,024 с в сутки и 90,7 с за месяц. Регистр RTC_CALR исправляет это без подстройки кварца: за цикл из 2²⁰ импульсов — 32 с — выбрасывает CALM импульсов (0…511), а бит CALP добавляет 512 (RM0440, 35.3.13):

FCAL = F × [1 + (CALP × 512 − CALM) / (2²⁰ + CALM − CALP × 512)].

Диапазон — от −487,1 до +488,5 ppm, шаг — 0,9537 ppm. Подбор: CALM − 512 × CALP ≈ 2²⁰ × ошибку. Для +20 ppm это 20,97 — CALM = 21; для −35 ppm это −36,70 — ставим CALP и CALM = 512 − 37 = 475. Модель на ПК перебирает все 1024 сочетания и выбирает лучшее:

Кварц Цикл CALR Остаток Уход в сутки
+20 ppm 32 с 0x0015 −0,027 ppm 0,002 с
+20 ppm 16 с 0x2014 +0,926 ppm 0,080 с
+20 ppm 8 с 0x4014 +0,926 ppm 0,080 с
−35 ppm 32 с 0x81DB +0,286 ppm 0,025 с
−35 ppm 16 с 0xA1DC −0,668 ppm 0,058 с
−35 ppm 8 с 0xC1DC −0,668 ppm 0,058 с

Короткий цикл (CALW16, CALW8) нужен, чтобы быстрее проверить результат, но он огрубляет шаг: младший бит CALM залипает в нуле при 16 с, два младших — при 8 с. Проверяют ход по выходу CALIB на RTC_OUT1 (PC13): при делителях по умолчанию это 512 Гц при COSEL = 0 или 1 Гц при COSEL = 1. Если мерить ровно цикл, ошибка измерения — полтакта за окно: 0,477 ppm за 32 с, 0,954 ppm за 16 с, 1,907 ppm за 8 с.

Выброшенные импульсы разбросаны по циклу: по RM0440, бит CALM[0] убирает импульс при cal_cnt = 0x80000, CALM[1] — при 0x40000 и 0xC0000 и так далее до CALM[8] — 256 импульсов вида 0xXX800. Модель на ПК раскладывает их по этим правилам: при любом CALM на любом начале цикла отставание от равномерного — не больше 1,89 импульса. Поэтому часы калиброваны и на коротких отрезках, а не только раз в 32 с. Простой подсчёт импульсов за цикл расходится с формулой RM не больше чем на 0,24 ppm — это меньше шага.

CALR можно менять на ходу, но только когда RECALPF = 0: пока прошлое значение не принято, регистр заблокирован. Новое вступает в силу за три такта ck_apre — 11,72 мс при 256 Гц:

void rtc_calibrate(uint32_t calr)
{
    rtc_unlock();
    while (RTC->ICSR & RTC_ICSR_RECALPF) { }    /* прошлая запись ещё не принята */
    RTC->CALR = calr;
    rtc_lock();
}

12а.4 Таймер пробуждения и будильник с долями секунды

Будильник из раздела 12.6 привязан к календарю, а для «проснуться через N» есть отдельный 16-битный таймер WUT: он считает вниз от значения RTC_WUTR, раз в WUT + 1 тактов поднимает WUTF и перезагружается сам. Такт выбирает WUCKSEL (RM0440, 35.3.7):

WUCKSEL Такт Шаг Период
000 RTCCLK / 16 488,28 мкс от 0,488 мс до 32 с
001 RTCCLK / 8 244,14 мкс от 0,244 мс до 16 с
010 RTCCLK / 4 122,07 мкс от 0,122 мс до 8 с
011 RTCCLK / 2 61,04 мкс от 0,122 мс до 4 с
10x ck_spre, 1 Гц 1 с от 1 до 65 536 с — 18,2 ч
11x ck_spre и ещё 2¹⁶ 1 с от 65 537 до 131 072 с — 36,4 ч

При RTCCLK / 2 значение WUT = 0 запрещено, поэтому минимум — два такта. При 11x к счётчику добавляется 2¹⁶: таймер становится 17-битным. Выбор по периоду — самый мелкий шаг, в который период влезает:

Период WUCKSEL WUT Получится
1 мс 011 15 (0x000F) 0,977 мс, −2,344 %
100 мс 011 1637 (0x0665) 99,976 мс, −0,024 %
1 с 011 16383 (0x3FFF) ровно 1 с
10 мин 100 599 (0x0257) ровно 600 с
24 ч 110 20863 (0x517F) ровно 86 400 с

Секунду можно взять и от ck_spre с WUT = 0 — тот же такт, что ведёт календарь. Миллисекунда ровно не получается: у кварца на 32 768 Гц нет целого числа тактов в 1 мс.

void rtc_wakeup(uint32_t wucksel, uint32_t wut)
{
    rtc_unlock();
    RTC->CR &= ~(RTC_CR_WUTE | RTC_CR_WUTIE);
    while (!(RTC->ICSR & RTC_ICSR_WUTWF)) { }   /* WUTR и WUCKSEL можно менять */
    RTC->WUTR = wut;
    RTC->CR = (RTC->CR & ~RTC_CR_WUCKSEL) | wucksel;
    RTC->SCR = RTC_SCR_CWUTF;
    RTC->CR |= RTC_CR_WUTIE | RTC_CR_WUTE;
    rtc_lock();
    EXTI->IMR1 |= EXTI_IMR1_IM20;           /* из Stop будит линия EXTI 20 */
    EXTI->RTSR1 |= EXTI_RTSR1_RT20;
    NVIC_EnableIRQ(RTC_WKUP_IRQn);
}

void RTC_WKUP_IRQHandler(void)
{
    if (RTC->SR & RTC_SR_WUTF) {
        RTC->SCR = RTC_SCR_CWUTF;           /* единица в SCR — без ключей и без INIT */
        wakeups++;
    }
    EXTI->PR1 = EXTI_PR1_PIF20;
}

Будить из Stop через таймер — линия EXTI 20 по переднему фронту (RM0440, 6.3.10), будильник — линия 17. Первый WUTF приходит не через WUT + 1 тактов, а между WUT и WUT + 1 после WUTE: для 10 минут — между 599 и 600 с, а при ck_spre с WUT = 0 — в любой момент первой секунды.

Будильник с долями секунды

Рядом с RTC_ALRMAR из раздела 12.6 у G4 стоит RTC_ALRMASSR: значение SS и MASKSS — сколько его младших бит сравнивать. Если все поля ALRMAR замаскированы, одна только маска даёт периодический будильник. Модель на ПК при PREDIV_S = 255:

MASKSS Сравниваются Срабатывает
0 ничего, срабатывает на смене секунды раз в 1 с
1 SS[0] раз в 7,812 мс
6 SS[5:0] раз в 0,25 с
7 SS[6:0] раз в 0,5 с
8 SS[7:0] раз в 1 с
15 все 15 бит раз в 1 с

Будильник «каждый день в 07:15:00,500» — это ALRMAR = 0x80071500 из раздела 12.6 и ALRMASSR = 0x0F00007F: SS = 127 — середина секунды. Запись — как у F4, после ALRAWF, только флаг сбрасывается единицей в SCR:

void rtc_alarm_a_ss(uint32_t alrmar, uint32_t maskss, uint32_t ss)
{
    rtc_unlock();
    RTC->CR &= ~(RTC_CR_ALRAE | RTC_CR_ALRAIE);
    while (!(RTC->ICSR & RTC_ICSR_ALRAWF)) { }
    RTC->ALRMAR = alrmar;
    RTC->ALRMASSR = (maskss << RTC_ALRMASSR_MASKSS_Pos) | ss;
    RTC->SCR = RTC_SCR_CALRAF;
    RTC->CR |= RTC_CR_ALRAE | RTC_CR_ALRAIE;
    rtc_lock();
}

12а.5 Чтение календаря: теневые регистры и BYPSHAD

RTC_SSR, RTC_TR и RTC_DR по умолчанию читаются из теневых копий: календарь всё время копируется туда и поднимает RSF. Правила RM0440 (35.3.9):

void rtc_resync(void)
{
    rtc_unlock();
    RTC->ICSR &= ~RTC_ICSR_RSF;
    rtc_lock();
    while (!(RTC->ICSR & RTC_ICSR_RSF)) { }
}

С BYPSHAD = 1 ждать RSF не нужно — значения берутся прямо из счётчиков, но и согласованности никто не обещает: такт может прийти между чтениями. Модель на ПК ставит часы на 12:00:00 с SS = 0 — последний такт секунды — и даёт тикнуть сразу после первого чтения:

Как читали Показано На самом деле Ошибка
TR, потом SSR 12:00:00,000 12:00:01,000 −1 с
SSR, потом TR 12:00:01,996 12:00:01,000 +0,996 с
SSR, TR, DR и SSR ещё раз 12:00:01,000 12:00:01,000 нет

Лечение — то же, что для счётчика F1 в разделе 12.6: перечитать младший регистр и повторить, если он изменился. RM0440 так и советует — сравнить два чтения младшего регистра календаря. В редком случае с тактом между чтениями это восемь обращений вместо четырёх:

void rtc_read(rtc_now_t *t)
{
    do {
        t->ss = RTC->SSR;
        t->tr = RTC->TR;
        t->dr = RTC->DR;
    } while (t->ss != RTC->SSR);
}

12а.6 TAMP: вмешательство, метка времени, резервные регистры

У G474 32 резервных регистра TAMP_BKP0R…BKP31R — с 0x4000 2500 по 0x4000 257C. Их не сбрасывает ни системный сброс, ни выход из Standby, и они питаются от VBAT. Запись требует DBP, как и всё в резервном домене (раздел 10.5):

void bkp_write(uint32_t i, uint32_t v)
{
    (&TAMP->BKP0R)[i] = v;                  /* нужен DBP; пока стоит флаг вмешательства — не запишется */
}

armclang превращает её в одну запись по адресу 0x4000 2500 + 4 × i:

bkp_write: MOVW  r2,#0x2500
           MOVT  r2,#0x4000
           STR   r1,[r2,r0,LSL #2]
           BX    lr

Вмешательство стирает их. По умолчанию любое событие TAMP обнуляет все резервные регистры, и они держатся в нуле, пока стоит хоть один флаг вмешательства. Не стирать можно по входу: TAMPxNOER или TAMPxMSK в TAMP_CR2 — в заголовке эти биты называются TAMP1NOERASE и TAMP1MF. Ещё регистры стираются при снятии защиты чтения флеш с уровня 1 на 0.

Источников вмешательства семь. Внешних входов три — TAMP_IN1…TAMP_IN3; внутренних четыре: сбой LSE, сбой HSE, переполнение календаря (31 декабря 99-го года, 23:59:59 — дальше календарь стоит) и заводское чтение ST. После сброса резервного домена TAMP_CR1 = 0xFFFF 0000 — биты ITAMP3E…ITAMP6E уже стоят.

Внешний вход работает двумя способами:

Детектор работает и при снятом VDD, поэтому вход должен быть надёжно притянут к неактивному уровню — иначе регистры сотрёт помеха. Пример: при вскрытии крышка корпуса замыкает TAMP_IN1 (PC13) на землю, 4 выборки при 16 Гц — 0,25 с:

void tamp_in1_setup(void)
{
    TAMP->FLTCR = (2u << TAMP_FLTCR_TAMPFLT_Pos) | (4u << TAMP_FLTCR_TAMPFREQ_Pos);
    TAMP->CR2 &= ~(TAMP_CR2_TAMP1NOERASE | TAMP_CR2_TAMP1MF | TAMP_CR2_TAMP1TRG);  /* низкий уровень — вскрытие */
    TAMP->IER |= TAMP_IER_TAMP1IE;
    TAMP->CR1 |= TAMP_CR1_TAMP1E;
    rtc_unlock();
    RTC->CR |= RTC_CR_TAMPTS;               /* метка времени на вмешательство, даже при TSE = 0 */
    rtc_lock();
    EXTI->IMR1 |= EXTI_IMR1_IM19;           /* вмешательство и метка — линия EXTI 19 */
    EXTI->RTSR1 |= EXTI_RTSR1_RT19;
    NVIC_EnableIRQ(RTC_TAMP_LSECSS_IRQn);
}

Метка времени — календарь, записанный в RTC_TSSSR, RTC_TSTR и RTC_TSDR в момент события. Событий три: фронт на RTC_TS (бит TSE), переход на батарейку (ITSE) и вмешательство (TAMPTS). TSF поднимается через 2 такта ck_apre — 7,81 мс при 256 Гц; второе событие при стоящем TSF поднимает TSOVF, а в регистрах остаётся первое. Года в метке нет: в RTC_TSDR только день недели, месяц и число. И главное — сброс TSF обнуляет метку, поэтому сначала прочитать, потом сбросить:

void RTC_TAMP_LSECSS_IRQHandler(void)
{
    if (TAMP->SR & TAMP_SR_TAMP1F) {
        if (RTC->SR & RTC_SR_TSF) {         /* сначала прочитать: сброс TSF обнуляет метку */
            ts_ss = RTC->TSSSR;
            ts_time = RTC->TSTR;
            ts_date = RTC->TSDR;
            RTC->SCR = RTC_SCR_CTSF;
        }
        TAMP->SCR = TAMP_SCR_CTAMP1F;       /* пока флаг стоит, резервные регистры держатся в нуле */
        tampers++;
    }
    EXTI->PR1 = EXTI_PR1_PIF19;
}

На PC13 сидят сразу RTC_OUT1, RTC_TS и TAMP_IN1, и эти функции работают во всех режимах сна и от батарейки.


12а.7 Грабли

Регистры RTC не отвечают. У G4 у часов свой такт шины: не включён RTCAPBEN в RCC_APB1ENR1.

Источник такта не меняется. RTCSEL выбирается один раз; сменить — только сбросом резервного домена (BDRST), а он стирает и часы.

CALP не действует. PREDIV_A меньше 3 — тогда RM0440 считает CALP равным нулю. Самый мелкий шаг с полной калибровкой — делители 3 и 8191.

Запись в CALR пропала. Стоял RECALPF: регистр заблокирован, пока прошлое значение не принято.

Нечётный CALM не работает. Включён CALW16 (или CALW8): младшие биты залипают в нуле, шаг — 1,907 ppm (или 3,815 ppm).

Будильник по секундам молчит. PREDIV_S меньше 3, а MSK1 = 0.

После пробуждения время старое. В Stop теневые копии не обновляются: сбросить RSF и дождаться его.

Время изредка прыгает на секунду. BYPSHAD = 1, а SSR и TR прочитаны без повтора — такт пришёлся между чтениями.

Первое пробуждение раньше срока. Первый WUTF — между WUT и WUT + 1 тактами; при ck_spre и WUT = 0 — в любой момент первой секунды.

WUTR не записывается. Не снят WUTE или не дождались WUTWF.

Сдвиг не сработал. Стоял SHPF (прошлый сдвиг идёт), SS[15] = 1 или включён REFCKON — с подстройкой по сети сдвиг несовместим.

Флаг сбрасывают как на F4. Нуль в RTC_SR ничего не делает — у G4 флаги гасит единица в RTC_SCR.

Резервный регистр не пишется. Нет DBP — или стоит флаг вмешательства: пока он не сброшен, регистры держатся в нуле.

Резервные регистры стёрлись сами. Сработал вход вмешательства (висит в воздухе, по фронту включён при активном уровне), внутренний источник или снята защита чтения с уровня 1 на 0.

Метка времени пустая. TSF сброшен раньше, чем прочитаны TSTR и TSDR, — сброс их обнуляет.

Код резервных регистров с F4 не собирается. У G4 они в TAMP, а не в RTC: TAMP->BKP0R.


12а.8 Итоги

Полные исходники

rtc12a.c — RTC и TAMP у STM32G474 по регистрам.

/* 12а: RTC и TAMP у STM32G474 по регистрам — LSE, делители, гладкая калибровка, сдвиг на доли
 * секунды, таймер пробуждения, будильник с субсекундами, чтение календаря, резервные регистры
 * TAMP, вход вмешательства с меткой времени. Собирается armclang: -DSTM32G474xx (и -DSTM32G431xx —
 * у него резервных регистров 16). Числа — RM0440, главы 35 и 36; проверка — _verify/rtc12a_check.py. */
#include <stdint.h>
#include <stddef.h>

#define WUCKSEL_DIV2    3u                  /* 011: RTCCLK / 2, шаг 61 мкс */
#define WUCKSEL_1HZ     4u                  /* 10x: ck_spre, обычно 1 Гц */
#define WUCKSEL_1HZ_17  6u                  /* 11x: ck_spre, к WUT добавляется 2^16 */
#define CALR_FAST_20PPM 21u                 /* кварц спешит на 20 ppm: замаскировать 21 импульс из 2^20 */
#define CALR_SLOW_35PPM (RTC_CALR_CALP | 475u)  /* отстаёт на 35 ppm: +512 − 475 = +37 импульсов */

_Static_assert(RTC_BASE == 0x40002800u && TAMP_BASE == 0x40002400u, "RTC и TAMP — соседние блоки на APB1");
_Static_assert(offsetof(RTC_TypeDef, SSR) == 0x08u && offsetof(RTC_TypeDef, ICSR) == 0x0Cu
               && offsetof(RTC_TypeDef, PRER) == 0x10u && offsetof(RTC_TypeDef, WUTR) == 0x14u
               && offsetof(RTC_TypeDef, CR) == 0x18u && offsetof(RTC_TypeDef, WPR) == 0x24u
               && offsetof(RTC_TypeDef, CALR) == 0x28u && offsetof(RTC_TypeDef, SHIFTR) == 0x2Cu
               && offsetof(RTC_TypeDef, TSSSR) == 0x38u && offsetof(RTC_TypeDef, ALRMASSR) == 0x44u
               && offsetof(RTC_TypeDef, SR) == 0x50u && offsetof(RTC_TypeDef, SCR) == 0x5Cu,
               "смещения регистров RTC — по таблице 336 RM0440");
_Static_assert(offsetof(TAMP_TypeDef, BKP0R) == 0x100u && RTC_TAMP_NB == 3u && RTC_TAMP_INT_NB == 4u,
               "резервные регистры с 0x100; три внешних входа и четыре внутренних источника");
#if defined(STM32G474xx)
_Static_assert(RTC_BACKUP_NB == 32u && offsetof(TAMP_TypeDef, BKP31R) == 0x17Cu, "у G474 — 32 регистра");
#elif defined(STM32G431xx)
_Static_assert(RTC_BACKUP_NB == 16u && offsetof(TAMP_TypeDef, BKP15R) == 0x13Cu, "у G431 — 16 регистров");
#endif
_Static_assert(RTC_PRER_PREDIV_A_Pos == 16u && RTC_PRER_PREDIV_A_Msk == (0x7Fu << 16)
               && RTC_PRER_PREDIV_S_Msk == 0x7FFFu, "PREDIV_A — 7 бит с 16-го, PREDIV_S — 15 бит");
_Static_assert(RTC_SSR_SS_Msk == 0xFFFFu && RTC_ALRMASSR_SS_Msk == 0x7FFFu && RTC_ALRMASSR_MASKSS_Pos == 24u,
               "SS — 16 бит (15-й — переполнение после сдвига), в будильнике — 15, MASKSS с 24-го бита");
_Static_assert(RTC_CALR_CALP == (1u << 15) && RTC_CALR_CALW8 == (1u << 14) && RTC_CALR_CALW16 == (1u << 13)
               && RTC_CALR_CALM_Msk == 0x1FFu, "CALP, CALW8, CALW16 — биты 15, 14, 13; CALM — 9 бит");
_Static_assert(CALR_FAST_20PPM == 0x0015u && CALR_SLOW_35PPM == 0x81DBu, "CALR для +20 и −35 ppm");
_Static_assert(RTC_SHIFTR_ADD1S == (1u << 31) && RTC_SHIFTR_SUBFS_Msk == 0x7FFFu, "ADD1S — бит 31, SUBFS — 15 бит");
_Static_assert(RTC_CR_WUCKSEL_Msk == 7u && RTC_WUTR_WUT_Msk == 0xFFFFu && RTC_CR_WUTE == (1u << 10)
               && RTC_CR_WUTIE == (1u << 14) && RTC_CR_BYPSHAD == (1u << 5),
               "WUCKSEL — 3 бита, WUT — 16, WUTE — бит 10, WUTIE — 14, BYPSHAD — 5");
_Static_assert(RTC_ICSR_WUTWF == (1u << 2) && RTC_ICSR_SHPF == (1u << 3) && RTC_ICSR_RSF == (1u << 5)
               && RTC_ICSR_INITF == (1u << 6) && RTC_ICSR_INIT == (1u << 7) && RTC_ICSR_RECALPF == (1u << 16),
               "флаги ICSR");
_Static_assert(RTC_SR_WUTF == (1u << 2) && RTC_SCR_CWUTF == RTC_SR_WUTF && RTC_SCR_CTSF == RTC_SR_TSF,
               "флаг в SR и бит его сброса в SCR — на одном месте");
_Static_assert(TAMP_FLTCR_TAMPFREQ_Pos == 0u && TAMP_FLTCR_TAMPFLT_Pos == 3u && TAMP_FLTCR_TAMPPRCH_Pos == 5u
               && TAMP_FLTCR_TAMPPUDIS == (1u << 7), "поля TAMP_FLTCR");
_Static_assert(TAMP_CR2_TAMP1NOERASE == 1u && TAMP_CR2_TAMP1MF == (1u << 16) && TAMP_CR2_TAMP1TRG == (1u << 24),
               "TAMP1NOER и TAMP1MSK из RM в заголовке называются TAMP1NOERASE и TAMP1MF");
_Static_assert(RCC_BDCR_RTCSEL_0 == (1u << 8) && RCC_BDCR_RTCEN == (1u << 15) && RCC_APB1ENR1_RTCAPBEN == (1u << 10)
               && PWR_CR1_DBP == (1u << 8), "RTCSEL = 01 — LSE; RTCEN; такт шины RTC; DBP");
_Static_assert(EXTI_IMR1_IM20 == (1u << 20) && RTC_WKUP_IRQn == 3 && RTC_TAMP_LSECSS_IRQn == 2,
               "таймер пробуждения — линия EXTI 20");

volatile uint32_t wakeups, tampers;
volatile uint32_t ts_time, ts_date, ts_ss;

static void rtc_unlock(void)
{
    RTC->WPR = 0xCAu;                       /* ключи те же, что у F4 */
    RTC->WPR = 0x53u;
}

static void rtc_lock(void)
{
    RTC->WPR = 0xFFu;
}

void rtc_lse_on(void)
{
    RCC->APB1ENR1 |= RCC_APB1ENR1_PWREN | RCC_APB1ENR1_RTCAPBEN;   /* у G4 у RTC свой такт шины */
    PWR->CR1 |= PWR_CR1_DBP;                /* доступ к резервному домену */
    RCC->BDCR |= RCC_BDCR_LSEON;
    while (!(RCC->BDCR & RCC_BDCR_LSERDY)) { }
    RCC->BDCR |= RCC_BDCR_RTCSEL_0 | RCC_BDCR_RTCEN;   /* 01 — LSE; сменить потом — только сбросом домена */
}

/* 1 Гц = 32768 / ((a + 1) × (s + 1)); шаг долей секунды — 1 / (s + 1) */
void rtc_prescalers(uint32_t a, uint32_t s)
{
    rtc_unlock();
    RTC->ICSR |= RTC_ICSR_INIT;             /* календарь стоит */
    while (!(RTC->ICSR & RTC_ICSR_INITF)) { }
    RTC->PRER = s;                          /* RM0440: две раздельные записи */
    RTC->PRER = s | (a << RTC_PRER_PREDIV_A_Pos);
    RTC->ICSR &= ~RTC_ICSR_INIT;            /* пошёл через 4 такта RTCCLK */
    rtc_lock();
}

void rtc_calibrate(uint32_t calr)
{
    rtc_unlock();
    while (RTC->ICSR & RTC_ICSR_RECALPF) { }    /* прошлая запись ещё не принята */
    RTC->CALR = calr;
    rtc_lock();
}

/* назад на subfs / (PREDIV_S + 1) с; с add1s — вперёд на 1 − subfs / (PREDIV_S + 1) */
int rtc_shift(uint32_t add1s, uint32_t subfs)
{
    if (RTC->SSR & 0x8000u)                 /* SS[15] = 1 — новый сдвиг переполнит SS */
        return -1;
    rtc_unlock();
    while (RTC->ICSR & RTC_ICSR_SHPF) { }
    RTC->SHIFTR = (add1s ? RTC_SHIFTR_ADD1S : 0u) | subfs;
    rtc_lock();
    return 0;
}

void rtc_wakeup(uint32_t wucksel, uint32_t wut)
{
    rtc_unlock();
    RTC->CR &= ~(RTC_CR_WUTE | RTC_CR_WUTIE);
    while (!(RTC->ICSR & RTC_ICSR_WUTWF)) { }   /* WUTR и WUCKSEL можно менять */
    RTC->WUTR = wut;
    RTC->CR = (RTC->CR & ~RTC_CR_WUCKSEL) | wucksel;
    RTC->SCR = RTC_SCR_CWUTF;
    RTC->CR |= RTC_CR_WUTIE | RTC_CR_WUTE;
    rtc_lock();
    EXTI->IMR1 |= EXTI_IMR1_IM20;           /* из Stop будит линия EXTI 20 */
    EXTI->RTSR1 |= EXTI_RTSR1_RT20;
    NVIC_EnableIRQ(RTC_WKUP_IRQn);
}

void RTC_WKUP_IRQHandler(void)
{
    if (RTC->SR & RTC_SR_WUTF) {
        RTC->SCR = RTC_SCR_CWUTF;           /* единица в SCR — без ключей и без INIT */
        wakeups++;
    }
    EXTI->PR1 = EXTI_PR1_PIF20;
}

/* будильник A: календарь как в разделе 12 плюс доли секунды — сравниваются SS[maskss−1:0] */
void rtc_alarm_a_ss(uint32_t alrmar, uint32_t maskss, uint32_t ss)
{
    rtc_unlock();
    RTC->CR &= ~(RTC_CR_ALRAE | RTC_CR_ALRAIE);
    while (!(RTC->ICSR & RTC_ICSR_ALRAWF)) { }
    RTC->ALRMAR = alrmar;
    RTC->ALRMASSR = (maskss << RTC_ALRMASSR_MASKSS_Pos) | ss;
    RTC->SCR = RTC_SCR_CALRAF;
    RTC->CR |= RTC_CR_ALRAE | RTC_CR_ALRAIE;
    rtc_lock();
}

typedef struct {
    uint32_t ss, tr, dr;
} rtc_now_t;

/* BYPSHAD = 1: SSR прочитан дважды одинаковым — между чтениями календарь не менялся */
void rtc_read(rtc_now_t *t)
{
    do {
        t->ss = RTC->SSR;
        t->tr = RTC->TR;
        t->dr = RTC->DR;
    } while (t->ss != RTC->SSR);
}

/* BYPSHAD = 0, после выхода из Stop: теневые регистры ещё старые */
void rtc_resync(void)
{
    rtc_unlock();
    RTC->ICSR &= ~RTC_ICSR_RSF;
    rtc_lock();
    while (!(RTC->ICSR & RTC_ICSR_RSF)) { }
}

/* доля секунды в миллисекундах; после сдвига SS бывает больше PREDIV_S — доля отрицательная */
int32_t rtc_ms(uint32_t ss, uint32_t prediv_s)
{
    return ((int32_t)prediv_s - (int32_t)ss) * 1000 / ((int32_t)prediv_s + 1);
}

void bkp_write(uint32_t i, uint32_t v)
{
    (&TAMP->BKP0R)[i] = v;                  /* нужен DBP; пока стоит флаг вмешательства — не запишется */
}

uint32_t bkp_read(uint32_t i)
{
    return (&TAMP->BKP0R)[i];
}

/* TAMP_IN1 (PC13): крышка замыкает вход на землю; 4 выборки подряд при 16 Гц — 0,25 с;
 * резервные регистры стираются, метка времени пишется */
void tamp_in1_setup(void)
{
    TAMP->FLTCR = (2u << TAMP_FLTCR_TAMPFLT_Pos) | (4u << TAMP_FLTCR_TAMPFREQ_Pos);
    TAMP->CR2 &= ~(TAMP_CR2_TAMP1NOERASE | TAMP_CR2_TAMP1MF | TAMP_CR2_TAMP1TRG);  /* низкий уровень — вскрытие */
    TAMP->IER |= TAMP_IER_TAMP1IE;
    TAMP->CR1 |= TAMP_CR1_TAMP1E;
    rtc_unlock();
    RTC->CR |= RTC_CR_TAMPTS;               /* метка времени на вмешательство, даже при TSE = 0 */
    rtc_lock();
    EXTI->IMR1 |= EXTI_IMR1_IM19;           /* вмешательство и метка — линия EXTI 19 */
    EXTI->RTSR1 |= EXTI_RTSR1_RT19;
    NVIC_EnableIRQ(RTC_TAMP_LSECSS_IRQn);
}

void RTC_TAMP_LSECSS_IRQHandler(void)
{
    if (TAMP->SR & TAMP_SR_TAMP1F) {
        if (RTC->SR & RTC_SR_TSF) {         /* сначала прочитать: сброс TSF обнуляет метку */
            ts_ss = RTC->TSSSR;
            ts_time = RTC->TSTR;
            ts_date = RTC->TSDR;
            RTC->SCR = RTC_SCR_CTSF;
        }
        TAMP->SCR = TAMP_SCR_CTAMP1F;       /* пока флаг стоит, резервные регистры держатся в нуле */
        tampers++;
    }
    EXTI->PR1 = EXTI_PR1_PIF19;
}

Дальше: раздел 12б «Самопроверка прошивки: ОЗУ, Flash, такты и ядро во время работы» — марш ОЗУ, CRC образа против эталона, проверка частоты, регистров ядра и стека.

Открыть в интерактивном справочнике