笔者在接着聊一下bootloader,主要针对MCU的Bootloader。
笔者之前介绍过一篇Bootloader文章,主要是其概念、一些升级包的格式和升级流程,本次接着来说一下。
1、MCU代码运行方式
之前文章也介绍过,MCU的代码运行方式有两种,
- XIP方式:(Excute In Place),在位置上执行,即在存储地址上执行代码,如果是这样,则对存储芯片有要求,则要求是支持memory map的方式,把存储空间当做是CPU的memory一样,可以通过总线获取到code指令,然后执行。
内部ROM Code会跳转到Code中,然后Flash中的Code就会执行,接着把data段和bss段进行分散加载到对应的ram,然后就可以执行,STM32就是这种方式。
- RAM方式:一般情况下,MCU Boot会把代码从存储空间搬到RAM上去运行,例如一些SPI Flash或者Nand Flash,无法直接执行指令,需要搬到RAM方式执行,然后再跳到RAM地址执行,RAM地址执行,还有个好处是速度执行更快,相对Flash上面执行。
下图以NXP的LPC54016为例进行展示,Boot将代码从SPI Flash搬到SRAMX,然后再将执行权交给SRAMX,至此代码就在Sramx上面执行,最后同样需要进行分散加载,将bss段和data段搬到Sram0-3上面执行。
分散加载可以参考笔者本篇文章。
2、NXP单片机运行方式修改
以NXP LPC54016单片机为例,笔者来介绍一下如何从RAM方式切到XIP方式。
2.1 代码启动流程
-
首先需要看一下手册,研究一下如何支持XIP方式,MCU的code都是rom code搬出来的,如果要执行XIP方式,则不搬code,所以需要看相关的配置
-
开始笔者想着是相关的寄存器设置,可以让ROM Code中的boot不搬代码,但是实际看手册没有找到相关的寄存器信息。
-
找到了相关的流程图,通过引脚来决定怎么启动
-
OTP BOOT SRC 引脚被设置,则会从外部flash(Parallel Mmeory)搬数据(512Byte)到SRAM,通过该512Byte来决定在怎么启动(XIP还是RAM方式)
-
如果OTP BOOT SRC没有被设置,则有ISP的引脚,来决定进入ISP模式下载数据到SRAM,然后BOOT,还是从外部Flash Boot(搬移)。
-
这里有个AUTO BOOT的方式,首先寻找有效的Image(0x0/0x20000000 within internal SRAM (SRAMX or SRAM0) or 0x10000000 for
SPIFI XIP and 0x80000000 for parallel flash XIP image),如果地址都没找到,则进入ISP模式,等待命令下载代码。
-
如何确定有效的Image呢,NXP巧妙的利用了中断向量表的信息,来装载NXP特有的Mark,从而确定是否有效的代码。
- 中断向量表中,有几个位置是保留,且中断向量表处于代码开始放置,方便确认,可参考笔者之前文章ARM学习(5) 异常模式学习(CortexM3/M4)
- 从0x20的位置确定是否是0xEDDC94BD
- 从0x24的位置,确定真正NXP的Image Header位置信息,从而跳转过去,找到Image Header决定走XIP还是SRAM等
-
NXP LPC单片机有自己的Image Header,可以设置一些属性,通过链接脚本的方式,包括分散加载的地址,也是链接脚本里面指定。
- Image Type:选择ram方式还是xip方式
- Load_adderess:加载地址,SRAMX还是SPIFI地址
- CRC check选择:设置是否进行CRC 校验
- Image Type:选择ram方式还是xip方式
-
经过这些研究,就可以知道XIP的设置和RAM的设置表了,主要关心的还是 Image Type和Loadaddress。
2.2 链接脚本修改
中断向量表
extern void (* const g_pfnVectors[])(void);
extern void * __Vectors attribute ((alias (“g_pfnVectors”)));
WEAK extern void __imghdr_loadaddress();
WEAK extern void __imghdr_imagetype();
void (* const g_pfnVectors[])(void) = {
// Core Level - CM4
&_vStackTop, // The initial stack pointer
ResetISR, // The reset handler
.......,
0, // ECRP
(void (*)(void))0xEDDC94BD, // Reserved
(void (*)(void))0x160, // Reserved
.....,
(void (*)(void))0xFEEDA5A5, // Header Marker,0x160
__imghdr_imagetype, // (0x04) Image Type
__imghdr_loadaddress, // (0x08) Load_address
(void (*)(void))(((unsigned)_image_size) - 4), // (0x0C) load_length, exclude 4 bytes CRC field.
从上面中断向量表中可以看出,NXP LPC单片机在中断向量表中做了手脚,设置了一些参数,和启动方式结合起来,包括还可以知道ImageSize等。
注意观察,其声明是一个虚函数的方式(如果链接脚本里面没指定,也可以链接过),符合中断向量表数组的定义,其实际是链接脚本里面指定的数据。
RAM方式的链接脚本:
memory 布局定义,外部Flash地址,SRAMX,SRAM0-3地址,在链接脚本指定布局时,可以指定其位置。
MEMORY
{
/* Define each memory region */
BOARD_FLASH(rwx) : ORIGIN = 0x10000000, LENGTH = 0x1000000 /* 16M bytes (alias RAM) */
SRAMX (rwx) : ORIGIN = 0x0, LENGTH = 0x30000 /* 192K bytes (alias RAM) */
SRAM_0_1_2_3 (rwx) : ORIGIN = 0x20000000, LENGTH = 0x28000 /* 160K bytes (alias RAM2) */
USB_RAM (rwx) : ORIGIN = 0x40100000, LENGTH = 0x2000 /* 8K bytes (alias RAM3) */
}
/* Define a symbol for the top of each memory region */
__base_SRAMX = 0x0 ; /* SRAMX */
__base_RAM = 0x0 ; /* RAM */
__top_SRAMX = 0x0 + 0x30000 ; /* 192K bytes */
__top_RAM = 0x0 + 0x30000 ; /* 192K bytes */
__base_SRAM_0_1_2_3 = 0x20000000 ; /* SRAM_0_1_2_3 */
__base_RAM2 = 0x20000000 ; /* RAM2 */
__top_SRAM_0_1_2_3 = 0x20000000 + 0x28000 ; /* 160K bytes */
__top_RAM2 = 0x20000000 + 0x28000 ; /* 160K bytes */
__base_USB_RAM = 0x40100000 ; /* USB_RAM */
__base_RAM3 = 0x40100000 ; /* RAM3 */
__top_USB_RAM = 0x40100000 + 0x2000 ; /* 8K bytes */
__top_RAM3 = 0x40100000 + 0x2000 ; /* 8K bytes */
中断向量表定义,可看到在其前面定义了一些信息,data段的信息,data ram和data ram3都没有用到,我们就不考虑了,
“AT > xxxx”:加载地址
“> xxx”:执行地址
分散加载做的就是将加载地址数据搬到执行地址,如果本身两者地址一样,则无需处理。
/* 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_RAM) - LOADADDR(.text)) + __base_SRAMX);
LONG( ADDR(.data_RAM));
LONG( SIZEOF(.data_RAM));
LONG((LOADADDR(.data) - LOADADDR(.text)) + __base_SRAMX);
LONG( ADDR(.data));
LONG( SIZEOF(.data));
LONG((LOADADDR(.data_RAM3) - LOADADDR(.text)) + __base_SRAMX);
LONG( ADDR(.data_RAM3));
LONG( SIZEOF(.data_RAM3));
__data_section_table_end = .;
__bss_section_table = .;
LONG( ADDR(.bss_RAM));
LONG( SIZEOF(.bss_RAM));
LONG( ADDR(.bss));
LONG( SIZEOF(.bss));
LONG( ADDR(.bss_RAM3));
LONG( SIZEOF(.bss_RAM3));
__bss_section_table_end = .;
__section_table_end = . ;
/* End of Global Section Table */
*(.after_vectors*)
} > SRAMX AT> SRAMX
从上面脚本来分析,目前代码段的加载地址就处于SRAMX,所以直接写LOADADDR(.data)也行,看图2,加载地址和图1是一样的,
加载地址其实就是存放的位置,相对代码段的位置,但是却不是执行的地址。
代码段的定义,其加载地址和执行地址一样,均是SRAMX,同时一些只读数据和const数据,也存放在代码段。
.text : ALIGN(4)
{
*(.text*)
*(.rodata .rodata.* .constdata .constdata.*)
. = ALIGN(4);
} > SRAMX AT> SRAMX
_etext = .;
data段的定义,加载地址和执行地址不一样,前者是执行地址,后者是加载地址,说明其正式执行的时候,在SRAM0-3,加载的时候在SRAMX。
加载地址和存储地址也不是一个概念,存储地址是真正存放代码的位置,程序运行需要再特殊的地方,比如内存,所以肯定需要搬到对应的位置,BOOT做的就是这个事情。搬到对应的位置后,就是真正执行的地址吗,也不竟然,BOOT通常是做很简单的事情,整体都搬过去,包括代码和数据,但是代码和数据往往要分开,所以就有了分散加载。
/* Main DATA section (SRAM_0_1_2_3) */
.data : ALIGN(4)
{
FILL(0xff)
_data = . ;
PROVIDE(__start_data_RAM2 = .) ;
PROVIDE(__start_data_SRAM_0_1_2_3 = .) ;
*(vtable)
*(.ramfunc*)
KEEP(*(CodeQuickAccess))
KEEP(*(DataQuickAccess))
*(RamFunction)
*(.data*)
. = ALIGN(4) ;
_edata = . ;
PROVIDE(__end_data_RAM2 = .) ;
PROVIDE(__end_data_SRAM_0_1_2_3 = .) ;
} > SRAM_0_1_2_3 AT>SRAMX
BSS段又不一样,其不用存储,加载地址和执行地址一样,只要初始化成0就行。
/* MAIN BSS SECTION */
.bss : ALIGN(4)
{
_bss = .;
PROVIDE(__start_bss_RAM2 = .) ;
PROVIDE(__start_bss_SRAM_0_1_2_3 = .) ;
*(.bss*)
*(COMMON)
. = ALIGN(4) ;
_ebss = .;
PROVIDE(__end_bss_RAM2 = .) ;
PROVIDE(__end_bss_SRAM_0_1_2_3 = .) ;
PROVIDE(end = .);
} > SRAM_0_1_2_3 AT> SRAM_0_1_2_3
堆栈区域,存放于SRAMX区域,堆后面预留了栈的4K空间,如果空间最后不够,链接脚本会报错。
.stack区域指明了栈的起始地址,没预留空间,由上面那个空间预留保证,很巧妙解决了栈递减的空间逻辑。
SECTIONS
{
/* Reserve and place Heap within memory map */
_HeapSize = 0x1000;
.heap : ALIGN(4)
{
_pvHeapStart = .;
. += _HeapSize;
. = ALIGN(4);
_pvHeapLimit = .;
} > SRAMX
_StackSize = 0x1000;
/* Reserve space in memory for Stack */
.heap2stackfill :
{
. += _StackSize;
} > SRAMX
/* Locate actual Stack in memory map */
.stack ORIGIN(SRAMX) + LENGTH(SRAMX) - _StackSize - 0: ALIGN(4)
{
_vStackBase = .;
. = ALIGN(4);
_vStackTop = . + _StackSize;
} > SRAMX
最后这些都是符号,不占用任何空间size,如果程序用到了,直接将对于的数据连接进入,而不是变量进行加载。
如果确实是运行态的数据,就需要占用空间size。
/* ## Create checksum value (used in startup) ## */
PROVIDE(__valid_user_code_checksum = 0 -
(_vStackTop
+ (ResetISR + 1)
+ (NMI_Handler + 1)
+ (HardFault_Handler + 1)
+ (( DEFINED(MemManage_Handler) ? MemManage_Handler : 0 ) + 1) /* MemManage_Handler may not be defined */
+ (( DEFINED(BusFault_Handler) ? BusFault_Handler : 0 ) + 1) /* BusFault_Handler may not be defined */
+ (( DEFINED(UsageFault_Handler) ? UsageFault_Handler : 0 ) + 1) /* UsageFault_Handler may not be defined */
) );
/* Provide basic symbols giving location and size of main text
* block, including initial values of RW data sections. Note that
* these will need extending to give a complete picture with
* complex images (e.g multiple Flash banks).
*/
_image_start = LOADADDR(.text);
_image_end = LOADADDR(.data) + SIZEOF(.data);
_image_size = _image_end - _image_start;
/* Provide symbols for LPC540xx parts for startup code to use
* to set image to be plain load image or XIP.
* Config : Plain load image = true
*/
__imghdr_loadaddress = ADDR(.text);
__imghdr_imagetype = 1;
}
XIP方式链接脚本