目录
在标准库的system_stm32f4xx.c注释中,可以确定STM32F410和STM32F411默认的系统时钟源为PLL(HSI):
本篇的主要工作是将时钟源由HSI改为HSE,并通过仿真确定使用内部晶振和外部晶振时的系统时钟频率分别是多少。
1. SystemInit()及相关函数解析
SystemInit() 是整个设置系统时钟的入口函数。如果我们使用ST提供的 STM32F4 固件库的话,会在系统启动之后先执行SystemInit() 函数实现系统相关时钟的设置。这个过程是在启动文件 startup_stm32f40_41xxx.s 中间设置的,启动代码如下:
1.1 SystemInit() 函数及注释
void SystemInit(void)
{
/* 复位 RCC 时钟配置为默认配置 ------------*/
/* 打开 HSION 位 */
RCC->CR |= (uint32_t)0x00000001;
/* 复位 CFGR 寄存器 */
/* **************************************
* CFGR register bit1:0
* 由软件置1和清零,用于选择系统时钟源
* 00:选择HSI作为系统时钟
* 01:选择HSE作为系统时钟
* 10:选择PLL作为系统时钟
* 11:不允许
* **************************************/
RCC->CFGR = 0x00000000;
/* 复位 HSEON, CSSON and PLLON 位 */
RCC->CR &= (uint32_t)0xFEF6FFFF;
/* 复位寄存器 PLLCFGR */
RCC->PLLCFGR = 0x24003010;
/* 复位 HSEBYP 位 */
RCC->CR &= (uint32_t)0xFFFBFFFF;
/* 关闭所有中断 */
RCC->CIR = 0x00000000;
/* Configure the System clock source, PLL Multiplier and Divider factors,
AHB/APBx prescalers and Flash settings ----------------------------------*/
SetSysClock();
/* Configure the Vector Table location add offset address ------------------*/
#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
}
1.2 SystemInit()函数的主要作用
复位 PLLCFGR,CFGR 寄存器,同时通过设置 CR 寄存器的 HSI 时钟使能位来打开 HSI 时钟。默认情况下如果 CFGR 寄存器复位,那么是选择 HSI 作为系统时钟,当低两位配置为 00 的时候(复位之后),会选择 HSI 振荡器为系统时钟。也就是说,调用 SystemInit 函数之后,首先是选择 HSI 作为系统时钟。
1.3 SetSysClock() 函数
SetSysClock() 函数中包含有大量条件编译,因为使用的是STM32F411芯片,所以找到STM32F411xE的条件编译:
该条件编译下又跟了一个新的条件编译语句 :
#if defined(USE_HSE_BYPASS)
在system_stm32f4xx.c中有如下几句:
如果需要通过STM32F103的STLINK MCO引脚使用HSE旁路对STM32F410xx和STM32F411xE提供时钟,则取消注释以下行,频率固定在8MHz不能修改。
HES Bypass即HSE旁路时钟源,也就是不使用晶振/陶瓷振荡器,直接通过外部提供一个可靠的4~26MHz时钟作为HSE。由于我们使用的是外部晶振,不使用旁路时钟,所以不要取消注释。
2. STM32F411时钟设置代码
SetSysClock()函数中关于STM32F411的时钟设置代码如下:
#else /* HSI will be used as PLL clock source */
/* Select regulator voltage output Scale 1 mode */
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
PWR->CR |= PWR_CR_VOS;
/* HCLK = SYSCLK / 1*/
RCC->CFGR |= RCC_CFGR_HPRE_DIV1;
/* PCLK2 = HCLK / 2*/
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;
/* PCLK1 = HCLK / 4*/
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;
/* Configure the main PLL */
RCC->PLLCFGR = PLL_M | (PLL_N << 6) | (((PLL_P >> 1) -1) << 16) | (PLL_Q << 24);
/* Enable the main PLL */
RCC->CR |= RCC_CR_PLLON;
/* Wait till the main PLL is ready */
while((RCC->CR & RCC_CR_PLLRDY) == 0)
{
}
/* Configure Flash prefetch, Instruction cache, Data cache and wait state */
FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN |FLASH_ACR_DCEN |FLASH_ACR_LATENCY_5WS; //FLASH_ACR_LATENCY_2WS
//
/* Select the main PLL as system clock source */
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
RCC->CFGR |= RCC_CFGR_SW_PLL;
/* Wait till the main PLL is used as system clock source */
while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS ) != RCC_CFGR_SWS_PLL);
{
}
该段代码表明STM32F411芯片默认是使用HSI作为时钟源的,也就是走的以下这条路:(编写代码使用的是标准库,CubeMX时钟树因为看起来更直观,仅用于参考)
3. PLL_M,PLL_N和PLL_P值的设置
PLL时钟输出频率计算公式:
f(VCO 时钟) = f(PLL 时钟输入) × (PLLN / PLLM)
f(PLL 常规时钟输出) = f(VCO 时钟) / PLLP
f(USB OTG FS, SDIO, RNG 时钟输出) = f(VCO 时钟) / PLLQ
所以在使用PLL时,系统时钟的计算方式为:
SystemCoreClock = 时钟源 /M *N /P
关于PLL_M,PLL_N和PLL_P的设置同样在system_stm32f4xx.c中,代码如下:
因为我使用的是外部25MHz的晶振,设置的PLL_M,PLL_N,PLL_P值分别为13,104和2,有:
SystemCoreClock = 25/13*104/2 = 100MHz
但是由于没有开启外部时钟,系统启动时默认使用HSI作为时钟源,所以SystemCoreClock变成了:
SystemCoreClock = 16/13*104/2 = 64MHz
所以理论上的定时1秒实际约为1.56秒。
注意:时钟源/M的值要落在1~2之间,所以M值也需要根据晶振的频率来确定,不能随意取值。
4. STM32F411使用外部时钟源HSE的方法
4.1 HSE的启动代码
将SetSysClock()函数中原有关于STM32F411条件编译部分使用HIS启动的代码注释掉,并添加以下代码:
/******************************************************************************
* 如果不使用HSE_BYPASS模式,依然使用HSE作为时钟源
* PLL (clocked by HSE) used as System clock source
******************************************************************************/
__IO uint32_t StartUpCounter = 0, HSEStatus = 0;
/* Enable HSE and HSE BYPASS */
RCC->CR |= (uint32_t)RCC_CR_HSEON;
/* Wait till HSE is ready and if Time out is reached exit */
do
{
HSEStatus = RCC->CR & RCC_CR_HSERDY;
StartUpCounter++;
} while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));
if ((RCC->CR & RCC_CR_HSERDY) != RESET)
{
HSEStatus = (uint32_t)0x01;
}
else
{
HSEStatus = (uint32_t)0x00;
}
if (HSEStatus == (uint32_t)0x01)
{
/* Select regulator voltage output Scale 1 mode */
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
PWR->CR |= PWR_CR_VOS;
/* HCLK = SYSCLK/1 */
RCC->CFGR |= RCC_CFGR_HPRE_DIV1;
/* PCLK2 = HCLK/1 */
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;
/* PCLK1 = HCLK/2 */
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;
/* Configure the main PLL */
RCC->PLLCFGR = PLL_M | (PLL_N << 6) | (((PLL_P >> 1) -1) << 16) |
(RCC_PLLCFGR_PLLSRC_HSE) | (PLL_Q << 24);
/* Enable the main PLL */
RCC->CR |= RCC_CR_PLLON;
/* Wait till the main PLL is ready */
while((RCC->CR & RCC_CR_PLLRDY) == 0)
{
}
/* Configure Flash prefetch, Instruction cache, Data cache and wait state */
FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN |FLASH_ACR_DCEN |FLASH_ACR_LATENCY_2WS;
/* Select the main PLL as system clock source */
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
RCC->CFGR |= RCC_CFGR_SW_PLL;
/* Wait till the main PLL is used as system clock source */
while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS ) != RCC_CFGR_SWS_PLL);
{
}
}
4.2 关键代码解释
RCC->CR |= (uint32_t)RCC_CR_HSEON;
作用:打开HSE振荡器
由宏定义
#define RCC_CR_HSEON ((uint32_t)0x00010000)
可知,该行代码是将CR寄存器的第16位置1,查看CR寄存器各个位的描述可知,该行代码的作用是打开HSE振荡器。
do
{
HSEStatus = RCC->CR & RCC_CR_HSERDY;
StartUpCounter++;
} while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));
作用:等待HSE时钟稳定。
HSE振荡器开启时会有波动,所以需要一端时间等待其稳定,当HSE振荡器稳定后,CR寄存器的第17位会置1,表示HSE时钟已准备就绪。
/* HCLK = SYSCLK/1 */
RCC->CFGR |= RCC_CFGR_HPRE_DIV1;
/* PCLK2 = HCLK/1 */
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;
/* PCLK1 = HCLK/2 */
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;
作用:设置HCLK时钟为系统时钟(100M),PCLK2时钟为系统时钟(100M),PCLK1时钟为系统时钟的2分频(50MHz)
对应CubeMX时钟树:
RCC->PLLCFGR = PLL_M | (PLL_N << 6) | (((PLL_P >> 1) -1) << 16) |
(RCC_PLLCFGR_PLLSRC_HSE) | (PLL_Q << 24);
作用:将我们设定好的M,N,P,Q值写入到PLLCFGR寄存器。
Q值适用于USB OTG FS、 SDIO 和随机数发生器时钟。为使 USB OTG FS 能够正常工作,需要 48 MHz 的时钟。对于 SDIO 和随即数生成器,频率需要低于或等于 48 MHz 才可正常工作。我们并没有使用到该参数。
该句含义是在PLLCFGR寄存器中写入如下值:0000 0100 0100 0000 0001 1010 0000 1101
查看PLLCFGR寄存器各个位的描述可知,该句功能为:
- 设置PLLQ=4;
- 选择HSE时钟作为PLL时钟输入;
- 设置PLLP=2;
- 设置PLLN=104,
- 设置PLLM=13.
对应CubeMX时钟树:
RCC->CR |= RCC_CR_PLLON;
作用:打开主PLL
由宏定义
#define RCC_CR_PLLON ((uint32_t)0x01000000)
可知,该行代码将CR寄存器第24位置1,查看CR寄存器各个位的描述可知,该行代码的作用是主PLL使能。
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
RCC->CFGR |= RCC_CFGR_SW_PLL;
作用:选择主PLL作为系统时钟源
由宏定义
#define RCC_CFGR_SW ((uint32_t)0x00000003)
#define RCC_CFGR_SW_PLL ((uint32_t)0x00000002)
可知,是将CFGR寄存器的位1:0置为10,即选择PLL作为系统时钟。
5. STM32F411使用时钟源的切换及仿真
在主函数中加入如下两行代码,然后进行仿真。
RCC_ClocksTypeDef RCC_ClocksStatus;
RCC_GetClocksFreq(&RCC_ClocksStatus);
5.1 使用外部晶振HSE
1)HSE=25MHz;M=13;N=104;P=2
计算值:SYSCLK=25/13*104/2=100MHz
仿真结果如下:
系统时钟频率SYSCLK,HCLK频率,PCLK2频率为0x05F5E0D0,十进制为99.999952MHz
PCLK1时钟是SYSCLK的2分频,为0X02FAF068,十进制为49.999976MHz
2) HSE=25MHz;M=16;N=128;P=2
计算值:SYSCLK=25/16*128/2=100MHz
仿真结果如下:
系统时钟频率SYSCLK,HCLK频率,PCLK2频率为0x05F5E100,十进制为100MHz
PCLK1时钟是SYSCLK的2分频,为0X02FAF080,十进制为50MHz
5.2 使用内部晶振HSI
1)HSI=16MHz;M=8;N=100;P=2
计算值: SYSCLK=16/8*100/2=100MHz
仿真结果如下:
系统时钟频率SYSCLK,HCLK频率,PCLK2频率为0x05F5E100,十进制为100MHz
PCLK1时钟是SYSCLK的2分频,为0X02FAF080,十进制为50MHz
2)HSI=16MHz;M=13;N=104;P=2
计算值:SYSCLK=16/13*104/2=64MHz
仿真结果如下:
系统时钟频率SYSCLK,HCLK频率,PCLK2频率为0x03D08FF4,十进制为63.999988MHz
PCLK1时钟是SYSCLK的2分频,为0X01E847FA,十进制为31.999994MHz
由以上仿真结果可知,虽然通过公式计算出最终的SYSCLK都是100MHz,但是选择不同的M,N值,仿真频率的最终结果会略微出现偏差,所以M要尽量选择2的整数倍。因此重新选择M=16,N=128,P=2。