当前位置: 首页 > news >正文

Day51 时钟系统与定时器(EPIT/GPT)

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. 配置步骤概览

  1. 切换时钟源: 在修改PLL1之前,先将内核时钟源临时切换到24MHz OSC,避免高频率下修改导致内核不稳定。
  2. 配置PLL1: 设置PLL1的倍频系数为88,使其输出1056MHz。
  3. 配置分频器: 设置CACRR寄存器,使PLL1输出经过二分频,得到528MHz。
  4. 恢复时钟源: 将内核时钟源切回PLL1,此时内核工作在528MHz。
  5. 配置PFDs: 配置528 PLL和480 PLL下的PFD分频器,生成所需的外设时钟频率。
  6. 配置时钟根 (Clock Root): 配置AHB_CLK_ROOT (132MHz)、IPG_CLK_ROOT (66MHz) 和 PERCLK_CLK_ROOT (66MHz)。

2. 关键寄存器说明与代码实现 (clock.c)

#include "clock.h"
#include "MCIMX6Y2.h"void clock_init(void)
{// 1. ARM工作频率(主频)配置 - 步骤1: 切换时钟源到OSCCCM->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. 恢复时钟源到PLL1CCM->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=32t |= ((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=19t |= ((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_SELPL1_SW 位,在修改PLL前将内核切换到安全的24MHz OSC。
  • PLL配置: 通过 CCM_ANALOG->PLL_ARM 寄存器设置倍频系数 (DIV_SELECT) 和使能位 (ENABLE)。
  • PFD配置: 通过 CCM_ANALOG->PFD_528CCM_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)

#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_CLKt |= (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;            // 设置比较值为0EPIT1->CNR = 1000*1000;     // 设置计数器初始值为1,000,000 (可选,CR[1]已配置)// 3. 注册并使能中断GIC_EnableIRQ(EPIT1_IRQn);          // 使能EPIT1中断GIC_SetPriority(EPIT1_IRQn, 0);     // 设置中断优先级为0system_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),这是良好的编程习惯。

理想运行结果: 程序运行后,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)

#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的计数频率。
    • 启动定时器: 最后使能定时器。
  • delay_us(): 核心延时函数。
    • 原理: 利用GPT的自由运行模式,不断读取计数器 (CNT) 的值。通过计算两次读取值之间的差值来累加时间。
    • 回滚处理: 由于计数器是32位的,当它从0xFFFF_FFFF溢出回滚到0时,新值会小于旧值。此时需要特殊处理:(0xFFFFFFFF - old_counter) + new_counter 来计算总时间差。
    • 循环: 持续累加时间差,直到达到指定的微秒数。
  • delay_ms(): 基于 delay_us() 实现,通过循环调用 delay_us(1000) 来实现毫秒级延时。

理想运行结果: 在 main.c 中调用 delay_us(1000 * 1000)delay_ms(1000),LED灯会精确地每1秒钟翻转一次状态,证明延时函数准确无误。


六、 主函数整合 (main.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();              // 初始化LEDbeep_init();             // 初始化蜂鸣器key_init();              // 初始化按键// epit1_init();         // 注释掉EPIT,因为我们使用GPT进行延时gpt1_init();             // 初始化GPTwhile(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等)和操作系统的基础。务必理解每个寄存器配置背后的逻辑和目的。

http://www.dtcms.com/a/405839.html

相关文章:

  • Django 搭配数据库开发智慧园区系统全攻略
  • 前端基础知识---10 Node.js(三)
  • article.3345645398
  • 国内如何使用GPT-5-Codex
  • Xcode 26 could not locate developer disk image for this device 无法定位开发者磁盘镜像
  • 用Python打造离线语音控制浏览器:基于VOSK的实用案例
  • 【ARDUINO】在arduino ide中下载安装开发包失败了,如何手动安装开发包
  • 上架 App 全流程解析,iOS 应用上架步骤、App Store 审核流程、ipa 文件上传与测试分发经验
  • 网站审核要多久老铁外链
  • 网站建设公司的服务公司湖南做网站 在线磐石网络
  • Linux的写作日记:Linux基础开发工具(二):vim编辑器
  • nginx缓存、跨域 CORS与防盗链设置(2)
  • 多级缓存架构:性能与数据一致性的平衡处理(原理及优势详解+项目实战)
  • 今天我们开始学习nginx缓存功能,CORS以及nginx防盗链
  • 前端缓存好还是后端缓存好?缓存方案实例直接用
  • 小九源码-springboot050-基于spring boot的苏蔚家校互联管理系统
  • 陕西西安网站建设公司大学生网页设计
  • Redis 面试常考问题(高频核心版)
  • 开发时如何彻底禁用浏览器表单自动填充缓存
  • 零基础新手小白快速了解掌握服务集群与自动化运维(七)Nginx模块--Nginx反向代理与缓存功能(二)
  • 【项目实战 Day7】springboot + vue 苍穹外卖系统(微信小程序 + 微信登录模块 完结)
  • python+springboot+uniapp基于微信小程序的停车场管理系统 弹窗提示和车牌识别
  • -bash: ssh-copy-id: command not found的两种解决方法
  • 电商网站新闻怎么做即速应用小程序官网
  • 上海网站建设接单互联网+大学生创新创业项目官网
  • 我是如何用Claude Code打造通用AI Agent的
  • 使用Nexus Repository Manager搭建私有自建 pip 源
  • 9.二叉树(上)
  • DNS 服务器与 DHCP 服务器详解及配置指南
  • 中国建设银行官网站招聘频道如何不花钱做网站