IAP的知识网上的各种资料也说的比较明白,在此简单介绍一下。IAP( In Application Programming)即在线应用编程,也就是用户可以使用自己的程序对单片机的User Flash的某一区域(一般为存放自己程序的区域)进行烧写。在真正的工作中产品发布后,可以很方便的使用预留的通信接口(串口、USB、网口、蓝牙等)来完成程序的升级,从而避免了把机器拆开使用下载器烧写程序。
要实现IAP功能一般要设计两部分代码,
最终要实现的是:
参考博文:STM32 IAP 在线升级详解
操作前我们先来说一下内存映射:
下图在stm32f100芯片手册的29页,我们只截取关键部分
注意: 根据启动方式不同,地址空间中从0x0000 0000到0x07FF FFFF这段空间,可以是flash空间或system memory的映射(别名)。
一般而言,flash内存的开始地址是0x0800 0000.
所以我们需要先查看一下misc.h文件中的中断向量表的初始位置宏定义为 NVIC_VectTab_Flash 0x0800 0000
还要设置编译器keil 中的 options for target 的target选项中的 IROM1地址 为0x0800 0000 大小为 0x20000即128K; IRAM1地址为0x2000 0000 大小为0x2000;
(提示:这一项IROM1 地址 即为当前程序下载到flash的地址的起始位置)
具体的启动文件和解读,请见另一篇博文:stm32–启动文件(.s)与启动过程
当前的嵌入式应用程序开发过程里,并且C语言成为了绝大部分场合的最佳选择。如此一来main函数似乎成为了理所当然的起点——因为C程序往往从main函数开始执行。但一个经常会被忽略的问题是:微控制器(单片机)上电后,是如何寻找到并执行main函数的呢?很显然微控制器无法从硬件上定位main函数的入口地址,因为使用C语言作为开发语言后,变量/函数的地址便由编译器在编译时自行分配,这样一来main函数的入口地址在微控制器的内部存储空间中不再是绝对不变的。相信读者都可以回答这个问题,答案也许大同小异,但肯定都有个关键词,叫“启动文件”,用英文单词来描述是“Bootloader”。
无论性能高下,结构简繁,价格贵贱,每一种微控制器(处理器)都必须有启动文件,启动文件的作用便是负责执行微控制器从“复位”到“开始执行main函数”中间这段时间(称为启动过程)所必须进行的工作。最为常见的51,AVR或MSP430等微控制器当然也有对应启动文件,但开发环境往往自动完整地提供了这个启动文件,不需要开发人员再行干预启动过程,只需要从main函数开始进行应用程序的设计即可。
话题转到STM32微控制器,无论是keil uvision4还是IAR EWARM开发环境,ST公司都提供了现成的直接可用的启动文件,程序开发人员可以直接引用启动文件后直接进行C应用程序的开发。这样能大大减小开发人员从其它微控制器平台跳转至STM32平台,也降低了适应STM32微控制器的难度(对于上一代ARM的当家花旦ARM9,启动文件往往是第一道难啃却又无法逾越的坎)。
相对于ARM上一代的主流ARM7/ARM9内核架构,新一代Cortex内核架构的启动方式有了比较大的变化。ARM7/ARM9内核的控制器在复位后,CPU会从存储空间的绝对地址0x000000取出第一条指令执行复位中断服务程序的方式启动,即固定了复位后的起始地址为0x000000(PC = 0x000000)同时中断向量表的位置并不是固定的。
而Cortex-M3内核则正好相反,有3种情况:
(1)没有IAP,只有APP时的正常启动流程:
STM32的FLASH地址起始于0x08000000,程序文件就从此地址开始写入。此外STM32内部通过“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而“中断向量表”的起始地址是0x08000004,当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
根据上图分析启动和运行过程,
① STM32在复位后,先从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,
② 在复位中断服务程序执行完之后,会跳转到的main函数(如使用KEIL MDK调试时一下载进程序,会发现需要运行几次下一步才会跳转到main函数的位置)
③ main函数一般都是超循环体(while(1)死循环),在main函数执行过程中,如果收到中断请求(发生重中断),此时STM32强制将PC指针指回中断向量表处
④ 根据中断源进入相应的中断服务程序,
⑤ 在执行完中断服务程序以后,程序再次返回main函数执行。
根据上图分析加入IAP后的起动和运行过程
① STM32复位后,还是从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到IAP的main函数,如将IAP看作是一个APP的话,那么此部分和正常起动是一样的。(此步=执行复位中断服务程序+跳转main,即将正常运行的①和②合并了)。
② 在执行完IAP以后(固件升级或直接跳转),跳转至APP的复位向量表(APP的复位中断向量起始地址为0X08000004+N+M)。
③ 取出APP的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至APP的main函数(此步=执行复位中断服务程序+跳转main)
④ 同样main函数为一个超循环,并且注意到此时STM32的FLASH,在不同位置上,共有两个中断向量表。在main函数执行过程中,如果CPU得到一个中断请求,PC指针仍强制跳转到地址0X08000004中断向量表处,而不是APP程序的中断向量表。
⑤ 程序再根据我们设置的中断向量表偏移量,跳转到对应中断源的APP的中断服务程序中,
⑥ 在执行完中断服务程序后,程序返回main函数继续运行。
IAP时,flash空间如何分布?
例子1:
我们在设计程序时把FLASH分成3部分,第一部分从0x08000000到0x0800FFFF共64K来存放BootLoader程序,第二部分为0x08010000 到0x0802FFFF共128K来存放App程序,第三部分从0x08030000开始到0x803FFFF共64K来存放程序运行的标志位和其他,如下所示:
例子2:
参考:如何使用STM32F4的BootLoader和APP程序
怎么使用stm32写IAP的bootloader和APP
假设我用的是stm32f103c8t6,它的flash的大小是64k,所以把它分成如上所示:
0x08000000 —0x0800 33FF分配给bootloader使用,大小是13k
0x0800 3400----0x080097FF分配给第一个APP的使用,大小是25k
0x08009800----0x0800 FBFF分配给第二个APP(或者缓存远程下载的新的代码文件)的使用,大小是25k
0x0800FC00----0x0800 FFFF 分配给user_flag和其它标志使用,大小是1k
在keil中设置rom的大小(因为bootloader和每个app都是单独的工程,所以要分别设置其rom的大小)
A、bootloader中rom大小的设置
B、APP1中rom大小的设置
C、APP2(缓冲区)中rom大小的设置
参考:STM32具备升级功能的bootloader及APP/IAP的实现
附件:示例工程链接
https://github.com/zengzhaorong/stm32_IAP-demo
整体上,在flash上烧写2个程序,bootloader和APP。
bootloader程序位于0x80000000处,即默认的程序启动地址;
APP程序则位于bootloader程序的往后某地址,空间大小需自行定义。
flash空间规划
STM32F103RCT6的flash大小为256K。如我flash空间分配如下:
bootloader的主要功能:校验数据、启动APP、升级APP。
bootloader的工作流程如下:
1、基础功能初始化(时钟、外设等);
主要进行一些BSP板级初始化:(仅供参考,因工程而异)
/******************************************************************/
/**
* BSP初始化函数\n
*
*/
/******************************************************************/
void BSP_Init(void)
{
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
/* Initialize GPIO */
MX_GPIO_Init();
/* Initialize usart */
MX_UART_Init();
/* Initialize timer */
Timer_ParamInit();
MX_TIM3_Init();
MX_TIM4_Init();
}
2、数据校验(参数区信息、APP程序的检验)
此步骤为后续启动或升级过程读取一些基础参数(存于参数区),以及校验数据的准确性等。
流程:先读取参数,判断下一步是直接启动APP还是留在bootloader等待升级。若是启动APP则校验APP程序数据是否正常。
/* flash参数区信息结构 */
struct param_info
{
UINT16 usStartFlag; // 启动标志 0x0A-跳至APP 0x0B-等待升级 0x0F-已强制启动过
}
3、跳转APP或升级APP。
3.1 如何跳转至APP呢?
跳转函数:
/*****************************************************************/
/**
* 加载APP \n
*
*/
/******************************************************************/
void loadAPP(INT32U unLoadAddr)
{
void (*fnJump2APP)(void);
INT32U unJumpAddr;
if(((*(__IO INT32U *)unLoadAddr) & 0x2FFE0000) == 0x20000000) /* 检查栈顶地址是否合法 */
{
printf("%s: ----------------------> run APP addr: 0x%x\r\n", __FUNCTION__, unLoadAddr);
/* 用户代码区第5~8字节为程序开始地址(复位地址) */
unJumpAddr = *(__IO INT32U *)(unLoadAddr + 4);
fnJump2APP = (void (*)(void))unJumpAddr;
/* 初始化APP堆栈指针(用户代码区的前4个字节用于存放栈顶地址) */
__set_MSP(*(__IO INT32U *)unLoadAddr);
fnJump2APP();
}
else
{
printf("ERROR: %s: Stack top address is not valid! Can not run func!\r\n", __FUNCTION__);
while(1);
}
}
关于跳转部分的代码的理解(转)
这里重点说一下几句经典且非常重要的代码:
第一句: if ((((__IO uint32_t)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000) //判断栈定地址值是否在0x2000 0000 - 0x 2000 2000之间
怎么理解呢? (1)在程序里#define ApplicationAddress 0x8003000 ,那么*(__IO uint32_t*)ApplicationAddress) 即取0x8003000开始到0x8003003 的4个字节的值,(2) 因为我们的应用程序APP中设置把 中断向量表 放置在0x08003000 开始的位置;而中断向量表里第一个放的就是栈顶地址的值。
也就是说,这句话即通过判断栈顶地址值是否正确(是否在0x2000 0000 - 0x 2000 2000之间,因为栈顶地址就是整个地址空间中SRAM所在的位置) 来判断是否应用程序已经下载了,因为应用程序的启动文件刚开始就去初始化栈空间,如果栈顶值对了,说应用程已经下载了并且启动文件的初始化也执行了;
第二句: JumpAddress = (__IO uint32_t) (ApplicationAddress + 4); [ common.c文件第18行定义了: pFunction Jump_To_Application;]
ApplicationAddress + 4 即为0x0800 3004 ,里面放的是中断向量表的第二项“复位地址” JumpAddress = (__IO uint32_t) (ApplicationAddress + 4); 之后此时JumpAddress
第三句: Jump_To_Application = (pFunction) JumpAddress;
此时Jump_To_Application指向了复位函数所在的地址。
( startup_stm32f10x_md_lv. 文件中别名 typedef void (pFunction)(void);
void (pFunction)(void); 是声明一个函数指针,加上一个typedef 之后 pFunction只不过是类型 void ()(void) 的一个别名)
第四 、五句: __set_MSP((__IO uint32_t*) ApplicationAddress); \设置主函数栈指针
Jump_To_Application(); \执行复位函数
3.2 升级APP
升级,即是将新的程序数据替换旧的程序数据,因此,只需在程序数据所在区域擦除旧数据再写上新数据即可。
具体而言,无非就是一包一包地接收数据,然后一包一包地写入flash。
最好分为两个阶段,防止升级文件传输不完整或损坏,导致变砖:
其中,通信是通过串口,自己定义协议传送数据,将程序数据拆成N个包,一包一包地传输,例如设定3条协议,分别是:开始升级、发送升级数据、升级完成确认(自定义啦)。
程序数据位置CPU内部flash区,因此需要以flash的读写擦等函数操作为基础。
/****************************************************************
* Func :
* Desc : 读取CPU内部flash
* Input :
* Output:
* Return:
*****************************************************************/
INT32 cpuflash_read(UINT32 unStartAddr, UINT8 *pData, UINT16 usSize)
{
if(pData == NULL)
return -1;
memcpy(pData, (INT8U *)unStartAddr, usSize);
return 0;
}
/****************************************************************
* Func :
* Desc : 写入CPU内部flash (要先erase才能写)
* Input :
* Output:
* Return:
*****************************************************************/
INT32 cpuflash_write(UINT32 unStartAddr, UINT8 *pData, UINT16 usSize)
{
INT32 i = 0;
INT32 nRet = 0;
UINT16 usTemp1 = 0;
UINT16 usTemp2 = 0;
UINT16 usTempALL = 0;
if(usSize%2 != 0)
{
usSize += 1;
}
HAL_FLASH_Unlock(); // unlock
for(i=0; i<usSize/2; i++)
{
usTemp1 = *pData;
usTemp2 = *(pData+1);
usTempALL = ((usTemp1&0X00FF) | ((usTemp2<<8)&0XFF00));
//usTemp = ((*pData>>8)&0X00FF) | (*(pData+1)&0XFF00);
//usTemp = *(INT16U *)pData;/*这个会导致硬件崩溃*/
nRet = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, unStartAddr, usTempALL);
if(nRet != HAL_OK)
{
HAL_FLASH_Lock(); // lock
printf("ERROR: %s: program[%d %d] failed-code[%d]\n", __FUNCTION__, usTemp1, usTemp2, nRet);
return -1;
}
unStartAddr += 2;
pData += 2;
}
HAL_FLASH_Lock(); // lock
return 0;
}
/****************************************************************
* Func :
* Desc : 擦除CPU内部flash(整页)
* Input :
* Output:
* Return:
*****************************************************************/
INT32 cpuflash_erase(UINT32 unStartAddr, UINT32 unEndAddr)
{
FLASH_EraseInitTypeDef stEraseInit;
UINT32 ucPageErr = 0;
UINT32 unTempAddr = 0;
INT32 nRet = 0;
HAL_FLASH_Unlock(); // unlock
for(unTempAddr=unStartAddr; unTempAddr<=unEndAddr; unTempAddr+=FLASH_PAGE_SIZE)
{
stEraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
stEraseInit.PageAddress = unTempAddr;
stEraseInit.NbPages = 1;
nRet = HAL_FLASHEx_Erase(&stEraseInit, &ucPageErr);
if(nRet != HAL_OK)
{
HAL_FLASH_Lock();
return -1;
}
GPIO_feedDog();
}
HAL_FLASH_Lock(); // lock
return 0;
}
顺便说下,STM32内部flash库的保护问题,若不加保护,则内部程序可轻易被J-Flash等工具读出。因此,常用的措施是:对内部flash添加读写保护机制。锁定与解除函数如下:
/****************************************************************
* Func :
* Desc : 使能读保护函数
* Input :
* Output:
* Return:
*****************************************************************/
void cpuflash_enableReadProtect(void)
{
FLASH_OBProgramInitTypeDef OBInit;
__HAL_FLASH_PREFETCH_BUFFER_DISABLE();
HAL_FLASHEx_OBGetConfig(&OBInit);
if(OBInit.RDPLevel == OB_RDP_LEVEL_0)
{
printf("%s: ------------ set ----------\n", __FUNCTION__);
OBInit.OptionType = OPTIONBYTE_RDP;
OBInit.RDPLevel = OB_RDP_LEVEL_1;
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
HAL_FLASHEx_OBProgram(&OBInit);
HAL_FLASH_OB_Lock();
HAL_FLASH_Lock();
//HAL_FLASH_OB_Launch();
}
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
}
/****************************************************************
* Func :
* Desc : 失能读保护函数
* Input :
* Output:
* Return:
*****************************************************************/
void cpuflash_disableReadProtect(void)
{
FLASH_OBProgramInitTypeDef OBInit;
__HAL_FLASH_PREFETCH_BUFFER_DISABLE();
HAL_FLASHEx_OBGetConfig(&OBInit);
if(OBInit.RDPLevel == OB_RDP_LEVEL_1)
{
printf("%s: ------------ set ----------\n", __FUNCTION__);
OBInit.OptionType = OPTIONBYTE_RDP;
OBInit.RDPLevel = OB_RDP_LEVEL_0;
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
HAL_FLASHEx_OBProgram(&OBInit);
HAL_FLASH_OB_Lock();
HAL_FLASH_Lock();
//HAL_FLASH_OB_Launch();
}
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
}
APP的主要功能: 1、除升级功能外的所有应用功能, 2、跳转至bootloader准备升级。
void SoftReset(void)
{
__set_FAULTMASK(1); // ¹Ø±ÕËùÓÐÖжË
NVIC_SystemReset(); // ¸´Î»
}
关于APP与IAP互跳之间的中断处理问题
跳转时中断问题还是一个比较棘手的问题。。经常跳转之后无法进入中断,自己理解大概是,跳转时只是强制改变了PC指正的位置,但是里面的中断寄存器什么的都没有变,这样中断存在,但是中断函数什么的都没有了,造成程序死掉。。我在写的过程中也遇到了问题,第一次从iap跳到app正常,但是从app跳回iap的时候由于残留的中断太多,在iap中程序死了。
处理方式(具体可参考文章http://dzdesigned80.blog.163.com/blog/static/203259238201272425313152/):
1、把app中的跳转命令换成了系统复位NVIC_SystemReset();(不同的固件库可能函数名不同)
2、跳转之前复位或者关闭所有打开的中断
3、跳转后在初始化时加入RCC_DeInit();,,NVIC_DeInit ();等让中断恢复默认值。
参考博文:stm32实现iap远程固件更新
虽然本文标题是实现远程固件更新,但是具体远程方案本文不做详细说明,重点在于介绍mcu接收到新的固件后怎么保存更新,以及更新失败回滚等。
下面简单说明一下远程的事情。
stm32的通信方式有串口,spi,iic,以及sdio等。也就是说我们的固件可以通过这些方式传输到mcu,不过普遍常用的是串口或者用sdio(外接sd卡)这两种方式。简单点还是再加一个串口网络模块,然后把固件存到服务器,经由串口网络模块透传到mcu。比如用http协议把固件发送下来。远程下载就这么简单一说。
接下来重点分析更新的事情。
远程更新使用的固件和我们平时烧录程序用的固件格式有点区别,我们需要用二进制格式(.bin)文件。生成方式以mdk为例介绍一下,只需要添加一条命令行。
在mdk工程配置选项选择User,这个页面是让我们添加自定义命令行的,我们要添加的命令添加到第三个选项,即在编译完成后执行。
下面是命令内容,需要注意的是 bin前面两个-,app1.bin就是生成的固件,名字可以自定义,**.axf是你工程实际的.axf文件,路径要正确。不知道你的axf在那在output页面查看。
fromelf.exe --bin -o ../app1.bin ./**.axf
现在我们就可以生成bin文件了,但是还差一点步骤。
一般使用下mcu启动后会自动把0x0800 0000映射到地址0x0000 0000,然后取指令执行。但是现在我们程序可以理解成分成了两部分。
可以看到加入iap升级功能后我们app的起始地址变了,所以对应工程也要做这部分修改
如图,我这里把地址偏移了0x20000,同时在Linker中把“Use Memory Layout from Target Dialog”勾选,让我们的修改生效。
如此设置以后就一切ok了
关于固件有一点需要注意,因为起始地址修改了,所以导致我们的中断向量表也整体偏移了,所以需要在app程序起始添加一行代码,本文是偏移0x20000,根据实际使用做相应改动
NVIC_SetVectorTable(NVIC_VectTab_FLASH,0x2000);
第一步:把固件下载到板子的缓存区,缓存区可以是SD卡或者flash的某块专用区域(该区域不是bootloader和APP的区域)
)
第二步:下到板子后,我们需要把固件保存到内置flash(APP区域)对应的地址。
本步骤可分为三个部分:
如何读写flash第二章的代码:
cpuflash_write{
...
}
本文上面设置的偏移是0x2 0000,所以此处写入flash的地址也必须是0x802 0000(0x800 0000 + 0x2 0000)
现在万事具备了,接下来就是更新的事情了,简单说一下更新的思路。
上电启动后运行BootLoader程序,在bootloader中检查是否是否需要更新,不需要的话就引导之前的app程序运行,需要更新就引导新的app程序。
引导步骤大体就是重置栈顶指针,强制跳转app的reset复位中断。
代码见第二章的
void loadAPP(INT32U unLoadAddr)
{
...
}