3、时钟系统优化:外部晶振 vs 内部RC振荡器、动态切换时钟频率、关闭未使用的外设时钟

时钟系统,说白了就是MCU的心脏。心跳多快、稳不稳,直接决定了整个系统的功耗和可靠性。我见过不少工程师,上来就开最高主频,结果功耗压不下去,或者为了省电用了内部振荡器,结果通信老丢包。嗯,这里头门道不少。

3.1 外部晶振 vs 内部RC振荡器:选哪个?

这个问题,几乎每个项目都会遇到。我个人习惯是:能不用外部晶振,就不用。但这不是绝对的。

对比项 外部晶振 内部RC振荡器
精度 高(±10~50ppm) 一般(±1%~3%)
功耗 高(几百μA~mA级) 低(几十μA)
启动时间 慢(几ms) 快(几μs)
抗振动 差(晶振怕摔) 好(集成在芯片内)
成本 高(多一颗物料) 低(免费)

我在一个工业数据采集项目中遇到过这么个事:产品在实验室跑得好好的,一到现场就频繁死机。查了三天,最后发现是现场振动太大,外部晶振接触不良,导致时钟丢失。从那以后,只要不是对时序要求特别苛刻的场景,我优先用内部RC。

我的选型原则:

  • 需要UART通信、CAN通信 → 必须用外部晶振(内部RC误差太大,容易丢帧)
  • 纯逻辑控制、传感器采集 → 内部RC完全够用
  • 电池供电设备 → 内部RC + 低功耗模式
  • 高可靠性环境(振动、高温) → 内部RC优先

小技巧:有些MCU支持内部RC自动校准。比如STM32的HSI,可以借助外部晶振(比如LSE)来校准内部HSI。这样既省了外部主晶振,又保证了精度。我常用这招。

3.2 动态切换时钟频率:该快则快,该慢则慢

你想想看,MCU大部分时间其实都在闲着。比如一个温度采集器,每秒只采一次数据,剩下999ms都在空转。这时候跑72MHz主频,纯粹是浪费电。

动态调频的思路很简单:干活时跑高频,闲着时跑低频

// 伪代码示例:动态切换时钟频率
void task_busy(void) {
    // 切换到高频模式(比如48MHz)
    SetSysClock(CLOCK_HIGH);
    // 执行计算密集型任务
    process_data();
    // 任务完成,切回低频(比如2MHz)
    SetSysClock(CLOCK_LOW);
}

void task_idle(void) {
    // 低频模式下轮询
    if (need_to_work()) {
        task_busy();
    }
    // 进入低功耗睡眠
    __WFI();
}

我曾经在一个手持设备上做过实测:固定48MHz跑,电流约8mA;动态切换(大部分时间跑2MHz),平均电流降到1.2mA。省了将近7倍。你想想看,这要是电池供电,续航差距就出来了。

注意:切换时钟频率时,要留意外设的时钟依赖。比如你正在用UART发数据,突然把时钟切了,波特率就乱了。我建议在切换前,先确保没有关键外设在运行,或者先关掉外设再切。

3.3 关闭未使用的外设时钟:最容易被忽略的功耗杀手

很多MCU默认上电后,所有外设时钟都是开着的。ADC、定时器、SPI、I2C……哪怕你一个都没用,它们也在偷偷耗电。我见过一个案例,工程师只用了GPIO和UART,结果功耗比预期高了30%。查到最后,发现是没用的SPI和ADC模块时钟没关。

正确的做法是:用哪个外设,开哪个时钟。用完就关。

// 初始化时,只开启需要的外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
// 其他外设时钟保持关闭

// 使用ADC时临时开启
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE);
// 采集完成
ADC_Cmd(ADC1, DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, DISABLE);

我的习惯做法:

  • 在系统初始化函数里,只开启必要的时钟
  • 每个外设模块,自己负责开关自己的时钟
  • 进入低功耗模式前,遍历关闭所有外设时钟
  • 用代码检查工具(比如静态分析)扫描未关闭的时钟

避坑指南:我曾经犯过一个错——在中断服务函数里开关外设时钟。结果中断频繁触发,时钟开关也跟着频繁操作,反而增加了功耗和延迟。后来我改成:在中断里只置标志位,在主循环里统一处理时钟开关。嗯,这样稳多了。

3.4 综合优化策略:一个实际案例

讲个我去年做的项目:一个工业无线传感器节点,用电池供电,要求续航一年以上。

时钟方案是这样的:

  • 平时用内部RC振荡器(HSI),跑2MHz,只做定时唤醒和简单采集
  • 需要无线通信时,切换到外部晶振(HSE),跑32MHz,保证射频时序精度
  • 通信完成后,立即切回内部RC,关闭外部晶振
  • 所有未使用的外设时钟,在初始化时就关掉

最终实测:待机电流2.5μA,工作峰值电流15mA(通信时),平均电流约12μA。用一节CR2032电池(225mAh),理论续航超过1.5年。客户很满意。

最后提醒一句:时钟切换时,一定要等新时钟稳定后再操作外设。尤其是外部晶振,起振需要时间。我习惯加一个超时判断,防止晶振坏了导致系统卡死。

好了,时钟系统这块就聊到这儿。下一节我们聊聊电源管理,那又是另一个大坑。记住一句话:时钟是功耗的源头,管好时钟,就管好了一半的功耗。