STM32通过IAP实现固件升级的分析与示例

大部分MCU都可以通过IAP对片内flash进行读写来实现固件升级。

这里主要是STM32如何实现IAP升级。

不同内核的stm32方式可能略有不同。这里先说F1内核的IAP过程,以STM32F103C8T6为例。

一、片内FLASH读写

实现IAP,首先要实现片内FLASH读写

1、擦除程序区,调用库函数FLASH_ErasePage可以按页做擦除

int FlashErase(uint32_t addr)
{
	uint8_t retry_time;
  uint8_t i;
	retry_time = 200;
	
  FLASH_Unlock();
  for(i=0; i<55; i++)
  {
		FLASH_ErasePage((uint32_t)addr);
		while((FLASH->SR & FLASH_FLAG_EOP) == 0)
		{
			delay_ms(1);
			retry_time--;
			if(retry_time == 0)
			{
				return 1;
			}		
		}
		//标记清零
		FLASH->SR |= FLASH_FLAG_EOP;
    addr += 0x400;
		delay_ms(1);
  }
  FLASH_Lock();
	return 0;
}

2、读片内FLASH

直接指针读指定FLASH地址就可以了

#define PARA_START_ADDR1     0x0800f800 	//参数首地址


uint8_t *src;
src = (uint8_t *)PARA_START_ADDR1;
printf("flash data = %x\r\n",*(src));

需特别注意,字节的存储顺序,默认应该是小端的。比如一个2字节的变量,存入0xAA55,则*(src)取到的是0x55,,*(src+1)取到的才是0xAA

FALSH读取可以用来检查写入的程序是否正确或者用来检查一些保存的参数

3、写入片内FLASH

//保存配置参数
void Save_Para(uint32_t addr)
{
        uint8_t *s;
        uint16_t i,data,len
    	uint8_t retry_time;

        s = (uint8_t *)&System_Para;    //要写入的数据地址

		FLASH_Unlock();
		FLASH_ErasePage(addr);
		retry_time = 200;
		while((FLASH->SR & FLASH_FLAG_EOP) == 0)
		{
			delay_ms(1);
			retry_time--;
			if(retry_time == 0)
			{
				break;
			}		
		}	
		//标记清零
		FLASH->SR |= FLASH_FLAG_EOP;	
		
		for(i=0; (i+1)<=len; i+=2)
		{
			data = *(uint16_t *)(s+i);
			FLASH_ProgramHalfWord(addr+i,data);
			retry_time = 10;
			while((FLASH->SR & FLASH_FLAG_EOP) == 0)
			{
				delay_ms(1);
				retry_time--;
				if(retry_time == 0)
				{
					break;
				}		
			}		
			//标记清零
			FLASH->SR |= FLASH_FLAG_EOP;		
		}
		if(i == len)
		{
			data = 0xff00+*(s+i);//ff00
			FLASH_ProgramHalfWord(addr+i,data);
			retry_time = 10;
			while((FLASH->SR & FLASH_FLAG_EOP) == 0)
			{
				delay_ms(1);
				retry_time--;
				if(retry_time == 0)
				{
					break;
				}		
			}		
			//标记清零
			FLASH->SR |= FLASH_FLAG_EOP;		
		}
		//标记清零
		FLASH->SR |= FLASH_FLAG_EOP;
		FLASH_Lock();
}

写入片内FLASH之前一定要先擦除,擦除一次最少擦一页(page),写入则可以一次写word或者halfword(4字节或者2字节)

二、程序间跳转

知道如何读写片内FLASH,可以开始实现IAP了

1、首先需要确认程序的地址

STM32一般flash的起始地址是0x08000000

为了实现IAP,我们需要一个启动程序(boot)和一个主程序(app),所以需要搭建两个工程,这两个工程的程序地址是不同的

比如STM32F103C8T6的程序空间为64KB,我们为启动程序留4KB

则:

启动程序起始地址为0x08000000,程序空间为0x1000
主程序起始地址为0x08001000,程序空间为0xF000

这个参数在工程的这里设置

有时可以用片外Flash存储程序端,程序先写到片外,在升级过程中再读进来更新片内程序,这样需要有片外的存储器,当然也可以用片内存储。

比如F103C8T6,我们把0x08000000到0x08001000这4KB做启动程序

0x08001000到0x08008000这28KB做主程序

0x08008000至0x0800FFFF这32KB做程序和参数备份区

这样不用片外存储器也可以做IAP,只是程序空间会被浪费掉一半,而且STM32片内FLASH读写寿命不是很长。

2、启动程序设置跳转,启动程序中需要添加跳转到主程序的代码

#define PGM_START_ADDR  0x08001000   //程序起始地址

void Check_And_Jump(void)
{
	//跳转到主程序
	/* Test if user code is programmed starting from address "ApplicationAddress" */
	if (((*(__IO uint32_t*)PGM_START_ADDR) & 0x2FFE0000 ) == 0x20000000)
	{
		/* Jump to user application */
			JumpAddress = *(__IO uint32_t*) (PGM_START_ADDR + 4);
			Jump_To_Main = (pFunction) JumpAddress;
			/* Initialize user application's Stack Pointer */
			__set_MSP(*(__IO uint32_t*) PGM_START_ADDR);
			Jump_To_Main();
	}
}

3、主程序启动时设置中断向量偏移,加在MAIN函数开始的地方就行,偏移量就是启动程序的长度0x1000

int main(void)
{
	NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x1000);   		//中断向量转移

这样分别烧进去启动程序,主程序。设备上电后,就会先进入启动程序,随后跳到主程序中

三、固件升级

结合片内FLASH读写和程序间跳转,就可以实现片内FALSH升级了

1、首先需要主程序的BIN文件,需要配置这一项

C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o D:\stm32Work\bin\wifi.bin  D:\STM32F1\WifiBoard\OBJ\LED.axf

开始的参数为编译器位置,然后是你要写入的位置,最后是工程所在的axf文件位置。

配置好这一项,对主程序工程点击rebuild,就可以在前面指定的路径找到主程序的bin文件了

2、传输BIN文件

传输主程序的BIN文件可以通过本地传输,也可以是网络传输,具体看需求。简单测试可以考虑用TTL串口来传输。可以用通用的一些串口调试工具以HEX格式直传文件,也可以自己写一个上位机指定一些串口通讯规则。

(1)一般由于RAM有限,一次读写的缓冲区最好设小一点,分多次传完文件

(2)写进去之后,最好再读出来检查是否正确,因为确实存在写入失败的情况

//写入256字节
FlashWriteMultiData((pack_index)*256+start_pos, PACK_LEN, (uint8_t *)RS232_RX_BUF+4);

//将写入的字节再读回来,检查校验和
FlashReadData((pack_index)*256+start_pos,PACK_LEN,checkBuf);

(3)应当考虑加校验来验证单包和整包数据的完整性。因为升级一旦出问题,整个系统就无法恢复了,只能重新烧写。所以应该尽量小心,避免程序写入可能存在的错误

3、回到启动程序

主程序通过串口接收数据,写到片内FALSH后半段后,重启就可以回到启动程序。

//保存升级状态
SystemPara.IAP_Status = 0x0F;
SavePara(PARA_START_ADDR1);
//重启
delay_ms(500);
NVIC_SystemReset();

4、要有参数保存的机制

因为升级写完FALSH重启回到启动程序时,启动程序是不知道当前是不是处于升级过程中的,所以我们最少需要1个存在FALSH里的标志位来标识当前进程是正常上电还是升级重启,最好直接指定FALSH里的一片区域专门做参数存储的地方。

这样进入启动程序时,先判断当前所处的状态,再决定是直接跳转到主程序,还是开始程序刷写。

以下代码是直接从工程里取出来的,没有做修改,只能做做参考,我就不重新写一遍完整流程了

启动时,将保存在程序备份区的程序,刷到主程序所在位置:

uint8_t IAP_Write(void)
{
	uint16_t j;
	uint32_t data;
	uint32_t left_len,addr=0;
	static uint16_t check_sum = 0;
	static uint16_t flash_read_checknum;
	
	left_len = System_Para.Filesize-4;

	while(left_len)
	{
		if(left_len > 256)
		{
			Flash_Read_Data(addr,256,Prog_data);
			addr += 256;
			left_len = System_Para.Filesize-4-addr;
			for(j=0;j<256;j++)
			{
				check_sum += Prog_data[j];
			}
		}
		else
		{
			Flash_Read_Data(addr,left_len,Prog_data);
			
			for(j=0;j<left_len;j++)
			{
				check_sum += Prog_data[j];
			}
			left_len = 0;
		}
	}

	flash_read_checknum = Flash_Read_Byte(System_Para.Filesize-2);
	flash_read_checknum = (flash_read_checknum<<8) + Flash_Read_Byte(System_Para.Filesize-1);

	if(flash_read_checknum == check_sum)
	{
		//校验和正确
		Flash_Read_Data(0,256,Prog_data);
		if(((*(__IO uint32_t*)Prog_data) & 0x2FFE0000 ) != 0x20000000)
		{
			//程序错误,直接返回主程序
			return 10;
		}
	}
	else
	{
		//校验和错误,直接返回主程序
		return 10;
	}
	
	total_data = System_Para.Filesize;
	
	//擦除程序区,失败就返回0
	if(FlashErase((uint32_t)PGM_START_ADDR))   //清除程序空间
	{
		//FLASH烧写擦除过程中失败,不能直接返回主程序,因为主程序已经没有代码了
		return 0;
	}
	
	FLASH_Unlock();
	addr = 0;
	
	left_len = total_data;
	
	while(left_len)
	{
		if(left_len > 256)
		{
			Flash_Read_Data(addr,256,Prog_data);
			for(j=0;j<256;j+=4)
			{
				data = *((uint32_t *)(Prog_data+j));
				if(Inner_FLASH_Program(addr+PGM_START_ADDR+j,data) == 0)
				{
				  return 0;	//失败
				}
			}			
			addr += 256;
			left_len = total_data-addr;
		}
		else
		{
			Flash_Read_Data(addr,left_len,Prog_data);
			
			for(j=0;j<left_len;j+=4)
			{
				data = *((uint32_t *)(Prog_data+j));
				if(Inner_FLASH_Program(addr+PGM_START_ADDR+j,data) == 0)
				{
				  return 0;
				}
			}
			left_len = 0;			
		}
	}
	FLASH_Lock();
	return 10;
}

//片内程序读取
void Flash_Read_Data(uint32_t data_addr,uint32_t len,uint8_t *addr)
{
	uint16_t i;
	uint8_t *src;
	
	data_addr += 0x8008000;

	src = (uint8_t *)data_addr;
  for(i=0;i<len;i++)
  {
    *(addr+i) = *(src+i);
  }
}

void Data_Init(void)
{
	uint8_t *dst;
  uint8_t *src;
  uint16_t i;
	uint16_t checksum = 0;

  //读取系统参数
  dst = (uint8_t *)&System_Para;
	src = (uint8_t *)PARA_START_ADDR1;
  if((*(src) == 0x55)&&(*(src+1) == 0xAA))   //参数没溢出
  {
		for(i=0;i<sizeof(System_Para);i++)
		{
			checksum += *(src+i);
			*(dst+i) = *(src+i);
		}

		//校验正确
		if(checksum == (*(src+1023)<<8) + *(src+1022))    //校验成功
		{
			if(System_Para.IAP_Status == 0x0F)	//FTP下载完成
			{
				if((System_Para.Filesize>0x100) && (System_Para.Filesize<0x10000))
				{
					if(IAP_Write() == 0)
					{
						delay_ms(100);
						NVIC_SystemReset();
					}
				}
				System_Para.IAP_Status = 0x00;
				Save_Para(PARA_START_ADDR1);
				NVIC_SystemReset();
			}
		}
	}
	Check_And_Jump();
}

主程序,将升级程序写到程序备份区域:

void RS232_Rec_Deal(void)
{
	uint32_t i,j;
	uint8_t pack_type;
	uint16_t pack_index;
	uint8_t pack_check;
	uint8_t checksum;
	uint8_t * pos;
	uint8_t check_buf[256];
	uint8_t *addr;
	u8 dx;
	uint32_t start_pos = 0;
	

	if(RS232_RX_STA&0x8000)	//接收完成
	{
		if(RS232_RX_BUF[0] == 0xAA)	//串口升级
		{
            start_pos = 0x8008000;

			pack_type = RS232_RX_BUF[1];	//帧类型
			pack_index = ((uint16_t)RS232_RX_BUF[2]<<8)+RS232_RX_BUF[3];	//帧序号
			pack_check = RS232_RX_BUF[4+PACK_LEN];	//帧校验
			
			//算校验和
			checksum = 0;
			for(i=0;i<PACK_LEN+3;i++)
			{
				checksum += RS232_RX_BUF[1+i];
			}
			
			if(pack_type == UPDATE_START)	//启动帧
			{
				if(checksum == pack_check) //校验正确
				{
					Update_Sta.total_pack = ((uint16_t)RS232_RX_BUF[4]<<8)+RS232_RX_BUF[5];	//总包数
					Update_Sta.prog_check = ((uint16_t)RS232_RX_BUF[6]<<8)+RS232_RX_BUF[7];	//文件校验和
					memset(Update_Sta.pack_sta,0,256);
					FlashErase(start_pos);							//擦除片内
					Update_Rep(REP_STA,START_REC);
				}
				else
				{
					Update_Rep(REP_STA,DATA_ERR);
				}
			}
			else if(pack_type == UPDATE_DATA)	//数据帧
			{
				if(checksum == pack_check) //校验正确
				{
					//先写入,然后再读出来,检查是否正确
					Flash_Write_MultiData((pack_index)*256+start_pos, PACK_LEN, (uint8_t *)RS232_RX_BUF+4);
					Flash_Read_Data((pack_index)*256+start_pos,PACK_LEN,check_buf);
					for(i=0;i<PACK_LEN;i++)
					{
						if(RS232_RX_BUF[4+i] != check_buf[i])
						{
							break;
						}
					}
					if(i >= PACK_LEN)
					{
						Update_Sta.pack_sta[pack_index] = 1;
						Update_Rep(REP_REC,DATA_RECOK);	//接收成功

						if(pack_index == (Update_Sta.total_pack-1))	//最后一包
						{
							//中间漏包,则判定失败
							for(j=0;j<Update_Sta.total_pack;j++)
							{
								if(Update_Sta.pack_sta[j] == 0)
								{
									Update_Rep(REP_STA,DATA_ERR);
									RS232_RX_STA = 0;
									return;
								}
							}
							for(j=0;j<255;j++)
							{
							  if(check_buf[255-j] != 0)
								{
								  break;
								}
							}
							System_Para.Filesize = Update_Sta.total_pack*PACK_LEN-j;

							pos = (uint8_t *)start_pos;
							if(( *(pos+System_Para.Filesize-4) == ((TERMINAL_TYPE&0xff00)>>8)) \
								&&( *(pos+System_Para.Filesize-3) == (TERMINAL_TYPE&0xff)))
							{
								Update_Rep(REP_STA,UPDATA_OK);
								System_Para.IAP_Status = 0x0F;
								Save_Para(PARA_START_ADDR1);
								delay_ms(500);
								NVIC_SystemReset();
							}
							else
							{
							  Update_Rep(REP_STA,DATA_ERR);
							}
						}
					}
					else
					{
						Update_Rep(REP_REC,DATA_RECFAIL);
						Update_Rep(REP_STA,DATA_ERR);
					}
				}
				else
				{
					Update_Rep(REP_REC,DATA_RECFAIL);
				}
			}
		}
		RS232_RX_STA = 0;
	}
}	

参照以上流程,就基本可以实现用片内FLASH完成IAP升级了。如果用片外存储器,方法基本是一样的,就不多说了。

猜你喜欢

转载自blog.csdn.net/zhangfls/article/details/111685866
今日推荐