IMX6ULL 时钟系统配置与定时器 (EPIT/GPT)

这篇笔记就结合实际代码,讲解时钟配置、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,其他的按需配置就行:

  1. PLL1(ARM PLL):专门给 Cortex-A7 内核提供工作主频,我们配置后最终给内核的是 528MHz。
  2. PLL2(System PLL):固定输出 528MHz,主要给总线、外设提供时钟,通过 4 路 PFD 输出不同频率。
  3. 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 就行。

四、开发注意事项

  1. 配置 ARM PLL 必须先切临时时钟:直接在主时钟下修改 PLL 配置,大概率会导致内核跑飞,板子直接死机,这个是最基础也是最关键的点。
  2. 中断标志位是写 1 清零:不止 EPIT,IMX6ULL 的绝大多数外设中断标志位,都是写 1 清零,新手很容易写成写 0,导致中断一直触发,程序卡死。
  3. 分频系数要和时钟频率对应:我们这里的分频系数是基于 66MHz 的 PERCLK/IPG 时钟,如果后面修改了系统时钟,分频系数必须跟着改,不然定时和延时的时间就不准。
  4. 32 位计数器必须处理溢出:不管是向上计数还是向下计数,32 位计数器总会溢出,延时函数里必须做溢出处理,不然偶尔会出现延时异常的问题,很难排查。
  5. 外设时钟门控必须打开:如果外设配置完完全不工作,先检查对应的 CCGR 寄存器,时钟门控有没有打开,很多时候问题都出在这里。

五、最后总结

时钟是嵌入式系统的心脏,所有外设的运行都离不开时钟信号。做 IMX6ULL 裸机开发,第一步就是把时钟树搞明白,把系统时钟配置好,后面的 GPIO、串口、定时器、SPI 这些外设,才能正常工作。

EPIT 和 GPT 是最基础的两个定时器,EPIT 适合做固定周期的中断任务,GPT 适合做精准的硬件延时,这两个工具掌握了,后面做按键消抖、传感器数据采集、PWM 输出、串口超时判断这些功能,都能得心应手。

相关推荐
会编程的小孩2 小时前
stm32f103c8t6工程模板 配置成stm32f407zgt6工程模板
stm32·单片机·嵌入式硬件
乌恩大侠2 小时前
【WNC】R1220 参数
fpga开发
somi72 小时前
ARM-08-I.MX6U UART 串口
arm开发·单片机·嵌入式硬件·自用
mcupro2 小时前
TQTT_KU5P开发板教程---在Windows下XCKU5P+AD9361测试
嵌入式硬件·fpga开发·模块测试
Zevalin爱灰灰3 小时前
零基础入门学用物联网(ESP8266) 第二部分 MQTT基础篇(五)
单片机·物联网·mqtt·嵌入式·esp8266
欢乐熊嵌入式编程3 小时前
做一个智能温湿度监控系统(含显示与数据上传)
单片机·温湿度·嵌入式学习·智能温湿度监控系统
辰哥单片机设计3 小时前
STM32智能家用垃圾桶(升级版)
stm32·单片机·嵌入式硬件
qq_150841993 小时前
浅析光模块固件之PC-MCU-Driver构架下的二级I2C从机的透传编程(再续)
单片机·嵌入式硬件
惶了个恐3 小时前
嵌入式硬件第六弹——ARM(3)
arm开发·stm32·嵌入式硬件·arm