Dynamic interrupt management in ARM
When and where may it be needed? Firstly, you can replace interrupt handlers if you are faced with the task of writing a program that is compatible with different hardware platforms. ARM processors have several kernel interrupts that are required for any architecture implementation. But the remaining interrupts are designed for peripherals, and processor manufacturers are free to install these vectors for any peripheral devices available in the processor. This requires dynamically substituting the necessary interrupt handlers for each architecture implementation. Secondly, if your product has high demands on the speed of reaction to external events, sometimes the choice of the desired action inside the interrupt handler is ineffective, and it will be more profitable to change the interrupt vector dynamically.
In order to understand how to programmatically change the interrupt handler, consider how the processor determines what exactly needs to be done when an interrupt occurs. In STM32 microcontrollers, the interrupt vector table is located at the very beginning of the executable code. The first 32-bit word of the executable is the stack pointer. Usually it is equal to the maximum address of the controller RAM. Next comes a pointer to Reset_Handler, NMI_Handler and other interrupt handlers. Theoretically, in order to dynamically set a new function for interrupt processing, you just need to rewrite one of these pointers. But the hardware limitations of the platform will not allow this, because the program in STM32 is executed from FLASH-memory, and to write a new word into it, we must first erase the entire page, and this is not included in our plans: the program cannot be damaged. Therefore, let's try to transfer the interrupt table into RAM and change the vectors already there. But the question remains: how does the kernel know that the table has been moved? After all, simply copying the table will not give a result if, when an interrupt occurs, the kernel will turn to the old table and call the old handler. To resolve this situation, there is a VTOR (Vector Table Offset Register) register. You will not find a description of this register in the documentation for the controller, and debuggers do not know about it. Information on this register should be sought in the documentation for the ARM core, you can also find it in the header file core_cm3.h. The register is located at 0xE000ED08, and its value must be a multiple of 0x400. This means that you can not put the interrupt table wherever you want. Let's not rack our brains and just place it at the beginning of the RAM, and after that set the new value of the VTOR register. Filling out the new interrupt table, we will test it with the interrupt of the system timer.
To implement this task, we will use the gcc compiler, the CMSIS library. We will need to modify the startup_stm32f103xb.asm file and the linker script. In the linker script, you must explicitly specify the location of the interrupt table in RAM and declare the variables of the beginning and end of the interrupt table. In the startup_stm32f103xb.asm file, you need to copy the table and set a new VTOR register value. Why did I decide to modify the library file, which is usually not recommended? The fact is that the operations of allocating sections of memory should be performed as early as possible, and it is this operation that executes the code of this file: copies global variables from the .data section and initializes static memory (.bss) with zeros. We will only add the copy of the .isr_vector section.
We proceed to modify the linker script. We rewrite the .isr_vector section as follows:
/* 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-памяти.
We announced a place to place the interrupt table in RAM. Now the variables declared in the linker script must be additionally declared in assembler. Paste this code somewhere at the top of the file.
.word _svector
.word _evector
.word _sivector
Now copy the interrupt table. To do this, insert the following code between the instructions {bcc FillZerobss} and {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 //Если нет, переходим к копированию текущего слова
The table is copied. Now you need to set the value of the VTOR register. As already mentioned, the address of this register is specified in the core_cm3.h file, but let's not knock on it from assembler, and just declare it directly in this file. We write the definition:
.equ VTOR, 0xE000ED08
And then just put this figure at the end of the interrupt table. To do this, add to the end of the .isr_vector section:
.word VTOR
We made sure that the address of the VTOR register is located in the flash-memory of the controller. Now write the desired value in the register. To do this, add the following code after the copy code for the interrupt table:
ldr r0, =_svector //Записываем адрес новой таблицы прерываний в регистр r0
ldr r2, =VTOR //Записываем адрес регистра VTOR в регистр r2
str r0, [r2] //Записываем по адреcу, содержащемся в r2 значение r0
All. We got a new interrupt table that is identical to the standard one, but now we have the ability to dynamically change it. Now you can safely switch to the main function:
bl __libc_init_array
b main
And check how our work works:
#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();//Уходим спать. Таймер нас разбудит.
}
}
Here, right after the controller starts, we change the system timer interrupt handler. After that, we set up the timer, and in each interrupt handler we transfer the vector to another function. Thus, each time the timer is activated, the processor will alternately fall into one or the other function.
The code was checked on the STM32F103 controller. If you have questions or comments, write in the comments.
Literature
ARM Cortex M3
Documentation ARM Assembler Documentation