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 в папку с проектом и выбрать новый путь к файлу.

Далее создаем следующие файлы

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);
   } 
 }

...
//инит вотчдога и остальная программа

Результат:

#электроника #микроконтроллер #мк