STM32F1的IAP升级

IAP

1.内部flash相关知识:

        不同型号的 STM32,其 FLASH 容量也有所不同,分为小容量。中等容量,大容量产品,内部flash大于256k的的产品是大容量产品。

        对于我的大容量STM32F103RCT6来说,其被划分为 128 页,每页 2K 字节。注意,小容量和中容量产品则每页只有 1K 字节。

        1)读内部FLASH(标准库库函数)

        读取 FLASH 指定地址的半字的函数固件库并没有给出来,这里我们自己写的一个函数:

u16 STMFLASH_ReadHalfWord(u32 faddr)
{
    return *(vu16*)faddr;
}

//只要是擦除后的区域,读取没有那么多的要求,实际上就是取到对应地址,然后取内容即可。

        2)写内部FLASH(标准库库函数)

固件库提供了三个 FLASH 写函数

        32位写入 : FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);

32 位字节写入,其内部源码实际上是写入的两次 16 位数据。

        16位写入 :FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);

我们用这个16位,STM32 闪存的编程每次必须写入 16 位,即写入半字(halfworld)。

        8位写入 :FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);

写入 8位实际也是占用的两个地址了即2字节,跟写入 16 位基本上没啥区别。

 

        3)解锁和加锁内部FALSH(标准库库函数)

        在对内部FLASH进行写入操作时必须解锁,本质时在FLASH_KEYR 寄存器写入特定的序列(KEY1 和 KEY2)(KEY1=0X45670123,KEY2=0XCDEF89AB,stm32规定的),标准库函数以及帮我们写好这个解锁和加锁函数了,内部自动写KEY1和KEY2。

解锁:void FLASH_Unlock(void);

加锁:   void FLASH_Lock(void);

 

        4)获取内部FLASH状态(标准库库函数)

                                        FLASH_Status FLASH_GetStatus(void);

返回值代表其状态,类型如下:

typedef enum
{
FLASH_BUSY = 1,    //忙
FLASH_ERROR_PG,    //编程错误
FLASH_ERROR_WRP,   //写保护错误
FLASH_COMPLETE,    //操作完成
FLASH_TIMEOUT      //操作超时
}FLASH_Status;

         5)擦除内部FLASH函数 (标准库库函数)

擦除特定地址的页FLASH_Status FLASH_ErasePage(uint32_t Page_Address);

                                返回一个FLASH_Status类型的值来表示操作的结果

擦除所有的闪存页FLASH_Status FLASH_EraseAllPages(void);

                                返回一个FLASH_Status类型的值来表示操作的结果

擦除芯片的选项字节:FLASH_Status FLASH_EraseOptionBytes(void);

(一般用不到,因为选项字节通常用于配置芯片的特定功能,例如读写保护、电源管理设置、存储区域的访问权限等。FLASH_EraseOptionBytes 函数的作用就是擦除这些选项字节,以便重新配置芯片的特定设置。)

                                返回一个FLASH_Status类型的值来表示操作的结果

 

2.启动方式:

        STM32有三种启动方式:

0dba4ef2805341889f5143750975973b.png

        这里的0和1代表相对应的引脚接低高电平,我们采用主闪存存储器FLASH启动,也时默认启动方式。

        在映射地址中主存储器FLASH的起始地址是 0X08000000(参考官方手册中地址映射)。

 

3.主闪存(FLASH)的启动流程

        当产生复位,并且离开复位状态后, CM3 内核做的第一件事就是读取下列两个 32 位整数的值:

  1.         (1)从地址 0x0800 0000 处取出堆栈指针 MSP 的初始值(一般指向SRAM的映射地址),该值就是栈顶地址。
  2.         (2)从地址 0x0800 0004 处取出复位向量(一般指向FLASH映射地址区域内,即复位处理程序的入口地址,从而引导系统启动,复位处理程序通常包括初始化硬件、设置堆栈指针、跳转到主程序入口等操作)。
  3.         (3)在复位中断服务程序执行完之后,会跳转到我们的main 函数(死循环)。
    1.         (4)main 函数执行过程中,如果收到中断请求(发生重中断),此时 STM32 强制将 PC 指针指回中断向量表处,然后,根据中断源进入相应的中断服务程序在执行完中断服务程序以后,程序再次返回 main 函数执行。

7fcbb12fb0cd40bb9fc29fb7f804cc93.png

 

4.内存分配(芯片flash共256kb)

 

名称                大小                        映射flash地址范围                                作用

BootLoader   16k  0X4000           0X0800 0000 - 0X8003FFF                  引导程序,根据

APP1             120k 0x1E000      0X0800 4000 - 0X0820 1FFF               程序运行区域

APP2             120K 0X1E000     0X082 2000 - 0X0804 0000                 备份升级区域

APP1_FLAG   4Byte                  0X0802 1FFC - 0X0802 2000         app1标志位,更新完后清理

APP2_FALG   4Byte                  0X0803 FFFB - 0X0804 0000    app2标志位,判断是否有app升级

 

5.具体升级流程

7c628a5982a6410a8f69a2471fd94d65.png


首先我们需要3个工程,都保存在FLASH内部,使用STLINK下载时,直接修改下载地址即可。

        (1) BootLoader :  引导工程(分16kb的flash)有以下功能

                1)编写读写内部flash的相关功能代码

                2)判断APP2标志位是否为我们设置的更新标志,

                    有的化将APP2所在的区域(120k)写入APP1所在的区域(120k)(一定要记得先清除APP1区域的内容,否则无法写入,在 FLASH_SR 寄存器的 PGERR 位将得到一个警告)。

                    没有的话跳转到APP1去运行。

               3)将栈顶指针MSP偏移到APP1区域(汇编)

               4)利用函数指针跳转到APP1代码的首地址,即0X0800 4000

                ps:STlink烧录到0X0800 0000 , 复位后最先运行此程序,如右下图所示,魔法棒里更改

cb769e471656456eb94521256b691f8d.pngc358fca6b53a4e12a3916e74525df956.png

BootLoader   的main.c

#include "stm32f10x.h"
#include "stdio.h"
#include "led.h"
#include "delay.h"
#include "lcd_init.h"
#include "lcd.h"
#include "usart.h"
#include "key.h"
#include "stmflash.h"

/*****************************************************************
FLASH大小 256k

BOOTLOADER  16K  0X0800 0000 - 0X0800 3FFF		0X4000
APP1				120K 0X0800 4000 - 0X0820 1FFF 		0X1E000
APP2				120K 0X0802 2000 - 0X0804 0000		0X1E000
*******************************************************************/
#define FLASH_APP1_ADDR 0X08004000		 //app1起始地址
#define FLASH_APP2_ADDR 0X08022000		 //app2起始地址
#define APP1_FLAG_ADDR  (FLASH_APP2_ADDR-4) //app1更新标志位
#define APP2_FLAG_ADDR  (0X08040000-4) //是否有更新程序标志位
#define APP_SIZE 				(0X1E111/STM_SECTOR_SIZE)	//多少扇区,大容量一个扇区2k
#define APP_ADDR_SIZE   0X1E111
//定义一个类型,该类型为一个指向无参无返回值的函数的指针。
typedef  void (*iapfun)(void);				
iapfun jump2app; 					//定义一个函数指针 jump2app(进行函数的地址跳转)
uint16_t readBuff[STM_SECTOR_SIZE/2] = {0};



/*****************************************************************
 *函 数 名 称:MSR_MSP (汇编写的) 
 *函 数 功 能:设置栈顶地址,传入的参数地址会被认为是栈顶指针
 *函 数 形 参:addr:栈顶地址
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
__asm void MSR_MSP(u32 addr) 
{
	/*
		MSR :用于将数据写入到特殊寄存器中。
		MSP :主栈指针。
		r0 :在函数调用中,r0 被用作第一个参数的传递寄存器。当一个函数被调用时,
			第一个参数的值会被放置在 r0 中,然后传递给被调用的函数。
	*/
    MSR MSP, r0 	//从r0寄存器,即参数写入主栈指针		

    /*
        BX:分支交换(Branch and Exchange)指令。它可以实现程序的跳转控制,同时还能够切换处理器    
              的工作状态。
        r14:链接寄存器,在函数调用过程中,用于保存函数的返回地址。
    */
    BX r14    //返回到调用这个函数的地方继续执行程序。
}
/*****************************************************************
 *函 数 名 称:iap_load_app
 *函 数 功 能:跳转到应用程序段
 *函 数 形 参:appxaddr:用户代码起始地址.
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
iapfun jump2app; //设置一个函数指针,该指针指向一个无参无返回值的函数
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
	if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)	//检查栈顶地址是否合法,确保栈顶落在0x24000000-0x24FFFFFF 之间,刚好在stm32的RAM范围内
	{ 
		printf("栈顶地址是%x\r\n",*(vu32*)appxaddr);
		//将jump2app赋值为用户代码段的地址大小
		jump2app=(iapfun)*(vu32*)(appxaddr+4);		//用户代码区第二个字为程序开始地址(复位地址),		
		//
		MSR_MSP(*(vu32*)appxaddr);					//初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
				printf("即将退出bootloader,运行用户代码\r\n");
		Delay_nms(3000);
		jump2app();									//跳转到APP.
	}
}		 




/*****************************************************************
 *函 数 名 称:UserFlashAppRun
 *函 数 功 能:运行Flash中app1区域内的代码
 *函 数 形 参:无
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
void UserFlashAppRun(void)
{
	printf("开始执行FLASH中APP1区的用户代码!!\r\n");
	//判断里面有没有程序代码,有的话,从栈顶指针偏移4字节后是PC指针
	if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
	{	 
		iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码
	}else 
	{
		printf("APP程序加载失败!\r\n");   
	}									 
}


/*****************************************************************
 *函 数 名 称:UpdateFun
 *函 数 功 能:进行IAP升级
 *函 数 形 参:无
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
void UpdateFun(void)
{
	uint16_t app2FlagBuff[2] = {0xFFFF, 0xFFFF};
	printf("开始升级\r\n");
	//app对应的多少扇区,用app2全部覆盖app1
	for(uint16_t i=0 ; i<APP_SIZE ; i++)
	{
			printf("正在更新,已更新 %d 字节,%d K\r\n",i*2046,i*2);	//必须16位16位写入
		//app2的地址+偏移的扇区的地址       				   ,读取缓存区  ,一扇区半字个数 

			FLASH_WaitForLastOperation(1000);
			STMFLASH_Read(FLASH_APP2_ADDR+i*STM_SECTOR_SIZE, readBuff, STM_SECTOR_SIZE/2);
		
			FLASH_WaitForLastOperation(1000);
		//从这里开始卡住 
		//APP1的地址+偏移的扇区的地址									 ,读取缓存区		,一扇区半字个数
			STMFLASH_Write(FLASH_APP1_ADDR+i*STM_SECTOR_SIZE, readBuff, STM_SECTOR_SIZE/2);	
	}
		printf("固件库更新完成!\r\n");
	  //升级完后清除APP2和APP1标志位,(实际上只擦除APP2即可先擦除,在写入)
		STMFLASH_Erase(APP2_FLAG_ADDR,4);
		STMFLASH_Erase(APP1_FLAG_ADDR,4);
//	  //在app2的标志位地址写入2个半字即4个字节的标志位
//		STMFLASH_Write(APP2_FLAG_ADDR, app2FlagBuff, 2);
//		//同理在app1的标志位地址写入2个半字即4个字节的标志位
//		STMFLASH_Write(APP1_FLAG_ADDR , app2FlagBuff, 2);
	
	
		printf("即将运行更新后的代码!\r\n");
	//运行app1区域内更新完成的代码
		UserFlashAppRun();
}






int main(void)
{
	//中断优先级分组
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//2位(4)抢占,2位(4)相应。
	LED_Init();			//初始化灯
	LCD_Init();			//初始化lcd屏幕	
	USART_Config(); //串口初始化
	printf("你好世界\r\n");
	LCD_Fill(0,0,128,128,GRAY);//指定区域填充颜色
	KEY_Config();		//按键初始化
	EXTI_Config();	//按键中断初始化

//	printf("即将进入睡眠!\r\n");
//	Delay_nms(5000);	
//	printf("进入睡眠,并且关闭屏幕\r\n");
//	LCD_BLK_Clr();
		uint16_t app2FlagBuff[2] = {0};
		
	while(1)
	{
		//读取更新标志位
		STMFLASH_Read(APP2_FLAG_ADDR , app2FlagBuff , 2);
		printf("更新标志位是%x,%x",app2FlagBuff[0],app2FlagBuff[1]);
		if(app2FlagBuff[0] == 0XAAAA && app2FlagBuff[1] == 0XAAAA)
		{
				//说明有程序更新
			printf("监测到程序更新,即将更新程序\r\n");
			//擦除app1的代码,不擦就会卡住
			STMFLASH_Erase(FLASH_APP1_ADDR,APP_ADDR_SIZE);
			printf("已经擦除APP1区域数据!\r\n");
			Delay_nms(3000);
			UpdateFun();
		}		
		else
		{
				printf("没有新的程序,执行原有app \r\n");
				Delay_nms(3000);
				UserFlashAppRun();
		}
		Delay_nms(3000);
		printf("出错,即将退出bootloader\r\n");
		
		//APP1里由东西就不会到这里
		while(1)
		{
			printf("出错!\r\n");
		}
		
	}
	
}


BootLoader   的flash.c

#include "stmflash.h"

/*****************************************************************
 *函 数 名 称:STMFLASH_ReadHalfWord
 *函 数 功 能:读取指定地址的半字(16bit),没什么特殊的,直接取相应地址的内容即可
 *函 数 形 参:faddr 要读取的地址
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
u16 STMFLASH_ReadHalfWord (u32 faddr)
{
		//vu16 = volatile unsigned short
		return *(vu16 *)faddr;
}

/*****************************************************************
 *函 数 名 称:STMFLASH_Read
 *函 数 功 能:从指定地址开始读出指定长度的数据
 *函 数 形 参://ReadAddr:起始地址
							 //pBuffer:数据指针
							 //NumToWrite:半字(16位)数
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
void STMFLASH_Read(u32 ReadAddr, u16 *pBuffer, u16 NumToRead)   	
{
	u16 i;
	for(i=0; i<NumToRead; i++)
	{
		pBuffer[i] = STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.
		ReadAddr += 2;//偏移2个字节.	
	}
}


/*****************************************************************
 *函 数 名 称:STMFLASH_Write_NoCheck
 *函 数 功 能:不检查的写入
 *函 数 形 参://WriteAddr:起始地址
							 //pBuffer:数据指针
							 //NumToWrite:半字(16位)数   
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
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的整数倍,即16位,2字节
							 pBuffer    数据指针,即要写入的数据
							 NumToWrite 要写入的半字(16bit)的个数
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
u16 STMFLASH_BUF[STM_SECTOR_SIZE/2];//最多是2K字节,即2048/2个半字
void STMFLASH_Write(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite)	
{
	u32 sec_addr;			//扇区地址 
	u16 sec_offset;		//扇区内偏移地址(16bit,2字节,半字半字的算)
	u16 sec_remain;		//扇区内剩余地址(16bit,2字节,半字半字的算)
	u32 offset_addr;	//去掉0X0800 0000后的地址
	u16 i;
	//起始地址不在芯片的FLASH范围内 < 0x08000000 或者 > 0x08000000+FLASH大小
	if(WriteAddr < STM32_FLASH_BASE || (WriteAddr >= STM32_FLASH_BASE +1024 * STM32_FLASH_SIZE))
		return;
	
	FLASH_Unlock();						//解锁
	offset_addr = WriteAddr - STM32_FLASH_BASE;				//实际偏移的地址=要写入的地址 - flash起始地址
	sec_addr = offset_addr / STM_SECTOR_SIZE;					//扇区地址即第几个扇区 = 实际偏移的地址 / 扇区大小 2048(2k)
	sec_offset = (offset_addr % STM_SECTOR_SIZE)/2;		//扇区内偏移的地址 = 实际偏移地址 % 扇区大小/2 (2个字节位基本单位半字)
	sec_remain = STM_SECTOR_SIZE/2 -sec_offset;				//扇区内剩余地址 = 扇区大小/2 (2个字节位基本单位半字)
	//若写入字节不大于本扇区剩余字节
	if(NumToWrite <= sec_remain)
		sec_remain = NumToWrite;		//令剩余长度 = 写入的长度
	
	while(1)
	{
		//读取整个扇区内容			//要操作的扇区地址             //临时存入数据的数组  //半字个数
		STMFLASH_Read(sec_addr * STM_SECTOR_SIZE + STM32_FLASH_BASE , STMFLASH_BUF , STM_SECTOR_SIZE/2 );
		for(i=0 ; i < sec_remain ; i++)	//校验数据
		{
			if(STMFLASH_BUF[sec_offset+i] != 0xFFFF) 	//如果扇区内有一个半字里非初始化的0XFFFF,则擦除整个扇区 
				break;																	//因为擦除只能针对扇区或者整个芯片
		}
		if(i < sec_remain)	//条件成立说明扇区内有不为0xffff的半字,擦除整个扇区
		{
			FLASH_ErasePage(sec_offset * STM_SECTOR_SIZE + STM32_FLASH_SIZE);	//擦除整个扇区
			//将要写入的数据复制页内剩余长度的大小进入缓存区
			for(i=0 ; i<sec_remain ; i++)	
				STMFLASH_BUF[i+sec_offset] = pBuffer[i];
			//直接写入							写入的地址																		要写入的内容		写入的半字长度
			STMFLASH_Write_NoCheck(sec_addr * STM_SECTOR_SIZE + STM32_FLASH_BASE , STMFLASH_BUF , STM_SECTOR_SIZE/2);
		}
		else STMFLASH_Write_NoCheck(WriteAddr , pBuffer , sec_remain);//已经擦除,直接写入扇区剩余区间.
		
		if(NumToWrite==sec_remain)
			break;//写入结束了
		else	//写未结束
		{
			sec_addr++;								//扇区地址增1,即第几个扇区
			sec_offset = 0;						//偏移位置置0
			pBuffer += sec_remain;		//写的内容指针偏移
			WriteAddr += sec_remain;	//写地址偏移
			NumToWrite -= sec_remain;	//字节写入熟练 -= 写入扇区的剩余半字
			if(NumToWrite > (STM_SECTOR_SIZE/2))
				sec_remain = STM_SECTOR_SIZE/2;	//下一个扇区还是写不完
			else
				sec_remain = NumToWrite;				//下一个扇区可以写完
		}
	}
	FLASH_Lock();			//上锁
}


/*****************************************************************
 *函 数 名 称:STMFLASH_Erase
 *函 数 功 能:STMFLASH擦除函数,擦除任意大小,
 *函 数 形 参:startAddr :开始擦除的地址
							 eraseSize :擦除的大小
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
//startAddr地址必须是2K的整数倍
//numToErase 要擦除的内存大小,必须是2K的整数倍,即必须要从扇区首地址开始
uint8_t STMFLASH_Erase(u32 startAddr, u32 eraseSize) 
{
	uint16_t i=0;
	uint16_t len = eraseSize/STM_SECTOR_SIZE;
	if(startAddr<STM32_FLASH_BASE||(startAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))
		return -1;//非法地址
	if((startAddr % STM_SECTOR_SIZE))//非扇区首地址
		return -2;//非法地址
	FLASH_Unlock();//解锁
	for(i=0; i<len; i++) {
		FLASH_ErasePage(startAddr+i*STM_SECTOR_SIZE);//擦除这个扇区
	}
	FLASH_Lock();//上锁	
	return 0;
}


BootLoader   的flash.h

#ifndef __STM_FLASH_H
#define __STM_FLASH_H

#include "stm32f10x.h"

#define STM32_FLASH_SIZE 256          //本芯片内部flash容量有多少KB
#define STM_SECTOR_SIZE	2048          //大容量产品一个扇区2k
#define STM32_FLASH_BASE 0x08000000   //STM32 FLASH的起始地址

u16 STMFLASH_ReadHalfWord (u32 faddr);
void STMFLASH_Read(u32 ReadAddr, u16 *pBuffer, u16 NumToRead);
void STMFLASH_Write_NoCheck(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite) ;
void STMFLASH_Write(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite);
uint8_t STMFLASH_Erase(u32 startAddr, u32 eraseSize);

#endif

 

 

        (2) APP1 :  我们运行的区域(分120kb的flash)有以下功能

                  1)你要的功能,比如传感器什么的,即项目运行的程序,我以屏幕颜色来区分3个工程

                  2)偏移中断向量表起始地址(汇编)

                  3)擦除APP2区域代码,方便后续写入数据到APP2区域(因为不擦除就不能写)

                  4)通过串口接收APP2的.bin文件,并且写入APP2区域。

ps:STlink烧录到0X0800 4000 , 复位后BootLoader会跳转到这里,如右下图所示,魔法棒里更改

9e3b27a3279d46a2a918daf485d6464d.pnga04e8916a3e2473db54c6f599e87755b.png

APP1  的 mian.c

#include "stm32f10x.h"
#include "stdio.h"
#include "led.h"       //这是我的lcd初始化,我两个app的差别就在于lcd屏幕显示不同颜色背景
#include "delay.h"
#include "lcd_init.h"
#include "lcd.h"
#include "usart.h"
#include "key.h"
#include "stmflash.h"  //


extern uint32_t cnt;

int main(void)
{
		/*
		SCB:是系统控制块(System Control Block)的缩写,在许多微控制器中,它包含了各种系统级的控制和状态寄存器。
		VTOR:(Vector Table Offset Register)是向量表偏移寄存器。它用于指定中断向量表的起始地址。
		FLASH_BASE : 代表 Flash 存储器起始地址.
	*/

    //将中断向量表的起始地址设置为 Flash 存储器的某个特定地址加上偏移量0x4000。
	SCB->VTOR = FLASH_BASE | 0x4000;//中断向量表的地址偏移 16kb

	//中断优先级分组
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//2位(4)抢占,2位(4)相应。
	LED_Init();			        //初始化灯
	LCD_Init();			        //初始化lcd屏幕	
	USART_Config();             //串口初始化
	printf("你好世界\r\n");
	LCD_Fill(0,0,128,128,RED);  //指定区域填充颜色

       //在写入新程序前必须擦除,不擦除不行
		printf("新的APP正在执行\r\n");
		if(STMFLASH_Erase(FLASH_APP2_ADDR, APP_MAX_SIZE) == 0) 
		{
			printf("备份区域APP2擦除成功\r\n");
	    }
		while(1)
		{
			RecvOverFun();	
			printf("APP1 is running!\r\n");
			printf("已经写入%d个字节数据\r\n",cnt);
			Delay_nms(100);
		}
}

APP1  的 usart.c

1#include "usart.h"
#include "stdio.h"
#include "stmflash.h"
#include "delay.h"

//固定数据长度 0xAA 0x55 0x01 0x55 0xAA,
//如果不知道数据有多长?怎么判断接受完成?
uint8_t Rx_buff[2]={0};//保存串口的数据
uint32_t cnt=0;//接受数据计数
uint8_t Rx_flag = 0;//接受完成标志

uint32_t addr = FLASH_APP2_ADDR;
uint8_t recvTime = 0;

uint16_t app2FlagBuff[2] = {0XAAAA, 0XAAAA};

/*****************************************************************
 *函 数 名 称:RecvOverFun
 *函 数 功 能:判断是否将APP2的bin文件写入了内部FLASAH的APP2区域
 *函 数 形 参:无
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
void RecvOverFun(void)
{
	if(Rx_flag == 1) {
		printf("APP数据接收完成,共:%d个字节,%lf KB数据\r\n", cnt,cnt/1024.0);
		cnt = 0;
		Rx_flag = 0;
		recvTime = 0;
		u16 test_buff[2] = {0};
		addr = FLASH_APP2_ADDR;
		//半字半字的写,在app2末尾地址写入0XAAAA AAAA以便bootloader可以识别出来有更任务
		STMFLASH_WriteHalfWord(APP2_FLAG_ADDR, 0xAAAA);
		STMFLASH_WriteHalfWord(APP2_FLAG_ADDR+2, 0XAAAA);
		Delay_1us();Delay_1us();Delay_1us();Delay_1us();Delay_1us();
		
		FLASH_WaitForLastOperation(1000);
		STMFLASH_Read(APP2_FLAG_ADDR,test_buff,2);
		printf("app2读取到:%x,%x",test_buff[0],test_buff[1]);
		printf("核对数据无误后,请按下复位按键进行数据更新r\n");
	}
}




//PA9/USART1_TX   PA10/USART1_RX
/*****************************************************************
 *函 数 名 称:USART_Config
 *函 数 功 能:初始化串口1,我这里用的是USART1,PC9,10
 *函 数 形 参:无
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
void USART_Config(void)
{

	USART_InitTypeDef USART_InitStruct={0};
	NVIC_InitTypeDef NVIC_InitStructure={0};
	//开时钟的代码
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	//配置GPIO
	GPIO_InitTypeDef GPIO_InitStructure={0};
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;//服用推挽输出模式
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;//初始化的引脚
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;//引脚速度
	GPIO_Init(GPIOA,&GPIO_InitStructure);		
	
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;//初始化的引脚	
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;//浮空输入模式
	GPIO_Init(GPIOA,&GPIO_InitStructure);		
	//开USART1时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	//配置USART
	USART_InitStruct.USART_BaudRate = 115200;//波特率
	USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//RTS和CTS管脚
	USART_InitStruct.USART_Mode = USART_Mode_Tx|USART_Mode_Rx;//收发模式
	USART_InitStruct.USART_Parity = USART_Parity_No;//校验选择
	USART_InitStruct.USART_StopBits = USART_StopBits_1;//停止位
	USART_InitStruct.USART_WordLength = USART_WordLength_8b;//数据长度
	USART_Init(USART1,&USART_InitStruct);
	
	//配置串口中断
	USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//接收中断
	USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);//空闲中断
	//配置NVIC
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//配置中断通道
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;//抢占优先级
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;//响应优先级
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能通道
  NVIC_Init(&NVIC_InitStructure);	
	//使能串口1
	USART_Cmd(USART1, ENABLE);

}

/*****************************************************************
 *函 数 名 称:UART1_SendData
 *函 数 功 能:串口1发送数据
 *函 数 形 参:无
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
void UART1_SendData(uint8_t data)
{

	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
	USART_SendData(USART1,data);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);

}

/*****************************************************************
 *函 数 名 称:UART1_SendStr
 *函 数 功 能:串口1发送数据
 *函 数 形 参:无
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
void UART1_SendStr(uint8_t *pdata)
{
	while(*pdata != '\0')//判断字符串结束
	{
		UART1_SendData(*pdata);//发送字符串
		pdata++;//字符串偏移
	}
}

/*****************************************************************
 *函 数 名 称:fputc
 *函 数 功 能:重写fputc,实现串口输出
 *函 数 形 参:无
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
int fputc(int c,FILE* p)
{
	UART1_SendData(c);
	return c;
}



/*****************************************************************
 *函 数 名 称:USART1_IRQHandler
 *函 数 功 能:串口1中断函数,串口接收APP2的更新数据,并且写入APP2区域内,空闲中断用来判断是否发送完毕
 *函 数 形 参:无
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
void USART1_IRQHandler(void)//(再写入字库时候先屏蔽)
{
	uint8_t data=0;
	u16 app_temp =0;
	//接收中断
	if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
	{
		data = USART_ReceiveData(USART1);
		Rx_buff[cnt%2] = data;

		//将两个8bite拼接成1个16bit的数据
		if((cnt%2) == 1)
		{
			app_temp = ((u16)Rx_buff[1]<<8) + (u16)Rx_buff[0];
			//写入FLASH
			STMFLASH_WriteHalfWord(addr, app_temp);
			addr+=2;
			recvTime = 1;//重新计时
		}
		cnt++;
		
//		//将串口1收到的数据,通过串口3发出去
//		USART3->DR = data;
	}
	//空闲中断
	if(USART_GetITStatus(USART1,USART_IT_IDLE) == SET)
	{
		//清空闲中断,不需要具体读到什么东西
		data = USART1->SR;
		data = USART1->DR;
		Rx_flag = 1;//接受完成,记得用完后置0
	}
}

APP1  的 usart.h

#ifndef __USART_H
#define __USART_H

#include "stm32f10x.h"



#define APP_MAX_SIZE			0x1E000				//第二个app所占flsh的大小,为120k
#define FLASH_APP1_ADDR		0x08004000  		//第一个应用程序起始地址(存放在FLASH)
#define FLASH_APP2_ADDR		0x08022000			//第二个应用程序起始地址(存放在FLASH)
#define APP2_FLAG_ADDR	 (0x08040000-4)	//是否有更新程序标志位


void USART_Config(void);
void UART1_SendData(uint8_t data);
void UART1_SendStr(uint8_t *pdata);
void RecvOverFun(void);
#endif

APP1  的 stmflash.c

#include "stmflash.h"


/*****************************************************************
 *函 数 名 称:STMFLASH_ReadHalfWord
 *函 数 功 能:读取指定地址的半字(16位数据)
 *函 数 形 参:faddr:读地址(此地址必须为2的倍数!!)
 *函 数 返 回:读取的对应地址的16位数据.
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
u16 STMFLASH_ReadHalfWord(u32 faddr)
{
	return *(vu16*)faddr; 
}

/*****************************************************************
 *函 数 名 称:STMFLASH_Write_NoCheck
 *函 数 功 能:不检查的写入
 *函 数 形 参:WriteAddr:起始地址
							 pBuffer:数据指针
							 NumToWrite:半字(16位)数 
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
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.
	}  
} 



/*****************************************************************
 *函 数 名 称:STMFLASH_Read
 *函 数 功 能:从指定地址开始读出指定长度的数据
 *函 数 形 参:ReadAddr:起始地址
							 pBuffer:数据指针
							 NumToWrite:半字(16位)数
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
void STMFLASH_Read(u32 ReadAddr, u16 *pBuffer, u16 NumToRead)   	
{
	u16 i;
	for(i=0; i<NumToRead; i++)
	{
		pBuffer[i] = STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.
		ReadAddr += 2;//偏移2个字节.	
	}
}


/*****************************************************************
 *函 数 名 称:STMFLASH_Erase
 *函 数 功 能:擦除eraseSize大小的半字数据
 *函 数 形 参:startAddr  :擦除的起始地址必须是2K的整数倍
						   numToErase :要擦除的内存大小,必须是2K的整数倍,即必须要从扇区首地址开始
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/

uint8_t STMFLASH_Erase(u32 startAddr, u32 eraseSize) 
{
	uint16_t i=0;
	uint16_t len = eraseSize/STM_SECTOR_SIZE;
	if(startAddr<STM32_FLASH_BASE||(startAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))
		return -1;//非法地址
	if((startAddr % STM_SECTOR_SIZE))//非扇区首地址
		return -2;//非法地址
	FLASH_Unlock();//解锁
	for(i=0; i<len; i++) {
		FLASH_ErasePage(startAddr+i*STM_SECTOR_SIZE);//擦除这个扇区
	}
	FLASH_Lock();//上锁	
	return 0;
}


/*****************************************************************
 *函 数 名 称:STMFLASH_WriteHalfWord
 *函 数 功 能:半字数据写入
 *函 数 形 参:startAddr:写入的地址
								startAddr :写入的数据 16bit
 *函 数 返 回:无
 *作       者:YYK
 *修 改 日 期:xx/xx/xx
*******************************************************************/
void STMFLASH_WriteHalfWord(u32 startAddr, u16 data)
{
	FLASH_Unlock();						//解锁
	//printf("flash已经解锁,写入%x,到%x\r\n",data,startAddr);
	FLASH_ProgramHalfWord(startAddr, data);
	FLASH_Lock();//上锁
}


APP1  的 stmflash.h

#ifndef __STMFLASH_H__
#define __STMFLASH_H__

#include "stm32f10x.h"  

#define STM32_FLASH_SIZE 512 	 		//所选STM32的FLASH容量大小(单位为K)

#if STM32_FLASH_SIZE<256
#define STM_SECTOR_SIZE 1024 //字节
#else 
#define STM_SECTOR_SIZE	2048
#endif	




//FLASH起始地址
#define STM32_FLASH_BASE 0x08000000 	//STM32 FLASH的起始地址
//FLASH解锁键值
 

u16 STMFLASH_ReadHalfWord(u32 faddr);		  //读出半字  
void STMFLASH_WriteLenByte(u32 WriteAddr,u32 DataToWrite,u16 Len);	//指定地址开始写入指定长度的数据
u32 STMFLASH_ReadLenByte(u32 ReadAddr,u16 Len);						//指定地址开始读取指定长度数据
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite);		//从指定地址开始写入指定长度的数据
void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead);   		//从指定地址开始读出指定长度的数据


uint8_t STMFLASH_Erase(u32 startAddr, u32 eraseSize);
//测试写入
void Test_Write(u32 WriteAddr,u16 WriteData);		
void STMFLASH_WriteHalfWord(u32 startAddr, u16 data);
#endif

 

         (2) APP2 :  我们更新的代码(分120kb的flash),需要生产.bin文件,在APP1运行时通过串口发送过去。

         1)在APP1基础上修改或添加的功能,除此之外与APP1一致,保证后续能够继续升级。

我这里只把屏幕改了个颜色。

         2)在APP2中需要以.axf为基础,生成bin文件需要下面的指令

D:\TOLL\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o .\Objects\STM32F1_Template.bin .\Objects\STM32F1_Template.axf

D:\TOLL\Keil_v5\ARM\ARMCC\bin\fromelf.exe 是咱们keil根目录下的一个程序,他可以根据.axf生成.bin文件。

.\Objects\STM32F1_Template.axf 是我们工程文件夹下project下object目录里的文件

把fromelf.exe路径和STM32F1_Template.axf换成你们自己的目录

        添加到如下图所示位置

b0b238991bb740838b2dd71f73f31692.png

之后编译生成.bin文件即可,

d90bbe655ceb4d7ea741efcfbfd89d41.png0e8ca942c37845b683d8947c61ac0be3.png如左图所示,生产了.bin文件。

 

6.结果展示

        (1)下载进去BootLoader后的程序,找不到APP1,也没有APP2更新,屏幕颜色为BootLoader程序里屏幕的初始化的浅灰色。

5d6d10d57ffb47a0a8f3942c155ea6d3.png2e3fd8a8be5a404783a8e9d3c513fa39.png

        (2)下载进去APP1到0X0800 4000后复位,BootLoader会找到APP1起始地址后4个地址后,即PC指向是否在FLASH内,不会再报错,APP1烧录进去后就不会报错了。

        程序会从BootLoader跳转到APP1里运行,屏幕颜色为APP1程序里屏幕初始化为了红色

49872dcde361495ebb961d5da1b76a81.png22844da936cb439f9daca7a78c61d627.png

(3)生成APP2的.bin文件后通过串口发送给单片机,此时单片机正在执行APP1,会一直等待接收消息。

        01d5a009c958409392dc15d7704b2ac9.png

        进入到APP1里面后即可发送APP2的.bin文件

b480765c3e1b4d42bae13455b75d273f.png

 

(4)复位后BootLoader就开始进行数据的搬移,会先删除APP1区域(120kb)代码,然后将APP2(120kb)代码搬到APP1区域中。

49b12a800d314751bdfa5f0d7ba02a73.png27bec47573b645d996a0aa8f1d4f984a.png

 

屏幕颜色为APP2程序中屏幕初始化为黄色:

4f6232cf2349446e829e09a70ab4e27a.png

 

至此IAP结束,后面我会更新OTA的文章,IAP中需要注意两点

        1)受限于内存大小原因BootLoader中我直接删除了APP1区域的代码,搬运APP2到APP1。这是极其不应该的,因为如果APP2区域的内容有损坏或者搬运过程中断电,那么程序就会丢失,单片机会变砖头。用户又没有keil软件,可烧录不了程序。稳妥起见应该预留一个APP3区域来备份当前可以运行的程序。

        2)  本次操作受限于设备,所有FLASH均为内部FLASH,当然也可以用外部的FLASH,流程一样,只不过APP2的区域要在外部FLASH。

       

 

 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值