{alertInfo}遠端韌體更新是嵌入式系統開發者不可或缺的方便工具之一,筆者使用TI 的TMS320F280025C 並搭配 Raspberry Pi 作為傳送更新的系統,透過 I2C 介面將編譯好的 .hex 韌體檔案由 Raspberry Pi傳送到 MCU ,並且正確執行更新後的應用程式。這篇文章著重在 MCU 啟動程序 Bootloader的工作原理,以及切換韌體的方法。還有說明在同一塊 Flash之中存在兩種韌體時,要如何切換與執行。
目錄
前言
前一篇文章:微處理機韌體更新設計實例 (一) 流程設計一文中說明韌體更新功能流程,以及主要思路。這篇文章要說明 MCU啟動的流程以及切換韌體版本的方法。現在多數的 MCU都有內建的 Flash可用來儲存程式碼,它的大小依據型號不同而有所不同,有 8MB、16MB,一直到 512MB等等,記憶體越大價格越貴,但設計彈性也越高。特定系列的 C2000 MCU有特別設計 Dual Flash Bank(如::F280039C),原廠考慮到韌體更新的優勢以及無窮的可能性,設計兩組 Flash Bank 在 MCU內,以實現不需要中斷當前程式或者重置系統就更新韌體的功能。程式先將當前正在執行的函式移動到 RAM中並持續運行,此時背景執行韌體更新的 Kernel Code。新韌體下載成功後,再跳轉到新的韌體上,實現無縫切換。詳細的設計文件可以參考: Live Firmware Update Without Device Reset on C2000™ MCUs。
但是我們使用的 F280025C 是一顆低階 MCU,沒有 Dual bank flash 的狀況下也進行更新呢?
那就要將唯一的 Flash Bank 手動切成兩相同大小的部分,再透過韌體切換機制跳轉主程式的入口。如同下圖的 Flash Bank0,起始位址是0x8000,大小是128KB。將他切兩半,另一半的起始位址就變成 0x88000,大小也降低成64KB。假設0x80000放的是原本的韌體,而0x88000就可以保留給新的韌體更新檔使用。
筆者在這邊多保留最後一段區域用來存放 constant variable,裡面放Firmware version,serial number 或者其他需要讀取的非揮發性變數。當新的韌體要更新的時候,MCU需要確認要傳來的韌體是不是對應到相同的產品型號,以及他的版本號是不是比當前版本更新,這段資料就需要存放在一個非揮發性記憶體區塊,也就是最後從 0x8F000到 0x8FFFE的地方。
前面的 0x80000也保留一小部份留給我們要寫的跳轉程式。
啟動引導程序 Bootloader
假設我們已經接收好一包完整的韌體,那我希望在下次重新開機的時候,MCU 可以執行新的韌體而不是舊的,我應該要怎麼切換呢?首先需要先了解大多數 MCU的啟動流程,可以參考:C2000上电引导模式解析------【TI FAE 经验分享】一文說明的內容,筆者整理成以下:
上電 -> POR(Power on reset) -> 重設硬體暫存器 -> 初始化時脈設定 -> 啟動程式執行 -> 檢查 Boot mode設定-> 主程式入口(使用者程式碼)
韌體更新的內容只會更新使用者程式碼的部分,前面的初始化設定都是相同的,所以從上面的流程中我們可以把切換韌體的機制設計在進入主程式入口之前,也就是下方的流程:
上電 -> POR(Power on reset) -> 重設硬體暫存器 -> 初始化時脈設定 -> 啟動程式執行 -> 檢查 Boot mode設定-> 韌體切換機制 -> 主程式入口(使用者程式碼)
加入自訂義的韌體切換機制後,MCU 就可以自由選擇要進入的程式碼入口,進而切換執行的程式碼,而類型這個韌體切換機制就是 Bootloader。
Bootloader 又分成硬引導以及軟引導,硬引導負責開機通電後的硬底診斷或重置,軟引導則是切換作業系統或軟體。
以上述的啟動流程來說其實有兩個 Bootloader,第一個是檢查 Boot mode設定這個步驟,他是由 TI原廠的 Bootloader負責,設定 MCU的啟動方式;下一個是我們自己撰寫的"韌體切換機制",透過檢查指定 Flash位址的資訊來判斷是否有心韌體需要跳轉。
TI 原廠的 Bootloader做的事情可以參考:TMS320F28x Boot Features and Configurations,文件說明了啟動模式的設定,此外,MCU 自己的TRM 也有說明 Boot mode select pin的設定與對應的模式。
雙重引導 Second bootloader
除了上述的第一階段引導程序之外,我們要撰寫一個自己的引導程序,讓 MCU可以決定要執行 0x80000的程式碼,還是0x88000的新版本程式碼。這段程式碼在第一階段引導程序之後,所以就是第二階段引導程序,兩個加在一起就是雙重引導。
這功能聽起來用一個 if-else就可以完美解決了吧?
答案是... 沒錯!就是用一個條件式就可以解決這件事情,那條件如何撰寫呢?我們可以用變數去判斷嗎?
這時候就不行了。程式碼內宣告的變數都會被放置在 RAM中,當 MCU斷電後變數便不復存在,必須找一個斷電仍可維持的記憶體來記錄這個更動,於是先前提到的 0x8F000這個小區塊就派上用場了!
藉由將變數存在 Flash內,即使 MCU斷電我們也能讀到變更,讓我們自己的引導程序知道是不是要執行新的韌體,這也是開發的重點之一!
二階引導程序的跳轉方法
首先,先定義好會使用到的變數以及他實際存放在 Flash的位址,要操作 Flash也要針對上一篇文章提到的 link command file 下手,在 MEMORY的地方設定資料段落名稱,起始位址以及長度:
// flash address of variable please reference to "0025PM_factory_bootloader.cmd"
const uint32_t fw_branch_addr = 0x08F000; // FW status flash address, factory = 100, updated = 101
const uint32_t fw_number_addr = 0x08F008; // FW version number flash address
const uint32_t dev_number_addr = 0x08F010; // Device number address
const uint32_t flash_data1_addr = 0x08F028; // Flash test data address
FW_STATUS : origin = 0x08F000, length = 0x000008 /* on-chip Flash */
FW_NUMBER : origin = 0x08F008, length = 0x000008 /* on-chip Flash */
DEVICE_NUMBER : origin = 0x08F010, length = 0x000018 /* on-chip Flash */
FLASH_DATA1 : origin = 0x08F028, length = 0x000040 /* on-chip Flash */
FLASH_BANK0_SEC15 : origin = 0x08F068, length = 0x000F88 /* on-chip Flash */
FLASH_BANK0_SEC15_DO_NOT_USE : origin = 0x08FFF0, length = 0x000010 /* Reserve and do not use for code as per the errata advisory "Memory: Prefetching Beyond Valid Memory" */
在 SECTION的地方把變數名稱與資料段落名稱綁定再一起:
retain : > FLASH_BANK0_SEC15, ALIGN(8) // storage const variables
firmware_status : > FW_STATUS
firmware_number : > FW_NUMBER
device_number : > DEVICE_NUMBER
flash_data : > FLASH_DATA1
main_app : > FLASH_BANK0_SEC2
此外,在原始碼中加入 #pragma指令,告訴編譯器將變數放到指定的 Data section中:
// retain section makes variable not optimize by compiler
// These number should only be write once when product shipment
#pragma DATA_SECTION(FW_STATUS, "firmware_status"); // TODO: rename to Entry point?
#pragma RETAIN (FW_STATUS);
const uint16_t FW_STATUS = ORIGINAL; // ORIGINAL = 0 /MODIFIED = 1
#pragma DATA_SECTION(FW_NUMBER, "firmware_number");
#pragma RETAIN (FW_NUMBER);
const uint16_t FW_NUMBER[2] = {0x01, 0x01}; // firmware number factory: 0x01 0x01
#pragma DATA_SECTION(DEVICE_NUMBER, "device_number");
#pragma RETAIN (DEVICE_NUMBER);
const char DEVICE_NUMBER[22] = "POWER-1234567"; // Product Number
#pragma DATA_SECTION(remain, "flash_data");
#pragma RETAIN (remain);
const char remain[33] = "THIS IS DATA TO BE ERASE IN FLASH"; // test info
以上這些小調整可以讓變數存在 Flash中,不會在編譯過程被編譯器分配 RAM裡面,關於"retain" 的使用說明還有 #pragma 的說明請參考:TMS320C28x Optimizing C/C++ Compiler v22.6.0.LTS 編譯器最佳化說明。
但還沒有結束!!
接下來才是跳轉韌體的重點,f28002x_codestartbranch.asm 檔案是離開 TI Bootloader第一個被執行的程式,他檢查 watchdog並且將程式跳轉到 _c_init00,這裡非常重要,_c_init00就是程式碼中 main()迴圈的進入點,我們只要改變這個地方就可以改變我們要執行的程式是新韌體還是舊韌體。我們撰寫一個 App_branch 作為切換 App的程式,程式如下:
/* @brief branch to 0x88000 if firmware status is MODIFIED
* @param none
* @return none
*/
#pragma CODE_SECTION(App_branch, "codestart");
void App_branch(void){
uint16_t branch_direction = 0;
#ifdef mov2ram
memcpy(&RamfuncsRunStart, &RamfuncsLoadStart, (size_t)&RamfuncsLoadSize);
memcpy(&IQfuncsRunStart, &IQfuncsLoadStart, (size_t)&IQfuncsLoadSize);
InitFlash();
InitFlashAPI();
#endif
DeviceInit(); // Initialize device, including CPUTimer
// branch_direction branch App to where
branch_direction = HWREG((uint32_t)fw_branch_addr);
if(branch_direction != 0)
asm(" LB 0x88000");
else
_c_int00();
}
我們用 App_branch取代本來要被執行的 _c_init00,再利用 HWREG直接讀取Flash address,判斷是否需要跳轉程序。如果是,就直接用組合語言的 LB指令跳轉程式碼;反之若不是,就繼續執行原本該被執行的 _c_init00。
是不是很簡單呢?....不是...我參考了 TI所有關於韌體更新的手冊,編譯器最佳化的方法,Flash API的使用,linker command file的編寫(尤其官方給的超不完整,很多東西模稜量可)才找出 App跳轉的方法,但看到現在的你已經站在我的肩膀上做事了! 希望這篇文章可以幫助到你。
張貼留言
留個言吧