IAP(In Application Programming)即在应用编程,IAP 是用户自己的程序在运行过程中对 User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。 通常实现 IAP 功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信方式(如 USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作:
1)检查是否需要对第二部分代码进行更新
2)如果不需要更新则转到 4)
3)执行更新操作
4)跳转到第二部分代码执行
第一部分代码必须通过其它手段,如 JTAG 或 ISP 烧入;第二部分代码可以使用第一部分代码 IAP 功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新是再通过第一部分 IAP 代码更新。
我们将第一个项目代码称之为 Bootloader 程序,第二个项目代码称之为 APP 程序,他们存放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader,紧跟其后的就是 APP 程序(注意,如果 FLASH 容量足够,是可以设计很多 APP 程序的,本章我们只讨论两个 APP 程序的情况)。这样我们就是要实现 3 个程序:Bootloader 和 APP1、APP2。
也就是说我们正常运行时,开机进入 Bootloader 程序,从 Bootloader 程序跳转到 APP1 或者 APP2 来执行真正的用户功能。这样在执行 APP1 的时候,如果要升级,程序就会进入 Bootloader ,在 Bootloader 下将需要更新的固件数据写道 APP2 的位置。写完后重启,进入 Bootloader ,用户自行判断运行 APP2 ,即升级成功了。其实APP1和APP2功能一摸一样,只是地址不同而已。
STM32 的 APP 程序不仅可以放到 FLASH 里面运行,也可以放到 SRAM 里面运行,本章,我们介绍的两个 APP,全部都用于 FLASH 运行。
我们先来看看 STM32 正常的程序运行流程,如下图所示:
STM32 的内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是 0x08000004,当中断来临,STM32 的内部硬件机制亦会自动将 PC 指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
在上图中,STM32 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的 main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生重中断),此时 STM32 强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。
当加入 IAP 程序之后,程序运行流程如下图所示:
在上图所示流程中,STM32 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示,此部分同图 53.1.1 一样;在执行完 IAP 以后(即将新的 APP 代码写入 STM32的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32 的 FLASH,在不同位置上,共有两个中断向量表。
在 main 函数执行过程中,如果 CPU 得到一个中断请求,PC 指针仍强制跳转到地址0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。
通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:
1) 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;
2) 必须将新程序的中断向量表相应的移动,移动的偏移量为 x;
介绍到此结束,下面让我们到实例中去体验吧。
bsp_iap.h
#ifndef __BSP_IAP_H__
#define __BSP_IAP_H__
/* 包含头文件 ----------------------------------------------------------------*/
#include "stm32f1xx_hal.h"
/* 类型定义 ------------------------------------------------------------------*/
/************************** IAP 数据类型定义********************************/
typedef void ( * pIapFun_TypeDef ) ( void ); //定义一个函数类型的参数.
/* 宏定义 --------------------------------------------------------------------*/
/************************** IAP 宏参数定义********************************/
//是否更新 APP 到 FLASH,否则更新到 RAM
#define User_Flash
#ifdef User_Flash
#define APP_START_ADDR 0x8010000 //应用程序起始地址(存放在FLASH)
#else
#define APP_START_ADDR 0x20001000 //应用程序起始地址(存放在RAM)
#endif
/************************** IAP 外部变量********************************/
#define APP_FLASH_LEN 15320u //定义 APP 固件最大容量,55kB=55*1024=56320
/* 扩展变量 ------------------------------------------------------------------*/
extern struct STRUCT_IAP_RECIEVE //串口数据帧的处理结构体
{
uint8_t ucDataBuf[APP_FLASH_LEN];
uint16_t usLength;
} strAppBin;
/* 函数声明 ------------------------------------------------------------------*/
void IAP_Write_App_Bin( uint32_t appxaddr, uint8_t * appbuf, uint32_t applen); //在指定地址开始,写入bin
void IAP_ExecuteApp( uint32_t appxaddr ); //执行flash里面的app程序
#endif /* __BSP_IAP_H__ */
bsp_iap.c
/* 包含头文件 ----------------------------------------------------------------*/
#include "bsp_iap.h"
#include "stm_flash.h"
/* 私有类型定义 --------------------------------------------------------------*/
/* 私有宏定义 ----------------------------------------------------------------*/
/* 私有变量 ------------------------------------------------------------------*/
#if defined ( __CC_ARM ) // 使用Keil编译环境
struct STRUCT_IAP_RECIEVE strAppBin __attribute__((at(0x20001000)))={{0},0};
#elif defined ( __ICCARM__ ) // 使用IAR编译环境
struct STRUCT_IAP_RECIEVE strAppBin;//={{0},0};
#endif
static uint16_t ulBuf_Flash_App[1024];
/* 扩展变量 ------------------------------------------------------------------*/
/* 私有函数原形 --------------------------------------------------------------*/
/* 函数体 --------------------------------------------------------------------*/
void IAP_Write_App_Bin ( uint32_t ulStartAddr, uint8_t * pBin_DataBuf, uint32_t ulBufLength )
{
uint16_t us, usCtr=0, usTemp;
uint32_t ulAdd_Write = ulStartAddr; //当前写入的地址
uint8_t * pData = pBin_DataBuf;
for ( us = 0; us < ulBufLength; us += 2 )
{
usTemp = ( uint16_t ) pData[1]<<8;
usTemp += ( uint16_t ) pData[0];
pData += 2; //偏移2个字节
ulBuf_Flash_App [ usCtr ++ ] = usTemp;
if ( usCtr == 1024 )
{
usCtr = 0;
STMFLASH_Write ( ulAdd_Write, ulBuf_Flash_App, 1024 );
ulAdd_Write += 2048; //偏移2048 16=2*8.所以要乘以2.
}
}
if ( usCtr )
STMFLASH_Write ( ulAdd_Write, ulBuf_Flash_App, usCtr );//将最后的一些内容字节写进去.
}
#if defined ( __CC_ARM ) // 使用Keil编译环境
__asm void MSR_MSP ( uint32_t ulAddr )
{
MSR MSP, r0 //set Main Stack value
BX r14
}
#elif defined ( __ICCARM__ ) // 使用IAR编译环境
void MSR_MSP ( uint32_t ulAddr )
{
asm("MSR MSP, r0"); //set Main Stack value
asm("BX r14");
}
#endif
//跳转到应用程序段
//ulAddr_App:用户代码起始地址.
void IAP_ExecuteApp ( uint32_t ulAddr_App )
{
pIapFun_TypeDef pJump2App;
if ( ( ( * ( __IO uint32_t * ) ulAddr_App ) & 0x2FFE0000 ) == 0x20000000 ) //检查栈顶地址是否合法.
{
pJump2App = ( pIapFun_TypeDef ) * ( __IO uint32_t * ) ( ulAddr_App + 4 ); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP( * ( __IO uint32_t * ) ulAddr_App ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
pJump2App (); //跳转到APP.
}
}
stm_flash.h
#ifndef __STMFLASH_H__
#define __STMFLASH_H__
/* 包含头文件 ----------------------------------------------------------------*/
#include "stm32f1xx_hal.h"
/* 类型定义 ------------------------------------------------------------------*/
/* 宏定义 --------------------------------------------------------------------*/
/************************** STM32 内部 FLASH 配置 *****************************/
#define STM32_FLASH_SIZE 512 // 所选STM32的FLASH容量大小(单位为K)
#define STM32_FLASH_WREN 1 // stm32芯片内容FLASH 写入使能(0,禁用;1,使能)
/* 扩展变量 ------------------------------------------------------------------*/
/* 函数声明 ------------------------------------------------------------------*/
uint16_t STMFLASH_ReadHalfWord(uint32_t faddr); //读出半字
void STMFLASH_WriteLenByte(uint32_t WriteAddr, uint32_t DataToWrite, uint16_t Len ); //指定地址开始写入指定长度的数据
uint32_t STMFLASH_ReadLenByte(uint32_t ReadAddr, uint16_t Len ); //指定地址开始读取指定长度数据
void STMFLASH_Write( uint32_t WriteAddr, uint16_t * pBuffer, uint16_t NumToWrite ); //从指定地址开始写入指定长度的数据
void STMFLASH_Read( uint32_t ReadAddr, uint16_t * pBuffer, uint16_t NumToRead ); //从指定地址开始读出指定长度的数据
void Test_Write( uint32_t WriteAddr, uint16_t WriteData ); //测试写入
#endif /* __STMFLASH_H__ */
stm_flash.c
/* 包含头文件 ----------------------------------------------------------------*/
#include "stm_flash.h"
/* 私有类型定义 --------------------------------------------------------------*/
/* 私有宏定义 ----------------------------------------------------------------*/
#if STM32_FLASH_SIZE < 256
#define STM_SECTOR_SIZE 1024 //字节
#else
#define STM_SECTOR_SIZE 2048
#endif
/* 私有变量 ------------------------------------------------------------------*/
#if STM32_FLASH_WREN //如果使能了写
static uint16_t STMFLASH_BUF [ STM_SECTOR_SIZE / 2 ];//最多是2K字节
static FLASH_EraseInitTypeDef EraseInitStruct;
#endif
/* 扩展变量 ------------------------------------------------------------------*/
/* 私有函数原形 --------------------------------------------------------------*/
/* 函数体 --------------------------------------------------------------------*/
/**
* 函数功能: 读取指定地址的半字(16位数据)
* 输入参数: faddr:读地址(此地址必须为2的倍数!!)
* 返 回 值: 返回值:对应数据.
* 说 明:无
*/
uint16_t STMFLASH_ReadHalfWord ( uint32_t faddr )
{
return *(__IO uint16_t*)faddr;
}
#if STM32_FLASH_WREN //如果使能了写
/**
* 函数功能: 不检查的写入
* 输入参数: WriteAddr:起始地址
* pBuffer:数据指针
* NumToWrite:半字(16位)数
* 返 回 值: 无
* 说 明:无
*/
void STMFLASH_Write_NoCheck ( uint32_t WriteAddr, uint16_t * pBuffer, uint16_t NumToWrite )
{
uint16_t i;
for(i=0;i<NumToWrite;i++)
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,WriteAddr,pBuffer[i]);
WriteAddr+=2; //地址增加2.
}
}
/**
* 函数功能: 从指定地址开始写入指定长度的数据
* 输入参数: WriteAddr:起始地址(此地址必须为2的倍数!!)
* pBuffer:数据指针
* NumToWrite:半字(16位)数(就是要写入的16位数据的个数.)
* 返 回 值: 无
* 说 明:无
*/
void STMFLASH_Write ( uint32_t WriteAddr, uint16_t * pBuffer, uint16_t NumToWrite )
{
uint32_t SECTORError = 0;
uint16_t secoff; //扇区内偏移地址(16位字计算)
uint16_t secremain; //扇区内剩余地址(16位字计算)
uint16_t i;
uint32_t secpos; //扇区地址
uint32_t offaddr; //去掉0X08000000后的地址
if(WriteAddr<FLASH_BASE||(WriteAddr>=(FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址
HAL_FLASH_Unlock(); //解锁
offaddr=WriteAddr-FLASH_BASE; //实际偏移地址.
secpos=offaddr/STM_SECTOR_SIZE; //扇区地址 0~127 for STM32F103RBT6
secoff=(offaddr%STM_SECTOR_SIZE)/2; //在扇区内的偏移(2个字节为基本单位.)
secremain=STM_SECTOR_SIZE/2-secoff; //扇区剩余空间大小
if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围
while(1)
{
STMFLASH_Read(secpos*STM_SECTOR_SIZE+FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容
for(i=0;i<secremain;i++)//校验数据
{
if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除
}
if(i<secremain)//需要擦除
{
//擦除这个扇区
/* Fill EraseInit structure*/
EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
EraseInitStruct.PageAddress = secpos*STM_SECTOR_SIZE+FLASH_BASE;
EraseInitStruct.NbPages = 1;
HAL_FLASHEx_Erase(&EraseInitStruct, &SECTORError);
for(i=0;i<secremain;i++)//复制
{
STMFLASH_BUF[i+secoff]=pBuffer[i];
}
STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区
}else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间.
if(NumToWrite==secremain)break;//写入结束了
else//写入未结束
{
secpos++; //扇区地址增1
secoff=0; //偏移位置为0
pBuffer+=secremain; //指针偏移
WriteAddr+=secremain; //写地址偏移
NumToWrite-=secremain; //字节(16位)数递减
if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
else secremain=NumToWrite;//下一个扇区可以写完了
}
};
HAL_FLASH_Lock();//上锁
}
#endif
/**
* 函数功能: 从指定地址开始读出指定长度的数据
* 输入参数: ReadAddr:起始地址
* pBuffer:数据指针
* NumToRead:半字(16位)数
* 返 回 值: 无
* 说 明:无
*/
void STMFLASH_Read ( uint32_t ReadAddr, uint16_t *pBuffer, uint16_t NumToRead )
{
uint16_t i;
for(i=0;i<NumToRead;i++)
{
pBuffer[i]=STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.
ReadAddr+=2;//偏移2个字节.
}
}
/**
* 函数功能: 向内部flash写入数据测试
* 输入参数: WriteAddr:起始地址
* WriteData:要写入的数据
* 返 回 值: 无
* 说 明:无
*/
void Test_Write( uint32_t WriteAddr, uint16_t WriteData )
{
STMFLASH_Write(WriteAddr,&WriteData,1);//写入一个字
}
至于user_iap是我串口数据的处理,这个不涉及IAP功能。
main.c
#define APP_SELECT_ADDR 0x800FFF0
int main()
{
uint16_t g_appSelect[1] = {0x1100};
//找个合理的地址写个数据进去
STMFLASH_Write ( APP_SELECT_ADDR, g_appSelect, 1 );
//这里重新赋值以下
g_appSelect[0] = 0x1234;
//读出来看看是什么
STMFLASH_Read(APP_SELECT_ADDR, g_appSelect, 1 );
printf ( "读 %x,APP\n" ,g_appSelect[0]);
}
• 由 青梅煮久 写于 2021 年 08 月 11 日