MCU 啟動(dòng)和向量表
當(dāng) STM32F429 MCU 啟動(dòng)時(shí),它會(huì)從 flash 存儲(chǔ)區(qū)最前面的位置讀取一個(gè)叫作 “向量表” 的東西。“向量表” 的概念所有 ARM MCU 都通用,它是一個(gè)包含 32 位中斷處理程序地址的數(shù)組。對(duì)于所有 ARM MCU,向量表前 16 個(gè)地址由 ARM 保留,其余的作為外設(shè)中斷處理程序入口,由 MCU 廠商定義。越簡(jiǎn)單的 MCU 中斷處理程序入口越少,越復(fù)雜的 MCU 中斷處理程序入口則會(huì)更多。
STM32F429 的向量表在數(shù)據(jù)手冊(cè)表 62 中描述,我們可以看到它在 16 個(gè) ARM 保留的標(biāo)準(zhǔn)中斷處理程序入口外還有 91 個(gè)外設(shè)中斷處理程序入口。
在向量表中,我們當(dāng)前對(duì)前兩個(gè)入口點(diǎn)比較感興趣,它們?cè)?MCU 啟動(dòng)過(guò)程中扮演了關(guān)鍵角色。這兩個(gè)值是:初始堆棧指針和執(zhí)行啟動(dòng)函數(shù)的地址(固件程序入口點(diǎn))。
所以現(xiàn)在我們知道,我們必須確保固件中第 2 個(gè) 32 位值包含啟動(dòng)函數(shù)的地址,當(dāng) MCU 啟動(dòng)時(shí),它會(huì)從 flash 讀取這個(gè)地址,然后跳轉(zhuǎn)到我們的啟動(dòng)函數(shù)。
最小固件
現(xiàn)在我們創(chuàng)建一個(gè) main.c
文件,指定一個(gè)初始進(jìn)入無(wú)限循環(huán)什么都不做的啟動(dòng)函數(shù),并把包含 16 個(gè)標(biāo)準(zhǔn)入口和 91 個(gè) STM32 入口的向量表放進(jìn)去。用你常用的編輯器創(chuàng)建 main.c
文件,并寫入下面的內(nèi)容:
// 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
};
對(duì)于 _reset()
函數(shù),我們使用了 GCC 編譯器特定的 naked
和 noreturn
屬性,這意味著標(biāo)準(zhǔn)函數(shù)的進(jìn)入和退出不會(huì)被編譯器創(chuàng)建,這個(gè)函數(shù)永遠(yuǎn)不會(huì)返回。
void (*tab[16 + 91])(void)
這個(gè)表達(dá)式的意思是:定義一個(gè) 16+91 個(gè)指向沒(méi)有返回也沒(méi)有參數(shù)的函數(shù)的指針數(shù)組,每個(gè)這樣的函數(shù)都是一個(gè)中斷處理程序,這個(gè)指針數(shù)組就是向量表。
我們把 tab
向量表放到一個(gè)獨(dú)立的叫作 .vectors
的區(qū)段,后面需要告訴鏈接器把這個(gè)區(qū)段放到固件最開始的地址,也就是 flash 存儲(chǔ)區(qū)最開始的地方。前 2 個(gè)入口分別是:堆棧指針和固件入口,目前先把向量表其它值用 0 填充。
編譯
我們來(lái)編譯下代碼,打開終端并執(zhí)行:
$ arm-none-eabi-gcc -mcpu=cortex-m4 main.c -c
成功了!編譯器生成了 main.o
文件,包含了最小固件,雖然這個(gè)固件程序什么都沒(méi)做。這個(gè) main.o
文件是 ELF 二進(jìn)制格式的,包含了多個(gè)區(qū)段,我們來(lái)具體看一下:
$ 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
注意現(xiàn)在所有區(qū)段的 VMA/LMA 地址都是 0,這表示 main.o
還不是一個(gè)完整的固件,因?yàn)樗鼪](méi)有包含各個(gè)區(qū)段從哪個(gè)地址空間載入的信息。我們需要鏈接器從 main.o
生成一個(gè)完整的固件 firmware.elf
。
.text
區(qū)段包含固件代碼,在上面的例子中,只有一個(gè) _reset()
函數(shù),2 個(gè)字節(jié)長(zhǎng),是跳轉(zhuǎn)到自身地址的 jump
指令。.data
和 .bss
(初始化為 0 的數(shù)據(jù)) 區(qū)段都是空的。我們的固件將被拷貝到偏移 0x8000000 的 flash 區(qū),但是數(shù)據(jù)區(qū)段應(yīng)該被放到 RAM 里,因此 _reset()
函數(shù)應(yīng)該把 .data
區(qū)段拷貝到 RAM,并把整個(gè) .bss
區(qū)段寫入 0。現(xiàn)在 .data
和 .bss
區(qū)段是空的,我們修改下 _reset()
函數(shù)讓它處理好這些。
為了做到這一點(diǎn),我們必須知道堆棧從哪開始,也需要知道 .data
和 .bss
區(qū)段從哪開始。這些可以通過(guò) “鏈接腳本” 指定,鏈接腳本是一個(gè)帶有鏈接器指令的文件,這個(gè)文件里存有各個(gè)區(qū)段的地址空間以及對(duì)應(yīng)的符號(hào)。
鏈接腳本
創(chuàng)建一個(gè)鏈接腳本文件 link.ld
,然后把一下內(nèi)容拷進(jìn)去:
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” 屬性的值。沒(méi)錯(cuò),這跟向量表重復(fù)了,這個(gè)的目的是為像 Ozone 這樣的調(diào)試器設(shè)置固件起始的斷點(diǎn)。調(diào)試器是不知道向量表的,所以只能依賴 ELF 文件頭。
MEMORY {
flash(rx) : ORIGIN = 0x08000000, LENGTH = 2048k
sram(rwx) : ORIGIN = 0x20000000, LENGTH = 192k /* remaining 64k in a separate address space */
}
這是告訴鏈接器有 2 個(gè)存儲(chǔ)區(qū)空間,以及它們的起始地址和大小。
_estack = ORIGIN(sram) + LENGTH(sram); /* stack points to end of SRAM */
這行告訴鏈接器創(chuàng)建一個(gè) _estack
符號(hào),它的值是 RAM 區(qū)的最后,這也是初始化堆棧指針的值。
.vectors : { KEEP(*(.vectors)) } > flash
.text : { *(.text*) } > flash
.rodata : { *(.rodata*) } > flash
這是告訴鏈接器把向量表放在 flash 區(qū)最前,然后是 .text
區(qū)段(固件代碼),再然后是只讀數(shù)據(jù) .rodata
。
.data : {
_sdata = .; /* .data section start */
*(.first_data)
*(.data SORT(.data.*))
_edata = .; /* .data section end */
} > sram AT > flash
_sidata = LOADADDR(.data);
這是 .data
區(qū)段,告訴鏈接器創(chuàng)建 _sdata
和 _edata
兩個(gè)符號(hào),我們將在 _reset()
函數(shù)中使用它們將數(shù)據(jù)拷貝到 RAM。
.bss : {
_sbss = .; /* .bss section start */
*(.bss SORT(.bss.*) COMMON)
_ebss = .; /* .bss section end */
} > sram
.bss
區(qū)段也是一樣。
啟動(dòng)代碼
現(xiàn)在我們來(lái)更新下 _reset
函數(shù),把 .data
區(qū)段拷貝到 RAM,然后把 .bss
區(qū)段初始化為 0,再然后調(diào)用 main()
函數(shù),在 main()
函數(shù)有返回的情況下進(jìn)入無(wú)限循環(huán):
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
(數(shù)據(jù))。這些部分根據(jù)鏈接腳本被分配到不同的存儲(chǔ)空間:.vectors
在 flash 的最前面,.text
緊隨其后,.data
則在那之后很遠(yuǎn)的地方。.text
中的地址在 flash 區(qū),.data
在 RAM 區(qū)。例如,一個(gè)函數(shù)的地址是 0x8000100
,則它位于 flash 中。而如果代碼要訪問(wèn) .data
中的變量,比如位于 0x20000200
,那里將什么也沒(méi)有,因?yàn)樵趩?dòng)時(shí) firmware.bin
中 .data
還在 flash 里!這就是為什么必須要在啟動(dòng)代碼中將 .data
區(qū)段拷貝到 RAM。
現(xiàn)在我們可以生成完整的 firmware.elf
固件了:
$ arm-none-eabi-gcc -T link.ld -nostdlib main.o -o firmware.elf
再次檢驗(yàn) firmware.elf
中的區(qū)段:
$ 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
區(qū)段在 flash 的起始地址 0x8000000,.text
緊隨其后。我們?cè)诖a中沒(méi)有創(chuàng)建任何變量,所以沒(méi)有 .data
區(qū)段。
燒寫固件
現(xiàn)在可以把這個(gè)固件燒寫到板子上了!
先把 firmware.elf
中各個(gè)區(qū)段抽取到一個(gè)連續(xù)二進(jìn)制文件中:
$ arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
然后使用 st-link
工具將firmware.bin
燒入板子,連接好板子,然后執(zhí)行:
$ st-flash --reset write firmware.bin 0x8000000
這樣就把固件燒寫到板子上了。
評(píng)論