day51 时钟系统与定时器(EPIT/GPT)
本日内容聚焦于嵌入式系统的核心------时钟系统 的硬件原理、寄存器配置,以及基于此构建的两种关键外设:增强型周期中断定时器 (EPIT) 和 通用目的定时器 (GPT)。我们将从基础概念入手,深入剖析IMX6ULL芯片的时钟树架构,并通过代码实践掌握如何精准配置主频、外设时钟,并利用定时器实现精确延时和周期性中断。
一、 核心硬件概念:时钟系统的三大基石
理解时钟系统是掌握所有外设工作的前提。它如同整个SOC的"心跳"和"节拍器",为所有模块提供同步工作的基准信号。其核心由以下三个硬件组件构成:
1. 时钟源 (Clock Source)
- 定义: 产生稳定、周期性振荡信号的电路或组件。
- 作用: 为数字电路中的各种操作提供同步时序基准。
- IMX6ULL 实例: 外部24MHz晶体振荡器 (OSC)。其工作原理是将石英晶体切割成音叉形状,施加电压后产生极其稳定的机械振动,进而转化为电信号。
2. 锁相环 (PLL - Phase Locked Loop)
- 定义: 一种频率合成电路,用于对输入时钟信号进行倍频。
- 作用: 将低频的时钟源(如24MHz)倍频到高频(如1056MHz),以满足CPU内核等高性能模块的需求。
- 工作原理 : 输入一个固定频率的脉冲信号,输出一个频率为
输入频率 * 倍频系数
的脉冲信号。例如,输入24MHz,设置倍频系数为44,则输出1056MHz。 - IMX6ULL 实例: 芯片内部集成了7个PLL,其中PLL1 (ARM PLL) 专为Cortex-A7内核设计,可配置输出高达1.3GHz的时钟。
3. 分频器 (Prescaler / PFD)
- 定义: 对输入时钟信号进行分频的电路。
- 作用: 将高频时钟降低到适合特定外设工作的频率。
- 类型 :
- Prescaler (预分频器): 只能进行分频,输出频率 = 输入频率 / 分频系数。
- PFD (Phase Fractional Divider, 相位分数分频器): 功能更强大,既能升频也能降频。输出频率 = 输入频率 * (倍频系数 / 分频系数)。在IMX6ULL中,PFD的倍频系数通常是固定的(如528 PLL的PFD倍频系数为18),我们主要配置分频系数。
二、 IMX6ULL 时钟系统架构解析
IMX6ULL的时钟系统是一个复杂的"时钟树",以24MHz外部晶振为根,通过PLL、PFD、Prescaler、MUX(多路选择器)和CG(时钟门控)等组件,为不同的功能模块分配时钟。

这张图展示了以24M外部晶振为时钟源,通过多个锁相环(PLL)及相关分频/倍频模块,为不同功能模块提供时钟的结构:
- PLL 1 (ARM PLL): 为CortexA7内核提供时钟,频率可配置,图中标注为1056M。
- PLL 2 (528 PLL): 固定输出528M时钟,再通过528 PFD0 - 528 PFD3,分别产生352M、594M、400M(396M)、297M的时钟。
- PLL 3 (480 PLL): 固定输出480M时钟,通过480 PFD0 - 480 PFD3,分别产生720M、540M、508.0M、454.7M的时钟。
- 其他PLL: 包括PLL 4 (Audio PLL)、PLL 5 (Video PLL)、PLL 6 (Ethernet PLL)、PLL 7 (USB2 PLL)等,各自为对应的功能模块提供时钟支持。

这张图更详细地展示了时钟分配与控制流程:
- PLL模块: 如上所述,包含PLL1-PLL7。
- 时钟分配与控制: 通过CCM_ANALOG、CCSR、CBCDR、CSCMR1等寄存器配置,结合多路选择器(MUX)、时钟门控(LPCG Gating Option)等,将时钟分配给kernel、ARM-uSDHC、EPIT、I2C、ADC-WDOG等系统和外设单元。
三、 时钟系统寄存器配置
我们的目标是将ARM内核主频从默认的396MHz提升至528MHz,并配置好相关的外设时钟。
1. 配置步骤概览
- 切换时钟源: 在修改PLL1之前,先将内核时钟源临时切换到24MHz OSC,避免高频率下修改导致内核不稳定。
- 配置PLL1: 设置PLL1的倍频系数为88,使其输出1056MHz。
- 配置分频器: 设置CACRR寄存器,使PLL1输出经过二分频,得到528MHz。
- 恢复时钟源: 将内核时钟源切回PLL1,此时内核工作在528MHz。
- 配置PFDs: 配置528 PLL和480 PLL下的PFD分频器,生成所需的外设时钟频率。
- 配置时钟根 (Clock Root): 配置AHB_CLK_ROOT (132MHz)、IPG_CLK_ROOT (66MHz) 和 PERCLK_CLK_ROOT (66MHz)。
2. 关键寄存器说明与代码实现 (clock.c
)
c
#include "clock.h"
#include "MCIMX6Y2.h"
void clock_init(void)
{
// 1. ARM工作频率(主频)配置 - 步骤1: 切换时钟源到OSC
CCM->CCSR &= ~(1 << 8); // 清除 SET_SEL 位 (bit 8),选择 OSC 作为时钟源
CCM->CCSR |= (1 << 2); // 置位 PL1_SW 位 (bit 2),将PL1输出切换到旁路(即OSC)
// 2. 配置PLL1的二分频器 (CACRR)
CCM->CACRR &= ~(7 << 0); // 清除 CACRR[0:2] 位,设置为二分频模式 (值为1)
CCM->CACRR |= (1 << 0); // 置位 CACRR[0] 位,完成二分频配置
// 3. 配置PLL1 (ARM PLL)
unsigned int t;
t = CCM_ANALOG->PLL_ARM; // 读取当前PLL_ARM寄存器值
t &= ~(3 << 14); // 清除 BYPASS_SRC 位 (bit 14-15),确保使用OSC作为输入
t |= (1 << 13); // 置位 ENABLE 位 (bit 13),使能PLL输出
t &= ~(0x7F << 0); // 清除 DIV_SELECT 位 (bit 0-6)
t |= (88 << 0); // 置位 DIV_SELECT 位 (bit 0-6),设置倍频系数为88 (24MHz * 88 / 2 = 1056MHz)
CCM_ANALOG->PLL_ARM = t; // 写回PLL_ARM寄存器
// 4. 恢复时钟源到PLL1
CCM->CCSR &= ~(1 << 2); // 清除 PL1_SW 位 (bit 2),将PL1输出切回正常路径
// 5. 配置528 PLL的PFDs (PFD_528)
t = CCM_ANALOG->PFD_528; // 读取当前PFD_528寄存器值
// 清除所有4个PFD的分频系数位 (bit 0-5, 8-13, 16-21, 24-29)
t &= ~((0x3F << 0) | (0x3F << 8) | (0x3F << 16) | (0x3F << 24));
// 设置分频系数: PFD0=27, PFD1=16, PFD2=24, PFD3=32
t |= ((27 << 0) | (16 << 8) | (24 << 16) | (32 << 24));
CCM_ANALOG->PFD_528 = t; // 写回PFD_528寄存器
// 6. 配置480 PLL的PFDs (PFD_480)
t = CCM_ANALOG->PFD_480; // 读取当前PFD_480寄存器值
// 清除所有4个PFD的分频系数位
t &= ~((0x3F << 0) | (0x3F << 8) | (0x3F << 16) | (0x3F << 24));
// 设置分频系数: PFD0=12, PFD1=16, PFD2=17, PFD3=19
t |= ((12 << 0) | (16 << 8) | (17 << 16) | (19 << 24));
CCM_ANALOG->PFD_480 = t; // 写回PFD_480寄存器
// 7. 配置AHB_CLK_ROOT (目标132MHz)
t = CCM->CBCMR; // 读取CBCMR寄存器值
t &= ~(3 << 18); // 清除 PRE_PERIPH_CLK_SEL 位 (bit 18-19)
t |= (1 << 18); // 置位 PRE_PERIPH_CLK_SEL 位 (bit 18),选择PLL2_PFD2 (396MHz) 作为来源
CCM->CBCMR = t; // 写回CBCMR寄存器
// 8. 配置IPG_CLK_ROOT 和 PERCLK_CLK_ROOT (目标均为66MHz)
t = CCM->CBCDR; // 读取CBCDR寄存器值
t &= ~(1 << 25); // 清除 PERIPH_CLK_SEL 位 (bit 25),选择PRE_PERIPH_CLK作为来源
t &= ~(0x07 << 10); // 清除 AHB_PODF 位 (bit 10-12)
t |= (2 << 10); // 置位 AHB_PODF 位 (bit 10-12),设置为三分频 (396MHz / 3 = 132MHz)
t &= ~(0x03 << 8); // 清除 IPG_PODF 位 (bit 8-9)
t |= (1 << 8); // 置位 IPG_PODF 位 (bit 8-9),设置为二分频 (132MHz / 2 = 66MHz)
CCM->CBCDR = t; // 写回CBCDR寄存器
// 9. 配置PERCLK_CLK_ROOT (目标66MHz)
t = CCM->CSCMR1; // 读取CSCMR1寄存器值
t &= ~(1 << 6); // 清除 PERCLK_CLK_SEL 位 (bit 6),选择IPG_CLK作为来源
t &= ~(0x3F << 0); // 清除 PERCLK_PODF 位 (bit 0-5),设置为一分频 (66MHz / 1 = 66MHz)
CCM->CSCMR1 = t; // 写回CSCMR1寄存器
// 10. 打开所有外设时钟门控
clock_gating_enable();
}
void clock_gating_enable(void)
{
// 使能所有外设的时钟门控,确保它们能正常工作
CCM->CCGR0 = 0xFFFFFFFF;
CCM->CCGR1 = 0xFFFFFFFF;
CCM->CCGR2 = 0xFFFFFFFF;
CCM->CCGR3 = 0xFFFFFFFF;
CCM->CCGR4 = 0xFFFFFFFF;
CCM->CCGR5 = 0xFFFFFFFF;
CCM->CCGR6 = 0xFFFFFFFF;
}
代码讲解:
clock_init()
: 主函数,按顺序执行上述10个步骤。- 位操作 : 使用
&=~
清零特定比特位,使用|=
置位特定比特位,这是配置寄存器的标准方法。 - 时钟切换 : 通过
CCSR
寄存器的SET_SEL
和PL1_SW
位,在修改PLL前将内核切换到安全的24MHz OSC。 - PLL配置 : 通过
CCM_ANALOG->PLL_ARM
寄存器设置倍频系数 (DIV_SELECT
) 和使能位 (ENABLE
)。 - PFD配置 : 通过
CCM_ANALOG->PFD_528
和CCM_ANALOG->PFD_480
寄存器设置各个PFD的分频系数。 - 时钟根配置 : 通过
CBCMR
,CBCDR
,CSCMR1
寄存器选择时钟源并设置分频系数,最终得到132MHz (AHB), 66MHz (IPG), 66MHz (PERCLK)。 clock_gating_enable()
: 打开所有外设的时钟门控,这是让外设能够工作的必要步骤。
理想运行结果: 编译并烧录程序后,开发板上的LED灯会以比之前更快的速度闪烁(因为内核主频从396MHz提升到了528MHz),表明时钟配置成功。
四、 增强型周期中断定时器 (EPIT)
EPIT是一种32位的减计数器,用于产生周期性的中断。它非常适合用于操作系统的时间片调度等需要精确周期性事件的场景。
1. EPIT 工作原理
- 核心 : 一个32位的减计数器 (
CNR
)。 - 时钟源: 可选,通常选择IPG_CLK (66MHz)。
- 预分频器: 可以对时钟源进行分频,以获得所需的计数频率。
- 加载寄存器 (
LR
): 存储计数器的初始值。 - 比较寄存器 (
CMPR
): 存储与计数器比较的值。 - 工作模式 :
- Free Running: 计数器减到0后,从0xFFFF_FFFF继续减。
- Set and Forget : 计数器减到0后,自动从加载寄存器 (
LR
) 重新加载初始值。这是我们常用的工作模式。
- 中断 : 当计数器值 (
CNR
) 等于比较寄存器值 (CMPR
) 时,产生中断。
2. EPIT 寄存器配置与代码实现 (epit.c
)
c
#include "epit.h"
#include "MCIMX6Y2.h"
#include "interrupt.h"
#include "led.h"
// EPIT1 中断服务函数
void epit_irq_handler(void)
{
// 检查中断标志位 (SR[0])
if((EPIT1->SR & (1 << 0)) != 0)
{
led_nor(); // 翻转LED灯状态
EPIT1->SR |= (1 << 0); // 清除中断标志位 (写1清零)
}
}
void epit1_init(void)
{
unsigned int t;
// 1. 配置控制寄存器 (CR)
t = EPIT1->CR; // 读取当前CR寄存器值
t &= ~(3 << 24); // 清除 CLKSRC 位 (bit 24-25),选择IPG_CLK作为时钟源
t |= (1 << 24); // 置位 CLKSRC 位 (bit 24),选择IPG_CLK
t |= (1 << 17); // 置位 OM 位 (bit 17),启用输出模式 (非必需,但有时用于调试)
t &= ~(0xFFF << 4); // 清除 PRESCALER 位 (bit 4-15)
t |= (65 << 4); // 置位 PRESCALER 位 (bit 4-15),设置分频系数为65 (66MHz / 66 = 1MHz)
t |= (1 << 3); // 置位 RLD 位 (bit 3),启用重载模式 (Set and Forget)
t |= (1 << 2); // 置位 OCIE 位 (bit 2),使能输出比较中断
t |= (1 << 1); // 置位 ENMOD 位 (bit 1),选择从加载寄存器开始计数
EPIT1->CR = t; // 写回CR寄存器
// 2. 配置加载寄存器 (LR) 和比较寄存器 (CMPR)
EPIT1->LR = 1000*1000; // 设置加载值为1,000,000 (1秒)
EPIT1->CMPR = 0; // 设置比较值为0
EPIT1->CNR = 1000*1000; // 设置计数器初始值为1,000,000 (可选,CR[1]已配置)
// 3. 注册并使能中断
GIC_EnableIRQ(EPIT1_IRQn); // 使能EPIT1中断
GIC_SetPriority(EPIT1_IRQn, 0); // 设置中断优先级为0
system_interrupt_register(EPIT1_IRQn, epit_irq_handler); // 注册中断处理函数
// 4. 启动定时器
EPIT1->CR |= (1 << 0); // 置位 EN 位 (bit 0),启动定时器
}
代码讲解:
epit_irq_handler()
: 中断服务函数。检查中断标志位 (SR[0]
),如果置位则翻转LED灯并清除标志位。epit1_init()
: 初始化函数。- 配置CR : 设置时钟源为IPG_CLK,预分频器为65 (得到1MHz时钟),启用重载模式 (
RLD
) 和输出比较中断 (OCIE
)。 - 配置LR/CMPR: 设置加载值为1,000,000,比较值为0。这意味着计数器每减到0就会触发一次中断,间隔时间为1秒 (1,000,000次计数 * 1微秒/次)。
- 注册中断: 调用GIC相关函数使能中断、设置优先级并注册处理函数。
- 启动定时器 : 最后才使能定时器 (
EN
),这是良好的编程习惯。
- 配置CR : 设置时钟源为IPG_CLK,预分频器为65 (得到1MHz时钟),启用重载模式 (
理想运行结果: 程序运行后,LED灯会精确地每1秒钟翻转一次状态。
五、 通用目的定时器 (GPT)
GPT是一种32位的增计数器,除了基本的定时功能外,还支持输入捕获和输出比较功能。本日我们主要利用其自由运行模式来实现高精度的延时函数。
1. GPT 工作原理
- 核心 : 一个32位的增计数器 (
CNT
)。 - 时钟源: 通常选择IPG_CLK (66MHz)。
- 预分频器: 对时钟源进行分频。
- 工作模式 :
- Free Running: 计数器从0增加到0xFFFF_FFFF后,自动回滚到0,继续增加。
- Reset Mode : 计数器增加到与输出比较寄存器 (
OCR
) 相等时,自动清零。
- 应用 : 本日用于实现
delay_us()
和delay_ms()
函数。
2. GPT 寄存器配置与代码实现 (gpt.c
)
c
#include "gpt.h"
#include "MCIMX6Y2.h"
// GPT1软件复位函数
void reset_gpt1()
{
GPT1->CR |= (1 << 15); // 置位 SWR 位 (bit 15),启动软件复位
while((GPT1->CR & (1 << 15)) != 0); // 等待复位完成 (SWR位自动清零)
}
void gpt1_init(void)
{
reset_gpt1(); // 先进行软件复位
unsigned int t;
// 1. 配置控制寄存器 (CR)
t = GPT1->CR; // 读取当前CR寄存器值
t &= ~(7 << 26); // 清除 OM 位 (bit 26-28),禁用输出比较功能
t &= ~(3 << 18); // 清除 IC 位 (bit 18-19),禁用输入捕获功能
t |= (1 << 9); // 置位 FRR 位 (bit 9),选择自由运行模式
t &= ~(7 << 6); // 清除 CLKSRC 位 (bit 6-8)
t |= (1 << 6); // 置位 CLKSRC 位 (bit 6),选择IPG_CLK作为时钟源
t &= ~(1 << 1); // 清除 ENMOD 位 (bit 1),计数器失能后保留原值
GPT1->CR = t; // 写回CR寄存器
// 2. 配置预分频寄存器 (PR)
GPT1->PR &= ~(0xFFF << 0); // 清除 PRESCALER 位 (bit 0-11)
GPT1->PR |= (65 << 0); // 置位 PRESCALER 位 (bit 0-11),设置分频系数为65 (66MHz / 66 = 1MHz)
// 3. 启动定时器
GPT1->CR |= (1 << 0); // 置位 EN 位 (bit 0),启动定时器
}
// 微秒级延时函数
void delay_us(unsigned int us)
{
unsigned int counter = 0; // 累计的时间差
unsigned int old_counter = 0; // 上一次读取的计数值
unsigned int new_counter = 0; // 当前读取的计数值
old_counter = GPT1->CNT; // 记录初始计数值
while(1)
{
new_counter = GPT1->CNT; // 读取当前计数值
if (old_counter != new_counter){ // 如果计数值发生了变化
if (old_counter < new_counter){
// 没有发生回滚,直接计算时间差
counter += new_counter - old_counter;
}else{
// 发生了回滚,计算回滚前后两段的时间差之和
counter += 0xFFFFFFFF - old_counter + new_counter;
}
if (counter >= us) // 如果累计的时间差达到了要求的微秒数
{
return; // 返回,延时结束
}
old_counter = new_counter; // 更新旧计数值,准备下一次循环
}
}
}
// 毫秒级延时函数
void delay_ms(unsigned int ms)
{
while(ms--)
{
delay_us(1000); // 调用微秒延时函数,每次延时1000微秒 (1毫秒)
}
}
代码讲解:
reset_gpt1()
: 软件复位函数,用于在初始化前将GPT模块恢复到初始状态。gpt1_init()
: 初始化函数。- 配置CR : 禁用OM和IC功能,选择自由运行模式 (
FRR
),时钟源为IPG_CLK,设置计数器失能后保留原值。 - 配置PR: 设置预分频器为65,得到1MHz的计数频率。
- 启动定时器: 最后使能定时器。
- 配置CR : 禁用OM和IC功能,选择自由运行模式 (
delay_us()
: 核心延时函数。- 原理 : 利用GPT的自由运行模式,不断读取计数器 (
CNT
) 的值。通过计算两次读取值之间的差值来累加时间。 - 回滚处理 : 由于计数器是32位的,当它从0xFFFF_FFFF溢出回滚到0时,新值会小于旧值。此时需要特殊处理:
(0xFFFFFFFF - old_counter) + new_counter
来计算总时间差。 - 循环: 持续累加时间差,直到达到指定的微秒数。
- 原理 : 利用GPT的自由运行模式,不断读取计数器 (
delay_ms()
: 基于delay_us()
实现,通过循环调用delay_us(1000)
来实现毫秒级延时。
理想运行结果 : 在 main.c
中调用 delay_us(1000 * 1000)
或 delay_ms(1000)
,LED灯会精确地每1秒钟翻转一次状态,证明延时函数准确无误。
六、 主函数整合 (main.c
)
c
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "core_ca7.h"
#include "led.h"
#include "beep.h"
#include "key.h"
#include "interrupt.h"
#include "clock.h"
#include "epit.h"
#include "gpt.h"
void led_delay(int t)
{
while(t--);
}
int main(void)
{
system_interrupt_init(); // 初始化中断系统
clock_init(); // 初始化时钟系统
led_init(); // 初始化LED
beep_init(); // 初始化蜂鸣器
key_init(); // 初始化按键
// epit1_init(); // 注释掉EPIT,因为我们使用GPT进行延时
gpt1_init(); // 初始化GPT
while(1)
{
// delay_us(1000 * 1000); // 使用GPT实现1秒延时
// delay_ms(1000); // 使用GPT实现1秒延时
led_nor(); // 翻转LED灯状态
}
return 0;
}
代码讲解:
- 初始化: 依次初始化中断、时钟、LED、蜂鸣器、按键和GPT。
- 主循环 : 在无限循环中,调用
led_nor()
翻转LED灯状态。为了实现1秒的间隔,我们在led_nor()
之前调用delay_us(1000 * 1000)
或delay_ms(1000)
。这样,LED灯就会每1秒闪烁一次。
理想运行结果: 开发板上的LED灯会精确地每1秒钟闪烁一次,验证了时钟系统、GPT定时器及延时函数的正确性。
总结
本日学习了嵌入式系统中最核心的两个部分:时钟系统 和定时器。
- 时钟系统是所有外设工作的基础,我们掌握了PLL、PFD、Prescaler三大硬件组件的作用,并通过寄存器配置成功将ARM内核主频提升至528MHz。
- EPIT 定时器用于产生精确的周期性中断,适用于需要定时唤醒或调度的任务。
- GPT 定时器功能更丰富,我们利用其自由运行模式实现了高精度的
delay_us()
和delay_ms()
延时函数,解决了传统软件延时不精确的问题。
这些知识是后续学习更复杂外设(如UART、I2C、PWM等)和操作系统的基础。务必理解每个寄存器配置背后的逻辑和目的。