国产单片机GD32系列开坑,带你零死角玩转GD32 第三章

2,598 阅读15分钟

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


第三章 GD32F103xx时钟系统分析

(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份开发板!

在这里插入图片描述