资讯详情

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

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


第三章 GD32F103xx时钟系统分析

目录

(1)前言

第二章 GD32开发环境建设,获取常用数据初步介绍GD创建32个项目的方式和获取常用数据,但可能有彦祖发现,好像没有说怎么点灯?

在这里插入图片描述 理论上讲,完成工程创建后,确实应该再么点灯,但总觉得中间少了什么,拿到一个MCU之后,很多人会测试它IO口,串口,还有I2C但很多时候,我们忽略了一个非常重要的部分,即时钟系统,时钟系统MCU它的重要性相当于彦祖对我的关注。

好了!言归正传,在MCU无论哪种外设,它都需要时钟系统为其提供一个基本的工作时钟,以便正常和协调运行。下面我们将详细讨论GD32F103xx时钟系统。(PS:已经开始画GD32F103RCT6板,后续点灯等操作,都会在这个板上进行!)


(2)GD32F103xx时钟架构分析

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

(2.1) GD32F103xx芯片架构图

如图1所示的GD32F103xx可以看到芯片架构图,如GPIO,SPI,ADC,DAC等外设,都是挂载的APB1,APB在总线上,而APB1和APB总线,都是由的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份开发板!

标签: gd8216电容器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台