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。

CPU定时器内部结构 SYSCLKOUT TDDR ÷ (TDDR+1) TIM PRD TIM==0? 中断 工作流程 SYSCLKOUT → TDDR分频 → TIM递减 → TIM==0时触发中断并重载PRD

4.2 定时器寄存器配置

配置定时器,其实就是配那几个寄存器。我见过不少新手一上来就对着数据手册发呆,其实没那么复杂。

以TI C2000系列为例,核心寄存器就这几个:

寄存器 全称 作用 配置要点
TIM Timer Counter 当前计数值 只读,硬件自动递减
PRD Period Register 周期值 决定定时周期
TDDR Timer Divide Register 时钟分频 实际分频系数 = TDDR + 1
TCR Timer Control Register 控制位 启动/停止/中断使能

配置步骤其实就四步:

  1. 停定时器:写TCR寄存器,把TSS位置1
  2. 设周期:把目标计数值写入PRD
  3. 设分频:根据时钟频率算好TDDR
  4. 启动并使能中断: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,核心思路就一个:在定时器周期内,控制输出引脚的电平翻转时机。

具体做法分两步:

  1. 定时器工作在连续模式,周期由PRD决定
  2. 在中断服务函数里,根据当前计数值和占空比设定值,决定引脚输出高还是低

但说实话,用中断做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的脉搏,用好了,你的系统就有了稳定的时间基准;用不好,整个系统的时间关系都是乱的。