目 录
1. 前言
2. 初识BootLoader
2.1 百度百科的BootLoader
2.2 BootLoader的简单理解
2.3 BootLoader的作用
3. BootLoader预备知识
3.1 复位序列
3.1.1 栈指针
3.1.2 复位向量
3.2 重定位中断向量表
3.2.1 STM32的中断向量表
3.2.2 设置中断向量表偏移
3.3 分散加载文件相关
3.3.1 C语言的函数地址
3.3.2 BootLoader占用的ROM
3.3.3 修改ROM起始地址
3.4 hex文件和bin文件
3.4.1 hex文件
3.4.2 bin文件
3.5 Bin文件生成
4. 分几步实现BootLoader
4.1 跑FAT文件系统
4.2 读写Flash程序
4.2.1 Flash写入步骤
4.2.2 读写Flash调用的库函数
4.2.3 实现Flash读写
4.3 跳转到新程序运行
4.3.1 跳转到复位向量
4.3.2 App开始运行
5. Bootloader具体流程
5.1 主函数流程
5.2 BootLoader流程
5.3 跳转到新程序流程
附录A主函数
附录B更新说明
参考文献
1. 前言自从几个月前接触到有Bootloader这回事,就有一种强烈的冲动,想写一个BootLoader出来。很快在飞思卡尔的Cortex-M4单片机上实现,已经是好几个月前的事情了。然后关于BootLoader的事搁在一边好久了,这次弄个STM32的BootLoader出来,Cortex-M3的,顺便发表下博客,跟大家分享一下。
。。。
又过了大半年了吧,慢慢对BootLoader的认识也有点长进啦。特别是跟网友讨论后发现BootLoader的实现还是需要靠BootLoader程序和App程序的配合才能正常使用。在这里特别感谢网友cary_yingj ,对本BootLoader的研究后发现App程序需要重定位中断向量表,才能正常工作。
在其他网友的反馈下,本人准备再将次文档完善,把不够详细的地方写得再详细,并且力求通俗易懂一点。希望对学习BootLoader的同学们有所帮助。
2. 初识BootLoader可能有的同学听说过BootLoader,有的同学没有听说过,这个都很正常。关于BootLoader的概念大家可以上网查一下,有比较详细的说明,我在这里说说我自己比较片面的理解,并且是针对CortexM3说明的,实现平台为STM32F103VET6。
2.1百度百科的BootLoader这里借用一下百度百科对BootLoader的解释。在嵌入式操作系统中,BootLoader是在操作系统内核运行之前运行。可以初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统内核准备好正确的环境。在嵌入式系统中,通常并没有像BIOS那样的固件程序(注,有的嵌入式CPU也会内嵌一段短小的启动程序),因此整个系统的加载启动任务就完全由BootLoader来完成。在一个基于ARM7TDMIcore的嵌入式系统中,系统在上电或复位时通常都从地址0x00000000处开始执行,而在这个地址处安排的通常就是系统的BootLoader程序。
2.2BootLoader的简单理解BootLoader就是单片机启动时候运行的一段小程序,这段程序负责单片机固件更新,也就是单片机选择性的自己给自己下程序。可以更新也可以不更新,更新的话,BootLoader更新完程序后,跳转到新程序运行;不更新的话,BootLoader直接跳转到原来的程序去运行。
2.3BootLoader的作用BootLoader使单片机能自己给自己下载程序,所以在程序升级方面非常有作用。比如我们的BootLoader是通过GSM更新程序的,我们在升级单片机程序的时候,只要把新程序通过GSM发送给单片机,单片机自己实现程序更新,然后跳转到新程序执行,这样就省去我们很多升级的功夫啦。
可以想象一下如果把单片机安装在非常高的地方,或者危险的工业现场,或者封装得很难拆下来,我们很难直接给单片机下载程序,那么BootLoader的作用就体现出来了。简单的说,有了BootLoader,我们更新程序的话是省心又省力。
想想是不是很高级,还带点小兴奋哈哈。不用急,下面我们会继续介绍,让大家都能自己实现BootLoader。至于是通过什么方式升级,这个大家自由发挥,相信会设计出丰富多彩的BootLoader升级方式呢。
3.BootLoader预备知识我们这里是为ARM的Cortex-M3单片机写的BootLoader,需要了解一下M3内核的架构,并且要了解M3单片机是怎么启动的等等。这个方面的知识,可以参考《Cortex-M3权威指南》,这里的话我只是为了实现BootLoader简单介绍一下,大家有什么不清楚的请参考权威指南。并且这里是以STM32为例说明问题的,使用的开发环境是RVMDK(Keil)。
3.1复位序列这里参考的是《Cortex-M3权威指南》的3.8节,复位序列。
M3单片机复位后,从0x00000000取栈指针(SP),从0x00000004取复位向量(PC),有了栈指针和复位向量后,单片机就按照正常流程运行了,在BootLoader里面,我们更新完程序后需要做的步骤之一就是设置栈指针,跳转到复位向量。
3.1.1栈指针栈是一种数据结构,后进先出LIFO。借用百度百科的解释:栈由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。它使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放。
3.1.2复位向量复位向量是一个函数地址,在CortexM3单片机里是复位函数的地址。也就是单片机启动后第一个执行的函数。
3.2重定位中断向量表这里参考《Cortex-M3权威指南》的7.3节,向量表。
BootLoader是一个完整的程序,下载的新程序(以下称为App)也是一个完整的程序。都包含中断向量表,所以的话,我们是有两个中断向量表,相信因为有两个向量表,大家都知道我们应该需要对这两个向量表做点什么吧。
3.2.1STM32的中断向量表我们只看前16个向量,因为其余的向量属于外设使用,与CortexM3内核无关。
__Vectors DCD __initial_spTop ;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
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
单片机启动默认先运行BootLoader,所以默认的中断向量表位置是BootLoader的中断向量表。为了App可以正常运行,下载完App后,我们还需要把中断向量表重新定位到App程序那里。根据《CortexM3权威指南》,介绍一下怎样重定位中断向量表。
3.2.2设置中断向量表偏移Cortex-M3单片机有一个管理中断向量表的寄存器,叫做向量表偏移量寄存器(VTOR)(地址:0xE000_ED08)。具体可以看看截图:
STM332程序的起始地址一般在0x08000000。所以BootLoader程序是在0x08000000,不是在0x00000000是因为STM32的重映射技术(不符合Cortex-M3的设计,有点搞另类的感觉)。所以BootLoader的中断向量表在0x08000000那里。如果我们的App程序起始地址在0x08070000,并且App的中断向量表在起始地址,那么BootLoader程序下载App后,为了App程序能正确运行,开始App程序的运行后第一步,就要把中断向量表重定位到0x08070000那里。
具体实现下面会再介绍,接下来介绍分散加载文件相关内容。
3.3分散加载文件相关这一节涉及的内容主要属于分散加载文件,大家具体上网了解,这里只是介绍了能够实现BootLoader的一小部分。
3.3.1C语言的函数地址我们知道C语言的函数名就是函数的地址,并且STM32单片机ROM的起始地址是在0x08000000,那么使用编译器编译程序的话(这里使用的是RVMDK),函数的地址默认都在以0x08000000为首的一段ROM里面了。比如我们一个函数Delay(),它的地址可以是0x08000167(CortexM3中函数的地址0bit位一般是1),也就是Delay函数的代码在0x08000167,C语言函数调用Delay时,就是执行0x08000167的代码。
3.3.2BootLoader占用的ROM我们需要注意的问题是,如果不修改程序默认的起始地址的话,那么BootLoader和新App程序的起始地址都是0x08000000,也就是他们重叠了(代码重叠),这样的话肯定相互之间有影响,程序是不能正常工作的。
这里的解决方法是,BootLoader程序依然占用0x08000000为首的那段ROM,因为STM32的默认就是从0x08000000运行程序的。保持BootLoader程序先能正确运行。然后App使用除BootLoader占用ROM以外的空间。这里需要知道BootLoader到底占用了多少ROM,很简单,查看MAP文件就行了。这里以我的BootLoader的MAP文件为例说明一下,看截图:
Memory Map of the image
Image Entry point :0x08000131
Load Region LR_IROM1 (Base: 0x08000000,Size: 0x00006da4, Max: 0x00080000, ABSOLUTE)
Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x00006d54, Max:0x00080000, ABSOLUTE)
主要是这句话“Base:0x08000000, Size: 0x00006da4, Max: 0x00080000”,这句话说明了我的BootLoader程序是从0x08000000开始,占用了0x00006DA4大小。只要我们的App不要和BootLoader程序占用的空间冲突就可以了。我的App程序的起始地址选择为0x08070000,不与BootLoader程序冲突。具体怎么修改ROM起始地址,下面介绍。
3.3.3修改ROM起始地址编译新程序的时候,我们要修改程序的起始地址,我的修改方法如下(开发环境是RVMDK):打开TargetOption...,切换到Target选项卡,如下
修改IROM1的起始地址和长度:
比如,为了不产生地址冲突,我将起始地址0x08000000修改成0x0807000,将ROM长度0x80000修改成0x10000。如下图所示(左图为修改前、右图为修改后):
注意:BootLoader程序是不需要修改的,只是App需要修改(App就是使用BootLoader下载的程序)。
3.4hex文件和bin文件3.4.1hex文件平时我们用j-Link或者串口下载程序的话,都是打开hex文件下载的,因为hex文件包含地址信息,下载程序的时候知道程序下载到ROM的哪个区域。从另一个角度上说,也就是hex文件是不能直接写进ROM的,一边写需要一边转换(解码出地址信息,将对应内容写入ROM)。
3.4.2bin文件bin文件的话,很好理解,是直接的可执行代码。也就是bin文件的内容跟下载ROM里面的内容是一样的。bin文件是没有包含地址信息的,所以在下载之前要知道bin文件是要下载到ROM的那个区域。
我们的BootLoader下载的是bin文件,直接写进STM32的Flash里面,地址信息的话就是上一节的IROM,0x08070000,从0x08070000开始连续写入,中间不间断。
3.5Bin文件生成默认情况下编译后生成的是hex文件,不过很轻松可以生成bin文件。介绍具体怎么生成bin文件,工具的话是使用fromelf.exe(目录一般是在Keil安装目录里面,本人的fromelf.exe目录是在C:KeilARMARMCCin),我们是使用fromelf工具将axf文件转换为bin文件。
熟悉命令行的同学可能会选择直接敲命令,不过这里介绍使用RVMDK提供的用户命令(编译时可以自动生成bin,省去每次生成bin文件都要敲命令的过程)。
打开TargetOption...,切换到User选项卡,如下
主要是在运行用户命令,Run#1
具体命令是(记得在Run#1前打勾,才会在编译后执行用户命令生成bin文件):
命令可以分为五部分,简化后是fromelf --bin -o xxx.bin xxx.axf,需要注意的是命令的五个部分之间要有空格。还需要说明的是路径问题,这里的路径都是相对.uvproj文件的,下面是我的目录(注意MY_STM32.uvproj文件和Output文件夹)。
我的bin文件和axf文件都在Output文件夹里面,并且路径是相对MY_STM32.uvproj的,Output文件夹里的bin文件(MY_STM32.bin)相对于MY_STM32.uvproj应该写成“.OutputMY_STM32.bin”。
l 第一部分
这部分是fromelf.exe文件的路径,根据自己的安装目录而变。我这里因为Keil是安装在C盘的,所以我的路径如下所示。
参考命令:C:KeilARMARMCCinfromelf.exe
l 第二部分
这部分是固定的,--bin表示生成bin文件。
参考命令:--bin
l 第三部分
这部分也是固定的,-o表示输出。
参考命令:-o
l 第四部分
这部分是生成文件的目录和文件名,我是输出在Output文件夹的,也就是bin文件在Output文件夹里面。
参考命令:.OutputMY_STM32.bin
l 第五部分
这部分是axf文件的目录和文件名,我们的bin文件是根据axf文件生成的,也就是说axf文件相当于输入,bin文件相当于输出。我的axf文件也在Output文件夹的。
参考命令:.OutputMY_STM32.axf
介绍了这些基本知识后,我们可以来实现BootLoader了。
4. 分几步实现BootLoader有了前面的基础知识后,应该是比较容易理解BootLoader需要怎么实现了。这一章,我们分几个步骤,一步一步实现BootLoader。
4.1跑FAT文件系统我们的BootLoader是从SD卡更新程序的,把在电脑上编译后的App程序,也就是bin文件,复制到SD卡中,然后让单片机读取相应的bin文件,就可以实现程序的更新。需要注意的是,App程序需要修改ROM的起始地址,再编译,并且要生成bin文件才支持正常下载。
我跑的文件系统是FATFS_R0.07c,很经典的一个版本。如果大家对文件系统方面不了解的话,请自己网上查找教程,或者说很多同学对这一步应该已经很熟悉啦。
只要单片机上实现读取bin文件,结合Flash写入程序,就可以实现程序更新。下面介绍读写Flash。
4.2读写Flash程序要实现BootLoader,还有一个前提是可以写入Flash了。如果是STM32单片机的话是很容易实现的,因为我们有官方库。本人使用的是3.0.0版本,参考官方例程,很容易实现Flash的读写,这里同样是为了实现BootLoader简单介绍一下。
4.2.1Flash写入步骤l 解锁Flash
l 擦除Flash
l 写入Flash
l 验证读写是否正确
4.2.2读写Flash调用的库函数l voidFLASH_Unlock(void) Flash解锁
l FLASH_Status FLASH_ErasePage(uint32_tPage_Address) Flash擦除
l FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_tData) Flash写入
4.2.3实现Flash读写稍微封装一下STM32的官方库函数,就能实现Flash的读写,并验证读写是否正确,具体我实现的接口函数为以下截图,大家可以参考一下:
来到这里,我们可以实现在bin文件写入Flash了,写入完后,就要跳转到App程序执行了,接下来继续介绍。
4.3跳转到新程序运行
l 跳转到复位向量
l 重定位中断向量表
l 设置栈指针
4.3.1跳转到复位向量BootLoader程序需要做的是跳转到复位向量,具体实现可以参考以下代码。
( (void (*)()) (Reset))(); //跳转到复位向量
注意( (void (*)()) (Reset) )();是一去就不返回的,执行完这条语句,单片机就直接跳转到App程序运行的,所以BootLoader程序下载完App后,做一些简单的处理(根据自己的应用,也可以不做任何处理),就用这条语句跳转到App执行。
4.3.2App开始运行BootLoader跳转到App后,App需要做的是先设置栈指针,然后重定位中断向量表地址,具体可以参考以下代码。
__set_MSP( Msp); //设置栈指针
NVIC_SetVectorTable( base, offset); //重定位中断向量表
其中Msp是栈指针,也就是中断向量表第一个字的内容,我们这里的内容是*((uint32_t)(0x08070000) )。
base是中断向量表的基地址,一般情况下就是ROM的起始地址,这里是0x08070000。
至此,BootLoader实现步骤完了,相信熟悉了这几个步骤后,大家可以自己给自己的单片机写个BootLoader。顺便说一下,Cortex-M4的BootLoader跟Cortex-M3几乎是一样的。我在STM32上的实现完全是参考自己上次在飞思卡尔Cortex-M4上的实现。下面说一下我的主函数吧,我们再看看具体的BootLoader流程,再熟悉一下BootLoader。
5.Bootloader具体流程
5.1主函数流程先看截图。
主函数的流程如下所示:
l 时钟初始化
l LED初始化(无关紧要)
l 调试接口初始化(无关紧要)
l Flash初始化(解锁Flash)
l FAT初始化(挂载文件系统)
l 我们的BootLoader(重点,下面展开继续介绍)
l 主循环(实际不会运行到这里)
然后在具体讲解BootLoader_FromSDCard函数,这就是我们的重点,传说中STM32的BootLoader从SD卡更新固件。
5.2BootLoader流程
具体流程如下所示:
l 打开bin文件,检查文件打开是否正确
l 设置Flash下载起始地址(App程序起始地址)
l 读取bin文件,检查读取是否正确
l 获取栈指针SP和复位向量PC
l 进入循环(这里是第5步),条件为如果读取bin文件字节数不为零
l 将读取到的bin写入Flash,并判写入状态
l 调整Flash地址,根据写入字节调整
l 继续读取bin文件,检查读取是否正确,回到5继续循环
来到这里已经是退出循环了,也就是说我们已经将bin写入Flash完成了,准备跳转到新程序运行
5.3跳转到新程序流程其实上面已经讲过了,这里继续啰嗦,截图:
l 重定位中断向量
l 设置栈指针
l 跳转到复位向量(开始运行App程序)
说明一下,在这里重定位中断向量其实是多余的,App程序执行初始化后,又回到STM32初始状态,所以在App程序中需要执行重定位中断向量表操作,具体同以上操作相同。
啰嗦了又一遍,BootLoader完全结束,感谢大家都支持啦~
#include "main.h"
int main(void)
{
SystemInit(); //配置系统时钟为72M
LED_GPIO_Config(); //初始化LED端口
Debug_TraceIOEnable(); //使能调试printf的IO口
Flash_Init(); //初始化Flash
FAT_Init(); //初始化文件系统
BootLoader_FromSDCard(); //Bootloader从SD卡更新固件
while(1)
{
LED_StatShow( FuncErr); //LED显示Bootloader状态
}