![demi的头像 demi的头像](https://cdn.eetrend.com/files/styles/picture200/public/pictures/picture-600-1663388563.jpg?itok=FUbcd7Ft)
STM32等单片机是可编程处理器,内部运行着我们编写的程序,而把我们编写的程序“下载”到单片机中,方法有两种:
一、使用烧写器,如jlink,stlink,串口下载(需要配置boot0,boot1)。
二、通过IAP实现一个在线更新功能。
对于很多使用单片机作为主要处理器的电子产品,如遇到需要替换芯片内部程序以满足需求的情况,通常的解决办法是寄回该产品然后通过烧写器直接替换程序。但这样做无疑会增加相关的成本。所以很多的电子产品都会实现一个能远程更新(通过网络更新芯片程序,如手机更新操作系统等)或者自主更新程序(通过U盘,SD卡等方式更新,芯片读取并识别这些外部存储器存放的程序,并读取到自身内部空间中)。
首先简单说一下STM32等单片机,程序的存储位置是内部Flash空间,查看STM32F1参考手册等相关资料可以得知程序存放的起始地址为0x08000000(不同的单片机这个可能会有所不同)。
实现IAP功能的基本思路:划分芯片内部的Flash空间,分别存放不同的程序实现不同的功能。实现一个简单的跳转函数,使我们能控制芯片什么时候运行什么程序。通常划分的做法是:Bootloader+APP+APPBackup。
先介绍一下跳转函数,先看代码:
//定义一个函数类型:返回类型是void,函数参数是void typedef void (*iapfun)(void); //声明jump2app函数的类型 iapfun jump2app; // //appxaddr(需要跳转的地址) // void iap_load_app(u32 appxaddr) { //判断栈顶地址是否合法 if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) { //指定程序跳转的位置 jump2app=(iapfun)*(vu32*)(appxaddr+4); MSR_MSP(*(vu32*)appxaddr); //直接跳转,运行用户指定的程序。 jump2app(); }else { printf("error....\r\n"); } }
介绍一下Flash空间划分及对应存放的程序:
①:0x08000000 - 0x0800FFFF :共64Kb,存放Bootloader程序,用于判断是否需要更新及负责获取更新的数据并进行Flash操作写入数据。
②:0x08010000 - 0x0802FFFF :共128Kb,存放APP程序,即用户程序,会获得实际运行权的程序。
③:0x08030000 - 0x0804FFFF :共128Kb,作为IAP缓存区,Bootloader在更新过程中获取到的数据会先写入到这部分Flash空间,等待所有数据获取完成后,通过Flash搬移操作,把这部分Flash上的数据复制到第②部分的存储空间中。Bootloader不直接写第②部分的空间是为了避免在更新过程中出现的意外情况,如电量耗尽等,损坏了原来在芯片中的可运行程序。即确保芯片至少有一个可运行的程序,防止设备“变砖”。
④:0x08050000 - 0x0806FFFF:共128Kb,作为出厂程序备份区,针对更新出错等意外情况,提供一种解决办法能让设备恢复原来的状态。
⑤:0x08070000 - 0x0807FFFF:共64Kb,作为用户重要数据存储区。
运行流程:系统启动后,运行Bootloader程序,通过读取相关存储标志,如果需要更新,启动数据获取-转换-写Flash-跳转的更新流程。如果不需要更新,则直接跳转至用户程序:iap_load_app(0x08010000);
更新流程:程序数据获取-数据转换-写入Flash,循环直到数据全部写入完成。由于可能会出现需要更新的程序(目标程序)比较大,多达几十k上百k,因此要求Bootloader一次读取完全部程序数据是不现实的。更为可靠的做法是每次读入一定数量的程序数据,如2k等。
由于程序数据获取的方式太多,有串口输入,网络请求,读取外部存储器等方式,这里就不再介绍数据获取部分,无论是哪种方式实现的,其本质都是一样的,只是把需要更新的程序通过某一种方式让处理器能获取到。
顺带提一下,目标程序的文件格式是.bin文件(编译选项加入参数,见下图),且在编译前指定好了相关的Flash位置(可直接在target中设置)和中断向量偏移位置(通过修改SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;)。如按照上述的Flash分区设置,这里VECT_TAB_OFFSET的值应为0x10000。
![](http://mcu.eetrend.com/files/2021-01/wen_zhang_/100061187-120192-001.png)
假设我们的更新程序的前面2k数据已经获取完成了,保存在了u8 appbuf[1024*2+1]的数组中,我们需要把这些数据写入到内存中,但由于ST提供的Flash操作函数要求的是半字写入,即u16类型,所以这里我们获取到的数据还需要处理一下,转换成u16类型。
//不检查的写入 //WriteAddr:起始地址 //pBuffer:数据指针 //NumToWrite:半字(16位)数 void STMFLASH_Write_NoCheck(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite) { u16 i; for(i=0;i < NumToWrite;i++) { FLASH_ProgramHalfWord(WriteAddr,pBuffer[i]); WriteAddr+=2;//地址增加2. } } //从指定地址开始写入指定长度的数据 //WriteAddr:起始地址(此地址必须为2的倍数!!) //pBuffer:数据指针 //NumToWrite:半字(16位)数(就是要写入的16位数据的个数.) #if STM32_FLASH_SIZE<256 #define STM_SECTOR_SIZE 1024 //字节 #else #define STM_SECTOR_SIZE 2048 #endif u16 STMFLASH_BUF[STM_SECTOR_SIZE/2];//最多是2K字节 void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite) { u32 secpos; //扇区地址 u16 secoff; //扇区内偏移地址(16位字计算) u16 secremain; //扇区内剩余地址(16位字计算) u16 i; u32 offaddr; //去掉0X08000000后的地址 if(WriteAddr < STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址 FLASH_Unlock(); //解锁 offaddr=WriteAddr-STM32_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+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容 // printf("\nwrite flash\n"); for(i=0;i < secremain;i++)//校验数据 { if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除 } if(i < secremain)//需要擦除 { FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区 for(i=0;i< secremain;i++)//复制 { STMFLASH_BUF[i+secoff]=pBuffer[i]; } STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_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;//下一个扇区可以写完了 } }; FLASH_Lock();//上锁 } #endif // //用户接口程序 //实现写入用户获取到的程序数据 // u32 iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize) { u16 t; u16 i=0; u16 temp; u32 fwaddr=appxaddr; u8 *dfu=appbuf; for(t=0;t<appsize;t+=2) { temp=(u16)dfu[1]<<8; temp+=(u16)dfu[0]; dfu+=2; iapbuf[i++]=temp; } STMFLASH_Write(fwaddr,iapbuf,i); return fwaddr+appsize; }
声明:以上贴出的代码片出自正点原子提供的例程中。
调用 iap_write_appbin(0x08030000+x,appbuf, 1024*2) 。即可完成 转换-写入的操作流程。其中x为已经写入的大小。
由于每个程序大小都是不固定的,因此会出现获取最后一帧数据的时候,数据量不足2k的情况,这时候需要对最后的这一部分做处理,只写入实际获取到的长度,而不能直接写入2k的数据。
全部数据获取完成后,由于之前写入的地址是0x08030000,而这个地址不是我们设置的跳转地址,它在这里只是起到一个存储的功能,不会获得实际的程序运行权,所以,我们还需要实现一个功能,把0x08030000开始的128k数据复制到0x08010000开始的存储空间里。这里的实现方法可以参考我们用烧写器烧录的流程:擦除-写入-校验。代码片在这里就不再贴出了,可以照这个思路自己实现一下。
到这里整个在线更新的流程差不多就走完了,可以在清除更新标志后,直接跳转到用户程序区继续执行,也可以通过主动软件复位功能重启系统,然后让Bootloader判断跳转。
(完)2020.01.21
版权声明:本文为CSDN博主(weymin)原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/u013053268/article/details/104058622