IMX6ULL学习笔记(19)——时钟系统

2023-05-16

一、时钟系统简介

I.MX6U 的系统主频为 528MHz,有些型号可以跑到 696MHz,但是默认情况下内部 boot rom 会将 I.MX6U 的主频设置为 396MHz。我们在使用 I.MX6U 的时候肯定是要发挥它的最大性能,那么主频肯定要设置到 528MHz(其它型号可以设置更高,比如 696MHz),其它的外设时钟也要设置到 NXP 推荐的值。I.MX6U 的系统时钟在 《I.MX6ULL/I.MX6UL 参考手册》的第 10 章“Chapter 10 Clock and Power Management”和第 18 章“Chapter 18 Clock Controller Module (CCM)” 这两章有详细的讲解。

二、时钟源

2.1 系统时钟来源

i.MX6U 外部连接了两个晶振,分别用于提供 32.768KHz24MHz 时钟。

  • 32.768KHz 晶振 是 I.MX6U 的 RTC 时钟源。
  • 24MHz 晶振 是 I.MX6U 内核和其它外设的时钟源。

2.2 七路PLL时钟源

I.MX6U 的外设有很多,不同的外设时钟源不同,NXP 将这些外设的时钟源进行了分组,一共有 7 组,这 7 组时钟源都是从 24MHz 晶振 PLL 而来的,因此也叫做 7 组 PLL。结构如下:

  • ①:ARM_PLL(PLL1),此路 PLL 是供 ARM 内核使用的,ARM 内核时钟就是由此 PLL 生成的,此 PLL 通过编程的方式最高可倍频到 1.3GHz。
  • ②:528_PLL(PLL2),此路 PLL 也叫做 System_PLL,此路 PLL 是固定的 22 倍频,不可编程修改。因此,此路 PLL 时钟=24MHz*22=528MHz,这也是为什么此 PLL 叫做 528_PLL 的原因。此 PLL 分出了 4 路 PFD(Phase Fractional Dividers 分数分频 ),分别为:PLL2_PFD0~PLL2_PFD3,这 4 路 PFD 和 528_PLL 共同作为其它很多外设的根时钟源。通常 528_PLL 和这 4 路 PFD 是 I.MX6U 内部系统总线的时钟源,比如内处理逻辑单元、DDR 接口、NAND/NOR 接口等等。
  • ③:USB1_PLL(PLL3),此路 PLL 主要用于 USBPHY,此 PLL 也有 4 路 PFD,为 PLL3_PFD0~PLL3_PFD3,USB1_PLL 是固定的 20 倍频,因此 USB1_PLL=24MHz*20=480MHz。USB1_PLL 虽然主要用于 USB1PHY,但是其和 4 路 PFD 同样也可以作为其他外设的根时钟源
  • ④:USB2_PLL(PLL7),此路 PLL 是给 USB2PHY 使用的。同样的,此路 PLL 固定为 20 倍频,因此也是 480MHz。
  • ⑤:ENET_PLL(PLL6),此路 PLL 固定为 20+5/6 倍频,因此 ENET_PLL=24MHz*(20+5/6)=500MHz。此路 PLL 用于生成网络所需的时钟,可以在此 PLL 的基础上生成 25/50/100/125MHz 的网络时钟。
  • ⑥:VIDEO_PLL(PLL5),此路 PLL 用于显示相关的外设,比如 LCD,此路 PLL 的倍频可以调整,PLL 的输出范围在 650MHz~1300MHz。此路 PLL 在最终输出的时候还可以进行分频,可选 1/2/4/8/16 分频。
  • ⑦:AUDIO_PLL(PLL4),此路 PLL 用于音频相关的外设,此路 PLL 的倍频可以调整,PLL 的输出范围同样也是 650MHz~1300MHz,此路 PLL 在最终输出的时候也可以进行分频,可选 1/2/4 分频。

三、时钟树

i.MX6U 芯片时钟的结构以时钟树的方式进行描述。当我们设置外设时钟时大多会参考时钟树进行设置。


一共有三部分:

  • CLOCK_SWITCHER(时钟源选择器)
    7 路 PLL 和 8 路 PFD。输出了多个频率不同的PLL时钟和PFD时钟。
  • CLOCK ROOT GENERATOR(根时钟生成模块)
    给左边的 CLOCK_SWITCHER 和右边的 SYSTEM CLOCKS 进行桥接。外设时钟源是有多路可以选择的,CLOCK ROOT GENERATOR 就负责从 7 路 PLL 和 8 路 PFD 中选择合适的时钟源给外设使用。具体操作肯定是设置相应的寄存器。
  • SYSTEM CLOCKS(系统时钟模块)
    芯片外设。

3.1 根时钟生成模块CLOCK ROOT GENERATOR

根时钟生成模块主要完成两个工作:

  • 第一选择时钟。
  • 第二设置时钟分频。

以 ESAI 这个外设为例,ESAI 的时钟图如下所示:

分为了三部分:

  • ①:时钟源选择器,ESAI 有 4 个可选的时钟源:PLL4、PLL5、PLL3_PFD2 和
    pll3_sw_clk 。具体选择哪一路作为 ESAI 的时钟源是由寄存器 CCM->CSCMR2 的 ESAI_CLK_SEL 位来决定的,用户可以自由配置。
  • ②:ESAI 时钟的前级分频,分频值由寄存器 CCM_CS1CDR 的 ESAI_CLK_PRED
    来确定的,可设置 1~8 分频,假如现在 PLL4=650MHz,我们选择 PLL4 作为 ESAI 时钟,前级
    分频选择 2 分频,那么此时的时钟就是 650/2=325MHz。
  • ③:分频器,对②中输出的时钟进一步分频,分频值由寄存器 CCM_CS1CDR 的 ESAI_CLK_PODF 来决定,可设置 1~8 分频。假如我们设置为 8 分频的话,经过此分频器以后的时钟就是 325/8=40.625MHz。因此最终进入到 ESAI 外设的时钟就是 40.625MHz。

四、时钟控制模块(CCM)

i.MX6U 的时钟系统由时钟控制模块(CCM)进行控制,其主要功能如下:

  • 使用PLL锁相环电路将参考时钟倍频,得到频率更高的时钟。
    为芯片内核和外设提供可选的时钟源。i.MX6U 共有 7 个 PLL 锁相环电路,分别为:
    ARM PLL(PLL 1)、System PLL(PLL 2)、USB1 PLL(PLL 3)、Audio PLL(PLL 4)、Video PLL(PLL5)ENET PLL(PLL 6)、USB2 PLL(PLL 7)。

  • 提供PLL控制寄存器、时钟选择寄存器、时钟分频寄存器。
    灵活控制输出到外设和内核的时钟频率。

  • 控制低功耗模块。

时钟控制模块(CCM)结构如下:

  • ①CCM_CLK_IGNITION模块
    管理从外部晶振时钟到稳定的根时钟输出的整个过程。CCM完成重置之后CCM_CLK_IGNITION模块立即启动。 GPC是General Power Controller的缩写,即总电源管理模块,它不属于CCM,系统电压与时钟关系密切, 简单来说,系统电压影响系统最高的时钟频率,CCM又可以控制总电源管理模块(GPC)进入待机或低功耗状态。

  • ②CCM_ANALOG模块和CCM_CLK_SWITCHER模块
    为CCM的模拟部分,作用是将频率较低的参考时钟(例如24MHz的XTALOSC时钟)使用PLL锁相环电路倍频到更高的时钟。CCM_CLK_SWITCHER模块接收来自CCM_ANALOG模块的锁相环时钟输输出,以及锁相环的旁路时钟, 并为CCM_CLK_ROOT_GEN子模块生成切换时钟输出(pll3_sw_clk)。i.MX 6U共有7个PLL锁相环电路,可以独立配置。 其中PLL2与PLL3结合PFD能够输出多个频率可调的时钟

  • ③CCM_CLK_ROOT_GEN模块
    接收来自CCM_CLK_SWITCHER模块的PLL或PFD时钟,经过时钟的选择、分频等操作之后产生并输出根时钟。根时钟将会作内核或外设的时钟源。

  • ④CCM_HND_SK模块
    当更改某些时钟的时钟源时需要进行时钟的同步CCM_HND_SK模块用于管理时钟握手,即时钟的同步

  • ⑤CCM_LPM模块和CCM_CLK_LOGIC模块
    CCM_LPM用于管理低功耗模式,管理时钟的开启与关闭。CCM_CLK_LOGIC,根据来自CCM_LPM模块和CCM_IP的信号产生时钟启用或关闭信号

  • ⑥LPCG模块
    低功耗时钟门控模块(LPCG)根据CCM_CLK_LOGIC模块输出信号控制时钟输出。时钟越多、频率越高功耗也就越高。关闭没有使用的时钟或降低时钟频率能够有效的降低功耗。

五、内核时钟设置

ARM_CLK_ROOT 时钟是 CPU 时钟,也就是我们常说的主频。修改该时钟之前首先要将 CPU 时钟切换到另外一个可用的时钟,修改完成后再切换回来。

假设要将 CPU 时钟修改为 792MHz。


上图中,标号①与标号②处是CCSR时钟选择寄存器的两个配置位,用于设置时钟源。这里假设要将CPU时钟修改为792MHz。步骤如下:

5.1 配置CCM_CCSR寄存器,切换到24MHz晶振时钟

ARM_CLK_ROOT 时钟切换到 osc_clk(24MHz)

这里共用到了CCSR寄存器的两个控制位:

  1. CCM_CCSR[STEP_SEL] 对应图标号①处,
  • CCM_CCSR[STEP_SEL] = 0,表示选择 24MHz 的 osc_clk 时钟,osc_clk 时钟是固定的,默认我们选择这个时钟
  • CCM_CCSR[STEP_SEL] = 1,表示选择 secondary_clk 时钟,这个时钟暂时用不到,不用关心。
  1. CCM_CCSR[PLL1_SW_CLK_SEL] 对应图标号②处,
  • CCM_CCSR[PLL1_SW_CLK_SEL] = 0,表示选择 pll1_main_clk 时钟。
  • CCM_CCSR[PLL1_SW_CLK_SEL] = 1,表示选择 step_clk 时钟。

我们设置 CCSR[STEP_SEL] = 0、CCSR[PLL1_SW_CLK_SEL] = 1,这样CPU时钟源被切换到了24MHz的osc_clk时钟,下一步就可以修改PLL1的输出时钟。

5.2 配置CCM_ANALOG_PLL_ARMn寄存器,修改PLL1输出时钟

ARM PLL(PLL 1) 输出频率切换到 792MHz

从24MHz参考时钟到ARM PLL(PLL 1)时钟的过程如下:

ARM PLL(PLL 1)只有一个控制寄存器 CCM_ANALOG_PLL_ARMn

这里共用到了CCM_ANALOG_PLL_ARMn寄存器的三个控制位:

  1. CCM_ANALOG_PLL_ARMn[BYPASS_CLK_SRC]
  • CCM_ANALOG_PLL_ARMn[BYPASS_CLK_SRC] = 0,表示选择用于选择24MHz参考时钟。
  • CCM_ANALOG_PLL_ARMn[BYPASS_CLK_SRC] = 1,表示选择外部引脚输入引脚(CLK1_N /CLK1_P)输入的外部时钟。
  1. CCM_ANALOG_PLL_ARMn[DIV_SELECT]
    选择锁相环分频值(DIV_SELECT)。 取值范围为54到108。输出频率计算公式为 ARM_PLL = Fin * DIV_SELECT / 2.0。如果选择24MHz参考时钟作为时钟输入,DIV_SELECT选择66则ARM PLL的输出频率为792MHz。

  2. CCM_ANALOG_PLL_ARMn[ENABLE]
    用于配置是否使能ARM PLL输出,如果要使用ARM PLL就需要将该位设置为1。

5.3 配置CCM_CCSR寄存器,切换回PLL1时钟

重新将 pll1_sw_clk 的时钟源切换回 pll1_main_clk,切换回来以后的 pll1_sw_clk 就等于 792MHz。

CCM_CCSR[PLL1_SW_CLK_SEL] = 0,表示将CPU时钟切换到 pll1_main_clk 即PLL1输出时钟。

5.4 配置CCM_CACRR寄存器,修改时钟分频

从PLL1到 ARM_CLK_ROOT 还要经过 CCM_CACRR[ARM_PODF] 时钟分频寄存器。

CCM_CACRR[ARM_PODF] 时钟分频寄存器可以设置为 0~7,分别对应 1~8 分频。

经过上一步PLL1的输出时钟被设置为792MHz,所以这里设置 CCM_CACRR[ARM_PODF] = 0,不分频。

六、PFD时钟设置

设置好主频以后我们还需要设置好其他的 PLL 和 PFD 时钟,PLL1 上一节已经设置了,PLL2、PLL3 和 PLL7 固定为 528MHz、480MHz 和 480MHz,PLL4~PLL6 都是针对特殊外设的,用到的时候再设置。因此,接下来重点就是设置 PLL2 和 PLL3 的各自 4 路 PFD,NXP 推荐的这 8 路 PFD 频率如下:

6.1 配置CCM_ANALOG_PFD_528n寄存器,修改PLL2的4路PFD频率

寄存器 CCM_ANALOG_PFD_528n 分为四组,分别对应 PFD0~PFD3,每组 8 个 bit。

PFD0 对应的寄存器位如下:

  1. CCM_ANALOG_PFD_528n[PFD0_FRAC]
    PLL2_PFD0 的分频数,PLL2_PFD0 的计算公式为 528*18/PFD0_FRAC,可设置的范围为 12~35。如果 PLL2_PFD0 的频率要设置为 352MHz 的话,PFD0_FRAC=528*18/352=27。
  2. CCM_ANALOG_PFD_528n[PFD0_STABLE]
    此位为只读位,可以通过读取此位判断 PLL2_PFD0 是否稳定。
  3. CCM_ANALOG_PFD_528n[PFD0_CLKGATE]
  • CCM_ANALOG_PFD_528n[PFD0_CLKGATE] = 0,表示使能 PLL2_PFD0 的输出。
  • CCM_ANALOG_PFD_528n[PFD0_CLKGATE] = 1,表示关闭 PLL2_PFD0 的输出。

如果我们要设置 PLL2_PFD0 的频率为 352MHz 的话就需要设置 PFD0_FRAC 为 27, PFD0_CLKGATE 为 0
PLL2_PFD1~PLL2_PFD3 设置类似,频率计算公式都是 528*18/PFDX_FRAC(X=1~3) ,因此 PLL2_PFD1=594MHz 的话, PFD1_FRAC=16
如果 PLL2_PFD2=400MHz 的话 PFD2_FRAC 不能整除,因此取最近的整数值,即 PFD2_FRAC=24,这样 PLL2_PFD2 实际为 396MHz;
如果 PLL2_PFD3=297MHz 的话,PFD3_FRAC=32

6.2 配置CCM_ANALOG_PFD_480n寄存器,修改PLL3的4路PFD频率

寄存器 CCM_ANALOG_PFD_480n 和 CCM_ANALOG_PFD_528n 的结构是一模一样的,只是一个是 PLL2 的,一个是 PLL3 的。寄存器位的含义也是一样的,只是频率计算公式不同,比如 PLL3_PFDX=480*18/PFDX_FRAC(X=0~3)

如果 PLL3_PFD0=720MHz 的话,PFD0_FRAC=12
如果 PLL3_PFD1=540MHz 的话,PFD1_FRAC=16
如果 PLL3_PFD2=508.2MHz 的话,PFD2_FRAC=17
如果 PLL3_PFD3=454.7MHz 的话,PFD3_FRAC=19

七、AHB、IPG和PERCLK根时钟设置

7 路 PLL 和 8 路 PFD 设置完成以后最后还需要设置 AHB_CLK_ROOTIPG_CLK_ROOT 的时钟,I.MX6U 外设根时钟可设置范围如下:

AHB_CLK_ROOT 最高可以设置 132MHz,IPG_CLK_ROOTPERCLK_CLK_ROOT 最高可以设置 66MHz。

AHB_CLK_ROOTIPG_CLK_ROOT 的时钟图如下:

7.1 配置CCM_CBCMR寄存器,选择pre_periph_clk时钟源

对应图标号①处,此选择器用来选择 pre_periph_clk 的时钟源,可以选择 PLL2、PLL2_PFD2、PLL2_PFD0 和 PLL2_PFD2/2。

寄存器 CCM_CBCMR 的 PRE_PERIPH_CLK_SEL 位决定选择哪一个,默认选择 PLL2_PFD2,因此 pre_periph_clk=PLL2_PFD2=396MHz。

  1. CCM_CBCMR[LCDIF1_PODF]
    lcdif1 的时钟分频,可设置 0~7,分别对应 1~8 分频。
  2. CCM_CBCMR[PRE_PERIPH2_CLK_SEL]
    pre_periph2 时钟源选择。
  • CCM_CBCMR[PRE_PERIPH2_CLK_SEL] = 0x00,选择 PLL2。
  • CCM_CBCMR[PRE_PERIPH2_CLK_SEL] = 0x01,选择 PLL2_PFD2。
  • CCM_CBCMR[PRE_PERIPH2_CLK_SEL] = 0x10,选择 PLL2_PFD0。
  • CCM_CBCMR[PRE_PERIPH2_CLK_SEL] = 0x11,选择 PLL4。
  1. CCM_CBCMR[PERIPH2_CLK2_SEL]
    periph2_clk2 时钟源选择。
  • CCM_CBCMR[PERIPH2_CLK2_SEL] = 0,选择 pll3_sw_clk。
  • CCM_CBCMR[PERIPH2_CLK2_SEL] = 1,选择 OSC。
  1. CCM_CBCMR[PRE_PERIPH_CLK_SEL]
    pre_periph 时钟源选择。
  • CCM_CBCMR[PRE_PERIPH_CLK_SEL] = 0x00,选择 PLL2。
  • CCM_CBCMR[PRE_PERIPH_CLK_SEL] = 0x01,选择 PLL2_PFD2。
  • CCM_CBCMR[PRE_PERIPH_CLK_SEL] = 0x10,选择 PLL2_PFD0。
  • CCM_CBCMR[PRE_PERIPH_CLK_SEL] = 0x11,选择 PLL2_PFD2/2。
  1. CCM_CBCMR[PERIPH_CLK2_SEL]
    peripheral_clk2 时钟源选择。
  • CCM_CBCMR[PERIPH_CLK2_SEL] = 0x00,选择 pll3_sw_clk。
  • CCM_CBCMR[PERIPH_CLK2_SEL] = 0x01,选择 osc_clk。
  • CCM_CBCMR[PERIPH_CLK2_SEL] = 0x10,选择 pll2_bypass_clk。

7.2 配置CCM_CBCDR寄存器,选择periph_clk时钟源

对应图标号②处,此选择器用来选择 periph_clk 的时钟源。

由寄存器 CCM_CBCDR 的 PERIPH_CLK_SEL 位与 PLL_bypass_en2 组成的或来选择。当 CCM_CBCDR 的 PERIPH_CLK_SEL 位为 0 的时候 periph_clk=pr_periph_clk=396MHz。

  1. CCM_CBCDR[PERIPH_CLK2_PODF]
    periph2 时钟分频,可设置 0~7,分别对应 1~8 分频。
  2. CCM_CBCDR[PERIPH2_CLK_SEL]
    选择 peripheral2 的主时钟。
  • CCM_CBCDR[PERIPH2_CLK_SEL] = 0,选择 PLL2。
  • CCM_CBCDR[PERIPH2_CLK_SEL] = 1,选择 periph2_clk2_clk。
    修改此位会引起一次与 MMDC 的握手,所以修改完成以后要等待握手完成,握手完成信号由寄存器 CCM_CDHIPR 中指定位表示。
  1. CCM_CBCDR[PERIPH_CLK_SEL]
    选择 peripheral1 的主时钟。
  • CCM_CBCDR[PERIPH_CLK_SEL] = 0,选择 PLL2。
  • CCM_CBCDR[PERIPH_CLK_SEL] = 1,选择 periph2_clk2_clk。
    修改此位会引起一次与 MMDC 的握手,所以修改完成以后要等待握手完成,握手完成信号由寄存器 CCM_CDHIPR 中指定位表示。
  1. CCM_CBCDR[AXI_PODF]
    axi 时钟分频,可设置 0~7,分别对应 1~8 分频。
  2. CCM_CBCDR[AHB_PODF]
    ahb 时钟分频,可设置 0~7,分别对应 1~8 分频。修改此位会引起一次与 MMDC 的握手,所以修改完成以后要等待握手完成,握手完成信号由寄存器 CCM_CDHIPR 中指定位表示。
  3. CCM_CBCDR[IPG_PODF]
    ipg 时钟分频,可设置 0~3,分别对应 1~4 分频。
  4. CCM_CBCDR[AXI_ALT_CLK_SEL]
    axi 时钟源选择。
  • CCM_CBCDR[AXI_ALT_CLK_SEL] = 0,选择 periph_clk。
  • CCM_CBCDR[AXI_ALT_CLK_SEL] = 1,选择 axi_alt。
  1. CCM_CBCDR[FABRIC_MMDC_PODF]
    fabric/mmdc 时钟分频设置,可设置 0~7,分别对应 1~8 分频。
  2. CCM_CBCDR[PERIPH2_CLK2_PODF]
    periph2_clk2 的时钟分频,可设置 0~7,分别对应 1~8 分频。

7.3 配置CCM_CBCDR寄存器,设置AHB_CLK_ROOT分频值

对应图标号③处,通过 CBCDR 的 AHB_PODF 位来设置 AHB_CLK_ROOT 的分频值,可以设置 1~8 分频,如果想要 AHB_CLK_ROOT=132MHz 的话就应该设置为 3 分频:396/3=132MHz。图中虽然写的是默认 4 分频,但是 I.MX6U 的内部 boot rom 将其改为了 3 分频!

7.4 配置CCM_CBCDR寄存器,设置AHB_CLK_ROOT分频值

对应图标号④处,通过 CBCDR 的 IPG_PODF 位来设置 IPG_CLK_ROOT 的分频值,可以设置 1~4 分频,IPG_CLK_ROOT 时钟源是 AHB_CLK_ROOT,要想 IPG_CLK_ROOT=66MHz 的话就应该设置 2 分频:132/2=66MHz

7.5 配置CCM_CSCMR1寄存器,设置PERCLK_CLK_ROOT时钟频率

PERCLK_CLK_ROOT 来源有两种:OSC(24MHz)IPG_CLK_ROOT,由寄存器 CCM_CSCMR1 的 PERCLK_CLK_SEL 位来决定,如果为 0 的话 PERCLK_CLK_ROOT 的时钟源就是 IPG_CLK_ROOT=66MHz 。可以通过寄存器 CCM_CSCMR1 的 PERCLK_PODF 位来设置分频,如果要设置 PERCLK_CLK_ROOT 为 66MHz 的话就要设置为 1 分频。

此寄存器主要用于外设时钟源的选择,比如 QSPI1、ACLK、GPMI、BCH 等外设,我们重点看一下下面两个位:

  1. CCM_CSCMR1[PERCLK_CK_SEL]
    perclk 时钟源选择。
  • CCM_CSCMR1[PERCLK_CK_SEL] = 0,选择 ipg clk。
  • CCM_CSCMR1[PERCLK_CK_SEL] = 1,选择 osc clk。
  1. CCM_CSCMR1[PERCLK_PODF]
    perclk 的时钟分频,可设置 0~7,分别对应 1~8 分频。
    在修改如下时钟选择器或者分频器的时候会引起与 MMDC 的握手发生:
    ①、mmdc_podf
    ②、periph_clk_sel
    ③、periph2_clk_sel
    ④、arm_podf
    ⑤、ahb_podf
    发生握手信号以后需要等待握手完成,寄存器 CCM_CDHIPR 中保存着握手信号是否完成,如果相应的位为 1 的话就表示握手没有完成,如果为 0 的话就表示握手完成。

另外在修改 arm_podf 和 ahb_podf 的时候需要先关闭其时钟输出,等修改完成以后再打开,否则的话可能会出现在修改完成以后没有时钟输出的问题。需要修改寄存器 CCM_CBCDR 的 AHB_PODF 位来设置 AHB_ROOT_CLK 的时钟,所以在修改之前必须先关闭 AHB_ROOT_CLK 的输出。但是没有找到相应的寄存器,因此目前没法关闭,那也就没法设置 AHB_PODF 了。不过 AHB_PODF 内部 boot rom 设置为了 3 分频,如果 pre_periph_clk 的时钟源选择 PLL2_PFD2 的话,AHB_ROOT_CLK 也是 396MHz/3=132MHz。

八、编程流程

1. 创建工程文件夹
2. 移植官方SDK寄存器定义文件
3. 编写启动文件
4. 编写链接文件
5. 编写makefile文件
6. 编写C语言代码
(1) 内核时钟设置
(2) PLL根时钟设置
(3) PFD根时钟设置
(4) 外设时钟设置

九、创建工程文件夹

  1. 创建一个文件夹 clock_init
  2. 创建一个用于存放头文件的文件夹 include
  3. 创建一个用于存放驱动源码的文件 device
  4. 创建一个启动文件 start.S
  5. 创建一个源文件 main.c
  6. 创建一个链接脚本 base.lds

十、移植官方SDK寄存器定义文件

/clock_init/include 目录下添加官方SDK寄存器定义文件 MCIMX6Y2.h,位于 SDK_2.2_MCIM6ULL_EBF6ULL/devices/MCIMX6Y2 目录下。

在官方SDK的头文件 MCIMX6Y2.h 文件多达4万多行,包含了i.MX6U芯片几乎所有的寄存器定义以及中断编号的定义。

这里只列 GPIO1相关寄存器 的部分代码。其他寄存器定义与此类似。 添加这些定义之后我们就可以 直接使用 “GPIO1->DR” 语句操作GPIO1的DR寄存器。操作方法与STM32非常相似。

typedef struct {
   __IO uint32_t DR;     /**< GPIO data register, offset: 0x0 */
   __IO uint32_t GDIR;   /**< GPIO direction register, offset: 0x4 */
   __I  uint32_t PSR;    /**< GPIO pad status register, offset: 0x8 */
   __IO uint32_t ICR1;   /**< GPIO interrupt configuration register1,*/
   __IO uint32_t ICR2;   /**< GPIO interrupt configuration register2, */
   __IO uint32_t IMR;   /**< GPIO interrupt mask register, offset: 0x14 */
   __IO uint32_t ISR; /**< GPIO interrupt status register, offset: 0x18 */
   __IO uint32_t EDGE_SEL;/**< GPIO edge select register, offset: 0x1C */
} GPIO_Type;

/*********************以下代码省略***************************8*/
/** Peripheral GPIO1 base address */
#define GPIO1_BASE                               (0x209C000u)
/** Peripheral GPIO1 base pointer */
#define GPIO1                                    ((GPIO_Type *)GPIO1_BASE)

十一、编写启动文件

在 Ubuntu 下创建 start.S 文件用于编写启动文件。
在汇编文件中设置“栈地址”并执行跳转命令跳转到main函数执行C代码。

11.1 完整代码

/***********************第一部分*********************/
  .text            //代码段
  .align 2         //设置2字节对齐
  .global _start   //定义一个全局标号

/*************************第二部分*************************/
  _start:          //程序的开始
    b reset      //跳转到reset标号处

/*************************第三部分*************************/
reset:
   mrc     p15, 0, r0, c1, c0, 0     /*  将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中   */
   bic     r0,  r0, #(0x1 << 12)     /*  清除第12位(I位)禁用 I Cache  */
   bic     r0,  r0, #(0x1 <<  2)     /*  清除第 2位(C位)禁用 D Cache  */
   bic     r0,  r0, #0x2             /*  清除第 1位(A位)禁止严格对齐   */
   bic     r0,  r0, #(0x1 << 11)     /*  清除第11位(Z位)分支预测   */
   bic     r0,  r0, #0x1             /*  清除第 0位(M位)禁用 MMU   */
   mcr     p15, 0, r0, c1, c0, 0     /*  将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中   */

/***********************第四部分*********************/
      ldr sp, =0x84000000   //设置栈地址64M
      b main                //跳转到main函数

/***********************第五部分*******************/
    /*进入死循环*/
  loop:
      b loop

11.2 分析代码

  • 第一部分
    .text 定义代码段。
    .align 2 设置字节对齐。
    .global _start 生命全局标号_start。
/*************************第一部分*************************/
.text            //代码段
.align 2         //设置2字节对齐
.global _start   //定义一个全局标号
  • 第二部分
    _start: 定义标号_start: ,它位于汇编的最前面,说以会首先被执行。
    b reset 使用b指令将程序跳转到reset标号处。
/*************************第二部分*************************/
_start:          //程序的开始
   b reset      //跳转到reset标号处
  • 第三部分
    通过修改CP15寄存器(系统控制寄存器) 关闭 I Cache 、D Cache、MMU 等等。
    我们暂时用不到的功能,如果开启可能会影响我们裸机运行,为避免不必要的麻烦暂时关闭这些功能。
/*************************第三部分*************************/
reset:
   mrc     p15, 0, r0, c1, c0, 0     /*  将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中   */
   bic     r0,  r0, #(0x1 << 12)     /*  清除第12位(I位)禁用 I Cache  */
   bic     r0,  r0, #(0x1 <<  2)     /*  清除第 2位(C位)禁用 D Cache  */
   bic     r0,  r0, #0x2             /*  清除第 1位(A位)禁止严格对齐   */
   bic     r0,  r0, #(0x1 << 11)     /*  清除第11位(Z位)分支预测   */
   bic     r0,  r0, #0x1             /*  清除第 0位(M位)禁用 MMU   */
   mcr     p15, 0, r0, c1, c0, 0     /*  将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中   */
  • 第四部分
    ldr sp, =0x84000000 用于设置栈指针。野火i.MX6ULL开发板标配512M的DDR内存,裸机开发用不了这么多。程序中我们将栈地址设置到DDR的64M地址处。 这个值也可以根据需要自行定义。
    b main 只用跳转指令跳转到main函数中执行。
/***********************第四部分*********************/
      ldr sp, =0x84000000   //设置栈地址64M
      b main                //跳转到main函数
  • 第五部分
    b loop 是“无返回”的跳转指令。正常情况下,不会执行第五部分代码。
/***********************第五部分*******************/
  /*进入死循环*/
  loop:
      b loop

十二、编写链接脚本

写好的代码(无论是汇编还是C语言)都要经过编译、汇编、链接等步骤生成二进制文件或者可供下载的文件。在编译阶编译器会对每个源文件进行语法检查并生成对应的汇编语言,汇编是将汇编文件转化为机器码。

使用 arm-none-eabi-gcc -g -c led.S -o led.o 命令完成源码的编译、汇编工作,生成了 .o文件。编译和汇编是针对单个源文件,也就编译完成后一个源文件(.c.S.s)对应一个 .o 文件。程序链接阶段就会将这些 .o 链接成一个文件。

链接脚本的作用就是告诉编译器怎么链接这些文件,比如那个文件放在最前面,程序的代码段、数据段、bss段分别放在什么位置等等。

在 Ubuntu 下创建 base.lds 链接脚本。

12.1 完整代码

 ENTRY(_start)
 SECTIONS {
   . = 0x80000000;

   . = ALIGN(4);
   .text :
   {
   start.o (.text)
   *(.text)
   }

   . = ALIGN(4);
   .data :
   {
   *(.data)
   }

   . = ALIGN(4);
   .bss :
   {
   *(.bss)
   }
 }

12.2 分析代码

  • 指定程序的入口
    ENTRY(_start) 用于指定程序的入口,ENTRY() 是设置入口地址的命令, “_start” 是程序的入口,led程序的入口地址位于 start.S“_start” 标号处。
 ENTRY(_start)
  • 定义SECTIONS
    SECTIONS 可以理解为是一块区域,我们在这块区域排布我们的代码,链接时链接器就会按照这里的指示链接我们的代码。
 SECTIONS {
···
···
}
  • 定义链接起始地址
    “.” 运算符代表当前位置。 我们在SECTION的最开始使用 “.= 0x80000000” 就是将链接起始地址设置为0x80000000。
. = 0x80000000;
  • 设置字节对齐
    “. = ALIGN(4);” 它表示从当前位置开始执行四字节对齐。假设当前位置为0x80000001,执行该命令后当前地址将会空出三个字节转到0x80000004地址处。

  • 设置代码段
    “.text :” 用于定义代码段,固定的语法要求,我们按照要求写即可。在“{}”中指定那些内容放在代码段。
    start.o 中的代码放到代码段的最前面。start.S是启动代码应当首先被执行,所以通常情况下要把它放到代码段的最前面,其他源文件的代码按照系统默认的排放顺序即可,通配符 “*” 在这里表示其他剩余所有的 .o文件。

   . = ALIGN(4);
   .text :
   {
   start.o (.text)
   *(.text)
   }
  • 设置数据段
    同设置代码段类似,首先设置字节对齐,然后定义代码段。在数据段里使用 “*” 通配符, 将所有源文件中的代码添加到这个数据段中。
   . = ALIGN(4);
   .data :
   {
   *(.data)
   }
  • 设置BSS段
    设置方法与设置数据段完全相同。
. = ALIGN(4);
   .bss :
   {
   *(.bss)
   }

十三、编写makefile文件

程序编写完成后需要依次输入编译、链接、格式转换命令才能最终生成二进制文件。这种编译方式效率低、容易出错。

使用makefile只需要在所在文件夹下执行make命令,makefile工具便会自动完成程序的编译、链接、格式转换等工作。正常情况下我们可以在当前目录看到生成的一些中间文件以及我们期待的.bin文件。

修改makefile主要包括两部分

  • 第一部分,在“device”文件夹下添加并编写子makefile。
  • 第二部分,修改主makefile。

13.1 编写子makefile

/clock_init/device 下创建 makefile

子makefile: 用于将“device”文件夹下的驱动源文件编译为一个“.o”文件

all : led.o system_MCIMX6Y2.o clock.o
	arm-none-eabi-ld -r $^  -o device.o
	
%.o : %.c
	arm-none-eabi-gcc ${header_file} -c $^
	
%.o : %.S
	arm-none-eabi-gcc ${header_file} -c $^

clean:
	-rm -f *.o *.bak
  • 添加最终目标以及依赖文件
    生成最终目标“device.o”。如果程序中新增了某个外设驱动程序,只需要将对应的“.o”文件填入“依赖”处即可。
    “$^” 代表所有的依赖文件。
    “-o” 指定输出文件名。
all : button.o  led.o system_MCIMX6Y2.o
    arm-none-eabi-ld -r $^  -o device.o
  • 添加编译C文件的命令
    编译“device”文件夹下的所有“.c”文件并生成对应的“.o”文件,其中“header_file”是头文件路径,它是定义在主makefile的变量。
    “$^” 替代要编译的源文件。
%.o : %.c
  arm-none-eabi-gcc ${header_file} -c $^
  • 添加汇编文件编译命令
    编译“device”文件夹下的所有“.S”文件并生成对应的“.o”文件,其中“header_file”是头文件路径,它是定义在主makefile的变量。
    “$^” 替代要编译的源文件。
%.o : %.S
  arm-none-eabi-gcc ${header_file} -c $^
  • 添加清理命令
    “clean” 为目标用于删除make生成的文件。
clean:
  -rm -f *.o *.bak

13.2 修改主makefile

主makefile的改动主要有两点:

  1. 在编译命令中指明头文件位置。
  2. 使用命令调用子makefile,生成依赖文件。
#定义变量,用于保存编译选项和头文件保存路径
header_file := -fno-builtin -I$(shell pwd)/include
export header_file


all : start.o main.o device/device.o 
	arm-none-eabi-ld -Tbase.lds $^ -o base.elf 
	arm-none-eabi-objcopy -O binary -S -g base.elf base.bin


%.o : %.S
	arm-none-eabi-gcc -g -c $^ 
%.o : %.c
	arm-none-eabi-gcc $(header_file) -c $^ 	

#调用其他文件的makefile
device/device.o :
	make -C device all


.PHONY: copy
copy:
	cp ./base.bin  /home/pan/download/embedfire

#定义清理伪目标
.PHONY: clean
clean:
	make -C device clean
	-rm -f *.o *.elf *.bin 
  • 添加编译选项和头文件保存路径
    定义变量 “header_file”。在makefile中“变量”更像C原因中的宏定义。
    “-fno-builtin” 是一个编译选项,用于解决库函数与自己编写函数同名问题。
    “-I$(shell pwd)/include” 用于指定头文件路径。
    “export header_file” 声明后可以在其他makefile中调用。
header_file := -fno-builtin -I$(shell pwd)/include
export header_file
  • 添加最终目标以及依赖文件
all : start.o main.o device/device.o
  • 添加链接命令
    “-Tbase.lds” 表示使用base.lds链接脚本链接程序。
    “$^” 代表所有的依赖文件。
    “-o” 指定输出文件名。
arm-none-eabi-ld -Tbase.lds $^ -o base.elf
  • 添加格式转换命令
    “-O binary” 指定输出二进制文件。
    “-S” 不从源文件中复制重定位信息和符号信息。
    “-g” 不从源文件中复制可调试信息。
arm-none-eabi-objcopy -O binary -S -g base.elf base.bin
  • 添加汇编文件编译命令
    “$^” 替代要编译的源文件。
%.o : %.S
  arm-none-eabi-gcc -g -c $^
  • 添加编译C文件的命令
    “$^” 替代要编译的源文件。
%.o : %.c
  arm-none-eabi-gcc $(header_file) -c $^
  • 添加调用其他文件的makefile
    定义生成“device/device.o”的命令,“device.o”文件由子makefile生成,所以这里只需要调用子makefile即可。
device/device.o :
  make -C device all
  • 添加清理命令
    在清理命令中不但要清理主makefile所在文件夹的内容还要调用子makefile的清理命令以清理子makefile所在文件夹的内容。
    “.PHONY” 定义了伪目标“clean”。伪目标一般没有依赖,并且 “clean” 伪目标一般放在Makefile文件的末尾。
    “clean” 为目标用于删除make生成的文件。
.PHONY: clean
clean:
  make -C device clean
  -rm -f *.o *.elf *.bin

十四、编写C语言代码

14.1 添加时钟初始化代码

14.1.1 clock.h

/clock_init/include 下创建 clock.h

#ifndef clock_h
#define clock_h

/*系统时钟初始化函数*/
void system_clock_init(void);

#endif

14.1.2 clock.c

/clock_init/device 下创建 clock.c

主要实现更改 CPU 时钟、设置 PLL2、PLL3 的输出时钟以及对应的 PFD 时钟。

#include "clock.h"
#include "MCIMX6Y2.h"


void system_clock_init(void)
{
    /******************* 第一层时钟设置--晶振时钟***********************/
    if ((CCM->CCSR & (0x01 << 2)) == 0) //CPU 使用的是 ARM PLL
    {
        /*将CPU时钟切换到XTAL (OSC) 时钟*/                   
        CCM->CCSR &= ~(0x01 << 8); //控制CCSR: step_sel ,选择 osc_clk 作为时钟源
        CCM->CCSR |= (0x01 << 2);  //设置GLITCHLESS MUX 选择 step_clk 作为时钟源
    }

    /*设置PLL1输出时钟为792MHz,它将作为CPU时钟*/
    CCM_ANALOG->PLL_ARM |= (0x42 << 0);


    /******************* 第二层时钟设置--PLL根时钟***********************/
    /*将CPU 时钟重新切换到 ARM PLL*/
    CCM->CCSR &= ~(0x01 << 2);

    /*设置时钟分频系数为0,即不分频*/
    CCM->CACRR &= ~(0x07 << 0); //清零分频寄存器   不分频
   // CCM->CACRR |= (0x07 << 0);     // 8分频


    /*设置PLL2(System PLL) 输出时钟*/
    /* Configure SYS PLL to 528M */
    CCM_ANALOG->PLL_SYS_SS &= ~(0x8000);     //使能PLL2 PFD输出
    CCM_ANALOG->PLL_SYS_NUM &= ~(0x3FFFFFFF);//设置分频系数为0,即不分频。
    CCM_ANALOG->PLL_SYS |= (0x2000); //使能PLL2 输出
    CCM_ANALOG->PLL_SYS |= (1 << 0); //设置输出频率为528M
    while ((CCM_ANALOG->PLL_SYS & (0x80000000)) == 0) //等待设置生效
    {
    }

    /*设置PLL3(System PLL) 输出时钟*/
    /* Configure USB PLL to 480M */
    CCM_ANALOG->PLL_USB1 |= (0x2000);    //使能 PLL3时钟输出
    CCM_ANALOG->PLL_USB1 |= (0x1000);    //PLL3上电使能
    CCM_ANALOG->PLL_USB1 |= (0x40);      // 使能USBPHYn
    CCM_ANALOG->PLL_USB1 &= ~(0x01 << 0);//设置输出频率为480MHz
    while ((CCM_ANALOG->PLL_SYS & (0x80000000)) == 0)//等待设置生效
    {
    }

    /*关闭暂时不使用的 PLL4 、PLL5  、PLL6 、PLL7*/
    CCM_ANALOG->PLL_AUDIO = (0x1000);    //关闭PLL4
    CCM_ANALOG->PLL_VIDEO = (0x1000);    //关闭PLL5
    CCM_ANALOG->PLL_ENET =  (0x1000);    //关闭PLL6
    CCM_ANALOG->PLL_USB2 =  (0x00);           //关闭PLL7

 
    /******************第三层时钟设置--PFD*******************/
    /*禁用PLL2 的所有PFD输出*/
    CCM_ANALOG->PFD_528 |=(0x80U) ;      //关闭PLL2 PFD0
    CCM_ANALOG->PFD_528 |=(0x8000U) ;    //关闭PLL2 PFD1
    // CCM_ANALOG->PFD_528 |=(0x800000U) ;  //关闭PLL2 PFD2 ,DDR使用的是该时钟源,关闭后程序不能运行。暂时不关闭
    CCM_ANALOG->PFD_528 |=(0x80000000U); //关闭PLL2 PFD3
    
    /*设置PLL2 的PFD输出频率*/
    CCM_ANALOG->PFD_528 &= ~(0x3FU); //清零PLL2 PFD0 时钟分频
    CCM_ANALOG->PFD_528 &= ~(0x3F00U); //清零PLL2 PFD1 时钟分频
    CCM_ANALOG->PFD_528 &= ~(0x3F00U); //清零PLL2 PFD2 时钟分频
    CCM_ANALOG->PFD_528 &= ~(0x3F00U); //清零PLL2 PFD3 时钟分频

    CCM_ANALOG->PFD_528 |= (0x1B << 0); //设置PLL2 PFD0 输出频率为 352M
    CCM_ANALOG->PFD_528 |= (0x10 << 8); //设置PLL2 PFD0 输出频率为 594M
    CCM_ANALOG->PFD_528 |= (0x18 << 16); //设置PLL2 PFD0 输出频率为 396M
    CCM_ANALOG->PFD_528 |= (0x30 << 24); //设置PLL2 PFD0 输出频率为 198M

    /*启用PLL2 的所有PFD输出*/
    CCM_ANALOG->PFD_528 &= ~(0x80U) ;      //开启PLL2 PFD0
    CCM_ANALOG->PFD_528 &= ~(0x8000U) ;    //开启PLL2 PFD1
    CCM_ANALOG->PFD_528 &= ~(0x800000U) ;  //开启PLL2 PFD2
    CCM_ANALOG->PFD_528 &= ~(0x80000000U); //开启PLL2 PFD3


    /*禁用PLL3 的所有PFD输出*/
    CCM_ANALOG->PFD_480 |=(0x80U) ;      //关闭PLL3 PFD0
    CCM_ANALOG->PFD_480 |=(0x8000U) ;    //关闭PLL3 PFD1
    CCM_ANALOG->PFD_480 |=(0x800000U) ;  //关闭PLL3 PFD2
    CCM_ANALOG->PFD_480 |=(0x80000000U); //关闭PLL3 PFD3

    /*设置PLL3 的PFD输出频率*/
    CCM_ANALOG->PFD_480 &= ~(0x3FU);   //清零PLL3 PFD0 时钟分频
    CCM_ANALOG->PFD_480 &= ~(0x3F00U); //清零PLL3 PFD1 时钟分频
    CCM_ANALOG->PFD_480 &= ~(0x3F00U); //清零PLL3 PFD2 时钟分频
    CCM_ANALOG->PFD_480 &= ~(0x3F00U); //清零PLL3 PFD3 时钟分频

    CCM_ANALOG->PFD_480 |= (0xC << 0); //设置PLL3 PFD0 输出频率为 720M
    CCM_ANALOG->PFD_480 |= (0x10 << 8); //设置PLL3 PFD0 输出频率为 540M
    CCM_ANALOG->PFD_480 |= (0x11 << 16); //设置PLL3 PFD0 输出频率为 508.2M
    CCM_ANALOG->PFD_480 |= (0x13 << 24); //设置PLL3 PFD0 输出频率为 454.7M

    /*启用PLL3 的所有PFD输出*/
    CCM_ANALOG->PFD_480 &= ~(0x80U) ;      //开启PLL3 PFD0
    CCM_ANALOG->PFD_480 &= ~(0x8000U) ;    //开启PLL3 PFD1
    CCM_ANALOG->PFD_480 &= ~(0x800000U) ;  //开启PLL3 PFD2
    CCM_ANALOG->PFD_480 &= ~(0x80000000U); //开启PLL3 PFD3
  

    /******************第四层时钟设置--外设****************/
    CCM->CSCDR1 &= ~(0x01 << 6); //设置UART选择 PLL3 / 6 = 80MHz
    CCM->CSCDR1 &= ~(0x3F);     //清零
    CCM->CSCDR1 |= ~(0x01 << 0); //设置串口根时钟分频值为1,UART根时钟频率为:80M / (dev + 1) = 40MHz
}
  • 第一部分:内核时钟设置
    对应上面五、内核时钟设置。首先判断当 CPU 时钟是否使用 pll1_main_clk,如果是,则将其切换到 osc_clk 时钟。修改 PLL1 输出频率为 792MHz,并将 CPU 时钟源切换到pll1_main_clk
if ((CCM->CCSR & (0x01 << 2)) == 0) //CPU 使用的是 ARM PLL
{
    /*将CPU时钟切换到XTAL (OSC) 时钟*/                   
    CCM->CCSR &= ~(0x01 << 8); //控制CCSR: step_sel ,选择 osc_clk 作为时钟源
    CCM->CCSR |= (0x01 << 2);  //设置GLITCHLESS MUX 选择 step_clk 作为时钟源
}

/*设置PLL1输出时钟为792MHz,它将作为CPU时钟*/
CCM_ANALOG->PLL_ARM |= (0x42 << 0);

/*将CPU 时钟重新切换到 ARM PLL*/
CCM->CCSR &= ~(0x01 << 2);
  • 第二部分:PLL根时钟设置
    设置 PLL2~PLL7 的输出频率。其中 PLL2 的时钟被设置为 528M 并开启了PFD输出功能。PLL3 的时钟被设置为 480M 并开启了 PFD 输出功能。PLL4~PLL7 我们暂时用不到,直接关闭时钟输出。关闭不使用的时钟能够有效的减少功耗。
/*设置时钟分频系数为0,即不分频*/
CCM->CACRR &= ~(0x07 << 0); //清零分频寄存器   不分频
// CCM->CACRR |= (0x07 << 0);     // 8分频


/*设置PLL2(System PLL) 输出时钟*/
/* Configure SYS PLL to 528M */
CCM_ANALOG->PLL_SYS_SS &= ~(0x8000);     //使能PLL2 PFD输出
CCM_ANALOG->PLL_SYS_NUM &= ~(0x3FFFFFFF);//设置分频系数为0,即不分频。
CCM_ANALOG->PLL_SYS |= (0x2000); //使能PLL2 输出
CCM_ANALOG->PLL_SYS |= (1 << 0); //设置输出频率为528M
while ((CCM_ANALOG->PLL_SYS & (0x80000000)) == 0) //等待设置生效
{
}

/*设置PLL3(System PLL) 输出时钟*/
/* Configure USB PLL to 480M */
CCM_ANALOG->PLL_USB1 |= (0x2000);    //使能 PLL3时钟输出
CCM_ANALOG->PLL_USB1 |= (0x1000);    //PLL3上电使能
CCM_ANALOG->PLL_USB1 |= (0x40);      // 使能USBPHYn
CCM_ANALOG->PLL_USB1 &= ~(0x01 << 0);//设置输出频率为480MHz
while ((CCM_ANALOG->PLL_SYS & (0x80000000)) == 0)//等待设置生效
{
}

/*关闭暂时不使用的 PLL4 、PLL5  、PLL6 、PLL7*/
CCM_ANALOG->PLL_AUDIO = (0x1000);    //关闭PLL4
CCM_ANALOG->PLL_VIDEO = (0x1000);    //关闭PLL5
CCM_ANALOG->PLL_ENET =  (0x1000);    //关闭PLL6
CCM_ANALOG->PLL_USB2 =  (0x00);           //关闭PLL7
  • 第三部分:PFD时钟设置
    对应上面六、PFD时钟设置。

    设置 PLL2 的 PFD 输出。PLL2 共有 4 个 PFD 输出(PFD0~PFD3),PLL2 的 PFD 设置通过 CCM_ANALOG_PFD_528n 寄存器实现。PLL2 的 PFD 输出大致分为三部分。

    • 第一,禁用 PLL2 的 PFD 输出。
    • 第二,设置 PFD 的输出频率。
    • 第三 ,启用 PFD 输出。

    特别注意的是这里没有禁用 PFD2,因为这是 DDR 的时钟源。关闭后程序无法运行。

    设置 PLL3 的 PFD 输出,设置方法与设置 PLL2 的 PFD 输出完全相同,只是这里设置的是CCM_ANALOG_PFD_480 时钟。

/*禁用PLL2 的所有PFD输出*/
CCM_ANALOG->PFD_528 |=(0x80U) ;      //关闭PLL2 PFD0
CCM_ANALOG->PFD_528 |=(0x8000U) ;    //关闭PLL2 PFD1
// CCM_ANALOG->PFD_528 |=(0x800000U) ;  //关闭PLL2 PFD2 ,DDR使用的是该时钟源,关闭后程序不能运行。暂时不关闭
CCM_ANALOG->PFD_528 |=(0x80000000U); //关闭PLL2 PFD3
    
/*设置PLL2 的PFD输出频率*/
CCM_ANALOG->PFD_528 &= ~(0x3FU); //清零PLL2 PFD0 时钟分频
CCM_ANALOG->PFD_528 &= ~(0x3F00U); //清零PLL2 PFD1 时钟分频
CCM_ANALOG->PFD_528 &= ~(0x3F00U); //清零PLL2 PFD2 时钟分频
CCM_ANALOG->PFD_528 &= ~(0x3F00U); //清零PLL2 PFD3 时钟分频

CCM_ANALOG->PFD_528 |= (0x1B << 0); //设置PLL2 PFD0 输出频率为 352M
CCM_ANALOG->PFD_528 |= (0x10 << 8); //设置PLL2 PFD0 输出频率为 594M
CCM_ANALOG->PFD_528 |= (0x18 << 16); //设置PLL2 PFD0 输出频率为 396M
CCM_ANALOG->PFD_528 |= (0x30 << 24); //设置PLL2 PFD0 输出频率为 198M

/*启用PLL2 的所有PFD输出*/
CCM_ANALOG->PFD_528 &= ~(0x80U) ;      //开启PLL2 PFD0
CCM_ANALOG->PFD_528 &= ~(0x8000U) ;    //开启PLL2 PFD1
CCM_ANALOG->PFD_528 &= ~(0x800000U) ;  //开启PLL2 PFD2
CCM_ANALOG->PFD_528 &= ~(0x80000000U); //开启PLL2 PFD3


/*禁用PLL3 的所有PFD输出*/
CCM_ANALOG->PFD_480 |=(0x80U) ;      //关闭PLL3 PFD0
CCM_ANALOG->PFD_480 |=(0x8000U) ;    //关闭PLL3 PFD1
CCM_ANALOG->PFD_480 |=(0x800000U) ;  //关闭PLL3 PFD2
CCM_ANALOG->PFD_480 |=(0x80000000U); //关闭PLL3 PFD3

/*设置PLL3 的PFD输出频率*/
CCM_ANALOG->PFD_480 &= ~(0x3FU);   //清零PLL3 PFD0 时钟分频
CCM_ANALOG->PFD_480 &= ~(0x3F00U); //清零PLL3 PFD1 时钟分频
CCM_ANALOG->PFD_480 &= ~(0x3F00U); //清零PLL3 PFD2 时钟分频
CCM_ANALOG->PFD_480 &= ~(0x3F00U); //清零PLL3 PFD3 时钟分频

CCM_ANALOG->PFD_480 |= (0xC << 0); //设置PLL3 PFD0 输出频率为 720M
CCM_ANALOG->PFD_480 |= (0x10 << 8); //设置PLL3 PFD0 输出频率为 540M
CCM_ANALOG->PFD_480 |= (0x11 << 16); //设置PLL3 PFD0 输出频率为 508.2M
CCM_ANALOG->PFD_480 |= (0x13 << 24); //设置PLL3 PFD0 输出频率为 454.7M

/*启用PLL3 的所有PFD输出*/
CCM_ANALOG->PFD_480 &= ~(0x80U) ;      //开启PLL3 PFD0
CCM_ANALOG->PFD_480 &= ~(0x8000U) ;    //开启PLL3 PFD1
CCM_ANALOG->PFD_480 &= ~(0x800000U) ;  //开启PLL3 PFD2
CCM_ANALOG->PFD_480 &= ~(0x80000000U); //开启PLL3 PFD3
  • 第四部分:外设时钟设置
    对应上面七、AHB、IPG和PERCLK根时钟设置。设置串口的根时钟。这里只设置了串口,其他外设的时钟频率呢?在 BOOT ROM 中已经初始化了部分外设的时钟,为减少难度,那些没有使用到的外设或者对频率没有严格要求的外设我们暂时保持默认的时钟频率。这里设置 UART 根时钟的目的是以 UART 为例讲解如何设置外设时钟。UART 时钟源产生路径如下所示。

从图中可以看出,从PLL时钟到UART时钟共用用到了两个时钟选择寄存器(标号①和③),两个时钟分频寄存器(标号②和标号④)。 我们最终目的是将PLL3时钟作为UART根时钟(UART_CLK_ROOT)的根时钟。按照标号顺序讲解如下:

  1. 标号①选择 PLL3 时钟还是 CCM_PLL3_BYP。我们选择 PLL3 输出时钟,寄存器 CCSR[PLL3_SW_CLK_SEL] = 0, 则表示选择 PLL3 时钟。默认情况下是这样设置的。所以我们代码中并没有设置该寄存器。

  2. 标号②设置时钟分频,根据之前的设置,PLL3 的输出频率为 480MHz ,这里的时钟分频是固定的 6 分频, 经过分频后的时钟为 480MHz / 6 = 80MHz。

  3. 标号③ 再次选择时钟源。一个是 PLL3 分频得到的 80MHz 时钟,另外一个是 OSC 时钟即 24MHz 的系统参考时钟。 设置 CSCDR1[UART_CLK_SEL] = 0,选择第一个(80MHz)时钟。

  4. 标号④再次进行时钟分频。这是一个 6 位的时钟分频寄存器。分频值为 CSCDR1[UART_CLK_PODF] 寄存器值加一。 程序中将其设置为 1,则分频系数为 2,UART_CLK_ROOT 时钟频率实际为 80MHz / 2 = 40MHz 。

14.2 main.c

/clock_init 下创建 main.c

使用RGB灯闪烁频率大致判断程序运行速度。

#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "pad_config.h"

#include "led.h"
#include "clock.h"

uint8_t button_status = 0;

/*简单延时函数*/
void delay(uint32_t count)
{
    volatile uint32_t i = 0;
    for (i = 0; i < count; ++i)
    {
        __asm("NOP"); /* 调用nop空指令 */
    }
}


int main()
{
    int i = 0;
    system_clock_init();
    rgb_led_init();                          //初始化 RGB 灯,初始化后 默认所有灯都不亮。
    
    while (1)
    {
        red_led_off;
        green_led_on;
        delay(0xFFFFF);

        green_led_off;
        red_led_on;
        delay(0xFFFFF);
    }

    return 0;
}

十五、编译下载验证

15.1 编译代码

make

执行make命令,生成base.bin文件。

15.2 代码烧写

编译成功后会在当前文件夹下生成.bin文件,这个.bin文件也不能直接放到开发板上运行, 这次是因为需要在.bin文件缺少启动相关信息。

为二进制文件添加头部信息并烧写到SD卡。查看 IMX6ULL学习笔记(12)——通过SD卡启动官方SDK程序

进入烧写工具目录,执行 ./mkimage.sh <烧写文件路径> 命令,例如要烧写的 base.bin 位于 home 目录下,则烧写命令为 ./mkimage.sh /home/button.bin

执行上一步后会列出linux下可烧写的磁盘,选择你插入的SD卡即可。这一步 非常危险!!!一定要确定选择的是你插入的SD卡!!,如果选错很可能破坏你电脑磁盘内容,造成数据损坏!!! 确定磁盘后SD卡以“sd”开头,选择“sd”后面的字符即可。例如要烧写的sd卡是“sdb”则输入“b”即可。

15.3 实验现象

在while(1)中控制RGB闪烁。通过修改时钟分频寄存器修改CPU时钟频率。我们可以对比不分频和8分频的实验效果。

void system_clock_init(void)
{
   /******************* PLL 输出时钟设置************************/
   if ((CCM->CCSR & (0x01 << 2)) == 0) //CPU 使用的是 ARM PLL
   {
      /*将CPU时钟切换到XTAL (OSC) 时钟*/
      CCM->CCSR &= ~(0x01 << 8); //控制CCSR: step_sel ,选择 osc_clk 作为时钟源
      CCM->CCSR |= (0x01 << 2);  //设置GLITCHLESS MUX 选择 step_clk 作为时钟源
   }

   /*设置PLL1输出时钟为792MHz,它将作为CPU时钟*/
   CCM_ANALOG->PLL_ARM |= (0x42 << 0);

   /*将CPU 时钟重新切换到 ARM PLL*/
   CCM->CCSR &= ~(0x01 << 2);

   /*设置时钟分频系数为0,即不分频*/
   CCM->CACRR &= ~(0x07 << 0); //清零分频寄存器   不分频
   //CCM->CACRR |= (0x07 << 0);     // 8分频

   ...
}

• 由 Leung 写于 2023 年 3 月 28 日

• 参考:11. 时钟控制模块(CCM)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

IMX6ULL学习笔记(19)——时钟系统 的相关文章

  • 实用机器学习(hw1/hw4)

    实用机器学习 hw1 hw4 文章目录 实用机器学习 hw1 hw4 1 环境安装2 baseline 代码分析3 提升精度代码4 机器学习模型 1 环境安装 autogluon 2 baseline 代码分析 span class tok
  • 没有与这些操作数匹配的运算符

    没有与这些操作数匹配的 lt lt 运算符 include与 include lt string h gt 的区别 lt string h gt 的区别 是C 43 43 特化的字符容器 xff0c 内含string类 lt string
  • gazebo模型下载以及配置

    最近在学习ROS xff0c 主要是为了结合SLAM仿真使用 启动gazebo命令 roscore 在另一个终端执行 gazebo 就可以进入清爽的gazebo界面 xff08 如果屏幕出现黑屏并不是安装错误可以稍微等待一会 xff09 x
  • SLAM中常用数据集下载链接(TUM KITTI DSO Mono EuRoC)

    TUM 链接 xff1a https pan baidu com s 1nwXtGqH 密码 xff1a lsgr KITTI 链接 xff1a https pan baidu com s 1htFmXDE 密码 xff1a uu20 KI
  •  windows docker 更改镜像安装目录

    目录 1 问题 1 1 版本信息 2 修改Docker盘位操作 2 1 停止docker 2 2 备份已有的数据 2 3 删除旧数据 数据未备份前请谨慎操作 2 4 导入数据到新盘 2 5 启动Docker START 1 问题 Windo
  • gnssins代码阅读

    这个代码是GNSS和INS紧组合的 xff1a https github com marcoamm gnssins xff0c 实现了ppp和ins紧组合 改变数据需要改代码的地方 xff1a imu tactical 61 fopen 3
  • 深度解析FUTABA的SBUS协议(/天地飞遥控器的WBUS协议/Robomaster接收机的DBUS协议)到底是啥?

    写在前面 xff1a 无论是SBUS xff08 日本FUTABA xff0c 所以航模 xff0c 车模爱好者都知道的公司 xff0c 一个好点遥控器近万了 xff09 xff0c 还是WBUS xff08 天地飞遥控器接收机用 xff0
  • 贝塞尔曲线动画C++简单实践

    目录 贝塞尔曲线简介一阶贝塞尔二阶贝塞尔三阶贝塞尔N阶贝塞尔曲线 贝塞尔曲线在动画中的应用实践求曲线散点坐标将曲线应用到动画动画框架cmd动画窗口动画 完整代码示例代码核心类代码BezierCurve Animator Console 参考
  • package.xml文件介绍

    package xml文件介绍 在ROS中创建功能包时 xff0c 会自动生成package xml文件 xff0c pacakge xml 包含了package的名称 版本号 内容描述 维护人员 软件许可 编译构建工具 编译依赖 运行依赖
  • ubuntu误修改了bashrc文件的解决办法

    在安装Pycharm的过程中配置JAVA的JDK环境变量时 xff0c 将bashrc内的内容不小心修改了 xff0c 导致命令窗口中的很多命令不能执行 xff0c 并且su及sudo这些权限的命令也用不了 xff0c 问题信息如下图所示
  • docker安装canal1.1.5监控mysql的binlog日志并配置rocketmq进行数据同步到elasticsearch(超级大干货)

    直接来 xff0c 不逼逼 xff08 canal官网说的很明白 xff0c 伪从节点请求dump 然后这个那个的 xff0c 自行查阅资料 xff09 1 直接拉取canal镜像 docker pull canal canal serve
  • 相机定位、相机重定位和视觉里程计的概念定义

    相机定位 相机重定位和视觉里程计的概念定义 什么是相机定位 xff1f 什么是相机重定位 xff1f 什么是视觉里程计 xff1f 相机定位 相机定位 xff08 Camera Localization xff09 是求解基于基本坐标系下的
  • vscode使用restClient实现各种http请求

    vscode使用restClient实现各种http请求 一 xff0c 安装插件 首先 xff0c 我们要在vscode的扩展中 xff0c 搜索rest Client xff0c 然后安装它 xff0c 这里我已经安装过了 安装后 xf
  • akka-2 利用模式匹配,实现worker节点向master报告本机配置信息

    1 创建本机配置信息类 WorkInfo Worker的本机信息类 id 主机名称 momery 内存大小 cores CPU核数 直接使用默认构造创建消息类 Worker的本机信息类 id 主机名称 momery 内存大小 cores C
  • 多路归并排序-Python实现大文件排序,合并排序

    使用python实现多 K 路归并外部排序 xff0c 解决小内存排序大文件问题 上一篇中 xff0c 我们实现了一般的归并排序 归并排序递归与非递归 Python实现 在实际工作中 xff0c 多个有序数列合并成一个 xff0c 大文件或
  • GINAV使用

    在配置文件中 xff0c 绝对不能 psd gyro 61 8 46e 14 等号后面必须要有空格才能区分开 psd gyro 61 8 46e 14 运行谷歌数据直接是空的图像
  • ZED双目相机(c++程序实现)

    1 前提条件 SDK最新版 xff08 从官网直接下载安装 xff0c 默认安装路径 xff09 CUDA xff08 安装对应版本 xff0c 此步需要电脑GPU支持 xff08 此步劝退一部人 xff09 xff09 下载zed示例包
  • fork()使用详解

    其他参考 xff1a linux中fork xff08 xff09 函数详解 一 fork入门知识 进程的定义 xff1a 进程是一个执行中的程序的实例 xff0c 是系统进行资源分配和调度的一个独立单位 PCB是进程存在的唯一标识 PCB
  • conda 安装包出现问题 :Collecting package metadata (current_repodata.json): failed

    如果有重装过anaconda的经历的同学 xff0c 应该注意到会有一个 condarc文件会自动生成 xff0c 所以当使用conda install和conda create命令会出现问题 删除 condarc文件之后 xff0c 这个
  • DockerFile+Python2安装和python3和Pip2安装request

    dockerfile 的使用步骤 sudo docker build t cve 11890 cve 11890是文件名 docker images 查看文件是否成功创建 sudo docker run d p 8089 80 cve 11

随机推荐