资讯动态

STM32寄存器开发:RCC时钟配置从8MHz倍频到72MHz实战

发布时间:2026/10/18 13:14:47 来源:尧图企业网站定制
上一章我们把“寄存器地址、偏移量、位操作”这三板斧捋了一遍至少看到芯片手册里那一堆表格心里不那么怵了。这一章按理说该接GPIO了但我考虑了一下决定先把时钟RCC拎出来单独讲透。不是因为我偏爱时钟而是在实际带新人的过程中我见过太多次“板子不工作”的求助最后一查超过一半都落在同一个原因上外设的时钟根本没使能。这一篇就是“STM32寄存器白话手册”的第二章主角是RCC寄存器目标很直接——让你亲手写出一个把系统时钟从8MHz整到72MHz的初始化函数再把板载LED点亮。1. 为什么第二章就碰时钟寄存器的“总闸”和“红绿灯”1.1 一块“不亮”的板子从A同学的问题说起大概半年前有个刚入门的朋友就叫他A同学吧。他拿着一块常见的STM32F103C8T6核心板照着网上的教程写GPIO控制代码逻辑看着全对引脚配成推挽输出、输出寄存器拉高拉低甚至复位寄存器也用了。下载到板子以后LED就是不亮。他折腾了一个晚上把GPIO相关的寄存器翻来覆去地改什么复用功能、上下拉、输出速度都试了一遍毫无反应。最后我让他查一件事RCC的APB2ENR寄存器里GPIOC的时钟使能位置1了没有他补了一句“RCC-APB2ENR | (1 4);”LED瞬间亮了。这个案例特别典型因为它说明一个核心问题STM32的外设在上电之后默认是“断电”状态。只有先把对应外设的时钟使能外设寄存器里的配置才会真正生效。1.2 时钟到底是什么从“心跳”到“电网”的生活类比把时钟理解成“心跳”不太准确我更喜欢把它类比成电网。MCU内部有CPU有Flash有GPIO、串口、定时器这些外设。CPU要干活外设也要干活但所有动作都需要一个统一的节拍来驱动这个节拍就是时钟。你给GPIO写输出高电平本质上是在一个时钟脉冲的触发下让输出锁存器更新状态定时器计数也是每个时钟脉冲加一串口发送数据更是按时钟节拍一位一位地往外送位流。如果某个外设的时钟没有打开相当于你买了个电器却没合闸。电器插着电、电线接好了、开关也按了但电源总闸没推上去它当然不工作。GPIO、串口、定时器、ADC、SPI每一个外设都挂在一条时钟总线上总闸就在RCC模块里。1.3 为什么第二章就讲它后面每一章都绕不开很多教材把RCC放在系统初始化那一大段里用启动文件一带而过导致初学者对RCC的印象停留在“别管初始化代码里全配好了”。但我们是寄存器路线不能跳过。后面写GPIO、USART、定时器时每一步几乎都逃不开“使能时钟”这一步。如果你不理解RCC里每个位的含义遇到外设不工作就只能瞎猜是不是模式配错了是不是寄存器地址不对其实八成只是忘了合闸。因此这一章把RCC拆开讲透往后的代码才有地气。2. 时钟树白话拆解四个“供货商”和两条大马路2.1 四个时钟源各有分工HSI、HSE、LSI、LSESTM32的时钟源一共有四个来源很多人被“时钟树”那张大图吓住感觉枝枝杈杈全是线。其实你不用背整棵树你只需要认识这四个源头HSI内部高速RC振荡器频率8MHz芯片上电默认就是用它跑。好处是啥都不用接、上电就能用坏处是精度一般温度一变化频率会漂。HSE外部高速晶振开发板上通常接8MHz。精度高适合给系统提供稳定主频。LSI内部低速RC大约40kHz主要给独立看门狗和RTC备用时钟用。LSE外部32.768kHz晶振RTC专用低功耗场景下靠它走时。四个时钟源里真正跟系统主频相关的就是HSI和HSE。LSI、LSE一般只在RTC、看门狗那种地方出现平时写业务代码基本不会碰。我把它们的特点整理成一个表方便对比时钟源来源典型频率主要用途特点HSI芯片内部RC8MHz上电默认主时钟、备用时钟启动快、精度一般HSE外部晶振8MHz板载系统主时钟来源精度高、需要起振时间LSI芯片内部RC约40kHz独立看门狗、RTC备用超低功耗LSE外部低速晶振32.768kHzRTC精确计时频率低、极省电2.2 PLL锁相环把8MHz“翻倍”到72MHz的变压器时钟源直接拿来当系统主频行不行也行比如直接用HSI 8MHz系统就能跑。但问题在于STM32F103最高可以跑到72MHz只拿8MHz当主频性能就浪费了一大截。这时候就需要PLL锁相环。你可以把它理解成一个变压器输入一个低频信号通过内部倍频逻辑输出一个更高频的时钟。比如HSE是8MHzPLL倍频系数设成9输出就是8×972MHz。注意PLL不是想翻几倍就翻几倍。F103的PLL倍频系数有明确范围同时PLL的输入频率也有上下限。所以配置的时候你先得确认自己的晶振频率是多少再算倍频系数。常见做法就是8MHz晶振倍频9得到72MHz。2.3 AHB、APB1、APB2三条“大马路”时钟怎么分配下去PLL输出72MHz之后时钟信号不会直接灌给所有外设中间还要经过三条总线AHB给CPU、Flash、DMA这些核心部件提供时钟之后再用预分频器分出APB1和APB2。APB1低速总线挂的是USART2/3、TIM2~7、I2C、SPI2/3等外设。F103上APB1最高36MHz。APB2高速总线挂的是GPIO、USART1、TIM1、SPI1、ADC等外设。F103上APB2最高72MHz。为什么要分一条慢一条快因为不同外设的工作频率上限不同。有些外设设计上就跑不了那么快硬喂72MHz反而容易不稳定。于是一方面把速度高的外设放在APB2另一方面用分频器把APB1压到一半。用文字把整个链路表示出来就是这样HSE 8MHz → PLL×9 → 72MHz SYSCLK → AHB不分频 → 72MHz ├→ APB2不分频 → 72MHzGPIO、USART1... └→ APB1÷2 → 36MHzUSART2/3、TIM2...看到没核心就是“一个来源两个马路”。你不需要背什么复杂图表抓到这条主线RCC配置就成功了一半。3. 把RCC寄存器逐个翻出来CR、CFGR与使能位3.1 CR时钟控制寄存器每个“开关”在哪一位RCC控制寄存器的基地址是0x40021000CR寄存器就在偏移0x00处。它的作用说白了就是一堆开关和状态灯。CR里真正常用的位有这么几个bit0 HSIONHSI开关写1打开HSI。bit1 HSIRDYHSI就绪标志硬件自动置1表示HSI已经稳定。bit16 HSEONHSE开关写1打开HSE。bit17 HSERDYHSE就绪标志。bit18 HSEBYPHSE旁路模式当前不是用晶振振荡而是由外部时钟信号直接输入时才置1。普通开发板不碰它。bit19 CSSON时钟安全系统开关打开后一旦HSE失效会自动切回HSI。bit24 PLLONPLL开关。bit25 PLLRDYPLL就绪标志。这个寄存器没什么花活就是“给位、置位、查标志”。打开时钟后你不能马上就去用必须等对应的RDY标志变成1因为晶振起振和PLL锁定都是要花时间的。很多初学者不查就绪位结果系统启动时有时正常有时跑飞那就是时序上出了问题。3.2 CFGR配置寄存器PLL选谁、倍频多少、总线分频怎么填CFGR寄存器在偏移0x04处负责的是“怎么算”这件事。其中你迟早要用的位段有bit1:0 SW系统时钟切换00选HSI01选HSE10选PLL。bit3:2 SWS系统时钟状态回读告诉你当前实际跑在哪个时钟上。bit7:4 HPREAHB预分频0000表示不分频。bit10:8 PPRE1APB1预分频100表示/2101是/4110是/8111是/16。bit13:11 PPRE2APB2预分频000表示不分频。bit16 PLLSRCPLL输入源选择0选HSI/21选HSE。bit21:18 PLLMULLPLL倍频系数比如0111表示×9。CFGR这组位段几乎可以看作一路“组合逻辑”。你告诉芯片PLL吃的是HSE倍9倍APB1分2APB2不分频。芯片就按这套参数把时钟算出来。这里有个细节值得注意SW、PLLMULL这些多bit字段修改前最好先清掉旧值。如果原值是别的倍频你直接按位或上一个新值可能会残留旧位配置就乱了。正确做法是先用“ ~(mask)”把这一整段清零再写入新值。3.3 APB1ENR与APB2ENR各个外设的“电源开关”如果说CR和CFGR决定系统时钟怎么产生那APB1ENR和APB2ENR就决定这个时钟分给谁。它们的偏移地址是固定的APB2ENR偏移0x18主管APB2总线上的外设时钟GPIO、USART1、ADC、TIM1都在这里。APB1ENR偏移0x1C主管APB1总线上的外设时钟USART2/3、TIM2~7、I2C、SPI2/3、CAN都在这里。使用方式极其简单把对应位置1即可。比如RCC-APB2ENR | RCC_APB2ENR_IOPCEN; // 使能GPIOC时钟 RCC-APB1ENR | RCC_APB1ENR_USART2EN; // 使能USART2时钟别小看这一步外设不工作、寄存器写了没反应十有八九就是漏了它。3.4 常用外设时钟使能位速查表以F103为例把最常见的外设和它对应的使能位整理出来。这张表我建议你存起来写代码时随时翻外设所在总线使能寄存器使能位GPIOAAPB2APB2ENRbit2GPIOBAPB2APB2ENRbit3GPIOCAPB2APB2ENRbit4USART1APB2APB2ENRbit14TIM1APB2APB2ENRbit11ADC1APB2APB2ENRbit9TIM2APB1APB1ENRbit0TIM3APB1APB1ENRbit1USART2APB1APB1ENRbit17USART3APB1APB1ENRbit18I2C1APB1APB1ENRbit21SPI2APB1APB1ENRbit14不同型号的STM32GPIO挂的总线可能不一样。比如F4系列GPIO的时钟使能位在AHB1ENR里F1系列则放在APB2ENR。写代码之前先确认自己用的是哪个系列别把F4的例子往F1上硬套。4. 从0写一个时钟初始化8MHz晶振到72MHz全流程4.1 先开HSE再等它“就绪”正式的时钟初始化第一步不是配PLL而是先把HSE打开、确认它起振。晶振上电后需要一点时间稳定输出所以必须查HSERDY标志。// 打开外部高速晶振 RCC-CR | RCC_CR_HSEON; // 等待HSE就绪超时保护可以加上这里先写最简形式 while ((RCC-CR RCC_CR_HSERDY) 0) { }这个循环看起来简单很多人不理解“为什么要等”。你可以这样想刚给晶振通电它不可能瞬间输出稳定波形就跟开灯后灯管要闪几下才稳定一样。HSERDY位就是硬件给你的“灯已亮稳”信号。4.2 配置PLL和总线分频关键一步必须算清楚HSE稳定之后就可以配置PLL和总线分频了。这一步的“算法”就是前文的链路图8MHz HSE→PLL×9→72MHz系统时钟→AHB不分频→APB2不分频→APB1分2。对应到CFGR寄存器就是四件事选PLL输入源为HSE、倍频系数设为9、APB1分频为2、APB2不分频。// 配置PLL输入源为HSE RCC-CFGR | RCC_CFGR_PLLSRC; // 配置PLL倍频为98MHz × 9 72MHz RCC-CFGR ~RCC_CFGR_PLLMULL; // 清空倍频字段 RCC-CFGR | RCC_CFGR_PLLMULL9; // PLLMULL 0111对应×9 // 配置APB1分频为2APB2不分频 RCC-CFGR | RCC_CFGR_PPRE1_DIV2; // APB1 72MHz / 2 36MHz RCC-CFGR ~RCC_CFGR_PPRE2; // APB2不分频保持72MHz为什么APB1要分频因为F103的APB1外设最高只能承受36MHz。如果你把APB1也配成72MHz串口和定时器可能表现异常比如波特率换算出来的分频值不对或者通信偶发错误。这是很多人配完时钟后遇到“怪问题”的根源之一。4.3 FLASH等待周期最容易被忽略的“隐藏关卡”配置完总线分频还不能急着切系统时钟。有一个坑几乎所有初学者都会踩系统时钟提高到72MHz后Flash读取速度跟不上CPU访问速度程序会随机跑飞、进硬件错误。解决方法是配置Flash接口的等待周期。F103的Flash接口寄存器地址是0x40022000ACR寄存器偏移0x00其中bit2:0表示等待周期数。72MHz主频下需要设置2个等待周期。FLASH-ACR 0x32; // 等待周期2bit2:0 010同时开启预取缓冲bit4 1为什么是2而不是3这是芯片手册给出的硬性规定主频在48MHz到72MHz范围内Flash等待周期必须为2。低于48MHz时可以是1更高频率才需要更多。这条如果你查手册会发现写得很清楚但很多示例代码不解释导致学习者只抄了PLL配置、漏掉了Flash配置。4.4 打开PLL、等待锁定、切换系统时钟万事俱备最后一步就是打开PLL等PLLRDY标志然后把系统时钟切换到PLL输出。这三步不能省顺序也不能乱。// 打开PLL RCC-CR | RCC_CR_PLLON; // 等待PLL锁定 while ((RCC-CR RCC_CR_PLLRDY) 0) { } // 切换系统时钟到PLL RCC-CFGR | RCC_CFGR_SW_PLL; // 确认切换完成SWS位会回读当前系统时钟状态 while ((RCC-CFGR RCC_CFGR_SWS) ! RCC_CFGR_SWS_PLL) { }为什么要读SWS因为SW只是写“我想切到PLL”切换动作本身是硬件完成的需要时间。你不等SWS变成PLL状态就去操作外设系统时钟可能还悬在HSI上配置用了72MHz的参数实际却是8MHz的节奏外设算出来的分频值全错。调试器里看到串口乱码往往就是这个原因。到这里时钟初始化函数完整版如下void Clock_Init_To_72MHz(void) { // 1. 打开HSE并等待就绪 RCC-CR | RCC_CR_HSEON; while ((RCC-CR RCC_CR_HSERDY) 0) { } // 2. 配置Flash等待周期为2开启预取缓冲 FLASH-ACR 0x32; // 3. 配置PLLHSE 8MHz × 9 72MHz RCC-CFGR | RCC_CFGR_PLLSRC; // PLL输入 HSE RCC-CFGR ~RCC_CFGR_PLLMULL; // 清倍频字段 RCC-CFGR | RCC_CFGR_PLLMULL9; // ×9 // 4. 配置总线分频APB1 /2APB2不分频 RCC-CFGR | RCC_CFGR_PPRE1_DIV2; RCC-CFGR ~RCC_CFGR_PPRE2; // 5. 打开PLL并等待锁定 RCC-CR | RCC_CR_PLLON; while ((RCC-CR RCC_CR_PLLRDY) 0) { } // 6. 切换到PLL时钟 RCC-CFGR | RCC_CFGR_SW_PLL; while ((RCC-CFGR RCC_CFGR_SWS) ! RCC_CFGR_SWS_PLL) { } }你把这个函数放在main函数最开始调用系统就跑在72MHz上了。5. 点亮PC13让时钟配合GPIO完成首次实战5.1 外设的第一步永远是“上电”时钟初始化完成之后接下来用一个最经典的动作来验证点亮核心板上的LED。大多数核心板把LED接在PC13这颗引脚上而且通常是低电平点亮。要操作GPIOC第一件事还是那句老话先开GPIOC时钟。// 使能GPIOC的时钟 RCC-APB2ENR | RCC_APB2ENR_IOPCEN; // 相当于 (1 4)这一句你能真正看懂了APB2ENR的bit4是GPIOC的时钟开关。很多初学朋友把GPIO初始化代码从头到尾背下来却不知道第一句到底在干嘛现在应该清楚了。5.2 把PC13配成推挽输出GPIO端口本身有一堆控制寄存器。F103的GPIOC基地址是0x40011000PC13属于高8位引脚配置寄存器要用CRH而不是CRL。PC13对应的4位是CRH的bit23:20。在这4位里低2位是MODE输出模式/速度高2位是CNF输出配置。写成代码// 清空PC13相关4位MODE13和CNF13都归零 GPIOC-CRH ~(0x0F 20); // MODE13 01输出模式最大速度10MHz // CNF13 00通用推挽输出 GPIOC-CRH | (0x01 20);为什么用10MHz而不是50MHz驱动LED只需要很低的翻转速度输出速度设太高会增加EMI和功耗。能用低的就用低的这是实践中总结出来的经验不是越高速越好。5.3 拉低电平让LED真正亮起来PC13板载LED一般低电平点亮所以要把ODR寄存器bit13清0GPIOC-ODR ~(1U 13); // OD13 0输出低电平全流程代码连起来int main(void) { Clock_Init_To_72MHz(); // 使能GPIOC时钟 RCC-APB2ENR | RCC_APB2ENR_IOPCEN; // 配置PC13为推挽输出 GPIOC-CRH ~(0x0F 20); GPIOC-CRH | (0x01 20); // 输出低电平点亮LED GPIOC-ODR ~(1U 13); while (1) { } }到这里你就拥有了一套“时钟初始化 GPIO操作”的完整寄存器代码。这段代码虽然短但它走完了整个链路时钟源选择、倍频、总线分频、外设时钟使能、引脚模式、电平输出。后面写串口、定时器思路是一模一样的。6. 时钟路上最容易翻车的四个坑现象、根因与排查6.1 现象一代码复制了一遍LED就是不亮最经典的问题。很多人拿到例程改了GPIO引脚号下载后没反应就开始怀疑GPIO配置。其实问题往往在时钟使能上可能漏写了GPIOCEN或者使能的是GPIOA实际用的却是GPIOC。排查方法很简单打开调试器看RCC-APB2ENR的值确认对应引脚所在端口的使能位是否已经置1。没置1其他一切都是白搭。6.2 现象二改了PLL倍频之后程序直接跑飞或进硬件错误常见原因有两个。第一个是倍频结果超过了芯片上限比如用16MHz晶振配×9得到144MHzF103根本跑不到。第二个是系统时钟提高到72MHz后Flash等待周期没跟着调CPU取指速度跟不上。第二个原因尤其隐蔽因为代码本身没有任何错误程序却时好时坏看起来像“玄学问题”。遇到这种第一反应就去检查FLASH-ACR的等待周期是不是2。这也是我把Flash配置放在时钟初始化里的原因这一环不能省。6.3 现象三串口输出乱码波特率明明没配错串口乱码往往是APB分频和波特率分频的联动问题。如果你把APB1配置成了72MHz而不是36MHzUSART2/3的时钟频率就变成了两倍但波特率寄存器算出来的分频值是按36MHz算的最终发出去的波特率就偏了。还有一种情况是用了HSI做主时钟。HSI精度一般温度一高频率就漂串口波特率也会跟着偏。所以只要板上有晶振就尽量用HSE通信稳定不是玄学而是时钟源本身要准。6.4 怎么判断当前系统时钟到底是多少有时候你怀疑系统时钟没切过去又懒得用示波器测MCO引脚那就直接读CFGR的SWS字段。// 读SWS字段判断当前系统时钟来源 uint32_t sws (RCC-CFGR RCC_CFGR_SWS) 2; // 0HSI 1HSE 2PLL如果SWS显示0说明系统还在HSI上跑。这时候就去检查PLLON有没有置1PLLRDY有没有变1SW位有没有写对。按这个顺序排查几分钟就能定位问题。在实际调试中我还习惯在初始化函数里临时加上一个“超时保护”防止外部晶振损坏或者接线虚焊时函数死循环卡死。做法很简单设一个计数变量循环超过一定次数就强制跳到HSI模式继续运行。生产环境不一定需要但调试阶段能帮你快速判断是不是晶振没起振而不是对着一块黑屏的板子发呆。时钟这部分内容就这些了但它的影响会贯穿整本书。下一篇开始我们就可以放心地拿着这套时钟配置去摆弄GPIO的高电平、低电平、按键输入、推挽开漏这些真正“看得见”的操作了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑