目錄
一、功能描述?
1、BootLoader部分:
2、APP部分:
二、BootLoader程序制作
1、分區定義
2、 主函數
3、YMODEM協議的實現
4、程序跳轉
三、APP程序制作
四、工程配置(默認KEIL5)
五、運行測試?
結束語
概述
????????IAP(In Application Programming)即在應用中編程,允許在應用程序運行時更新或切換固件。STM32通過修改MSP(主堆棧指針)和PC(程序計數器)實現從不同地址啟動,包括Flash或RAM地址。默認情況下,嵌入式程序以連續二進制形式燒錄到STM32的可尋址Flash區域。若Flash容量足夠存儲多個完整程序,每個程序獨立且完整,上電后可通過修改MSP值選擇不同程序入口,從而實現多固件切換或升級。
?????????BootLoader(引導加載程序)是嵌入式系統或計算機啟動時運行的一段小型程序,負責初始化硬件、加載操作系統內核并將其控制權移交。它是系統從關機狀態到操作系統完全運行之間的橋梁。
????????所以,固件升級的基本思路是將stm32的flash劃分為若干個區域,其中包括BootLoader區域和APP區等,將各自的程序寫到對應的flash區域里,本文將采用YMODEM協議來接收固件,只重實踐不講原理。
一、功能描述?
????????使用STM32的串口總線和Ymodem協議實現串口IAP升級程序。將FLASH分為3個部分。
分區介紹:
????????本文使用stm32f103vet6,flash是512k,sector是1k,BootLoader整個代碼編譯下來有11K左右,所以使用0x08000000~0x00002FFF,SETTING主要存放升級標志位,使用0x08003000~0x00003FFF,剩下的FLASH將分為兩個部分都用來存放代碼,APP使用0x08004000~0x0807C000。(我這里已經極致壓縮FLASH了,如果大家要移植的話,肯定要修改各個區域大小的)
區域 | 起始地址 | 區域大小 | 功能 |
---|---|---|---|
BOOT | 0x08000000 | 0x00003000(12k) | 存放BootLoader程序 |
SETTING | 0x08003000 | 0x00001000(4k) | 存放升級標志位/其它掉電不丟失標志位 |
APP | 0x08004000 | 0x0007C000(496k) | 存放產品主程序 |
tips:?選擇以上分區方式的好處是在上電瞬間可以觸發升級,在進入主程序運行期間也可以觸發升級,這樣就算升級了錯誤的程序,只要復位了再次進行升級就行,就不會導致變磚了。如果選擇在主程序執行接收升級文件步驟,那如果程序有誤,就很大概率會變磚,當然也有辦法,那就是加校驗或者將bin文件加密,這部分就不做了。
1、BootLoader部分:
- 運行程序時首先從SETTING區域讀取升級標志位,如果需要升級就一直用串口發送字符C(Ymodem協議的一部分),否則就直接跳轉到APP。
- 上電長按KEY1并復位,直接設置升級標志位進入升級,一直發送字符C,用xshell來發送升級bin文件。先將APP區域的程序全部擦除掉,然后xshell會按照Ymodem協議發送128或1024字節給MCU,MCU分別寫入到APP區域內后復位即可實現升級程序,具體請查看本文源碼。
2、APP部分:
????????該部分需要在程序起始設置中斷向量跳轉指針,為了符合需求我用串口接收到‘1’就會設置程序更新標志位,復位后進入BootLoader升級。(大家也可以設置一個串口命令或者其它觸發方式,只要能寫更新標志位和復位就行)
二、BootLoader程序制作
????????主要包含了YMODEM協議的驅動代碼。
1、分區定義
#define FLASH_SECTOR_SIZE 1024 //MCU sector size
#define FLASH_SECTOR_NUM 512 // 512k
#define FLASH_START_ADDR ((uint32_t)0x08000000)
#define FLASH_END_ADDR ((uint32_t)(0x08000000 + FLASH_SECTOR_NUM * FLASH_SECTOR_SIZE))#define BOOT_SECTOR_ADDR 0x08000000 // BOOT sector start address
#define BOOT_SECTOR_SIZE 0x3000
#define SETTING_SECTOR_ADDR 0x08003000 // APP設置的boot升級標志位
#define SETTING_SECTOR_SIZE 0x1000
#define APP_SECTOR_ADDR 0x08004000 // APP sector start address
#define APP_SECTOR_SIZE 0x7C000 // APP sector size
#define APP_ERASE_SECTORS (APP_SECTOR_SIZE / FLASH_SECTOR_SIZE) //507904/1024=496#define SETTING_BOOT_STATE 0x08003000
#define START_PROGRAM_STATE 2
#define UPDATE_PROGRAM_STATE 3
#define UPDATE_SUCCESS_STATE 4typedef enum {NONE = 0,BUSY,START_PROGRAM, //進入APP主程序或者有更新就執行更新UPDATE_PROGRAM, //進入更新UPDATE_SUCCESS //更新成功寫標志位
}process_status; //更新狀態
2、 主函數
????????這部分包含了升級的所有狀態,該框架可以說對比上一篇文章是完全不變的,在UPDATE_PROGRAM需要一直發送字符C來請求數據,具體看代碼。
static void iap_process(void)
{process_status process;process = get_boot_state();switch (process) {case NONE:break;case START_PROGRAM:printf("start app...\r\n");delay_ms(50);if ((((*(vu32*)(APP_SECTOR_ADDR+4))&0xFF000000)==0x08000000)&&(!iap_load_app(APP_SECTOR_ADDR))) {printf("no program\r\n");delay_ms(1000);}printf("start app failed\r\n");break;case UPDATE_PROGRAM:ymodem_c();LED1_TOGGLE;delay_ms(1000);break;case UPDATE_SUCCESS:write_setting_boot_state(START_PROGRAM_STATE);NVIC_SystemReset();break;default:break;}
}int main(void)
{LED_GPIO_Config();ymodem_init();Key_GPIO_Config();printf("jin ru boot\r\n");set_boot_state(read_setting_boot_state());if(Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == 1){set_boot_state(UPDATE_PROGRAM);}while(1){iap_process();}
}
3、YMODEM協議的實現
? ? ? ? 接收固件按幀來接收的,默認使用1024個字節每幀接收,起始幀和結束幀(YMODEM_SOH)都不傳輸數據,數據幀(YMODEM_STX)以1024個字節接收固件,所以只需要接收YMODEM_STX,至于串口收發、隊列和定時器的配置就不說了,只可以意會不可言傳,反正根據YMODEM協議發完一幀需要MCU給應答才會發第二幀。
推薦以下兩篇文章學習YMODEM協議
Ymodem協議詳解-CSDN博客
Ymodem文件傳輸協議_ymodem協議-CSDN博客
#define YMODEM_SOH 0x01 // start of 128
#define YMODEM_STX 0x02 // start of 1024
#define YMODEM_EOT 0x04 // end of transmission
#define YMODEM_ACK 0x06 // positive acknowledge
#define YMODEM_NAK 0x15 // negative acknowledge
#define YMODEM_CA 0x18 // cancel終止
#define YMODEM_C 0x43 // control character 'C'
#define YMODEM_END 0x4F // control character 'O'關閉傳輸
void ymodem_ack(void)
{uint8_t buf;buf = YMODEM_ACK;Usart_Send_Data(&buf, 1);
}void ymodem_nack(void)
{uint8_t buf;buf = YMODEM_NAK;Usart_Send_Data(&buf, 1);
}void ymodem_c(void)
{uint8_t buf;buf = YMODEM_C;Usart_Send_Data(&buf, 1);
}void ymodem_end(void)
{uint8_t buf;buf = YMODEM_END;Usart_Send_Data(&buf, 1);
}void ymodem_start(ymodem_callback cb)
{if (ymodem.status == 0) {ymodem.cb = cb;}
}void ymodem_recv(download_buf_t *p)
{uint8_t type = p->data[0];switch (ymodem.status) {case 0:if (type == YMODEM_SOH) {ymodem.process = BUSY;ymodem.addr = APP_SECTOR_ADDR;mcu_flash_erase(ymodem.addr, APP_ERASE_SECTORS);ymodem_ack();ymodem_c();ymodem.status++;}break;case 1:if (type == YMODEM_SOH || type == YMODEM_STX) {if (type == YMODEM_SOH)//起始幀、結束幀這里可以不操作flash{mcu_flash_write(ymodem.addr, &p->data[3], 128);ymodem.addr += 128;}else {mcu_flash_write(ymodem.addr, &p->data[3], 1024);ymodem.addr += 1024;}ymodem_ack();}else if (type == YMODEM_EOT) {ymodem_nack();ymodem.status++;}else {ymodem.status = 0;}break;case 2:if (type == YMODEM_EOT) {ymodem_ack();ymodem_c();ymodem.status++;}break;case 3:if (type == YMODEM_SOH) {ymodem_ack();ymodem_end();ymodem.status = 0;ymodem.process = UPDATE_SUCCESS;}}p->len = 0;
}
4、程序跳轉
????????跳轉這部分網上也很多,基本沒什么區別,關于中斷可能要注意一下,可能跳轉之前,某些外設中斷是開啟的,跳轉之后,中斷產生了,但是APP代碼中沒有處理對應該中斷的中斷處理函數,所以就可能會直接死機。
tips:本文的開發環境只有64k ram,所以需要特別注意這里,雖然bootloader的ram不太可能會超。
if (((*(__IO uint32_t*)appxaddr) & 0x2FFF0000 ) == 0x20000000)?
詳細參考以下文章:
關于STM32單片機IAP升級中if(((*(__IO uint32_t*)ulAddr_App) & 0x2FFE0000) == 0x20000000)語句的理解-CSDN博客
uint8_t iap_load_app(uint32_t appxaddr)
{uint8_t i;uint32_t jump_addr;if (((*(__IO uint32_t*)appxaddr) & 0x2FFF0000 ) == 0x20000000) { jump_addr = *(__IO uint32_t*) (appxaddr + 4); jump2app = (iapfun)jump_addr; /* 關閉所有中斷,清除所有中斷掛起標志 */ for (i = 0; i < 8; i++){NVIC->ICER[i]=0xFFFFFFFF;NVIC->ICPR[i]=0xFFFFFFFF;} __set_MSP(*(__IO uint32_t*)appxaddr); jump2app();return 1;}return 0;
}
三、APP程序制作
????????這部分需要設置一下flash的偏移量,為了滿足需求,還需要在串口中斷函數添加觸發更新標志位。
NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x4000);
// 串口中斷服務函數
void USART1_IRQHandler(void)
{uint8_t ucTemp;if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET){ ucTemp = USART_ReceiveData(DEBUG_USARTx);USART_SendData(DEBUG_USARTx,ucTemp); if(ucTemp=='1'){write_setting_boot_state(UPDATE_PROGRAM_STATE);NVIC_SystemReset();}}
}
四、工程配置(默認KEIL5)
BootLoader部分:0x08000000~0x08002FFF
?
APP部分:0x08004000~0x0807C000
?
五、運行測試?
? ? ? ? 用串口助手執行升級操作,我用Xshell 8來操作(用其它支持YMODEM協議的串口助手也可以)。
1、配置Xshell 8。點擊文件新建->點擊連接->修改名稱(隨便改)和協議(選SERIAL)->點擊串口->修改常規設置確認(圖不截了,不會自己百度了解一下)
2、直接跳轉APP。根據下圖看到首先進來boot,然后直接跳轉APP了。
?
?3、進入更新。按下KEY1按鍵后復位板子會重新進入boot,然后進入更新,MCU會一直發送字符C請求數據,根據下圖發送bin文件給MCU接收。
可以看到已經更新成功,根據需求,大家也可以在主程序發送字符1給單片機也可以實現該功能。
tips: 測試完有個瑕疵,每次更新程序都需要把APP的全部扇區都擦除掉,那樣很費時間,當然也有解決的辦法,就是YMODEM協議可以抓取數據包的長度,根據數據包的長度去擦除固定的扇區,但是需要琢磨一下,我懶得弄,在我看來這個瑕疵是可以接受的。
結束語
????????以上基于YMODEM協議的串口IAP升級功能已實現,這只是其中的一種升級方式,后面大家看到的也希望可以得到大家的指點或者互動,感謝各位亦菲彥祖們了。下面給出了完整工程,也包含了另一種分區(BOOT+SETTING+APP+DOWNLOAD)的代碼。
完整代碼下載地址:
基于YMODEM協議的串口IAP升級固件資源-CSDN下載