[ARM裸机开发] 主频和时钟设置

i.MUX6U 的内部 boot rom 会将其主频设置为 396MHz,但是使用的时候都是希望它能发挥出最大的性能,所以需要将主频设置到推荐的 528MHz。同时,外设的使用也需要时钟,所以也需要将用到的外设时钟设置到推荐的值。本章的内容就是记录如何设置系统时钟以及外设时钟的。

既然谈及了时钟设置,就会有类似 “时钟有什么用” 这样的问题。因为自己没有学习过数电和模电,所以只能找知乎上的问答(为什么 CPU 需要时钟才能工作?)大致了解一下。自己理解的可能有误:时钟的作用大抵上应该是用于同步的,时钟脉冲用于通知“处理事件”,直到下一次脉冲来之前需要保持稳定。

1. 7 路 PLL 时钟源

如图 1 (参考手册第十章)所示,i.MUX6U 的时钟生成部分包括 7 组 PLL (Phase Locked Loop , 锁相环)。其中两路 PLL 配备 4 个 PFD (Phase Fractional Divider , 相位分压器),以产生额外的频率。这 7 组 PLL 如下:

1. PLL1(又称 ARM_PLL):此路 PLL 是供 ARM 内核使用的,可以通过编程的方式最高倍频到 1.3GHz。

2. PLL2(又称 System_PLL528_PLL):此 PPL 的输出频率是固定的,不可编程改变,为 XTALOSC 晶振(24MHz)的 22 倍频,即 528MHz。此路 PLL 驱动 4 路 PFD(PLL2_PFD0 ~ PLL2_PFD3)。通常此路 PLL 或分出的 4 路 PFD 是内部系统总线、内部处理逻辑单元、DDR 接口、NAND/NOR 接口等模块的时钟。

3. PLL3(又称 USB1_PLL):此路 PLL 主要用于 USB PHY,也驱动 4 路 PFD(PLL3_PFD0 ~ PLL3_PFD3)。此路 PPL 的输出频率是固定的,为 XTALOSC 晶振(24MHz)的 20 倍频,即 480MHz。通常此路 PLL 和分出的 4 路 PFD 的输出被用作许多需要恒定频率的时钟源,比如 UART 和其他串行接口、音频接口等等。

4. PLL4(又称 Audio PLL):此路 PLL 用于音频相关外设,能产生低抖动和高精度的音频时钟,频率输出范围为 650MHz ~ 1300MHz。此路 PLL 在最终输出的时候可以进行分频,可选 1/2/4 分频。

5. PLL5(又称 Video PLL):此路 PLL 用于视频相关外设,能产生低抖动和高精度的视频时钟,频率输出范围为 650MHz ~ 1300MHz。此路 PLL 在最终输出的时候可以进行分频,可选 1/2/4/8/16 分频。

6. PLL6(又称 ENET_PLL):此路 PPL 的输出频率是固定的,为 XTALOSC 晶振(24MHz)的 20+(5/6) 倍频,即 500MHz。此路 PLL 用于生成网络所需的时钟,可以在其基础上生成 25/50/100/125MHz 的网络时钟。

3. PLL7(又称 USB2_PLL):此路 PLL 主要用于 USB2 PHY。此路 PPL 的输出频率是固定的,为 XTALOSC 晶振(24MHz)的 20 倍频,即 480MHz。

PLL 百度百科:

一般的晶振由于工艺与成本原因,做不到很高的频率,而在需要高频应用时,由相应的器件VCO,实现转成高频,但并不稳定,故利用锁相环路就可以实现稳定且高频的时钟信号。

图1 主时钟生成

2. 时钟树简介

在第 1 章中已经介绍了 i.MUX6U 所有外设需要用到的 7 路 PLL 以及其驱动的 PFD,这章则说明如何选择使用这些 PLL 或者 PFD。如图 2 所示,在参考手册的第 18.3 节详细的给出了 i.MUX6U 的时钟树。

时钟树由三部分组成:CLOCK SWITCHERCLOCK ROOT GENERATORSYSTEM CLOCKS。最左边的 CLOCK SWITCHER 部分包括了第 1 章讲的 7 路 PLL 以及 8 路 PFD;最右边的 SYSTEM CLOCKS 部分包括了芯片外设。中间的 CLOCK ROOT GENERATOR 是最复杂的,从名字上可以看出它的作用是 “根时钟生成”:这一部分将左右两边牵线搭桥,选择左边的 PLL 或 PFD,以生成相应的根时钟供右边的外设使用。

CLOCK ROOT GENERATOR 这部分有两处需要注意:一处是橘色梯形状的时钟源选择器,它会选择使用某一路的时钟源,具体选择哪路时钟源可以通过上方黄底黑字中的寄存器进行指定;第二处是青蓝色方形的分频器,具体的分频值可以通过上方红字中的寄存器进行设置。

图2 时钟树部分内容

3. 时钟设置

第 2 章中讲了可以通过编程方式控制寄存器的值达到时钟源选择和分频的功能,这章则具体展开说明如何进行时钟设置。这章首先会介绍如何设置内核时钟,熟悉了内核时钟的设置方法之后,其他时钟的设置也就能举一反三了。在第 1 章中可以了解到大多数的 PLL 是固定频率的,所以本章接着会说明如何设置 8 路可编程控制频率的 PFD。最后本章会初始化两路大部分外设会使用到的根时钟源,至于其余外设的根时钟源在后续使用到这个外设的时候再进行讲解。

3.1 内核时钟设置

图 3 是从图 2 中摘取的内核时钟树部分,如图所示,中间生成部分没有时钟源选择器,只有分频器。如果需要设置内核主频为 528MHz,可以将 PLL1 设置为 1056MHz,然后通过 CACRR[ARM_PODF] 寄存器位选择 2 分频,即 1056 / 2 = 528MHz。

PPL1 最高倍频到 1.3GHz。如果需要设置内核主频为 696MHz,就不能使用 2 分频了。

图3 内核时钟树

CCM_ANALOG_PLL_ARM 寄存器可以设置 PLL 的输出频率,其内容如图 4 所示。这边主要关注以下两个域:

ENABLE : 时钟输出使能位。设置为 1 使能 PLL1 输出;设置为 0 关闭 PLL1 输出。

DIV_SELECT : 用于设置 PLL1 的输出频率,Fout = Fin * div_select / 2.0。所以 PLL1 要输出 1056MHz 的话,div_select 需要设置为 88。

图4 CCM_ANALOG_PLL_ARM 寄存器

CCM_CACRR 寄存器的 ARM_PODF 域可以设置 PLL1 的分频,值 001 代表 2 分频。

图5 CCM_CACRR 寄存器

目前 PPL1 的输出频率和分频都可以设置了,但是在设置之前还有一个问题需要考虑!因为 PLL1 原本用于内核时钟,内核本身就按 PPL1 设置的频率运行着,现在就发生了使用着它又要改变它的情况,难免会产生不可预期的结果。解决的方法就是先把内核时钟源换成其他时钟源,等 PLL1 设置好了之后再换回来。

图 6 摘取了 PPL1 的时钟开关部分,可以看到原本 PPL1 最终输出的时钟对应为 pll1_sw_clk,前面的时钟源选择器可供切换使用。如图 7 所示,可以通过 CCSR: pll1_sw_clk_sel 选择到 step_clk,然后通过 CCSR: step_sel 选择到 osc_clk,即切换到晶振 24MHz 的频率。

图6 PPL1 时钟开关

图7 CCM_CCSR 寄存器

至此,内核时钟的设置要点已经全部说明完毕,下面将设置步骤进行总结:

1. 通过 pll1_sw_clkCCSR: pll1_sw_clk_sel 将内核时钟源从 PLL1 临时切换到 osc_clk。

2. 通过 CCM_ANALOG_PLL_ARMCCM_CACRR 寄存器设置 PLL1 输出频率以及分频值。

3. 通过 pll1_sw_clk 将内核时钟源切回 PLL1。


3.1.1 程序编写

这部分代码,可以在之前 BSP 工程 的 bsp_clk 基础上进行增添。如下面代码所示,添加了 clk_init 函数,函数中依次按照前面说明的步骤进行操作。代码中实现了内核时钟 528MHz 和 696MHz 的初始化,可以分别编译烧写,通过 LED 闪灭的频率了解到内核时钟频率的变化。

  1. void clk_init(void)
  2. {
  3.     uint32_t reg_val = CCM->CCSR;
  4.  
  5.     /* osc_clk */
  6.     if ((CCM->CCSR & 0x04) == 0)
  7.     {
  8.         reg_val |= (1 << 2);
  9.         reg_val &= ~(1 << 8);
  10.         CCM->CCSR = reg_val;
  11.     }
  12.    
  13.     reg_val = CCM_ANALOG->PLL_ARM;
  14.     reg_val |= (1 << 13); /* enable = 1 */
  15. #ifdef __696MHz__
  16.     reg_val &= (~0x7f); reg_val |= 88; /* div_select  = 58 */
  17.     CCM->CACRR = 0; /* divide by 1 */
  18. #else
  19.     reg_val &= (~0x7f); reg_val |= 88; /* div_select  = 88 */
  20.     CCM->CACRR = 1; /* divide by 2 */
  21. #endif
  22.     CCM_ANALOG->PLL_ARM = reg_val;
  23.  
  24.     /* resume pll1_main_clk */
  25.     CCM->CCSR &= ~(1 << 2);
  26. }

3.2 PFD 时钟设置

在上一小节已经将 PLL1 设置好了,PLL2、PLL3 和 PLL7 固定为 528MHz、480MHz 和 480MHz,PLL4 ~ PLL6 都是针对特殊外设的,可以用到的时候再进行设置。因此,这一节需要继续设置 PLL2 和 PLL3 各自驱动的 4 路 PFD。各路 PFD 的推荐值已经在图 2 中写出。

CCM_ANALOG_PFD_528 寄存器可以设置 PLL2 的 4 路 PFD 的频率;CCM_ANALOG_PFD_480 寄存器可以设置 PLL3 的 4 路 PFD 的频率。如图 8 所示,它们的结构都是相似的,包括 CLKGATE、STABLE 和 FRAC:

CLKGATE : 输出使能位。置 1 的时候关闭输出;置 0 的时候使能输出。

STABLE : 此位为只读位,可以通过读取此位判断当前 PFD 是否稳定。

FRAC : 分频数。PLL2 4 路 PFD 都是依照 528*18/PFDn_FRAC 这个公式生成相应频率的,PLL3 4 路 PFD 都是依照 480*18/PFDn_FRAC 这个公式生成相应频率的。比如,需要设置 PLL2_PFD0 的频率为 352MHz,则需要将 CCM_ANALOG_PFD_528:PFD0_FRAC 的值设置为 27。

图8 CCM_ANALOG_PFD_528/480 寄存器

3.2.1 程序编写

紧接着 3.1.1 的内容,如下代码从第 27 行开始就是 PFD 时钟设置的相关内容,其中填写的相关数值都是按照频率生成公式以及需要输出的频率计算得到的,并没有什么难度。

  1. void clk_init(void)
  2. {
  3.     uint32_t reg_val = CCM->CCSR;
  4.  
  5.     /* osc_clk */
  6.     if ((CCM->CCSR & 0x04) == 0)
  7.     {
  8.         reg_val |= (1 << 2);
  9.         reg_val &= ~(1 << 8);
  10.         CCM->CCSR = reg_val;
  11.     }
  12.    
  13.     reg_val = CCM_ANALOG->PLL_ARM;
  14.     reg_val |= (1 << 13); /* enable = 1 */
  15. #ifdef __696MHz__
  16.     reg_val &= (~0x7f); reg_val |= 88; /* div_select  = 58 */
  17.     CCM->CACRR = 0; /* divide by 1 */
  18. #else
  19.     reg_val &= (~0x7f); reg_val |= 88; /* div_select  = 88 */
  20.     CCM->CACRR = 1; /* divide by 2 */
  21. #endif
  22.     CCM_ANALOG->PLL_ARM = reg_val;
  23.  
  24.     /* resume pll1_main_clk */
  25.     CCM->CCSR &= ~(1 << 2);
  26.  
  27.     /* PLL2_PFD */
  28.     reg_val = CCM_ANALOG->PFD_528;
  29.     reg_val &= ~0x3f3f3f3f;
  30.     reg_val |= 27; /* PLL2_PFD0 = 352MHz */
  31.     reg_val |= (16 << 8); /* PLL2_PFD1 = 594MHz */
  32.     reg_val |= (24 << 16); /* PLL2_PFD2 = 400MHz */
  33.     reg_val |= (48 << 24); /* PLL2_PFD3 = 200MHz */
  34.     CCM_ANALOG->PFD_528 = reg_val;
  35.  
  36.     /* PLL3_PFD */
  37.     reg_val = CCM_ANALOG->PFD_480;
  38.     reg_val &= ~0x3f3f3f3f;
  39.     reg_val |= 12; /* PLL2_PFD0 = 720MHz */
  40.     reg_val |= (16 << 8); /* PLL2_PFD1 = 540MHz */
  41.     reg_val |= (17 << 16); /* PLL2_PFD2 = 508.2MHz */
  42.     reg_val |= (19 << 24); /* PLL2_PFD3 = 454.7MHz */
  43.     CCM_ANALOG->PFD_480 = reg_val;
  44. }

3.3 AHB/IPG/PERCLK 根时钟设置

由图 2 所示,PERCLK_CLK_ROOTIPG_CLK_ROOT 作为大多数常用外设的时钟源,又因为这两个时钟源都会用到 AHB_CLK_ROOT,所以都需要将它们进行初始化。由图 9 可以得到各个根时钟可设置的频率范围,这边都选择其最大值:AHB_CLK_ROOT 设置为 132 MHz;PERCLK_CLK_ROOTIPG_CLK_ROOT 设置为 66MHz。

图9 外设根时钟频率设置范围

由图 2 所示,AHB_CLK_ROOT 从右到左首先会遇到 CBCDR[AHB_PODF] 分频器(第 12 - 10 位);接着遇到 CBCDR[PERIPH_CLK_SEL] (第 25 位),这时候选择下方一路(值 0);最后遇到 CBCMR[PRE_PERIPH_CLK_SEL] (第 19 - 18 位),这时候选择 PLL2_PFD2 (值 1)。编程的时候需要反路径设置。由于 PLL2_PFD2 为 396MH,所以如果想要将 AHB_CLK_ROOT 设置为 132 MHz,则需要 3 分频。

操作 CBCDR 的时候需要注意:

Any change of this divider might involve handshake with EMI. See CDHIPR register for the handshake busy bits.

由图 2 所示,IPG_CLK_ROOT 从右到左首先会遇到 CBCDR[IPG_PODF] 分频器(第 9 - 8 位);后面直接连着 AHB_CLK_ROOT ,为 132 MHz,所以如果想要将 IPG_CLK_ROOT 设置为 66 MHz,则需要 2 分频。

由图 2 所示,PERCLK_CLK_ROOT 从右到左首先会遇到 CSCMR1[PERCLK_PODF] 分频器(第 5 - 0 位);接着会遇到 CSCMR1[PERCLK_PODF] (第 6 位),这时候选择下方 ipg 一路(值 0),则 1 分频就能和 IPG_CLK_ROOT 一样为 66MHz。

3.3.1 程序编写

紧接着 3.2.1 的内容,如下代码从第 58 行开始就是 AHB/IPG/PERCLK 根时钟设置的相关内容,内容同样是时钟源选择器和分频器的指定。并且至此,下述的代码就是本章时钟初始化的全部内容。值得注意的是,配套开发板教程里说明 AHB_CLK_ROOT 的分频器会设置错误,但是自己这边设置运行后 LED 灯正常亮灭。这点还需要后续关注,可能是开发板教程中的代码直接在时钟设置寄存器上操作和保存中间结果导致的。

  1. #include "bsp_clk.h"
  2.  
  3. void clk_enable(void)
  4. {
  5.     CCM->CCGR0 = 0XFFFFFFFF;
  6.     CCM->CCGR1 = 0XFFFFFFFF;
  7.     CCM->CCGR2 = 0XFFFFFFFF;
  8.     CCM->CCGR3 = 0XFFFFFFFF;
  9.     CCM->CCGR4 = 0XFFFFFFFF;
  10.     CCM->CCGR5 = 0XFFFFFFFF;
  11.     CCM->CCGR6 = 0XFFFFFFFF;
  12. }
  13.  
  14. void clk_init(void)
  15. {
  16.     uint32_t reg_val = CCM->CCSR;
  17.  
  18.     /* osc_clk */
  19.     if ((CCM->CCSR & 0x04) == 0)
  20.     {
  21.         reg_val |= (1 << 2);
  22.         reg_val &= ~(1 << 8);
  23.         CCM->CCSR = reg_val;
  24.     }
  25.    
  26.     reg_val = CCM_ANALOG->PLL_ARM;
  27.     reg_val |= (1 << 13); /* enable = 1 */
  28. #ifdef __696MHz__
  29.     reg_val &= (~0x7f); reg_val |= 88; /* div_select  = 58 */
  30.     CCM->CACRR = 0; /* divide by 1 */
  31. #else
  32.     reg_val &= (~0x7f); reg_val |= 88; /* div_select  = 88 */
  33.     CCM->CACRR = 1; /* divide by 2 */
  34. #endif
  35.     CCM_ANALOG->PLL_ARM = reg_val;
  36.  
  37.     /* resume pll1_main_clk */
  38.     CCM->CCSR &= ~(1 << 2);
  39.  
  40.     /* PLL2_PFD */
  41.     reg_val = CCM_ANALOG->PFD_528;
  42.     reg_val &= ~0x3f3f3f3f;
  43.     reg_val |= 27; /* PLL2_PFD0 = 352MHz */
  44.     reg_val |= (16 << 8); /* PLL2_PFD1 = 594MHz */
  45.     reg_val |= (24 << 16); /* PLL2_PFD2 = 400MHz */
  46.     reg_val |= (48 << 24); /* PLL2_PFD3 = 200MHz */
  47.     CCM_ANALOG->PFD_528 = reg_val;
  48.  
  49.     /* PLL3_PFD */
  50.     reg_val = CCM_ANALOG->PFD_480;
  51.     reg_val &= ~0x3f3f3f3f;
  52.     reg_val |= 12; /* PLL2_PFD0 = 720MHz */
  53.     reg_val |= (16 << 8); /* PLL2_PFD1 = 540MHz */
  54.     reg_val |= (17 << 16); /* PLL2_PFD2 = 508.2MHz */
  55.     reg_val |= (19 << 24); /* PLL2_PFD3 = 454.7MHz */
  56.     CCM_ANALOG->PFD_480 = reg_val;
  57.  
  58.     /* AHB_CLK_ROOT */
  59.     reg_val = CCM->CBCMR;
  60.     reg_val &= ~(3 << 18);
  61.     reg_val |= (1 << 18);
  62.     CCM->CBCMR = reg_val;
  63.  
  64.     reg_val = CCM->CBCDR;
  65.     reg_val &= ~(1 << 25);
  66.  
  67.     reg_val &= ~(7 << 10);
  68.     reg_val |= (2 << 10);
  69.  
  70.     CCM->CBCDR = reg_val;
  71.     while (CCM->CDHIPR & (1 << 5));
  72.  
  73.     /* IPG_CLK_ROOT */
  74.     reg_val = CCM->CBCDR;
  75.     reg_val &= ~(3 << 8);
  76.     reg_val |= (1 << 8);
  77.     CCM->CBCDR = reg_val;
  78.  
  79.     /* PERCLK_CLK_ROOT */
  80.     reg_val = CCM->CSCMR1;
  81.     reg_val &= ~(0x7f);
  82.     CCM->CSCMR1 = reg_val;
  83. }

4. 总结

全文可以看到,除了小些的专业概念介绍之外,其他内容都是参考手册中的图片以及寄存器介绍。所以最终又变成了寄存器上面的操作,会有一种让人什么都没学的错觉。可能寄存器赋值就是裸机编程的“通讯方式”,这点还要在后续的学习过程中,继续感受验证。