Сегодня я расскажу, как можно динамически подменять обработчики прерываний в процессорах 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