FreeRTOS, STM32: отладка ошибок и исключений Печать
Добавил(а) microsin   

Ядро ARM Cortex-M реализует набор исключений отказов (fault exceptions). Каждое исключение относится к определенному условию возникновения ошибки. Если ошибка произошла, то ядро ARM Cortex-M останавливает выполнение текущей инструкции и делает ветвление на функцию обработчика исключения (exception handler). Этот механизм очень похож на тот, который используется для прерываний, где ядро ARM Cortex-M делает ветвление на обработчик прерывания (interrupt handler, ISR), когда принимает прерывание.

CMSIS определяет следующие имена для обработчиков отказов (fault handlers):

UsageFault_Handler()
BusFault_Handler()
MemMang_Handler()
HardFault_Handler()

Перечисление всех причин и обстоятельств, при которых ядро ARM Cortex-M вызывает каждый из этих обработчиков, выходит за рамки этого документа (перевод статьи [1]). См. литературу по ARM Cortex-M для ARM и разные другие источники, если нужны подробности. Ошибки типа HardFault встречаются наиболее часто, поскольку другие типы отказов, не разрешенные по отдельности, пройдут эскалацию, чтобы превратиться в hard fault.

Несмотря на многочисленные запросы поддержки RTOS, когда люди жаловались, что при использовании ядра RTOS, их приложение падает в обработчик ошибки hard fault, причина аппаратного сбоя оказывалась вовсе не в ядре. Обычно это было одно из следующего:

• Неправильное понимание приоритетов прерываний ядра ARM Cortex-M (эту оплошность допустить весьма просто!), или неправильное понимание, как использовать модель вложенности прерываний FreeRTOS (см. [2]).
• Общая пользовательская ошибка RTOS. См. статью FAQ "My Application Does Not Run – What Could Be Wrong" [3], специально написанную для помощи в подобных случаях.
• Баг в коде приложения.

Отладка ошибки Hard Fault должна начаться с проверки, что программа приложения следует руководствам [2, 3]. Если после этого ошибка hard fault все еще не исправлена, то необходимо определить состояние системы (system state) в момент времени, когда произошел сбой. Отладчики не всегда упрощают эту задачу, поэтому остальная часть этой статьи описывает техники программирования, используемые для отладки.

[Какой Exception Handler выполнился?]

В таблице векторов прерываний обычно устанавливается один и тот же обработчик для каждого источника прерывания/исключения. Обработчики по умолчанию (default handlers) декларируются как weak-символы (код заглушки), чтобы разработчик приложения мог установить свой собственный обработчик простой реализацией функции с корректным именем. Если произошло прерывание, для которого разработчик приложения не предоставил свой отдельный обработчик, то будет выполнен обработчик по умолчанию (default handler).

[weak-функции на ассемблере]

Символ weak это по сути метка с указанием на код, который может быть при необходимости переопределен простым созданием функции с таким же именем. К примеру, weak-обработчики прерываний проекта IAR для STM32, сгенерированного с помощью STM32CubeMX, для микроконтроллера STM32F407 в коде запуска startup_stm32f407xx.s ;будут выглядеть примерно так (weak-обработчики отказов выделены жирным шрифтом):

...
 
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Default interrupt handlers.
;;
        THUMB
        PUBWEAK Reset_Handler
        SECTION .text:CODE:REORDER:NOROOT(2)
Reset_Handler
 
        LDR     R0, =SystemInit
        BLX     R0
        LDR     R0, =__iar_program_start
        BX      R0
 
        PUBWEAK NMI_Handler
        SECTION .text:CODE:REORDER:NOROOT(1)
NMI_Handler
        B NMI_Handler
 
        PUBWEAK HardFault_Handler
        SECTION .text:CODE:REORDER:NOROOT(1)
HardFault_Handler
        B HardFault_Handler
 
        PUBWEAK MemManage_Handler
        SECTION .text:CODE:REORDER:NOROOT(1)
MemManage_Handler
        B MemManage_Handler
 
        PUBWEAK BusFault_Handler
        SECTION .text:CODE:REORDER:NOROOT(1)
BusFault_Handler
        B BusFault_Handler
 
        PUBWEAK UsageFault_Handler
        SECTION .text:CODE:REORDER:NOROOT(1)
UsageFault_Handler
        B UsageFault_Handler
 
        PUBWEAK SVC_Handler
        SECTION .text:CODE:REORDER:NOROOT(1)
SVC_Handler
        B SVC_Handler
 
        PUBWEAK DebugMon_Handler
        SECTION .text:CODE:REORDER:NOROOT(1)
DebugMon_Handler
        B DebugMon_Handler
...

В модуле кода (это тоже автоматически сгенерированный код) заглушки обработчиков отказов выглядят следующим образом:

...
 
/**  * @brief This function handles Hard fault interrupt.  */
void HardFault_Handler(void)
{
  /* USER CODE BEGIN HardFault_IRQn 0 */
  /* USER CODE END HardFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_HardFault_IRQn 0 */
    /* USER CODE END W1_HardFault_IRQn 0 */
  }
}
 
/**  * @brief This function handles Memory management fault.  */
void MemManage_Handler(void)
{
  /* USER CODE BEGIN MemoryManagement_IRQn 0 */
  /* USER CODE END MemoryManagement_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_MemoryManagement_IRQn 0 */
    /* USER CODE END W1_MemoryManagement_IRQn 0 */
  }
}
 
/**  * @brief This function handles Pre-fetch fault, memory access fault.  */
void BusFault_Handler(void)
{
  /* USER CODE BEGIN BusFault_IRQn 0 */
  /* USER CODE END BusFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_BusFault_IRQn 0 */
    /* USER CODE END W1_BusFault_IRQn 0 */
  }
}
 
/**  * @brief This function handles Undefined instruction or illegal state.  */
void UsageFault_Handler(void)
{
  /* USER CODE BEGIN UsageFault_IRQn 0 */
  /* USER CODE END UsageFault_IRQn 0 */
  while (1)
  {
    /* USER CODE BEGIN W1_UsageFault_IRQn 0 */
    /* USER CODE END W1_UsageFault_IRQn 0 */
  }
}
 
...

[weak-функции на языке C]

Пример:

/**
  ******************************************************************************
  * File Name          : freertos.c
  * Description        : Код для приложений FreeRTOS.
  ******************************************************************************
  */
#include "pins.h"
#include "FreeRTOS.h"
#include "task.h"
 
/* Прототипы Hook-функций */
void vApplicationTickHook(void);
 
// Эта функция может быть переопределена в любом месте кода пользователя,
// но уже без атрибута __weak:
__weak void vApplicationTickHook( void )
{
   /* Эта функция будет вызвана на каждом прерывании тика, если в файле
   FreeRTOSConfig.h параметр configUSE_TICK_HOOK установлен в 1. Сюда
   может быть добавлен код пользователя, однако нужно помнить, что
   tick hook вызывается из контекста прерывания, поэтому вставленный
   здесь код не должен делать попытки блокирования, и должен использовать
   только те API-функции FreeRTOS, которые специально разрешено
   вызывать из прерываний (т. е. те, которые оканчиваются на
   ...FromISR). */
}

Обработчики по умолчанию обычно реализуются как бесконечный цикл. Если приложение заканчивает работу на таком default handler, то сначала необходимо определить, где именно произошло прерывание выполнения кода.

Следующий кусок кода демонстрирует, как добавить несколько инструкций к бесконечному циклу обработчика по умолчанию, чтобы загрузить номер выполняющегося прерывания в регистр 2 (r2) перед входом в бесконечный цикл.

Номера прерываний здесь считываются из NVIC относительно начала таблицы векторов, в которой есть записи для системных исключений (таких как hard fault), они находятся выше записей прерываний периферийных устройств. Если в r2 находится значение 3, то обработано исключение hard fault. Если r2 содержит значение, равное или больше 16, то это обрабатывается прерывание периферии, и периферийное устройство, которое вызвало прерывание, можно определить вычитанием 16 из номера прерывания.

Default_Handler:
   /* Загрузка адреса регистра управления прерываниями в r3. */
   ldr r3, NVIC_INT_CTRL_CONST
   /* Загрузка значения регистра управления прерываниями в r2 из
      адреса, находящегося в r3. */
   ldr r2, [r3, 0]
   /* Номер прерывания находится в младшем байте - очистка всех других бит. */
   uxtb r2, r2
Infinite_Loop:
   /* Теперь садимся в бесконечный цикл, номер выполненного прерывания
      находится в r2. */
   b  Infinite_Loop
   .size  Default_Handler, .-Default_Handler
 
.align 4
/* Адрес регистра управления прерываниями NVIC. */
NVIC_INT_CTRL_CONST: .word 0xe000ed04

[Отладка ARM Cortex-M Hard Fault]

Окно стека (stack frame) обработчика fault handler содержит состояние регистров ARM Cortex-M в момент времени, когда произошла ошибка. Код ниже показывает, как прочитать значения регистров из стека в переменные C. Когда это сделано, значения этих переменных могут быть проинспектированы в отладчике точно так же, как и другие переменные.

Сначала определяется очень короткая функция на ассемблере, чтобы определить, какой стек использовался, когда произошла ошибка. Как только это выполнено код ассемблера fault handler передает указатель на стек в C-функцию с именем prvGetRegistersFromStack().

Обработчик fault handler показан ниже в синтаксисе GCC. Обратите внимание, что функция была декларирована как naked, так что она не содержит никакого кода, генерированного компилятором (например, здесь нет кода пролога входа в функцию).

/* Реализация fault handler, которая вызывает функцию prvGetRegistersFromStack(). */
static void HardFault_Handler(void)
{
   __asm volatile
   (
      " tst lr, #4                                                n"
      " ite eq                                                    n"
      " mrseq r0, msp                                             n"
      " mrsne r0, psp                                             n"
      " ldr r1, [r0, #24]                                         n"
      " ldr r2, handler2_address_const                            n"
      " bx r2                                                     n"
      " handler2_address_const: .word prvGetRegistersFromStack    n"
   );
}

Реализация функции prvGetRegistersFromStack() показана ниже. Она копирует значения из стека в переменные C, после чего падает в цикл. Имена переменных выбраны, в соответствии с именами регистров, чтобы было проще проанализировать значения, считанные из соответствующих регистров. Другие регистры не будут изменяться с момента возникновения ошибки, и их можно просмотреть в окне отображения регистров CPU отладчика.

void prvGetRegistersFromStack( uint32_t *pulFaultStackAddress )
{
   /* Здесь используется volatile в попытке предотвратить оптимизацию
      компилятора/линкера, которые любят выбрасывать переменные,
      которые реально нигде не используются. Если отладчик не показывает
      значение этих переменных, то сделайте их глобальными, вытащив
      их декларацию за пределы этой функции. */
   volatile uint32_t r0;
   volatile uint32_t r1;
   volatile uint32_t r2;
   volatile uint32_t r3;
   volatile uint32_t r12;
   volatile uint32_t lr; /* регистр связи (Link Register) */
   volatile uint32_t pc; /* программный счетчик (Program Counter) */
   volatile uint32_t psr;/* регистр состояния (Program Status Register) */
 
   r0 = pulFaultStackAddress[ 0 ];
   r1 = pulFaultStackAddress[ 1 ];
   r2 = pulFaultStackAddress[ 2 ];
   r3 = pulFaultStackAddress[ 3 ];
 
   r12 = pulFaultStackAddress[ 4 ];
   lr = pulFaultStackAddress[ 5 ];
   pc = pulFaultStackAddress[ 6 ];
   psr = pulFaultStackAddress[ 7 ];
 
   /* Когда выполнение дойдет до этой точки, переменные будут
      содержать значения регистров. */
   for( ;; );
}

[Использование значений регистров]

Самый первый из интересующих регистров это программный счетчик. В показанном выше коде переменная pc как раз и содержит значение программного счетчика. Когда ошибка это точный отказ (precise fault), pc хранит адрес инструкции, которая была выполнена, когда произошла ошибка hard fault (или другой fault). Когда ошибка это неточный отказ (imprecise fault), то требуются дополнительные шаги, чтобы найти адрес инструкции, которая привела к ошибке.

Чтобы найти инструкцию по адресу которая хранится в переменной pc, сделайте одно из следующего.

1. Откройте окно кода ассемблера (точнее дизассемблированного кода) в отладчике, и вручную введите значение адреса из переменной pc, чтобы посмотреть инструкции по этому адресу.

2. Откройте окно точек останова (break point) в отладчике, и вручную определите точку останова (break point) на этом адресе (execution break) или точку останова по доступу (access break) к этому адресу. С установленной break point перезапустите приложение, чтобы увидеть строку кода, относящуюся к адресу инструкции, которая соответствует переменной pc.

Когда известна инструкция, которая была выполнена при возникновении fault, можно узнать интересующие значения в других регистрах. Например, если инструкция использовала значение R7 в качестве адреса, то нужно знать значение R7. В будущем, анализируя код ассемблера и код C, из которого был сгенерирован этот ассемблерный код, можно увидеть, что реально содержится в R7 (например, это может быть значение переменной).

[Как разобраться с неточным отказом]

Отказы (faults) платформы ARM Cortex-M могут быть точными (precise fault) или неточными (imprecise fault). Если установлен бит IMPRECISERR (бит 2) в регистре отказа шины (BusFault Status Register, или BFSR, который доступен как байт по адресу 0xE000ED29), то это сигнал неточного отказа (imprecise fault).

Обнаружить причину imprecise fault сложнее, потому что fault не обязательно будет происходить одновременно с выполнением инструкции, приведшей к отказу. Например, если идет кэшированная запись в память, то может быть задержка между инструкцией ассемблера, инициировавшей запись в память, и реальной операцией записи в память. Если такая отложенная запись в память недопустима (например, попытка записи в несуществующую физически память) то произойдет imprecise fault, и значение программного счетчика, полученного с помощью вышеуказанного примера кода, не будет соответствовать адресу инструкции, которая инициировала недопустимую операцию записи.

В примере, показанном выше, выключите буферизацию записи установкой бита DISDEFWBUF (бит 1) в регистре ACTLR (Auxiliary Control Register), что в результате превратит imprecise fault в precise fault, и это упростит отладку ценой замедления выполнения программы.

[Ссылки]

1. Debugging Hard Fault & Other Exceptions site:freertos.org.
2. Приоритеты прерываний Cortex-M и приоритеты FreeRTOS.
3. FreeRTOS: базовые техники отладки и поиска ошибок (FAQ).
4FreeRTOS: использование стека и проверка стека на переполнение.
5FreeRTOS: практическое применение, часть 6 (устранение проблем).
6Проектирование стека и кучи в IAR.
7FreeRTOS, STM32: отладка ошибок и исключений.
8IAR C-SPY: предупреждение о переполнении стека.