1. 概念混淆
1.1 IAP
IAP是In Application Programming的简写,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。
IAP主要包括BootLoader和应用程序两部分
而进行IAP升级最核心的两点是实现:
1.BootLoader程序的编写 存储在FLASH中,主要负责引导APP程序的升级
2.APP程序的编写 实现产品功能升级的程序或者对程序的修复
1.2 OTA
OTA是Over-the-Air的简写,即空中下载技术,通过网络远程给用户进行系统更新和升级
基于IAP的OTA设计思路是先通过OTA接收升级固件,再利用IAP进行固件搬移、校验和程序跳转,实现空中升级的目的。
1.3 ICP(In Circuit Programing)
使用硬件对应厂家的软件以及仿真器都可以烧录程序,目前主流的有JTAG和SWD接口。而ICP编程就是以SWD接口进行的。
执行ICP功能,仅需要三个引脚:RESET、ICPDA、和ICPCK。RESET用于进入或退出ICP模式,ICPDA为数据输入输出引脚,ICPCK为编程时钟输入引脚。用户需要在系统板上预留VDD、GND和这三个引脚。
大致的工作流程:PC上运行的软件(ICP编程工具)通过SWD的接口更新芯片内部APROM、LDROM、数据闪存(DataFlash)和目标用户配置字(Config)
1.4 ISP(In System Programing):
在系统编程,可借助MCU厂商预置的Bootloader 实现通过板载UART或USB接口烧录代码,比如STM32存储映射Code分区中的System memory可以预置厂商的Bootloader,让MCU支持通过UART下载(不限于UART,具体由厂商预置Bootloader实现而定);
写入器将code烧入,不过,芯片可以在目标板上,不用取出来,在设计目标板的时候就将接口设计在上面,所以叫"在系统编程",即不用脱离系统;
STM32 在出厂时由ST 在这个存储区间内部预置了一段BootLoader(也即ISP 程序),这段程序出厂后无法修改。厂家提供的BootLoader 一般支持UART 协议,可以让我们直接通过串口将程序代码烧录到Main Flash memory 中;
各自的烧录工具
1.ICP使用SWD接口进行烧录程序。常用的烧录工具为J-Link、ST-Link、Nu-Link。与之配套的烧录软件为J-Flash、NuMicro_ICP_Programming_Tool、st-link utility。
2.ISP是使用引导程序通过USB/UART等接口进行烧录的,首先就是需要有bootloader。最常见的烧录方式就是学习8051单片机时使用的STC-ISP烧录工具了。
3.IAP就是通过软件实现在线电擦除和编程的方法,没有使用任何工具,仅仅是通过软件的方法来更新Flash中的数据
1.5 SWD(Serial Wire Debug)
是一种两线调试接口,由 ARM 公司提出,用于替代 JTAG 接口,提高调试效率和降低成本。
SWD 接口只需要两根信号线,分别是:
SWCLK(Serial Wire Clock):串行时钟线,提供同步时钟信号;
SWDIO(Serial Wire Data Input/Output):串行数据输入输出线,用于双向数据传输。
需要注意的是,玩单片机的还会经常见到 STLink、JLink、DAPLink、CMSIS DAP 等名词,这些属于是仿真器的类型,而不是调试接口。
3. Bootloader
BootLoader就是一段单片机的引导加载程序,单片机出厂时就已经写好了,用户是无法访问更改的,但是我们可以在用户代码区域再自己写一个BootLoader程序用于更新固件。
BootLoader程序根据判断是否需要升级固件,并进行固件搬运,运行代码跳转。
APP工程中需要编写OTA数据包交互协议,将接收到的升级固件保存至FLASH,
校验一致性
然后设置升级标志位并重启
4. STM32启动模式分析
Cortex-M3 内核启动有3种情况:
- 通过boot引脚设置可以将中断向量表定位于SRAM区,即起始地址为0x2000000,同时复位后 PC 指针位于0x2000000处。
- 通过boot引脚设置可以将中断向量表定位于FLASH区,即起始地址为0x8000000,同时复位后PC指针位于0x8000000处。
- 通过boot引脚设置可以将中断向量表定位于内置Bootloader区
Cortex-M3内核规定,起始地址必须存放栈顶指针,
而第二个地址则必须存放复位中断入口向量地址,
在Cortex-M3内核复位后,
会自动从起始地址的下一个32位空间取出复位中断入口向量,
跳转执行复位中断服务程序。
Boot引脚设置见下表
STM32的启动文件(startup_stm32f103xb.s)和启动过程。
(1)首先对栈和堆的大小进行定义,并在代码区的起始处建立中断向量表, 其第一个表项是栈顶地址(32位),第二个表项是复位中断服务入口地址; (2)然后执行复位中断,在复位中断服务程序中跳转 C/C++标准实时库的main函数(__main), 完成用户堆栈等的初始化后,跳转.c 文件中的main函数(真正的用户main函数)开始执行程序。
假设STM32被设置为从内部FLASH启动,
中断向量表起始地位为0x8000000,
则栈顶地址存放于0x8000000处,
复位中断服务入口地址存放于0x8000004处。
当STM32遇到复位信号后,
则从0x80000004处取出复位中断服务入口地址,
继而执行复位中断服务程序,
然后跳转__main函数,
最后进入main函数。
5.OTA流程示例
Step1:上电启动单片机,首先执行BootLoader程序。
Step2:BootLoader读取Parameter区参数,查看升级标志是否有升级任务,若没有升级任务,则进入 Step3,否则进入 Step4。
Step3: BootLoader程序跳转至APPA,执行用户代码,并判断是否收到升级任务请求,若有升级任务请求则进入 Step7。
Step4:根据Parameter区升级参数,校验APPB固件数据并将APPB升级固件拷贝到APPA区,执行 Step5 和 Step6。
Step5:将升级标志位清除。
Step6:重启,再次执行BootLoader。
Step7:将接受到的升级数据包存入APPB区,并将相关升级参数存入Parameter区,若接收数据完成,固件检验通过则写入Parameter区升级标志,并进入 Step8 。
Step8:重启,再次执行BootLoader
6. 工程设置示例:
Flash:
Bootloader :0x08000000-0x08004FFF = 20KB
APP1: 0x08005000-0x08017FFF = 50KB
APP2: 0x08018000-0x0801DFFF = 50KB
- 由于BootLoader需要在上电复位后首先执行,因此,设置BootLoader存放在FLASH起始位置0x08000000,
- 设置IROM1 Start = 0x08000000,Size = 0x5000(0x5000 = 20kB)
- 在Flash Download中设置下载Flash位置,参数与IROM1中设置Start和Size相同
- APP代码存放在紧挨着BootLoader之后,因此APP的起始位置应该为0x08000000+0x5000 = 0x08005000,占用内存大小为0xC800(50kB)
- 设置IROM1 Start = 0x08005000,Size = 0xC800,在Flash Download中设置下载Flash位置,参数与IROM1中设置的Start和Size相同,
- 由于APP代码运行的起始位置不在0x08000000,其栈顶地址发生偏移,对应的中断向量表地址也整体发生偏移
- 具体的在system_stm32f1xxx.c中启动代码中可以看到定义的中断向量表偏移量宏定义
中断向量表设置偏移
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif
/* #define VECT_TAB_SRAM */
#define VECT_TAB_OFFSET 0x5000 /*!< Vector Table base offset field.
This value must be a multiple of 0x200. */
/*该指数必须为0x200的整数倍*/
KEIL配置编译后运行fromelf.exe生成.bin文件
fromelf.exe --bin -o “$L@L.bin” “#L”
7. 备注事项
-
如果bootloader里头有中断关闭它
__disable_irq();建议先关闭中断在跳转,
关闭之后千万要记得在APP函数中打开,
不然可能会导致APP函数运行不下去的情况 -
判断栈顶地址
-
代码示例:
- 设置中断向量表的偏移量
SCB->VTOR == ((volatile uint32_t) 0xE000ED08) - APP中要打开irq
void execute_firmware(uint32_t addr)
{
USART1_DEBUG("execute_firmware\r\n");
__disable_irq();
if(((*(uint32_t*)addr)&0x2FFE0000)==0x20000000)
{
JumpAddress = *(uint32_t*) (addr);
__set_MSP(JumpAddress);//把堆栈指针复位了,然后再跳转到APP。APP开始运行,APP也占有全部
//RAM,它不用再关心IAP是否会占用堆栈或变量,因为IAP不会再运行了(当然可能有的RAM里面是还有数
//据存在的,比如IAP中的变量和堆栈数据等,但APP运行时会重新初始化它自己的变量,不用关心之前的
//数据是什么,同时堆栈的时候也会先入栈再出栈,先前的数据会被覆盖)
}
}
( 这边描述一下为什么if((((uint32_t)addr)&0x2FFE0000)==0x20000000)需要这一条?
答:这是为了保证app的数据执行是在0x20000000开始的,所以这句话的意思是保证app程序执行ram是有效的!
为什么是0x2FFE0000?
这是针对64K RAM的,保证ram在0x10000以内!
这个条件语句检查addr指向的地址是否位于一个以0x20000000为起始地址的内存区域。对于STM32,0x20000000通常是内部SRAM的基地址,因此这个条件语句常用于确认一个地址是否位于内部RAM内,这对于一些操作如堆栈指针(SP)的检查特别有用,以确保堆栈在正确的内存范围内
if(((*(volatile uint32_t *)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法
0x2FFE0000在二进制下是 0010 1111 1111 1110 0000 0000 0000 0000
0x20000000在二进制下是 0010 0000 0000 0000 0000 0000 0000 0000
要最后的值为0x2000000
那这个值要小于FFFF
0X0000FFFF
&
0X2FFE0000
结果符合
64K的大小16进制是0x10000
栈顶是
0x20010000
&
0x2FFE0000
结果
0x20000000
STM32微控制器通常具有多个内存区域,包括片上Flash、SRAM、外设寄存器区等。对于64KB的SRAM,其地址范围通常是从0x20000000开始,直到0x2001FFFF结束。64KB的大小意味着地址空间为0x10000(即64 * 1024字节)。
判断栈顶在SRAM范围内
- “__set_MSP”函数原型为:
__STATIC_INLINE void __set_MSP(uint32_t topOfMainStack);
该函数是ARM定义好的内核功能函数之一:
设置主栈指针,该函数以存在于头文件(cmsis_armcc.h)的形式提供给各芯片厂商、IDE等使用(不是哪个芯片厂商或IDE特有的)。
简单说,一般需要包含形如“CMSIS\Include”的路径
(针对多IDE环境可能导致包含交叉而找不到定义)。可以搜索相关路径(在mdk-arm中)找到cmsis_armcc.h所在的路径,然后明显的包含该路径即可
-
跳转APP运行需要三个步骤
关闭外设和中断
设置APP堆栈顶地址, 重新初始化堆栈
运行复位中断服务函数 -
APP
app程序在main函数开始需要设置中断向量表偏移,然后正常运行
8. 复位
- 系统复位
直接调用 CMSIS 提供的接口NVIC_SystemReset()即可,该操作会把内核及所有外设重置
void reboot(void)
{
__set_FAULTMASK(1); // 关闭所有中断
NVIC_SystemReset(); // 系统复位,以上两个函数位于 core_cm3.h 文件或 core_cm4.h
}
- Bootloader跳转至APP代码片段
主要工作流程为芯片上电->
执行bootloader代码->
初始化时钟和配置必要外设->
检测是否需要进行固件更新,
是-则将新固件从存储区拷贝替换至应用程序运行区,然后跳转至应用程序入口;
否-则直接跳转至应用程序入口开始执行业务
- 内部Flash和片外Flash的擦除单位可能不一致,2K、4K字节不等
- 写内部Flash要以4的倍数字节写进,外部不限制
- 写Flash须考虑4字节对齐
- 内部Flash编程以一个字为单位,如果非4字节对齐,会导致硬件错误
- 跳转复位需要关中断、清标志位
- 另外在外设初始化时,需要deinit或者disable一下,否则可能会导致ADC、DMA配置失败
- 看门狗的喂狗问题,即使在ymodem传输过程中,也要保证喂狗避免芯片复位
常规原因排查:
- 跳转前未关中断;
- 中断控制器没复位清零;
- 中断向量表偏移未设置正确;
- 部分会影响到程序运行的外设未关闭,如DMA会刷新SRAM,可能会影响到bootloader程序的运行,导致crash
- 系统时钟配置不一致,bootloader与APP的系统时钟初始化应该保持一致
- 可能的原因:bootloader与APP的编译链接选项尽可能保持一致,如硬件浮点、函数/数据编译分小节、所用C库、优化选项等
操作系统在线程中使用PSP堆栈指针,在中断中使用MSP主堆栈指针
设置CONTROL寄存器的bit[1]选择使用哪个堆栈指针。CONTROL[1]=0选择主堆栈指针;CONTROL[1]=1选择进程堆栈指针
直接PC跳转复位,实际上内核并不会自动切换MSP和PSP,因为此过程没有触发内核Handler模式,需要用户手动切换
9. 代码整理:
9.1 Bootloader跳转函数
//设置栈顶地址
//addr:栈顶地址
__asm void MSR_MSP(uint32_t addr) {
MSR MSP, r0 //set Main Stack value
BX r14
}iapfun jump2app;
#include "sys.h"
typedef void (*iapfun)(void); //定义一个函数类型的参数.
#define FLASH_APP1_ADDR 0x08010000
void iap_load_app(u32 appxaddr); //跳转到 APP 程序执行
iapfun jump_app;
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(volatile uint32_t *)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
//栈顶地址存放于0x8000000处,
//复位中断服务入口地址存放于0x8000004处
//用户代码区第二个字为程序开始地址(复位地址)
jump2app=(iapfun)*(volatile uint32_t *)(appxaddr+4);
//MSR_MSP 函数(该函数在 sys.c 文件)设置栈顶地址
//初始化 APP 堆栈指针(用户代码区的第一个字用于存放栈顶地址)
MSR_MSP(*(volatile uint32_t *)appxaddr);
__disable_irq();
//跳转到 APP.
jump_app();
}
}
__set_MSP(STACK_ADDR)的作用为重新初始化堆栈
__set_MSP(JumpAddress);//把堆栈指针复位了,然后再跳转到APP。APP开始运行,APP也占有全部 //RAM,它不用再关心IAP是否会占用堆栈或变量,因为IAP不会再运行了(当然可能有的RAM里面是还有数
//据存在的,比如IAP中的变量和堆栈数据等,但APP运行时会重新初始化它自己的变量,不用关心之前的
//数据是什么,同时堆栈的时候也会先入栈再出栈,先前的数据会被覆盖)
9.2 APP中设置
对于 FLASH APP,
我们设置为 FLASH_BASE+偏移量 0x10000,
所以我们可以在 SystemInit 函数里面修改 SCB->VTOR 的值
当然为了尽可能不修改系统级别文件,
我们可以也可以在 FLASH APP 的 main 函数最开头处添加如下代码
实现中断向量表的起始地址的重设
SCB->VTOR = FLASH_BASE | 0x10000;
中断向量表设置偏移
app是主程序,启动后第一步就是设置中断向量表偏移。
这是为了打开跳转前关闭的中断,并且重定位向量表,否则APP程序里的与中断相关的程序会炸
BOOT1
void Reset_test(void)
{
typedef void (*iapfun)(void);
uint32_t JUMP_ADDR = 0x08004000;
uint32_t STACK_ADDR = 0x20000000;
uint32_t RESET_IRQ_ADDR = JUMP_ADDR + 4;
iapfun jump2app;
jump2app = (iapfun)*(volatile uint32_t *)RESET_IRQ_ADDR;
__set_MSP(STACK_ADDR);
__disable_irq();
jump2app();
}
APP
int main(void)
{
__enable_irq();
SCB->VTOR = 0x8004000;
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
int count = 0;
while (1)
{
HAL_Delay(1000);
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_7);
printf("This is APP\n");
if(count >= 5)
HAL_NVIC_SystemReset();
count++;
}
BOOT2
#define APP_START_ADDR 0x08020000
int main(void)
{
uint32_t app_addr=0;
start_app_fxn app_start;
//init
//do something in boot
__disable_irq();
app_addr = *(__IO uint32_t*) (APP_START_ADDR + 4);
app_start = (start_app_fxn) app_addr;
__set_MSP( *(__IO uint32_t*) (APP_START_ADDR));
app_start();
}
/*!
* @brief 跳转到应用程序段
* 执行条件:无
* @param[in1] : 用户代码起始地址.
*
* @retval: 无
*/
void jump_to_app(uint32_t app_addr)
{
pFunction jump_to_application;
uint32_t jump_address;
/* Check if valid stack address (RAM address) then jump to user application */
if (((*(__IO uint32_t*)app_addr) & 0x2FFE0000 ) == 0x20000000)
{
/* Jump to user application */
jump_address = *(__IO uint32_t*) (app_addr + 4);
jump_to_application = (pFunction) jump_address;
/* Initialize user application's Stack Pointer */
__set_MSP(*(__IO uint32_t*) jump_address);
jump_to_application();
}
}
typedef void (*iapfun)(void);
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(uint32_t appxaddr)
{
uint32_t STACK_ADDR = 0x20000000;//栈初始地址
iapfun jump_app = NULL;
LOG_OUT("appxaddr p =%x\r\n", appxaddr);
LOG_OUT("appxaddr data=%x\r\n", *(volatile uint32_t *)appxaddr);
LOG_OUT("IRQ addr=%x\r\n", *(volatile uint32_t *)(appxaddr+4));
if(((*(volatile uint32_t *)appxaddr)&0x2FFF0000)==0x20000000) //检查栈顶地址是否合法
{
jump_app=(iapfun)*(volatile uint32_t *)(appxaddr+4);//复位中断的地址
LOG_OUT("jump_app=%x\r\n", jump_app);
HAL_Delay(1000);
__disable_irq();
__set_MSP(*(volatile uint32_t *)appxaddr);//重新初始化堆栈
jump_app();
}
else
{
LOG_OUT("jump_err\r\n");
}
}
__set_MSP 是一个内联汇编函数,用于设置ARM Cortex-M微控制器的主栈指针(Main Stack Pointer,MSP)。在ARM Cortex-M系列处理器中,有两个栈指针:MSP和PSP(Process Stack Pointer)。MSP通常在系统启动时使用,以及在异常处理和中断服务例程中作为栈指针。PSP则在正常任务运行时使用。
__set_MSP 函数通常在Cortex-M微控制器的启动代码中被调用,以初始化MSP到一个预设的地址,通常是SRAM的高端地址,以便在复位或异常发生时,处理器能够正确地保存和恢复寄存器上下文。
下
__STATIC_INLINE void __set_MSP(uint32_t addr);
其中 uint32_t addr 参数是你要设置为主栈指针的地址。这个地址通常指向SRAM的一个特定位置,该位置将作为栈的顶部,所有后续的栈操作都会从这个地址向下扩展
__set_MSP(*(uint32_t *) ApplicationAddress);
这里的 ApplicationAddress 是一个指向存储区的指针,该存储区包含了初始化MSP所需的地址值。通常,这个地址值会在链接器脚本中被定义,以确保MSP被设置在一个安全且合适的位置。
在使用 __set_MSP 设置完MSP后,通常会有一个跳转指令,如 Jump_To_Application(),将控制权交给应用程序的主要入口点。
一篇
开始做BOOTloader