本文章结合了正点原子的 i.mx6u嵌入式Linux开发指南和笔者的理解。
本文章应配合《 【嵌入式Linux】i.MX6ULL 时钟树——理论分析》使用。
0 基础时钟初始化概述
通过文章《【嵌入式Linux】i.MX6ULL 时钟树——理论分析》,我们得出下面的基础时钟初始化流程:
- PLL锁相环的配置(这里配置 3 个最基础的)
- ARM_PLL——编号PLL1,用于ARM内核
- 528_PLL——编号PLL2,可生成 4 路 PFD 用作内部系统总线、DDR等时钟源
- USB1_PLL——编号PLL3,可生成 4 路 PFD 用于多种外设
- 配置 PLL2 的 4 个 PFD 和 PLL3 的 4 个 PFD
- 配置 IPG 时钟、PERCLK时钟
1 PLL1 初始化
1.1 pll1_main_clk 修改前先把 pll1_sw_clk 切换到备用时钟源
// 首先设置ARM内核时钟为528MHz
// ① 判断当前ARM内核时钟(pll1_sw_clk)的来源
if((((CCM->CCSR)>>2)&0x1)==0){ // 如果pll1_sw_clk的来源是pll1_main_clk(PLL1)
// 此时为了修改pll1_main_clk(PLL1),先要把系统时钟接到其他地方,这里接到step_clk
CCM->CCSR &= ~(1<<8);// 首先配置step_clk的时钟源为24M晶振
CCM->CCSR |= (1<<2);// 然后将pll1_sw_clk接到step_clk
}
解释:
这段代码首先判断当前ARM内核时钟(pll1_sw_clk
)的来源。
CCM->CCSR
寄存器包含了系统时钟的配置信息。(CCM->CCSR)>>2
将寄存器右移两位,提取出pll1_sw_clk
的时钟源信息。&0x1
与 1 进行按位与运算,判断pll1_sw_clk
的来源是否为pll1_main_clk
。
如果pll1_sw_clk
的来源是pll1_main_clk
,由于我们要修改的就是pll1_main_clk
,所以需要将系统时钟切换到其他地方,这里切换到step_clk
。
CCM->CCSR &= ~(1<<8)
将step_clk
的时钟源设置为24M晶振。CCM->CCSR |= (1<<2)
将pll1_sw_clk
的时钟源切换到step_clk
。
1.2 开始配置PLL1
// ② 为了设置ARM内核时钟为528MHz,由于一般设置2分频,所以pll1_main_clk设置为1056MHz
// 设置寄存器Analog ARM PLL control Register(CCM_ANALOG_PLL_ARMn)
// bit 13: 使能时钟输出
// bit 6:0: 此处设置为88(Fout = Fin * div_select / 2.0)
// 也就是1056=24*div_select/2.0 -> div_select=88
CCM_ANALOG->PLL_ARM = (1<<13) | ((88<<0)&0x7F);
解释:
这段代码设置PLL1的输出频率为1056MHz。
CCM_ANALOG->PLL_ARM
寄存器控制PLL1的配置。(1<<13)
设置PLL_ARM
的使能位,使能时钟输出。((88<<0)&0x7F)
设置分频值,div_select
为 88,根据公式Fout = Fin * div_select / 2.0
,div_select
为 88 可以得到得到PLL1输出频率为 1056MHz。
1.3 配置完成后,将 pll1_sw_clk 时钟源切换回 pll1_main_clk
// ③ 将pll1_sw_clk时钟源切换回pll1_main_clk
CCM->CCSR &= ~(1<<2);
解释:
这段代码将pll1_sw_clk
的时钟源切换回pll1_main_clk
。
1.4 设置 PLL1 的分频系数
// ④ 设置分频寄存器CCM Arm Clock Root Register (CCM_CACRR)
CCM->CACRR = 1; // 设置为1表示2分频,将1056MHz的时钟源分频为528MHz
解释:
这段代码设置分频寄存器,将PLL1的输出频率分频为528MHz,最终得到ARM内核时钟。
CCM->CACRR
寄存器控制ARM内核时钟的分频。CCM->CACRR = 1
设置分频值为1,将1056MHz的时钟源分频为528MHz。
1.5 总结
这段代码通过一系列操作,将ARM内核时钟设置为528MHz。首先判断当前ARM内核时钟来源,并切换到其他时钟源,然后设置PLL1的输出频率为1056MHz,最后将ARM内核时钟源切换回PLL1并进行分频,最终得到528MHz的ARM内核时钟。
2 配置 PLL2 的 4 个 PFD 和 PLL3 的 4 个 PFD
- 下面代码中使用了变量reg 来读取寄存器,进行操作之后再写入回寄存器。
- 如果需要多次读取和修改同一个寄存器,使用变量可以减少对寄存器的访问次数,提高代码执行效率。
2.1 PLL2 的 4 个 PFD配置
//下面设置各个PFD
//① 首先设置PLL(systemPLL)的4个PFD
reg = CCM_ANALOG->PFD_528;
reg &= ~(0x3f3f3f3f);//清除原来的设置(每个PFD设置为8位,将分频因子6位清零)
reg |= 32<<24; //分频因子设为32,PLL2_PFD3=528*18/32=297Mhz
reg |= 24<<16;
reg |= 16<<8;
reg |= 27<<0;
CCM_ANALOG->PFD_528=reg;
解释:
这段代码设置了名为systemPLL
的PLL的4个PFD(Phase Frequency Detector,相位频率检测器)。
CCM_ANALOG->PFD_528
寄存器控制systemPLL
的PFD配置。reg &= ~(0x3f3f3f3f)
清除寄存器中PFD分频因子相关的位。每个PFD占8位,其中6位用来设置分频因子。reg |= 32<<24
设置第一个PFD的分频因子为32。reg |= 24<<16
设置第二个PFD的分频因子为24。reg |= 16<<8
设置第三个PFD的分频因子为16。reg |= 27<<0
设置第四个PFD的分频因子为27。
例如,第一个PFD的输出频率为528 * 18 / 32 = 297MHz
。
//② 设置PLL3(USB1)的4个PFD
reg = 0;
reg = CCM_ANALOG->PFD_480;
reg &= ~(0x3f3f3f3f);//清除原来的设置(每个PFD设置有8位,将分频因子6位清零)
reg |= 19<<24;
reg |= 17<<16;
reg |= 16<<8;
reg |= 12<<0;
CCM_ANALOG->PFD_480 = reg;
解释:
这段代码设置了名为USB1
的PLL的4个PFD。
CCM_ANALOG->PFD_480
寄存器控制USB1
的PFD配置。reg &= ~(0x3f3f3f3f)
清除寄存器中PFD分频因子相关的位。reg |= 19<<24
设置第一个PFD的分频因子为19。reg |= 17<<16
设置第二个PFD的分频因子为17。reg |= 16<<8
设置第三个PFD的分频因子为16。reg |= 12<<0
设置第四个PFD的分频因子为12。
2.2 PLL3 的 4 个 PFD配置
//③ 设置AHB时钟(min:6MHz, max:132MHz)
reg = 0;
reg = CCM->CBCMR;
reg &= ~(3<<18); //清除原来的设置
reg |= (1<<18); //时钟源选择为PLL2_PFD2
CCM->CBCMR = reg;
reg = CCM->CBCDR;
reg &= ~(1<<25); //清除原来的设置
reg &= ~(7<<10); //清除原来的设置(AHB_PODF)
reg |= (2<<10); //AHB_PODF设置为2,也就是3分频,AHB_CLK_ROOT=132MHz
CCM->CBCDR = reg;
解释:
这段代码设置AHB时钟的频率。
-
CCM->CBCMR
寄存器控制AHB时钟的来源。 -
reg &= ~(3<<18)
清除寄存器中AHB时钟来源相关的位。 -
reg |= (1<<18)
设置AHB时钟来源为PLL2_PFD2
。 -
CCM->CBCDR
寄存器控制AHB时钟的分频。 -
reg &= ~(1<<25)
清除寄存器中AHB时钟使能相关的位。 -
reg &= ~(7<<10)
清除寄存器中AHB时钟分频因子相关的位。 -
reg |= (2<<10)
设置AHB时钟分频因子为2,也就是3分频。AHB时钟的根频率为PLL2_PFD2
的输出频率,即132MHz
。
3 配置 IPG 时钟、PERCLK时钟
- 查看时钟树:
- IPG 时钟、PERCLK 时钟的来源是 AHB_CLK_ROOT,所以要先配置AHB时钟
3.1 配置AHB时钟
3.1.1 纠正正点原子的错误
错误:
- 在正点原子的讲解视频中,左老师在配置AHB时钟的时候直接对同一个寄存器进行多次操作
- 在设置分频系数的时候,先清除原来的分频系数设置,此时如果是直接对寄存器操作会导致分频过小,超过最大时钟频率,导致系统卡死
原因: - 寄存器操作并非原子操作,这意味着对同一个寄存器的多次操作,并非一次性完成,而是分步骤进行的。
- 原子操作(Atomic operation)是指一个不可分割的操作,要么全部执行成功,要么全部执行失败,不会出现中间状态。
- 某些寄存器的配置可能存在依赖关系,例如,某个寄存器的某个位可能需要在另一个寄存器的某个位被设置之后才能生效。
举例: - 如果您先设置使能位,再设置工作模式位,那么在设置工作模式位之前,该模块可能就已经被使能了,而此时工作模式位还没有生效,可能会导致该模块工作异常。
如果您先设置工作模式位,再设置使能位,那么在设置使能位之前,该模块可能还没有被使能,而此时工作模式位已经生效,可能会导致该模块无法正常工作。
3.1.2 寄存器操作的正确方式
- 正确方式应该是先用变量存储要修改的寄存器的值,然后对该变量进行完整步骤的操作后,再写入寄存器。这样可以使所有设置同时生效
3.1.3 正确代码
//③ 设置AHB时钟(min:6MHz, max:132MHz)
reg = 0;
reg = CCM->CBCMR;
reg &= ~(3<<18); //清除原来的设置
reg |= (1<<18); //时钟源选择为PLL2_PFD2
CCM->CBCMR = reg;
解释:
这段代码设置了AHB时钟的时钟源。
CCM->CBCMR
寄存器控制AHB时钟的来源。reg &= ~(3<<18)
清除寄存器中AHB时钟来源相关的位。reg |= (1<<18)
设置AHB时钟来源为PLL2_PFD2
。
reg = CCM->CBCDR;
reg &= ~(1<<25); //清除原来的设置
reg &= ~(7<<10); //清除原来的设置(AHB_PODF)
reg |= (2<<10); //AHB_PODF设置为2,也就是3分频,AHB_CLK_ROOT=132MHz
CCM->CBCDR = reg;
解释:
这段代码设置了AHB时钟的分频。
CCM->CBCDR
寄存器控制AHB时钟的分频。reg &= ~(1<<25)
清除寄存器中AHB时钟使能相关的位。reg &= ~(7<<10)
清除寄存器中AHB时钟分频因子相关的位。reg |= (2<<10)
设置AHB时钟分频因子为2,也就是3分频。经过3分频后,AHB时钟频率为132MHz
。
3.2 配置 IPG 时钟
//④ 设置IPG时钟(min:6MHz, max:66MHz)
reg = 0;
reg = CCM->CBCDR;
reg &= ~(3<<8); //清除IPG_PODF
reg |= (1<<8); //设置为2分频,IPG_CLK_ROOT=66MHz
CCM->CBCDR = reg;
解释:
这段代码设置了IPG时钟的频率。
CCM->CBCDR
寄存器控制IPG时钟的分频。reg &= ~(3<<8)
清除寄存器中IPG时钟分频因子相关的位。reg |= (1<<8)
设置IPG时钟分频因子为1,也就是2分频,经过2分频后,IPG时钟频率为66MHz。
3.3 配置 PERCLK 时钟
//⑤ 设置PERCLK时钟
reg = 0;
reg = CCM->CSCMR1;
reg &= ~(1<<6); //设置时钟源为IPG
reg &= ~(7<<0); //PERCLK_PODF设置1分频
CCM->CSCMR1 = reg;
解释:
这段代码设置了PERCLK时钟的频率和时钟源。
CCM->CSCMR1
寄存器控制PERCLK时钟的配置。reg &= ~(1<<6)
设置PERCLK时钟的来源为IPG时钟。reg &= ~(7<<0)
清除寄存器中PERCLK时钟分频因子相关的位。reg &= ~(7<<0)
设置PERCLK时钟分频因子为0,也就是1分频。PERCLK时钟的频率与IPG时钟频率相同,即66MHz。
总结:
这段代码设置了IPG时钟和PERCLK时钟的频率和时钟源。IPG时钟的频率被设置为66MHz,PERCLK时钟的频率与IPG时钟频率相同,并且其时钟源被设置为IPG时钟。
4 完整代码
/**
* @brief 初始化arm内核时钟、以及PLL2和PLL3的所有PFD(相位分数分频器 (Phase Fractional Divider, PFD))
* @param None
* @retval None
*/
void imx6u_clk_Init(void)
{
unsigned int reg = 0;
//首先设置ARM内核时钟为528MHz
//① 判断当前ARM内核时钟(pll1_sw_clk)的来源
if((((CCM->CCSR)>>2)&0x1)==0){ //如果pll1_sw_clk的来源是pll1_main_clk(PLL1)
//此时为了修改pll1_main_clk(PLL1),先要把系统时钟接到其他地方,这里接到step_clk
CCM->CCSR &= ~(1<<8);//首先配置step_clk的时钟源为24M晶振
CCM->CCSR |= (1<<2);//然后将pll1_sw_clk接到step_clk
}
//② 为了设置ARM内核时钟为528MHz,由于一般设置2分频,所以pll1_main_clk设置为1056MHz
// 设置寄存器Analog ARM PLL control Register(CCM_ANALOG_PLL_ARMn)
// bit 13: 使能时钟输出
// bit 6:0: 此处设置为88(Fout = Fin * div_select / 2.0)
// 也就是1056=24*div_select/2.0 -> div_select=88
CCM_ANALOG->PLL_ARM = (1<<13) | ((88<<0)&0x7F);
CCM->CCSR &= ~(1<<2);//将pll1_sw_clk时钟源切换回pll1_main_clk
CCM->CACRR = 1;//这个寄存器也就是分频寄存器CCM Arm Clock Root Register (CCM_CACRR)
//下面设置各个PFD
//① 首先设置PLL(systemPLL)的4个PFD
reg = CCM_ANALOG->PFD_528;
reg &= ~(0x3f3f3f3f);//清除原来的设置(每个PFD设置为8位,将分频因子6位清零)
reg |= 32<<24; //分频因子设为32,PLL2_PFD3=528*18/32=297Mhz
reg |= 24<<16;
reg |= 16<<8;
reg |= 27<<0;
CCM_ANALOG->PFD_528=reg;
//② 设置PLL3(USB1)的4个PFD
reg = 0;
reg = CCM_ANALOG->PFD_480;
reg &= ~(0x3f3f3f3f);//清除原来的设置(每个PFD设置有8位,将分频因子6位清零)
reg |= 19<<24;
reg |= 17<<16;
reg |= 16<<8;
reg |= 12<<0;
CCM_ANALOG->PFD_480 = reg;
//③ 设置AHB时钟(min:6MHz, max:132MHz)
reg = 0;
reg = CCM->CBCMR;
reg &= ~(3<<18); //清除原来的设置
reg |= (1<<18); //时钟源选择为PLL2_PFD2
CCM->CBCMR = reg;
reg = CCM->CBCDR;
reg &= ~(1<<25); //清除原来的设置
reg &= ~(7<<10); //清除原来的设置(AHB_PODF)
reg |= (2<<10); //AHB_PODF设置为2,也就是3分频,AHB_CLK_ROOT=132MHz
CCM->CBCDR = reg;
//④ 设置IPG时钟(min:6MHz, max:66MHz)
reg = 0;
reg = CCM->CBCDR;
reg &= ~(3<<8); //清除IPG_PODF
reg |= (1<<8); //设置为2分频,IPG_CLK_ROOT=66MHz
CCM->CBCDR = reg;
//⑤ 设置PERCLK时钟
reg = 0;
reg = CCM->CSCMR1;
reg &= ~(1<<6); //设置时钟源为IPG
reg &= ~(7<<0); //PERCLK_PODF设置1分频
CCM->CSCMR1 = reg;
}