【GD32系列开坑:零死角玩转GD32】

【开坑国产单片机GD32系列,带你零死角玩转GD32】


第三章 GD32F103xx时钟系统分析

目录

  • 【开坑国产单片机GD32系列,带你零死角玩转GD32】
  • 第三章 GD32F103xx时钟系统分析
  • (1)前言
  • (2)GD32F103xx时钟架构分析
  • (2.1) GD32F103xx芯片架构图
  • (2.2)GD32F103xx时钟源介绍
  • (2.3)GD32F103xx时钟设置及分配
  • (2.3.1)系统时钟源选择
  • (2.2.2)时钟配置函数system_clock_108m_hxtal()
  • (3)结语
  • 有效评论,加关注收藏的彦祖们,我会随机送出共计10份开发板!
  • (1)前言

    三十功名尘与土,八千里路云和月;

          第二章 GD32开发环境的搭建,常用资料的获取初步地介绍了GD32工程的创建方式以及常用资料获取,但是可能会有彦祖发现,好像没有讲怎么点灯呀?

          理论上来说,讲完工程创建,确实应该再讲一下怎么点灯,但是总觉得这中间少了什么,拿到一款MCU之后,很多人会去测试它的IO口,串口,以及I2C等功能,但很多时候我们忽略了一个很重要的部分,也就是时钟系统,时钟系统对于MCU的重要性,等同于彦祖们的关注对我的重要性。


          好了!言归正传,在MCU上,不管是哪个外设,它都需要时钟系统对其提供一个基本工作时钟,这个外设才能正常而又协调地运行,下面我们会详细讨论GD32F103xx的时钟系统。(PS:已经开始画GD32F103RCT6的板子了,后续点灯之类的操作,都会在这个板子上进行啦!)


    (2)GD32F103xx时钟架构分析

          在开始这段话题之前,希望彦祖们已经有GD32F103xx的数据手册了,因为我们这部分的分析,都是围绕数据手册所涉及的内容进行的。

    (2.1) GD32F103xx芯片架构图

          如图1所示的GD32F103xx芯片架构图可以看出,诸如GPIO,SPI,ADC,DAC等外设,都是挂载在APB1,APB2总线上的,而APB1和APB2总线,都是由AHB总线桥接而成。

          包括数据的传输,外设的时钟频率基准,都是由这三条横贯在MCU内部的高速总线提供的,如果把MCU比作一座城市,那么诸如APB1,APB2,AHB等总线,就可以看做是纵横在这座城市的高速公路,如果想要熟悉这座城市,第一件事,应该是要自己去熟悉这座城市的交通吧?难道用高德地图么?

    (2.2)GD32F103xx时钟源介绍

           这部分是今天重点讨论的地方,结合数据/用户手册和代码,通过重构整个时钟系统的方式,来分析GD32F103xx的时钟系统的构成和功能。
           我们会发现,有时候我们手上的板子,在MCU的边上,会有一到两个晶振,晶振的外形,焊接方式,封装可能都不一样,就像下面这两个。


          但不管是哪一种,它们都有一个统一的名字,叫做“外部晶振” ,顾名思义,外部晶振就是放在外面的晶振。


          而且外部晶振也分两种,第一种是外部高速晶振,第二种是外部低速晶振,至于这两种的区别,我们待会细讲。
          有时候,我们会发现,某个板子,明明没有外接晶振,但还是能跑起来,跑得还很流畅,这能说明一件事,那就是在MCU内部,同样也是有晶振的,对于GD32F103xx来说,就是以下两个内部晶振:
          在没有外接晶振,或者外部晶振没有被使能的情况下,系统是可以使用内部晶振的,那么这时候就会有彦祖问:怎么通过代码来选择需要使用的晶振和总线的频率呢?问得好!接下来我们通过代码和手册,来一步步地分析GD32F103xx的时钟选择方法以及如何把各个总线设置为不同的时钟频率。

    (2.3)GD32F103xx时钟设置及分配

    (2.3.1)系统时钟源选择

          首先,在Keil中打开之前提供的模板工程,找到文件名为:system_gd32f103x.c 源文件(路径是:GD32F103xxxx工程模板\Libraries\Src,也可以直接在Keil的工程列表界面打开),这个文件存放就是在主函数执行之前,系统所要进行的初始化工作,其中便包含时钟系统的初始化。
    打开文件之后,首先出现是以下代码:

    /* system frequency define */
    #define __IRC8M           (IRC8M_VALUE)            /* internal 8 MHz RC oscillator frequency */
    #define __HXTAL           (HXTAL_VALUE)            /* high speed crystal oscillator frequency */
    #define __SYS_OSC_CLK     (__IRC8M)                /* main oscillator frequency */
    

          其中的 __IRC8M ,就是在 (2.2)GD32F103xx时钟源介绍 中提到的内部晶振,而 __HXTAL 就是外部高速晶振,这里要注意一点,由于外部晶振的频率不是固定的,我们要根据我们实际使用的外部晶振的频率修改这个宏定义的数值,修改的方法是:在 HXTAL_VALUE 右键跳转,出现以下代码:

    #define HXTAL_VALUE    ((uint32_t)8000000) /* !< from 4M to 16M *!< value of the external oscillator in Hz*/
    

          我使用的是8MHZ的外部晶振,所以设置为 8000000 ,彦祖们可以根据实际使用来修改,避免出现实际的外部晶振是12MHZ,但是这里写的却是8MHZ(我是不会告诉你因为这个错误,我的串口波特率调了一早上)。

          另外,__SYS_OSC_CLK 是系统主时钟,这里是系统默认设置,在system_gd32f103x.c中的==SystemInit()==函数 ,会把__IRC8M 设置为系统默认时钟源,代码如下:

        /* enable IRC8M */
        RCU_CTL |= RCU_CTL_IRC8MEN;
    

          当RCU_CTL寄存器的IRC_8MEN位被置位(也就是设置为1)时,内部的8MHZ时钟就会被开启,不过虽然内外部时钟一样都是8MHZ,但是内部时钟是RC原理,所以在精度上,是不如外部高速时钟的,如果对时钟精度要求不高的话,倒是可以省下一个外部晶振的成本。
          刚刚我们提到了 SystemInit() 函数 ,这个函数很特殊,之所以这么说,是因为它的执行顺序,在main函数之前,我们可以打开 startup_gd32f10x_hd.s 文件,在159行到165行,会出现以下代码:

    IMPORT  __main							;代码1
    IMPORT  SystemInit  				    ;代码2
    LDR     R0, =SystemInit		            ;代码3
    BLX     R0							    ;代码4
    LDR     R0, =__main				        ;代码5
    BX      R0								;代码6
    ENDP							        ;代码7
    

          简单解释以下这几行ARM汇编代码的意思, IMPORT 表示,后面跟着的函数是在其他文件中的定义的,有点像C语言中的extern关键字,这里的__main函数,System_Init函数都是在其他文件中定义的,所以这里会使用IMPORT,而 LDR ,是一种加载指令,用于从存储器中将一个32位的字数据传送到目的寄存器中,然后对数据进行处理。
          如 代码3所示,System_Init函数代码段的首地址,被加载到了R0寄存器,而 BLX 指令,可以简单地认为是一个子程序调用指令,将System_Init函数代码段的首地址,赋给PC(程序运行指针),系统就会转头去执行System_Init函数,并且把原先的PC值存储在R14寄存器,用于现场保存和恢复,代码4代码5 是把main函数的代码段首地址加载到了PC中,这也是为什么System_Init函数会在main函数之前执行的原因了。

          有点偏题了,哈哈!我们继续说这个时钟设置的主题!

          介绍了几种时钟后,我们接下来要讨论的,就是如何把系统时钟设置为我们的外部晶振,同样还是system_gd32f103x.c 源文件,在47行到61行之间,会有如下代码:

    /* select a system clock by uncommenting the following line */
    /* use IRC8M */
    //#define __SYSTEM_CLOCK_48M_PLL_IRC8M            (uint32_t)(48000000)
    //#define __SYSTEM_CLOCK_72M_PLL_IRC8M            (uint32_t)(72000000)
    //#define __SYSTEM_CLOCK_108M_PLL_IRC8M           (uint32_t)(108000000)
    
    /* use HXTAL (XD series CK_HXTAL = 8M, CL series CK_HXTAL = 25M) */
    //#define __SYSTEM_CLOCK_HXTAL                    (uint32_t)(__HXTAL)
    //#define __SYSTEM_CLOCK_24M_PLL_HXTAL            (uint32_t)(24000000)
    //#define __SYSTEM_CLOCK_36M_PLL_HXTAL            (uint32_t)(36000000)
    //#define __SYSTEM_CLOCK_48M_PLL_HXTAL            (uint32_t)(48000000)
    //#define __SYSTEM_CLOCK_56M_PLL_HXTAL            (uint32_t)(56000000)
    //#define __SYSTEM_CLOCK_72M_PLL_HXTAL            (uint32_t)(72000000)
    //#define __SYSTEM_CLOCK_96M_PLL_HXTAL            (uint32_t)(96000000)
    #define __SYSTEM_CLOCK_108M_PLL_HXTAL           (uint32_t)(108000000)
    

          以上代码,只需要你把相应的代码行取消注释,那么时钟就设置成功了,这里我把最后一行给取消注释了,也就意味着现在时钟系统最大可以输出108MHZ,很神奇对不对?但是凭各位彦祖的直觉,肯定会觉得不会那么简单,没错!
          我们继续往下看system_gd32f103x.c,在111行到113行,我们会看到以下代码:

    #elif defined (__SYSTEM_CLOCK_108M_PLL_HXTAL)
    uint32_t SystemCoreClock = __SYSTEM_CLOCK_108M_PLL_HXTAL;
    static void system_clock_108m_hxtal(void);
    

    这段代码意为:若宏定义了 __SYSTEM_CLOCK_108M_PLL_HXTAL ,则系统时钟设置为108MHZ,且采用外部高速时钟,经PLL锁相环倍频,输送其他总线,这些操作,由 system_clock_108m_hxtal() 函数执行。

    (2.2.2)时钟配置函数system_clock_108m_hxtal()

          好的!现在压力来到了system_clock_108m_hxtal()函数,让我们再一次右键跳转至第822行,在这里我们就能看到时钟配置函数的具体代码(具体跳转行数是由你选择的时钟决定的,不过内部代码套路是一样的),由于代码长度较长,我们分段来看。
          和系统时钟设置密切相关的功能,主要是RCU,而RCU中,常用的寄存器,是控制寄存器 (RCU_CTL),时钟配置寄存器 0 (RCU_CFG0),时钟配置寄存器 1 (RCU_CFG1),接下来我们结合代码,按序分析流程。

  • 第一部分:时钟使能以及就绪检查
  • 	uint32_t timeout = 0U;
    	uint32_t stab_flag = 0U;
    	RCU_CTL |= RCU_CTL_HXTALEN;	                   //代码1					
    	do
    	{
    		timeout++;
    		stab_flag = (RCU_CTL & RCU_CTL_HXTALSTB);  //代码2
    	}
    	while((0U == stab_flag) && (HXTAL_STARTUP_TIMEOUT != timeout));
    	if(0U == (RCU_CTL & RCU_CTL_HXTALSTB))	       //代码3					
    	{
    		while(1){}
    	}
    

          代码1的功能,是使能HXTAL,即开启外部高速,修改了原先SystemInit()函数将时钟源设置为内部晶振的操作,主要对控制寄存器 (RCU_CTL)进行操作,寄存器结构如下图所示:



          代码2和3的功能,是检查HXTAL是否就绪,检查RCU_CTL的HXTALSTB标志,RCU_CTL_HXTALSTB表示的是((uint32_t)((uint32_t)0x01U<<(17))),用于与RCU_CTL的值进行与运算,如果代码2的stab_flag结果为1,则表示HXTAL已经稳定,代码3的运算也是类似的,用于确定HXTAL是否未准备就绪,RCU_CTL相关位定义如下图所示:
          有些时候系统跑不起来,仿真的话,就有可能卡在这一步,具体原因有可能是外部晶振电路工作异常,一般来说,要去检查晶振是否合格,或者说是耦合电容是否合适等,具体原因具体分析。

  • 第二部分:电源管理单元PMU设置
  • RCU_APB1EN |= RCU_APB1EN_PMUEN;	    //代码4
    PMU_CTL |= PMU_CTL_LDOVS;			//代码5
    

          代码4和5的功能,是电源管理单元时钟使能,以及设置LDO的输出为高电压模式,这个可以暂时不处理。

  • 第三部分:PLL以及总线时钟设置
  • RCU_CFG0 |= RCU_AHB_CKSYS_DIV1;	         //代码6    							
    RCU_CFG0 |= RCU_APB2_CKAHB_DIV1;         //代码7						
    RCU_CFG0 |= RCU_APB1_CKAHB_DIV2;         //代码8
    
    /* select HXTAL/2 as clock source */
    RCU_CFG0 &= ~(RCU_CFG0_PLLSEL | RCU_CFG0_PREDV0);	//代码9
    RCU_CFG0 |= (RCU_PLLSRC_HXTAL | RCU_CFG0_PREDV0);   //代码10
    
    /* CK_PLL = (CK_HXTAL/2) * 27 = 108 MHz */
    RCU_CFG0 &= ~(RCU_CFG0_PLLMF | RCU_CFG0_PLLMF_4);   //代码11
    RCU_CFG0 |= RCU_PLL_MUL27;							//代码12
    RCU_CTL |= RCU_CTL_PLLEN;                           //代码13
    /* wait until PLL is stable */
    while(0U == (RCU_CTL & RCU_CTL_PLLSTB))             //代码14
    {}
    /* select PLL as system clock */
    RCU_CFG0 &= ~RCU_CFG0_SCS;							//代码15
    RCU_CFG0 |= RCU_CKSYSSRC_PLL;						//代码16
    /* wait until PLL is selected as system clock */
    while(0U == (RCU_CFG0 & RCU_SCSS_PLL))              //代码17
    {}
    

          讲道理,如果代码能执行到这里,HXTAL就已稳定了,下一步要进行的,就是对PLL的分频系数,倍频系数,以及之后的AHB,APB1和APB2的时钟分频设置,这里主要是操作RCU_CFG0和RCU_CFG1寄存器。

          代码6的功能,是设置AHB总线的时钟,RCU_AHB_CKSYS_DIV1是把RCU_CFG0的AHBPSC[3:0]设置为0xxxx,如图7所示,这里的x表示该位的数据可以随意设置,最终效果是让AHB的时钟等于CK_SYS,至于CK_SYS是什么,是多少,稍后会具体分析,RCU_CFG0寄存器结构如下图所示:


          代码7的功能,是设置APB2总线的时钟,RCU_APB2_CKAHB_DIV1其实和代码6类似,是把RCU_CFG0的APB2PSC[2:0]设置为0xx,即APB2的时钟等于CK_SYS,RCU_CFG0相关位定义如下:
          代码8的功能,是设置APB1总线的时钟,RCU_APB1_CKAHB_DIV2其实和代码6类似,是把RCU_CFG0的APB1PSC[2:0]设置为100,即APB1的时钟等于CK_SYS的1/2,RCU_CFG0相关位定义如下:

          代码9和10,这两项代码是关键代码,决定了PLL的输入时钟的种类,以及是否分频,如图1所示,结构1和结构2的功能就对应代码9和代码10,在这里,PLL的输入时钟被选择为HXTAL,且对输入PLL的HXTAL进行了二分频,具体的RCU_CFG0寄存器的位定义如图2所示:

                                                              图1

                                                                 图2

          代码11,12,13和14,这段代码实际上,就是设置了图1的结构3,对输入PLL的时钟,进行了倍频,最后输出CK_PLL时钟,此处的代码11,12最终效果就是把PLL输出的CK_PLL设置为108MHZ,即(HXTAL/2)*27 = 108MHZ,也就是把结构3处的倍频系数设置为27,RCU_CFG0寄存器具体的位定义如图3所示:
                                                                 图3

          代码13,14 的功能,就是在设置完相关的总线频率后,启动PLL,就会有彦祖问了,为啥要现在启动?那是因为只有在PLL未启动的情况下,之前的寄存器设置才会有效,在PLL启动时修改分频和倍频系数,是无效的,或者说会有很大的延迟,而代码14的功能,就是通过检测RCU_CTL寄存器的PLLSTB位来确定,PLL是否已经稳定,如果有彦祖的代码卡在了这里,那么就很有必要检查一下之前的PLL设置是否正确了。


          而代码15,16和17 的功能,其实就是把输出为108MHZ的CK_PLL设置为系统时钟,也就是我们之前在代码6出埋下伏笔的CK_SYS ,此处的代码15和16,就是将CK_PLL设置为系统时钟,也就是CK_PLL=CK_SYS,RCU_CFG0相关的位定义如图4所示,最后的代码17,就是等待系统将CK_PLL设置为系统时钟,这玩意设置还是有延迟的,代码17完成后,GD32F103xx的时钟系统设置就大功告成,AHB,APB1,APB2总线的频率,也很明朗了。
                                                                 图4

  • 最后一问:system_clock_108m_hxtal函数在哪里调用了?
          我们会发现,system_clock_108m_hxtal函数好像没有被调用,那我们设置个毛线呀?其实,这个函数已经被调用了,被谁调用了?我们重新回到SystemInit(),在第206行,发现了system_clock_config(),我们继续跳转至system_clock_config(),最终在142行处,发现了我们的system_clock_108m_hxtal(),而SystemInit(),早就已经在main函数之前被调用,所以很多时候,我们在主函数里找不到系统时钟设置函数呀!

  • (3)结语

    下一章:(2)在Hal库和标准库下对GD32进行编程

    另外说一下,我已经开始设计GD32F103xx的小开发板了,板上资源主要有:

  • GD32F103RCT6
  • 240*240 1.54寸IPS屏幕
  • WH BLE103蓝牙模块
  • RS232和RS485接口
  • 一组RGB灯
  • ESP12F WIFI模块
  • LIS2DW12加速度计
  • 等等
  • 有效评论,加关注收藏的彦祖们,我会随机送出共计10份开发板!

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【GD32系列开坑:零死角玩转GD32】

    发表评论