首页 / 资讯中心 / 文章详情

STM32 RCC时钟配置详解:从寄存器到72MHz实战

STM32 RCC时钟配置详解:从寄存器到72MHz实战 ★ FEATURED ARTICLE
1. 从点灯到控时为什么第三章必须啃下RCC这块硬骨头很多人学STM32是从GPIO点灯开始的第一章摸清楚寄存器怎么读写第二章搞明白GPIO的CRL、CRH、ODR、IDR这几个寄存器怎么摆弄到了第三章突然就卡住了。卡在哪卡在时钟上。你写了一段代码配置了GPIOA的寄存器编译下载灯不亮。你反复检查位操作没错啊。然后你翻手册发现GPIOA的时钟压根没开。这就是第三章要解决的问题——RCCReset and Clock Control寄存器组。它不控制任何一个引脚的高低电平但它决定了你写的每一行寄存器代码到底有没有电去执行。我见过太多人在这章翻车。有人直接把HAL库的SystemInit()抄过来能用但问他为什么这么配说不出来。有人照着别人的工程改改完发现串口波特率不对查了半天发现是APB1和APB2的分频系数搞混了。还有人更惨低功耗项目里忘了关某个外设时钟电池三天就没电了。这一章的核心就一件事搞清楚STM32的时钟树是怎么长的以及RCC那一堆寄存器里每一位到底在管什么。你不需要背下所有寄存器的所有位但你必须知道遇到外设不工作的时候该去RCC的哪个寄存器里找答案。提示这一章的内容和第一章、第二章是递进关系。第一章讲的是怎么读写寄存器第二章讲的是GPIO寄存器怎么用第三章讲的是怎么让外设真正跑起来。如果你前两章还没看建议先回去补一下不然这章会有点吃力。STM32的时钟系统你可以把它想象成一个小区的供电网络。RCC就是配电房的总控面板HSI、HSE、PLL这些是不同来源的电AHB、APB1、APB2是不同电压等级的供电线路每个外设就是小区里的一户人家。你要让某户人家用上电得先在总控面板上把对应的开关合上然后确保供电线路的电压等级匹配。这个类比虽然不完美但足够帮你理解RCC的核心逻辑。2. RCC寄存器全景哪些位在管你的外设2.1 RCC_CR时钟源的总开关RCC_CRClock Control Register是整个时钟系统的起点。这个寄存器里最重要的几位是HSIONbit 0内部高速时钟使能。复位后默认是1也就是说HSI是默认开着的。HSI的频率在F1系列里是8MHz在F4系列里是16MHz。它的优点是启动快、不需要外部晶振缺点是精度差温漂大不适合做串口通信或者USB。HSEONbit 16外部高速时钟使能。这个要你自己置1。置1之后不能马上用得等HSERDYbit 17变成1说明外部晶振起振稳定了。PLLONbit 24PLL使能。PLL是把低频时钟倍频到高频的电路STM32的主频通常都是通过PLL从HSE或HSI倍频上来的。CSSONbit 19时钟安全系统使能。这个功能是当HSE失效时自动切换到HSI防止系统死机。工业项目里建议开消费类项目看情况。我个人的习惯是在写RCC初始化代码之前先把RCC_CR的复位值查清楚。F1系列复位值是0x0000 0083也就是说HSI和HSI的修调值默认是开的。F4系列复位值是0x0000 0083但HSI频率是16MHz。这个细节很多人忽略导致算PLL参数的时候用错了输入频率。2.2 RCC_CFGR时钟树的路由表RCC_CFGRClock Configuration Register是RCC里最复杂的一个寄存器它决定了时钟信号从哪来、到哪去、分频多少。关键位段包括SW[1:0]bit 1:0系统时钟源选择。00HSI01HSE10PLL。切换之后要等SWS[1:0]变成对应的值才说明切换完成。HPRE[3:0]bit 7:4AHB预分频器。AHB是高速总线GPIO、DMA、SRAM都挂在这上面。通常不分频保持和系统时钟一样。PPRE1[2:0]bit 10:8APB1预分频器。APB1是低速总线挂的是USART2/3、I2C、SPI2、TIM2-7这些外设。F1系列APB1最高36MHzF4系列最高42MHz。PPRE2[2:0]bit 13:11APB2预分频器。APB2是高速总线挂的是GPIO、USART1、SPI1、ADC、TIM1这些。APB2通常和AHB同频。PLLSRCbit 16PLL输入源选择。0HSI/21HSE。注意F1系列里HSI进PLL之前会先除以2这个坑很多人踩过。PLLXTPREbit 17HSE进PLL之前是否分频。0不分频1二分频。如果你的HSE是8MHz想得到72MHz通常不分频然后PLLMUL设为9。PLLMUL[3:0]bit 21:18PLL倍频系数。F1系列支持2到16倍频但要注意PLL输出频率不能超过芯片的最高主频。这里有一个非常容易搞混的点APB1和APB2的时钟频率不仅影响外设的工作速度还影响定时器的时钟频率。如果APB的分频系数是1定时器时钟就是APB时钟如果分频系数大于1定时器时钟是APB时钟的2倍。这个规则在F1和F4系列里都适用但很多人算波特率的时候忘了这一茬导致串口通信乱码。2.3 RCC_APB1ENR和RCC_APB2ENR外设时钟的分闸这两个寄存器是日常开发中读写最频繁的。每一位对应一个外设的时钟使能。比如RCC_APB2ENR的bit 2GPIOA时钟使能RCC_APB2ENR的bit 3GPIOB时钟使能RCC_APB2ENR的bit 14USART1时钟使能RCC_APB1ENR的bit 17USART2时钟使能RCC_APB1ENR的bit 21I2C1时钟使能写这些位的时候我强烈建议用位带操作或者|的方式不要直接赋值。比如你要开GPIOA和USART1的时钟应该写RCC-APB2ENR | (1 2) | (1 14);而不是RCC-APB2ENR (1 2) | (1 14); // 危险会关掉其他已使能的外设直接赋值会把其他位清零导致之前开好的外设时钟被关掉。这个坑我在早期项目中踩过现象是明明初始化了SPI但就是没输出查了一下午才发现是后面某处直接赋值把SPI时钟关了。2.4 RCC_BDCR和RCC_CSR后备区域和低功耗相关RCC_BDCRBackup Domain Control Register管的是RTC和后备寄存器的时钟源。如果你要用RTC做日历就得在这里选择LSE外部低速晶振或者LSI内部低速时钟。LSE的精度比LSI高得多但需要外接32.768kHz晶振。RCC_CSRControl/Status Register里有LSION、LSIRDY这些位还有复位标志位可以用来判断上次复位的原因。这两个寄存器在普通项目里用得少但一旦涉及低功耗或者RTC就绕不开。我建议你在学第三章的时候至少把这两个寄存器的位定义看一遍不用记住但要知道有这么回事以后遇到了知道去哪查。3. 时钟树配置实战从8MHz晶振到72MHz主频的完整推导3.1 先搞清楚你的芯片到底能跑多快在动手配置之前你必须确认两件事你的外部晶振频率是多少以及你的芯片最高主频是多少。这两个参数决定了PLL的倍频系数怎么选。以STM32F103C8T6为例外部晶振通常是8MHz最高主频是72MHz。那么PLL的倍频系数就是72/89。但这里有个细节F1系列的PLL输入如果选HSE是不经过二分频的PLLXTPRE0所以PLL输入就是8MHz倍频9倍得到72MHz。如果你用的是STM32F407外部晶振还是8MHz但最高主频是168MHz。这时候PLL的配置就复杂多了因为F4系列的PLL有M、N、P、Q四个参数。通常的配置是M8N336P2这样PLL输出就是8/8*336/2168MHz。这个计算过程在F4的参考手册里有详细说明但很多人第一次看的时候会被绕晕。我个人的经验是先把目标主频写下来然后倒推PLL参数。不要正着算正着算容易选错组合。比如你要168MHz就先确定PLL输入是1MHz8MHz除以M8然后N336得到336MHz的VCO输出最后P2得到168MHz。这样每一步都有明确的物理意义。3.2 分频系数的选择逻辑系统时钟确定之后接下来要配AHB、APB1、APB2的分频系数。这里的原则是AHB通常不分频保持和系统时钟一样。因为AHB挂的是内存和DMA速度越快越好。APB2通常也不分频和AHB同频。APB2挂的是GPIO、ADC、USART1这些高速外设。APB1这个要看芯片规格。F1系列APB1最高36MHz所以如果系统时钟是72MHzAPB1必须二分频。F4系列APB1最高42MHz如果系统时钟是168MHzAPB1至少四分频。这里有一个很多人忽略的细节APB1的分频系数会影响挂载在APB1上的定时器时钟。比如TIM2挂在APB1上如果APB1分频系数是2那么TIM2的时钟是APB1时钟的2倍也就是和系统时钟一样。这个规则在计算定时器预分频值的时候非常关键。我见过一个案例有人用TIM2做1ms定时中断系统时钟72MHzAPB1二分频得到36MHz他以为TIM2的时钟就是36MHz于是预分频值设为36000-1结果实际中断周期是0.5ms。原因就是TIM2的时钟实际上是72MHz不是36MHz。这个坑在F1和F4系列里都存在F7和H7系列略有不同但逻辑类似。3.3 完整的RCC初始化代码拆解下面是一段基于寄存器操作的RCC初始化代码以F103为例目标是从8MHz HSE得到72MHz系统时钟// 1. 使能HSE RCC-CR | (1 16); // HSEON 1 while (!(RCC-CR (1 17))); // 等待HSERDY 1 // 2. 配置Flash等待周期 // 72MHz需要2个等待周期这个在Flash_ACR寄存器里配 FLASH-ACR | (1 4) | (1 3); // LATENCY 2 // 3. 配置PLLHSE不分频9倍频 RCC-CFGR | (1 16); // PLLSRC 1选HSE RCC-CFGR ~(1 17); // PLLXTPRE 0不分频 RCC-CFGR | (9 18); // PLLMUL 9 // 4. 使能PLL RCC-CR | (1 24); // PLLON 1 while (!(RCC-CR (1 25))); // 等待PLLRDY 1 // 5. 配置AHB、APB1、APB2分频 RCC-CFGR ~(0xF 4); // HPRE 0AHB不分频 RCC-CFGR | (4 8); // PPRE1 4APB1二分频 RCC-CFGR ~(0x7 11); // PPRE2 0APB2不分频 // 6. 切换系统时钟到PLL RCC-CFGR | (2 0); // SW 2选PLL while ((RCC-CFGR (0x3 2)) ! (2 2)); // 等待SWS 2这段代码里每一步都有明确的意图。第2步配置Flash等待周期经常被忽略但如果不配72MHz下取指会出错现象是程序跑飞或者HardFault。第5步的PPRE1设为4对应的是二分频因为PPRE1的编码规则是0xxx不分频100二分频101四分频以此类推。注意F1系列的PPRE1和PPRE2编码规则和F4系列不同。F4系列的PPRE1是3位编码0xx不分频100二分频101四分频110八分频111十六分频。写代码之前一定要查对应型号的参考手册不要凭记忆写。4. 那些年我在RCC上踩过的坑4.1 外设时钟忘了开查错查半天这是最经典的坑没有之一。你写好了USART的初始化代码波特率、数据位、停止位都配对了但串口就是没输出。你查了引脚配置查了中断优先级查了DMA都没问题。最后发现是RCC_APB2ENR里USART1的时钟使能位没置1。这个坑之所以经典是因为它的现象和很多其他问题相似容易让人往错误的方向排查。我的建议是在写任何外设初始化代码之前先把对应的RCC时钟使能写上。哪怕你还没开始配外设寄存器先把时钟开了这样至少排除了一个变量。另外STM32CubeMX生成的代码里时钟使能是自动生成的所以用CubeMX的人反而不容易踩这个坑。但一旦你脱离CubeMX手写代码这个坑就很容易冒出来。4.2 APB1和APB2搞混外设挂错总线STM32的外设不是随便挂的每个外设固定挂在某条总线上。比如USART1挂APB2USART2挂APB1。如果你在代码里把USART2的时钟使能写到了RCC_APB2ENR里那USART2永远不会工作。这个问题的排查方法是查参考手册的外设总线映射章节。每个型号的参考手册里都有一张表明确列出每个外设挂在哪条总线上。我建议你把常用外设的总线归属记下来比如外设总线时钟使能寄存器GPIOA-GPIOEAPB2RCC_APB2ENRUSART1APB2RCC_APB2ENRUSART2/3APB1RCC_APB1ENRSPI1APB2RCC_APB2ENRSPI2APB1RCC_APB1ENRI2C1/2APB1RCC_APB1ENRTIM1APB2RCC_APB2ENRTIM2-7APB1RCC_APB1ENRADC1/2APB2RCC_APB2ENR这张表在F1系列里基本通用F4系列略有调整但大框架一致。4.3 PLL配置后忘记切换系统时钟这个坑的现象是你配置了PLL使能了PLL等待PLLRDY置位但系统时钟还是HSI。你测了一下主频发现还是8MHz不是72MHz。原因是你忘了写最后一步——把RCC_CFGR的SW位段设为10切换到PLL。这个坑的隐蔽性在于PLL确实在运行PLLRDY也确实置位了但系统时钟并没有自动切换到PLL。你必须显式地写SW位段然后等待SWS位段变成对应的值。我建议在切换之后加一个超时判断防止PLL一直不稳定导致死循环uint32_t timeout 0xFFFF; RCC-CFGR | (2 0); while (((RCC-CFGR (0x3 2)) ! (2 2)) (timeout--)); if (timeout 0) { // 切换失败做错误处理 }4.4 低功耗项目里忘了关外设时钟这个坑在电池供电的项目里特别致命。你初始化了一堆外设跑完初始化之后进入低功耗模式但忘了关掉那些不再使用的外设时钟。结果就是电流降不下来电池寿命远低于预期。STM32的每个外设时钟使能位都可以单独关闭。在进入低功耗之前把不用的外设时钟全部关掉可以显著降低功耗。我做过一个测试F103在72MHz下运行所有外设时钟都开着电流大约是30mA关掉所有不用的外设时钟电流降到10mA左右再进入Stop模式电流降到几十微安。提示关外设时钟的时候要注意有些外设关闭之后会丢失配置下次使用需要重新初始化。比如USART关闭时钟后波特率寄存器会复位。所以关时钟之前要确认这个外设确实不再使用了。5. 从寄存器到实际项目RCC配置的工程化思路5.1 把时钟配置封装成独立函数在实际项目中我从来不把RCC配置散落在各个外设初始化函数里。我的做法是写一个SystemClock_Init()函数把所有时钟相关的配置集中在一起。这个函数在main()的最开始调用之后所有外设初始化都默认时钟已经配好了。这样做的好处是时钟配置只写一次改的时候只改一个地方。如果你的项目从F103换到F407只需要改SystemClock_Init()里的PLL参数和分频系数外设初始化代码基本不用动。这个函数的典型结构是void SystemClock_Init(void) { // 1. 使能HSE // 2. 配置Flash等待周期 // 3. 配置PLL // 4. 使能PLL并等待稳定 // 5. 配置AHB/APB分频 // 6. 切换系统时钟到PLL // 7. 更新SystemCoreClock变量 }第7步的SystemCoreClock变量是CMSIS定义的一个全局变量很多库函数依赖它来计算延时。如果你不更新它SysTick的延时函数会不准。更新方法是调用SystemCoreClockUpdate()这个函数会根据RCC寄存器的当前值自动计算系统时钟频率。5.2 用宏定义管理时钟参数为了让代码更容易移植我习惯把时钟参数用宏定义写在头文件里#define HSE_VALUE 8000000U #define HSI_VALUE 8000000U #define PLL_M 8 #define PLL_N 336 #define PLL_P 2 #define PLL_Q 7 #define AHB_PRESCALER 1 #define APB1_PRESCALER 4 #define APB2_PRESCALER 2这样换芯片的时候只需要改这几个宏不用去翻RCC寄存器的位定义。这个做法在F4和F7系列里特别有用因为这两个系列的PLL参数多手算容易出错。5.3 时钟配置的验证方法配好时钟之后怎么验证配置是否正确我常用的方法有三种第一种用MCO引脚输出时钟。STM32有一个MCOMicrocontroller Clock Output功能可以把系统时钟、PLL输出、HSE等时钟信号从PA8引脚输出。你用示波器或者频率计测一下PA8的频率就知道时钟配得对不对。配置MCO的方法是设置RCC_CFGR的MCO位段比如设为100就是输出系统时钟。第二种用SysTick测延时。配一个1ms的SysTick中断然后在主循环里翻转一个GPIO用示波器测GPIO的翻转周期。如果周期是2ms说明SysTick配置正确间接说明系统时钟正确。第三种用串口输出验证。配好USART之后用固定的波特率发送一个已知字符串看PC端接收是否正常。如果波特率不对接收到的就是乱码。这个方法最直接但前提是USART的时钟配置也要正确。我通常三种方法都用一遍互相印证。特别是MCO方法虽然需要示波器但最直观一眼就能看出频率对不对。5.4 不同系列的RCC差异对比STM32的RCC寄存器在不同系列之间有差异但核心逻辑一致。下面这张表列出了几个常见系列的主要差异特性F1系列F4系列F7系列H7系列最高主频72MHz168MHz216MHz480MHzHSI频率8MHz16MHz16MHz64MHzPLL参数倍频系数M/N/P/QM/N/P/QM/N/P/QAPB1最高36MHz42MHz54MHz120MHzAPB2最高72MHz84MHz108MHz120MHz时钟树复杂度简单中等中等复杂从F1到H7时钟树越来越复杂但基本概念是一样的选时钟源、配PLL、设分频、切系统时钟。你只要把F1的时钟树吃透了后面换系列的时候只需要查一下新系列的PLL参数计算方法和分频系数编码规则就能快速上手。6. 关于RCC寄存器操作的一些个人习惯写RCC寄存器的时候我有几个坚持了很多年的习惯分享出来供你参考。第一个习惯读-改-写永远不用直接赋值。前面已经说过直接赋值会清掉其他位。我见过有人写RCC-APB2ENR 0x00000004;来开GPIOA时钟结果把之前开的USART1时钟关掉了。正确的做法是RCC-APB2ENR | 0x00000004;。第二个习惯等待标志位的时候加超时。while (!(RCC-CR (1 17)));这种写法在实验室里没问题但在实际产品里如果晶振坏了程序就死在这里了。加一个超时计数器超时之后做错误处理比如切换到HSI或者点亮错误指示灯。第三个习惯配置完时钟之后立刻更新SystemCoreClock。这个变量影响HAL_Delay()、SysTick_Config()等函数的精度。我见过有人配了168MHz的主频但SystemCoreClock还是默认的16MHz结果HAL_Delay(1000)实际延时了10秒多。第四个习惯把RCC配置和Flash等待周期配置放在一起。Flash等待周期和系统时钟频率是绑定的主频越高等待周期越多。F1系列72MHz需要2个等待周期F4系列168MHz需要5个等待周期。这两个配置分开写容易漏放在一起就不会忘。第五个习惯低功耗项目里在进入低功耗之前遍历所有RCC使能寄存器关掉不用的位。这个操作我通常写成一个函数RCC_DisableUnusedClocks()在main()进入低功耗循环之前调用一次。这些习惯看起来都是小事但在实际项目中每一个都帮我省过至少半天的时间。特别是超时处理那一条在工业现场晶振失效是真实会发生的事情没有超时处理的代码在现场就是一颗定时炸弹。7. 从第三章往后RCC只是开始RCC这一章的内容说到底就是解决外设为什么不工作这个问题的第一步。你把时钟配对了外设才有资格谈初始化。但时钟配对了不代表外设就能工作后面还有GPIO的复用功能配置、外设自身的工作模式配置、中断配置等等。我建议你在学完这一章之后做一个小练习用寄存器操作的方式配置一个定时器中断每500ms翻转一次LED。这个练习会用到RCC的APB1时钟使能、TIM的预分频和自动重装载寄存器、NVIC的中断使能。如果你能独立完成这个练习说明你对RCC的理解已经到位了。另外如果你用的是F4或者F7系列我建议你花点时间研究一下CubeMX生成的SystemClock_Config()函数。这个函数是CubeMX根据你的时钟树配置自动生成的里面的代码结构很清晰可以作为你手写RCC配置的参考模板。但不要直接抄要理解每一行在做什么。最后说一个我自己的教训我曾经在一个项目里为了省事直接把另一个项目的RCC配置复制过来结果那个项目用的是8MHz晶振新项目用的是12MHz晶振PLL倍频系数没改系统时钟跑到了108MHz超过了F103的72MHz上限。芯片虽然没烧但工作不稳定偶尔死机。查了两天才发现是晶振频率不匹配。从那以后我每次新建工程第一件事就是确认晶振频率第二件事就是重新计算PLL参数绝不复制粘贴。
阅读完成 · 觉得有帮助?
咨询建站