Error handler stm32

Техника, Программирование, Linux, Разработка приложений

HAL (Hardware Abstraction Layer) — это библиотека для создания приложений на stm32, разработанная компанией

ST

в 2014 году. HAL пришёл на смену SPL.

Заранее скажу, что эта статья никакой не мануал, это просто попытка описать внутреннее устройство HAL, ну или можно сказать, что это метод изучения сабжа.

Итак, HAL позволяет абстрагироваться от работы с регистрами и прочей сложной магии. Грубо говоря, HAL это обёртка над низкоуровневыми операциями. Конечно же это не отменяет необходимости понимания устройства микроконтроллеров, но значительно снижает уровень вхождения.

Например, чтоб запустить таймер, достаточно перед бесконечным циклом прописать вот такую функцию…

HAL_TIM_Base_Start_IT(&htim1);

То есть нам не нужно знать какие регистры отвечают за это, и что в них записывать. Более того, эта функция будет работать на любых микроконтроллерах серии stm32.

Сама функция выглядит так:

Вначале происходит проверка параметров на ошибки (assert_param), и после этого активируется прерывание и запускается таймер.

Строчки начинающиеся с

__двойного подчеркивания

, это макросы, с помощью которых можно устанавливать/снимать необходимые биты в регистрах. Как и в случае с самой функцией, макросы будут одинаковы для всей линейки микроконтроллеров.

Однако я немного забежал вперёд. Прежде чем изучать HAL, нужно познакомиться с программой CubeMX (в просторечии «Куб») так как HAL является неотъемлемой частью «Куба», и именно в нём генерится весь начальный код будущего приложения включая описанные выше функции. Подробно про CubeMX читайте здесь…

Познакомились — тогда продолжим…

Воспользуемся примером, который сделан по ссылке выше, и рассмотрим подробно всю программу.

Итак мы сгенерировали проект, в котором есть таймер вызывающий прерывание при переполнении, и GPIO. Открываем этот проект в среде разработки (у меня TrueStudio) и в левой панели клацаем файл main.c…

Куб создал все необходимые функции инициализации…

void SystemClock_Config(void)

— инициализация тактирования.

static void MX_TIM1_Init(void)

— инициализация таймера.

static void MX_GPIO_Init(void)

— инициализация GPIO.

… и избавил нас от возни с настройками, и от возможных ошибок.

Все функции типичны

— параметры записываются в структуры, и адреса этих структур передаются в соответствующие HAL-функции. Каждая функция возвращает статус. Если возвращается ошибка, то вызывается функция

Error_Handler()

находящаяся в самом низу. В эту функцию хорошо бы прописать что-то, что сигнализировало бы об ошибке, например мигнуть лампочкой…

void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  // ШЕФ ВСЁ ПРОПАЛО
  /* USER CODE END Error_Handler_Debug */
}

Ниже есть ещё одна функция проверок на ошибки —

void assert_failed(uint8_t *file, uint32_t line)

. Если макрос

assert_param

(упомянутый в начале статьи) возвращает ошибку, то в эту функцию прилетает имя файла в котором произошла ошибка, и номер строки.

Функция работает при условии, что задефайнен

USE_FULL_ASSERT

. Сам по себе этот дефаин находится в файле

stm32f1xx_hal_conf.h

, но он закомментирован…

посмотреть

/* ########################## Assert Selection ############################## */
/**
  * @brief Uncomment the line below to expanse the "assert_param" macro in the 
  *        HAL drivers code
  */
/* #define USE_FULL_ASSERT    1U */

В конце файла обрисован механизм передачи assert_param() в void assert_failed()…

/* Exported macro ------------------------------------------------------------*/
#ifdef  USE_FULL_ASSERT
/**
  * @brief  The assert_param macro is used for function's parameters check.
  * @param  expr: If expr is false, it calls assert_failed function
  *         which reports the name of the source file and the source
  *         line number of the call that failed. 
  *         If expr is true, it returns no value.
  * @retval None
  */
  #define assert_param(expr) ((expr) ? (void)0U : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
  void assert_failed(uint8_t* file, uint32_t line);
#else
  #define assert_param(expr) ((void)0U)
#endif /* USE_FULL_ASSERT */

Если хотите чтоб он раскомментировался, то надо в Кубе сделать так…


Enable Full Assert. Эти ассерты занимают определённое количество памяти, поэтому их лучше использовать только для отладки, а в релизе отключать.

В общем с проверками на ошибки у HAL’а всё очень удобно и информативно.

Теперь давайте рассмотрим процесс инициализации на примере таймера.

В функции

static void MX_TIM1_Init(void)

, в объявленную глобально структуру

htim1

заносятся различные параметры таймера, после чего эта структура передаётся в функцию

HAL_TIM_Base_Init(&htim1)

.

Теперь клацните функцию

if (HAL_TIM_Base_Init(&htim1) != HAL_OK)

левой кнопкой, а потом правой — вылезет контекстное меню, в котором нужно выбрать

Open Declaration

. Откроется файл

stm32f1xx_hal_tim.c

Здесь происходит следующее:

Проверяется не пустой ли указатель структуры (htim == NULL) и заполнены ли все элементы структуры (assert_param).

Проверяется статус таймера (htim->State == HAL_TIM_STATE_RESET). В данном случае статус HAL_TIM_STATE_RESET говорит о том, что устройство еще не инициализировано или отключено.

посмотреть


Заголовочный файл stm32f1xx_hal_tim.h.

Если статус удовлетворяет, то снимается блокировка (htim->Lock = HAL_UNLOCKED) и вызывается функция

HAL_TIM_Base_MspInit(htim)

посмотреть

Здесь проверяется какой именно таймер настраивается (htim_base->Instance==TIM1) и вызываются функции которые включают тактирование таймера, активирует прерывание и настраивают приоритет.

Далее устанавливается статус «занято» (htim->State= HAL_TIM_STATE_BUSY) — если по каким-то причинам, параллельно будет вызвана ещё одна функция инициализации таймера, то она не сможет ничего испортить.

После этого вызывается функция

TIM_Base_SetConfig(htim->Instance, &htim->Init)

(у этой функции нет приставки HAL, поэтому можно назвать её низкоуровневой) работающая напрямую с регистрами…

посмотреть


Файл stm32f1xx_hal_tim.c

Ну и наконец устанавливается статус «готов к труду и обороне» (htim->State= HAL_TIM_STATE_READY) и возвращается —

return HAL_OK;

Функции связанные с таймером находятся либо в том же файле (stm32f1xx_hal_tim.c), либо в

stm32f1xx_hal_tim_ex.c

.

Все функции имеют характерные названия определяющие их назначение…

Окончание

_IT

означает, что устройство будет вызывать прерывание. Это относится к любым функциям используемым в HAL.

Например запуск таймера без прерываний выглядит так:

HAL_TIM_Base_Start(&htim1);

Как вы уже наверно поняли, одна из особенностей библиотеки HAL это большое количество проверок защищающих разработчиков от массы неприятностей. Кто-то расценит это как добродетель, а кто-то решит что такое количество проверок избыточно. Однако не стоит занимать какую-либо позицию на этот счёт так как в процессе изучения вы поймёте где нужно оставлять эти проверки, а где их можно удалить. То же самое касается и всего остального, со временем вы будете отказываться от каких-то функций HAL и заменять их работой с регистрами.

При работе с любой другой периферией, все необходимые функции вы найдёте в соответствующих файлах…

Названия файлов говорят сами за себя.

Функция запуска таймера…

/* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start_IT(&htim1);
  /* USER CODE END 2 */

… сама по себе не особо интересна.

посмотреть

Функция устанавливает бит разрешающий прерывания по переполнению — __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE) и бит активации таймера — __HAL_TIM_ENABLE(htim).

А вот механизм вызова прерывания поможет понять устройство библиотеки HAL. Разберём его…

Когда мы в Кубе активируем прерывание от какой-либо периферии, то в файле

stm32f1xx_it.c

автоматически создаётся обработчик с соответствующим именем…


Сюда программа переходит как только сработает прерывание от любого из событий таймера №1.

Этот обработчик (условно назовём его низкоуровневым) вызывает HAL-обработчик

HAL_TIM_IRQHandler(&htim1)

находящийся в файле

stm32f1xx_hal_tim.c

. HAL-обработчик состоит из нескольких блоков, каждый из которых отвечает за определённое событие — захват/сравнение, переполнение, триггерный сигнал и т.д…

события

Программа войдя в функцию

HAL_TIM_IRQHandler

проверяет какой из флагов был установлен и найдя нужный блок выполняет его содержимое.

Нас интересует блок TIM Update event…

про макросы

Библиотека HAL под завязку напичкана различными макросами. Как уже говорилось в начале статьи, они начинаются с __двойного подчёркивания и имеют характерные имена определяющие их назначение. Эти макросы очёнь клёвая штука, они позволяют оперировать различными битами в различных регистрах без необходимости копаться в даташитах.

_GET_

— читать биты,

_SET_

— устанавливать биты,

_CLEAR_

— очищать биты, и т.д. Посмотреть макросы можно в хедерах соответствующей периферии, например, всё что связано с таймерами находится в файле

stm32f1xx_hal_tim.h

.

Внутри макроса

__HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE)

содержится вот такая конструкция…

#define __HAL_TIM_CLEAR_IT(__HANDLE__, __INTERRUPT__) ((__HANDLE__)->Instance->SR = ~(__INTERRUPT__))

Этот макрос сбрасывает бит (указанный вторым аргументом) в регистре состояния (Status Register).

В первый аргумент подставляется указатель на структуру таймера, а вторым аргументом идёт дефаин флага который взводится при возникновении прерывания…

#define TIM_IT_UPDATE  (TIM_DIER_UIE)

Написав программу на HAL вы можете проследить где-какие макросы/функции вызываются, и работать с регистрами напрямую. То есть HAL можно с лёгкостью использовать как пособие для изучения низкоуровневых операций.

Если установлен флаг переполнения (TIM_FLAG_UPDATE) и источником является прерывание по переполнению (TIM_IT_UPDATE), тогда флаг сбрасывается и вызывается колбек

HAL_TIM_PeriodElapsedCallback(htim)

.

Колбек это характерная фишка HAL’а.

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

заметка

В принципе нам ничто не мешает мигать лампочкой прямо в обработчике, да ещё и оперировать регистрами напрямую (немного хардкора)


В этом примере делается то же самое, что делает HAL — сбрасывается флаг прерывания и вместо вызова колбека сразу же выполняется действие (мигание светиком).

Таким образом количество кода сократилось бы в разы, но не было бы никаких проверок, и в любом случае нужно было бы самостоятельно писать проверку — какое именно событие случилось (если бы их было несколько). Тем не менее, в дальнейшем, когда хорошенько прокачаетесь, будете потихонечку переходить на работу с регистрами.

Если вы внимательно посмотрите, то увидите что у каждого события есть свой колбек. Например у захвата/сравнения их несколько…

посмотреть

HAL_TIM_IC_CaptureCallback, HAL_TIM_OC_DelayElapsedCallback и HAL_TIM_PWM_PulseFinishedCallback.

Все эти колбеки прописаны в том же файле, с атрибутом

__weak

.

weak

Атрибут

__weak

говорит компилятору, что эта функция может быть переопределена. То есть если такую же функцию, но без weak, прописать ещё где-то, то функция с weak будет игнорироваться.

Находим нужный нам колбек…

… и переопределяем его в файл

main.c

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == TIM1) //check if the interrupt comes from TIM1
	{
		HAL_GPIO_TogglePin(led13_GPIO_Port, led13_Pin); //Toggle the state of pin
	}
}

Проверяем что прерывание пришло от таймера №1 и мигаем светиком.

Проверять от какого таймера пришло прерывание нужно в том случае, если используется несколько таймеров. Тут дело вот в чём: если мы настроим ещё один таймер, например №2, и он тоже будет вызывать прерывания, тогда в файле

stm32f1xx_it.c

появится второй обработчик…

Не смотря на то, что обработчиков два, функция

HAL_TIM_IRQHandler()

одна и та же. Соответственно и колбек будет вызываться один и тот же. Поэтому для двух таймеров нужно делать так…

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == TIM1)
	{
		HAL_GPIO_TogglePin(led13_GPIO_Port, led13_Pin); 
	}

	if(htim->Instance == TIM2)
	{
		HAL_GPIO_TogglePin(led13_GPIO_Port, led13_Pin);
	}
}

Это касается не только таймеров, но и прочей периферии — USART, SPI, I2C и т.д.

Программирование всего остального выглядит примерно так же как и таймера. Открываем соответствующий файл, например

stm32f1xx_hal_uart.c

, если работаем с USART’ом, находим там нужные функции, а в файле

stm32f1xx_hal_uart.h

макросы. Читаем комментарии (все функции и макросы прокомментированы) и пишем код…

Рассмотрим работу USART’а с DMA, там механизм несколько сложнее чем с таймером. В Кубе настройте USART с использованием DMA на приём…

Инициализация USART’а точно такая же как и у таймера…


Параметры загружаются в структуру и передаются в функцию.

Команда запуска опять же схожа с таймером (передаётся структура + доп. аргументы)

HAL_UART_Receive_DMA(&huart1, (uint8_t*)rx_buff, 10);

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

Здесь у нас много чего интересненького.

В первую очередь происходит проверка — занят USART или нет (HAL_UART_STATE_READY).

Если до этого функция уже запускалась и данные ещё не получены, то эта проверка не пройдёт и функция вернёт статус «занято» (return HAL_BUSY). Если же необходимо перезапустить функцию, то предварительно надо вызвать — HAL_UART_AbortReceive(&huart1). Как видите названия функций говорят сами за себя.

Далее проверяется не пустой ли указатель на приёмный буфер, и чтоб размер данных был не нулевой. Устанавливается блокировка (__HAL_LOCK) и начинается заполнение структуры

huart

. Первые четыре пункта вопросов не вызывают, а дальше в соответствующие элементы структуры записываются указатели на функции

(в языке СИ имя функции без скобок является указателем на эту функцию)

содержащие колбеки…

посмотреть

huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt;

Здесь помимо проверки и нового вида макроса (CLEAR_BIT) мы наконец-то видим колбек —

HAL_UART_RxCpltCallback(huart)

, который и нужно прописывать в

main.c

. Этот колбек вызывается когда буфер будет заполнен полностью.

Прерывание может вызываться при заполнении половины буфера. За это отвечает

huart->hdmarx->XferHalfCpltCallback = UART_DMARxHalfCplt

.

посмотреть

Для ошибки тоже есть функция с колбеком —

huart->hdmarx->XferErrorCallback = UART_DMAError

.

посмотреть

Следом идёт запуск DMA —

HAL_DMA_Start_IT()

посмотреть

В функцию передаётся: указатель на структуру, источник данных (в нашем случае это регистр данных (DR) USART’а), получатель данных (адрес буфера), и ожидаемое кол-во байт.

Потом всё это хозяйство передаётся в функцию конфигурирования —

DMA_SetConfig(hdma, SrcAddress, DstAddress, DataLength)

, после чего происходит это…

Если элемент структуры

hdma->XferHalfCpltCallback

не пустой, то разрешаются прерывания по заполнению буфера полностью (DMA_IT_TC), по заполнению буфера наполовину (DMA_IT_HT), и при ошибке (DMA_IT_TE). Если нам не нужно отслеживать заполнение половины буфера, то надо в

huart->hdmarx->XferHalfCpltCallback

записать NULL.

Далее сбрасывается флаг ошибки переполнения (__HAL_UART_CLEAR_OREFLAG), снимается блокировка (__HAL_UNLOCK), с помощью макроса

SET_BIT

устанавливаются различные биты и возвращается статус —

return HAL_OK

.

На этом функция

HAL_UART_Receive_DMA(&huart1, (uint8_t*)rx_buff, BUFSIZE)

закончена.

Низкоуровневый обработчик прерываний от DMA выглядит так же как и в случае с таймером…

… вызывает HAL-обработчик

HAL_DMA_IRQHandler(&hdma_usart1_rx);

И опять же как и у таймера, функция состоит из нескольких блоков. Первый блок срабатывает при заполнении половинки буфера, второй — целиком, а третий при ошибке. Для примера рассмотрим блок полного буфера…

Проверяются флаги полного буфера (DMA_FLAG_TC1) и разрешённого прерывания (DMA_IT_TC).

Если отключён циклический режим DMA —

hdma->Instance->CCR & DMA_CCR_CIRC) == 0U

, тогда отключаются прерывания —

__HAL_DMA_DISABLE_IT(hdma, DMA_IT_TE | DMA_IT_TC)

. При работе DMA в циклическом режиме, отключать прерывания конечно же не нужно.

Заметьте, разработчики HAL снабдили всё функции, макросы и флаги связанные с прерываниями буквами

IT.

Далее устанавливается статус готовности к очередному приёму —

hdma->State = HAL_DMA_STATE_READY

, сбрасывается флаг окончания приёма через DMA —

__HAL_DMA_CLEAR_FLAG(hdma, __HAL_DMA_GET_TC_FLAG_INDEX(hdma))

, и снимается блокировка…

блокировка

Блокировка организована очень просто…

Если сделать

__HAL_LOCK(huart)

, то при обращении к структуре

huart

будет возвращаться статус «занято» —

return HAL_BUSY;

Файл stm32f1xx_hal_def.h.

Последнее условие связано с тем, что было сделано в функции запуска. Если мы там сделали так —

huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt

, то в элементе структуры будет лежать указатель на функцию

UART_DMAReceiveCplt()

. Соответственно условие сработает и будет вызвана функция

UART_DMAReceiveCplt()

, которая в свою очередь вызовет колбек.

Такая вот хитроумная конструкция

Если приём ведётся без DMA…

HAL_UART_Receive_IT(&huart1, (uint8_t*)rx_buff, 10);

Тогда после включения глобального прерывания USART’а появится его обработчик…

Перейдём к функции

HAL_UART_IRQHandler(&huart1)

. Полностью её рассматривать не будем, разбёрём только часть отвечающую за приём. Отправка схожа с приёмом.

Тут появился ещё один макрос —

READ_REG

, с помощью которого читаются регистры и проверяется нет ли ошибок — (errorflags == RESET).

Далее проверятся что произошло:

USART_SR_RXNE

— в USART пришёл байт,

USART_CR1_RXNEIE

— было сгенерировано прерывание. Если всё так, то вызывается функция

UART_Receive_IT(huart)

. Эта функция вызывается каждый раз при приёме очередного байта.

В зависимости от длины принимаемого слова (8 или 9 бит) выбирается первая или вторая конструкция, и данные из регистра DR (Data Register) записываются в приёмный буфер —

pRxBuffPtr

.
Если длина слова 9 бит, то для его сохранения используется два байта — huart->pRxBuffPtr += 2U;

Следом проверяется счётчик принятых байт —

RxXferCount

(он считает «вниз» от максимального значения буфера), и если он равен нулю (то есть приняты все запрошенные данные), то вызывается колбек —

HAL_UART_RxCpltCallback(huart);

Вы наверно обратили внимание, что при принятии одного байта происходит очень много операций

(как раз за такую избыточность некоторые пользователи и ругают HAL, хотя если подумать, то там только проверки и ничего лишнего)

, поэтому при большом количестве данных и/или интенсивном обмене лучше использовать DMA, там это всё происходит на аппаратном уровне.

В завершение хочется рассказать про копирование через DMA. Для этого режима у DMA есть механизм создания колбеков.

Настроим Куб для копирование массива из одной области памяти в другую при помощи DMA…


Длина слова указана Word (32 бита), то есть копироваться будет по четыре байта за один такт.

программа

#include "main.h"

#define BUFFSIZE 20

DMA_HandleTypeDef hdma_memtomem_dma1_channel1;

uint8_t src_buff[BUFFSIZE] = {0,};
uint8_t dst_buff[BUFFSIZE] = {0,};

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART1_UART_Init(void);

void DMA_m2m_Callback(DMA_HandleTypeDef *hdma_memtomem_dma1_channel1) // колбек по окончанию копирования через DMA
{
	// копирование завершено
}

int main(void)
{
  HAL_Init();

  SystemClock_Config();

  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();

  // регистрация колбека по окончанию копирования через DMA
  if(HAL_DMA_RegisterCallback(&hdma_memtomem_dma1_channel1, HAL_DMA_XFER_CPLT_CB_ID, DMA_m2m_Callback) != HAL_OK)
  {
      Error_Handler();
  }

  while (1)
  {
	  // запускаем копирование через DMA
	  HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src_buff, (uint32_t)dst_buff, BUFFSIZE / 4);
	  HAL_Delay(1000);
  }
}

Функция регистрации колбека…

В функцию передаются три аргумента:

1. Указатель на структуру.
2. Ключ, по которому определяется какое событие должно вызвать колбек — скопирован весь буфер, скопирована половина буфера и т.д.


В нашем случае указан полный буфер — HAL_DMA_XFER_CPLT_CB_ID.

3. Название колбека. Придумайте сами.

Таким образом мы зарегистрировали колбек — DMA_m2m_Callback(), который будет вызываться после полного копирования.

Функция запуска копирования…

HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src_buff, (uint32_t)dst_buff, BUFFSIZE / 4);

Аргументы: указатель на структуру, массив из которого копируется, массив в который копируется, количество байт (ячейки массива 8-ми битные, а DMA будет копировать по 32 бита за раз).

Содержимое этой функции поизучайте самостоятельно, вы уже всё знаете

По окончанию копирования произойдёт прерывание и будет вызван обработчик…

В функции

HAL_DMA_IRQHandler()

прописан такой же механизм как и в случае с USART’ом — несколько блоков отвечающих за каждое событие (полный буфер, половинка и т.д.) и вот это…

Элемент структуры

hdma->XferCpltCallback

был заполнен во время регистрации колбека.


На этом наверно всё.

Всем спасибо

User Manual — UM1850

Телеграм-чат istarik

Телеграм-чат STM32


STM32 »

A fault is an exception generated by the processor to indicate an error. If an associated exception is enabled, the exception handler will be called to report, resolve, or recover the system from the fault.


Table of Content

STM32-Tutorials
F411RE_Fault_Handlers.zip

Fault exception#

A fault is an exception generated by the processor to indicate an error.

When there is something violates the design rules of the processor, a fault is triggered.

Whenever a fault happens, internal processor registers will be updated to record the type of fault, the address of instruction at which fault happened, and if an associated exception is enabled, the exception handler will be called.

The fault handler can report, resolve, or recover the system from the fault.

For example:

Dividing a number by zero causes DIVBYZERO fault, which will invoke Usage Fault Handler in which you can get rid of the problem such as closing the problematic task.

The Vector Interrupt Table defines different types with priority of handling order as below:

Exception Number IRQ Number Exception Type Priority Function
3 -13 Hard Fault -1 All faults that hang the processor
4 -12 Memory Fault Configurable Memory issue
5 -11 Bus Fault Configurable Data Bus issue
6 -10 Usage Fault Configurable Instruction/State/Access issue

By default, not all exceptions and interrupts are enabled to be handled.

Exception Default state Handling behavior
Hard Fault Always enabled, can be masked
Memory Fault Disabled by default Synchronous
Bus Fault Disabled by default Synchronous
Usage Fault Disabled by default Synchronous

The Vector Interrupt Table is implemented in assembly code in the startup file of MCU startup_*.s.

g_pfnVectors:
  .word _estack                 /* MSP value */
  .word Reset_Handler           /* Reset routine */
  .word NMI_Handler             /* No-Maskable Interrupt */
  .word HardFault_Handler       /* System faults */
  .word MemManage_Handler       /* Memory access issues */
  .word BusFault_Handler        /* Bus access issues */
  .word UsageFault_Handler      /* Instruction/State issues */
  ...

Fault types#

Fault Handler Bit name Fault status register
Bus error on a vector read HardFault_Handler VECTTBL Hard fault status register (HFSR)
Fault escalated to a hard fault FORCED
MPU or default memory map mismatch: MemManage_Handler Memory management fault address register (MMFSR, MMFAR)
– on instruction access IACCVIOL
– on data access DACCVIOL
– during exception stacking MSTKERR
– during exception unstacking MUNSKERR
– during lazy floating-point state preservation MLSPERR
Bus error BusFault_Handler Bus fault address register (BFSR, BFAR)
– During exception stacking STKERR
– During exception unstacking UNSTKERR
– During instruction prefetch IBUSERR
– During lazy floating-point state preservation LSPERR
Precise data bus error PRECISERR
Imprecise data bus error IMPRECISERR
Attempt to access a coprocessor Usage fault NOCP Configurable fault status register (CFSR ; UFSR+BFSR+MMFSR)
Undefined instruction UNDEFINSTR
Attempt to enter an invalid instruction set state INVSTATE
Invalid EXC_RETURN value INVPC
Illegal unaligned load or store UNALIGNED
Divide By 0 DIVBYZERO

Fault escalation and hard faults

All faults exceptions except for hard fault have configurable exception priority, as described in System handler priority registers (SHPRx). Software can disable execution
of the handlers for these faults.

Usually, the exception priority, together with the values of the exception mask registers, determines whether the processor enters the fault handler, and whether a fault handler can preempt another fault handler.

In some situations, a fault with configurable priority is treated as a hard fault. This is called priority escalation, and the fault is described as escalated to hard fault. Escalation to hard fault occurs when:

  • A fault handler causes the same kind of fault as the one it is servicing. This escalation to hard fault occurs when a fault handler cannot preempt itself because it must have the same priority as the current priority level.
  • A fault handler causes a fault with the same or lower priority as the fault it is servicing. This is because the handler for the new fault cannot preempt the currently executing fault handler.
  • An exception handler causes a fault for which the priority is the same as or lower than the currently executing exception.
  • A fault occurs and the handler for that fault is not enabled.

If a bus fault occurs during a stack push when entering a bus fault handler, the bus fault does not escalate to a hard fault. This means that if a corrupted stack causes a fault, the fault handler executes even though the stack push for the handler failed. The fault handler operates, but the stack contents are corrupted.

Only Reset and NMI can preempt the fixed priority hard fault. A hard fault can preempt any exception other than Reset, NMI, or another hard fault.

Lockup state

The processor enters a lockup state if a hard fault occurs when executing the NMI or hard fault handlers. When the processor is in lockup state it does not execute any instructions.

The processor remains in lockup state until either:

  • It is reset
  • An NMI occurs
  • It is halted by a debugger

If lockup state occurs from the NMI handler a subsequent NMI does not cause the processor to leave lockup state.

Example#

This example enables all configurable fault exceptions, implement fault exceptions handlers, and trigger faults by following methods:

  • Execute an undefined instruction
  • Divide by Zero
  • Execute instruction from peripheral region
  • Execute SVC inside the SVC Handler
  • Execute SVC inside an interrupt handler whose priority is same or less than SVC priority

Step 0: Create a new project

You should create a bare-metal project which just has a few files including a linker and a main.

Step 1: Enable all fault exceptions

In the document PM0214: STM32 Cortex®-M4 MCUs and MPUs programming manual, look at the below section:

4.4 System control block (SCB
The System control block (SCB) provides system implementation information, and system
control. This includes configuration, control, and reporting of the system exceptions.

The System handler control and state register (SHCSR) is at the address 0xE000ED24

4.4.7 Configuration and control register (CCR)

The CCR controls entry to Thread mode and enables:

  • The handlers for NMI, hard fault and faults escalated by FAULTMASK to ignore bus faults
  • Trapping of divide by zero and unaligned accesses
  • Access to the STIR by unprivileged software

Configuration and control register (CCR)

4.4.9 System handler control and state register (SHCSR)

The SHCSR enables the system handlers, and indicates:

  • The pending status of the bus fault, memory management fault, and SVC exceptions
  • The active status of the system handlers

If you disable a system handler and the corresponding fault occurs, the processor treats the fault as a hard fault.

System handler control and state register (SHCSR)

We can use direct memory access method to configure this SHCSR register:

// enable handlers
uint32_t* pSHCSR = (uint32_t *)0xE000ED24;
*pSHCSR |= (1 << 16); // Memory Fault
*pSHCSR |= (1 << 17); // Bus Fault
*pSHCSR |= (1 << 18); // Usage Fault

// enable Divide by Zero Trap
uint32_t* pCCR = (uint32_t*)0xE000ED14;
*pCCR |= (1 << 4); // Div by Zero

Step 2: Implement Fault Handlers

The names of handlers are defined in the Interrupt Vector Table:

g_pfnVectors:
  .word _estack                 /* MSP value */
  .word Reset_Handler           /* Reset routine */
  .word NMI_Handler             /* No-Maskable Interrupt */
  .word HardFault_Handler       /* System faults */
  .word MemManage_Handler       /* Memory access issues */
  .word BusFault_Handler        /* Bus access issues */
  .word UsageFault_Handler      /* Instruction/State issues */

We can directly override those functions in the main file, for example:

void UsageFault_Handler() {
  printf("Exception: Usage Faultn");
  while(1);
}

Step 3: Trigger Usage Fault

We will try to call a function at a location where there is invalid instruction:

  /* Fill a meaningless value */
  *(uint32_t*)0x20010000 = 0xFFFFFFFF;

  /* Set PC with LSB being 1 to indicate Thumb State */
  void (*pFunc)(void) = (void*)0x20010001;

  /* call function */
  pFunc();

Compile and run the program, you will get Usage Fault exception:

Usage Fault: Undefined Instruction

To find out which line of code caused the exception, you can refer to the Fault Analyzer tool. Note that this tool dumps all saved registers during Stacking of Context Switching.

Note that the LR register save the address of the next instruction of what was being executed.

In our example, if LR contains 0x80004b7, we can find the address 0x80004b7 or 0x80004b6 in the disassembly file. The previous instruction of the found instruction at 0x80004b6 mostly the hot spot which caused the fault.

Use Fault Analyzer to find the executing instruction

Exercise

  1. Try to cause Divide by Zero exception
  2. If disable Usage Fault in SHCSR register, which Fault Exception will be raised?

Fault Handler#

We can not plug a debugger all the time to catch the state of system after a Fault happened. A good method is to capture the system state to file or memory for later analysis.

We know that when exception occurs, CPU automatically saves some context registers before jumping to a Fault Handler. We can implement a way to dump those saved registers.

Naked function to capture Stack Frame

A normal function call always has Prologue and Epilogue sequences added by compiler. In the Prologue, some line of code is added to prepare the stack for the function. However, that action will change the Stack Pointer value. Therefore, a naked function should be used to keep the Stack Pointer value.

__attribute__ ((naked)) void UsageFault_Handler(void) {
    // get current Stack Pointer
    __asm volatile("MRS R0, MSP");
    __asm volatile("B UsageFault_Handler_main");
}

This naked function will save the MSP register to R0, and pass R0 to the actual handler:

void UsageFault_Handler_main(uint32_t* pMSP) {
  printf("Exception: Usage Faultn");
  DumpExceptionRegister(pMSP);

  uint32_t* pUFSR = (uint32_t*)0xE000ED2A;
  printf("UFSR = 0x%lxn", *pUFSR & 0xFFFF);

  while(1);
}

Helper function to dump Stack Frame

We can write a general dumper to print out the Stack Frame:

void DumpExceptionRegister(uint32_t* pMSP)
{
  printf(" MSP = %pn", pMSP);
  printf("  R0 = 0x%lxn", pMSP[0]);  // May have argument of function
  printf("  R1 = 0x%lxn", pMSP[1]);  // May have argument of function
  printf("  R2 = 0x%lxn", pMSP[2]);  // May have argument of function
  printf("  R3 = 0x%lxn", pMSP[3]);  // May have argument of function
  printf(" R12 = 0x%lxn", pMSP[4]);  // IP holds an intermediate value of a calculation
  printf("  LR = 0x%lxn", pMSP[5]);  // Address of the next instruction before the exception
  printf("  PC = 0x%lxn", pMSP[6]);  // CPU was executing the instruction at PC
  printf("xPSR = 0x%lxn", pMSP[7]);  // Status of system before execution at PC completes
}

You can use any technique to redirect the Standard IO from printf to UART terminal, or SWD Terminal.

Dump saved Stack Frame to SWD

Всех рад снова приветствовать 🤝 Думаю многие читали статьи Эдуарда в Сообществе, посвященные созданию набора библиотек для работы с микроконтроллерами STM32 на C++. Собственно, на этой почве у Эдуарда  и возник вопрос, как максимально гармонично вписать обработку прерываний, поэтому я сегодня опишу на демо-примере мою концепцию реализации.

И поскольку речь пойдет именно об общей идее, то я не буду убирать HAL и т. д., не вижу особого смысла в данном случае. При желании соответствующие вызовы библиотеки без проблем заменяются на аналогичные манипуляции напрямую с регистрами контроллера.

Я набросаю базовый функционал для пары периферийных модулей, пусть будет Timer и GPIO, чего вполне хватит для наглядной демонстрации. Глобально я не просчитывал всевозможные варианты, буду реализовывать ту идею, которая у меня сформировалась сразу после прочтения темы на форуме, так что не исключено, что в будущем архитектуру нужно будет подправить (в случае желания развить проект дальше). А теперь переходим к сути, которую я буду разбивать на отдельно взятые этапы.

Перенос таблицы векторов прерываний в RAM.

Рассмотрим в двух словах, как организована обработка прерываний по умолчанию. Я далее по тексту буду использовать контроллер семейства STM32F10x, для других названия файлов будут незначительно отличаться. Итак, в файле startup_stm32f103c8tx.s в g_pfnVectors определены точки входа в конкретные функции, вызывающиеся при возникновении того или иного прерывания.

Таким образом, для добавления своего кода в тот или иной обработчик прерываний, присутствующий в startup_stm32f103c8tx.s, функции переопределяются в пользовательском коде, именно так и происходит при генерации проекта через STM32CubeMx. Я же хочу дефолтные обработчики заменить своими, из чего вытекает необходимость создать свою таблицу векторов прерываний в RAM и поместить ее адрес в регистр SCB->VTOR. Так, разбираем пошагово идею:

  1. Создаем свою кастомную таблицу векторов прерываний, физически это означает определение массива. Поместим в main.cpp (о создании демо-проекта — во второй части статьи):
uint32_t ramVectorTable[constants::isrVectorTableSize] __attribute__(( aligned (constants::isrVectorTableAlingment) ));

Здесь используются константы, определенные в constants.h (полный код и проект в конце статьи):

inline constexpr uint8_t isrVectorTableSize = 67;
inline constexpr int isrVectorTableAlingment = 0x100;

Размер таблицы векторов определяется количеством обработчиков в g_pfnVectors.

  • Переместим все дефолтные обработчики из g_pfnVectors в ramVectorTable – это и будет вторым шагом. Впоследствии мы изменим те элементы таблицы, которые потребуется, остальные же спокойно будут пребывать в первозданном виде, что, в свою очередь, избавит нас от множества потенциальных проблем.
for (uint16_t i = 0; i < constants::isrVectorTableSize; i++)
{
	ramVectorTable[i] = g_pfnVectors[i];
}
  • Остается один шаг – поместить адрес ramVectorTable в регистр SCB->VTOR:
__disable_irq();
SCB->VTOR = (uint32_t)&ramVectorTable;
__DSB();
__enable_irq();

Периферия. База.

Плавно переходим к следующему этапу – непосредственной работе с периферией контроллера. И, опять же, опишу мой вариант реализации. Заключается он в следующем: для каждого типа периферийных модулей создается свой класс, инкапсулирующий в себе все взаимодействие с данным типом. Таким образом, для данного проекта я добавляю классы Gpio и Timer:

Структура проекта для STM32.

При этом все эти классы наследуют от базового PeripheralUnit, который в себе заключает общий функционал:

class PeripheralUnit
{
public:
	PeripheralUnit();
	virtual ~PeripheralUnit();
  virtual void interruptHandler();
	bool getInterruptFlag();
	void clearInterruptFlag();
	static void setIsrVectorTable(uint32_t *table);

protected:
	void init();
	void initIsr(IRQn_Type irq);

	bool isInitialized;
	bool interruptFlag;

private:
	static uint32_t *isrVectorTable;
};

Пробежимся прямо по очереди:

  • interruptHandler() – обработчик прерывания, данная функция будет переопределяться в случае необходимости в производных классах.
  • getInterruptFlag() / clearInterruptFlag() – получение/очистка флага прерываний.
  • setIsrVectorTable(uint32_t *table) – статический метод для модификации указателя на актуальную таблицу векторов прерываний.
  • init() / initIsr(IRQn_Type irq) – базовая инициализация, а также конфигурация прерывания, опять же – в случае необходимости.
  • isInitialized / interruptFlag – флаги, сигнализирующие об успешной инициализации и о срабатывании прерывания.
  • isrVectorTable – и, наконец, тот самый указатель на таблицу векторов, которая уже создана на первом этапе.

Далее реализация:

#include "PeripheralUnit.h"
#include "IrqCallback.h"



constexpr int isrVectorTableOffset = 16;

uint32_t* PeripheralUnit::isrVectorTable;
std::vector<PeripheralUnit*> IrqCallbackBase::irqPeripherals;



PeripheralUnit::PeripheralUnit() :
    isInitialized(false),
    interruptFlag(false)
{
}



PeripheralUnit::~PeripheralUnit()
{
}



void PeripheralUnit::setIsrVectorTable(uint32_t* table)
{
	PeripheralUnit::isrVectorTable = table;
}



bool PeripheralUnit::getInterruptFlag()
{
  return interruptFlag;
}



void PeripheralUnit::clearInterruptFlag()
{
  interruptFlag = false;
}



void PeripheralUnit::init()
{
  isInitialized = true;
}



void PeripheralUnit::initIsr(IRQn_Type irq)
{
  IrqCallbackBase::irqPeripherals.push_back(this);

  isrVectorTable[isrVectorTableOffset + irq] =
      (uint32_t)((SimpleCallback)(*createCallback(IrqCallbackBase::irqPeripherals.size() - 1)));
}


void PeripheralUnit::interruptHandler()
{
  interruptFlag = true;
}

Здесь все логично и, в целом, понятно уже из названия функций. Инициализация на данном этапе фактически пустая, в частности по той причине, что ее мы отдали CubeMx. Обработчик прерываний по умолчанию просто устанавливает флаг interruptFlag в true. Регистрация кастомного обработчика прерывания происходит так:

void PeripheralUnit::initIsr(IRQn_Type irq)
{
  IrqCallbackBase::irqPeripherals.push_back(this);

  isrVectorTable[isrVectorTableOffset + irq] =
      (uint32_t)((SimpleCallback)(*createCallback(IrqCallbackBase::irqPeripherals.size() - 1)));
}

К описанию этого процесса и переходим.

Обработка прерываний.

Итак, особо углубляться я не буду, если что пишите в комментарии, на форум или в группу, буду рад помочь )

Базовая ситуация такая – для того, чтобы обеспечить вызов своего обработчика прерывания, необходимо всего-то поместить его адрес на нужную позицию в ramVectorTable. Но! Указатель на функцию и указатель на метод класса это две принципиально разные вещи, поэтому изначально стандарт допускает для данных манипуляций использовать только статические методы, что кардинально противоречит моей концепции.

Допустим, есть класс Timer и два объекта tim1Instance, tim2Instance. Естественно, необходимо, чтобы каждый из объектов имел свой обработчик прерывания, поскольку физически именно так и есть:

Timer interrupt.

Для статической же функции получим:

Использование static-функции.

По существу, это ничем не лучше использования HAL’овских обработчиков, засунутых в один отдельный файл stm32f1xx_it.c. И более того, если попытаться прогуглить решение данной проблемы, то в подавляющем большинстве случаев ответом будет именно выделение функционала в статическую функцию. У нас случай нестандартный, данный вариант априори не подходит, поэтому у меня будет другое решение.

Итоговый код таков, файл IrqCallback.h:

typedef void (*SimpleCallback)(void);



class IrqCallbackBase
{
public:
  IrqCallbackBase(SimpleCallback function)
  {
    callback = function;
  }

  static void staticInvoke(uint8_t index)
  {
    irqPeripherals[index]->interruptHandler();
  }

  static std::vector<PeripheralUnit*> irqPeripherals;

  operator SimpleCallback() const
  {
    return callback;
  }

private:
  SimpleCallback callback;
};



template <uint8_t I> class IrqDynamicCallback : public IrqCallbackBase
{
public:
  IrqDynamicCallback() : IrqCallbackBase(&IrqDynamicCallback<I>::generatedStaticFunction)
  {
  }

  static void generatedStaticFunction()
  {
    return staticInvoke(I);
  }
};



template<uint8_t I> struct IrqDynamicCallbackFactory
{
  static inline std::shared_ptr<IrqCallbackBase> create(uint8_t index)
  {
    if (index == I)
    {
      return std::shared_ptr<IrqCallbackBase>(new IrqDynamicCallback<I>());
    }
    else
    {
      return IrqDynamicCallbackFactory<I + 1>::create (index);
    }
  }
};



struct Overflow
{
  static inline std::shared_ptr<IrqCallbackBase> create(uint8_t index)
  {
    return NULL;
  }
};
template<> struct IrqDynamicCallbackFactory<constants::isrVectorTableSize> : Overflow {};



std::shared_ptr<IrqCallbackBase> createCallback(uint8_t index)
{
  return IrqDynamicCallbackFactory<0>::create(index);
}

Использование в PeripheralUnit мы уже видели:

void PeripheralUnit::initIsr(IRQn_Type irq)
{
  IrqCallbackBase::irqPeripherals.push_back(this);

  isrVectorTable[isrVectorTableOffset + irq] =
      (uint32_t)((SimpleCallback)(*createCallback(IrqCallbackBase::irqPeripherals.size() - 1)));
}

Помещаем текущий периферийный модуль, для которого и реализуем обработку прерываний, в вектор IrqCallbackBase::irqPeripherals. А в таблицу векторов прерываний заносим адрес функции, что в конечном итоге приведет к вызову метода interruptHandler() текущего объекта PeripheralUnit.

Конкретная периферия.

Теперь по-быстрому добавляем классы для работы с таймерами и портами ввода-вывода, начав с первого из перечисленных:

class Timer : public PeripheralUnit
{
public:
  enum class Mode {
    Base,
    Irq,
    Dma
  };

  Timer();
  virtual ~Timer();
  void init(TIM_HandleTypeDef *timerHandle, IRQn_Type irq);
  void start(Mode);

private:
  void interruptHandler();

  TIM_HandleTypeDef *handle;
};

Собственно, три основных метода – инициализация, запуск в одном из режимов и обработчик прерывания:

void Timer::init(TIM_HandleTypeDef *timerHandle, IRQn_Type irq)
{
  handle = timerHandle;
  PeripheralUnit::initIsr(irq);
  PeripheralUnit::init();
}


void Timer::start(Mode operationMode)
{
  switch(operationMode)
  {
    case (Mode::Base):
      HAL_TIM_Base_Start(handle);
      break;

    case (Mode::Irq):
      HAL_TIM_Base_Start_IT(handle);
      break;

    case (Mode::Dma):
    default:
      break;
  }
}



void Timer::interruptHandler()
{
  if (__HAL_TIM_GET_FLAG(handle, TIM_FLAG_UPDATE) != RESET)
  {
    if (__HAL_TIM_GET_IT_SOURCE(handle, TIM_IT_UPDATE) != RESET)
    {
      __HAL_TIM_CLEAR_IT(handle, TIM_IT_UPDATE);
      interruptFlag = true;
    }
  }
}

Для этого демо-проекта я сделал только обработку прерывания по событию переполнения, в результате которого флаг interruptFlag будет выставлен в единицу.

Пару слов по поводу обработки в целом – как по мне, так удобнее всего сделать полноценную event-driven архитектуру, при которой на событие прерывания одного из модулей может приходиться N-ое количество подписчиков. В примере я сделаю максимально просто, в while(1) будем проверять соответствующие флаги, по значению которых уже выполнять те или иные действия.

Итак, движемся к GPIO. Здесь все еще более незатейливо, просто оборачиваем соответствующие вызовы HAL:

class Gpio : public PeripheralUnit
{
public:
  enum class State {
    Set,
    Reset
  };

  Gpio();
  virtual ~Gpio();
  void init(GPIO_TypeDef *gpioPort, uint16_t gpioPin);
  void write(State state);
  State read();
  void toggle();

private:
  GPIO_TypeDef *port;
  uint16_t pin;
};
void Gpio::init(GPIO_TypeDef *gpioPort, uint16_t gpioPin)
{
  port = gpioPort;
  pin = gpioPin;
  PeripheralUnit::init();
}



void Gpio::write(State state)
{
  switch(state)
  {
    case State::Set:
      HAL_GPIO_WritePin(port, pin, GPIO_PIN_SET);
      break;

    case State::Reset:
      HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET);
      break;

    default:
      break;
  }
}



Gpio::State Gpio::read()
{
  State currentState = State::Reset;

  if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_SET)
  {
    currentState = State::Set;
  }

  return currentState;
}



void Gpio::toggle()
{
  HAL_GPIO_TogglePin(port, pin);
}

Демо-проект.

Переходим к тестированию и для начала создаем в STM32CubeIDE новый проект. Все точно так же, как и для «обычного» C-проекта, за исключением «Targeted language» непосредственно после выбора целевого контроллера:

С++ проект в STM32CubeIDE.

В STM32CubeMx активируем необходимую периферию, у меня будут два порта ввода-вывода – один на вход (PA3), другой на выход (PC13):

GPIO.

На PA3 повесим кнопку, на PC13 светодиод, как же без него. И парочка таймеров, TIM2:

Timer configuration.

TIM3:

TIM3 settings.

Тактирование:

Настройки тактирования.

Из данной конфигурации вытекает тот факт, что таймер TIM2 будет переполняться каждые 500 мс, а TIM3 – каждые 100 мс. Также в CubeMx включаем прерывания таймеров, на примере TIM3:

Активация прерываний.

Генерируем код и осуществляем два действия:

  • переименовываем main.c в main.cpp
  • из файла stm32f1xx_it.c выкидываем с корнем обработчики TIM2_IRQHandler() и TIM3_IRQHandler(), что естественно, так как прерывания обслуживаются внутри соответствующих модулей.

Инициализацию оставляем на откуп CubeMx, при желании выкинуть HAL — просто помещаем все настройки в функцию init() конкретного класса. И да, если в процессе работы еще раз сгенерировать код через Cube, то будет создан main.c без пользовательского кода, вместо того, чтобы обновить main.cpp = Так что с этим разбираться нужно будет вручную, перенося либо код из main.cpp в новый main.c с последующим переименованием, либо в обратном направлении.

Для демо-проекта берем простейшую задачу: при нажатой кнопке (PA3) по переполнению TIM2  (каждые 500 мс в данном случае) изменяем состояние PC13 с целью обеспечить мигание диода. Если кнопка не нажата – бездействуем. По переполнению TIM3 просто инкрементируем счетчик extraTimerCounter.

Так будет выглядеть реализация с учетом созданного в первой части статьи, создаем соответствующие объекты:

Timer timer;
Timer extraTimer;
Gpio output;
Gpio input;

Инициализируем все и запускаем таймеры, в принципе, инициализацию можно перекинуть и в конструктор:

output.init(GPIOC, GPIO_PIN_13);
input.init(GPIOA, GPIO_PIN_3);

timer.init(&htim2, TIM2_IRQn);
timer.start(Timer::Mode::Irq);

extraTimer.init(&htim3, TIM3_IRQn);
extraTimer.start(Timer::Mode::Irq);

В while(1) проверяем прерывания и производим необходимые действия:

// Handle TIM2 interrupt
bool interruptOccured = timer.getInterruptFlag();
if (interruptOccured == true)
{
  if (input.read() == Gpio::State::Set)
  {
	output.toggle();
  }
  timer.clearInterruptFlag();
}
// Handle TIM3 interrupt
interruptOccured = extraTimer.getInterruptFlag();
if (interruptOccured == true)
{
  extraTimerCounter++;
  extraTimer.clearInterruptFlag();
}

Все, на этом процесс завершен, собираем, прошиваем, проверяем. В результате чего имеем наглядную возможность убедиться в полной работоспособности 👍 Я опустил кусок, связанный с переносом таблицы векторов прерываний, который мы уже обсудили. Он присутствует в самом начале main():

for (uint16_t i = 0; i < constants::isrVectorTableSize; i++)
{
	ramVectorTable[i] = g_pfnVectors[i];
}

__disable_irq();
SCB->VTOR = (uint32_t)&ramVectorTable;
__DSB();
__enable_irq();

PeripheralUnit::setIsrVectorTable(ramVectorTable);

Вот, в общем-то, на этом и все, пожалуй. Я не добавлял никаких проверок на передаваемые аргументы и т. д. и т. п., просто быстрый вариант решения конкретной задачи.  И по итогу, полный код файлов и ссылка на полный проект:

#ifndef IRQCALLBACK_H_
#define IRQCALLBACK_H_



#include "stm32f1xx_hal.h"
#include "constants.h"
#include <vector>
#include <memory>



typedef void (*SimpleCallback)(void);



class IrqCallbackBase
{
public:
  IrqCallbackBase(SimpleCallback function)
  {
    callback = function;
  }

  static void staticInvoke(uint8_t index)
  {
    irqPeripherals[index]->interruptHandler();
  }

  static std::vector<PeripheralUnit*> irqPeripherals;

  operator SimpleCallback() const
  {
    return callback;
  }

private:
  SimpleCallback callback;
};



template <uint8_t I> class IrqDynamicCallback : public IrqCallbackBase
{
public:
  IrqDynamicCallback() : IrqCallbackBase(&IrqDynamicCallback<I>::generatedStaticFunction)
  {
  }

  static void generatedStaticFunction()
  {
    return staticInvoke(I);
  }
};



template<uint8_t I> struct IrqDynamicCallbackFactory
{
  static inline std::shared_ptr<IrqCallbackBase> create(uint8_t index)
  {
    if (index == I)
    {
      return std::shared_ptr<IrqCallbackBase>(new IrqDynamicCallback<I>());
    }
    else
    {
      return IrqDynamicCallbackFactory<I + 1>::create (index);
    }
  }
};



struct Overflow
{
  static inline std::shared_ptr<IrqCallbackBase> create(uint8_t index)
  {
    return NULL;
  }
};
template<> struct IrqDynamicCallbackFactory<constants::isrVectorTableSize> : Overflow {};



std::shared_ptr<IrqCallbackBase> createCallback(uint8_t index)
{
  return IrqDynamicCallbackFactory<0>::create(index);
}



#endif /* IRQCALLBACK_H_ */
#include "PeripheralUnit.h"
#include "IrqCallback.h"



constexpr int isrVectorTableOffset = 16;

uint32_t* PeripheralUnit::isrVectorTable;
std::vector<PeripheralUnit*> IrqCallbackBase::irqPeripherals;



PeripheralUnit::PeripheralUnit() :
    isInitialized(false),
    interruptFlag(false)
{
}



PeripheralUnit::~PeripheralUnit()
{
}



void PeripheralUnit::setIsrVectorTable(uint32_t* table)
{
	PeripheralUnit::isrVectorTable = table;
}



bool PeripheralUnit::getInterruptFlag()
{
  return interruptFlag;
}



void PeripheralUnit::clearInterruptFlag()
{
  interruptFlag = false;
}



void PeripheralUnit::init()
{
  isInitialized = true;
}



void PeripheralUnit::initIsr(IRQn_Type irq)
{
  IrqCallbackBase::irqPeripherals.push_back(this);

  isrVectorTable[isrVectorTableOffset + irq] =
      (uint32_t)((SimpleCallback)(*createCallback(IrqCallbackBase::irqPeripherals.size() - 1)));
}


void PeripheralUnit::interruptHandler()
{
  interruptFlag = true;
}
#ifndef PERIPHERALUNIT_H_
#define PERIPHERALUNIT_H_



#include "stm32f1xx_hal.h"



class PeripheralUnit
{
public:
	PeripheralUnit();
	virtual ~PeripheralUnit();
  virtual void interruptHandler();
	bool getInterruptFlag();
	void clearInterruptFlag();
	static void setIsrVectorTable(uint32_t *table);

protected:
	void init();
	void initIsr(IRQn_Type irq);

	bool isInitialized;
	bool interruptFlag;

private:
	static uint32_t *isrVectorTable;
};



#endif /* PERIPHERALUNIT_H_ */
#include "Timer.h"



Timer::Timer() :
   handle(NULL)
{
}



Timer::~Timer()
{
}



void Timer::init(TIM_HandleTypeDef *timerHandle, IRQn_Type irq)
{
  handle = timerHandle;
  PeripheralUnit::initIsr(irq);
  PeripheralUnit::init();
}


void Timer::start(Mode operationMode)
{
  switch(operationMode)
  {
    case (Mode::Base):
      HAL_TIM_Base_Start(handle);
      break;

    case (Mode::Irq):
      HAL_TIM_Base_Start_IT(handle);
      break;

    case (Mode::Dma):
    default:
      break;
  }
}



void Timer::interruptHandler()
{
  if (__HAL_TIM_GET_FLAG(handle, TIM_FLAG_UPDATE) != RESET)
  {
    if (__HAL_TIM_GET_IT_SOURCE(handle, TIM_IT_UPDATE) != RESET)
    {
      __HAL_TIM_CLEAR_IT(handle, TIM_IT_UPDATE);
      interruptFlag = true;
    }
  }
}
#ifndef TIMER_TIMER_H_
#define TIMER_TIMER_H_



#include "PeripheralUnit.h"



class Timer : public PeripheralUnit
{
public:
  enum class Mode {
    Base,
    Irq,
    Dma
  };

  Timer();
  virtual ~Timer();
  void init(TIM_HandleTypeDef *timerHandle, IRQn_Type irq);
  void start(Mode);

private:
  void interruptHandler();

  TIM_HandleTypeDef *handle;
};



#endif /* TIMER_TIMER_H_ */
#include "Gpio.h"



Gpio::Gpio() :
  port(NULL),
  pin(0)
{
}



Gpio::~Gpio()
{
}



void Gpio::init(GPIO_TypeDef *gpioPort, uint16_t gpioPin)
{
  port = gpioPort;
  pin = gpioPin;
  PeripheralUnit::init();
}



void Gpio::write(State state)
{
  switch(state)
  {
    case State::Set:
      HAL_GPIO_WritePin(port, pin, GPIO_PIN_SET);
      break;

    case State::Reset:
      HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET);
      break;

    default:
      break;
  }
}



Gpio::State Gpio::read()
{
  State currentState = State::Reset;

  if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_SET)
  {
    currentState = State::Set;
  }

  return currentState;
}



void Gpio::toggle()
{
  HAL_GPIO_TogglePin(port, pin);
}
#ifndef GPIO_GPIO_H_
#define GPIO_GPIO_H_



#include "PeripheralUnit.h"



class Gpio : public PeripheralUnit
{
public:
  enum class State {
    Set,
    Reset
  };

  Gpio();
  virtual ~Gpio();
  void init(GPIO_TypeDef *gpioPort, uint16_t gpioPin);
  void write(State state);
  State read();
  void toggle();

private:
  GPIO_TypeDef *port;
  uint16_t pin;
};



#endif /* GPIO_GPIO_H_ */
/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2022 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "constants.h"
#include "Timer.h"
#include "Gpio.h"

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim2;
TIM_HandleTypeDef htim3;

/* USER CODE BEGIN PV */
Timer timer;
Gpio output;
Gpio input;

Timer extraTimer;
uint32_t extraTimerCounter = 0;

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM2_Init(void);
static void MX_TIM3_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
uint32_t ramVectorTable[constants::isrVectorTableSize] __attribute__(( aligned (constants::isrVectorTableAlingment) ));
extern uint32_t g_pfnVectors[constants::isrVectorTableSize];

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  for (uint16_t i = 0; i < constants::isrVectorTableSize; i++)
  {
    ramVectorTable[i] = g_pfnVectors[i];
  }

  __disable_irq();
    SCB->VTOR = (uint32_t)&ramVectorTable;
  __DSB();
  __enable_irq();

  PeripheralUnit::setIsrVectorTable(ramVectorTable);

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM2_Init();
  MX_TIM3_Init();
  /* USER CODE BEGIN 2 */

  output.init(GPIOC, GPIO_PIN_13);
  input.init(GPIOA, GPIO_PIN_3);
  
  timer.init(&htim2, TIM2_IRQn);
  timer.start(Timer::Mode::Irq);

  extraTimer.init(&htim3, TIM3_IRQn);
  extraTimer.start(Timer::Mode::Irq);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    // Handle TIM2 interrupt
    bool interruptOccured = timer.getInterruptFlag();
    if (interruptOccured == true)
    {
      if (input.read() == Gpio::State::Set)
      {
        output.toggle();
      }
      timer.clearInterruptFlag();
    }

    // Handle TIM3 interrupt
    interruptOccured = extraTimer.getInterruptFlag();
    if (interruptOccured == true)
    {
      extraTimerCounter++;
      extraTimer.clearInterruptFlag();
    }
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief TIM2 Initialization Function
  * @param None
  * @retval None
  */
static void MX_TIM2_Init(void)
{

  /* USER CODE BEGIN TIM2_Init 0 */

  /* USER CODE END TIM2_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  /* USER CODE BEGIN TIM2_Init 1 */

  /* USER CODE END TIM2_Init 1 */
  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 7199;
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = 5000;
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM2_Init 2 */

  /* USER CODE END TIM2_Init 2 */

}

/**
  * @brief TIM3 Initialization Function
  * @param None
  * @retval None
  */
static void MX_TIM3_Init(void)
{

  /* USER CODE BEGIN TIM3_Init 0 */

  /* USER CODE END TIM3_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  /* USER CODE BEGIN TIM3_Init 1 */

  /* USER CODE END TIM3_Init 1 */
  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 719;
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 10000;
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM3_Init 2 */

  /* USER CODE END TIM3_Init 2 */

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);

  /*Configure GPIO pin : PC13 */
  GPIO_InitStruct.Pin = GPIO_PIN_13;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  /*Configure GPIO pin : PA3 */
  GPIO_InitStruct.Pin = GPIO_PIN_3;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %drn", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

Ссылка на проект: MT_STM32_CppDemoProject.

Ядро 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).
4. FreeRTOS: использование стека и проверка стека на переполнение.
5. FreeRTOS: практическое применение, часть 6 (устранение проблем).
6. Проектирование стека и кучи в IAR.
7. FreeRTOS, STM32: отладка ошибок и исключений.
8. IAR C-SPY: предупреждение о переполнении стека.

Универсальные асинхронные последовательные средства связи

231

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(RingBuffer_GetDataLength(&txBuf) > 0) {

RingBuffer_Read(&txBuf, &txData, 1); HAL_UART_Transmit_IT(huart, &txData, 1);

}

}

Функция RingBuffer_Read() является не очень быстрой, какой могла бы быть ее более производительная реализация. В некоторых реальных ситуациях общая нагрузка на процедуру HAL_UART_TxCpltCallback() (которая вызывается из процедуры ISR) может быть слишком высокой. Если это ваш случай, вы можете создать такую функцию:

void processPendingTXTransfers(UART_HandleTypeDef *huart) { if(RingBuffer_GetDataLength(&txBuf) > 0) {

RingBuffer_Read(&txBuf, &txData, 1); HAL_UART_Transmit_IT(huart, &txData, 1);

}

}

Затем вы могли бы вызвать данную функцию из основного кода приложения или из задачи с более низким уровнем привилегий, если используете ОСРВ.

При работе с внешними подключениями обработка ошибок является аспектом, который мы должны строго учитывать. Периферийное устройство UART в STM32 предлагает несколько флагов ошибок, относящихся к ошибкам обмена данными. Более того, возможно, что соответствующее ошибке прерывание не будет обработано при ее возникновении.

CubeHAL предназначен для автоматического обнаружения условий ошибок и предупреждения нас о них. Нам нужно только реализовать функцию HAL_UART_ErrorCallback() внутри кода нашего приложения. HAL_UART_IRQHandler() автоматически вызовет ее в случае возникновения ошибки. Чтобы понять, какая именно произошла ошибка, мы можем проверить значение поля UART_HandleTypeDef->ErrorCode. Список кодов ошибок приве-

ден в таблице 7.

Таблица 7: Список возможных значений UART_HandleTypeDef->ErrorCode

Код ошибки UART

Описание

HAL_UART_ERROR_NONE

Ошибка не произошла

HAL_UART_ERROR_PE

Ошибка при проверке четности

HAL_UART_ERROR_NE

Ошибка вследствие зашумления

HAL_UART_ERROR_FE

Ошибка кадрирования данных

HAL_UART_ERROR_ORE

Ошибка вследствие переполнения

HAL_UART_ERROR_DMA

Ошибка передачи посредством DMA

HAL_UART_IRQHandler() разработан таким образом, что нам не нужно вдаваться в подробности реализации обработки ошибок UART. Код HAL автоматически выполнит все необходимые шаги для обработки ошибки (например, сброс флагов событий, бита отложенного состояния и т. д.), оставляя нам ответственность за обработку ошибки на уровне

HAL_UART_ErrorCallback()

Универсальные асинхронные последовательные средства связи

232

приложения (например, мы можем попросить другой узел повторно отправить поврежденный кадр данных).

Прочитайте внимательно

Во время написания данной главы, 2 декабря 2015 года, едва уловимый баг не позволял правильно контролировать ошибку переполнения Overrun error. Вы можете прочитать больше о нем на официальном форуме ST17. Этот баг можно воспроизвести даже со вторым примером данной главы. Запустите пример на Nucleo и нажмите клавишу «3» на клавиатуре, оставив ее нажатой. Через некоторое время микропрограмма зависнет. Это происходит потому, что после возникновения ошибки переполнения HAL вновь не перезапускает процесс получения. Вы можете устранить данный баг с помощью функции

следующим образом:

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if(huart->ErrorCode == HAL_UART_ERROR_ORE)

HAL_UART_Receive_IT(huart, readBuf, 1);

}

}

8.6. Перенаправление ввода-вывода

В Главе 5 мы узнали, как использовать функцию полухостинг для отправки отладочных сообщений на консоль OpenOCD с помощью функции Си printf(). Если вы уже использовали данную функцию, то вы знаете, что существует два сильных ограничения:

полухостинг значительно замедляет выполнение микропрограммы;

он также не позволяет вашей микропрограмме работать, если она выполняется без сеанса отладки (из-за того, что полухостинг реализован с использованием программных точек останова).

Теперь, когда мы знакомы с управлением UART, мы можем переопределить необходимые системные вызовы (_write(), _read() и т. д.), чтобы перенаправить стандартные потоки STDIN, STDOUT и STDERR на USART2 Nucleo. Это можно легко сделать следующим образом:

Имя файла: system/src/retarget/retarget.c

14 #if !defined(OS_USE_SEMIHOSTING)

15

16#define STDIN_FILENO 0

17#define STDOUT_FILENO 1

18#define STDERR_FILENO 2

20UART_HandleTypeDef *gHuart;

22void RetargetInit(UART_HandleTypeDef *huart) {

23gHuart = huart;

25/* Отключение буферизации ввода/вывода для потока STDOUT,

17 https://community.st.com/s/question/0D50X00009XkflRSAR/haluartirqhandler-bug

Универсальные асинхронные последовательные средства связи

233

26* чтобы символы отправлялись сразу после их печати. */

27setvbuf(stdout, NULL, _IONBF, 0);

28}

29

30int _isatty(int fd) {

31if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)

32return 1;

33

34errno = EBADF;

35return 0;

36}

37

38int _write(int fd, char* ptr, int len) {

39HAL_StatusTypeDef hstatus;

40

41if (fd == STDOUT_FILENO || fd == STDERR_FILENO) {

42hstatus = HAL_UART_Transmit(gHuart, (uint8_t *) ptr, len, HAL_MAX_DELAY);

43if (hstatus == HAL_OK)

44return len;

45else

46return EIO;

47}

48errno = EBADF;

49return -1;

50}

51

52int _close(int fd) {

53if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)

54return 0;

55

56errno = EBADF;

57return -1;

58}

59

60int _lseek(int fd, int ptr, int dir) {

61(void) fd;

62(void) ptr;

63(void) dir;

64

65errno = EBADF;

66return -1;

67}

68

69int _read(int fd, char* ptr, int len) {

70HAL_StatusTypeDef hstatus;

71

72if (fd == STDIN_FILENO) {

73hstatus = HAL_UART_Receive(gHuart, (uint8_t *) ptr, 1, HAL_MAX_DELAY);

74if (hstatus == HAL_OK)

75return 1;

Универсальные асинхронные последовательные средства связи

234

76else

77return EIO;

78}

79errno = EBADF;

80return -1;

81}

82

83int _fstat(int fd, struct stat* st) {

84if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) {

85st->st_mode = S_IFCHR;

86return 0;

87}

88

89errno = EBADF;

90return 0;

91}

92

93 #endif //#if !defined(OS_USE_SEMIHOSTING)

Чтобы перенаправить стандартные потоки в вашей микропрограмме, вы должны удалить макрос OS_USE_SEMIHOSTING на уровне проекта и инициализировать библиотеку, вызывая RetargetInit(), передавав указатель на экземпляр UART2 UART_HandleTypeDef. Например, следующий код показывает, как использовать функции printf()/scanf() в вашей микропрограмме:

int main(void) { char buf[20]; HAL_Init();

SystemClock_Config();

MX_GPIO_Init();

MX_USART2_UART_Init();

RetargetInit(&huart2);

printf(«Write your name: «); scanf(«%s», buf); printf(«rnHello %s!rn«, buf); while(1);

}

Если вы собираетесь использовать функции printf()/scanf() для печати/чтения типов данных с плавающей точкой float в консоль последовательного порта (также как и если вы собираетесь использовать sprintf() и аналогичные процедуры), вам нужно явно включить поддержку чисел формата float в newlib-nano – более компактной версии библиотеки среды выполнения Си для встраиваемых систем. Для этого перейдите в меню

Project → Properties…, затем перейдите в C/C++ Build → Settings → Cross ARM C++ Linker → Miscellaneous и установите флажок Use float with nano printf/scanf в соот-

ветствии с нужной вам функцией, как показано на рисунке 13. Это увеличит размер бинарного файла микропрограммы.

Универсальные асинхронные последовательные средства связи

235

Рисунок 13: Как включить поддержку чисел формата float в printf() и scanf()

Понравилась статья? Поделить с друзьями:
  • Error handler stardew valley
  • Error handler re entered exiting now перевод
  • Error handler re entered exiting now autocad 2016 при печати
  • Error handler npm
  • Error handler kotlin