使用C语言实现STM32的启动文件
适用型号:stm32f103c8t6
编译器: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
};
最后,对启动文件进行调整润色,就得到了C语言实现的启动文件。