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

12. Служебные блоки: сторожевые таймеры, CRC, RTC

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

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

Оригинал Перевод
independent watchdog независимый сторожевой таймер
window watchdog оконный сторожевой таймер
to refresh / kick / feed перезагрузить счётчик, «покормить»
reload value значение перезагрузки
reset flag признак причины сброса
cyclic redundancy check циклический избыточный код
backup domain резервный домен
prescaler предделитель
refresh window окно, в котором разрешено кормление
early wakeup interrupt (EWI) раннее прерывание перед сбросом
reset pulse on NRST импульс сброса на выводе NRST
alarm будильник часов
alarm mask маска будильника: поле не сравнивается
binary-coded decimal (BCD) двоично-десятичный код
write protection key ключ снятия защиты записи
configuration mode (CNF) режим настройки регистров RTC

12.1 Зачем сторожевой таймер

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

Сторожевой таймер — счётчик, который непрерывно считает вниз и при достижении нуля перезагружает камень. Программа обязана регулярно его сбрасывать («кормить»). Перестала кормить — значит зависла, и перезапуск произойдёт сам.

У STM32 таких таймеров два, и они решают разные задачи.


12.2 IWDG: независимый

Тактируется от собственного генератора LSI и работает, даже если остановилась вся остальная тактовая система. Именно это делает его надёжным: отказ PLL или кварца его не касается.

#define IWDG_KEY_RELOAD   0xAAAAu   /* перезагрузить счётчик */
#define IWDG_KEY_ENABLE   0xCCCCu   /* запустить */
#define IWDG_KEY_UNLOCK   0x5555u   /* открыть доступ к настройкам */

void iwdg_start_2s(void)
{
    IWDG->KR = IWDG_KEY_ENABLE;      /* пошёл; заодно включает LSI */
    IWDG->KR = IWDG_KEY_UNLOCK;      /* снять защиту записи */
    IWDG->PR = IWDG_PR_PR_2;         /* 100b -> делитель 64 */
    IWDG->RLR = 1250u;               /* значение перезагрузки */

    while (IWDG->SR) { }             /* дождаться применения настроек — нужен идущий LSI */

    IWDG->KR = IWDG_KEY_RELOAD;
}

void iwdg_feed(void)
{
    IWDG->KR = IWDG_KEY_RELOAD;
}

Порядок важен. Новые PR и RLR доходят до счётчика за несколько тактов LSI, и флаги в SR снимаются только при идущем генераторе. Ключ 0xCCCC включает LSI сам, поэтому сторожа запускают первым: если ждать SR раньше, а LSI до этого никто не включал, цикл не кончится никогда. До конца настройки сторож считает с тем, что стоит после сброса, — делитель 4 и 4095, у F1 это 4096 × 4 / 40000 ≈ 0,4 секунды; на запись двух регистров этого хватает с запасом.

Расчёт таймаута:

таймаут = (RLR + 1) × предделитель / частота LSI
        = (1250 + 1) × 64 / 40000 ≈ 2 секунды

Предделитель задаётся тремя битами: 000 — деление на 4, каждый следующий шаг удваивает, до 256. Счётчик перезагрузки 12-битный, максимум 4095. Отсюда предельный таймаут: 4096 × 256 / 40000 ≈ 26 секунд.

Частота LSI зависит от семейства. У F1 номинал 40 кГц, у F4 и F7 — 32 кГц: тот же код даёт там (1250 + 1) × 64 / 32000 ≈ 2,5 секунды вместо двух, а предел — 4096 × 256 / 32000 ≈ 32,8 секунды против 26,2. И считать надо не по номиналу: если генератор окажется на четверть быстрее, двухсекундный таймаут превращается в 1,6 секунды — кормить сторожа надо исходя из самого короткого таймаута.

Как это выглядит во времени — счётчик сторожа, кормление и сброс после зависания:

Сторож IWDG: счётчик от 1250 вниз, кормление раз в 0,5 с, сброс через 2 с после последнего

⚠️ LSI неточен. Его частота гуляет в пределах десятков процентов в зависимости от температуры и экземпляра кристалла. Считать таймаут впритык нельзя: если по расчёту получается 2 секунды, кормить надо хотя бы раз в полсекунды. Точные границы разброса — в datasheet, раздел электрических характеристик.

⚠️ Запущенный IWDG не выключается. Ни программно, ни как-либо ещё — только сбросом камня. Это сделано намеренно: сбойный код не должен иметь возможности отключить сторожа.


12.3 WWDG: оконный

Второй сторож устроен хитрее. Он ругается не только на позднее кормление, но и на слишком раннее: есть окно, внутри которого счётчик разрешено обновлять, а вне его — сброс.

Зачем так: если программа сорвалась в цикл, который случайно проходит мимо вызова «покормить» чаще, чем надо, обычный сторож этого не заметит. Оконный — заметит, потому что нарушен ожидаемый ритм.

Тактируется WWDG от шины APB1, поэтому останавливается вместе с ней и годится скорее для контроля временных характеристик программы, чем для защиты от полного зависания. На практике чаще берут IWDG, а оконный применяют там, где важно доказать соблюдение ритма — например, в изделиях, проходящих сертификацию по функциональной безопасности.

Как считать. Счётчик 7-битный и тактируется от PCLK1 через делитель 4096 и ещё 2^WDGTB. Сброс происходит, когда падает бит T6, то есть счётчик переходит с 0x40 на 0x3F, либо когда кормят, пока счётчик выше окна W. Запускают с 0x7F: до сброса 64 тика.

F103, PCLK1 36 МГц F407, PCLK1 42 МГц
тик при WDGTB = 3 (÷8) 4096 × 8 / 36 МГц = 910 мкс 4096 × 8 / 42 МГц = 780 мкс
с 0x7F до сброса — 64 тика 58,3 мс 49,9 мс
окно W = 0x50 открывается через 47 тиков 42,8 мс 36,7 мс

Кормить надо между этими границами: на F103 — от 42,8 до 58,3 мс после прошлого раза, на F407 — от 36,7 до 49,9 мс. Ритм 45 мс подходит обоим.

Окно WWDG на F103 и F407: рано — сброс, окно, поздно — сброс; кормление раз в 30, 45, 50 и 60 мс

#define WWDG_T0 0x7Fu                   /* с чего считать: до сброса 64 тика */
#define WWDG_W  0x50u                   /* кормить можно, когда счётчик ≤ 0x50 */

void wwdg_start(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_WWDGEN;
    WWDG->CFR = WWDG_CFR_WDGTB | WWDG_W;    /* PCLK1 / 4096 / 8, окно 0x50 */
    WWDG->CR  = WWDG_CR_WDGA | WWDG_T0;     /* пуск: до сброса 64 тика */
}

void wwdg_feed(void)                        /* только когда счётчик уже ≤ WWDG_W */
{
    WWDG->CR = WWDG_T0;
}

Эти две функции прогнаны на компьютере против модели счётчика. Делитель в модели идёт непрерывно и кормлением не сбрасывается — как в железе, — поэтому границы окна плавают на один тик.

Кормим раз в F103 F407
30 мс сброс «рано» на первом кормлении, 30,000 мс сброс «рано», 30,000 мс
45 мс работает работает
50 мс работает сброс «поздно», 49,932 мс
60 мс сброс «поздно», 58,254 мс сброс «поздно», 49,932 мс

Одни и те же 0x7F и 0x50 на двух сериях дают разные окна: тик зависит от частоты шины.

⚠️ Счётчик 0x40 — это уже край. Запуск или кормление значением 0x40 оставляет до сброса один тик: модель сбросила камень через 0,910 мс. То же с окном W = 0x40 — кормить разрешено лишь в течение последнего тика. Запускают с 0x7F, окно берут заметно ниже.

Раннее предупреждение. Бит EWI в CFR даёт прерывание, когда счётчик доходит до 0x40, — за один тик до сброса. Кормить из него не стоит (12.7), а вот сохранить след того, что делала программа, — самое место.


12.4 Как узнать, что был сброс от сторожа

После перезапуска полезно понять его причину: включили питание, нажали кнопку или сработал сторож. Признаки лежат в RCC->CSR:

uint8_t reset_was_watchdog(void)
{
    uint8_t flag = (RCC->CSR & RCC_CSR_IWDGRSTF) ? 1u : 0u;
    RCC->CSR |= RCC_CSR_RMVF;        /* сбросить признаки причин сброса */
    return flag;
}

Флаги накапливаются: кнопка, сторож и программный сброс их не трогают, очищают их только сброс по питанию и бит RMVF. Если флаги не очистить, после следующего нажатия кнопки или программного сброса вы увидите и старый IWDGRSTF и решите, что программа снова зависла.

Практический приём: при обнаружении сброса от сторожа записать это в журнал или резервный регистр (раздел 10). Изделие, которое молча перезагружается раз в сутки, иначе никогда не будет замечено.

Весь список причин. Внутренний сброс — от сторожа, программный, при выходе из сна — тоже дёргает вывод NRST, поэтому флаг PINRSTF стоит почти при любом сбросе. Кто проверит его первым, всё сочтёт нажатием кнопки. Первым проверяют питание, а PINRSTF — последним:

/* внутренний сброс тоже дёргает вывод NRST, поэтому PINRSTF стоит почти
 * при любом сбросе — его проверяют последним */
rst_t reset_cause(uint32_t csr)
{
    if (csr & RCC_CSR_PORRSTF)
        return RST_POWER;
#ifdef RCC_CSR_BORRSTF
    if (csr & RCC_CSR_BORRSTF)
        return RST_BROWNOUT;
#endif
    if (csr & RCC_CSR_IWDGRSTF)
        return RST_IWDG;
    if (csr & RCC_CSR_WWDGRSTF)
        return RST_WWDG;
    if (csr & RCC_CSR_SFTRSTF)
        return RST_SOFT;
    if (csr & RCC_CSR_LPWRRSTF)
        return RST_LOWPOWER;
    if (csr & RCC_CSR_PINRSTF)
        return RST_PIN;
    return RST_UNKNOWN;
}
Событие Флаги в RCC->CSR Ответ
включили питание PORRSTF, PINRSTF (у F4 ещё BORRSTF) питание
нажали кнопку сброса PINRSTF кнопка
сработал IWDG IWDGRSTF, PINRSTF IWDG
сработал WWDG WWDGRSTF, PINRSTF WWDG
NVIC_SystemReset() SFTRSTF, PINRSTF программный

Порядок «кнопка первой» назвал бы кнопкой четыре случая из пяти. У F4 есть ещё флаг просадки питания BORRSTF; при включении он стоит вместе с PORRSTF, поэтому проверяется вторым.

⚠️ Отладка со сторожем. Останов на точке прерывания замораживает программу, но не сторожа — через пару секунд плата перезагрузится прямо из-под отладчика, и это выглядит крайне загадочно. Лечится битами в DBGMCU->CR: они останавливают счётчик, пока ядро стоит на паузе.

На F4 те же биты разнесены по DBGMCU->APB1FZ и DBGMCU->APB2FZ, и HAL закрывает их макросами __HAL_DBGMCU_FREEZE_IWDG() и подобными (раздел 35.5).

c DBGMCU->CR |= DBGMCU_CR_DBG_IWDG_STOP | DBGMCU_CR_DBG_WWDG_STOP;


12.5 Аппаратный CRC

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

uint32_t crc32_words(const uint32_t *data, uint32_t count)
{
    RCC->AHBENR |= RCC_AHBENR_CRCEN;    /* F1; у F4 — RCC->AHB1ENR |= RCC_AHB1ENR_CRCEN */

    CRC->CR = CRC_CR_RESET;          /* начать новый расчёт */
    for (uint32_t i = 0; i < count; i++) {
        CRC->DR = data[i];           /* записал слово — результат уже готов */
    }
    return CRC->DR;                  /* чтение того же регистра даёт результат */
}

Регистр один и тот же: запись подаёт данные, чтение забирает накопленный результат.

⚠️ У F1 и F4 вариант фиксирован — CRC-32/MPEG-2: полином 0x04C11DB7, начальное значение 0xFFFFFFFF, слово идёт старшим битом вперёд, без отражений и без завершающей инверсии. С crc32 из zlib и FCS Ethernet он не совпадёт, даже если выровнять байты. Их вариант на этом блоке тоже получают: каждое слово подают через __RBIT, а результат берут как ~__RBIT(CRC->DR). Все шесть параметров CRC и таблица вариантов — раздел 43.1; если сумму проверяет другая сторона, вариант согласуют заранее.

Свой полином и ширину задают у F3, F7, G0, G4, L0, L4, H7 и более новых семейств, а из F0 — только у F071, F072, F078 и F09x; у остальных F0 полином тот же, настраиваются лишь начальное значение и отражения. Если протокол требует конкретного варианта CRC-16, на F1 его придётся считать программно.

⚠️ Данные подаются словами. Массив байтов надо либо дополнять до кратности четырём, либо учитывать порядок байтов — иначе результат не сойдётся с тем, что посчитала другая сторона.


12.6 RTC: часы реального времени

Живут в резервном домене (раздел 10) и продолжают идти от батарейки, когда всё остальное обесточено. Тактируются обычно от часового кварца LSE на 32 768 Гц — число не случайное: это 2¹⁵, и делением пополам пятнадцать раз получается ровно одна секунда.

Разница между поколениями существенная:

STM32F1 STM32F4
что считает 32-битный счётчик секунд полноценный календарь
формат число секунд, дату считаете сами часы, минуты, дата, день недели в BCD
будильник одно значение счётчика два, A и B, с масками
дополнительно — периодическое пробуждение, метки времени

У F1 придётся самому переводить счётчик секунд в дату — обычно берут готовую функцию преобразования из библиотеки времени. У F4 всё считает сам блок.

Общее для обоих: доступ к резервному домену нужно открыть битом DBP, а после записи в регистры RTC — дождаться завершения операции. Синхронизация занимает такты медленного генератора, то есть десятки микросекунд, и без ожидания вы прочитаете старое значение.

Практическая мелочь: после подачи питания регистры RTC могут содержать мусор, если батарейка не стояла. Признак «часы уже настроены» обычно кладут в резервный регистр — записали туда условное число при первой настройке, и при старте проверяете его наличие.

Будильник: у F1 — число, у F4 — маски

У F1 будильник — такое же 32-битное число секунд, как и сам счётчик: прерывание приходит, когда CNT становится равен ALR. «Разбудить через минуту» — значит прочитать счётчик и записать в ALRH:ALRL сумму. Переход через 2³² сумма делает сама: 0xFFFFFFF0 + 32 = 0x00000010, и будильник сработает вовремя. Такого переполнения ждать 136 лет, но и проверять тут нечего — беззнаковая арифметика права всегда.

Запись в ALR у F1 идёт через режим настройки, как и в сам счётчик: дождаться RTOFF, поставить CNF, записать, снять CNF — только в этот момент запись уходит в резервный домен — и снова дождаться RTOFF (rtc_now — чтение счётчика, о нём ниже):

void rtc_alarm_in(uint32_t seconds)
{
    PWR->CR |= PWR_CR_DBP;
    RTC->CRL &= ~RTC_CRL_RSF;               /* после сброса или Stop — дождаться синхронизации */
    while (!(RTC->CRL & RTC_CRL_RSF)) { }
    uint32_t at = f1_alarm_at(rtc_now(), seconds);
    while (!(RTC->CRL & RTC_CRL_RTOFF)) { } /* прошлая запись закончилась */
    RTC->CRL |= RTC_CRL_CNF;                /* режим настройки */
    RTC->ALRH = (uint16_t)(at >> 16);
    RTC->ALRL = (uint16_t)at;
    RTC->CRL &= ~(RTC_CRL_CNF | RTC_CRL_ALRF);  /* выход — запись пошла; флаг сбросить */
    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
    RTC->CRH |= RTC_CRH_ALRIE;
    EXTI->IMR |= EXTI_IMR_MR17;
    EXTI->RTSR |= EXTI_RTSR_TR17;
    NVIC_EnableIRQ(RTCAlarm_IRQn);
}

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

У F4 будильников два, A и B, и сравниваются они не с числом, а с календарём. Каждое поле — секунды, минуты, часы, дату — можно исключить из сравнения своим битом маски: MSK1 … MSK4, биты 7, 15, 23 и 31 регистра ALRMAR. Отсюда расписания без всякой арифметики:

Расписание Не сравнивать ALRMAR
каждую минуту в :30 дату, часы, минуты 0x80808030
каждый час в :00:00 дату, часы 0x80800000
каждый день в 07:15:00 дату 0x80071500

Собрать ALRMAR для своего расписания и времени — вкладка «RTC» в калькуляторах приложения; там же для F1 — будильник-число с переходом через 2³² и счётчик секунд как дата.

Значения полей — в двоично-десятичном коде: 15 минут записываются как 0x15, а не как 0x0F. Регистры RTC у F4 закрыты от случайной записи: сначала два ключа в WPR, потом выключить будильник и дождаться ALRAWF — без этого запись в ALRMAR не проходит:

uint32_t alrmar(uint32_t masks, uint32_t h, uint32_t m, uint32_t s)
{
    return masks | (bcd(h) << 16) | (bcd(m) << 8) | bcd(s);
}

void rtc_alarm_a(uint32_t value)
{
    PWR->CR |= PWR_CR_DBP;                  /* доступ к резервному домену (раздел 10) */
    RTC->WPR = 0xCAu;                       /* снять защиту записи — два ключа подряд */
    RTC->WPR = 0x53u;
    RTC->CR &= ~(RTC_CR_ALRAE | RTC_CR_ALRAIE);
    while (!(RTC->ISR & RTC_ISR_ALRAWF)) { }    /* ALRMAR можно менять */
    RTC->ALRMAR = value;
    RTC->ISR = ~(RTC_ISR_ALRAF | RTC_ISR_INIT) | (RTC->ISR & RTC_ISR_INIT);   /* сбросить флаг, INIT не трогать */
    RTC->CR |= RTC_CR_ALRAE | RTC_CR_ALRAIE;
    RTC->WPR = 0xFFu;                       /* защиту обратно */
    EXTI->IMR |= EXTI_IMR_MR17;             /* будильник — линия EXTI 17 */
    EXTI->RTSR |= EXTI_RTSR_TR17;
    NVIC_EnableIRQ(RTC_Alarm_IRQn);
}

Флаги в ISR сбрасываются записью нуля, а единица их не трогает, — поэтому пишут «всё единицами, кроме ALRAF». Бит INIT в том же регистре обычный: единица в нём останавливает календарь, поэтому его значение сохраняют.

Общее у обоих — линия EXTI 17. Будильник выведен на неё, и именно через неё он будит камень из Stop (раздел 10.3): маска и передний фронт настраиваются так же, как для вывода. В обработчике сбрасывают два флага:

void RTC_Alarm_IRQHandler(void)
{
    if (RTC->ISR & RTC_ISR_ALRAF) {
        RTC->ISR = ~(RTC_ISR_ALRAF | RTC_ISR_INIT) | (RTC->ISR & RTC_ISR_INIT);
        alarms++;
    }
    EXTI->PR = EXTI_PR_PR17;                /* не сбросить — обработчик вызывается снова и снова */
}

Не сброшен флаг самого RTC (ALRAF, у F1 — ALRF) — сигнал на линии 17 так и остаётся поднятым, нового фронта не будет, и будильник сработает один раз. Не сброшен флаг линии в EXTI->PR — обработчик будет вызываться без конца. Ключи WPR в обработчике не нужны: флаг будильника защита записи не прикрывает.

Счётчик F1 читают трижды

32-битный счётчик F1 читается двумя 16-битными половинами, а младшая раз в 65 536 с — каждые 18,2 часа — переходит через 0xFFFF. Если переход пришёлся между двумя чтениями, результат врёт на 18 часов с лишним. Модель на компьютере ставит счётчик на 0x0000FFFF и даёт ему тикнуть сразу после первого чтения:

Как читали Получили На самом деле Ошибка
старшую, потом младшую 0x00000000 0x00010000 на 65 536 с назад
младшую, потом старшую 0x0001FFFF 0x00010000 на 65 535 с вперёд
старшую, младшую и старшую ещё раз 0x00010000 0x00010000 нет

Лечение — перечитать старшую половину и повторить, если она изменилась (на компьютере вместо регистров — модель):

#define CNT_HI() RTC->CNTH
#define CNT_LO() RTC->CNTL
uint32_t rtc_now(void)
{
    uint16_t hi, lo;
    do {
        hi = CNT_HI();
        lo = CNT_LO();
    } while (hi != CNT_HI());
    return ((uint32_t)hi << 16) | lo;
}

Обычно это три чтения; шесть — только в тот редкий раз, когда счётчик тикнул между ними. Ошибка такого рода опасна именно редкостью: на столе её не увидеть, а прибор в поле раз в сутки с небольшим получает неверное время.


12.7 Грабли

Сторож срабатывает при отладке. Не выставлены биты DBGMCU->CR.

Таймаут посчитан впритык. LSI гуляет на десятки процентов — берите двойной запас.

Программа зависла в настройке сторожа. SR ждут до ключа 0xCCCC, а LSI никто не включал — флаги не снимутся. Сначала 0xCCCC (12.2).

Кормление из прерывания. Соблазнительно повесить iwdg_feed на таймер — и тем самым обессмыслить всю затею: зависшая основная программа продолжит «кормиться» из работающего прерывания. Кормить надо из главного цикла, и лучше — с проверкой, что все задачи отчитались.

Флаги причин сброса не очищены. Признак живёт до выключения питания и вводит в заблуждение после следующих сбросов кнопкой или программой.

Причина сброса всегда «кнопка». PINRSTF проверен первым, а он стоит почти при любом сбросе (12.4).

WWDG сбрасывает сразу после запуска. Счётчик запущен со значением 0x40 или окно задано равным ему — до сброса один тик. Запускать с 0x7F.

WWDG сбрасывает при кормлении. Кормят раньше, чем счётчик опустился до окна: на F103 с W = 0x50 — не раньше чем через 42,8 мс.

Настройка WWDG не переносится между сериями. Тик зависит от PCLK1: одни и те же 0x7F и 0x50 дают на F103 окно 42,8–58,3 мс, на F407 — 36,7–49,9 мс.

CRC не сброшен перед новым расчётом. Забыли CRC_CR_RESET — получите продолжение предыдущей суммы.

Часы «сбрасываются» при перезагрузке. Значит инициализация RTC выполняется безусловно при каждом старте. Она должна выполняться один раз, а признак настройки храниться в резервном регистре.

Будильник срабатывает один раз. В обработчике не сброшен флаг самого RTC — ALRAF у F4, ALRF у F1: сигнал на линии EXTI 17 остаётся поднятым, и нового фронта не будет.

Обработчик будильника вызывается без конца. Не сброшен флаг линии — EXTI->PR = EXTI_PR_PR17.

Время на F1 изредка прыгает на 18 часов. Счётчик прочитан двумя половинами без повтора, и младшая перешла через 0xFFFF между чтениями — ошибка 65 536 с. Старшую половину перечитывают (12.6).

Будильник F4 не записывается. Не сняты ключи WPR или не дождались ALRAWF — запись в ALRMAR не проходит.


12.8 Итоги

Дальше: раздел 12а «RTC нового образца: доли секунды, калибровка, пробуждение, TAMP» — у G4: доли секунды, подстройка хода в ppm, таймер пробуждения и резервные регистры в блоке TAMP.

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