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