简介:电压空间矢量调制(SVM)的C语言实现,基于TI TMS320F2407 DSP平台,面向电机控制、电力电子方向的开发者与学习者。资源将SVM核心算法拆分成.c与.h两个文件,分别承载调制逻辑实现与函数/常量接口声明,模块边界清晰,便于快速移植。压缩包仅2KB,共2个文件,适合作为理解SVM在实时控制系统中落地的入门参考。已有170人学习/下载。代码呈现了三相逆变器六个基本电压矢量的组合、开关周期内时间片分配等关键概念,并结合DSP定时器中断与浮点运算特性,展示了维持输出波形质量、降低谐波的处理思路。代码虽短,却涵盖了控制器、调制器到硬件接口的模块化封装思路,便于在HIL或实物原型上验证算法正确性。通过学习可掌握SVM调制流程与实时性优化要点,为后续开发PMSM或感应电机驱动算法打下基础。 前阵子整理旧项目,翻出一台还在产线上运行的变频器,控制板核心是TMS320LF2407。这颗16位定点DSP没有硬件浮点单元,主频40MHz,内部Flash只有32K字,但它要承担的却是电压空间矢量调制,也就是SVPWM,而且是纯C语言写的中断服务程序。当时我把整套算法用C语言移植到2407上,从原理推导、定点化到底层PWM外设配置,前前后后调了一周多,中间踩了不少坑。这篇博文就把这段经历完整写出来,从SVPWM的原理拆解到2407平台上的C代码实现,再到实测波形和排错思路,给正在做电机控制、变频器或伺服驱动开发的朋友做个参考。
1. 为什么还要在2407上写SVPWM
1.1 这颗老DSP的现状与价值
TMS320LF2407是TI C2000系列里非常经典的一颗芯片,虽然现在新项目基本都用F2806x、F2833x甚至更新的F28P65了,但存量设备里有大量2407在服役,尤其是一些工业变频器、车载辅助电机控制器和纺织机械驱动。所以“在2407上做SVPWM”并不是一个过时话题,很多工程师的日常工作就是维护和升级这类平台。
2407的特点非常鲜明:16位定点内核,最高40MHz主频,没有FPU,不能像F2833x那样直接写float类型做控制算法。它有两个事件管理器EVA和EVB,其中EVA带三个全比较单元,可以产生6路带死区的PWM,配合周期中断做电流环控制非常顺手。虽然这颗DSP算力放到今天看很弱,但在10kHz的PWM频率下,一个中断周期只有50微秒,只要算法结构合理,SVPWM这种计算量并不大的调制算法完全可以跑得过来。
1.2 软件SVPWM和硬件SVPWM模式怎么选
2407的事件管理器其实内置了一个空间矢量PWM硬件模式,设置ACTR寄存器里的SVM位,再配合D寄存器指定扇区,硬件就能自动完成扇区切换和PWM输出。但这种方式灵活性有限,比如你想在同一个载波周期里自定义零矢量分配方式,或者想方便地切换到SPWM、DPWM等其他调制策略,硬件模式就不够用了。
我最终选择了软件计算扇区和作用时间、再用全比较单元通用PWM模式输出的方案。这样扇区判断、作用时间计算、死区配置和比较值装载完全在C代码里可控,后续想换调制策略、做中点电位平衡、加死区补偿都很方便。哪怕以后把代码平移到STM32或者其它MCU平台,算法主体也基本不用动。
2. 从矢量合成到可落码的作用时间公式
2.1 六个基本矢量与扇区划分
SVPWM的核心思想,是用逆变器六个功率开关管组成的三相桥臂,产生六个非零电压空间矢量和两个零矢量。以三相两电平逆变器为例,上桥臂导通记为1,下桥臂导通记为0,那么(100)、(110)、(010)、(011)、(001)、(101)对应六个非零矢量,它们在αβ平面上把整个平面均匀分成六个60度扇区,(000)和(111)对应零矢量。
每个控制周期里,目标电压矢量Uref由当前扇区相邻的两个基本矢量和零矢量按照伏秒平衡合成。举个生活里的例子:你要用两种不同颜色的沙子按比例混合出一种目标颜色,那两种原色就是相邻的基本电压矢量,混合比例就是各自的作用时间,而零矢量就像加进去调节总量、不改变颜色的填充剂。
2.2 伏秒平衡推公式:我建议你亲手推一遍
很多资料直接给出X、Y、Z变量和扇区查表,但如果不理解公式怎么来的,一旦平台变了、标幺基准变了,很容易栽跟头。我在2407上做的第一件事,就是把作用时间公式从头推了一遍。
假设基本矢量以(2/3)Vdc为基准做了归一化,那么V1(100)就是单位矢量1∠0°,V2(110)是1∠60°。在扇区I里,目标矢量Uref等于:
Uref = (T1/Ts)*V1 + (T2/Ts)*V2
把V1和V2的αβ分量代入,得到:
Uα = T1/Ts + T2/(2Ts)
Uβ = (√3/2)*(T2/Ts)
解这个二元一次方程组,就得到:
T2 = (2Ts/√3) * Uβ
T1 = Ts * (Uα - Uβ/√3)
在扇区II、III乃至其余扇区,用V2、V3组合或V3、V4组合继续推,最后定义三个中间变量:
X = (2/√3) * Uβ * Ts
Y = (Uα + Uβ/√3) * Ts
Z = (Uα - Uβ/√3) * Ts
各扇区的相邻矢量作用时间T1、T2就能统一查表得到:
- 扇区I:T1=Z,T2=X
- 扇区II:T1=Y,T2=-Z
- 扇区III:T1=X,T2=-Y
- 扇区IV:T1=-Z,T2=-X
- 扇区V:T1=-Y,T2=Z
- 扇区VI:T1=-X,T2=Y
自己推一遍最大的好处是,如果网上的资料用的坐标定义、扇区编号和你不一样,你可以用自己的推导去核对,而不是盲目抄表。
2.3 扇区判断:不需要atan2
判断目标矢量落在哪个扇区,最直接的办法是算atan2角度,但在2407这种定点DSP上,做一次atan2的开销太大,不划算。工程上常用三个投影判据:
A = (Uβ > 0)
B = (√3Uα - Uβ > 0)
C = (-√3Uα - Uβ > 0)
三个布尔值组合成一个0到7的数,再查一次表映射到实际的1到6扇区编号。整个过程只有比较和查表,没有三角函数,非常适合在中断里跑。要注意的是,这个编码方式和你最终使用的T1/T2查表必须配套,不能混用。
3. 2407定点平台上的数据设计
3.1 IQ格式怎么选
2407没有浮点单元,C语言里直接写float变量,编译器会调用软浮点库,计算速度非常慢,而且每次都占用大量栈空间和ROM。所以第一步就是把所有参与SVPWM计算的量改成定点格式。
我习惯用Q15格式表示-1到0.9999之间的标幺值,用一个int16_t变量存储。但Q15的乘法需要特别注意:两个Q15数相乘,结果是Q30格式,要右移15位取高16位才能回到Q15。比如:
typedef int16_t iq15; typedef int32_t iq30; static inline iq15 iq15_mul(iq15 a, iq15 b) { return (iq15)(((iq30)a * b) >> 15); }2407的C编译器支持16位乘16位得到32位结果的乘法指令,所以这种写法在2407上开销并不大。但要注意,如果两个输入都接近1,相乘后的中间结果可能超过Q15表示范围,所以中间变量必须用int32_t,不能为了省事用int16_t直接存。
3.2 每个中断周期的计算预算
PWM频率按10kHz算,一个控制周期只有100微秒。如果采用对称PWM,周期中断在三角波谷触发,留给SVPWM计算的时间大约是50微秒。2407在40MHz主频下,一个指令周期25纳秒,50微秒就是2000个指令周期。
SVPWM本身的运算量其实不大:扇区判断几个比较,作用时间几次乘法,饱和处理一两次除法,然后装载比较寄存器。这些用定点运算全部加起来大概几百个指令周期,完全够用。真正会拖垮时间的是软浮点除法,一次float除法可能吃掉几百个周期,所以在中断里我基本杜绝浮点运算,全部用整数移位和查表替代。
3.3 中断里该放什么不该放什么
我最终把SVPWM计算放在EVA的周期中断里,也就是三角载波的波谷时刻。此时计数器归零,更新比较值不会产生毛刺。中断服务程序里只做几件事:读电流采样、坐标变换、电流环PI、SVPWM扇区计算、比较值装载。所有变量都定义成全局变量,中断里直接操作,避免函数调用层级过深带来的压栈开销。
有一个很容易忽略的点:2407的C编译器对int类型默认是16位,所以两个int乘法必须先强制转换成long再做,否则结果会被截断。我在调试中遇到过一个很隐蔽的波形异常,最后定位就是这句强制转换没写,乘法结果被截断成16位,导致占空比计算错误。
4. 核心C代码与装载细节
4.1 扇区判断与作用时间计算代码
下面这段是核心算法的C语言片段,输入是αβ轴的电压分量u_alpha和u_beta,均为Q15格式,输出是扇区和两个基本矢量的作用时间t1、t2:
#define K_SQRT3_Q15 ((iq15)(1.7320508f * 32768.0f)) #define K_2SQ3_Q15 ((iq15)(1.1547005f * 32768.0f)) #define K_1SQ3_Q15 ((iq15)(0.5773503f * 32768.0f)) static const int16_t code_to_sector[8] = {0, 2, 6, 1, 4, 3, 5, 0}; int16_t svpwm_calc(iq15 u_alpha, iq15 u_beta, iq15 *t1, iq15 *t2) { int16_t code = 0; iq15 x, y, z; /* 扇区判断 */ if (u_beta > 0) code += 1; if (iq15_mul(u_alpha, K_SQRT3_Q15) - u_beta > 0) code += 2; if (-iq15_mul(u_alpha, K_SQRT3_Q15) - u_beta > 0) code += 4; int16_t sector = code_to_sector[code]; /* 中间变量 */ x = iq15_mul(u_beta, K_2SQ3_Q15); y = u_alpha + iq15_mul(u_beta, K_1SQ3_Q15); z = u_alpha - iq15_mul(u_beta, K_1SQ3_Q15); /* 按扇区查表 */ switch (sector) { case 1: *t1 = z; *t2 = x; break; case 2: *t1 = y; *t2 = -z; break; case 3: *t1 = x; *t2 = -y; break; case 4: *t1 = -z; *t2 = -x; break; case 5: *t1 = -y; *t2 = z; break; case 6: *t1 = -x; *t2 = y; break; default: *t1 = 0; *t2 = 0; break; } return sector; }这段代码里的扇区编号是按0度到360度逆时针排列的,扇区1对应0到60度。如果你从其它资料抄来的代码用的是不同编号体系,一定要把扇区映射表和这里的T1/T2分配一并修改。
4.2 七段式发波和比较寄存器装载
作用时间算出来后,还要转换成比较寄存器CMPR1、CMPR2、CMPR3的装载值。我采用最常见的七段式对称PWM,每个PWM周期内零矢量被分成四段,两个非零矢量各分成两段,这样每相在一个周期里最多开关两次,谐波性能好,开关损耗也相对均衡。
在2407上,CMPR1、CMPR2、CMPR3分别对应A、B、C三路的全比较输出。以扇区I为例,七段式顺序是(000)-(100)-(110)-(111)-(110)-(100)-(000),三相的切换点不同,装载方式不能简单按同一规则硬套,否则输出完全是乱的。
实践中我建议先把作用时间转成计数周期值,然后按下面方式计算三个基准切换点:
int16_t ta, tb, tc; int16_t tperiod; /* T1PER 周期寄存器的值 */ ta = (tperiod - t1 - t2) / 4; tb = ta + t1 / 2; tc = tb + t2 / 2;然后按扇区查表分配给三个比较寄存器:
switch (sector) { case 1: cmp1 = ta; cmp2 = tb; cmp3 = tc; break; case 2: cmp1 = tb; cmp2 = ta; cmp3 = tc; break; case 3: cmp1 = tc; cmp2 = ta; cmp3 = tb; break; case 4: cmp1 = tc; cmp2 = tb; cmp3 = ta; break; case 5: cmp1 = tb; cmp2 = tc; cmp3 = ta; break; case 6: cmp1 = ta; cmp2 = tc; cmp3 = tb; break; default: cmp1 = 0; cmp2 = 0; cmp3 = 0; break; }这里有一个很容易踩的坑:不同板子的功率电路、PWM极性配置可能不同,ACTRA寄存器里的输出极性设置直接影响“比较值越大占空比越大”还是“比较值越大占空比越小”。我第一次在一个IGBT模块的驱动板上直接套用之前的代码,结果电机嗡嗡响、母线电流很大,后来用示波器逐个检查PWM输出才发现是极性取反了。所以这套表不是万能的,必须配上你实际使用的极性配置验证一次。
4.3 饱和处理与过调制
当电压矢量超出六边形内切圆时,T1+T2会超过一个PWM周期,这时候必须做饱和处理。最简单的办法是等比例缩小:
if (t1 + t2 > tperiod) { int16_t sum = t1 + t2; t1 = (int16_t)(((int32_t)t1 * tperiod) / sum); t2 = (int16_t)(((int32_t)t2 * tperiod) / sum); }这段代码在定点DSP上有一个隐患:除法运算比较慢。10kHz中断下每秒执行一万次,2407勉强扛得住,但如果中断里还要做电流环、通讯等其它任务,就要考虑用查表法或者把除法简化成移位加修正。我当时为了省时间,直接设了一个限制:调制比超过0.95就强制钳位到0.95,牺牲一点点电压利用率,换来中断时间的大幅缩减。
5. 实测波形与参数整定
5.1 空载先看马鞍波
代码烧进去之后,先不要带电机,直接在开环V/F模式下给一组Uα、Uβ正弦信号,用示波器探头测逆变器输出的线电压,再经过一个低通滤波器看相电压波形。SVPWM最明显的特征是相电压呈现马鞍形,也就是在三相正弦波的基础上叠加了三次谐波,而线电压是纯正弦。
我第一次测到马鞍波的时候,心里的一块石头才算落了地,这说明扇区判断、作用时间计算、比较值装载整个链路是通的。如果你测出来相电压是普通正弦波,基本可以断定算法没有真正进入SVPWM状态,多半是配置成了普通的SPWM生成方式,或者扇区判断后的T1/T2装载表没有生效。
5.2 带着电机调死区和母线电压
空载开环转起来之后,就要开始调细节了。第一是死区时间,2407的DBTCON寄存器可以配置死区,我最初设了2微秒,IGBT模块工作正常,后来为了降低谐波想缩到1微秒,结果电机噪音明显变大,最后又改回2微秒。第二是母线电压Udc的标幺值,Udc采样不准会导致SVPWM的电压基准不对,电机带载时会提前进入过调制状态,表现为电流波形畸变、转矩脉动。
这两个参数调完之后,我习惯看一下稳态电流波形。SVPWM在电机空载时电流应该是比较干净的正弦,THD在合理范围内。如果电流波形有很明显的毛刺,我一般先怀疑死区补偿没做或者采样时刻不对。
5.3 波形异常的常见定位手段
调试SVPWM时,示波器是最好的老师。我先看PWM1和PWM2这一对互补输出的死区波形,确认上下管没有直通;再看三相输出线电压之间是否相差120度,这一步能快速排除相序错误;最后看低通滤波后的相电压是不是马鞍波,这一步能确认调制算法本身是否正确。
很多人在这一步浪费了大量时间,其实只要按“PWM引脚输出 -> 线电压 -> 低通后相电压”的顺序逐级看,很快就能定位问题是在底层外设配置、极性设置还是算法逻辑上。
6. 踩坑实录:三条完整排查链路
6.1 电流波形不对称:比较值装载时机
现象是电机带载后电流波形每半周期明显不对称,一高一低。我用示波器抓了PWM输出,发现高电平宽度在奇数周期和偶数周期不一样。
排查过程分了三步。第一步先怀疑算法算错了,但把Uα、Uβ固定成某个恒定矢量,观察PWM占空比,发现输出稳定,说明算法本身没问题。第二步怀疑死区不对称,但把死区关掉后现象依旧。第三步用仿真器单步跟踪,发现我是在PWM中断的入口装载比较值,而2407的计数器在连续增/减模式下,不同时刻装载比较值对当前周期的影响不同。在波峰附近装载和波谷附近装载,硬件行为有一拍差异。最终把比较值装载操作放到周期中断里,并在当前周期结束时统一生效,问题就解决了。
6.2 一上电就过流:死区与功率极板
现象是第一次上电,没有给任何给定信号,母线就过流报警。排查链路从硬件开始:先断开IGBT驱动信号,测量母线电压正常;然后只打开一路PWM,把死区设为0,结果上下管直通,母线和短路一样。把死区加上去之后再打开PWM,母线电流恢复正常。
根因很简单:DBTCON寄存器配置没生效,死区时间为零。2407的死区配置涉及到外设时钟分频和死区周期寄存器,我最初为了赶进度漏配了分频位,导致死区实际为0。这块的经验是,任何PWM驱动IGBT的调试,第一步永远是确认死区时间,否则后续所有波形都不可信。
6.3 中断超时导致波形乱跳:浮点陷阱
现象是电机低速运行正常,速度稍一提高就会突然抖动,电流波形像被打散了一样。我用示波器看PWM输出,发现偶尔会丢掉一个完整的PWM周期,导致波形乱跳。
后来用仿真器统计中断执行时间,发现中断服务程序整体执行时间在最坏情况下超过了50微秒,而我在中断里用了两处float除法。2407执行一次软浮点除法的时间非常长,而且编译器还可能因为中断现场保存浮点寄存器而额外浪费大量周期。把这两处除法换成定点乘法加移位之后,中断执行时间降到了15微秒以下,波形恢复正常。
这件事给我一个很深的教训:在2407上做控制算法,不是写完功能就完了,还要时刻关注中断执行时间预算。定点化不是可选项,而是标配。
最后分享一点个人经验
如果你也是第一次在2407这类定点DSP上做SVPWM,我的建议是从开环V/F模式起步,把发波链路彻底调通了再上闭环。不要一上来就跑FOC或者带重载,否则问题混在一起,定位起来非常痛苦。调试时可以把扇区号、T1、T2、CMPR1到CMPR3这些中间变量打包成一个结构体,在CCS的变量窗口里实时观察,比看一坨分散的全局变量直观得多。还有一个我自己常用的技巧:在给定Uα、Uβ为恒定值时检查PWM占空比是否符合手算结果,这一步可以在不上电、不接电机的情况下提前暴露大量逻辑错误。SVPWM本身并不复杂,但在老平台上写C实现,真正考验人的是极性和定点细节。
本文还有配套的精品资源,点击获取