STM32 OTA学习:Bootloader开发实践【超子物联网分享】

STM32 OTA学习 Bootloader 【超子说物联网】

一个学习贴,学习超子说物联网的“GD32/STM32单片机,OTA网络远程升级,手把手编写BootLoader程序教程”视频,记录下一些学习内容。

超子哥的视频真心不错,可惜我发现的有些晚,跟着超哥这个Bootloader视频手把手一路写过来收获很多,前面的学习没有记录,看后面有没有机会补上吧

超哥视频教学用的是GD32F103C8T6,本人用的STM32F103C8T6,从零写起基本没有碰到大问题,感谢超哥的教学视频,如果本帖有侵权还请联系我删除!

本章的任务是读取AT24C02中存储的OTA更新标志位,并根据标志位内容执行APP区的跳转,实现以下3点:

  • 读取OTA更新标志位
  • 实现标志位判断函数
  • 跳转到APP区代码
  • 一、读取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区代码”所要做的事:

  • 修改主堆栈指针MSP
  • 运行Reset_Handler函数,也就是修改指向当前的程序地址的R15(程序计数寄存器)PC
  • 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:个人水平有限,如果文中有错误还请指正!

    未完待续…

    作者:朝阳晴朗天

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32 OTA学习:Bootloader开发实践【超子物联网分享】

    发表回复