3. 硬件初始化:时钟系统初始化(PLL配置)、GPIO初始化、中断向量表设置、堆栈指针设置
好,咱们接着往下走。上一章我们把启动流程的脉络理清了,这一章要动真格的了——硬件初始化。
说白了,芯片上电后就像个刚出生的婴儿,啥都不会。你得手把手教它:时钟怎么跑、引脚怎么用、中断来了找谁、程序栈放哪儿。这四个点,一个都不能少。
我个人习惯把硬件初始化分成四步走:时钟 → GPIO → 中断向量表 → 堆栈指针。顺序别搞反了,尤其是时钟,它是所有外设的“心脏”。
3.1 时钟系统初始化(PLL配置)
时钟系统,我愿称之为整个系统的“命脉”。没有时钟,CPU就是一块死硅。但光有还不行,你得让它跑得又快又稳。
大多数MCU上电后默认用的是内部低速振荡器(比如8MHz的HSI),这玩意儿精度一般,跑个点灯还行,跑USB、以太网?想都别想。所以我们要做的是:配置PLL,把时钟倍频到目标频率。
核心思路:外部晶振(HSE)→ PLL倍频 → 系统时钟(SYSCLK)→ 总线时钟(AHB/APB)
我在项目中遇到过一件事:有个同事把PLL配置错了,芯片直接“死机”,连调试器都连不上。后来发现是PLL的倍频系数超出了芯片允许范围。嗯,这里要注意——PLL的输出频率不能超过芯片手册规定的最大值。
来看一个典型的PLL配置代码(以STM32F4为例):
void SystemClock_Config(void)
{
// 1. 使能HSE外部晶振
RCC->CR |= RCC_CR_HSEON;
while(!(RCC->CR & RCC_CR_HSERDY)); // 等待HSE就绪
// 2. 配置PLL参数
RCC->PLLCFGR = (8 << 24) | // PLL_Q = 8(用于USB等外设)
(336 << 6) | // PLL_N = 336(倍频系数)
(4 << 0); // PLL_M = 4(分频系数)
// 最终PLL输出 = 8MHz(HSE) / 4 * 336 / 8 = 84MHz
// 3. 使能PLL并等待锁定
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
// 4. 配置AHB/APB预分频
RCC->CFGR |= RCC_CFGR_HPRE_DIV1; // AHB = SYSCLK
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = AHB/2
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // APB2 = AHB
// 5. 切换系统时钟为PLL
RCC->CFGR |= RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
小技巧:配置PLL时,我建议先把所有时钟源都关掉,再逐个使能。这样可以避免时钟切换时的毛刺。另外,别忘了配置Flash等待周期——频率高了,Flash读取速度跟不上,程序会跑飞。
3.2 GPIO初始化
时钟搞定了,接下来就是GPIO。你想想看,芯片和外界的交互全靠这些引脚。但GPIO不是上电就能用的,你得告诉它:你是输入还是输出?要不要上拉?速度多快?
GPIO初始化的核心就三件事:使能时钟、配置模式、设置初始电平。
我曾经犯过一个低级错误:GPIO初始化时忘了使能对应的外设时钟,结果引脚死活没反应。查了半天,最后发现是RCC那一步漏了。所以我现在写代码,第一件事就是检查时钟使能。
来看一个标准的GPIO初始化流程:
void GPIO_Init(void)
{
// 1. 使能GPIO时钟(这一步最容易忘!)
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// 2. 配置引脚模式
GPIOA->MODER &= ~(0x3 << (5 * 2)); // 清除PA5原有配置
GPIOA->MODER |= (0x1 << (5 * 2)); // 设置为输出模式
// 3. 配置输出类型(推挽/开漏)
GPIOA->OTYPER &= ~(1 << 5); // 推挽输出
// 4. 配置速度
GPIOA->OSPEEDR |= (0x2 << (5 * 2)); // 高速模式
// 5. 配置上下拉
GPIOA->PUPDR &= ~(0x3 << (5 * 2)); // 无上下拉
// 6. 设置初始电平(输出高)
GPIOA->BSRR = (1 << 5);
}
警告:配置GPIO时,一定要先清除原有配置再写入新值。否则可能会因为复位后的默认值导致意想不到的行为。我见过有人直接用赋值操作,结果把相邻引脚也改了。
3.3 中断向量表设置
中断向量表,说白了就是一张“紧急联系人名单”。芯片遇到中断时,会查这张表,找到对应的处理函数地址,然后跳过去执行。
默认情况下,向量表放在Flash的起始地址(0x08000000)。但有时候我们需要把它搬到RAM里——比如做IAP在线升级时,要动态修改中断处理函数。
我记得第一次做IAP项目时,中断向量表没重定位,结果跳转到APP后,一按按键就死机。后来才发现,中断还是指向了Bootloader的向量表。
向量表重定位的代码很简单:
// 设置向量表偏移(以STM32为例)
SCB->VTOR = (uint32_t)APP_ADDRESS & 0x1FFFFF80;
这里有个坑:VTOR寄存器的地址必须按128字节对齐。为什么?因为向量表里每个条目是4字节,一共32个异常向量,正好128字节。嗯,这是ARM架构的规定,咱们照做就行。
避坑指南:我曾经在某个项目里,把向量表地址设成了0x08001000,结果发现中断响应不对。查了半天手册才发现,这个地址没有128字节对齐。从那以后,我每次写VTOR都会加个对齐宏:#define ALIGN_128(x) ((x) & ~0x7F)
3.4 堆栈指针设置
堆栈指针(SP),就是CPU用来记录栈顶位置的寄存器。程序里的局部变量、函数调用时的返回地址,全都要靠栈来管理。
芯片上电后,SP的值是从哪里来的?答案是:从Flash的0地址读取。没错,向量表的第一个条目就是栈顶地址。所以你在链接脚本里定义的栈大小和起始地址,最终会通过启动文件写入到0地址。
来看启动文件里是怎么做的:
__initial_sp EQU 0x20010000 ; 栈顶地址
AREA RESET, DATA, READONLY
DCD __initial_sp ; 向量表第一个条目:栈顶地址
DCD Reset_Handler ; 第二个条目:复位中断入口
你可能会问:为什么栈顶地址要放在第一个?因为CPU上电后,硬件会自动把SP初始化为这个值。如果这里写错了,程序连第一条指令都执行不了。
个人经验:栈的大小怎么定?我一般按实际需求再加30%的余量。比如你的函数调用深度是1KB,我就设1.3KB。太小了会栈溢出,太大了浪费RAM。你可以用编译器的栈分析工具来精确计算。
另外,别忘了在C代码里重新设置SP的情况——比如从Bootloader跳转到APP时,APP的栈可能和Bootloader不一样。这时候需要手动设置:
// 手动设置堆栈指针
__asm void SetSP(uint32_t addr)
{
MSR MSP, R0
BX LR
}
// 跳转到APP前调用
SetSP(*(uint32_t*)APP_ADDRESS);
小结
好了,硬件初始化的四个核心步骤就讲完了。总结一下:
- 时钟系统:先配PLL,再配总线分频,别忘了Flash等待周期
- GPIO:先使能时钟,再配模式,最后设电平
- 中断向量表:默认在Flash,重定位时注意128字节对齐
- 堆栈指针:启动文件里定义好,跳转时记得重新设置
下一章,我们会把这些初始化代码整合到一起,写一个完整的启动流程。到时候你会发现,前面这些零散的知识点,其实都是环环相扣的。