时钟可以说是整个芯片的"心跳",就像乐队演奏离不开节拍器,CPU、各类外设、定时器的所有操作,全部要跟随稳定的时钟信号同步工作。一旦时钟配置出错,整个系统就直接跑飞。除了普通工作时钟,还有RTC实时时钟模块,就算板子主电源断电,依靠纽扣电池供电,依旧可以保存日历和时间,这就是开发板掉电时间不丢失的秘密。
芯片最原始的时钟来源是板载石英晶振,我们开发板基础晶振频率是24MHz。但ARM内核需要几百兆的高频,外设又需要各种各样不同的工作频率,不可能给每一个外设都焊接独立晶振。这时就需要PLL锁相环、Prescale预分频器、PFD相位分数分频器来完成频率变换。
简单区分三者的作用:
PLL锁相环:负责倍频,把晶振输出的低频信号拉升到几百兆赫兹的高频,供给内核和高速外设;
Prescale预分频器:和倍频相反,把高频时钟向下分频,得到适合低速外设使用的时钟;
PFD相位分数分频器:输出频率可升可降,在PLL的基础上衍生多路不同时钟,满足USB、网络等外设多样化需求。
IMX6ULL内部拥有多个PLL,分别服务ARM内核、音频、以太网等不同模块;其中528 PLL、480 PLL各自引出4路PFD,总共8路PFD输出,给外设提供丰富的时钟选择。
注意:配置ARM PLL内核时钟的时候,不能直接修改倍频,极易造成内核死机。
标准配置流程:
- 先把ARM内核临时切换到24MHz晶振的低速时钟,保证系统处于安全状态;
- 配置PLL相关寄存器,要优先设置PLL后端的二分频,再修改倍频因子;
- 等待PLL时钟输出稳定之后,再把内核时钟源切回PLL输出的高频时钟。
配置完PLL之后,还需要配置AHB、IPG、PERCLK这些根时钟,总线和外设都依赖它们,每一路时钟都有对应的CCM系列寄存器,用来选择时钟源、设置分频系数。光配置时钟树还不够,每个外设还有独立的时钟门控,如果门控没打开,外设直接不工作。练习的时候可以直接把全部门控打开,正式项目里建议按需开启。
时钟初始化完整代码如下:
c
#include "clock.h"
#include "MCIMX6Y2.h"
//开启全部外设时钟门控,练习使用,实际项目按需开启
void clock_gt_int(void)
{
CCM->CCGR0 = 0xFFFFFFFF;
CCM->CCGR1 = 0xFFFFFFFF;
CCM->CCGR2 = 0xFFFFFFFF;
CCM->CCGR3 = 0xFFFFFFFF;
CCM->CCGR4 = 0xFFFFFFFF;
CCM->CCGR5 = 0xFFFFFFFF;
CCM->CCGR6 = 0xFFFFFFFF;
}
void clock_init(void)
{
//1.配置ARM Core PLL1,内核切换至528MHz
CCM->CCSR &= ~(1 << 8);
CCM->CCSR |= (1 << 2);
CCM->CACRR &= ~(7 << 0);
CCM->CACRR |= (1 << 0);
unsigned int r = CCM_ANALOG->PLL_ARM;
r &= ~(3 << 14);
r &= ~(0x7F << 0);
r |= (88 << 0); //设置倍频因子
CCM_ANALOG->PLL_ARM = r;
CCM_ANALOG->PLL_ARM |= (1 << 13);
CCM->CCSR &= ~(1 << 2);
//2.配置528PLL 和480PLL 的PFD分频输出
r = CCM_ANALOG->PFD_528;
r &= ~((0x3F << 0) | (0x3F << 8) | (0x3F << 16) | (0x3F << 24));
r |= ((27 << 0) | (16 << 8) | (24 << 16) | (32 << 24));
CCM_ANALOG->PFD_528 = r;
r = CCM_ANALOG->PFD_480;
r &= ~((0x3F << 0) | (0x3F << 8) | (0x3F << 16) | (0x3F << 24));
r |= ((12 << 0) | (16 << 8) | (17 << 16) | (19 << 24));
CCM_ANALOG->PFD_480 = r;
//3.配置AHB_CLK_ROOT总线时钟
CCM->CBCMR &= ~(3 << 18);
CCM->CBCMR |= (1 << 18);
r = CCM->CBCDR;
r &= ~(1 << 25);
r &= ~(7 << 10);
r |= (2 << 10);
//配置IPG_CLK_ROOT
r &= ~(3 << 8);
r |= (1 << 8);
CCM->CBCDR = r;
//配置PERCLK_CLK_ROOT
CCM->CSCMR1 &= ~((0x3F << 0) | (1 << 6));
//打开所有外设时钟门控
clock_gt_int();
}
搞定时钟这个地基,接下来学习定时器。定时器本质就是对稳定的时钟脉冲做计数,以此实现延时、定时中断、事件计数。顺带回忆51单片机定时器:8位支持自动重装,16位模式需要在中断服务函数里手动重装初值。对比之下IMX6ULL的硬件定时器功能更强大,今天重点学习EPIT和GPT这两个定时器,二者看着相似,实际定位差别很大。
EPIT增强型周期中断定时器,主打周期性中断,属于向下递减计数器。写入重装载值之后,计数器不断向下计数,计数值匹配比较寄存器时触发中断,并且可以自动重装初值,循环产生定时信号,非常适合周期性任务,比如定时采样、周期闪烁LED。
实验需求:1秒产生一次中断,在中断服务函数翻转LED。这里一定要注意,中断触发后需要写1清除中断标志位,否则会重复进入中断,造成程序卡死。
c
#include "epit.h"
#include "MCIMX6Y2.h"
#include "led.h"
#include "interrupt.h"
//EPIT1中断服务函数
void epit1_irq_handler(void)
{
if ((EPIT1->SR & (1 << 0)) != 0)
{
led_nor(); //翻转LED
EPIT1->SR |= (1 << 0); //写1清除中断标志,重中之重
}
}
void epit1_init(void)
{
unsigned int r = EPIT1->CR;
r &= ~(3 << 24);
r |= (1 << 24);
r |= (1 << 17);
r &= ~(0xFFF << 4);
r |= (65 << 4);
r |= ((1 << 3) | (1 << 2) | (1 << 1));
EPIT1->CR = r;
EPIT1->LR = 1000*1000; //重装载值
EPIT1->CMPR = 0;
EPIT1->CNR = 1000 * 1000;
//GIC中断控制器配置
GIC_SetPriority(EPIT1_IRQn, 0);
system_interrupt_register(EPIT1_IRQn, epit1_irq_handler);
GIC_EnableIRQ(EPIT1_IRQn);
EPIT1->CR |= (1 << 0); //使能EPIT定时器
}
GPT通用定时器,采用向上计数,功能更加丰富。除普通定时以外,还支持输入捕获、比较输出,可以测量脉冲宽度、生成PWM波形。本次实验使用GPT自由运行模式,实现硬件精准延时。
软件空循环延时会受CPU主频影响,换个主频延时就不准;而GPT硬件延时读取硬件CNT计数值,精度不受CPU负载影响。写delay_us的时候还要处理计数器溢出回零的情况,不然溢出之后计算出错,延时直接失效。
c
#include "gpt.h"
#include "MCIMX6Y2.h"
void gpt1_init(void)
{
GPT1->CR |= (1 << 15); //GPT软件复位
while((GPT1->CR & (1 << 15)) != 0); //等待复位完成
unsigned int r = GPT1->CR;
r &= ~(0xFF << 16);
r |= (1 << 9); //选择时钟源
r &= ~(7 << 6);
r |= (1 << 6); //设置自由运行模式
r &= ~(1 << 1);
GPT1->CR = r;
GPT1->PR &= ~(0XFFF << 0);
GPT1->PR |= (65 << 0); //配置预分频
GPT1->CR |= (1 << 0); //使能GPT定时器
}
void delay_ms(unsigned int ms)
{
while(ms--)
{
delay_us(1000);
}
}
//硬件微秒延时,处理计数器溢出
void delay_us(unsigned int us)
{
unsigned int count = 0;
unsigned int old_count = 0, 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;
}
//计数器溢出回0的处理逻辑
if (new_count < old_count)
{
count += 0xFFFFFFFF - old_count + new_count;
}
}
if (us <= count)
{
return;
}
old_count = new_count;
}
}
最后看主函数,初始化顺序非常重要:优先初始化中断控制器,再配置系统时钟,之后再初始化LED、蜂鸣器这类外设。如果外设初始化跑在时钟初始化前面,外设没有时钟供给,初始化完全无效。
下面的代码我暂时注释掉EPIT,测试GPT硬件延时实现LED闪烁;把注释打开,就可以切换为EPIT中断的方式来完成同样的效果。这里还要注意:硬件计数里面1M代表10001000,存储容量的1M才是10241024,写定时数值的时候不要搞混。
c
#include "fsl_iomuxc.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(unsigned int t)
{
while(t--);
}
int main(void)
{
system_interrupt_init(); //初始化GIC中断控制器
clock_init(); //系统时钟初始化
led_init(); //LED初始化
beep_init(); //蜂鸣器初始化
key_init(); //按键初始化
//epit1_init(); //注释,当前测试GPT延时
gpt1_init(); //GPT定时器初始化
while(1)
{
led_nor();
delay_us(1000 * 1000); //1s硬件延时翻转LED
}
return 0;
}
今天理论结合代码跑完整套实验,最大的感悟就是嵌入式裸机开发,时钟就是地基。很多小伙伴调试外设,中断不触发、定时器不工作,排查很久,最后发现只是忘记开时钟门控或者分频配置错误。
再次总结两个定时器的区别:
EPIT:向下计数,自动重装载,适合周期性中断场景;
GPT:向上计数,功能更强,适合精准延时、输入捕获、PWM输出。