STM32固件升级设计——内部FLASH模拟U盘升级固件_stm32如何将程序更新到内部flash
目录
一、功能描述
1、BootLoader部分:
2、APP部分:
二、BootLoader程序制作
1、分区定义
2、 主函数
3、配置USB
4、配置fatfs文件系统
5、程序跳转
三、APP程序制作
四、工程配置(默认KEIL5)
五、运行测试
结束语
概述
IAP(In Application Programming)即在应用中编程,允许在应用程序运行时更新或切换固件。STM32通过修改MSP(主堆栈指针)和PC(程序计数器)实现从不同地址启动,包括Flash或RAM地址。默认情况下,嵌入式程序以连续二进制形式烧录到STM32的可寻址Flash区域。若Flash容量足够存储多个完整程序,每个程序独立且完整,上电后可通过修改MSP值选择不同程序入口,从而实现多固件切换或升级。
BootLoader(引导加载程序)是嵌入式系统或计算机启动时运行的一段小型程序,负责初始化硬件、加载操作系统内核并将其控制权移交。它是系统从关机状态到操作系统完全运行之间的桥梁。
所以,固件升级的基本思路是将stm32的flash划分为若干个区域,其中包括BootLoader区域和APP区等,将各自的程序写到对应的flash区域里。
一、功能描述
使用STM32的USB总线和内部FLASH实现USB模拟U盘升级程序。将FLASH分为4个部分,最后的DOWNLOAD区域用作模拟U盘存储固件。
分区介绍:
本文使用stm32f103vet6,flash是512k,sector是1k,BootLoader整个代码编译下来有23K左右,所以使用0x08000000~0x00007FFF,SETTING主要存放升级标志位,使用0x08008000~0x00008FFF,剩下的FLASH将分为两个部分都用来存放代码,APP使用0x08009000~0x080447FF,DOWNLOAD使用0x08044800~0x0807FFFF。(如果想更大化地利用flash,可以不要setting区域,具体看自己如何写了)
1、BootLoader部分:
运行程序时首先从SETTING区域读取升级标志位,如果需要升级就进入识别U盘程序,否则就直接跳转到APP。上电长按KEY1并复位,电脑上即可模拟出U盘,识别到U盘后复制固件bin文件到U盘,然后将内置FLASH里的fatfs文件系统的升级文件拷贝到APP起始地址,即可实现升级程序,具体请查看本文源码。
tips:触发模拟U盘功能不一定要按键才能触发,例如上电前插入USB线也可以触发,工程已预留代码,只要注释掉按键代码即可。
2、APP部分:
该部分只需要设置中断向量跳转指针就行,如果想通过串口等下发升级标志位,也可以设置SETTING区域后复位进入BootLoader升级。
二、BootLoader程序制作
需要包含USB Device中的Mass_Strorage和内部FLASH以及fatfs文件系统的驱动代码。(这部分是需要仔细研究做好的,我是根据正点原子和野火的教程移植的,具体流程不做了)
1、分区定义
#define FLASH_SECTOR_SIZE 1024 //MCU sector size#define FLASH_SECTOR_NUM 512 // 512K#define FLASH_START_ADDR ((uint32_t)0x8000000)#define FLASH_END_ADDR ((uint32_t)(0x8000000 + FLASH_SECTOR_NUM * FLASH_SECTOR_SIZE))#define BOOT_SECTOR_ADDR 0x08000000 // BOOT sector start address#define BOOT_SECTOR_SIZE 0x8000#define SETTING_SECTOR_ADDR 0x08008000 // APP设置的boot升级标志位#define SETTING_SECTOR_SIZE 0x1000#define APP_SECTOR_ADDR 0x08009000 // APP sector start address #define APP_SECTOR_SIZE 0x3B800#define DOWNLOAD_SECTOR_ADDR 0x08044800 // Download sector start address#define DOWNLOAD_SECTOR_SIZE 0x3B800 // Download sector size #define APP_ERASE_SECTORS (APP_SECTOR_SIZE / FLASH_SECTOR_SIZE) typedef enum {NONE = 0,START_PROGRAM, //进入APP主程序或者有更新就执行更新UPDATE_PROGRAM, //进入更新UPDATE_SUCCESS //更新成功写标志位}Update_Process; //更新状态
2、 主函数
这部分包含了升级的所有状态,主要思路是判断firmware.bin文件是否存在,存在就执行升级。该框架可以说对比上一篇文章是完全不变的,具体看代码。
Update_Process bootupdate_process;void devflash_update(void) {u8 file_buffer[1024]={0}; uint32_t flash_addr = APP_SECTOR_ADDR;unsigned long total_bytes_read = 0;UINT bytes_read;fres=f_mount(&fs,\"2:\",1); //挂载FLASH.if(fres==FR_OK)//FLASH磁盘,FAT文件系统正常{printf(\"Flash disk OK!\\r\\n\");}// 判断flash根目录下是否有firmware.bin文件fres = f_stat(\"2:/firmware.bin\", &fno);if(fres == FR_OK){printf(\"2:/firmware.bin”文件信息:\\n\");printf(\"》文件大小: %ld(字节)\\n\", fno.fsize);iap_flash_erase(flash_addr, (fno.fsize/FLASH_SECTOR_SIZE)+1);}else{printf(\"firmware.bin not found!\\r\\n\");}if(fres == FR_OK) {// 文件存在printf(\"firmware.bin found, size: %lu bytes\\r\\n\", fno.fsize);printf(\"开始更新固件...\\r\\n\");// 打开固件文件fres = f_open(&firmware_file, \"2:/firmware.bin\", FA_READ);if(fres == FR_OK) {// 循环读取文件内容while (total_bytes_read < fno.fsize){// 读取数据块到缓冲区fres = f_read(&firmware_file, file_buffer, 1024, &bytes_read);if (fres != FR_OK || bytes_read == 0){// 读取出错或到达文件末尾printf(\"读取文件失败或文件已结束,错误码: %d\\r\\n\", fres);break;}// 写入到FLASHprintf(\"正在写入地址 0x%08X,大小: %u 字节\\r\\n\", flash_addr, bytes_read);iap_write_appbin(flash_addr, file_buffer, bytes_read);// 更新计数器和地址total_bytes_read += bytes_read;flash_addr += bytes_read;// 显示进度printf(\"更新进度: %lu/%lu bytes\\r\\n\", total_bytes_read, fno.fsize);}// 关闭文件f_close(&firmware_file);if (total_bytes_read == fno.fsize) {printf(\"固件更新完成! 共写入 %lu 字节\\r\\n\", total_bytes_read);//固件更新完成后删除firmware.bin文件fres = f_unlink(\"2:/firmware.bin\");if (fres == FR_OK) {printf(\"firmware.bin文件删除成功\\r\\n\");} else {printf(\"删除firmware.bin文件失败,错误码: %d\\r\\n\", fres);}} else {printf(\"固件更新未完成! 已写入 %lu/%lu 字节\\r\\n\", total_bytes_read, fno.fsize);}}}else {// 文件不存在printf(\"firmware.bin not found!\\r\\n\");}}static void iap_process(void){uint8_t offline_cnt=0;uint8_t tct=0;uint8_t USB_STA;uint8_t Device_STA; // 定义最大重试次数和每次等待间隔const uint8_t max_retries = 5;const uint8_t wait_interval_ms = 100;uint8_t retry_count = 0;switch (bootupdate_process) {case NONE:break;case START_PROGRAM:spiflash_update();printf(\"start app...\\r\\n\");delay_ms(50);if ((((*(vu32*)(APP_SECTOR_ADDR+4))&0xFF000000)==0x08000000)&&(!iap_load_app(APP_SECTOR_ADDR))) {printf(\"no program\\r\\n\");delay_ms(1000);}printf(\"start app failed\\r\\n\");break;case UPDATE_PROGRAM:MAL_Init(0);Max_Lun=0;USB_Interrupts_Config();/*设置USB时钟为48M*/Set_USBClock();USB_Init();//while (bDeviceState != CONFIGURED); //等待配置完成// 等待配置完成,最多重试 5 次 while (bDeviceState != CONFIGURED && retry_count 10)bDeviceState=UNCONNECTED;//2s内没收到在线标记,代表USB被拔出了}USB_STATUS_REG=0;}}break;case UPDATE_SUCCESS:bootupdate_process=START_PROGRAM;write_setting_boot_state(bootupdate_process);NVIC_SystemReset();break;default:break;}}int main(void){USART_Config();LED_GPIO_Config();printf(\"\\r\\n 使用指南者底板时 左上角排针位置 不要将PC0盖有跳帽 防止影响PC0做SPIFLASH片选脚 \\r\\n\");bootupdate_process=(Update_Process)read_setting_boot_state();if(Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == 1){bootupdate_process=UPDATE_PROGRAM;}while (1){iap_process();}}
3、配置USB
这部分根据野火代码移植而来,usb硬件配置自己看代码理解就好,一般没什么问题,根据上面的分区,读写内部FLASH时需要偏移0x08044800。U盘底层读写函数需要特别注意,如果U盘出bug,大概率是以下几个函数有误。
tips:在写函数时不能全部擦除一个扇区,需要把不需要更改的数重新写上,我就是一直卡在这一步,自己每次都是擦除一个扇区,再写一个扇区,然后永远都格式化不了U盘,但是我换到N32的硬件环境上又能格式化U盘,使用正点原子的STMFLASH_Write和STMFLASH_Read就没问题了,这问题就。。。以后有时间再解决了。
uint16_t MAL_Write(uint8_t lun, uint64_t Memory_Offset, uint32_t *Writebuff, uint16_t Transfer_Length){switch (lun)//这里,根据lun的值确定所要操作的磁盘{case 0: //磁盘0为 DEV FLASH盘STMFLASH_Write(DOWNLOAD_SECTOR_ADDR + Memory_Offset,(u16*)Writebuff,Transfer_Length/2);break; case 1://磁盘1为SD卡 //STA=SD_WriteDisk((u8*)Writebuff, Memory_Offset>>9, Transfer_Length>>9); break;default:return MAL_FAIL;}return MAL_OK; }uint16_t MAL_Read(uint8_t lun, uint64_t Memory_Offset, uint32_t *Readbuff, uint16_t Transfer_Length){switch (lun)//这里,根据lun的值确定所要操作的磁盘{case 0://磁盘0为 DEV FLASH盘 STMFLASH_Read(DOWNLOAD_SECTOR_ADDR + Memory_Offset,(u16*)Readbuff,Transfer_Length/2);break; case 1://磁盘1为SD卡 //STA=SD_ReadDisk((u8*)Readbuff, Memory_Offset>>9, Transfer_Length>>9); break;default:return MAL_FAIL;}return MAL_OK;}uint16_t MAL_GetStatus (uint8_t lun){ switch(lun) {case 0:Mass_Block_Size[0] =0x400;//设置FLASH的操作扇区大小为1024Mass_Block_Count[0]=DOWNLOAD_SECTOR_SIZE/0x400; //238Mass_Memory_Size[0]=DOWNLOAD_SECTOR_SIZE;//总字节 return MAL_OK;case 1: return MAL_OK;default:return MAL_FAIL; } }
4、配置fatfs文件系统
这部分也是根据野火代码移植而来,新增了一个卷标2,需要特别注意扇区大小和数量需要和USB配置的一样。
#define SD_CARD 0 //SD卡,卷标为0#define EX_FLASH 1//外部flash,卷标为1#define DEV_FLASH 2 //内部flash,卷标为2//读扇区DRESULT disk_read (BYTE pdrv,/* Physical drive nmuber to identify the drive */BYTE *buff,/* Data buffer to store read data */DWORD sector,/* Sector address in LBA */UINT count/* Number of sectors to read */){DRESULT status = RES_PARERR; switch(pdrv){case SD_CARD://SD卡break;case EX_FLASH://外部flashbreak;case DEV_FLASH: for(;count>0;count--){STMFLASH_Read(DOWNLOAD_SECTOR_ADDR + sector*1024,(u16*)buff,1024/2);sector++;buff+=1024;}status = RES_OK;break;default:status = RES_PARERR; }return status;}//写扇区DRESULT disk_write (BYTE pdrv,/* Physical drive nmuber to identify the drive */const BYTE *buff,/* Data to be written */DWORD sector,/* Sector address in LBA */UINT count/* Number of sectors to write */){uint32_t write_addr; DRESULT status = RES_PARERR;if (!count) {return RES_PARERR;/* Check parameter */}switch(pdrv){case SD_CARD://SD卡break;case EX_FLASH://外部flashbreak;case DEV_FLASH: for(;count>0;count--)//写函数不能全部擦除一个扇区,需要把不需要更改的数重新写上{STMFLASH_Write(DOWNLOAD_SECTOR_ADDR + sector*1024,(u16*)buff,1024/2); sector++;buff+=1024;}status = RES_OK;break;default:status = RES_PARERR; } return status;}//其他表参数的获得DRESULT disk_ioctl (BYTE pdrv,/* Physical drive nmuber (0..) */BYTE cmd,/* Control code */void *buff/* Buffer to send/receive control data */){DRESULT status = RES_PARERR;switch (pdrv) {case SD_CARD:/* SD CARD */status = RES_OK;break;case EX_FLASH:break;case DEV_FLASH: switch(cmd){case CTRL_SYNC:status = RES_OK; break; /* 扇区大小 */case GET_SECTOR_SIZE:*(WORD*)buff = 1024;status = RES_OK;break; /* 同时擦除扇区个数 */case GET_BLOCK_SIZE:*(DWORD*)buff = 1;status = RES_OK;break; /* 扇区数量:119*2*1024/1024=238(KB) */case GET_SECTOR_COUNT: *(DWORD*)buff =119*2;status = RES_OK;break;default:status = RES_PARERR;break;}break;default:status = RES_PARERR;} return status;}
5、程序跳转
跳转这部分网上也很多,基本没什么区别,关于中断可能要注意一下,可能跳转之前,某些外设中断是开启的,跳转之后,中断产生了,但是APP代码中没有处理对应该中断的中断处理函数,所以就可能会直接死机。
tips:本文的开发环境只有64k ram,所以需要特别注意这里,虽然bootloader的ram不太可能会超。
if (((*(__IO uint32_t*)appxaddr) & 0x2FFF0000 ) == 0x20000000)
详细参考以下文章:
关于STM32单片机IAP升级中if(((*(__IO uint32_t*)ulAddr_App) & 0x2FFE0000) == 0x20000000)语句的理解-CSDN博客
uint8_t iap_load_app(u32 appxaddr){uint8_t i; uint32_t jump_addr; if (((*(__IO uint32_t*)appxaddr) & 0x2FFF0000 ) == 0x20000000) { jump_addr = *(__IO uint32_t*) (appxaddr + 4); jump2app = (iapfun)jump_addr; /* 关闭所有中断,清除所有中断挂起标志 */ for (i = 0; i ICER[i]=0xFFFFFFFF;NVIC->ICPR[i]=0xFFFFFFFF;} __set_MSP(*(__IO uint32_t*)appxaddr); jump2app(); return 1; } return 0;}
三、APP程序制作
这部分设置一下flash的偏移量就行。
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x9000);
四、工程配置(默认KEIL5)
BootLoader部分:0x08000000~0x08007FFF
APP部分:0x08009000~0x080447FF
五、运行测试
长按KEY1点击复位(断电重启)可以看到识别到U盘(H:),大小也正常,然后往里面复制一个固件firmware.bin(注意这里的固件名一定要是唯一的,不然程序识别不到),复制进去就会执行升级程序,可以看到打印信息显示固件更新成功,本文和上一篇文章不同的是每次都按照一个扇区1024byte来写和擦除。
结束语
以上内部FLASH模拟U盘升级固件功能已实现,这只是其中的一种升级方式,后面大家看到的也希望可以得到大家的指点。主要的USB库和fatfs库移植教程就不给出来了,网上很多,基本都能实现。
完整代码下载地址:内部FLASH模拟U盘升级固件资源-CSDN下载