问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

STM32 OTA学习 Bootloader

创作时间:
作者:
@小白创作中心

STM32 OTA学习 Bootloader

引用
CSDN
1.
https://blog.csdn.net/zzzYuGe/article/details/139500210

本文是关于STM32 OTA(Over-The-Air)升级中Bootloader部分的详细学习笔记。记录了作者跟随"超子说物联网"视频教程学习GD32/STM32单片机OTA网络远程升级的过程,重点介绍了如何读取AT24C02存储器中的OTA更新标志位、实现标志位判断函数以及如何跳转到APP区代码。

本章的任务是读取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
  1. 实现

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首地址有效性的判断,超哥的代码中也存在,上面我没有给出因为我在学习时有点困扰,我担心前面就贴上影响大家思考,现在我再给出:

中断向量表中的首地址(即__initial_sp)存放的一定是栈顶指针,堆和栈的属性都是 READWRITE 可读写,可读写段保存于 SRAM区,即地址0x2000 0000 地址后。STM32F103C8T6的SRAM大小为20k,所以可访问范围从0x0到0x4fff,加上基地址就是0x2000 0000到0x2000 4fff。

BootLoader_Clear函数中是一些DeInit的操作,就不展开了。

关于启动文件中的一些分析,参考下面的链接。

STM32单片机启动文件分析

ps:个人水平有限,如果文中有错误还请指正!

未完待续…

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号