真正最小的STM32应用程序:链接器失败

K.M*_*ier 3 microcontroller gcc arm stm32 linker-scripts

我正在构建一个微型微控制器,其中只包含用于自学目的的基本要素.通过这种方式,我可以刷新我对linkerscript,启动代码等主题的了解......


编辑:
我收到了很多评论,指出下面显示的"绝对最小STM32应用"并不好.当你注意到向量表不完整,.bss-section没有处理,外围地址不完整时,你是完全正确的,...请允许我解释原因.

  1. 作者的目的从未在本章中编写完整且有用的应用程序.他的目的是逐步解释链接器如何工作,启动代码如何工作,STM32的启动过程是什么样的,......纯粹用于教育目的.我可以欣赏这种方法,并学到了很多东西.

  2. 我在下面的例子取自相关章节的中间部分.本章继续向linkerscript和启动代码添加更多部分(例如初始化.bss-section).
    我从他的章节中间放置文件的原因是因为我遇到了特定的错误消息.我想在继续之前解决这个问题.

  3. 有问题的章节在他书的最后.它适用于想要获得更多人们甚至不考虑的主题的更有经验或好奇的读者(大多数人使用制造商给出的标准linkerscript和启动代码而不读它).

请记住这一点,请让我们关注手头的技术问题(如下面的错误消息中所述).也请接受我真诚的道歉,我之前没有澄清作者的意图.但我现在已经完成了,所以我们可以继续前进;-)


 

1.绝对最小的STM32应用程序

我正在阅读的教程是本书的第20章:"掌握STM32"(https://leanpub.com/mastering-stm32).该书解释了如何使用两个文件制作一个微型微控制器应用程序:main.clinkerscript.ld.由于我没有使用IDE(如Eclipse),我还添加build.batclean.bat生成编译命令.所以我的项目文件夹看起来像这样:

在此输入图像描述

在继续之前,我应该提供一些关于我的系统的更多细节:

  • 操作系统: Windows 10,64位

  • 微控制器:带STM32F401RE微控制器的NUCLEO-F401RE板.

  • 编译器: arm-none-eabi-gcc版本6.3.1 20170620(发布)[ARM/embedded-6-branch revision 249437].

主文件如下所示:

/* ------------------------------------------------------------ */
/*                     Minimal application                      */
/*                      for NUCLEO-F401RE                       */
/* ------------------------------------------------------------ */
typedef unsigned long uint32_t;

/* Memory and peripheral start addresses (common to all STM32 MCUs) */
#define FLASH_BASE      0x08000000
#define SRAM_BASE       0x20000000
#define PERIPH_BASE     0x40000000

/* Work out end of RAM address as initial stack pointer
 * (specific of a given STM32 MCU) */
#define SRAM_SIZE       96*1024 //STM32F401RE has 96 KB of RAM
#define SRAM_END        (SRAM_BASE + SRAM_SIZE)

/* RCC peripheral addresses applicable to GPIOA
 * (specific of a given STM32 MCU) */
#define RCC_BASE        (PERIPH_BASE + 0x23800)
#define RCC_APB1ENR     ((uint32_t*)(RCC_BASE + 0x30))

/* GPIOA peripheral addresses
 * (specific of a given STM32 MCU) */
#define GPIOA_BASE      (PERIPH_BASE + 0x20000)
#define GPIOA_MODER     ((uint32_t*)(GPIOA_BASE + 0x00))
#define GPIOA_ODR       ((uint32_t*)(GPIOA_BASE + 0x14))

/* Function headers */
int main(void);
void delay(uint32_t count);

/* Minimal vector table */
uint32_t *vector_table[] __attribute__((section(".isr_vector"))) = {
    (uint32_t*)SRAM_END,    // initial stack pointer (MSP)
    (uint32_t*)main         // main as Reset_Handler
};

/* Main function */
int main() {
    /* Enable clock on GPIOA peripheral */
    *RCC_APB1ENR = 0x1;

    /* Configure the PA5 as output pull-up */
    *GPIOA_MODER |= 0x400;  // Sets MODER[11:10] = 0x1

    while(1) {    // Always true
        *GPIOA_ODR = 0x20;
        delay(200000);
        *GPIOA_ODR = 0x0;
        delay(200000);
    }
}

void delay(uint32_t count) {
    while(count--);
}
Run Code Online (Sandbox Code Playgroud)

 
linkerscript看起来像这样:

/* ------------------------------------------------------------ */
/*                        Linkerscript                          */
/*                      for NUCLEO-F401RE                       */
/* ------------------------------------------------------------ */

/* Memory layout for STM32F401RE */
MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
    SRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K
}

/* The ENTRY(..) directive overrides the default entry point symbol _start.
 * Here we define the main-routine as the entry point.
 * In fact, the ENTRY(..) directive is meaningless for embedded chips,
 * but it is informative for debuggers. */
ENTRY(main)

SECTIONS
{
    /* Program code into FLASH */
    .text : ALIGN(4)
    {
        *(.isr_vector)          /* Vector table */
        *(.text)                /* Program code */
        *(.text*)               /* Merge all .text.* sections inside the .text section */
        KEEP(*(.isr_vector))    /* Don't allow other tools to strip this off */
    } >FLASH


    _sidata = LOADADDR(.data);  /* Used by startup code to initialize data */

    .data : ALIGN(4)
    {
        . = ALIGN(4);
        _sdata = .;             /* Create a global symbol at data start */

        *(.data)
        *(.data*)

        . = ALIGN(4);
        _edata = .;             /* Define a global symbol at data end */
    } >SRAM AT >FLASH

}
Run Code Online (Sandbox Code Playgroud)

 
build.bat文件在main.c上调用编译器,接下来是链接器:

@echo off
setlocal EnableDelayedExpansion

echo.
echo ----------------------------------------------------------------
echo.             )\     ***************************
echo.   ( =_=_=_=^<  ^|    * build NUCLEO-F401RE     *     
echo.             )(     ***************************
echo.             ""                        
echo.                                       
echo.
echo.   Call the compiler on main.c
echo.
@arm-none-eabi-gcc main.c -o main.o -c -MMD -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -O0 -g3 -Wall -fmessage-length=0 -Werror-implicit-function-declaration -Wno-comment -Wno-unused-function -ffunction-sections -fdata-sections
echo.
echo.   Call the linker
echo.
@arm-none-eabi-gcc main.o -o myApp.elf -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -specs=nosys.specs -specs=nano.specs -T linkerscript.ld -Wl,-Map=output.map -Wl,--gc-sections
echo.
echo.   Post build
echo.
@arm-none-eabi-objcopy -O binary myApp.elf myApp.bin
arm-none-eabi-size myApp.elf
echo.
echo ----------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

 
clean.bat文件删除所有编译器输出:

@echo off
setlocal EnableDelayedExpansion

echo ----------------------------------------------------------------
echo.        __         **************    
echo.      __\ \___     *   clean    *    
echo.      \ _ _ _ \    **************    
echo.       \_`_`_`_\                     
echo.                                     
del /f /q main.o
del /f /q main.d
del /f /q myApp.bin
del /f /q myApp.elf
del /f /q output.map
echo ----------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

建立这个工作.我得到以下输出:

C:\Users\Kristof\myProject>build

----------------------------------------------------------------
             )\     ***************************
   ( =_=_=_=<  |    * build NUCLEO-F401RE     *
             )(     ***************************
             ""


   Call the compiler on main.c


   Call the linker


   Post build

   text    data     bss     dec     hex filename
    112       0       0     112      70 myApp.elf

----------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

 

2.正确的启动代码

也许你已经注意到最小的应用程序没有正确的启动代码来初始化.data-section中的全局变量.第20.2.2.data和.bss "Mastering STM32"一书中的章节初始化说明了如何执行此操作.

随着我的跟进,我的main.c文件现在看起来像这样:

/* ------------------------------------------------------------ */
/*                     Minimal application                      */
/*                      for NUCLEO-F401RE                       */
/* ------------------------------------------------------------ */
typedef unsigned long uint32_t;

/* Memory and peripheral start addresses (common to all STM32 MCUs) */
#define FLASH_BASE      0x08000000
#define SRAM_BASE       0x20000000
#define PERIPH_BASE     0x40000000

/* Work out end of RAM address as initial stack pointer
 * (specific of a given STM32 MCU) */
#define SRAM_SIZE       96*1024 //STM32F401RE has 96 KB of RAM
#define SRAM_END        (SRAM_BASE + SRAM_SIZE)

/* RCC peripheral addresses applicable to GPIOA
 * (specific of a given STM32 MCU) */
#define RCC_BASE        (PERIPH_BASE + 0x23800)
#define RCC_APB1ENR     ((uint32_t*)(RCC_BASE + 0x30))

/* GPIOA peripheral addresses
 * (specific of a given STM32 MCU) */
#define GPIOA_BASE      (PERIPH_BASE + 0x20000)
#define GPIOA_MODER     ((uint32_t*)(GPIOA_BASE + 0x00))
#define GPIOA_ODR       ((uint32_t*)(GPIOA_BASE + 0x14))

/* Function headers */
void __initialize_data(uint32_t*, uint32_t*, uint32_t*);
void _start (void);
int main(void);
void delay(uint32_t count);

/* Minimal vector table */
uint32_t *vector_table[] __attribute__((section(".isr_vector"))) = {
    (uint32_t*)SRAM_END,    // initial stack pointer (MSP)
    (uint32_t*)_start       // _start as Reset_Handler
};

/* Variables defined in linkerscript */
extern uint32_t _sidata;
extern uint32_t _sdata;
extern uint32_t _edata;

volatile uint32_t dataVar = 0x3f;

/* Data initialization */
inline void __initialize_data(uint32_t* flash_begin, uint32_t* data_begin, uint32_t* data_end) {
    uint32_t *p = data_begin;
    while(p < data_end)
        *p++ = *flash_begin++;
}

/* Entry point */
void __attribute__((noreturn,weak)) _start (void) {
    __initialize_data(&_sidata, &_sdata, &_edata);
    main();

    for(;;);
}

/* Main function */
int main() {
    /* Enable clock on GPIOA peripheral */
    *RCC_APB1ENR = 0x1;

    /* Configure the PA5 as output pull-up */
    *GPIOA_MODER |= 0x400;  // Sets MODER[11:10] = 0x1

    while(dataVar == 0x3f) {    // Always true
        *GPIOA_ODR = 0x20;
        delay(200000);
        *GPIOA_ODR = 0x0;
        delay(200000);
    }
}

void delay(uint32_t count) {
    while(count--);
}
Run Code Online (Sandbox Code Playgroud)

我在main(..)函数上方添加了初始化代码.linkerscript也有一些修改:

/* ------------------------------------------------------------ */
/*                        Linkerscript                          */
/*                      for NUCLEO-F401RE                       */
/* ------------------------------------------------------------ */

/* Memory layout for STM32F401RE */
MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
    SRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 96K
}

/* The ENTRY(..) directive overrides the default entry point symbol _start.
 * In fact, the ENTRY(..) directive is meaningless for embedded chips,
 * but it is informative for debuggers. */
ENTRY(_start)

SECTIONS
{
    /* Program code into FLASH */
    .text : ALIGN(4)
    {
        *(.isr_vector)          /* Vector table */
        *(.text)                /* Program code */
        *(.text*)               /* Merge all .text.* sections inside the .text section */
        KEEP(*(.isr_vector))    /* Don't allow other tools to strip this off */
    } >FLASH


    _sidata = LOADADDR(.data);  /* Used by startup code to initialize data */

    .data : ALIGN(4)
    {
        . = ALIGN(4);
        _sdata = .;             /* Create a global symbol at data start */

        *(.data)
        *(.data*)

        . = ALIGN(4);
        _edata = .;             /* Define a global symbol at data end */
    } >SRAM AT >FLASH

}
Run Code Online (Sandbox Code Playgroud)

小应用程序不再编译.其实,从编辑main.cmain.o仍然是好的.但链接过程陷入困境:

C:\Users\Kristof\myProject>build

----------------------------------------------------------------
             )\     ***************************
   ( =_=_=_=<  |    * build NUCLEO-F401RE     *
             )(     ***************************
             ""


   Call the compiler on main.c


   Call the linker

c:/gnu_arm_embedded_toolchain/bin/../lib/gcc/arm-none-eabi/6.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m/fpv4-sp/hard/crt0.o: In function `_start':
(.text+0x64): undefined reference to `__bss_start__'
c:/gnu_arm_embedded_toolchain/bin/../lib/gcc/arm-none-eabi/6.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m/fpv4-sp/hard/crt0.o: In function `_start':
(.text+0x68): undefined reference to `__bss_end__'
collect2.exe: error: ld returned 1 exit status

   Post build

arm-none-eabi-objcopy: 'myApp.elf': No such file
arm-none-eabi-size: 'myApp.elf': No such file

----------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

 

我尝试了什么

我省略了这部分,否则这个问题太长了;-)

 

4.解决方案

@berendi提供了解决方案.谢谢@berendi!显然我需要添加标志-nostdlib-ffreestandinggcc以及链接器.该build.bat文件现在看起来像这样:

@echo off
setlocal EnableDelayedExpansion

echo.
echo ----------------------------------------------------------------
echo.             )\     ***************************
echo.   ( =_=_=_=^<  ^|    * build NUCLEO-F401RE     *     
echo.             )(     ***************************
echo.             ""                        
echo.                                       
echo.
echo.   Call the compiler on main.c
echo.
@arm-none-eabi-gcc main.c -o main.o -c -MMD -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -O0 -g3 -Wall -fmessage-length=0 -Werror-implicit-function-declaration -Wno-comment -Wno-unused-function -ffunction-sections -fdata-sections -ffreestanding -nostdlib
echo.
echo.   Call the linker
echo.
@arm-none-eabi-gcc main.o -o myApp.elf -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -specs=nosys.specs -specs=nano.specs -T linkerscript.ld -Wl,-Map=output.map -Wl,--gc-sections -ffreestanding -nostdlib
echo.
echo.   Post build
echo.
@arm-none-eabi-objcopy -O binary myApp.elf myApp.bin
arm-none-eabi-size myApp.elf
echo.
echo ----------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

现在它有效!在他的回答中,@ berendi也对该main.c文件发表了一些有趣的评论.我申请了大部分:

  1. 缺少volatile关键字

  2. 空循环

  3. 缺少内存屏障(我是否将内存屏障放在正确的位置?)

  4. RCC启用后缺少延迟

  5. 误导性的象征性名称(显然应该是RCC_AHB1ENR代替RCC_APB1ENR).

  6. 向量表:这部分我已经跳过了.现在,我并不真的需要一个HardFault_Handler,MemManage_Handler......因为这只是为了教育目的一个小测试.
    尽管如此,我确实注意到@berendi在他声明向量表的方式上做了一些有趣的修改.但我并没有完全理解他正在做的事情.

main.c文件现在看起来像这样:

/* ------------------------------------------------------------ */
/*                     Minimal application                      */
/*                      for NUCLEO-F401RE                       */
/* ------------------------------------------------------------ */
typedef unsigned long uint32_t;

/**
  \brief   Data Synchronization Barrier
  \details Acts as a special kind of Data Memory Barrier.
           It completes when all explicit memory accesses before this instruction complete.
 */
__attribute__((always_inline)) static inline void __DSB(void)
{
  __asm volatile ("dsb 0xF":::"memory");
}


/* Memory and peripheral start addresses (common to all STM32 MCUs) */
#define FLASH_BASE      0x08000000
#define SRAM_BASE       0x20000000
#define PERIPH_BASE     0x40000000

/* Work out end of RAM address as initial stack pointer
 * (specific of a given STM32 MCU) */
#define SRAM_SIZE       96*1024 //STM32F401RE has 96 KB of RAM
#define SRAM_END        (SRAM_BASE + SRAM_SIZE)

/* RCC peripheral addresses applicable to GPIOA
 * (specific of a given STM32 MCU) */
#define RCC_BASE        (PERIPH_BASE + 0x23800)
#define RCC_AHB1ENR     ((volatile uint32_t*)(RCC_BASE + 0x30))

/* GPIOA peripheral addresses
 * (specific of a given STM32 MCU) */
#define GPIOA_BASE      (PERIPH_BASE + 0x20000)
#define GPIOA_MODER     ((volatile uint32_t*)(GPIOA_BASE + 0x00))
#define GPIOA_ODR       ((volatile uint32_t*)(GPIOA_BASE + 0x14))

/* Function headers */
void __initialize_data(uint32_t*, uint32_t*, uint32_t*);
void _start (void);
int main(void);
void delay(uint32_t count);

/* Minimal vector table */
uint32_t *vector_table[] __attribute__((section(".isr_vector"))) = {
    (uint32_t*)SRAM_END,    // initial stack pointer (MSP)
    (uint32_t*)_start       // _start as Reset_Handler
};

/* Variables defined in linkerscript */
extern uint32_t _sidata;
extern uint32_t _sdata;
extern uint32_t _edata;

volatile uint32_t dataVar = 0x3f;

/* Data initialization */
inline void __initialize_data(uint32_t* flash_begin, uint32_t* data_begin, uint32_t* data_end) {
    uint32_t *p = data_begin;
    while(p < data_end)
        *p++ = *flash_begin++;
}

/* Entry point */
void __attribute__((noreturn,weak)) _start (void) {
    __initialize_data(&_sidata, &_sdata, &_edata);
    asm volatile("":::"memory"); // <- Did I put this instruction at the right spot?
    main();

    for(;;);
}

/* Main function */
int main() {
    /* Enable clock on GPIOA peripheral */
    *RCC_AHB1ENR = 0x1;
    __DSB();

    /* Configure the PA5 as output pull-up */
    *GPIOA_MODER |= 0x400;  // Sets MODER[11:10] = 0x1

    while(dataVar == 0x3f) {    // Always true
        *GPIOA_ODR = 0x20;
        delay(200000);
        *GPIOA_ODR = 0x0;
        delay(200000);
    }
}

void delay(uint32_t count) {
    while(count--){
        asm volatile("");
    }
}
Run Code Online (Sandbox Code Playgroud)

PS:Carmine Noviello的"掌握STM32"这本书绝对是杰作.你应该读它!=> https://leanpub.com/mastering-stm32

ber*_*ing 6

您可以告诉gcc不要使用该库.

编译器

默认情况下,gcc假定您使用的是标准C库,并且可以发出调用某些函数的代码.例如,当启用优化时,它会检测复制一块内存的循环,并可以通过调用来替换它们memcpy().禁用它-ffreestanding.

链接器

链接器也假设您希望将程序与C库和启动代码链接起来.库启动代码负责初始化库和程序执行环境.它有一个名为的函数_start(),必须在复位后调用.其功能之一是.bss用零填充段(见下文).如果.bss未定义分隔符号,则_startup()无法链接.假如你命名你的启动函数别的,但是_startup(),那么库启动会被siletly通过链接下降为未使用的功能和代码可能已联系.

您可以告诉链接器不要链接任何标准库或启动代码-nostdlib,然后库提供的启动函数名称不会与您的文件冲突,每次意外调用库函数时都会出现链接器错误.

失踪 volatile

您的注册表定义缺少volatile限定符.没有它,后续写入*GPIOA_ODR将被优化.编译器会将这个"不变代码"移出循环.更改寄存器定义中的类型(volatile uint32_t*)将修复该问题.

空循环

优化器可以识别出延迟循环什么都不做,并完全消除它以加速执行.asm volatile("");向延迟循环添加空但不可移除的指令.

缺少记忆障碍

您正在初始化C函数.data中包含的部分dataVar.该*p__initialize_data()实际上是一个别名dataVar,编译器没有办法知道.理论上,优化器可以重新安排dataVar之前的测试__initialize_data().即使dataVarvolatile,*p也不是,因此订购不能保证.

在数据初始化循环之后,您应该告诉编译器程序变量是由编译器未知的机制更改的:

asm volatile("":::"memory");
Run Code Online (Sandbox Code Playgroud)

这是一个老式的gcc扩展,最新的C标准可能已经定义了一种可移植的方式(旧的g​​cc版本无法识别).

RCC启用后缺少延迟

勘误说,

应考虑RCC外设时钟使能和有效外设使能之间的延迟,以便管理外设对寄存器的读/写.

此延迟取决于外设映射:

•如果外设映射在AHB上:延迟应等于2个AHB周期.

•如果外设映射到APB:延迟应等于1 +(AHB/APB预分频器)周期.

解决方法

  1. 使用DSB指令停止Cortex®-M4 CPU管道,直到指令完成.

因此,插入一个

__DSB();
Run Code Online (Sandbox Code Playgroud)

之后*RCC_APB1ENR = 0x1;(应该叫别的东西)

误导性的象征性名称

虽然地址使GPIOARCC似乎是正确的,该寄存器被称为RCC_AHB1ENR的文件中.它会让试图理解你的代码的人感到困惑.

矢量表

虽然从技术上讲你只能使用堆栈pinter和一个重置处理程序,但我也建议再添加一些条目,至少是故障处理程序,以便进行简单的故障排除.

__attribute__ ((section(".isr_vector"),used))
void (* const _vectors[]) (void) = {
          (void (*const)(void))(&__stack),
  Reset_Handler,
  NMI_Handler,
  HardFault_Handler,
  MemManage_Handler,
  BusFault_Handler,
  UsageFault_Handler
}
Run Code Online (Sandbox Code Playgroud)

链接脚本

至少,它必须为向量表和代码定义一个部分.程序必须具有起始地址和一些代码,静态数据是可选的.其余的取决于您的程序使用什么类型的数据.如果没有特定类型的数据,您可以在技术上从链接描述文件中省略它们.

  • .rodata:只读数据,const数组和结构在这里.他们仍然在闪光.(简单const变量通常放在代码中)
  • .data:初始化变量,您用=符号声明的所有内容,以及没有const.
  • .bss:应该在C中零初始化的变量,即全局变量和static1.

因为你不需要.rodata.bss现在,它没关系.

  • @K.Mulier 你不需要 -Wl, 前缀,只需在第二次调用时将 `-nostdlib -ffreestanding` 传递给 `gcc`。你不需要`-nostartfiles`,因为它是由`-nostdlib` 隐含的。还要注意我添加的 `volatile` 和延迟循环的进一步问题。 (2认同)