STM32F401 Problems when relocating vector table on mbed project

25 Jul 2015

STM32F401 Problems when relocating vector table on mbed project

Can someone please check if I've got the wrong idea: https://developer.mbed.org/users/riaancillie/code/F401RE_VectorTableRelocation_Issues/

The short story: I'm working on a bootloader and main application project and my mbed main application has issues with the vector table. In the example provided I've removed the bootloader from my testing and identified that just relocating the vector table on an mbed project already causes a crash/freeze and I have no idea why. This is the output on the serial port: /media/uploads/riaancillie/capture.png

As you can see, it froze midway during a printf.

The longer story: I'm working on a proof-of-concept project that involves a main application and a bootloader. So far I've adapted ST application note AN3965 (In-Application Programming) to work as a decent bootloader which checks an SD card for an updated binary and writes the binary to flash. It works pretty good. I'm using Keil uVision 5 by the way and my bootloader is 17kb so I decided to offset my main application to 0x08008000.

My main application started as an mbed project in the online compiler and has progressed well due to the good amount of libraries for external components (LCD, USB MSD etc). To offset my main application to 0x08008000 I exported my mbed project and loaded it up in uVision. I set the linker to offset 0x08008000 and since there is no system_stm32f4xx.c file to edit I can't set the #define VECT_TAB_OFFSET, so I called NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x8000)

With the application being loaded by the bootloader, I can see it starting and then just stopping completely. The printf statement that goes the Serial on the computer stops halfway. After struggling for days I noticed if I call disable_irqs() in my bootloader before jumping to the main application, my application works fine, except obviously there are no interrupts breaking some of the hardware like the USB MSD. If I try enabling the IRQs again in the main application I get the same crash/hang.

Figuring the problem must be related the vector table and its offset, I've been trying for days now to determine what exactly is going wrong. When I modify the blinky example (which includes a systick_handler) that comes with the Nucleo's pack in uVision to be used as a main application (i.e. flash offset to the same address and vector table offset set) it works fine with the interrupts enabled.

Here's where it gets interesting. If I take my mbed main application, set its flash offset to the default 0x08000000 and make VECT_TAB_OFFSET 0x0 again and program that on the Nucleo everything works fine. I then tried copying the vector table and setting SCB-> VTOR to the new array, it also breaks. I can remap the blinky uVision example's vector table anywhere I want, but getting an mbed project to work properly in a bootloader environment has been keeping me busy for days now.

I've only been using Nucleo for two weeks after spending most of my adult life working with Arduinos so likely I've made some elementary mistake somewhere. Please let me know if you need for information.

25 Jul 2015

My personal pet peeve: There is no technical reason for an ARM device why the bootloader should be at the start and not at the end of the flash, making user programs easier ;).

Question one I have with your code: what does it matter where your vector table is? You have no timer active and I have no idea which other interrupt could fire between changing the table and sending serial. Or is there still something from the bootloader active? Or does this happen also with a regular Nucleo F401 without bootloader?

Anyway, what I think the issue is: You copy it from:

 __Vectors

This seems to be the flash address of the vector table. You can verify it by printing its address, I assume the result is 0x800000, the start of Nucleo flash space (plus or minus a few zeros). The problem then is, this vector table is wrong. This is the code mbed uses for the NVIC_SetVector command, which is the default in mbed to use interrupt vectors:

https://developer.mbed.org/users/mbed_official/code/mbed-src/file/5e59b9938d4a/targets/cmsis/TARGET_STM/TARGET_STM32F4/TARGET_NUCLEO_F401RE/cmsis_nvic.c

The moment this command is called for the first time, it copies the vector table to RAM, and then it modifies the table for the vector which is set to a new function. So if you copy the original one from flash, you copy the wrong one.

Now unless your bootloader keeps occupying RAM, I don't think there is much reason to change anything about that vector table location: The start of the RAM memory. A possible problem: it checks if SCB->VTOR is equal to a specific value (start of flash), and it might very well be that when an offset is added to your program, this is not the case, and it fails at remapping it. For some targets this has been solved by checking if it recides anywhere in flash, instead of checking if it is at start of flash.

So easiest option: use mbed-src, modify that file. Plan B is use this for another workaround.

25 Jul 2015

Hey

For this test application the location of the vector table isn't important. My real application is a bit bigger and it uses USB host code which does have an interrupt. My primary problem is that any mbed project that I build with a flash offset to make it work with the bootloader freezes. My assumption is that must be the vector table causing the problem because when I disable IRQs in my bootloader as my first line in the main application, everything works except the USB host mode.

The address of Vectors is 0x20000000. It seems mbed copies the vector table to memory.

If the vector table is being copied to memory, do I still need to update SCB->VTOR (or call NVIC_SetVectorTable) to make it work with a bootloader?

I'll quickly try replacing the mbed library with mbed-src and building everything again.

26 Jul 2015

Ah, if thats the address of vectors it already points to RAM. I really expected it to point to the original flash location. I assume that once running user code, the bootloader does not take any RAM anymore? Then I don't think there is a reason to copy the vector table yourself. However you then do need to make sure the code actually copies it to memory: If there is an offset, the initial check in NVIC_SetVector on the VTOR location might fail.

26 Jul 2015

I have made significant progress.

It seems that the standard mbed library isn't going to allow you to build an application that can be loaded by a bootloader with interrupts still working. Well at least as the library is at this moment in time.

As Erik recommened, i replaced the mbed library with mbed-src and exported everything again to uVision. Here are the changes I made to get everything working:

  • I modified cmsis_nvic.c from this:

cmsis_nvic.c

...
#define NVIC_RAM_VECTOR_ADDRESS   (0x20000000)  // Vectors positioned at start of RAM
#define NVIC_FLASH_VECTOR_ADDRESS (0x08000000)  // Initial vector position in flash
 
void NVIC_SetVector(IRQn_Type IRQn, uint32_t vector) {
    uint32_t *vectors = (uint32_t *)SCB->VTOR;
    uint32_t i;
 
    // Copy and switch to dynamic vectors if the first time called
    if (SCB->VTOR == NVIC_FLASH_VECTOR_ADDRESS) {
        uint32_t *old_vectors = vectors;
        vectors = (uint32_t*)NVIC_RAM_VECTOR_ADDRESS;
        for (i=0; i<NVIC_NUM_VECTORS; i++) {
            vectors[i] = old_vectors[i];
        }
        SCB->VTOR = (uint32_t)NVIC_RAM_VECTOR_ADDRESS;
    }
    vectors[IRQn + NVIC_USER_IRQ_OFFSET] = vector;
}
... 

I changed it to this, following the example of some of the other targets that have already been fixed.

cmsis_nvic.c

...
#define NVIC_RAM_VECTOR_ADDRESS   (0x20000000)  // Location of vectors in RAM
 
static unsigned char vtor_relocated;
 
void NVIC_SetVector(IRQn_Type IRQn, uint32_t vector) {
    uint32_t *vectors = (uint32_t*)SCB->VTOR;
    uint32_t i;
 
    // Copy and switch to dynamic vectors if the first time called
    if (!vtor_relocated) {
        uint32_t *old_vectors = vectors;
        vectors = (uint32_t*)NVIC_RAM_VECTOR_ADDRESS;
        for (i=0; i<NVIC_NUM_VECTORS; i++) {
            vectors[i] = old_vectors[i];
        }
        SCB->VTOR = (uint32_t)NVIC_RAM_VECTOR_ADDRESS;
        vtor_relocated = 1;
    }
    vectors[IRQn + 16] = vector;
}
...
  • I changed VECT_TAB_OFFSET in system_stm32f4xx.c

If i'm reading everything correctly: During initialization, the vector table at the address specified VECT_TAB_OFFSET is used to set SCB-VTOR Later on the cmsis.nvic copies the vector table from wherever address SCB->VTOR points to into memory to allow dynamic updates at runtime of interrupt handler vectors/addresses. Using the "precompiled" mbed library, you don't have this file so you are stuck using VECT_TAB_OFFSET = 0x00, which sucks.

My VECT_TAB_OFFSET is now 0x08008000, which is the offset where my main application will be in flash.

  • Most importantly, the uVision project exported from the mbed online IDE has a memory offset of 0x2000000. During initialization, while debugging, I noticed my variable vtor_relocated had an address 0x2000001c, right where the vector table is living. I changed the linker settings so that the R/W memory is from 0x20000200 and suddenly everything worked. I was under the impression the project file the mbed IDE exports would have the correct settings since they obviously know by default they copy the vector table to 0x20000000 at startup.

And that was that: everything worked. Except, when using mbed-src the project size grew significantly. My 65kb bin file now became 108kb. So I copied my cmsis_nvic.o and system_stm32f4xx.o file from the build folder, opened my project that still uses the precombiled mbed library and just replaced those two files everything works. My bootloader loads, my main application runs. USB is working, timer interrupts... everything.

Thanks for the help Erik! Much appreciated.

If I understand the workings correctly, once the mbed library people update cmsis_nvic.c to reflect the vector table copying like they did with the other targets, it still wouldn't work since cmsis_nvic.c copies the vector table from wherever SCB->VTOR points to, and because you don't have system_stm32f4xx.c to change VECT_TAB_OFFSET, it will still copy the vector table at address 0x08000000. So in my bootloader (assuming they eventually update cmsis_nvic.c), I need to update SCB->VTOR to 0x08008000 before jumping to the main application? Is this correct?

26 Jul 2015

Glad that you got further with it. You can try playing around with optimizer settings: If you use uVision it should be possible to get roughly same size out of it, since mbed uses the same compiler.

I agree it seems like you will need to modify that source files, since you probably will always need those. It is nice trick to copy those two .o files, didn't know that worked actually.

I have never done this myself, so my advice might be wrong. However it might work without changing the VECT_TAB_OFFSET if you don't have any static interrupt handler functions: So all interrupts being set by NVIC_SetVector, and none by using the default handlers. However I am afraid the USBHost lib will use a default handler for USB.

27 Jul 2015

My I suggest a tiny change: static unsigned char vtor_relocated = 0; Just in case that memory location isn't 0 on startup.

27 Jul 2015

Sure, will update it just for safety. Under what condition will memory not be zero at startup?

27 Jul 2015

The obvious one would be if the bootloader happened to also use that area of RAM. Brownout situations where the power dips enough to reset the processor but was never completely removed are another possible situation.

I've not checked the data for that particular part but often there isn't any guarantee that memory will be initialized on reset or power up.

I've seen a wonderful bug on another product where everything worked fine until unless you switched the power on at temperatures below about 5C. It turned out the RAM chips would power up all 0 at normal temperatures but at low temperatures would have the occasional 1 in there that the software didn't expect. That took a while to track down.

02 May 2016

I am interested in Erik's comment:

My personal pet peeve: There is no technical reason for an ARM device why the bootloader should be at the start and not at the end of the flash, making user programs easier ;).

I would be interested in understanding more about this approach. For development, it sure would be a lot easier if I could just leave the application at its default location. But if the app is at 0x0800 0000, then doesn't it mean the app starts first? Then the app would have to hand off to the bootloader. The risk would seem to be, what if your firmware update for the main app fails, now you can't access the bootloader.

28 Mar 2018

Hi Riian, Is there any chance you still have those re-compiled .o files and willing to share them?

28 Mar 2018

Joshua Cowpland wrote:

Hi Riian, Is there any chance you still have those re-compiled .o files and willing to share them?

Hi Joshua

Unfortunately, I don't have my Mbed bootloader anymore, but it would likely not work for you in any case since it is for the specific Mbed board and mbed SDK version I used. I read your other post about the bootloader and saw you also started out on the Mbed online compiler. Easiest would be to duplicate your project in the online compiler and delete the mbed library. Then you can import the "mbed-src" library and export your project to a .zip file for offline usage.

You can then make the changes same as I made above (https://os.mbed.com/forum/mbed/post/39129/) and you should find the .o files required in the output folder.

Oh and don't forget to disable your IRQs before jumping from the bootloader.

__disable_irq(); 

    JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);
  /* Jump to user application */
  Jump_To_Application = (pFunction) JumpAddress;
  /* Initialize user application's Stack Pointer */
    __set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);
    Jump_To_Application();

and also to re-enable them when you enter your main application

__enable_irq();
29 Mar 2018

My existing project doesn't actually seem to have a cmsis_nvic.o file. Any idea why I wouldn't have one?

29 Mar 2018

You are most likely using a newer version of the mbed sdk and they have moved the intitialization code somewhere. Best is to download the source of the mbed-os sdk and search through all *.c files for the relevant place where SCB->VTOR is set. (e.g. search for "SVB->VTOR =")

Looks like they changed the source file where the vectors are copied: https://github.com/ARMmbed/mbed-os/blob/f895392374f50ffe53996459bbbf776e78006a67/rtos/TARGET_CORTEX/mbed_boot.c#L295

or maybe here https://github.com/ARMmbed/mbed-os/blob/f895392374f50ffe53996459bbbf776e78006a67/platform/mbed_sdk_boot.c#L63

You will also need to change this line https://github.com/ARMmbed/mbed-os/blob/f895392374f50ffe53996459bbbf776e78006a67/targets/TARGET_STM/TARGET_STM32F4/TARGET_STM32F401xE/device/stm32f401xe.h#L656 to match where your application now starts.

Easiest is to compile your project using the mbed source code and not the precompiled libraries and tweak the code until it works. Swapping out the .o files can be a dangerous process, especially if you aren't sure yet whether building from source code actually works.

29 Mar 2018

Hi Joshua

I just thought of something else you could try. I have no way of testing this at the moment, so I'll just leave the idea here for you to try. Since the vector table is copied from the default flash address to RAM during the startup code, and your app runs OK except for when an interrupt happens, you should in theory be able to copy the vector table yourself in your own startup code. This would mean you don't have to fiddle with changes to mbed source files and .o files. I have no idea if this would work, but it's worth a try.

So, as early as you can in your main application, copy your vector table from the correct location:

int main() {
    __disable_irq(); //Should still be disabled from when you jumped out of the bootloader, but disable again for safety.
    uint32_t *old_vectors = (uint32_t *)(0x08020000U); //Or wherever you placed your main application
    uint32_t *vectors = (uint32_t*)NVIC_RAM_VECTOR_ADDRESS;
    for (int i = 0; i < NVIC_NUM_VECTORS; i++) {
        vectors[i] = old_vectors[i];
    }
    SCB->VTOR = (uint32_t)NVIC_RAM_VECTOR_ADDRESS;
    __enable_irq(); //Re-enable IRQ. In theory your vector table should now be copied and interrupt *should* work.
    
    //Rest of code

Let me know if it works.

29 Mar 2018

Magic! That last solution worked! Thank you so much.

29 Mar 2018

Ah that's great to hear. All the best with your project Joshua

02 Jun 2018

Out of curiosity what would happen if the relocation in the example at the top took place right at the very beginning, before any serial communications?

I'm not clear whether the table is being copied from its current location or from the original location in ROM. If ROM then assuming the serial handler initalized some interrupt handlers (with vectors in RAM) wouldn't they get nuked at that point?

04 Jun 2018

Out of curiosity what would happen if the relocation in the example at the top took place right at the very beginning, before any serial communications?

I'm not clear whether the table is being copied from its current location or from the original location in ROM. If ROM then assuming the serial handler initalized some interrupt handlers (with vectors in RAM) wouldn't they get nuked at that point? <</quote>>

Hi Oliver

My understanding is that the MBED SDK copies the vector table from a hard-coded location during startup (for the F401RE this is 0x8000 0000) . This means that if your bootloader is in this default location (e.g. the code that runs at startup), the vector table that will be vector table for your bootloader, causing havoc, because now your interrupts are handled by the code in the bootloader.

In the example I posted, it would not matter when the relocation code is executed, because the MBED online compiler doesn't have the option to offset to code to a different address and thus the vectors in the linker file are still pointing to the default location. As you know, the vector table is just a sequential list of memory addresses at the beginning of the binary. e.g. 1. Timer interrupt - Goto 0x080030e 2. UART interrupt - Goto 0x08003ba ...

Those memory addresses for each interrupt handler in the vector table are hard coded by the linker. So you can see if you decide to program your main application at a different location without telling the linker about it, the interrupt handler that get's called will be in a memory space where your code isn't residing, most likely an arbitrary location in your bootloader code.

Here is an example of ".ld" file you can give the linker to tell it that you intent on using a different location to write your binary to:

/* Linker script to configure memory regions. */
MEMORY
{ 
  FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 464K
  RAM (rwx)  : ORIGIN = 0x20000194, LENGTH = 96k - 0x194
}

This will cause the interrupt table in the binary to be offset from the origin given.

It's very important to remember that the interrupt handler vectors are determined during linking and not during runtime. I.e. Your binary actually contains a vector table that says for instance, a UART interrupt will cause the controller to jump to address 0x0800 03ba for instance.

BUT... this isn't enough

Like I mentioned, the MBED platform copies the vector table from a hard-coded location (0x0800 0000 for the F401RE) at startup. So if you move your application elsewhere, you will have to ensure the vector table is copied from the correct location.

The code in a nutshell in the post I made 29 March, takes the vector table from the location where you offset programmed your binary, and copies it into RAM. Strictly speaking, you can place your vector table anywhere in RAM, but I re-used the default memory location. The reason you can use any location is because the register SCB->VTOR stands for Vector Table Offset Register and simply points the microcontroller to where to read interrupt handler addresses from.