第七章 时钟体系
时钟信号是数字时序电路的“脉搏”,电路每接收到一个周期的时钟信号,就做一个相应的动作。因此,在允许的范围内,时钟信号的快慢直接决定着电路性能的好坏。在片上系统(SOC)中,不同的模块通常需要工作在不同的时钟频率。为了满足这些需求,芯片将时钟源信号进行稳定、倍频、分频、分发以及屏蔽(gate)等操作,产生不同频率的时钟信号。这些时钟信号和它们的管理电路构成了芯片的时钟体系,驱动着各种各样的功能模块协同工作。
6.1 IMX6ULL时钟体系介绍
6.1.1 晶体振荡电路
时钟信号不是凭空产生的,芯片首先需有一个频率较低的源时钟信号。imx6ull包含两个偏压放大器(biased amplifier),当外部连接合适的晶振和负载电容时,能够分别产生24MHZ和32KHZ的时钟信号。下面是开发板的时钟引脚连接原理图:
当imx6ull的时钟引脚XTALI和XTALO连上合适的晶振和电容时,模块XTALOSC24M会产生24MHZ的时钟信号。注意,输出频率到达24MHZ并不意味着模块XTALOSC24M已经稳定工作,仍然需要等待一段时间。一旦时钟信号稳定,可以通过设置寄存器来降低它的工作电流;但是注意,在关闭模块XTALOSC24M电源之前,相应的值应当被恢复,否则恢复供电时模块不能正常启动。
另外,imx6ull还有一个内置的24MHZ RC振荡器,它以RTC时钟(32KHZ)做基准产生时钟信号。尽管能源消耗显著低于模块XTALOSC24M,RC振荡器的精确度存在较大误差,实际应用中应当避免使用它。
当imx6ull的时钟引脚RTC_XTALI和RTC_XTALO连接32KHZ或32.768KHZ的晶振时,用模块RTC_XTAL产生32KHZ的RTC时钟信号。除此之外,imx6ull还包含一个内部的32KHZ振荡器,当时钟系统探测到RTC振荡器丢失时钟信号时,会自动切换到内部的32KHZ振荡器。但是由于该内部振荡器精度不如模块RTC_XTAL,不能作为替代品长时间使用。
RTC时钟信号主要用来记录时间,而XTALOSC24M的输出时钟信号为芯片的时钟体系提供基础的源时钟信号,是本篇讨论的重点。除此之外,芯片本身也可以直接接收时钟信号作为源时钟信号,这需要芯片外部提供一个稳定的时钟信号源。这种方式较不常用,本文不再进行描述。
6.1.2 锁相环电路
XTALOSC24M的时钟信号只有24MHZ,远远不能满足实际需求,在芯片中需要进行稳定和倍频操作,这主要是由锁相环电路完成的。
锁相环(PLL)由鉴相器PD、低通滤波器LPF和压控振荡器VCO组成。鉴相器(PD)用来鉴别两个输入时钟信号之间的相位差,并输出误差电压,经过低通滤波(LPF)后,形成压控振荡器(VCO)的控制。压控振荡器的输出经过分频(DIV)后反馈到鉴相器与基准信号进行比较,最终,VCO的输出就会稳定下来。下图是锁相环工作原理示意图:
imx6ull包含7个锁相环电路,它们的输入时钟信号称为源时钟信号,可通过寄存器选择,通常为XTALOSC24M产生24MHZ时钟信号。它们的输出进过进一步选择和分频,形成不同的根时钟信号,分发到各个模块使用。这些锁相环电路及它们的分频器输出如下图所示:
下面分别介绍这些PLL的功能。
1. PLL1:
被称为ARM_PLL,用来驱动ARM核心工作。它能够倍频达到1.3GHZ,注意这个频率超过了芯片能够工作的最大频率1GHZ。
2. PLL2:
被称为SYS_PLL或者528_PLL。它的倍频参数固定在x22,在使用XTALOSC24M产生的24MHZ时钟作为参考时钟时,产生528MHZ的输出。除了这个主输出,SYS_PLL还包含四个分频器,主输出和分频器的输出可用来作为根时钟。SYS_PLL的这些输出时钟信号的频率并不需要是确定或者精确的值,可在运行时动态进行改变。通常,它们用来驱动芯片内部的系统总线,内部处理逻辑,DDR接口以及NAND/NOR等等。
3. PLL3:
被称为USB1_PLL,用来驱动第一个USB物理层实体USBPHY1。它的倍频参数固定在x20,在使用24MHZ参考时钟时产生480MHZ的输出。除了主输出之外,USB1_PLL同样包含四个分频器,它们的输出用来作为需要固定频率的根时钟,比如UART和其它的串行接口,音频接口等。
4. PLL4:
被称为AUDIO_PLL,能够进行倍频和分频操作,产生低抖动、高精度标准音频时钟信号。AUIDIO_PLL的输出频率范围从650MHZ到1300MHZ,时钟的频率分辨率要好于1HZ。该输出时钟信号主要用来驱动串行音频接口或者作为外部音频解码器的参考时钟。另外,AUDIO_PLL的分频器,可对VCO的输出时钟信号进行/1、/2或/4分频。
5. PLL5:
被称为VIDEO_PLL。它同样具有倍频和分频功能,能够产生低抖动、高精度标准视频时钟信号。VIDEO_PLL的输出频率范围从650MHZ到1300MHZ,时钟的频率分辨率要好于1HZ。该输出时钟主要作为显示和视频接口的时钟信号。另外,VIDEO_PLL的分频器,可对VCO的输出时钟信号进行/1、/2、/4或/8分频。
6. PLL6:
被称为ENET_PLL。它的倍频参数固定为x20 (5/6),在使用24MHZ参考时钟时产生500MHZ的输出。它主要用来生成:(1)50MHZ或25MHZ时钟,用于外部以太网接口;(2)125MHZ时钟,用于精简的千兆以太网接口;(3)100MHZ时钟,用于通用功能。
7. PLL7:
被称为USB2_PLL,专门用于驱动第二个USB物理层实体USBPHY2。它的倍频参数固定为x20,输出480MHZ的时钟信号。
上述锁相环电路都有自己专门的控制和状态寄存器,它们可独立配置为以下3个模式中的一种:
1) Bypass模式:PLL输入的参考时钟直接传递到输出,由BYPASS位控制;
2) 输出禁止模式:无论bypass时钟还是PLL生成的时钟均被禁止,无输出时钟信号,由ENABLE位控制;
3) 断电模式:PLL中大部分电路断电,无输出时钟信号,由POWERDOWN位控制。
以ARM_PLL为例,单独截取出来说明,见下图。PLL正常工作时,时钟信号通过路径1传输作为信号ref_armpll_clk输出;处于Bypass模式时,源时钟信号不经过PLL放大,由路径2直接输出;处于输出禁止模式时,时钟信号在armpll_enable处被屏蔽,ref_armpll_clk无输出;处于断电模式时,ARM_PLL大部分电路断电,电路不工作,ref_armpll_clk无输出信号。
其中,两个锁相环电路SYS_PLL和USB1_PLL,分别带有四个分相器(PFD)对其产生的PLL输出信号进行分频(每个分相器可独立设置分频参数),用来产生额外的频率输出。由于分相器完全由数字器件组成且不包含反馈回路,我们只需要改变逻辑组合就能改变分频参数的值,不影响锁相环电路的锁定状态,因此分相器能够比锁相环更快的改变输出频率。除此之外,分相器的值还能在运行时改变,不需要在改变前后关闭和开启时钟的输出。
注意,对于那些包含分相器的锁相环电路,每个分相器有自己独立的时钟屏蔽控制位。当相连的锁相环电路加电启动或者重新锁定时,分相器自动进入屏蔽状态,需要手动对每个PFD进行一次屏蔽和开启操作。
6.1.3 根时钟信号电路
如前所述,锁相环电路的输出时钟信号并不能直接供其它模块使用。在imx6ull中,它们的输出时钟信号、PFD时钟信号以及对应的bypass时钟信号经过选择、分频后形成根时钟信号,才会分发到各个模块。这部分电路又细分为两部分,前半部分称为时钟切换电路(switcher),后半部分称为根时钟生成电路(root generator)。
时钟切换电路主要对PLL1和PLL3的输出进行选择,被选中的信号形成pll1_sw_clk和pll3_sw_clk信号。另外,它还对PLL4和PLL5进行额外的分频操作,形成pll4_main_clk和pll5_main_clk信号。如下图所示:
后续电路不直接使用上述PLL的输出,而是使用switcher形成的这些输出信号。例如,如果我们想改变CPU的工作频率,可以先修改CCSR[pll1_sw_clk_sel]将pll1_sw_clk切换到step_clk,然后修改PLL1的参数,等待其输出时钟信号稳定到新的频率上,再切换回PLL1的输出信号pll1_main_clk。因为使用了无抖动的多路选择器(glitchless multiplexer),在切换过程中CPU仍正常运行,我们将在第一个编程示例中演示上述过程。
根时钟生成电路对前面的这些信号和PLL2的输出信号进一步筛选和分频形成根时钟信号,它们直接或者间接驱动芯片中的模块来实现各自功能。这里仅以总线相关的根时钟信号进行说明,如下图所示:
其它的根时钟信号和模块对这些根时钟信号的使用参见CCM和模块各自的寄存器设置,这里不再展开叙述。本章第二个编程示例计算锁相环电路输出时钟和这些总线的根时钟的频率并打印,有兴趣的同学也可以参照示例代码和imx6ull手册计算其它的时钟信号的频率。
6.2 寄存器介绍
imx6ull时钟相关的寄存器主要分布在CCM和ANALOG_DIG这两个模块,它们均连接在AIPS-1总线上,它们的地址范围如下所示:
ANALOG_DIG模块主要负责晶体振荡电路和锁相环电路的相关设置。它的寄存器分为两类:CCM_ANALOG_PLL_xxx寄存器设置对应PLL的参数和工作状态,CCM_ANALOG_MISCx (x = 0-2)寄存器进行其它一些杂项的设置或状态显示,包括晶体振荡电路的控制参数。这些寄存器数量较多,这里不一一列出。
另外需要注意,ANALOG_DIG与电源管理模块(PMU)共用这些CCM_ANALOG_MISCx寄存器,它们在PMU中被称为PMU_MISCx (x = 0-2)。由于晶体振荡电路在系统启动时已初始化完毕,输出频率固定的时钟信号,在芯片运行期间通常不需要修改设置,这里不再进行说明。
而CCM模块控制根时钟信号的产生和分发,大部分寄存器用来对PLL及PFD产生的时钟信号进行分发和分频控制,如下表所示:
以及下面的表,注意红框部分的寄存器CCM_CCGRx(x = 0-6),它们用来控制各个时钟信号在不同功耗模式下是否被屏蔽:
6.2.1 锁相环电路寄存器
如前所述,imx6ull共包含7个锁相环电路,除ENET_PLL之外,其它的锁相环电路控制和状态寄存器的结构都很类似。以ARM_PLL为例,它的寄存器结构如下所示:
其中,控制位BYPASS、ENABLE和POWERDOWN用来控制PLL的工作模式(bypass模式、输出禁止模式和断电模式);BYPASS_CLK_SRC选择输入时钟源,DIV_SELECT设置频率放大倍数。正常工作时,需要设置BYPASS和POWRDOWN为0,ENABLE为1。当LOCK值为1时,锁相环电路输出稳定的时钟信号。
稳定工作时,ARM_PLL的输出频率为 Fref * DIV_SELECT/2,其它PLL的设置方法和输出频率计算公式与ARM_PLL类似,但略有差别。例如,锁相环电路USB1_PLL、USB2_PLL和SYS_PLL虽然都有自己的DIV_SELECT,它们应当被设为固定的值。
需要特别说明的是,锁相环电路AUDIO_PLL和VIDEO_PLL还增加了额外的分频参数NUM和DENOM。以AUDIO_PLL为例,它的相应寄存器如下所示:
(1)CCM_ANALOG_PLL_AUDIO_NUM
(2)CCM_ANALOG_PLL_AUDIO_DENOM
它们的输出频率为Fref * (DIV_SELECT NUM/DENOM)。除此之外,AUIDIO_PLL和VIDEO_PLL还可以在时钟切换电路(switcher)中设置额外的分频参数为/1、/2、/4、/8或/16,这些值分布在寄存器CCM_ANALOG_PLL_AUDIO、CCM_ANALOG_PLL_VIDEO和CCM_ANALOG_MISC2中。
除此之外,SYS_PLL和USB1_PLL还各自配有四个分相器,它们分别对SYS_PLL和USB1_PLL的输出时钟信号进行分频,分频参数分别在CCM_ANALOG_PFD_480n和CCM_ANALOG_PFD_528n中设置。这两个寄存器的结构完全一样,如下图所示:
每个PFD的输出频率为Fvco*18/PFD_FRAC,其中Fvco是相应PLL的输出频率,而PFD_FRAC的数值取值范围为12到35。
6.2.2 根时钟控制寄存器
上述锁相环电路以及它们的bypass时钟信号、PFD输出信号,经过时钟切换电路(switcher)和根时钟生成电路(root generator)处理后形成各种根时钟信号。其中,时钟切换电路寄存器CCM_CCSR选择时钟信号pll1_sw_clk和pll3_sw_clk的来源,如下图所示:
根时钟生成电路的寄存器也包含在CCM模块中,比如ARM根时钟信号由pll1_sw_clk分频得来,相应的分频寄存器CCM_CACRR如下图所示:
ARM的时钟信号频率为pll1_sw_clk/(ARM_PODF 1)。
而之前提到的总线根时钟信号由寄存器CCM_CBCDR进行选择和分频,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XdqIiUvC-1642060018419)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/07_clock_system_image016.png)]
其中各个字段的作用可参见根时钟信号电路一节中的原理图,具体设置步骤可参见后面的编程示例。其它根时钟的寄存器与其类似,详细的控制方式要参见imx6ull手册中CCM模块的寄存器描述,这里不再一一列举。
6.2.3 模块时钟屏蔽寄存器
为了降低功耗,imx6ull可以工作在以下三种模式:
- RUN模式:CCM_CLPCR[LPM]的值为0,CPU正常工作,各个模块的时钟信号可以在寄存器CCGRx中开启和关闭。
- WAIT模式:CCM_CLPCR[LPM]的值为1,当CPU执行WFI指令时,开始进入WAIT模式。在此模式下,CPU时钟被关闭,依据寄存器CCGRx中的设置,相应模块的时钟信号也会被关闭。
- STOP模式:CCM_CLPCR[LPM]的值为2。STOP模式同样重复上述WAIT模式的操作,并且禁用所有的PLL。如果设置了CCM_CLPCR[SBYOS],该模式还将激活cosc_pwrdown信号,关闭晶体振荡电路的电源。
对于每个时钟信号,imx6ull提供了在不同工作模式下是否屏蔽这些时钟信号的控制方法,这些控制位集中放在寄存器CCGRx(x = 0-6)中,每个CCGRx寄存器结构如下图所示:
每两位为一个单位,控制一个时钟信号。比如,在寄存器CCGR0中,CG15控制时钟信号gpio2_clocks,CG14控制时钟信号uart2_clock等等。这两位值的含义如下所示:
CGx (x=0-15) | 时钟信号活动状态描述 |
---|---|
00 | 时钟信号在三个模式中均被屏蔽。 |
01 | 时钟信号仅在RUN模式开启,在其它模式中被屏蔽。 |
10 | 保留 |
11 | 时钟信号在RUN和WAIT模式开启,在STOP模式中被屏蔽。 |
当用户某个模块驱动时,应当根据该模块是否需要在低功耗模式下工作来设置寄存器CCGRx中相应的值,以达到降低功耗的目的。另外,在正常工作模式中,用户也可以在模块空闲时将CCGRx中相应的值设为0,动态调节模块的功耗。
6.3 编程示例
6.3.1 改变CPU工作频率
本示例动态改变CPU的工作频率,先将CPU设置工作在一个较低频率(81MHZ),然后切换至较高的工作频率(648MHZ)。为验证CPU频率的变化,本例使用忙等待的延时方式,控制LED灯闪烁–随着CPU频率的提升,延时时间变短,LED灯闪烁频率变快。
正常工作时,CPU使用锁相环电路ARM_PLL的输出信号作为时钟源。在改变CPU频率之前,我们首先切换到其它时钟信号(示例中选择晶体振荡电路XTALOSC24M的输出),修改ARM_PLL设置并稳定在新的频率之后,再切换回ARM_PLL的输出时钟信号。这里我们再次引用时钟切换电路(switcher)的部分原理图,如下所示:
6.3.1.1 设置PLL1_SW_CLK的时钟路径
改变CPU频率前,pll1_sw_clk时钟路径如图中路径1所示。我们首先将其切换至路径2,待ARM_PLL(PLL1)稳定输出后,再切换回路径1。这些时钟路径的控制位标示位于图中黄色方框处。对应的控制函数为set_pll1_sw_clk(int sel_pll1),当参数sel_pll1值为0时,选择路径2;当参数sel_pll1值非0时,选择路径1。函数set_pll1_sw_clk定义在文件switcher.c中,内容如下所示:
代码语言:javascript复制4 extern struct ccm_regs *ccm;
5
/**********************************************************************
* 函数名称: set_pll1_sw_clk
* 功能描述: 设置PLL1_SW_CLK的时钟路径
* 输入参数: sel_pll1: 0-选择XTALOSC24M的输出,1-选择PLL1的输出
* 输出参数: 无
* 返 回 值: 无
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
void sel_pll1_sw_clk(int sel_pll1)
{
/* PLL1_SW_CLK_SEL: 0表示pll1_main_clk,1表示step_clk */
if (sel_pll1)
clr_bit(&ccm->ccsr, 2); /* 选择pll1_main_clk */
else {
clr_bit(&ccm->ccsr, 8); /* step_clk选择使用OSC的输出 */
set_bit(&ccm->ccsr, 2); /* 选择step_clk */
}
}
6.3.1.2 重新设置ARM_PLL的输出频率
切换完pll1_sw_clk时钟路径之后,我们就可以重新设置ARM_PLL的输出频率,相应的设置函数set_pll设置指定PLL的倍频参数并等待其输出频率稳定。为了简化函数接口,AUDIO_PLL和VIDEO_PLL的NUM和DENOM参数统一设置为0xF,而且不支持ENET_PLL的设置,感兴趣的同学可以自己添加相关代码。该函数位于文件pll.c中,内容如下:
代码语言:javascript复制struct anadig_regs *anadig = (struct anadig_regs *)ANADIG_BASE_ADDR;
static void wait_to_lock(u32 *pll_reg)
{
while (read32(pll_reg) & LOCK_MASK == 0); /* 等待指定的PLL进入锁定状态 */
}
/**********************************************************************
* 函数名称: set_pll
* 功能描述: 设置PLL的倍频参数并等待其进入锁定状态
* 输入参数: pll: 指定PLL的标识,div: PLL的倍频参数
* 输出参数: 无
* 返 回 值: 无
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
void set_pll(pll_e pll, u32 div)
{
switch (pll) {
case ARM_PLL:
if (div < 54 && div > 108) return; /* ARM_PLL的倍频参数的有效范围为54到108 */
write32(ENABLE_MASK | div, &anadig->analog_pll_arm);
wait_to_lock(&anadig->analog_pll_arm); /* 等待ARM_PLL锁定 */
break;
case USB1_PLL: /* 设置USB1_PLL的倍频参数并等待锁定 */
write32(ENABLE_MASK | (div&0x3), &anadig->analog_pll_usb1);
wait_to_lock(&anadig->analog_pll_usb1);
break;
case USB2_PLL: /* 设置USB2_PLL的倍频参数并等待锁定 */
write32(ENABLE_MASK | (div&0x3), &anadig->analog_pll_usb2);
wait_to_lock(&anadig->analog_pll_usb2);
break;
case SYS_PLL: /* 设置SYS_PLL的倍频参数并等待锁定 */
write32(ENABLE_MASK | (div&0x1), &anadig->analog_pll_sys);
wait_to_lock(&anadig->analog_pll_sys);
break;
case AUDIO_PLL:
if (div < 27 && div > 54) return; /* AUDIO_PLL的倍频参数的有效范围为27到54 */
/* 简便起见,AUDIO_PLL的分频参数NUM和DENOM都设置为0xF */
write32(0xF, &anadig->analog_pll_video_num);
write32(0xF, &anadig->analog_pll_video_denom);
write32(ENABLE_MASK | div, &anadig->analog_pll_video);
wait_to_lock(&anadig->analog_pll_video);/* 等待AUDIO_PLL锁定 */
break;
case VIDEO_PLL:
if (div < 27 && div > 54) return; /* VIDEO_PLL的倍频参数的有效范围为27到54 */
/* 简便起见,VIDEO_PLL的分频参数NUM和DENOM都设置为0xF */
write32(0xF, &anadig->analog_pll_audio_num);
write32(0xF, &anadig->analog_pll_audio_denom);
write32(ENABLE_MASK | div, &anadig->analog_pll_video);
wait_to_lock(&anadig->analog_pll_video);/* 等待VIDEO_PLL锁定 */
break;
case ENET_PLL:
/* ENET_PLL寄存器设置方式与其它PLL差别较大,为了简化函数接口,这里不支持对它的设置 */
break;
}
}
6.3.1.3 设置ARM_CLK_ROOT的分频参数
时钟信号pll1_sw_clk在成为arm_clk_root送往CPU之前,还要在根时钟生成电路(root generator)经过一次分频操作,其分频参数的设置函数为setup_arm_podf,位于文件clkroot.c中,内容如下:
代码语言:javascript复制extern struct ccm_regs *ccm;
/**********************************************************************
* 函数名称: setup_arm_podf
* 功能描述: 设置ARM_CLK_ROOT的分频参数
* 输入参数: 取值范围为1-8,ARM_CLK_ROOT = PLL1_SW_CLK / PODF
* 输出参数: 无
* 返 回 值: 无
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
void setup_arm_podf(u32 podf)
{
if (podf < 1 || podf > 8) return; /* ARM_PODF分频范围是1到8 */
write32(podf-1, &ccm->cacrr);
}
6.3.1.4 修改led闪烁函数
最后,为了方便比较led的闪烁频率,我们对led的接口函数稍作修改,增加led_toggle函数,led的状态改变一次(从亮到灭或从灭到亮),这里不再展示其代码。
在main函数中,我们首先初始化并点亮led灯,设置CPU频率到81MHZ(ARM_PLL输出为648MHZ,分频参数为8),亮灯和灭灯各5次;然后设置CPU频率为648MHZ(ARM_PLL输出为1296MHZ,分频参数为2),之后无限闪灯。我们可通过肉眼观测到led的闪烁频率明显变快。main.c文件内容如下所示:
代码语言:javascript复制#include "regs.h"
#include "pll.h"
#include "clkroot.h"
struct ccm_regs *ccm = (struct ccm_regs *)CCM_BASE_ADDR;
#define LOOPS 1000000
static void busy_wait(void)
{
for(u32 i = 0; i < LOOPS; i ); /* 忙等待进行延时 */
}
/* LED灯接口函数 */
extern void led_init(void);
extern void led_toggle(void);
void led_on(void);
/* PLL1时钟路径及其PODF分频参数设置 */
extern void setup_arm_podf(u32 podf);
extern void sel_pll1_sw_clk(int sel_pll1);
void main(void)
{
/* 首先初始化并点亮LED灯 */
int blinks = 0;
led_init();
led_on();
sel_pll1_sw_clk(0); /* 将ARM_ROOT时钟切换至OSC */
setup_arm_podf(8); /* ARM_ROOT的分频参数设置为8 */
set_pll(ARM_PLL, 54); /* 设置ARM_PLL: 24*54/2 = 648MHZ, ARM_ROOT: 81MHZ */
sel_pll1_sw_clk(1); /* 将ARM_ROOT切换回ARM_PLL,此时CPU工作频率为81MHZ */
/* 循环点亮/熄灭LED共10次,观察LED闪烁频率 */
for (blinks = 10; blinks > 0; blinks--)
{
busy_wait();
led_toggle();
}
sel_pll1_sw_clk(0); /* 将ARM_ROOT时钟切换至OSC */
setup_arm_podf(2); /* ARM_ROOT的分频参数设置为2 */
set_pll(ARM_PLL, 108); /* 设置ARM_PLL: 24*108/2 = 1296MHZ, ARM_ROOT: 648MHZ */
sel_pll1_sw_clk(1); /* 将ARM_ROOT切换回ARM_PLL,此时CPU工作频率为648MHZ */
/* 无限循环点亮/熄灭LED,观察此时LED闪烁频率明显变快 */
while(1)
{
busy_wait();
led_toggle();
}
}
/* 本程序的除法运算使用了GCC提供的函数,需要提供raise函数以正常编译 */
void raise(void)
{
}
最后说明一下,由于本程序用到了GCC的除法操作例程,需要添加一个空的raise函数,以通过编译。
注明: 代码目录在裸机Git仓库 NoosProgramProject/(7_时钟体系/fastcpu) 文件夹下。
6.2.1.4 参考章节《4-1.4编译程序》编译程序
6.2.1.5 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序
运行成功后可以观察到开发板用户绿色led灯闪烁频率由慢变快。
6.3.2 打印时钟信号的频率值
上个示例中我们可以通过led的闪烁频率观察到CPU的频率确实变快了。为了得到imx6ull中时钟的确切值,我们在本例程中将它们通过串口打印出来,这里我们使用了uart模块的功能,其代码参见后面的uart串口编程章节。
本例程仅打印各个PLL(不包括ENET_PLL)及其PFD的输出频率,以及前面介绍的总线根时钟频率,这些频率值的计算需要分为三步进行。
6.3.2.1 获取PLL的输出频率
首先,我们需要确定PLL和PFD的输出频率,在文件pll.c中添加以下代码:
代码语言:javascript复制/**********************************************************************
* 函数名称: get_pll
* 功能描述: 获取PLL的输出频率
* 输入参数: pll: 指定PLL的标识
* 输出参数: 无
* 返 回 值: PLL的输出频率
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
u32 get_pll(pll_e pll)
{
u32 div, post_div, pll_num, pll_denom;
switch (pll) {
case ARM_PLL:
div = read32(&anadig->analog_pll_arm);
if (div & BYPASS_MASK) /* 判断ARM_PLL是否处于Bypass模式 */
return CKIH;
else {
div &= 0x7F; /* 获取ARM_PLL的倍频参数 */
return (CKIH * div) >> 1; /* ARM_PLL的输出频率只有倍频后的一半 */
}
case USB1_PLL:
div = read32(&anadig->analog_pll_usb1);
if (div & BYPASS_MASK) /* 判断USB1_PLL是否处于Bypass模式 */
return CKIH;
else {
div = div&0x1 ? 22 : 20; /* USB1_PLL只有两种倍频模式,1表示x22,0表示x20 */
return CKIH * div;
}
case USB2_PLL:
div = read32(&anadig->analog_pll_usb2);
if (div & BYPASS_MASK) /* 判断USB2_PLL是否处于Bypass模式 */
return CKIH;
else {
div = div&0x1 ? 22 : 20; /* USB2_PLL只有两种倍频模式,1表示x22,0表示x20 */
return CKIH * div;
}
case SYS_PLL:
div = read32(&anadig->analog_pll_sys);
if (div & BYPASS_MASK) /* 判断SYS_PLL是否处于Bypass模式 */
return CKIH;
else {
div = div&0x1 ? 22 : 20; /* SYS_PLL只有两种倍频模式,1表示x22,0表示x20 */
return CKIH * div;
}
case AUDIO_PLL:
div = read32(&anadig->analog_pll_audio);
if (!(div & ENABLE_MASK)) /* 判断AUDIO_PLL是否处于禁止输出模式 */
return 0;
if (div & BYPASS_MASK) /* 判断AUDIO_PLL是否处于Bypass模式 */
return CKIH;
else {
post_div = (div & 0x3) >> 19;
if (post_div == 3) /* reserved value */
return 0;
/* AUDIO_PLL的分频参数:0表示除以4,1表示除以2,2表示除以1 */
post_div = 1 << (2 - post_div);
pll_num = read32(&anadig->analog_pll_audio_num);
pll_denom = read32(&anadig->analog_pll_audio_denom);
return CKIH * (div pll_num / pll_denom) / post_div;
}
case VIDEO_PLL:
div = read32(&anadig->analog_pll_video);
if (!(div & ENABLE_MASK)) /* 判断VIDEO_PLL是否处于禁止输出模式 */
return 0;
if (div & BYPASS_MASK) /* 判断VIDEO_PLL是否处于禁止输出模式 */
return CKIH;
else {
post_div = (div & 0x3) >> 19;
if (post_div == 3) /* reserved value */
return 0;
/* VIDEO_PLL的分频参数:0表示除以4,1表示除以2,2表示除以1 */
post_div = 1 << (2 - post_div);
pll_num = read32(&anadig->analog_pll_video_num);
pll_denom = read32(&anadig->analog_pll_video_denom);
return CKIH * (div pll_num / pll_denom) / post_div;
}
default:
return 0;
}
/* NOTREACHED */
}
static void set_pfd(u32 *reg, pfd_e pfd, int gate, u32 frac)
{
u32 value = read32(reg); /* 读取指定PLL的PFD寄存器 */
value &= ~PFD_MASK(pfd);
if (gate) value |= PFD_GATE_MASK(pfd); /* 设置是否屏蔽该PFD的输出 */
value |= (frac<<PFD_SHIFT(pfd)) & PFD_FRAC_MASK(pfd); /* 设置该PFD的分频参数 */
write32(value, reg);
while(read32(reg) & PFD_STABLE_MASK(pfd));
}
/**********************************************************************
* 函数名称: set_pll_pfd
* 功能描述: 设置SYS_PLL或USB1_PLL的PFD状态和分频参数
* 输入参数: pll: 指定PLL的标识,pfd: 指定PFD的编号,gate: 是否屏蔽该PFD的输出,frac: PFD的分频参数
* 输出参数: 无
* 返 回 值: 无
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
void set_pll_pfd(pll_e pll, pfd_e pfd, int gate, u32 frac)
{
u32 *reg;
if (pll = SYS_PLL)
reg = &anadig->analog_pfd_528;
else if (pll = USB1_PLL)
reg = &anadig->analog_pfd_480;
else
/* 只有SYS_PLL和USB1_PLL支持PFD输出 */
return ;
set_pfd(reg, pfd, gate, frac);
}
/**********************************************************************
* 函数名称: get_pll_pfd
* 功能描述: 获取SYS_PLL或USB1_PLL的PFD输出频率
* 输入参数: pll: 指定PLL的标识,pfd: 指定PFD的编号
* 输出参数: 无
* 返 回 值: 无
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
u32 get_pll_pfd(pll_e pll, pfd_e pfd)
{
u32 div;
u64 freq;
switch (pll) {
case SYS_PLL:
div = read32(&anadig->analog_pfd_528);
freq = (u64)get_pll(SYS_PLL);
break;
case USB1_PLL:
div = read32(&anadig->analog_pfd_480);
freq = (u64)get_pll(USB1_PLL);
break;
default:
/* 只有SYS_PLL和USB1_PLL支持PFD输出 */
return 0;
}
/* PFD输出频率为fPLL x 18 / N(N是PFD的分频参数) */
return (freq * 18) / PFD_FRAC_VALUE(div, pfd);
}
6.3.2.2 获取PLL1_SW_CLK的时钟频率
其次,这些时钟信号要经过时钟切换电路(switcher)的筛选,在文件switcher.c中增加接口获取筛选后的时钟信号频率:
代码语言:javascript复制/**********************************************************************
* 函数名称: get_pll1_sw_clk
* 功能描述: 获取PLL1_SW_CLK的时钟频率
* 输入参数: 无
* 输出参数: 无
* 返 回 值: PLL1_SW_CLK的时钟频率
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
u32 get_pll1_sw_clk(void)
{
u32 reg = read32(&ccm->ccsr);
if (reg & (1u<<2)) { /* PLL1_SW_CLK_SEL:0表示pll1_main_clk,1表示step_clk */
if (reg & (1u<<8)) { /* STEP_SEL:1表示secondary_clk, 0表示OSC */
if (reg & (1u<<3)) /* SECONDARY_CLK_SEL:1表示PLL2,0表示PLL2 PFD2 */
return get_pll(SYS_PLL);
else
return get_pll_pfd(SYS_PLL, PFD2);
} else
return CKIH; /* OSC输出 */
} else
return get_pll(ARM_PLL);
}
/**********************************************************************
* 函数名称: get_pll3_sw_clk
* 功能描述: 获取PLL3_SW_CLK的时钟频率
* 输入参数: 无
* 输出参数: 无
* 返 回 值: PLL3_SW_CLK的时钟频率
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
u32 get_pll3_sw_clk(void)
{
u32 reg = read32(&ccm->ccsr);
if (reg & 1) /* PLL3_SW_CLK_SEL: 1表示pll3,0表示pll3_bypass(即OSC输出) */
return get_pll(USB1_PLL);
else
return CKIH; /* OSC输出 */
}
/**********************************************************************
* 函数名称: get_pll4_main_clk
* 功能描述: 获取PLL4_MAIN_CLK的时钟频率
* 输入参数: 无
* 输出参数: 无
* 返 回 值: PLL4_MAIN_CLK的时钟频率
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
extern struct anadig_regs *anadig;
u32 get_pll4_main_clk(void)
{
u32 reg, audio_div;
reg = read32(&anadig->pmu_misc2);
/* AUDIO_DIV_MSB(23): AUDIO_DIV_LSB(15)
* 00:除以1
* 01:除以2
* 10:除以1
* 11:除以4
*/
audio_div = reg & (1u<<15) ? (reg & (1u<<23) ? 4 : 2) : 1;
return get_pll(AUDIO_PLL) / audio_div;
}
/**********************************************************************
* 函数名称: get_pll5_main_clk
* 功能描述: 获取PLL5_MAIN_CLK的时钟频率
* 输入参数: 无
* 输出参数: 无
* 返 回 值: PLL5_MAIN_CLK的时钟频率
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
u32 get_pll5_main_clk(void)
{
u32 reg, video_div;
reg = read32(&anadig->pmu_misc2);
/* AUDIO_DIV_MSB(31): AUDIO_DIV_LSB(30)
* 00:除以1
* 01:除以2
* 10:除以1
* 11:除以4
*/
video_div = reg & (1u<<30) ? (reg & (1u<<31) ? 4 : 2) : 1;
return get_pll(VIDEO_PLL) / video_div;
}
6.3.2.3 获取PLL1_SW_CLK的时钟频率
最后,根时钟生成电路(root generator)对上述信号进一步选择和分频,得到根时钟信号,在文件clkroot.c中添加以下代码:
代码语言:javascript复制/**********************************************************************
* 函数名称: get_arm_clk_root
* 功能描述: 获取ARM_CLK_ROOT的频率
* 输入参数: 无
* 输出参数: 无
* 返 回 值: ARM_CLK_ROOT的频率
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
u32 get_arm_clk_root(void)
{
u32 reg, freq;
reg = read32(&ccm->cacrr);
reg = (reg & 0x7) 1; /* 获取ARM_PODF的分频范围 */
freq = get_pll(ARM_PLL);
return freq / reg;
}
/**********************************************************************
* 函数名称: get_periph_clk
* 功能描述: 获取PERIPH_CLK的频率
* 输入参数: 无
* 输出参数: 无
* 返 回 值: PERIPH_CLK的频率
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
static u32 get_periph_clk(void)
{
u32 reg, per_clk2_podf = 0, freq = 0;
reg = read32(&ccm->cbcdr);
/* PERIPH_CLK_SEL选择periph_clk的时钟源:1表示periph_clk2, 0表示pre_periph_clk */
if (reg & (1u << 25)) { /* 选择periph_clk2 */
per_clk2_podf = (reg >> 27) & 0x7; /* 获取PERIPH_CLK2_PODF */
reg = read32(&ccm->cbcmr);
reg = (reg >> 12) & 0x3; /* PERIPH_CLK2_SEL */
/* PERIPH_CLK2_SEL: 0表示pll3_sw_clk,1表示osc_clk,2表示pll2_bypass_clk(即osc_clk) */
switch (reg) {
case 0:
freq = get_pll(USB1_PLL);
break;
case 1:
case 2:
freq = CKIH;
break;
default:
break;
}
freq /= (per_clk2_podf 1);
} else { /* 选择pre_periph_clk */
reg = read32(&ccm->cbcmr);
reg = (reg >> 18) & 0x3; /* PRE_PERIPH_CLK_SEL */
/* PRE_PERIPH_CLK_SEL:0表示PLL2输出,1表示PLL2 PFD2输出,2表示PLL2 PFD0输出,3表示PLL2 PFD2输出频率的一半 */
switch (reg) {
case 0:
freq = get_pll(SYS_PLL);
break;
case 1:
freq = get_pll_pfd(SYS_PLL, PFD2);
break;
case 2:
freq = get_pll_pfd(SYS_PLL, PFD0);
break;
case 3: /* static / 2 divider */
freq = get_pll_pfd(SYS_PLL, PFD2) / 2;
break;
default:
break;
}
}
return freq;
}
/**********************************************************************
* 函数名称: get_ahb_clk_root
* 功能描述: 获取AHB_CLK_ROOT的频率
* 输入参数: 无
* 输出参数: 无
* 返 回 值: AHB_CLK_ROOT的频率
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
u32 get_ahb_clk_root(void)
{
u32 reg, ahb_podf;
reg = read32(&ccm->cbcdr);
ahb_podf = (reg >> 10) & 0x7; /* 获取AHB_PODF的分频设置 */
return get_periph_clk() / (ahb_podf 1);
}
/**********************************************************************
* 函数名称: get_ipg_clk_root
* 功能描述: 获取IPG_CLK_ROOT的频率
* 输入参数: 无
* 输出参数: 无
* 返 回 值: IPG_CLK_ROOT的频率
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
u32 get_ipg_clk_root(void)
{
u32 reg, ipg_podf;
reg = read32(&ccm->cbcdr);
ipg_podf = (reg >> 8) & 0x3; /* 获取IPG_PODF的分频设置 */
return get_ahb_clk_root() / (ipg_podf 1);
}
/**********************************************************************
* 函数名称: get_axi_clk_root
* 功能描述: 获取AXI_CLK_ROOT的频率
* 输入参数: 无
* 输出参数: 无
* 返 回 值: AXI_CLK_ROOT的频率
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
u32 get_axi_clk_root(void)
{
u32 root_freq, axi_podf;
u32 reg = read32(&ccm->cbcdr);
axi_podf = (reg >> 16) & 0x7; /* 获取AXI_PODF的分频设置 */
if (reg & (1u << 6)) { /* AXI_SEL: 1表示axi_alt_clk,0表示periph_clk */
if (reg & (1u << 7)) /* AXI_ALT_SEL:1表示PLL3 PFD1的输出,0表示PLL2 PFD2的输出 */
root_freq = get_pll_pfd(USB1_PLL, PFD1);
else
root_freq = get_pll_pfd(SYS_PLL, PFD2);
} else
root_freq = get_periph_clk(); /* periph_clk */
return root_freq / (axi_podf 1);
}
/**********************************************************************
* 函数名称: get_fabric_mmdc_clk_root
* 功能描述: 获取FABRIC_MMDC_CLK_ROOT的频率
* 输入参数: 无
* 输出参数: 无
* 返 回 值: FABRIC_MMDC_CLK_ROOT的频率
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2020/03/08 V1.0 今朝 创建
***********************************************************************/
u32 get_fabric_mmdc_clk_root(void)
{
u32 cbcmr = read32(&ccm->cbcmr);
u32 cbcdr = read32(&ccm->cbcdr);
u32 freq, podf, per2_clk2_podf;
podf = (cbcdr >> 3) & 0x7; /* 获取FABRIC_MMDC_PODF的分频设置 */
if (cbcdr & (1u << 26)) { /* PERIPH2_CLK_SEL: 1选择periph2_clk2,0选择pre_periph2_clk */
per2_clk2_podf = cbcdr & 0x7; /* 获取PERIPH2_CLK2_PODF的分频设置 */
if (cbcmr & (1u << 20)) /* PERIPH2_CLK2_SEL:1选择osc_clk,0选择pll3_sw_clk */
freq = CKIH;
else
freq = get_pll(USB1_PLL);
freq /= (per2_clk2_podf 1);
} else { /* pre_periph2_clk */
extern u32 get_pll4_main_clk(void);
/* PRE_PERIPH2_CLK_SEL:0表示PLL2输出,1表示PLL2 PFD2输出,2表示PLL2 PFD0输出,3表示_main_clk */
switch ((cbcmr >> 21) & 0x3) {
case 0:
freq = get_pll(SYS_PLL);
break;
case 1:
freq = get_pll_pfd(SYS_PLL, PFD2);
break;
case 2:
freq = get_pll_pfd(SYS_PLL, PFD0);
break;
case 3:
freq = get_pll4_main_clk();
break;
}
}
return freq / (podf 1);
}
6.3.2.4 函数打印时钟值
最终,所有的时钟通过函数show_clocks打印,该函数定义在main.c中:
代码语言:javascript复制 27 /**********************************************************************
28 * 函数名称: show_clocks
29 * 功能描述: 打印PLL输出时钟频率和总线根时钟频率
30 * 输入参数: 无
31 * 输出参数: 无
32 * 返 回 值: 无
33 * 修改日期 版本号 修改人 修改内容
34 * -----------------------------------------------
35 * 2020/03/08 V1.0 今朝 创建
36 ***********************************************************************/
37 void show_clocks(void)
38 {
39 u32 freq;
40
41 printf("Show IMX6ULL Clocks: rn");
42 freq = get_pll(ARM_PLL);
43 printf("ARM_PLL � MHzrn", freq / 1000000);
44
45 freq = get_pll(SYS_PLL);
46 printf("SYS_PLL � MHzrn", freq / 1000000);
47 freq = get_pll_pfd(SYS_PLL, PFD0);
48 printf("|-SYS_PLL_PFD0 � MHzrn", freq / 1000000);
49 freq = get_pll_pfd(SYS_PLL, PFD1);
50 printf("|-SYS_PLL_PFD1 � MHzrn", freq / 1000000);
51 freq = get_pll_pfd(SYS_PLL, PFD2);
52 printf("|-SYS_PLL_PFD2 � MHzrn", freq / 1000000);
53 freq = get_pll_pfd(SYS_PLL, PFD3);
54 printf("|-SYS_PLL_PFD3 � MHzrn", freq / 1000000);
55
56 freq = get_pll(USB1_PLL);
57 printf("USB1_PLL � MHzrn", freq / 1000000);
58 freq = get_pll_pfd(USB1_PLL, PFD0);
59 printf("|-USB1_PLL_PFD0 � MHzrn", freq / 1000000);
60 freq = get_pll_pfd(USB1_PLL, PFD1);
61 printf("|-USB1_PLL_PFD1 � MHzrn", freq / 1000000);
62 freq = get_pll_pfd(USB1_PLL, PFD2);
63 printf("|-USB1_PLL_PFD2 � MHzrn", freq / 1000000);
64 freq = get_pll_pfd(USB1_PLL, PFD3);
65 printf("|-USB1_PLL_PFD3 � MHzrn", freq / 1000000);
66
67 freq = get_pll(USB2_PLL);
68 printf("USB2_PLL � MHzrn", freq / 1000000);
69 freq = get_pll(AUDIO_PLL);
70 printf("AUDIO_PLL � MHzrn", freq / 1000000);
71 freq = get_pll(VIDEO_PLL);
72 printf("VIDEO_PLL � MHzrn", freq / 1000000);
73
74 printf("rn");
75 freq = get_arm_clk_root();
76 printf("ARM_CLK_ROOT � KHZrn", freq / 1000);
77 freq = get_ahb_clk_root();
78 printf("AHB_CLK_ROOT � KHZrn", freq / 1000);
79 freq = get_ipg_clk_root();
80 printf("IPG_CLK_ROOT � KHZrn", freq / 1000);
81 freq = get_axi_clk_root();
82 printf("AXI_CLK_ROOT � KHZrn", freq / 1000);
83 freq = get_fabric_mmdc_clk_root();
84 printf("FABRIC_MMDC_CLK_ROOT � KHZrn", freq / 1000);
85 printf("rn");
86 }
在第二次设置完时钟后,调用show_clocks函数打印。我们通过串口看到CPU最终的频率为648000KHZ,即648MHZ,与程序中设置的一致。
注明: 代码目录在裸机Git仓库 NoosProgramProject/(7_时钟体系/showclocks) 文件夹下。
6.3.2.5 参考章节《4-1.4编译程序》编译程序
6.3.2.6 参考章节《4-1.4映像文件烧写、运行》烧写、运行程序
运行成功后可以观察到串口打印所有的时钟频率信息。
6.3.3 补充说明
第二个示例中CPU频率增加到8倍,然而led的闪烁频率似乎并没有变为原来的8倍。这是因为延时函数busy_wait执行过程访问了内存,内存的速度限制了程序的性能。反汇编showclock.elf可以看到以下代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FtzvXFwN-1642060018421)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/07_clock_system_image020.png)]
函数busy_wait在空循环过程中访问了三次内存,造成了性能瓶颈。解决方法之一是使用嵌入式汇编重新定义该函数,消除内存访问操作,代码如下:
代码语言:javascript复制static void busy_wait(void)
{
__asm__ __volatile__ (
"ldr r0, =3000000n"
"1:n"
"subs r0, r0, #1n"
"bne 1bn"
:::"r0");
}
修改后可发现,在CPU频率设置为648MHZ之后,led灯闪烁频率明显比修改前快很多。另一个避免直接访问内存的方法是开启D-Cache,有兴趣的同学可以尝试一下。
另外,为了更加直观的查看时钟频率,用户还可以将时钟信号通过CCM_CLKO1和CCM_CLKO2输出,使用示波器直接观察。相关的代码在文件clkout.c中,默认这些代码并不执行,有条件的同学可以开启这段代码进行实验。