📜 ⬆️ ⬇️

Dynamic Interrupt Management in ARM

Today I will tell you how you can dynamically replace interrupt handlers in ARM processors using the example of STM32 microcontrollers. The method I described works in ARM Cortex M3 processors and above.

When and where might it be needed? First, you can replace interrupt handlers if you are faced with the task of writing a program that is compatible with different hardware platforms. There are several kernel interrupts in ARM processors that are required for any implementation of the architecture. But the remaining interrupts are for the periphery, and processor manufacturers are free to set these vectors for any peripheral devices present in the processor. This requires dynamically substituting the necessary interrupt handlers for each implementation of the architecture. 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 to do 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 program is the stack pointer. Usually it is equal to the maximum address of the controller's RAM. Next comes the pointer to Reset_Handler, NMI_Handler and other interrupt handlers. Theoretically, to dynamically set a new function to handle an interrupt, you simply need to rewrite one of these pointers. But the hardware limitations of the platform will not allow this, because the program in the STM32 is executed from FLASH-memory, and to write a new word into it, you must first erase the entire page, and this is not included in our plans: the program cannot be damaged. So let's try to transfer the interrupt table to the RAM and change the vectors already there. But the question remains: how does the kernel know that the table has been moved? After all, a simple copying of the table will not work if the kernel turns to the old table when an interrupt occurs and calls the old handler. To resolve this situation there is a VTOR (Vector Table Offset Register) register. You will not find the description of this register in the documentation for the controller; debuggers do not know about it either. Information about this registry should be found in the documentation for the ARM core, you can also find it in the header file core_cm3.h. The register is located at address 0xE000ED08, and its value must be a multiple of 0x400. This means that you can not put the interrupt table wherever they want. Let's not puzzle, and just place it at the beginning of the RAM, and then set the new value of the VTOR register. Having filled the new interrupt table, we will test it by interrupting the system timer.

To accomplish the 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 need to 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, copy the table and set a new value for the VTOR register. Why did I decide to modify the library file, what 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 performs the code of this file: it copies global variables from the .data section and initializes the static memory (.bss) to zero. We will only finish copying the .isr_vector section.
')
Let's start modifying the linker script. 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 have declared a place to place the interrupt table in RAM. Now the variables declared in the linker script must be additionally declared in the assembler. Paste this code somewhere at the beginning of the file.

 .word _svector .word _evector .word _sivector 

Now let's copy the interrupt table. To do this, between the instructions {bcc FillZerobss} and {bl SystemInit} we insert the following code:

  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 we 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 the 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 ensured 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, after the code to copy the interrupt table, add the following code:

  ldr r0, =_svector //       r0 ldr r2, =VTOR //   VTOR   r2 str r0, [r2] //  c,   r2  r0 

Everything. We received a new interrupt table, identical to the standard one, but now we can dynamically change it. Now you can safely go to the function main:

  bl __libc_init_array b main 

And check how our works work:

 #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 we immediately after the start of the controller change the interrupt handler of the system timer. 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 triggered, the processor will alternately fall into one or another function.

The code was checked on the controller STM32F103. If you have any questions or comments, please write in the comments.

Literature

ARM Cortex M3 Documentation
ARM Assembler Documentation

Source: https://habr.com/ru/post/275351/


All Articles