3. 时钟与中断系统:系统时钟配置、PLL锁相环、外设时钟使能、PIE中断控制器、外部中断、定时器中断

时钟和中断,说白了就是伺服驱动器的「心跳」和「神经」。

心跳乱了,电机就抖;神经断了,系统就崩。我做了这么多年伺服驱动,见过太多板子因为时钟配置不对导致跑飞,或者中断优先级没处理好导致丢步。今天咱们就把这块彻底讲透。

3.1 系统时钟架构概览

先看整体结构。DSP的时钟树,其实没那么复杂。核心就三个部分:

  • 时钟源:外部晶振或内部振荡器
  • PLL锁相环:把低频时钟倍频到高频
  • 时钟分发网络:给CPU、外设、定时器分别送时钟

我个人习惯把时钟树画成一张图,贴在工位上。每次改时钟配置,先看图再动手,能省不少debug时间。

核心原则:CPU时钟要快,外设时钟要稳,看门狗时钟要独立。

DSP时钟系统架构图 外部晶振 10MHz / 20MHz 内部振荡器 10MHz (±1%) PLL锁相环 VCO: 200MHz~600MHz 倍频系数: x10 ~ x60 分频系数: /2 ~ /128 时钟分发网络 CPU时钟 (SYSCLK) 外设时钟 (PCLK) 定时器时钟 (TCLK) 时钟源 → PLL倍频 → 时钟分发 → CPU/外设/定时器 ⚠ 注意 PLL输出频率不能超过芯片规格书标称最大值 例如TMS320F28335: SYSCLK最大150MHz

3.2 PLL锁相环配置

PLL这东西,说难不难,说简单也不简单。它的本质就是一个「频率放大器」。

我遇到过最坑的一次,是某项目量产阶段,发现部分板子启动后电机异响。查了两天,最后发现是PLL锁定时间不够,导致时钟频率在启动瞬间不稳定。从那以后,我每次配PLL都会加一个延时等待锁定。

3.2.1 配置步骤

  1. 选择时钟源:外部晶振还是内部振荡器
  2. 设置倍频系数:决定PLL输出频率
  3. 设置分频系数:得到最终CPU时钟
  4. 等待PLL锁定:必须检查锁定标志位
  5. 切换时钟源:从晶振切换到PLL输出

我的经验:PLL锁定时间通常需要几百微秒到几毫秒。别偷懒,老老实实加延时或者轮询标志位。我曾经见过有人直接跳过等待,结果芯片跑在未锁定的PLL上,频率忽高忽低,电机电流波形惨不忍睹。

3.2.2 代码示例

// TMS320F28335 PLL配置示例
// 目标:SYSCLK = 150MHz (外部晶振30MHz)

void InitPll(uint16_t pllcr, uint16_t clkindiv)
{
    // 1. 断开PLL,使用晶振直通模式
    SysCtrlRegs.PLLSTS.bit.DIVSEL = 2;  // 不分频
    
    // 2. 设置PLL倍频系数
    SysCtrlRegs.PLLCR.bit.DIV = pllcr;  // 例如pllcr=10, 30MHz*10/2=150MHz
    
    // 3. 等待PLL锁定
    while(SysCtrlRegs.PLLSTS.bit.PLLLOCKS == 0)
    {
        // 等待,啥也不干
    }
    
    // 4. 设置最终分频
    SysCtrlRegs.PLLSTS.bit.DIVSEL = clkindiv;
    
    // 5. 确认切换完成
    while(SysCtrlRegs.PLLSTS.bit.PLLLOCKS == 0);
}

警告:不要在PLL未锁定时操作外设寄存器!这会导致不可预测的行为。我有个同事就因为这个,把ADC采样时序搞乱了,查了三天才找到原因。

3.3 外设时钟使能

DSP的外设时钟使能,说白了就是「谁干活就给谁供电」。不用的外设,把时钟关了,能省不少功耗。

但这里有个坑——你必须在配置外设寄存器之前,先把它的时钟打开。否则写寄存器等于白写。

3.3.1 外设时钟控制寄存器

寄存器 控制的外设 位宽 说明
PCLKCR0 HRPWM、TBCLKSYNC 16位 高分辨率PWM时钟
PCLKCR1 ePWM1~ePWM6、eCAP1~eCAP6 16位 PWM和捕获单元
PCLKCR2 eQEP1~eQEP2、SCI、SPI 16位 编码器和串口
PCLKCR3 ADC、CAN、I2C 16位 模拟和通信外设

实用技巧:我习惯在初始化函数开头,先把所有用到的外设时钟都打开。这样后面配置寄存器时,就不用担心时钟没开的问题了。

3.3.2 代码示例

// 使能伺服驱动常用外设时钟
void EnablePeripheralClocks(void)
{
    // ePWM1~ePWM3用于电机控制
    SysCtrlRegs.PCLKCR1.bit.EPWM1ENCLK = 1;
    SysCtrlRegs.PCLKCR1.bit.EPWM2ENCLK = 1;
    SysCtrlRegs.PCLKCR1.bit.EPWM3ENCLK = 1;
    
    // eQEP1用于编码器
    SysCtrlRegs.PCLKCR2.bit.EQEP1ENCLK = 1;
    
    // ADC用于电流采样
    SysCtrlRegs.PCLKCR3.bit.ADCENCLK = 1;
    
    // SCI用于上位机通信
    SysCtrlRegs.PCLKCR2.bit.SCIAENCLK = 1;
}

3.4 PIE中断控制器

PIE(外设中断扩展)是DSP的中断管理核心。它把96个外设中断映射到12个CPU中断上。

你想想看,如果没有PIE,CPU得处理96个中断源,那得多累?PIE就像一个「秘书」,帮你把紧急的事情优先汇报给CPU。

3.4.1 PIE中断映射表

CPU中断 PIE组 中断源示例 优先级
INT1 PIE组1 PWM1~PWM3、TZ1~TZ3 最高
INT2 PIE组2 eQEP1、eCAP1
INT3 PIE组3 ePWM4~ePWM6
INT9 PIE组9 SCIA、SCIB
INT12 PIE组12 ADC、XINT1~XINT3

我的习惯:伺服驱动中,PWM中断优先级最高,因为电流环需要实时响应。编码器中断次之,通信中断优先级最低。这样能保证电机控制不受通信干扰。

3.4.2 PIE配置步骤

  1. 禁止全局中断(DINT)
  2. 清除PIE中断标志位
  3. 设置PIE中断向量表地址
  4. 使能PIE模块
  5. 配置具体中断的使能和优先级
  6. 使能CPU中断
  7. 使能全局中断(EINT)
// PIE中断初始化示例
void InitPie(void)
{
    DINT;  // 关全局中断
    
    // 清除PIE控制寄存器
    PieCtrlRegs.PIECTRL.bit.ENPIE = 0;
    
    // 清除所有中断标志
    PieCtrlRegs.PIEIFR1.all = 0xFFFF;
    PieCtrlRegs.PIEIFR2.all = 0xFFFF;
    // ... 清除所有12组
    
    // 设置中断向量表
    PieVectTable.EPWM1_INT = &Epwm1Isr;
    PieVectTable.EQEP1_INT = &Eqep1Isr;
    PieVectTable.ADCINT1   = &AdcIsr;
    
    // 使能PIE
    PieCtrlRegs.PIECTRL.bit.ENPIE = 1;
    
    // 使能具体中断
    PieCtrlRegs.PIEIER1.bit.INTx1 = 1;  // ePWM1中断
    PieCtrlRegs.PIEIER2.bit.INTx1 = 1;  // eQEP1中断
    PieCtrlRegs.PIEIER12.bit.INTx1 = 1; // ADC中断
    
    EINT;  // 开全局中断
}

3.5 外部中断

外部中断,就是通过芯片引脚触发的紧急事件。比如急停按钮、限位开关、过流保护信号。

我记得有一次调试伺服驱动器,电机突然失控,差点把工作台撞坏。后来发现是外部中断引脚没做滤波,一个毛刺就触发了急停。从那以后,我所有外部中断都加硬件滤波和软件去抖。

3.5.1 外部中断配置要点

  • 触发方式:上升沿、下降沿、双边沿
  • 去抖处理:硬件RC滤波 + 软件延时确认
  • 中断优先级:急停类中断优先级最高
  • 中断服务函数:尽量短,只做标志位设置

血的教训:外部中断服务函数里千万别做复杂运算!我见过有人把FOC算法放在外部中断里,结果中断嵌套导致系统崩溃。正确的做法是:中断里只置位一个标志,主循环里处理具体逻辑。

3.6 定时器中断

伺服驱动里,定时器中断是「节拍器」。电流环、速度环、位置环,都靠定时器中断来驱动。

我一般用CPU定时器0做电流环,定时器1做速度环,定时器2做位置环。三个定时器级联,形成多级控制架构。

3.6.1 定时器配置示例

// CPU定时器0配置 - 电流环 10kHz
void InitCpuTimers(void)
{
    // 定时器0:10kHz (100us)
    // SYSCLK = 150MHz, 预分频 = 1
    // 周期 = 150MHz / 1 / 10000Hz = 15000
    CpuTimer0Regs.PRD.all = 14999;  // 周期寄存器
    CpuTimer0Regs.TPR.all  = 0;     // 预分频
    CpuTimer0Regs.TPRH.all = 0;
    
    // 定时器1:1kHz (1ms) - 速度环
    CpuTimer1Regs.PRD.all = 149999;
    
    // 定时器2:100Hz (10ms) - 位置环
    CpuTimer2Regs.PRD.all = 1499999;
    
    // 使能定时器中断
    PieVectTable.TINT0 = &CpuTimer0Isr;
    PieCtrlRegs.PIEIER1.bit.INTx7 = 1;
}

关键点:定时器周期计算要精确。我习惯用宏定义,方便后期调整。比如电流环频率从10kHz改到20kHz,只需要改一个宏。

3.6.2 中断服务函数模板

// 电流环中断服务函数
interrupt void CpuTimer0Isr(void)
{
    // 1. 清除中断标志
    PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
    CpuTimer0Regs.TCR.bit.TIF = 1;
    
    // 2. 读取ADC电流采样值
    // 3. 执行Clark/Park变换
    // 4. 执行PI调节器
    // 5. 执行逆Park/逆Clark变换
    // 6. 更新PWM比较值
    
    // 注意:这里只做核心控制,别干别的!
}

优化建议:中断服务函数里尽量用局部变量,少用全局变量。全局变量访问慢,还容易引起缓存一致性问题。我一般把电流环需要的变量都定义在结构体里,中断里只传指针。

3.7 常见问题与避坑指南

  • PLL锁不住:检查晶振是否起振,供电是否稳定。我曾经遇到过晶振负载电容焊错,导致PLL死活锁不住。
  • 中断不响应:检查PIE使能位、CPU中断使能位、全局中断使能位。这三层有一个没开,中断就进不去。
  • 中断嵌套死机:高优先级中断里调用了延时函数,导致低优先级中断无法退出。解决办法:中断里只做标志位,主循环里处理。
  • 时钟频率不准:PLL倍频系数和分频系数组合不当,导致输出频率不是整数。用公式算清楚再写代码。

最后提醒:量产前一定要做时钟频率测试。用示波器测PWM输出引脚,看频率是否准确。我见过太多项目,开发板跑得好好的,量产板因为晶振批次差异导致频率偏移。


公众号:蓝海资料掘金营,微信deep3321