DFU : Device Firmware Upgrade,一般是指通过USB/UART接口进行固件升级。
OTA : Over the air,一般是指通过无线进行空中固件升级。
目标是使用OTA完成升级,下文主要介绍STM32 IAP的相关知识
这里使用前者(优点是好理解,缺点是对FLASH擦除次数多)
STM32将整个FLASH分为4部分,分别存放不同程序
bootloader 引导程序
APP1 应用主程序
APP2 升级程序存放暂存 或者是备份区
信息区
先在flash的信息区设定一块小区域用于存放更新标志及更新网址,这里暂定为updateState
和updateUrl
其中updateState
有以下几种状态:
系统上电后,首先运行的是bootloader,检查FLASH中是否待升级标志updateState
是否为shouldUpdate
,如果有,执行以下操作
updateState
设为programError
前几步都好理解,只有最后一步运行新程序比较复杂,要搞懂bootloader如何跳转到APP,必须先理解中断向量表的概念。
中断向量表用于存放中断服务函数的入口地址,当中断发生时,程序从该地址找到各个中断函数的入口,从而进入执行。
中断向量表存放在STM32程序起始地址的后四字节处。STM32起始地址为0x0800 0000
存放堆栈栈顶地址,中断向量表存放地址为0x0800 0004
STM32 地址分布:
中断向量表分布:
上电后运行流程示意图:
图片来源:https://blog.csdn.net/u013184273/article/details/85305078?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-0&spm=1001.2101.3001.4242
如果BOOT引脚配置为从FLASH启动时,,系统上电后,从0x8000 0004
取出复位中断函数入口地址(中断向量表中第一个地址是复位中断向量),然后跳转复位中断服务函数执行,执行结束后在进入main函数,死循环在用户编写的大while1中。
在main函数运行时,有中断发生,STM32强制将PC 强制指向中断向量表,找到对应中断服务器函数,跳转执行,执行完毕后再返回到main函数中。
当使用flash中存放bootloader和app1时,需要设置两套堆栈和中断向量表,当从bootloader跳转到app中,app的main函数运行过程中发生中断,PC仍然会被强制指向中断向量表(bootloader的中端向量表),由于现在已经处于用户app,所以应该跳转到用户程序的中断向量表,因此运行到app中的第一步就是修改中断向量表的地址!或者准确来说是设置中断向量表偏移。
图片来源:https://blog.csdn.net/u013184273/article/details/85305078?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-0&spm=1001.2101.3001.4242
如上图,bootloader切换到app外,还需要修改栈顶地址。
用程序实现为
/* 采用汇编设置栈的值 */
__asm void SET_MSP (uint32_t ulAddr)
{
MSR MSP, r0 //设置Main Stack的值
BX r14 //子程序返回
}
/* 程序跳转函数 */
typedef void (*Jump_Fun)(void); //函数类型
void IAP_jumpApp (uint32_t App_Addr)
{
Jump_Fun JumpToApp; //函数指针
if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
{
JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4); //用户代码区第二个字为上文提到的中断向量表起始地址,起始地址中第一个为复位中断函数入口
SET_MSP( * ( __IO uint32_t * ) App_Addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
JumpToApp(); //执行app函数的复位中断函数,执行后跳转到APP的main函数,完成跳转
}
}
SET_MSP
函数解析:
**MSR MSP, r0 **
设置栈顶函数通过汇编实现,包裹在一个函数中。R0-R12 是STM32中 32 位通用寄存器,跳转到子函数执行时,R0存放函数入参,也就是ulAddr
。MSR是通用寄存器到状态寄存器的传送汇编指令,所以MSR MSP, r0
的作用就是把ulAddr
放入到MSP,设置栈顶指针为ulAddr
。
BX R14
STM32中, r14 是LR 即连接寄存器(Link Register, LR) ,在ARM体系结构中LR的特殊用途有两种:一是用来保存子程序返回地址;二是当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2), 当通过BL或BLX指令调用子程序时,硬件自动将子程序返回地址保存在R14寄存器中。在子程序返回时,把LR的值复制到程序计数器PC即可实现子程序返回。 如,可以使用MOV PC, LR或者BX LR来完成子程序返回。另外,也可以在在子程序入口处使用下面的指令将LR保存到栈中
bx r14
等同于bx lr
等同于mov pc,lr
,也就是将PC修改为lr的值,作用为程序从lr存放的地址执行,而lr存放的是子程序返回地址,也就是执行子程序返回。
IAP_jumpApp
函数解析:
参见注释,主要就是执行用户app程序中距离栈顶4字节的复位中断函数,从而跳转到真正的用户main函数执行。
注意: 在执行IAP_jumpApp
函数前,需要把bootloader
中所有开启定时器或者串口中断关掉,否则即使跳转到用户app
,外设中定时器仍然会运行,计时到达后会跳转到中断服务函数执行,而此时bootloader
中断服务函数的各个变量因为堆栈的改变已经生效,就会造成硬件异常。
app是主程序,启动后第一步就是设置中断向量表偏移。
int main(void)
{
/* USER CODE BEGIN 1 */
SCB->VTOR = FLASH_BASE | 0x4000;//设置中断偏移,偏移量为用户APP起始地址减去0x08000 0000,这里是16KB
/* USER CODE END 1 */
.......
}
另外需要设置编译生成的hex地址(hex中包含了写入的地址,bin不包含)
KEIL在编译程序时默认有程序生成占用的FLASH地址,一般bootloader
使用低地址,也就是默认的flash
分配,用户使用高地址,编译时修改所在位置。
默认bootloader设置:
0x8000 0000 是起始地址,0x80000=512KB是STM32F407ZE flash大小
app设置:
0x8004000是APP程序在FLASH的起始地址,对应SCB->VTOR
偏移量,0x4000是APP程序大小,这里为16KB
另外需要设置 下载擦除操作为只擦除部分程序。
编译后查看程序占用大小,bootlader也需要查看,不能超过设定区域。
编译完成后,打开生成的hex文件
上图是hex文件内容,前一部分和最后一个字节(有颜色字体)为地址信息,白色字体为有效数据,bin文件与hex的区别就在于bin没有这些信息。
注意红色部分,为上文提到的放在FLASH程序中的第0个和第1个字。 前4个字节是栈顶地址,后4个是中断向量表的起始地址(起始地址为复位中断服务函数入口地址)
由于stm32是小端模式,低字节在低位,高字节在高位,所以栈顶地址为:0x2000 0A58
也就是程序中一共用了0x2000 0A58
-0x2000 0A58
(RAM起始地址) = 2648个byte。
中断向量表起始地址为:0x0800 41D9
在APP程序中运行一段时间后将updateState
置为UpdateOk
,(证明程序可以正常运行)
单纯 用来备份app1程序,没有实际作用
bootloader 负责写固件到APP所在FLASH并跳转APP运行,难点在于跳转APP运行,跳转APP运行需要三个步骤:
app程序在main
函数开始需要设置中断向量表偏移,然后正常运行。