使用C语言实现重写stm32的启动文件

适用型号:stm32f103c8t6

编译器:GCC

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

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

c 复制代码
#include <stddef.h>
#include <stdint.h>
#include <string.h>

// some macros
#define cast(__value, __type) ((__type)(__value))

#define ptr(__type)           typeof(__type *)
#define array(__type, ...)    typeof(__type[##__VA_ARGS__])
#define func(__ret, ...)      typeof(__ret(__VA_ARGS__))
#define readonly(__type)      typeof(const __type)

/**
 * @syntax unified
 * @cpu cortex-m3
 * @fpu softvfp
 * @thumb
 */

typedef func(void) inteHandlerType;                    // 定义中断处理函数类型
typedef readonly(ptr(inteHandlerType)) inteVectorType; // 定义中断向量表元素类型

array(inteVectorType) f_pfnVectors; // 声明 中断向量表
inteHandlerType Default_Handler;    // 声明 默认中断处理函数
inteHandlerType Reset_Handler;      // 声明 复位函数

extern func(void) SystemInit; // defined in @system_stm32f1xx.c
extern func(int) main;        // defined in @main.c
extern func(void) __libc_init_array;

static func(void) data_init;
static func(void) bss_init;


// 栈顶地址
/* Highest address of the user mode stack */
extern uint8_t _estack; // 为了和 @sysmem.c 中的定义保持一致,使用uint8_t

// 定义在链接器脚本中的符号
/* defined in linker script */
extern uint32_t _sidata; /* start address for the initialization values of the .data section.*/
/* start address for the .data section. defined in linker script */
extern uint32_t _sdata;
/* end address for the .data section. defined in linker script */
extern uint32_t _edata;
/* start address for the .bss section. defined in linker script */
extern uint32_t _sbss;
/* end address for the .bss section. defined in linker script */
extern uint32_t _ebss;

readonly(uint32_t) BootRAM = 0xF108F85F;

/*******************************************************************************
 *
 * 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 __HandlerAttribute [[gnu::weak]] [[gnu::alias("Default_Handler")]]

__HandlerAttribute func(void) NMI_Handler;
__HandlerAttribute func(void) HardFault_Handler;
__HandlerAttribute func(void) MemManage_Handler;
__HandlerAttribute func(void) BusFault_Handler;
__HandlerAttribute func(void) UsageFault_Handler;
__HandlerAttribute func(void) SVC_Handler;
__HandlerAttribute func(void) DebugMon_Handler;
__HandlerAttribute func(void) PendSV_Handler;
__HandlerAttribute func(void) SysTick_Handler;
__HandlerAttribute func(void) WWDG_IRQHandler;
__HandlerAttribute func(void) PVD_IRQHandler;
__HandlerAttribute func(void) TAMPER_IRQHandler;
__HandlerAttribute func(void) RTC_IRQHandler;
__HandlerAttribute func(void) FLASH_IRQHandler;
__HandlerAttribute func(void) RCC_IRQHandler;
__HandlerAttribute func(void) EXTI0_IRQHandler;
__HandlerAttribute func(void) EXTI1_IRQHandler;
__HandlerAttribute func(void) EXTI2_IRQHandler;
__HandlerAttribute func(void) EXTI3_IRQHandler;
__HandlerAttribute func(void) EXTI4_IRQHandler;
__HandlerAttribute func(void) DMA1_Channel1_IRQHandler;
__HandlerAttribute func(void) DMA1_Channel2_IRQHandler;
__HandlerAttribute func(void) DMA1_Channel3_IRQHandler;
__HandlerAttribute func(void) DMA1_Channel4_IRQHandler;
__HandlerAttribute func(void) DMA1_Channel5_IRQHandler;
__HandlerAttribute func(void) DMA1_Channel6_IRQHandler;
__HandlerAttribute func(void) DMA1_Channel7_IRQHandler;
__HandlerAttribute func(void) ADC1_2_IRQHandler;
__HandlerAttribute func(void) USB_HP_CAN1_TX_IRQHandler;
__HandlerAttribute func(void) USB_LP_CAN1_RX0_IRQHandler;
__HandlerAttribute func(void) CAN1_RX1_IRQHandler;
__HandlerAttribute func(void) CAN1_SCE_IRQHandler;
__HandlerAttribute func(void) EXTI9_5_IRQHandler;
__HandlerAttribute func(void) TIM1_BRK_IRQHandler;
__HandlerAttribute func(void) TIM1_UP_IRQHandler;
__HandlerAttribute func(void) TIM1_TRG_COM_IRQHandler;
__HandlerAttribute func(void) TIM1_CC_IRQHandler;
__HandlerAttribute func(void) TIM2_IRQHandler;
__HandlerAttribute func(void) TIM3_IRQHandler;
__HandlerAttribute func(void) TIM4_IRQHandler;
__HandlerAttribute func(void) I2C1_EV_IRQHandler;
__HandlerAttribute func(void) I2C1_ER_IRQHandler;
__HandlerAttribute func(void) I2C2_EV_IRQHandler;
__HandlerAttribute func(void) I2C2_ER_IRQHandler;
__HandlerAttribute func(void) SPI1_IRQHandler;
__HandlerAttribute func(void) SPI2_IRQHandler;
__HandlerAttribute func(void) USART1_IRQHandler;
__HandlerAttribute func(void) USART2_IRQHandler;
__HandlerAttribute func(void) USART3_IRQHandler;
__HandlerAttribute func(void) EXTI15_10_IRQHandler;
__HandlerAttribute func(void) RTC_Alarm_IRQHandler;
__HandlerAttribute func(void) USBWakeUp_IRQHandler;

/******************************************************************************
 *
 * 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.
 *
 ******************************************************************************/

[[gnu::section(".isr_vector")]] // gnu extension to place the vector table in a specific address
array(inteVectorType) f_pfnVectors =
    {
        cast(&_estack, ptr(void)),          /* Stack pointer */
        Reset_Handler,                      /* Reset Handler */
        NMI_Handler,                        /* NMI Handler */
        HardFault_Handler,                  /* Hard Fault Handler */
        MemManage_Handler,                  /* MPU Fault Handler */
        BusFault_Handler,                   /* Bus Fault Handler */
        UsageFault_Handler,                 /* Usage Fault Handler */
        nullptr, nullptr, nullptr, nullptr, /* Reserved */
        SVC_Handler,                        /* SVCall Handler */
        DebugMon_Handler,                   /* Debug Monitor Handler */
        nullptr,                            /* Reserved */
        PendSV_Handler,                     /* PendSV Handler */
        SysTick_Handler,                    /* SysTick Handler */

        /* IRQ Handlers */
        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,
        RTC_Alarm_IRQHandler,
        USBWakeUp_IRQHandler,
        nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, /* Reserved */
        cast(BootRAM, ptr(void))
        /* @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 (true) {
        /* Infinite loop */
    }
}

/**
 * @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
 */
[[noreturn]] // the function will never return
void Reset_Handler(void)
{
    /* Call the clock system initialization function */
    SystemInit();

    /* Initialize data and bss sections */
    data_init();
    bss_init();

    /* Call static constructors */
    __libc_init_array();

    /* Call the application's entry point */
    main();

    /* Should never reach here */
    while (true) {
        /* Infinite loop */
    }
}

/**
 * @brief data sector initialization function
 *
 */
static void data_init(void)
{
    auto   src       = cast(&_sidata, ptr(uint8_t)); // flash addr
    auto   dst_start = cast(&_sdata, ptr(uint8_t));  // ram start
    auto   dst_end   = cast(&_edata, ptr(uint8_t));  // ram end
    size_t data_size = dst_end - dst_start;          // get data size
    memcpy(dst_start, src, data_size);               // copy data from flash to ram
}

/**
 * @brief bss sector zero initialization function
 *
 */
/* BSS zero initialization function */
static void bss_init(void)
{
    auto   dst_start = cast(&_sbss, ptr(uint8_t)); // ram start
    auto   dst_end   = cast(&_ebss, ptr(uint8_t)); // ram end
    size_t bss_size  = dst_end - dst_start;        // get bss size
    memset(dst_start, 0x00, bss_size);             // clear bss section
}

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

首先分析链接器文件:

ld 复制代码
/* Entry Point */
ENTRY(Reset_Handler)

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

ld 复制代码
/* Specify the memory areas */
MEMORY
{
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 20K
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 64K
}

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

  • RAM 区域,可执行(x)、可读(r)、可写(w)的区域,大小为 20K;
  • FLASH 区域,可读(r)、可执行(x),大小为 64K,起始地址为 0x8000000。
ld 复制代码
/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM);    /* end of RAM */

定义程序堆栈起始地址 _estack,由于堆栈是从高地址向低地址延伸,所以起始地址定义为 RAM 区域的结束位置。考虑到寻址方式为"word",因此在C语言中,可以使用 uint32_t 类型来表示。

ld 复制代码
/* 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。

然后是段定义:

ld 复制代码
/* Define output sections */
SECTIONS
{
    /*...... */
}

分为几个段:

ld 复制代码
  /* The startup code goes first into FLASH */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >FLASH

为中断向量表所在的地址

ld 复制代码
  /* 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++有关,略过。

接着是

ld 复制代码
  /* used by the startup to initialize data */
  _sidata = LOADADDR(.data);

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

ld 复制代码
  /* 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中的对应位置。

ld 复制代码
  .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。

ld 复制代码
 /* 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中。

ld 复制代码
  /* Remove information from the standard libraries */
  /DISCARD/ :
  {
    libc.a:* ( * )
    libm.a:* ( * )
    libgcc.a:* ( * )
  }

移除 libc.alibm.alibgcc.a 等标准库符号,因为使用newlib。

接着分析启动文件:

首先是

asm 复制代码
  .syntax unified
  .cpu cortex-m3
  .fpu softvfp
  .thumb

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

然后

asm 复制代码
.global g_pfnVectors
.global Default_Handler

相当于C语言中定声明了两个全局变量,第一个是中断向量组,第二个是默认的中断处理函数。中断向量组中是紧凑排列的各种中断函数的入口地址,我们知道所有的中断函数都是 void handler(void) 类型,因此它等价为一个函数指针数组,为了防止不小心修改,可以添加 const 限定符:

c 复制代码
typedef void(*const inteFunp)(void);
const inteFunp g_pfnVectors[];

然后是

asm 复制代码
/* 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中启动的地址,需要加在中断向量组的最后。

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

asm 复制代码
  .section .text.Reset_Handler // 定义代码段
  .weak Reset_Handlel           // 定义为弱符号
  .type Reset_Handler, %function // 定义为函数类型

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

c 复制代码
[[gnu::weak]] void Reset_Handler(void);

其中 [[gnu::weak]] 是C23语法,用于定义弱符号。

然后是函数体:

asm 复制代码
Reset_Handler:
    bl SystemInit // 调用 SystemInit 函数

    // 从 Flash 中复制数据到 data 段
    // 清零 bss 段

    bl __libc_init_array // 调用 C++ 静态构造函数
    bl main // 调用 main 函数,进入主程序
    bx lr   // 相当于 main 函数中的 return

可以改写为C代码:

c 复制代码
void data_init(void);
void bss_init(void);

[[noreturn]] // c23 属性,标记函数永不返回
void Reset_Handler(void){
    SystemInit();
    data_init(); // 复制数据到data段
    bss_init(); // 清零bss段
    __libc_init_array(); // 调用C++静态构造函数
    main(); // 进入主程序
    while(true);
}

其中 data_initbss_init 是我们自己定义的两个函数,用于初始化 .data.bss 段,其汇编代码如下:

asm 复制代码
/* 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

改写为C代码如下:

c 复制代码
extern uint32_t &_sdata;
extern uint32_t &_edata;
extern uint32_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 = (uint32_t *)(source_addr + offset);
    uint32_t *dst = (uint32_t *)(data_start + offset);

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

// 清零部分略

可以看到,本质上就是把 flash 中的数据拷贝到 ram 中,拷贝的长度为 _edata - _sdata,源地址为 flash 中的 _sidata,目的地址为 ram 中的 _sdata。清零部分同理,只不过是把拷贝改为设置为0.

因此,可以借助C库函数 memcpymemset 来实现 data_initbss_init 函数:

c 复制代码
/* 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 函数:

asm 复制代码
/**
 * @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
  .size Default_Handler, .-Default_Handler

它是中断处理函数的默认实现,当发生未知中断时,它会进入一个无限循环,保持系统状态,等待调试器来查看。C语言实现为:

c 复制代码
void Default_Handler(void){
  while(true){}
}

最后是中断向量组定义:

asm 复制代码
/******************************************************************************
*
* 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. */

以及其弱符号定义:

asm 复制代码
  .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代码:

c 复制代码
// 函数声明
[[gnu::weak]] [[gnu::alias("Default_Handler")]] func(void) NMI_Handler;
[[gnu::weak]] [[gnu::alias("Default_Handler")]] func(void) HardFault_Handler;
// ......
[[gnu::weak]] [[gnu::alias("Default_Handler")]] func(void) USBWakeUp_IRQHandler;

// 向量组定义
[[gnu::section(".isr_vector")]]  
const (*const f_pfnVectors[]) = {
  (void(*)void)&_estack,
  Reset_Handler,
  // ...///
  (void(*)void)BootRAM
};