基于STM32系列芯片的 IAP实现的探索
基于STM32系列芯片的 IAP实现的探索
- 什么是IAP?
- 如何实现IAP?
- 第一步:学习官方源代码
- 第二步:了解STM32芯片基本硬件参数
- 第三步、搞清除STM32内置Flash
- 第四步、 STM32程序运行机制
- IAP代码实现:
- 1、实现flash写入,删除,修改。
- 2、IAP 通信协议设计
- 3、APP代码部分修改
- 总结:
什么是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 STM32F103RBT6secoff=(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++; //扇区地址增1secoff=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字节写入FlashRx_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字节数加1Tx_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 0x50002、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的操作,以及通信控制流的设计。抓住这里两个要点,其他的迎刃而解。
与50位技术专家面对面20年技术见证,附赠技术全景图总结
以上是生活随笔为你收集整理的基于STM32系列芯片的 IAP实现的探索的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: STM32 进入Stop模式后电流还是很
- 下一篇: 索尼游戏手柄SP2的开发体会