自己寫一個STM32 Bootloader

個人

前言

自從開始工作之後,由於工作的關係,平常寫的程式從韌體慢慢轉變成了軟體,也因此都沒什麼碰MCU,在之前也把手上所有的Ti開發板全都送了出去。
但也因為在職場見到STM32在這產業的大量應用,因此於近期開始學習STM32的開發。也開始規劃一些Side project,藉此提升自己在韌體的開發能力。

外面的32是如何執行程式?

在開發MCU韌體的時候,如果是自己畫的電路板,都會掛著一個很像「尿袋」的燒錄器/偵錯器。如STLink / JLink…等。

此時工程師可以在支援的IDE上,直接透過燒錄器/偵錯器,燒錄剛寫好的韌體到電路板上MCU。也可以透過特定的幾根線直接跟MCU溝通,在偵錯模式下執行Code。

但若交到客戶手上,大部分都是可以透過一根USB線,或者是一個UART Dongle(如:CH340 / CP2102…等)燒錄新的韌體。

這種燒錄方式稱為:In Application Programming(簡稱IAP),可以透過一段預先寫好的程式(意即Bootloader),將新的binary code燒錄至MCU。

執行流程

如果有在玩Arduino,應該知道Arduino板子內都有個Bootloader。

Bootloader像是電腦中的BIOS。在STM32,最基本的Bootloader與App Program在FLASH內的占比如下:

在STM32開機的時候,若有事先燒錄Bootloader,則一般Bootloader的執行流程如下:

  • 印出基本的裝置訊息
  • 偵測與與進入新的程式燒錄模式
  • 到另一個記憶體段執行應用程式

開發過程

由於不想一個Side Project配合獨立一個程式,於是配合之前定好的的通訊協議,開始著手撰寫自己的Bootloader。

Bootloader - 前置作業

首先設定燒錄程式的大小,我這裡使用STM32F407ZG這塊ROM有1MB的MCU,所以我規劃64KB當作Bootloader。

跳入另一段程式

要讓STM32進入另一個程式執行,首先:

  1. 檢查該位置是否有寫入正確的Stack Address。(通常都會當作是檢查是否有程式的一個條件)
  2. 找到該位置的Reset Handler Address。(大部分會在Stack Address+4位)
  3. Deinit所有用到的外部IO,否則進入另一個區塊執行程式會出現不明的錯誤。
  4. 透過函數指標跳至另一個Address,開始執行另一段程式。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #define USER_APP_BASE_ADDRESS			0x08010000U	// Bootloader: 64KB
    #define USER_APP_RESET_HANDLER_ADDRESS (USER_APP_BASE_ADDRESS + 4)

    // just a function pointer to hold the address of the reset handler of the user app
    typedef void (*pFunction)(void);

    void iap_jump_to_app(void)
    {
    uint32_t jump_address = USER_APP_RESET_HANDLER_ADDRESS;
    pFunction jmp;

    /* 0. Check if vaild stack address (RAM Address). */
    if (((*(__IO uint32_t *)USER_APP_BASE_ADDRESS) & 0x2FFE0000) == 0x20000000)
    {
    /* 1. configure the app_address by reading the value from base address. */
    jump_address = *(__IO uint32_t *)jump_address;
    /* 2. Fetch The reset handler address (jump_address) of the user app. */
    jmp = (pFunction)jump_address;

    // 3. Deinit Any Perphrial
    HAL_GPIO_DeInit(User_LED_GPIO_Port, User_LED_Pin);
    HAL_UART_MspDeInit(&huart1);
    HAL_RCC_DeInit();
    HAL_DeInit();

    /* 4. Jump to the reset handler of the user app. */
    __set_MSP(jump_address); // This Function comes from CMSIS.
    jmp();
    }
    }

燒錄程式

相比跳入另一段程式執行,燒錄程式的動作就複雜許多:

進入燒錄模式

首先要先讓STM32知道是否進入燒錄模式。

這裡做法通常有很多種,可以用按鍵的方式,也可以透過UART / CAN傳一個指令給STM32。

傳入新的程式大小

再來將新的程式長度傳至STM32,讓32知道等等要燒錄多大的程式。(這裡長度用bytes表示)

1
2
3
4
void iap_set_new_firmware_length(uint32_t length)
{
new_firmware_length = length;
}

清除記憶體

在此感謝同事提點我這步驟的重要性。

在燒錄程式之前,需要將原本儲存程式的記憶體區塊抹除前一份資料,否則燒錄過程中若出現問題,在執行的時候就會發生不可預期的錯誤。

  • 清除記憶體前需要解鎖Flash,清除完畢後需要重新將Flash上鎖。
  • STM32官方的HAL Library內提供的方式,僅有全部清除、與清除特定的記憶體區塊兩種。這裡我選擇清除特定的記憶體區塊。透過其他的方式實現全部清除。
  • 我手上的STM32F407ZG、供電的電壓為3.3V,在VoltageRange選擇RANGE_3 (2.7V ~ 3.6V)即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void iap_erase_flash_sector(uint32_t sector)
{
FLASH_EraseInitTypeDef erase;
erasing_or_complete_t erase_info;
uint32_t sectorError = 0;

// Configure Erase
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
erase.Sector = sector;
erase.NbSectors = 1;

// Unlock The Flash
HAL_FLASH_Unlock();

// Erase The Flash
if (HAL_FLASHEx_Erase(&erase, &sectorError) != HAL_OK)
{
// Error Erase
Error_Handler();
}

// Lock The Flash
HAL_FLASH_Lock();
}

燒錄新程式

到此為止就可以燒錄新的程式碼,我這裡配合之前寫好的協議,每次會傳入16個為新的程式Bytes。

  • 燒錄前與清除完畢後,如同清除記憶體方式。
  • STM32官方的HAL Library內提供的方式,在每次僅能燒錄4個bytes的資料,燒錄完畢後需往後4個記憶體位置。
  • 為了避免燒過頭,我在燒錄完畢後加入長度偵測,避免觸碰到其他記憶體位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void iap_write_flash(uint32_t * data)
{
int i = 0;

// Unlock The Flash
HAL_GPIO_WritePin(User_LED_GPIO_Port, User_LED_Pin, GPIO_PIN_RESET);
HAL_FLASH_Unlock();

// Fill 4 Bytes Data To Flash
for (i = 0; i < 4; i++)
{
// Fill New Bytes
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, user_app_flash_address, data[i]) != HAL_OK)
{
// Error Fill
Error_Handler();
}

user_app_flash_address += 4; // To Next Address

// If The Length Is Over, Then Break The Loop.
if (user_app_flash_address >= USER_APP_BASE_ADDRESS + new_firmware_length)
{
break;
}
}

// Lock The Flash
HAL_GPIO_WritePin(User_LED_GPIO_Port, User_LED_Pin, GPIO_PIN_SET);
HAL_FLASH_Lock();
}

重新開機

燒錄完畢將STM32重新開機,我這裡使用Internal WatchDog (IWDG)進行重新開機的動作。

1
2
3
4
5
void iap_reboot(void)
{
// Init & Start The IWDG
MX_IWDG_Init();
}

App - 前置作業

相比Bootloader,App的記憶體位置需要比Bootloader還要後面,同時修改的地方也多。

  • ROM開始位置為0x0801_0000、長度為0xF0000。(對應64KB的Bootloader,App的大小為960KB)`。
  • 燒錄範圍與ROM開始位置的設定相同。

設定偏移位置

system_stm32xxx.c定義USER_VECT_TAB_ADDRESS,且在下方的VECT_TAB_OFFSET設定APP程式的於記憶體中偏移的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define USER_VECT_TAB_ADDRESS

#if defined(USER_VECT_TAB_ADDRESS)
/*!< Uncomment the following line if you need to relocate your vector Table
in Sram else user remap will be done in Flash. */
/* #define VECT_TAB_SRAM */
#if defined(VECT_TAB_SRAM)
#define VECT_TAB_BASE_ADDRESS SRAM_BASE /*!< Vector Table base address field.
This value must be a multiple of 0x200. */
#define VECT_TAB_OFFSET 0x00000000U /*!< Vector Table base offset field.
This value must be a multiple of 0x200. */
#else
#define VECT_TAB_BASE_ADDRESS FLASH_BASE /*!< Vector Table base address field.
This value must be a multiple of 0x200. */
#define VECT_TAB_OFFSET 0x00010000U /*!< Vector Table base offset field.
This value must be a multiple of 0x200. */
#endif /* VECT_TAB_SRAM */
#endif /* USER_VECT_TAB_ADDRESS */

編譯bin檔案

通常給使用者的程式有兩種:Hex與Bin,我這裡選擇使用產生Bin檔案的方式。
某些IDE在編譯的時候會產生Bin檔案,但部分IDE。如Keil,需要Configure內手動輸入以下指令額外編譯。

  • Configure位置:魔術棒按鈕 > User > After Build/Rebuild
1
fromelf --bin ".\@L\@L.axf" --output ".\obj_bin\@L.bin"

燒錄程式

由於這次我使用的的是自己的協議撰寫Bootloader,為了驗證可行性,我額外用C#寫了套在電腦的燒錄程式。程式只有幾樣功能:

  1. 獲取32的基本資訊
  2. 重新開機
  3. 進入燒錄模式,傳送新的程式給STM32

成果

參考資料

Comments

Unable to load Disqus, please make sure your network can access.