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下载

相关推荐
h13728697869几秒前
Type-C PD快充协议智能芯片S312L详解
嵌入式硬件
不想学习\??!2 小时前
STM32-外部中断
stm32·单片机·嵌入式硬件
不想学习\??!2 小时前
STM32-定时器
stm32·单片机·嵌入式硬件
LIN-JUN-WEI3 小时前
[ESP32]VSCODE+ESP-IDF环境搭建及blink例程尝试(win10 win11均配置成功)
c语言·开发语言·ide·vscode·单片机·学习·编辑器
LS_learner3 小时前
嵌入式系统中实现串口重定向
嵌入式硬件
景彡先生4 小时前
STM32中SPI协议详解
stm32·单片机·嵌入式硬件
趣多多代言人4 小时前
嵌入式面试八股文100题(二)
单片机·嵌入式硬件
Star Curry5 小时前
【新手小白的嵌入式学习之路】-STM32的学习_GPIO 8种模式学习心得
stm32·嵌入式硬件·学习
猫猫的小茶馆5 小时前
【STM32】ADC模数转换基本原理
stm32·单片机·嵌入式硬件·mcu·51单片机