Динамическое управление прерываниями в ARM

в 10:11, , рубрики: C, прерывания, программирование микроконтроллеров

Сегодня я расскажу, как можно динамически подменять обработчики прерываний в процессорах ARM на примере микроконтроллеров STM32. Описанный мною способ работает в процессорах ARM Cortex M3 и выше.

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

Для того, чтобы понять, как программно изменить обработчик прерывания, рассмотрим, как процессор определяет, что именно нужно делать при возникновении прерывания. В микроконтроллерах STM32 таблица векторов прерываний располагается в самом начале исполняемого кода. Первое 32-разрядное слово исполняемой программы — это указатель стека. Обычно он равен максимальному адресу оперативной памяти контроллера. Далее идёт указатель на Reset_Handler, NMI_Handler и другие обработчики прерываний. Теоретически, чтобы динамически устанавливать для обработки прерывания новую функцию, нужно просто переписать один из этих указателей. Но аппаратные ограничения платформы не позволят этого сделать, ведь программа в STM32 исполняется из FLASH-памяти, и чтобы записать в неё новое слово, надо сначала стереть всю страницу, а это не входит в наши планы: программу нельзя повредить. Поэтому давайте попробуем перебросить таблицу прерываний в оперативную память и менять векторы уже там. Но остаётся вопрос: а как ядро узнает, что таблица перемещена? Ведь простое копирование таблицы не даст результата, если при возникновении прерывания ядро обратится к старой таблице и вызовет старый обработчик. Для разрешения этой ситуации есть регистр VTOR (Vector Table Offset Register). Описание этого регистра вы не найдёте в документации на контроллер, не знают о нём и отладчики. Информацию об этом регистре следует искать в документации на ядро ARM, также можете найти его в заголовочном файле core_cm3.h. Регистр располагается по адресу 0xE000ED08, причём значение его должно быть кратным 0x400. Это означает, что нельзя помещать таблицу прерываний куда вздумается. Не будем ломать голову, и просто расположим её в начале оперативной памяти, а после этого установим новое значение регистра VTOR. Заполнив новую таблицу прерываний, испытаем её с помощью прерывания системного таймера.

Для реализации поставленной задачи воспользуемся компилятором gcc, библиотекой CMSIS. Нам потребуется модифицировать файл startup_stm32f103xb.asm и скрипт линкера. В скрипте линкера нужно явно указать расположение таблицы прерываний в оперативной памяти и объявить переменные начала и конца таблицы прерываний. В файле startup_stm32f103xb.asm нужно выполнить копирование таблицы и установить новое значение регистра VTOR. Почему я решил модифицировать библиотечный файл, чего делать обычно не рекомендуется? Дело в том, что операции размещения секций памяти следует выполнять как можно раньше, и именно такую операцию и выполняет код этого файла: копирует глобальные переменные из секции .data и инициализирует статическую память (.bss) нулями. Мы лишь допишем копирование секции .isr_vector.

Приступим к модификации скрипта линкера. Перепишем секцию .isr_vector следующим образом:

  /* The startup code goes first into FLASH */
  .isr_vector :
  {
    . = ALIGN(4);	//Выровнять курсор по 4-байтному слову
    _svector = .;   //Взять текущее значение курсора - это будет указатель на начало секции
    KEEP(*(.isr_vector)) //Записать секцию .isr_vector в текущую память	
    . = ALIGN(4);	//Выровнять курсор по 4-байтному слову
    _evector = .;	//Взять указатель на конец секции
  } >RAM AT> FLASH 	//Таблица изначально размещена во flash-памяти, но после перемещена в оперативную память
  _sivector = LOADADDR(.isr_vector);	//Взять расположение таблицы прерываний во flash-памяти.

Мы объявили место для размещения таблицы прерываний в оперативной памяти. Теперь переменные, объявленные в скрипте линкера, надо дополнительно объявить в ассемблере. Вставьте этот код куда-нибудь в начало файла.

.word _svector

.word _evector

.word _sivector

Теперь выполним копирование таблицы прерываний. Для этого между инструкциями {bcc FillZerobss} и {bl SystemInit} вставим следующий код:

  movs r1, #0	//Счетчик цикла
  b LoopCopyVectorTable	//Переходим к началу цикла

CopyVectorTable:
  ldr r3, =_sivector	//Записываем в регистр r3 начальный адрес таблицы прерываний во flash-памяти
  ldr r3, [r3, r1]		//Считываем из памяти значение по адресу r3+r1, результат записываем в r3 (r3=*(r3+r1))
  str r3, [r0, r1]		//Записываем по адресу r0+r1 значение регистра r3
  adds r1, r1, #4		//Переходим к следующему слову
LoopCopyVectorTable:
  ldr r0, =_svector 	//Записываем в регистр r0 начальный адрес таблицы прерываний в оперативной памяти
  ldr r3, =_evector		//Записываем в регистр r3 конечный адрес таблицы прерываний в оперативной памяти
  adds r2, r0, r1		//r2=r0+r1
  cmp r2, r3			//Дошли до конца?
  bcc CopyVectorTable 	//Если нет, переходим к копированию текущего слова

Таблица скопирована. Теперь нужно установить значение регистра VTOR. Как уже упоминалось, адрес этого регистра указан в файле core_cm3.h, но давайте не будем стучаться в него из ассемблера, и просто объявим его прямо в этом файле. Напишем определение:

.equ  VTOR, 0xE000ED08

И далее просто разместим эту цифру в конец таблицы прерываний. Для этого в конец секции .isr_vector добавим:

.word VTOR

Мы добились того, чтобы адрес регистра VTOR расположился во flash-памяти контроллера. Теперь запишем в регистр нужное значение. Для этого после кода копирования таблицы прерываний добавим следующий код:

  ldr r0, =_svector //Записываем адрес новой таблицы прерываний в регистр r0
  ldr r2, =VTOR 	//Записываем адрес регистра VTOR в регистр r2
  str r0, [r2]		//Записываем по адреcу, содержащемся в r2 значение r0

Всё. Мы получили новую таблицу прерываний, идентичную стандартной, но теперь имеем возможность динамически её изменять. Теперь можно спокойно переходить к функции main:

  bl __libc_init_array
  b main

И проверять, как наши труды работают:

#define SysTickVectorLoc 0x2000003c	//Адрес вектора прерывания системного таймера
void main();
void SysTick_Handler();
void SysTick_Handler2();

//Обработчик системного таймера, назначается по умолчанию
void SysTick_Handler()
{
	*(uint32_t*)SysTickVectorLoc = (uint32_t)SysTick_Handler2;
}

//Второй обработчик системного таймера
void SysTick_Handler2()
{
        *(uint32_t*)SysTickVectorLoc = (uint32_t)SysTick_Handler;
}

void main()
{
	//Сразу записываем указатель на новую функцию обработки прерывания в таблицу прерываний
	*(uint32_t*)SysTickVectorLoc = (uint32_t)SysTick_Handler2;
	//Настраиваем системный таймер
	SysTick_Config(300);
	while(1)
	{
		__WFI();//Уходим спать. Таймер нас разбудит.
	}

}

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

Код проверялся на контроллере STM32F103. Если есть вопросы или замечания, пишите в комментарии.

Литература

Документация ARM Cortex M3
Документация ARM Assembler

Автор: Divanius

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js