2. 硬件初始化流程:从复位向量到C语言入口

硬件初始化,说白了就是给芯片“接生”。芯片上电那一刻,所有寄存器都是默认值,内存还没准备好,时钟还在乱跑。你得一步步把它扶上正轨,最后才能跳进C语言的世界。

我刚开始做Bootloader时,总觉得这步很简单——不就是配几个寄存器吗?结果有一次,板子死活跑不起来,折腾了两天才发现是PLL锁定时间没等够。嗯,从那以后,我对初始化流程再也不敢马虎了。

2.1 复位向量:芯片的第一口气

芯片上电后,PC指针会直接跳到一个固定地址——复位向量。这个地址通常放在Flash或ROM的最开头,比如0x00000000或0x08000000。

复位向量里放的是什么?一条跳转指令,直接跳到复位处理函数。比如ARM Cortex-M系列,向量表第一个字是栈顶指针,第二个字才是复位向量。

; 典型的ARM Cortex-M向量表示例
__Vectors       DCD     __stack_top        ; 栈顶指针
                DCD     Reset_Handler      ; 复位向量
                DCD     NMI_Handler        ; NMI中断
                DCD     HardFault_Handler  ; 硬错误中断
                ; ... 其他中断向量

我个人习惯在复位向量后面留几个字的填充,方便以后加功能。你想想看,万一要加个安全启动校验,向量表空间不够就尴尬了。

注意: 复位向量必须放在链接脚本指定的地址上。我曾经见过有人改了链接脚本但忘了更新向量表地址,结果芯片上电直接飞了。

2.2 从汇编到C语言:关键的几步

复位处理函数通常用汇编写,因为它要做几件C语言跑起来之前必须干的事:

  1. 关中断——防止在初始化过程中被意外打断
  2. 设置栈指针——C语言需要栈才能调用函数
  3. 清零BSS段——未初始化的全局变量必须归零
  4. 拷贝数据段——从Flash把初始化数据搬到RAM
  5. 使能FPU(如果有)——浮点运算单元要手动打开
  6. 跳转到main——终于可以写C代码了
Reset_Handler PROC
    ; 1. 关中断
    CPSID   I

    ; 2. 设置栈指针
    LDR     R0, =__stack_top
    MOV     SP, R0

    ; 3. 清零BSS段
    LDR     R0, =__bss_start
    LDR     R1, =__bss_end
    MOV     R2, #0
    BL      memset

    ; 4. 拷贝数据段
    LDR     R0, =__data_start
    LDR     R1, =__data_end
    LDR     R2, =__data_flash_start
    BL      memcpy

    ; 5. 使能FPU(Cortex-M4/M7)
    LDR     R0, =0xE000ED88
    LDR     R1, [R0]
    ORR     R1, R1, #(0xF << 20)
    STR     R1, [R0]
    DSB
    ISB

    ; 6. 跳转到main
    BL      main
    ENDP

这里有个坑:BSS段清零必须在数据段拷贝之前。为什么?因为有些链接脚本会把data段的加载地址放在BSS段里,顺序搞反了数据就全乱了。我踩过这个坑,查了两天才发现。

2.3 时钟系统初始化:PLL配置

时钟是芯片的心脏。默认情况下,芯片用的是内部低速振荡器,比如8MHz的HSI。你要把它切换到PLL,倍频到几百兆赫兹。

PLL配置的典型步骤:

  1. 选择参考时钟源(HSE/HSI)
  2. 配置分频系数(M、N、P、Q)
  3. 使能PLL并等待锁定
  4. 切换系统时钟到PLL输出
参数 说明 典型值
HSE 外部高速晶振 8MHz / 25MHz
M 输入分频 8(8MHz输入时)
N 倍频系数 336(输出336MHz)
P 系统时钟分频 2(得到168MHz)
Q USB/SDIO分频 7(得到48MHz)
void SystemClock_Config(void)
{
    // 1. 使能HSE
    RCC->CR |= RCC_CR_HSEON;
    while(!(RCC->CR & RCC_CR_HSERDY));

    // 2. 配置PLL参数
    RCC->PLLCFGR = (8 << 0)  |  // M = 8
                   (336 << 6) |  // N = 336
                   (0 << 16)  |  // P = 2
                   (7 << 24)  |  // Q = 7
                   RCC_PLLCFGR_PLLSRC_HSE;

    // 3. 使能PLL并等待锁定
    RCC->CR |= RCC_CR_PLLON;
    while(!(RCC->CR & RCC_CR_PLLRDY));

    // 4. 配置Flash等待周期(168MHz需要5个等待周期)
    FLASH->ACR = FLASH_ACR_LATENCY_5WS | FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN;

    // 5. 切换系统时钟到PLL
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
经验之谈: PLL锁定时间不是固定的。温度、电压都会影响。我一般会在等待锁定循环里加个超时保护,防止晶振坏了导致死锁。

2.4 内存控制器初始化:DDR/SDRAM

内存初始化是Bootloader里最麻烦的部分之一。DDR的时序参数多、寄存器复杂,配错了要么跑不起来,要么跑起来不稳定。

DDR初始化的核心步骤:

  • 供电稳定——DDR需要1.8V/1.35V/1.2V,必须先确认电压到位
  • 时钟使能——给DDR提供差分时钟
  • 复位释放——拉高DDR的复位引脚
  • CKE使能——时钟使能信号拉高
  • 初始化序列——发送NOP、预充电、刷新、模式寄存器配置
  • ZQ校准——DDR3/DDR4需要ZQ校准
void DDR_Init(void)
{
    // 1. 配置DDR控制器时序参数
    FMC->SDCR1 = FMC_SDCR1_SDCLK_2 |      // 时钟分频
                 FMC_SDCR1_NBANK_4 |       // 4个Bank
                 FMC_SDCR1_NR_13 |         // 13行地址
                 FMC_SDCR1_NC_10;          // 10列地址

    // 2. 配置时序寄存器
    FMC->SDTR1 = (7 << 0)  |  // TMRD: 7个时钟
                 (2 << 4)  |  // TXSR: 2个时钟
                 (7 << 8)  |  // TRAS: 7个时钟
                 (7 << 12) |  // TRC: 7个时钟
                 (2 << 16) |  // TRP: 2个时钟
                 (2 << 20) |  // TRCD: 2个时钟
                 (2 << 24);   // TWR: 2个时钟

    // 3. 发送初始化命令序列
    // 先发NOP,等待200us
    FMC->SDCMR = FMC_SDCMR_CTB1 | (0 << 5);  // NOP命令
    delay_us(200);

    // 预充电所有Bank
    FMC->SDCMR = FMC_SDCMR_CTB1 | (1 << 5);  // 预充电
    delay_us(10);

    // 自动刷新(8次)
    for(int i = 0; i < 8; i++) {
        FMC->SDCMR = FMC_SDCMR_CTB1 | (2 << 5);  // 自动刷新
        delay_us(1);
    }

    // 配置模式寄存器
    FMC->SDCMR = FMC_SDCMR_CTB1 | (3 << 5) | (0x33 << 9);
    delay_us(10);

    // 4. ZQ校准(DDR3必须)
    FMC->SDCMR = FMC_SDCMR_CTB1 | (4 << 5);
    delay_us(1);
}
特别注意: DDR的时序参数必须和芯片数据手册完全一致。我曾经遇到过一个问题:板子上用的是DDR3L(1.35V),但代码里配的是DDR3(1.5V)的时序,结果高温下频繁死机。查了三天才发现是电压等级配错了。

2.5 初始化流程总览

把上面这些串起来,完整的硬件初始化流程就是:

Bootloader 硬件初始化流程 复位向量跳转 汇编:关中断/设栈/BSS清零 时钟系统:PLL配置 内存控制器:DDR初始化 外设:UART/GPIO/定时器 跳转到 C 语言 main() 每个步骤都必须确认状态寄存器,不要盲目往下走。一步错,步步错。

这张图看起来简单,但每一步都有坑。我个人习惯在每个初始化函数里加状态打印,用UART输出关键寄存器的值。这样万一板子挂了,至少知道是卡在哪一步。

核心原则: 硬件初始化要“先慢后快”。先用低速时钟把基本功能跑通,再切换到高速PLL。内存初始化要“先稳后准”,先保证能读写,再优化时序。