1. 案例1

案例1:基于ARM Cortex-M的启动代码编写 学习卡片

任务目标

编写基于ARM Cortex-M3/M4处理器的完整启动代码,实现:向量表定义、堆栈指针初始化、系统时钟配置(外部晶振→PLL→系统时钟)、以及跳转至C语言main()函数。本案例以STM32F407(Cortex-M4)为硬件参考,代码可直接用于实际工程。

环境与依赖

  • 硬件:STM32F407VET6开发板(25MHz外部晶振HSE)
  • 工具链:arm-none-eabi-gcc + Makefile(或任意支持ARM的GCC交叉编译器)
  • 链接脚本:需提供stm32f407.ld(定义FLASH起始0x08000000,RAM起始0x20000000)

关键完整代码

/* startup_stm32f407.c - ARM Cortex-M4启动代码 */
#include <stdint.h>

/* 外设寄存器地址定义(仅列出本案例所需) */
#define RCC_BASE        0x40023800U
#define RCC_CR          (*(volatile uint32_t *)(RCC_BASE + 0x00))
#define RCC_CFGR        (*(volatile uint32_t *)(RCC_BASE + 0x08))
#define RCC_PLLCFGR     (*(volatile uint32_t *)(RCC_BASE + 0x04))
#define FLASH_ACR       (*(volatile uint32_t *)(0x40023C00U + 0x00))

/* 系统主频:168MHz(HSE=25MHz,PLL_M=25,PLL_N=336,PLL_P=2) */
#define PLL_M           25U
#define PLL_N           336U
#define PLL_P           0U      /* PLLP=0 → /2 */
#define PLL_Q           7U

/* 栈顶地址(由链接脚本提供) */
extern uint32_t _estack;
/* C运行时需要的符号 */
extern uint32_t _sdata, _edata, _sidata;
extern uint32_t _sbss, _ebss;

/* 函数声明 */
void Reset_Handler(void);
void Default_Handler(void);
void SystemClock_Init(void);
extern int main(void);

/* 向量表:必须位于0x08000000 */
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
    (void *)&_estack,          /* 初始SP */
    Reset_Handler,              /* Reset */
    Default_Handler,            /* NMI */
    Default_Handler,            /* HardFault */
    Default_Handler,            /* MemManage */
    Default_Handler,            /* BusFault */
    Default_Handler,            /* UsageFault */
    0, 0, 0, 0,                 /* 保留 */
    Default_Handler,            /* SVCall */
    0, 0,                       /* 调试保留 */
    0,                          /* PendSV */
    Default_Handler             /* SysTick */
};

/* 默认中断处理:死循环 */
void Default_Handler(void) {
    while (1) { ; }
}

/* 系统时钟初始化:HSE → PLL → 168MHz */
void SystemClock_Init(void) {
    /* 1. 打开HSE并等待就绪 */
    RCC_CR |= (1U << 16);                    /* HSEON */
    while (!(RCC_CR & (1U << 17)));          /* 等待HSERDY */

    /* 2. 配置FLASH预取与等待周期(168MHz需5个等待周期) */
    FLASH_ACR = (5U << 0) | (1U << 8) | (1U << 9);  /* LATENCY=5, PRFTEN, ICEN */

    /* 3. 配置PLL:HSE作为输入,分频/倍频 */
    RCC_PLLCFGR = (PLL_M << 0) | (PLL_N << 6) |
                  (PLL_P << 16) | (PLL_Q << 24) |
                  (1U << 22);                /* PLLSRC=HSE */

    /* 4. 打开PLL并等待就绪 */
    RCC_CR |= (1U << 24);                    /* PLLON */
    while (!(RCC_CR & (1U << 25)));          /* 等待PLLRDY */

    /* 5. 切换系统时钟到PLL */
    RCC_CFGR |= (2U << 0);                   /* SW=PLL */
    while ((RCC_CFGR & 0x0CU) != (2U << 2)); /* 等待SWS=PLL */
}

/* 复位处理:C运行时初始化 + 跳转main */
void Reset_Handler(void) {
    uint32_t *pSrc, *pDest;

    /* 拷贝.data段(从FLASH到RAM) */
    pSrc = &_sidata;
    pDest = &_sdata;
    while (pDest < &_edata) {
        *pDest++ = *pSrc++;
    }

    /* 清零.bss段 */
    pDest = &_sbss;
    while (pDest < &_ebss) {
        *pDest++ = 0;
    }

    /* 配置系统时钟 */
    SystemClock_Init();

    /* 跳转至C主函数 */
    main();

    /* main不应返回,若返回则死循环 */
    while (1) { ; }
}

链接脚本关键段定义

/* stm32f407.ld 关键段(完整脚本需包含MEMORY与SECTIONS命令) */
MEMORY {
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS {
    .isr_vector : {
        . = ALIGN(4);
        KEEP(*(.isr_vector))
        . = ALIGN(4);
    } > FLASH

    .text : { *(.text*) } > FLASH
    .rodata : { *(.rodata*) } > FLASH
    _sidata = LOADADDR(.data);

    .data : {
        _sdata = .;
        *(.data*)
        _edata = .;
    } > RAM AT> FLASH

    .bss : {
        _sbss = .;
        *(.bss*)
        _ebss = .;
    } > RAM

    _estack = ORIGIN(RAM) + LENGTH(RAM);
}

编译与烧录

# 编译命令(假设startup.c与main.c在同一目录)
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -nostartfiles \
  -T stm32f407.ld -o firmware.elf startup_stm32f407.c main.c

# 生成二进制并烧录(使用ST-Link)
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
st-flash write firmware.bin 0x08000000

验证方法与判定标准

  1. 编译验证:上述编译命令无错误、无警告,生成firmware.elf。
  2. 向量表验证:执行arm-none-eabi-nm firmware.elf | grep g_pfnVectors,确认向量表地址为0x08000000;arm-none-eabi-objdump -h firmware.elf中.isr_vector段起始地址为0x08000000。
  3. 运行验证:烧录后连接调试器,在main()入口设断点。全速运行后若停止在断点处,说明启动代码正确完成:SP初始化→向量表跳转→时钟配置→C运行时初始化→进入main。
  4. 时钟验证:在调试器中读取RCC_CFGR寄存器,其SWS位([3:2])应为0b10(PLL作为系统时钟);读取RCC_CR的PLLRDY位([25])应为1。

常见问题

  • 程序跑飞:检查_estack是否指向RAM末尾(0x20020000),向量表首项是否为该地址。
  • HSE就绪超时:确认外部晶振焊接正确,或改用内部HSI(将PLLSRC置0并调整分频系数)。
  • 进入HardFault:检查FLASH_ACR等待周期是否与系统时钟匹配(168MHz需5个等待周期)。