【STM32】STM32F1时钟系统

00. 目录

01. STM32概述

STM32是一款由意法半导体(STMicroelectronics,简称ST)公司制造的微控制器(MCU),它基于ARM Cortex-M内核设计,专为要求高性能、低成本、低功耗的嵌入式应用而设计。以下是对STM32的详细介绍:

产品概述

  • 内核:STM32采用了ARM Cortex-M系列内核,包括M0、M0+、M3、M4和M7等多种系列,能够满足不同应用场景的需求。
  • 特点:高性能、低成本、低功耗、可裁剪,这些特点使得STM32在嵌入式开发领域具有广泛的应用前景。
  • 应用领域:STM32微控制器广泛应用于工业自动化、汽车电子、消费电子、物联网、无人机、嵌入式设备等多个领域。

产品系列

STM32系列微控制器按照内核架构和性能特点可以分为多个系列,主要包括:

  • 主流产品:如STM32F0、STM32F1、STM32F3等,适用于一般的嵌入式应用。
  • 超低功耗产品:如STM32L0、STM32L1、STM32L4等,专为低功耗设计,适用于对功耗要求较高的应用。
  • 高性能产品:如STM32F2、STM32F4、STM32F7、STM32H7等,具有更高的处理速度和更强的外设功能,适用于需要高性能的应用场景。

技术特点

  • 高性能:STM32微控制器的主频可达72MHz以上,能够满足各种高速数据处理需求。
  • 丰富的外设资源:STM32集成了GPIO、USART、SPI、I2C等多种外设资源,可满足各种通信和控制需求。
  • 低功耗:STM32采用了先进的低功耗技术,如休眠模式、待机模式等,可大幅降低功耗。
  • 易用的开发工具:STM32提供了丰富的开发工具,如Keil、IAR等,支持多种编程语言(如C、C++、汇编等),方便开发人员进行开发和调试。

02. STM32F103时钟树概述

众所周知,时钟系统是 CPU 的脉搏,就像人的心跳一样。所以时钟系统的重要性就不言而喻了。 STM32F103的时钟系统比较复杂,不像简单的51单片机一个系统时钟就可以解决一切。于是有人要问,采用一个系统时钟不是很简单吗?为什么 STM32 要有多个时钟源呢? 因为首先 STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,比如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。

三种不同的时钟源可被用来驱动系统时钟(SYSCLK):
● HSI振荡器时钟
● HSE振荡器时钟
● PLL时钟

这些设备有以下2种二级时钟源:
● 40kHz低速内部RC,可以用于驱动独立看门狗和通过程序选择驱动RTC。RTC用于从停机/待机模式下自动唤醒系统。
● 32.768kHz低速外部晶体也可用来通过程序选择驱动RTC(RTCCLK)。

当不被使用时,任一个时钟源都可被独立地启动或关闭,由此优化系统功耗。
在这里插入图片描述

在 STM32 中,有五个时钟源,为 HSI、HSE、LSI、LSE、PLL。从时钟频率来分可以分为高速时钟源和低速时钟源,在这 5 个中 HIS,HSE 以及 PLL 是高速时钟,LSI 和 LSE 是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中 HSE 和 LSE 是外部时钟源,其他的是内部时钟源。下面我们看看 STM32 的 5 个时钟源,我们讲解顺序是按图中红圈标示的顺序:

①、HSI 是高速内部时钟,RC 振荡器,频率为 8MHz。

②、HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为 4MHz~16MHz。我们的开发板接的是 8M 的晶振。

③、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. 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 外设等等。当需要使用某模块时,记得一定要先使能对应的时钟。后面我们讲解实例的时候回讲解到时钟使能的方法。

03. STM32F103时钟系统配置

我们对 STM32F103 时钟树进行了详细讲解,接下来我们来讲解通过 STM32F1 的HAL 库进行 STM32F103 时钟系统配置步骤。实际上,STM32F1 的时钟系统配置也可以通过图形化配置工具 STM32CubeMX 来配置生成,这里我们讲解初始化代码,是为了让大家对 STM32
时钟系统有更加清晰的理解。

前面我们讲解过,在系统启动之后,程序会先执行 HAL 库定义的 SystemInit 函数,进行系统一些初始化配置。那么我们先来看看 SystemInit 程序:

void SystemInit (void)
{
    /* 将 RCC 时钟配置重置为默认重置状态(用于调试)*/
    RCC->CR |= (uint32_t)0x00000001; //打开 HSION 位
    /* 设置 SW, HPRE, PPRE1, PPRE2, ADCPRE 和 MCO 位 */
    #if !defined(STM32F105xC) && !defined(STM32F107xC)
    RCC->CFGR &= (uint32_t)0xF8FF0000;
    #else
    RCC->CFGR &= (uint32_t)0xF0FF0000;
    #endif /* STM32F105xC */
    
    RCC->CR &= (uint32_t)0xFEF6FFFF; // 复位 HSEON, CSSON 和 PLLON 位
    RCC->CR &= (uint32_t)0xFFFBFFFF; // 复位 HSEBYP 位
    RCC->CFGR &= (uint32_t)0xFF80FFFF; //复位 CFGR 寄存器
    
    #if defined(STM32F105xC) || defined(STM32F107xC)
    RCC->CR &= (uint32_t)0xEBFFFFFF; // 复位 PLL2ON 和 PLL3ON 位
    RCC->CIR = 0x00FF0000; // 禁用所有中断并清除挂起位
    RCC->CFGR2 = 0x00000000; // 重置 CFGR2 注册
    #elif defined(STM32F100xB) || defined(STM32F100xE)
    RCC->CIR = 0x009F0000; // 禁用所有中断并清除挂起位
    RCC->CFGR2 = 0x00000000; // 重置 CFGR2 注册
    #else
    RCC->CIR = 0x009F0000; // 禁用所有中断并清除挂起位
    #endif /* STM32F105xC */
    
    #if  defined(STM32F100xE)  ||  defined(STM32F101xE)  ||  defined(STM32F101xG)  ||
    defined(STM32F103xE) || defined(STM32F103xG)
    #ifdef DATA_IN_ExtSRAM
    SystemInit_ExtMemCtl();
    #endif /* DATA_IN_ExtSRAM */
    
    #endif
    
    /* 配置中断向量表地址=基地址+偏移地址 ------------------*/
    #ifdef VECT_TAB_SRAM
    SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; //内部 SRAM 中的向量表重定位
    #else
    SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //在内部 FLASH 中的向量表重定位
#endif
}

从上面代码可以看出,SystemInit 主要做了如下三个方面工作:
1) 复位 RCC 时钟配置为默认复位值(默认开始了 HIS)
2) 外部存储器配置
3) 中断向量表地址配置

HAL 库的 SystemInit 函数并没有像标准库的 SystemInit 函数一样进行时钟的初始化配置。HAL库的 SystemInit 函数除了打开 HSI 之外,没有任何时钟相关配置,所以使用 HAL 库我们必须编写自己的时钟配置函数。首先我们打开工程模板看看我们在工程SYSTEM分组下面定义的sys.c文件中的时钟初始化函数 Stm32_Clock_Init 的内容:


//时钟系统配置函数
//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);
    RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;		//设置系统时钟时钟源为PLL
    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);
}

从函数注释可知,函数 Stm32_Clock_Init 的作用是进行时钟系统配置,除了配置 PLL 相关参数确定 SYSCLK 值之外,还配置了 AHB,APB1 和 APB2 的分频系数,也就是确定了 HCLK,PCLK1 和 PCLK2 的时钟值。

接下来我们看看结构体 RCC_OscInitTypeDef 的定义:

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;    

对于这个结构体,前面几个参数主要是用来选择配置的振荡器类型。比如我们要开启 HSE,那么我们会设置 OscillatorType 的值为 RCC_OSCILLATORTYPE_HSE,然后设置 HSEState 的值为 RCC_HSE_ON 开启 HSE。对于其他时钟源 HSI,LSI 和 LSE,配置方法类似。这个结构体还有一个很重要的成员变量是 PLL,它是结构体 RCC_PLLInitTypeDef 类型。它的作用是配置 PLL相关参数,我们来看看它的定义:

typedef struct
{
    uint32_t PLLState; //PLL 状态
    uint32_t PLLSource; //PLL 时钟源
    uint32_t PLLMUL; //PLL VCO 输入时钟的乘法因子
}RCC_PLLInitTypeDef;

从 RCC_PLLInitTypeDef;结构体的定义很容易看出该结构体主要用来设置 PLL 时钟源以及相关分频倍频参数。

这个结构体的定义我们就不做过多讲解,接下来我们看看我们的时钟初始化函数Stm32_Clock_Init 中的配置内容:

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 相关参数。

接下来我们来看看步骤 5 中提到的 HAL_RCC_ClockConfig()函数,声明如下:

HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct, uint32_t FLatency);

该函数有两个入口参数,第一个入口参数 RCC_ClkInitStruct 是结构体 RCC_ClkInitTypeDef指针类型,用来设置 SYSCLK 时钟源以及 AHB,APB1 和 APB2 的分频系数。第二个入口参数FLatency 用来设置 FLASH 延迟,这个参数我们放在后面讲解。

RCC_ClkInitTypeDef 结构体类型定义非常简单,这里我们就不列出来,我们来看看Stm32_Clock_Init 函数中的配置内容:

//选中 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

04. STM32F1时钟使能和配置

在配置好时钟系统之后,如果我们要使用某些外设,例如 GPIO,ADC 等,我们还要使能这些外设时钟。这里大家必须注意,如果在使用外设之前没有使能外设时钟,这个外设是不可能正常运行的。STM32 的外设时钟使能是在 RCC 相关寄存器中配置的。因为 RCC 相关寄存器非常多,有兴趣的同学可以直接打开《STM32 中文参考手册 V10》6.3 小节查看所有 RCC 相关寄存器的配置。接下来我们来讲解通过 STM32F1 的 HAL库使能外设时钟的方法。

在 STM32F1 的 HAL 库中,外设时钟使能操作都是在 RCC 相关固件库文件头文件stm32f1xx_hal_rcc.h 定义的。大家打开 stm32f1xx_hal_rcc.h 头文件可以看到文件中除了少数几个函数声明之外大部分都是宏定义标识符。外设时钟使能在 HAL 库中都是通过宏定义标识符来实现的。首先,我们来看看 GPIOA 的外设时钟使能宏定义标识符:

#define __HAL_RCC_GPIOA_CLK_ENABLE()   do { \
                                        __IO uint32_t tmpreg; \
                                        SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\
                                        /* Delay after an RCC peripheral clock enabling */\
                                        tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);\
                                        UNUSED(tmpreg); \
                                      } while(0U)

这几行代码非常简单,主要是定义了一个宏定义标识符__HAL_RCC_GPIOA_CLK_ENABLE(),它的核心操作是通过下面这行代码实现的:

SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);

这行代码的作用是,设置寄存器 RCC_APB2ENR 的相关位为 1,至于是哪个位,是由宏定义标识符 RCC_APB2ENR_IOPAEN 的值决定的,而它的值为:

#define RCC_APB2ENR_IOPAEN ((uint32_t)0x00000001)

所以,我们很容易理解上面代码的作用是设置寄存器 RCC->APB2ENR 寄存器的位 2 为 1。我们可以从 STM32F1 的中文参考手册中搜索 APB2ENR 寄存器定义,位 2 的作用是用来使用GPIOA 时钟。APB2ENR 寄存器的位 2 描述如下

位 2 IOPAEN:IO 端口 A 时钟使能
由软件置 1 和清零
0:禁止 IO 端口 A 时钟
1:使能 IO 端口 A 时钟

那么我们只需要在我们的用户程序中调用宏定义标识符__HAL_RCC_GPIOA_CLK_ENABLE()就可以实现 GPIOA 时钟使能。使用方法为:

__HAL_RCC_GPIOA_CLK_ENABLE();//使能 GPIOA 时钟

对于其他外设,同样都是在 stm32f1xx_hal_rcc.h 头文件中定义,大家只需要找到相关宏定义标识符即可,这里我们列出几个常用使能外设时钟的宏定义标识符使用方法:

__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_GPIOA_CLK_DISABLE()的作用是设置 RCC->APB2ENR 寄存器的位 2 为 0,也就是禁止 GPIOA 时钟。具体使用方法我们这里就不做过多讲解,我们这里同样列出几个常用的禁止外设时钟的宏定义标识符使用方法:

__HAL_RCC_DMA1_CLK_DISABLE();//禁止 DMA1 时钟
__HAL_RCC_USART2_CLK_DISABLE();//禁止串口 2 时钟
__HAL_RCC_TIM1_CLK_DISABLE();//禁止 TIM1 时钟

05. 附录

网址:【STM32】STM32系列教程汇总

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值