一:stm32时钟树介绍
时钟对于单片机来说是非常重要的, 它为单片机工作提供一个稳定的机器周期从而使系统能够正常运行。 时钟系统犹如人的心脏, 一旦有问题整个系统就崩溃。 我们知道 STM32 属于高级单片机, 其内部有很多的外设, 但不是所有外设都使用同一时钟频率工作, 比如内部看门狗和 RTC, 它只需 30 几 KHz 的时钟频率即可工作, 所以内部时钟源就有多种选择。 在前面章节的介绍中, 我们知道 STM32系统复位后首先进入 SystemInit 函数进行时钟的设置, 将 STM32F1 系统时钟设置为 72MHz(我们开发板上使用的 STM32F103ZET6 最大可达到 72M(超频除外) ),然后进入主函数。 那么这个系统时钟大小如何得来, 其他外设的时钟又如何划分,这些问题都可以通过一张时钟树图找到答案, 只要理解好时钟树, STM32 一切时钟的来龙去脉就会非常清楚。 下面就来了解下时钟树, 如图所示, 我们把时钟树拆分逐个介绍。
在 STM32 时钟系统中, 有 5 个重要的时钟源, 分别是 LSI、 LSE、 HSI、 HSE、PLL。 按照时钟频率分可分为高速时钟源和低速时钟源, 在这 5 个中 HSI, HSE 以及 PLL 属于高速时钟, LSI 和 LSE 属于低速时钟。 按照时钟来源可分为外部时钟源和内部时钟源, 外部时钟源就是在 STM32 晶振管脚处接入外部晶振的方式获取时钟源, 其中 HSE 和 LSE 是外部时钟源, 其他的是内部时钟源。 下面我们就按照上图中数字顺序来介绍。
(1) 图标 1 HSI 是内部高速时钟, RC 振荡器, 频率为 8MHz。 可作为系统时钟或 PLL 锁相环的输入。
(2) 图标 2 HSE 是外部高速时钟, 芯片的 23 和 24 引脚即为外部高速晶振管脚。 可通过外接一个频率范围是 4-16MHz 的时钟或者晶振, 我们开发板上接的是一个 8MHz 的外部晶振。 HSE 可以作为系统时钟和 PLL 锁相环输入, 还可以经过 128 分频后输入给 RTC。
(3) 图标 3 LSI 是内部低速时钟, RC 振荡器, 频率大约为 40K, 可供独立看门狗和 RTC 使用, 并且独立看门狗只能使用 LSI 时钟。
(4) 图标 4 LSE 是外部低速时钟, 我们开发板上 STM32 芯片的 PC14 和 PC15即为外部低速时钟管脚。 通常在此管脚上外接一个 32.768KHz 的晶振, 供 RTC使用。 我们开发板上已经外接了一个 32.768K 的晶振。图标 5 PLL 是锁相环, 用于倍频输出, 因为开发板外部高速晶振也只有 8M,而我们这块芯片的最大时钟频率是 72M, 因此可通过 PLL 锁相环来倍频。 从图标5 中可以看到, PLL 时钟输入源可选择为 HSI/2、 HSE 或者 HSE/2, 时钟源经过 2-16倍频后输入给 PLLCLK, 如果系统时钟选择由 PLLCLK 提供, 则 PLLCLK 最大值不要超过 72M。
那么它是怎么倍频产生72MHz系统时钟的呢? 我们看到在主PLL内有倍频器和分频器, 如图
可以看出, PLL 时钟源的输入信号要先经过一个 PLLMUL 倍频器,将 HSE 或 HSI 倍频(2-16)后输入给 PLLCLK, 如果系统时钟源 SYSCLK 选择 PLLCLK作为它的来源, 则最大值不能超过 72M。 虽然可以做超频处理, 但会打破系统的稳定性, 这个是不划算的。 假如 PLLSRC 的时钟来源由 HSE 提供, 我们开发板使用的 HSE 是 8M 晶振, 经过 PLLMUL 9 倍频后可以输出 72M 时钟频率给 PLLCLK。
总结: 如果我们选择 HSE 是 PLL 的时钟源, PLL 是 SYSCLK 的时钟源, 即 SYSCLK为 72MHz, 这个也是我们库函数模板中 SystemInit 所配置的最终系统时钟。
上面我们简单介绍了下 STM32 的 5 个时钟源, 那么它们是怎么给其他外设和系统提供时钟的呢? 在上图 时钟树图中我们把常用的时钟用字母框起来, 按照它们顺序依次介绍。
(A) MCO 是 STM32 的一个时钟输出 IO(PA8), 它可以选择一个时钟信号输出, 可以选择为 PLL 输出的 2 分频、 HSI、 HSE 或者系统时钟。 这个时钟可以用来给外部其他系统提供时钟源。
(B) RTC 时钟。 从图中线的流向可知, RTC 时钟来源可以是内部低速的 LSI时钟, 外部低速 LSE 时钟(32.768K) , 还可以通过 HSE 128 分频后得到。
(C) USB 时钟。 STM32 中有一个全速功能的 USB 模块, 其串行接口引擎需要一个频率为 48MHz 的时钟源, 该时钟源只能从 PLL 输出端获取, 可以选择为1.5 分频或者 1 分频, 也就是当需要使用 USB 模块时, PLL 必须使能, 并且PLLCLK 时钟频率配置为 48MHz 或 72MHz。
(D) SYSCLK 系统时钟。 它是 STM32 中绝大部分部件工作的时钟源。 它的时钟来源可以由 HSI、 HSE、 PLLCLK 提供, 相信大家选择 STM32F1 这种高级芯片,都希望有一个比较大的时钟频率, 因此选择 PLLCLK 作为系统时钟。 PLLCLK 又是从 HSE 或 HSI 经过 PLL 倍频得到。 根据前面 PLL 计算关系大家就可以算出系统时钟是多少。
(E) 其他所有外设。 从时钟图上可以看出, 其他所有外设的时钟最终来源都是 SYSCLK。 SYSCLK 通过 AHB 分频器分频后送给各模块使用。 这些模块包括:
①、 AHB 总线、 内核、 内存和 DMA 使用的 HCLK 时钟。
②、 通过 8 分频后送给 Cortex 系统定时器时钟, 即 SysTick。
③、 直接送给 Cortex 的空闲运行时钟 FCLK。
④、 送给 APB1 分频器。 APB1 分频器输出一路供 APB1 外设使用(PCLK1,最大频率 36MHz), 另一路送给定时器(Timer)1、 2 倍频使用。
⑤、 送给 APB2 分频器。 APB2 分频器分频输出一路供 APB2 外设使用(PCLK2, 最大频率 72MHz), 另一路送给定时器(Timer)1 倍频器使用。
⑥、 送给 ADC 分频器。 ADC 分频器经过 2、 4、 6、 8 分频后送给 ADC1/2/3 使用, ADC 最大频率为 14M。
⑦、 二分频后送给 SDIO 使用。
其中需要理解的是 APB1 和 APB2 的区别, APB1 上面连接的是低速外设,包括电源接口、 备份接口、 CAN、 USB、 I2C1、 I2C2、 UART2、 UART3 等, APB2上面连接的是高速外设包括 UART1、 SPI1、 Timer1、 ADC1、 ADC2、 GPIO 等。大家可以简单这样记忆: 2>1, 所以 APB2 的速度大于 APB1 的速度。
在时钟树图中我们还可以得到一个重要信息, 大多数有关时钟输出部分都有一个使能控制, 比如 AHB 总线、 APB1 外设、 APB2 外设、 内核时钟等。 当需要使用某个时钟的时候一定要开启它的使能, 否则将不工作。 在前面我们介绍库函数点亮一个 LED 实验的时候就使能了 GPIO 的外设时钟, 如果不开启, LED 将不工作。
二:时钟配置函数
1.时钟配置初始化函数
在前面章节的介绍中, 我们知道 STM32 系统复位后首先进入 SystemInit 函数进行时钟的设置, 然后进入主函数 main。 那么我们就来看下 SystemInit()函数到底做了哪些操作, 首先打开我们前面使用库函数编写的 LED 程序, 在system_stm32f10x.c 文件中可以找到 SystemInit()函数, 如果不想找的可以直接打开其头文件, 通过前面教大家的快速进入函数的方法进入到 SystemInit()内。 SystemInit()代码如下:
void SystemInit (void)
{
/* Reset the RCC clock configuration to the default reset state(for debug purpose) */
/* Set HSION bit */
RCC->CR |= (uint32_t)0x00000001;
/* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CL
RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */
/* Reset HSEON, CSSON and PLLON bits */
RCC->CR &= (uint32_t)0xFEF6FFFF
/* Reset HSEBYP bit */
RCC->CR &= (uint32_t)0xFFFBFFFF;
/* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
RCC->CFGR &= (uint32_t)0xFF80FFFF;
#ifdef STM32F10X_CL
/* Reset PLL2ON and PLL3ON bits */
RCC->CR &= (uint32_t)0xEBFFFFFF;
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x00FF0000;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (definedSTM32F10X_HD_VL)
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000;
#else
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */
#ifdefined (STM32F10X_HD) || (defined STM32F10X_XL) || (definedSTM32F10X_HD_VL)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM */
#endif
/* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
/* Configure the Flash Latency cycles and enable prefetch buffer */
SetSysClock();
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif
}
SystemInit 函数开始通过条件编译, 先复位 RCC 寄存器, 同时通过设置 CR寄存器的 HSI 时钟使能位来打开 HSI 时钟。 默认情况下如果 CR 寄存器复位,是选择 HSI 作为系统时钟, 这点大家可以查看 RCC->CR 寄存器相关位描述可以得知, 当低两位配置为 00 的时候(复位之后) , 会选择 HSI 振荡器为系统时钟。 也就是说, 调用 SystemInit 函数之后, 首先是选择 HSI 作为系统时钟。在设置完相关寄存器后才换成 HSE 作为系统时钟, 接下来 SystemInit 函数内部会调用 SetSysClock()函数。 这个函数内部是根据宏定义设置系统时钟频率。 函数如下:
static void SetSysClock(void)
{
#ifdef SYSCLK_FREQ_HSE
SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
SetSysClockTo24();
#elif defined SYSCLK_FREQ_36MHz
SetSysClockTo36();
#elif defined SYSCLK_FREQ_48MHz
SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
SetSysClockTo56();
#elif defined SYSCLK_FREQ_72MHz
SetSysClockTo72();
#endif
}
在system_stm32f10x.c 文件的开头就有对此宏定义, 系统默认的宏定义是72MHz, 如下:
#define SYSCLK_FREQ_72MHz 72000000
如果你要设置为 36MHz, 只需要注释掉上面代码, 然后加入下面代码即可:
#define SYSCLK_FREQ_36MHz 36000000
根据该函数内部实现过程可知, 直接调用 SetSysClockTo72()函数,此函数功能是将系统时钟 SYSCLK 设置为 72M,AHB 总线时钟设置为 72M,APB2 总线时钟设置为 72M, APB1 总线时钟设置为 36M, PLL 时钟设置为 72M。 函数具体实现大家可以打开库函数查看, 这里我们就不截取出来。 如果 SystemInit 内实现过程看不懂没有关系, 大家只要知道 SystemInit 函数执行完, 时钟大小设置如下:
SYSCLK(系统时钟) =72MHz
AHB 总线时钟(HCLK=SYSCLK) =72MHz
APB1 总线时钟(PCLK1=SYSCLK/2) =36MHz
APB2 总线时钟(PCLK2=SYSCLK/1) =72MHz
PLL 主时钟 =72MHz
这些时钟值大家要记住。
2. 时钟使能函数
上一节我们说到, 当使用一个外设时, 必须先使能它的时钟。 那么怎么通过库函数使能时钟呢? 如需了解寄存器配置时钟, 可以参考《STM32F10x 中文参考手册》 “复位和时钟控制(RCC) ” 章节内容, 里面有详细寄存器的介绍。 固件库已经把时钟相关寄存器的使能配置都封装好, 放在 stm32f10x_rcc.c 和stm32f10x_rcc.h 中。 只需要打开 stm32f10x_rcc.h 文件, 会发现有很多的宏定义和时钟使能函数的声明。 这些时钟函数可大致分为三类。 一类是外设时钟使能函数, 一类是时钟源和倍频因子配置函数, 还有一类是外设复位函数。 当然还有几个获取时钟源配置的函数。 下面就来简单介绍下这些函数的使用。首先我们看下时钟使能函数, 时钟使能函数包括外设时钟使能和时钟源使能。 首先我们看下外设时钟使能相关函数, 如下:
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
上面 3 个时钟使能函数也正是 STM32 的 3 条总线(这个在前面介绍存储器与寄存器章节讲过) 。 由于 STM32 的外设都是挂接在 AHB 和 APB 总线上的, 所以要使能外设时钟, 也就是使能对应外设所挂接的总线时钟。 比如 GPIO 外设它是挂接在 APB2 总线上的, 如果使用 GPIO 外设, 就需要先调用
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph,FunctionalState NewState);
函数使能 APB2 时钟。 有的朋友就会问: 我怎么知道哪个外设挂接在哪个总线上呢? 很简单, 可以通过 STM32 中文参考手册查找, 还可以在固件库stm32f10x_rcc.h 文件中查找。
外设时钟使能函数有两个形参, 第一个是你所使用的外设所挂接的时钟, 第二个是选择你用的外设时钟使能还是失能。 比如我们要使能端口 GPIOC, 那么第一个传递的参数是: RCC_APB2Periph_GPIOC 宏, 第二个传递的参数是 ENABLE 使能。 从第一个参数名来看也非常好理解, RCC 表示复位和时钟控制器, APB2 表示GPIOC 是挂接在 APB2 总线上, Periph 表示外设, 后面的 GPIOC 表示我们使能的是 GPIOC 端口。 第二个参数 ENABLE 表示使能。 假如使能 GPIOA 端口时钟, 那么只需要修改第一个参数值即可, 按照刚才介绍的名意义, 可以无需查找即可写出RCC_APB2Periph_GPIOA。 其他的外设初始化方法类似。
下面我们介绍下时钟源使能函数, 通过前面的讲解, 知道 STM32 有 5 大类时钟源, 这里我们只挑几个重要的时钟源使能函数介绍, 如下:
void RCC_HSICmd(FunctionalState NewState);
void RCC_LSICmd(FunctionalState NewState);
void RCC_PLLCmd(FunctionalState NewState);
void RCC_RTCCLKCmd(FunctionalState NewState);
这些函数都是用来使能相应的时钟源, 比如我们要使能 PLL 时钟, 那么就调用 RCC_PLLCmd 函数, 函数有一个形参, 和前面外设时钟的第二个参数一样, 如果为 ENABLE 表示使能, DISABLE 表示失能。
我们再来介绍下另外一类时钟函数——时钟源和倍频因子配置函数。 这类函数主要用来选择相应的时钟源和配置时钟倍频因子, 比如系统时钟, 它可以由HSE、 HSI 或者 PLLCLK 作为它的时钟源, 具体选择哪个, 就是通过时钟源配置函数实现。 比如我们设置 HSE 作为系统时钟源, 那么调用的函数就是:
RCC_SYSCLKConfig(RCC_SYSCLKSource_HSE);//配置时钟源为 HSE
在前面也介绍了 APB1 的时钟频率是 HCLK 的 2 分频。 那么可以调用下面这个函数来实现:
RCC_PCLK1Config(RCC_HCLK_Div2);//设置低速 APB1 时钟(PCLK1)
时钟倍频因子配置函数主要用来修改系统的时钟频率。 在本章最后一节我们会通过一个简单 LED 闪烁程序来说明修改倍频因子后时钟的变化。最后介绍下另外一类时钟函数——外设复位函数。 其函数如下:
void RCC_APB1PeriphResetCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
void RCC_APB2PeriphResetCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
在 STM32F10x 高容量的芯片中没有 RCC_AHBPeriphResetCmd 函数。 这类函数与前面讲解的外设时钟使能函数用法一样, 只不过外设时钟使能函数是用于使能外设时钟, 而这类函数是用于外设复位, 从函数名也可以区分出来。其他的函数大家可以自行查找其功能和用法。
三:自定义系统时钟
在时钟树的讲解中我们知道, 通过修改 PLLMUL 中的倍系数值(2-16) 可以改变系统的时钟频率。 在库函数中也有对时钟倍频因子配置的函数, 如下:
void RCC_PLLConfig(uint32_t RCC_PLLSource, uint32_t RCC_PLLMul);
第一个参数是 PLL 时钟源选择, 我们例程中采用的都是 HSE 作为 PLL 的时钟源,可以设置为 RCC_PLLSource_HSE_Div1/RCC_PLLSource_HSE_Div2。 第二个参数就是倍频因子值(RCC_PLLMul_2~RCC_PLLMul_16) 。
为了方便朋友们能够修改系统时钟, 我们这里自定义一个系统时钟初始化函数, 我们将函数放在对应实验程序的 main.c 中。
void RCC_HSE_Config(u32 div,u32 pllm) //自定义系统时间(可以修改时钟)
{
RCC_DeInit(); //将外设 RCC 寄存器重设为缺省值
RCC_HSEConfig(RCC_HSE_ON);//设置外部高速晶振(HSE)
if(RCC_WaitForHSEStartUp()==SUCCESS) //等待 HSE 起振
{
RCC_HCLKConfig(RCC_SYSCLK_Div1);//设置 AHB 时钟(HCLK)
RCC_PCLK1Config(RCC_HCLK_Div2);//设置低速 AHB 时钟(PCLK1)
RCC_PCLK2Config(RCC_HCLK_Div1);//设置高速 AHB 时钟(PCLK2)
RCC_PLLConfig(div,pllm);//设置 PLL 时钟源及倍频系数
RCC_PLLCmd(ENABLE); //使能或者失能 PLL
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY)==RESET);//检查指定的 RCC标志位设置与否,PLL 就绪
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);// 设 置 系 统 时 钟(SYSCLK)
while(RCC_GetSYSCLKSource()!=0x08);//返回用作系统时钟的时钟源,0x08:PLL 作为系统时钟
}
}
函数具体实现过程在程序中已经注释, 大家可以参考注释。 在函数中设置倍频因子时, 我们给他传递了形参中的变量, 这样做的好处是当你调用此函数时,只需要修改传递给函数形参内的值即可修改系统时钟, 无需修改函数内部程序。
在未修改系统时钟时, 系统初始化后的时钟是 72M, 对应着此函数参数设置如下:
RCC_HSE_Config(RCC_PLLSource_HSE_Div1,RCC_PLLMul_9);
如果现在我们想让系统时钟为 36M, 只需要将参数值修改即可, 如下:
RCC_HSE_Config(RCC_PLLSource_HSE_Div2,RCC_PLLMul_9);
此时修改的是 div 这个参数值, 此参数用来对 HSE 时钟分频系数设置, 从时钟树可知, HSE 可以直接流入到 PLLSRC, 还可以经过 2 分频后给 PLLSRC。 它的取值为 RCC_PLLSource_HSE_Div1 或 RCC_PLLSource_HSE_Div2。
最后我们可以通过一个 LED 指示灯闪烁速度来反映系统时钟修改后的效果。主函数代码如下:
int main()
{
RCC_HSE_Config(RCC_PLLSource_HSE_Div2,RCC_PLLMul_9); //36M
LED_Init();
while(1)
{
GPIO_ResetBits(LED_PORT,GPIO_Pin_0);//点亮 D1
delay(6000000);
GPIO_SetBits(LED_PORT,GPIO_Pin_0);
delay(6000000);
}
}
如果将 div 原先的 1 值修改为 2, 此时系统时钟即为 36M, 相当于速度慢了一倍。 LED 闪烁的速度也就慢了一倍。 注意: 不要把 STM32 系统时钟设置超过 72M使用, 否则很容易崩溃。