STM32 系统跑飞了,示波器一测才发现 HSE 没起振,CPU 在不确定频率下瞎跑。结论先给:STM32 芯片里自带三件时钟安全武器——CSS 检测 HSE 失效并自动切 HSI、SysTick 把延时从时钟依赖改到频率无关、MCO 让你能直接观察当前时钟源。
STM32 系统跑飞了,示波器一测才发现——不是代码逻辑错,是 HSE 外部晶振没起振,CPU 在某个不确定频率下瞎跑。工程师的第一反应是换晶振,但换完又坏,最后才发现问题不在晶振本身,而在设计里没有任何"晶振失效后该怎么办"的预案。结论先给:STM32 芯片里自带三件时钟安全武器——CSS 时钟安全系统负责检测 HSE 失效并自动切 HSI、SysTick 负责把延时函数从"时钟频率依赖"改到"时钟频率无关"、MCO 时钟输出负责验证当前系统时钟到底跑在哪个源上。三件事一起配,时钟可靠性从"完全指望晶振不坏"变成"坏了也能活"。
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(同上) | CSSF | CSS 中断标志 | 硬件置 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 输出。
写完 CSS 之后,很多人立刻遇到第二个问题:系统时钟从 72MHz 降到 8MHz HSI,原来的 Delay() 函数输出的毫秒数就不对了——延时函数里 for 循环的次数是按 72MHz 算的,降频后延时被拉长了 9 倍。
Cortex-M3 内核自带的 SysTick 就是用来解决这个问题的。它是一个 24 位递减计数器,设定初值后使能,每经过 1 个系统时钟周期就减 1,减到 0 时自动重装初值、同时置 COUNTFLAG、(如果中断使能)触发中断。
| 时钟频率 | 实现 1ms 中断的 SysTick 重装载值 | 说明 |
|---|---|---|
| 72MHz(PLL 输出) | 9000 - 1 = 8999 | 72M / 8 = 9M 计数频率;9000 计数 = 1ms |
| 8MHz(HSI,未分频) | 1000 - 1 = 999 | 8M / 8 = 1M 计数频率;1000 计数 = 1ms |
| 4MHz(HSE 4MHz 分频) | 500 - 1 = 499 | 4M / 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 的代码。
CSS 生效、时钟切 HSI 之后,你怎么知道系统真的跑在 HSI 上而不是卡在 PLL 故障状态?示波器量 PA.8——这是 STM32 唯一的 MCO(Microcontroller Clock Output)引脚,可以把 SYSCLK、HSI、HSE 或 PLLCLK 输出到外部,让你实时观察时钟源。
| MCO 源 | RCC_MCO 位 | 典型频率 | 用途 |
|---|---|---|---|
| SYSCLK | 00 | CSS 切换前 72MHz / 切换后 8MHz | 验证 CSS 切换是否成功 |
| HSI | 01 | 8MHz | 确认内部 RC 振荡器是否正常 |
| HSE | 10 | 4~16MHz | 确认外部晶振是否起振 |
| PLLCLK/2 | 11 | PLL 输出频率的一半 | 验证 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 配置了但故障时没触发 NMI | RCC_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小时内响应