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位递减计数器。说白了,就是一个简单的倒计时器。
它的工作原理:
- 设置一个重装载值(比如72,000,000)
- 计数器从重装载值开始,每个时钟周期减1
- 减到0时,触发一次中断(可选),然后自动重装载
- 如此循环往复
如果系统时钟是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灯会按照你设定的节奏闪烁。那感觉,挺爽的。