一、零地址映射
以 ARM Cortex-M 为例,CPU上电后就从地址 0x0000000 开始读取数据和指令。但是,片内 ROM 地址一般不会以 0x0000000 开始,这就需要芯片把片内 ROM 的真实地址映射到 0x0000000 上。如下图:
图中,芯片上电时自动根据 BOOT 引脚电平,可以选择把系统区 ROM(0x1FFFF000) 和用户编程区 ROM(0x08000000) 中的一个映射到 0x0000000 地址。其中,系统存储区已经被厂家烧录了一个固定程序,用来支持串口、USB等烧录功能,以方便用户(在没有 jlink 工具的情况下)烧录程序。
芯片上电时, 0x0000000 处存储的数据格式,是被 Cortex-M 核规定好的,由启动代码生成。
二、栈指针与复位函数
Cortex-M 核对 0x0000000 处的前8个字节(两个32位int值)的数据是有要求的:第一个int值会被赋给栈指针SP,第二个int值是复位函数指针,指向上电后第一个被执行的函数。
这两个int值是由启动代码决定的,启动代码一般由芯片厂家的SDK库或者编程工具按芯片型号自动提供,例如下面的代码片段:
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
上述代码,指定了最终生成的bin文件的开头,是一个SP栈指针和一张向量表。其中,复位函数 Reset_Handler 的地址就是复位向量,该函数主要负责系统初始化、C环境初始化,然后进入 main 函数,例如:
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
- SystemInit 函数一般是由芯片厂家的SDK库提供的C函数,负责初始化时钟。
- __main 这个函数除了初始化C环境,还会干一些别的事情,不同的编译工具、不同的厂家SDK库都会不尽相同。例如KEIL编程工具,它还会干一件比较重要的事情就是分散加载,如果你需要把程序加载到RAM里跑,这个分散加载功能就会很有用。
三、程序加载与重定位
另外,关于加载地址,网上很多人说是bin文件的存储地址,应该是不够准确的,容易引起误会。
加载地址是指存储器(例如ram、flash或硬盘)里的一段程序运行之前将要被(系统或引导程序)搬运到哪里去。一般,我们需要把一个程序搬运到它的链接地址上,才能正确运行,但是也有例外,比如:
- 像一些简单的程序,是天然可重定位的(指令访问的地址都是基于PC的相对值),那么它被加载到哪里都是能跑的;
- 像linux内核这种,加载到哪里都能跑,因为它自己的镜像文件头里有链接地址,内核会自行把自己重新复制到正确的链接地址上跑;
- 像uboot这种引导程序,它被编译成一个可重定位程序,无论它被加载到哪里,那么uboot都可以把自己重定位到任何地址上跑。
可见,存储地址不一定是加载地址,加载地址不一定是链接地址,链接地址也不一定是运行地址。
一般,对于单片机而言,存储地址、加载地址、链接地址 、运行地址都是一样的,比如 0x08000000,或者再加上一个偏移 0x0800xxxx。当你使用分散加载功能把单片机程序放在RAM里跑时,存储地址就不再等于加载地址了,但是加载地址和链接地址还是要一样的;如果你还希望程序像uboot一样可重定位(就是同一个bin既能在A地址上跑,也能在B地址上跑,即运行地址不等于链接地址),那么这将比uboot重定位要复杂得多,因为KEIL支持的可重定位是一个阉割版,使用时还有一些限制,具体要参考KEIL手册。
为了方便,除非必要,不建议使用分散加载和重定位功能!更简单的代码逻辑,工程上就很有优势。
所以,对于启动代码,我们就使用SDK库给定的文件,最好不要再修改。
总结
- 单片机一上电,就有能力读取并执行我们烧录到 0x08000000 处的程序,无需任何引导;
- 启动代码文件由芯片厂商SDK库提供,我们尽量不要修改和关注,写好自己的 main 函数即可;
- 为了简单和高效执行,程序尽量放在片内ROM上跑,避免出现加载到RAM甚至重定位;