**IAP-Bootloader程序总结**
IAP-Bootloader程序总结 带详细源码
串口IAP-Bootloader程序就是为程序写一段引导程序方便后续的升级和更新,基本上现在大多数产品都会使用是一定要掌握的技能。具体原理就是在程序开始部分预留2k~20k大小视情况而定的Bootloader程序,正式程序为APP程序在Bootloader程序之后,在上电之后检测是否更新不更新则跳转到APP程序,更新则下载新的APP程序到内部flash之后自动跳转到APP程序。
硬件平台:原子的战舰开发板 MCU STM32F103ZET6内部flash512k 使用串口1
Bootloader程序留64k实际用了11k APP程序用剩余的448k
代码部分为一原子代码为参考修改,同时参考借鉴了
阿卡基猿的文章 下载烧录部分
原文链接:[]
fandelxin的文章 跳转和原理部分
原文链接:[]
主要功能:下载完成以后可以自动检测有没有APP程序没有则等待更新,同时擦除要更新部分的FLASH,擦除完成后等待串口下载程序,下载完成后可以自动跳转,在APP运行的同时只要接受到串口的升级命令可以跳回Bootloader程序进行更新。与原子的源代码不同部分,可以下载超过55K的APP程序,无需按键跳转可以回跳。
特别说明:文章后提供全部源码供大家借鉴参考,若使用的是原子战舰开发板则可以直接下载运行。本人技术经验有限,程序可能还有很多BUG,欢迎大家提出来一起学习,而已串口下载部分也没有校验,容易出错无法用于实际的生产。
![效果演示,上电擦除完成![]()
一共分为几个关键点和难点:
1,Bootloader跳转部分
2,app回跳部分
3,串口下载和烧录部分
结合代码详细讲解
1,Bootloader跳转部分
增加全局变量u16 IAP_UP = 0; 上电检测若IAP_UP = 0则直接跳转到APP,若无程序则把改IAP_UP = 1,进入下载函数。
还需u16 IAP_JP = 0;这个变量是检查是否重APP中跳转到Bootloader的变量在倒数第二个flash地址。
if((((vu32)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)还需要这条语句检查APP地址中是否有程序。IAP_UP变量在上电时要重flash的最后一个地址读取,同时每次更改都要写入到对应的flash中。
Main函数中的代码:
/********************************************************************
#include "delay.h"
#include "sys.h"
#include "usart.h"
#include "iap.h"
#include "swtimer.h"
u16 IAP_UP = 0;
u16 IAP_JP = 0;
/*
增加一个全局变量 IAP_UP 存储在内部flash的最后一个地址 0x807ffff
若IAP = 1;则需要更新显示"wait update order..." 等待更新完成 显示“update order Successed!!”并且把
IAP 写为 0,无需更新同时跳转到APP程序
*/
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
uart_init(115200); //串口初始化为115200
delay_init(); //延时初始化
Timer3_init();
STMFLASH_Write_hw(0x807fffe,IAP_UP);
while(1)
{
IAP_UP = STMFLASH_ReadHalfWord(0x807fffe);
IAP_JP = STMFLASH_ReadHalfWord(0x807fffc);
printf("IAP_UP =%d\r\n",IAP_UP);
printf("IAP_JP =%d\r\n",IAP_JP);
if((IAP_UP == 1)||(IAP_JP == 1))
{
IAP_UpdataProgram();
printf("固件下载完成!\r\n");
if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000) {
printf("固件更新完成!\r\n");
IAP_UP = 0;//更新完成
IAP_JP = 0;
STMFLASH_Write_hw(0x807fffe,IAP_UP);
STMFLASH_Write_hw(0x807fffc,IAP_UP);
iap_load_app(FLASH_APP1_ADDR); }
else
{
printf("固件更新失败!\r\n");
break;
}
}
if((IAP_UP == 0)&&(IAP_JP != 1))//上电运行则直接跳转
{
if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)
{
iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码
}else
{
printf("非FLASH应用程序,无法执行!\r\n");
IAP_UP = 1;
STMFLASH_Write_hw(0x807fffe,IAP_UP);
}
}
}
}
跳转部分的核心函数iap_load_app(FLASH_APP1_ADDR);
typedef void (*iapfun)(void); //定义一个函数类型的参数.
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
else printf("固件更新失败!\r\n");
}
程序的开始部分前四个字节放着栈顶地址,接下来的四个字节放着复位地址,if((((vu32)appxaddr)&0x2FFE0000)==0x20000000)检查顶地址是否合法可以避免错误下载或者下载失败后的错误跳转。
//设置栈顶地址
//addr:栈顶地址
__asm void MSR_MSP(u32 addr)
{
MSR MSP, r0 //set Main Stack value
BX r14
}__asm关键字可以执行汇编语句,用于初始化APP堆栈指针
最后jump2app();执行APP程序的服务函数地址,跳转到APP。
2,从APP回跳到Bootloader
SCB->VTOR = FLASH_BASE | 0x10000;
在程序的开始要把APP函数的中断向量表进行偏移大小为你Bootloader程序大小,整个程序是有两张向量表但是必须保证PC指针指向APP程序的中断向量表。
可以用系统复位函数也可以用开门狗复位,
void SoftReset(void)
{
__set_FAULTMASK(1); // 关闭所有中端
IAP_UP[0] = 1;//需更新
IAP_JP[0] = 1;
STMFLASH_Write(0x807fffc,IAP_JP,1);//当程序在串口更新完之后改为1;
NVIC_SystemReset();// 复位
}
同时不要忘了要把IAP_UP和IAP_JP更新写入flash中,__set_FAULTMASK(1); // 关闭所有中端进入复位函数后关闭全部中断防止程序卡死。
3,串口下载部分
下载部分要用环形缓冲区的思想,同时要把串口下载的8位数据转换成16位半字才能进行烧录。
#define MAXBUFFER 512
u8 UsartBuffer[MAXBUFFER];
u16 UsartWptr = 0;
u16 UsartRptr = 0;
创建一读一个写两个变量分别进行边下载边写flash的操作,
/*************************************************************
Function : IAP_BufferWrite
Description: 写缓冲区
Input : none
return : none
*************************************************************/
void IAP_BufferWrite(void)
{
if(UsartWptr == (UsartRptr - 1))//缓冲区存满了 当读速度比较慢时,写不能和读同一个地方前一个数据还没读走不能写入。
{
return;//返回
}
UsartBuffer[UsartWptr] = USART1->DR;//存取串口数据
UsartWptr++;//缓冲写位置值自增
UsartWptr = UsartWptr%MAXBUFFER;//保证写位置值不溢出
}
/*************************************************************
Function : IAP_BufferRead
Description: 读缓冲区
Input : none
return : none
*************************************************************/
u8 IAP_BufferRead(u8 *data)
{
if(UsartRptr == UsartWptr)//无数据可读
{
return 0;
}
*data = UsartBuffer[UsartRptr];//读取缓冲区数据
UsartRptr++;//读位置值自增
UsartRptr = UsartRptr % MAXBUFFER;//保证读位置值不溢出
return 1;
}
void USART1_IRQHandler(void)
{
u8 char_value = 0; //记录数据
if(USART1->SR&(1<<5))//接收到数据
{
USART1->SR &= ~(1<<5);
IAP_BufferWrite();
USART_RX_CNT++;
timer_enable(1,100,timer0_backcall_func); //接受超时要尽量长一点,避免丢失数据。
}
if(USART1->SR&(1<<4)) //进入空闲中断
{
char_value = USART1->DR; //清空空闲中断标志位
}
}
在串口的接受中断函数里面我们要进行写缓存操作,每次写入一个字节UsartWptr则进行加1,每当读走一个字节则UsartWptr加1;
UsartWptr = UsartWptr%MAXBUFFER; 这条语句可以清零。在空闲中断里面我们不做任何操作。
然后就是在主函数中调用烧写函数
/*************************************************************
Function : IAP_UpdataProgram
Description: 更新程序
Input : none
return : none
*************************************************************/
void IAP_UpdataProgram(void)
{
u8 n = 0;
u8 data = 0;
u8 datalow = 0;
u8 datahigh = 0;
u16 pBuffer =0;
u32 applenth = 0; //接收到的app代码长度
u16 Erase = FLASH_APP1_SIZE/ECTOR_SIZE; //每个扇区2k
//u32 Erase = FLASH_APP1_ADDR; //需要擦除的地址空间
rcvTimeout = 0;
FLASH_EraseOptionBytes (); //关闭flash写保护
printf("正在擦除扇区!\r\n");
FLASH_Unlock();//flash解锁
for(Erase = 0;Erase < FLASH_APP1_SIZE/ECTOR_SIZE;Erase++)
{
FLASH_ErasePage(Erase*ECTOR_SIZE+FLASH_APP1_ADDR);//擦除这个APP1扇区
printf(".");
delay_ms(150);
}
FLASH_Lock();//flash锁
printf("擦除扇区完成请更新程序!\r\n");
TIM6->CR1 |= 0x01; //使能定时器6
while(1)
{
switch(n)
{
case 0:
if(IAP_BufferRead(&data)) //接收地字节数据
{
datalow = data;
n = 1;
}
else
{
break;
}
case 1:
if(IAP_BufferRead(&data)) //接收高字节数据
{
datahigh = data;
n = 0;
pBuffer= ((u16)(datalow)) | ((u16)(datahigh << 8));
IAP_FlashProgramdata(pBuffer);
}
if(rcvTimeout)
{
datahigh = 0xff;
n = 0;
pBuffer= ((u16)(datalow)) | ((u16)(datahigh << 8));
IAP_FlashProgramdata(pBuffer);
}
default:
break;
}
if(rcvTimeout)
{
TIM6->CR1 &= ~(1<<0); //使能定时器6
rcvTimeout = 0;
applenth = USART_RX_CNT;
printf("用户程序接收完成!\r\n");
printf("代码长度:%dBytes\r\n",applenth);
return;//结束函数
}
}
}
在烧录函数里面我们先进行扇区擦除,把需要烧录的地址的全部扇区都擦除,TIM6->CR1 |= 0x01; //使能定时器6
开启定时器是为了下载完成之后,进行自动结束烧录函数跳转到APP中使用的。
设置全局变量rcvTimeout如果超过500ms没有接受到新的数据了,我们则认为烧录完成了,自动退出该函数。使用的是基本定时器6,代码比较简单我就不讲解了。
比较容易出错的地方有:
1,下载烧录部分要把8位数据转换成16位,这里如果出错会导致跳转到APP程序卡死。
2,写flash部分要记得先要擦除,不然也会烧录失败。 3,超时跳转的定时器函数的中断优先级要比串口的低,而且时间不能太短,不然也会导致出错。