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

Posted Leung_ManWah

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IMX6ULL学习笔记(19)——时钟系统相关的知识,希望对你有一定的参考价值。

一、时钟系统简介

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寄存器定义文件

/interrupt_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 下创建 led.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 -

HI3861学习笔记——HarmonyOS(CMSIS-RTOS2)软件定时器

一、简介

软件定时器,是基于系统Tick时钟中断且由软件来模拟的定时器,当经过设定的Tick时钟计数值后会触发用户定义的回调函数。定时精度与系统Tick时钟的周期有关。

硬件定时器受硬件的限制,数量上不足以满足用户的实际需求,因此为了满足用户需求,提供更多的定时器,LiteOS操作系统提供软件定时器功能。

软件定时器扩展了定时器的数量,允许创建更多的定时业务。
软件定时器功能上支持:

  • 静态裁剪:能通过宏关闭软件定时器功能。
  • 软件定时器创建。
  • 软件定时器启动。
  • 软件定时器停止。
  • 软件定时器删除。
  • 软件定时器剩余Tick数获取。

软件定时器运作机制:
软件定时器使用了系统的一个队列和一个任务资源,软件定时器的触发遵循队列规则,先进先出。定时时间短的定时器总是比定时时间长的靠近队列头,满足优先被触发的准则。

软件定时器以Tick为基本计时单位,当用户创建并启动一个软件定时器时,LiteOS会根据当前系统Tick时间及用户设置的定时时间间隔确定该定时器的到期Tick时间,并将该定时器控制结构挂入计时全局链表。

当Tick中断到来时,在Tick中断处理函数中扫描软件定时器的计时全局链表,看是否有定时器超时,若有则将超过的定时器记录下来。

Tick中断处理函数结束后,软件定时器任务(优先级为最高)被唤醒,在该任务中调用之前记录下来的定时器的超时回调函数。

二、API说明

以下任务管理接口位于 kernel/liteos_m/components/cmsis/2.0/cmsis_os2.h

业务BUILD.gn中包含路径

include_dirs = [
        "//utils/native/lite/include",
        "//kernel/liteos_m/components/cmsis/2.0",
    ]

2.1 osTimerNew

功能创建定时器,不能在中断服务调用该函数
函数定义osTimerId_t osTimerNew (osTimerFunc_t func, osTimerType_t type, void *argument, const osTimerAttr_t *attr)
参数func:函数指针指向回调函数
type:定时器类型,osTimerOnce表示单次定时器,ostimer周期表示周期性定时器
argument:定时器回调函数的参数
attr:计时器属性
返回定时器ID

2.2 osTimerStart

功能启动定时器,不能在中断服务调用该函数
函数定义osStatus_t osTimerStart (osTimerId_t timer_id,uint32_t ticks)
参数timer_id:定时器ID
ticks:时间滴答计时器的值
返回0 - 成功,非0 - 失败

2.3 osTimerStop

功能停止定时器
函数定义osStatus_t osTimerStop (osTimerId_t timer_id)
参数timer_id:定时器ID
返回0 - 成功,非0 - 失败

2.4 osTimerDelete

功能删除定时器
函数定义osStatus_t osTimerDelete (osTimerId_t timer_id)
参数timer_id:定时器ID
返回0 - 成功,非0 - 失败

三、创建及启动定时器

编译时在业务BUILD.gn中包含路径

include_dirs = [
        "//utils/native/lite/include",
        "//kernel/liteos_m/components/cmsis/2.0",
    ]

在Timer_example函数中,通过osTimerNew()函数创建了回调函数为Timer1_Callback的定时器1,并通过osTimerStart()函数将该定时器设置为100个tick,因为hi3861默认10ms为一个tick,所以100个tick正好为1S钟,1S计时到后会触发Timer1_Callback()函数并打印日志。定时器2也同理为3S触发Timer2_Callback()函数并打印日志。

/*
 * Copyright (c) 2020 Nanjing Xiaoxiongpai Intelligent Technology Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include "ohos_init.h"
#include "cmsis_os2.h"

uint32_t exec1, exec2;

/***** 定时器1 回调函数 *****/
void Timer1_Callback(void *arg)
{
    (void)arg;
    printf("This is BearPi Harmony Timer1_Callback!\\r\\n");
}

/***** 定时器2 回调函数 *****/
void Timer2_Callback(void *arg)
{
    (void)arg;
    printf("This is BearPi Harmony Timer2_Callback!\\r\\n");
}

/***** 定时器创建 *****/
static void Timer_example(void)
{
    osTimerId_t id1, id2;
    uint32_t timerDelay;
    osStatus_t status;

    exec1 = 1U;
    id1 = osTimerNew(Timer1_Callback, osTimerPeriodic, &exec1, NULL);
    if (id1 != NULL)
    {

        // Hi3861 1U=10ms,100U=1S
        timerDelay = 100U;

        status = osTimerStart(id1, timerDelay);
        if (status != osOK)
        {
            // Timer could not be started
        }
    }

    exec2 = 1U;
    id2 = osTimerNew(Timer2_Callback, osTimerPeriodic, &exec2, NULL);
    if (id2 != NULL)
    {

        // Hi3861 1U=10ms,300U=3S
        timerDelay = 300U;

        status = osTimerStart(id2, timerDelay);
        if (status != osOK)
        {
            // Timer could not be started
        }
    }
}

APP_FEATURE_INIT(Timer_example);

查看打印:


• 由 Leung 写于 2021 年 6 月 6 日

• 参考:【鸿蒙2.0设备开发教程】小熊派HarmonyOS 鸿蒙·季 开发教程

以上是关于IMX6ULL学习笔记(19)——时钟系统的主要内容,如果未能解决你的问题,请参考以下文章

IMX6ULL学习笔记——挂载NFS网络文件系统

IMX6ULL学习笔记——挂载NFS网络文件系统

Buildroot系统构建学习笔记(以百问网imx6ull开发板为例)

编程\_配置时钟\_基于IMX6ULL

Buildroot系统构建学习笔记(以百问网imx6ull开发板为例)

IMX6ULL学习笔记——获取和编译U-Boot