• STM32 裸机编程 03


    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 文件,并写入下面的内容:

    1. // Startup code
    2. __attribute__((naked, noreturn)) void _reset(void) {
    3.   for (;;) (void) 0;  // Infinite loop
    4. }
    5. extern void _estack(void);  // Defined in link.ld
    6. // 16 standard and 91 STM32-specific handlers
    7. __attribute__((section(".vectors"))) void (*tab[16 + 91])(void) = {
    8.   _estack, _reset
    9. };

    对于 _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 二进制格式的,包含了多个区段,我们来具体看一下:

    1. $ arm-none-eabi-objdump -h main.o
    2. ...
    3. Sections:
    4. Idx Name          Size      VMA       LMA       File off  Algn
    5.   0 .text         00000002  00000000  00000000  00000034  2**1
    6.                   CONTENTS, ALLOC, LOAD, READONLY, CODE
    7.   1 .data         00000000  00000000  00000000  00000036  2**0
    8.                   CONTENTS, ALLOC, LOAD, DATA
    9.   2 .bss          00000000  00000000  00000000  00000036  2**0
    10.                   ALLOC
    11.   3 .vectors      000001ac  00000000  00000000  00000038  2**2
    12.                   CONTENTS, ALLOC, LOAD, RELOC, DATA
    13.   4 .comment      0000004a  00000000  00000000  000001e4  2**0
    14.                   CONTENTS, READONLY
    15.   5 .ARM.attributes 0000002e  00000000  00000000  0000022e  2**0
    16.                   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,然后把一下内容拷进去:

    1. ENTRY(_reset);
    2. MEMORY {
    3.   flash(rx)  : ORIGIN = 0x08000000LENGTH = 2048k
    4.   sram(rwx) : ORIGIN = 0x20000000LENGTH = 192k  /* remaining 64in a separate address space */
    5. }
    6. _estack     = ORIGIN(sram) + LENGTH(sram);    /* stack points to end of SRAM */
    7. SECTIONS {
    8.   .vectors  : { KEEP(*(.vectors)) }   > flash
    9.   .text     : { *(.text*) }           > flash
    10.   .rodata   : { *(.rodata*) }         > flash
    11.   .data : {
    12.     _sdata = .;   /* .data section start */
    13.     *(.first_data)
    14.     *(.data SORT(.data.*))
    15.     _edata = .;  /* .data section end */
    16.   } > sram AT > flash
    17.   _sidata = LOADADDR(.data);
    18.   .bss : {
    19.     _sbss = .;              /* .bss section start */
    20.     *(.bss SORT(.bss.*COMMON)
    21.     _ebss = .;              /* .bss section end */
    22.   } > sram
    23.   . = ALIGN(8);
    24.   _end = .;     /* for cmsis_gcc.h  */
    25. }

    下面分段解释下:

    ENTRY(_reset);
    

    这行是告诉链接器在生成的 ELF 文件头中 "entry point" 属性的值。没错,这跟向量表重复了,这个的目的是为像 Ozone 这样的调试器设置固件起始的断点。调试器是不知道向量表的,所以只能依赖 ELF 文件头。

    1. MEMORY {
    2.   flash(rx)  : ORIGIN = 0x08000000LENGTH = 2048k
    3.   sram(rwx) : ORIGIN = 0x20000000LENGTH = 192k  /* remaining 64in a separate address space */
    4. }

    这是告诉链接器有 2 个存储区空间,以及它们的起始地址和大小。

    _estack     = ORIGIN(sram) + LENGTH(sram);    /* stack points to end of SRAM */
    

    这行告诉链接器创建一个 _estack 符号,它的值是 RAM 区的最后,这也是初始化堆栈指针的值。

    1.   .vectors  : { KEEP(*(.vectors)) }   > flash
    2.   .text     : { *(.text*) }           > flash
    3.   .rodata   : { *(.rodata*) }         > flash

    这是告诉链接器把向量表放在 flash 区最前,然后是 .text 区段(固件代码),再然后是只读数据 .rodata

    1.   .data : {
    2.     _sdata = .;   /* .data section start */
    3.     *(.first_data)
    4.     *(.data SORT(.data.*))
    5.     _edata = .;  /* .data section end */
    6.   } > sram AT > flash
    7.   _sidata = LOADADDR(.data);

    这是 .data 区段,告诉链接器创建 _sdata 和 _edata 两个符号,我们将在 _reset() 函数中使用它们将数据拷贝到 RAM。

    1.   .bss : {
    2.     _sbss = .;              /* .bss section start */
    3.     *(.bss SORT(.bss.*COMMON)
    4.     _ebss = .;              /* .bss section end */
    5.   } > sram

    .bss 区段也是一样。

    启动代码

    现在我们来更新下 _reset 函数,把 .data 区段拷贝到 RAM,然后把 .bss 区段初始化为 0,再然后调用 main() 函数,在 main() 函数有返回的情况下进入无限循环:

    1. int main(void) {
    2.   return 0// Do nothing so far
    3. }
    4. // Startup code
    5. __attribute__((naked, noreturn)) void _reset(void) {
    6.   // memset .bss to zeroand copy .data section to RAM region
    7.   extern long _sbss, _ebss, _sdata, _edata, _sidata;
    8.   for (long *src = &_sbss; src < &_ebss; src++*src = 0;
    9.   for (long *src = &_sdata, *dst = &_sidata; src < &_edata;) *src++ = *dst++;
    10.   main();             // Call main()
    11.   for (;;) (void) 0;  // Infinite loop in the case if main() returns
    12. }

    下面的框图演示了 _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 中的区段:

    1. $ arm-none-eabi-objdump -h firmware.elf
    2. ...
    3. Sections:
    4. Idx Name          Size      VMA       LMA       File off  Algn
    5.   0 .vectors      000001ac  08000000  08000000  00010000  2**2
    6.                   CONTENTS, ALLOC, LOAD, DATA
    7.   1 .text         00000058  080001ac  080001ac  000101ac  2**2
    8.                   CONTENTS, ALLOC, LOAD, READONLY, CODE
    9. ...

    可以看到,.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
    

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

  • 相关阅读:
    耗时半月,把牛客网最火Java面试题总结成PDF,涵盖所有面试高频题
    Java8-新特性
    Session详解
    青阳网络文件传输系统 kiftd 1.1.0 正式发布!
    一文3000字解析Pytest单元测试框架【保姆级教程】
    边缘计算物联网网关在机械加工行业中的效用分享
    初识html
    LeetCode·每日一题·1582.二进制矩阵中的特殊位置·模拟
    C++ 使用栈求解中缀、后缀表达式的值
    Java--MybatisPlus Wrapper构造器;分页;MP代码生成器(四)
  • 原文地址:https://blog.csdn.net/m0_61687959/article/details/133909339