2.3 时钟与复位
新制作或焊接完成的最小系统板在烧录进最初的点灯固件后,常会遇到各种难以理解的“反常”现象:板载 LED 的闪烁节奏明显迟滞;串口终端以 115200 波特率打开后,屏幕上刷出的全是无规则乱码;插上 USB 接口,主机操作系统没有任何设备枚举反应。这些现象往往容易被误判为 PCB 虚焊或元器件损坏,然而在绝大多数情况下,微控制器只是默默运行在默认的内部 8 MHz RC 振荡器上,而软件代码中计算波特率、定时器分频以及 USB 物理接口时钟时,却全部预设微控制器运行在 72 MHz。芯片硬件本身完好无损,核心问题在于系统尚未建立起正确的基准节拍。
微控制器是完全由时钟脉冲驱动的同步时序电路。时钟树的拓扑结构决定了系统算力以及各片内外设的动作步调,而复位管理则定义了芯片从上电到可控执行的生命周期起点。理清多路时钟源的汇聚与分发路径,掌握系统主频与外设时钟的寄存器级配置,并能够精准辨析不同类型的硬件复位事件,是构建稳健嵌入式系统的基石。
1. 时钟决定什么
STM32F103 在上电复位完成后,时钟配置寄存器 RCC_CFGR 的系统时钟切换状态位(SW 与 SWS)默认均为 00,微控制器自动选择内部高速 RC 振荡器(HSI)作为系统时钟源,运行于 8 MHz 基础频率。在此初始状态下,CPU 内核虽能正常取指并执行指令循环,但片上所有与时间基准相关的外设若仍按 72 MHz 全速工况进行参数计算,就会产生严重的物理偏差:
- 通用异步收发器(USART)的波特率发生器依赖外设总线时钟(PCLK1 或 PCLK2)进行整数与小数分频。当总线实际时钟相比预期降低了 9 倍(8 MHz 对比 72 MHz)时,物理引脚输出的脉宽就会被拉长 9 倍,远超出串口接收端所能容忍的波特率误差窗口,导致终端程序仅能捕获到校验错误与乱码数据。
- 硬件定时器的计数器递增依赖时钟预分频器的分频输出。若总线时钟偏离预期,定时器溢出与中断触发的实际周期同样会成比例缩放,导致系统延时与控制节拍彻底失准。
- 模数转换器(ADC)内部采样保持电容的充放电时间窗口严格由 ADCCLK 决定,输入时钟的漂移将直接影响采样转换精度与建立时间。
- USB 2.0 全速物理层接口对时钟频率有着严苛的物理规范,强制要求 48 MHz 且时钟精度维持在 ±0.25% 以内,未经过锁相环倍频的 8 MHz 基础时钟根本无法满足 USB 通信协议的硬件握手条件。
除了系统主时钟的频率匹配外,STM32 内部各外设的时钟门控在复位后默认处于关闭状态。参考手册 RM0008 为每个外设分配了专用的总线时钟使能位,复位默认值均为 0。在此时钟关闭的静默状态下,CPU 对该外设寄存器的读写操作虽然不会触发总线硬件故障中断,但写操作会被总线硬件悄然丢弃,读操作则恒定返回 0。这种时钟门控机制是降低现代微控制器动态功耗的核心手段:未经使能的外设电路不会产生充放电翻转电流,从而最大限度地减少闲置能耗。
从本节开始,示例代码统一引入 CMSIS 设备头文件 stm32f1xx.h。该头文件会依据工程配置宏 STM32F103xB 自动展开具体的芯片外设定义 stm32f103xb.h。头文件内将各外设的物理寄存器组建模为 C 语言结构体,并对外暴露基地址指针宏(例如 #define RCC ((RCC_TypeDef *)RCC_BASE))。使用 RCC->CR 访问寄存器,与前文中利用绝对物理地址强转指针解引用的机制完全等价,但极大地增强了代码的可读性与可移植性。
#include "stm32f1xx.h"
void led_clock_on(void) {
/* 将 APB2ENR 的 IOPCEN 位置 1,开启 GPIOC 端口的外设时钟门控 */
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
}2
3
4
5
6
上述语句采用“读-改-写”形式操作 APB2ENR 寄存器。由于时钟控制寄存器在系统初始化阶段通常由 CPU 单一执行流排他性配置,不会受到外部中断或 DMA 控制器的并发干扰,因此此类置位操作在工程上是安全可靠的。
2. 时钟源与时钟树
F103 有四个时钟源,还有一个把其中某一个倍频到高频率的 PLL:
| 时钟源 | 频率 | 来源 | 主要用途 |
|---|---|---|---|
| HSI | 8 MHz | 片内 RC 振荡器 | 复位后的系统时钟、PLL 输入 |
| HSE | 4~16 MHz | 外部晶振,板上为 8 MHz | PLL 输入、系统时钟、USB |
| LSI | 约 40 kHz(30~60 kHz) | 片内 RC 振荡器 | 独立看门狗、RTC |
| LSE | 32.768 kHz | 外部晶振 | RTC |
| PLL | 输入 × 2~16 | HSI/2、HSE 或 HSE/2 | 系统时钟、USB |
HSI 振荡器在出厂时由厂商进行了硬件校准,在 25 °C 常温环境下的频率精度约为 ±1%;但在工业级温宽(0~85 °C 乃至更低温度)环境下,频漂可能扩大至 −1.1%~+1.8%。对于普通的异步串口通信,此等精度的时钟已足够维持正常的数据帧识别;但对于高速或具有严格同步时序要求的协议(如 USB 全速传输),则必须依赖外接无源晶体谐振器或有源晶振所驱动的 HSE,其长期稳定度与温度漂移通常在几十 ppm 级别。
当需要将微控制器系统主频推升至 72 MHz 全速工作时,各级时钟源的流动与分配关系如下图所示:
微控制器内部的各个总线网络与功能单元受半导体工艺和电气特性的限制,均设定了最高工作频率上限。若配置超频,轻则导致外设时序错乱,重则引发总线传输丢失甚至内核死锁:
| 时钟 | 上限 | 本节配置 | 说明 |
|---|---|---|---|
| SYSCLK | 72 MHz | 72 MHz | 系统时钟 |
| HCLK | 72 MHz | 72 MHz | AHB 总线、内核、SRAM、DMA |
| PCLK1 | 36 MHz | 36 MHz | APB1,低速外设 |
| PCLK2 | 72 MHz | 72 MHz | APB2,高速外设 |
| ADCCLK | 14 MHz | 12 MHz | ADC1、ADC2 |
| USBCLK | 48 MHz | 48 MHz | 必须正好 48 MHz |
APB 分频不为 1 时定时器时钟乘 2
RM0008 规定:APB 预分频为 1 时,定时器时钟等于该 APB 域的频率;预分频不为 1 时,定时器时钟是该 APB 域频率的 2 倍。所以 PCLK1 = 36 MHz 时,TIM2~TIM4 拿到的是 72 MHz,而不是 36 MHz。
在进行定时器分频计算时,该特性经常成为隐蔽的工程陷阱。很多初学者看到 APB1 总线频率被预分频至 36 MHz,便习惯性地认为挂接其上的 TIM2~TIM4 同样以 36 MHz 递增,由此算出的 PWM 周期或输入捕获时间会出现整整一倍的误差。与之相对,挂接在 APB2 上的高级定时器 TIM1 由于其所在总线预分频比为 1,因此其输入时钟直接等于 PCLK2(72 MHz),不触发二次翻倍机制。
USB 时钟必须由 HSE 驱动的 PLL 提供
USB 设备要求 48 MHz 时钟,且误差在 ±0.25% 以内。这个频率只能由 PLL 产生:PLL 输入取 HSE(8 MHz),倍频到 72 MHz,再经 USB 预分频 ÷1.5 得到 48 MHz。HSI 的精度不够,用 HSI 倍频出来的 48 MHz 不能用于 USB。
系统时钟为 72 MHz、APB1 预分频为 2 时,TIM3 的计数时钟是多少?
3. 外设时钟使能
三个使能寄存器分管三片区域,常用位如下:
| 寄存器 | 位 | 名称 | 外设 |
|---|---|---|---|
| RCC_AHBENR | 0 | DMA1EN | DMA1 |
| RCC_AHBENR | 2 | SRAMEN | SRAM |
| RCC_AHBENR | 4 | FLITFEN | Flash 接口 |
| RCC_APB2ENR | 0 | AFIOEN | AFIO(重映射、EXTI 端口选择) |
| RCC_APB2ENR | 2~5 | IOPAEN~IOPDEN | GPIOA~GPIOD |
| RCC_APB2ENR | 9 | ADC1EN | ADC1 |
| RCC_APB2ENR | 11 | TIM1EN | TIM1 |
| RCC_APB2ENR | 12 | SPI1EN | SPI1 |
| RCC_APB2ENR | 14 | USART1EN | USART1 |
| RCC_APB1ENR | 0~2 | TIM2EN~TIM4EN | TIM2~TIM4 |
| RCC_APB1ENR | 11 | WWDGEN | 窗口看门狗 |
| RCC_APB1ENR | 14 | SPI2EN | SPI2 |
| RCC_APB1ENR | 17、18 | USART2EN、USART3EN | USART2、USART3 |
| RCC_APB1ENR | 21、22 | I2C1EN、I2C2EN | I2C1、I2C2 |
| RCC_APB1ENR | 23 | USBEN | USB |
| RCC_APB1ENR | 25 | CANEN | CAN |
| RCC_APB1ENR | 28 | PWREN | 电源控制(低功耗用) |
在编写任何片内外设的驱动逻辑时,首要操作始终是开启该外设挂载总线上的时钟门控。漏掉该使能步骤并不会导致编译告警或触发硬件异常,但由于外设控制逻辑处于断电停钟状态,后续所有的寄存器写入操作都将无法送达硬件内部:
/* GPIOC 的时钟还是关的 */
GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | (0x2u << 20);
GPIOC->BSRR = GPIO_BSRR_BR13;2
3
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | (0x2u << 20);
GPIOC->BSRR = GPIO_BSRR_BR13;2
3
从底层电气行为来看,访问一个时钟处于关闭状态的外设,总线控制器不会抛出 Bus Fault 异常,其表现为写操作被静默丢弃,读操作恒返回 0。如果在调试过程中仅通过单步断点观察寄存器窗口,常会让人误以为数据已成功写入,而实际外设物理引脚与状态机毫无动静。电源控制模块 PWR 同样遵循此规则,若未在 APB1ENR 中置位 PWREN,后续任何针对后备域解锁或低功耗模式的配置均属无效操作。
4. 用寄存器配置 72 MHz
将系统主频从复位默认的 8 MHz HSI 安全过渡至 72 MHz PLL 全速工况,必须严格遵循半导体物理时序的依赖关系。整个配置流程在底层可拆解为六个阶段:
- 开启外部高速晶体振荡器 HSE,并循环等待内部起振电路稳定(直到
HSERDY标志位置位); - 根据目标 72 MHz 频率增加 Flash 存储器的访问等待周期(Latency),并开启指令预取缓冲;
- 配置 AHB、APB1、APB2 总线的分频系数,设定 ADC 预分频,并选择 PLL 的时钟输入源与倍频系数;
- 开启锁相环 PLL,等待锁相环完成闭环锁定(直到
PLLRDY标志位置位); - 将系统时钟切换至 PLL 输出端,并等待硬件多路选择器确认切换完成(
SWS状态位显示为10); - 同步更新记录当前 CPU 核心主频的全局对象
SystemCoreClock,为上层软件提供正确的时间参考。
完整函数在 code/嵌入式/2_MCU裸机开发/2_3_时钟与复位/clock.c:
#include "clock.h"
#include "stm32f1xx.h"
uint32_t SystemCoreClock = 8000000u;
void clock_init_72mhz(void) {
/* 1. 开启外部 8 MHz 晶振(HSE),等待振荡稳定 */
RCC->CR |= RCC_CR_HSEON;
while ((RCC->CR & RCC_CR_HSERDY) == 0) {
}
/* 2. 提高 SYSCLK 前先开启 Flash 预取缓冲并设为 2 个等待周期(48~72 MHz) */
FLASH->ACR = (FLASH->ACR & ~FLASH_ACR_LATENCY) |
FLASH_ACR_LATENCY_2 | FLASH_ACR_PRFTBE;
/* 3. 配置总线分频与 PLL */
uint32_t cfgr_mask = RCC_CFGR_HPRE | RCC_CFGR_PPRE1 | RCC_CFGR_PPRE2 |
RCC_CFGR_ADCPRE | RCC_CFGR_PLLSRC |
RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL |
RCC_CFGR_USBPRE;
uint32_t cfgr_val = RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE2_DIV1 |
RCC_CFGR_PPRE1_DIV2 | RCC_CFGR_ADCPRE_DIV6 |
RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9;
RCC->CFGR = (RCC->CFGR & ~cfgr_mask) | cfgr_val;
/* 4. 开启 PLL,等待锁定 */
RCC->CR |= RCC_CR_PLLON;
while ((RCC->CR & RCC_CR_PLLRDY) == 0) {
}
/* 5. 将系统时钟切换到 PLL,等待硬件确认切换完成 */
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
}
/* 6. 更新全局系统时钟频率对象 */
SystemCoreClock = 72000000u;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
第 3 步写进去的值对应:HPRE = 0000(HCLK = SYSCLK)、PPRE2 = 000(PCLK2 = HCLK)、PPRE1 = 100(PCLK1 = HCLK / 2)、ADCPRE = 10(ADCCLK = PCLK2 / 6)、PLLSRC = 1(PLL 输入取 HSE)、PLLXTPRE = 0(HSE 不分频)、PLLMUL = 0111(×9)、USBPRE = 0(PLL / 1.5)。完整的位表见本节后面。
SystemCoreClock 不是硬件寄存器,是 CMSIS 约定给软件用的一个对象,SysTick_Config、HAL_Init 和串口初始化都会读它。时钟改了而它没改,后面按它算出来的节拍、波特率全错。工程里如果时钟频率在运行时变化,调用 SystemCoreClockUpdate(),让 CMSIS 从寄存器反推;本节的频率固定,直接赋值。
要让 SYSCLK = 72 MHz、PCLK1 = 36 MHz、PCLK2 = 72 MHz,且 USB 可用,PLL 与分频应该怎么配?
提高 SYSCLK 之前先增加 Flash 等待周期
Flash 存储器的物理存取周期受半导体工艺限制,其响应速度无法匹配高达 72 MHz 的 CPU 核心时钟。微控制器通过插入等待周期(Wait States)来维持取指时序:当 HCLK 不超过 24 MHz 时采用 0 个等待周期(零等待);处于 24~48 MHz 区间时配置 1 个等待周期;提升至 48~72 MHz 时则必须配置 2 个等待周期。在提高系统主频时,必须先在 FLASH_ACR 寄存器中增加 LATENCY 并启用预取缓冲,然后再执行时钟源切换;若顺序颠倒,内核将在 Flash 读时序尚未建立的工况下以超频速率取指,导致指令总线数据畸变。
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
}
FLASH->ACR = (FLASH->ACR & ~FLASH_ACR_LATENCY) | FLASH_ACR_LATENCY_2;2
3
4
FLASH->ACR = (FLASH->ACR & ~FLASH_ACR_LATENCY) |
FLASH_ACR_LATENCY_2 | FLASH_ACR_PRFTBE;
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
}2
3
4
5
左栏的错误时序在时钟切换生效后的几个总线周期内,Flash 存储器接口仍按照旧有等待周期工作,CPU 取指逻辑必然会采样到非法指令操作码,从而瞬时引发 HardFault 硬件故障中断,乃至导致微控制器彻底跑飞挂起。这类故障往往在连接调试器单步执行时表现不明显,因为调试器探针介入时总线访问步调已被彻底打散,唯有让芯片全速自由运行时方会暴露。
示例的构建结果:
$ arm-none-eabi-gcc -std=c11 -mcpu=cortex-m3 -mthumb -O2 -g -Wall -Wextra \
-ffunction-sections -fdata-sections -DSTM32F103xB -I. \
-nostartfiles --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections \
-T stm32f103c8.ld clock.c main.c startup_stm32f103.c -o clock_demo.elf
$ arm-none-eabi-size clock_demo.elf
text data bss dec hex filename
652 4 1540 2196 894 clock_demo.elf2
3
4
5
6
7
-I. 指向的目录里放着 stm32f1xx.h、stm32f103xb.h、core_cm3.h 等头文件。它们来自 CMSIS_5 5.9.0 的 CMSIS/Core/Include 与 cmsis_device_f1 v4.3.5 的 Include,仓库不提交第三方源码,构建前按这两个版本取一份即可。
main.c 启动后先记录复位原因,再配置时钟,然后把 PLL 二分频从 PA8 送出去,最后点亮 PC13 表示切换完成。板上能看到的现象是 PA8 上出现 36 MHz 方波、LED 常亮;没有示波器时,看 LED 亮不亮也能判断程序有没有走过时钟初始化。
RCC_CFGR 位表(RM0008 第 7.3.2 节)
| 位 | 名称 | 含义 |
|---|---|---|
| 1:0 | SW | 系统时钟选择:00 HSI、01 HSE、10 PLL、11 保留 |
| 3:2 | SWS | 当前系统时钟(只读),编码同 SW |
| 7:4 | HPRE | AHB 分频:0000~0111 ÷1,1000 ÷2,1001 ÷4,1010 ÷8,1011 ÷16,1100 ÷64,1101 ÷128,1110 ÷256,1111 ÷512 |
| 10:8 | PPRE1 | APB1 分频:0xx ÷1,100 ÷2,101 ÷4,110 ÷8,111 ÷16 |
| 13:11 | PPRE2 | APB2 分频,编码同 PPRE1 |
| 15:14 | ADCPRE | ADC 分频:00 ÷2,01 ÷4,10 ÷6,11 ÷8 |
| 16 | PLLSRC | PLL 输入:0 HSI/2,1 HSE |
| 17 | PLLXTPRE | HSE 分频:0 ÷1,1 ÷2 |
| 21:18 | PLLMUL | 倍频:0000 ×2 … 0111 ×9 … 1110、1111 ×16 |
| 22 | USBPRE | USB 分频:0 PLL/1.5,1 PLL |
| 26:24 | MCO | 时钟输出:100 SYSCLK,101 HSI,110 HSE,111 PLL/2 |
配好之后,各路时钟是不是预期的值,可以先用一个主机程序算一遍。clock_calc.c 把 CFGR 的值拆成各个字段,按同样的规则推出频率,并检查 USB 是否正好 48 MHz:
$ gcc -std=c17 -Wall -Wextra clock_calc.c -o clock_calc
$ ./clock_calc
Preset SYSCLK HCLK PCLK1 TIM2-4 PCLK2 ADCCLK USBCLK WS
---------------------------------------------------------------------------------------
Reset default (HSI 8 MHz) 8 M 8 M 8 M 8 M 8 M 4 M off 0
HSE 8 MHz -> PLL x9 (72 MHz) 72 M 72 M 36 M 72 M 72 M 12 M 48.0 2
HSE 8 MHz -> PLL x6 (48 MHz) 48 M 48 M 24 M 48 M 48 M 12 M 48.0 1
HSI/2 4 MHz -> PLL x16 (64 MHz) 64 M 64 M 32 M 64 M 64 M 10 M 42.6* 22
3
4
5
6
7
8
最后一行是只用 HSI 能得到的最高频率。USB 那一列带星号,因为 64 MHz 的 PLL 输出除以 1.5 是 42.67 MHz,不是 48 MHz;即使换一种分频凑出 48 MHz,时钟源是 HSI,精度也不满足 USB 的要求。这个程序也顺手验证了 48 MHz 和 64 MHz 两种配置下的 Flash 等待周期:48 MHz 只需要 1 个,64 MHz 需要 2 个。
5. 验证时钟
完成寄存器层面的时钟配置后,验证物理时序是否如期建立的最直接手段,是将芯片内部的时钟信号引出到外部物理引脚进行测量。STM32 提供了微控制器时钟输出引脚(Microcontroller Clock Output, MCO),固定复用在 PA8 引脚上。通过配置 RCC_CFGR 寄存器中的 MCO 选择位,可以将内部时钟源路由输出。这里我们选择输出 PLL 经过二分频后的 36 MHz 信号:
void mco_enable_pll_div2(void) {
/* 开启 GPIOA 时钟,把 PA8(CRH[3:0])配置为 50 MHz 复用推挽输出(0xB) */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
GPIOA->CRH = (GPIOA->CRH & ~0xFu) | 0xBu;
/* 选择 PLLCLK / 2(72 MHz / 2 = 36 MHz)从 PA8(MCO)输出 */
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_MCO) | RCC_CFGR_MCO_PLLCLK_DIV2;
}2
3
4
5
6
7
8
之所以不直接选择 72 MHz 全速输出,是因为 STM32F103 的 GPIO 物理输出级在电气规格上标称的最高翻转频率为 50 MHz。72 MHz 高频信号直接经由引脚走线引出时,受引脚分布电容与压摆率限制,方波波形会严重畸变为正弦波甚至发生幅度衰减。使用示波器探头探测 PA8 与板载地线,正常情况下应观测到频率稳定在 36 MHz 的方波信号,其频偏精度直接取决于外部晶振品质。
需要特别留意的是,MCO 的信号源直接取自 PLL 倍频器的输出分频点,并没有经过系统时钟切换开关(SW)。因此在 PA8 上测得稳定的 36 MHz 信号,仅证明外部晶振与 PLL 电路工作正常且已成功锁频,并不等同于系统内核主频已经切实切换至 PLL。若要确证内核当前的实际运行基准,仍需查验 RCC_CFGR 寄存器中由硬件只读更新的 SWS(System Clock Switch Status)位,当其回读值为 10 时,方代表切换动作彻底闭环。在缺乏示波器等硬件测试工具的场景下,除通过调试器读取全局对象 SystemCoreClock 外,后文中基于 SysTick 的定时闪灯节拍以及串口在 115200 波特率下是否能够准确收发,均能构成对时钟系统的跨层级交叉验证。
6. 复位
嵌入式微控制器的复位事件可按硬件影响深度划分为不同层级。系统复位(System Reset)会将 CPU 内核及绝大多数片内外设的寄存器恢复至出厂初始状态,但保留后备寄存器(BKP)以及实时时钟(RTC)所在供电域的数据;电源复位(Power Reset)则发生在上电(POR)或电网跌落欠压(PDR)瞬间,除备份域外对整颗芯片实施彻底的冷启动复位。诸如独立看门狗超时、软件主动触发以及外部 NRST 引脚被拉低,在体系上均归属于系统复位。
芯片运行过程中究竟因何种诱因发生重启,可通过读取控制与状态寄存器 RCC_CSR 中的各个硬件标志位进行溯源:
| 位 | 名称 | 含义 |
|---|---|---|
| 26 | PINRSTF | NRST 引脚复位 |
| 27 | PORRSTF | 上电/掉电复位 |
| 28 | SFTRSTF | 软件复位 |
| 29 | IWDGRSTF | 独立看门狗复位 |
| 30 | WWDGRSTF | 窗口看门狗复位 |
| 31 | LPWRRSTF | 低功耗管理复位 |
| 24 | RMVF | 清除复位标志,写 1 生效 |
这些复位标志在底层具有锁存特性,属于典型的“逻辑或”累积机制:一旦某一硬件事件发生,对应的状态位即被置 1 并持续保持,直到软件主动向 RMVF(Remove Reset Flag)位写 1 才能统一清除。值得注意的是,微控制器内部的大多数复位发生器在动作时,片上硬件会自动联动将外部物理管脚 NRST 同步拉低数十微秒,这导致 PINRSTF 标志位几乎总会被伴随置起。因此,在编写复位原因鉴别函数时必须严格遵守判断顺序:优先排查具体的看门狗、低功耗或软件复位源,将引脚复位放在最后作为兜底判断,否则真实的内部故障根因将被普遍置位的引脚复位标志掩盖。
clock.c 里的 reset_cause_get() 即遵循了这一判决时序:
reset_cause_t reset_cause_get(void) {
uint32_t csr = RCC->CSR;
reset_cause_t cause = RESET_CAUSE_UNKNOWN;
if ((csr & RCC_CSR_LPWRRSTF) != 0) {
cause = RESET_CAUSE_LOW_POWER;
} else if ((csr & RCC_CSR_WWDGRSTF) != 0) {
cause = RESET_CAUSE_WWDG;
} else if ((csr & RCC_CSR_IWDGRSTF) != 0) {
cause = RESET_CAUSE_IWDG;
} else if ((csr & RCC_CSR_SFTRSTF) != 0) {
cause = RESET_CAUSE_SOFTWARE;
} else if ((csr & RCC_CSR_PORRSTF) != 0) {
cause = RESET_CAUSE_POR;
} else if ((csr & RCC_CSR_PINRSTF) != 0) {
cause = RESET_CAUSE_PIN;
}
/* 向 RMVF 写 1 清除所有复位标志,以免影响下一次复位后的判别 */
RCC->CSR |= RCC_CSR_RMVF;
return cause;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
返回的枚举在 2.10 看门狗、低功耗与故障处理 里用来区分看门狗复位,6.2 日志、断言与故障诊断 里会把它写进日志。读完之后必须清标志,否则下一次启动读到的还是上一次的记录,判断就失去意义。
软件复位用 CMSIS 的 NVIC_SystemReset(),它往 SCB->AIRCR 写 SYSRESETREQ 和访问密钥,内核在几个周期后复位。函数不会正常返回。故障处理函数记录完现场后就调用它,让设备自己重启。
外设复位是另一回事:RCC_APB1RSTR 和 RCC_APB2RSTR 的每一位对应一个外设,置 1 再清 0,该外设的所有寄存器回到复位值,但不影响 CPU 和其他外设。I2C 总线被从机拉死、重新初始化也恢复不了时,常见做法就是先复位 I2C1:
RCC->APB1RSTR |= RCC_APB1RSTR_I2C1RST;
RCC->APB1RSTR &= ~RCC_APB1RSTR_I2C1RST;2
调试时先看复位原因
设备莫名其妙重启,第一件事是读 reset_cause_get()。看到 IWDG 就去查喂狗,看到 POR 就去查供电,看到 PIN 再去看 NRST 上的电平和电容。复位标志只有一次机会,读完就被清掉,所以要在启动的最前面打印或保存。
程序每次启动都调用 reset_cause_get() 并把标志清掉了。现在按一下板上的复位键,本次启动读 RCC_CSR 时哪些标志会置位?
7. HAL 对照
在工业界常用的 STM32Cube 生态中,图形化配置工具 CubeMX 生成的 SystemClock_Config() 函数在底层执行的硬件时序与前述寄存器操作完全一致,只是将参数解耦并封装到了两个独立的初始化配置对象中:
RCC_OscInitTypeDef osc = {0};
RCC_ClkInitTypeDef clk = {0};
osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
osc.HSEState = RCC_HSE_ON;
osc.PLL.PLLState = RCC_PLL_ON;
osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
osc.PLL.PLLMUL = RCC_PLL_MUL9;
HAL_RCC_OscConfig(&osc);
clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK |
RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
clk.APB1CLKDivider = RCC_HCLK_DIV2;
clk.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在此抽象分层下,HAL_RCC_OscConfig 专门负责振荡源的开启(如 HSE 起振判断)与 PLL 的闭环配置;而 HAL_RCC_ClockConfig 则负责计算并设置多级总线分频比、调整 Flash 等待周期以及执行最终的系统主频切换。该函数最后一个入参显式传入 FLASH_LATENCY_2,HAL 驱动库内部会确保在时钟频率抬升前预先写入 Flash 等待周期,从而避免了裸机手写寄存器时容易出现的时序倒置缺陷。
外设时钟使能与复位控制在 HAL 库中的封装映射关系如下:
| 寄存器写法 | HAL 写法 |
|---|---|
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN | __HAL_RCC_GPIOC_CLK_ENABLE() |
RCC->CSR & RCC_CSR_IWDGRSTF | __HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST) |
RCC->CSR |= RCC_CSR_RMVF | __HAL_RCC_CLEAR_RESET_FLAGS() |
RCC->APB1RSTR |= RCC_APB1RSTR_I2C1RST | __HAL_RCC_I2C1_FORCE_RESET()、__HAL_RCC_I2C1_RELEASE_RESET() |
RCC->CFGR 的 MCO 位 | HAL_RCC_MCOConfig(RCC_MCO1, RCC_MCO1SOURCE_PLLCLK_DIV2, RCC_MCODIV_1) |
HAL 库所提供的控制宏与函数在本质上仍然是在读写上述同一组硬件寄存器。在透彻理解了底层总线时钟树与硬件时序约束之后,无论使用精简的寄存器直接操作,还是调用高度封装的 HAL 驱动,其背后的物理机理均别无二致。
8. 习题
8.1 巩固
下面这行代码编译通过,GPIOC 的时钟却始终没打开:
RCC->APB1ENR |= RCC_APB2ENR_IOPAEN;(1) 说明这行实际写进了哪个寄存器、置起了哪一位,打开了哪个外设的时钟,为什么编译器和链接器都不会报警。
(2) 从第 3 节的表里查出 GPIOC、TIM4、USART1、ADC1、DMA1、I2C1 的使能位,整理成四列的表:外设、寄存器、位、CMSIS 宏名。
(3) 改正上面那行,并写出同时打开 GPIOC、TIM4、USART1 的三条语句。
(4) 假设 RCC->APB2ENR 里 USART1EN 一直没置位,串口初始化代码照常执行、不报错,printf 却一个字也发不出来。说明这类「配置写进去但外设没收到」的现象与第 3 节讲的写无效是同一回事,调试时看哪个寄存器能最快确认。
把 RCC->CFGR 的各位读出来,回答下面这个配置下各路时钟的频率:
RCC->CFGR = 0x001D840Au;(1) 按第 4 节的位表逐字段拆开这个值,每个字段写成「位段 = 二进制 = 含义」,字段包括 SW、HPRE、PPRE1、PPRE2、ADCPRE、PLLSRC、PLLXTPRE、PLLMUL、USBPRE。
(2) 按拆出来的字段算出 SYSCLK、HCLK、PCLK1、TIM2~TIM4、PCLK2、ADCCLK、USBCLK。
(3) 用 code/嵌入式/2_MCU裸机开发/2_3_时钟与复位/clock_calc.c 核对:
$ gcc -std=c17 -Wall -Wextra clock_calc.c -o clock_calc
$ ./clock_calc2
指出 clock_calc 输出的哪一行正好对应这个值。对照时注意:clock_calc 的预设是用 SWS 字段的值拼出来的,与这个值在最低两位上不同。说明 SW 与 SWS 各是什么、为什么频率仍然对得上。
clock_calc.c 的 presets 数组里加一组 RCC->CFGR = 0x0038840Au,重新编译运行。
(1) 写出这一组的输出行,说明 USBCLK 列末尾的星号是按什么规则打上的。
(2) 这一组的 SYSCLK 是多少,按第 4 节的 Flash 等待周期表需要几个等待周期。如果 acr 只给 0x31(1 个等待周期),现场会看到什么。
(3) 这一组能不能用来跑 USB?从频率与时钟源精度两个角度分别说明。
reset_cause_get() 按固定顺序判断复位来源,读完之后用 RCC->CSR |= RCC_CSR_RMVF 清标志。
(1) 为什么判断时把 PINRSTF 放在最后?如果先判断 PINRSTF,一次独立看门狗复位会被报成什么?
(2) 下面这段代码抄自某工程,指出它的问题:
reset_cause_t cause = reset_cause_get();
if (reset_cause_get() == RESET_CAUSE_IWDG) {
log_write("watchdog reset");
}2
3
4
(3) 写一个 reset_flags_dump(),把 RCC->CSR 的第 26~31 位逐个按 PINRSTF=1 PORRSTF=0 ... 的格式打印出来,不清标志,供调试时在 reset_cause_get() 之前调用。
8.2 编程
基于初始代码 code/嵌入式/2_MCU裸机开发/2_3_时钟与复位/exercises/cfgr_decode_starter.c,补全 cfgr_inspect():
- 按第 4 节的位表拆出 HPRE、PPRE1、PPRE2、ADCPRE、PLLSRC、PLLXTPRE、PLLMUL,算出 SYSCLK、HCLK、PCLK1、TIM2~TIM4、PCLK2、ADCCLK,填进
clock_report_t。 limits_ok要求 SYSCLK ≤ 72 MHz、HCLK ≤ 72 MHz、PCLK1 ≤ 36 MHz、PCLK2 ≤ 72 MHz、ADCCLK ≤ 14 MHz 同时成立。flash_ws_ok按FLASH_ACR的 LATENCY[2:0] 判断:SYSCLK ≤ 24 MHz 需要 0 个等待周期,≤ 48 MHz 需要 1 个,≤ 72 MHz 需要 2 个。
用 gcc -std=c17 -Wall -Wextra cfgr_decode_starter.c -o cfgr_decode 编译运行,期望输出:
72 MHz valid SYSCLK=72000000 PCLK1=36000000 TIM2=72000000 ADC=12000000 limits=1 ws=1
APB1 overclock SYSCLK=72000000 PCLK1=72000000 TIM2=72000000 ADC=12000000 limits=0 ws=1
WS too small SYSCLK=72000000 PCLK1=36000000 TIM2=72000000 ADC=12000000 limits=1 ws=02
3
注意 ws 这一列打印的是布尔结论(1 表示满足),不是等待周期数。acr = 0x32 的低 3 位是 2,先想清楚这个字段在 FLASH_ACR 里的位置再动手。
clock_init_72mhz() 里的三个 while 循环等不到条件时会永远转下去,芯片看起来像死机。
(1) 把 clock_init_72mhz() 改成返回 int:HSERDY、PLLRDY、SWS 三处等待各加一个循环上限,任一超时返回 -1,全部成功返回 0。上限用简单的计数器,不要引入 SysTick。
(2) 写一个主机程序 timeout_model.c 验证这段逻辑:把三处等待建模成三个 for 循环,循环上限与「第几轮条件成立」都从命令行读入,输出每一步在第几轮等到,或者在超时后打印 HSE timeout、PLL timeout、SW timeout 并返回非零。
(3) 说明为什么超时分支里不能调用任何依赖 SystemCoreClock 的函数(比如 printf 重定向到串口)。
给 clock.c 增加 clock_init_hse_48mhz(),把系统时钟配成 48 MHz:
- HSE 8 MHz 不分频,PLL ×6;
- HCLK = 48 MHz,PCLK1 = 24 MHz,PCLK2 = 48 MHz;
- ADCCLK = 12 MHz,USB 时钟取 PLL 直接输出(
USBPRE = 1); - 同样更新
SystemCoreClock。
要求:
(1) 写出写进 CFGR 的值(十六进制)与每一段字段的取值,再写出切换完成后从调试器读回的值,说明两者差在哪一段。
(2) 把这组配置加进 clock_calc.c 的 presets(SWS 字段按实际值填),贴出输出行,核对 USB 正好 48 MHz、ADCCLK 正好 12 MHz、Flash 等待周期是 1 个。
(3) 说明这组配置下 TIM2~TIM4 的计数时钟是多少,为什么不是 24 MHz。
(4) 用第 7 节的 HAL 写法写出同一个配置:两个结构体各字段填什么,HAL_RCC_ClockConfig 的最后一个参数传什么。说明 HAL 是怎样把等待周期和切换顺序绑在一起的。
写一个主机程序 mco_plan.c,命令行参数为 --sysclk <Hz> 与 --want <Hz>,输出让 MCO 最接近 --want 的源与分频。
MCO 的四个源见第 4 节的位表:100 SYSCLK、101 HSI、110 HSE、111 PLL/2。HSI、HSE 都按 8 MHz 算。
(1) 对四个源各算出输出频率、绝对误差与相对误差,按相对误差从小到大打印。
(2) --sysclk 72000000 --want 36000000 时四个源各是什么频率,哪一个误差最小?既然 SYSCLK 已经是 72 MHz,为什么第 5 节还要绕道 PLL/2 输出 36 MHz,而不把 SYSCLK 直接送出去。
(3) 再加一条规则:输出超过 50 MHz 时在该行末尾打印 [over IO rating]。用 --want 72000000 运行一次,说明最优源是哪一个、为什么它带上了这条标记。
验收:gcc -std=c17 -Wall -Wextra mco_plan.c -o mco_plan,./mco_plan --sysclk 72000000 --want 36000000 与 ./mco_plan --sysclk 72000000 --want 72000000 两次运行都不崩溃、输出四行。
8.3 综合与挑战
写一个主机程序 clock_audit.c,对任意一组 CFGR 与 FLASH_ACR 做审查,代替人工查表。
- 从标准输入按行读
cfgr acr(十六进制),读到文件结束。 - 每组输出 SYSCLK、HCLK、PCLK1、TIM2~TIM4、PCLK2、ADCCLK、USBCLK,再跟一行结论。
- 结论按 RM0008 的约束给出,至少检查这几条:SYSCLK ≤ 72 MHz、HCLK ≤ 72 MHz、PCLK1 ≤ 36 MHz、PCLK2 ≤ 72 MHz、ADCCLK ≤ 14 MHz、USB 为 48 MHz 且时钟源为 HSE、Flash 等待周期足够。SYSCLK 取自 PLL 时还要检查 PLL 输入 1~25 MHz、PLL 输出 16~72 MHz;系统时钟不从 PLL 取时 PLL 根本不开,这两条不适用。有违反就打印
FAIL: <原因>,全部满足打印OK。同一组同时违反多条时全部列出。 - USB 只在系统时钟取自 PLL 时检查;
SW与SWS不一致(写了切换但硬件还停在 HSI)也要报出来。用下面八组输入验收:
00000000 00000030
001D840A 00000032
0051840A 00000031
0038840A 00000032
001D800A 00000032
007D840A 00000032
001D8402 00000032
001D840A 000000302
3
4
5
6
7
8
第一组是复位默认,第二组是正文的 72 MHz,第三组是一个合法的 48 MHz 配置,第四组只用 HSI 倍频(频率合法,但 USB 不能用);后四组各含一处错误。
- 写出每组的结论,并对应到现场故障(USB 不识别、APB1 超频、PLL 超频、切换没完成、随机 HardFault)。说明哪一条只有拿示波器量 PA8 或读
SWS才能确认。
9. 参考资料
- STMicroelectronics, RM0008 Reference manual: STM32F101xx, STM32F102xx, STM32F103xx, STM32F105xx and STM32F107xx advanced Arm-based 32-bit MCUs, Rev 21:第 7 章 Reset and clock control(RCC)、第 3 章 Flash 访问控制(LATENCY 表)。
- STMicroelectronics, DS5319 Datasheet: STM32F103x8, STM32F103xB medium-density performance line Arm-based 32-bit MCU, Rev 20:HSI、HSE、LSI 的频率与精度,USB 时钟要求。
- STMicroelectronics, cmsis_device_f1:
system_stm32f1xx.c中的SystemCoreClockUpdate与设备头文件。 - Arm, CMSIS-Core (Cortex-M):
SystemCoreClock、SystemInit与NVIC_SystemReset。 - cpq, Bare-metal programming guide:Setting up clocks 一节,HSE + PLL 的配置顺序。
- STMicroelectronics, STM32F1 HAL driver:
stm32f1xx_hal_rcc.c中HAL_RCC_ClockConfig的实现。