ARTICLE DETAIL

资讯详情

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

RCC与时钟树案例与源码解析

RCC与时钟树案例与源码解析 1. 测试⽅案我们使⽤LED灯来进⾏演⽰RCC配置时钟树的效果。之前闪灯我们采⽤HAL库提供的HAL_Delay()函数 完成延时我们不⽤这种做法采⽤void CPU_Delay(uint32_t delay) { while(delay--); }让CPU疯狂死循环的⽅式进⾏延时。因为CPU执⾏的速度会受到时钟树的FCLK时钟控制也就是说 我们把这个时钟频率调⾼执⾏同样CPU_Delay(XXX)的代码完成的时间就会短如果把这个时钟频 率调低执⾏同样CPU_Delay(XXX)的代码完成的时间就⻓根据时间的⻓短变化就会引起闪灯速度 的变化进⽽感受到时钟配置对系统的影响。就好像军训喊⼝号喊的快⾛正步⾛100m时间就短否则就⻓2. LED灯选择⽐特 STM32 灵犀版中 STM32 芯⽚与 LED 灯的连接关系3. 开始配置3.1 开启调试我们开发板的预留的仿真器调试接⼝是 SWD ( Serial Wire Debug ) 在如下图中 所标识区域选择 “ Serial Wire ” 。 注意! 这⼀步⾮常重要. 如果没有修改这⾥, 使⽤了默认的 No Debug, 第⼀次烧录程序可以成功, 但是后续烧录程序就 会烧录失败 DAP vs SWDDAP Link是⼀种调试器硬件⽽SWD是⼀种调试通信协议DAP Link通过SWD协议与⽬标芯 ⽚通信。• DAP 全称是 Debug Access Port 调试访问端⼝是⼀个物理仿真器• SWD 全称是 Serial Wire Debug (串⾏线调试)• 两者的关系类似DAP考试的时候坐在座位上的同学本⼈SWD⽤来传递纸条的通道约定好的⼿势或者暗号3.2 开启外部时钟源在如下图所标识区域选择 “ Crystal / Ceramic Resonator ” 表⽰选择外部晶振做为 HSE3.3 配置时钟树我们开发板上是⼀个 8MHz 的 HSE 晶振如下图标识区域所⽰需要选通 8MHz 晶振作为时钟 源并将系统运⾏最⼤频率配置到 72MHz 。3.4 开启针脚GPIO输出默认是高电平3.5 项⽬路径⼯具链配置填写项⽬名称• 填写项⽬路径• 选择 Tool Chain 为 MDK-ARM MDK-ARM 就是 Keil 这⼀套开发⼯具.STM32CubeMX 只是帮我们简化创建⼯程的过程, 开发代码/编译/烧录/调试 这些过程仍然是通过 Keil 完成.3.6 ⽣成项⽬最后点击右上⻆的 Generate Code, 此时 STM32CubeMX 就会⽣成⼀个完整的⼯程了.4. 编写代码代码块 /* USER CODE BEGIN PFP */ void CPU_Delay(volatile uint32_t delay); /* USER CODE END PFP */ /* USER CODE BEGIN WHILE */ while (1) { HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_8); CPU_Delay(XXXXX); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE BEGIN 4 */ void CPU_Delay(volatile uint32_t delay) { while(delay--); } / * USER CODE END 4 */# 整体程序运行流程1. 系统初始化HAL 库初始化、时钟、GPIO 初始化CubeMX 自动生成你看不到2. 进入while(1)主循环3. 翻转 PF8 引脚电平4. 进入CPU_DelayCPU 空循环等待5. 等待结束回到循环开头再次翻转引脚无限重复# 重点区分两个关键点1. **CPU_Delay**软件循环延时- 延时时间取决于芯片主频、编译器优化等级- 单位循环次数**不是 ms**- 全程占用 CPU忙等待2. **HAL_Delay**SysTick 硬件定时器延时- 单位固定是毫秒时间稳定- 依赖 SysTick 定时器# 潜在问题总结1. volatile放在形参优化等级高时延时可能失效2. 延时精度差换主频就变只能粗略延时3. 阻塞式延时延时期间 CPU 无法处理别的任务4. 开启 O2 优化空循环很容易被编译器直接删掉PF8 引脚会高速翻转。# 运行现象PF8 引脚输出方波方波高低电平持续时间≈CPU_Delay的耗时。如果你在 PF8 接 LEDLED 会**周期性闪烁**接示波器能看到方波波形。AI提⽰词 在stm32F103ZET6中CPU时钟频率是72MHz不考虑编译器优化等任何情况粗粒度 估算while(delay--);这⾏代码执⾏需要多少个时钟周期把从Flash读取指令过程也考虑进去并且 帮我计算出如果我要让CPU死循环200ms左右⼤概把这⾏代码执⾏多少次列出计算过8. 源码解析8.1 使能GPIOX和RCC物理地址RCC本质也是⼀个⽚上外设那么在统⼀编址的背景下在 STM32F103ZET6 中RCC复位与时钟控制外设的地址空间范围是 0x4002 1000 到0x4002 13FF ⼤⼩为 1KB代码块 static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; /* USER CODE BEGIN MX_GPIO_Init_1 */ /* USER CODE END MX_GPIO_Init_1 */ /* GPIO Ports Clock Enable */ __HAL_RCC_GPIOF_CLK_ENABLE(); /*Configure GPIO pin Output Level */ HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_SET); /*Configure GPIO pin : PF8 */ GPIO_InitStruct.Pin GPIO_PIN_8; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOF, GPIO_InitStruct); /* USER CODE BEGIN MX_GPIO_Init_2 */ /* USER CODE END MX_GPIO_Init_2 */ } # define __HAL_RCC_GPIOF_CLK_ENABLE() do { \ __IO uint32_t tmpreg; \ SET_BIT(RCC-APB2ENR, RCC_APB2ENR_IOPFEN);\ /* Delay after an RCC peripheral clock enabling */ \ tmpreg READ_BIT(RCC-APB2ENR, RCC_APB2ENR_IOPFEN);\ UNUSED(tmpreg); \ } while(0U) 代码块 #define RCC ((RCC_TypeDef *)RCC_BASE) #define RCC_BASE (AHBPERIPH_BASE 0x00001000UL) #define AHBPERIPH_BASE (PERIPH_BASE 0x00020000UL) 1 2 3#define PERIPH_BASE 0x40000000UL /*! Peripheral base address in the alias region */ 代码块 typedef struct { __IO uint32_t CR; __IO uint32_t CFGR; __IO uint32_t CIR; __IO uint32_t APB2RSTR; __IO uint32_t APB1RSTR; __IO uint32_t AHBENR; __IO uint32_t APB2ENR; __IO uint32_t APB1ENR; __IO uint32_t BDCR; __IO uint32_t CSR; } RCC_TypeDef;1. GPIO_InitTypeDef GPIO_InitStruct {0};定义 GPIO 初始化配置结构体全部成员先清零用来存放引脚配置。2. __HAL_RCC_GPIOF_CLK_ENABLE();**开启 GPIOF 外设时钟**。STM32 外设默认时钟关闭**不开启时钟GPIO 寄存器读写无效引脚不工作**。3. HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_SET);在正式初始化引脚模式**之前**先把 PF8 电平设置为高电平。目的防止初始化瞬间引脚出现毛刺、误触发。4. 填充结构体- .Pin GPIO_PIN_8; 选择 PF8 引脚- .Mode GPIO_MODE_OUTPUT_PP; **推挽输出**Push-Pull可以输出高低电平驱动 LED- .Pull GPIO_NOPULL; 不开启上拉 / 下拉电阻- .Speed GPIO_SPEED_FREQ_LOW; IO 速度低速适合普通 LED5. HAL_GPIO_Init(GPIOF, GPIO_InitStruct);把上面配置写入 GPIOF 硬件寄存器完成 PF8 配置。 效果初始化完成后 PF8 是推挽输出上电默认高电平。配合你前面while(1)里的HAL_GPIO_TogglePin就可以翻转引脚输出方波。---## 第二块宏 __HAL_RCC_GPIOF_CLK_ENABLE()#define __HAL_RCC_GPIOF_CLK_ENABLE() do { \__IO uint32_t tmpreg; \SET_BIT(RCC-APB2ENR, RCC_APB2ENR_IOPFEN);\/* Delay after an RCC peripheral clock enabling */ \tmpreg READ_BIT(RCC-APB2ENR, RCC_APB2ENR_IOPFEN);\UNUSED(tmpreg); \} while(0U) do{ ... }while(0) 是 C 语言宏经典写法保证宏可以像一条语句使用加分号也不会语法错误。拆解里面 3 件事1. SET_BIT(RCC-APB2ENR, RCC_APB2ENR_IOPFEN);- RCC-APB2ENRRCC 的 APB2 外设时钟使能寄存器- RCC_APB2ENR_IOPFEN对应 GPIOF 的时钟位- 作用**把 APB2ENR 寄存器里 IOPF 这一位置 1打开 GPIOF 时钟**2. tmpreg READ_BIT(RCC-APB2ENR,RCC_APB2ENR_IOPFEN);读取这个寄存器。**读寄存器的目的等待时钟稳定**。STM32 手册要求开启外设时钟后需要一点建立时间读一下寄存器来产生延时保证时钟稳定。3. UNUSED(tmpreg);告诉编译器变量 tmpreg 虽然定义了但没使用消除unused variable警告。__IO 等价 volatile告诉编译器这是寄存器不要优化读写。---## 第三块基地址宏定义地址映射核心#define RCC ((RCC_TypeDef *)RCC_BASE)#define RCC_BASE (AHBPERIPH_BASE 0x00001000UL)#define AHBPERIPH_BASE (PERIPH_BASE 0x00020000UL)#define PERIPH_BASE 0x40000000ULSTM32 所有外设都映射在**内存地址**上访问寄存器 访问内存地址。1. PERIPH_BASE 0x40000000UL外设区域起始基地址2. AHBPERIPH_BASE PERIPH_BASE 0x200000 → AHB 总线外设起始地址3. RCC_BASE AHBPERIPH_BASE 0x1000 → RCC 外设的物理地址4. RCC ((RCC_TypeDef *)RCC_BASE)把 RCC_BASE 这个地址强制转换成RCC_TypeDef结构体指针。 于是 RCC-APB2ENR 本质就是**去 RCC 外设的基地址取结构体里面 APB2ENR 成员对应的偏移地址**。 一句话用结构体来映射一片连续寄存器地址RCC-成员就是访问对应寄存器。---## 第四块RCC_TypeDef 结构体typedef struct{__IO uint32_t CR;__IO uint32_t CFGR;__IO uint32_t CIR;__IO uint32_t APB2RSTR;__IO uint32_t APB1RSTR;__IO uint32_t AHBENR;__IO uint32_t APB2ENR;__IO uint32_t APB1ENR;__IO uint32_t BDCR;__IO uint32_t CSR;} RCC_TypeDef;- typedef struct定义 RCC 外设寄存器的结构体模板- __IO uint32_tvolatile 32 位无符号整数代表 32 位寄存器- 结构体里面成员**顺序不能乱**顺序对应硬件寄存器在 RCC 外设里的**偏移地址**- CR时钟控制寄存器- CFGR时钟配置寄存器- APB2ENRAPB2 外设时钟使能寄存器我们上面用到的开启 GPIOF 时钟就在这个寄存器- APB1ENRAPB1 外设时钟使能寄存器 原理结构体首地址 RCC_BASE。 结构体第一个成员 CR偏移 0第二个 CFGR 偏移 4**APB2ENR 在结构体里是第 7 个偏移固定 0x18**。 RCC-APB2ENR 等价于访问 RCC_BASE 0x18 这个物理地址的寄存器。# 完整调用链路串起来重点__HAL_RCC_GPIOF_CLK_ENABLE()→ SET_BIT(RCC-APB2ENR, RCC_APB2ENR_IOPFEN)→ RCC是RCC_TypeDef*指针指向RCC_BASE地址→ RCC-APB2ENR 找到APB2ENR寄存器地址→ 将IOPFEN位置1 → GPIOF外设时钟上电→ 时钟开启后才能读写GPIOF寄存器配置PF8引脚# 和你前面主循环代码联动1. main里先调用MX_GPIO_Init()- 开启 GPIOF 时钟- PF8 配置为推挽输出初始高电平2. 进入while(1)- HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_8)翻转 PF8 电平- CPU_Delay()软件延时3. 循环往复引脚输出方波## 考点小结嵌入式考试常问1. STM32 外设**默认时钟关闭必须 RCC 开启时钟外设才工作**2. 外设寄存器用结构体 基地址映射RCC-寄存器就是访问硬件内存地址3. __IOvolatile防止编译器优化寄存器读写4. do{...}while(0)宏技巧5. 开启时钟后读寄存器用来等待时钟稳定代码块 __HAL_RCC_LSE_CONFIG(RCC_OscInitStruct-LSEState); #define __HAL_RCC_LSE_CONFIG(__STATE__) \ do{ \ if ((__STATE__) RCC_LSE_ON) \ { \ SET_BIT(RCC-BDCR, RCC_BDCR_LSEON); \ } \ else if ((__STATE__) RCC_LSE_OFF) \ { \ CLEAR_BIT(RCC-BDCR, RCC_BDCR_LSEON); \ CLEAR_BIT(RCC-BDCR, RCC_BDCR_LSEBYP); \ } \ else if ((__STATE__) RCC_LSE_BYPASS) \ { \ SET_BIT(RCC-BDCR, RCC_BDCR_LSEBYP); \ SET_BIT(RCC-BDCR, RCC_BDCR_LSEON); \ } \ else \ { \ CLEAR_BIT(RCC-BDCR, RCC_BDCR_LSEON); \ CLEAR_BIT(RCC-BDCR, RCC_BDCR_LSEBYP); \ } \ }while(0U) #define RCC_BDCR_LSEON_Pos (0U) 25#define RCC_BDCR_LSEON_Msk (0x1UL RCC_BDCR_LSEON_Pos) /*! 0x00000001 */ #define RCC_BDCR_LSEON RCC_BDCR_LSEON_Msk /*! External Low Speed oscillator enabl8.3 配置分频倍频代码块 void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; // 振荡器配置结构体 RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; // 时钟配置结构体 /** Initializes the RCC Oscillators according to the specified parameters * in the RCC_OscInitTypeDef structure. */ RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; // 配置HSE外部晶振 RCC_OscInitStruct.HSEState RCC_HSE_ON; // 使能HSE RCC_OscInitStruct.HSEPredivValue RCC_HSE_PREDIV_DIV1; // HSE不分频 RCC_OscInitStruct.HSIState RCC_HSI_ON; // 使能HSI作为备⽤ RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; // 使能PLL RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; // PLL选择HSE作为输⼊源 RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL9; // PLL 9倍频HSE 8MHz * 9 72MHz 26 27 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15if (HAL_RCC_OscConfig(RCC_OscInitStruct) ! HAL_OK) // 写⼊寄存器并等待就绪 { Error_Handler(); // 配置失败错误处理 } / ** Initializes the CPU, AHB and APB buses clocks */ RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 配置 HCLK、SYSCLK、PCLK1、PCLK2 RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; // 系统时钟选择PLL输 出 RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV8; // AHB总线 8分频HCLK 72/8 9MHz RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV2; // APB1 2分频PCLK1 9/2 4.5MHz RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; // APB2 1分频PCLK2 9MHz if (HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_2) ! HAL_OK) // Flash 2等待周期72MHz需≥2 { Error_Handler(); // 配置失败错误处理 } } 以⼀个为例说明其他类似 代码块 #define RCC_HCLK_DIV2 RCC_CFGR_PPRE1_DIV2 /*! HCLK divided by 2 */ #define RCC_CFGR_PPRE1_DIV2 0x00000400U /*! HCLK divided by 2 */ // 100 0000 0000 代码块 HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct, uint32_t FLatency) { ... /*-------------------------- PCLK1 Configuration --------------------------- */ 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 1 2 3 4 5 1 2 3 4 5if (((RCC_ClkInitStruct-ClockType) RCC_CLOCKTYPE_PCLK1) RCC_CLOCKTYPE_PCLK1) { assert_param(IS_RCC_PCLK(RCC_ClkInitStruct-APB1CLKDivider)); MODIFY_REG(RCC-CFGR, RCC_CFGR_PPRE1, RCC_ClkInitStruct-APB1CLKDivider); } / *-------------------------- PCLK2 Configuration --------------------------- */ if (((RCC_ClkInitStruct-ClockType) RCC_CLOCKTYPE_PCLK2) RCC_CLOCKTYPE_PCLK2) { assert_param(IS_RCC_PCLK(RCC_ClkInitStruct-APB2CLKDivider)); MODIFY_REG(RCC-CFGR, RCC_CFGR_PPRE2, ((RCC_ClkInitStruct-APB2CLKDivider) 3)); } } / / 7是111 #define RCC_CFGR_PPRE1_Pos (8U) #define RCC_CFGR_PPRE1_Msk (0x7UL RCC_CFGR_PPRE1_Pos) /*! 0x00000700 */ #define RCC_CFGR_PPRE1 RCC_CFGR_PPRE1_Msk /*! PRE1[2:0] bits (APB1 prescaler) */配置时钟树的三个步骤和GPIO的配置方式相同 图里是 STM32F1 的时钟树 RCC_CFGR时钟配置寄存器的位图正好对应你前面SystemClock_Config里这两行RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV2; // APB1 2分频RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; // APB2 不分频## 1. 时钟树部分中间大图回顾你这套配置- HSE 8M → PLL×9 → SYSCLK72MHz- AHB 分频DIV8 → **HCLK 72/8 9MHz**- APB1 预分频DIV2 → PCLK1 HCLK /2 **4.5MHz**- APB2 预分频DIV1 → PCLK2 HCLK /1 **9MHz** APB1 总线的硬性限制**PCLK1 最大不能超过 36MHz**你这里 4.5MHz完全安全。 GPIOF 挂载在 APB2 总线外设时钟 PCLK29MHz。## 2. RCC_CFGR 寄存器位图下方寄存器图RCC_CFGR 是 32 位寄存器我们重点看这几个位段表格| 位段 | 名字 | 作用 || --- | --- | --- || Bit[10:8] | **PPRE1[2:0]** | APB1 低速总线预分频器就是你现在圈出来的 || Bit[13:11] | PPRE2[2:0] | APB2 高速总线预分频器 || Bit[7:4] | HPRE[3:0] | AHB 总线预分频器 || Bit[1:0] | SW[1:0] | SYSCLK 的时钟源选择00:HSI01:HSE10:PLL |### ✅ PPRE1 [2:0] 编码规则Bit10 Bit9 Bit8 手册定义- 0xxHCLK 不分频÷1- 100HCLK ÷2- 101HCLK ÷4- 110HCLK ÷8- 111HCLK ÷16你代码APB1CLKDivider RCC_HCLK_DIV2对应编码**100**也就是Bit10 1Bit9 0Bit8 0 二进制100写入 CFGR 寄存器 bit10~bit8。## 3. HAL 库底层MODIFY_REG 宏图左下角#define MODIFY_REG(REG, CLEARMASK, SETMASK) \WRITE_REG((REG), (((READ_REG(REG)) (~(CLEARMASK))) | (SETMASK)))MODIFY_REG 是 HAL 库**修改寄存器部分位**的标准宏作用1. READ_REG(REG)读出 RCC-CFGR 原始 32 位值2. (~CLEARMASK)**先把 PPRE1 对应的 3 个位清零**清除旧的分频配置3. | (SETMASK)**把新的分频值100写进这 3 个位**4. WRITE_REG 写回寄存器 不会改动寄存器其他 bit只修改 PPRE1 这 3 位这是嵌入式操作寄存器位的经典写法。## 4. 宏定义映射右上角#define RCC_CFGR_PPRE1_Pos (8U) // PPRE1从第8位开始#define RCC_CFGR_PPRE1_Msk (0x7UL RCC_CFGR_PPRE1_Pos)// 0x7 111左移8位 → 0x00000700掩码选中bit8~bit10#define RCC_HCLK_DIV2 (0x4UL RCC_CFGR_PPRE1_Pos)// 0x4 100左移8位 → 0x00000400就是PPRE1100÷2- Pos起始 bit 位置- Msk掩码用来选中这 3 个位CLEARMASK- RCC_HCLK_DIV2SETMASK要写入的100## 5. 完整执行流程串联 SystemClock_ConfigRCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV2;↓HAL_RCC_ClockConfig()↓内部调用 MODIFY_REG(RCC-CFGR, RCC_CFGR_PPRE1_Msk, RCC_HCLK_DIV2);↓1. 读取RCC-CFGR2. 把bit10~8清零3. 或上 1000x484. 写回RCC-CFGR写完成后CFGR 寄存器 bit10:8 100硬件自动开启 APB1 二分频PCLK1 HCLK/2。## 6. 联动你前面所有代码1. SystemClock_Config配置 CFGR设置 AHB、APB1、APB2 分频 → HCLK9MHz2. MX_GPIO_Init开启 GPIOF 时钟APB2 总线 PCLK29MHz3. while (1)翻转 PF8 引脚 CPU_Delay 延时 ⚠️ CPU 内核时钟就是 HCLK9MHz直接决定CPU_Delay空循环的执行速度## 7. 重点考点总结1. PPRE1 在 RCC_CFGR 寄存器 bit10~8PPRE2 在 bit13~112. MODIFY_REG先清目标位再设置新值**只修改指定位不影响其他位**3. APB1 分频器的特殊规则- 当 PPRE10xx不分频PCLK1 HCLK- 当 PPRE1≥1002/4/8/16 分频定时器时钟 2 × PCLK14. F1 的 APB1 外设最高时钟不能超过 36MHz硬件限制。上面是开启哪一个时钟源下面是配置时钟树的相关信息
返回列表