前言
此代码兼容STM32F103全系列
为避免添加上升级程序造成内存不足,请使用128KB Flash及其以上的型号
这篇文章是为了能够让大家快速移植我的升级模板程序到自己的项目
BootLoader 程序制作
拷贝文件到自己的项目
拷贝到自己的项目(我准备了一个工程作为叙述)
在自己工程的定时器里面添加以下信息
if(IAPStructValue.PutDataFlage && IAPStructValue.UpdateFlage)IAPStructValue.DownloadTimeout++; else IAPStructValue.DownloadTimeout=0; IAPStructValue.MainTimeout++;
在自己工程的主函数添加如下信息
#include "IAP.h" IAP();
IAPLoadAPPProgram(); IAPDownloadTimeoutFunction(); IAPMainTimeoutFunction(); IAPWriteData();
大家把当前的程序下载到单片机,然后看一下串口1的打印信息
user1ROMStart: 0x8004000 用户程序1 Flash存储的开始地址
user1ROMSize : 0x5c00 用户程序1 程序大小
user2ROMStart: 0x8009c00 用户程序2 Flash存储的开始地址
user2ROMSize : 0x5c00 用户程序2 程序大小
大家可以在下面这个文件根据自己的芯片进行设置
所选芯片Flash大小:这个根据自己的芯片设置
BootLoader程序大小: BootLoader程序产生的bin文件大小
假设自己的BootLoader程序的bin文件大小是 15K
则可以设置上面的值 为16,18,20等
假设自己的BootLoader程序的bin文件大小是 20K
则可以设置上面的值 为22,24,26等
存储用户数据所用Flash大小: 这个根据自己需要的设置,
但是必须设置,因为咱升级的时候也需要记录数据
可以是2,4,6,8等等等等
设置好以后系统便会根据大家的设置打印出来APP用户程序的信息
当前Flash存储分配如下图
BootLoader程序占用 16KB
两份用户程序各占23KB,
第一份APP用户程序从0x08004000开始存储
第二份APP用户程序从0x08009C00开始存储
剩余的2KB用来存储其它信息
大家调整完以后编译一下自己的APP用户程序,看下自己的APP用户程序大小
超了就重新调整,要么换芯片.....
然后接着说
1.单片机升级程序需要对比版本号
{"version":"1.0.456"} 标准JSON格式 版本号最长20个字符
格式不能变哈,我程序内部就是这样提取版本号的!!!
大家无论用啥子网络模块,啥子串口等等等等
必须通过自己的方式获取 版本号文件内容
然后 把获取的信息 交给函数 IAPVersionDispose 处理
/** * @brief 处理从服务器获取的版本号,并获取另一份用户程序 * @warn * @param data 传入从云端获取的版本号信息 * @param None * @param None * @retval None * @example **/ void IAPVersionDispose(char *data) { if(!IAPStructValue.PutDataFlage)//升级状态下不再进入判断 { if(strstr(data,"version"))//接收到版本//{"version":"1.02.56"} { IAPStructValue.Str = StrBetwString(data,"version\":\"","\"");//提取版本号 if(IAPStructValue.Str != NULL && strlen(IAPStructValue.Str)<=20)//版本号没有问题,设置的版本号最长20位 { memset(IAPStructValue.VersionServer,0,sizeof(IAPStructValue.VersionServer)); memcpy(IAPStructValue.VersionServer,IAPStructValue.Str,strlen(IAPStructValue.Str));//获取当前云端版本 if(memcmp(IAPStructValue.VersionServer,IAPStructValue.VersionDevice,20)==0)//云端版本和当前版本一致 { IAPSetUpdateStatus(UpdateStatus_VersionAlike);//版本号和服务器上面的一致 IAPResetMCU();//重启 } else { cStringRestore(); if(IAPStructValue.RunProgram == 1)//运行的第一份程序 { IAPStructValue.Str = StrBetwString(data,"SumBin2\":",",");//提取第二份bin文件的数据和 } else//运行的第二份程序 { IAPStructValue.Str = StrBetwString(data,"SumBin1\":",",");//提取第一份bin文件的数据和 } if(IAPStructValue.Str != NULL)//有数据 { IAPStructValue.Len = strlen(IAPStructValue.Str);//获取字符串长度 if(IAPStructValue.Len == 1 && IAPStructValue.Str[0]>='0'&& IAPStructValue.Str[0]<='9') {//1位 IAPStructValue.SumBin = IAPStructValue.Str[0]-'0'; } else if(IAPStructValue.Len==2 && IAPStructValue.Str[0]>='0'&& IAPStructValue.Str[0]<='9' && IAPStructValue.Str[1]>='0'&& IAPStructValue.Str[1]<='9') {//2位 IAPStructValue.SumBin = (IAPStructValue.Str[0]-'0')*10+(IAPStructValue.Str[1]-'0'); } else if(IAPStructValue.Len==3&&IAPStructValue.Str[0]>='0'&& IAPStructValue.Str[0]<='9'&& IAPStructValue.Str[1]>='0'&& IAPStructValue.Str[1]<='9'&& IAPStructValue.Str[2]>='0'&& IAPStructValue.Str[2]<='9') {//3位 IAPStructValue.SumBin = (IAPStructValue.Str[0]-'0')*100+(IAPStructValue.Str[1]-'0')*10+(IAPStructValue.Str[2]-'0'); } else { IAPStructValue.Len = 4; } if(IAPStructValue.Len>3 || IAPStructValue.SumBin < 0 || IAPStructValue.SumBin >255) { IAPSetUpdateStatus(UpdateStatus_SumBinRangeErr);//校验和范围错误 IAPResetMCU();//重启 } } if(FlashErasePage(IAPStructValue.UpdateAddress,FLASH_USER_SIZE)!=4)//擦除接收用户程序Flash地址 { IAPSetUpdateStatus(UpdateStatus_FlashEraseErr);//Flash 擦除失败 IAPResetMCU();//重启 } IAPSetUpdateVersionServer(IAPStructValue.VersionServer);//存储云端版本 if(IAPStructValue.RunProgram == 1)//运行的第一份程序 { //发送请求第二份程序文件指令 } else//运行的第二份程序 { //发送请求第一份程序文件指令 } IAPStructValue.PutDataFlage = 1;//可以向环形队列写入数据 } cStringRestore(); } else { IAPSetUpdateStatus(UpdateStatus_VersionLenErr);//版本号长度错误 IAPResetMCU();//重启 } } } }
咱现在没有加上校验和,只有版本号 {"version":"1.0.456"} 后面一点一点的说明怎么做.
然后注意:
大家发送完获取程序文件的指令以后,服务器便会发送过来程序数据
然后大家需要调用
if(IAPStructValue.PutDataFlage && (IAPStructValue.PutDataFlage^IAPStructValue.Overflow))//可以往环形队列里面写数据,同时没有溢出
{
if(PutData(&rb_tIAP,NULL,&Res,1) == -1) //&Res :为数据地址 1:写一个数据,可以写多个
{
IAPStructValue.Overflow = 1;//环形队列溢出
}
}
举个例子:
假设是串口过来的程序数据
把数据写进去的时候,主循环就开始把数据写进Flash里面了.
/** * @brief 把接收到的程序文件写入Flash * @warn * @param * @param None * @param None * @retval None * @example **/ void IAPWriteData(void) { if(rbCanRead(&rb_tIAP)>1)//接收到更新程序就开始写入 { IAPResetDownloadTimeoutFunction();//重置程序下载超时 rbRead(&rb_tIAP, &IAPStructValue.ReadDat, 2);//读取两个数据 IAPStructValue.ReadDate = (u16)IAPStructValue.ReadDat[1]<<8; IAPStructValue.ReadDate = IAPStructValue.ReadDate|IAPStructValue.ReadDat[0];//拼接数据 if(IAPStructValue.UpdateAddressCnt< FLASH_DATA_ADDR)//不能超过 数据区 { #ifdef UserContentLength //自己的Web服务器返回 Length: XXXXXXXX (本次的数据长度) //写入的数据个数不能超出,http实际返回的数据个数 if( (IAPStructValue.UpdateAddressCnt - IAPStructValue.UpdateAddress) <= HttpDataLength) #endif { if(!IAPStructValue.FlashWriteErrFlage)//写Flash没有错误 { //计算数据累加和 IAPStructValue.Sum = IAPStructValue.Sum + IAPStructValue.ReadDat[0] + IAPStructValue.ReadDat[1]; if(WriteFlashHalfWord(IAPStructValue.UpdateAddressCnt,IAPStructValue.ReadDate) != 0)//写Flash { IAPStructValue.FlashWriteErrFlage = 1;//写Flash错误 } } IAPStructValue.UpdateAddressCnt+=2;//地址增加 } } } else//环形队列里面没有数据了.并不证明接收完了数据,可能写入环形队列慢,读的快 { if(IAPStructValue.ReadDataEndFlage)//接收完更新程序 { IAPStructValue.PutDataFlage = 0;//停止向环形队列写入数据 IAPStructValue.UpdateFlage = 0; //更新标志清零 IAPStructValue.ReadDataEndFlage = 0;//清零接收完更新程序标志 #ifdef UserContentLength //自己的Web服务器返回 Length: XXXXXXXX (本次的数据长度) //写入的数据个数和http实际返回的数据个数不相等 if( (IAPStructValue.UpdateAddressCnt - IAPStructValue.UpdateAddress) != HttpDataLength) { IAPSetUpdateStatus(UpdateStatus_MissingData);//数据错误 } else if(IAPStructValue.FlashWriteErrFlage == 1)//Flash写错误 #else if(IAPStructValue.FlashWriteErrFlage == 1)//Flash写错误 #endif { IAPSetUpdateStatus(UpdateStatus_FlashWriteErr);//Flash写错误 } else if(!IAPStructValue.Overflow)//没有溢出过 { if(IAPCheckRamFlashAddress(IAPStructValue.UpdateAddress))//检测某些位置的Flash的高位地址是不是0x08 //RAM的高位地址是不是0x20 { if(IAPStructValue.SumBin != -1)//获取了云端的校验和 { if(IAPStructValue.SumBin == IAPStructValue.Sum)//校验和正确 { IAPSetUpdateChangeProgram();//切换运行程序地址 IAPSetUpdateStatus(UpdateStatus_WriteAppOk);//写入0x01标志 } else { IAPSetUpdateStatus(UpdateStatus_SumCheckErr);//数据和校验错误 } } else { IAPSetUpdateChangeProgram();//切换运行程序地址 IAPSetUpdateStatus(UpdateStatus_WriteAppOk);//写入0x01标志 } } else { IAPSetUpdateStatus(UpdateStatus_DataAddressError);//数据错误 } } else//数据溢出 { IAPSetUpdateStatus(UpdateStatus_DataOverflow);//数据溢出 } IAPResetMCU();//重启 } } }
现在在接收数据,什么时候接收完了???
你需要给我个信号
在确定接收完的地方写上
IAPStructValue.ReadDataEndFlage = 1;
为了确保刚刚确实是在升级程序,可以这样写
if(IAPStructValue.PutDataFlage)//写入环形队列的标志位置位了
{
IAPStructValue.ReadDataEndFlage=1;//接收完了程序
}
列如:使用的串口接收的程序文件,判断串口接收完数据了
按照以上的叙述,就已经完成了BootLoader部分了
现在看细节处理1
假设咱是用http获取的程序文件,然后http会给咱加上数据头,咱需要去掉
同时假设,数据是通过串口过来的
我直接贴出来处理源码.
/*处理HTTP数据*/ u8 HttpHeadCnt = 0; u8 HttpHeadOK = 0;//接收到正常的http数据 u8 HttpDataLengthOK= 0;//获取了数据长度 u32 HttpDataLength = 0;//http数据长度 u8 HttpHeadEndOK = 0;//http的heap接收完成,后面发过来的是数据 u8 HttpDataStartFlage = 0;//下次传进来的是消息体 void USART1_IRQHandler(void)//串口1中断服务程序 { u8 Res; if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { Res =USART_ReceiveData(USART1); //读取接收到的数据 Usart1ReadBuff[Usart1ReadCnt] = Res; //接收的数据存入数组 Usart1ReadCnt++; if(Usart1ReadCnt > Usart1ReadLen -10)//防止数组溢出 { Usart1ReadCnt = 0; } Usart1IdleCnt = 0; if(HttpDataStartFlage)//下次来的是真实数据 { if(IAPStructValue.PutDataFlage && (IAPStructValue.PutDataFlage^IAPStructValue.Overflow))//可以往环形队列里面写数据,同时没有溢出 { if(PutData(&rb_tIAP,NULL,&Res,1) == -1) //&Res :为数据地址 1:写一个数据,可以写多个 { IAPStructValue.Overflow = 1;//环形队列溢出 } } } //解析http数据-------------------------------Start //HTTP/1.1 200 OK if(!HttpHeadOK && IAPStructValue.PutDataFlage) { if(Res=='H' && HttpHeadCnt==0)HttpHeadCnt++; else if(Res=='T' && HttpHeadCnt==1)HttpHeadCnt++; else if(Res=='T' && HttpHeadCnt==2)HttpHeadCnt++; else if(Res=='P' && HttpHeadCnt==3)HttpHeadCnt++; else if(Res=='/' && HttpHeadCnt==4)HttpHeadCnt++; else if(Res=='1' && HttpHeadCnt==5)HttpHeadCnt++; else if(Res=='.' && HttpHeadCnt==6)HttpHeadCnt++; else if(Res=='1' && HttpHeadCnt==7)HttpHeadCnt++; else if(Res==' ' && HttpHeadCnt==8)HttpHeadCnt++; else if(Res=='2' && HttpHeadCnt==9)HttpHeadCnt++; else if(Res=='0' && HttpHeadCnt==10)HttpHeadCnt++; else if(Res=='0' && HttpHeadCnt==11)HttpHeadCnt++; else if(Res==' ' && HttpHeadCnt==12)HttpHeadCnt++; else if(Res=='O' && HttpHeadCnt==13)HttpHeadCnt++; else if(Res=='K' && HttpHeadCnt==14){HttpHeadOK = 1;HttpHeadCnt=0;HttpDataLength=0;} else { HttpHeadCnt=0; } } #ifdef UserContentLength //Content-Length: XXXXXXXX if(HttpHeadOK && !HttpDataLengthOK)//获取http发过来的数据个数 { if(Res=='-' && HttpHeadCnt==0) HttpHeadCnt++; else if(Res=='L' && HttpHeadCnt==1)HttpHeadCnt++; else if(Res=='e' && HttpHeadCnt==2)HttpHeadCnt++; else if(Res=='n' && HttpHeadCnt==3)HttpHeadCnt++; else if(Res=='g' && HttpHeadCnt==4)HttpHeadCnt++; else if(Res=='t' && HttpHeadCnt==5)HttpHeadCnt++; else if(Res=='h' && HttpHeadCnt==6)HttpHeadCnt++; else if(Res==':' && HttpHeadCnt==7)HttpHeadCnt++; else if(Res==' ' && HttpHeadCnt==8)HttpHeadCnt++; else if(HttpHeadCnt>=9 && HttpHeadCnt<=16 )//最大99999999个字节. 16:99999999 17:999999999 18:9999999999 { if(Res!=0x0D) { HttpDataLength = HttpDataLength*10 + Res - '0'; HttpHeadCnt++; } else { HttpDataLengthOK = 1; HttpHeadCnt = 0; } } else { HttpHeadCnt = 0; } } if(HttpHeadOK && HttpDataLengthOK && HttpDataLength && !HttpHeadEndOK) #else if(HttpHeadOK && !HttpHeadEndOK) #endif {//0D 0A 0D 0A if(Res==0x0D && HttpHeadCnt==0)HttpHeadCnt++; else if(Res==0x0A && HttpHeadCnt==1)HttpHeadCnt++; else if(Res==0x0D && HttpHeadCnt==2)HttpHeadCnt++; else if(Res==0x0A && HttpHeadCnt==3){HttpHeadEndOK = 1;} else HttpHeadCnt = 0; } if(HttpHeadEndOK == 1)//http数据的head已经过去,后面的是真实数据 { HttpHeadEndOK=0; HttpHeadCnt = 0; HttpDataLengthOK=0; HttpDataStartFlage=1; } //解析http数据-------------------------------end } }
然后呢 有的Web服务器 http 还会返回本次传输文件的大小
我的升级程序,也写了使用这个变量
如果大家使用这个判断,则define下
#define UserContentLength
现在看细节处理2
要想让单片机执行升级,有两种途径
1.BootLoader程序里面
2.用户程序里面
先说一下我的原则
所有的处理升级由 BootLoader搞定,APP用户程序只是获取升级的状态,清除升级状态,写入升级标志
在BootLoader程序里面我推荐这样写
用一个按钮控制升级
按下按钮超过3S,指示灯快闪,快闪3S后,写入升级标志,清除版本号,重启
BootLoader判断有升级标志,则获取 版本号,然后获取程序文件,然后就升级了
while(1) { IAPLoadAPPProgram();//没有更新的状态下加载用户程序 IAPDownloadTimeoutFunction();//程序下载超时,超时重启; IAPMainTimeoutFunction();//整体运行超时,超时重启; IAPWriteData();//把接收到的程序文件写入Flash if(IAPStructValue.UpdateFlage)//有升级标志 { // 通过自己的方式获取 版本号数据 // 假设获取的版本号数据存储在了 Readbuff数组 // 然后交给这个函数 IAPVersionDispose } // if(通过自己的方式获取到了版本号信息) // { // IAPVersionDispose(Readbuff); // } Key1Function();//按键检测 if(Key1Value[3] == 1)//按键按下 { if(Key1Value[5]>=3000)//按下时间大于3S { //指示灯快闪,松开按钮 for(i=0;i<30;i++) { delay_ms(100); PCout(13) = ~ PCout(13); } sprintf(IAPStructValue.VersionDevice,"%s","00000000000000000000"); IAPSetUpdateVersionDevice(IAPStructValue.VersionDevice);//设备版本初始值 IAPSetUpdateFlage();//设置更新标志 __disable_fault_irq(); NVIC_SystemReset();//重启 } } }
虽然我的升级模板可以保证可靠的把程序写入Flash并且如果检测有问题则自动切换到上一份程序运行,
但是需要避免另一件事情(用户程序本身执行了一段时间以后出问题了.....)
结果造成了不停的重启....
解决方案是利用那个按钮
我一直按着那个按钮.
在进入BootLoader程序的时候,我检测下那个按钮是不是按下了,如果按下了就不加载用户程序了
那么过了 3S以后便会执行,指示灯快闪,快闪3S后,写入升级标志,清除版本号,重启
然后重新升级程序
当然大家也可以这样写,按下3S是 切换程序 按下10S是 写入升级标志,清除版本号,重启
切换执行程序: IAPSetUpdateChangeProgram();
然后重启即可
升级校验放到下一节,APP用户程序制作里面