目录
需求
为了方便给远程 的客户设备进行傻瓜式更新,所以我们需要在程序中加入IAP技术。使用IAP技术能够使设备通过自身的通信接口(例如串口、USB、以太网等)进行固件更新,而无需连接专门的编程器 或者移除芯片 。满足了客户远程设备升级维护的需求。
一、IAP是什么?
IAP(In Application Programming)即在应用编程(离线升级 ), IAP 是用户自己的程序在运行过程中对User Flash 的部分区域进行烧写,目的 是为了在产品发布后可以方便地通过预留的通信接口对产品中的固件程序进行更新升级。
通常实现 IAP 功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信方式(如 USB、 USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作:
1.检查是否需要对第二部分代码进行更新;
2.如果不需要更新则转到 4;
3.执行更新操作;
4.跳转到第二部分代码执行;
理解 :
1.程序升级实现的逻辑是什么样的?
Bootloader 引导加载程序
(1).检测APP2区域有没有新代码的标志位 ,如果有,把APP2区域的代码拷贝到APP1区域,拷贝之前一定要保证已经擦除过,
(2).如果APP2区域没有代码,执行APP1区域的代码
(3).如果APP1区域也没有代码,执行bootloader
跳转的时候,一定要保证地址是否合法
App1 正常功能存放位置
(1).接收升级文件,将程序写入到APP2区域,接收完成置标志位
(2).开始运行之前 ,要修改中断向量表 的地址偏移
App2 升级程序存放位置
(1).在原有基础上,修改一些问题
(2).也要支持升级 也要修改中断向量表 的地址偏移
2.如何确定是否有升级文件 ?
(1)bootloader 确定是否有升级程序 -- 根据标志位
(2)联网升级(OTA),请求平台询问版本号,如果自己运行的不是最新版本,确认是否需要更新
3.有文件的标志位什么时候写?
APP1代码正常运行,获取到升级文件(串口中断接收),获取完成之后(100ms未接收到串口中断的数据),在APP2区域 的最后几个字节写入标志位
4.IAP和OTA的区别
原理是一样的,无非是升级文件的获取途径不同 。
IAP 的升级文件是从串口或者SD卡或者U盘获取的,OTA的升级文件是从网络获取的(4G,ESP8266)。
5.内部FLASH空间不够,是否可以写入外部FLASH中?
可以,看实际需求和习惯。
6.如果升级文件受损 ,程序宕机 如何处理?
1.升级文件在获取的过程中会加校验,校验通过之后才会写入到运行区域中。
2.如果真的宕机,可以本地下载或者在存储空间中备份一份代码,通过引导程序检测按键状态,运行出厂时提前备份的程序。
二、内部FLASH
本次讲解,是以内部FLASH更新为例。
1.空间划分
不同型号的 STM32,其 FLASH 的容量也不同,最小的只有 16K 字节,最大的则达到了1024K 字节。
本次例程使用的是STM32F103ZET6芯片,该芯片为大容量产品,内部FLASH 容量为 512K 字节。
空间划分为:12,250,250。(Bootloader所需空间很小)
2.读取
例:想要从地址addr,读取一个半字(半字为 16 位,全字为 32 位),通过如下的语句读取:
data=*(vu16*)addr;//0x0800 0100
将 addr 强制转换为 vu16 指针,然后取该指针所指向的地址的值,即得到了 addr 地址的值。类似的,将上面的 vu16 改为 vu8,即可读取指定地址的一个字节。
3.写入与擦除
STM32内部FLASH的编程是由FPEC(闪存编程和擦除控制器)模块控制的。无论是写入或擦除,都要通过该模块的寄存器进行操作。
FPEC模块包含 7 个32 位寄存器:
- FPEC 键寄存器(FLASH_KEYR)
- 选择字节键寄存器(FLASH_OPTKEYR)
- 闪存控制寄存器(FLASH_CR)
- 闪存状态寄存器(FLASH_SR)
- 闪存地址寄存器(FLASH_AR)
- 选择字节寄存器(FLASH_OBR)
- 写保护寄存器(FLASH_WRPR)
当STM32上电复位后, FPEC模块会被自动锁上进行保护,此时无法写入 FLASH_CR 寄存器。想要写入,就要使用到FPEC键寄存器3个键值中的(KEY1 和KEY2 )。这两个键的组合确保了只有授权的操作才能对Flash进行修改,从而保护Flash中的数据不被非授权访问或更改。这种机制提高了Flash存储操作的安全性。
RDPRT 键 (0x000000A5):
功能: RDPRT键用于解锁Flash读保护寄存器。它是用于解除Flash读保护的一个密钥。
使用: 在某些操作中(如解除读保护),需要写入这个键值到相关寄存器中,以解除对Flash区域的读保护。
KEY1 (0x45670123):
功能: KEY1是FPEC(Flash Program Erase Controller)的第一个解锁密钥,用于解锁Flash控制寄存器,以允许对Flash进行编程或擦除。
使用: 要进行Flash编程或擦除,必须首先写入KEY1到Flash控制寄存器。
KEY2 (0xCDEF89AB):
功能: KEY2是FPEC的第二个解锁密钥。它与KEY1一起使用,以完成Flash控制寄存器的解锁过程。
使用: 在写入KEY1之后,必须写入KEY2,以成功解锁Flash控制寄存器,允许后续的编程或擦除操作。
注意:STM32 闪存的编程每次必须写入 16 位,当 FLASH_CR 寄存器的 PG 位为' 1'时,在一个闪存地址写入一个半字将启动一次编程。写入任何非半字的数据, FPEC 都会产生总线错误。在编程过程中(BSY 位为' 1' ),任何读写闪存的操作都会使 CPU暂停,直到此次闪存编程结束。
写入流程 :
写入理解:
- 检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁。
- 检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的编程操作。
- 设置 FLASH_CR 寄存器的 PG 位为' 1'。
- 在指定的地址写入要编程的半字。
- 等待 BSY 位变为' 0'。
- 读出写入的地址并验证数据。
擦除流程 :
擦除理解:
- 检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁,。
- 检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的闪存操作。
- 设置 FLASH_CR 寄存器的 PER 位为' 1'。
- 用 FLASH_AR 寄存器选择要擦除的页。
- 设置 FLASH_CR 寄存器的 STRT 位为' 1'。
- 等待 BSY 位变为' 0'。
- 读出被擦除的页并做验证。
4.使用到的固件库函数
解锁,上锁
c
void FLASH_Unlock(void);
void FLASH_Lock(void);
写操作
FLASH_ProgramWord 为 32 位字写入函数,该函数实际上是写入的两次 16 位数据,写完第一次后地址+2。
c
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);
擦除
主要就是用到页擦除
c
FLASH_Status FLASH_ErasePage(uint32_t Page_Address);
获取Flash状态
该函数返回值是通过枚举类型定义的,从这里面我们可以看到 FLASH 操作的 5 个状态。
c
FLASH_Status FLASH_GetStatus(void);
typedef enum
{
FLASH_BUSY = 1,//忙
FLASH_ERROR_PG,//编程错误
FLASH_ERROR_WRP,//写保护错误
FLASH_COMPLETE,//操作完成
FLASH_TIMEOUT//操作超时
}FLASH_Status;
等待操作完成
在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行;既在进行写或擦除操作时,不能进行代码或数据的读取操作。所以在每次操作之前,我们都要等待上一次操作完成这次操作才能开始。
c
FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout);
Flash读取
由于固件库中没有读取 FLASH 指定地址的半字的函数所以这里自己写了一个。
c
u16 STMFLASH_ReadHalfWord(u32 faddr)
{
return *(vu16*)faddr;
}
二、IAP更新
1.系统启动流程详解
当我们使用的STM32板子正常启动上电时,程序文件会从STM32 的内部闪存(FLASH)地址 0x08000000开始写入。此时,由于STM32 是基于 Cortex-M3 内核的微控制器,其内部是通过一张"中断向量表 "来响应中断。程序启动后,会首先从"中断向量表"中取出复位中断向量 执行复位中断程序来完成启动。
这张"中断向量表"的起始地址是 0x08000004 ,当中断来临 时,STM32 的内部硬件机制亦会自动将 PC 指针定位到"中断向量表"处,并根据中断源取出其对应的中断向量执行中断服务程序。
当我们按下复位键时,系统会先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生重中断),此时 STM32 强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。
2.IAP启动流程详解
上图是加入 IAP 之后程序运行的流程图 。此时我们按下复位键,系统还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示,在执行完 IAP 以后(即将新的 APP 代码写入 STM32的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32 的 FLASH,在当前程序和待升级的程序中,分别有两个中断向量表 。
在 main 函数执行过程中,如果 CPU 得到一个中断请求,PC 指针仍强制跳转到地址0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。
通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:
1.新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;
2.必须将新程序的中断向量表相应的移动,移动的偏移量为 x;
3.BootLoader
理解了以上知识后,下面就是实战部分了
IAP.c
c
#include "iap.h"
#include "stmflash.h"
#include "stdio.h"
//采用如下方法实现执行汇编指令WFI
void WFI_SET(void)
{
__ASM volatile("wfi");
}
//关闭所有中断
void INTX_DISABLE(void)
{
__ASM volatile("cpsid i");
}
//开启所有中断
void INTX_ENABLE(void)
{
__ASM volatile("cpsie i");
}
//设置栈顶地址
//addr:栈顶地址
__asm void MSR_MSP(u32 addr)
{
MSR MSP, r0 //set Main Stack value
BX r14
}
//用iapfun表示一个无参数无返回值的函数
typedef void (*iapfun)(void); //定义一个函数类型的参数.
iapfun jump2app; //void jump2app(void),定义一个函数指针
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
//FLASH_APP1_ADDR == appxaddr
if(((*(vu32*)appxaddr)&0x2FFF0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
}
//Flash中用户代码执行
void UserFlashAppRun(void)
{
printf("开始执行FLASH用户代码!!\r\n");
//判断复位中断函数的地址是否在0X08XXXXXX之间
if(((*(vu32*)(0x08003000+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码
}else
{
printf("APP程序加载失败!\r\n");
}
}
uint16_t readBuff[STM_SECTOR_SIZE/2] = {0};
//固件更新函数
void UpdateFun(void)
{
uint16_t app2FlagBuff[2] = {0xFFFF, 0xFFFF};
printf("开始更新固件...\r\n");
//搬运数据
for(uint16_t i=0; i<APP_SIZE; i++) {
printf("正在更新固件%d...\r\n", i);
STMFLASH_Read(FLASH_APP2_ADDR+i*STM_SECTOR_SIZE, readBuff, STM_SECTOR_SIZE/2);
STMFLASH_Write(FLASH_APP1_ADDR+i*STM_SECTOR_SIZE, readBuff, STM_SECTOR_SIZE/2);
}
printf("固件更新完成!\r\n");
STMFLASH_Write(APP2_FLAG_ADDR, app2FlagBuff, 2);
UserFlashAppRun();
}
BootLoader主函数
c
#include "delay.h"
#include "led.h"
#include "key.h"
#include "usart.h"
#include "stdio.h"
#include "stmflash.h"
#include "iap.h"
int main(void)
{
uint16_t data=0;
uint16_t buff[2]={0};
//中断优先级分组
NVIC_SetPriorityGrouping(5);//2位抢占 两位响应
LED_Config();
BEEP_Config();
KEY_Config();
USART_Config();
UART1_SendStr((uint8_t *)"IAP Bootloader Runing\r\n");
STMFLASH_Read(APP2_FLAG_ADDR,buff,2);
printf("更新标志:%x %x\r\n",buff[0],buff[1]);
if(buff[0] == 0xAAAA && buff[1] == 0xAAAA)
{
//有更新
UpdateFun();
}else
{
//无更新
UserFlashAppRun();
}
while (1)
{
printf("BOOTLOADER RUN\r\n");
LED1_Toggle();
Delay_nms(100);
}
}
4.App1(当前程序)
c
#include "delay.h"
#include "led.h"
#include "key.h"
#include "usart.h"
#include "stdio.h"
#include "stmflash.h"
//addr:栈顶地址
__asm void MSR_MSP(u32 addr)
{
MSR MSP, r0 //set Main Stack value
BX r14
}
//用iapfun表示一个无参数无返回值的函数
typedef void (*iapfun)(void); //定义一个函数类型的参数.
iapfun jump2app; //void jump2app(void),定义一个函数指针
void iap_load_app(u32 appxaddr)
{
//FLASH_APP1_ADDR == appxaddr
if(((*(vu32*)appxaddr)&0x2FFF0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
}
extern uint8_t recvtime;
extern uint32_t addr;
int main(void)
{
uint16_t buff[2]={0xAAAA,0xAAAA};
//修改中断向量表,结合实际情况调整
// SCB->VTOR = FLASH_BASE | 0x3000;//
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x3000);
//中断优先级分组
NVIC_SetPriorityGrouping(5);//2位抢占 两位响应
mySysTick_Config();
LED_Config();
KEY_Config();
USART_Config();
UART1_SendStr((uint8_t *)"APP1 Runing\r\n");
STMFLASH_Erase(FLASH_APP2_ADDR, 250*1024);
while (1)
{
if(LED_Time >= 500)
{
printf("APP1 Runing\r\n");
LED2_Toggle();
LED_Time = 0;
}
if(recvtime >= 200)
{
recvtime = 0;
printf("核对数据无误:%d,系统即将自动重启更新:%d\r\n",addr);
addr = 0;
addr = FLASH_APP2_ADDR;
STMFLASH_WriteHalfWord(APP2_FLAG_ADDR, 0xAAAA);
STMFLASH_WriteHalfWord(APP2_FLAG_ADDR+2, 0xAAAA);
printf("开始执行FLASH用户代码!!\r\n");
//判断复位中断函数的地址是否在0X08XXXXXX之间
if(((*(vu32*)(0x08003000+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_load_app(0x08000000);//执行FLASH APP代码
}else
{
printf("APP程序加载失败!\r\n");
}
}
}
}
5.App2(待更新程序)
c
#include "delay.h"
#include "led.h"
#include "key.h"
#include "usart.h"
#include "stdio.h"
#include "stmflash.h"
extern uint8_t recvtime;
extern uint32_t addr;
int main(void)
{
uint16_t buff[2]={0xAAAA,0xAAAA};
//修改中断向量表,结合实际情况调整
// SCB->VTOR = FLASH_BASE | 0x3000;//
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x3000);
//中断优先级分组
NVIC_SetPriorityGrouping(5);//2位抢占 两位响应
mySysTick_Config();
LED_Config();
BEEP_Config();
KEY_Config();
USART_Config();
UART1_SendStr((uint8_t *)"APP2 Runing\r\n");
// STMFLASH_Erase(FLASH_APP2_ADDR, 250*1024);
while (1)
{
if(LED_Time >= 500)
{
printf("APP2 Runing\r\n");
LED2_Toggle();
LED_Time = 0;
}
if(recvtime >= 200)
{
recvtime = 0;
printf("核对数据无误后,请按下复位按键进行数据更新:%d\r\n",addr);
addr = FLASH_APP2_ADDR;
STMFLASH_WriteHalfWord(APP2_FLAG_ADDR, 0xAAAA);
STMFLASH_WriteHalfWord(APP2_FLAG_ADDR+2, 0xAAAA);
addr = 0;
}
}
}
如何烧录 ?
先将Bootloader(引导加载并运行程序 )和App1(当前程序 )的代码烧录进去(记得烧录时不要全片擦除),最后烧录待更新的App2。当烧录完Bootloader和App1时,此时运行的就是App1代码。当把App2烧录进去时,此时上电复后Bootloader会自动加载App2运行该代码。