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

Комментарии (9)


  1. zolkko
    18.01.2016 13:48

    Вроде бы в cmsis определена функция NVIC_SetVector?


    1. sam_satan
      18.01.2016 15:47

      Как часть mbed.
      Сам делаю похожим на описанный в статье способ, но все является статическим массивом указателей в классе InterruptManager


  1. ProstoTyoma
    18.01.2016 21:40
    +1

    А не проще завести указатель на функцию, и в обработчике вызывать функцию по этому указателю? А уже сам указатель менять как вам надо. Или расходы на +1 вызов имеют значение?


    1. Pugnator
      19.01.2016 00:40

      Я вот даже использую для этих целей статический член класса. И могу описывать прерывание внутри объекта, и каждый тип «драйвера» имеет свой обработчик. крайне читаемый код получается


      1. Mirn
        19.01.2016 08:24

        просьба выложить пример кода с таким стат. обработчиком в классе


        1. Pugnator
          19.01.2016 09:03
          +1

          В целом я делаю так.

          Тут описание
          Это просто идея, которая мне нравится. Я не делают время-критичных устройств и лишний call-другой мне погоду не делает.
          Как мне кажется, каждый такой «драйвер» получается крайне читаемым с точки зрения алгоритма работы. Но это лишь мое мнение

          К примеру, у нас есть класс для уарт.
            class Uart
            {
            public:
              Uart (short ch, word bd, bool doinit);
              Uart (short ch, word bd, word isrptr = 0);
              ~Uart(void);
          
              static class Uart *self;
              static void isr (void);
              //тут какой-то код
            protected:
              //тут всякое разное
              USART_TypeDef* Reg;
            };
          


          static class Uart *self;
          

          Данный указатель на себя нужен для того, чтобы получить доступ к самому классу изнутри статической функции — нашего обработчика прерываний. Так как хоть привязки к классу нет, но статический метод имеет доступ к другим статическим членам класса.
          USART_TypeDef* Reg
          
          Является указателем на нужный нам регистр нужного порта для автоматизации.
          В инициализации он устанавливается через блок switch, к примеру, так:
          __disable_irq();
          self = this;
          ....
          switch (channel)
          .....
          Reg = (USART_TypeDef*) USART1_BASE;
          IRQ_VECTOR_TABLE[USART1_IRQn + IRQ0_EX] = isrptr;
          ....
          init();
          __enable_irq();
          


          Где наш обработчик выглядит так
          class Uart *Uart::self = nullptr;
          
          ...
          
          
          void Uart::isr(void)
            {
              if (self->Reg->SR & USART_SR_RXNE) //receive
                {
                  self->Reg->SR &= ~USART_SR_RXNE;
                }
              else if (self->Reg->SR & USART_SR_TC) //transfer
                {
                  self->Reg->SR &= ~USART_SR_TC;
                }
            }
          


          Это наш базовый placeholder, тут можно выполнять нужные нам действия.
          Теперь мы поверх этого класса наследуемся, скажем, в класс GPS

          class Gps: public Uart
          {
          public:
          	Gps(short ch, word bd) :
          			Uart::Uart(ch, bd, (word) &gpsisr)
          	{
          		Gps::self = this;
          	}
          	static void gpsisr(void);
          	static class Gps *self;
          };
          


          Спокойно пишем свой обработчик внутри GPS класса

          void Gps::gpsisr(void)
          {
          	if (self->Reg->SR & USART_SR_RXNE)
          	{
          		short a = self->Reg->DR;
          		self->Reg->SR &= ~USART_SR_RXNE;
          		if (self->ready)
          		{
          			return;
          		}
          
          		if (0 == self->nmeastr_len && '$' != a)
          		{
          			return;
          		}	
          .....................И.Т.Д.
          	}
          }
          


          Каждый объект — устройство, работающий поверх UART при создании ставят свой обработчик в таблицу прерываний, сохраняя предыдущее значение. В деструкторе восстанавливают предыдущее состояние.

          Всё то же самое для SPI, I2C, DMA и прочих SDIO


  1. NeoCode
    19.01.2016 09:21

    Я писал для ARM7, там все было проще — есть регистры в которых хранятся указатели на обработчики прерываний, просто записывал в такой регистр новое значение и все. Понадобилось такое, когда нужно было обрабатывать четные и нечетные прерывания от таймера или периферии немного по-разному… Оказалось быстрее написать два обработчика и переключаться между ними в конце обработки очередного прерывания, чем писать «if» внутри единого обработчика.


  1. VitGo
    19.01.2016 13:53
    +1

    одно не понял, а зачем так мудрено копировать ?!!! или это компилятор Си так поизвращался?

    <source lang=«XML»
    CopyVectorTable:
    ldr r0, =_sivector //Записываем в регистр R0 начальный адрес таблицы прерываний во flash-памяти
    ldr r1, =_svector //Записываем в регистр R1 начальный адрес таблицы прерываний в оперативной памяти
    MOV R4, R1
    ldr r2, =_evector //Записываем в регистр R2 конечный адрес таблицы прерываний в оперативной памяти
    loop:
    LDR R3, [R0]+4!
    STR R3, [R1]+4!
    CMP R2, R3
    BNE LOOP

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


    1. VitGo
      19.01.2016 13:59

      ой, правильнее
      CMP R1, R2
      нужно было сделать в цикле…