MCU 啟動和向量表
當 STM32F429 MCU 啟動時,它會從 flash 存儲區最前面的位置讀取一個叫作 “向量表” 的東西。“向量表” 的概念所有 ARM MCU 都通用,它是一個包含 32 位中斷處理程序地址的數組。對于所有 ARM MCU,向量表前 16 個地址由 ARM 保留,其余的作為外設中斷處理程序入口,由 MCU 廠商定義。越簡單的 MCU 中斷處理程序入口越少,越復雜的 MCU 中斷處理程序入口則會更多。
STM32F429 的向量表在數據手冊表 62 中描述,我們可以看到它在 16 個 ARM 保留的標準中斷處理程序入口外還有 91 個外設中斷處理程序入口。
在向量表中,我們當前對前兩個入口點比較感興趣,它們在 MCU 啟動過程中扮演了關鍵角色。這兩個值是:初始堆棧指針和執行啟動函數的地址(固件程序入口點)。
所以現在我們知道,我們必須確保固件中第 2 個 32 位值包含啟動函數的地址,當 MCU 啟動時,它會從 flash 讀取這個地址,然后跳轉到我們的啟動函數。
最小固件
現在我們創建一個 main.c
文件,指定一個初始進入無限循環什么都不做的啟動函數,并把包含 16 個標準入口和 91 個 STM32 入口的向量表放進去。用你常用的編輯器創建 main.c
文件,并寫入下面的內容:
// Startup code
__attribute__((naked, noreturn)) void _reset(void) {
for (;;) (void) 0; // Infinite loop
}
extern void _estack(void); // Defined in link.ld
// 16 standard and 91 STM32-specific handlers
__attribute__((section(".vectors"))) void (*tab[16 + 91])(void) = {
_estack, _reset
};
對于 _reset()
函數,我們使用了 GCC 編譯器特定的 naked
和 noreturn
屬性,這意味著標準函數的進入和退出不會被編譯器創建,這個函數永遠不會返回。
void (*tab[16 + 91])(void)
這個表達式的意思是:定義一個 16+91 個指向沒有返回也沒有參數的函數的指針數組,每個這樣的函數都是一個中斷處理程序,這個指針數組就是向量表。
我們把 tab
向量表放到一個獨立的叫作 .vectors
的區段,后面需要告訴鏈接器把這個區段放到固件最開始的地址,也就是 flash 存儲區最開始的地方。前 2 個入口分別是:堆棧指針和固件入口,目前先把向量表其它值用 0 填充。
編譯
我們來編譯下代碼,打開終端并執行:
$ arm-none-eabi-gcc -mcpu=cortex-m4 main.c -c
成功了!編譯器生成了 main.o
文件,包含了最小固件,雖然這個固件程序什么都沒做。這個 main.o
文件是 ELF 二進制格式的,包含了多個區段,我們來具體看一下:
$ arm-none-eabi-objdump -h main.o
...
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000002 00000000 00000000 00000034 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00000000 00000000 00000036 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000036 2**0
ALLOC
3 .vectors 000001ac 00000000 00000000 00000038 2**2
CONTENTS, ALLOC, LOAD, RELOC, DATA
4 .comment 0000004a 00000000 00000000 000001e4 2**0
CONTENTS, READONLY
5 .ARM.attributes 0000002e 00000000 00000000 0000022e 2**0
CONTENTS, READONLY
注意現在所有區段的 VMA/LMA 地址都是 0,這表示 main.o
還不是一個完整的固件,因為它沒有包含各個區段從哪個地址空間載入的信息。我們需要鏈接器從 main.o
生成一個完整的固件 firmware.elf
。
.text
區段包含固件代碼,在上面的例子中,只有一個 _reset()
函數,2 個字節長,是跳轉到自身地址的 jump
指令。.data
和 .bss
(初始化為 0 的數據) 區段都是空的。我們的固件將被拷貝到偏移 0x8000000 的 flash 區,但是數據區段應該被放到 RAM 里,因此 _reset()
函數應該把 .data
區段拷貝到 RAM,并把整個 .bss
區段寫入 0。現在 .data
和 .bss
區段是空的,我們修改下 _reset()
函數讓它處理好這些。
為了做到這一點,我們必須知道堆棧從哪開始,也需要知道 .data
和 .bss
區段從哪開始。這些可以通過 “鏈接腳本” 指定,鏈接腳本是一個帶有鏈接器指令的文件,這個文件里存有各個區段的地址空間以及對應的符號。
鏈接腳本
創建一個鏈接腳本文件 link.ld
,然后把一下內容拷進去:
ENTRY(_reset);
MEMORY {
flash(rx) : ORIGIN = 0x08000000, LENGTH = 2048k
sram(rwx) : ORIGIN = 0x20000000, LENGTH = 192k /* remaining 64k in a separate address space */
}
_estack = ORIGIN(sram) + LENGTH(sram); /* stack points to end of SRAM */
SECTIONS {
.vectors : { KEEP(*(.vectors)) } > flash
.text : { *(.text*) } > flash
.rodata : { *(.rodata*) } > flash
.data : {
_sdata = .; /* .data section start */
*(.first_data)
*(.data SORT(.data.*))
_edata = .; /* .data section end */
} > sram AT > flash
_sidata = LOADADDR(.data);
.bss : {
_sbss = .; /* .bss section start */
*(.bss SORT(.bss.*) COMMON)
_ebss = .; /* .bss section end */
} > sram
. = ALIGN(8);
_end = .; /* for cmsis_gcc.h */
}
下面分段解釋下:
ENTRY(_reset);
這行是告訴鏈接器在生成的 ELF 文件頭中 “entry point” 屬性的值。沒錯,這跟向量表重復了,這個的目的是為像 Ozone 這樣的調試器設置固件起始的斷點。調試器是不知道向量表的,所以只能依賴 ELF 文件頭。
MEMORY {
flash(rx) : ORIGIN = 0x08000000, LENGTH = 2048k
sram(rwx) : ORIGIN = 0x20000000, LENGTH = 192k /* remaining 64k in a separate address space */
}
這是告訴鏈接器有 2 個存儲區空間,以及它們的起始地址和大小。
_estack = ORIGIN(sram) + LENGTH(sram); /* stack points to end of SRAM */
這行告訴鏈接器創建一個 _estack
符號,它的值是 RAM 區的最后,這也是初始化堆棧指針的值。
.vectors : { KEEP(*(.vectors)) } > flash
.text : { *(.text*) } > flash
.rodata : { *(.rodata*) } > flash
這是告訴鏈接器把向量表放在 flash 區最前,然后是 .text
區段(固件代碼),再然后是只讀數據 .rodata
。
.data : {
_sdata = .; /* .data section start */
*(.first_data)
*(.data SORT(.data.*))
_edata = .; /* .data section end */
} > sram AT > flash
_sidata = LOADADDR(.data);
這是 .data
區段,告訴鏈接器創建 _sdata
和 _edata
兩個符號,我們將在 _reset()
函數中使用它們將數據拷貝到 RAM。
.bss : {
_sbss = .; /* .bss section start */
*(.bss SORT(.bss.*) COMMON)
_ebss = .; /* .bss section end */
} > sram
.bss
區段也是一樣。
啟動代碼
現在我們來更新下 _reset
函數,把 .data
區段拷貝到 RAM,然后把 .bss
區段初始化為 0,再然后調用 main()
函數,在 main()
函數有返回的情況下進入無限循環:
int main(void) {
return 0; // Do nothing so far
}
// Startup code
__attribute__((naked, noreturn)) void _reset(void) {
// memset .bss to zero, and copy .data section to RAM region
extern long _sbss, _ebss, _sdata, _edata, _sidata;
for (long *src = &_sbss; src < &_ebss; src++) *src = 0;
for (long *src = &_sdata, *dst = &_sidata; src < &_edata;) *src++ = *dst++;
main(); // Call main()
for (;;) (void) 0; // Infinite loop in the case if main() returns
}
下面的框圖演示了 _reset()
如何初始化 .data
和 .bss
:
firmware.bin
文件由 3 部分組成:.vectors
(中斷向量表)、.text
(代碼)、.data
(數據)。這些部分根據鏈接腳本被分配到不同的存儲空間:.vectors
在 flash 的最前面,.text
緊隨其后,.data
則在那之后很遠的地方。.text
中的地址在 flash 區,.data
在 RAM 區。例如,一個函數的地址是 0x8000100
,則它位于 flash 中。而如果代碼要訪問 .data
中的變量,比如位于 0x20000200
,那里將什么也沒有,因為在啟動時 firmware.bin
中 .data
還在 flash 里!這就是為什么必須要在啟動代碼中將 .data
區段拷貝到 RAM。
現在我們可以生成完整的 firmware.elf
固件了:
$ arm-none-eabi-gcc -T link.ld -nostdlib main.o -o firmware.elf
再次檢驗 firmware.elf
中的區段:
$ arm-none-eabi-objdump -h firmware.elf
...
Sections:
Idx Name Size VMA LMA File off Algn
0 .vectors 000001ac 08000000 08000000 00010000 2**2
CONTENTS, ALLOC, LOAD, DATA
1 .text 00000058 080001ac 080001ac 000101ac 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
...
可以看到,.vectors
區段在 flash 的起始地址 0x8000000,.text
緊隨其后。我們在代碼中沒有創建任何變量,所以沒有 .data
區段。
燒寫固件
現在可以把這個固件燒寫到板子上了!
先把 firmware.elf
中各個區段抽取到一個連續二進制文件中:
$ arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
然后使用 st-link
工具將firmware.bin
燒入板子,連接好板子,然后執行:
$ st-flash --reset write firmware.bin 0x8000000
這樣就把固件燒寫到板子上了。
評論
查看更多