一、时钟系统简介
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.768KHz
和 24MHz
时钟。
- 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寄存器的两个控制位:
CCM_CCSR[STEP_SEL]
对应图标号①处,
- CCM_CCSR[STEP_SEL] = 0,表示选择 24MHz 的 osc_clk 时钟,osc_clk 时钟是固定的,
默认我们选择这个时钟
。 - CCM_CCSR[STEP_SEL] = 1,表示选择 secondary_clk 时钟,这个时钟暂时用不到,不用关心。
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寄存器的三个控制位:
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)输入的外部时钟。
-
CCM_ANALOG_PLL_ARMn[DIV_SELECT]
选择锁相环分频值(DIV_SELECT)。 取值范围为54到108。输出频率计算公式为 ARM_PLL = Fin * DIV_SELECT / 2.0
。如果选择24MHz参考时钟作为时钟输入,DIV_SELECT选择66则ARM PLL的输出频率为792MHz。
-
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 对应的寄存器位如下:
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。CCM_ANALOG_PFD_528n[PFD0_STABLE]
此位为只读位,可以通过读取此位判断 PLL2_PFD0 是否稳定。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_ROOT
和 IPG_CLK_ROOT
的时钟,I.MX6U 外设根时钟可设置范围如下:
AHB_CLK_ROOT
最高可以设置 132MHz,IPG_CLK_ROOT
和 PERCLK_CLK_ROOT
最高可以设置 66MHz。
AHB_CLK_ROOT
和 IPG_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。
CCM_CBCMR[LCDIF1_PODF]
lcdif1 的时钟分频,可设置 0~7,分别对应 1~8 分频。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。
CCM_CBCMR[PERIPH2_CLK2_SEL]
periph2_clk2 时钟源选择。
- CCM_CBCMR[PERIPH2_CLK2_SEL] = 0,选择 pll3_sw_clk。
- CCM_CBCMR[PERIPH2_CLK2_SEL] = 1,选择 OSC。
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。
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。
CCM_CBCDR[PERIPH_CLK2_PODF]
periph2 时钟分频,可设置 0~7,分别对应 1~8 分频。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 中指定位表示。
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 中指定位表示。
CCM_CBCDR[AXI_PODF]
axi 时钟分频,可设置 0~7,分别对应 1~8 分频。CCM_CBCDR[AHB_PODF]
ahb 时钟分频,可设置 0~7,分别对应 1~8 分频。修改此位会引起一次与 MMDC 的握手,所以修改完成以后要等待握手完成,握手完成信号由寄存器 CCM_CDHIPR 中指定位表示。CCM_CBCDR[IPG_PODF]
ipg 时钟分频,可设置 0~3,分别对应 1~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。
CCM_CBCDR[FABRIC_MMDC_PODF]
fabric/mmdc 时钟分频设置,可设置 0~7,分别对应 1~8 分频。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 等外设,我们重点看一下下面两个位:
CCM_CSCMR1[PERCLK_CK_SEL]
perclk 时钟源选择。
- CCM_CSCMR1[PERCLK_CK_SEL] = 0,选择 ipg clk。
- CCM_CSCMR1[PERCLK_CK_SEL] = 1,选择 osc clk。
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) 外设时钟设置
九、创建工程文件夹
- 创建一个文件夹
clock_init
- 创建一个用于存放头文件的文件夹
include
- 创建一个用于存放驱动源码的文件
device
- 创建一个启动文件
start.S
- 创建一个源文件
main.c
- 创建一个链接脚本
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;
__IO uint32_t GDIR;
__I uint32_t PSR;
__IO uint32_t ICR1;
__IO uint32_t ICR2;
__IO uint32_t IMR;
__IO uint32_t ISR;
__IO uint32_t EDGE_SEL;
} GPIO_Type;
#define GPIO1_BASE (0x209C000u)
#define GPIO1 ((GPIO_Type *)GPIO1_BASE)
十一、编写启动文件
在 Ubuntu 下创建 start.S
文件用于编写启动文件。
在汇编文件中设置“栈地址”
并执行跳转命令跳转到main函数
执行C代码。
11.1 完整代码
.text
.align 2
.global _start
_start:
b reset
reset:
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #(0x1 << 12)
bic r0, r0, #(0x1 << 2)
bic r0, r0, #0x2
bic r0, r0, #(0x1 << 11)
bic r0, r0, #0x1
mcr p15, 0, r0, c1, c0, 0
ldr sp, =0x84000000
b main
loop:
b loop
11.2 分析代码
- 第一部分
.text
定义代码段。
.align 2
设置字节对齐。
.global _start
生命全局标号_start。
.text
.align 2
.global _start
- 第二部分
_start:
定义标号_start: ,它位于汇编的最前面,说以会首先被执行。
b reset
使用b指令将程序跳转到reset标号处。
_start:
b reset
- 第三部分
通过修改CP15寄存器(系统控制寄存器)
关闭 I Cache 、D Cache、MMU 等等。
我们暂时用不到的功能,如果开启可能会影响我们裸机运行,为避免不必要的麻烦暂时关闭这些功能。
reset:
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #(0x1 << 12)
bic r0, r0, #(0x1 << 2)
bic r0, r0, #0x2
bic r0, r0, #(0x1 << 11)
bic r0, r0, #0x1
mcr p15, 0, r0, c1, c0, 0
- 第四部分
ldr sp, =0x84000000
用于设置栈指针。野火i.MX6ULL开发板标配512M的DDR内存,裸机开发用不了这么多。程序中我们将栈地址设置到DDR的64M地址处。 这个值也可以根据需要自行定义。
b main
只用跳转指令跳转到main函数中执行。
ldr sp, =0x84000000
b 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)
}
. = 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的改动主要有两点:
- 在编译命令中指明头文件位置。
- 使用命令调用子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)
{
CCM->CCSR &= ~(0x01 << 8);
CCM->CCSR |= (0x01 << 2);
}
CCM_ANALOG->PLL_ARM |= (0x42 << 0);
CCM->CCSR &= ~(0x01 << 2);
CCM->CACRR &= ~(0x07 << 0);
CCM_ANALOG->PLL_SYS_SS &= ~(0x8000);
CCM_ANALOG->PLL_SYS_NUM &= ~(0x3FFFFFFF);
CCM_ANALOG->PLL_SYS |= (0x2000);
CCM_ANALOG->PLL_SYS |= (1 << 0);
while ((CCM_ANALOG->PLL_SYS & (0x80000000)) == 0)
{
}
CCM_ANALOG->PLL_USB1 |= (0x2000);
CCM_ANALOG->PLL_USB1 |= (0x1000);
CCM_ANALOG->PLL_USB1 |= (0x40);
CCM_ANALOG->PLL_USB1 &= ~(0x01 << 0);
while ((CCM_ANALOG->PLL_SYS & (0x80000000)) == 0)
{
}
CCM_ANALOG->PLL_AUDIO = (0x1000);
CCM_ANALOG->PLL_VIDEO = (0x1000);
CCM_ANALOG->PLL_ENET = (0x1000);
CCM_ANALOG->PLL_USB2 = (0x00);
CCM_ANALOG->PFD_528 |=(0x80U) ;
CCM_ANALOG->PFD_528 |=(0x8000U) ;
CCM_ANALOG->PFD_528 |=(0x80000000U);
CCM_ANALOG->PFD_528 &= ~(0x3FU);
CCM_ANALOG->PFD_528 &= ~(0x3F00U);
CCM_ANALOG->PFD_528 &= ~(0x3F00U);
CCM_ANALOG->PFD_528 &= ~(0x3F00U);
CCM_ANALOG->PFD_528 |= (0x1B << 0);
CCM_ANALOG->PFD_528 |= (0x10 << 8);
CCM_ANALOG->PFD_528 |= (0x18 << 16);
CCM_ANALOG->PFD_528 |= (0x30 << 24);
CCM_ANALOG->PFD_528 &= ~(0x80U) ;
CCM_ANALOG->PFD_528 &= ~(0x8000U) ;
CCM_ANALOG->PFD_528 &= ~(0x800000U) ;
CCM_ANALOG->PFD_528 &= ~(0x80000000U);
CCM_ANALOG->PFD_480 |=(0x80U) ;
CCM_ANALOG->PFD_480 |=(0x8000U) ;
CCM_ANALOG->PFD_480 |=(0x800000U) ;
CCM_ANALOG->PFD_480 |=(0x80000000U);
CCM_ANALOG->PFD_480 &= ~(0x3FU);
CCM_ANALOG->PFD_480 &= ~(0x3F00U);
CCM_ANALOG->PFD_480 &= ~(0x3F00U);
CCM_ANALOG->PFD_480 &= ~(0x3F00U);
CCM_ANALOG->PFD_480 |= (0xC << 0);
CCM_ANALOG->PFD_480 |= (0x10 << 8);
CCM_ANALOG->PFD_480 |= (0x11 << 16);
CCM_ANALOG->PFD_480 |= (0x13 << 24);
CCM_ANALOG->PFD_480 &= ~(0x80U) ;
CCM_ANALOG->PFD_480 &= ~(0x8000U) ;
CCM_ANALOG->PFD_480 &= ~(0x800000U) ;
CCM_ANALOG->PFD_480 &= ~(0x80000000U);
CCM->CSCDR1 &= ~(0x01 << 6);
CCM->CSCDR1 &= ~(0x3F);
CCM->CSCDR1 |= ~(0x01 << 0);
}
- 第一部分:内核时钟设置
对应上面五、内核时钟设置。首先判断当 CPU 时钟是否使用 pll1_main_clk
,如果是,则将其切换到 osc_clk
时钟。修改 PLL1 输出频率为 792MHz
,并将 CPU 时钟源切换到pll1_main_clk
。
if ((CCM->CCSR & (0x01 << 2)) == 0)
{
CCM->CCSR &= ~(0x01 << 8);
CCM->CCSR |= (0x01 << 2);
}
CCM_ANALOG->PLL_ARM |= (0x42 << 0);
CCM->CCSR &= ~(0x01 << 2);
- 第二部分:PLL根时钟设置
设置 PLL2~PLL7 的输出频率。其中 PLL2 的时钟被设置为 528M
并开启了PFD输出功能。PLL3 的时钟被设置为 480M
并开启了 PFD 输出功能。PLL4~PLL7 我们暂时用不到,直接关闭时钟输出。关闭不使用的时钟能够有效的减少功耗。
CCM->CACRR &= ~(0x07 << 0);
CCM_ANALOG->PLL_SYS_SS &= ~(0x8000);
CCM_ANALOG->PLL_SYS_NUM &= ~(0x3FFFFFFF);
CCM_ANALOG->PLL_SYS |= (0x2000);
CCM_ANALOG->PLL_SYS |= (1 << 0);
while ((CCM_ANALOG->PLL_SYS & (0x80000000)) == 0)
{
}
CCM_ANALOG->PLL_USB1 |= (0x2000);
CCM_ANALOG->PLL_USB1 |= (0x1000);
CCM_ANALOG->PLL_USB1 |= (0x40);
CCM_ANALOG->PLL_USB1 &= ~(0x01 << 0);
while ((CCM_ANALOG->PLL_SYS & (0x80000000)) == 0)
{
}
CCM_ANALOG->PLL_AUDIO = (0x1000);
CCM_ANALOG->PLL_VIDEO = (0x1000);
CCM_ANALOG->PLL_ENET = (0x1000);
CCM_ANALOG->PLL_USB2 = (0x00);
-
第三部分: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
时钟。
CCM_ANALOG->PFD_528 |=(0x80U) ;
CCM_ANALOG->PFD_528 |=(0x8000U) ;
CCM_ANALOG->PFD_528 |=(0x80000000U);
CCM_ANALOG->PFD_528 &= ~(0x3FU);
CCM_ANALOG->PFD_528 &= ~(0x3F00U);
CCM_ANALOG->PFD_528 &= ~(0x3F00U);
CCM_ANALOG->PFD_528 &= ~(0x3F00U);
CCM_ANALOG->PFD_528 |= (0x1B << 0);
CCM_ANALOG->PFD_528 |= (0x10 << 8);
CCM_ANALOG->PFD_528 |= (0x18 << 16);
CCM_ANALOG->PFD_528 |= (0x30 << 24);
CCM_ANALOG->PFD_528 &= ~(0x80U) ;
CCM_ANALOG->PFD_528 &= ~(0x8000U) ;
CCM_ANALOG->PFD_528 &= ~(0x800000U) ;
CCM_ANALOG->PFD_528 &= ~(0x80000000U);
CCM_ANALOG->PFD_480 |=(0x80U) ;
CCM_ANALOG->PFD_480 |=(0x8000U) ;
CCM_ANALOG->PFD_480 |=(0x800000U) ;
CCM_ANALOG->PFD_480 |=(0x80000000U);
CCM_ANALOG->PFD_480 &= ~(0x3FU);
CCM_ANALOG->PFD_480 &= ~(0x3F00U);
CCM_ANALOG->PFD_480 &= ~(0x3F00U);
CCM_ANALOG->PFD_480 &= ~(0x3F00U);
CCM_ANALOG->PFD_480 |= (0xC << 0);
CCM_ANALOG->PFD_480 |= (0x10 << 8);
CCM_ANALOG->PFD_480 |= (0x11 << 16);
CCM_ANALOG->PFD_480 |= (0x13 << 24);
CCM_ANALOG->PFD_480 &= ~(0x80U) ;
CCM_ANALOG->PFD_480 &= ~(0x8000U) ;
CCM_ANALOG->PFD_480 &= ~(0x800000U) ;
CCM_ANALOG->PFD_480 &= ~(0x80000000U);
- 第四部分:外设时钟设置
对应上面七、AHB、IPG和PERCLK根时钟设置。设置串口的根时钟。这里只设置了串口,其他外设的时钟频率呢?在 BOOT ROM 中已经初始化了部分外设的时钟,为减少难度,那些没有使用到的外设或者对频率没有严格要求的外设我们暂时保持默认的时钟频率。这里设置 UART 根时钟的目的是以 UART 为例讲解如何设置外设时钟。UART 时钟源产生路径如下所示。
从图中可以看出,从PLL时钟到UART时钟共用用到了两个时钟选择寄存器(标号①和③),两个时钟分频寄存器(标号②和标号④)。 我们最终目的是将PLL3时钟作为UART根时钟(UART_CLK_ROOT)的根时钟。按照标号顺序讲解如下:
-
标号①选择 PLL3 时钟还是 CCM_PLL3_BYP。我们选择 PLL3 输出时钟,寄存器 CCSR[PLL3_SW_CLK_SEL] = 0, 则表示选择 PLL3 时钟。默认情况下是这样设置的。所以我们代码中并没有设置该寄存器。
-
标号②设置时钟分频,根据之前的设置,PLL3 的输出频率为 480MHz ,这里的时钟分频是固定的 6 分频, 经过分频后的时钟为 480MHz / 6 = 80MHz。
-
标号③ 再次选择时钟源。一个是 PLL3 分频得到的 80MHz 时钟,另外一个是 OSC 时钟即 24MHz 的系统参考时钟。 设置 CSCDR1[UART_CLK_SEL] = 0,选择第一个(80MHz)时钟。
-
标号④再次进行时钟分频。这是一个 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");
}
}
int main()
{
int i = 0;
system_clock_init();
rgb_led_init();
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)
{
if ((CCM->CCSR & (0x01 << 2)) == 0)
{
CCM->CCSR &= ~(0x01 << 8);
CCM->CCSR |= (0x01 << 2);
}
CCM_ANALOG->PLL_ARM |= (0x42 << 0);
CCM->CCSR &= ~(0x01 << 2);
CCM->CACRR &= ~(0x07 << 0);
...
}
• 由 Leung 写于 2023 年 3 月 28 日
• 参考:11. 时钟控制模块(CCM)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)