How can I duplicate the Interrupt Vector Table using the linker script on a Cortex M0+?

2.4k views Asked by At

First of all, I hope I'm not asking something that has already been asked before. I've searched as much as I can but I haven't found an answer to my specific problem or something useful.

I'm working on a FRDM-KL82Z board which runs a Cortex M0+ core. I'm using MCUXpresso IDE v10.0.2 and a Segger J-Link programmer, although I think this is not relevant for this question.

This project will need a custom bootloader and app coded by different developers, each block with its own flash memory space: 8K for the bootloader and 120K for the app (this may change in the future but it's no big deal at the moment).

Once the bootloader is completed, it will manage the jump to App space and the app will change de Vector Table Offset Register (VTOR) so that the Interrupt Vector Table changes form the Boot IVT to the App IVT. This has already been tested successfully.

My aim is to set up the linker script file so that the app developers can build and debug their project on the board before the bootloader is completed, as they will be developed at the same time. The reason for this is that they can work with the App space as it will be in the final version.

I think the Reset vector and the Config bits must be at their default position because the hardware will go to the same position every time it needs to read them.

My first idea consist in disabling the automatic linker script generation and modifying the MyProject_Debug.ld file.

What the script automatically generates:

INCLUDE "LEDTest_Debug_library.ld"
INCLUDE "LEDTest_Debug_memory.ld"

ENTRY(ResetISR)

SECTIONS
{
    /* MAIN TEXT SECTION */
    .text : ALIGN(4)
    {
        FILL(0xff)
    __vectors_start__ = ABSOLUTE(.) ;
    KEEP(*(.isr_vector))
    /* Global Section Table */
    . = ALIGN(4) ;
    __section_table_start = .;
    __data_section_table = .;
    LONG(LOADADDR(.data));
    LONG(    ADDR(.data));
    LONG(  SIZEOF(.data));
    LONG(LOADADDR(.data_RAM2));
    LONG(    ADDR(.data_RAM2));
    LONG(  SIZEOF(.data_RAM2));
    __data_section_table_end = .;
    __bss_section_table = .;
    LONG(    ADDR(.bss));
    LONG(  SIZEOF(.bss));
    LONG(    ADDR(.bss_RAM2));
    LONG(  SIZEOF(.bss_RAM2));
    __bss_section_table_end = .;
    __section_table_end = . ;
    /* End of Global Section Table */

    *(.after_vectors*)


    /* Kinetis Flash Configuration data */
    . = 0x400 ;
    PROVIDE(__FLASH_CONFIG_START__ = .) ;
    KEEP(*(.FlashConfig))
    PROVIDE(__FLASH_CONFIG_END__ = .) ;
    ASSERT(!(__FLASH_CONFIG_START__ == __FLASH_CONFIG_END__), "Linker Flash Config Support Enabled, but no .FlashConfig section provided within application");
    /* End of Kinetis Flash Configuration data */


    } >PROGRAM_FLASH

    .text : ALIGN(4)
    {
        *(.text*)
        *(.rodata .rodata.* .constdata .constdata.*)
        . = ALIGN(4);
    } > PROGRAM_FLASH
    /*
     * for exception handling/unwind - some Newlib functions (in common
     * with C++ and STDC++) use this. 
     */
    .ARM.extab : ALIGN(4) 
    {
        *(.ARM.extab* .gnu.linkonce.armextab.*)
    } > PROGRAM_FLASH
    __exidx_start = .;

    .ARM.exidx : ALIGN(4)
    {
        *(.ARM.exidx* .gnu.linkonce.armexidx.*)
    } > PROGRAM_FLASH
    __exidx_end = .;

    _etext = .;


    /* USB_RAM */
    .m_usb_data (NOLOAD) :
    {
        *(m_usb_bdt)
        *(m_usb_global)
    } > USB_RAM
    /* possible MTB section for USB_RAM */
    .mtb_buffer_RAM2 (NOLOAD) :
    {
        KEEP(*(.mtb.$RAM2*))
        KEEP(*(.mtb.$USB_RAM*))
    } > USB_RAM

    /* DATA section for USB_RAM */
    .data_RAM2 : ALIGN(4)
    {
         FILL(0xff)
        PROVIDE(__start_data_RAM2 = .) ;
        *(.ramfunc.$RAM2)
        *(.ramfunc.$USB_RAM)
        *(.data.$RAM2*)
        *(.data.$USB_RAM*)
        . = ALIGN(4) ;
        PROVIDE(__end_data_RAM2 = .) ;
     } > USB_RAM AT>PROGRAM_FLASH

    /* MAIN DATA SECTION */
        /* Default MTB section */
        .mtb_buffer_default (NOLOAD) :
        {
           KEEP(*(.mtb*))
        } > SRAM
    .uninit_RESERVED : ALIGN(4)
    {
        KEEP(*(.bss.$RESERVED*))
        . = ALIGN(4) ;
        _end_uninit_RESERVED = .;
    } > SRAM
    /* Main DATA section (SRAM) */
    .data : ALIGN(4)
    {
       FILL(0xff)
       _data = . ;
       *(vtable)
       *(.ramfunc*)
       *(.data*)
       . = ALIGN(4) ;
       _edata = . ;
    } > SRAM AT>PROGRAM_FLASH
    /* BSS section for USB_RAM */
    .bss_RAM2 : ALIGN(4)
    {
       PROVIDE(__start_bss_RAM2 = .) ;
       *(.bss.$RAM2*)
       *(.bss.$USB_RAM*)
       . = ALIGN (. != 0 ? 4 : 1) ; /* avoid empty segment */
       PROVIDE(__end_bss_RAM2 = .) ;
    } > USB_RAM 
    /* MAIN BSS SECTION */
    .bss : ALIGN(4)
    {
        _bss = .;
        *(.bss*)
        *(COMMON)
        . = ALIGN(4) ;
        _ebss = .;
        PROVIDE(end = .);
    } > SRAM
    /* NOINIT section for USB_RAM */
     .noinit_RAM2 (NOLOAD) : ALIGN(4)
    {
       *(.noinit.$RAM2*)
       *(.noinit.$USB_RAM*)
       . = ALIGN(4) ;
    } > USB_RAM 
    /* DEFAULT NOINIT SECTION */
    .noinit (NOLOAD): ALIGN(4)
    {
        _noinit = .;
        *(.noinit*) 
         . = ALIGN(4) ;
        _end_noinit = .;
     } > SRAM
    .heap :  ALIGN(4)
    {
        _pvHeapStart = .;
        . += 0x1000;
        . = ALIGN(4);
        _pvHeapLimit = .;
    } > SRAM
       .heap2stackfill  : 
    {
        . += 0x1000;
    } > SRAM
     .stack ORIGIN(SRAM) + LENGTH(SRAM) - 0x1000 - 0:  ALIGN(4)
    {
        _vStackBase = .;
        . = ALIGN(4);
        _vStackTop = . + 0x1000;
    } > SRAM
}

I've tried to find information in this guide about de GNU linker but my ideas haven't worked so far. What I've tried:

  1. Setting the location counter to a different value after the Config Words and copying the ISR_vector code snipped before the text section:

    ...
    /* End of Kinetis Flash Configuration data */
    
    
    } >PROGRAM_FLASH
    
    .text : ALIGN(4)
    {   
        /*  MODIFIED CODE   */
    
        . = 0x2000;     /*  First position of App code  */
         FILL(0xff)
        __vectors_start__ = ABSOLUTE(.) ;
        KEEP(*(.isr_vector))
    
        /*  END OF MODIFIED CODE    */
    
        *(.text*)
        *(.rodata .rodata.* .constdata .constdata.*)
         . = ALIGN(4);
    } > PROGRAM_FLASH
    ...
    

When I do this and I open the .hex file, the space between the Config Words (0x400) and the start of the App space (0x2000) is effectively empty (full of 0xFF) but the code after 0x2000 is nothing like the IVT table.

  1. If I move location counter to 0x2000 before the IVT code lines it effectively moves the IVT adresses to the 0x2000 position. To do this, I move the Config Words part before the IVT part because de location counter can't move backwards.

  2. I've tried creating a Bootloader section in the memory map, with the correct starting and length positions, and copying every line that by default gets placeD in the PROGRAM_FLASH section into a new one that goes to BOOTLOADER (the same code with ">BOOTLOADER" at the end). In this case de IVT only appears in the Boot space.

Is it possible that the linker script places de IVT only in the first place it is indicated and then ignores every other call? What am I doing wrong? Should I try another way to achieve this?

Thank you very much, I know it's quite a long!

4

There are 4 answers

0
Devan On

I don't think it's possible to make a copy of the vector table using only linker shenanigans. The linker script will not let you match the same symbol multiple times so that you can output it twice.

From the binutils 2.29 manual:

If a file name matches more than one wildcard pattern, or if a file name appears explicitly and is also matched by a wildcard pattern, the linker will use the first match in the linker script.

I tested it without using any wildcard patterns at all with similar results, so I don't think the linker will ever let you output the same symbol twice.

I also tried using objcopy to create a renamed copy of the vector table that could referenced from the linker script but that table ended up as all zeroes and the whole approach was rather convoluted, so I don't think that's worth pursuing.

If you want to keep the application code as similar as possible between now and when the bootloader is completed, I would suggest a different approach:

Make use of the __vectors_start__ symbol provided by the existing linker script so that your code always knows where the vector table is placed, even if you make changes to the linker script.

void relocate_vector_table(void) {
    extern unsigned __vectors_start__;
    SCB->VTOR = (unsigned)&__vectors_start__;
}

This will allow the same code to work with your current configuration (no bootloader, ROM starting at 0x0) and your eventual bootloader configuration (ROM starting at 0x2000).

0
Chris Stratton On

My MCU projects typically have Makefile targets that will actually flash the chip (with building as a dependency of course), so what I did for this was make a special target that flashes the main firmware "solo".

My openocd-driven programmers can flash flat binaries and not just hex files, so I was able to do this by using dd to copy just the vector table off the start of the main firmware binary into its own file. I then write this to the start of flash, and the main firmware to its usual location in separate operations. The chip boots, gets the reset and stack addresses out of the copied vector table, starts the main firmware, and that then repoints the vector table address to its own copy at the higher address.

If your programmer doesn't support flat binaries you can use objdump or some other tool to turn a flat binary back into a hex file, or likely to change the base address of a hex file / fragment.

0
EmbeddedGuru On

This is an old question, but I didn't see what I thought was a correct answer for the stated problem. Here is the answer I figured out.

Recap of the Problem When linking an application to work with a bootloader, you have to modify the linker script to, essentially, tell it that flash memory begins at some offset from 0, since addresses 0 to BOOT_END is where the bootloader resides. This is all you have to do in order to compile a program that the bootloader itself can burn into flash. When the bootloader jumps to the burned-in program, it resets the stack ptr, and jumps to it's execution address. There, the function Reset_Handler will reset the Arm M0+ VTOR to whatever address you offset the app to be, and it runs like a champ. So far, so good.

BUT, if you also want to be able to debug the application on a debugger, you quickly find that the debugger can't start the app. Why not? Well, let's recap how the debugger tries to do this.

Typically the debugger will burn the new application into flash, set a breakpoint at the beginning of main(), then resets the CPU. But this also resets VTOR = 0. So after resetting, the CPU tries to read the initial stack and exec addresses from what it expects to be an IVT at address 0; but it's not there, it's at BOOT_END. WHAMO! Hard fault!

The Solution The poster is correct in thinking that the solution is that you need an IVT at address 0, or at least the 1st two vectors, that will point the CPU to your application's Reset_Handler() after reset. He is also correct in thinking that the linker script will NOT emit the same section to two different locations; the first match is used, the second ignored. The trick is to create a new section that contains the same vectors.

In my project, I'm using a Microchip SamL21 SOC that has an Arm Cortex M0+ CPU. My Reset_Handler() and IVT were provided in an ASF module called startup_saml21.c. In that same module, I declare my debugger startup IVT:

    /**
 * \brief   In adapting this app to run w/ a bootloader, yet still want to be able to
 *          debug it in the debugger, we need a psuedo-IVT that the debugger can burn
 *          into 0x00000000 flash that provides the stack and exec addrs while VTOR==0.
 *          The Reset_Handler will then change VTOR to pt to the full IVT above.
 */
__attribute__ ((section(".dbgvectors"))) const DeviceVectors startup_table = {
    (void*) (&_estack),
    (void*) Reset_Handler,
};

Notice I only define the 1st two vectors, as that's all the debugger needs. This IVT references a new section called .dbgvectors, and I give it a new variable name 'startup_table' so the compiler is happy. The vectors themselves are the exact same vars used in the full-up IVT, exception_table[] assigned to section .vectors. Now all that remains is to define where this new section goes. In the linker script snippet below, I created a new memory region 'dbg', and output section '.dbg_ivt':

    MEMORY
{
  dbg      (rx)  : ORIGIN = 0x00000000, LENGTH = 0x00004000 /* NEW */
  rom      (rx)  : ORIGIN = 0x00004000, LENGTH = 0x00020000
  ram      (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00008000
  lpram    (rwx) : ORIGIN = 0x30000000, LENGTH = 0x00002000
}

/* Section Definitions */
SECTIONS
{
    /* NEW */
    .dbg_ivt :
    {
        KEEP(*(.dbgvectors .dbgvectors.*))
    } > dbg

    .text :
    {
        . = ALIGN(4);
        _sfixed = .;
        KEEP(*(.vectors .vectors.*))

       /* remainder cut for conciseness */
    } > rom
}

System Considerations The addition of the .dbgvectors section allows the debugger to program and debug the application, however for this to work you have to allow the debugger to erase the bootloader and write this new section at address 0. Therefore to test if your bootloader can then burn the same application, you will have to re-flash the bootloader application first.

Also, your bootloader app must be smart enough to ignore any data addresses below BOOT_END in a loaded hex file, so that it doesn't destroy itself. Either that, or you'll need some post-compile script to strip the same out of your .hex file whenever you deploy new application images.

0
Srdjan Nikolic On

My experience with M4 application and bootloader shows that it is enough to set the Flash start at some offset address, and then in the application to initialize VTOR to this address.

From linker script:

#

/* Specify the memory areas */

MEMORY
{
CLASSBRAM (rw)   : ORIGIN = 0x20000000, LENGTH = 0x80
/*RAM length = 192K - CLASSBRAM-length */
RAM (xrw)      : ORIGIN = 0x20000080, LENGTH = 0x2FF80 
CCMRAM (rw)      : ORIGIN = 0x10000000, LENGTH = 64K
/* FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 512K */
FLASH (rx)      : ORIGIN = 0x08010000, LENGTH = 448K  /*in case of 64K for Bootloader*/
}

/* Define output sections */
SECTIONS
{
  /* The startup code goes first into FLASH */
  .isr_vector :
  {
    . = ALIGN(4);
    PROVIDE( _Rom_Start = . );
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >FLASH

.....

#

code:

extern const uint32_t _Rom_Start;
....
#define ROM_START ((uint32_t *)&_Rom_Start)
...
SCB->VTOR = (uint32_t)ROM_START;