STM32裸机编程的基础知识(3)

发布时间:2023-09-25  

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

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


文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。

我们与500+贴片厂合作,完美满足客户的定制需求。为品牌提供定制化的推广方案、专属产品特色页,多渠道推广,SEM/SEO精准营销以及与公众号的联合推广...详细>>

利用葫芦芯平台的卓越技术服务和新产品推广能力,原厂代理能轻松打入消费物联网(IOT)、信息与通信(ICT)、汽车及新能源汽车、工业自动化及工业物联网、装备及功率电子...详细>>

充分利用其强大的电子元器件采购流量,创新性地为这些物料提供了一个全新的窗口。我们的高效数字营销技术,不仅可以助你轻松识别与连接到需求方,更能够极大地提高“闲置物料”的处理能力,通过葫芦芯平台...详细>>

我们的目标很明确:构建一个全方位的半导体产业生态系统。成为一家全球领先的半导体互联网生态公司。目前,我们已成功打造了智能汽车、智能家居、大健康医疗、机器人和材料等五大生态领域。更为重要的是...详细>>

我们深知加工与定制类服务商的价值和重要性,因此,我们倾力为您提供最顶尖的营销资源。在我们的平台上,您可以直接接触到100万的研发工程师和采购工程师,以及10万的活跃客户群体...详细>>

凭借我们强大的专业流量和尖端的互联网数字营销技术,我们承诺为原厂提供免费的产品资料推广服务。无论是最新的资讯、技术动态还是创新产品,都可以通过我们的平台迅速传达给目标客户...详细>>

我们不止于将线索转化为潜在客户。葫芦芯平台致力于形成业务闭环,从引流、宣传到最终销售,全程跟进,确保每一个potential lead都得到妥善处理,从而大幅提高转化率。不仅如此...详细>>