
做数字电源这些年三相功率因数校正一直是我心里门槛最高也最容易翻车的一类控制对象。电网谐波、动态响应、无损启动、中点平衡每一个拿出来都可以让人在调试台前耗掉两三周。这篇帖子是我在某工业电源厂商三相PFC控制项目里整理并反复优化过的一套DSP控制代码的功能说明我尽量把模块划分、执行时序、核心算法、参数整定和保护逻辑一次讲透也把最近调试中踩过的坑一并交底。项目拓扑是三电平维也纳PFC控制芯片是一颗200MHz主频、带浮点单元的主流DSP开关频率20kHz算是比较典型的三相PFC工程配置。如果你想入门数字电源或者正在调同类型拓扑这篇应该能帮你省下不少摸索时间。1. 代码骨架模块划分与执行时序1.1 为什么控制算法必须放在中断里拿到一套三相PFC的工程代码第一件事不是看PI参数而是先看它的执行架构。我手上这套代码采取的方案是所有和功率强相关的控制算法全部放在PWM触发的中断服务函数里主循环只做通信、显示和慢速监测。这个选择不是随意拍的。三相PFC是强实时系统在20kHz开关频率下一个控制周期只有50微秒。三相电压电流采样、锁相环更新、坐标变换、两个电流环、一个电压环、空间矢量调制这些运算叠加起来通常要占掉10到15微秒。如果把这些逻辑放到主循环里执行循环周期会因为通信报文、界面刷新等任务而产生明显抖动。负载轻的时候看不出来一加到额定负载电流波形就长毛刺。把控制算法放在中断里的第二个好处是采样时刻可控。ADC采样由PWM载波信号触发采样点固定在三角载波的顶点或底点也就是开关纹波的中心位置。此时采样窗口内的高频噪声最小采样值最有代表性。如果控制任务不在中断里就很难保证采样点与PWM周期的严格同步关系相位误差和采样噪声会直接恶化电流环的性能。提示工程约定上中断里只做控制算法和必要的状态记录绝对不做延时、浮点打印、EEPROOM写入这类不可控耗时的操作。这类任务丢给主循环哪怕慢几十毫秒都无所谓的。1.2 代码模块清单这套工程按功能拆成了几个相对独立的模块每个模块对应一到两个源文件模块职责对外接口采样与信号调理ADC配置、触发源选择、原始码转物理量、标幺化提供三相电压/电流、母线电压、中性点电压的标幺值锁相环PLL电网相位锁定、频率跟踪、失锁判断输出电角度theta、频率freq、锁定标志坐标变换Clarke变换、Park变换、反Park变换输入abc值输出dq值或αβ值控制器d轴电流环、q轴电流环、母线电压环输入误差输出调节量带限幅和抗积分饱和调制与平衡三电平SVPWM、中点电位平衡修正输出三相互补PWM比较值保护与状态机故障检测、状态迁移、封波逻辑故障标志、当前状态通信与监控主循环内的报文解析、参数读写寄存器读写、电压电流监视这个划分方式最大的好处是依赖方向清晰控制器不会直接碰ADC寄存器调制模块也不关心PI参数来源。我调试时最怕一种代码一个PI函数内部悄悄改了全局数组另一个文件又依赖这个副作用最后查问题查到怀疑人生。所以从一开始就约定每个模块只读写自己结构体里的数据模块之间尽量通过函数形参或明确的接口结构体交互。1.3 一个开关周期内的数据流把整个控制流程放进50微秒的时间窗口里看它的执行顺序非常固定一个控制周期开始时ADC采样完成触发中断。中断程序读取采样值先做原始码到标幺值的换算得到三相电压、三相电流和母线电压。紧接着PLL利用三相电压更新电角度。有了角度就可以把三相电流变换到dq坐标系得到id和iq。此时电压环以母线电压给定和实际值的偏差作为输入计算出一份有功电流幅值指令再结合PLL角度生成d轴电流给定id_refq轴电流给定直接给零或者给无功指令。两个电流环各自完成PI运算输出vd和vq再经过解耦补偿和反Park变换得到αβ坐标系下的电压矢量。最后进入SVPWM模块算矢量作用时间、生成三相互补PWM比较值更新比较寄存器中断退出。这个过程执行完硬件自动根据新的比较值输出PWM波形不需要CPU再干预。下一个载波周期重复同样流程。整套系统就是靠这个固定的“采样-运算-更新”节奏稳住电流波形和母线电压的。2. 核心算法模块逐项拆解2.1 同步坐标系PLL的实现细节讨论三相PFC代码绕不开PLL。这套代码用的是同步旋转坐标系PLL。原理上三相电网电压经过Clark变换得到αβ分量再经过Park变换到dq坐标系。当锁相环角度与电网实际角度一致时q轴电压分量为零。利用这个特性把vq作为误差信号送入PI调节器PI输出叠加在电网基频上得到角频率修正量再加权积分就得到实时电角度。核心伪代码如下// 输入alpha/beta电压输出theta pll.err v_beta * cos(theta) - v_alpha * sin(theta); // 等效于vq pll.freq 2.0f * PI * GRID_FREQ pll.kp * pll.err pll.ki * pll.integ; pll.integ pll.err * ISR_PERIOD; theta pll.freq * ISR_PERIOD; // 角度归一化到[0, 2pi) if(theta 2.0f * PI) { theta - 2.0f * PI; }代码里有一个容易忽视的细节误差公式里用的是eq v_betacos(theta) - v_alphasin(theta)而不是先做完整Park变换算vq。两者的数学本质一样但这个写法省掉了两个乘法和一个加法在中断里积少成多。我在一些参考工程里见过先用查表法做坐标变换再取vq的也可以只是实时运算开销会略大一点。PLL的PI参数需要单独整定和电流环电压环的PI是两套参数。一般把PLL带宽设在10到30Hz。带宽太低时跟踪相位变化太慢电网频率漂移或相位跳变时动态响应跟不上带宽太高则会把电网电压里的背景谐波和采样噪声引入到电角度里电流环参考信号跟着抖输入电流波形反而变差。注意启动阶段的处理。PLL还未锁定时不要用它输出的角度去驱动电流环。我的做法是启动初期先使用固定50Hz的开环斜坡角度运行等PLL的锁定标志置位后再切换到PLL角度。这样能避免锁相收敛过程中的大角度误差把电流环搞出浪涌。另外三相电网不平衡时q轴电压里会出现二倍频分量。PLL输出角度会带有二次脉动体现在输入电流上就是2次和4次谐波。对不平衡工况有指标要求的话需要在PLL内部增加陷波器把二倍频分量滤掉。陷波器的实现也不复杂二阶IIR就能搞定代码量大概增加几十行。2.2 Clarke变换、Park变换与标幺化三相PFC控制里坐标变换是家常便饭。等幅值约束的Clarke变换把abc三相分量变换到αβ正交坐标系v_alpha (2.0f/3.0f) * (va - 0.5f*vb - 0.5f*vc); v_beta (2.0f/3.0f) * (SQRT3_2 * (vb - vc));Park变换在PLL和电流环里都要用vd v_alpha * cos(theta) v_beta * sin(theta); vq -v_alpha * sin(theta) v_beta * cos(theta);反Park变换把电流环输出的vd/vq送回αβ坐标系送给调制模块v_alpha_ref vd_ref * cos(theta) - vq_ref * sin(theta); v_beta_ref vd_ref * sin(theta) vq_ref * cos(theta);这些公式本身不复杂真正影响代码质量的是标幺化。我个人强烈建议DSP里所有控制环路的计算都使用标幺值。三相PFC系统里电压基值取相电压峰值比如220V系统对应311V电流基值取额定电流峰值比如10kW设备在400V母线等级下大概取25A左右阻抗基值是两者之比角频率基值取电网角频率2π*50。标幺化的好处是控制环路的PI参数在不同功率等级的项目之间可以直接复用。我做过一个从10kW往30kW搬的活儿硬件电感变了、电流传感器量程变了但控制环路的Kp和Ki在标幺化基础上只微调了不到10%。如果不做标幺化每个项目都要重新推导一次增益既费时间又容易出错。ADC原始码转标幺值这一步要格外仔细。采样芯片或DSP自带ADC的位数不同电流传感器变比不同信号调理电路的增益也不同这里最好统一封装成一个函数输入原始码输出标幺值。同时做一次静态偏置校准没有输入电流时ADC读数对应的偏置要记录并补偿。2.3 电流内环PI解耦项、限幅和积分抗饱和id和iq电流环是整个系统动态性能的根基。三相PFC的电感在dq坐标系下存在交叉耦合项也就是d轴电流变化会影响q轴电压反过来也一样。如果不做处理两个电流环会相互干扰动态响应变差。实际工程里常用前馈解耦// 数字PI位置式实现带输出限幅和积分分离 float id_err id_ref - id_fb; float id_out id_kp * id_err id_ki * id_integ; // 解耦前馈 float vd_ref id_out - grid_w * L * iq_fb vd_feedforward; float vq_ref iq_out grid_w * L * id_fb vq_feedforward; // 输出限幅 if(vd_ref VOLT_LIMIT_MAX) vd_ref VOLT_LIMIT_MAX; else if(vd_ref VOLT_LIMIT_MIN) vd_ref VOLT_LIMIT_MIN;解耦项里用了网格角频率grid_w乘以电感L再乘以另一轴电流反馈这个乘积在满载时可能占到输出电压指令的百分之十几不是可忽略的小量。如果省略解耦动态过程会出现明显的轴间振荡表现出来就是输入电流波形在负载突变时扭动。电流环PI要做限幅而且积分器必须处理抗饱和。我的习惯是积分器输出单独设置一个积分限幅并且小于总输出限幅的一半。这样即使用户把PI参数调得比较激进积分器也不会积累到离谱的程度退出饱和的速度会快很多。抗饱和的具体写法有好几种我常用的是积分分离加条件积分只有当前误差和输出都未进入饱和区时积分项才累加一旦输出顶到限幅值积分器就冻结。这样可以避免一个典型的失控场景输出电压短时间内跌落电流指令冲到限幅值积分器继续往上累积等到负载恢复时积分器里还憋着一大堆余量系统要花很长时间才恢复正常。这个过程表现在波形上就是母线电压过冲和电流拖尾。// 条件积分未饱和才累加 if((vd_ref VOLT_LIMIT_MAX) (vd_ref VOLT_LIMIT_MIN)) { id_integ id_ki * id_err * ISR_PERIOD; // 额外积分限幅 if(id_integ ID_INTEG_LIMIT) id_integ ID_INTEG_LIMIT; if(id_integ -ID_INTEG_LIMIT) id_integ -ID_INTEG_LIMIT; }电流环的运算周期就是开关周期20kHz所以PI离散化直接用前向欧拉就可以。控制频率足够高时离散化误差很小没必要上双线性变换。电压前馈项值得多说一句。d轴电压前馈通常取电网电压在d轴的分量也就是PLL算出来的vd。加入电压前馈之后电流环的输出一开始就给了一个接近稳态的电压值PI只需要修正偏差部分这样静态误差小动态响应也快。实测下来不加前馈时电流环带宽做到3kHz就会有明显的稳态误差加了前馈之后带宽可以再往上推一点。3. 调制与中点平衡三电平SVPWM的代码实现3.1 三电平SVPWM的基本思路维也纳整流器每相输出P、O、N三种状态三个桥臂组合起来有27个开关状态对应19个基本电压矢量。直接穷举这27个状态做优化选择运算量太大也没有必要。实际工程中常用“等效两电平”的方法简化计算。先把参考电压矢量根据所在扇区做坐标平移再在两电平空间里计算基础矢量的作用时间。我代码里的实现大致分四步扇区判断、参考矢量旋转到扇区内的坐标系、计算两个相邻基本矢量作用时间、按七段式或五段式生成三相比较值。扇区判断可以通过比较αβ坐标系下的角度搞定也可以用更简单的数学关系直接判断三相电压的极性组合。后一种方式没有三角函数运算执行速度更快。三相电压的正负号组合一共有6种情况和6个大扇区一一对应。3.2 矢量作用时间计算以第一扇区为例参考电压矢量可以由两个相邻的大矢量和一个中矢量合成。作用时间的计算基于伏秒平衡方程。代码里通常直接把计算过程展开成查表加乘法的形式// 根据扇区选择时间因子 float t1 KA * v_alpha KB * v_beta; float t2 KC * v_alpha KD * v_beta; float t0 T_SW - t1 - t2; if(t0 0) { // 过调制处理等比缩放t1、t2 float scale T_SW / (t1 t2); t1 * scale; t2 * scale; t0 0; }这段代码里有几个关键点。首先是过调制处理。当调制度超过线性区上限时t0计算出来是负的如果直接送去生成PWM输出波形会乱。我这里的做法是等比缩放到总时间刚好等于开关周期这样虽然输出电压会略低于指令值但波形保持连续。当然三相PFC正常工况下不会长期工作在线性区之外过调制一般都是故障或极端输入电压时才会碰到代码有这层保护心里踏实一些。其次是时间分配的对称性。三电平调制要保证脉冲序列在半个开关周期内对称分布这直接关系到电流纹波和谐波含量。七段式发波把零矢量拆成两段放在周期首尾中间按T1-T2-T1的顺序排列每一相每个开关周期只动作两次开关损耗和纹波都兼顾了。死区时间在这里也要处理。比较值写入硬件寄存器时需要为上下管互补PWM对自动插入死区。DSP的PWM模块通常自带死区生成单元配置好死区时间即可。对IGBT死区一般取2到3微秒如果换成SiC MOSFET死区可以压到100到200纳秒。死区时间不匹配时输入电流过零附近会出现畸变这个后面会在调试实录里专门讲。3.3 中点电位平衡算法的嵌入点维也纳拓扑的一个特殊问题是母线中点电位漂移。上下两个母线电容承担的电压不均衡时一是会导致电容应力不一致二是会产生额外的低频纹波严重时威胁开关管安全工作区。中点漂移的根源是三电平各开关状态下中点电流不为零。小矢量和中矢量的作用会改变中点电荷而正负小矢量对中点电流的影响正好相反。所以工程上最常用的平衡方法就是在调制环节根据上下电容电压差调整正负小矢量的作用比例。我的实现里增加了一个平衡系数根据上下电容电压差算出一个小量叠加到各相调制波上。这样不影响主控制器的输出只是让PWM比较值发生微小偏移从而改变中点电流的方向。// 中点电压偏差 float v_mid_err (v_bus_top - v_bus_bot) * MID_BALANCE_KP; // 调制波修正 duty_a v_mid_err * phase_factor_a; duty_b v_mid_err * phase_factor_b; duty_c v_mid_err * phase_factor_c; // 限幅 duty_a clamp(duty_a, DUTY_MIN, DUTY_MAX);这个修正量不是越大越好过大会引入额外的低频电压纹波。工程上我一般会把修正量限制在最大调制波的3%到5%以内。调试时从0开始缓慢增加平衡系数观察上下母线电压差是否收敛找到那个“既能压住偏差又不会让波形变差”的点。需要特别提醒的是中点平衡算法千万不要和电流环的调制波限幅搞混。我见过有人把平衡修正直接加到PI输出上导致电流环受到扰动这种做法等于把两个耦合的控制量强行叠在一起调试时很难分清问题是来自电流环还是平衡环。4. 电压外环、软启动与保护状态机4.1 电压环带宽设计与代码写法三相PFC的电压外环和电流内环带宽要求完全不同。电压环如果带宽太高会为了跟随母线电压的纹波而迫使输入电流产生畸变破坏功率因数。工程经验上电压环带宽通常设计在10到20Hz比电网电压基波频率低一个数量级这样电压环输出的电流幅值指令变化平缓不会引入低次谐波。电压环的输出是d轴电流指令的幅值。这个幅值要和电网相位信息结合才能生成正弦电流指令。我会在PLL锁定正常后把电流指令幅值乘以与电网电压同相位的正弦波形得到id_ref。这样电流环跟踪的参考本身就是正弦形状功率因数和输入电流波形都由这个参考直接决定// 电压环PI输出为有功电流幅值 float i_amp votage_pi_output(v_bus_ref - v_bus_fb); float id_ref i_amp * sin_theta; // 单位功率因数 float iq_ref 0.0f; // 无功指令为零 // 限幅保护 if(id_ref I_REF_MAX) id_ref I_REF_MAX; if(id_ref 0.0f) // 单向PFC不允许倒灌电流 id_ref 0.0f;这里iq_ref直接给零因为三相PFC的目标就是单位功率因数。如果系统有超前或滞后的无功需求iq_ref可以独立给定但一般情况下不需要。电压环还有个常用技巧负载电流前馈。如果三相PFC的后级是逆变器或直流负载我们可以从直流侧电流采样值折算出一份电流指令直接加到电压环输出上。这样负载突变时电流指令几乎同步变化电压环的PI只要处理误差余量母线电压跌落量能减小到原来的三分之一左右。4.2 软启动流程防浪涌的斜坡策略三相PFC上电瞬间母线电容充电电流非常大。如果不做限制整流桥和母线电容都面临很大应力还可能触发过流保护。工程上软启动分硬件和软件两级。硬件侧启动瞬间通过限流电阻给母线电容预充电等母线电压达到一定值后闭合旁路接触器。软件侧的软启动则负责让控制环平稳切入。我的状态机里软启动阶段做了三件事母线电压给定从当前预充电值开始按照一个可配置的斜率缓慢爬到目标值电压环输出限幅从小到大逐步放开有功电流幅值指令最大值也按时间递增。// 软启动斜坡 case STATE_SOFTSTART: v_bus_ref SOFTSTART_RATE * ISR_PERIOD; if(v_bus_ref V_BUS_TARGET) { v_bus_ref V_BUS_TARGET; state STATE_RUN; } i_ref_max SOFTSTART_CURRENT_MAX * ramp_factor; break;软启动斜坡速率要选择合适的值。太快了限流电阻还没完成预充电控制环就已经介入照样有过冲太慢了延长启动时间影响用户体验。我一般会以母线电压从220V爬到400V用时1到2秒为基准来设定速率。同时把电流指令限幅初始值设为额定值的5%到10%然后逐步放宽。4.3 保护状态机软件保护与硬件保护的配合三相PFC属于大功率设备保护逻辑再怎么强调都不过分。我的代码里保护分两层硬件保护用PWM模块的比较器输入引脚直接接外部过流信号通断时间在纳秒级不经过CPU软件保护则在中断里检查ADC采样值判断后决定是否封波和锁存故障。状态机定义了5个状态上电待机、预充电、软启动、正常运行、故障锁存。故障锁存后即使故障源消失也要等用户手动复位或收到复位指令才恢复。这个“锁存”的机制很重要避免故障源还在抖动时系统反复重启、二次冲击。软件保护检查项包括保护类型触发条件响应动作软件过流任意相电流有效值超过阈值立即封波进入故障锁存母线过压母线电压高于上限立即封波故障锁存母线欠压母线电压低于下限并持续一定时间停止调制复位状态机中点不平衡上下电容电压差过大降低功率或停机PLL失锁PLL误差长时间超过范围停止调制重新锁相过温保护NTC检测温度超限保持运行但降额或直接停机故障响应顺序上我先关闭中断里PWM模块输出的使能立刻封波再在故障状态字里记录具体故障源。所有故障源用位域或枚举管理方便通过通信接口上报。此外还有一个容易漏掉的点保护触发后母线电容上还存着大量能量如果直接重新上电会非常危险。所以故障锁存状态下我还会让软件主动泄放启动放电电阻回路观察母线电压跌落到安全值后才允许下一次复位。这个细节在量产设备里非常重要。5. 调试实录与参数整定的坑5.1 我的调试顺序先开环再闭环很多朋友一拿到代码就想上电闭环跑这是最容易炸板子的做法。我吃过亏之后养成了一个固定的调试顺序。第一步不接功率板只烧录代码利用DSP的仿真器看内部变量。把ADC通道输入切换到内部参考电压确认采样值和预期值一致。这一步能把信号链路的配置错误都筛掉。第二步功率板接低压调压器输出调到50V左右不启动PWM手动给一个占空比固定的PWM波形观察三相电感电流是否平衡电流波形是否正常。这一步是确认硬件采样和PWM通道映射有没有错位。如果PWM相序和采样相序不一致后面PLL和电流环全会出问题。第三步上控制环但跑极轻的负载给定很小的电流指令观察PLL是否锁相、电流环是否收敛。这里我会用一个极小值0.5A作为电流指令看闭环能不能稳定跟随。第四步逐步抬升母线电压给定按软启动流程从低压慢慢爬向额定400V。这一阶段重点监视母线电压过冲和电流波形。最后才是满载和动态负载试验。这个顺序的核心逻辑是每一层风险在投入更大的能量之前都被暴露和排除掉了。我见过有人跳过第二步直接上母线到400V相序反了也不自知最后母线电容炸得整个实验室跳闸。这个真的不是开玩笑。5.2 电流环和电压环PI参数怎么给初值PI参数是三相PFC调试里最容易卡壳的地方但工程上有比较靠谱的估算路径。电流环带宽一般取开关频率的十分之一到五分之一。20kHz开关频率下电流环带宽定在2kHz到4kHz。电感量L是已知量电流环Kp和Ki可以从系统开环传递函数反推。对电流环被控对象近似为一阶惯性环节L/sKp的初值可以用带宽对应的感抗来估算Kp约等于L乘以2π乘以带宽。以3mH电感、3kHz带宽为例Kp初值约为56然后根据标幺化系数换算成实际代码值。Ki设置为Kp的1/10到1/5先留足相位裕度波形稳定后再逐步提高。电压环带宽低得多10到20Hz就够了。以母线电容容量C和额定负载功率P估算电压环Kp初值取电容充电电流和误差电压的比值Ki再取Kp的1/5到1/10。给初值的时候宁可保守不要激进。低带宽只是动态响应慢不会导致系统不稳定Kp给大了现场会直接振荡。我自己调试时的指标参考如下环路带宽目标Kp初值思路Ki初值思路电流内环2-4kHz电感感抗对应增益Kp/8左右电压外环10-20Hz电容充放电等效增益Kp/8左右PLL10-30Hz相位余量优先尽量小实测下来按这个路径给初值再通过波形微调通常两三轮就能收敛到一个比较理想的状态。5.3 常见故障现象排查表调试过程中遇到过的典型问题我整理成了一张速查表现象可能原因解决办法输入电流过零畸变死区时间偏大或采样点偏移缩短死区或把采样点移到载波峰谷电流波形高频振荡电流环Kp太大、相位裕度不足降低Kp检查解耦项是否正确母线电压过冲严重软启动斜坡太快、电压环积分饱和放慢斜坡增加积分限幅母线电压低频波动电压环带宽过高或负载前馈缺失降电压环带宽加入负载前馈PLL锁不住相电网电压太低或不平衡严重增加PLL陷波器调整初始化角度某相电流明显偏大采样偏置未校准或相序接反重新校准ADC偏置检查接线相序中点电压偏差持续增大平衡系数太小或负载严重不平衡增大平衡系数检查电容容量是否一致满载保护误触发过流阈值太靠近峰值电流调整阈值加入短时滤波窗口这几个问题的排查思路有共通之处先确认采样对不对再确认算法输出对不对最后才怀疑硬件连接。用仿真器在线抓取变量把关键中间量拉成波形比拿示波器怼功率板更高效。我的习惯是在中断里把电流给定、实际电流、调制比、母线电压这几个变量循环记录到内存数组里故障发生后冻结数组再通过通信接口导出来分析。还有一个小坑三电平PWM死区时间和硬件拓扑强相关。有的平台上下管不是对称的换流路径不一样死区效果也不同。手册上给的建议值只能作为起点最终一定要在满载电流波形下确认死区时间内电流畸变没有大到不可接受的程度。写在最后的经验这套三相PFC代码前前后后折腾了大半年个人体会最深的一点是代码结构比算法本身重要。PLL公式、Clark/Park变换、SVPWM这些都是教科书里有的东西真正拉开工程差距的是执行架构是否清晰、保护逻辑是否完整、参数是否标幺化、调试手段是否方便。把模块切清楚把状态机写严谨把关键变量留出观测接口后续所有调试工作都会顺很多。最后再分享一个我个人的习惯在中断里专门留一个长度为几百字的环形缓冲周期性记录电流给定、电流反馈、母线电压、PLL角度等关键变量。运行中遇到异常不是抓现场而是先冻结缓冲区把故障前几百个控制周期的数据导出来看。很多时候问题波形在示波器上看不出来在这段内部数据里一眼就能定位是哪里先脱离预期的。这个手段救过我很多次推荐各位在做三相PFC调试的时候也试试。