3. 时钟与中断系统:系统时钟配置、PLL锁相环、外设时钟使能、PIE中断控制器、外部中断、定时器中断
时钟和中断,说白了就是伺服驱动器的「心跳」和「神经」。
心跳乱了,电机就抖;神经断了,系统就崩。我做了这么多年伺服驱动,见过太多板子因为时钟配置不对导致跑飞,或者中断优先级没处理好导致丢步。今天咱们就把这块彻底讲透。
3.1 系统时钟架构概览
先看整体结构。DSP的时钟树,其实没那么复杂。核心就三个部分:
- 时钟源:外部晶振或内部振荡器
- PLL锁相环:把低频时钟倍频到高频
- 时钟分发网络:给CPU、外设、定时器分别送时钟
我个人习惯把时钟树画成一张图,贴在工位上。每次改时钟配置,先看图再动手,能省不少debug时间。
核心原则:CPU时钟要快,外设时钟要稳,看门狗时钟要独立。
3.2 PLL锁相环配置
PLL这东西,说难不难,说简单也不简单。它的本质就是一个「频率放大器」。
我遇到过最坑的一次,是某项目量产阶段,发现部分板子启动后电机异响。查了两天,最后发现是PLL锁定时间不够,导致时钟频率在启动瞬间不稳定。从那以后,我每次配PLL都会加一个延时等待锁定。
3.2.1 配置步骤
- 选择时钟源:外部晶振还是内部振荡器
- 设置倍频系数:决定PLL输出频率
- 设置分频系数:得到最终CPU时钟
- 等待PLL锁定:必须检查锁定标志位
- 切换时钟源:从晶振切换到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配置步骤
- 禁止全局中断(DINT)
- 清除PIE中断标志位
- 设置PIE中断向量表地址
- 使能PIE模块
- 配置具体中断的使能和优先级
- 使能CPU中断
- 使能全局中断(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