導言
想象一下,我們的單片機 App 正在穩定地運行著,突然我們想給它升級一下,添加個新功能。我們該如何安全地通知它:“嘿,準備好接收新固件了” ? 這就需要 App 和 Bootloader 之間建立一個可靠的"秘密握手"機制。
整個流程就像一次精心策劃的"交接儀式":
- 外部通知:上位機(比如我們的電腦)通過串口向正在運行的 App 發送一個預設好的"升級"指令。
- 設置信物:App 收到指令后,并不會立刻停止工作,而是在一塊共享的 RAM 內存中寫入一個"暗號"(我們稱之為"升級標志位")。這塊內存就像是 App 留給 Bootloader 的一個"秘密信箱"。
- 主動重啟:留下"暗號"后,App 主動觸發一次軟件復位,開始"交接"。
- 驗證信物:系統重啟后,控制權首先交給 Bootloader。Bootloader 會立刻檢查那個"秘密信箱"。
- 如果發現了預設的"暗號",它就知道:“哦,App 需要我來執行升級任務”,隨即進入 IAP(In-Application Programming)模式,準備接收新固件。
- 如果信箱是空的,它就認為一切正常,直接跳轉到 App,讓 App 像往常一樣運行。
為了簡化演示,我們約定,當 App 的串口收到字符串 A5A5A5A5
時,就觸發這次跳轉。
友情提示:在真實的商業項目中,簡單的字符串匹配是遠遠不夠的。為了保證通信的絕對可靠,防止數據干擾導致意外的固件升級,我們必須引入 CRC 校驗 等錯誤檢測機制。這里的
A5A5A5A5
只是一個為了教學目的而簡化的觸發信號。
項目地址:
- Gitee (國內推薦): https://gitee.com/wallace89/MCU_Develop/tree/main/bootloader11_stm32f103_app_jump_boot
- GitHub: https://github.com/q164129345/MCU_Develop/tree/main/bootloader11_stm32f103_app_jump_boot
一、App 端代碼
現在,我們來看看 App 程序需要增加哪些代碼來實現這個"交接儀式"。
1.1、 jump_boot.c
這個文件負責處理從 App 跳轉到 Bootloader 的所有核心邏輯。
#include "bootloader_define.h"
#include "flash_map.h"
#include "jump_boot.h"#if defined(__IS_COMPILER_ARM_COMPILER_5__)
volatile uint64_t update_flag __attribute__((at(FIRMWARE_UPDATE_VAR_ADDR), zero_init));#elif defined(__IS_COMPILER_ARM_COMPILER_6__)#define __INT_TO_STR(x) #x#define INT_TO_STR(x) __INT_TO_STR(x)volatile uint64_t update_flag __attribute__((section(".bss.ARM.__at_" INT_TO_STR(FIRMWARE_UPDATE_VAR_ADDR))));#else#error "variable placement not supported for this compiler."
#endif/*** @brief 獲取固件升級標志位* @note 讀取保存于指定RAM地址的升級標志變量(通常用于判斷bootloader的運行狀態)* @retval uint64_t 固件升級標志的當前值*/
static uint64_t IAP_GetUpdateFlag(void)
{return update_flag;
}/*** @brief 設置固件升級標志位* @param flag 需要設置的標志值* @note 修改指定RAM地址的升級標志變量* @retval 無*/
static void IAP_SetUpdateFlag(uint64_t flag)
{update_flag = flag;
}/*** @brief 解析串口接收到的數據* @note 根據接收到的數據,判斷是否需要跳轉到Bootloader* 目標字符串: "A5A5A5A5" (8字節)* ASCII: 0x41,0x35,0x41,0x35,0x41,0x35,0x41,0x35* @param data 接收到的數據* @retval 無*/
void IAP_Parse_Command(uint8_t data)
{// 目標字符串"A5A5A5A5"的ASCII碼序列static const uint8_t target_sequence[8] = {0x41, 0x35, 0x41, 0x35, // "A5A5"0x41, 0x35, 0x41, 0x35 // "A5A5"};// 靜態變量:記錄當前匹配的字節位置static uint8_t match_index = 0;// 檢查當前字節是否與目標序列匹配if (data == target_sequence[match_index]) {match_index++; // 匹配成功,移動到下一個位置// 檢查是否接收完整的8字節序列if (match_index >= sizeof(target_sequence)) {// 完整匹配成功,跳轉到Bootloadermatch_index = 0; // 重置狀態,為下次做準備//! 設置固件升級標志位IAP_SetUpdateFlag(FIRMWARE_UPDATE_MAGIC_WORD);//! 等待10ms,確保標志位設置成功LL_mDelay(10);//!此函數不會返回,MCU將復位NVIC_SystemReset();}} else {// 不匹配,重置狀態機match_index = 0;// 特殊處理:如果當前字節恰好是序列的第一個字節,則開始新的匹配if (data == target_sequence[0]) {match_index = 1;}}
}
代碼亮點解析:
-
update_flag
變量的定義:__attribute__((at(...)))
: 這是告訴編譯器:“請務必把update_flag
這個變量放在FIRMWARE_UPDATE_VAR_ADDR
這個內存地址上”。這就像給 App 和 Bootloader 一個共同的、固定的"信箱"地址,確保雙方都能找到對方。volatile
: 這個關鍵字至關重要。它告訴編譯器:“這個變量的值隨時可能被意想不到的方式改變(比如被硬件或其他程序),所以你不要自作聰明地去優化它。每次使用它的時候,都必須老老實實地從內存里重新讀取”。這可以防止 App 重啟后,Bootloader 讀到一個被緩存的、不正確的值。
-
IAP_Parse_Command
函數:- 這個函數就像一個"哨兵",時刻監聽著串口發來的每一個字節。
- 它內部的
match_index
就像一個進度條,記錄著"秘密口令"的匹配進度。每收到一個正確的字符,進度條就加一;一旦收到錯誤的字符,進度條就清零重來,非常嚴謹。 - 當8個字符全部匹配成功,就意味著"口令正確",哨兵就會立刻執行預設的三個步驟:寫標志、延時、復位。
1.2、jump_boot.h
頭文件很簡單,主要是聲明 IAP_Parse_Command
函數,以便在其他文件中(比如串口中斷服務程序中)調用它。
/*** @file jump_boot.h* @brief 應用程序與Bootloader跳轉功能的頭文件聲明* @author Wallace.zhang* @date 2025-05-25* @version 1.0.0*/#ifndef __JUMP_BOOT_H
#define __JUMP_BOOT_H#ifdef __cplusplus
extern "C" {
#endif#include <stdint.h>
#include "main.h"
#include <stdbool.h>/*** @brief 解析串口接收到的數據,判斷是否需要跳轉到Bootloader* @param data 接收到的單個字節*/
void IAP_Parse_Command(uint8_t data);#ifdef __cplusplus
}
#endif#endif /* __JUMP_BOOT_H */
1.3、myUsartDrive_reg.c
如上圖所示,在USART1的接收ringbuffer里,一個一個字節拿出去解析。
二、功能測試
現在,讓我們來驗證一下"交接儀式"是否能順利進行。
2.1、測試 App 跳轉到 Bootloader
我們使用一個 Python 腳本來模擬上位機,向單片機發送"秘密口令"。
操作步驟:
- 將 App 程序下載到 STM32 開發板并運行。
- 在項目
iap_py
文件夾下打開終端,運行以下指令(請根據你的實際情況修改 COM 口):python3 jump_command.py --port COM8 --baud 115200
- 觀察 RTT Viewer 的日志。Bootloader 的日志出現,這表明 App 成功復位并跳轉到了 Bootloader!
jump_command.py
腳本是做什么的?
它其實很簡單,就是通過指定的串口(--port
)和波特率(--baud
),向上位機發送了一串文本A5A5A5A5
。
腳本參數說明:
--port
: 你的設備所連接的串口號 (必填),例如COM8
(Windows) 或/dev/ttyUSB0
(Linux)。--baud
: 波特率 (可選),默認為115200
。