真正最小的 STM32 应用程序:链接器失败
Really Minimal STM32 Application: linker failure
我正在构建一个微型微控制器,仅包含用于自学目的的基本要素。这样,我可以更新我对链接描述文件、启动代码等主题的知识,...
编辑:
我收到了很多评论指出下面显示的 "absolute minimal STM32-application" 不好。当注意到向量 table 不完整、.bss
部分未处理、外设地址不完整时,你是完全正确的……请允许我解释原因。
作者的目的从来都不是在这一章中编写一个完整且有用的应用程序。他的目的是逐步解释链接描述文件的工作原理、启动代码的工作原理、STM32 的启动过程是什么样的……纯粹是出于教育目的。我很欣赏这种做法,也学到了很多。
我在下面举的例子是从相关章节的中间摘录的。本章继续向链接描述文件和启动代码添加更多部分(例如 .bss
部分的初始化)。
我将文件从他的章节中间放在这里的原因是因为我卡在了一条特定的错误消息上。我想在继续之前解决这个问题。
有问题的那一章在他书的末尾 某处。它适用于更有经验或好奇心的 reader 他们想要深入了解大多数人甚至不考虑的主题(大多数人使用制造商提供的标准链接描述文件和启动代码,但从未阅读过)。
记住这一点,请让我们专注于手头的技术问题(如下面的错误消息所述)。也请接受我诚挚的歉意,我之前没有澄清作者的意图。但我现在已经完成了,所以我们可以继续前进 ;-)
1。绝对最小的 STM32 应用程序
我正在学习的教程是本书的第 20 章:"Mastering STM32" (https://leanpub.com/mastering-stm32)。这本书解释了如何使用两个文件制作一个微型微控制器应用程序:main.c
和 linkerscript.ld
。由于我没有使用 IDE(如 Eclipse),我还添加了 build.bat
和 clean.bat
来生成编译命令。所以我的项目文件夹如下所示:
在我继续之前,我或许应该提供一些关于我的系统的更多细节:
OS: Windows 10、64 位
微控制器:带有STM32F401RE微控制器的NUCLEO-F401RE板。
编译器: arm-none-eabi-gcc
version 6.3.1 20170620 (release) [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--);
}
链接描述文件如下所示:
/* ------------------------------------------------------------ */
/* 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
}
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 ----------------------------------------------------------------
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 ----------------------------------------------------------------
构建此工程。我得到以下输出:
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
----------------------------------------------------------------
2。正确的启动代码
也许您已经注意到最小应用程序没有正确的启动代码来初始化 .data 部分中的全局变量。 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--);
}
我在 main(..)
函数的正上方添加了初始化代码。链接描述文件也有一些修改:
/* ------------------------------------------------------------ */
/* 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
}
小应用程序不再编译。其实从main.c
编译到main.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
----------------------------------------------------------------
3。我试过的
我省略了这部分,否则这个问题会太长;-)
4。解决方案
@berendi 提供了解决方案。谢谢@berendi!显然我需要将标志 -nostdlib
和 -ffreestanding
添加到 gcc 和链接器。 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 ----------------------------------------------------------------
现在可以了!
在他的回答中,@berendi 还对 main.c
文件给出了一些有趣的评论。我已经应用了其中的大部分:
缺少 volatile
关键字
空循环
缺少内存屏障(我是否将内存屏障放在正确的位置?)
启用 RCC 后缺少延迟
误导性的符号名称(显然应该是RCC_AHB1ENR
而不是RCC_APB1ENR
)。
向量table:这部分我已经跳过了。现在我真的不需要 HardFault_Handler
, MemManage_Handler
, ...因为这只是一个用于教育目的的小测试。
尽管如此,我确实注意到@berendi 在他声明向量 table 的方式中做了一些有趣的修改。但我并没有完全理解他到底在做什么。
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("");
}
}
PS:Carmine Noviello 的书 "Mastering STM32" 绝对是杰作。你应该阅读它! => https://leanpub.com/mastering-stm32
您正在阅读的这本书让您误入歧途。丢弃它并开始从其他来源学习。
我发现它告诉您的操作至少有四个 主要 问题:
您包含的 linker 脚本和 _start
函数缺少许多重要部分,并且会出现故障或无法 link 多次执行 table秒。最值得注意的是,它缺少对 BSS(零填充)部分的任何处理。
main.c
中的矢量table超出了"minimal";它缺少 required 甚至标准 ARM 中断向量的定义。没有这些,调试硬故障将变得非常困难,因为当发生故障时,微控制器会将向量 table 之后的随机代码视为中断向量,这可能会导致二次故障,因为它无法从中加载代码"address".
你书上给出的启动函数绕过了libc的启动函数。这将导致标准 C 库的某些部分以及任何 C++ 代码无法正常工作。
您在main.c
中自己定义外设地址。这些地址都定义在标准的ST头文件中(例如<stm32f4xx.h>
),所以不需要自己定义。
作为初学者,我建议您参考 ST 在他们的任何示例中提供的启动代码。这些都将包括完整的 linker 脚本和启动代码。
链接器脚本通常是一种艺术形式,它们是自己的编程语言,而 gnu 的肯定有点像噩梦。将任务划分为从制作工作二进制文件中找出 linker 脚本,一旦您看到 linker 脚本正在执行您想要的操作,然后制作 bootstrap 代码来使用它。利用工具链。
作者使用的示例源自专门编写的代码,用作最大限度成功的裸机示例。避免了通用语言和工具链问题,但可以table 跨工具链的许多版本并轻松移植到其他工具链(对工具链的依赖最小,特别是导致 linker 脚本bootstrap)。该书的作者使用了该代码,但增加了风险,使其不如示例可靠。
在编写裸机代码时特别避免 .data 并且不依赖 .bss 归零对长期成功大有帮助。
它也被修改为优化将阻止该代码工作(好吧 blinking 以您可以看到的速度)。
一个稍微简单的示例 linker binutils 脚本,您可以对其进行修改以实现 .data 和 .bss 初始化,大致如下所示
test.ld
MEMORY
{
bob : ORIGIN = 0x8000, LENGTH = 0x1000
ted : ORIGIN = 0xA000, LENGTH = 0x1000
}
SECTIONS
{
.text : { *(.text*) } > bob
__data_rom_start__ = .;
.data : {
__data_start__ = .;
*(.data*)
} > ted AT > bob
__data_end__ = .;
__data_size__ = __data_end__ - __data_start__;
.bss : {
__bss_start__ = .;
*(.bss*)
} > ted
__bss_end__ = .;
__bss_size__ = __bss_end__ - __bss_start__;
}
(注意内存名称不必是 rom 或 ram 或闪存或数据或任何 bob 是程序 space 并且 ted 是内存顺便说一句,根据需要更改地址)
您如何看待正在发生的事情,您可以 link 通过一个简单的示例或您的代码,您需要一些 .data 和一些 .bss(以及一些 .text)。
vectors.s
.thumb
.globl _start
_start:
.word 0x20001000
.word reset
.thumb_func
reset:
bl notmain
b .
.globl bss_start
bss_start: .word __bss_start__
.globl bss_end
bss_end: .word __bss_end__
.word __bss_size__
.globl data_rom_start
data_rom_start:
.word __data_rom_start__
.globl data_start
data_start:
.word __data_start__
.globl data_end
data_end:
.word __data_end__
.word __data_size__
so.c
unsigned int a=1;
unsigned int b=2;
unsigned int c;
unsigned int d;
unsigned int e;
unsigned int notmain ( void )
{
return(a+b+c+d+e);
}
建设
arm-none-eabi-as vectors.s -o vectors.o
arm-none-eabi-gcc -O2 -c -mthumb so.c -o so.o
arm-none-eabi-ld -T test.ld vectors.o so.o -o vectors.elf
arm-none-eabi-objdump -D vectors.elf
到目前为止的代码并不特定于 arm-none-whatever 或 arm-linux-whatever 版本的工具链。 If/when 您需要 gcclib 项,您可以使用 gcc 而不是 ld,但这样做时必须小心......或者提供 libgcc 的路径并使用 ld。
我们从这段代码中得到的是link便宜的脚本调试:
Disassembly of section .text:
00008000 <_start>:
8000: 20001000 andcs r1, r0, r0
8004: 00008009 andeq r8, r0, r9
00008008 <reset>:
8008: f000 f810 bl 802c <notmain>
800c: e7fe b.n 800c <reset+0x4>
0000800e <bss_start>:
800e: 0000a008 andeq sl, r0, r8
00008012 <bss_end>:
8012: 0000a014 andeq sl, r0, r4, lsl r0
8016: 0000000c andeq r0, r0, ip
0000801a <data_rom_start>:
801a: 00008058 andeq r8, r0, r8, asr r0
0000801e <data_start>:
801e: 0000a000 andeq sl, r0, r0
00008022 <data_end>:
8022: 0000a008 andeq sl, r0, r8
8026: 00000008 andeq r0, r0, r8
...
我们关心正在创建的 32 位值,andeq 反汇编是因为反汇编器试图将这些值反汇编为指令,而它们不是。重置指令是真实的,其余是我们生成的 32 位值。可能可以使用 readelf,但要习惯反汇编,确保 vector table 作为第一步是正确的,这在反汇编中很容易看到。将反汇编程序作为一种习惯使用会导致像上面那样使用它来向您展示 linker 生成的内容。
如果您没有正确获取 linker 脚本变量,您将无法编写成功的 bootstrap,如果您没有一个很好的方法来查看 linker 是什么正在生产你会定期失败。
是的,您可以用 C 而不是汇编公开它们,工具链仍然可以帮助您。
你现在可以朝着这个方向努力了,因为你可以看到 linker 在做什么:
.thumb
.globl _start
_start:
.word 0x20001000
.word reset
.thumb_func
reset:
ldr r0,=__bss_start__
ldr r1,=__bss_size__
@ zero this
ldr r0,=__data_rom_start__
ldr r1,=__data_start__
ldr r2,=__data_size__
@ copy this
bl notmain
b .
给这样的东西
00008000 <_start>:
8000: 20001000 andcs r1, r0, r0
8004: 00008009 andeq r8, r0, r9
00008008 <reset>:
8008: 4803 ldr r0, [pc, #12] ; (8018 <reset+0x10>)
800a: 4904 ldr r1, [pc, #16] ; (801c <reset+0x14>)
800c: 4804 ldr r0, [pc, #16] ; (8020 <reset+0x18>)
800e: 4905 ldr r1, [pc, #20] ; (8024 <reset+0x1c>)
8010: 4a05 ldr r2, [pc, #20] ; (8028 <reset+0x20>)
8012: f000 f80b bl 802c <notmain>
8016: e7fe b.n 8016 <reset+0xe>
8018: 0000a008 andeq sl, r0, r8
801c: 0000000c andeq r0, r0, ip
8020: 00008058 andeq r8, r0, r8, asr r0
8024: 0000a000 andeq sl, r0, r0
8028: 00000008 andeq r0, r0, r8
0000802c <notmain>:
802c: 4b06 ldr r3, [pc, #24] ; (8048 <notmain+0x1c>)
802e: 6818 ldr r0, [r3, #0]
8030: 685b ldr r3, [r3, #4]
8032: 18c0 adds r0, r0, r3
如果您随后对齐 linker 脚本中的项目,copy/zero 代码会变得更加简单,您可以坚持使用 1 到 N 个整个寄存器而不是处理字节或半字,可以使用 ldr/str、ldrd/strd(如果可用)或 ldm/stm(不需要 ldrb/strb 或 ldrh/strh),紧密简单的几行循环来完成工作。
我强烈建议您不要为 bootstrap 使用 C。
注意 ld linker 脚本变量对位置非常敏感(大括号内或外)
上面的 linker 脚本有点典型,你会在库存中找到 linker 脚本定义的开始和结束,有时大小是在 linker 中计算的脚本有时 bootstrap 代码计算大小或者 bootstrap 代码可以循环直到地址等于结束值,这取决于两者之间的整体系统设计。
你的具体问题 顺便说一句,你 link 在两个 bootstrap 中编辑,在我写这篇文章的时候,我没有在问题中看到你的命令行,所以这会告诉我们更多.这就是为什么您会看到 bss_start 等您没有放入 linker 脚本但经常在带有预构建工具链的库存工具中找到的东西(类似于上面但更复杂)
可能是通过使用 gcc 而不是 ld 并且没有各种 -nostartfiles 选项(它引入 crt0.o),只需尝试 ld 而不是 gcc 并查看有什么变化。如果原来的例子是这样的话,你会失败的,所以我认为这不是这里的问题。如果您使用相同的命令行,失败应该出现在两个示例上,而不仅仅是后者。
你可以告诉gcc
不要使用这个库。
编译器
默认情况下,gcc 假定您使用的是标准 C 库,并且可以发出调用某些函数的代码。例如,当启用优化时,它会检测复制一块内存的循环,并可能用对 memcpy()
的调用来替换它们。 用-ffreestanding
禁用它。
链接器
linker 还假设您希望link您的程序带有 C 库和启动代码。库启动代码负责初始化库和程序执行环境。它有一个名为 _start()
的函数,必须在重置后调用。它的功能之一是用零填充 .bss
段(见下文)。如果分隔 .bss
的符号未定义,则 _startup()
不能被 link 编辑。如果您将启动函数命名为 任何其他 但 _startup()
,那么库启动将被 linker 作为未使用的函数悄悄丢弃,并且代码本来可以 linked.
你可以告诉 linker 不要 link 任何标准库或带有 -nostdlib
的启动代码,然后库提供启动函数名称不会与您的名称冲突,并且每次您不小心调用库函数时都会出现 linker 错误。
缺失volatile
您的寄存器定义缺少 volatile
限定符。没有它,后续对 *GPIOA_ODR
的写入将被优化掉。编译器会将这个“不变代码”移出循环。将寄存器定义中的类型更改为 (volatile uint32_t*)
将解决此问题。
空循环
优化器可以识别延迟循环什么都不做,并完全消除它以加快执行速度。在延迟循环中添加一个空的但不可移动的 asm volatile("");
指令。
缺少内存屏障
您正在初始化 C 函数中包含 dataVar
的 .data
部分。 __initialize_data()
中的 *p
实际上是 dataVar
的别名,编译器无法知道它。优化器理论上可以在 __initialize_data()
之前重新安排 dataVar
的测试。即使 dataVar
是 volatile
,*p
不是,因此不能保证顺序。
在数据初始化循环之后,你应该告诉编译器程序变量被编译器不知道的机制改变了:
asm volatile("":::"memory");
这是一个老式的 gcc 扩展,最新的 C 标准可能已经定义了一个 portable 方法来做到这一点(旧的 gcc 版本无法识别)。
启用 RCC 后缺少延迟
勘误表说,
A delay between an RCC peripheral clock enable and the effective peripheral enabling should be taken into account in order to manage the peripheral read/write to registers.
This delay depends on the peripheral mapping:
• If the peripheral is mapped on AHB: the delay should be equal to 2 AHB cycles.
• If the peripheral is mapped on APB: the delay should be equal to 1 + (AHB/APB prescaler) cycles.
Workarounds
- Use the DSB instruction to stall the Cortex®-M4 CPU pipeline until the instruction is completed.
因此,插入一个
__DSB();
在 *RCC_APB1ENR = 0x1;
之后(应该叫别的名字)
误导性符号名称
虽然在RCC
中启用GPIOA
的地址看起来是正确的,但是在文档中该寄存器被称为RCC_AHB1ENR
。它会使试图理解您的代码的人感到困惑。
向量Table
虽然从技术上讲,您可以在其中只包含一个堆栈 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
}
链接描述文件
至少,它必须为您的矢量 table 和代码定义一个部分。一个程序必须有一个起始地址和一些代码,静态数据是可选的。其余取决于您的程序使用的数据类型。如果没有特定类型的数据,您可以在技术上从 linker 脚本中省略它们。
.rodata
:只读数据,const
数组和结构放在这里。他们留在闪光。 (简单的const
变量一般放在代码中)
.data
:初始化的变量,你声明的所有带=
符号,不带const
. 的东西
.bss
:在 C 中应该零初始化的变量,即全局变量和 static
变量。
因为你现在不需要.rodata
或.bss
,没关系。
正如old_timer在评论中暗示的那样,使用gcc
到link是一个问题。
如果您将批处理文件中的 linker 调用更改为使用 ld
,它 link 不会出错。请尝试以下操作:
echo.
echo. Call the linker
echo.
@arm-none-eabi-ld main.o -o myApp.elf -T linkerscript.ld
我正在构建一个微型微控制器,仅包含用于自学目的的基本要素。这样,我可以更新我对链接描述文件、启动代码等主题的知识,...
编辑:
我收到了很多评论指出下面显示的 "absolute minimal STM32-application" 不好。当注意到向量 table 不完整、.bss
部分未处理、外设地址不完整时,你是完全正确的……请允许我解释原因。
作者的目的从来都不是在这一章中编写一个完整且有用的应用程序。他的目的是逐步解释链接描述文件的工作原理、启动代码的工作原理、STM32 的启动过程是什么样的……纯粹是出于教育目的。我很欣赏这种做法,也学到了很多。
我在下面举的例子是从相关章节的中间摘录的。本章继续向链接描述文件和启动代码添加更多部分(例如
.bss
部分的初始化)。
我将文件从他的章节中间放在这里的原因是因为我卡在了一条特定的错误消息上。我想在继续之前解决这个问题。有问题的那一章在他书的末尾 某处。它适用于更有经验或好奇心的 reader 他们想要深入了解大多数人甚至不考虑的主题(大多数人使用制造商提供的标准链接描述文件和启动代码,但从未阅读过)。
记住这一点,请让我们专注于手头的技术问题(如下面的错误消息所述)。也请接受我诚挚的歉意,我之前没有澄清作者的意图。但我现在已经完成了,所以我们可以继续前进 ;-)
1。绝对最小的 STM32 应用程序
我正在学习的教程是本书的第 20 章:"Mastering STM32" (https://leanpub.com/mastering-stm32)。这本书解释了如何使用两个文件制作一个微型微控制器应用程序:main.c
和 linkerscript.ld
。由于我没有使用 IDE(如 Eclipse),我还添加了 build.bat
和 clean.bat
来生成编译命令。所以我的项目文件夹如下所示:
在我继续之前,我或许应该提供一些关于我的系统的更多细节:
OS: Windows 10、64 位
微控制器:带有STM32F401RE微控制器的NUCLEO-F401RE板。
编译器:
arm-none-eabi-gcc
version 6.3.1 20170620 (release) [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--);
}
链接描述文件如下所示:
/* ------------------------------------------------------------ */
/* 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
}
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 ----------------------------------------------------------------
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 ----------------------------------------------------------------
构建此工程。我得到以下输出:
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
----------------------------------------------------------------
2。正确的启动代码
也许您已经注意到最小应用程序没有正确的启动代码来初始化 .data 部分中的全局变量。 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--);
}
我在 main(..)
函数的正上方添加了初始化代码。链接描述文件也有一些修改:
/* ------------------------------------------------------------ */
/* 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
}
小应用程序不再编译。其实从main.c
编译到main.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
----------------------------------------------------------------
3。我试过的
我省略了这部分,否则这个问题会太长;-)
4。解决方案
@berendi 提供了解决方案。谢谢@berendi!显然我需要将标志 -nostdlib
和 -ffreestanding
添加到 gcc 和链接器。 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 ----------------------------------------------------------------
现在可以了!
在他的回答中,@berendi 还对 main.c
文件给出了一些有趣的评论。我已经应用了其中的大部分:
缺少
volatile
关键字空循环
缺少内存屏障(我是否将内存屏障放在正确的位置?)
启用 RCC 后缺少延迟
误导性的符号名称(显然应该是
RCC_AHB1ENR
而不是RCC_APB1ENR
)。向量table:这部分我已经跳过了。现在我真的不需要
HardFault_Handler
,MemManage_Handler
, ...因为这只是一个用于教育目的的小测试。
尽管如此,我确实注意到@berendi 在他声明向量 table 的方式中做了一些有趣的修改。但我并没有完全理解他到底在做什么。
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("");
}
}
PS:Carmine Noviello 的书 "Mastering STM32" 绝对是杰作。你应该阅读它! => https://leanpub.com/mastering-stm32
您正在阅读的这本书让您误入歧途。丢弃它并开始从其他来源学习。
我发现它告诉您的操作至少有四个 主要 问题:
您包含的 linker 脚本和
_start
函数缺少许多重要部分,并且会出现故障或无法 link 多次执行 table秒。最值得注意的是,它缺少对 BSS(零填充)部分的任何处理。main.c
中的矢量table超出了"minimal";它缺少 required 甚至标准 ARM 中断向量的定义。没有这些,调试硬故障将变得非常困难,因为当发生故障时,微控制器会将向量 table 之后的随机代码视为中断向量,这可能会导致二次故障,因为它无法从中加载代码"address".你书上给出的启动函数绕过了libc的启动函数。这将导致标准 C 库的某些部分以及任何 C++ 代码无法正常工作。
您在
main.c
中自己定义外设地址。这些地址都定义在标准的ST头文件中(例如<stm32f4xx.h>
),所以不需要自己定义。
作为初学者,我建议您参考 ST 在他们的任何示例中提供的启动代码。这些都将包括完整的 linker 脚本和启动代码。
链接器脚本通常是一种艺术形式,它们是自己的编程语言,而 gnu 的肯定有点像噩梦。将任务划分为从制作工作二进制文件中找出 linker 脚本,一旦您看到 linker 脚本正在执行您想要的操作,然后制作 bootstrap 代码来使用它。利用工具链。
作者使用的示例源自专门编写的代码,用作最大限度成功的裸机示例。避免了通用语言和工具链问题,但可以table 跨工具链的许多版本并轻松移植到其他工具链(对工具链的依赖最小,特别是导致 linker 脚本bootstrap)。该书的作者使用了该代码,但增加了风险,使其不如示例可靠。
在编写裸机代码时特别避免 .data 并且不依赖 .bss 归零对长期成功大有帮助。
它也被修改为优化将阻止该代码工作(好吧 blinking 以您可以看到的速度)。
一个稍微简单的示例 linker binutils 脚本,您可以对其进行修改以实现 .data 和 .bss 初始化,大致如下所示
test.ld
MEMORY
{
bob : ORIGIN = 0x8000, LENGTH = 0x1000
ted : ORIGIN = 0xA000, LENGTH = 0x1000
}
SECTIONS
{
.text : { *(.text*) } > bob
__data_rom_start__ = .;
.data : {
__data_start__ = .;
*(.data*)
} > ted AT > bob
__data_end__ = .;
__data_size__ = __data_end__ - __data_start__;
.bss : {
__bss_start__ = .;
*(.bss*)
} > ted
__bss_end__ = .;
__bss_size__ = __bss_end__ - __bss_start__;
}
(注意内存名称不必是 rom 或 ram 或闪存或数据或任何 bob 是程序 space 并且 ted 是内存顺便说一句,根据需要更改地址)
您如何看待正在发生的事情,您可以 link 通过一个简单的示例或您的代码,您需要一些 .data 和一些 .bss(以及一些 .text)。
vectors.s
.thumb
.globl _start
_start:
.word 0x20001000
.word reset
.thumb_func
reset:
bl notmain
b .
.globl bss_start
bss_start: .word __bss_start__
.globl bss_end
bss_end: .word __bss_end__
.word __bss_size__
.globl data_rom_start
data_rom_start:
.word __data_rom_start__
.globl data_start
data_start:
.word __data_start__
.globl data_end
data_end:
.word __data_end__
.word __data_size__
so.c
unsigned int a=1;
unsigned int b=2;
unsigned int c;
unsigned int d;
unsigned int e;
unsigned int notmain ( void )
{
return(a+b+c+d+e);
}
建设
arm-none-eabi-as vectors.s -o vectors.o
arm-none-eabi-gcc -O2 -c -mthumb so.c -o so.o
arm-none-eabi-ld -T test.ld vectors.o so.o -o vectors.elf
arm-none-eabi-objdump -D vectors.elf
到目前为止的代码并不特定于 arm-none-whatever 或 arm-linux-whatever 版本的工具链。 If/when 您需要 gcclib 项,您可以使用 gcc 而不是 ld,但这样做时必须小心......或者提供 libgcc 的路径并使用 ld。
我们从这段代码中得到的是link便宜的脚本调试:
Disassembly of section .text:
00008000 <_start>:
8000: 20001000 andcs r1, r0, r0
8004: 00008009 andeq r8, r0, r9
00008008 <reset>:
8008: f000 f810 bl 802c <notmain>
800c: e7fe b.n 800c <reset+0x4>
0000800e <bss_start>:
800e: 0000a008 andeq sl, r0, r8
00008012 <bss_end>:
8012: 0000a014 andeq sl, r0, r4, lsl r0
8016: 0000000c andeq r0, r0, ip
0000801a <data_rom_start>:
801a: 00008058 andeq r8, r0, r8, asr r0
0000801e <data_start>:
801e: 0000a000 andeq sl, r0, r0
00008022 <data_end>:
8022: 0000a008 andeq sl, r0, r8
8026: 00000008 andeq r0, r0, r8
...
我们关心正在创建的 32 位值,andeq 反汇编是因为反汇编器试图将这些值反汇编为指令,而它们不是。重置指令是真实的,其余是我们生成的 32 位值。可能可以使用 readelf,但要习惯反汇编,确保 vector table 作为第一步是正确的,这在反汇编中很容易看到。将反汇编程序作为一种习惯使用会导致像上面那样使用它来向您展示 linker 生成的内容。
如果您没有正确获取 linker 脚本变量,您将无法编写成功的 bootstrap,如果您没有一个很好的方法来查看 linker 是什么正在生产你会定期失败。
是的,您可以用 C 而不是汇编公开它们,工具链仍然可以帮助您。
你现在可以朝着这个方向努力了,因为你可以看到 linker 在做什么:
.thumb
.globl _start
_start:
.word 0x20001000
.word reset
.thumb_func
reset:
ldr r0,=__bss_start__
ldr r1,=__bss_size__
@ zero this
ldr r0,=__data_rom_start__
ldr r1,=__data_start__
ldr r2,=__data_size__
@ copy this
bl notmain
b .
给这样的东西
00008000 <_start>:
8000: 20001000 andcs r1, r0, r0
8004: 00008009 andeq r8, r0, r9
00008008 <reset>:
8008: 4803 ldr r0, [pc, #12] ; (8018 <reset+0x10>)
800a: 4904 ldr r1, [pc, #16] ; (801c <reset+0x14>)
800c: 4804 ldr r0, [pc, #16] ; (8020 <reset+0x18>)
800e: 4905 ldr r1, [pc, #20] ; (8024 <reset+0x1c>)
8010: 4a05 ldr r2, [pc, #20] ; (8028 <reset+0x20>)
8012: f000 f80b bl 802c <notmain>
8016: e7fe b.n 8016 <reset+0xe>
8018: 0000a008 andeq sl, r0, r8
801c: 0000000c andeq r0, r0, ip
8020: 00008058 andeq r8, r0, r8, asr r0
8024: 0000a000 andeq sl, r0, r0
8028: 00000008 andeq r0, r0, r8
0000802c <notmain>:
802c: 4b06 ldr r3, [pc, #24] ; (8048 <notmain+0x1c>)
802e: 6818 ldr r0, [r3, #0]
8030: 685b ldr r3, [r3, #4]
8032: 18c0 adds r0, r0, r3
如果您随后对齐 linker 脚本中的项目,copy/zero 代码会变得更加简单,您可以坚持使用 1 到 N 个整个寄存器而不是处理字节或半字,可以使用 ldr/str、ldrd/strd(如果可用)或 ldm/stm(不需要 ldrb/strb 或 ldrh/strh),紧密简单的几行循环来完成工作。
我强烈建议您不要为 bootstrap 使用 C。
注意 ld linker 脚本变量对位置非常敏感(大括号内或外)
上面的 linker 脚本有点典型,你会在库存中找到 linker 脚本定义的开始和结束,有时大小是在 linker 中计算的脚本有时 bootstrap 代码计算大小或者 bootstrap 代码可以循环直到地址等于结束值,这取决于两者之间的整体系统设计。
你的具体问题 顺便说一句,你 link 在两个 bootstrap 中编辑,在我写这篇文章的时候,我没有在问题中看到你的命令行,所以这会告诉我们更多.这就是为什么您会看到 bss_start 等您没有放入 linker 脚本但经常在带有预构建工具链的库存工具中找到的东西(类似于上面但更复杂)
可能是通过使用 gcc 而不是 ld 并且没有各种 -nostartfiles 选项(它引入 crt0.o),只需尝试 ld 而不是 gcc 并查看有什么变化。如果原来的例子是这样的话,你会失败的,所以我认为这不是这里的问题。如果您使用相同的命令行,失败应该出现在两个示例上,而不仅仅是后者。
你可以告诉gcc
不要使用这个库。
编译器
默认情况下,gcc 假定您使用的是标准 C 库,并且可以发出调用某些函数的代码。例如,当启用优化时,它会检测复制一块内存的循环,并可能用对 memcpy()
的调用来替换它们。 用-ffreestanding
禁用它。
链接器
linker 还假设您希望link您的程序带有 C 库和启动代码。库启动代码负责初始化库和程序执行环境。它有一个名为 _start()
的函数,必须在重置后调用。它的功能之一是用零填充 .bss
段(见下文)。如果分隔 .bss
的符号未定义,则 _startup()
不能被 link 编辑。如果您将启动函数命名为 任何其他 但 _startup()
,那么库启动将被 linker 作为未使用的函数悄悄丢弃,并且代码本来可以 linked.
你可以告诉 linker 不要 link 任何标准库或带有 -nostdlib
的启动代码,然后库提供启动函数名称不会与您的名称冲突,并且每次您不小心调用库函数时都会出现 linker 错误。
缺失volatile
您的寄存器定义缺少 volatile
限定符。没有它,后续对 *GPIOA_ODR
的写入将被优化掉。编译器会将这个“不变代码”移出循环。将寄存器定义中的类型更改为 (volatile uint32_t*)
将解决此问题。
空循环
优化器可以识别延迟循环什么都不做,并完全消除它以加快执行速度。在延迟循环中添加一个空的但不可移动的 asm volatile("");
指令。
缺少内存屏障
您正在初始化 C 函数中包含 dataVar
的 .data
部分。 __initialize_data()
中的 *p
实际上是 dataVar
的别名,编译器无法知道它。优化器理论上可以在 __initialize_data()
之前重新安排 dataVar
的测试。即使 dataVar
是 volatile
,*p
不是,因此不能保证顺序。
在数据初始化循环之后,你应该告诉编译器程序变量被编译器不知道的机制改变了:
asm volatile("":::"memory");
这是一个老式的 gcc 扩展,最新的 C 标准可能已经定义了一个 portable 方法来做到这一点(旧的 gcc 版本无法识别)。
启用 RCC 后缺少延迟
勘误表说,
A delay between an RCC peripheral clock enable and the effective peripheral enabling should be taken into account in order to manage the peripheral read/write to registers.
This delay depends on the peripheral mapping:
• If the peripheral is mapped on AHB: the delay should be equal to 2 AHB cycles.
• If the peripheral is mapped on APB: the delay should be equal to 1 + (AHB/APB prescaler) cycles.
Workarounds
- Use the DSB instruction to stall the Cortex®-M4 CPU pipeline until the instruction is completed.
因此,插入一个
__DSB();
在 *RCC_APB1ENR = 0x1;
之后(应该叫别的名字)
误导性符号名称
虽然在RCC
中启用GPIOA
的地址看起来是正确的,但是在文档中该寄存器被称为RCC_AHB1ENR
。它会使试图理解您的代码的人感到困惑。
向量Table
虽然从技术上讲,您可以在其中只包含一个堆栈 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
}
链接描述文件
至少,它必须为您的矢量 table 和代码定义一个部分。一个程序必须有一个起始地址和一些代码,静态数据是可选的。其余取决于您的程序使用的数据类型。如果没有特定类型的数据,您可以在技术上从 linker 脚本中省略它们。
.rodata
:只读数据,const
数组和结构放在这里。他们留在闪光。 (简单的const
变量一般放在代码中).data
:初始化的变量,你声明的所有带=
符号,不带const
. 的东西
.bss
:在 C 中应该零初始化的变量,即全局变量和static
变量。
因为你现在不需要.rodata
或.bss
,没关系。
正如old_timer在评论中暗示的那样,使用gcc
到link是一个问题。
如果您将批处理文件中的 linker 调用更改为使用 ld
,它 link 不会出错。请尝试以下操作:
echo.
echo. Call the linker
echo.
@arm-none-eabi-ld main.o -o myApp.elf -T linkerscript.ld