基于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的开发体会