这篇笔记就结合实际代码,讲解时钟配置、EPIT 和 GPT 定时器的使用。
一、先搞懂时钟系统:
嵌入式里的时钟,就像乐队的节拍器,所有操作都要跟着这个节拍走。IMX6ULL 的时钟源头是一颗 24MHz 的外部晶振 (OSC),但单靠 24MHz,根本带不动几百 MHz 的内核和各类外设,所以就需要一系列的时钟处理单元,把 24MHz 转换成各路外设需要的频率。
1.1 核心概念
- 时钟源:就是 24MHz 无源晶振,整个芯片所有时钟的源头,频率稳定、误差极小。
- PLL (锁相环) :核心作用是倍频,把 24MHz 的低频信号,放大到几百 MHz 的高频,比如给 ARM 内核用的 PLL1,能把 24MHz 倍频到 1056MHz。
- Prescaler (分频器) :和 PLL 相反,作用是分频,把高频率降下来,给低速外设用,比如 66MHz 的时钟,66 分频后就得到 1MHz,方便我们做微秒级的定时。
- PFD (相位分数分频器):比普通分频器更灵活,能对 PLL 输出的时钟做精细的频率调整,输出频率可升可降,用来给不同外设生成精准的时钟。
1.2 IMX6ULL 的核心时钟树
IMX6ULL 的时钟树看着复杂,其实我们裸机开发常用的就 3 个核心 PLL,其他的按需配置就行:
- PLL1(ARM PLL):专门给 Cortex-A7 内核提供工作主频,我们配置后最终给内核的是 528MHz。
- PLL2(System PLL):固定输出 528MHz,主要给总线、外设提供时钟,通过 4 路 PFD 输出不同频率。
- PLL3(USB1 PLL):固定输出 480MHz,主要给 USB、串口等外设提供时钟,同样有 4 路 PFD。
最终我们要得到的几个核心系统时钟:
ARM_CLK_ROOT:528MHz,给内核用AHB_CLK_ROOT:132MHz,给高速总线用IPG_CLK_ROOT:66MHz,给外设总线用PERCLK_CLK_ROOT:66MHz,给 EPIT、GPT 定时器用
1.3 时钟初始化代码拆解
时钟初始化的核心代码在clock.c里,每一步都有明确的目的,最容易踩的坑就是直接改 PLL 导致内核跑飞,所以配置顺序非常关键。
第一步:先把内核切到临时时钟,避免跑飞
c
运行
// 切换支路,step_clk选择24MHz晶振
CCM->CCSR &= ~(1 << 8);
// 把PLL1的输出改成step_clk,让ARM暂时工作在24MHz
CCM->CCSR |= (1 << 2);
// 配置PLL1之后的二分频
CCM->CACRR &= ~(7 << 0);
CCM->CACRR |= (1 << 0);
这一步是重中之重!如果直接在 528MHz 的主时钟下修改 PLL 配置,很容易导致内核时序错乱,直接死机。所以先把内核临时切换到 24MHz 的晶振时钟,等 PLL 配置完成后,再切回主时钟。
第二步:配置 ARM 内核主 PLL (PLL1)
c
运行
uint32_t t = 0;
t = CCM_ANALOG->PLL_ARM;
t &= ~(3 << 14);
t |= (1 << 13); // 使能PLL输出
t &= ~(0x7F << 0);
t |= (88 << 0); // 倍频因子设为88
CCM_ANALOG->PLL_ARM = t;
这里的计算很简单:24MHz 晶振 * 88 倍频 = 2112MHz,再经过固定的二分频,得到 1056MHz,再经过前面CACRR配置的二分频,最终给内核的就是 528MHz,完全符合芯片的额定主频。
第三步:切回主时钟,配置外设 PLL 的 PFD
c
运行
// 切回pll1_main_clk,内核正式运行在528MHz
CCM->CCSR &= ~(1 << 2);
// 配置PLL2(528M)的4路PFD
t = CCM_ANALOG->PFD_528;
t &= ~((0x3F << 0) | (0x3F << 8) | (0x3F << 16) | (0x3F << 24));
t |= ((27 << 0) | (16 << 8) | (24 << 16) | (32 << 24));
CCM_ANALOG->PFD_528 = t;
// 配置PLL3(480M)的4路PFD
t = CCM_ANALOG->PFD_480;
t &= ~((0x3F << 0) | (0x3F << 8) | (0x3F << 16) | (0x3F << 24));
t |= ((12 << 0) | (16 << 8) | (17 << 16) | (19 << 24));
CCM_ANALOG->PFD_480 = t;
PFD 的计算公式是:输出频率 = PLL输出频率 * 18 / FRAC值。比如 528MHz 的 PLL2,FRAC 设为 27,输出就是 528*18/27=352MHz,完全符合我们的需求。
第四步:配置总线和外设时钟
c
运行
// 配置AHB_CLK_ROOT=132MHz
t = CCM->CBCMR;
t &= ~(3 << 18);
t |= (1 << 18);
CCM->CBCMR = t;
t = CCM->CBCDR;
t &= ~(1 << 25);
t &= ~(7 << 10);
t |= (2 << 10);
// 配置IPG_CLK_ROOT=66MHz
t &= ~(3 << 8);
t |= (1 << 8);
CCM->CBCDR = t;
// 配置PERCLK_CLK_ROOT=66MHz
t = CCM->CSCMR1;
t &= ~(1 << 6);
t &= ~(0x3F << 0);
CCM->CSCMR1 = t;
这一步就是把 PLL 输出的高频时钟,经过分频器降到外设需要的频率,最终给定时器用的 PERCLK 时钟就是 66MHz,后面定时器的分频计算都要基于这个频率。
第五步:打开所有外设时钟门控
c
运行
void enable_clock(void)
{
CCM->CCGR0 = 0xFFFFFFFF;
CCM->CCGR1 = 0xFFFFFFFF;
CCM->CCGR2 = 0xFFFFFFFF;
CCM->CCGR3 = 0xFFFFFFFF;
CCM->CCGR4 = 0xFFFFFFFF;
CCM->CCGR5 = 0xFFFFFFFF;
CCM->CCGR6 = 0xFFFFFFFF;
}
IMX6ULL 的每个外设都有对应的时钟门控,默认很多是关闭的,不打开的话外设根本无法工作。裸机开发阶段不用考虑功耗,直接把所有 CCGR 寄存器都设为 0xFFFFFFFF,打开全部外设的时钟,省得后面调试外设的时候,找半天问题发现是时钟没开。
二、EPIT 定时器:周期中断
EPIT 全称是 Enhanced Periodic Interrupt Timer,增强型周期中断定时器,核心功能就是固定周期触发中断,非常适合做周期性的任务,比如 1 秒翻转一次 LED、定时采集传感器数据、周期性触发蜂鸣器等。
2.1 EPIT 核心特性
- 32 位向下计数器,支持自动重载
- 可配置分频系数,灵活调整计数频率
- 支持比较中断,计数器减到 0 时触发中断
- 时钟源可选 PERCLK_CLK_ROOT,我们配置的是 66MHz
2.2 EPIT 初始化代码拆解
我们要实现的功能是 1 秒触发一次中断,中断里翻转蜂鸣器状态,核心代码在epit1.c里。
第一步:配置 EPIT 的核心寄存器
c
运行
volatile unsigned int t = 0;
t = EPIT1->CR;
t &= ~(3 <<24);
t |= (1 << 24); // 时钟源选择PERCLK_CLK_ROOT(66MHz)
t |= (1 << 17); // 使能自动重载模式
t &= ~(0xFFF << 4);
t |= (65 << 4); // 分频系数设为66,66MHz/66=1MHz,1us计数一次
t |= (1 << 3); // 计数器减到0时,重载LR寄存器的值
t |= (1 << 2); // 使能比较中断
t |= (1 << 1); // 软件复位计数器
EPIT1->CR = t;
这里最关键的是分频计算:66MHz 的时钟,66 分频后得到 1MHz 的计数频率,也就是计数器每 1 微秒减 1。这样我们要定时 1 秒,只需要给重载寄存器设 110001000 就行。
第二步:设置重载值和比较值
c
运行
EPIT1->LR = 1 * 1000 * 1000; // 重载值1M,1MHz频率下刚好1秒
EPIT1->CMPR = 0; // 计数器减到0时触发中断
LR 是重载寄存器,计数器减到 0 后,会自动把 LR 的值加载到计数器里,实现周期循环。CMPR 是比较寄存器,计数器的值和 CMPR 相等时,就会触发中断。
第三步:注册中断并使能
c
运行
// 注册中断服务函数
system_interrupt_register(EPIT1_IRQn, epit1_irq_handler);
// 设置中断优先级
GIC_SetPriority(EPIT1_IRQn, 0);
// 使能GIC对应中断线
GIC_EnableIRQ(EPIT1_IRQn);
// 启动EPIT定时器
EPIT1->CR |= (1 << 0);
IMX6ULL 的中断都要通过 GIC 中断控制器管理,所以必须注册中断服务函数,设置优先级,使能中断线,最后启动定时器,整个配置就完成了。
第四步:中断服务函数
c
运行
void epit1_irq_handler(void)
{
if ((EPIT1->SR & (1 << 0)) != 0)
{
beep_nor(); // 翻转蜂鸣器状态
EPIT1->SR |= (1 << 0) ; // 写1清除中断标志位
}
}
这里有个坑:IMX6ULL 的外设中断标志位,是写 1 清零,不是写 0。如果中断触发后不清除标志位,CPU 会一直进入中断,程序就卡死在中断里了。
三、GPT 定时器:做精准延时
GPT 全称是 General Purpose Timer,通用目的定时器,比 EPIT 功能更丰富,支持输入捕获、输出比较,也能做自由运行模式的延时。我们裸机开发里,最常用的就是用 GPT 做精准的 us、ms 级延时。
3.1 GPT 延时的核心原理
我们用 GPT 的自由运行模式,32 位向上计数器,配置成分频后 1MHz 的计数频率,也就是 1us 计数加 1。要延时多少 us,就循环读取计数器的值,计算经过的计数次数,达到目标值就退出,完全靠硬件计数器保证延时精度。
3.2 GPT 初始化代码拆解
核心代码在gpt1.c里,我们先初始化 GPT,再实现延时函数。
第一步:先复位 GPT
c
运行
void reset(void)
{
GPT1->CR |= (1 << 15); // 写1触发软件复位
while((GPT1->CR & (1 << 15)) != 0); // 等待复位完成
}
初始化外设的好习惯:先复位,把寄存器恢复到默认状态,避免之前的配置影响本次使用。
第二步:配置 GPT 核心参数
c
运行
unsigned int t = 0;
t = GPT1->CR;
t &= ~(7 << 6);
t |= (1 << 6); // 时钟源选择ipg_clk(66MHz)
t |= (1 << 9); // 计数器在debug模式下停止
t |= (1 << 1); // 计数器清零
GPT1->CR = t;
// 分频系数设为66,66MHz/66=1MHz,1us计数一次
GPT1->PR &= ~(0xFFF << 0);
GPT1->PR |= (65 << 0);
// 启动GPT定时器
GPT1->CR |= (1 << 0);
和 EPIT 一样,分频后得到 1MHz 的计数频率,1us 计数加 1,这样延时的计算就非常简单。
第三步:微秒级延时函数
c
运行
void delay_us(unsigned int us)
{
unsigned int count = 0;
unsigned int old_count = 0;
unsigned int new_count = 0;
old_count = GPT1->CNT;
while(1)
{
new_count = GPT1->CNT;
if(new_count != old_count)
{
// 处理计数器溢出的情况
if(new_count > old_count)
{
count = new_count - old_count;
}
else if(new_count < old_count)
{
count = 0xFFFFFFFF - old_count + new_count;
}
// 达到延时时间,退出循环
if(count > us)
{
return;
}
}
}
}
这里的关键是处理计数器溢出:GPT 是 32 位向上计数器,最大值是 0xFFFFFFFF,计数到顶后会溢出从 0 重新开始。如果不处理溢出,当延时过程中计数器溢出时,延时就会完全错误。
第四步:毫秒级延时函数
c
运行
void delay_ms(unsigned int ms)
{
while(ms--)
{
delay_us(1000);
}
}
这个就很简单了,1 毫秒等于 1000 微秒,循环调用 delay_us 就行。
四、开发注意事项
- 配置 ARM PLL 必须先切临时时钟:直接在主时钟下修改 PLL 配置,大概率会导致内核跑飞,板子直接死机,这个是最基础也是最关键的点。
- 中断标志位是写 1 清零:不止 EPIT,IMX6ULL 的绝大多数外设中断标志位,都是写 1 清零,新手很容易写成写 0,导致中断一直触发,程序卡死。
- 分频系数要和时钟频率对应:我们这里的分频系数是基于 66MHz 的 PERCLK/IPG 时钟,如果后面修改了系统时钟,分频系数必须跟着改,不然定时和延时的时间就不准。
- 32 位计数器必须处理溢出:不管是向上计数还是向下计数,32 位计数器总会溢出,延时函数里必须做溢出处理,不然偶尔会出现延时异常的问题,很难排查。
- 外设时钟门控必须打开:如果外设配置完完全不工作,先检查对应的 CCGR 寄存器,时钟门控有没有打开,很多时候问题都出在这里。
五、最后总结
时钟是嵌入式系统的心脏,所有外设的运行都离不开时钟信号。做 IMX6ULL 裸机开发,第一步就是把时钟树搞明白,把系统时钟配置好,后面的 GPIO、串口、定时器、SPI 这些外设,才能正常工作。
EPIT 和 GPT 是最基础的两个定时器,EPIT 适合做固定周期的中断任务,GPT 适合做精准的硬件延时,这两个工具掌握了,后面做按键消抖、传感器数据采集、PWM 输出、串口超时判断这些功能,都能得心应手。