
1. 为什么第三章要死磕RCC和APB翻到第三章很多人第一反应是终于要讲外设了结果一看标题是RCC和APB立刻泄气。我特别理解这种心情——寄存器手册里最枯燥的就是时钟树一堆分频系数、使能位、就绪标志看着就头疼。但恰恰是这一章决定了你后面所有外设能不能跑起来。我见过太多人卡在串口没输出、ADC采不到值、定时器不计数排查半天最后发现是RCC里某个使能位没置1或者APB分频算错了。STM32的时钟系统本质上就是整个芯片的供电调度中心。内核、总线、外设每一路都有自己的时钟开关和频率来源。你写寄存器操作外设第一步永远是开时钟第二步才是配寄存器。这个顺序不能反反了就是写进去没反应。第三章讲的就是这套调度规则RCC寄存器怎么控制时钟源切换、PLL怎么倍频、AHB和APB怎么分频、外设时钟怎么单独使能。关键词里提到的RCC、时钟系统、APB总线这三个东西是绑在一起的。RCC是寄存器组时钟系统是它管理的对象APB是它分频后输出给外设的总线之一。你把这三个搞清楚了后面GPIO、USART、TIM、ADC的时钟配置就是套公式。搞不清楚每次都要翻手册现查效率极低。这一章我打算按实际操作的顺序来讲先搞清楚时钟从哪来再搞清楚怎么倍频到系统频率然后看AHB和APB怎么分频最后落到具体外设的时钟使能。每一步都给出寄存器级别的操作和实际验证方法不搞纯理论堆砌。2. 时钟源选择HSI、HSE、LSI、LSE到底用哪个2.1 四个时钟源的物理来源和精度差异STM32的时钟源有四个HSI、HSE、LSI、LSE。名字里的H和L代表高速和低速S代表内部E代表外部。合起来就是高速内部RC、高速外部晶振、低速内部RC、低速外部晶振。HSI是芯片内部自带的RC振荡器F1系列典型频率8MHzF4系列16MHz。它的优点是上电就能用不需要外部元件缺点是精度差温漂大手册里给的精度通常是±1%左右。你拿它当系统时钟跑串口波特率误差可能到2%以上高速通信就会丢包。HSE是外部晶振通常接8MHz或25MHz的无源晶振。精度取决于晶振本身普通晶振±20ppm好一点的±10ppm。它的起振需要时间通常几百微秒到几毫秒所以代码里要等HSERDY标志置位才能切换。LSI是内部低速RC典型频率40kHz主要给独立看门狗和RTC用。精度很差手册标称范围是30kHz到60kHz所以RTC用LSI走时一天可能差几分钟。LSE是外部32.768kHz晶振专门给RTC用。32.768kHz是2的15次方经过15级二分频正好得到1Hz这是RTC设计的经典方案。精度取决于晶振通常±20ppm走时一个月差几十秒。选哪个取决于你的应用场景。跑USB或者CAN必须用HSE因为协议对时钟精度有硬性要求。只跑个LED闪烁HSI完全够用。要用RTC做日历LSE是首选没有LSE就用LSI凑合但走时精度别指望太高。2.2 寄存器层面的时钟源切换流程以F1系列为例RCC_CR寄存器控制时钟源的开启和就绪状态。Bit0是HSION写1开启HSI。Bit1是HSIRDY硬件置1表示HSI稳定。Bit16是HSEON写1开启HSE。Bit17是HSERDY硬件置1表示HSE稳定。切换系统时钟用RCC_CFGR寄存器的SW位段Bit0和Bit1。写00选HSI写01选HSE写10选PLL。切换完成后SWS位段会反映当前实际使用的时钟源。代码里必须轮询SWS直到它等于你设置的SW值否则后续操作可能还在用旧时钟。// 开启HSE并等待就绪 RCC-CR | (1 16); // HSEON 1 while(!(RCC-CR (1 17))); // 等待HSERDY 1 // 切换系统时钟到HSE RCC-CFGR ~(3 0); // 清除SW位 RCC-CFGR | (1 0); // SW 01, 选择HSE while(((RCC-CFGR 2) 3) ! 1); // 等待SWS 01这段代码里有个细节清除SW位的时候用了~(3 0)3是二进制11左移0位还是11取反后低两位是00其他位是1。与操作会把低两位清零其他位保持不变。然后或上1低两位变成01。这是寄存器操作的常规套路先清后置避免影响其他位。等待SWS的循环里(RCC-CFGR 2) 3是把CFGR右移2位然后取低两位正好是SWS位段。判断它是否等于1等于1说明切换完成。这个轮询不能省我见过有人切换后直接配PLL结果PLL的输入还是HSI因为切换还没生效。2.3 HSE起振失败的排查思路HSE起振失败是新手最常遇到的问题之一。现象是代码卡在while(!(RCC-CR (1 17)))这个循环里出不来。原因通常有三个晶振没焊好、负载电容不匹配、启动时间不够。晶振没焊好包括虚焊、焊反、焊错封装。无源晶振没有极性但封装尺寸要对。负载电容通常选20pF左右具体值要查晶振手册。电容太大起振慢太小可能不起振。启动时间不够的情况少见但如果你用的是低频晶振或者高ESR晶振可能需要加长等待时间。排查方法先用示波器看晶振引脚有没有波形。没有波形说明根本没起振检查焊接和电容。有波形但幅度很小可能是负载电容不对。波形正常但HSERDY不置位可能是芯片坏了或者寄存器配置有问题。注意HSE起振失败时代码会死循环。产品代码里应该加超时机制超时后自动切回HSI并报错而不是无限等待。3. PLL倍频从8MHz到72MHz的完整计算3.1 PLL的工作原理和寄存器映射PLL是锁相环本质是一个反馈控制系统。它把输入频率乘以一个系数输出稳定的高频时钟。STM32的PLL输入可以选HSI/2或者HSE输出经过分频后给系统时钟。F1系列的PLL配置涉及RCC_CFGR寄存器的几个位段PLLSRC选择输入源PLLXTPRE决定HSE是否二分频PLLMUL设置倍频系数。PLLMUL是4位但编码不是线性的。0000到1111对应2到16倍频但中间有个坑0000是2倍频0001是3倍频以此类推1111是16倍频。但实际写的时候你要写的是倍频数减2。比如要8倍频写6。要9倍频写7。这个偏移量容易记错我建议直接查手册的编码表别自己算。PLL的输出频率公式是PLL输出 PLL输入 × PLLMUL。PLL输入如果是HSE且PLLXTPRE0就是HSE原频。如果PLLXTPRE1就是HSE/2。如果PLLSRC选HSIPLL输入固定是HSI/2。以常见的8MHz HSE为例要得到72MHz系统时钟PLLMUL要设9倍频。8MHz × 9 72MHz。PLLMUL写79-27。如果HSE是25MHz要得到72MHz25 × 9 225超了。这时候要么换晶振要么用HSE/2 12.5MHz12.5 × 6 75接近但不精确。所以F1系列跑72MHz8MHz晶振是最佳选择。3.2 系统时钟切换和Flash等待周期PLL配置好后不能直接切过去。还要设置Flash等待周期因为Flash的读取速度跟不上72MHz。F1系列在48MHz到72MHz之间需要2个等待周期24MHz到48MHz需要1个24MHz以下0个。等待周期在FLASH_ACR寄存器里设置Bit0到Bit2是LATENCY位段。写2表示2个等待周期。这个设置必须在切换系统时钟之前完成否则CPU取指会出错。// 设置Flash等待周期为2 FLASH-ACR ~(7 0); // 清除LATENCY FLASH-ACR | (2 0); // 2个等待周期 // 配置PLL: HSE输入, 9倍频 RCC-CFGR ~(1 16); // PLLSRC 0, 选HSE RCC-CFGR ~(1 17); // PLLXTPRE 0, HSE不分频 RCC-CFGR ~(0xF 18); // 清除PLLMUL RCC-CFGR | (7 18); // PLLMUL 7, 即9倍频 // 开启PLL RCC-CR | (1 24); // PLLON 1 while(!(RCC-CR (1 25))); // 等待PLLRDY // 切换系统时钟到PLL RCC-CFGR ~(3 0); RCC-CFGR | (2 0); // SW 10, 选PLL while(((RCC-CFGR 2) 3) ! 2); // 等待SWS 10这段代码的顺序很重要先设Flash等待周期再配PLL再开PLL最后切系统时钟。顺序错了可能跑飞。我试过先切系统时钟再设Flash等待周期结果程序直接HardFault因为CPU取指速度超过了Flash的响应速度。3.3 PLL配置的常见错误和验证方法最常见的错误是PLLMUL编码写错。比如要9倍频写了9而不是7结果变成11倍频8MHz × 11 88MHz超频了。芯片可能能跑但发热大长期运行不稳定。第二个错误是忘记等PLLRDY。PLL锁定需要时间通常几十微秒。不等就切系统时钟可能切过去的时候PLL还没稳定系统时钟会抖动。第三个错误是PLL输入频率超出范围。F1系列的PLL输入要求是4MHz到16MHz。如果你用8MHz HSEPLLXTPRE0输入8MHz没问题。如果用16MHz HSEPLLXTPRE0输入16MHz也没问题。但如果用25MHz HSEPLLXTPRE0输入25MHz超范围了PLL可能不锁定。这时候必须PLLXTPRE1输入12.5MHz。验证方法切换完成后用MCO引脚输出系统时钟用示波器测频率。MCO在PA8配置RCC_CFGR的MCO位段写100选系统时钟。或者用定时器测配一个定时器让它每秒中断一次翻转LED用秒表看准不准。4. AHB和APB分频外设频率的分配逻辑4.1 三条总线的层级关系和分频寄存器STM32的时钟树从系统时钟出来先经过AHB分频器得到HCLK。HCLK给内核、DMA、Flash、SRAM用。然后HCLK再经过APB1和APB2分频器得到PCLK1和PCLK2。PCLK1给低速外设PCLK2给高速外设。F1系列的AHB分频在RCC_CFGR的HPRE位段Bit4到Bit7。APB1分频在PPRE1Bit8到Bit10。APB2分频在PPRE2Bit11到Bit13。这些位段的编码也不是线性的0000是1分频0001到0011保留0100是2分频0101是4分频0110是8分频0111是16分频1000到1111是64到512分频。APB1的最大频率是36MHzAPB2的最大频率是72MHz。所以系统时钟72MHz时APB1必须至少2分频APB2可以1分频。如果你把APB1设成1分频PCLK1就是72MHz超频了外设可能工作不正常。// 配置AHB不分频, APB1二分频, APB2不分频 RCC-CFGR ~(0xF 4); // 清除HPRE RCC-CFGR | (0 4); // HPRE 0, AHB不分频 RCC-CFGR ~(7 8); // 清除PPRE1 RCC-CFGR | (4 8); // PPRE1 4, APB1二分频 RCC-CFGR ~(7 11); // 清除PPRE2 RCC-CFGR | (0 11); // PPRE2 0, APB2不分频这段配置的结果是HCLK 72MHzPCLK1 36MHzPCLK2 72MHz。这是F1系列跑72MHz时的标准配置。4.2 APB分频对外设时序的影响APB分频不只是影响外设的工作频率还影响定时器时钟。APB1上的定时器如果APB1分频系数是1定时器时钟就是PCLK1。如果分频系数大于1定时器时钟是PCLK1的2倍。这个规则很多人不知道。比如APB1二分频PCLK1 36MHz但APB1上的定时器时钟是72MHz。你配定时器的时候预分频值要按72MHz算不是36MHz。算错了定时周期就错了。串口波特率也受影响。USART1在APB2上PCLK2 72MHz。USART2在APB1上PCLK1 36MHz。配波特率的时候BRR寄存器的值要根据对应的PCLK算。USART1和USART2的BRR值不一样因为时钟源频率不同。提示配外设之前先确认它挂在哪条总线上然后查对应的PCLK频率。这个信息在参考手册的时钟树图里或者外设章节的开头都会写。4.3 外设时钟使能寄存器的操作细节RCC有三个外设时钟使能寄存器RCC_APB2ENR、RCC_APB1ENR、RCC_AHBENR。每个bit对应一个外设。写1使能写0关闭。APB2ENR的Bit2是GPIOABit3是GPIOBBit4是GPIOCBit14是USART1Bit0是AFIO。APB1ENR的Bit17是USART2Bit0是TIM2Bit1是TIM3。AHBENR的Bit0是DMA1Bit2是SRAM。操作的时候用位或赋值不要用直接赋值。比如RCC-APB2ENR | (1 2)使能GPIOA。如果写成RCC-APB2ENR (1 2)会把其他外设的时钟全关了可能导致程序跑飞。// 使能GPIOA和USART1时钟 RCC-APB2ENR | (1 2); // GPIOA RCC-APB2ENR | (1 14); // USART1 // 使能TIM2时钟 RCC-APB1ENR | (1 0); // TIM2使能时钟后最好加几个NOP或者读一次寄存器确保时钟稳定。虽然手册没说必须等但实际调试中遇到过刚使能就配寄存器导致第一次写失败的情况。加个短延时更稳妥。5. 用寄存器操作验证时钟配置的实操方法5.1 通过MCO引脚输出时钟波形MCO是微控制器时钟输出可以把内部时钟引到外部引脚上测。F1系列的MCO在PA8配置RCC_CFGR的MCO位段Bit24到Bit26。写100选系统时钟101选HSI110选HSE111选PLL/2。配置步骤先使能GPIOA和AFIO时钟把PA8配成复用推挽输出然后设MCO位段。用示波器测PA8的频率就能知道系统时钟对不对。// 使能GPIOA和AFIO RCC-APB2ENR | (1 2) | (1 0); // PA8配复用推挽输出, 50MHz GPIOA-CRH ~(0xF 0); GPIOA-CRH | (0xB 0); // MCO输出系统时钟 RCC-CFGR ~(7 24); RCC-CFGR | (4 24);这个方法最直接示波器一测就知道。没有示波器的话可以用逻辑分析仪或者用另一个STM32的定时器输入捕获来测。5.2 用SysTick做软件延时验证SysTick是内核自带的定时器时钟源是HCLK或者HCLK/8。用它可以做精确延时。如果系统时钟配对了SysTick延时1秒就是1秒。如果配错了延时就不准。配置SysTickLOAD寄存器写重装载值VAL写0清除当前值CTRL写7使能bit0使能bit1中断bit2时钟源选HCLK。// SysTick延时1ms (72MHz) SysTick-LOAD 72000 - 1; SysTick-VAL 0; SysTick-CTRL 7; void delay_ms(uint32_t ms) { for(uint32_t i 0; i ms; i) { while(!(SysTick-CTRL (1 16))); } }写个LED闪烁延时500ms用秒表看是不是1秒闪一次。如果明显偏快或偏慢说明系统时钟不对。5.3 串口输出验证波特率准确性串口是最方便的验证工具。配好USART1波特率115200发一串字符到电脑。如果时钟不对波特率就会偏电脑收到的就是乱码。USART1的BRR寄存器计算公式BRR PCLK2 / 波特率。72MHz / 115200 625。BRR写625即0x271。如果PCLK2不是72MHz比如是36MHzBRR应该是312.5取312实际波特率就是36MHz / 312 115384误差0.16%可以接受。但如果PCLK2是72MHz而你按36MHz算BRR写312实际波特率就是72MHz / 312 230769误差100%电脑收到的全是乱码。// USART1配置, PCLK272MHz, 波特率115200 USART1-BRR 625; // 72MHz / 115200 625 USART1-CR1 | (1 13); // UE 1 USART1-CR1 | (1 3); // TE 1发个Hello到电脑串口助手能正常显示就说明时钟和波特率都对了。6. 时钟配置中那些手册没写的坑6.1 切换时钟源时的瞬间停摆问题切换系统时钟的时候如果新时钟源还没稳定就切过去CPU会停摆。虽然硬件有保护机制但保护不是万能的。我遇到过从HSI切HSEHSE还没起振就写SW位结果系统时钟停了几个毫秒看门狗直接复位。正确的做法是先开新时钟源等RDY标志置位再切SW再等SWS确认。三步都不能省。特别是从低频率切高频率或者从内部切外部等待时间要留够。6.2 关闭时钟源之前的依赖检查关闭HSI之前要确认系统时钟不是HSIPLL输入不是HSI没有任何外设在用HSI。如果PLL还在用HSI你把HSI关了PLL就失锁了系统时钟就没了。F1系列的RCC_CR里HSION是Bit0。写0关闭HSI。但硬件有个保护如果HSI还在被使用写0也不生效。这个保护是好的但别依赖它。自己写的代码自己清楚关之前先确认依赖。6.3 低功耗模式下的时钟行为睡眠模式下CPU时钟停但外设时钟还在跑。停止模式下所有时钟停只有LSI和LSE还在跑给RTC和IWDG用。待机模式下除了LSE其他全停。如果你用RTC唤醒必须保证LSE或LSI在停止模式下还在跑。RCC_CSR寄存器的LSION和LSEON位控制低速时钟。停止模式唤醒后系统时钟默认切回HSI需要重新配置PLL和系统时钟。这个坑我在做低功耗项目时踩过停止模式唤醒后直接跑主循环结果串口输出乱码因为系统时钟变成HSI了波特率全错。后来在唤醒后加了时钟重配置代码才解决。6.4 寄存器读写时的位带操作技巧STM32支持位带操作可以把某个bit映射到一个32位地址读写这个地址就等于读写那个bit。RCC的寄存器不支持位带但GPIO和部分外设支持。位带的好处是原子操作不用读-改-写。比如要置位GPIOA的ODR的bit5普通操作是GPIOA-ODR | (1 5)这是读-改-写可能被中断打断。位带操作是直接写映射地址一条指令完成不会被打断。位带映射公式别名地址 0x42000000 (寄存器地址 - 0x40000000) × 32 bit × 4。这个公式看着复杂但用宏定义封装后就很简单。#define BITBAND(addr, bit) ((volatile uint32_t *)(0x42000000 ((addr - 0x40000000) 5) (bit 2))) #define GPIOA_ODR_5 BITBAND(0x4001080C, 5) *GPIOA_ODR_5 1; // 置位PA5RCC寄存器虽然不支持位带但可以用位带别名区来操作其他支持的外设提高代码的原子性和执行效率。7. 从第三章延伸出去的学习路径第三章讲完RCC和APB其实只是时钟系统的入门。后面还有几个方向可以深入CSS时钟安全系统当HSE失效时自动切回HSI并产生中断时钟输出MCO的更多配置低功耗模式下的时钟管理以及不同系列之间的时钟树差异。F4系列的时钟树比F1复杂得多有多个PLL、多个AHB分频、APB分频系数也不同。H7系列更复杂每个外设几乎都有自己的时钟选择器。但万变不离其宗先选源再倍频再分频最后使能。把这套逻辑吃透换任何系列都能快速上手。我个人的习惯是拿到一个新系列的芯片先看时钟树图把从源到外设的路径画出来标上每个节点的频率。然后写一个时钟配置函数用MCO和串口双重验证。验证通过后这个配置就固化下来后面所有项目都复用。这样每次新项目启动时钟配置就是复制粘贴不用重新踩坑。寄存器操作看起来繁琐但它的好处是透明。每一行代码对应一个硬件动作出了问题能精确定位。HAL库封装了很多细节方便是方便但出问题时你只能看库源码反而更费时间。第三章把这些底层逻辑讲清楚后面用HAL库的时候你也能知道它背后在干什么。