MCU 啟動和向量表
當 STM32F429 MCU 啟動時,它會從 flash 存儲區(qū)最前面的位置讀取一個叫作 “向量表” 的東西?!跋蛄勘怼?的概念所有 ARM MCU 都通用,它是一個包含 32 位中斷處理程序地址的數(shù)組。對于所有 ARM MCU,向量表前 16 個地址由 ARM 保留,其余的作為外設(shè)中斷處理程序入口,由 MCU 廠商定義。越簡單的 MCU 中斷處理程序入口越少,越復(fù)雜的 MCU 中斷處理程序入口則會更多。
STM32F429 的向量表在數(shù)據(jù)手冊表 62 中描述,我們可以看到它在 16 個 ARM 保留的標準中斷處理程序入口外還有 91 個外設(shè)中斷處理程序入口。
在向量表中,我們當前對前兩個入口點比較感興趣,它們在 MCU 啟動過程中扮演了關(guān)鍵角色。這兩個值是:初始堆棧指針和執(zhí)行啟動函數(shù)的地址(固件程序入口點)。
所以現(xiàn)在我們知道,我們必須確保固件中第 2 個 32 位值包含啟動函數(shù)的地址,當 MCU 啟動時,它會從 flash 讀取這個地址,然后跳轉(zhuǎn)到我們的啟動函數(shù)。
最小固件
現(xiàn)在我們創(chuàng)建一個 main.c
文件,指定一個初始進入無限循環(huán)什么都不做的啟動函數(shù),并把包含 16 個標準入口和 91 個 STM32 入口的向量表放進去。用你常用的編輯器創(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
屬性,這意味著標準函數(shù)的進入和退出不會被編譯器創(chuàng)建,這個函數(shù)永遠不會返回。
void (*tab[16 + 91])(void)
這個表達式的意思是:定義一個 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 二進制格式的,包含了多個區(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?,F(xiàn)在 .data
和 .bss
區(qū)段是空的,我們修改下 _reset()
函數(shù)讓它處理好這些。
為了做到這一點,我們必須知道堆棧從哪開始,也需要知道 .data
和 .bss
區(qū)段從哪開始。這些可以通過 “鏈接腳本” 指定,鏈接腳本是一個帶有鏈接器指令的文件,這個文件里存有各個區(qū)段的地址空間以及對應(yīng)的符號。
鏈接腳本
創(chuàng)建一個鏈接腳本文件 link.ld
,然后把一下內(nèi)容拷進去:
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ù)有返回的情況下進入無限循環(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
則在那之后很遠的地方。.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ù)二進制文件中:
$ arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
然后使用 st-link
工具將firmware.bin
燒入板子,連接好板子,然后執(zhí)行:
$ st-flash --reset write firmware.bin 0x8000000
這樣就把固件燒寫到板子上了。
評論