时钟系统架构与配置¶
概述¶
时钟系统是嵌入式系统的心脏,为CPU和所有外设提供工作节拍。正确配置时钟系统不仅关系到系统的运行速度,还直接影响功耗、稳定性和外设功能。完成本文学习后,你将能够:
- 理解时钟系统的整体架构和工作原理
- 掌握不同时钟源的特点和选择方法
- 学会配置PLL实现时钟倍频
- 理解时钟树的结构和分频配置
- 掌握外设时钟的使能和管理方法
背景知识¶
为什么需要复杂的时钟系统?¶
在简单的单片机中,可能只有一个固定频率的时钟源。但现代嵌入式系统需要复杂的时钟系统,原因包括:
- 灵活的频率需求:CPU需要高频率,而某些外设需要低频率
- 功耗管理:不同工作模式需要不同的时钟频率
- 精度要求:某些应用需要高精度时钟(如USB、以太网)
- 成本考虑:使用低成本晶振通过PLL倍频到高频率
- 多时钟域:不同外设可能需要独立的时钟源
时钟系统的基本组成¶
典型的时钟系统包含: - 时钟源:提供基准时钟(晶振、RC振荡器) - PLL:锁相环,用于倍频 - 时钟树:分频器和选择器,分配时钟 - 时钟门控:控制外设时钟的开关
核心内容¶
1. 时钟源类型¶
1.1 外部高速晶振(HSE - High Speed External)¶
外部晶振是最常用的高精度时钟源。
特点: - 高精度:典型精度±20ppm(百万分之二十) - 稳定性好:温度漂移小 - 频率范围:通常4-26MHz - 功耗较高:需要外部元件
典型应用: - 需要精确时序的通信接口(USB、以太网、CAN) - 高精度定时器应用 - 作为PLL的输入源
// STM32 HSE配置示例
void HSE_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
// 配置HSE
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 使能HSE
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
}
硬件连接:
MCU 晶振电路
┌─────────┐
OSC_IN │ │ [晶振]
────────┤ ├────/\/\/\────┐
│ MCU │ [负载电容] │
OSC_OUT │ │ GND
────────┤ ├────/\/\/\────┘
└─────────┘ [负载电容]
1.2 外部低速晶振(LSE - Low Speed External)¶
32.768kHz晶振,专门用于RTC和低功耗应用。
特点: - 超低功耗:微安级电流消耗 - 固定频率:32.768kHz(2^15,便于分频得到1Hz) - 高精度:适合长时间计时 - 启动慢:可能需要几秒钟
典型应用: - 实时时钟(RTC) - 低功耗定时器 - 独立看门狗时钟
// LSE配置示例
void LSE_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
// 使能PWR时钟
__HAL_RCC_PWR_CLK_ENABLE();
// 使能备份域访问
HAL_PWR_EnableBkUpAccess();
// 配置LSE
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE;
RCC_OscInitStruct.LSEState = RCC_LSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
}
1.3 内部高速RC振荡器(HSI - High Speed Internal)¶
片内RC振荡器,无需外部元件。
特点: - 无需外部元件:降低成本和PCB面积 - 快速启动:微秒级启动时间 - 精度较低:典型精度±1% - 温度漂移:受温度影响较大
典型应用: - 系统启动时的默认时钟 - 低成本应用 - 备用时钟源 - 独立看门狗时钟
// HSI配置示例
void HSI_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
// 配置HSI
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
}
HSI校准:
// HSI频率校准
void HSI_Calibration(void) {
// 读取校准值(出厂校准)
uint32_t cal_value = RCC->CR & RCC_CR_HSITRIM;
// 可以根据实际测量调整
// 每步约40kHz(16MHz HSI)
cal_value += 2; // 增加约80kHz
// 写入新的校准值
MODIFY_REG(RCC->CR, RCC_CR_HSITRIM, cal_value << RCC_CR_HSITRIM_Pos);
}
1.4 内部低速RC振荡器(LSI - Low Speed Internal)¶
片内低速RC振荡器,用于独立看门狗。
特点: - 超低功耗:适合低功耗模式 - 无需外部元件:降低成本 - 精度很低:±5%或更差 - 频率不固定:通常30-60kHz
典型应用: - 独立看门狗(IWDG) - 低功耗模式下的唤醒 - 对精度要求不高的定时
// LSI配置示例
void LSI_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
// 配置LSI
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI;
RCC_OscInitStruct.LSIState = RCC_LSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
}
1.5 时钟源对比¶
| 时钟源 | 频率范围 | 精度 | 功耗 | 启动时间 | 成本 | 典型应用 |
|---|---|---|---|---|---|---|
| HSE | 4-26MHz | ±20ppm | 中 | 几ms | 高 | 主系统时钟、USB |
| LSE | 32.768kHz | ±20ppm | 极低 | 几秒 | 中 | RTC、低功耗定时 |
| HSI | 8-16MHz | ±1% | 低 | 几μs | 无 | 启动时钟、备用 |
| LSI | 30-60kHz | ±5% | 极低 | 几μs | 无 | 看门狗、低功耗 |
2. PLL(锁相环)配置¶
2.1 PLL的作用¶
PLL(Phase-Locked Loop)用于将低频时钟倍频到高频时钟。
为什么需要PLL? - 降低成本:使用低频晶振(如8MHz)倍频到高频(如168MHz) - 灵活性:可以动态调整系统频率 - 精度保持:倍频后仍保持晶振的精度
PLL基本原理:
2.2 STM32的PLL结构¶
以STM32F4为例,PLL结构如下:
┌─────────────────────────────┐
HSE/HSI ──→ /M ──→ │ VCO (×N) │ ──→ /P ──→ SYSCLK
(8MHz) (÷8) │ (100-432MHz) │ (÷2) (168MHz)
│ │
│ │ ──→ /Q ──→ USB/SDIO
│ │ (÷7) (48MHz)
└─────────────────────────────┘
PLL参数说明: - M(预分频):将输入时钟分频到1-2MHz(推荐2MHz) - N(倍频系数):VCO频率 = 输入频率 × N(50-432) - P(主分频):生成系统时钟(2, 4, 6, 8) - Q(USB分频):生成USB/SDIO时钟(2-15)
约束条件:
1. 输入频率/M = 1-2MHz(推荐2MHz)
2. VCO频率 = (输入频率/M) × N = 100-432MHz
3. 系统时钟 = VCO频率/P ≤ 168MHz(F4系列)
4. USB时钟 = VCO频率/Q = 48MHz(精确)
2.3 PLL配置示例¶
目标:使用8MHz HSE配置系统时钟到168MHz
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
// 配置电源
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
// 配置HSE和PLL
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
// PLL参数计算:
// VCO输入 = HSE/M = 8MHz/8 = 1MHz
// VCO输出 = 1MHz × N = 1MHz × 336 = 336MHz
// SYSCLK = VCO/P = 336MHz/2 = 168MHz
// USB时钟 = VCO/Q = 336MHz/7 = 48MHz
RCC_OscInitStruct.PLL.PLLM = 8; // 预分频
RCC_OscInitStruct.PLL.PLLN = 336; // 倍频
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; // 主分频
RCC_OscInitStruct.PLL.PLLQ = 7; // USB分频
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
// 配置时钟树
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK |
RCC_CLOCKTYPE_SYSCLK |
RCC_CLOCKTYPE_PCLK1 |
RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB = 168MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; // APB1 = 42MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; // APB2 = 84MHz
// 配置Flash等待周期(168MHz需要5个等待周期)
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK) {
Error_Handler();
}
}
2.4 PLL配置计算工具¶
// PLL参数计算辅助函数
typedef struct {
uint32_t input_freq; // 输入频率(Hz)
uint32_t target_freq; // 目标频率(Hz)
uint32_t m; // 预分频
uint32_t n; // 倍频
uint32_t p; // 主分频
uint32_t q; // USB分频
uint32_t actual_freq; // 实际频率
} PLL_Config_t;
// 计算PLL参数
void Calculate_PLL_Params(PLL_Config_t *config) {
// 目标:VCO输入 = 2MHz(推荐值)
config->m = config->input_freq / 2000000;
uint32_t vco_in = config->input_freq / config->m;
// 计算倍频系数
config->n = (config->target_freq * 2) / vco_in;
config->p = 2;
// 计算USB分频(目标48MHz)
uint32_t vco_out = vco_in * config->n;
config->q = vco_out / 48000000;
// 计算实际频率
config->actual_freq = vco_out / config->p;
printf("PLL配置:\n");
printf(" 输入频率:%lu Hz\n", config->input_freq);
printf(" M=%lu, N=%lu, P=%lu, Q=%lu\n",
config->m, config->n, config->p, config->q);
printf(" VCO输入:%lu Hz\n", vco_in);
printf(" VCO输出:%lu Hz\n", vco_out);
printf(" 系统时钟:%lu Hz\n", config->actual_freq);
printf(" USB时钟:%lu Hz\n", vco_out / config->q);
}
// 使用示例
void PLL_Config_Example(void) {
PLL_Config_t pll_config = {
.input_freq = 8000000, // 8MHz HSE
.target_freq = 168000000 // 目标168MHz
};
Calculate_PLL_Params(&pll_config);
}
3. 时钟树结构¶
3.1 时钟树概念¶
时钟树是将系统时钟分配到各个总线和外设的分频网络。
STM32F4时钟树结构:
┌──────────────────────────────────┐
│ 时钟源选择 │
│ HSI / HSE / PLL │
└──────────┬───────────────────────┘
│
↓
SYSCLK (168MHz)
│
┌──────────────┼──────────────┐
│ │ │
↓ ↓ ↓
AHB总线 Cortex系统 其他
(168MHz) 定时器
│
┌───────┼───────┐
│ │ │
↓ ↓ ↓
APB1 APB2 AHB外设
(42MHz) (84MHz) (168MHz)
│ │
↓ ↓
APB1外设 APB2外设
3.2 主要时钟域¶
SYSCLK(系统时钟): - CPU核心时钟 - 最高频率(如STM32F4的168MHz) - 来源:HSI、HSE或PLL
HCLK(AHB时钟): - AHB总线时钟 - 通常等于SYSCLK - 驱动:DMA、内存、GPIO等
PCLK1(APB1时钟): - APB1总线时钟 - 最高频率限制(如42MHz) - 驱动:UART、I2C、SPI1等低速外设
PCLK2(APB2时钟): - APB2总线时钟 - 较高频率(如84MHz) - 驱动:ADC、高速定时器、SPI1等
// 获取各时钟域频率
void Print_Clock_Frequencies(void) {
uint32_t sysclk = HAL_RCC_GetSysClockFreq();
uint32_t hclk = HAL_RCC_GetHCLKFreq();
uint32_t pclk1 = HAL_RCC_GetPCLK1Freq();
uint32_t pclk2 = HAL_RCC_GetPCLK2Freq();
printf("时钟频率:\n");
printf(" SYSCLK: %lu Hz\n", sysclk);
printf(" HCLK: %lu Hz\n", hclk);
printf(" PCLK1: %lu Hz\n", pclk1);
printf(" PCLK2: %lu Hz\n", pclk2);
}
3.3 时钟分频配置¶
// 配置时钟分频
void Clock_Divider_Config(void) {
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
// 配置所有时钟域
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK |
RCC_CLOCKTYPE_SYSCLK |
RCC_CLOCKTYPE_PCLK1 |
RCC_CLOCKTYPE_PCLK2;
// 选择系统时钟源
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
// AHB分频:HCLK = SYSCLK / 1 = 168MHz
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
// APB1分频:PCLK1 = HCLK / 4 = 42MHz(不能超过42MHz)
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
// APB2分频:PCLK2 = HCLK / 2 = 84MHz(不能超过84MHz)
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
// 应用配置(需要设置Flash等待周期)
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK) {
Error_Handler();
}
}
分频选项:
// AHB分频选项
RCC_SYSCLK_DIV1 // 不分频
RCC_SYSCLK_DIV2 // 2分频
RCC_SYSCLK_DIV4 // 4分频
RCC_SYSCLK_DIV8 // 8分频
RCC_SYSCLK_DIV16 // 16分频
// ... 最大512分频
// APB分频选项
RCC_HCLK_DIV1 // 不分频
RCC_HCLK_DIV2 // 2分频
RCC_HCLK_DIV4 // 4分频
RCC_HCLK_DIV8 // 8分频
RCC_HCLK_DIV16 // 16分频
3.4 定时器时钟倍频¶
重要特性:APB1和APB2上的定时器时钟有特殊规则。
示例:
HCLK = 168MHz
APB1分频 = 4 → PCLK1 = 42MHz
APB1定时器时钟 = 42MHz × 2 = 84MHz
APB2分频 = 2 → PCLK2 = 84MHz
APB2定时器时钟 = 84MHz × 2 = 168MHz
// 获取定时器时钟频率
uint32_t Get_Timer_Clock(TIM_TypeDef *TIMx) {
uint32_t timer_clock;
// 判断定时器在哪个APB总线上
if (TIMx == TIM2 || TIMx == TIM3 || TIMx == TIM4 || TIMx == TIM5) {
// APB1定时器
uint32_t pclk1 = HAL_RCC_GetPCLK1Freq();
RCC_ClkInitTypeDef clk_init;
uint32_t flash_latency;
HAL_RCC_GetClockConfig(&clk_init, &flash_latency);
if (clk_init.APB1CLKDivider == RCC_HCLK_DIV1) {
timer_clock = pclk1;
} else {
timer_clock = pclk1 * 2;
}
} else {
// APB2定时器
uint32_t pclk2 = HAL_RCC_GetPCLK2Freq();
RCC_ClkInitTypeDef clk_init;
uint32_t flash_latency;
HAL_RCC_GetClockConfig(&clk_init, &flash_latency);
if (clk_init.APB2CLKDivider == RCC_HCLK_DIV1) {
timer_clock = pclk2;
} else {
timer_clock = pclk2 * 2;
}
}
return timer_clock;
}
4. 外设时钟管理¶
4.1 时钟门控¶
为了降低功耗,未使用的外设时钟应该关闭。
时钟使能寄存器: - AHB1ENR:AHB1总线外设时钟使能 - AHB2ENR:AHB2总线外设时钟使能 - APB1ENR:APB1总线外设时钟使能 - APB2ENR:APB2总线外设时钟使能
// 使能外设时钟
void Enable_Peripheral_Clocks(void) {
// 使能GPIO时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// 使能UART时钟
__HAL_RCC_USART1_CLK_ENABLE(); // APB2
__HAL_RCC_USART2_CLK_ENABLE(); // APB1
// 使能定时器时钟
__HAL_RCC_TIM2_CLK_ENABLE(); // APB1
__HAL_RCC_TIM3_CLK_ENABLE(); // APB1
// 使能DMA时钟
__HAL_RCC_DMA1_CLK_ENABLE();
__HAL_RCC_DMA2_CLK_ENABLE();
}
// 禁用外设时钟(节省功耗)
void Disable_Unused_Clocks(void) {
// 禁用未使用的GPIO
__HAL_RCC_GPIOC_CLK_DISABLE();
__HAL_RCC_GPIOD_CLK_DISABLE();
// 禁用未使用的外设
__HAL_RCC_SPI2_CLK_DISABLE();
__HAL_RCC_I2C2_CLK_DISABLE();
}
4.2 外设时钟源选择¶
某些外设可以选择独立的时钟源。
RTC时钟源:
// 配置RTC时钟源
void RTC_Clock_Config(void) {
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
// 选择LSE作为RTC时钟源
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) {
Error_Handler();
}
// 使能RTC时钟
__HAL_RCC_RTC_ENABLE();
}
I2S时钟源:
// 配置I2S时钟
void I2S_Clock_Config(void) {
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
// 配置PLLI2S
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_I2S;
PeriphClkInit.PLLI2S.PLLI2SN = 192;
PeriphClkInit.PLLI2S.PLLI2SR = 2;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) {
Error_Handler();
}
}
4.3 时钟输出(MCO)¶
可以将内部时钟输出到GPIO引脚,用于调试或为外部设备提供时钟。
// 配置MCO输出
void MCO_Config(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA8为MCO功能
GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF0_MCO;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 输出SYSCLK到MCO1(PA8)
HAL_RCC_MCOConfig(RCC_MCO1, RCC_MCO1SOURCE_SYSCLK, RCC_MCODIV_4);
// 输出频率 = SYSCLK / 4 = 168MHz / 4 = 42MHz
}
MCO输出选项:
// MCO1可选时钟源(PA8)
RCC_MCO1SOURCE_HSI // HSI时钟
RCC_MCO1SOURCE_LSE // LSE时钟
RCC_MCO1SOURCE_HSE // HSE时钟
RCC_MCO1SOURCE_PLLCLK // PLL时钟
// MCO2可选时钟源(PC9)
RCC_MCO2SOURCE_SYSCLK // 系统时钟
RCC_MCO2SOURCE_PLLI2SCLK // PLLI2S时钟
RCC_MCO2SOURCE_HSE // HSE时钟
RCC_MCO2SOURCE_PLLCLK // PLL时钟
// 分频选项
RCC_MCODIV_1 // 不分频
RCC_MCODIV_2 // 2分频
RCC_MCODIV_3 // 3分频
RCC_MCODIV_4 // 4分频
RCC_MCODIV_5 // 5分频
实践示例¶
示例1:完整的时钟系统配置¶
从零开始配置一个完整的时钟系统:
#include "stm32f4xx_hal.h"
// 系统时钟配置:HSE 8MHz → PLL → 168MHz
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
// 1. 配置电源调节器
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
// 2. 配置HSE和PLL
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 8; // 8MHz / 8 = 1MHz
RCC_OscInitStruct.PLL.PLLN = 336; // 1MHz × 336 = 336MHz
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; // 336MHz / 2 = 168MHz
RCC_OscInitStruct.PLL.PLLQ = 7; // 336MHz / 7 = 48MHz (USB)
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
// 3. 配置时钟树
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK |
RCC_CLOCKTYPE_SYSCLK |
RCC_CLOCKTYPE_PCLK1 |
RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // 168MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; // 42MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; // 84MHz
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK) {
Error_Handler();
}
// 4. 配置SysTick(1ms中断)
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / 1000);
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}
int main(void) {
// 系统初始化
HAL_Init();
// 配置时钟
SystemClock_Config();
// 打印时钟信息
printf("系统时钟配置完成\n");
printf("SYSCLK: %lu Hz\n", HAL_RCC_GetSysClockFreq());
printf("HCLK: %lu Hz\n", HAL_RCC_GetHCLKFreq());
printf("PCLK1: %lu Hz\n", HAL_RCC_GetPCLK1Freq());
printf("PCLK2: %lu Hz\n", HAL_RCC_GetPCLK2Freq());
while(1) {
// 主循环
}
}
示例2:动态时钟切换¶
在运行时切换时钟源和频率:
// 切换到HSI(低功耗模式)
void Switch_To_HSI(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.PLL.PLLState = RCC_PLL_NONE;
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和HSE(节省功耗)
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_OFF;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_OFF;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
printf("已切换到HSI,SYSCLK = %lu Hz\n", HAL_RCC_GetSysClockFreq());
}
// 切换回PLL(高性能模式)
void Switch_To_PLL(void) {
// 重新配置系统时钟
SystemClock_Config();
printf("已切换到PLL,SYSCLK = %lu Hz\n", HAL_RCC_GetSysClockFreq());
}
// 动态调频示例
void Dynamic_Frequency_Scaling(void) {
printf("进入低功耗模式\n");
Switch_To_HSI();
// 执行低功耗任务
HAL_Delay(5000);
printf("进入高性能模式\n");
Switch_To_PLL();
// 执行高性能任务
HAL_Delay(5000);
}
示例3:时钟监控和故障处理¶
实现时钟安全系统(CSS)和故障恢复:
// 使能时钟安全系统
void Enable_CSS(void) {
// CSS会监控HSE,如果HSE失效,自动切换到HSI
HAL_RCC_EnableCSS();
printf("时钟安全系统已使能\n");
}
// NMI中断处理(CSS触发)
void NMI_Handler(void) {
// 检查是否是CSS触发的NMI
if (__HAL_RCC_GET_FLAG(RCC_FLAG_CSSF)) {
printf("警告:HSE时钟失效!\n");
printf("系统已自动切换到HSI\n");
// 清除CSS标志
__HAL_RCC_CLEAR_IT(RCC_IT_CSS);
// 执行故障处理
Clock_Failure_Handler();
}
}
// 时钟故障处理
void Clock_Failure_Handler(void) {
// 1. 记录故障
printf("时钟故障时间:%lu ms\n", HAL_GetTick());
// 2. 降低系统性能要求
// 禁用非关键外设
__HAL_RCC_DMA2_CLK_DISABLE();
__HAL_RCC_ADC1_CLK_DISABLE();
// 3. 尝试恢复HSE
for (int i = 0; i < 3; i++) {
HAL_Delay(100);
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) == HAL_OK) {
printf("HSE恢复成功,重新配置PLL\n");
SystemClock_Config();
return;
}
}
// 4. 如果无法恢复,继续使用HSI
printf("HSE无法恢复,继续使用HSI运行\n");
}
// 时钟状态监控
void Monitor_Clock_Status(void) {
printf("\n=== 时钟状态监控 ===\n");
// 检查各时钟源状态
if (__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY)) {
printf("HSE: 就绪\n");
} else {
printf("HSE: 未就绪\n");
}
if (__HAL_RCC_GET_FLAG(RCC_FLAG_HSIRDY)) {
printf("HSI: 就绪\n");
} else {
printf("HSI: 未就绪\n");
}
if (__HAL_RCC_GET_FLAG(RCC_FLAG_PLLRDY)) {
printf("PLL: 就绪\n");
} else {
printf("PLL: 未就绪\n");
}
if (__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY)) {
printf("LSE: 就绪\n");
} else {
printf("LSE: 未就绪\n");
}
// 打印当前时钟源
uint32_t sysclk_source = __HAL_RCC_GET_SYSCLK_SOURCE();
printf("当前系统时钟源: ");
switch (sysclk_source) {
case RCC_SYSCLKSOURCE_STATUS_HSI:
printf("HSI\n");
break;
case RCC_SYSCLKSOURCE_STATUS_HSE:
printf("HSE\n");
break;
case RCC_SYSCLKSOURCE_STATUS_PLLCLK:
printf("PLL\n");
break;
default:
printf("未知\n");
break;
}
printf("==================\n\n");
}
深入理解¶
Flash等待周期¶
当系统时钟频率提高时,必须增加Flash等待周期,否则CPU可能读取到错误的指令。
为什么需要等待周期? - Flash存储器的访问速度有限 - 高频率下,一个时钟周期内Flash无法完成读取 - 需要插入等待周期让Flash有足够时间响应
STM32F4的Flash等待周期:
| 系统时钟频率 | 电压范围2.7-3.6V | 电压范围2.4-2.7V | 电压范围2.1-2.4V |
|---|---|---|---|
| 0-30MHz | 0 WS | 0 WS | 0 WS |
| 30-60MHz | 1 WS | 1 WS | 2 WS |
| 60-90MHz | 2 WS | 2 WS | 3 WS |
| 90-120MHz | 3 WS | 3 WS | 4 WS |
| 120-150MHz | 4 WS | 4 WS | 5 WS |
| 150-168MHz | 5 WS | 5 WS | 6 WS |
// 配置Flash等待周期
void Configure_Flash_Latency(uint32_t sysclk_freq) {
uint32_t latency;
// 根据频率选择等待周期(假设电压2.7-3.6V)
if (sysclk_freq <= 30000000) {
latency = FLASH_LATENCY_0;
} else if (sysclk_freq <= 60000000) {
latency = FLASH_LATENCY_1;
} else if (sysclk_freq <= 90000000) {
latency = FLASH_LATENCY_2;
} else if (sysclk_freq <= 120000000) {
latency = FLASH_LATENCY_3;
} else if (sysclk_freq <= 150000000) {
latency = FLASH_LATENCY_4;
} else {
latency = FLASH_LATENCY_5;
}
// 配置Flash等待周期
__HAL_FLASH_SET_LATENCY(latency);
// 验证配置
if (__HAL_FLASH_GET_LATENCY() != latency) {
Error_Handler();
}
}
时钟启动时间¶
不同时钟源的启动时间差异很大:
| 时钟源 | 典型启动时间 | 说明 |
|---|---|---|
| HSI | 1-2 μs | 最快,适合快速唤醒 |
| LSI | 85 μs | 较快 |
| HSE | 几ms | 取决于晶振和负载电容 |
| LSE | 1-3秒 | 最慢,但最稳定 |
| PLL | 200 μs | 需要HSE/HSI先就绪 |
// 等待时钟源就绪
HAL_StatusTypeDef Wait_For_Clock_Ready(uint32_t clock_flag, uint32_t timeout_ms) {
uint32_t tickstart = HAL_GetTick();
// 等待时钟就绪
while (!__HAL_RCC_GET_FLAG(clock_flag)) {
if ((HAL_GetTick() - tickstart) > timeout_ms) {
return HAL_TIMEOUT;
}
}
return HAL_OK;
}
// 使用示例
void HSE_Init_With_Timeout(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
printf("HSE启动失败\n");
// 回退到HSI
Fallback_To_HSI();
} else {
printf("HSE启动成功\n");
}
}
时钟精度和抖动¶
时钟精度: - 晶振精度:±20ppm(百万分之二十) - RC振荡器精度:±1%(10000ppm)
计算示例:
8MHz晶振,精度±20ppm:
误差 = 8MHz × 20/1000000 = 160Hz
实际频率范围:7.999840MHz - 8.000160MHz
16MHz HSI,精度±1%:
误差 = 16MHz × 1/100 = 160kHz
实际频率范围:15.84MHz - 16.16MHz
时钟抖动(Jitter): - 时钟周期的短期变化 - 影响高速通信和ADC采样 - 使用高质量晶振和良好的PCB设计可以减小抖动
最佳实践¶
-
选择合适的时钟源
-
合理配置PLL参数
-
注意APB频率限制
-
使用时钟安全系统
-
功耗优化
常见问题¶
Q1: 为什么系统时钟配置后程序不工作?¶
A: 可能的原因和解决方法:
-
Flash等待周期配置错误
-
PLL参数超出范围
-
APB频率超过限制
-
HSE未正确启动
Q2: 如何选择合适的晶振频率?¶
A: 选择依据:
常用晶振频率: - 8MHz:最常用,适合大多数应用 - 12MHz:适合USB应用(易于生成48MHz) - 16MHz:适合高性能应用 - 25MHz:适合以太网应用
选择原则:
// 1. 能否生成所需的系统时钟?
// 例如:目标168MHz
8MHz → PLL → 168MHz ✓ (常用)
12MHz → PLL → 168MHz ✓
16MHz → PLL → 168MHz ✓
// 2. 能否生成精确的USB时钟(48MHz)?
8MHz → PLL → 48MHz ✓ (336MHz / 7 = 48MHz)
12MHz → PLL → 48MHz ✓ (288MHz / 6 = 48MHz)
25MHz → PLL → 48MHz ✗ (难以精确生成)
// 3. 成本和可用性
8MHz晶振:最便宜,最容易获得
Q3: 定时器时钟频率如何计算?¶
A: 定时器时钟有特殊规则:
// 规则:
// 如果APBx分频 = 1,定时器时钟 = APBx时钟
// 如果APBx分频 > 1,定时器时钟 = APBx时钟 × 2
// 示例1:APB1分频 = 4
HCLK = 168MHz
APB1分频 = 4 → PCLK1 = 42MHz
TIM2-7时钟 = 42MHz × 2 = 84MHz // 因为分频>1
// 示例2:APB1分频 = 1
HCLK = 42MHz
APB1分频 = 1 → PCLK1 = 42MHz
TIM2-7时钟 = 42MHz // 因为分频=1,不倍频
获取定时器时钟的函数:
uint32_t Get_TIM_Clock_Freq(TIM_TypeDef *TIMx) {
uint32_t pclk, timer_clk;
RCC_ClkInitTypeDef clk_init;
uint32_t flash_latency;
HAL_RCC_GetClockConfig(&clk_init, &flash_latency);
// 判断定时器在哪个APB上
if (/* TIMx在APB1上 */) {
pclk = HAL_RCC_GetPCLK1Freq();
if (clk_init.APB1CLKDivider == RCC_HCLK_DIV1) {
timer_clk = pclk;
} else {
timer_clk = pclk * 2;
}
} else {
pclk = HAL_RCC_GetPCLK2Freq();
if (clk_init.APB2CLKDivider == RCC_HCLK_DIV1) {
timer_clk = pclk;
} else {
timer_clk = pclk * 2;
}
}
return timer_clk;
}
Q4: 如何在运行时切换时钟频率?¶
A: 动态切换时钟的步骤:
// 从高频切换到低频(降低功耗)
void Switch_To_Low_Frequency(void) {
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
// 1. 先降低Flash等待周期(可选)
// 注意:必须在降频之前或之后,不能同时
// 2. 切换到HSI
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0);
// 3. 关闭PLL(节省功耗)
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_OFF;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
// 4. 更新SystemCoreClock变量
SystemCoreClockUpdate();
}
// 从低频切换到高频(提高性能)
void Switch_To_High_Frequency(void) {
// 1. 使能并配置PLL
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 8;
RCC_OscInitStruct.PLL.PLLN = 336;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 7;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
// 2. 先增加Flash等待周期
__HAL_FLASH_SET_LATENCY(FLASH_LATENCY_5);
// 3. 切换到PLL
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5);
// 4. 更新SystemCoreClock变量
SystemCoreClockUpdate();
}
注意事项: - 切换时钟时,外设可能会受影响 - 需要重新配置依赖时钟的外设(如UART波特率) - Flash等待周期必须正确配置
Q5: 时钟配置工具(STM32CubeMX)生成的代码可靠吗?¶
A: STM32CubeMX生成的时钟配置代码通常是可靠的,但需要注意:
优点: - 自动检查参数合法性 - 图形化配置,直观易懂 - 自动计算PLL参数 - 生成完整的初始化代码
注意事项:
// 1. 检查生成的参数是否符合需求
void Verify_Clock_Config(void) {
printf("SYSCLK: %lu Hz\n", HAL_RCC_GetSysClockFreq());
printf("HCLK: %lu Hz\n", HAL_RCC_GetHCLKFreq());
printf("PCLK1: %lu Hz\n", HAL_RCC_GetPCLK1Freq());
printf("PCLK2: %lu Hz\n", HAL_RCC_GetPCLK2Freq());
}
// 2. 验证USB时钟是否精确48MHz
uint32_t usb_clk = /* 计算USB时钟 */;
if (usb_clk != 48000000) {
printf("警告:USB时钟不精确:%lu Hz\n", usb_clk);
}
// 3. 检查Flash等待周期
uint32_t latency = __HAL_FLASH_GET_LATENCY();
printf("Flash等待周期:%lu\n", latency);
建议: - 使用CubeMX生成初始配置 - 理解生成的代码 - 根据实际需求调整 - 在实际硬件上验证
总结¶
本文详细介绍了嵌入式系统的时钟系统架构和配置方法,核心要点包括:
- 时钟源选择:HSE/HSI/LSE/LSI各有特点,根据精度、功耗、成本需求选择
- PLL配置:通过预分频、倍频、后分频实现灵活的频率生成
- 时钟树结构:SYSCLK → HCLK → PCLK1/PCLK2,合理分配时钟到各总线
- 外设时钟管理:使用时钟门控降低功耗,注意定时器时钟的特殊规则
- 时钟安全:使用CSS监控时钟,实现故障恢复机制
掌握时钟系统配置是嵌入式开发的基础技能,正确的时钟配置能够确保系统稳定运行并优化功耗。
延伸阅读¶
- 系统滴答定时器(SysTick)应用 - 学习使用SysTick实现精确延时
- 通用定时器高级应用 - 深入学习定时器的高级功能
- 低功耗时钟管理策略 - 学习功耗优化技术
参考资料¶
- STM32F4xx Reference Manual - ST官方参考手册
- AN4055: Clock configuration tool for STM32F40x/41x microcontrollers - ST应用笔记
- "Mastering STM32" by Carmine Noviello - 第3章:时钟系统
- STM32CubeMX User Manual - ST官方用户手册
练习题:
- 计算使用8MHz HSE配置系统时钟到180MHz(STM32F4系列)所需的PLL参数。
- 解释为什么APB1定时器时钟可以达到84MHz,而APB1总线时钟只有42MHz。
- 编写一个函数,在运行时动态切换系统时钟频率,并验证切换是否成功。
- 设计一个时钟监控系统,能够检测时钟故障并自动恢复。
下一步:建议学习 系统滴答定时器(SysTick)应用,了解如何使用SysTick实现精确的时间基准。