STM32 时钟可靠性怎么搭:CSS 失效检测 + SysTick 延时 + MCO 时钟输出,三件事一起做对

STM32 系统跑飞了,示波器一测才发现 HSE 没起振,CPU 在不确定频率下瞎跑。结论先给:STM32 芯片里自带三件时钟安全武器——CSS 检测 HSE 失效并自动切 HSI、SysTick 把延时从时钟依赖改到频率无关、MCO 让你能直接观察当前时钟源。

2026-10-27
时钟可靠性STM32CSSSysTickMCO嵌入式

STM32 系统跑飞了,示波器一测才发现——不是代码逻辑错,是 HSE 外部晶振没起振,CPU 在某个不确定频率下瞎跑。工程师的第一反应是换晶振,但换完又坏,最后才发现问题不在晶振本身,而在设计里没有任何"晶振失效后该怎么办"的预案。结论先给:STM32 芯片里自带三件时钟安全武器——CSS 时钟安全系统负责检测 HSE 失效并自动切 HSI、SysTick 负责把延时函数从"时钟频率依赖"改到"时钟频率无关"、MCO 时钟输出负责验证当前系统时钟到底跑在哪个源上。三件事一起配,时钟可靠性从"完全指望晶振不坏"变成"坏了也能活"。

一、CSS 时钟安全系统:HSE 死了能自救

CSS(Clock Security System)是 STM32 内置的晶振失效检测器。按 RM0008 第 6.2.7 节的定义,它的工作流程分五步:CSSON 位置 1 激活 CSS → 时钟监测器在 HSE 振荡器启动延迟(默认由 RCC_CR 的 HSERDY 事件标记)后开始监控 → HSE 故障时自动关闭 HSE、切 HSI 为系统时钟 → 同时把 CSSI 中断连到 Cortex-M3 的 NMI(不可屏蔽中断) → NMI 里必须清 RCC_CIR 的 CSSC 位,否则 NMI 会持续触发。

寄存器位功能操作方式
RCC_CR(地址 0x40021000)CSSON时钟安全系统使能软件置 1 激活;故障发生后硬件自动关闭 HSE 和 PLL
RCC_CIR(地址 0x40021008)CSSC清除 CSS 中断标志软件置 1 清除 CSSF;NMI 处理里必须调用
RCC_CIR(同上)CSSFCSS 中断标志硬件置 1 表示 HSE 失效;读 RCC_CIR 可查询
——TIM1 刹车输入F103 特有的 CSS 联动:HSE 失效事件自动送 TIM1_BRK 做电机保护

一个必须记住的硬约束:CSS 的 NMI 是不可屏蔽的(Cortex-M3 架构决定),所以 NMI 处理函数必须尽量短,不能调用任何可能被再次中断的代码。研讨会PPT里给出的典型流程是:

/* STM32F103 CSS 配置与 NMI 处理(寄存器级,RM0008 命名) */
void css_enable(void)
{
    RCC->CR |= RCC_CR_CSSON;          /* 置 1 激活时钟安全系统 */
    /* HSE 就绪后 CSS 监测器自动开始工作 */
}

/* NMI 处理函数(尽量短,不可被再次中断) */
void NMI_Handler(void)
{
    if (RCC->CIR & RCC_CIR_CSSF) {
        /* 1. HSE、PLL 已被硬件自动关闭(设置未清除) */
        /* 2. 放系统保护代码:关输出、发告警、记录日志 */
        /* 3. 清 CSS 中断挂起位 —— 必须做,否则 NMI 持续触发 */
        RCC->CIR = RCC_CIR_CSSC;

        /* 4. HSE 恢复后的预设置:使能 HSE + HSE/PLL 就绪中断 */
        RCC->CR |= RCC_CR_HSEON;
        RCC->CIR = RCC_CIR_HSERDYC | RCC_CIR_PLLRDYC;
    }
}

/* HSE 恢复后,在 RCC 中断处理程序里切回原系统时钟 */
void RCC_IRQHandler(void)
{
    if (RCC->CIR & RCC_CIR_HSERDYF) {
        /* 切回 HSE 或 PLL 作为系统时钟 */
    }
}

F103 系列还多了一个独特联动——CSS 故障事件会被硬件直接送到高级定时器 TIM1 的刹车输入端。这在电机控制里价值极大:外部晶振失效导致系统时钟混乱,电机相电流采样随之错乱,CSS 自动关 HSE 的同时,TIM1 也自动刹车关断功率级,防止电机失速或烧毁。这是芯片级的"双保险",不需要软件在 NMI 里再去关 PWM 输出。

二、SysTick 延时:从"时钟依赖"改到"时钟无关"

写完 CSS 之后,很多人立刻遇到第二个问题:系统时钟从 72MHz 降到 8MHz HSI,原来的 Delay() 函数输出的毫秒数就不对了——延时函数里 for 循环的次数是按 72MHz 算的,降频后延时被拉长了 9 倍。

Cortex-M3 内核自带的 SysTick 就是用来解决这个问题的。它是一个 24 位递减计数器,设定初值后使能,每经过 1 个系统时钟周期就减 1,减到 0 时自动重装初值、同时置 COUNTFLAG、(如果中断使能)触发中断。

时钟频率实现 1ms 中断的 SysTick 重装载值说明
72MHz(PLL 输出)9000 - 1 = 899972M / 8 = 9M 计数频率;9000 计数 = 1ms
8MHz(HSI,未分频)1000 - 1 = 9998M / 8 = 1M 计数频率;1000 计数 = 1ms
4MHz(HSE 4MHz 分频)500 - 1 = 4994M / 8 = 500K 计数频率;500 计数 = 1ms

关键点在于重装载值 = fSYSCLK / 8 / 1000 − 1,但这个公式本身依赖 fSYSCLK——CSS 切 HSI 后,你需要在切换时钟源的代码里同步更新 SysTick 的重装载值。更好的做法是让 SysTick 始终输出 1ms 中断,全局变量 TimingDelay 在中断里递减,Delay(N) 只循环检查 TimingDelay 是否为 0。这样延时函数本身与任何时钟频率解耦,只与 SysTick 中断的间隔有关。

/* SysTick 延时:时钟频率无关 */
volatile u32 TimingDelay;

void delay_init(u32 f_sysclk_hz)
{
    /* SysTick 计数频率 = fSYSCLK / 8;重装值 = 目标延时 × 计数频率 − 1 */
    SysTick_Config(f_sysclk_hz / 8 / 1000);   /* 每 1ms 产生一次中断 */
    NVIC_SetPriority(SysTick_IRQn, 0);
}

void SysTick_Handler(void)
{
    if (TimingDelay != 0) TimingDelay--;
}

void Delay_ms(u32 ms)
{
    SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;   /* 使能计数器 */
    TimingDelay = ms;
    while (TimingDelay != 0) {}
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /* 关计数器 */
    SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk;
}

当 CSS 触发 HSE 失效切到 HSI 时,在 RCC 中断处理里写一句 delay_init(8000000),整个系统的延时就自动适配到了 8MHz HSI 时钟下,不需要改任何调用 Delay 的代码。

三、MCO 时钟输出:用 PA.8 看系统时钟到底跑在哪个源上

CSS 生效、时钟切 HSI 之后,你怎么知道系统真的跑在 HSI 上而不是卡在 PLL 故障状态?示波器量 PA.8——这是 STM32 唯一的 MCO(Microcontroller Clock Output)引脚,可以把 SYSCLK、HSI、HSE 或 PLLCLK 输出到外部,让你实时观察时钟源。

MCO 源RCC_MCO 位典型频率用途
SYSCLK00CSS 切换前 72MHz / 切换后 8MHz验证 CSS 切换是否成功
HSI018MHz确认内部 RC 振荡器是否正常
HSE104~16MHz确认外部晶振是否起振
PLLCLK/211PLL 输出频率的一半验证 PLL 倍频是否正确

手册有两个硬约束:PA.8 GPIO 输出最大响应频率 50MHz,超过这个频率波形会严重失真;MCO 与 PA.8 的其他功能(SPI1_MISO、TIM1_CH1 等)复用,需要在初始化前先选择好用途,避免冲突。CSS 调试时的标准做法是:主循环里不断读 RCC_CFGR 的 SWS 位(System Clock Switch Status),同时配置 MCO 输出当前时钟源,示波器量 PA.8 看有没有稳定波形,配合代码里的 RCC_GetSYSCLKSource() 就能完全确认 CSS 切换的实际执行路径。

四、完整落地:七个步骤把三件事串起来

把 CSS、SysTick、MCO 三件事串在启动流程里,落地顺序是:HSE+PLL 配置 → SysTick 初始化(按 72MHz 重装载)→ MCO 使能(输出 PLLCLK/2 验证)→ CSS 激活(置 CSSON)→ 主循环跑应用 → NMI 处理 HSE 故障 → RCC 中断切 HSI + delay_init(8M) 重初始化 SysTick → HSE 恢复后切回 PLL。

/* 启动流程中串起三件时钟安全武器 */
void clock_reliability_init(void)
{
    /* Step 1: HSE 8MHz + PLL ×9 = 72MHz */
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY)) {}
    RCC->CFGR = RCC_CFGR_PLLSRC_HSE | (9 << RCC_CFGR_PLLMULL_Pos);
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY)) {}
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {}

    /* Step 2: SysTick 按 72MHz 配置 1ms 中断 */
    delay_init(72000000);

    /* Step 3: MCO 输出 PLLCLK/2 = 36MHz 验证 PLL 倍频 */
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    GPIOA->CRH = (GPIOA->CRH & ~(0xFF << 0)) | (0xB << 0);
    RCC->CFGR |= RCC_CFGR_MCO_PLLCLK_DIV2;

    /* Step 4: CSS 激活,开始监控 HSE 有效性 */
    css_enable();   /* RCC_CR |= RCC_CR_CSSON */

    /* Step 5: 主循环继续跑应用,CSS 后台监控 */
}

五、常见坑:按现象对号入座

现象根因处理
NMI 不停触发、看门狗复位也清不掉NMI 处理函数里没清 CSSC 位NMI 里必须先 RCC->CIR = RCC_CIR_CSSC 再做保护代码
CSS 生效后 Delay() 输出慢了 9 倍SysTick 重装载值还是按 72MHz 算的时钟切换后同步调用 delay_init(新频率)
MCO 输出波形严重失真输出频率超过 GPIO 50MHz 上限降 PLL 倍频或改用 MCO 输出 PLLCLK/2
HSE 恢复后系统时钟没切回去CSS 切 HSI 后没使能 HSE/PLL 就绪中断NMI 处理里置 RCC_CIR_HSERDYC/PLLRDYC,在 RCC_IRQHandler 里切回
CSS 配置了但故障时没触发 NMIRCC_CR 的 CSSON 没真的置 1(被其他代码清了)用调试器读 RCC_CR 确认 bit 19 是否为 1;置 CSSON 后不能再清零
HSE 刚起振就触发 CSS 故障HSE 启动延迟太短,HSERDY 事件还没到 CSS 就开始监控等 HSERDY 后再置 CSSON,或用 RCC_CFGR 的 SW 位先确认系统时钟已切 HSE/PLL

一句话总结:CSS 让 HSE 死了能自救,SysTick 让延时函数与时钟频率解耦,MCO 让你能直接观察当前时钟源——三件事加起来,STM32 的时钟可靠性就从"依赖硬件不坏"变成了"坏了也能活"。真正落地时,先把 CSSON/CSSC 两个位搞对,再把 SysTick 的重装载值公式记住 fSYSCLK/8/1000-1,最后用 MCO 示波器验证整个链路。

本文基于 ST《STM32F10x 常见应用解析》全国巡回研讨会PPT(2008-09)与 RM0008《STM32F10xxx 参考手册》(中文版,参照 2008 年 12 月英文第 7 版)中的时钟安全系统、SysTick 与 RCC 时钟控制章节整理优化。

需要定制开发?

沧州艾诺威电子 — 国家高新技术企业,20+项国家专利
嵌入式系统开发 · 物联网方案 · AI智能硬件 · 一站式交付

立即微信咨询

电话:13930711029 | 邮箱:tech@czinv.com | 24小时内响应