在嵌入式开发领域,GD32 微控制器凭借其强大的性能和丰富的功能得到了广泛应用。而启动文件作为系统启动的基石,其内部的代码逻辑和机制对于系统的正常运行起着至关重要的作用。本文将对 GD32 启动文件进行详细解读,深入剖析其各个功能模块的实现原理和意义。
一、引言
GD32 微控制器基于 ARM Cortex-M 内核,其启动过程涉及到硬件初始化、内存设置、中断向量表配置以及程序执行的引导等多个关键环节。启动文件通过一系列精心设计的汇编代码,有条不紊地完成这些任务,为后续的 C/C++ 程序运行搭建稳定的基础。深入理解启动文件的工作机制,有助于开发者更好地掌握 GD32 系统的底层运行逻辑。
二、启动文件的基本设置与全局符号
(一)汇编指令集与处理器相关设置
启动文件开头的指令:
.syntax unified
.cpu cortex-m3
.fpu softvfp
.thumb
.syntax unified声明采用统一汇编语法,这种语法结合了 ARM 传统汇编和 Thumb 汇编的特点,使得代码编写更加规范和易于理解,同时也方便不同编译器之间的兼容。.cpu cortex-m3明确指定了目标处理器为 Cortex-M3 内核,这确保了后续生成的指令集能够与该内核的硬件架构完美匹配,充分发挥其性能优势。例如,Cortex-M3 内核的流水线结构、寄存器组等硬件特性都需要相应的指令集来高效利用。.fpu softvfp配置了软件实现的浮点运算单元(softvfp),在硬件资源有限无法提供硬件浮点运算支持的情况下,通过软件模拟的方式实现浮点运算,这为一些需要处理浮点数据但又受限于硬件成本的应用场景提供了可能。.thumb指令指示编译器生成 Thumb 指令集代码,Thumb 指令集以 16 位和 32 位混合编码的形式存在,相较于 ARM 指令集,它能够显著提高代码密度,减少程序占用的存储空间,这对于资源受限的嵌入式系统来说是非常关键的优化措施。
(二)重要全局符号的作用
.global g_pfnVectors
.global Default_Handler
.global g_pfnVectors声明了一个全局变量g_pfnVectors,它在 GD32 系统中具有极其重要的地位,作为中断向量表的起始地址,在系统运行过程中,当发生中断事件时,处理器会依据中断向量表中存储的地址信息快速定位并调用相应的中断处理程序,而g_pfnVectors就是这个关键地址表的入口指针。例如,在外部中断触发时,处理器会首先从g_pfnVectors指向的地址开始查找对应的中断处理程序地址,然后跳转执行,确保系统能够及时响应外部事件。.global Default_Handler定义了默认的中断处理程序,在系统遇到未预期的中断或者特定中断处理程序尚未实现时,程序流程会自动跳转到Default_Handler。通常情况下,Default_Handler会进入一个无限循环,保持系统状态,以便开发人员在调试过程中能够通过调试工具查看系统的状态信息,从而找出中断异常的原因,这种机制有效地防止了系统在异常情况下出现不可控的行为,为系统的稳定性和可调试性提供了保障。
三、数据段与 BSS 段的初始化过程
(一)数据段复制的详细步骤
Reset_Handler:
/* Copy the data segment initializers from flash to SRAM */
movs r1, #0
b LoopCopyDataInit
CopyDataInit:
ldr r3, =_sidata
ldr r3, [r3, r1]
str r3, [r0, r1]
adds r1, r1, #4
LoopCopyDataInit:
ldr r0, =_sdata
ldr r3, =_edata
adds r2, r0, r1
cmp r2, r3
bcc CopyDataInit
在Reset_Handler中,首先movs r1, #0将寄存器r1初始化为 0,这个寄存器用于记录数据复制的偏移量。接着跳转到LoopCopyDataInit循环。在CopyDataInit标签处,ldr r3, =_sidata将r3指向 Flash 中数据段初始值的起始地址_sidata,然后ldr r3, [r3, r1]从该地址加载数据到r3寄存器,这里的[r3, r1]表示以r3为基地址,偏移量为r1的内存位置。之后str r3, [r0, r1]将r3中的数据存储到 SRAM 中数据段的起始地址_sdata加上偏移量r1的位置,完成一次数据从 Flash 到 SRAM 的复制。随后adds r1, r1, #4将偏移量增加 4 字节,准备复制下一个数据。在LoopCopyDataInit循环中,ldr r0, =_sdata和ldr r3, =_edata分别获取 SRAM 中数据段的起始地址和结束地址,adds r2, r0, r1计算当前复制的目标地址,cmp r2, r3比较当前目标地址和结束地址,如果当前地址小于结束地址(bcc CopyDataInit),则继续循环复制数据,直到数据段所有数据都被复制到 SRAM 中。这一过程是必要的,因为在程序运行过程中,数据段中的变量需要可读写,而 Flash 通常是只读的,将数据复制到可读写的 SRAM 中才能保证程序正常运行。
(二)BSS 段清零的实现机制
ldr r2, =_sbss
b LoopFillZerobss
FillZerobss:
movs r3, #0
str r3, [r2]
adds r2, r2, #4
LoopFillZerobss:
ldr r3, = _ebss
cmp r2, r3
bcc FillZerobss
首先ldr r2, =_sbss将寄存器r2指向 BSS 段的起始地址_sbss,然后跳转到LoopFillZerobss循环。在FillZerobss标签处,movs r3, #0将r3寄存器设置为 0,str r3, [r2]将 0 值存储到r2指向的 BSS 段地址,接着adds r2, r2, #4更新地址指针,指向下一个未初始化变量的存储位置。在LoopFillZerobss循环中,ldr r3, = _ebss获取 BSS 段的结束地址,cmp r2, r3比较当前地址和结束地址,如果当前地址小于结束地址(bcc FillZerobss),则继续循环清零操作,直到 BSS 段所有变量都被初始化为 0。BSS 段主要用于存放未初始化的全局变量和静态变量,将其清零可以防止这些变量在程序运行过程中产生随机值,影响程序的逻辑正确性。
四、系统初始化与函数调用流程
(一)时钟系统初始化的关键作用
bl SystemInit
bl SystemInit指令调用SystemInit函数进行时钟系统初始化。时钟系统是微控制器运行的核心组件之一,其配置的准确性和合理性直接影响到系统的性能和稳定性。在SystemInit函数内部,通常会根据硬件设计和应用需求进行一系列复杂的操作。例如,会设置外部时钟源(如晶振频率),这是系统时钟的基础,稳定的外部时钟源是保证系统正常运行的前提。同时,还会配置内部时钟分频系数,根据不同外设和系统模块的工作频率要求,合理分配时钟频率,确保各个模块能够在合适的时钟节拍下工作。比如,对于一些对实时性要求较高的通信模块,可能需要较高的时钟频率来保证数据传输的及时性;而对于一些功耗敏感的模块,则可能会配置较低的时钟频率以降低功耗。通过精确的时钟系统初始化,能够使系统在性能和功耗之间达到平衡,提高系统的整体运行效率。
(二)C++ 静态构造函数调用的意义
bl __libc_init_array
bl __libc_init_array用于调用 C++ 静态构造函数。在 C++ 编程中,静态对象在程序启动时需要进行初始化操作。这些静态构造函数负责初始化静态数据成员、分配资源等任务。例如,在一个包含多个静态对象的 C++ 程序中,每个静态对象的构造函数可能会初始化其自身的特定数据结构,或者与其他静态对象进行交互和初始化。通过在启动文件中调用__libc_init_array函数,能够确保 C++ 程序中的所有静态对象都按照正确的顺序进行初始化,维护了 C++ 语言的语义和对象生命周期规则。这一步骤是保证 C++ 程序在进入main函数之前,所有相关的静态资源都已准备就绪的关键,避免了因静态对象未正确初始化而导致的程序运行错误。
(三)应用程序入口点调用的核心地位
bl main
bx lr
bl main指令将程序执行权转移到main函数,标志着用户应用程序的正式开始。main函数作为 C/C++ 程序的核心,承载着用户自定义的业务逻辑和功能实现代码。在main函数中,开发者可以根据具体的应用需求进行各种操作,如初始化外设、处理传感器数据、实现控制算法等。例如,在一个智能温度控制系统中,main函数可能会首先初始化温度传感器和加热或制冷设备的驱动程序,然后不断读取传感器数据,根据设定的温度阈值进行判断,并控制加热或制冷设备的运行状态。bx lr指令用于从main函数返回后,恢复程序的执行状态,确保系统能够正确地继续执行后续的代码或处理其他事件。
五、中断向量表与异常处理机制
(一)中断向量表的结构与原理
g_pfnVectors:
.word _estack /* Top of Stack */
.word Reset_Handler /* Reset Handler */
.word NMI_Handler /* NMI Handler */
.word HardFault_Handler /* Hard Fault Handler */
.word MemManage_Handler /* MPU Fault Handler */
.word BusFault_Handler /* Bus Fault Handler */
.word UsageFault_Handler /* Usage Fault Handler */
.word 0 /* Reserved */
.word 0 /* Reserved */
.word 0 /* Reserved */
.word 0 /* Reserved */
.word SVC_Handler /* SVCall Handler */
.word DebugMon_Handler /* Debug Monitor Handler */
.word 0 /* Reserved */
.word PendSV_Handler /* PendSV Handler */
.word SysTick_Handler /* SysTick Handler */
.word WWDGT_IRQHandler /* 16:Window Watchdog Timer */
.word LVD_IRQHandler /* 17:LVD through EXTI Line detect */
.word TAMPER_IRQHandler /* 18:Tamper Interrupt */
.word RTC_IRQHandler /* 19:RTC through EXTI Line */
.word FMC_IRQHandler /* 20:FMC */
.word RCU_IRQHandler /* 21:RCU */
.word EXTI0_IRQHandler /* 22:EXTI Line 0 */
.word EXTI1_IRQHandler /* 23:EXTI Line 1 */
.word EXTI2_IRQHandler /* 24:EXTI Line 2 */
.word EXTI3_IRQHandler /* 25:EXTI Line 3 */
.word EXTI4_IRQHandler /* 26:EXTI Line 4 */
.word DMA0_Channel0_IRQHandler /* 27:DMA0 Channel 0 */
.word DMA0_Channel1_IRQHandler /* 28:DMA0 Channel 1 */
.word DMA0_Channel2_IRQHandler /* 29:DMA0 Channel 2 */
.word DMA0_Channel3_IRQHandler /* 30:DMA0 Channel 3 */
.word DMA0_Channel4_IRQHandler /* 31:DMA0 Channel 4 */
.word DMA0_Channel5_IRQHandler /* 32:DMA0 Channel 5 */
.word DMA0_Channel6_IRQHandler /* 33:DMA0 Channel 6 */
.word ADC0_1_IRQHandler /* 34:ADC0 and ADC1 */
.word USBD_HP_CAN0_TX_IRQHandler /* 35:USBD and CAN0 TX */
.word USBD_LP_CAN0_RX0_IRQHandler /* 36:USBD and CAN0 RX0 */
.word CAN0_RX1_IRQHandler /* 37:CAN0 RX1 */
.word CAN0_EWMC_IRQHandler /* 38:CAN0 EWMC */
.word EXTI5_9_IRQHandler /* 39:EXTI Line 5 to EXTI Line 9 */
.word TIMER0_BRK_IRQHandler /* 40:TIMER0 Break */
.word TIMER0_UP_IRQHandler /* 41:TIMER0 Update */
.word TIMER0_TRG_CMT_IRQHandler /* 42:TIMER0 Trigger */
.word TIMER0_Channel_IRQHandler /* 43:TIMER0 Channel Capture Compare */
.word TIMER1_IRQHandler /* 44:TIMER1 */
.word TIMER2_IRQHandler /* 45:TIMER2 */
.word TIMER3_IRQHandler /* 46:TIMER3 */
.word I2C0_EV_IRQHandler /* 47:I2C0 Event */
.word I2C0_ER_IRQHandler /* 48:I2C0 Error */
.word I2C1_EV_IRQHandler /* 49:I2C1 Event */
.word I2C1_ER_IRQHandler /* 50:I2C1 Error */
.word SPI0_IRQHandler /* 51:SPI0 */
.word SPI1_IRQHandler /* 52:SPI1 */
.word USART0_IRQHandler /* 53:USART0 */
.word USART1_IRQHandler /* 54:USART1 */
.word USART2_IRQHandler /* 55:USART2 */
.word EXTI10_15_IRQHandler /* 56:EXTI Line 10 to EXTI Line 15 */
.word RTC_Alarm_IRQHandler /* 57:RTC Alarm through EXTI Line */
.word USBD_WKUP_IRQHandler /* 58:USBD WakeUp from suspend through EXTI Line */
.word 0 /* Reserved */
.word 0 /* Reserved */
.word 0 /* Reserved */
.word 0 /* Reserved */
.word 0 /* Reserved */
.word EXMC_IRQHandler /* 64:EXMC */
中断向量表g_pfnVectors以特定的顺序存储了各个中断和异常处理程序的入口地址。首先是栈顶指针_estack,它定义了系统栈的顶部位置,在系统运行过程中,栈用于存储函数调用的局部变量、返回地址等信息,正确设置栈顶指针是保证函数调用和中断处理过程中栈操作正常进行的关键。接着是Reset_Handler,它是系统复位后首先执行的程序,负责完成系统的初始化工作,如上述的数据段和 BSS 段初始化、时钟系统初始化等。后续依次是各种中断和异常处理程序的入口地址,例如NMI_Handler用于处理不可屏蔽中断,当系统发生严重错误或紧急事件时,该中断会被触发,处理器会立即跳转到NMI_Handler执行相应的处理逻辑。每个中断处理程序在系统相应中断事件发生时,都会被处理器快速调用,确保系统能够及时响应和处理各种外部和内部事件,维护系统的正常运行秩序。
(二)异常处理程序弱别名机制的优势
.weak NMI_Handler
.thumb_set NMI_Handler,Default_Handleweak HardFault_Handler
.thumb_set HardFault_Handler,Default_Handler
.weak MemManage_Handler
.thumb_set MemManage_Handler,Default_Handler
.weak BusFault_Handler
.thumb_set BusFault_Handler,Default_Handler
.weak UsageFault_Handler
.thumb_set UsageFault_Handler,Default_Handler
...省略
通过一系列的.weak 声明和.thumb_set 指令为每个异常处理程序设置了弱别名指向 Default_Handler。这种机制具有显著的优势。在实际开发中,如果开发者没有为某个特定的中断或异常编写专门的处理程序,系统在遇到相应情况时会自动跳转到 Default_Handler。例如在一个简单的项目中,如果没有实现 NMI_Handler,当不可屏蔽中断发生时,程序会执行 Default_Handler 进入无限循环,这有助于保持系统状态,方便调试人员通过调试工具查看系统在中断发生时的寄存器值、内存状态等信息,从而快速定位问题所在。而当开发者需要为某个中断或异常提供定制化的处理逻辑时,只需在代码中定义与弱别名同名的处理程序,新定义的函数就会覆盖原有的弱别名指向,使得系统在相应事件发生时执行开发者自定义的代码。比如在一个对定时器中断精度要求较高的应用中,开发者可以编写 TIMER0_UP_IRQHandler 函数来实现更精确的定时器中断处理逻辑,而不必担心与启动文件中的默认设置冲突,这种机制极大地提高了系统的灵活性和可扩展性,既为开发者提供了便捷的默认处理方式,又允许他们根据具体需求进行深度定制。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/pigliuxu/article/details/145167024
|
|