WatchDog ― устраиваем собаке допрос (с пристрастием)

1. Водная часть
Во всех микроконтроллерах (мк) есть такая полезная штука, как сторожевая псина (watchdog). Работает от внутреннего низкочастотного генератора (LSI), то есть за редким исключением можно сказать, что LSI ― «личный» таймер вотчдога, который будет работать даже если главный генератор подохнет (например, отвалится нога кварца). В угоду пуристам можно отметить, что при желании можно тактовать от LSI всю программу, но сейчас речь не об этом.Итак, сторожевая псина призвана для одной цели ― ребутать мк в случае его зависания. Если основная программа в каком-то месте не успела сбросить таймер watchdog, то ее ждет аварийный ребут. И если в любительских устройствах поговорка «семь бед ― один резет» еще допустима, то в случае более серьезного использования желательно узнать причину перезагрузки. Но ведь после перезагрузки все данные потеряны, искать больше нечего. Или нет?
2. Инструменты
В качестве среды разработки мы будем использовать IAR (IAR Embedded Workbench for Arm, version 8.50.6), в качестве подопытного мк ― stm32f030f4p6, в качестве тоника ― чай, кофе, шило в…3. Ой, а кто это сдееелал?
Если верить мануалу, то при перезагрузке мк сбрасываются в дефолт все регистры, кроме RCC_CSR. По его состоянию можно определить, почему произошел ребут. Вотчдогу выделен бит IWDGRSTF, вот его и будем использовать:
...
//инициализация переменных итд
...
void main(void) {
if (RCC->CSR & RCC_CSR_IWDGRSTF) {
while(1) {
lcd_str("Ошибка в программе", 0); //вывод сообщения на дисплей/в терминал/etc
sleep(100);
}
}
//whatchdog init - обратите внимание, что собаку инитим ПОСЛЕ сообщения об ошибке
IWDG->KR = 0xCCCC; /* (1) */
IWDG->KR = 0x5555; /* (2) */
IWDG->PR = IWDG_PR_PR_1; /* (3) */
IWDG->RLR = 0xFFF; /* (4) */
while(IWDG->SR); /* (5) */
while (1) {
//тут код программы
}
Бит IWDGRSTF сбрасывается вручную, либо по питанию (просто замкнуть reset не прокатит). Это очень удобно — после аварийного ребута можно например корректно остановить все оборудование, которым управляет мк, вывести на экран сообщение об ошибке, включить сигнализацию и ждать
Все дело в том, что ОЗУ при перезагрузке мк через watchdog практически остается нетронутой. Вот если бы можно было заранее записать туда полезную информацию (например, номер строки зависания), а при ребуте прочитать…
4. Память: краткий экскурс по секциям
При компиляции программы формируется объектный файл, который поделен на секции:- .text — исполняемый код
- .data — данные (переменные, инициализированные не нулем)
- .bss — данные, которые нулевые по умолчанию
- итд
Строго говоря, секция bss физически в файле отсутствует, т.к. она инициализирована нулем, но это тема отдельной статьи, для упрощения примем следующую схему: код (.text) попадает во флеш-память, данные (.data) – тоже во флеш, но из расчета, что они будут доступны в оперативной памяти, .bss — в оперативную память.
5. Хачим конфиг линкера
Сначала идем в конфиг проекта и выбираем раздел Linker
Во вкладке Config надо нажать галку Override default, потом скопировать дефотный файл icf в папку с проектом и выбрать новый путь к файлу.
Далее создаем следующие файлы
separately_inited_vars.h
extern unsigned int debugvar; // - explicit init
separately_inited_vars.c
#include "separately_inited_vars.h"
unsigned int debugvar=0; // - explicit init
И подключаем, как обычную библиотеку.
После этого хачим новый файл icf.
Было:
/*###ICF### Section handled by ICF editor, don't touch! ****/
/*-Editor annotation file-*/
/* IcfEditorFile="$TOOLKIT_DIR$\config\ide\IcfEditor\cortex_v1_0.xml" */
/*-Specials-*/
define symbol __ICFEDIT_intvec_start__ = 0x08000000;
/*-Memory Regions-*/
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000;
define symbol __ICFEDIT_region_ROM_end__ = 0x08003FFF;
define symbol __ICFEDIT_region_RAM_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM_end__ = 0x20000FFF;
/*-Sizes-*/
define symbol __ICFEDIT_size_cstack__ = 0x400;
define symbol __ICFEDIT_size_heap__ = 0x400;
/**** End of ICF editor section. ###ICF###*/
define memory mem with size = 4G;
define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__];
define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__];
define block CSTACK with alignment = 8, size = __ICFEDIT_size_cstack__ { };
define block HEAP with alignment = 8, size = __ICFEDIT_size_heap__ { };
initialize by copy { readwrite };
place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec };
/* vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv CUT HERE vvvvvvvvvvvvvvvvvvvvvvvvvv */
place in ROM_region { readonly };
place in RAM_region { readwrite,
block CSTACK, block HEAP };
/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CUT HERE ^^^^^^^^^^^^^^^^^^^^^^^^^^ */
export symbol __ICFEDIT_region_RAM_start__;
export symbol __ICFEDIT_region_RAM_end__;
Стало:
/*###ICF### Section handled by ICF editor, don't touch! ****/
/*-Editor annotation file-*/
/* IcfEditorFile="$TOOLKIT_DIR$\config\ide\IcfEditor\cortex_v1_0.xml" */
/*-Specials-*/
define symbol __ICFEDIT_intvec_start__ = 0x08000000;
/*-Memory Regions-*/
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000;
define symbol __ICFEDIT_region_ROM_end__ = 0x08003FFF;
define symbol __ICFEDIT_region_RAM_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM_end__ = 0x20000FFF;
/*-Sizes-*/
define symbol __ICFEDIT_size_cstack__ = 0x400;
define symbol __ICFEDIT_size_heap__ = 0x400;
/**** End of ICF editor section. ###ICF###*/
define memory mem with size = 4G;
define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__];
define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__];
define block CSTACK with alignment = 8, size = __ICFEDIT_size_cstack__ { };
define block HEAP with alignment = 8, size = __ICFEDIT_size_heap__ { };
initialize by copy { readwrite };
place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec };
/* vvvvvvvvvvvvvvvvvvvvvvvvvvvvv PASTE HERE vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv */
do not initialize { section .bss object separately_inited_vars.o }; /* 1 */
initialize manually { section .data object separately_inited_vars.o }; /* 2 */
define block MYBLOCK { section .data object separately_inited_vars.o }; /* 3 */
place in ROM_region { readonly };
place in RAM_region { readwrite,
block MYBLOCK, /* 4 */
block CSTACK, block HEAP };
/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PASTE HERE ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */
export symbol __ICFEDIT_region_RAM_start__;
export symbol __ICFEDIT_region_RAM_end__;
Что тут происходит? Попробуем разобраться:
- Не инитить переменные из либы separately_inited_vars в RAM (совсем)
- Инитить переменные секции data из либы separately_inited_vars, но только по запросу
- Переменные из либы separately_inited_vars будут доступны в секции data и определены секцией MYBLOCK (наше любое название)
- Секция MYBLOCK должна лежать в оперативке
Далее прописываем в main.c следующую директиву:
#pragma section = "MYBLOCK"
Готово! Теперь напишем простейшую функцию:
void ram_debug(int line) {
if (RCC->CSR & RCC_CSR_IWDGRSTF) return; //при аварийной перезагрузке не переназначать переменную debugvar
debugvar = line; //иначе - вписать в переменную номер строки
}
Как это работает. Вставляем функцию в критические участки кода:
...
ram_debug(__LINE__);
while ((USART1->ISR & USART_ISR_TC) != USART_ISR_TC); //line 156
...
__LINE__ — встроенный макрос, который вставит номер строки из исходника программы. Таким образом, при аварийном ребуте мы получим сообщение, выполнение какой строки кода этому предшествовало. Конечно, нет 100% гарантий, что причиной была именно эта строка, но в любом случае это хорошее подспорье для разбора полетов. Поправим первоначальный код:
...
//инициализация переменных итд
...
void main(void) {
if (RCC->CSR & RCC_CSR_IWDGRSTF) {
sprintf(buf, "%03d", debugvar);
while(1) {
lcd_str("Ошибка в строке ", 0);
lcd_str(buf, 64);
lcd_str(" !!!", -1);
ram_debug(__LINE__);
sleep(100);
}
}
...
//инит вотчдога и остальная программа
Результат:

Это значение переменной будет сохраняться даже при хардварном резете и сбросится только при выключении из розетки.
Послесловие
Зачем такие трудности? Ведь можно писать нормально во флеш, без этих извращений с конфигами линковщика. Но какой же флеш выдержит столько перезаписей? Например, у STM32F030 по даташиту вообще гарантированное количество перезаписей — 1000. Тогда, при использовании всего одного байта памяти, 16 Кб хватит грубо на (16000 * 1000) перезаписей. Перезапись будет происходить при каждом вызове ram_debug(__LINE__). Пусть раз в миллисекунду (хотя конечно чаще). Несложно посчитать, что ресурса хватит на 16000 сек — меньше пяти часов! Не слишком обнадеживающе.Другой вариант обойтись без извращений, это заюзать внешнюю SRAM, но это и медленней, да и ног у камня как всегда не хватает, да к тому же i2c нужен, который занимает те же порты, что и USART (кто так придумал)… Да и потом, а если зависание произойдет именно при общении с внешней памятью, например глюканет i2c? Вот и приходится искать варианты.
Спасибо прочитавшим, критика и улучшайзинги — приветствуются.
- +6
- 22 сентября 2020, 18:14
- DySprozin
Во всех микроконтроллерахи тут же о LSI из STM. Неужели все микроконтроллеры STM?
Взгляд поверхностный, с точки зрения Сишника, примеры тоже. В общем случае можно понять, что содержимое ОЗУ на момент ребута по любой причине хранит слепок последнего состояния программы. При желании можно зарезервировать место под переменную и периодически ее обновлять номером исполняемого блока (если строкой, это слишком часто, ИМХО). Это не всегда возможно, не при всех ошибках, например не при срыве стека.
А так прием годный, но с ограничениями.
> Неужели все микроконтроллеры STM?
ну как бы статья в разделе STM32 и было бы глупо писать «все микроконтроллеры STM32...»
> При желании можно зарезервировать место под переменную и
> периодически ее обновлять номером исполняемого блока
> (если строкой, это слишком часто, ИМХО)
почему часто? Почти никаких накладных расходов, просто обновляем переменную, разве нет?
Про срыв стека согласен, но мне показалось это настолько очевидным, что в статью добавлять не стал.
> Взгляд поверхностный, с точки зрения Сишника, примеры тоже
как и ваш комментарий =) вот если бы вы конкретизировали, цены бы вам не было, я бы добавил это в статью и получился бы общими усилиями годный мануал. Например, в сегментах кода или настройке icf настройке icf я конкретно плаваю, можете что-то добавить?
ну как бы статья в разделе STM32 и было бы глупо писать «все микроконтроллеры STM32...»
> При желании можно зарезервировать место под переменную и
> периодически ее обновлять номером исполняемого блока
> (если строкой, это слишком часто, ИМХО)
почему часто? Почти никаких накладных расходов, просто обновляем переменную, разве нет?
Про срыв стека согласен, но мне показалось это настолько очевидным, что в статью добавлять не стал.
> Взгляд поверхностный, с точки зрения Сишника, примеры тоже
как и ваш комментарий =) вот если бы вы конкретизировали, цены бы вам не было, я бы добавил это в статью и получился бы общими усилиями годный мануал. Например, в сегментах кода или настройке icf настройке icf я конкретно плаваю, можете что-то добавить?
Комментарии (5)
RSS свернуть / развернуть