基于STM32系列芯片的 IAP实现的探索

什么是IAP?

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

哇塞! 这个功能一般在产品正式发布的时候都会实现,我们的手机,MP3 等数码产品都会预留接口实现固件的升级。

如何实现IAP?

关于IAP的相关技术内容确实很多,但是总结起来无非就是通过上位机,它可以是手机或者电脑上的程序,通过无线或者有线的通讯方式,将APP(应用程序)的二进制文件,写入到指定地址的User Flash的区块中。

IAP 将User Flash 区域分成两部分,Bootloader程序引导区和APP 应用程序区域。

上电或者重启时,程序会首先进入Boodloader区,通过特定的标志位,判断程序停留在当前的Bootloader程序还是跳转到APP区。

在这里插入图片描述

IAP需要解决的两大问题:

1、Flash操作,包括Flash区域的划分(确定Blootloader程序大小,APP程序大小,设定从哪个地址开始为APP程序地址)、指定Flash区域的操作,包括写入或者擦除。

2、数据传输,选择串口或者网络等方式烧写。

第一步:学习官方源代码

STM32官网上有IAP的实例源代码,用兴趣的朋友可以下载来学习借鉴。单纯的看源代码会使人发晕。但是它提供了相对完整的IAP解决方案。

接下来的步骤是为了让我们更好地掌握IAP技术

第二步:了解STM32芯片基本硬件参数

STM32官网上有关于STM32 系列芯片的选型,其中也提到各类型号的命名规则:
在这里插入图片描述
我们这里关心的是闪存存储器的容量。这里要注意Bootloader+APP 的程序大小不要超过闪存存储区的容量.

第三步、搞清除STM32内置Flash

在只有一个程序的情况下,是直接加载到Flash区的。

在这里插入图片描述

当设计Bootloader程序后:
在这里插入图片描述

第四步、 STM32程序运行机制

  • STM32Fx有一个中断向量表,这个中断向量表存放代码开始部分的后4个字节处(即0x08000004),代码开始的4个字节存放的是栈顶地址。

  • 当发生中断后程序通过查找该表得到相应的中断服务程序入口,然后跳到相应的中断服务程序中执行。

  • 上电后从0x08000004处取出复位中断向量的地址,然后跳转到复位中断程序入口(标号1所示),执行结束后跳转到main函数。

  • 在执行main函数的过程中发生中断,则STM32强制将PC指针指回中断向量表处(标号3所示),从中断向量表中找到相应的中断函数入口地址,跳转到相应的中断服务函数,执行完中断服务函数后再返回到main函数中来。

IAP代码实现:

IAP的基础是Flash的操作。

1、实现flash写入,删除,修改。

STM32 官方自带Flash操作的函数文件 stm32f10x_flash.c, 我们只需要在该基础上进一步封装即可。

Flash操作中,STM32只允许页操作。这里需要搞清楚的是一页是2k还是1K的大小。(详细可以查看MCU的datasheet技术文档)

部分关键代码如下:


```c
//从指定地址开始写入指定长度的数据
//WriteAddr:起始地址(此地址必须为2的倍数!!)
//pBuffer:数据指针
//NumToWrite:半字(16位)数(就是要写入的16位数据的个数.)
#if STM32_FLASH_SIZE<256
#define STM_SECTOR_SIZE 1024 //字节
#else 
#define STM_SECTOR_SIZE	2048
#endif		 
u16 STMFLASH_BUF[STM_SECTOR_SIZE/2];//最多是2K字节
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,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);//擦除这个扇区
			for(i=0;i<secremain;i++)//复制
			{
				STMFLASH_BUF[i+secoff]=pBuffer[i];	  
			}
			STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区  
		}else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间. 				   
		if(NumToWrite==secremain)break;//写入结束了
		else//写入未结束
		{
			secpos++;				//扇区地址增1
			secoff=0;				//偏移位置为0 	 
		   	pBuffer+=secremain;  	//指针偏移
			WriteAddr+=secremain;	//写地址偏移	   
		   	NumToWrite-=secremain;	//字节(16位)数递减
			if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
			else secremain=NumToWrite;//下一个扇区可以写完了
		}	 
	};	
	FLASH_Lock();//上锁
}
#endif
//从指定地址开始读出指定长度的数据
//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个字节.	
	}
}

软件开发的重要的一项内容在于调试,调试可以让思路进一步清晰明确。

我们调试下该Flash的写入函数是否正确。

在程序中添加如下代码:

		uint16 testbuff[2]= {0x11,0x22}		
		STMFLASH_Write(0x8008000,testbuff,2);

在这里插入图片描述
Keil 中断点查看,指定地址已经写入数据。

接着,我们进一步封装,该函数用于判断按是否按照1K写入:

void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize)
{
	u16 t;
	u16 i=0;
	u16 temp;
	u32 fwaddr=appxaddr;//当前写入的地址
	u8 *dfu=appbuf;
	for(t=0;t<appsize;t+=2)
	{						    
		temp=(u16)dfu[1]<<8;
		temp+=(u16)dfu[0];	  
		dfu+=2;//偏移2个字节
		iapbuf[i++]=temp;	    
		if(i==512)
		{
			i=0;
			STMFLASH_Write(fwaddr,iapbuf,512);	
			delay_ms(100);
			fwaddr+=1024;//偏移2048  16=2*8.所以要乘以2.
		}
	}
	if(i)
	{
		STMFLASH_Write(fwaddr,iapbuf,i);//将最后的一些内容字节写进去.  
		delay_ms(100);
	}
}


这里我们进一步可以测试封装后Flash 写入函数是否正确。这里留给读者自行去测试。

2、IAP 通信协议设计

FLash 操作函数搞定之后,接下来就是搞定通信的驱动。这里可以是蓝牙、WIFI 或者有线网络TCP/IP,串口通信等。

针对通信驱动的实现,这里不做展开。我们只针对通信协议部分的探讨。

通信协议可以自定义,也可以使用Y-moden 协议。

这里有一篇文章写得非常详细。
Ymodem协议介绍

我们这里来自行设计一套最简易的协议。不包含起始数据帧和CRC,我们只对控制流进行设计:
在这里插入图片描述

服务端采用C#代码编写:界面设计如下:

在这里插入图片描述

上位机关键代码:

     private void button2_Click(object sender, EventArgs e)                      //选择固件按钮
        {
            OpenFileDialog openfile = new OpenFileDialog();                         //打开文件控件
            try
            {
                openfile.ShowDialog();                                                  //显示打开文件对话框
                txt_filename.Text = openfile.FileName;                                  //获取所选择固件的名称
                fs = new FileStream(openfile.FileName, FileMode.Open);                  //获取文件流
                str = "文件共" + fs.Length.ToString() + "字节" + "\n";                      //获取文件的总字节数
                textBox1.AppendText(str);                                               //显示文件的总字节数
                packet_zheng = (int)fs.Length / 1024;                                   //获取文件的整K字节数
                packet_yu =(int) fs.Length % 1024;                                      //获取不足1K的剩余字节数
                btn_open.Enabled = false;                                               //禁用选择固件文件按钮
                btn_send.Enabled = true; 
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                MessageBox.Show("请重新选择固件");
            }
            
                                                       //使能发送/下载数据按钮
            
        }
        private void send_length()                                                  //发送固件的长度数据
        {
            datatosend[0] = 0xb1;
            datatosend[1] = 0xb2;
            datatosend[2] = 0xb3;
            datatosend[3] = (byte)(packet_zheng / 256);                             //获取整K字节数的高八位
            datatosend[4] = (byte)(packet_zheng % 256);                             //获取整K字节数的低八位
            datatosend[5] = (byte)(packet_yu / 256);                                //获取不足1K字节数的高八位
            datatosend[6] = (byte)(packet_yu % 256);                                //获取不足1K字节数的低八位
            datatosend[7] = 0x1b;
            datatosend[8] = 0x2b;
            datatosend[9] = 0x3b;
            mystream.Write(datatosend, 0, 10);

        }
        private void send_data_zheng()                                              //发送整K字节数据
        {
            fs.Read(datatosend, 0, 1024);
            mystream.Write(datatosend, 0, 1024);
        }

        private void send_data_yu()                                                //发送不足1K字节数据
        {
            fs.Read(datatosend, 0, packet_yu);
            mystream. Write(datatosend, 0, packet_yu);
        }

        private void btn_reset_Click(object sender, EventArgs e)                  //发送进入BootLoader命令
        {   
            if (myclient.Connected == false)
            {
                myclient.Connect(txtip.Text, 1550);
            }
            datatosend[0] = 0xe1;
            datatosend[1] = 0xe2;
            datatosend[2] = 0xe3;
            datatosend[3] = 0x1e;
            datatosend[4] = 0x2e;
            datatosend[5] = 0x0d;
            datatosend[6] = 0x0a;
            mystream .Write(datatosend, 0, 7);

        }

        private void receive_data()                         //数据接收线程
        {

            while (myclient.Connected)                    
            {
                if (mystream.CanRead)                            //当有数据可读时
                {

                    int len = (int)myclient.ReceiveBufferSize;   //获取数据的长度
                    datarec = new byte[len];                     //定义数据缓冲数组
                    
                    mystream.Read(datarec, 0, len);              //读取数据到数组
                    if (len >= 5)
                    {
                        if (datarec[0] == 0xf1 & datarec[1] == 0xf2 & datarec[2] == 0xf3 & datarec[3] == 0x1f & datarec[4] == 0x2f)          //判断是否已经连接到终端
                        {
                            str = "已连接到设备" + "\n";  
                            textBox1.AppendText(str);       //显示状态信息
                            btn_open.Enabled = true;
                            btn_boot.Enabled = true;
                        }
                        else if (datarec[0] == 0xa1 & datarec[1] == 0xa2 & datarec[2] == 0xa3 & datarec[3] == 0x2a & datarec[4] == 0x1a)     //判断终端是否收到连接信息
                        {

                            str = "客户端已确认下载信息" + "\n";
                            textBox1.AppendText(str);      //显示状态信息
                            send_length();                 //发送长度数据
                        }
                        else if (datarec[0] == 0xb1 & datarec[1] == 0xb2 & datarec[2] == 0xb3 & datarec[3] == 0x2b & datarec[4] == 0x1b)     //判断终端是否接受到长度消息
                        {
                            str = "客户端接收长度信息完毕,即将开始下载数据" + "\n";
                            textBox1.AppendText(str);      //显示状态信息
                            send_data_zheng();             //发送整K字节数据
                        }
                        else if (datarec[0] == 0xc1 & datarec[1] == 0xc2 & datarec[2] == 0xc3 & datarec[3] == 0x2c & datarec[4] == 0x1c)     //判断终端1K字节数据是否接受完毕
                        {

                            packet_send++;
                            str = "已下载" + packet_send.ToString() + "K" + "\n";     //显示已经发送的字节数
                            textBox1.AppendText(str);
                            if (packet_send < packet_zheng)                           //若整K字节数未发送完
                            {
                                send_data_zheng();                                    //发送整K字节数据
                            }
                            else if (packet_send == packet_zheng)                     //若整K字节数据已发送完
                            {
                                send_data_yu();                                       //发送余下不足1K的数据
                            }
                        }
                        else if (datarec[0] == 0xd1 & datarec[1] == 0xd2 & datarec[2] == 0xd3 & datarec[3] == 0x2d & datarec[4] == 0x1d)     //判断终端是否完全接受完数据
                        {
                            str = "程序下载完毕" + "\n";
                            textBox1.AppendText(str);
                            fs.Flush();                             //释放流资源
                            fs.Dispose();
                            packet_send = 0;                        //发送整K字节计数清零
                          
            
                            myclient.Close();                       //释放TcpClient资源
                            myclient = null;
                            mystream.Flush();                       //释放网络流资源

                            btn_connect.Enabled = true;             //使能连接按钮
                            btn_disconnect.Enabled = false;         //禁用断开连接按钮
                            btn_open.Enabled = false;               //禁用选择数据按钮
                            btn_boot.Enabled = false;               //禁用进入BootLoader按钮
                            btn_send.Enabled=false;                 //禁用发送/下载数据按钮

                            thread_recdata.Abort();                 //终端接收数据线程 
                          
                        }
                        else if (datarec[0] == 0xe1 & datarec[1] == 0xe2 & datarec[2] == 0xe3 & datarec[3] == 0x1e & datarec[4] == 0x2e)    //判断终端是否成功进入BootLoader
                        {
                            str = "进入bootloader成功" + "\n";
                            textBox1.AppendText(str);
                           
                            myclient.Close();                      //释放TcpClient资源
                            myclient = null;
                            mystream.Flush();                      //释放网络流资源

                            btn_connect.Enabled = true;            //使能连接按钮
                            btn_disconnect.Enabled = false;        //禁用断开连接按钮
                            btn_open.Enabled = false;              //禁用打开按钮
                            btn_boot.Enabled = false;              //禁用进入BootLoader按钮
                            btn_send.Enabled = false;              //禁用发送/下载按钮

                            thread_recdata.Abort();        //终止数据接收线程
                        }
                        else if (datarec[0] == 0xaa & datarec[1] == 0xbb & datarec[2] == 0xcc & datarec[3] == 0xdd & datarec[4] == 0xee)    //判断终端是否成功进入BootLoader
                        {
                            MessageBox.Show("已经进入BootLoader");
                        }
                    }
                }        
            }   
        }

MCU部分关键代码:

#define CONNECT_CMD1 0x44
#define CONNECT_CMD2 0x4D
#define CONNECT_CMD3 0x46
#define BINLEN_CMD1  0XB1
#define BINLEN_CMD2  0XB2
#define BINLEN_CMD3  0XB3
#define BINLEN_CMD4  0X1B
#define BINLEN_CMD5  0X2B
#define BINLEN_CMD6  0X3B

#define BINLEN_CMD6  0X3B
Pocess_Socket_Data(SOCKET s)	                 //Socket 接受数据的处理
{	
		if(bootsta==0)                                   //判断是否为初始状态		                         
		{
				if(Rx_Buffer[0]==CONNECT_CMD1)
				{
						delay_ms(1);
						if(Rx_Buffer[1]==CONNECT_CMD2&&Rx_Buffer[2]==CONNECT_CMD3)//接收到下载连接指令
						{
								bootsta=1;	      //进入接受长度数据状态
								Tx_Buffer[0]=0XA1; Tx_Buffer[1]=0XA2; Tx_Buffer[2]=0XA3; Tx_Buffer[3]=0X2A;  Tx_Buffer[4]=0X1A; 
							  send(SOCK_TCPS,Tx_Buffer,5);
							
								//Write_SOCK_Data_Buffer(s, Tx_Buffer,5,S0_Port);	          //向主机发送已接受到下载指令
						}
				}
		}
		else if(bootsta==1)			                            //判断是否为接受长度信息状态
		{
				if(Rx_Buffer[0]==BINLEN_CMD1)
				{
						/*上位机将BIN程序,分成1K的单位,每次最多发送1K字节,下位机接受后将1K字节写入Flash
						Rx_Buffer[3]为1K个数的高八位,Rx_Buffer[4]为1K个数的低八位。packet_zheng为程序的1K字节个数
						Rx_Buffer[5]不足1K字节个数的高八位,Rx_Buffer[6]为不足1K字节个数的低八位。packet_yu为不足1K字节的个数*/
						delay_ms(1);
						if(Rx_Buffer[1]==BINLEN_CMD2&&Rx_Buffer[2]==BINLEN_CMD3&&Rx_Buffer[7]==BINLEN_CMD4&&Rx_Buffer[8]==BINLEN_CMD5&&Rx_Buffer[9]==BINLEN_CMD6)//读取BIN数据长度
						{
								packet_zheng=Rx_Buffer[3]*256+Rx_Buffer[4];
								packet_yu=Rx_Buffer[5]*256+Rx_Buffer[6];
								bootsta=2;	                                              //进入接受程序数据状态
								packet_rev=0;
								Tx_Buffer[0]=0XB1; Tx_Buffer[1]=0XB2; Tx_Buffer[2]=0XB3; 
							  Tx_Buffer[3]=0X2B;  Tx_Buffer[4]=0X1B;	                  //发送确认接受完毕长度信息命令
							  
							  send(SOCK_TCPS,Tx_Buffer,5);
							
								//Write_SOCK_Data_Buffer(s, Tx_Buffer,5,S0_Port);	          //发送确认接受完毕长度信息命令
						}
				}
		}
		else if(bootsta==2)	          		                                                  //判断是否为接受程序数据状态
		{
				if(packet_rev<packet_zheng)				                                          //判断是否为整K字节接受状态
				{
						if(size>=1024)
						{
								iap_write_appbin(FLASH_APP1_ADDR+packet_rev*1024,Rx_Buffer,1024);	      //向指定地址写入1K字节的数据
								packet_rev++;															  //接受到的整K字节数加1
								Tx_Buffer[0]=0XC1; Tx_Buffer[1]=0XC2; Tx_Buffer[2]=0XC3; Tx_Buffer[3]=0X2C;  Tx_Buffer[4]=0X1C;
							
							  send(SOCK_TCPS,Tx_Buffer,5);
							//	Write_SOCK_Data_Buffer(s, Tx_Buffer,5,S0_Port);							  //向主机发送确认接收完毕1K字节命令
						}
				}
				else
				{
						if(size>=packet_yu)	                                                         //发送余下不足1K字节的数据               
						{
								iap_write_appbin(FLASH_APP1_ADDR+packet_zheng*1024,Rx_Buffer,size);		 //向指定地址写入不足1k字节的数据
								if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)               //判断是否为0X08XXXXXX.
								{	 
										Tx_Buffer[0]=0XD1; Tx_Buffer[1]=0XD2; Tx_Buffer[2]=0XD3; Tx_Buffer[3]=0X2D;  Tx_Buffer[4]=0X1D;
									
									  send(SOCK_TCPS,Tx_Buffer,5);
										//Write_SOCK_Data_Buffer(s, Tx_Buffer,5,S0_Port);	                     //向主机发送接收完毕全部数据命令
									
								    iap_load_app(FLASH_APP1_ADDR);                                       //执行FLASH APP代码
								}
								else
								{
								    bootsta=0;		                                                     //恢复初始状态
								}
						}
				}
		}
		if(Rx_Buffer[0]==0xe1&Rx_Buffer[1]==0xe2&Rx_Buffer[2]==0xe3&Rx_Buffer[3]==0x1e&Rx_Buffer[4]==0x2e)	    //判断是否为进入BootLoader命令
		{
				Tx_Buffer[0]=0xaa; Tx_Buffer[1]=0xbb; Tx_Buffer[2]=0xcc; Tx_Buffer[3]=0xdd; Tx_Buffer[4]=0xee;
			
			  send(SOCK_TCPS,Tx_Buffer,5);
				//Write_SOCK_Data_Buffer(s, Tx_Buffer,5,S0_Port);					                                    //向主机发送确认接收到进入BootLoader命令
		}
}

通信数据流的控制设计重点在于重点环节的命令设计。一般我们都会设计带CRC校验的通信协议设计。上面的控制流比较简单,但是可靠性不高。

在一些保密行业,还会对数据区进行DES加密等等。

3、APP代码部分修改

1、起始地址修改:

   FLASH_APP1_ADDR		0x08005000     SIZE: 0x1FB000	   
   VECT_TAB_OFFSET      0x5000	

2、bin文件生成

第一步:打开Options for Target ‘target 1’对话框,选择User标签页;

第二步:找到fromelf.exe的路径(keil5在ARMCC里)
在这里插入图片描述

3、跳转进入Bootloader,并且改写相应的标志位。

void Process_Socket_Data(SOCKET s)
{			if(Rx_Buffer[0]==0xe1&Rx_Buffer[1]==0xe2&Rx_Buffer[2]==0xe3&Rx_Buffer[3]==0x1e&Rx_Buffer[4]==0x2e)
		{
			  printf("go into Bootloader........!")	;
			
			  BKP_WriteBackupRegister(BKP_DR1, 0xABCD);  //改写标志位
			
			  send(SOCK_TCPS,Rx_Buffer,5);
				//Write_SOCK_Data_Buffer(s, Rx_Buffer,5,S0_Port);
				SCB->AIRCR =0X05FA0000|(u32)0x04;      	     //复位重启
		}
}

总结:

IAP技术的实现,重点要搞清楚Flash的操作,以及通信控制流的设计。抓住这里两个要点,其他的迎刃而解。

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值