WatchDog ― устраиваем собаке допрос
(с пристрастием)
1. Водная часть
Во всех микроконтроллерах (мк) есть такая полезная штука, как сторожевая псина (watchdog), призванная для одной цели: искать и уничтожать ребутать мк в случае его зависания. Если основная программа в каком-то месте не успела сбросить таймер 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 в папку с проектом и выбрать новый путь к файлу.
Далее создаем следующие файлы
extern unsigned int debugvar; // - explicit init
#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);
}
}
...
//инит вотчдога и остальная программа
Результат: