STM32F103x学习笔记(1)—— IAP应用编程

IAP应用编程

1、IAP简介

IAP是In Application Programming的首字母缩写,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。

2、STM32内置Flash

1、STM32内部FLASH的起始地址为0X08000000,Bootloader程序文件就从此地址开始写入,存放APP程序的首地址设置在紧跟Bootloader之后。当程序开始执行时,首先运行的是Bootloader程序,然后Bootloader收到BIN文件并将其复制到APP区域使固件得以更新,固件更新结束后还需要跳转到APP程序开始执行新的程序,完成这最后这一步要了解Cortex-M3的中断向量表。
2、程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,当复位中断程序运行完成后才跳转到main函数。由此可见,在最后一步的设计中需要根据存放APP程序的起始地址以及中断向量表来设置栈顶地址,并获取复位中断地址跳转到复位中断程序。

2.1、内置Flash的分配情况大致如下:

在这里插入图片描述

2.2、具体内置Flash大小参考选型手册

在这里插入图片描述

3、BootLoader程序

1、上电初始程序依然从0x08000004处取出复位中断向量地址,执行复位中断函数后跳转到IAP的main(标号①所示),
2、在IAP的main函数执行完成后强制跳转到0x08000004+N+M处(标号②所示),最后跳转到新的main函数中来(标号③所示),
3、当发生中断请求后,程序跳转到新的中断向量表中取出新的中断函数入口地址,再跳转到新的中断服务函数中执行(标号④⑤所示),执行完中断函数后再返回到main函数中来(标号⑥所示)。

3.1、标注如下图所示:

在这里插入图片描述

4、流程图

4.1、IAP升级过程中,我采用的大致方案是:
1、APP 收到上位机发来的升级指令后,将升级标志写入 Flash ,然后调用软件重启函数,进行重启;
2、重启后,STM32 先进入 Bootloader ,Bootloader 检查升级标志是否置位,如果未置位,则跳转到 APP 中,相反,则停留在 Bootloader 中;
3、Bootloader 初始化后发送准备完毕命令至上位机,上位机受到后将固件数据拆分成数据帧,依次发送给 STM32 ,每发送一帧需要等待 STM32 应答无误后,继续发送下一帧;
4、STM32 收到数据帧时,先保存在缓存数组中,然后发送应答给上位机,待接收的数据长度为一字或多字时,将数据一起写入 Flash ;
5、上位机发送完所有数据后,发送结束命令,STM32 收到后,将所有未写入的数据全部写完,将固件版本号写入 Flash 中,复位升级标志,检查 APP 程序区起始数据无误后,发送升级完毕应答给上位机,接着调用软件重启函数,重进 Bootloader ;
6、重进 Bootloader 后,Bootloader 检查升级标志为复位状态时,跳转到 APP 中执行 APP 程序;
7、至此,所有升级过程全部结束。

5、程序代码解析

5.1、bootloader程序设计

/Drivers/src/IAP.c
Step 1) 确定存放APP程序的首地址
#define FLASH_APP1_ADDR 0x08006000 //应用程序起始地址
Step 2) 跳转到新程序运行

void Run_application(void)
{
    
    
	if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
	{
    
    	 
		printf("准备执行自带的APP代码!!\r\n");
		iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码
		printf("\r\n");
	}
	else 
	{
    
    
		printf("Err:自带的APP代码 地址是非法的 无法执行!!!!\r\n");
		printf("准备软件复位 !!\r\n");
		Reset_MCU(); 
		printf("\r\n");
	}
}

if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)
串口接收过来的数据,是从:0X20001000开始存储的。 第一个4个字节是MSP地址,第二个4个字节,才是复位中断向量的入口地址。 &0xFF000000就是取最高8位。因为FLASH的地址范围是0X0800 0000开始的。这可以一定程度上确保地址范围正常。

//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
    
    
	if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)	//检查栈顶地址是否合法.
	{
    
     
		SysTick->CTRL = 0;  
		
		__disable_irq();

		jump2app=(iapfun)*(vu32*)(appxaddr+4);		//用户代码区第二个字为程序开始地址(复位地址)		
		MSR_MSP(*(vu32*)appxaddr);					//初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
// 		SCB->VTOR = FLASH_BASE | 0x06000;
		jump2app();									//跳转到APP.
	}
	else
	{
    
    
		printf("Err: 跳转 程序段栈顶地址非法 !!\r\n");
	}
}		 

if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000);
检查栈顶地址是否合法(用户代码的第一个字存放的是栈顶地址,即检查此地址)
jump2app=(iapfun)*(vu32*)(appxaddr+4);
用户代码区第二个字为程序开始地址(复位中断向量地址),强制把该地址转化为iapfun类型的函数指针,再赋给函数指针jump2app
Step 3) 串口接收缓存 IAP 数据

void USART1_IRQHandler(void)                	//串口1中断服务程序
{
    
    
	u8 Res;
  
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
	{
    
    
	   Res =USART_ReceiveData(USART1);	//读取接收到的数据

		 mscp_rcv_data_api(&usartMsg_t[TRANS_DIRECTION_RELAY], &Res, 1);
	} 
} 

这里列举USART1为例。mscp_rcv_data_api(&usartMsg_t[TRANS_DIRECTION_RELAY], &Res, 1);
是以数组实现的循环链表的具体实现。
Step 4) 轮询 IAP 数据

void Updata_application(void)
{
    
    
	__IO u32 UpdaCnt=0x5c50;//程序的大小
	
	printf("有更新程序,已经开启了看门狗 !!");
	printf("\r\n");
	
	if(EM_USER_BIN_2 == GetFlashAddrLuenchCsFlag())
	{
    
    
		printf("擦除了FLASH_APP1_ADDR....\r\n");
		STMFLASH_Erase_Sector(FLASH_APP1_ADDR,40);//擦除addr1地址以及以上40页 (1024/页)
		s_copy_address = FLASH_APP1_ADDR;//存储用户程序地址
		newDataAddr = FLASH_APP1_ADDR;
		printf("\r\n");
	}
	else
	{
    
    
		printf("擦除了FLASH_APP2_ADDR....\r\n");
		STMFLASH_Erase_Sector(FLASH_APP2_ADDR,40);//擦除addr2地址以及以上40页 (1024/页)
		s_copy_address = FLASH_APP2_ADDR;//存储用户程序地址
		newDataAddr = FLASH_APP2_ADDR;
		printf("\r\n");
	}

	printf("开始进入主函数\r\n");
	Task_delay[EM_USER_UPDATE_TIMEOUT_DELAY] = 0;
	printf("\r\n");
	
	while(1)
	{
    
    
		if(Task_delay[EM_PRINTF_TIMEOUT_DELAY] >= D_DELAY_S(5))
		{
    
    
			IWDG_FeedDog();//喂狗
			Task_delay[EM_PRINTF_TIMEOUT_DELAY] = 0;
			printf("等待接收用户程序, 喂狗!!!!\r\n");
		}
		
		if(Task_delay[EM_USER_UPDATE_TIMEOUT_DELAY] >= D_DELAY_S(2*60))
		{
    
    
			printf("Err:升级超时2分钟!!\r\n");
			printf("\r\n");
			Reset_MCU(); 
		}
		
		//串口数据分发(轮询)
		uart_receive_data_distribute();
		
		//接收升级数据完成
		if(uart_receive_data_success(true)) break;//数据接收完成
	}
	uart_receive_data_success(false);
}

/**
  * @brief  串口数据分发
  * @param  argument: Not used 
  * @retval None
  */
static void uart_receive_data_distribute(void)
{
    
    
	mscp_poll_packet_data(&usartMsg_t[TRANS_DIRECTION_RELAY]);//获取串口升级部分的数据
	mscp_poll_packet_data(&usartMsg_t[TRANS_DIRECTION_BLE]);//获取串口升级部分的数据
	mscp_poll_packet_data(&usartMsg_t[TRANS_DIRECTION_RS485_NOUSE]);//获取串口升级部分的数据
	mscp_poll_packet_data(&usartMsg_t[TRANS_DIRECTION_RS485_INUSE]);//获取串口升级部分的数据
	mscp_poll_packet_data(&usartMsg_t[TRANS_DIRECTION_4G]);//获取串口升级部分的数据
	mscp_poll_packet_data(&usartMsg_t[TRANS_DIRECTION_WIFI]);//获取串口升级部分的数据
	RFID_reader_poll_packet_data(&RFID_reader_msg); //获取RS485升级部分的数据
}

STMFLASH_Erase_Sector(FLASH_APP1_ADDR,40);//擦除addr1地址以及以上40页 (1024/页)
STMFLASH_Erase_Sector 是一个擦除 FLASH 的自定义函数
Step 5) 擦除 内部 FLASH

void STMFLASH_Erase_Sector(u32 adderss, u8 len)   
{
    
    
	u8 i;
	for( i=0;i<len;i++)
	{
    
    
		STMFLASH_Erase(adderss,512);//擦除addr2地址以及以上40页
		adderss +=1024;
	}
}

void STMFLASH_Erase(u32 WriteAddr,u16 NumToWrite)
{
    
    
  u32 secpos;	   //扇区地址
	u16 secoff;	   //扇区内偏移地址(16位字计算)
	u16 secremain; //扇区内剩余地址(16位字计算)	   
 	u16 i;    
	u32 offaddr;   //去掉0X08000000后的地址
	if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址
	FLASH_Unlock();						//解锁
	offaddr=WriteAddr-STM32_FLASH_BASE;		//实际偏移地址.
	secpos=offaddr/STM_SECTOR_SIZE;			//扇区地址  0~127 for STM32F103RBT6
	secoff=(offaddr%STM_SECTOR_SIZE)/2;		//在扇区内的偏移(2个字节为基本单位.)
	secremain=STM_SECTOR_SIZE/2-secoff;		//扇区剩余空间大小   
	if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围
	while(1) 
	{
    
    	
		STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容
		for(i=0;i<secremain;i++)//校验数据
		{
    
    
			if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除  	  
		}
		if(i<secremain)//需要擦除
		{
    
    
			FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
		}
		if(NumToWrite==secremain)break;//写入结束了
		else//写入未结束
		{
    
    
			secpos++;				//扇区地址增1
			secoff=0;				//偏移位置为0 	 
			WriteAddr+=secremain;	//写地址偏移	   
			NumToWrite-=secremain;	//字节(16位)数递减
			if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
			else secremain=NumToWrite;//下一个扇区可以写完了
		}	 
	};	
	FLASH_Lock();//上锁
}

//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
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个字节.	
	}
}

//读取指定地址的半字(16位数据)
//faddr:读地址(此地址必须为2的倍数!!)
//返回值:对应数据.
u16 STMFLASH_ReadHalfWord(u32 faddr)
{
    
    
	return *(vu16*)faddr; 
}

Step 6) 写入 IAP 数据到 FLASH

/**
  * @brief  cmd   dfu
  * @param  argument: Not used 
  * @retval None
  */
static void mcu_cmd_dfu_handle(Msg_t *p_msg_t)
{
    
    
	Copy_application(p_msg_t);
}

void Copy_application(Msg_t *p_msg_t)
{
    
    
	uint8_t index = 4;
	uint16_t _readData = 0;
	uint16_t _checkData = 0;
		
	while(index < p_msg_t->msgHead_t.data_size)
	{
    
    
		IWDG_FeedDog();//喂狗
		_readData = 0;
		if((index+1) < p_msg_t->msgHead_t.data_size)
		{
    
    
			_readData = (uint16_t)p_msg_t->rcv_buf[index+1]<<8;
		}
		_readData = _readData|p_msg_t->rcv_buf[index];
		STMFLASH_Write(s_copy_address,&_readData,1);	
		s_copy_address +=2;
		index +=2;
		printf("%x-%04x x ", s_copy_address, _readData);
	}
	printf("\n 升级数据接收完成,长度:%d d \n", p_msg_t->msgHead_t.data_size);

	//检测 总包长 发完
	_readData = ((p_msg_t->rcv_buf[0]) | (p_msg_t->rcv_buf[1]<<8));
	_checkData = ((p_msg_t->rcv_buf[2]) | (p_msg_t->rcv_buf[3]<<8));
	if(_checkData >= _readData)
	{
    
    
		dataReceivedSuccess();
	}
}

Step 7) 检测 写入的 IAP 数据合法性

void Check_application(void)
{
    
    
	if(((*(vu32*)(newDataAddr+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
	{
    
    	 
		printf("准备执行新的APP代码!!\r\n");
		printf("\r\n");
		printf("\r\n");
		printf("\r\n");
		Reset_MCU(); 
	}
	else 
	{
    
    
		printf("Err:接收的应用程序为 非FLASH应用程序,无法执行!\r\n");
		printf("\r\n");
		printf("\r\n");
		printf("\r\n");
		Reset_MCU(); 
	}
}

5.2、用户App设计

Step 1) 新建一个工程,编写一个 App LED 闪烁代码
在这里插入图片描述
这部分代码不用抄,自己任意写一个 LED 灯闪烁的程序即可。
Step 2) 设置 App 地址偏移起点(理解成中断向量表偏移地址)
在这里插入图片描述
这里的地址与 Bootloader 的FLASH_APP1_ADDR必须一致。
Step 3) Flash地址设置(这个地址需要和BootLoader中对应)
在这里插入图片描述
这里的地址与 Bootloader 的FLASH_APP1_ADDR必须一致。
0x7A000 是由 0x80000 - FLASH_APP1_ADDR 得来。
Step 4) App.bin生成
在这里插入图片描述
基本的命令格式是 这里需要注意空格,大小写。
D:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin --output=output\@L.bin !L
Step 5) 编译链接生成bin文件:
在这里插入图片描述
窗口中有如下的提示信息,表示bin文件生成成功。
在输出文件夹中找到BIN文件,这个文件就是用户App的BIN文件。

5.3、使用流程

Step 1) 烧入 Bootloader 程序:
在这里插入图片描述
Step 2) 打开 XCOM V2.1.exe 串口助手:
在这里插入图片描述
Step 3) 开发版接线图:
在这里插入图片描述
本例程以串口1测试为例。
Step 4) 使用 XCOM V2.1.exe 发送 BIN 文件:
在这里插入图片描述

在这里插入图片描述
过程中,显示等待接收用户程序!!!表示等待接收 BIN 文件 。
过程中,显示准备执行新的APP代码!!,准备执行自带的APP代码!!表示程序跳转完成。
Step 5) 效果展示:
在这里插入图片描述
跳转前灯灭,跳转成功后,LED 快闪。(开发版 STM32F103ZET6)


• 由 ChiKong_Tam 写于 2020 年 7 月 27 日
• 参考:stm32cubeMX学习九、带串口屏显示的BootLoader程序开发(基于野火STM32F103ZET6霸道开发板)
• 参考:STM32+IAP方案的实现,IAP实现原理(详细解决说明)
• 参考:STM32在线升级 (IAP)
• 参考:STM32 IAP 全解析

猜你喜欢

转载自blog.csdn.net/qq_42209354/article/details/107615193