本篇主要讲基于Stm32的IAP升级(即Bootloader升级)。主要是讲解Bootloader制作思路(该思路不只是用于该系列的芯片)。此篇文章内容较多,需要慢慢消化,部分内容可以先跳着看。
一、IAP & Bootloader
1.1 IAP & ISP
- ISP:In System Programing,在系统编程
- IAP:In applicating Programing,在应用编程
ISP是指可以在板级上进行编程,也就是不用拆芯片下来,写的是整个程序,一般是通过ISP接口线来写。
IAP虽然同样也是在板级上进行编程,但是是自已对自已进行编程,在应用中进行编程,也即可以只是更改某一部分而不影响系统的其它部分,另外接口程序是自已写的,这样可以进行远程升级而不影响应用。
ISP即是我们平常编程下载的方法,每次烧录程序都要把MCU的Flash全部都擦除一遍;IAP下载,则是擦除部分Flash(APP),另外部分未擦除部分(Bootloader)可以根据自己祖传留下的代码搞一些骚操作,例如把自己的活(擦写存储APP代码的Flash)搞完就跑去执行被擦除(刷新)过Flash程序(APP)。常见的方法就是在Bootloader擦除APP程序,擦除完再跳转到新的APP程序继续运行,实现不需要烧录线进行ISP下载就能升级。
1.2 Bootloader
源自linux上的BootLoader的概念,在linux上,BootLoader是首先执行的程序,BootLoader启动之后初始化CPU、RAM、Flash等设备,然后从Flash中读取Linux程序数据到RAM中去,最后跳转到RAM中Linux的起始地址中去启动Linux系统。除了从Flash中读取系统启动之外,BootLoader还能通过网络NFS协议从服务器上读取Linux并启动。BootLoader还能够更新Linux内核、配置Linux启动信息、测试系统等等。
在嵌入式操作系统中,BootLoader是在操作系统内核运行之前运行。可以初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统内核准备好正确的环境。在嵌入式系统中,通常并没有像Window自带BIOS那样的固件程序(注,有的嵌入式CPU也会内嵌一段短小的启动程序),因此整个系统的加载启动任务就完全由BootLoader来完成。
简单理解的话,Bootloader就是我们常见的计算机开机操作,而我们则是想要打开自己想用的软件Keil(APP)。我们没办法秒开机直接运行Keil(APP),需要等待一段时间;计算机需要做底层软、硬件的配置(Bootloader)。
我们要做的STM32的BootLoader也是类似的工作原理,但是没有Linux系统的BootLoader功能那么强大。我们要做的STM32的BootLoader只有两个主要目的:
- 跳转到应用程序并执行;
- 更新应用程序(App);
Ps: 因此,下载新(App)程序后并不擦除bootLoader程序,下次启动依然先运行BootLoader程序,可以选择性更新或者不更新程序,所以STM32的BootLoader作用往往就是用来管理单片机程序的更新。
1.3 Bootloader & App
其实IAP升级,就需要将原有APP,分割成Bootloader和新APP。Bootloader负责检查更新APP,新APP则是继续执行原有APP功能;但是在MCU的Flash上,会产生地址分块(Bootloader+App),新APP则是需要重映射中断向量表
Bootloader里面主要是
- 设置规划bootloader和app的空间
- 接收编译好的app的bin文件,写入flash
- 实现跳转至APP
App里面主要修改的地方是
- ROM起始地址和分配的空间大小
- 中断向量表 重定向
- 生成bin文件
Ps: 由于Bootloader可以对接收bin文件方法有多种多样,因此常见的升级方式为:
- 串口升级(私有协议或者X-Modem、Y-Modem)
- USB升级(DFU)
- U盘升级(OTG)
- 网络升级
- 无线升级(OTA,例如蓝牙)
二、IAP升级的预备知识
2.1 复位序列
M3单片机复位后,从0x00000000取栈指针(SP), 从0x00000004取复位向量(PC),有了栈指针和复位向量后,单片机就按照正常流程运行了;在BootLoader里面,我们更新完程序后需要做的步骤之一就是设置栈指针,跳转复位向量。
2.1.1 栈指针
CPU 按照 MSP 指针,到ROM存取地址或数据。
2.1.2 pc指针
CPU 按照 PC 指针,到ROM去取指令代码。PC,是 program calculate 的缩写,即程序计数器;
Ps: 当前PC在ROM的位置就是程序执行到的位置;在涉及到操作系统(ucOS)原理的时候,PC指针就扮演着十分重要的角色。
有了栈指针和复位向量后,单片机就能够运行了!
2.2 重定位中断向量表
2.2.1 中断向量表里面到底是什么,它放在哪里?到底有什么用?
- 中断向量表实际上就是存放在 code区 0地址开始的一个数组,数组的成员为4个字节,而且这些数组在启动文件的时候已经初始化好,既然初始化好,那里面存放的是什么?
- STM32根据内核和外设中断优先级,统一标号,标号越小,优先级越大。然后把内核和外设的中断服务函数的地址放到这个数组里面,数组的下标跟中断的优先级对应,我们也把这个中断的编号叫做中断向量。
- 在启动文件执行的时候,内核和每个外设的中断服务函数的地址都是已经确定好的,地址就存放在中断向量表中,而且在启动文件里面已经写好了中断服务函数,只是这些中断服务函数为空,而且带[weak]弱定义,那么我们就需要在C文件里面重新实现这个中断服务函数,用户写这个中断服务函数的时候,函数名必须跟启动文件里面写的中断函数名对应,因为函数名对应的就是中断服务函数的地址,如果名字搞错了,那么在响应中断的时候,就默认响应启动文件里面预先写好[weak]、空的中断服务函数,而且是一个死循环。
2.2.2 内核是如何响应中断的呢?
当中断来临的时候,首先取向量,每个中断的中断向量不一样,然后根据向量查询中断向量表,根据表里面的地址找到中断服务函数,从而实现整个中断的响应过程。
2.2.3 理解了中断向量表后
- 那么你在C文件里面写中断服务函数的时候就知道为什么要这样写中断服务函数的名字,而且你也可以修改启动文件里面的中断向量表里面的地址(即修改函数名字即可)。
- 在后面移植ucosiii等os的时候,也知道PendSV要怎么移植和修改
BootLoader是一个完整的程序,更新用的App也是一个完整的程序。一个完整嵌入式程序都包含中断向量表,用于响应中断;两者当然无法共用一套中断处理(用法可能不同),因此需要两个中断向量表,第一个中断向量表可以使用默认,而另外一个中断向量表则需要重定位(映射)。
2.3 ROM的起始地址
STM32的Flash在MDK里被设置为起始地址 0x08000000 ,也就是说如果上面的中断向量表要重定义向到Flash上,是以基地址 0x08000000 计算偏移的(也可以重定向到RAM);程序所有函数的地址默认都在以0x08000000为基地址的一段ROM里面了。
2.3.1 程序起始地址0x08000000
STM32的Flash在MDK里被设置为起始地址0x0800 0000,而CM3手册规定芯片复位时要从0x0000 0000地址开始取出中断向量,那STM32怎么样执行代码呢?是地址重映射?或者在0x0000 0000里有对应有实际存储器?
仔细阅读手册,STM32设计的Flash起始地址是在0x0800 0000位置开始的;全部代码都只能从这里开始存储,故要重映射。详见STM32 referenc manual手册第54页。
那既然从这里才能存储代码,就必须在MDK里设置Flash地址为0x0800 0000,下面是MDK设置页面,这个应该都看到过。
这样就产生一个问题,CM3中规定上电后CPU是从0地址开始执行,STM32设计的Flash起始地址是在0x0800 0000位置开始的,因此中断向量表烧写在0x0800 0000地址里,那启动时不就找不到中断向量表了?
既然CM3定下的规矩是从0地址启动,SMT32当然不能破坏ARM定下的“规矩”,所以它做了一个启动映射的过程,就是和芯片上总能见到的BOOT0和BOOT1有关了。
- 当选择从主Flash启动模式后,芯片一上电,Flash的0x0800 0000地址被映射到0地址处,不影响CM3内核的读取
- 所以这时的CM3既可以在0地址处访问中断向量表,也可以在0x0800 0000地址处访问中断向量表,而代码还是在0x0800 0000地址处存储的。
- 这就是最难理解的地方,其实,这是基本上所有ARM芯片采用的启动映射方法。ARM7,ARM9没有内部Flash的通常都是这样做的。这个过程出自STM32 referenc manual手册,里面是有说明的。
2.4 hex文件和bin文件
Bootloader程序升级,往往是采用写入bin文件;那hex文件和bin文件有什么关系呢?
hex文件
- hex文件是以ASCII文本形式保存编译后的二进制文件信息。Hex文件使用ASCII文本的形式保存Bin文件的内容和Bin文件的一些配置信息。hex文件可以由下载器(比如jlink)烧写到MCU的ROM中。
- 平时用J-LINK或者串口ISP下载程序,都是下载hex文件的;因为hex文件包含地址信息,下载程序的时候知道程序下载到ROM的哪个区域。反过来讲,hex文件是不能直接写进ROM的,一边写需要一边转换(解码出地址信息,将对应内容写入ROM)。
bin文件
- Bin文件是MCU固件烧写的最终形式,也就是说MCU的ROM中烧写的内容完全就是Bin文件的内容。
hex & bin 区别
- Hex文件有更好的可读性,最重要的是hex文件能够保证固件在保存与传输时的完整性。因此hex文件更适用于保存与传输。
- Bin文件是纯二进制文件,内部只包含程序编译后的机器码和变量数据。当文件损坏时,我们也无法知道文件已损坏。不过Bin文件作为固件的最终形式,在使用串口下载程序或者远程升级时,是不可替代的。
bin文件生成
默认情况下编译后生成的是hex文件,没有生成bin文件。keil的Bin文件生成方式有很多种,可以另外下载一个hex2bin工具,然后用Keil脚本执行;这里,介绍使用Keil自带的工具fromelf.exe。在Keil的安装目录下,例如:E:\Keil\ARM\ARMCC\bin\fromelf.exe
第一种方式:设置绝对路径(不建议这样做,别人用你工程需要再次修改路径)
1
D:\Program Files\MDK516\ARM\ARMCC\bin\fromelf.exe" --bin -o ./obj/test_app.bin ./obj/test_app.axf
第二种方式:相对路径,直接复制下面的路径就能直接使用
1
$K\ARM\ARMCC\bin\fromelf.exe --bin --output=@L.bin !L
bin文件生成在xxx.uvprojx
的当前目录下,在xxx.uvprojx
当前目录下你可看到一个test1.bin
(名字是根据你的hex文件名字一样)。
希望生成.bin文件输出在当前工程下的指定目录,比如Bin文件夹,可如下操作:
1 | $K\ARM\ARMCC\bin\fromelf.exe --bin --output=Bin\@L.bin !L |
生成的文件也是在xxx.uvprojx的当前目录下,在xxx.uvprojx当前目录下,可看到一个新生成的Bin文件夹,里面是test1.bin。
三、IAP升级的具体实现
这次的例子采用的是串口Y-modem协议进行IAP升级。
3.1 Bootloader程序的编写
程序编写主要几件事:
- 编写串口Y-modem(或者X-modem)协议,接收bin文件
- 把串口接收的bin文件缓存块,写入Stm32的flash指定地址
- 通过工程的 .map文件,大致规划好Bootloader和APP的Flash储存块地址(这肯定不能重叠了)
- Bootloader执行程序能跳转到APP去
- 编译完成后,注意查看 .map文件,在编译器配置限制一下Bootloader程序的大小。
对X-modem协议进一步了解的,可以看这篇博文: Xmodem协议
对Stm32的flash如何写数据,可以看这篇博文: stm32内部flash基础知识
放出官方源码:ST官方的IAP + Ymodem代码;提取码为:aan9
3.2 Bootloader 跳转代码的理解
跳转代码如下,后面逐条分析:
1 | typedef void (*pFunction)(void); |
1 | if (((*(__IO uint32_t*)Appxaddr) & 0x2FFE0000 ) == 0x20000000) |
- 在程序里
#define Appxaddr 0x8003000
*(__IO uint32_t*)Appxaddr)
,即取0x8003000开始到0x8003003 的4个字节的值- 因为我们的应用程序APP中设置把 中断向量表 放置在0x08003000 开始的位置;而中断向量表里第一个放的就是栈顶地址的值
也就是说,这句话即通过判断栈顶地址值是否正确(是否在0x2000 0000 - 0x 2000 2000之内)来判断是否应用程序已经下载了,因为应用程序的启动文件刚开始就去初始化化栈空间,如果栈顶值对了,说应用程已经下载了,启动文件的初始化也执行了。
1 | JumpApp = (pFunction)*(__IO uint32_t*)(Appxaddr + 4); |
ApplicationAddress + 4
即为0x0800 3004 ,里面放的是运行必不可少的第二项“复位地址”。此处强制转换,将地址值转换成指向地址。
void (*pFunction)(void);
是声明一个函数指针。将复位地址作为函数指针,当其执行该函数指针时,就是执行复位函数。
1 | __set_MSP(*(__IO uint32_t*) ApplicationAddress); //设置主函数栈指针 |
顾名思义,从上面可知内容,就是取 ApplicationAddress 开始到 ApplicationAddress+3 的4个字节的值,设置为栈顶地址(1个字,大小:4字节)。
总结: 因此Bootloader跳转到App,最核心的点就只有两个(保证程序运行):
- 1)设置新复位向量(地址),并跳转执行;
- 2)设置新栈顶地址,并将主函数栈指针指向该地址。
3.3 APP 重映射中断向量表
中断向量表是可以在程序中多次被映射的(可能你有多个APP程序)。在Cortex-M3系列芯片中,控制它的就是CM3已经规定的 NVIC寄存器 SCB->VTOR
。在STM32库中给出的启动代码里,startup_stm32f10x_hd.s文件里,第146行,是上电后读取中断向量表中的复位中断位置,并执行复位中断处理代码,代码如下:
1 | ; Reset handler |
注意复位后第一个被执行的是SystemInit代码,这个代码在库目录下的 system_stm32f10x.c 文件里,它初始化了时钟,NVIC等一系列操作;这里摘要与中断向量有关的代码:
1 | void SystemInit (void) |
可以看出中断向量重映射是一个选择性编译,通常宏定义 VECT_TAB_SRAM
都没有被定义,所以这里执行结束后, SCB->VTOR
就是 FLASH_BASE 了,值为 0x08000000 。以后CM3再取中断向量里,就会根据 SCB->VTOR
的设置,从这里取向量执行了。中断向量自此开始偏移。
Ps: 这时连__main
函数都还没进,中断向量的重映射位置还是够早的。
当然,有些其他系列ST芯片都没有SCB->VTOR
,例如M0系列;这里有专门一篇文章讲解M0如何编写App的中断向量表重映射:M0的中断向量表重映射。
四、多种升级方式的源码
基于Stm32F407的SD卡升级
基于Stm32F407的U盘Host,USB_FS升级
基于Stm32F407的U盘Host,USB_HS复用为USB_FS升级
基于Stm32F103的485串口Ymodem协议升级