Прерывания в STM32.
Или опус от том, что я хочу получить.
Прерывания генерируемые под HAL в CubeMX сделаны просто, до безобразия. В каталог с проектом кидается файл stm32f4xx_it.c, в котором описывается прерывание, если мы его выбрали в Кубе.
Примечание: В примере кода из этого файла и в дальнейшем я буду убирать строки не имеющие отношения к теме.
Вот код из этого файла:
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
}
void UART4_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart4);
}
void UART5_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart5);
}
Как видим, выбрав 3 периферийных устройства UART и USART, поставив галочки в закладке отведённой под прерывания, мы получаем три обработчика прерываний. В них идут строки отвечающие за стандартную обработку прерываний HALом. Так же в этом файле можно дописать свои строки для своих нужд. И расширить функциональность обработчика прерываний.
Перед тем, как рассказать что хочу я, небольшое вступление.
Так исторически сложилось, что я сначала попробовал писать на С++ и только позже мне в руки попал STM32. Концепция классов мне понравилась. Мне понравилось то, что на классах можно писать библиотеки, в код которых не нужно лазить ни мне, ни стороннему программисту, если класс написан корректно. И я начал писать свои библиотеки используя классы и готовые функции HAL. Столкнулся с тем, что HAL медлителен и некоторые его функции не совсем корректно работают. (Примеры показать не могу, так как после того, как у меня стало получаться, я не заархивировал то, что я делал до этого, а просто потёр). Кода я начал этим заниматься у меня не было опыта ни в HAL, ни С++. Поэтому у меня был выбор каким путём мне идти. Что самое интересное, огромное большинство программистов, которые привыкли к HAL, почему то воспринимают С++ и классы в штыки и не могут меня понять. Но это их путь, а это мой. Так как я писал раньше только на Ассембелере, мне ближе стало писать с помощью прямых обращений к периферии МК, пользуясь только дефайнами и функциями, которые предоставляет CMSIS.
Теперь по классам могу сказать только несколько слов о том, как я это себе представляю.
Примечание: Основные года, на которые пришлись мои программерские изыски - это 1984-1991 и 2017 и по сей день. А на С++ я начал программировать с 2020 года. Поэтому я сейчас могу путаться в современной терминологии и пользоваться старой, времён СССР. Кроме того я практик, а не теоретик. Я запоминаю и понимаю только то, что сам "ручками пощупал" Поэтому прошу относиться к этому снисходительно.
Класс для меня - это чёрный ящик. Обратившись к которому я могу получить результат. И в котором есть тоже небольшие "коробочки", к которым я могу так или иначе обратиться.
Теперь то же самое, но с прерыванием.
Таким образом у меня сделано в следующей реализации библиотек UART.
extern "C" void UART4_IRQHandler(void)
{
// Прерывание от приёмника.
if((UART4->SR & USART_SR_RXNE) && (UART4->CR1 & USART_CR1_RXNEIE)) // Если прерывание приема разрешено и что-то получили
{
// Обрабатываем
}
// Прерывание от передатчика.
if((UART4->SR & USART_SR_TXE) && (UART4->CR1 & USART_CR1_TXEIE)) // Если прерывание разрешено и произошло,
{
// Обрабатываем
}
}
// UART5
extern "C" void UART5_IRQHandler(void)
{
//Если прерывание приема разрешено и что-то получили
if((UART5->SR & USART_SR_RXNE) && (UART5->CR1 & USART_CR1_RXNEIE))
{
// Обрабатываем
}
if((UART5->SR & USART_SR_TXE) && (UART5->CR1 & USART_CR1_TXEIE)) // Если передачтик пуст
{
// Обрабатываем
}
}
Этот код находится внутри библиотеки и его нельзя поменять.
Достоинство:
-
Обработчик находится внутри класса UART и нам в него не нужно лазить, что бы изменить его, так как свою функцию он обрабатывает.
Недостаток:
-
Исходит из достоинства. Мы не можем поменять обработчик (Ну нельзя в библиотеку лазить.);
-
Все прерывания должны быть описаны заранее, что бы компилятор мог расставить их по своим местам. А это лишний код, который нам не нужен. И невозможность написать другую библиотеку, которая будет тоже пользоваться UART, например 1-Wire через UART.
Правда у второго пункта есть обходной манёвр. Использовать класс UART уже в самой библиотеке 1-Wire. Но этот способ уже не катит, если использовать DMA. А в будущем я рассчитываю это сделать.
Второй способ, но он годен только для периферии, которая представлена в единичном экземпляре, например RTC.
Это кусок кода из main():
uart myUART(USART1, false);
RTC_main myRTC(RTC_LSE);
void setIRQ(void);
int main(void)
{
myUART.init(256000);
myUART.println(myRTC.Reinit());
myRTC.setIT_WUCK(setIRQ, 1); // Инициализация внешнего обработчика прерываний
while (1)
{
delay(5000);
}
}
void setIRQ(void) // Это у нас внешний обработчик прерывания
{
Led_01.digitalTogglePin();
sprintf(SendBuff, "Date : %02d/%02d/%04d - %d", myRTC.getDate(), myRTC.getMonth(), myRTC.getYear(), myRTC.getWeek());
myUART.print(SendBuff);
sprintf(SendBuff, " Time : %02d:%02d:%02d", myRTC.getHour(), myRTC.getMinute(), myRTC.getSeconds());
myUART.println(SendBuff);
}
Часть содержимого класса RTC:
void RTC_main::setIT_WUCK(callback_IRQ ExternIT, uint16_t Timer)
{
// Устанавливаем прерывание
EXTI->IMR |= EXTI_IMR_MR22;
EXTI->RTSR |= EXTI_RTSR_TR22;
callback_RTC = ExternIT; // Здесь мы навешиваем внешний обработчик прерывания
NVIC_EnableIRQ(RTC_WKUP_IRQn);
}
И внутренний обработчик прерывания:
extern RTC_main myRTC;
// Прерывание
extern "C" void RTC_WKUP_IRQHandler(void)
{
PWR->CR |= PWR_CR_DBP; // Разрешить доступ к Backup области
RTC->ISR &= ~RTC_ISR_WUTF;
EXTI->PR |= EXTI_PR_PR22;
PWR->CR &= ~PWR_CR_DBP; // запретить доступ к Backup области
if (myRTC.callback_RTC != NULL) myRTC.callback_RTC();
}
Как видно, он ничего не делает, только сбрасывает флаги и обращается к внешней функции обработки.
Достоинство:
-
Мы добились внешней обработки прерывания, которую мы можем описать в функции main().
Недостаток:
-
Если RTC было бы два, то как и предыдущем примере, нам бы пришлось описывать каждый RTC в обязательном порядке;
-
Строка extern RTC_main myRTC; Она однозначно заставляет свой объект обзывать myRTC и никак иначе и, если эта библиотека нам не нужна и мы её не используем, компилятор начинает ругаться, что myRTC не определён.
Из пункта 2 есть два выхода:
-
На время убрать его из каталога, что неприемлемо по "религиозным мотивам";
-
В CubeIDE исключить его из проекта. Что нормально, но некрасиво.
Есть третий выход - сделать как в HAL. Просто вынести таблицу за пределы любых библиотек и писать все прерывания там. Но тогда теряется смысл делать всё на классах. Ведь некоторые библиотеки могут сами предоставить обработку прерывания и нам не нужно туда вмешиваться. Но иногда нам нужно вмешаться и как быть?
Есть четвёртый выход - Это перенести таблицу векторов из ПЗУ в ОЗУ. И менять адреса векторов по мере необходимости. Фирма ST предусмотрела в МК такой ход. Я уже научился это делать. Но тут возникают другие проблемы. Прерывание в классе можно сделать только статической функцией, а это, как я понимаю и проверил на практике, будет выглядеть так (на примере TIM6, TIM7.)
Получается, что инициализируя TIM6 мы получаем то что нужно. Но нам хочется использовать и TIM7 тоже. Мы создаём объект myTIM7. И его прерывание перекрывает прерывание которое ранее на себя зарезервировал класс myTIM6. Таким образом прерывание от TIM6 перестаёт выполняться.
#define VECTORTABLE_SIZE (97)
#define VECTORTABLE_ALIGNMENT (0x100ul)
volatile uint32_t vectorTable_RAM[VECTORTABLE_SIZE] __attribute__(( aligned (VECTORTABLE_ALIGNMENT) ));
extern uint32_t g_pfnVectors[]; // Указатель на вектора, находится в ASM скрипте
bool Flag_TIM6 = false;
bool Flag_TIM7 = false;
uart myUART(USART1, false);
TIM_Base myTIM6(Tim_Base_6);
TIM_Base myTIM7(Tim_Base_7);
int main(void)
{
SystemClock_Config(Quartz_8);
/*--------------------------------------------------------------------------------------
* Перекидывание векторов прерывания в ОЗУ
*/
__disable_irq(); // Отключаем прерывания
for (uint8_t i = 0; i < VECTORTABLE_SIZE; i++) vectorTable_RAM[i] = g_pfnVectors[i]; // Копируем таблицу векторов на новое место в ОЗУ
__DMB(); // Data Memory Barrier to ensure write to memory is completed
SCB->VTOR = (uint32_t)&vectorTable_RAM; // Set VTOR to the new vector table location
__DSB(); // Data Synchronization Barrier to ensure all
__enable_irq(); // Разрешаем прерывания
//--------------------------------------------------------------------------------------
myUART.init(256000);
myTIM6.Init(41999, 1000, myTIM_ISR6);
myTIM7.Init(41999, 2000, myTIM_ISR7);
while(1)
{
if(Flag_TIM6){ myUART.println("Hello TIM6"); Flag_TIM6 = false;}
if(Flag_TIM7){ myUART.println("Hello TIM7"); Flag_TIM7 = false;}
}
}
/*
* Функции
*/
void myTIM_ISR6(void)
{
__NOP();
Flag_TIM6 = true;
}
void myTIM_ISR7(void)
{
__NOP();
Flag_TIM7 = true;
}
И установка этого прерывания. Это не моя идея, я её подсмотрел. Человек дал не полный код, может быть у него это и решено. Но на связь он не выходит.
Это код из класса:
extern uint32_t vectorTable_RAM[];
class TIM_Base *TIM_Base::self = NULL;
void TIM_Base::Init(uint16_t Prescaler, uint16_t Counter, callback_IRQ ExternIT)
{
if(ExternIT != NULL)
{
__disable_irq();
self = this;
vectorTable_RAM[_TimIrq + 16] = (uint32_t) myISR_TIM; // Подменяем на свой вектор прерывания на класс, но не работает при создании
// второго подобного объекта, объект созданный позже перехватывает обработку
__DMB();
__enable_irq();
pfUserISR = myISR_TIM; // Ссылка на наш, внешний обработчик прерывания
_TimBaseN->DIER |= TIM_DIER_UIE; // Разрешаем прерывания от переполнения
NVIC_EnableIRQ(_TimIrq);
}
}
void TIM_Base::myISR_TIM(void)
{
self->_TimBaseN->SR &= ~TIM_SR_UIF;
self->pfUserISR();
}
Этот способ достаточен при создании одного объекта. Создание ещё одного объекта всё крашит. Прерывание продолжает вызываться, но из за того, что строка self->_TimBaseN->SR &= ~TIM_SR_UIF; сбрасывает только флаг TIM7, МК зависает в глухой обработке прерывания от TIM6.
Но работает следующий код:
в main()
void myTIM_ISR6(void)
{
if((TIM6->SR & TIM_SR_UIF) && (TIM6->CR1 & TIM_CR1_CEN))
{
TIM6->SR &= ~TIM_SR_UIF;
}
__NOP();
Flag_TIM6 = true;
}
void myTIM_ISR7(void)
{
if((TIM7->SR & TIM_SR_UIF) && (TIM7->CR1 & TIM_CR1_CEN))
{
TIM7->SR &= ~TIM_SR_UIF;
}
__NOP();
Flag_TIM7 = true;
}
В классе.
void TIM_Base::Init(uint16_t Prescaler, uint16_t Counter, callback_IRQ ExternIT)
{
if(ExternIT != NULL)
{
__disable_irq();
self = this;
vectorTable_RAM[_TimIrq + 16] = (uint32_t) ExternIT; // Подменяем на свой вектор прерывания, так работает, но обслуживание флагов
// приходится переносить во внешнюю функцию
__DMB();
__enable_irq();
pfUserISR = ExternIT; // Ссылка на наш, внешний обработчик прерывания
_TimBaseN->DIER |= TIM_DIER_UIE; // Разрешаем прерывания от переполнения
NVIC_EnableIRQ(_TimIrq);
}
}
void TIM_Base::myISR_TIM(void)
{
if((TIM6->SR & TIM_SR_UIF) && (TIM6->CR1 & TIM_CR1_CEN))
{
TIM6->SR &= ~TIM_SR_UIF;
self->pfUserISR();
}
if((TIM7->SR & TIM_SR_UIF) && (TIM7->CR1 & TIM_CR1_CEN))
{
TIM7->SR &= ~TIM_SR_UIF;
self->pfUserISR();
}
}
Почему так работает, не совсем понятно. Но опять же это не то, что нужно. Представьте, как будет выглядеть ранее описанная библиотека UART.
В общем то мне необходимо решение этой проблемы, я пока не нашёл ответа, может кто нибудь сможет помочь.
Плоховато читается, когда код не в своем блоке "код", а текстом.