Настраиваем WatchDog на микроконтроллере ATmega8

В этом уроке рассмотрим работу сторожевого таймера WatchDog микроконтроллера ATmega8. Сначала разберём простейший способ его применения, а затем построим более правильную систему контроля, которую уже можно использовать в реальных проектах.

Для чего нужен WatchDog

WatchDog — это отдельный аппаратный таймер, предназначенный для восстановления работы микроконтроллера в случае зависания программы.

После запуска WatchDog начинает отсчёт времени. Программа должна периодически выполнять команду wdt_reset().

Если программа зависла и команда wdt_reset() больше не выполняется, сторожевой таймер достигает заданного периода и выполняет аппаратный Reset микроконтроллера.

Основной смысл WatchDog:

программа должна доказать, что она работает правильно, прежде чем сбросить WatchDog.

Библиотека avr/wdt.h

Для работы со сторожевым таймером используется стандартная библиотека avr-libc:

#include <avr/wdt.h>
  • wdt_enable(time) — включает WatchDog и устанавливает его период.
  • wdt_reset() — сбрасывает счётчик WatchDog и начинает отсчёт заново.
  • wdt_disable() — отключает WatchDog.

Периоды WatchDog ATmega8

Значение Примерный период
WDTO_15MS ≈ 16 мс
WDTO_30MS ≈ 32 мс
WDTO_60MS ≈ 65 мс
WDTO_120MS ≈ 0,13 с
WDTO_250MS ≈ 0,26 с
WDTO_500MS ≈ 0,52 с
WDTO_1S ≈ 1 с
WDTO_2S ≈ 2 с
Важно: WatchDog ATmega8 работает от собственного внутреннего генератора. Поэтому его период нельзя использовать как точный временной интервал. WatchDog — это механизм защиты, а не прецизионный таймер.

Пример №1 — обычное использование WatchDog

В первом примере продолжаем программу предыдущего урока по USART. Команды 0, 1 и 2 управляют светодиодами.

При получении команды 3 специально создаём бесконечный цикл:

while (1)
{
    ;
}

Если WatchDog отключён, программа останется в этом цикле навсегда. Если WatchDog включён, программа перестанет выполнять wdt_reset(), и примерно через две секунды микроконтроллер перезагрузится.

Код первого примера

#define F_CPU 4000000UL
#define BAUD 9600UL

#define UBRR_VALUE \
    ((F_CPU / (16UL * BAUD)) - 1)


#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>
#include <util/atomic.h>
#include <stdint.h>


/* ----------------------------------------------------------
   Светодиоды
   ---------------------------------------------------------- */

#define HL1_on() \
    PORTB |= (1 << PB0)

#define HL1_off() \
    PORTB &= ~(1 << PB0)


#define HL2_on() \
    PORTB |= (1 << PB1)

#define HL2_off() \
    PORTB &= ~(1 << PB1)


/* ----------------------------------------------------------
   UART RX
   ---------------------------------------------------------- */

volatile uint8_t rx_data = 0;
volatile uint8_t rx_ready = 0;


/* ----------------------------------------------------------
   Приём данных UART
   ---------------------------------------------------------- */

ISR(USART_RXC_vect)
{
    rx_data = UDR;

    rx_ready = 1;
}


/* ----------------------------------------------------------
   Настройка UART
   ---------------------------------------------------------- */

void uart_init(void)
{
    UBRRL =
        (uint8_t)UBRR_VALUE;

    UBRRH =
        (uint8_t)(UBRR_VALUE >> 8);


    /*
        Разрешаем приём,
        передачу и прерывание RX
    */
    UCSRB =
        (1 << TXEN) |
        (1 << RXEN) |
        (1 << RXCIE);


    /*
        8 бит,
        1 стоп-бит,
        без контроля чётности
    */
    UCSRC =
        (1 << URSEL) |
        (1 << UCSZ1) |
        (1 << UCSZ0);
}


/* ----------------------------------------------------------
   Передача одного символа
   ---------------------------------------------------------- */

void uart_send_char(char data)
{
    while (!(UCSRA & (1 << UDRE)))
    {
        ;
    }

    UDR = data;
}


/* ----------------------------------------------------------
   Передача строки
   ---------------------------------------------------------- */

void uart_send_string(const char *str)
{
    while (*str)
    {
        uart_send_char(*str++);
    }
}


/* ----------------------------------------------------------
   Получение принятого символа
   ---------------------------------------------------------- */

uint8_t uart_get_char(uint8_t *data)
{
    uint8_t result = 0;


    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
        if (rx_ready)
        {
            *data = rx_data;

            rx_ready = 0;

            result = 1;
        }
    }


    return result;
}


/* ----------------------------------------------------------
   GPIO
   ---------------------------------------------------------- */

void gpio_init(void)
{
    PORTB = 0x00;


    /*
        PB0 и PB1 — выходы
    */
    DDRB |=
        (1 << PB0) |
        (1 << PB1);
}


/* ----------------------------------------------------------
   MAIN
   ---------------------------------------------------------- */

int main(void)
{
    uint8_t command;


    /*
        Сохраняем причину
        предыдущего Reset
    */
    uint8_t reset_flags =
        MCUCSR;


    /*
        Очищаем флаги Reset
    */
    MCUCSR = 0;


    /*
        Приводим WatchDog
        в известное состояние
    */
    wdt_reset();

    wdt_disable();


    gpio_init();

    uart_init();


    /*
        Показываем причину запуска
    */
    if (reset_flags & (1 << WDRF))
    {
        uart_send_string(
            "RESET: WATCHDOG\r\n"
        );
    }
    else
    {
        uart_send_string(
            "START\r\n"
        );
    }


    /*
        WatchDog примерно
        на 2 секунды
    */
    wdt_enable(WDTO_2S);


    sei();


    while (1)
    {

        if (uart_get_char(&command))
        {

            switch (command)
            {

                /* -------------------------
                   Команда 0
                   ------------------------- */

                case '0':

                    HL1_on();

                    HL2_off();

                    uart_send_string(
                        "LED1\r\n"
                    );

                    break;


                /* -------------------------
                   Команда 1
                   ------------------------- */

                case '1':

                    HL1_off();

                    HL2_on();

                    uart_send_string(
                        "LED2\r\n"
                    );

                    break;


                /* -------------------------
                   Команда 2
                   ------------------------- */

                case '2':

                    HL1_on();

                    HL2_on();

                    uart_send_string(
                        "LED1 + LED2\r\n"
                    );

                    break;


                /* -------------------------
                   Команда 3

                   Искусственное зависание
                   ------------------------- */

                case '3':

                    HL1_off();

                    HL2_off();

                    uart_send_string(
                        "HANG...\r\n"
                    );


                    /*
                        Программа зависает.

                        До wdt_reset()
                        больше не доходим.
                    */
                    while (1)
                    {
                        ;
                    }

                    break;


                default:

                    uart_send_string(
                        "UNKNOWN COMMAND\r\n"
                    );

                    break;
            }
        }


        /*
            Программа успешно дошла
            до конца цикла.

            Разрешаем WatchDog
            начать новый отсчёт.
        */
        wdt_reset();
    }
}
После обычного включения в терминал будет отправлено:

START

После команды 3 программа зависнет. Через некоторое время WatchDog выполнит Reset, программа снова запустится и отправит:

RESET: WATCHDOG

Почему простого wdt_reset() недостаточно

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

  • приём и передача данных;
  • обработка датчиков;
  • расчёты;
  • управление выходами;
  • контроль исполнительных устройств;
  • проверка связи.

Если просто вызывать wdt_reset() в каком-либо месте программы, мы не можем быть уверены, что остальные части действительно работают.

Нельзя безусловно сбрасывать WatchDog из прерывания таймера

Неправильный подход:
ISR(TIMER0_OVF_vect)
{
    wdt_reset();
}

Представим, что основной цикл программы завис. При этом аппаратный Timer0 продолжает работать, его прерывание продолжает вызываться, а WatchDog постоянно сбрасывается.

В результате основная программа уже не работает, но WatchDog считает, что всё исправно.

Поэтому обработчик таймера может только сообщить:

«пришло время проверить состояние программы».

Сам wdt_reset() должен выполняться только после проверки основных частей программы.

Пример №2 — улучшенное применение WatchDog

Теперь организуем контроль нескольких частей программы.

Каждая успешно выполненная задача устанавливает свой флаг:

health_flags |= HEALTH_UART;

health_flags |= HEALTH_APPLICATION;

Периодически проверяем, были ли установлены все необходимые флаги.

if ((health & HEALTH_ALL) == HEALTH_ALL)
{
    wdt_reset();
}

Если хотя бы одна критическая часть программы не выполнилась, WatchDog не сбрасывается.

Если проблема сохраняется, WatchDog перезагружает микроконтроллер.

Настройка Timer0

Timer0 будем использовать только для формирования контрольного интервала.

При частоте микроконтроллера 4 МГц и делителе Timer0 64:

4 000 000 / 64 = 62 500 Гц

256 / 62 500 = 4,096 мс

4,096 × 61 ≈ 250 мс

Таким образом, примерно каждые 250 мс Timer0 устанавливает флаг проверки.

Улучшенный программный код

#define F_CPU 4000000UL
#define BAUD 9600UL

#define UBRR_VALUE \
    ((F_CPU / (16UL * BAUD)) - 1)


#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>
#include <util/atomic.h>
#include <stdint.h>


/* ----------------------------------------------------------
   Светодиоды
   ---------------------------------------------------------- */

#define HL1_on() \
    PORTB |= (1 << PB0)

#define HL1_off() \
    PORTB &= ~(1 << PB0)


#define HL2_on() \
    PORTB |= (1 << PB1)

#define HL2_off() \
    PORTB &= ~(1 << PB1)


/* ----------------------------------------------------------
   Контролируемые части программы
   ---------------------------------------------------------- */

#define HEALTH_UART \
    (1 << 0)

#define HEALTH_APPLICATION \
    (1 << 1)


#define HEALTH_ALL \
    (HEALTH_UART | HEALTH_APPLICATION)


/* ----------------------------------------------------------
   UART
   ---------------------------------------------------------- */

volatile uint8_t rx_data = 0;

volatile uint8_t rx_ready = 0;


/* ----------------------------------------------------------
   Timer0
   ---------------------------------------------------------- */

volatile uint8_t supervisor_due = 0;


/*
    Флаги здоровья программы.

    Эти флаги устанавливаются
    только основной программой.
*/
uint8_t health_flags = 0;


/* ----------------------------------------------------------
   UART RX ISR
   ---------------------------------------------------------- */

ISR(USART_RXC_vect)
{
    rx_data = UDR;

    rx_ready = 1;
}


/* ----------------------------------------------------------
   TIMER0 ISR

   4 МГц / 64

   Одно переполнение:
   примерно 4,096 мс.

   61 переполнение:
   примерно 250 мс.
   ---------------------------------------------------------- */

ISR(TIMER0_OVF_vect)
{
    static uint8_t divider = 0;


    divider++;


    if (divider >= 61)
    {
        divider = 0;


        /*
            Только устанавливаем флаг.

            WatchDog здесь
            НЕ СБРАСЫВАЕМ.
        */
        supervisor_due = 1;
    }
}


/* ----------------------------------------------------------
   UART INIT
   ---------------------------------------------------------- */

void uart_init(void)
{
    UBRRL =
        (uint8_t)UBRR_VALUE;

    UBRRH =
        (uint8_t)(UBRR_VALUE >> 8);


    UCSRB =
        (1 << TXEN) |
        (1 << RXEN) |
        (1 << RXCIE);


    UCSRC =
        (1 << URSEL) |
        (1 << UCSZ1) |
        (1 << UCSZ0);
}


/* ----------------------------------------------------------
   UART SEND CHAR
   ---------------------------------------------------------- */

void uart_send_char(char data)
{
    while (!(UCSRA & (1 << UDRE)))
    {
        ;
    }

    UDR = data;
}


/* ----------------------------------------------------------
   UART SEND STRING
   ---------------------------------------------------------- */

void uart_send_string(const char *str)
{
    while (*str)
    {
        uart_send_char(*str++);
    }
}


/* ----------------------------------------------------------
   UART GET CHAR
   ---------------------------------------------------------- */

uint8_t uart_get_char(uint8_t *data)
{
    uint8_t result = 0;


    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
        if (rx_ready)
        {
            *data = rx_data;

            rx_ready = 0;

            result = 1;
        }
    }


    return result;
}


/* ----------------------------------------------------------
   GPIO
   ---------------------------------------------------------- */

void gpio_init(void)
{
    PORTB = 0x00;


    DDRB |=
        (1 << PB0) |
        (1 << PB1);
}


/* ----------------------------------------------------------
   TIMER0
   ---------------------------------------------------------- */

void timer0_init(void)
{
    /*
        Делитель Timer0 = 64

        CS02 CS01 CS00
         0    1    1
    */
    TCCR0 =
        (1 << CS01) |
        (1 << CS00);


    /*
        Разрешаем прерывание
        по переполнению Timer0
    */
    TIMSK |=
        (1 << TOIE0);
}


/* ----------------------------------------------------------
   ЗАДАЧА UART
   ---------------------------------------------------------- */

uint8_t uart_task(void)
{
    uint8_t command;


    if (uart_get_char(&command))
    {

        switch (command)
        {

            case '0':

                HL1_on();

                HL2_off();

                uart_send_string(
                    "LED1\r\n"
                );

                break;


            case '1':

                HL1_off();

                HL2_on();

                uart_send_string(
                    "LED2\r\n"
                );

                break;


            case '2':

                HL1_on();

                HL2_on();

                uart_send_string(
                    "LED1 + LED2\r\n"
                );

                break;


            case '3':

                HL1_off();

                HL2_off();

                uart_send_string(
                    "HANG...\r\n"
                );


                /*
                    Искусственное зависание.

                    Timer0 продолжает работать.

                    ISR Timer0 продолжает
                    устанавливаться.

                    Но основной код уже
                    не дойдёт до функции
                    watchdog_supervisor().

                    Поэтому WatchDog
                    перезагрузит ATmega8.
                */
                while (1)
                {
                    ;
                }


                break;


            default:

                uart_send_string(
                    "UNKNOWN COMMAND\r\n"
                );

                break;
        }
    }


    /*
        Функция успешно
        завершила свою работу.
    */
    return 1;
}


/* ----------------------------------------------------------
   ОСНОВНАЯ ЗАДАЧА ПРИЛОЖЕНИЯ
   ---------------------------------------------------------- */

uint8_t application_task(void)
{
    /*
        Здесь может находиться
        реальный код устройства:

        - работа с датчиками;
        - расчёты;
        - управление выходами;
        - контроль связи;
        - проверка оборудования.

        Возвращаем 1 только после
        успешного выполнения
        необходимой части программы.
    */


    return 1;
}


/* ----------------------------------------------------------
   SUPERVISOR WATCHDOG
   ---------------------------------------------------------- */

void watchdog_supervisor(void)
{
    uint8_t check_now = 0;

    uint8_t health;


    /*
        Безопасно проверяем флаг,
        установленный Timer0.
    */
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
        if (supervisor_due)
        {
            supervisor_due = 0;

            check_now = 1;
        }
    }


    /*
        Ещё не пришло
        время проверки.
    */
    if (!check_now)
    {
        return;
    }


    /*
        Запоминаем,
        какие задачи успешно
        выполнились.
    */
    health = health_flags;


    /*
        Начинаем новый
        контрольный интервал.
    */
    health_flags = 0;


    /*
        WatchDog сбрасываем
        ТОЛЬКО если ВСЕ
        критические части
        подтвердили работу.
    */
    if ((health & HEALTH_ALL) ==
        HEALTH_ALL)
    {
        wdt_reset();
    }


    /*
        Если хотя бы одного
        флага нет —

        wdt_reset() НЕ вызываем.

        WatchDog продолжает
        отсчёт времени.
    */
}


/* ----------------------------------------------------------
   MAIN
   ---------------------------------------------------------- */

int main(void)
{
    /*
        Причина предыдущего Reset
    */
    uint8_t reset_flags =
        MCUCSR;


    /*
        Очищаем флаги Reset
    */
    MCUCSR = 0;


    /*
        Начальное состояние WatchDog
    */
    wdt_reset();

    wdt_disable();


    /*
        Инициализация
    */
    gpio_init();

    uart_init();

    timer0_init();


    /*
        Показываем причину
        предыдущего Reset
    */
    if (reset_flags & (1 << WDRF))
    {
        uart_send_string(
            "RESET: WATCHDOG\r\n"
        );
    }
    else
    {
        uart_send_string(
            "START\r\n"
        );
    }


    /*
        WatchDog примерно
        на 2 секунды
    */
    wdt_enable(WDTO_2S);


    /*
        Разрешаем прерывания
    */
    sei();


    while (1)
    {

        /*
            -----------------------
            Задача UART
            -----------------------

            Флаг устанавливается
            только после того,
            как функция успешно
            вернулась.
        */

        if (uart_task())
        {
            health_flags |=
                HEALTH_UART;
        }


        /*
            -----------------------
            Основная задача
            -----------------------
        */

        if (application_task())
        {
            health_flags |=
                HEALTH_APPLICATION;
        }


        /*
            -----------------------
            Проверка системы
            -----------------------
        */

        watchdog_supervisor();
    }
}

Как работает улучшенный вариант

1. Timer0 работает независимо и примерно каждые 250 мс устанавливает supervisor_due = 1.

2. Обработчик Timer0 не вызывает wdt_reset().

3. Каждая контролируемая часть программы после нормального выполнения устанавливает свой флаг.

4. Функция watchdog_supervisor() проверяет все флаги.

5. Только если все критические части программы выполнились, вызывается wdt_reset().

6. Если программа зависнет в любой контролируемой части, WatchDog больше не будет сбрасываться и выполнит аппаратный Reset.

Как правильно использовать WatchDog

  • Не вызывайте wdt_reset() автоматически только потому, что прошло определённое время.
  • Сбрасывайте WatchDog только после успешного выполнения критических частей программы.
  • Не помещайте безусловный wdt_reset() в периодическое прерывание.
  • Для сложной программы используйте отдельные флаги состояния для разных задач.
  • Выбирайте период WatchDog с запасом относительно максимального нормального времени выполнения программы.
  • Для UART, I2C, SPI, датчиков и других устройств используйте также обычные программные тайм-ауты.
  • WatchDog должен быть последним уровнем восстановления, а не заменой нормальной обработки ошибок.
  • После перезагрузки желательно проверить флаг WDRF, чтобы определить, была ли перезагрузка вызвана WatchDog.
Фьюз WDTON: если WDTON запрограммирован, WatchDog работает постоянно. Для обычных учебных примеров, где WatchDog включается программой через wdt_enable(), предполагается обычная конфигурация, в которой WatchDog можно программно включать и отключать.

Результат работы

При обычной работе программа выполняется без перезагрузки. Если отправить символ 3, программа специально попадёт в бесконечный цикл.

WatchDog перестанет получать подтверждение нормальной работы и перезагрузит микроконтроллер.

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