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__;

Что тут происходит? Попробуем разобраться:

  1. Не инитить переменные из либы separately_inited_vars в RAM (совсем)
  2. Инитить переменные секции data из либы separately_inited_vars, но только по запросу
  3. Переменные из либы separately_inited_vars будут доступны в секции data и определены секцией MYBLOCK (наше любое название)
  4. Секция 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

Комментарии (4)

RSS свернуть / развернуть
Во всех микроконтроллерах
и тут же о LSI из STM. Неужели все микроконтроллеры STM?
Взгляд поверхностный, с точки зрения Сишника, примеры тоже. В общем случае можно понять, что содержимое ОЗУ на момент ребута по любой причине хранит слепок последнего состояния программы. При желании можно зарезервировать место под переменную и периодически ее обновлять номером исполняемого блока (если строкой, это слишком часто, ИМХО). Это не всегда возможно, не при всех ошибках, например не при срыве стека.
А так прием годный, но с ограничениями.
+1
> Неужели все микроконтроллеры STM?
ну как бы статья в разделе STM32 и было бы глупо писать «все микроконтроллеры STM32...»

> При желании можно зарезервировать место под переменную и
> периодически ее обновлять номером исполняемого блока
> (если строкой, это слишком часто, ИМХО)
почему часто? Почти никаких накладных расходов, просто обновляем переменную, разве нет?

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

> Взгляд поверхностный, с точки зрения Сишника, примеры тоже
как и ваш комментарий =) вот если бы вы конкретизировали, цены бы вам не было, я бы добавил это в статью и получился бы общими усилиями годный мануал. Например, в сегментах кода или настройке icf настройке icf я конкретно плаваю, можете что-то добавить?
0
Я бы вместо записи одной переменной сделал кольцевой буфер, позволяющий отследить попадания на несколько шагов назад. Тогда можно с некоторой вероятностью и срыв стека пропальпировать
0
  • avatar
  • xar
  • 25 сентября 2020, 10:45
Согласен, простор фантазии ограничен только размером ОЗУ )
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.