4. 定时器原理与应用:CPU定时器结构、寄存器配置与PWM生成
定时器这东西,说简单也简单,说复杂也复杂。我刚开始做DSP开发那会儿,总觉得定时器就是个计数的玩意儿,后来踩了几个坑才明白——定时器用得好不好,直接决定了你的系统实时性能不能达标。
今天咱们就聊聊DSP的CPU定时器。从内部结构讲起,再到寄存器怎么配,最后落地到两个最常用的场景:精准延时和PWM波形生成。
4.1 CPU定时器内部结构
DSP的CPU定时器,说白了就是一个递减计数器。它有三个核心寄存器:
- PRD(周期寄存器):设定计数周期
- TIM(计数寄存器):当前计数值,每个时钟周期减1
- TDDR(预定标寄存器):对输入时钟做分频
工作流程是这样的:TIM从PRD加载初值,每个时钟脉冲减1。减到0时触发中断,然后自动重载PRD的值,继续下一轮计数。
嗯,这里要注意——TIM减到0的那一刻,会产生一个脉冲信号。这个信号既可以触发CPU中断,也可以直接输出到GPIO引脚。PWM就是靠这个机制实现的。
核心要点:定时器的精度取决于两个因素——输入时钟频率和PRD值。时钟越快,PRD越大,精度越高,但中断频率也越高。
我个人习惯把定时器的工作模式分成两种:单次模式和连续模式。单次模式适合做一次性延时,连续模式适合做周期性任务或PWM。
4.2 定时器寄存器配置
配置定时器,其实就是配那几个寄存器。我见过不少新手一上来就对着数据手册发呆,其实没那么复杂。
以TI C2000系列为例,核心寄存器就这几个:
| 寄存器 | 全称 | 作用 | 配置要点 |
|---|---|---|---|
| TIM | Timer Counter | 当前计数值 | 只读,硬件自动递减 |
| PRD | Period Register | 周期值 | 决定定时周期 |
| TDDR | Timer Divide Register | 时钟分频 | 实际分频系数 = TDDR + 1 |
| TCR | Timer Control Register | 控制位 | 启动/停止/中断使能 |
配置步骤其实就四步:
- 停定时器:写TCR寄存器,把TSS位置1
- 设周期:把目标计数值写入PRD
- 设分频:根据时钟频率算好TDDR
- 启动并使能中断:TSS清0,中断使能位置1
我的习惯:配置前先算好目标频率。比如系统时钟150MHz,想要1ms中断一次,那PRD = 150000 - 1,TDDR = 0。如果觉得中断太频繁,可以加大TDDR做分频。
来看一段实际的配置代码:
void Timer0_Init(void)
{
// 1. 停定时器
CpuTimer0Regs.TCR.bit.TSS = 1;
// 2. 设分频:不分频
CpuTimer0Regs.TPR.bit.TDDR = 0;
CpuTimer0Regs.TPRH.bit.TDDRH = 0;
// 3. 设周期:150MHz时钟,1ms中断
// 计数值 = 150000000 / 1000 = 150000
CpuTimer0Regs.PRD.all = 150000 - 1;
// 4. 重载TIM
CpuTimer0Regs.TIM.all = CpuTimer0Regs.PRD.all;
// 5. 使能中断
CpuTimer0Regs.TCR.bit.TIE = 1;
// 6. 启动
CpuTimer0Regs.TCR.bit.TSS = 0;
}
我曾经踩过的坑:PRD的值是计数值减1!比如你要计1000个脉冲,PRD要写999。我第一次写的时候直接写了1000,结果定时周期长了1个时钟周期。虽然误差很小,但在高频PWM场景下,这个误差会被放大。
4.3 定时中断实现精准延时
精准延时,说白了就是让CPU等一段时间再干活。但怎么等,差别很大。
最简单的办法是软件延时——写个空循环。但这种方法有个致命问题:CPU被占死了,啥也干不了。而且延时精度受编译器优化影响,换个优化等级,延时时间就变了。
我推荐的做法是基于定时中断的延时:
- 配置定时器产生周期性中断
- 在中断服务函数里递增一个全局变量
- 主程序查询这个变量,达到目标值就继续执行
这样做的好处是:延时期间CPU可以处理其他任务,真正做到了"不空等"。
来看代码实现:
volatile unsigned long g_TimerTick = 0;
// 定时器中断服务函数
interrupt void Timer0_ISR(void)
{
g_TimerTick++;
// 清中断标志
CpuTimer0Regs.TCR.bit.TIF = 1;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
}
// 毫秒级延时函数
void Delay_ms(unsigned long ms)
{
unsigned long start = g_TimerTick;
while((g_TimerTick - start) < ms);
}
注意:g_TimerTick要用volatile修饰,防止编译器优化掉。我见过有人没加volatile,结果延时函数永远跳不出来——因为编译器把变量值优化到寄存器里了,中断改了内存值但寄存器没更新。
你想想看,这种延时方式精度能到多少?取决于定时器中断的频率。如果定时器1ms中断一次,那延时精度就是1ms。想要更高精度?把中断频率提高就行,但要注意中断服务函数的执行时间不能超过中断周期。
4.4 PWM波形生成
PWM,脉宽调制,说白了就是输出一个方波,通过调节占空比来控制功率。电机调速、LED调光、开关电源,哪哪都离不开它。
用定时器生成PWM,核心思路就一个:在定时器周期内,控制输出引脚的电平翻转时机。
具体做法分两步:
- 定时器工作在连续模式,周期由PRD决定
- 在中断服务函数里,根据当前计数值和占空比设定值,决定引脚输出高还是低
但说实话,用中断做PWM有个缺点——中断频率太高了。如果PWM频率是20kHz,那每50μs就要进一次中断。CPU大部分时间都在处理中断,干不了别的。
所以,真正的PWM都是用硬件做的。DSP内部有专门的ePWM模块,它内部有比较寄存器,硬件自动比较TIM和CMPA的值,自动翻转输出引脚,完全不需要CPU干预。
来看ePWM的配置代码:
void EPWM1_Init(void)
{
// 1. 时基配置:150MHz时钟,150分频,PWM频率 = 150M/150/10000 = 100kHz
EPwm1Regs.TBCTL.bit.CLKDIV = 0; // 不分频
EPwm1Regs.TBCTL.bit.HSPCLKDIV = 0; // 高速分频
EPwm1Regs.TBPRD = 1500 - 1; // 周期值
// 2. 计数模式:递增计数
EPwm1Regs.TBCTL.bit.CTRMODE = 0;
// 3. 比较值:占空比50%
EPwm1Regs.CMPA.bit.CMPA = 750;
// 4. 动作配置:TIM等于CMPA时输出低,TIM等于PRD时输出高
EPwm1Regs.AQCTLA.bit.CAU = 1; // 递增计数到CMPA时清0
EPwm1Regs.AQCTLA.bit.PRD = 2; // 到周期值时置1
// 5. 使能输出
EPwm1Regs.TBCTL.bit.FREE_SOFT = 2;
}
我的经验:配置PWM时,先算好频率再算占空比。频率由TBPRD决定,占空比由CMPA决定。想调占空比,改CMPA就行;想调频率,改TBPRD。但改了TBPRD后,CMPA也要跟着调,否则占空比会变。
实际项目中,我经常用PWM做电机控制。占空比从0%到100%连续可调,电机转速也跟着平滑变化。但要注意一点——死区时间。如果控制H桥,上下管不能同时导通,否则会短路。ePWM模块有死区控制寄存器,专门解决这个问题。
我曾经踩过的坑:有一次做电机驱动,PWM频率设得太高,结果MOS管发热严重。后来查资料才发现,MOS管的开关损耗和频率成正比。频率高了,开关损耗就大。最后把频率从100kHz降到20kHz,问题解决了。所以PWM频率不是越高越好,要根据负载特性来选。
4.5 定时器选型建议
DSP芯片里通常有多个定时器,怎么选?我一般按这个原则:
| 场景 | 推荐定时器 | 理由 |
|---|---|---|
| 系统心跳/操作系统时钟 | CPU Timer 0 | 优先级高,稳定可靠 |
| 精准延时 | CPU Timer 1/2 | 独立于操作系统,不受调度影响 |
| PWM生成 | ePWM模块 | 硬件自动输出,不占CPU |
| 输入捕获/脉冲计数 | eCAP模块 | 硬件捕获,精度高 |
说白了,能用硬件就别用软件。硬件PWM不占CPU,硬件捕获精度高,这些都是软件模拟比不了的。
好了,定时器这块就聊到这儿。记住一句话:定时器是DSP的脉搏,用好了,你的系统就有了稳定的时间基准;用不好,整个系统的时间关系都是乱的。