首页 > 中国高端品牌网 > 干货 > 内容页

STM32裸机编程的基础知识(3) 天天新视野

2023-06-28 23:10:57 来源:FunIO

MCU启动和向量表

STM32F429 MCU 启动时,它会从 flash 存储区最前面的位置读取一个叫作 “向量表” 的东西。“向量表” 的概念所有 ARMMCU 都通用,它是一个包含 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-specifichandlers__attribute__((section(".vectors"))) void (*tab[16 + 91])(void) = {  _estack, _reset};

对于 _reset()函数,我们使用了 GCC 编译器特定的 nakednoreturn属性,这意味着标准函数的进入和退出不会被编译器创建,这个函数永远不会返回。

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 = 2048ksram(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 regionextern 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  Algn0 .vectors      000001ac  08000000  08000000  00010000  2**2CONTENTS, ALLOC, LOAD, DATA1 .text         00000058  080001ac  080001ac  000101ac  2**2CONTENTS, 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

这样就把固件烧写到板子上了。

关键词:
x 广告
x 广告