news 2026/1/20 5:44:16

使用C语言实现STM32的启动文件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用C语言实现STM32的启动文件

编译器:GCC

传统的启动文件使用汇编语言实现,可读性很低,现在分析其内容,使用C语言重新实现一遍。

完整的代码

首先附上成品,使用C11标准:

项目地址 :Gitee仓库

#include <stddef.h>

#include <stdint.h>

/**

* @brief startup.c for stm32f103c8t6

* @syntax unified

* @cpu cortex-m3

* @fpu softvfp

* @thumb

*/

/* Highest address of the user mode stack */

extern uint8_t _estack[];

/* defined in linker script */

extern uint8_t _sidata[]; /* start address for the initialization values of the .data section.*/

/* start address for the .data section. defined in linker script */

extern uint8_t _sdata[];

/* end address for the .data section. defined in linker script */

extern uint8_t _edata[];

/* start address for the .bss section. defined in linker script */

extern uint8_t _sbss[];

/* end address for the .bss section. defined in linker script */

extern uint8_t _ebss[];

const uint32_t BootRAM = 0xF108F85F;

typedef void (*interruptHandlerType)(void); // use typedef to define the type of interruptHandlerType

const interruptHandlerType g_pfnVectors[]; // declare the interrupt vector table

void Default_Handler(void); // declare the default interrupt handler

void Reset_Handler(void) __attribute__((noreturn)); // declare the reset handler

/*******************************************************************************

*

* Provide weak aliases for each Exception handler to the Default_Handler.

* As they are weak aliases, any function with the same name will override

* this definition.

*

*******************************************************************************/

#define WEAK_ALIAS __attribute__((weak, alias("Default_Handler")))

WEAK_ALIAS void NMI_Handler(void);

WEAK_ALIAS void HardFault_Handler(void);

WEAK_ALIAS void MemManage_Handler(void);

WEAK_ALIAS void BusFault_Handler(void);

WEAK_ALIAS void UsageFault_Handler(void);

WEAK_ALIAS void SVC_Handler(void);

WEAK_ALIAS void DebugMon_Handler(void);

WEAK_ALIAS void PendSV_Handler(void);

WEAK_ALIAS void SysTick_Handler(void);

WEAK_ALIAS void WWDG_IRQHandler(void);

WEAK_ALIAS void PVD_IRQHandler(void);

WEAK_ALIAS void TAMPER_IRQHandler(void);

WEAK_ALIAS void RTC_IRQHandler(void);

WEAK_ALIAS void FLASH_IRQHandler(void);

WEAK_ALIAS void RCC_IRQHandler(void);

WEAK_ALIAS void EXTI0_IRQHandler(void);

WEAK_ALIAS void EXTI1_IRQHandler(void);

WEAK_ALIAS void EXTI2_IRQHandler(void);

WEAK_ALIAS void EXTI3_IRQHandler(void);

WEAK_ALIAS void EXTI4_IRQHandler(void);

WEAK_ALIAS void DMA1_Channel1_IRQHandler(void);

WEAK_ALIAS void DMA1_Channel2_IRQHandler(void);

WEAK_ALIAS void DMA1_Channel3_IRQHandler(void);

WEAK_ALIAS void DMA1_Channel4_IRQHandler(void);

WEAK_ALIAS void DMA1_Channel5_IRQHandler(void);

WEAK_ALIAS void DMA1_Channel6_IRQHandler(void);

WEAK_ALIAS void DMA1_Channel7_IRQHandler(void);

WEAK_ALIAS void ADC1_2_IRQHandler(void);

WEAK_ALIAS void USB_HP_CAN1_TX_IRQHandler(void);

WEAK_ALIAS void USB_LP_CAN1_RX0_IRQHandler(void);

WEAK_ALIAS void CAN1_RX1_IRQHandler(void);

WEAK_ALIAS void CAN1_SCE_IRQHandler(void);

WEAK_ALIAS void EXTI9_5_IRQHandler(void);

WEAK_ALIAS void TIM1_BRK_IRQHandler(void);

WEAK_ALIAS void TIM1_UP_IRQHandler(void);

WEAK_ALIAS void TIM1_TRG_COM_IRQHandler(void);

WEAK_ALIAS void TIM1_CC_IRQHandler(void);

WEAK_ALIAS void TIM2_IRQHandler(void);

WEAK_ALIAS void TIM3_IRQHandler(void);

WEAK_ALIAS void TIM4_IRQHandler(void);

WEAK_ALIAS void I2C1_EV_IRQHandler(void);

WEAK_ALIAS void I2C1_ER_IRQHandler(void);

WEAK_ALIAS void I2C2_EV_IRQHandler(void);

WEAK_ALIAS void I2C2_ER_IRQHandler(void);

WEAK_ALIAS void SPI1_IRQHandler(void);

WEAK_ALIAS void SPI2_IRQHandler(void);

WEAK_ALIAS void USART1_IRQHandler(void);

WEAK_ALIAS void USART2_IRQHandler(void);

WEAK_ALIAS void USART3_IRQHandler(void);

WEAK_ALIAS void EXTI15_10_IRQHandler(void);

WEAK_ALIAS void RTCAlarm_IRQHandler(void);

WEAK_ALIAS void USBWakeUp_IRQHandler(void);

/******************************************************************************

*

* The minimal vector table for a Cortex M3. Note that the proper constructs

* must be placed on this to ensure that it ends up at physical address

* 0x0000.0000.

*

******************************************************************************/

__attribute__((section(".isr_vector"), used))

const interruptHandlerType g_pfnVectors[] =

{

(void *)_estack,

Reset_Handler,

NMI_Handler,

HardFault_Handler,

MemManage_Handler,

BusFault_Handler,

UsageFault_Handler,

NULL, NULL, NULL, NULL,

SVC_Handler,

DebugMon_Handler,

NULL,

PendSV_Handler,

SysTick_Handler,

WWDG_IRQHandler,

PVD_IRQHandler,

TAMPER_IRQHandler,

RTC_IRQHandler,

FLASH_IRQHandler,

RCC_IRQHandler,

EXTI0_IRQHandler,

EXTI1_IRQHandler,

EXTI2_IRQHandler,

EXTI3_IRQHandler,

EXTI4_IRQHandler,

DMA1_Channel1_IRQHandler,

DMA1_Channel2_IRQHandler,

DMA1_Channel3_IRQHandler,

DMA1_Channel4_IRQHandler,

DMA1_Channel5_IRQHandler,

DMA1_Channel6_IRQHandler,

DMA1_Channel7_IRQHandler,

ADC1_2_IRQHandler,

USB_HP_CAN1_TX_IRQHandler,

USB_LP_CAN1_RX0_IRQHandler,

CAN1_RX1_IRQHandler,

CAN1_SCE_IRQHandler,

EXTI9_5_IRQHandler,

TIM1_BRK_IRQHandler,

TIM1_UP_IRQHandler,

TIM1_TRG_COM_IRQHandler,

TIM1_CC_IRQHandler,

TIM2_IRQHandler,

TIM3_IRQHandler,

TIM4_IRQHandler,

I2C1_EV_IRQHandler,

I2C1_ER_IRQHandler,

I2C2_EV_IRQHandler,

I2C2_ER_IRQHandler,

SPI1_IRQHandler,

SPI2_IRQHandler,

USART1_IRQHandler,

USART2_IRQHandler,

USART3_IRQHandler,

EXTI15_10_IRQHandler,

RTCAlarm_IRQHandler,

USBWakeUp_IRQHandler,

NULL, NULL, NULL, NULL, NULL, NULL, NULL,

(void *)BootRAM /* @0x108. This is for boot in RAM mode for STM32F10x Medium Density devices. */

};

/**

* @brief This is the code that gets called when the processor receives an

* unexpected interrupt. This simply enters an infinite loop, preserving

* the system state for examination by a debugger.

*

* @param None

* @retval : None

*/

void Default_Handler(void)

{

while (1) {

/* Infinite loop */

}

}

/**

* @brief data sector initialization function

*

*/

static void CopyDataInit(void)

{

const uint32_t *src = (void *)_sidata;

const uint32_t *data_end = (void *)_edata;

for (uint32_t *p = (void *)_sdata; p < data_end; p++, src++) {

*p = *src;

}

}

/**

* @brief bss sector zero initialization function

*

*/

/* BSS zero initialization function */

static void FillZerobss(void)

{

const uint32_t *bss_end = (void *)_ebss;

for (uint32_t *p = (void *)_sbss; p < bss_end; p++) {

*p = 0x00;

}

}

/**

* @brief This is the code that gets called when the processor first

* starts execution following a reset event. Only the absolutely

* necessary set is performed, after which the application

* supplied main() routine is called.

* @param None

* @retval : None

*/

// the function will never return

void Reset_Handler(void)

{

extern void SystemInit(void); // defined in @system_stm32f1xx.c

extern int main(void); // defined in @main.c

extern void __libc_init_array(void); // defined in @newlib

/* Call the clock system initialization function */

SystemInit();

/* Initialize data and bss sections */

CopyDataInit();

FillZerobss();

/* Call static constructors */

__libc_init_array();

/* Call the application's entry point */

main();

/* Should never reach here */

while (1) {

/* Infinite loop */

}

}

程序分析

链接器脚本分析

以STM32CubeMX生成使用的CMake工具链的stm32f103c8t6的项目为例,有一个启动文件 startup_stm32f103xb.s 和链接器脚本 STM32F103XX_FLASH.ld,启动文件中定义的内容是上电以后执行的第一件事情,而链接器脚本指定程序的链接方式和内存区域分配方式。

首先分析链接器脚本文件进行逐段分析:

/* Entry Point */

ENTRY(Reset_Handler)

定义了入口函数,也即上电之后执行的第一个函数,此处为 Reset_Handler。

/* Specify the memory areas */

MEMORY

{

RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K

FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 64K

}

定义了内存区域,分别为:

RAM 区域,可执行(x)、可读(r)、可写(w)的区域,大小为 20K,起始地址为 0x20000000。

FLASH 区域,可读(r)、可执行(x),大小为 64K,起始地址为 0x8000000。

/* Highest address of the user mode stack */

_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of RAM */

定义程序栈起始地址 _estack,由于栈是从高地址向低地址延伸,所以起始地址定义为 RAM 区域的结束位置。

/* Generate a link error if heap and stack don't fit into RAM */

_Min_Heap_Size = 0x0; /* required amount of heap */

_Min_Stack_Size = 0x400; /* required amount of stack */

定义堆和栈的大小。如果需要使用 malloc 等动态内存分配,则分配的内存就位于堆中。此处不用,因此设为0。

然后是段定义:

/* Define output sections */

SECTIONS

{

/*...... */

}

分为几个段:

/* The startup code goes first into FLASH */

.isr_vector :

{

. = ALIGN(4);

KEEP(*(.isr_vector)) /* Startup code */

. = ALIGN(4);

} >FLASH

为中断向量表所在的地址

/* Constant data goes into FLASH */

.rodata :

{

. = ALIGN(4);

*(.rodata) /* .rodata sections (constants, strings, etc.) */

*(.rodata*) /* .rodata* sections (constants, strings, etc.) */

. = ALIGN(4);

} >FLASH

定义const 变量存放在 Flash 中。

接下来的 .ARM.extab、.ARM、.preinit_array、.init_array、.fini_array 与C++有关,略过。

接着是

/* used by the startup to initialize data */

_sidata = LOADADDR(.data);

定义符号 _sidata,用于指定 .data 段在Flash中的起始地址,这样就可以在C代码文件中使用这个“变量”

/* Initialized data sections goes into RAM, load LMA copy after code */

.data :

{

. = ALIGN(4);

_sdata = .; /* create a global symbol at data start */

*(.data) /* .data sections */

*(.data*) /* .data* sections */

*(.RamFunc) /* .RamFunc sections */

*(.RamFunc*) /* .RamFunc* sections */

. = ALIGN(4);

} >RAM AT> FLASH

此处定义了 .data 段,用于存放已经初始化过的全局变量,这些变量的值会存放在Flash中,在程序运行时,在启动文件中将其复制到RAM中的对应位置。

.bss (NOLOAD) : ALIGN(4)

{

*(.bss)

*(.bss*)

*(COMMON)

. = ALIGN(4);

_ebss = .; /* define a global symbol at bss end */

__bss_end__ = _ebss;

PROVIDE( __bss_end = .);

} >RAM

定义了 .bss 段,用于存放未初始化的全局变量,这些变量的值在程序运行时会被初始化为0。

/* User_heap_stack section, used to check that there is enough RAM left */

._user_heap_stack (NOLOAD) :

{

. = ALIGN(8);

PROVIDE ( end = . );

PROVIDE ( _end = . );

. = . + _Min_Heap_Size;

. = . + _Min_Stack_Size;

. = ALIGN(8);

} >RAM

定义了一个 .user_heap_stack 段,用于堆栈的分配,保存在RAM中。

/* Remove information from the standard libraries */

/DISCARD/ :

{

libc.a:* ( * )

libm.a:* ( * )

libgcc.a:* ( * )

}

移除 libc.a、libm.a、libgcc.a 等标准库符号,减小程序体积。

启动文件分析并重写

接着分析启动并重写文件:

首先是

.syntax unified

.cpu cortex-m3

.fpu softvfp

.thumb

定义了cpu、fpu、指令集等内容,略过。

然后

.global g_pfnVectors

.global Default_Handler

相当于C语言中声明了两个全局变量。第一个是中断向量组,第二个是默认的中断处理函数。

中断向量组中是紧凑排列的各种中断函数的入口地址,我们知道所有的中断函数都是 void handler(void) 类型,因此它等价为一个函数指针数组,为了防止不小心修改,可以添加 const 限定符:

typedef void (*interruptHandlerType)(void); // 中断函数指针类型

const interruptHandlerType g_pfnVectors[]; // 声明中断向量表

然后是

/* start address for the initialization values of the .data section.

defined in linker script */

.word _sidata

/* start address for the .data section. defined in linker script */

.word _sdata

/* end address for the .data section. defined in linker script */

.word _edata

/* start address for the .bss section. defined in linker script */

.word _sbss

/* end address for the .bss section. defined in linker script */

.word _ebss

.equ BootRAM, 0xF108F85F

声明了几个 .word 类型的变量,相当于C语言中的 uint32_t 类型,这些符号定义在链接器脚本中,用于指定各个数据段的起始、终止地址。最后的 .equ BootRAM, 0xF108F85F 定义了从RAM中启动的地址,需要加在中断向量组的最后。因此在C语言中,我们可以定义:

extern uint32_t _sdata;

// ***

const uint32_t BootRAM = 0xF108F85F;

使用时,需要进行取地址操作,得到 data 段的起始地址。

但实际上我们并不关心变量的“类型”,只需要知道链接脚本中定义的“符号”,表示这个“变量”保存在这个位置,也即“变量的地址”是“data”段的起始地址。因此直接将其定义为 uint8_t[] 类型:extern uint8_t _sdata[];,这样便省去了去地址的操作。

接着是 Reset_Handler 函数的定义,它是上电之后第一个执行的函数。下面是它的声明:

.section .text.Reset_Handler // 定义代码段

.weak Reset_Handlel // 定义为弱符号

.type Reset_Handler, %function // 定义为函数类型

这一部分可以改写为C代码:

__attribute__((weak))

void Reset_Handler(void);

然后是函数体:

Reset_Handler:

bl SystemInit // 调用 SystemInit 函数

// 从 Flash 中复制数据到 data 段

// 清零 bss 段

bl __libc_init_array // 调用 C++ 静态构造函数

bl main // 调用 main 函数,进入主程序

bx lr // 相当于 main 函数中的 return

可以改写为C代码:

void data_init(void);

void bss_init(void);

__attribute__((noreturn))

void Reset_Handler(void)

{

SystemInit(); // 调用 SystemInit 函数

data_init(); // 复制数据到data段

bss_init(); // 清零bss段

__libc_init_array(); // 调用C++静态构造函数

main(); // 进入主程序

while (true);

}

其中 data_init 和 bss_init 是我们自己定义的两个函数,用于初始化 .data 和 .bss 段,其汇编代码如下(注释部分为C风格伪代码):

/* Copy the data segment initializers from flash to SRAM */

ldr r0, =_sdata // r0 = _sdata;

ldr r1, =_edata // r1 = _edata;

ldr r2, =_sidata // r2 = _sidata;

movs r3, #0 // r3 = 0;

b LoopCopyDataInit // goto LoopCopyDataInit;

CopyDataInit: // CopyDataInit:

ldr r4, [r2, r3] // r4 = *(uint32_t*)(r2 + r3);

str r4, [r0, r3] // *(uint32_t*)(r0 + r2) = r4;

// // 两句合起来等价于:

// // *(uint32_t*)(r0 + r2) = *(uint32_t*)(r2 + r2);

adds r3, r3, #4 // r3 += 4;

LoopCopyDataInit: // LoopCopyDataInit:

adds r4, r0, r3 // r4 = r0 + r3;

cmp r4, r1 // cmp res(r4,r1); // 假设有个enum cmp用于存储两个数字比较情况

bcc CopyDataInit // if(cmp == less){ goto CopyDataInit; }

/* Zero fill the bss segment. */

ldr r2, =_sbss

ldr r4, =_ebss

movs r3, #0

b LoopFillZerobss

FillZerobss:

str r3, [r2]

adds r2, r2, #4

LoopFillZerobss:

cmp r2, r4

bcc FillZerobss

可以看到,本质上就是把 flash 中的数据拷贝到 ram 中,拷贝的长度为 _edata - _sdata,源地址为 flash 中的 _sidata,目的地址为 ram 中的 _sdata。清零部分同理,只不过是把拷贝改为设置为0。需要注意为了增加处理效率,一次复制 4 字节,使用 uint32_t* 类型指针来操作。因此其逻辑改写为C代码如下:

extern uint8_t _sdata[];

extern uint8_t _edata[];

extern uint8_t _sidata[];

uint32_t *data_start = _sdata; // data段的起始地址

uint32_t *data_end = _edata; // data段的结束地址

uint32_t *source_addr = _sidata; // flash中data的数据的起始地址

uint32_t offset = 0;

// 以 word 为单位,也即4字节为单位,从 flash 中拷贝数据到 ram 中

while (offset < data_end) {

uint32_t *src = (void*)_sidata + offset;

uint32_t *dst = (void*)_sdata + offset;

*dst = *src; // 从 Flash 中拷贝数据到 RAM 中

offset += 4; // 指针偏移4字节,也即一个 word 的长度

}

// 清零部分略

可以借助C库函数 memcpy 和 memset 来实现 data_init 和 bss_init 函数,肯定比逐个复制更高效,但是会多占大约 400Byte 的 Flash 空间,因此此处仅作示例,实际使用中最好还是逐字复制。

/* Data copy function */

static void data_init(void)

{

size_t data_size = _edata - _sdata; // get data size

memcpy(_sdata, _sidata, data_size); // copy data from flash to ram

}

/* BSS zero initialization function */

static void bss_init(void)

{

size_t bss_size = _ebss - _sbss; // get bss size

memset(_sbss, 0x00, bss_size); // clear bss section

}

然后是 Default_Handler 函数:

/**

* @brief This is the code that gets called when the processor receives an

* unexpected interrupt. This simply enters an infinite loop, preserving

* the system state for examination by a debugger.

*

* @param None

* @retval : None

*/

.section .text.Default_Handler,"ax",%progbits

Default_Handler:

Infinite_Loop:

b Infinite_Loop // goto Infinite_Loop;

.size Default_Handler, .-Default_Handler

它是中断处理函数的默认实现,当发生未知中断时,它会进入一个无限循环,保持系统状态,等待调试器来查看。易知它是个死循环,因此可以改写为C代码:

void Default_Handler(void){

while(1){}

}

最后是中断向量组定义:

/******************************************************************************

*

* The minimal vector table for a Cortex M3. Note that the proper constructs

* must be placed on this to ensure that it ends up at physical address

* 0x0000.0000.

*

******************************************************************************/

.section .isr_vector,"a",%progbits

.type g_pfnVectors, %object

.size g_pfnVectors, .-g_pfnVectors

g_pfnVectors:

.word _estack

.word Reset_Handler

// ......

.word BootRAM /* @0x108. This is for boot in RAM mode for

STM32F10x Medium Density devices. */

以及其弱符号定义:

.weak NMI_Handler

.thumb_set NMI_Handler,Default_Handler

.weak HardFault_Handler

.thumb_set HardFault_Handler,Default_Handler

.weak MemManage_Handler

.thumb_set MemManage_Handler,Default_Handler

// ......

改写为C代码:

// 函数声明

__attribute__((weak, alias("Default_Handler"))) func(void) NMI_Handler;

__attribute__((weak, alias("Default_Handler"))) func(void) HardFault_Handler;

// ......

__attribute__((weak, alias("Default_Handler"))) func(void) USBWakeUp_IRQHandler;

// 向量组定义

__attribute__((section(".isr_vector")))

const (*const f_pfnVectors[]) = {

(void(*)void)&_estack,

Reset_Handler,

// ...///

(void(*)void)BootRAM

};

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/27 15:51:06

Ubuntu 22.04 开发环境 CA 证书签发完整笔记(完整版)

Ubuntu 22.04 开发环境 CA 证书签发完整笔记 开发环境 前端: Vue3+TS+Vite+ESM 后端:NestJS 数据库:MySQL+Redis 虚拟机OS:Ubuntu 22.04 LTS 工作拓扑 开发环境参数(VS Code) 版本: 1.106.3 (Universal) Electron: 37.7.0 ElectronBuildId: 12781156 Chromium: 138.0.72…

作者头像 李华
网站建设 2026/1/14 15:21:11

Janus-Pro-1B终极指南:快速构建下一代多模态AI应用

Janus-Pro-1B是DeepSeek推出的革命性多模态模型&#xff0c;以其创新的视觉编码解耦架构重新定义了AI的理解与生成能力边界。这款仅需10亿参数的轻量级模型在图像生成与视觉理解任务上实现了对行业巨头的性能超越&#xff0c;为开发者提供了前所未有的技术接入门槛。 【免费下载…

作者头像 李华
网站建设 2026/1/16 8:25:58

手把手教你用JS正则表达式,轻松实现密码强度分步校验

在构建前端登录或注册功能时&#xff0c;密码强度的校验是保障用户账户安全的第一道防线。JavaScript正则表达式为此提供了高效、灵活的验证手段。本文将结合实际开发场景&#xff0c;探讨如何设计正则表达式来匹配符合常见安全策略的密码。 密码强度校验需要哪些核心规则 一个…

作者头像 李华
网站建设 2025/12/27 20:02:02

SenseVoice终极指南:快速掌握多语言音频理解核心技术

SenseVoice终极指南&#xff1a;快速掌握多语言音频理解核心技术 【免费下载链接】SenseVoice Multilingual Voice Understanding Model 项目地址: https://gitcode.com/gh_mirrors/se/SenseVoice SenseVoice是一个革命性的多语言音频理解基础模型&#xff0c;集成了语音…

作者头像 李华
网站建设 2026/1/15 8:55:04

C++、Qt中打开文件夹获取文件

void Qt_operateping::onOpenFileClicked() {// 打开文件对话框&#xff0c;供用户选择图片文件// getOpenFileName() 函数的四个参数依次是&#xff1a;// 1. 父组件&#xff0c;通常传递当前对象 this// 2. 对话框标题// 3. 初始打开的文件路径&#xff08;此处是 D 盘根目录…

作者头像 李华
网站建设 2026/1/8 3:35:34

多存储源文件同步终极方案:5分钟搞定跨平台数据一致性

多存储源文件同步终极方案&#xff1a;5分钟搞定跨平台数据一致性 【免费下载链接】zfile 项目地址: https://gitcode.com/gh_mirrors/zfi/zfile 还在为不同存储设备间的文件版本混乱而烦恼吗&#xff1f;当团队成员在本地磁盘、云盘和服务器上同时编辑文档时&#xff…

作者头像 李华