三十功名尘与土,八千里路云和月;
第二章 GD32开发环境的搭建,常用资料的获取初步地介绍了GD32工程的创建方式以及常用资料获取,但是可能会有彦祖发现,好像没有讲怎么点灯呀?
理论上来说,讲完工程创建,确实应该再讲一下怎么点灯,但是总觉得这中间少了什么,拿到一款MCU之后,很多人会去测试它的IO口,串口,以及I2C等功能,但很多时候我们忽略了一个很重要的部分,也就是时钟系统,时钟系统对于MCU的重要性,等同于彦祖们的关注对我的重要性。
好了!言归正传,在MCU上,不管是哪个外设,它都需要时钟系统对其提供一个基本工作时钟,这个外设才能正常而又协调地运行,下面我们会详细讨论GD32F103xx的时钟系统。(PS:已经开始画GD32F103RCT6的板子了,后续点灯之类的操作,都会在这个板子上进行啦!)
在开始这段话题之前,希望彦祖们已经有GD32F103xx的数据手册了,因为我们这部分的分析,都是围绕数据手册所涉及的内容进行的。
如图1所示的GD32F103xx芯片架构图可以看出,诸如GPIO,SPI,ADC,DAC等外设,都是挂载在APB1,APB2总线上的,而APB1和APB2总线,都是由AHB总线桥接而成。
包括数据的传输,外设的时钟频率基准,都是由这三条横贯在MCU内部的高速总线提供的,如果把MCU比作一座城市,那么诸如APB1,APB2,AHB等总线,就可以看做是纵横在这座城市的高速公路,如果想要熟悉这座城市,第一件事,应该是要自己去熟悉这座城市的交通吧?难道用高德地图么?
这部分是今天重点讨论的地方,结合数据/用户手册和代码,通过重构整个时钟系统的方式,来分析GD32F103xx的时钟系统的构成和功能。
我们会发现,有时候我们手上的板子,在MCU的边上,会有一到两个晶振,晶振的外形,焊接方式,封装可能都不一样,就像下面这两个。
但不管是哪一种,它们都有一个统一的名字,叫做“外部晶振” ,顾名思义,外部晶振就是放在外面的晶振。
而且外部晶振也分两种,第一种是外部高速晶振,第二种是外部低速晶振,至于这两种的区别,我们待会细讲。
有时候,我们会发现,某个板子,明明没有外接晶振,但还是能跑起来,跑得还很流畅,这能说明一件事,那就是在MCU内部,同样也是有晶振的,对于GD32F103xx来说,就是以下两个内部晶振:
在没有外接晶振,或者外部晶振没有被使能的情况下,系统是可以使用内部晶振的,那么这时候就会有彦祖问:怎么通过代码来选择需要使用的晶振和总线的频率呢?问得好!接下来我们通过代码和手册,来一步步地分析GD32F103xx的时钟选择方法以及如何把各个总线设置为不同的时钟频率。
首先,在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() 函数执行。
好的!现在压力来到了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相关位定义如下图所示:
有些时候系统跑不起来,仿真的话,就有可能卡在这一步,具体原因有可能是外部晶振电路工作异常,一般来说,要去检查晶振是否合格,或者说是耦合电容是否合适等,具体原因具体分析。
RCU_APB1EN |= RCU_APB1EN_PMUEN; //代码4
PMU_CTL |= PMU_CTL_LDOVS; //代码5
代码4和5的功能,是电源管理单元时钟使能,以及设置LDO的输出为高电压模式,这个可以暂时不处理。
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
代码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
下一章:(2)在Hal库和标准库下对GD32进行编程
另外说一下,我已经开始设计GD32F103xx的小开发板了,板上资源主要有: