ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 时钟树实操与 SystemClock_Config 源码解析

STM32 时钟树实操与 SystemClock_Config 源码解析 Ciallo(∠・ω )⌒☆系列专栏直达链接 C 语言进入专栏 → 数据结构进入专栏 → C进入专栏 → Python进入专栏 → STM32进入专栏 → 博客主页暖焰核心 作者仓库github | gitee文章目录引言一、 频率探针 CPU_Delay二、 默认时钟下的第一版三、 CubeMX 配置与 SWD四、 SystemClock_Config 与 72MHz五、 分频实验与 CFGR 位段六、 从结构体到寄存器位七、 频率边界与等待周期引言前面几篇点灯、读按键代码里一直有一个我从没正经写过的函数SystemClock_Config()。每次新建工程CubeMX 都替我把它填好能跑我就没再管它。上一篇把时钟树的道理讲完了四个时钟源、PLL 倍频、AHB 与 APB 逐级分频、外设时钟门控。道理都懂了但那棵树怎么用代码配出来、参数填错一点会发生什么不亲手配一次心里还是没底。一、 频率探针 CPU_Delay延时函数代码如下voidCPU_Delay(volatileuint32_tcnt){while(cnt--);}参数上的volatile不是可有可无。这个循环递减变量、不产生任何外部效果开了优化之后编译器完全有权把它整个删掉延时直接消失加上volatile编译器就不许动它了每次循环都得老老实实读一次内存、减一次、写回去。代价是延时不精确好处是它完全跟着 CPU 的节奏走。这正好是我要的性质。HAL_Delay()靠 SysTick系统节拍定时器中断计的是真实毫秒主频怎么变它都准反而看不出时钟配置的效果CPU_Delay数的是 CPU 自己的节拍主频多高、数得多快。把 LED发光二极管接在PF8上翻转一次歇一个延时闪的快慢就成了时钟频率的直观展示一个不占用任何外设的频率探针。就像军训喊拍子走正步拍子喊得越密走完同样 100 米用时越短。数数快慢看不出来正步快慢谁都看得见。二、 默认时钟下的第一版先看什么都不配的情况。STM32F103 复位之后时钟控制寄存器RCC_CR的复位值是0x0000 0083最低两位HSION和HSIRDY都是 1内部 8 MHz 的HSIHigh Speed Internal高速内部 RC 振荡器已经在跑而且它直接接在SYSCLK系统时钟的选择器上。也就是说芯片出厂的默认状态就是能跑的复位值早就替你把第一版配置填好了。CubeMX 新建工程、什么都不动的时候生成的代码就维持了这个状态HSI 开着SYSCLK 直接选 HSI没有倍频也没有分频。配上 GPIO通用输入输出口翻转和探针第一版程序就齐了intmain(void){HAL_Init();SystemClock_Config();// 此时只把 HSI 打开8 MHz 直供 SYSCLKMX_GPIO_Init();while(1){HAL_GPIO_TogglePin(GPIOF,GPIO_PIN_8);CPU_Delay(654545*2);}}框架就这四样初始化 HAL、配时钟、配 GPIO、死循环里翻转加延时。后面无论怎么改只需要动SystemClock_Config()一个函数。CPU_Delay(654545 * 2)这个数是按主频估出来的数一次几个周期乘出来零点几秒让灯闪肉眼可辨具体多大不重要重要的是后面几组对比共用同一个数。HALHardware Abstraction Layer硬件抽象层把外设初始化收拢成固定套路时钟这一环单独拎出来配。三、 CubeMX 配置与 SWD用 CubeMX 把工程配到「能上 72 MHz」的状态一共五步SYS里把 Debug 选成Serial WireRCC里 HSEHigh Speed External高速外部晶振选Crystal/Ceramic Resonator意思是板上有 8 MHz 晶体时钟树界面里把 HSE 接进 PLL倍频拉到 ×9SYSCLK 顶到 72 MHzPF8配成推挽输出接灯项目管理里 Toolchain 选MDK-ARM生成代码。第 1 步非常重要不能省。默认的 No Debug 不给调试口留任何配置第一次烧录往往正常程序跑起来之后调试器就可能再也连不上第二次烧录就麻烦了。SWDSerial Wire Debug串行调试接口就四根线3.3V、GND、SWDIO、SWCLK选中 Serial Wire 之后CubeMX 生成的代码里才有对应的调试口配置调试通道才算真正可用。HSE 选 Crystal 之后时钟树界面会直接显示每级的频率倍频系数一点72 MHz 就出来了。这张图值得多看两眼写进代码的每个参数都能在图上找到自己的位置。四、 SystemClock_Config 与 72MHz第二版把 SYSCLK 拉到 F103 的顶格 72 MHz。代码如下voidSystemClock_Config(void){RCC_OscInitTypeDef RCC_OscInitStruct{0};RCC_ClkInitTypeDef RCC_ClkInitStruct{0};RCC_OscInitStruct.OscillatorTypeRCC_OSCILLATORTYPE_HSE;// 要配的振荡器HSERCC_OscInitStruct.HSEStateRCC_HSE_ON;// 打开 HSERCC_OscInitStruct.HSEPredivValueRCC_HSE_PREDIV_DIV1;// HSE 进 PLL 前先除以 1RCC_OscInitStruct.HSIStateRCC_HSI_ON;// HSI 保持开启RCC_OscInitStruct.PLL.PLLStateRCC_PLL_ON;// 打开 PLLRCC_OscInitStruct.PLL.PLLSourceRCC_PLLSOURCE_HSE;// PLL 的源选 HSERCC_OscInitStruct.PLL.PLLMULRCC_PLL_MUL9;// 9 倍频if(HAL_RCC_OscConfig(RCC_OscInitStruct)!HAL_OK){Error_Handler();}RCC_ClkInitStruct.ClockTypeRCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;RCC_ClkInitStruct.SYSCLKSourceRCC_SYSCLKSOURCE_PLLCLK;// SYSCLK 选 PLL 输出RCC_ClkInitStruct.AHBCLKDividerRCC_SYSCLK_DIV4;// AHB 四分频RCC_ClkInitStruct.APB1CLKDividerRCC_HCLK_DIV2;// APB1 再二分频RCC_ClkInitStruct.APB2CLKDividerRCC_HCLK_DIV1;// APB2 不分频if(HAL_RCC_ClockConfig(RCC_ClkInitStruct,FLASH_LATENCY_2)!HAL_OK){Error_Handler();}}函数分成两半。前一半HAL_RCC_OscConfig()管振荡器选哪个源、开不开、倍到多高。8 MHz 的 HSE 先过PREDIV除以 1再进 PLLPhase Locked Loop锁相环乘 9得到 72 MHz 的 PLLCLK。后一半HAL_RCC_ClockConfig()管总线SYSCLK 的选择器拨到 PLL然后 AHB、APB1、APB2 各自分频。ClockType那一行是在声明「这次要配哪几个时钟」四个都选上了。HSI 为什么还开着时钟已经切到 PLLHSI 留着不碍事真出问题的时候它还是唯一能立刻顶上的后备源。概念篇里讲过的 CSS时钟安全系统管的是 HSE 出事后自动切回 HSI这里先把 HSI 开着思路是一样的不把退路堵死。AHB 分频我填的却是 ÷472 MHz 的 SYSCLK 分完频之后HCLKAHB 总线时钟也是 Cortex-M3 内核的自由运行时钟 FCLKFree running CLocK只剩 18 MHz。这不是手滑FCLK 就是 CPU 数数的节拍故意留的低主频正好给下一节的分频实验当基准。五、 分频实验与 CFGR 位段同一份代码改一行分频灯的快慢肉眼可见。三组配置表格如下组SYSCLKAHB 分频HCLKAPB1 分频PCLK1APB2 分频PCLK2一72 MHz÷89 MHz÷24.5 MHz÷19 MHz二我的版本72 MHz÷418 MHz÷29 MHz÷118 MHz三72 MHz÷418 MHz÷82.25 MHz÷161.125 MHzCPU_Delay数数的节奏由 FCLK 决定FCLK 就是 HCLK。第二组烧进去同一个CPU_Delay(654545 * 2)灯翻一次的间隔是 AHB 不分频时的 4 倍慢下来的幅度肉眼一下就能分辨。第一组 HCLK 只剩 9 MHz间隔更长第三组分频看着最狠但注意看表它的 HCLK 和第二组一样都是 18 MHz。灯翻的快慢只由 FCLK 决定APB1 分到 ÷8、APB2 分到 ÷16 都不改变 CPU 数数的速度受影响的是挂在 APB 上的外设定时器、串口这些的节奏被放慢了。三组的 SYSCLK 都是 72 MHz对比下来就是一句话倍频决定 SYSCLK 能到多高AHB 分频决定 CPU 实际跑多快APB 分频管的是外设那一头。这些分频系数落在RCC_CFGR里。上一篇提到过这个寄存器它的位段表格如下位段位号管什么本篇用到的取值SW[1:0]1:0SYSCLK 选谁10选 PLLSWS[1:0]3:2切换状态硬件回填10表示已切到 PLLHPRE[3:0]7:4AHB 分频0xxx不分频1000起 ÷2、÷4、÷8、÷16PPRE1[2:0]10:8APB1 分频0xx不分频100÷2、101÷4、110÷8、111÷16PPRE2[2:0]13:11APB2 分频同 PPRE1 编码ADCPRE[1:0]15:14ADC 分频÷2/÷4/÷6/÷8PLLSRC16PLL 源1选 HSEPLLMUL[3:0]21:18倍频系数0111是 ×9APB1 二分频对应的宏是RCC_HCLK_DIV2值0x00000400翻译成二进制正好落在 bit 10RCC_HCLK_DIV2 0x00000400 0000 0100 0000 0000 ^^^ bit 10:8 100写进 PPRE1[2:0]APB1 就是 HCLK / 2注意写进去的手段不是|。分频位段的值是「编码」不是「开关」|只能把位从 0 变 1碰上已经置 1 的旧值就无能为力。HAL 里用的是MODIFY_REG先清位段、再填新值MODIFY_REG(RCC-CFGR,RCC_CFGR_PPRE1,RCC_ClkInitStruct.APB1CLKDivider);// 等价于RCC-CFGR(RCC-CFGR~RCC_CFGR_PPRE1)|RCC_ClkInitStruct.APB1CLKDivider;HAL_RCC_ClockConfig()把 SYSCLK 切到 PLL 用的也是同样的手法先写SW 10然后轮询SWS。SW 是给选择器的指令SWS 是硬件回报的实际状态等 SWS 也变成10切换才算真正完成。指令和状态分成两位硬件同步需要时间这件事寄存器设计里直接体现了出来。六、 从结构体到寄存器位HAL_RCC_OscConfig()和HAL_RCC_ClockConfig()只是入口参数从结构体传到寄存器位中间还隔着两层。拿「打开 HSE」这一步当例子图示如下RCC_OscInitStruct.HSEState RCC_HSE_ON | v HAL_RCC_OscConfig(RCC_OscInitStruct) HAL 函数编排配置流程 | v __HAL_RCC_HSE_CONFIG(RCC_HSE_ON) 宏按参数分流 | v SET_BIT(RCC-CR, RCC_CR_HSEON) 寄存器位CR 的 bit16 置 1第一层寄存器在哪。RCCReset and Clock Control复位与时钟控制器挂在 AHB 总线上这一段外设地址从0x40020000开始RCC 占据其中的0x40021000。库里用结构体描述它的寄存器组#defineRCC_BASE(AHBPERIPH_BASE0x00001000UL)// 0x40021000#defineRCC((RCC_TypeDef*)RCC_BASE)typedefstruct{__IOuint32_tCR;/*! 时钟控制寄存器HSE/HSI/PLL 的开关与就绪标志 */__IOuint32_tCFGR;/*! 时钟配置寄存器SW/HPRE/PPRE1/PPRE2/PLLMUL */__IOuint32_tCIR;/*! 时钟中断寄存器 */__IOuint32_tAPB2RSTR;/*! APB2 外设复位寄存器 */__IOuint32_tAPB1RSTR;/*! APB1 外设复位寄存器 */__IOuint32_tAHBENR;/*! AHB 外设时钟使能寄存器 */__IOuint32_tAPB2ENR;/*! APB2 外设时钟使能寄存器 */__IOuint32_tAPB1ENR;/*! APB1 外设时钟使能寄存器 */__IOuint32_tBDCR;/*! 备份域控制寄存器 */__IOuint32_tCSR;/*! 控制与状态寄存器 */}RCC_TypeDef;成员顺序就是寄存器地址顺序每个uint32_t占 4 字节。指针一指基地址RCC-CR就是0x40021000RCC-CFGR就是0x40021004。GPIO 篇拆GPIO_TypeDef时用的同一套指针映射把戏这里不过换了个外设。第二层开时钟的宏。点灯前必写的__HAL_RCC_GPIOF_CLK_ENABLE()展开代码如下#define__HAL_RCC_GPIOF_CLK_ENABLE()\do{\__IOuint32_ttmpreg;\SET_BIT(RCC-APB2ENR,RCC_APB2ENR_IOPFEN);\tmpregREAD_BIT(RCC-APB2ENR,RCC_APB2ENR_IOPFEN);\UNUSED(tmpreg);\}while(0)SET_BIT落到实处就是RCC-APB2ENR | RCC_APB2ENR_IOPFEN把IOPFENbit 7置 1GPIOF 的时钟门打开。后面跟着读一次同一个寄存器的tmpreg读出的值直接丢弃作用是给硬件留一点时钟稳定的时间这也是 HAL 代码里反复出现的固定节奏。时钟没开会发生什么GPIO 篇提过一句外设寄存器读永远返回0x0写也不生效。这次在 RCC 这里看得更清楚APB2ENR的每一位就是一扇门门不开门后的寄存器无论怎么读写都是空气。所以「外设怎么写都没反应」这类怪问题第一个该查的就是对应 ENR 位。第三层振荡器开关的宏。__HAL_RCC_HSE_CONFIG()按参数决定置位还是清位展开代码如下#define__HAL_RCC_HSE_CONFIG(__STATE__)\do{\if((__STATE__)RCC_HSE_ON)\{\SET_BIT(RCC-CR,RCC_CR_HSEON);\}\elseif((__STATE__)RCC_HSE_OFF)\{\CLEAR_BIT(RCC-CR,RCC_CR_HSEON);\}\elseif((__STATE__)RCC_HSE_BYPASS)\{\SET_BIT(RCC-CR,RCC_CR_HSEBYP);\SET_BIT(RCC-CR,RCC_CR_HSEON);\}\else\{\CLEAR_BIT(RCC-CR,RCC_CR_HSEON);\CLEAR_BIT(RCC-CR,RCC_CR_HSEBYP);\}\}while(0)RCC_CR_HSEON是0x00010000CR 的 bit 16。结构体里一句HSEState RCC_HSE_ON最终就是落到这一个位上。HAL_RCC_OscConfig()把结构体里所有振荡器参数翻译成一串这样的宏调用然后进入等状态的循环晶振起振需要时间HAL 轮询HSERDYHSE 就绪标志等不到就超时返回错误。开振荡器和开外设时钟不一样前者是模拟电路起振得等后者是数字门开关立即生效。第四层汇总。结构体是人机接口参数有名字、有默认值HAL 函数是流程编排决定先配谁后配谁、每一步等多久宏是位操作的缩写把「置位、清位、改位段」封装成可读的写法寄存器位是最终生效的一层。四层走完SystemClock_Config()里每一行对应的硬件动作就都清楚了。七、 频率边界与等待周期HAL_RCC_ClockConfig()的第二个参数FLASH_LATENCY_2一直没解释。CPU 从 Flash 取指令主频上去了Flash 的读出速度跟不上怎么办办法是每次取指多等几个周期。F103 的手册按 SYSCLK 划了三档SYSCLK 范围等待周期本篇取值0 SYSCLK ≤ 24 MHz0FLASH_LATENCY_024 SYSCLK ≤ 48 MHz1FLASH_LATENCY_148 SYSCLK ≤ 72 MHz2FLASH_LATENCY_272 MHz 落在第三档所以是FLASH_LATENCY_2。这条是硬件约束填小了取指出错不是能省就省的偏好项。还有一个容易想岔的地方判据是 SYSCLK不是 HCLK。本篇实验配置里 HCLK 只有 18 MHz但 SYSCLK 是 72 MHzCubeMX 照样生成FLASH_LATENCY_2对照 CubeMX 生成的工程配置一看便知。把主频降下来反而要跟着改。配一组 48 MHz 的对照PLLMUL从 ×9 换成 ×6AHB 不分频等待周期降到FLASH_LATENCY_1RCC_OscInitStruct.PLL.PLLMULRCC_PLL_MUL6;// 8 MHz x 6 48 MHzRCC_ClkInitStruct.AHBCLKDividerRCC_SYSCLK_DIV1;// AHB 不分频RCC_ClkInitStruct.APB1CLKDividerRCC_HCLK_DIV2;// APB1 二分频RCC_ClkInitStruct.APB2CLKDividerRCC_HCLK_DIV1;// APB2 不分频if(HAL_RCC_ClockConfig(RCC_ClkInitStruct,FLASH_LATENCY_1)!HAL_OK){Error_Handler();}48 MHz 落在第二档等待周期就是 1。同一颗芯片主频、分频、等待周期三个数绑在一起动改一个就得核对其余两个。这组配置里 AHB 不分频HCLK 直接跑 48 MHz和前面 ÷4 的实验配置正好是两个方向一个让总线跟满主频一个故意让 CPU 慢下来。APB1 还有条上限这条总线挂的是 TIM2~7、USART2/3、I2C 这类外设硬件规定它的时钟不得超过 36 MHz。72 MHz 全速时 APB1 至少得二分频实验里的极端分频反而离上限很远。方向别记反了「至少 ÷2」说的是必须分不是只能分到 2。到这里SystemClock_Config()已经不是黑盒了选源、倍频、分频、切换四步各对应一段寄存器每一步的参数都有硬件约束兜着。回头看那只灯它闪的每一次快慢变化都是这几段寄存器里某一位被改写的直接后果。
返回列表