STM32 OTA学习:Bootloader开发实践【超子物联网分享】
STM32 OTA学习 Bootloader 【超子说物联网】
一个学习贴,学习超子说物联网的“GD32/STM32单片机,OTA网络远程升级,手把手编写BootLoader程序教程”视频,记录下一些学习内容。
超子哥的视频真心不错,可惜我发现的有些晚,跟着超哥这个Bootloader视频手把手一路写过来收获很多,前面的学习没有记录,看后面有没有机会补上吧
超哥视频教学用的是GD32F103C8T6,本人用的STM32F103C8T6,从零写起基本没有碰到大问题,感谢超哥的教学视频,如果本帖有侵权还请联系我删除!
本章的任务是读取AT24C02中存储的OTA更新标志位,并根据标志位内容执行APP区的跳转,实现以下3点:
一、读取OTA更新标志位
为了便于标志位索引和日后对代码的拓展,我们选择将标志位放到一个结构体内进行管理:
typedef struct{
u32 OTA_flag;
}OTA_InfoCB;
#define OTA_INFOCB_SIZE (sizeof(OTA_InfoCB))
定义好结构体类型后我们就可以在main.c中声明一个全局变量,当然,为了能在别的C文件中也能调用到它,别忘了在头文件中进行一个外部变量声明
/* main.c */
OTA_InfoCB OTA_Info;
/* main.h */
extern OTA_InfoCB OTA_Info;
那么在读标志位的函数中我们这样实现
void AT24C02_ReadOTAInfo(void)
{
memset(&OTA_Info ,0 , OTA_INFOCB_SIZE);
AT24C02_ReadData(0, (u8 *)&OTA_Info, OTA_INFOCB_SIZE);
}
首先用memset函数对OTA_Info结构体这段内存区域进行赋初值0,OTA_INFOCB_SIZE宏定义表示OTA_InfoCB结构体的大小,采用宏定义便于代码拓展。
我们规定将AT24C02的前4个字节用作OTA更新标志位的存储,所以这里我们使用AT24C02_ReadData函数从0地址起,读我们所需的大小
二、实现标志位判断函数
先贴上代码
void BootLoader_Branch(void)
{
if(OTA_Info.OTA_flag == OTA_SET_FLAG){
u1_printf("OTA有更新!\r\n");
}else{
u1_printf("OTA无更新,跳转A分区代码\r\n");
}
}
OTA_SET_FLAG是在main.h中我们自己定义的标志位值,我们先定义为0xAABB 1122,同时我们打印出AT24C02前四个字节的值,上机测试一下
可以看到无更新分支可以正常进入,此时读到的OTA_flag值为0x0405 0607,因为之前在编写AT24C02代码进行读写测试时我曾经修改过其中的值。为了测试有更新分支是否正常,我们将OTA_SET_FLAG修改为0x0405 0607并重新烧录,可以看到,有更新分支也可以正常进入,本部分功能实现正常
三、跳转到APP区代码
1、分析
我们要实现Bootloader区代码在运行中跳转去运行APP区代码,实现的效果应该和上电复位后直接运行APP区代码效果一样,所以通用寄存器R0-R12,存储返回地址R14 等都不需要保护起来,我们只需要关心修改主堆栈指针MSP和指向当前的程序地址的R15(程序计数寄存器)PC。
在不做任何修改的情况下,默认Flash的起始地址从0x0800 0000处开始烧写程序,我们看一下一个普通的程序的启动文件startup_stm32f10x_md.s,其中建立了中断向量表,它的首地址(即__initial_sp)存放的一定是栈顶指针,接着偏移4字节,运行Reset_Handler函数,进行一些列初始化操作并最终运行到我们所编写的main函数中去(启动文件分析见本贴末尾链接)。从这里我们也能看出我们想要实现类似“上电复位后直接运行APP区代码”所要做的事:
2、实现
2.1、修改主堆栈指针MSP
__ASM void MSR_SP(u32 addr) //__ASM关键字表示以下为汇编指令,addr传入Flash地址,例如0x08000000
{
MSR MSP, R0 //将传入参数,也就是R0的值赋给主堆栈指针MSP
BX R14 //跳转到存储返回地址的R14寄存器,可以理解为退出本函数
}
keil中写这段代码会出现如下的红叉,但编译并没有问题,网上粗略搜了一下,只看到失能Edit–>Configuration–>Text Completion–>Dynamic Syntax Checking,关闭动态语法检查的掩耳盗铃做法🤣借用超哥的话,大家如果有什么好办法解决这个问题的话欢迎留言,让我们一起干掉这讨厌的红叉
2.2、运行Reset_Handler函数
/* boot.h */
typedef void (*pFunction)(void);
/* boot.c */
void LOAD_A(u32 addr)
{
pFunction Jump_To_Application;
Jump_To_Application = (pFunction)*(u32 *)(addr + 4);
Jump_To_Application();
}
中断向量表中0x8000 0004位置存储的是Reset_Handler函数的函数名,也就是函数指针,我们如果想要调用这个函数,首先需要获得到这个函数。
(pFunction)*(u32 *)(addr + 4):假设传入addr为0x0800 0000,那么(addr + 4)就偏移到0x0800 0004,;们要操作的是地址为0x0800 0004的空间,所以将其强转为(u32 *);这块空间存储的内容为函数指针,所以我们用*号来解引用;最后用(pFunction)将其强转为函数指针类型。最后像调用普通函数一样调用Jump_To_Application()就实现了Reset_Handler函数的调用。
2.3、合并并实验
/* boot.c
* 在LOAD_A中调用MSR_SP
*/
void LOAD_A(u32 addr)
{
pFunction Jump_To_Application;
MSR_SP(*(u32 *)addr);
Jump_To_Application = (pFunction)*(u32 *)(addr + 4);
Jump_To_Application();
}
/* 在无更新分支调用在LOAD_A中调用MSR_SP */
void BootLoader_Branch(void)
{
if(OTA_Info.OTA_flag == OTA_SET_FLAG){
u1_printf("OTA有更新!\r\n");
}else{
u1_printf("OTA无更新,跳转A分区代码\r\n");
LOAD_A(STM32_A_START_ADDR);
}
}
/* main.c
* 上面用到的STM32_A_START_ADDR之前定义在了main.h中,在此重新贴一下
*/
#define STM32_FLASH_STARTADDR (0x08000000) //STM32 Flash起始地址
#define STM32_PAGE_SIZE (1024) //一页(扇区)大小
#define STM32_PAGE_NUM (64) //总页数(扇区数)
#define STM32_B_PAGE_NUM (20) //bootloader区大小
#define STM32_A_PAGE_NUM (STM32_PAGE_NUM - STM32_B_PAGE_NUM) //程序块大小
#define STM32_A_START_PAGE STM32_B_PAGE_NUM //程序块起始页编号(扇区编号)
#define STM32_A_START_ADDR (STM32_FLASH_STARTADDR + STM32_B_PAGE_NUM * STM32_PAGE_SIZE) //程序块起始地址
至此已经能够实现APP区代码的跳转功能了,但我们的APP区是空的,并没有烧代码进去,下面我们来烧录个程序进去测试一下。
重新说明一下,我们所用的STM32F103C8T6是64kFlash,做的分区思路是前20k存放Bootloader代码,后20~64k存放APP代码,也就是从0x0800 5000开始,是我们的APP代码存储空间。我准备烧录的APP是串口回显的功能,给串口发什么它就打印什么。想要将代码烧录到0x0800 5000开始的区域,我们还需要进行一些修改:
①system_stm32f10x.c文件
如下图所示,我们需要修改整个向量表的偏移量,我们从20k处开始,20*1024=20480,16进制为0x5000。
②魔术棒–>Target
如下图所示,修改起始地址为0x0800 5000,后面的Size大小不用关心
③魔术棒–>Linker
如下图所示,修改链接基地址为0x0800 5000
现在准备工作就完成了,我们可以烧录Bootloader程序和APP程序,串口打印如下:
实验成功!
PS:
从ST官网下载的Bootloader——STM32F10x_AN2557_FW_V3.3.0部分代码如下图所示
可以看到,超哥的代码思想与官方Bootloader基本一致,73行还加入了APP首地址有效性的判断,超哥的代码中也存在,上面我没有给出因为我在学习时有点困扰,我担心前面就贴上影响大家思考,现在我再给出:
void LOAD_A(u32 addr)
{
pFunction Jump_To_Application;
if((*(u32 *)addr >= 0x20000000) && (*(u32 *)addr <= 0x20004fff)){
MSR_SP(*(u32 *)addr);
Jump_To_Application = (pFunction)*(u32 *)(addr + 4);
BootLoader_Clear();
Jump_To_Application();
}
}
中断向量表中的首地址(即__initial_sp)存放的一定是栈顶指针,堆和栈的属性都是 READWRITE 可读写,可读写段保存于 SRAM区,即地址0x2000 0000 地址后。STM32F103C8T6的SRAM大小为20k,所以可访问范围从0x0到0x4fff,加上基地址就是0x2000 0000到0x2000 4fff。
BootLoader_Clear函数中是一些DeInit的操作,就不展开了。
关于启动文件中的一些分析,参考下面的链接。
STM32单片机启动文件分析
ps:个人水平有限,如果文中有错误还请指正!
未完待续…
作者:朝阳晴朗天