STM32F103系列单片机时钟笔记(一)

时钟系统之于单片机就如同与心脏脉搏之于人体,可见时钟系统的重要性可见一斑。然而STM32的时钟系统极其复杂,不像51单片机一样一个时钟系统就可以解决一切问题。今天就来对时钟系统做一个总结。

一、时钟简介


首先,我们得搞清楚几个概念:

时钟:单片机的心脏,所有的外设的运作都需要时钟供能。
时钟周期:又称为振荡周期,可以简单理解为传输一个0或1所需要的时间
指令周期:执行一条指令(如 MOV A, #34H)所需要的时间。对于不同类型的指令,指令周期长度可能不同。
机器周期:执行一个动作的时间周期。如:执行一个指令需要”取指令并译码“、”执行操作数“两个动作。

以上三个周期的关系图如下:(图为转载)

二、时钟系统框图

三、时钟源讲解


 从图中可以看到,STM32共有五个时钟源,分别是HSI、HSE、LSI、LSE和PLL ,下面分别对它们进行讲解:

        ①、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。

注意:高速外部时钟HSE的引脚是OSC_OUT和OSC_IN这两个引脚芯片是独立引出的,可以接外部的晶振电路,而低速外部时钟LSE的引脚OSC32_IN和OSC32_OUT两个引脚不是独立的,而是在PC14和PC15上,对应关系为OSC32_IN-->PC14 ; OSC32_OUT-->PC15

为什么需要那么多时钟源,又分内部时钟源和外部时钟源?尽然外设都是受系统时钟管理,那么只用一个时钟不是就足够了吗?

       多个时钟源可以在单个时钟源发生故障时,起到救急的作用
       一个外设有多个时钟源,可以根据需要选择相应频率的时钟源

四、时钟去向讲解


        上面我们简要概括了STM32 的时钟源,那么这 5 个时钟源是怎么给各个外设以及系统提供时钟的呢?这里我们将一一讲解。这里我们使用官方手册提供的框图(图一)进行讲解,图中我们用A~E 标示我们要讲解的地方。

A   MCO 是 STM32 的一个时钟输出 IO(PA8) 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   其他所有外设。从时钟图上可以看出,其他所有外设的时钟最终来源都是 SYSCLK 。 SYSCLK 通过 AHB 分频器分频后送给各模块使用 。这些模块包括:
① AHB 总线、内核、内存和 DMA 使用的 HCLK 时钟。
② 通过 8 分频后送给 Cortex 的系统 定时器 时钟 ,也就是 systick 了 。
③ 直接送给 Cortex 的空闲运行时钟 FCLK 。
④ 送给 APB1 分频器。 APB1 分频器输出一路供 APB1 外设使用 (PCLK1 ,最大频率 36MHz)36MHz),另一路送给定时器 (Timer)2 、 3 、 4 倍频器使用。
⑤送给 APB2 分频器。 APB2 分频器分频输出一路供 APB2 外设使用 (PCLK2最大频率 72MHz)72MHz),另一 路送给定时器 (Timer)1 倍频器使用。
        APB1和APB2的区别: APB1上面连的是低速外设,包括电脑接口、备份接口、CAN、USB、I2C1、I2C2 、 UART2 、 UART3 等等, APB2 上面连接的是高速外设包括 UART1 、 SPI1 、 Timer1 、 ADC1 、 ADC2 、所有普通 IO 口 (PA~ 、第二功能 IO 口 等。 APB2 下面所挂的外设的时钟要比 APB1 的高 。

在以上的时钟输出中,有很多 是带使能控制的,例如 AHB 总线 时钟、内核时钟、各种 APB1
外设、 APB2 外设等等。当需要使用某模块时,记得一定要先使能对应的时钟。

系统时钟正常工作的时钟频率是72MHZ。但实际输入的系统时钟频率最大可以达到128MHZ,最低8MHZ,这意味着我们可以不遵循数据手册的规定,做点"违规操作"

网上找的的回答有:

频率高了,系统稳定性会受到影响,具体表现是进行通信的时候,外设可能无法接收到准确的信号以及功耗会变大
频率低了,不能满足设备的频率要求,可能无法启动设备

五、STM32时钟配置


1. 时钟配置简介
STM32时钟系统的配置除了初始化的时候在 system_stm32f10x.c 中的 SystemInit 函数中外,其他的配置主要在 stm32f10x_rcc.c 文件中,里面有很多时钟设置函数,大家可以打开这个文件浏览一下,基本上看看函数的名称就知道这个函数的作用。在大家设置时钟的时候,一定要仔细参考STM32 的时钟图,做到心中有数。

对于系统时钟,默认情况下是SystemInit 函数的 SetSysClock() 函数中间判断的。

这里总结一下SystemInit() 函数中设置的系统时钟大小:

  • SYSCLK 系统时钟 =72MHz
  • AHB 总线时钟 使用 SYSCLK) =72MHz
  • APB1 总线时钟 =36MHz
  • APB2 总线时钟 =72MHz
  • PLL 时钟 =72MHz 

2.时钟配置寄存器介绍
以上介绍了SystemInit()时钟配置函数,想必大家已经知道了STM32时钟的基本情况,如果你想要更深一步的了解底层原理,那么下面的知识可以帮你揭晓。

在STM32时钟配置的过程中使用到了RCC寄存器,它们分别是:

时钟控制寄存器(RCC_CR)
时钟配置 寄存器(RCC_CFGR)


首先介绍时钟控制寄存器,先上一张官方手册的寄存器说明:

这个是时钟控制寄存器,看似很复杂,实际上并没有使用到寄存器所有的位,而是红框标出的部分位。下表是RCC另一个重要的寄存器:时钟配置寄存器(RCC_CFGR)

 和时钟控制寄存器RCC_CR相比,时钟配置寄存器RCC_CFGR要复杂一些。具体信息在手册里有详细介绍。

六、Systick定时器及delay延时函数


1. Systick定时器
前面讲完了STM32系统时钟的基本配置,下面就是对延时函数进行介绍了,延时函数使用到了STM32的SysTick定时器,这是一个很简单的定时器,对于CM3和CM4内核芯片都具备。

SysTick定时器全称是System tick timer即系统滴答定时器,常用来做延时或者系统的心跳时钟。这样可以节省MCU资源,不用浪费一个定时器。比如在UCOS操作系统中分时复用,需要一个最小的时间戳,一般在STM32+UCOS系统中,都采用SysTick做UCOS心跳时钟。

SysTick定时器就是系统滴答定时器,一个24位倒计数定时器,计数到0时将从RELOAD寄存器中自动重装载定时器初值。只要不把它在SysTick控制及状态寄存器中的使能位清除,就永不停息,即使在睡眠模式也能工作。
SysTick定时器被捆绑在NVIC中,用于产生SYSTICK异常(异常号为15)。
SysTick中断的优先级也可以设置
2. 相关寄存器介绍
SysTick定时器一共有四个相关寄存器:

CTRL              SysTick 控制和状态寄存器 
LOAD              SysTick 自动重装载除值寄存器   
VAL                 SysTick 当前值寄存器 
CALIB             SysTick 校准值寄存器  


下面分别介绍这四个寄存器

 这是四个相关的寄存器,实际上我们一般只用到前三个。

3. 延时函数配置
对于STM32,外部时钟源是 HCLK(AHB总线时钟)的1/8 ,内核时钟是 HCLK时钟。配置函数:SysTick_CLKSourceConfig();这个函数在misc.c文件中,这里把它粘出来:

void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)
{
  /* Check the parameters */
  assert_param(IS_SYSTICK_CLK_SOURCE(SysTick_CLKSource));
  if (SysTick_CLKSource == SysTick_CLKSource_HCLK)
  {
    SysTick->CTRL |= SysTick_CLKSource_HCLK;
  }
  else
  {
    SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
  }
}


根据入口参数有效性可知,时钟源可以选择HCLK或者HCLK的8分频。这个函数在下面的delay_init会调用一次,完成延时函数的初始化。

core_cm3.h文件中有关于初始化systick时钟和开启中断的函数,如下所示:

static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{ 
  if (ticks > SysTick_LOAD_RELOAD_Msk)  return (1);            /* Reload value impossible */
                                                               
  SysTick->LOAD  = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;      /* set reload register */
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  /* set Priority for Cortex-M0 System Interrupts */
  SysTick->VAL   = 0;                                          /* Load the SysTick Counter Value */
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk | 
                   SysTick_CTRL_TICKINT_Msk   | 
                   SysTick_CTRL_ENABLE_Msk;                    /* Enable SysTick IRQ and SysTick Timer */
  return (0);                                                  /* Function successful */
}


需要注意的是第5行把重装载寄存器的值减1的目的是考虑到执行装在动作也需要一个时钟周期。

下面是用中断的方式来实现延时:

static __IO uint32_t TimingDelay;
void Delay(__IO uint32_t nTime)
{ 
   TimingDelay = nTime;
   while(TimingDelay != 0);
}
void SysTick_Handler(void)
{
    if (TimingDelay != 0x00) 
     { 
       TimingDelay--;
     }
}
 int main(void)
 {  …
    if (SysTick_Config(SystemCoreClock / 1000)) //systick时钟为HCLK,中断时间间隔1ms
     {
     while (1);
     }
    while(1)
     { Delay(200);//2ms
     … 
     }
}

程序中,SysTick_Config(SystemCoreClock / 1000)的目的是确保SysTick定时器每次发生中断的时间间隔都是1ms(因为使用系统时钟的时钟频率为72000000,SystemCoreClock / 1000等于72000,也就是给重装载寄存器值赋72000,这样计72000正好是1ms)

一般实际应用中我们不使用通过中断来驱动延时的方法,因为这样会多一个中断线路,我们采用的 是单独的延时函数,下面就是延时函数文件:

#include "delay.h"
    
//如果需要使用OS,则包括下面的头文件即可.
#if SYSTEM_SUPPORT_OS
#include "includes.h"                    //ucos 使用      
#endif
//      
 
static u8  fac_us=0;                            //us延时倍乘数               
static u16 fac_ms=0;                            //ms延时倍乘数,在ucos下,代表每个节拍的ms数
    
    
#if SYSTEM_SUPPORT_OS                            //如果SYSTEM_SUPPORT_OS定义了,说明要支持OS了(不限于UCOS).
//当delay_us/delay_ms需要支持OS的时候需要三个与OS相关的宏定义和函数来支持
//首先是3个宏定义:
//    delay_osrunning:用于表示OS当前是否正在运行,以决定是否可以使用相关函数
//delay_ostickspersec:用于表示OS设定的时钟节拍,delay_init将根据这个参数来初始哈systick
// delay_osintnesting:用于表示OS中断嵌套级别,因为中断里面不可以调度,delay_ms使用该参数来决定如何运行
//然后是3个函数:
//  delay_osschedlock:用于锁定OS任务调度,禁止调度
//delay_osschedunlock:用于解锁OS任务调度,重新开启调度
//    delay_ostimedly:用于OS延时,可以引起任务调度.
 
//本例程仅作UCOSII和UCOSIII的支持,其他OS,请自行参考着移植
//支持UCOSII
#ifdef     OS_CRITICAL_METHOD                        //OS_CRITICAL_METHOD定义了,说明要支持UCOSII                
#define delay_osrunning        OSRunning            //OS是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec    OS_TICKS_PER_SEC    //OS时钟节拍,即每秒调度次数
#define delay_osintnesting     OSIntNesting        //中断嵌套级别,即中断嵌套次数
#endif
 
//支持UCOSIII
#ifdef     CPU_CFG_CRITICAL_METHOD                    //CPU_CFG_CRITICAL_METHOD定义了,说明要支持UCOSIII    
#define delay_osrunning        OSRunning            //OS是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec    OSCfg_TickRate_Hz    //OS时钟节拍,即每秒调度次数
#define delay_osintnesting     OSIntNestingCtr        //中断嵌套级别,即中断嵌套次数
#endif
 
 
//us级延时时,关闭任务调度(防止打断us级延迟)
void delay_osschedlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD                   //使用UCOSIII
    OS_ERR err; 
    OSSchedLock(&err);                            //UCOSIII的方式,禁止调度,防止打断us延时
#else                                            //否则UCOSII
    OSSchedLock();                                //UCOSII的方式,禁止调度,防止打断us延时
#endif
}
 
//us级延时时,恢复任务调度
void delay_osschedunlock(void)
{    
#ifdef CPU_CFG_CRITICAL_METHOD                   //使用UCOSIII
    OS_ERR err; 
    OSSchedUnlock(&err);                        //UCOSIII的方式,恢复调度
#else                                            //否则UCOSII
    OSSchedUnlock();                            //UCOSII的方式,恢复调度
#endif
}
 
//调用OS自带的延时函数延时
//ticks:延时的节拍数
void delay_ostimedly(u32 ticks)
{
#ifdef CPU_CFG_CRITICAL_METHOD
    OS_ERR err; 
    OSTimeDly(ticks,OS_OPT_TIME_PERIODIC,&err);    //UCOSIII延时采用周期模式
#else
    OSTimeDly(ticks);                            //UCOSII延时
#endif 
}
 
//systick中断服务函数,使用ucos时用到
void SysTick_Handler(void)
{    
    if(delay_osrunning==1)                        //OS开始跑了,才执行正常的调度处理
    {
        OSIntEnter();                            //进入中断
        OSTimeTick();                           //调用ucos的时钟服务程序               
        OSIntExit();                                //触发任务切换软中断
    }
}
#endif
 
               
//初始化延迟函数
//当使用OS的时候,此函数会初始化OS的时钟节拍
//SYSTICK的时钟固定为HCLK时钟的1/8
//SYSCLK:系统时钟
void delay_init()
{
#if SYSTEM_SUPPORT_OS                              //如果需要支持OS.
    u32 reload;
#endif
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);    //选择外部时钟  HCLK/8
    fac_us=SystemCoreClock/8000000;                //为系统时钟的1/8  
#if SYSTEM_SUPPORT_OS                              //如果需要支持OS.
    reload=SystemCoreClock/8000000;                //每秒钟的计数次数 单位为M  
    reload*=1000000/delay_ostickspersec;        //根据delay_ostickspersec设定溢出时间
                                                //reload为24位寄存器,最大值:16777216,在72M下,约合1.86s左右    
    fac_ms=1000/delay_ostickspersec;            //代表OS可以延时的最少单位       
 
    SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk;       //开启SYSTICK中断
    SysTick->LOAD=reload;                         //每1/delay_ostickspersec秒中断一次    
    SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk;       //开启SYSTICK    
 
#else
    fac_ms=(u16)fac_us*1000;                    //非OS下,代表每个ms需要的systick时钟数   
#endif
}                                    
 
#if SYSTEM_SUPPORT_OS                              //如果需要支持OS.
//延时nus
//nus为要延时的us数.                                               
void delay_us(u32 nus)
{        
    u32 ticks;
    u32 told,tnow,tcnt=0;
    u32 reload=SysTick->LOAD;                    //LOAD的值             
    ticks=nus*fac_us;                             //需要的节拍数               
    tcnt=0;
    delay_osschedlock();                        //阻止OS调度,防止打断us延时
    told=SysTick->VAL;                            //刚进入时的计数器值
    while(1)
    {
        tnow=SysTick->VAL;    
        if(tnow!=told)
        {        
            if(tnow<told)tcnt+=told-tnow;        //这里注意一下SYSTICK是一个递减的计数器就可以了.
            else tcnt+=reload-tnow+told;        
            told=tnow;
            if(tcnt>=ticks)break;                //时间超过/等于要延迟的时间,则退出.
        }  
    };
    delay_osschedunlock();                        //恢复OS调度                                        
}
//延时nms
//nms:要延时的ms数
void delay_ms(u16 nms)
{    
    if(delay_osrunning&&delay_osintnesting==0)    //如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)        
    {         
        if(nms>=fac_ms)                            //延时的时间大于OS的最少时间周期 
        { 
               delay_ostimedly(nms/fac_ms);        //OS延时
        }
        nms%=fac_ms;                            //OS已经无法提供这么小的延时了,采用普通方式延时    
    }
    delay_us((u32)(nms*1000));                    //普通方式延时  
}
#else //不用OS时
//延时nus
//nus为要延时的us数.                                               
void delay_us(u32 nus)
{        
    u32 temp;             
    SysTick->LOAD=nus*fac_us;                     //时间加载               
    SysTick->VAL=0x00;                            //清空计数器
    SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;    //开始倒数      
    do
    {
        temp=SysTick->CTRL;
    }while((temp&0x01)&&!(temp&(1<<16)));        //等待时间到达   
    SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;    //关闭计数器
    SysTick->VAL =0X00;                           //清空计数器     
}
//延时nms
//注意nms的范围
//SysTick->LOAD为24位寄存器,所以,最大延时为:
//nms<=0xffffff*8*1000/SYSCLK
//SYSCLK单位为Hz,nms单位为ms
//对72M条件下,nms<=1864 
void delay_ms(u16 nms)
{                     
    u32 temp;           
    SysTick->LOAD=(u32)nms*fac_ms;                //时间加载(SysTick->LOAD为24bit)
    SysTick->VAL =0x00;                            //清空计数器
    SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;    //开始倒数  
    do
    {
        temp=SysTick->CTRL;
    }while((temp&0x01)&&!(temp&(1<<16)));        //等待时间到达   
    SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;    //关闭计数器
    SysTick->VAL =0X00;                           //清空计数器              
} 
#endif

这里需要注意的是我们使用了外部时钟即系统时钟的8分频,这样确保时间足够长。另外,毫秒延时函数有最大上限,最大为1864(72MHz时钟频率)。用这种方式来延时的好处是不用中断比较简单,但是坏处就是最大延时时长有限(不到2秒),所以当需要长延时时间的时候要多写几个delay函数。

七、总结


配置时钟的流程


开启时钟并等待就绪
若时钟异常就退出(用户可自行作相关异常处理)
开启正常,先配置相关总线的预分频系数
最后选择系统时钟是PLL还是HSI或者HSE,让外设得到时钟


学习一款MCU的时钟流程


看时钟树——分析时钟的来龙去脉
查数据手册,找到以下问题的答案:
时钟源怎么开启?
相关外设的时钟怎么控制
系统时钟源怎么选择
 

参考文章:https://blog.csdn.net/qq_44016222/article/details/123223733

https://blog.csdn.net/qq_43460068/article/details/122203020

  • 25
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值