ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DSP28335三相逆变程序源码解析与调试实战

DSP28335三相逆变程序源码解析与调试实战

说实话,我第一次拿到这套DSP28335三相逆变程序压缩包的时候,最头疼的不是代码量,而是根本不知道从哪里看起。压缩包里既有满屏的.c和.h源文件,又有一份PDF说明文档,标题一会儿写着“电压闭环程序”,一会儿写着“数字电源程序”,其实它们是同一套系统的两种叫法——电压闭环就是数字电源的核心控制策略,只是看问题的角度不一样。把整套东西彻底跑通之后,我发现真正有用的线索其实都集中在中断服务函数、ADC采样链路,以及那份PDF的参数表里。这篇文章就把我从源码阅读到实测调试的整个体会整理出来,适合正在做三相逆变、数字电源,或者拿到一套DSP28335程序却不知道从哪下手的开发者。

1. 这套三相逆变程序从启动到出波形的整体运行框架

1.1 一切从拓扑开始:程序控制的对象是什么

不管代码里写了多少算法,最终控制对象就那么一件事:直流母线电压经过三相全桥逆变,再经过LC滤波,输出三相对称交流电压。控制程序的任务,就是让输出端的三相电压稳定在目标值附近,负载变化的时候电压不能塌,波形谐波不能太大。

以常见的三相全桥拓扑来说,六个开关管构成三个桥臂,每个桥臂的上管和下管互补导通,中间点接滤波电感,电感后面接滤波电容,负载挂在电容两端。程序里的占空比信号,本质上就是在控制每个桥臂中间点的平均电压。你可能会看到源码里PWM模块配置了12条通道,但实际业务逻辑里只算出了三个调制波值,原因很简单:每相桥臂只需要一对互补的PWM信号,一个管子上桥,一个管子下桥,中间必须插入死区时间防止直通。

搞清楚这个拓扑再去看程序,思路就会清晰很多。程序里所有变量的名字,不管是Vdc、Va、Vb、Vc还是DutyA、DutyB、DutyC,都是在描述这个拓扑里面某个点的物理量。先建立“物理对象→程序变量”的对应表,后面读代码就不会迷路。

1.2 程序的文件结构和任务划分:先分清谁在跑、谁在闲着

一份完整的DSP28335工程,通常不会把所有代码堆在一个文件里。外设初始化单独一个文件,ADC采样一个文件,ePWM配置一个文件,控制算法一个文件,主函数再把这些东西串联起来。这样划分不是因为代码洁癖,而是因为实时控制任务和非实时任务必须物理隔离。

主循环里通常只做低速的事情:按键扫描、LED指示、通信处理、参数刷新。真正的高速任务全部放在中断服务函数里。三相逆变这套系统最典型的节拍是:ePWM模块产生载波,在载波周期匹配时刻触发ADC采样,采样完成后触发中断,中断里依次完成反馈计算、电压环PI、调制波计算、更新比较寄存器。整个过程在下一个PWM周期开始之前结束。

我的建议是,拿到源码先不要急着看PI算法,而是先在集成开发环境CCS里找到main.c,看它初始化调用了哪些函数,再找到对应的中断服务函数,看一下中断里到底做了哪几件事。一般情况下,你把这几件事列出来,程序的主线就已经出来了。

比如一个很典型的中断骨架,大致长这样:

interrupt void EPWM1_INT_ISR(void) { // 1. 读取本次ADC转换结果 ReadAdcResult(); // 2. 根据采样值计算三相输出电压实际值 CalcVoltageFeedback(); // 3. 电压闭环PI控制,得到三相调制波 RunVoltagePI(); // 4. 更新ePWM比较寄存器 UpdatePWMCompare(); // 5. 清除中断标志 EPwm1Regs.ETCLR.bit.INT = 1; }

主线出来之后,再往每个步骤里面钻,就会轻松很多。很多人读代码卡住,是因为一上来就扎进某个PI整定函数里,结果忘了整个中断每周期都在跑什么,最后越看越乱。

1.3 为什么六路驱动信号只对应三个调制波值

如果你在代码里搜CMPA,可能会困惑:明明一相桥臂需要两路互补PWM,为什么程序里每相只更新了一个比较值?实际上DSP28335的ePWM模块天生就是成对输出的,A通道和B通道可以配置成互补模式,只需设置一个比较值,硬件自动生成上下管互补信号,并插入死区。

例如,配置A通道在计数上升到比较值时置高,在计数下降到比较值时置低,B通道自动取反。程序里你只需要告诉硬件这一相当前周期的占空比是多少,剩下的互补、死区都由ePWM的外设逻辑完成。这就是为什么代码里通常只维护DutyA、DutyB、DutyC三个变量。

理解这一点非常重要。如果你在实测中发现某一相输出异常,排查方向往往不是控制算法,而是ePWM模块的引脚复用配置或者死区寄存器设置。硬件和软件的边界先划清楚,后面调试才不会被表面现象带偏。

2. 电压闭环的核心数据流:采样值如何一步一步变成占空比

2.1 反馈电压的采集与标度换算:ADC计算结果不是电压本身

三相逆变程序里的电压闭环,首先要知道当前输出电压是多少。程序里常用的办法是:从输出端取样电阻或者差分运放获取电压信号,送入DSP的ADC引脚。DSP28335的ADC输入范围一般是0V到3.3V,而交流电压既有正半周也有负半周,所以硬件上必须把信号抬升到1.65V中心电压附近。

这个抬升过程通常叫“交流采样偏置”。如果硬件偏置是1.65V,12位ADC的中间数字量就大约是2048。程序里换算实际电压的通用思路是:

// 假设ADC结果为16位右对齐,量程0~3.3V对应数字量0~4095 float Vraw = (float)adc_result; float Vmeas = (Vraw - adc_offset) * adc_scale;

这里的adc_offset是中心偏置对应的数字量,adc_scale是电压标度系数。要特别注意:如果硬件分压电阻网络给出的量程不是0~3.3V,而是0~5V或者其他范围,那adc_scale必须按实际量程计算。很多调试现场的怪问题,最后查来查去都是标度系数和小数位数的问题,而不是控制算法的问题。

从数字量反推回实际电压之后,程序还要根据是相电压反馈还是线电压反馈做系数修正。三相四线系统通常采样相电压,三相三线系统则可能采样线电压。读PDF说明的时候,第一件事就应该确认这个采样点在拓扑里的物理位置。

2.2 从模拟PID到数字PID:程序里用的到底是全量式还是增量式

模拟控制里,PID控制器是连续时间的积分运算;换到数字控制之后,每个PWM周期才计算一次,就必须把积分改写成累加。源码里最常见的是增量式PID写法。

离散形式的PI控制器可以写成:

float error = ref_voltage - meas_voltage; float integral += error * Ts; // Ts是控制周期 float output = Kp * error + Ki * integral;

写成这种形式之后,有两个地方必须处理。一是积分饱和:如果输出电压长期低于目标值,积分项会无限增大,导致占空比长时间顶在最大值,等电压上来之后恢复缓慢。程序里要有积分限幅,比如:

if (integral > INT_LIMIT) integral = INT_LIMIT; if (integral < -INT_LIMIT) integral = -INT_LIMIT;

二是输出限幅:电压环PI计算出来的调制波幅值,必须限制在0到最大调制比之间。超过最大调制比的指令已经超出物理可实现范围,直接饱和处理比让调制波畸形更好。

我曾经在调试中发现输出波形低频晃动,最后定位到就是积分限幅设置得太宽松,导致占空比频繁进入饱和区,整个环路变成了非线性系统。后来把积分限幅调到和输出限幅差不多量级,问题立刻消失。

2.3 目标电压为什么不能一上来就给满:软启动斜坡

三相逆变程序刚上电的时候,母线电压已经建立,如果立即把参考电压设置为额定值,输出电压和滤波电容之间的能量交换会产生很大的冲击电流,轻则触发保护,重则伤开关管。所以几乎所有的数字电源程序都会在参考电压路径上加一个软启动斜坡。

软启动的实现非常直接:每个控制周期在参考电压的基础上叠加一个固定步长,直到达到目标值为止。

if (ref_voltage < target_voltage) { ref_voltage += ramp_step; // 每一步增加一点 if (ref_voltage > target_voltage) { ref_voltage = target_voltage; } }

看到这类代码不要觉得多余,它的存在是为了保护功率器件。调试时如果你用仿真器强行修改参考电压变量,跳过软启动,很可能当场触发过流保护。这是我自己踩过的坑,所以每次都会提醒自己:软件斜坡的作用不是给用户看好看,而是真实的硬件保护。

2.4 采样误差会直接变成输出电压误差,这不是小事

电压闭环的本质是“以采样值为依据”的调节。如果采样链路有系统误差,那么不管PI参数调得多完美,输出电压的实际值和目标值都会差一截。常见的误差来源有三个:分压电阻精度不够、ADC偏置不准确、标度系数小数位数被截断。

我遇到过一种情况是程序里用了float,但换算系数写成了整数运算,导致算出来的电压值偏差好几个伏特。表面上看起来是“程序Bug”,实际上是“数值计算精度”问题。出现这种问题的时候,最好的办法是用信号发生器给ADC引脚一个已知电压,然后通过仿真器Watch窗口对比换算值和实际值,逐点校正系数。

3. 中断里每周期都在跑的那些计算:核心代码段逐行拆解

3.1 ePWM周期触发的节奏,是整个程序的节拍器

要理解中断里的计算,先得理解中断是怎么被触发的。DSP28335的ePWM模块内部有一个时基计数器,计数到设定周期值之后归零重新计数。可以在周期匹配的时刻触发ADC启动转换,等转换完成后产生中断,进中断服务函数。

这样一个PWM开关周期对应一次完整的采样和控制计算,控制频率等于开关频率。比如开关频率10kHz,那么每100微秒就完成一轮“采样→计算→更新占空比”。这个节奏非常重要,因为电压环的PI参数、积分步长、软启动斜坡的步进速度,全都依赖这个控制周期时间基准。程序里Ts这个变量,就是从这个节拍推出来的。

3.2 中断服务函数里的完整计算链条

我建议把整条计算链拆成四段来看。第一段是ADC结果读取,程序会从AdcResult寄存器里取三相电压采样值。第二段是反馈电压换算,对应上一章讲的标度变换。第三段是电压环PI,根据反馈和目标电压的差值算调制波幅值。第四段是占空比更新,把调制波幅值映射成比较寄存器的数值。

示意伪代码大致是这样:

interrupt void EPWM1_INT_ISR(void) { // 第一段:读ADC u16 adc_a = AdcResult.ADCRESULT0; u16 adc_b = AdcResult.ADCRESULT1; u16 adc_c = AdcResult.ADCRESULT2; // 第二段:换算实际电压 float va = (adc_a - offset_a) * scale_a; float vb = (adc_b - offset_b) * scale_b; float vc = (adc_c - offset_c) * scale_c; // 第三段:三相独立电压闭环 float d_a = VoltagePI(target_a, va); float d_b = VoltagePI(target_b, vb); float d_c = VoltagePI(target_c, vc); // 第四段:更新比较寄存器 UpdateCompare(d_a, d_b, d_c); }

读代码的时候,把注意力放在变量在四段之间的流动上,不要试图记住每个函数内部的所有局部变量。数据流一旦理清,源码的骨架就通了。

3.3 三相调制波是abc三相独立控制,还是坐标变换后控制

三相逆变程序的控制结构通常有两种流派。第一种是abc三相各自独立做一个电压环,每相输出一条正弦波调制波,适合三相四线带中性线的场合,实现简单,调试直观。第二种是先把三相电压从abc坐标系变换到dq同步旋转坐标系,在旋转坐标系里做PI控制,再将输出反变换回三相调制波,适合三相三线系统,稳态精度和动态性能更好。

拿到源码之后,先搜索代码里有没有sin、cos、Park、Clarke这类函数。如果全是三相独立变量,那就是第一种;如果存在坐标变换函数,那是第二种。千万不要抱着一套“通用理解”去套所有代码,我在这个上面吃过亏。

坐标变换的好处是可以把正弦量的控制变成直流量控制,PI控制器对直流量的跟踪没有稳态误差,这在三相逆变里是个很大的优势。代价是逻辑相对复杂,如果变换角度或者变换符号写错,输出波形会出现明显的谐振或相序错误。

3.4 从PI输出到CMPA寄存器:限幅和饱和的最后一关

电压环PI算出来的结果,在abc三相独立方案里就是调制波瞬时值,范围通常在0到1之间(标幺化)。但ePWM比较寄存器需要的不是0到1的小数,而是和时基周期成比例的数字量。比如时基周期是5000个计数,那么占空比0.8对应的比较值就是4000。

换算关系一句话就能说清:

uint16_t cmp_value = (uint16_t)(duty_ratio * TBPRD);

但这里有一个非常重要的工程细节:调制波瞬时值和输出电压的关系不是简单的线性映射,中间还受母线电压和开关管压降影响。程序里的调制比上限通常小于1,留出一点裕量给死区补偿。如果实际算出来的duty_ratio超过上限,输出波形被削顶,负载电压谐波会畸变。调试时如果发现空载波形尚可、带载波形变差,第一反应就应该是看调制波有没有进入饱和区。

4. PDF说明里最容易被跳过、但藏着关键参数的部分

4.1 参数表里没有标出来的是“它在代码哪个宏定义里”

很多PDF说明文档都有一张参数总表,列着开关频率、滤波电感、滤波电容、死区时间、额定功率这些值。但文档里往往不会告诉你这些参数在代码里对应哪个宏定义。这篇PDF我反复看了三遍才发现,关键参数不是集中在一个config.h里,而是散落在epwm.c、adc.c和主程序文件的三处地方。

参照关系可以按这样去查:

参数典型值在代码中的位置
开关频率10kHz时基周期寄存器,宏定义PWM_FREQ
死区时间约1微秒到3微秒死区模块上升沿/下降沿延时寄存器
ADC采样通道三相电压三通道ADC初始化文件里的通道使能
电压环比例系数通常零点几到几之间控制器初始化函数里的Kp常量
软启动步长每周期增加一小步主循环或中断里的RAMP_STEP

把这些对应关系做成自己的一份表,调试的时候会节省大量时间。参数表本身就是系统的“设计约束”,改任何一个参数之前,先问它来自哪张表、影响哪个闭环。

4.2 死区时间和最小脉宽,这两个值千万别乱调

死区的作用是防止同一桥臂上下管同时导通,但死区也会带来输出电压的误差。死区时间里,该导通的管子在等着,输出电压和理想值有偏差,相电流越大偏差越明显。所以稍微正规一点的程序都会在代码里加入死区补偿逻辑,而PDF说明里大多只用一句话带过。

另外一个容易被忽略的值是最小脉宽。当调制波接近0或接近满量程时,比较寄存器算出来的脉冲宽度可能只有几个计数周期,开关管的驱动信号还没完全建立就结束了。硬件上表现为驱动波形毛刺或者根本没输出。程序里通常会做一个最小脉宽限制,低于阈值的占空比直接归零或者钳位到最小值。

实测中发现输出相电压不对称,一部分情况下就是死区补偿参数和实际电流方向不匹配。改代码之前先在PDF里确认死区时间的单位,有些文档写的是纳秒,有些是时钟周期,差一个数量级,表现完全不一样。

4.3 PDF波形截图怎么看:空载、阻性负载、整流负载三张图的含义

说明文档里通常会附示波器截图。不要觉得这些只是展示效果,它们其实是环路性能的体检报告。空载波形最能反映启动过程和稳态谐波,阻性负载波形能看出电压跌落和恢复速度,整流负载波形则能暴露环路的动态响应能力。

三张图的观察重点可以这么归纳:

负载类型主要观察点波形异常说明什么
空载启动软启动时间、稳态正弦度谐波大,说明调制策略或滤波参数有问题
阻性负载加载瞬间电压跌落、恢复时间恢复慢,说明电压环带宽不够
整流性负载换相缺口波形、谐波畸变缺口大,说明采样滤波和环路响应需要优化

如果PDF里的波形和你在实验室复现出来的波形差别很大,优先检查是不是母线电压等级不同,或者负载类型不同。数字电源对硬件条件非常敏感,同样的代码在不同母线电压下表现可能完全是两回事。

4.4 版本说明和注释里的“TODO”,往往是下一版功能预告

PDF最后一页通常有版本更新记录,比如“V2.1 增加死区补偿”“V2.2 修复ADC初始采样异常”。这些更新记录非常宝贵,它告诉你这套程序在真实开发中踩过什么坑。代码注释里的TODO也值得看一遍,那往往是上一版开发者留下的话:“这里应该加限流”“这里参数没整定好”,这些信息比任何教程都有价值。

我自己读这类文档的习惯是:先看版本更新,再看参数表,最后才看原理框图。原理框图讲的是“应该是怎样”,版本更新讲的是“实际发生了什么”,实战里后者往往更接近真实情况。

5. 调试中反复出现的四类故障:现象、排查链路与根源

5.1 输出电压整体偏低:先看调制波是否“顶格”

现象是空载输出电压就比目标值低一截,怎么调PI参数都没用。这时候不要急着怀疑算法,先通过仿真器观察duty_ratio变量在运行中是否已经顶到限幅值。

如果调制波已经顶到最大,说明电压环给出的占空比指令超出了物理可执行范围,再加大PI输出也没有意义。排查链路可以这样走:第一步,查看实际母线电压,确认母线电压是否低于最低要求;第二步,查看调制波限幅值,确认没有被人为改小;第三步,断开负载看空载波形,如果空载仍偏低,基本可以排除负载影响,问题出在采样标度或母线电压。

我对这种问题的处理经验是:先检查采样反馈链路,如果反馈电压算小了,环路会误以为电压不够,拼命增大占空比,结果输出电压确实偏低——采样的锅,系统在替它背。

5.2 一带载电压就塌:母线电源限流还是电压环带宽不够

现象是空载正常,加载后输出电压明显下跌,甚至触发欠压保护。很多人第一反应是环路参数有问题,但还有一个高频原因:前级直流电源的限流保护。

排查时先量母线电压带载前后的变化。如果母线电压跟着跌落,说明前级供电能力不足,这不是控制程序能解决的。如果母线电压稳定,再观察电压波形跌落深度和恢复时间。电压单环控制在负载突变时天然存在滞后,因为输出电容要先放电,电压跌落了,环路才能感知到。

如果确认是环路问题,可以适当提高电压环比例系数,同时检查积分项恢复速度。但要注意,比例系数太大容易引起振荡,一次加一点,边改边看波形,而不是一次性跳到目标值。

5.3 输出波形高频震荡或发出尖锐噪声:PI参数和数字滤波的配合

现象是波形表面叠加了高频抖振,电感或者变压器发出吱吱声。这种问题常见于采样噪声直接进入环路,或者PI参数过于激进。

排查链路:第一步,重启上电观察是否一开机就震荡,还是带载后才出现。第二步,用示波器看PWM驱动波形,确认是否有窄脉冲或者抖动。第三步,在代码里把PI输出旁路掉,直接给固定调制波,如果震荡消失,说明问题在环路增益;如果震荡还在,说明是硬件驱动或采样链路问题。

为了消除采样噪声,很多程序会在ADC结果后面加一级低通滤波,比如一阶惯性滤波:

filtered_value = alpha * raw_value + (1.0f - alpha) * filtered_value;

这个滤波时间常数必须远小于电压环控制周期,否则会把环路延时加大,反而降低相位裕度,造成中频振荡。滤波系数和小数位数,往往比PI本身更值得花时间调。

5.4 某相完全没有输出:先从PWM引脚复用查起,别改算法

现象是三相输出只有两相正常,另外一相始终没有电压。这种问题十有八九不是控制算法,而是PWM引脚配置或硬件连接问题。

排查顺序:第一步,看ePWM模块的引脚复用寄存器,确认对应引脚没有处于GPIO模式。第二步,检查这一相的桥臂功率板是否有松动或者驱动芯片烧毁。第三步,拔掉功率板,让DSP单独输出低速试验波形,直接用示波器量引脚有没有PWM信号。第四步,如果引脚有信号但功率板无输出,再量驱动芯片供电。

我遇到过最典型的一次,是代码里把三相占空比算对了,但GPAMUX寄存器里有一项被误配置成了IO模式,导致某一路PWM根本没从引脚出来。这个问题看起来像是硬件故障,根子却在初始化代码里,排查了整整一下午。

6. 在现有源码上做二次开发时的改动思路与维护习惯

6.1 把固定电压参考改成可实时调压输出

很多项目用这套程序做普通三相交流输出,但实际要的是可变电压输出。改动方法并不复杂:把原本在初始化阶段写死的target_voltage改成全局变量,然后在主循环里根据通信指令修改它。

要注意的是修改参考电压时,不能让新值直接跳变,最好还是走软启动斜坡逻辑。哪怕是从220V调到230V,突变量看起来不大,功率回路也会产生冲击。我的做法是写一个简单的指令解析函数,把接受的电压指令放入待更新变量,中断里在软启动代码段的位置读取这个待更新变量,并逐步逼近。

6.2 从电压单环扩展到电压电流双闭环

如果负载要求更强,比如需要限流保护或者更好的动态响应,可以在这套代码基础上扩展出电流内环。把电流采样值纳入控制环:电压环外环输出不再是直接占空比,而是电流环的参考指令,电流内环再根据电感电流闭环计算占空比。

这种双闭环结构的改动量其实不大,先加电流采样通道,再增加一个电流环PI函数,最后把电压环输出接到电流环给定上。真正要小心的是两个环路的采样和控制顺序:通常在一个中断里,先算电压环,再算电流环,然后更新占空比。顺序反了,环路之间会出现明显的耦合振荡。

6.3 用一段内存数组记录关键变量,代替调试期的手抄数据

调试数字电源程序时,想观察PI输出、积分值、采样电压这些内部变量,示波器只能看到外部引脚波形,仿真器又只能在线看当前时刻。比较好的办法是在中断里定期把关键变量存入一段内存数组,程序运行一段时间后暂停或者等触发条件满足后,再把这组数组通过串口导出。

这个做法的实现成本很低,就是在中断里加几行赋值语句:

if (record_index < RECORD_SIZE) { record_buffer[record_index] = output_voltage; record_index++; }

有了录波数据之后,很多间歇性问题就能从数据回放里找到线索,而不用靠肉眼死盯示波器。这个习惯我一直推荐同行养成。

6.4 维护这套代码几年后,我坚持的三个小习惯

第一个习惯是变量命名尽量带物理含义和单位,比如PiOut_A、VoltRef,不要写一堆无意义的x1、x2。第二个习惯是每次修改PI参数前先用文本保存原始参数和存档名,方便对照整定前后差异。第三个习惯是只改一个变量再做一次实验,不要同时改两三个参数,否则出了问题根本不知道是哪一项引起的。

最后说一个我自己的体会:拿到这类程序或者文档,别急着去跑PI算法,先花半小时把“采样换算是真还是假、占空比有没有顶格、保护有没有误动”这三件基础事确认清楚,再上场调试,效率会高很多。调电压环,七成的时间其实不是在调PI,而是在调围绕PI的那些辅助链路。希望这篇拆解能帮你少走几天弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进