BootLoader
Bootloader与OTA
-
当Flash比较小的时候,程序指令在Flash上,同时也在Flash上运行,这种叫做XIP
-
XIP解释:
-
XIP 是 "eXecute In Place"(芯片内执行)的缩写。它是一种技术,允许程序代码直接从非易失性存储器(如 NOR Flash 闪存)中执行,而无需先将代码加载到 RAM(随机存取存储器)中。
-
原理:CPU 可以直接从存储器中读取指令(取指),然后进行解码和执行。通常用于 NOR Flash,因为它支持快速随机访问,而 NAND Flash 由于读写特性不适合直接执行。 原因:NOR Flash的读取类似与RAM随机读取,而NANDFlash顺序读取。
-
优点:减少系统启动时间,降低 RAM 使用量,适合资源受限的嵌入式设备。
-
例子:在嵌入式系统中,XIP 常用于存储引导程序(Bootloader)或固件,直接从 Flash 运行初始化代码。
-
-
NAND Flash 和 NOR FLash区别:
-
基本结构:
-
NAND Flash :
-
存储单元以串行方式连接,类似 NAND 逻辑门的结构。
-
数据按页面(page,通常 2KB-16KB)或块(block,通常 64KB-512KB)为单位访问,不支持字节级随机访问。
-
容量大。
-
-
NOR Flash:
-
存储单元以并行方式连接,类似于传统 RAM 的结构。
-
每个存储单元独立寻址,支持字节级(byte-level)随机访问。 、
-
内部设计更接近于逻辑门(NOR 门命名来源)
-
支持XIP技术
-
容量较小。
-
-
-
使用特性:
-
NAND Flash:
-
顺序访问:数据按页面和块读写,适合大容量存储,不支持 XIP。
-
可靠性:位错误率较高,需搭配纠错码(ECC)。
-
擦写寿命:通常 1 万到 10 万次,视具体技术(如 SLC、MLC、TLC)而定。
-
-
NOR Flash:
-
随机访问:支持直接执行代码(XIP,eXecute In Place),无需加载到 RAM。
-
可靠性:位错误率低,适合存储关键代码。
-
擦写寿命:一般为 10 万次左右。
-
-
-
-
-
Flash比较小的单片机APP更新自己是不可能的,在更新APP的过程中会覆盖下载函数和烧写函数,这就没有办法更新。
-
单片机的RAM比较大,把Flash的程序copy到RAM上面在运行,这样就可以下载更新APP。
-
缺点:
-
当烧写到一半,如果烧写中断,板子就变成砖头,必须返回原厂修复。
-
-
-
在单片机内,Flash上烧写有BootLoader和APP,启动过程如下:
-
上电的时候BootLoader先运行;
-
BootLoader判断发现:Flash上有APP并且无需升级,BootLoader就会启动APP
-
BootLoader判断发现:Flash上没有APP或者需要升级,BootLoader执行升级操作
-
当发现需要升级的时候,从WIFI获取要下载的APP,把它放到RAM上,下载完之后,将某个标志位置一,来标记是否完成,下次启动的时候检查标志位,如果发现不是1,重新去下载,这样开发板就不会变成砖。
-
BootLoader主要功能:
-
初始化硬件:比如设置时钟,初始化内存
-
启动内核/APP:从Flash读出内核,存入内存,给内核设置参数,启动内核
-
调试作用:在开发产品时需要经常调试内核,使用BootLoader可以方便的更新内核
-
当使用SPI外扩一个Flash的时候,它不是XIP设备,不能直接执行,需要靠BootLoader将外部Flash中的指令给copy到RAM中。CPU才能执行到相应的指令。
STM32中的start_up文件
-
不能人认为是BootLoader,只能认为是BootLoader的一小段代码
链接地址、加载地址、运行地址,烧录地址
在嵌入式开发(尤其是 STM32 这类 MCU)中,链接地址(Link Address)、烧录地址(Flash Address)、运行地址(Run Address)、加载地址(Load Address) 这四个地址有着不同的含义,它们之间的关系如下:
1. 各地址的定义
-
链接地址(Link Address)
-
由链接器(Linker)在链接阶段确定的地址,即编译生成的 ELF/HEX 文件中的代码和数据的逻辑地址。
-
这个地址决定了代码在程序执行时应该在哪个地址运行。
-
这个地址可以在链接脚本(.ld 文件)*或*链接选项中人为设定。
-
-
烧录地址(Flash Address)
-
代码最终被烧录到 MCU 的存储器(通常是 Flash)中的物理地址。
-
这个地址一般和 MCU 的 Flash 地址范围一致,比如 STM32F103 的 Flash 从
0x08000000
开始。 -
这个地址是 MCU 启动时 CPU 读取指令的首地址,通常由烧录工具(如 ST-Link Utility)决定。
-
-
运行地址(Run Address)
-
代码执行时,程序在RAM 或 Flash 中实际运行的地址。
-
在某些情况下,程序会从 Flash 拷贝到 RAM 运行(如 IAP、XIP),则运行地址 ≠ 烧录地址。
-
但在普通 STM32 应用中,代码直接从 Flash 运行,运行地址 = 烧录地址。
-
-
加载地址(Load Address)
-
二进制文件(如
.bin
或.hex
)中记录的代码加载的起始地址,通常和烧录地址相同。 -
该地址告诉烧录工具:程序应该烧录到 Flash 的哪个位置。
-
2. 地址之间的关系
-
一般情况(代码直接在 Flash 运行,典型的 STM32 项目)
-
链接地址 = 运行地址 = 烧录地址 = 加载地址
-
例如:
Link Address = 0x08000000 Load Address = 0x08000000 Run Address = 0x08000000 Flash Address = 0x08000000
-
这意味着代码在
0x08000000
运行,也从0x08000000
烧录到 Flash。
-
-
特殊情况(如 Bootloader + 应用程序的方式)
-
Bootloader 运行在
0x08000000
,应用程序从
0x08004000
开始:
Bootloader: Link Address = 0x08000000 Load Address = 0x08000000 Run Address = 0x08000000 Flash Address = 0x08000000 应用程序: Link Address = 0x08004000 Load Address = 0x08004000 Run Address = 0x08004000 Flash Address = 0x08004000
-
这里
0x08004000
是人为设定的,告诉 Bootloader 应用程序要从这个位置启动。
-
-
特殊情况(如代码拷贝到 RAM 运行)
-
代码烧录在 Flash,但运行时拷贝到 RAM,例如:
Link Address = 0x20000000 (RAM) Load Address = 0x08000000 (Flash) Run Address = 0x20000000 (RAM) Flash Address = 0x08000000 (Flash)
-
这里
0x20000000
是 STM32 的 SRAM 地址,代码在 Flash 中存储,但在启动时拷贝到 RAM 运行,提高执行速度。
-
3. 哪些地址可以人为改变?
-
可变的地址
-
链接地址:可以在
.ld
链接脚本或编译选项里改(如修改FLASH (rx) : ORIGIN = 0x08004000
)。 -
运行地址:可以通过软件控制,如 Bootloader 解析程序后跳转到某个地址执行。
-
烧录地址:可以用烧录工具(如 STM32CubeProgrammer)设定程序烧录到不同 Flash 地址。
-
加载地址:一般等于烧录地址,但如果是二次加载(如 Bootloader 先加载),可以人为控制。
-
-
等价关系(默认情况)
-
运行地址 = 链接地址(如果代码不拷贝到 RAM 运行)
-
加载地址 = 烧录地址(默认情况下一致)
-
4. 结论
-
大多数情况下(代码直接运行在 Flash)
-
链接地址 = 运行地址 = 烧录地址 = 加载地址
-
-
如果程序被 Bootloader 加载(如 IAP),运行地址可以改变
-
运行地址 ≠ 烧录地址
-
-
如果代码从 Flash 拷贝到 RAM 运行,运行地址也不同
-
运行地址 ≠ 烧录地址
-
你现在是要做 Bootloader 方案,还是要运行代码到 RAM,还是有别的应用场景?
不能将链接地址和加载地址理解为就是程序的烧录位置:
-
烧录通常指的是将程序写入非易失行存储器的过程;
-
链接地址:
-
链接地址不一定是烧录位置,而是程序设计时假设的运行位置。
-
如果程序需要直接在烧录位置运行(如 NOR Flash 的 XIP),链接地址通常与烧录位置一致。
-
如果程序烧录后需要加载到其他地方运行(如 NAND Flash 加载到 RAM),链接地址可能是 RAM 的地址,与烧录位置无关。
-
例子:
-
程序烧录到 NOR Flash(0x08000000),支持 XIP,链接地址也是 0x08000000。
-
程序烧录到 NAND Flash(0x80000000),但运行在 RAM(0x20000000),链接地址是 0x20000000。
-
-
-
加载地址:
-
对于直接运行的程序(如 XIP),加载地址通常就是烧录位置。
-
对于需要加载的程序(如从 NAND Flash 加载到 RAM),加载地址是程序被放入 RAM 的地址,与烧录位置不同。
-
例子:
-
NOR Flash XIP:烧录到 0x08000000,加载地址也是 0x08000000。
-
NAND Flash:烧录到 0x80000000,加载到 RAM(0x20000000),加载地址是 0x20000000。
-
-
函数地址的设定:
-
函数地址的bit0 = 1表示的是Thumb(16位的)指令集,bit0 = 0表示使用的是ARM指令集(32位的)
Cortex-M3,M4上电之后的流程:
-
上电之后从异常向量表里取出第一个数值,然会把数值赋值给SP寄存器,然后以第2个数值为指令地址,跳转,这是硬件来完成的。
当使用BootLoader时上电的启动流程;
-
BootLoader这段的程序的启动流程,还是由硬件来完成,但是APP程序就不是硬件来完成。
-
需要BootLoader来帮助APP完成启动流程:
-
_Vectors DCD 0 //地址为addressA DCD Reset_Handler //表示的是Reset_Handler这个函数的地址
-
取出栈的初始值赋给SP寄存器,也就是DCD 0位置上(addressA)的值赋值给SP
-
然后addressA的值+4到下一条指令;
-
把Reset_Handler函数的地址赋值给PC。
-
BootLoader在APP有向量表时的流程:
-
重新设置vector地址
-
从新的vector中取出第一个数值,也就是栈的首地址,赋值给SP
-
从新的vector中取出第二个数值,也就是函数Reset_Handler函数的地址,赋值给PC
start_app PROC EXPORT start_app ;设置Vector的偏移地址根据寄存器 0xE000ED08 ldr r3,=0xE000ED08 ;将地址0xe000ed08写入到r3中 str r0,[r3]; ;将r0的值写入r3所指的位置 r0 = 0x08040000 向量表的地址 ;设置新的vector值 ldr sp,[r0] ;取出r0地址上的值 sp = *r0 ldr r1,[r0,#4] ;r1 = *[r0+4],得到Reset_Handle函数的地址 bx r1 ENDP
BootLoader分类:
-
APP烧写在Flash上,APP也在Flash上直接运行,BootLoader直接跳到APP位置即可
-
APP烧写在Flash上,APP应该在RAM中去运行,但是APP会把自己复制到RAM,BootLoader直接跳到APP位置即可:
-
自己复制自己,根据自己设置的链接地址,把自己的程序从存储地址放到指定的链接地址处,APP会被拆分为只读段(代码段),可读可写段(数据段)。
-
-
APP烧写在Flash上,APP应该在RAM里运行,BootLoader把APP复制到RAM,再跳到RAM里执行APP
-
修改异常向量的地址:两种方法
-
方法1:硬件支持修改vector地址
-
方法2:硬件不支持,需要进行跳转
-
散列文件:
; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x20000000 0x00010000 { ; load region size_region ER_IROM1 0x20000000 0x00010000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) .ANY (+RW +ZI) } }
-
LR_IROM1:描述了程序的加载地址
-
ER_IROM1:描述了程序确切的运行地址
-
*.o (RESET, +First):将所有 .o 目标文件的包含 RESET(复位向量表),确保复位处理程序排在第一位,确保正确的启动。
-
*(InRoot$$Sections):确保某些关键代码(如 C 运行时初始化代码)放在根段(Root Section),避免被优化掉。
-
.ANY (+RO):所有只读段(如 代码、常量)都放在
0x20000000
RAM 里。 -
.ANY (+XO):可执行代码(通常是
code
段)放在0x20000000
。 -
.ANY (+RW +ZI): 所有可读写数据(初始化全局变量)放在
0x20000000
。所有没有初始化的全局变量(bss段)也放在0x20000000
BootLoader、APP烧写在Flash上,但是APP应该在RAM中去运行:
-
例如APP烧写的位置为0x08040000但是我们使用了散列文件,将加载地址和运行地址设置为了0x20000000,这个时候会出现问题,当MCU上电的时候,会从0x20000000的位置上,开始执行,但是这个位置上没有正确的指令或数据,导致程序崩溃或者无法启动,解决方法:
-
APP自己把0x08040000位置上的代码给复制到0x20000000上去。如果没有 Bootloader,通常需要在程序中编写代码,手动将 Flash 中的程序复制到
0x20000000
地址(RAM),然后跳转到该地址开始执行。 -
BootLoader来帮助APP进行复制操作,Bootloader 会在上电时运行,负责从 Flash 加载程序到 RAM 中,然后跳转到 RAM 地址进行执行。
-
-
适用场景:
-
RAM比较大,Flash比较小,希望APP尽快的运行,所以要把APP复制到RAM上
-
SPI Flash和SD卡不是XIP设备,不能直接运行,需要拷贝到RAM上运行,比如IMX6ULL的SD卡启动和eMMC启动方式,eMMC是对NANDFlash的封装
-
BootLoader、APP烧写在Flash上,但是APP应该在RAM中去运行,自我复制的代码:
__Vectors DCD 0x20000000+0x10000 DCD 0x08040009 ;Reset_Handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT mymain IMPORT copy_myself IMPORT |Image$$ER_IROM1$$Length| adr r0,Reset_Handler ;r0 = 0x08040000 bic r0,r0,#0xff ldr r1,=__Vectors ;r1 = 0x20000000 ldr r2,= |Image$$ER_IROM1$$Length| BL copy_myself ldr pc, =mymain ;BL mymain ENDP void copy_myself(int *from,int *to,int len) { //从哪里拷贝,拷贝到什么地方,拷贝多大// int i; for(i = 0;i<len/4+1;i++) { to[i] = from[i]; } }
-
使用了BL来进行跳转,进行的是相对跳转,还是在Flash里面运行,还有一个原因是Reset_Handler地址还是被设置为了程序在Flash里面的地址。
-
ldr pc, =mymain改为这个语句的时候,就会跳转到RAM里面执行,因为这里使用了绝对地址,并且链接地址在0x20000000的位置
BootLoader、APP烧写在外部的Flash上,Boot Loader来帮助APP复制到RAM中
-
因为外部的Flash不具有XIP功能,所以APP没有办法进行自己把自己copy到RAM里面,因此需要BootLoader来帮助完成
-
我么一般在APP程序的前面加入一个头部,这个头部包含:、
-
加载地址,程序加载到RAM的地址
-
入口地址,第一条指令运行的地址
-
APP的长度
-
CRC校验码
-
-
我们使用mkimage来制作带头部的APP.bin文件,加入这个文件烧写在0x08040000这个位置:
-
Bootloader读0x08040000这个位置得到头部
-
解析头部
-
读APP.bin文件写入RAM里面
-
执行第一条命令
-
主要代码:
-
start_app PROC EXPORT start_app ;设置Vector的地址根据寄存器 VTOR寄存器:0xe000ed0c ldr r3,=0xE000ED08 ;将地址0xe000ed0c写入到r3中 str r0,[r3]; ;将r0的值写入r3所指的位置 ;设置新的vector值 ldr sp,[r0] ;取出r0地址上的值 sp = *r0 ldr r1,[r0,#4] ;r1 = *[r0+4],得到Reset_Handle函数的地址 bx r1 ENDP #include "uart.h" typedef unsigned int __be32; typedef unsigned char uint8_t; #define IH_MAGIC 0x27051956 #define IH_NMLEN 32 //#define be32_to_cpu(x) ((((x) & 0xFF000000) >> 24) | \ // (((x) & 0x00FF0000) >> 8) | \ // (((x) & 0x0000FF00) << 8) | \ // (((x) & 0x000000FF) << 24)) unsigned int be32_to_cpu(unsigned int x) { unsigned char *p = (unsigned char *)&x; unsigned int le; //将原来在低地址的高字节段移动到高地址 le = (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + (p[3]); return le; } typedef struct image_header { __be32 ih_magic; /* Image Header Magic Number */ __be32 ih_hcrc; /* Image Header CRC Checksum */ __be32 ih_time; /* Image Creation Timestamp */ __be32 ih_size; /* Image Data Size */ __be32 ih_load; /* Data Load Address */ __be32 ih_ep; /* Entry Point Address */ __be32 ih_dcrc; /* Image Data CRC Checksum */ uint8_t ih_os; /* Operating System */ uint8_t ih_arch; /* CPU architecture */ uint8_t ih_type; /* Image Type */ uint8_t ih_comp; /* Compression Type */ uint8_t ih_name[IH_NMLEN]; /* Image Name */ } image_header_t; extern void start_app(unsigned int); void delay(int d) { while(d--); } void copy_myself(int *from,int *to,int len) { //从哪里拷贝,拷贝到什么地方,拷贝多大// int i; for(i = 0;i<len/4+1;i++) { to[i] = from[i]; } } void relocate_and_start_app(unsigned int pos) { image_header_t *head; unsigned int load; unsigned int size; unsigned int new_add; /*读出头部*/ head = (image_header_t *)pos; load = be32_to_cpu(head->ih_load); size = be32_to_cpu(head->ih_size); putstr("load = "); puthex(load); putstr("\r\n"); putstr("size = "); puthex(size); putstr("\r\n"); new_add = pos+sizeof(image_header_t); /*把程序复制到内存*/ copy_myself((int *)new_add,(int *)load,size); /*跳转执行*/ start_app(new_add); } int mymain() { unsigned int app_pos = 0x08040000; //获取带头节点的.bin文件的地址 uart_init(); putstr("bootloader\r\n"); relocate_and_start_app(app_pos); return 0; }
-
BootLoader中的异常向量表:
-
如果APP没有就绪,发生异常,执行BootLoader的异常处理函数,也就是当前运行的是BootLoader
-
如APP已经运行,如果处理器不能重新设置异常向量表的位置,那么就还是使用BootLoader的异常函数,但是在BootlLoaader的异常函数里面,要设置跳转去执行APP对应函数。
在内存中调试程序:
-
修改链接脚本(散列文件)把FreeRTOS程序的链接地址指定为RAM
-
配置工程使用散列文件
-
散列文件内容:
-
LR_IROM1 0x20000000 0x00010000 { ; load region size_region ER_IROM1 0x20000000 0x00010000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) .ANY (+RW +ZI) } }
-
-
设置初始文件
-
FUNC void Setup(void){ sp = _RDWORD(0x20000000); pc = _RDWORD(0x20000004); __WDWORD(0xE0000ED08,0x20000000); } LOAD F103_Module\F103_Module.axf INCREMENTAL Setup(); g,main
-
-
设置Debug Pack
第四个BootLoader问题:
-
static struct vectors* new_vector; //反汇编文件为: address size variable name type 0x20000000 0x4 new_vector pointer to vectors /*把程序复制到内存*/ copy_myself((int *)new_add,(int *)load,size); /*跳转执行*/ set_new_vector(new_add); start_app(new_add);
-
这就存在问题,这个变量的值存在RAM里面,并且和APP在RAM上的位置相同,当使用set_new_vector这个函数的时候会破坏先前在RAM上拷贝好的程序,这就会导致程序执行失败。
-
第四个BootLoader主要针对的是没有0xE000ED08这个寄存器的处理器。
-
FreeRTOS中的问题:
-
APP发生异常的时候硬件帮我们保存一部分的寄存器,让LR = EXC_RETURN这个特殊值。
-
当BX LR的时候这个特殊值会根据PSP或者MSP从中恢复寄存器。
-
-
bit3 = 1表示返回线程模式,说明
-
bit2 = 1 表示返回线程栈,第一个任务的时候伪造了线程栈,说明任务切换完成。
-
-
-
-
根据BootLoader中的代码和FreeRTOS中的代码
-
void SVC_Handler(void) { /*跳转去执行APP的SVC_Handler*/ //new_vector->SVC_Handler(); struct vectors* new_vector = (struct vectors*)(*((unsigned int*)RAM_END)); new_vector->SVC_Handler(); } __asm void vPortSVCHandler( void ) { PRESERVE8 ldr r3, =pxCurrentTCB /* Restore the context. */ ldr r1, [r3] /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */ ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */ ldmia r0!, {r4-r11} /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */ msr psp, r0 /* Restore the task stack pointer. */ isb mov r0, #0 msr basepri, r0 orr r14, #0xd bx r14 }
-
当发生异常的时候先到BootLoader中,去执行BootLoader的异常处理函数,再去跳到APP中去执行他的异常函数,但是vPortSVCHandler这个异常函数进行任务的切换的,需要LR是一个特殊值,这个特殊值产生在异常发生的时候,但是我们是从BootLoader中跳过来执行的所以LR已经不是一个特殊值,因此程序执行到这里就会崩溃。
-
解决方法:
-
SVC_Handler PROC EXPORT SVC_Handler ldr r0, =(0x20000000+0x10000-4) ldr r0, [r0] ;r0 = new vector ldr r0, [r0,#0x2c] ;r0 = (APP)SVC_Handler bx r0 ENDP
-
使用汇编来完成SVC_Handler这个函数,如果用C语言去跳转到APP去执行,会使用BLX指令来跳转,会改变LR的值,我们使用BX指令来进行跳转的话就不会修改LR的值了
-
-
-
-
-
FreeRTOS源码中
__asm void prvStartFirstTask( void ) { PRESERVE8 /* Use the NVIC offset register to locate the stack. */ //ldr r0, =0xE000ED08 //ldr r0, [r0] //r0 = vector address //ldr r0, [r0] //r0 = first item of vector ldr r0 ,=0x20000000 ldr r0, [r0] /* Set the msp back to the start of the stack. */ msr msp, r0 /* Globally enable interrupts. */ cpsie i cpsie f dsb isb /* Call SVC to start the first task. */ svc 0 nop nop }
使用了0xE000ED08这个寄存器,
-
-
命令系统:
-
执行help:列出所有命令的简短用法
-
执行help cmd:列出命令cmd的详细用法
-
执行 cmd
-
定义命令结构体:
struct command{ char *short_help; char *long_help; int (*function)(int argc,char*argv[]); };
-
管理命令:
struct command *g_commands[] = { &help_cmd, &load_cmd, &md_cmd, &mw_cmd, &flash_cmd, };
-
执行命令:
1. 在shell里接下字符串,得到第一个字符 2. 根据第一个字符在g_commands找到某项 3. 构造一系列参数(argc,argv),执行这项的function函数
实现zmodem下载命令:
-
单片机通过串口和PC机相连接,通过程序Mobuxterm发送文件给开发板
分析rz命令:
rz -b binary transfer rz -z use ZMODEM protocol rz -b -z 发送一个二进制文件到开发板
分析源码:
main() { Rxbinary=TRUE protocol=ZM_ZMODEM /* initialize zsendline tab */ zsendline_init(); readline_setup(0, HOWMANY, MAX_BLOCK*2); if (wcreceive(npats, patts)==ERROR) { exitcode=0200; canit(STDOUT_FILENO); } } //核心函数wcreceive
BootLoader的散列文件:
-
使用到了全局变量,全局变量的的初始值被放到Flash里面,当程序运行的时候把初始值给拷贝到RAM里面,因此要进行重定位。
-
如果没有使用这个重定位的话,会有这个情况:比如int rw = 10;这个全局变量在程序开始运行的时候放到了0x08000100,但程序运行时试图修改它,会失败(Flash 只读)。程序崩溃。
-
ZI段如果不进行清零的话,可能会导致随机值。