[Из песочницы] Динамическое управление прерываниями в ARM

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

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

© Habrahabr.ru