STM32L486运行时切换时钟频率如何避免影响外设?
运行时切换STM32L486系统时钟:常见性与外设问题解决方案
嘿,这个需求太常见了——但凡做电池供电的低功耗设备,比如便携传感器、智能手环这类,几乎都会用到这种“平时低功耗摸鱼,需要干活时拉满性能”的操作。STM32L4系列本身就为这种场景做了专门的时钟设计,所以完全不用担心可行性,重点是搞定外设失效的问题。
一、先给你吃颗定心丸:运行时切换系统时钟是常规操作
STM32的动态时钟切换是官方支持的核心特性之一,很多官方低功耗Demo里都有现成的实现。本质上就是利用时钟树的灵活性,在不同场景下切换系统时钟源/频率,平衡功耗和性能,属于低功耗设备的标准操作流程。
二、外设失效的核心原因&解决思路
你说的USART失效,本质是外设的工作参数是基于当前外设时钟计算的——比如USART的波特率依赖于APB总线时钟,当系统时钟切换后,APB总线的分频比或者时钟源变了,外设的实际工作时钟就变了,原来的配置(比如波特率寄存器BRR的值)自然就不匹配了。
解决思路也很直接:在时钟切换前后,对依赖系统时钟的外设做“先停后启+重新配置”的操作。
三、具体操作步骤(以HAL库为例)
1. 时钟切换前:安全关停外设,保存配置
- 对于USART这类有数据传输的外设,先确保当前传输完成:比如调用
HAL_UART_GetState()检查是否处于HAL_UART_STATE_READY状态,或者等待DMA传输完成(如果用了DMA的话)。 - 禁用外设时钟和相关中断:比如
__HAL_RCC_USART1_CLK_DISABLE()、HAL_NVIC_DisableIRQ(USART1_IRQn),避免切换时钟时出现异常中断。 - 保存外设的关键配置:把USART的波特率、数据位、停止位这些参数存在全局变量里,方便后续恢复:
typedef struct { uint32_t baudrate; uint32_t word_length; uint32_t stop_bits; uint32_t parity; uint32_t mode; uint32_t hw_flow_ctrl; } UART_SavedConfig; UART_SavedConfig uart1_config; // 保存当前配置 uart1_config.baudrate = huart1.Init.BaudRate; uart1_config.word_length = huart1.Init.WordLength; // ... 其他参数同理
2. 执行系统时钟切换
STM32L4的时钟切换要遵循“先切到临时时钟源,再配置目标时钟,最后切换系统时钟”的流程,避免切换过程中系统崩溃。
从80MHz PLL切换到1MHz MSI:
void SwitchToLowPowerClock(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 1. 先切换到HSI临时时钟,保证切换过程稳定 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSIDiv = RCC_HSI_DIV1; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } // 2. 配置系统时钟为HSI RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK) { Error_Handler(); } // 3. 关闭PLL RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_PLL; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_OFF; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } // 4. 配置MSI到1MHz RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_MSI; RCC_OscInitStruct.MSIState = RCC_MSI_ON; RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_0; // 对应1MHz RCC_OscInitStruct.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT; RCC_OscInitStruct.MSIDiv = RCC_MSI_DIV1; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } // 5. 切换系统时钟到MSI RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_MSI; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK) { Error_Handler(); } }
从1MHz MSI切回80MHz PLL:
void SwitchToFullSpeedClock(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 1. 先切换到HSI临时时钟 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSIDiv = RCC_HSI_DIV1; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } // 2. 配置系统时钟为HSI RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK) { Error_Handler(); } // 3. 配置PLL到80MHz(MSI 4MHz为源,PLLM=1, PLLN=20, PLLR=2) RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_PLL; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_MSI; RCC_OscInitStruct.PLL.PLLM = 1; RCC_OscInitStruct.PLL.PLLN = 20; RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } // 4. 切换系统时钟到PLL,配置FLASH延迟(80MHz需要FLASH_LATENCY_4) RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) { Error_Handler(); } }
3. 时钟切换后:重新初始化外设
切换完成后,把之前保存的配置重新加载给外设,恢复正常工作:
void RestoreUARTConfig(UART_HandleTypeDef *huart, UART_SavedConfig *config) { huart->Init.BaudRate = config->baudrate; huart->Init.WordLength = config->word_length; huart->Init.StopBits = config->stop_bits; huart->Init.Parity = config->parity; huart->Init.Mode = config->mode; huart->Init.HwFlowCtl = config->hw_flow_ctrl; huart->Init.OverSampling = UART_OVERSAMPLING_16; // 重新初始化USART if (HAL_UART_Init(huart) != HAL_OK) { Error_Handler(); } // 重新开启外设时钟和中断 __HAL_RCC_USART1_CLK_ENABLE(); HAL_NVIC_EnableIRQ(USART1_IRQn); }
四、一些额外的注意事项
- FLASH延迟配置:不同的系统时钟频率需要对应的FLASH读延迟,比如80MHz需要
FLASH_LATENCY_4,1MHz只需要FLASH_LATENCY_0,切换时一定要同步修改,否则会导致系统崩溃。 - 定时器等外设的处理:如果用了定时器,切换时钟后也要重新计算自动重装载值(ARR)和预分频器(PSC),确保定时周期正确。
- 低功耗模式的配合:切换到低频率时钟后,可以进入STOP/STOP2模式进一步降低功耗,唤醒后再切回全频率,这时候要注意唤醒后的时钟初始化顺序。
- 避免频繁切换:虽然支持动态切换,但过于频繁的时钟切换会增加功耗开销,尽量根据业务场景规划切换时机(比如定时唤醒处理数据,处理完立刻切回低功耗)。
内容的提问来源于stack exchange,提问作者Guillaume Petitjean




