分享

粗解单片机启动文件(startup_xxx.s)

 山峰云绕 2025-10-11 发布于贵州

https://www.toutiao.com/article/7559438290646991411/?log_from=d097221f0494c_1760245851971


启动脚本是连接C语言程序和底层硬件世界的“桥梁”和“向导”,没有它,程序根本不知道如何在微控制器上“启动”和“运行”。它主要做了以下几件事情:

1:搭建了C程序的运行环境 (堆栈、SystemInit、__main)

2: 定义了程序的入口和中断响应机制(中断向量表)

3: 为链接器提供了组织内存布局的起点。(RESET段)

启动脚本用汇编代码写成(需要用汇编器处理),与C程序又紧密关联。官方的两本参考书是《mdk_armasm_user_guide》、《mdk_libraries_user_guide》,以下内容参考上述工具书和AI助手,以STM32L051单片机启动脚本为例,加以理解整理而成,如有错误烦请指正。 mdk_armasm_user_guide

》、《 mdk_libraries_user_guide 》,只要你安装了Keil IDE环境就能找到,Keil->菜单栏->Help->uVision Help/Open books Window,可以看到一系列工具书,

打开今日头条查看图片详情

以下内容参考上述工具书和AI助手,以STM32L051单片机启动脚本为例,加以理解整理而成,如有错误烦请指正。

汇编语言以分号(;)作为注释符,所以启动脚本开头大量的行都是注释行,其中的 Description 部分给出了一些有用的信息,提及初始化SP(堆栈指针寄存器)、PC(程序计数寄存器)、中断向量表及中断服务程序、C库函数__main(它最终调用main函数)。

打开今日头条查看图片详情

这几行定义和分配栈(Stack)空间,为C程序运行准备最基本的环境。 EQU 指令码 类似于C语言中的 #define ,用于定义一个符号常量,Stack_Size为常量的名字,0x400为常量的值。 AREA指令码指示汇编器汇编新的代码段或数据段,STACK是这个段的名称,这个名字是约定俗成的,链接器会识别它并将其放置在内存的合适位置,NOINIT表示这个段“不需要初始化”,READWRITE指定这个段的属性为“可读可写”,ALIGN=3指定该段按2的3次方即8字节对齐。 SPACE指令码用于保留一块0值的内存块,Stack_Mem是一个标签,代表了这片内存区域的起始地址( 起始地址 的值是多少得由链接器来确定),Stack_Size为该块的大小(0x400)。 __initial_sp是一个标签,这个标签是约定俗成的,根据上面一行的内容,Stack_Mem是栈的起始地址(低地址,栈是向下生长的),Stack_Size是栈的大小(0x400),__initial_sp就栈的结束地址(Stack_Mem + Stack_Size),它会被链接器识别为程序初始的主堆栈指针(MSP)的值。

打开今日头条查看图片详情

在内存中分配一块512(0x200)字节、8字节对齐的、可读写的内存区域,专门用作程序的堆 (HEAP) 空间,并定义了两个关键符号__heap_base和__heap_limit来标识堆的边界,为C库的动态内存管理函数(如malloc、free)提供可用的内存池,HEAP、__heap_base、__heap_limit是约定俗成的名字。

C库的初始化代码有三种方式可以用来初始化堆栈:

1:使用标签__initial_sp、__heap_base、__heap_limit。

2:在分散加载文件(.sct)中定义ARM_LIB_STACKHEAP,ARM_LIB_STACK,或ARM_LIB_HEAP域。

3: 实现__user_setup_stackheap函数或__user_initial_stackheap函数。

打开今日头条查看图片详情

有的单片机开发包会注释掉启动脚本里面关于堆栈的代码(包括后文的 __user_initial_stackheap 相关部分),在分散加载文件 (.sct)中定义 ARM_LIB_STACK、 ARM_LIB_HEAP。

打开今日头条查看图片详情

PRESERVE8指令码告诉汇编器“确保当前文件接下来的代码保持8字节栈对齐”。 THUMB指令码告诉汇编器:“当前文件后面的所有代码都应该被汇编为Thumb指令”,Cortex-M系列处理器只支持Thumb指令集,它们无法执行ARM指令。

打开今日头条查看图片详情

定义名称为RESET的只读数据段(虽然向量表本身是只读的,但它里面存放的是指向可执行代码的地址)。 EXPORT指令码导出符号(声明为全局符号),使得链接器能够访问它们,这里导出了__Vectors(向量表的起始地址)、__Vectors_End(向量表的结束地址)、__Vectors_Size(向量表的总大小)三个符号(标签)。 DCD指令码用于“分配一个或多个32位的字(Word)”,它的参数要么是一个数值要么是一个 PC-relative 表达式( 使用程序计数器(PC)的当前值作为基准,通过加减偏移量来计算目标地址,这种表达式常用于跳转指令和子程序调用等场景,能够灵活指定指令执行的目标位置 ),__Vectors标签标志着向量表的开始,__initial_sp为之前定义的主堆栈指针(MSP)的值,芯片复位后要做的第一件事就是从地址0x00000000处取出这个值,并将其加载到主堆栈指针(MSP)寄存器中, 为什么中断向量表稳坐bin文件内容的“头把交椅” ,没有这一步,系统就没有可用的栈。 之后DCD后面都跟着一个函数名(如NMI_Handler),这个函数名就是对应中断的服务程序的入口地址,DCD后面跟0,表示这个中断源在当前的芯片上不存在或不启用,如果该中断发生,会导致硬件错误(HardFault)。ARM Cortex-M内核提供了16个系统异常编号0-15,最多240个外部中断(由芯片厂家(如ST)决定使用多少个)。

打开今日头条查看图片详情

打开今日头条查看图片详情

PROC(与FUNCTION是同义词)指令码标记一个函数的开始,和后面的ENDP成对出现,定义了Reset_Handler函数的范围。 [WEAK]表示“弱定义”,意思是“如果其他地方(比如库文件或用户文件)也定义了一个同名的Reset_Handler函数,那么那个定义将优先,这个弱定义会被忽略” 。 IMPORT指令码告诉汇编器其后的标签(__main、SystemInit)不在本文件中定义,它们是在其他地方(别的.c或.s文件)定义的,请链接器稍后去解析它们。 SystemInit通常是一个由芯片厂商提供的C函数,用于初始化MCU的时钟系统(如设置PLL,提高主频)、配置Flash加速器、配置中断向量表偏移寄存器等。__main是C库的一部分,它由ARM编译器提供,负责完成C运行时环境的最终设置,然后调用你的main函数。 LDR R0, =SystemInit:将SystemInit函数的地址加载到寄存器R0中。BLX R0:“带链接的跳转并切换指令集”,作用是跳转到SystemInit函数并执行它,'B'(Branch):跳转,'L'(Link):将下一条指令的地址(返回地址)保存到LR寄存器(Link Register, R14)中,这样函数执行完后能知道返回哪里,X(eXchange):根据目标地址,如果需要则切换指令集状态(对于 Cortex-M,始终是Thumb状态)。 LDR R0, =__main:将C库函数__main的地址加载到寄存器R0中。BX R0:“跳转并切换指令集”,这里没有'L'(Link),因为这是一个单向跳转,我们不需要返回到启动代码,__main函数将负责一切,最终会调用main函数,如果main函数返回,它可能会进入一个无限循环。

打开今日头条查看图片详情

这一系列标签(如WWDG_IRQHandler)实际上都是指向同一个地址,即它们下面的第一条指令(B .),这意味着,所有这些被弱定义的中断处理程序,如果没有被用户重写,最终都会落入同一个默认的处理逻辑。 B .:“跳转到当前地址”,也就是无限循环,B指令码,即跳转,.表示“当前地址”。 ALIGN:告诉汇编器确保接下来的代码按字(4字节)对齐,以满足处理器的对齐要求。

打开今日头条查看图片详情

IF :DEF:__MICROLIB:条件编译指令,检查是否定义了__MICROLIB这个宏。在Keil MDK中,你可以选择使用两种C库:标准C库,功能完整,但体积较大,微库(MicroLib),专为嵌入式系统优化的精简C库,体积小但功能较少,当使用微库时(if条件成立),链接器可以直接使用已经定义好的符号值(__initial_sp、__heap_base、__heap_limit)来设置堆栈,不需要调用初始化函数。如果使用标准C库(else分支),IMPORT __use_two_region_memory:告诉汇编器,符号__use_two_region_memory 是在其他地方定义的,这个符号表示使用双区域内存模型(栈和堆是分开的独立内存区域)。EXPORT __user_initial_stackheap:导出__user_initial_stackheap函数,标准C库在初始化时会自动调用这个函数来获取堆栈的配置信息。__user_initial_stackheap负责为标准C库提供堆栈的布局信息,该函数通过寄存器返回4个关键内存地址:LDR R0, = Heap_Mem:R0 = 堆的起始地址(Heap_Mem),C库的malloc、free等函数将使用这片区域,LDR R1, =(Stack_Mem + Stack_Size):R1 = 栈的结束地址(也就是初始栈指针值__initial_sp),因为栈是向下生长的,所以结束地址就是栈顶,LDR R2, = (Heap_Mem + Heap_Size):R2 = 堆的结束地址,定义了堆空间的上限,LDR R3, = Stack_Mem:R3 = 栈的起始地址(栈底),栈是从高地址向低地址生长的,所以这是栈的最低地址,BX LR:函数返回,将控制权交回C库初始化代码。ENDIF:结束最开始的IF :DEF:__MICROLIB条件编译块。END:汇编文件结束指令,告诉汇编器后面没有更多代码了。

《mdk_libraries_user_guide》 -> 1.8.1 Initialization of the execution environment and execution of the application,详细说明__main的执行过程。

《 mdk_armasm_user_guide 》 -> 2.7 ARM registers,ARM处理器提供的寄存器。

《 mdk_armasm_user_guide 》 -> 22.10 Predeclared core register names,寄存器的别名。

《 mdk_armasm_user_guide 》 -> 1.3 How the assembler works,详细说明汇编器两遍扫描的工作方式。

https://www.toutiao.com/article/7559438290646991411/?log_from=d097221f0494c_1760245851971

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多