STM32固件升级设计——内部FLASH模拟U盘升级固件

目录

一、功能描述

1、BootLoader部分:

2、APP部分:

二、BootLoader程序制作

1、分区定义

[2、 主函数](#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区域,具体看自己如何写了)

区域 起始地址 区域大小 功能
BOOT 0x08000000 0x00008000(32k) 存放BootLoader程序
SETTING 0x08008000 0x00001000(4k) 存放升级标志位/其它掉电不丢失标志位
APP 0x08009000 0x0003B800(238k) 存放产品主程序
DOWNLOAD 0x08044800 0x0003B800(238k) 存放待升级的程序(bin文件)
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、分区定义
cpp 复制代码
#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文件是否存在,存在就执行升级。该框架可以说对比上一篇文章是完全不变的,具体看代码。

cpp 复制代码
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;
				}
				// 写入到FLASH
				printf("正在写入地址 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 < max_retries) {
                delay_ms(wait_interval_ms);
                retry_count++;
            }
            // 若 5 次后仍未配置完成,进入 START_PROGRAM 状态
            if (bDeviceState != CONFIGURED) {
                printf("USB 配置失败,尝试 5 次后退出,进入 START_PROGRAM 状态。\r\n");
                bootupdate_process = START_PROGRAM;
                return;
            }
			while(1)
			{
				delay_ms(1);
				if(USB_STA!=USB_STATUS_REG)//状态改变了 
				{
					if(USB_STATUS_REG&0x01)//正在写
					{
						printf("USB Writing...\r\n");//提示USB正在写入数据	 
					}
					if(USB_STATUS_REG&0x02)//正在读
					{
						printf("USB Reading...\r\n");//提示USB正在读出数据  		 
					}		
					if(USBD_User_App() == FR_OK)  //检测到有对应的bin文件
					{
						bootupdate_process=UPDATE_SUCCESS;
						delay_ms(500);
						break;
					}
					if(USB_STATUS_REG&0x04)printf("USB Write Err\r\n");//提示写入错误	  
					if(USB_STATUS_REG&0x08)printf("USB Read Err\r\n");//提示读出错误 
					USB_STA=USB_STATUS_REG;//记录最后的状态
				}
				if(Device_STA!=bDeviceState) 
				{
					if(bDeviceState==CONFIGURED)
					{
						LED1_ON;//提示USB连接已经建立
					}
					else 
					{
						LED1_OFF;//提示USB被拔出了
					}
					Device_STA=bDeviceState;
				}
				tct++;
				if(tct==200)
				{
					tct=0;
					LED1_TOGGLE;//提示系统在运行
					if(USB_STATUS_REG&0x10)
					{
						LED1_ON;
						offline_cnt=0;//USB连接了,则清除offline计数器
						bDeviceState=CONFIGURED;
					}else//没有得到轮询 
					{
						LED1_OFF;
						offline_cnt++;  
						if(offline_cnt>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就没问题了,这问题就。。。以后有时间再解决了。

cpp 复制代码
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的操作扇区大小为1024
			Mass_Block_Count[0]=DOWNLOAD_SECTOR_SIZE/0x400;   //238
			Mass_Memory_Size[0]=DOWNLOAD_SECTOR_SIZE;	//总字节												  
			return MAL_OK;
		case 1:											  
			return MAL_OK;
		default:
			return MAL_FAIL;
    } 
}
4、配置fatfs文件系统

这部分也是根据野火代码移植而来,新增了一个卷标2,需要特别注意扇区大小和数量需要和USB配置的一样。

cpp 复制代码
#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://外部flash
			break;
		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://外部flash
			break;
		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博客

cpp 复制代码
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 < 8; i++)
		{
			NVIC->ICER[i]=0xFFFFFFFF;
			NVIC->ICPR[i]=0xFFFFFFFF;
		}	
        __set_MSP(*(__IO uint32_t*)appxaddr);  
        jump2app();
        return 1;
    }
    return 0;
}	

三、APP程序制作

这部分设置一下flash的偏移量就行。

cpp 复制代码
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下载

相关推荐
cjy_Somnr11 小时前
keil5报错显示stm32的SWDIO未连接不能烧录
stm32·单片机·嵌入式硬件
Lay_鑫辰12 小时前
西门子诊断-状态和错误位(“轴”工艺对象 V1...3)
服务器·网络·单片机·嵌入式硬件·自动化
无垠的广袤14 小时前
【工业树莓派 CM0 NANO 单板计算机】本地部署 EMQX
linux·python·嵌入式硬件·物联网·树莓派·emqx·工业物联网
雲烟16 小时前
嵌入式设备EMC安规检测参考
网络·单片机·嵌入式硬件
泽虞16 小时前
《STM32单片机开发》p7
笔记·stm32·单片机·嵌入式硬件
田甲16 小时前
【STM32】 数码管驱动
stm32·单片机·嵌入式硬件
up向上up17 小时前
基于51单片机垃圾箱自动分类加料机快递物流分拣器系统设计
单片机·嵌入式硬件·51单片机
纳祥科技1 天前
Switch快充方案,内置GaN,集成了多个独立芯片
单片机
单片机日志1 天前
【单片机毕业设计】【mcugc-mcu826】基于单片机的智能风扇系统设计
stm32·单片机·嵌入式硬件·毕业设计·智能家居·课程设计·电子信息
松涛和鸣1 天前
从零开始理解 C 语言函数指针与回调机制
linux·c语言·开发语言·嵌入式硬件·排序算法