第3章:系统时钟与定时器——时钟树解析与精准延时实战
各位同学,咱们今天来啃一块硬骨头——TC275的时钟系统。说实话,我刚接触这颗芯片时,看着时钟树框图也懵了好一阵。但搞嵌入式嘛,时钟就是心脏,跳错了整个系统都得瘫痪。今天我就把这块掰开了揉碎了讲给你听。
3.1 时钟树解析:PLL/OSC/CCU
TC275的时钟源主要有三个:外部晶振(OSC)、内部振荡器、还有PLL倍频。我个人习惯把时钟树想象成一条水管系统——源头是晶振,PLL就是增压泵,CCU则是分水闸。
OSC(振荡器):通常接8MHz或20MHz晶振。我遇到过新手直接焊了个40MHz晶振上去,结果芯片死活不工作——嗯,超规格了。
PLL(锁相环):这才是重头戏。TC275的PLL可以把输入时钟倍频到最高200MHz。公式很简单:
fPLL = fOSC * (N / P) / K2
其中N是倍频系数,P是预分频,K2是后分频。举个例子:8MHz晶振,N=40,P=2,K2=1,得到160MHz。
CCU(时钟控制单元):它负责把PLL输出的时钟分配给各个外设。比如CPU跑200MHz,CAN模块只能跑40MHz,那就得靠CCU分频。
核心要点:TC275的时钟树有三个主要分支——系统时钟(fSYS)、外设时钟(fPER)、以及备份时钟(fBACK)。调试时务必确认每个分支的时钟源和分频系数。
3.2 系统时钟配置实战
配置时钟,说白了就是操作几个寄存器。我最常用的步骤是:
- 先切到备份时钟,防止配置过程中系统死掉
- 配置PLL的N/P/K2参数
- 等待PLL锁定(检查LOCK位)
- 切回PLL时钟
来看一段实际代码:
void SystemClock_Init(void)
{
// 1. 切换到备份时钟(fBACK = fOSC/2)
SCU_CCUCON0.B.SYSCLKSEL = 0; // 选择备份时钟
// 2. 配置PLL
SCU_CCUCON1.B.PLLN = 40; // 倍频系数N=40
SCU_CCUCON1.B.PLLP = 2; // 预分频P=2
SCU_CCUCON1.B.PLLK2 = 1; // 后分频K2=1
// 3. 使能PLL并等待锁定
SCU_CCUCON0.B.PLLEN = 1;
while(SCU_CCUSTAT.B.PLLLOCK == 0); // 等待锁定
// 4. 切回PLL时钟
SCU_CCUCON0.B.SYSCLKSEL = 1;
}
避坑指南:我曾经在等待PLL锁定时忘了加超时处理,结果晶振坏了,程序死循环在那。建议加个超时计数,比如等待10000次还没锁定就报错。
3.3 STM(系统定时器)使用
STM是TC275内置的一个64位定时器,精度高、中断快。我一般用它做系统心跳——比如每1ms触发一次中断,用来做任务调度。
配置STM其实就三步:
- 设置比较值(决定定时周期)
- 使能比较中断
- 启动定时器
举个例子,实现1ms定时:
void STM_Init(void)
{
// 假设fSYS=160MHz,1ms = 160000个时钟周期
STM0_CMP0 = 160000; // 设置比较值
STM0_ICR.B.CMP0EN = 1; // 使能比较中断
STM0_CR.B.STEN = 1; // 启动定时器
}
// 中断服务函数
void STM_IRQHandler(void)
{
STM0_ISCR.B.CMP0IRR = 1; // 清除中断标志
// 这里放你的周期性任务
}
注意:STM是64位计数器,但比较寄存器只有32位。所以最大定时周期是2^32 / fSYS。160MHz下约26.8秒。超过这个时间需要软件扩展。
3.4 GPT12(通用定时器)使用
GPT12是TC275的通用定时器模块,有5个通道。和STM不同,GPT12更灵活——可以捕获、比较、PWM输出。我曾在电机控制项目里用它做编码器脉冲计数,效果很好。
GPT12的配置稍微复杂点:
void GPT12_Init(void)
{
// 使能GPT12模块时钟
SCU_GPT12CLR.B.GPT12EN = 1;
// 配置T2为定时器模式,预分频64
GPT12_T2CON.B.T2M = 1; // 定时器模式
GPT12_T2CON.B.T2I = 4; // 预分频64(160MHz/64=2.5MHz)
// 设置定时值(1秒 = 2.5M ticks)
GPT12_T2 = 2500000;
// 使能中断
GPT12_T2IC.B.T2INT = 1;
// 启动
GPT12_T2CON.B.T2R = 1;
}
你想想看,GPT12和STM最大的区别在哪?STM是系统级定时器,精度高但功能单一;GPT12是外设定时器,功能丰富但配置繁琐。选哪个?看需求。
3.5 实战:精准延时与周期性任务
好了,理论说完了,咱们来点实际的。精准延时在嵌入式里太常见了——驱动LED闪烁、控制步进电机、采样传感器数据,哪个都离不开。
方案一:使用STM实现微秒级延时
void delay_us(uint32_t us)
{
uint64_t start = STM0_TIM0; // 读取当前计数值
uint64_t ticks = (uint64_t)us * 160; // 160MHz下,1us=160 ticks
while((STM0_TIM0 - start) < ticks);
}
这个方案精度高,但会占用CPU。我一般在初始化阶段用,运行时尽量不用。
方案二:使用GPT12实现周期性任务
假设我们要每10ms翻转一次LED:
void GPT12_T2_IRQHandler(void)
{
// 清除中断标志
GPT12_T2CON.B.T2R = 0;
GPT12_T2 = 25000; // 10ms (2.5MHz * 0.01s)
GPT12_T2CON.B.T2R = 1;
// 翻转LED
P00_OMR.B.PS0 = 1; // 假设LED接在P0.0
}
实战经验:我在做工业控制器时,用GPT12的T3通道做PWM输出,T4通道做输入捕获测频率。两个通道共用同一个定时器基准,同步性特别好。你想想看,如果分别用两个独立定时器,相位差就很难控制。
最后说个坑:中断优先级。STM和GPT12的中断优先级要合理分配。我习惯把STM设成最高优先级(系统心跳不能丢),GPT12次之。曾经有一次我把GPT12优先级设得比STM还高,结果系统心跳偶尔丢失,整个任务调度乱套了——排查了两天才找到原因。
好了,时钟和定时器这块就讲这么多。下节课咱们聊PWM和输入捕获,到时候会用到今天讲的GPT12知识。记得动手写代码,光看是学不会的。