Настраиваем WatchDog на микроконтроллере ATmega8
Для чего нужен WatchDog
WatchDog — это отдельный аппаратный таймер, предназначенный для восстановления работы микроконтроллера в случае зависания программы.
После запуска WatchDog начинает отсчёт времени. Программа должна периодически выполнять команду wdt_reset().
Если программа зависла и команда wdt_reset() больше не выполняется, сторожевой таймер достигает заданного периода и выполняет аппаратный Reset микроконтроллера.
программа должна доказать, что она работает правильно, прежде чем сбросить 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 с |
Пример №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();
}
}
Как работает улучшенный вариант
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.
Результат работы
При обычной работе программа выполняется без перезагрузки. Если отправить символ 3, программа специально попадёт в бесконечный цикл.
WatchDog перестанет получать подтверждение нормальной работы и перезагрузит микроконтроллер.