ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

三相PFC控制系统源码阅读指南:从文件结构到控制算法与保护逻辑

三相PFC控制系统源码阅读指南:从文件结构到控制算法与保护逻辑

很多人第一次拿到三相PFC控制系统的源码时,最大的感受就是懵。几千行文件、几十个函数、一堆中断和宏定义,完全不知道从哪一行开始看。我当初啃这套台达三相PFC控制系统代码的时候,也是花了不少冤枉时间。后来把文件结构、控制链路、保护逻辑一条条捋清楚,才算是真正看懂了它的设计思路。这篇就把我的读代码方法整理出来,从整体框架到核心控制,再到保护逻辑和参数整定,一步步拆开讲,希望对做电力电子控制或者嵌入式电源开发的朋友有帮助。

这套代码本身不算特别复杂,但它的模块划分、中断设计和保护机制非常典型,非常适合用来学习三相PFC控制系统的工程化实现。我的建议是,拿到代码先别急着读函数实现,先把整个程序的脉络摸清楚,再逐层深入,最后才能把代码和实际硬件行为对应起来。

1. 拿到代码的第一件事:先把文件组织和主程序脉络理清楚

1.1 这套代码的文件结构里藏着设计思路

大多数人拿到代码就一头扎进控制算法的C文件里,这是错误的起点。三相PFC控制系统的代码功能说明,首先看的是文件结构,因为文件名和模块划分直接反映出设计者的思路。以我手里这套典型的DSP工程为例,目录大致是这样组织的:

  • main.c:主函数入口,负责初始化调度和主循环任务
  • system_init.c:系统时钟、看门狗、GPIO复用配置
  • peripherals.c:ADC、PWM、SCI/CAN通信等外设寄存器配置
  • pfc_ctrl.c:坐标变换、电压环、电流环、锁相环等核心算法
  • protection.c:故障检测、保护状态机的实现
  • pfc_conf.h:所有参数宏定义,包括开关频率、限幅值、PI参数、保护阈值

你会发现,算法文件和硬件配置文件是分开的。这非常重要,意味着后续芯片换代或者平台更换时,主要工作量集中在system_init.c和peripherals.c,而pfc_ctrl.c里的控制逻辑几乎不需要改动。这种分层设计是所有成熟控制代码的共同特点。

还有一个容易被忽略的文件是pfc_conf.h。很多参数像载波周期、死区时间、电流采样比例系数、电压环限幅值都定义在这里。调试的时候不要到处去翻代码里写死的数字,改这里就行。

1.2 主函数里初始化顺序为什么不能乱

看main.c的时候,建议先看初始化调用顺序。一套典型的主函数长这样:

void main(void) { InitSysCtrl(); // 1. 系统时钟、外设时钟使能 DINT; // 2. 先关总中断,很重要 InitPieCtrl(); // 3. 中断控制器初始化 InitGpio(); // 4. GPIO复用 InitAdc(); // 5. ADC采样配置 InitPwm(); // 6. PWM模块配置 InitProtection(); // 7. 保护相关变量初始化 InitPfcVariables(); // 8. 控制算法变量赋初值 EINT; // 9. 开总中断 for(;;) { StateMachine(); // 主循环:状态机扫描 CommTask(); // 通信报文处理 KeyScan(); // 按键或外部指令检测 } }

这里有个关键细节:必须先关中断,把所有外设和变量初始化完成,再开中断。如果顺序反了,中断可能在变量尚未赋值时就触发,控制代码里读到的是随机值,轻则启动异常,重则直接触发保护或者炸机。

另一个点在于,主循环只放状态机、通信、按键等低频任务,绝对不碰控制算法。原因是主循环的执行时间不确定,可能被某个通信报文处理卡住几毫秒,而控制算法必须固定周期运行,否则PI调节器的积分项和采样周期就乱了,PWM输出也会出现拍频抖动。高频控制全部放在PWM中断里,这也是后面要重点看的部分。

2. PWM中断里那条完整控制链路,才是三相PFC代码的核心

2.1 采样时机如何与PWM载波对齐

看完主函数,下一个问题就是:控制算法在哪里执行?答案是在PWM中断服务程序(ISR)里。三相PFC的开关频率一般在16kHz到20kHz,也就是一个载波周期执行一次完整的控制运算。

标准的做法是用PWM模块在载波周期中点产生ADC触发信号,ADC转换完成后触发PWM中断,进入ISR时直接读取转换结果。为什么要卡在载波中点采样?因为对于Boost型PFC来说,电感电流的开关纹波在载波中点处正好处于平均值附近,这时候采样得到的电流最接近真实平均电流,对控制环路的干扰最小。

如果采样点选在开关管的开关沿附近,采样到的是纹波的尖峰,电流反馈会掺杂大量谐波,控制环带宽稍微高一点就容易振荡。这一点在代码里可能只是一个触发源配置,但背后的逻辑直接影响控制性能。

2.2 从Clarke到Park:电流坐标变换的代码逻辑

三相PFC控制里,网侧三相电流是交流量,直接拿三个交流量去做PI调节,稳态误差是消不掉的。所以代码里必须有坐标变换,把三相静止坐标系下的交流量,变成两相旋转坐标系下的直流量,然后才能用PI实现无静差控制。

坐标变换分两步。第一步是Clarke变换,把Ia、Ib、Ic从abc坐标系转到αβ静止坐标系:

// Clarke变换(等幅值变换) Ialpha = (2.0f/3.0f) * (Ia - 0.5f*Ib - 0.5f*Ic); Ibeta = (2.0f/3.0f) * (SQRT3_2 * Ib - SQRT3_2 * Ic);

第二步是Park变换,把αβ坐标系转到dq旋转坐标系,旋转角度由锁相环PLL输出的电网相位角theta决定:

// Park变换 Id = Ialpha * cos(theta) + Ibeta * sin(theta); Iq = -Ialpha * sin(theta) + Ibeta * cos(theta);

注意,不同版本的代码里,Park变换的正负号定义可能不同,有的把d轴定义在电网电压矢量方向,有的定义在q轴方向。移植时一定要对着硬件采样的相序确认,否则会出现电流环调不上去甚至正反馈的问题。这套代码里theta来自软件锁相环,通过控制q轴电压为零来锁定电网相位,这也是三相PFC代码里非常标准的设计。

2.3 电压环、电流环和前馈环节怎么在代码里配合

坐标变换之后,控制链路就变成经典的电压外环加电流内环双闭环结构。看代码的顺序应该是:

  1. 电压外环:母线电压给定值Vdc_ref减去实际采样值Vdc,经过PI调节器,输出d轴电流参考Id_ref。q轴电流参考Iq_ref直接给0,因为PFC要的就是单位功率因数,不需要无功分量。

  2. 电流内环:Id_ref - Id和Iq_ref - Iq分别送入两个PI调节器,输出两个调制电压Vd_ref和Vq_ref。

  3. 电网电压前馈:在Vd_ref上叠加电网电压d轴分量,这样电网电压波动时,前馈项直接补偿掉一部分,不需要电流环慢慢去调,动态响应快很多。

  4. 反变换:把Vd_ref、Vq_ref反Park变换回αβ坐标系,再通过SVPWM或载波比较生成三相占空比。

代码里的关键一段大概是:

// 电压外环 Vdc_err = Vdc_ref - Vdc_fb; Id_ref = Kp_v * Vdc_err + Vi_v; Vi_v += Ki_v * Vdc_err; // 积分项,注意限幅 LIMIT(Id_ref, -I_MAX, I_MAX); // 电流内环 Id_err = Id_ref - Id_fb; Vd_tmp = Kp_i * Id_err + Vi_id + Vd_ff; // Vd_ff是电网电压前馈 Iq_err = Iq_ref - Iq_fb; Vq_tmp = Kp_i * Iq_err + Vi_iq;

如果你是第一次读这套代码,我建议按这个顺序在代码里找函数:先找哪个函数修改了PWM比较寄存器,再往回找它的输入是谁,一层一层回溯到最外层的电压给定。这样能把整个信号流串起来,比从头到尾顺序读有效得多。

3. 读代码时最容易被忽略的保护逻辑与状态机

3.1 几种工作状态的转移关系

控制代码里除了算法,还有一套状态机。它决定了系统在上电、运行、故障、停机这些阶段分别做什么。三相PFC的状态机一般包含这几个状态:

状态含义PWM输出备注
IDLE待机,等待启动指令封锁所有给定值为零
SOFT_START软启动,电压环逐步爬升输出但限幅防止充电冲击
RUN正常运行正常闭环双环全投入
FAULT故障锁定封锁需要复位指令退出
STOP正常停机封锁母线放电完成

代码里状态转移条件一般在StateMachine()函数中,通过一个switch语句实现。我见过很多初学者只看控制函数,完全忽略状态机,结果调试时发现代码有延迟启动、有随机停机,却不知道问题出在状态转移条件判断上。

3.2 软启动斜坡限流为什么放在电压环之后

软启动是三相PFC代码里特别值得看的一个模块。开机瞬间,母线电容相当于短路,如果不限制电流,整流桥和开关管会承受极大的浪涌电流。代码里通常有两种限流方式:一种是硬件上用预充电阻,另一种是软件上用斜坡给定。

这个系统的软启动逻辑是:软启动状态下,电压环的参考值不是直接跳到额定值,而是一个斜坡函数,从0开始以固定斜率爬升到额定值。同时,电流环的给定值也通过斜坡发生器限制变化率:

// 软启动斜坡 Vdc_ref_ramp += V_RAMP_STEP; if(Vdc_ref_ramp > VDC_NOMINAL) Vdc_ref_ramp = VDC_NOMINAL;

为什么用斜坡而不是直接把给定设成额定值?因为电压环积分器的输出对应着输入电流,如果给定阶跃跳变,积分器迅速饱和,输出电流瞬间冲到限幅值,一样会造成冲击。斜坡给定能把充电电流限制在可控范围内,这个过程在示波器上表现为母线电压缓慢上升,电流波形平滑。

3.3 故障位怎么在中断和主循环之间分工

保护逻辑在代码里有两条路径。一条是硬件快速保护,比如过流比较器输出电平变化,直接在PWM模块层面封锁输出,这个不经过CPU,响应时间纳秒级;另一条是软件保护,比如过温、过压、缺相,通过ADC采样后在中断里判断,置故障标志位。

这套代码里值得借鉴的设计是:严重故障在ISR里只置位和封锁PWM,具体执行停机流程放到主循环的状态机里。原因是ISR里不适合做复杂处理,比如通信关断、故障灯状态切换、故障代码记录等,这些需要时间,放在主循环里更安全。同时,有些故障比如瞬时母线过压,可能有干扰成分,先置故障位,主循环里做几次确认再去停机,可以有效防止误动作。

我遇到过一种情况:某个故障检测在ISR里直接调用了延时函数,结果中断时间被拉长,控制周期乱了,系统直接触发另一个保护。所以看代码时特别留意:ISR里绝对不能有阻塞式延时。

4. 中点电位平衡:三电平拓扑特有的一段代码

4.1 中点偏移的根源与危害

这套三相PFC控制系统用的是维也纳整流器拓扑,也就是三电平结构。正负母线之间有两个串联的电容,中点引出到三相桥臂。理想情况下,正负母线电压各占一半,中点电位为零。但实际运行中,由于三相调制不对称、电容参数不一致、负载不平衡等原因,流进中点的平均电流不为零,两个电容电压会出现偏差。

中点偏移的危害很大。一方面,两个电容电压不均会导致电容应力不同,寿命下降;另一方面,偏得太厉害时,其中一个电容电压可能超过额定值,触发过压保护。所以三相三电平PFC的代码里,必须有一块专门处理中点平衡的逻辑。

4.2 用零序分量修正中点电压的实现方式

读取中点平衡的代码时,核心思路就一句话:把中点电压偏差折算成一个零序分量,叠加到三相占空比上,调整中点电流的流向,把偏差拉回来。典型实现如下:

Vdiff = Vn_fb - Vp_fb; // 中点偏差,正负母线电压差 Vzero = Kp_np * Vdiff; // 比例调节,可加积分 LIMIT(Vzero, -VZ_MAX, VZ_MAX); // 限幅,防止过调制 DutyA_new = DutyA + Vzero; DutyB_new = DutyB + Vzero; DutyC_new = DutyC + Vzero;

为什么这里用比例调节而不是加积分?根据我的实测经验,中点平衡的动态要求不高,比例足够;加了积分之后,容易出现低频振荡,尤其在轻载情况下,更容易观察到中点电压来回摆动。

调试时可以通过在正负母线之间接一个不平衡负载来验证平衡效果。如果中点电压偏差在几个伏特级别,同时没有低频振荡,说明这段代码的参数是合理的。如果偏差很大或者振荡,先检查Vzero的限幅值,再看采样是否正确。

5. 从代码到实际运行:控制参数整定与联调经验

5.1 控制周期和PI参数的经验范围

代码读懂了,最终要落到实际运行上。第一个要确认的就是控制周期。这套系统的PWM开关频率如果是20kHz,控制周期就是50微秒,每个周期执行一次完整控制算法。控制周期的准确性直接影响PI参数设计。

电流环的带宽经验上取开关频率的1/10到1/20,也就是2kHz左右,对应PI参数中的比例项支持快速的电流响应;电压环带宽取10到20Hz,因为它的任务是稳住母线电压,不需要快,带宽太高反而容易和电流环发生交互振荡。实际调试时,先单独整电流环,让电流波形快速跟踪给定,再加上电压环,逐级确认稳定。

5.2 上电联调的正确顺序

把代码烧进控制器之后,千万别直接上满功率。我建议按下面这张表的步骤来:

步骤操作内容需要观察的现象
1仅给控制电(不给功率电)PWM波形是否正常,占空比是否符合给定
2接入调压器,输出低压(比如50V)母线电压是否跟随建立,有无异常震荡
3调压器逐渐升压到额定值软启动过程中电流是否受控
4带少量负载闭环运行电流波形、相位、THD是否正常
5逐步加到满载温升、效率、保护阈值是否在范围内

每一步都要看波形,不要只看仪表平均读数。电流波形至少要看启动冲击、稳态纹波、负载突变响应三种情况。

5.3 现场最容易踩的几个坑

挑几个我在联调中真实遇到过的坑,集中说一下:

  • 采样比例系数对不上。电流霍尔或者采样电阻的变比和代码里的I_GAIN不一致,导致电流反馈值偏大偏小,闭环特性完全偏离设计。排查方法是给一个已知直流电流,对比ADC读数和实测值。
  • 数字滤波滞后。为了滤采样噪声,很多代码里加了FIR滤波,时间常数太大,电流环相角裕度下降,轻载就振荡。滤波时间常数不要超过控制周期的两倍。
  • PI积分限幅缺失。有些版本的代码在PI调节器里没做积分项限幅,启动时积分饱和,超调直接触发过压保护。检查每个PI调节器的输出和积分项,限幅必须都做。
  • 死区时间补偿缺失。轻载或空载时,死区引起的电流畸变特别明显,THD超标。三相PFC代码里一般会加简单的死区补偿,根据电流方向调整占空比。
  • PLL带宽太低。电网频率波动稍微快一点,锁相环失锁,电流相位错误,系统直接被过流保护。观察PLL输出的theta是否平滑跟随电网电压过零点。

6. 关于这套代码的移植与复用

6.1 把算法模块和硬件模块分开的好处

读完整套代码后你会发现,真正有价值的不是某个具体芯片的寄存器配置,而是pfc_ctrl.c里那些坐标变换、PI调节器、斜波发生器等算法函数。它们和硬件无关,完全可以作为独立的模块复用到其他项目里。

我后来的做法是,把这些算法函数整理成独立的库:输入是采样值,输出是占空比给定,中间不出现任何寄存器操作。这样无论是换DSP还是换MCU,只需要重新实现底层ADC和PWM的接口,控制算法原样搬过去就能跑通。这套代码本身的模块划分就是朝着这个方向设计的,读代码时要刻意学习这种分层思想。

6.2 从定点DSP移植到浮点MCU时的改动点

如果你是奔着移植去看这套代码的,有几个改动点必须清楚。老一代DSP控制代码大量使用定点数和IQ格式,也就是Q格式小数运算,代码里随处可见_iq类型和_IQmpy这样的宏。移植到浮点MCU时,这些全部可以改回标准的float直接运算,运算精度还更高。

但要注意,Q格式下的数往往需要统一一个全局的Q值,比如Q24,所有数值都缩放到这个Q值下。移植时如果只把类型改成float而忘了去掉对应的定标宏,运算结果会差得离谱。我见过一次移植后电压环完全失控的情况,后来发现是一个PI参数的宏定义没有从_IQ(0.02)改成0.02f。

另外,PWM模块寄存器结构在不同型号芯片上差异很大,中断服务程序的入口写法也不同。移植时先跑通开环PWM输出,再逐个模块替换,比一次性全部重写靠谱得多。

对我来说,这套三相PFC代码最大的价值不是具体每一行怎么写,而是它展示了一套完整的工程化控制程序应该具备的结构——分层的模块、固定的控制周期、严格的状态机和保护机制。读代码的时候,我习惯先把信号流图画在纸上,把每个环节的输入输出标清楚,再动手逐行读,效率高很多。如果你刚开始接触这套代码,不妨也试试这个方法,别急着改参数。

RELATED READING

延伸阅读

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