前言
上次写系统时钟解析的时候说出一篇103的时钟解析,我就整理HAL库开发的正点的资料,给小白梳理,我也是小白,不做权威使用。
时钟树详解
在 STM32 中,有五个时钟源,为 HSI、HSE、LSI、LSE、PLL。从时钟频率来分可以分为高速时钟源和低速时钟源,在这 5 个中 HIS,HSE 以及 PLL 是高速时钟,LSI 和 LSE是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中 HSE 和 LSE 是外部时钟源,其他的是内部时钟源。下面我们看看 STM32 的5 个时钟源,我们讲解顺序是按图中红圈标示的顺序:
①、HSI 是高速内部时钟,RC 振荡器,频率为 8MHz。
②、HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为 4MHz~16MHz。
③、LSI 是低速内部时钟,RC 振荡器,频率为 40kHz。独立看门狗的时钟源只能是 LSI,同时 LSI 还可以作为 RTC 的时钟源。
④、LSE 是低速外部时钟,接频率为 32.768kHz 的石英晶体。这个主要是 RTC 的时钟源。
⑤、PLL 为锁相环倍频输出,其时钟输入源可选择为 HSI/2、HSE 或者 HSE/2。倍频可选择为 2~16 倍,但是其输出频率最大不得超过 72MHz。
上面我们简要概括了 STM32 的时钟源,那么这 5 个时钟源是怎么给各个外设以及系统提供时钟的呢?这里我们将一一讲解。我们还是从图的下方讲解起吧,因为下方比较简单。图中我们用 A~E 标示我们要讲解的地方。
A. MCO 是 STM32 的一个时钟输出 IO(PA8),它可以选择一个时钟信号输出,可以选择为 PLL 输出的 2 分频、HSI、HSE、或者系统时钟。这个时钟可以用来给外部其他系统提供时钟源。
B. 这里是 RTC 时钟源,从图上可以看出,RTC 的时钟源可以选择 LSI,LSE,以及HSE 的 128 分频。
C. 从图中可以看出 C 处 USB 的时钟是来自 PLL 时钟源。STM32 中有一个全速功能的 USB 模块,其串行接口引擎需要一个频率为 48MHz 的时钟源。该时钟源只能从 PLL 输出端获取,可以选择为 1.5 分频或者 1 分频,也就是,当需要使用 USB模块时,PLL 必须使能,并且时钟频率配置为 48MHz 或 72MHz。
D.处就是 STM32 的系统时钟 SYSCLK,它是供 STM32 中绝大部分部件工作的时钟源。系统时钟可选择为 PLL 输出、HSI 或者 HSE。系统时钟最大频率为 72MHz,当然你也可以超频,不过一般情况为了系统稳定性是没有必要冒风险去超频的。
E. 这里的 E 处是指其他所有外设了。从时钟图上可以看出,其他所有外设的时钟最终来源都是 SYSCLK。SYSCLK 通过 AHB 分频器分频后送给各模块使用。这些模块包括:
①、AHB 总线、内核、内存和 DMA 使用的 HCLK 时钟。
②、通过 8 分频后送给 Cortex 的系统定时器时钟,也就是 systick 了。
③、直接送给 Cortex 的空闲运行时钟 FCLK。
④、送给 APB1 分频器。APB1 分频器输出一路供 APB1 外设使用(PCLK1,最大频率 36MHz),另一路送给定时器(Timer)2、3、4 倍频器使用。
⑤、送给 APB2 分频器。APB2 分频器分频输出一路供 APB2 外设使用(PCLK2,最大频率 72MHz),另一路送给定时器(Timer)1 倍频器使用。
其中需要理解的是 APB1 和 APB2 的区别,APB1 上面连接的是低速外设,包括电源接口、备份接口、CAN、USB、I2C1、I2C2、UART2、UART3 等等,APB2 上面连接的是高速外设包 括 UART1、SPI1、Timer1、ADC1、ADC2、所有普通 IO 口(PA~PE)、第二功能 IO 口等。居宁老师的《稀里糊涂玩 STM32》资料里面教大家的记忆方法是 2>1, APB2 下面所挂的外设的时钟要比 APB1 的高。
在以上的时钟输出中,有很多是带使能控制的,例如 AHB 总线时钟、内核时钟、各种 APB1 外设、APB2 外设等等。当需要使用某模块时,记得一定要先使能对应的时钟。
系统时钟配置
在系统启动之后,程序会先执行 HAL 库定义的 SystemInit 函数,进行系统一些初始化配置。 SystemInit 程序中主要做了以下三个方面的工作:
1) 复位 RCC 时钟配置为默认复位值(默认开始了 HIS)
2) 外部存储器配置
3) 中断向量表地址配置
HAL 库的 SystemInit 函数并没有像标准库的 SystemInit 函数一样进行时钟的初始化配置。HAL 库的 SystemInit 函数除了打开 HSI 之外,没有任何时钟相关配置,所以使用 HAL 库我们必须编 写自己的时钟配置函数。首先我们打开工程模板看看我们在工程 SYSTEM 分组下面定义的 sys.c中包含,也可以使用Cubemx自动生成配置代码,具体可以参考STM32Cubemx的总结手册。
//时钟系统配置函数
//PLL:选择的倍频数,RCC_PLL_MUL2~RCC_PLL_MUL16
//返回值:0,成功;1,失败
void Stm32_Clock_Init(u32 PLL)
{
HAL_StatusTypeDef ret = HAL_OK;
RCC_OscInitTypeDef RCC_OscInitStructure;
RCC_ClkInitTypeDef RCC_ClkInitStructure;
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打开 HSE
RCC_OscInitStructure.HSEPredivValue=RCC_HSE_PREDIV_DIV1; //HSE 预分频
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON;
//打开 PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;
//PLL 时钟源选择 HSE
RCC_OscInitStructure.PLL.PLLMUL=PLL;
//主 PLL 倍频因子
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化
if(ret!=HAL_OK) while(1);
//选中 PLL 作为系统时钟源并且配置 HCLK,PCLK1 和 PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|
RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|
RCC_CLOCKTYPE_PCLK2);
//设置系统时钟时钟源为 PLL
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1;
//AHB 分频系数为 1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV2;
//APB1 分频系数为 2
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV1;
//APB2 分频系数为 1
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_2);
//同时设置 FLASH 延时周期为 2WS,也就是 3 个 CPU 周期。
if(ret!=HAL_OK) while(1);
}
在sys.c文件中,有一个Stm32_Clock_Init()函数,这个函数里面是时钟的配置。除了配置 PLL 相关参数确定 SYSCLK 值之外,还配置了 AHB,APB1 和 APB2 的分频系数,也就是确定了HCLK,PCLK1 和 PCLK2 的时钟值。
typedef struct
{
uint32_t OscillatorType; //需要选择配置的振荡器类型
uint32_t HSEState; //HSE 状态
uint32_t HSEPredivValue; // Prediv1 值
uint32_t LSEState; //LSE 状态
uint32_t HSIState; //HIS 状态
uint32_t HSICalibrationValue; //HIS 校准值
uint32_t LSIState;
//LSI 状态
RCC_PLLInitTypeDef PLL; //PLL 配置
}RCC_OscInitTypeDef;
typedef struct
{
uint32_t PLLState; //PLL 状态
uint32_t PLLSource; //PLL 时钟源
uint32_t PLLMUL; //PLL VCO 输入时钟的乘法因子
}RCC_PLLInitTypeDef;
在这个函数中初始化一个结构体,关于RCC_OscInitTypeDef外部晶振的结构体,里面包含选择配置振荡器的类型。在这个结构体中有关于PLL的配置的成员变量,RCC_PLLInitTypeDef 结构体,里面又包含了PLL状态、 PLL时钟源、PLL输入时钟的乘法因子,这三个成员变量主要是设置PLL的分频倍频参数。
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为 HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打开 HSE
RCC_OscInitStructure.HSEPredivValue=RCC_HSE_PREDIV_DIV1; //HSE 预分频
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON;
//打开 PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE;
//PLL 时钟源选择 HSE
RCC_OscInitStructure.PLL.PLLMUL=PLL;
//主 PLL 倍频因子
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化
在时钟配置函数中开启了 HSE 时钟源,同时选择 PLL 时钟源为 HSE,然后把Stm32_Clock_Init 的唯一的入口参数直接设置作为 PLL 的倍频因子。设置好 PLL 时钟源参数之后,也就是确定了 PLL 的时钟频率,接下来我们就需要设置系统时钟,以及 AHB,APB1 和 APB2 相关参数。
HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct,
uint32_t FLatency);
该函数有两个入口参数,第一个入口参数 RCC_ClkInitStruct 是结构体RCC_ClkInitTypeDef 指针类型,用来设置 SYSCLK 时钟源以及 AHB,APB1 和 APB2 的分频系数。第二个入口参数 FLatency 用来设置 FLASH 延迟。
//选中 PLL 作为系统时钟源并且配置 HCLK,PCLK1 和 PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|
RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|
RCC_CLOCKTYPE_PCLK2);
//设置系统时钟时钟源为 PLL
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1; //AHB 分频系数为 1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV2; //APB1 分频系数为 2
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV1; //APB2 分频系数为 1
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_2);
//同时设置 FLASH 延时周期为 2WS,也就是 3 个 CPU 周期。
第一个参数 ClockType 配置说明我们要配置的是 SYSCLK,HCLK,PCLK1 和 PCLK2 四个时钟。
第二个参数 SYSCLKSource 配置选择系统时钟源为 PLL。
第三个参数 AHBCLKDivider 配置 AHB 分频系数为 1。
第四个参数 APB1CLKDivider 配置 APB1 分频系数为 2。
第五个参数 APB2CLKDivider 配置 APB2 分频系数为 1。
根据我们在主函数中调用 Stm32_Clock_Init(RCC_PLL_MUL9)时候设置的入口参数值,我 们可以计算出,PLL 时钟为 PLLCLK=HSE9 =8MHz9=72MHz,同时我们选择系统时钟源为 PLL , 所 以 系 统 时 钟 SYSCLK=72MHz 。 AHB 分频系数为 1 ,故其频率为 HCLK=SYSCLK/1=72MHz。APB1 分频系数为 2,故其频率为 PCLK1=HCLK/2=36MHz。APB2 分频系数为 1,故其频率为 PCLK2=HCLK/1=72/1=72MHz。最后我们总结一下通过调用函数 Stm32_Clock_Init(RCC_PLL_MUL9)之后的关键时钟频率值:
SYSCLK(系统时钟) =72MHz
PLL 主时钟 =72MHz
AHB 总线时钟(HCLK=SYSCLK/1) =72MHz
APB1 总线时钟(PCLK1=HCLK/2) =36MHz
APB2 总线时钟(PCLK2=HCLK/1) =72MHz
系统时钟使能配置
在 STM32F1 的 HAL 库中,外设时钟使能操作都是在 RCC 相关固件库文件头文件 stm32f1xx_hal_rcc.h 定义的。大家打开stm32f1xx_hal_rcc.h 头文件可以看到文件中除了少数几 个函数声明之外大部分都是宏定义标识符。外设时钟使能在 HAL 库中都是通过宏定义标识符来实现的。我们的宏是__HAL_RCC_GPIOA_CLK_ENABLE(),宏定义里面有SET_BIT()函数,SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN); 意思很清楚哈,里面是寄存器配置APB2ENR,后面是配置的位,是一个宏
#define RCC_APB2ENR_IOPAEN ((uint32_t)0x00000001)
作用是设置寄存器 RCC->APB2ENR 寄存器的位 2 为 1。我们可以从 STM32F1 的中文参考手册中搜索 APB2ENR 寄存器定义,位 2 的作用是用来使用 GPIOA 时钟。而配置描述如下:
IOPAEN:IO 端口 A 时钟使能
由软件置 1 和清零
0:禁止 IO 端口 A 时钟
1:使能 IO 端口 A 时钟
那么我们对各个时钟的使能就可以用以下方式表述:
__HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 时钟
__HAL_RCC_DMA1_CLK_ENABLE();//使能 DMA1 时钟
__HAL_RCC_USART2_CLK_ENABLE();//使能串口 2 时钟
__HAL_RCC_TIM1_CLK_ENABLE();//使能 TIM1 时钟
我们使用外设的时候需要使能外设时钟,如果我们不需要使用某个外设,同样我们可以禁止某个外设时钟。禁止外设时钟使用方法和使能外设时钟非常类似,同样是头文件中定义的宏定义标识符。我们同样以 GPIOA 为例,宏定义标识符为:
#define __HAL_RCC_GPIOA_CLK_DISABLE() \
(RCC->APB2ENR &= ~(RCC_APB2ENR_IOPAEN))
禁止外设时钟的设置为:
__HAL_RCC_DMA1_CLK_DISABLE();//禁止 DMA1 时钟
__HAL_RCC_USART2_CLK_DISABLE();//禁止串口 2 时钟
__HAL_RCC_TIM1_CLK_DISABLE();//禁止 TIM1 时钟