Перейти к содержанию

Сторожевой таймер: настройка и стратегии сброса

Сторожевой таймер перезапускает микроконтроллер, если программа перестала его вовремя сбрасывать. Пользы от него ровно столько, сколько продумано место сброса: таймер, который сбрасывают в прерывании от системного тика, не заметит зависшего основного цикла. Разбираем WDT ATmega328P, независимый IWDG и оконный WWDG в STM32F103, расчёт таймаута с учётом разброса частоты генератора, схему контроля нескольких задач, чтение причины сброса, работу с отладчиком и загрузчиком.

Обновлено Редакция GAW

Сторожевой таймер (watchdog) — счётчик с собственным тактированием, который перезапускает микроконтроллер, если программа не сбросила его за заданное время. Он страхует от того, от чего не спасают никакие проверки в коде: бесконечного цикла из-за ошибки, ожидания флага, который никогда не взведётся, взаимной блокировки, перехода в мусор после порчи стека, «защёлкивания» периферии от помехи. Проблема в том, что сторожевой таймер видит только одно — сбрасывают его или нет. Если сброс стоит в неудачном месте, таймер будет исправно сбрасываться и в зависшей программе.

Коротко

  • ATmega328P: WDT от генератора 128 кГц, таймаут 16 мс … 8 с, режимы «сброс», «прерывание» и «прерывание, затем сброс»; настройка — через последовательность с WDCE за 4 такта.
  • IWDG (STM32F1): от LSI 40 кГц (по даташиту 30–60 кГц), 12-битный счётчик и делитель /4…/256; ключи 0xCCCC — пуск, 0xAAAA — сброс, 0x5555 — доступ к настройкам; выключить нельзя.
  • WWDG (STM32F1): от PCLK1 / 4096 / 2^WDGTB, сброс и при опоздании, и при слишком раннем обновлении — до входа счётчика в окно.
  • Где сбрасывать: в одном месте основного цикла после того, как все задачи отметились; не в прерывании таймера.
  • Причина сброса: WDRF в MCUSR, IWDGRSTF и WWDGRSTF в RCC_CSR; читают в начале программы и обнуляют.

Что ловит сторожевой таймер, а что нет

Таймер срабатывает, когда программа перестала выполнять участок со сбросом. Это зависание в цикле ожидания, переход по испорченному адресу, блокировка двух задач, бесконечная рекурсия, остановка в обработчике HardFault или в Default_Handler. Но таймер не заметит, что программа работает неправильно: шлёт неверные данные, пропустила событие, застряла в состоянии автомата, из которого нет выхода по таймауту, если при этом основной цикл крутится и сбрасывает таймер. Поэтому сторожевой таймер — последняя линия обороны, а не замена таймаутам в каждом ожидании.

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

ATmega328P: WDT

Сторожевой таймер ATmega328P считает такты отдельного генератора 128 кГц и по истечении заданного числа тактов вызывает прерывание, сброс или и то и другое (DS40002061B, 11.8). Период задают биты WDP3:0 регистра WDTCSR:

WDP3:0Такты генератораТипичный таймаут при 5 ВКонстанта avr-libc
0000204816 мсWDTO_15MS
0001409632 мсWDTO_30MS
0010819264 мсWDTO_60MS
001116 3840,125 сWDTO_120MS
010032 7680,25 сWDTO_250MS
010165 5360,5 сWDTO_500MS
0110131 0721,0 сWDTO_1S
0111262 1442,0 сWDTO_2S
1000524 2884,0 сWDTO_4S
10011 048 5768,0 сWDTO_8S

Значения в даташите названы типичными при 5 В: генератор 128 кГц не кварцевый, и реальный таймаут отличается. Поэтому период выбирают с запасом в несколько раз от самого долгого прохода основного цикла. Названия констант avr-libc (15 мс, 30 мс, 120 мс) исторически округлены иначе, чем числа в таблице даташита, — это одни и те же коды WDP. Подобрать период и получить готовый код можно в инструменте «Сторожевой таймер AVR».

Режим задают биты WDE и WDIE (табл. 11-1):

WDEWDIEРежимПо истечении
00остановлен—
01прерываниевызывается WDT_vect
10сброссброс микроконтроллера
11прерывание, затем сбросWDT_vect, а следующий таймаут — сброс

Если запрограммирован фьюз WDTON, таймер всегда в режиме сброса, и программа не может его выключить. Это защищает от программы, которая по ошибке сама выключит таймер.

Защищённая последовательность

Чтобы случайная запись не выключила таймер, изменение WDE и периода требует двух записей подряд: сначала WDCE и WDE в одной записи, затем — не позже чем через четыре такта — новые значения со сброшенным WDCE (11.8.2). Проще всего доверить это макросам avr-libc: wdt_enable() и wdt_disable() написаны встроенным ассемблером, запрещают прерывания на время записи и гарантированно укладываются в окно:

#include <avr/io.h>
#include <avr/wdt.h>

int main(void)
{
    wdt_enable(WDTO_1S);          /* timed sequence inside, interrupts handled */
    for (;;) {
        do_work();
        wdt_reset();              /* WDR instruction */
    }
}

Перед уменьшением периода таймер сбрасывают: если счётчик уже насчитал больше нового предела, смена WDP вызовет немедленный сброс (11.8.2).

Режим «прерывание, затем сброс»

В этом режиме первый таймаут вызывает WDT_vect, а аппаратура при входе в обработчик сбрасывает WDIE — таймер переходит в режим сброса. Если программа не сбросит его до следующего таймаута, произойдёт сброс (11.9.2). Обработчик успевает сохранить диагностику — например, адрес, где застряла программа, — в переменную из секции .noinit:

#include <avr/interrupt.h>
#include <avr/wdt.h>

volatile uint16_t wdt_last_state __attribute__((section(".noinit")));
extern volatile uint16_t app_state;   /* updated by the main loop */

ISR(WDT_vect)
{
    wdt_last_state = app_state;       /* survives the reset that follows */
    /* WDIE was cleared by hardware: the next time-out resets the MCU */
}

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

Таймер остаётся включённым после сброса

После сброса от сторожевого таймера установлен флаг WDRF, а пока он установлен, бит WDE принудительно равен единице (11.9.2). Таймер продолжает работать — по документации avr-libc с самым коротким периодом около 15 мс. Если программа не готова его сбрасывать с самого начала, она будет перезапускаться бесконечно. Поэтому в начале программы флаг WDRF сбрасывают, а таймер выключают — и делают это как можно раньше, в секции .init3 до инициализации переменных. Пример такого кода и порядок секций запуска — в статье о коде запуска.

STM32F103: IWDG

Независимый сторожевой таймер (IWDG) тактируется от внутреннего генератора LSI и поэтому продолжает работать, даже если отказал кварц или PLL, а также в режимах Stop и Standby (RM0008, 19.1–19.2). Он состоит из 8-битного делителя и 12-битного счётчика, который считает вниз от значения перезагрузки и при достижении нуля сбрасывает микроконтроллер.

Управление — ключами в регистре IWDG_KR (19.3):

КлючДействие
0xCCCCзапустить IWDG (если он не запущен аппаратно битами опций)
0xAAAAперезагрузить счётчик значением RLR — это и есть «сброс» таймера
0x5555открыть запись в IWDG_PR и IWDG_RLR; любой другой ключ закрывает её снова

Таймаут: t = 4 · 2^PR · (RL + 1) / f_LSI, где PR — код делителя от 0 (/4) до 6 (/256), RL — значение перезагрузки от 0 до 0xFFF. При номинальной частоте 40 кГц получаются значения таблицы 96 из RM0008: от 0,1 мс (/4, RL = 0) до 409,6 мс (/4, RL = 0xFFF) и до 26 214,4 мс (/256, RL = 0xFFF).

Разброс частоты LSI

LSI — RC-генератор, и у STM32F103 его частота по даташиту от 30 до 60 кГц при типичных 40 кГц (DS5319, табл. 25). Таймаут меняется в те же два раза. Считать надо от обоих краёв:

  • минимальный таймаут (при 60 кГц) должен быть больше самого долгого интервала между сбросами таймера в исправной программе — иначе исправная программа будет перезапускаться;
  • максимальный таймаут (при 30 кГц) определяет, сколько устройство проработает в зависшем состоянии.

Пример. Основной цикл в худшем случае сбрасывает таймер раз в 400 мс (включая стирание страницы флеш-памяти — до 40 мс по DS5319, табл. 28), нужен запас не меньше двух раз — минимум 800 мс при 60 кГц. Делитель /32 (PR = 3): тик 32 / 60 000 = 0,533 мс при 60 кГц. RL + 1 = 800 / 0,533 = 1500, RL = 1499. При 40 кГц таймаут будет 32 · 1500 / 40 000 = 1,2 с, при 30 кГц — 1,6 с. Если нужен более точный таймаут, частоту LSI измеряют таймером относительно кварца (RM0008 ссылается на это в 19.3 и 7.2.5), но запас на дрейф всё равно оставляют. Перебрать делители и значения перезагрузки можно в инструменте «Сторожевой таймер STM32».

#include "stm32f1xx.h"

void iwdg_init(void)
{
    IWDG->KR = 0xCCCCu;                 /* start; cannot be stopped until reset */
    IWDG->KR = 0x5555u;                 /* unlock PR and RLR */
    IWDG->PR = 3u;                      /* /32 */
    IWDG->RLR = 1499u;                  /* 1.2 s at 40 kHz, 0.8..1.6 s over LSI spread */
    while (IWDG->SR != 0u) { }          /* PVU and RVU: values reach the IWDG domain */
    IWDG->KR = 0xAAAAu;                 /* reload the counter with the new RLR */
}

static inline void iwdg_kick(void)
{
    IWDG->KR = 0xAAAAu;
}

Порядок важен. Ключ 0xCCCC запускает счётчик со значения после сброса 0xFFF (19.3), а не с того, что записано в RLR; новое значение попадёт в счётчик только при следующем ключе 0xAAAA. Флаги PVU и RVU в IWDG_SR показывают, что делитель и значение перезагрузки ещё переносятся в домен питания IWDG: на это уходит до пяти периодов LSI (19.4.4). RM0008 разрешает после записи PR и RLR не ждать этих флагов и продолжать работу — запись всё равно завершится. Но значение RLR в домене IWDG до окончания переноса ещё старое (19.4.3), и перезагрузка 0xAAAA, отправленная раньше, по нашему выводу может загрузить в счётчик именно его (прямо RM0008 этого не говорит), поэтому перед ней флаги надёжнее дождаться — так же устроена функция HAL_IWDG_Init() в драйвере ST: пуск, открытие записи, PR и RLR, ожидание нулевого IWDG_SR, перезагрузка. Без этой перезагрузки первый период отсчитывался бы от 0xFFF, а после переноса делителя — уже с новым делителем: при /32 и 40 кГц это до 3,3 с вместо 1,2 с. Регистры IWDG допускают только 16- и 32-битные обращения (19.4).

Если в битах опций включён аппаратный режим, IWDG запускается сам при подаче питания (19.3.1), и программа — а ещё раньше загрузчик — должна начать его сбрасывать до истечения таймаута по умолчанию: с RL = 0xFFF и делителем /4 это 409,6 мс при 40 кГц.

STM32F103: WWDG

Оконный сторожевой таймер (WWDG) тактируется от шины APB1 через делитель 4096 и дополнительный делитель 2^WDGTB. Его 7-битный счётчик T[6:0] считает вниз, и сброс происходит в двух случаях (RM0008, 20.3):

  1. счётчик перешёл из 0x40 в 0x3F (бит T6 обнулился) — программа опоздала;
  2. программа записала новое значение, когда счётчик был больше значения окна W[6:0] — программа поторопилась.

Второй случай отличает WWDG от обычных сторожевых таймеров. Он ловит ситуацию, когда цикл, который должен занимать 30–50 мс, выполняется за 2 мс — например, потому что пропущена ветка с ожиданием или датчик перестал отвечать и функция чтения сразу возвращает ошибку.

Время до сброса: t = t_PCLK1 · 4096 · 2^WDGTB · (T[5:0] + 1) (20.4). В RM0008 (20.4) для примера PCLK1 = 24 МГц, WDGTB = 3, T[5:0] = 63 указан ответ 21,85 мс, но это арифметическая ошибка документа: по той же формуле (1 / 24 МГц) · 4096 · 8 · 64 = 87,38 мс. Правильное значение подтверждает и таблица 98: при 36 МГц и тех же WDGTB и T максимум 58,25 мс, а на частоте в полтора раза ниже время в полтора раза больше. При максимальной частоте APB1 36 МГц (RM0008, 3.1) диапазон — от 113 мкс до 58,25 мс (табл. 98).

Пример окна. PCLK1 = 36 МГц, WDGTB = 3: один тик счётчика — 4096 · 8 / 36 000 000 = 0,910 мс. Записываем T = 0x7F (127): до перехода через 0x3F остаётся 127 − 63 = 64 тика = 58,25 мс. Окно W = 0x5F (95): обновлять можно, когда счётчик меньше 95, то есть не раньше чем через 127 − 95 = 32 тика = 29,1 мс. Итог: программа должна обновлять WWDG через 29,1–58,25 мс после предыдущего обновления.

#include "stm32f1xx.h"

#define WWDG_T  0x7Fu   /* counter reload, T6 must be 1 */
#define WWDG_W  0x5Fu   /* window: refresh only when the counter is below this */

void wwdg_init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_WWDGEN;
    WWDG->CFR = (3u << WWDG_CFR_WDGTB_Pos) | WWDG_W;   /* PCLK1/4096/8 */
    WWDG->CR  = WWDG_CR_WDGA | WWDG_T;                 /* enable; cannot be disabled */
}

static inline void wwdg_refresh(void)
{
    WWDG->CR = WWDG_T;            /* always write T6 = 1, else immediate reset */
}

Значение в WWDG_CR всегда записывают с установленным T6, то есть от 0x40 до 0x7F вместе с WDGA — от 0xC0 до 0xFF, иначе сброс произойдёт сразу (20.4). Бит WDGA, раз установленный, снимает только сброс.

Прерывание EWI

Если в WWDG_CFR установлен бит EWI, при значении счётчика 0x40 вызывается прерывание WWDG_IRQHandler — за один тик до сброса (20.3). Это последний шанс записать диагностику: в примере выше у обработчика около 0,9 мс. Обработчик может и обновить счётчик, чтобы сброса не было, — RM0008 описывает такой вариант для мягкой деградации, — но тогда WWDG перестаёт быть защитой: если зависла основная программа, а прерывания работают, обработчик будет продлевать ей жизнь бесконечно. Флаг EWIF сбрасывают записью нуля в WWDG_SR.

Причина сброса

Флаги причины сброса сохраняются при системном сбросе и сбрасываются только явно (RM0008, 7.1.1 и 7.3.10). Читать их нужно в начале программы, сохранить и тут же сбросить, иначе после следующего сброса по другой причине будут установлены оба флага.

ПричинаSTM32F1, RCC_CSRATmega328P, MCUSR
сторожевой таймерIWDGRSTF, WWDGRSTFWDRF
включение питанияPORRSTFPORF
вывод сбросаPINRSTFEXTRF
снижение питания— (входит в POR/PDR)BORF
программный сбросSFTRSTF—
сброс при входе в Stop или StandbyLPWRRSTF—
как сбросить флагизаписать 1 в RMVFзаписать 0 в MCUSR
#include "stm32f1xx.h"

typedef enum { RST_POWER, RST_PIN, RST_IWDG, RST_WWDG, RST_SOFT, RST_LOWPOWER, RST_OTHER } reset_cause_t;

reset_cause_t reset_cause_read(void)
{
    uint32_t csr = RCC->CSR;
    RCC->CSR |= RCC_CSR_RMVF;                    /* clear all reset flags */

    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_PORRSTF)  return RST_POWER;
    if (csr & RCC_CSR_PINRSTF)  return RST_PIN;
    return RST_OTHER;
}

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

Сам факт сброса от таймера мало что говорит. Полезнее сохранить, где программа была: номер состояния автомата, идентификатор задачи, которая не отметилась, счётчик подряд идущих сбросов. Для этого заводят структуру в секции .noinit с контрольным словом: при включении питания в ОЗУ мусор, и контрольное слово отличает сохранённые данные от него. Если сбросов от таймера много подряд, программа может перейти в безопасный режим — без той функции, которая, вероятно, и зависает.

Где сбрасывать таймер

Плохие места:

  • В прерывании таймера или SysTick. Прерывания приходят, даже если основной цикл завис в ожидании, — сторожевой таймер не сработает никогда.
  • Внутри циклов ожидания («пока ждём флаг, сбросим таймер»). Если флаг не придёт, программа будет ждать вечно, и таймер ей в этом поможет.
  • В нескольких местах. Любое из них может оказаться в зависшем участке.
  • Сразу после включения таймера, «чтобы был». Таймер, который сбрасывают в одной точке без проверки, ловит только полное зависание этой точки.

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

#include <stdint.h>

enum { TASK_COMM = 1u << 0, TASK_SENSOR = 1u << 1, TASK_CONTROL = 1u << 2 };
#define TASKS_ALL (TASK_COMM | TASK_SENSOR | TASK_CONTROL)

static volatile uint8_t alive;            /* tasks that reported since the last kick */

void task_alive(uint8_t task)             /* called by each task at the end of its pass */
{
    alive |= task;                        /* main loop only: no ISR touches it */
}

void watchdog_service(void)               /* called once per main loop pass */
{
    if ((alive & TASKS_ALL) == TASKS_ALL) {
        alive = 0;
        iwdg_kick();
    }
}

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

#include <stdbool.h>
#include <stdint.h>

typedef struct {
    uint32_t last_ms;                     /* time of the last report */
    uint32_t limit_ms;                    /* longest allowed silence */
} task_health_t;

static task_health_t health[] = {
    { 0, 100 },                           /* communication: polled every 10 ms */
    { 0, 6000 },                          /* sensor: every 5 s */
    { 0, 50 },                            /* control loop: every 5 ms */
};

void task_report(unsigned id, uint32_t now_ms)
{
    health[id].last_ms = now_ms;
}

void watchdog_service(uint32_t now_ms)
{
    for (unsigned i = 0; i < sizeof health / sizeof health[0]; i++)
        if ((uint32_t)(now_ms - health[i].last_ms) > health[i].limit_ms)
            return;                       /* a task is stuck: let the watchdog fire */
    iwdg_kick();
}

Разность now_ms − last_ms в беззнаковом типе правильна и при переполнении счётчика миллисекунд — почему, разобрано в статье о конечных автоматах. Если задачи вызываются из одного основного цикла, обращения к health[] не требуют защиты; если task_report вызывается из обработчика прерывания, запись 32-битного значения на Cortex-M неделима, а на AVR её берут в ATOMIC_BLOCK.

В системе с RTOS ту же роль играет отдельная задача-супервизор: остальные задачи отмечаются в ней через очередь или флаги событий, а она одна сбрасывает таймер. Её приоритет обычно делают низким — если задачу с низким приоритетом не пускают к процессору более важные, это тоже отказ, который должен закончиться сбросом.

Долгие операции

Таймаут должен быть больше самой долгой операции, внутри которой нельзя сбросить таймер. Типичные кандидаты: стирание флеш-памяти (страница STM32F103 — до 40 мс, DS5319, табл. 28), ожидание запуска кварца, запись во внешнюю память, ожидание ответа модема. Варианты:

  • увеличить таймаут так, чтобы минимальный (при самом быстром LSI) превышал самую долгую операцию;
  • разбить операцию на шаги и сбрасывать таймер между ними — но только в коде, который сам ограничен по времени, а не в цикле ожидания;
  • использовать окно WWDG для быстрых проверок основного цикла и длинный IWDG — как страховку на долгих операциях.

Опасен соблазн «сбросить таймер прямо перед долгой операцией». Если операция сама зависнет, таймер сработает, — это правильно. Но если сброс поставить и внутрь ожидания её конца, защита исчезает.

Отладчик и загрузчик

На точке останова ядро стоит, а IWDG считает от LSI и через таймаут сбрасывает микроконтроллер — сеанс отладки обрывается. У STM32F1 в регистре DBGMCU_CR есть биты DBG_IWDG_STOP и DBG_WWDG_STOP: с ними счётчики останавливаются, пока ядро остановлено (RM0008, 31.16.2). Регистр сбрасывается только при включении питания, а не при системном сбросе (31.16.3), и программа может записать его сама:

#ifdef DEBUG
    DBGMCU->CR |= DBGMCU_CR_DBG_IWDG_STOP | DBGMCU_CR_DBG_WWDG_STOP;
#endif

Загрузчик и сторожевой таймер нужно продумать вместе. Запущенный IWDG останавливается только сбросом (RM0008, 5.3.4), так что после программного сброса для обновления прошивки загрузчик стартует с выключенным таймером. Иначе с аппаратным режимом в битах опций: тогда IWDG запускается сам при каждом старте, и загрузчик, который долго ждёт новую прошивку, обязан его сбрасывать. На AVR после сброса от WDT таймер остаётся включённым с коротким периодом, и загрузчик должен либо выключить его, либо сразу передать управление программе.

Типичные ошибки

  • Сброс таймера в прерывании — зависание основного цикла не обнаруживается.
  • Таймаут посчитан по типичной частоте LSI — при 60 кГц исправная программа перезапускается, при 30 кГц зависшая работает вдвое дольше ожидаемого.
  • WWDG обновляется в начале цикла без учёта окна — при быстром проходе (например, когда датчик не отвечает) сброс от «слишком раннего» обновления; это правильная реакция, но её надо предусмотреть.
  • Запись в WWDG_CR без T6 — немедленный сброс.
  • WDIE включается в обработчике WDT_vect на AVR — таймер никогда не дойдёт до сброса.
  • Флаги причины сброса не сброшены — после следующего сброса непонятно, что произошло.
  • Нет DBG_IWDG_STOP при отладке — сброс через секунду после остановки на точке.
  • Таймер не выключен в начале программы на AVR после сброса от него — бесконечный цикл перезапусков с периодом около 15 мс.
  • Таймер включён «на всякий случай» в отладочной сборке и выключен в рабочей — в поле устройство остаётся без защиты, а все сценарии с ним не проверены. Включайте одинаково в обеих сборках.

Частые вопросы

Где правильно сбрасывать сторожевой таймер?

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

Чем IWDG отличается от WWDG в STM32?

IWDG тактируется от собственного генератора LSI, работает даже при отказе основного тактирования и в режимах Stop и Standby, но LSI неточен: у STM32F103 его частота от 30 до 60 кГц. WWDG считает от шины APB1, точен, умеет ловить слишком ранний сброс (окно) и даёт прерывание EWI перед сбросом, но останавливается вместе с тактированием. Их часто используют вместе.

Как узнать, что микроконтроллер перезапустился из-за сторожевого таймера?

По флагам причины сброса: на STM32F1 — IWDGRSTF и WWDGRSTF в регистре RCC_CSR, на ATmega328P — WDRF в MCUSR. Флаги переживают системный сброс, поэтому их читают в самом начале программы, сохраняют и затем сбрасывают: на STM32 записью бита RMVF, на AVR — записью нуля в MCUSR. Иначе следующий сброс по другой причине покажет оба флага сразу.

Почему отладка останавливается сбросом от сторожевого таймера?

Когда ядро стоит на точке останова, IWDG продолжает считать от LSI и через заданное время сбрасывает микроконтроллер. У STM32F1 для этого есть биты DBG_IWDG_STOP и DBG_WWDG_STOP в регистре DBGMCU_CR: с ними счётчики замирают, пока ядро остановлено. Многие отладчики ставят их сами, а программа может записать их при запуске.

Можно ли выключить IWDG после запуска?

Нет. Ключ 0xCCCC запускает IWDG, и остановить его может только сброс; при включённом в битах опций аппаратном режиме он запускается сам при подаче питания. Можно лишь увеличить таймаут до максимума — около 26 с при номинальной частоте LSI 40 кГц — и сбрасывать его там, где программа долго ждёт. WWDG после установки WDGA тоже выключается только сбросом.

Источники

  1. Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet, DS40002061B, 2020 — 11.8 — сторожевой таймер от отдельного генератора 128 кГц, режимы прерывания, сброса и прерывания со сбросом, таймаут от 16 мс до 8 с, фьюз WDTON фиксирует WDE = 1 и WDIE = 0; изменение WDE и WDP — последовательность: WDCE и WDE в одной записи, затем в течение четырёх тактов новые значения со сброшенным WDCE; таймер сбрасывают перед сменой WDP; если таймер включился случайно, после сброса он остаётся включённым — в начале программы сбрасывают WDRF и WDE; 11.9.1 — MCUSR: WDRF, BORF, EXTRF, PORF, PORF сбрасывается только записью нуля; 11.9.2 — WDTCSR по адресу 0x60, WDIF, WDIE, WDP3, WDCE, WDE, WDP2:0; выполнение обработчика сбрасывает WDIE, и следующий таймаут даёт сброс; WDIE не рекомендуется включать заново в самом обработчике; WDE принудительно установлен, пока установлен WDRF; табл. 11-1 и 11-2 — режимы и периоды 2K…1024K тактов: 16 мс … 8,0 с при 5 В
  2. AVR-LibC 2.3.2: <avr/wdt.h> — wdt_reset() — команда WDR; wdt_enable() и wdt_disable() — встроенный ассемблер с cli на время защищённой записи; константы WDTO_15MS = 0 … WDTO_8S = 9; после сброса от сторожевого таймера он остаётся включённым с самым быстрым периодом (около 15 мс), поэтому его отключают в .init3 после сохранения и обнуления MCUSR
  3. STMicroelectronics. RM0008 Reference Manual STM32F101/102/103/105/107, Rev 21, 2021 — 3.1 — частота APB1 не выше 36 МГц; 5.3.4 — запущенный IWDG останавливается только сбросом; 7.2.9 — при запуске IWDG генератор LSI включается принудительно и не может быть выключен; 7.1.1 — источники системного сброса, флаги в RCC_CSR не сбрасываются системным сбросом; 7.3.10 — RCC_CSR: LPWRRSTF, WWDGRSTF, IWDGRSTF, SFTRSTF, PORRSTF, PINRSTF, сброс флагов битом RMVF; 19.3 — IWDG: запуск ключом 0xCCCC, счёт вниз от 0xFFF, перезагрузка ключом 0xAAAA, доступ к PR и RLR после 0x5555; аппаратный режим через биты опций; работает в Stop и Standby; табл. 96 — от 0,1 до 26 214,4 мс при LSI 40 кГц; 19.4 — PR[2:0], RL[11:0], флаги PVU и RVU, обновление до 5 периодов LSI, после записи можно не ждать флагов, прочитанное значение достоверно только при сброшенном флаге; 20 — WWDG: 7-битный счётчик, сброс при переходе 0x40 → 0x3F и при обновлении, пока счётчик больше окна W, после включения WDGA выключить нельзя, EWI при 0x40, формула t = t_PCLK1 · 4096 · 2^WDGTB · (T[5:0] + 1), пример в 20.4 (24 МГц, WDGTB = 3, T[5:0] = 63) приведён с ответом 21,85 мс, хотя по той же формуле получается 87,38 мс, — ошибка в документе, согласуется с табл. 98; табл. 98 — 113 мкс…58,25 мс при 36 МГц; 31.16.2–31.16.3 — DBG_IWDG_STOP и DBG_WWDG_STOP в DBGMCU_CR, регистр сбрасывается только при включении питания
  4. STMicroelectronics. stm32f1xx-hal-driver, коммит bd368aa: Src/stm32f1xx_hal_iwdg.c — HAL_IWDG_Init(): запуск ключом 0xCCCC, ключ 0x5555, запись PR и RLR, ожидание сброса флагов обновления в IWDG_SR, затем перезагрузка 0xAAAA
  5. STMicroelectronics. STM32F103x8, STM32F103xB Datasheet, DS5319 Rev 20, 2025 — табл. 25 — частота LSI от 30 до 60 кГц, типично 40 кГц; табл. 28 — стирание страницы 1 КБ от 20 до 40 мс
  6. STMicroelectronics. cmsis-device-f1, коммит c8e9a4a: Include/stm32f103xb.h — IWDG->KR, PR, RLR, SR, IWDG_SR_PVU, IWDG_SR_RVU; WWDG->CR, CFR, SR, WWDG_CR_WDGA, WWDG_CFR_WDGTB_Pos = 7, WWDG_CFR_EWI, WWDG_SR_EWIF; RCC_CSR_IWDGRSTF, RCC_CSR_WWDGRSTF, RCC_CSR_SFTRSTF, RCC_CSR_PORRSTF, RCC_CSR_PINRSTF, RCC_CSR_LPWRRSTF, RCC_CSR_RMVF; DBGMCU_CR_DBG_IWDG_STOP, DBGMCU_CR_DBG_WWDG_STOP; WWDG_IRQn = 0

Нашли ошибку?