4、系统时钟配置:HSI/HSE 时钟源、PLL 倍频、系统时钟树、SysTick 定时器原理

时钟,是微控制器的「心跳」。没有时钟,芯片就是一块废铁。我刚开始学ARM Cortex的时候,总觉得时钟配置是个麻烦事——寄存器多、选项杂。但后来我发现,搞懂了时钟树,你就掌握了整个系统的命脉。

今天咱们就聊聊血压计项目里的时钟配置。说白了,就是回答三个问题:时钟从哪来?怎么变快?怎么分给各个外设?

4.1 时钟源的选择:HSI 与 HSE

ARM Cortex芯片内部有好几个时钟源。但最常用的,就两个:HSI(高速内部振荡器)和HSE(高速外部晶振)。

时钟源 频率 精度 特点
HSI 8 MHz(典型) ±1% 左右 上电即用,无需外部元件
HSE 4~16 MHz(常用8M) ±50 ppm 甚至更高 需要外部晶振,精度高

HSI 是芯片内部自带的RC振荡器。上电后默认就是它。好处是省事——不用画晶振电路,省两个引脚。坏处是精度一般,温度变化时频率会漂。我在项目中遇到过,用HSI做UART通信,天热的时候偶尔丢包。后来查了半天,发现是时钟漂了,波特率对不上了。

HSE 需要外接一个石英晶振。精度高、稳定性好。血压计这种医疗设备,对时序要求严格,我个人习惯用HSE。虽然多两个元件,但心里踏实。

我的建议: 产品原型阶段可以用HSI快速验证。量产时,如果对时序精度有要求,果断上HSE。

4.2 PLL 倍频:把时钟「吹」上去

HSI是8MHz,HSE也是8MHz。但Cortex内核通常要跑几十甚至上百MHz。怎么办?用PLL(锁相环)来倍频。

PLL的工作原理,说白了就是一个「频率放大器」。你给它一个低频输入,它通过内部反馈环路,输出一个高频信号。倍频系数可以配置,比如×6、×8、×9等等。

举个例子:

  • HSE = 8 MHz
  • PLL 倍频系数 = 9
  • PLL 输出 = 8 × 9 = 72 MHz

嗯,这里要注意:PLL的输出频率不能超过芯片的最大允许值。比如STM32F103最大是72MHz,你非要搞到80MHz,芯片可能工作不稳定,甚至烧掉。我曾经手贱试过一次,芯片直接罢工,吓得我赶紧断电。

避坑指南: 配置PLL时,一定要先看数据手册的「Maximum frequency」表格。别凭感觉设倍频系数。

4.3 系统时钟树:时钟的「交通枢纽」

时钟树,就是一张图,告诉你时钟信号怎么从源头流到各个模块。你想想看,一个芯片里有CPU、有定时器、有ADC、有串口……每个模块需要的时钟频率可能不一样。时钟树就是负责分配这些时钟的。

典型的时钟树结构是这样的:

时钟源(HSI/HSE)
    │
    ▼
PLL(可选,用于倍频)
    │
    ▼
系统时钟(SYSCLK)
    │
    ├──→ AHB总线(HCLK)
    │       │
    │       ├──→ Cortex内核
    │       ├──→ 存储器
    │       ├──→ DMA
    │       └──→ APB1/APB2 预分频器
    │               │
    │               ├──→ APB1外设(低速:定时器、UART、I2C等)
    │               └──→ APB2外设(高速:ADC、SPI、GPIO等)
    │
    └──→ SysTick定时器(后面讲)

配置时钟树,其实就是配置几个关键的分频器:

  • AHB预分频器:决定HCLK频率,通常等于SYSCLK
  • APB1预分频器:APB1总线最高36MHz(以STM32F1为例)
  • APB2预分频器:APB2总线最高72MHz

为什么APB1和APB2不一样?因为低速外设(比如UART、I2C)不需要那么高的时钟,跑快了反而费电。高速外设(比如ADC、SPI)需要高频。这是芯片设计时的权衡。

小技巧: 配置时钟树时,先确定SYSCLK,再逐级往下设分频系数。别倒着来,容易乱。

4.4 SysTick 定时器原理

SysTick,全称是System Tick Timer。它是Cortex内核自带的一个24位递减计数器。说白了,就是一个简单的倒计时器。

它的工作原理:

  1. 设置一个重装载值(比如72,000,000)
  2. 计数器从重装载值开始,每个时钟周期减1
  3. 减到0时,触发一次中断(可选),然后自动重装载
  4. 如此循环往复

如果系统时钟是72MHz,重装载值设为72,000,000,那么每秒触发一次中断。这就是「1毫秒」的时基来源。

代码示例:

// 配置SysTick,每1ms中断一次
// 假设系统时钟为72MHz
void SysTick_Init(void)
{
    // 1. 设置重装载值
    //    72,000,000 / 1000 = 72,000
    SysTick->LOAD = 72000 - 1;  // 1ms中断一次
    
    // 2. 清零当前值
    SysTick->VAL = 0;
    
    // 3. 配置控制寄存器
    //    Bit 0: 使能定时器
    //    Bit 1: 使能中断
    //    Bit 2: 时钟源选择(0=AHB/8, 1=AHB)
    SysTick->CTRL = 0x07;  // 使能 + 中断 + AHB时钟
    
    // 注意:这里时钟源选了AHB(72MHz),
    // 如果选AHB/8,LOAD值要改成72000/8
}

SysTick中断服务函数:

volatile uint32_t g_ulSysTickCnt = 0;

void SysTick_Handler(void)
{
    g_ulSysTickCnt++;  // 每1ms加1
}

// 获取当前毫秒数
uint32_t GetSysTick(void)
{
    return g_ulSysTickCnt;
}

// 延时函数(毫秒级)
void Delay_ms(uint32_t ulMs)
{
    uint32_t ulStart = GetSysTick();
    while((GetSysTick() - ulStart) < ulMs);
}

你看,有了SysTick,实现延时函数就变得特别简单。血压计里测量血压时,需要精确的定时——比如充气多久、放气多久、采样间隔多少。这些都可以基于SysTick来实现。

核心要点: SysTick是Cortex内核的标准外设,所有Cortex-M芯片都有。学会用它,你的代码移植性会好很多。

4.5 血压计项目中的时钟配置实战

好了,理论讲完了。咱们看看血压计项目里具体怎么配。

我的方案是这样的:

  • 时钟源:HSE(8MHz外部晶振)
  • PLL倍频:×9 → 72MHz
  • AHB分频:1分频 → HCLK = 72MHz
  • APB1分频:2分频 → PCLK1 = 36MHz
  • APB2分频:1分频 → PCLK2 = 72MHz
  • SysTick:每1ms中断一次

配置代码(简化版):

void SystemClock_Config(void)
{
    // 1. 使能HSE
    RCC->CR |= RCC_CR_HSEON;
    while(!(RCC->CR & RCC_CR_HSERDY));  // 等待HSE就绪
    
    // 2. 配置Flash预取(72MHz必须)
    FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;
    
    // 3. 配置PLL:HSE × 9 = 72MHz
    RCC->CFGR |= RCC_CFGR_PLLSRC_HSE;  // PLL时钟源=HSE
    RCC->CFGR |= RCC_CFGR_PLLMULL9;    // 倍频系数=9
    
    // 4. 使能PLL
    RCC->CR |= RCC_CR_PLLON;
    while(!(RCC->CR & RCC_CR_PLLRDY));  // 等待PLL就绪
    
    // 5. 配置总线分频
    RCC->CFGR |= RCC_CFGR_HPRE_DIV1;   // AHB = SYSCLK / 1
    RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;  // APB1 = HCLK / 2
    RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;  // APB2 = HCLK / 1
    
    // 6. 切换系统时钟到PLL
    RCC->CFGR &= ~RCC_CFGR_SW;         // 清除SW位
    RCC->CFGR |= RCC_CFGR_SW_PLL;      // 选择PLL作为系统时钟
    while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
    
    // 7. 初始化SysTick
    SysTick_Init();
}

这段代码,我每次做新项目都会拿出来改改。说白了,时钟配置的套路就这些——选源、倍频、分频、切换。你只要记住这个流程,换什么芯片都不怕。

重要提醒: 切换系统时钟前,一定要确保新时钟源已经稳定就绪。否则芯片会「死机」。我刚开始学的时候,就犯过这个错——PLL还没稳定就切过去了,结果程序跑飞,调试器都连不上。

4.6 小结

这一章内容不少,但核心就四点:

  • HSI 省事但精度低,HSE 精度高但需要外部晶振
  • PLL 负责把时钟频率「吹」上去,但别超限
  • 时钟树 就是一张分配图,告诉时钟信号往哪走
  • SysTick 是内核自带的定时器,用来产生系统时基

下一章,咱们要开始搞GPIO了。到时候,你会看到时钟配置的成果——LED灯会按照你设定的节奏闪烁。那感觉,挺爽的。