SOGI-PLL锁相环C语言实现:原理、离散化与调试实战
简介本资源是一套面向电力电子与嵌入式开发工程师的二阶广义积分器锁相环SOGI-PLLC语言实现代码专为单相电网相位实时跟踪场景设计适用于基于DSP的实时控制系统开发与HIL半实物验证。压缩包共4个文件2个.c源文件、2个.h头文件总大小仅2KB结构精炼头文件定义SOGI-PLL核心参数、状态结构体及函数接口源文件分别实现SOGI信号生成与正交提取、PLL闭环控制逻辑含鉴相、滤波与VCO相位更新代码高度模块化且注释清晰便于移植至CCS开发环境并集成到逆变器、有源滤波器或智能电表等实际项目中。目前已有6398人学习下载读者可直接获取经DSP平台实测、HIL硬件在环验证通过的完整算法实现掌握SOGI提升动态响应与抗扰性的关键设计思路并快速构建高精度数字锁相系统。 做并网逆变器、UPS和PFC的朋友基本都绕不开锁相环这三个字。电网电压的相位和频率一旦测不准电流波形乱掉还算小事严重的时候直接过流保护板子上一排红灯亮得人头皮发麻。最近整理项目代码发现不少同事还在用过零检测或者干脆用一阶的低通去凑相位电网谐波稍微大一点就拉不住。所以这次我把二阶广义积分器锁相环SOGI-PLL的C语言实现单独拎出来从原理、离散化到代码再到调试中踩过的坑一次性讲清楚。我这里写的是自己调过的工程思路不是那种跑不通的演示demo适合搞嵌入式电源、单相并网、储能变流器和电机控制的同学。SOGI-PLL最大的好处是能从单相电压信号里构造出一组互相正交的alpha/beta信号然后像三相锁相环那样用Park变换把相位误差解出来再用PI把频率和相位拉准。相比过零检测它没有半周期延时相比电网电压直接过低通然后atan2它对谐波有一定的抑制能力动态响应也更快。下面我按实际项目推进的顺序来讲后面每一段都有可以直接抄的代码和参数。1. 为什么最终选了SOGI-PLL1.1 单相系统缺的就是一个“虚拟正交相”三相锁相环做起来很舒服天然有abc三相Clarke变换之后得到alpha/beta再用Park变换相位误差一目了然。但是单相系统只有一个电压采样值没有天然的正交信号。做过单相并网的人都知道如果直接把输入电压当成alpha轴那beta轴从哪里来有人对输入信号做延时90度比如存储四分之一个周期数据再拿出来用这在频率固定、谐波少的时候勉强能用但电网频率一旦从50Hz漂到49.8Hz延时出来的角度就不是准确的90度了锁相误差会跟着频率波动。还有人直接用滤波电容弄一个移相网络问题同样出在频率适应性上。SOGI的思路不同它本质上是一个自适应谐振器以当前估计频率为中心做滤波。进入SOGI的原始电压被滤出两个信号一个和输入同相一个滞后90度。这两个信号的幅值在中心频率处都等于输入幅值而且中心频率会随PLL估计的频率实时调整。这样一来不管电网是49Hz、50Hz还是51HzSOGI的输出都保持正交关系整个锁相环也就有了在全频段工作的能力。1.2 过零检测、单相dqPLL、SOGI-PLL三选一过零检测在工程里仍然大量存在因为它简单、可靠洗衣机电机调速、电饭煲加热控制这种对相位精度要求不高的场合完全够用。但过零检测的致命缺点是更新率极低一个工频周期只更新一次相位如果是50Hz相当于每20ms才跳动一次。等并网逆变器去做电流波形同步时这个刷新率显然不够看。直接对输入电压做dqPLL也不是不行但要注意如果只用一个电压量强行构造静止坐标系比如把输入当成alpha轴用延时或差分构造beta轴相位误差里会带上幅值和高次谐波分量导致PI需要反复校正动态响应慢而且对直流偏置极其敏感。SOGI-PLL等于把信号整形、正交分解、锁相三个环节叠在一起。它虽然比过零检测复杂但相比其他单相锁相方案在谐波抑制、动态速度和实现复杂度之间平衡得最好。这也是它被大量用在单相并网逆变器和UPS中的原因。1.3 从并网需求反推方案实际项目中锁相环的需求可以拆成几条第一稳态相位误差要小最好在一个采样周期内几乎看不到相位抖动第二当电网相位跳变或频率漂移时PLL能在几十毫秒内跟上第三对5次、7次谐波和直流偏置要有一定的免疫能力。如果只用过零检测需求一和需求三都无法满足。如果只用简单的dqPLL需求三可能会让你在滤波参数上反复取舍滤得狠了动态慢滤得浅了相位抖。SOGI-PLL天然有一个二阶谐振滤波器谐波抑制能力比纯积分型PLL好动态响应可以通过PI参数整定所以最终我选了这个方案。2. SOGI-PLL的原理拆解与参数设计2.1 SOGI的传递函数和输出特征SOGI的连续域传递函数有两路输出。以输入电压为v同相信号记作v_alpha正交信号记作v_beta那么同相输出是一个带通滤波器H_alpha(s) (k * omega * s) / (s^2 k * omega * s omega^2)正交输出是一个低通滤波器H_beta(s) (k * omega^2) / (s^2 k * omega * s omega^2)其中omega是当前估计角频率k是阻尼系数通常取根号2也就是0.707阻尼比。把s j*omega代进去可以看出两个频率响应在中心频率处的幅值增益都是1alpha输出相位为0beta输出相位滞后90度。这正好是我们需要的“正交”信号。k的取值直接影响滤波器的带宽。k越大SOGI谐振器选频带宽越宽动态响应越快但对谐波和噪声的滤除能力变差k越小带宽越窄滤波效果好但跟踪频率变化的能力会下降。工程里我常用k 1.414前级再加一点抗混叠滤波系统总体稳定。如果现场谐波非常严重可以把k降到1.0到1.2之间代价是启动时间会稍微长一点。2.2 锁相环闭环Park变换加PI整定有了v_alpha和v_beta之后剩下的结构和三相锁相环就一样了。假设PLL当前角度为theta用Park变换把alpha/beta旋转到d/q旋转坐标系vd v_alpha * cos(theta) v_beta * sin(theta)vq -v_alpha * sin(theta) v_beta * cos(theta)当theta等于电网相位时vd峰值等于输入电压峰值vq等于0。如果theta和真实相位有偏差vq约等于输入幅值乘以相位误差也就是说vq把相位误差变成了一个接近直流的误差量。用PI控制器把vq拉回0输出的控制量就是角频率修正量。这里有个细节很多人第一次调的时候会踩坑vq的物理单位是伏特如果电网电压是311V峰值同样的相位误差5度下vq也有27V左右如果电压跌落到100Vvq可能只有8.7V。同样的PI参数在额定电压下合适在电压跌落时动态会突然变差。更好的做法是把vq做归一化处理除以额定电压峰值让PI控制器的输入变成标幺值。下面代码里也是这么处理的。2.3 离散化双线性变换和差分方程连续域公式再漂亮落到嵌入式里也得转成差分方程。SOGI的离散化方法很多前向欧拉实现最简单但在采样频率较低时误差比较大后向欧拉稳定性好一点但相位响应会有偏移。我推荐用双线性变换也就是Tustin变换公式是s (2 / Ts) * (z - 1) / (z 1)把s代入任意一个SOGI传递函数整理成标准IIR滤波器形式y[n] a1 * y[n-1] a2 * y[n-2] b0 * x[n] b1 * x[n-1] b2 * x[n-2]以alpha带通输出为例分子是komegas双线性变换后得到分子系数b0、b1、b2分母得到a1、a2。由于本文代码里会实时更新omega系数所以这些系数需要在每次频率更新后重新算一次。算起来很简单但每个omega变化点都要调用。双线性变换的好处是无条件稳定波形不会因为频率突变而发散相位特性也和连续系统比较接近。缺点是如果采样频率太低比如低于2kHz双线性变换在高频段会有频率畸变。好在并网电源的采样频率一般都在8kHz到20kHz电网基波50Hz离Nyquist频率很远畸变可以忽略。2.4 PI参数计算示例PLL的PI参数可以直接用二阶系统整定。把链路近似看作单位反馈线性化后闭环特征方程为s^2 Kp * s Ki 0所以自然角频率omega_n和阻尼比zeta满足Kp 2 * zeta * omega_nKi omega_n^2在标幺化输入的前提下我习惯先把阻尼比固定为0.707然后按需要的带宽选omega_n。比如希望PLL带宽约30Hz那么omega_n取2 * pi * 30 188.5 rad/sKp约266Ki约35530。如果现场噪声大把带宽降到20HzKp约177Ki约15800。别直接把这两组数字在所有项目里通用因为采样率、电压峰值、前级滤波参数不同最终参数多少会有差异但按这个思路去调方向不会错。3. C语言代码实现3.1 结构体和初始化我下面这段代码基于一个10kHz采样周期的实际项目简化而来核心是把SOGI滤波、Park变换和PI整合在一个结构体里。先定义结构体和常量#include math.h #include stdint.h #define SOGI_PLL_PI 3.14159265358979323846f #define SOGI_PLL_2PI 6.28318530717958647692f #define SOGI_PLL_K 1.4142135623730950488f typedef struct { float Ts; // 采样周期单位s float k; // SOGI阻尼增益通常取根号2 float omega0; // 工频标称角频率单位rad/s float omega; // 当前估计角频率单位rad/s float theta; // PLL输出相位单位rad // SOGI滤波器输入历史 float v_in[2]; // v[n-1], v[n-2] // SOGI两路输出历史 float a_out[2]; // alpha滤波输出历史 float b_out[2]; // beta滤波输出历史 // IIR滤波系数 float a1, a2; float b0a, b1a, b2a; float b0b, b1b, b2b; // 锁相环PI参数输入为标幺化vq float Kp, Ki; float integ_err; // PI积分器输出单位rad/s // 频率限幅 float omega_min, omega_max; // 额定电压峰值用于标幺化 float v_peak; float inv_v_peak; } SogiPll; static void sogi_pll_calc_coeff(SogiPll *pll) { float Ts pll-Ts; float k pll-k; float w pll-omega; float den 4.0f / (Ts * Ts) 2.0f * k * w / Ts w * w; pll-a1 (-8.0f / (Ts * Ts) 2.0f * w * w) / den; pll-a2 (4.0f / (Ts * Ts) - 2.0f * k * w / Ts w * w) / den; float b0a (2.0f * k * w / Ts) / den; pll-b0a b0a; pll-b1a 0.0f; pll-b2a -b0a; float b0b (k * w * w) / den; pll-b0b b0b; pll-b1b 2.0f * b0b; pll-b2b b0b; } static void sogi_pll_reset(SogiPll *pll) { pll-v_in[0] pll-v_in[1] 0.0f; pll-a_out[0] pll-a_out[1] 0.0f; pll-b_out[0] pll-b_out[1] 0.0f; pll-theta 0.0f; pll-integ_err 0.0f; pll-omega pll-omega0; sogi_pll_calc_coeff(pll); } void sogi_pll_init(SogiPll *pll, float Ts, float grid_freq, float v_peak, float Kp, float Ki) { pll-Ts Ts; pll-k SOGI_PLL_K; pll-omega0 SOGI_PLL_2PI * grid_freq; pll-omega pll-omega0; pll-v_peak v_peak; pll-inv_v_peak 1.0f / v_peak; pll-Kp Kp; pll-Ki Ki; pll-omega_min SOGI_PLL_2PI * (grid_freq - 10.0f); pll-omega_max SOGI_PLL_2PI * (grid_freq 10.0f); sogi_pll_reset(pll); }初始化时要把Ts、电网频率、额定电压峰值和PI参数都传进去。Ts一定要和实际中断周期一致否则频率跟踪到稳定后会有静差。v_peak建议取实际额定电压的峰值比如单相220V RMS对应的峰值是311V三相线电压380V RMS对应的相电压峰值也是311V左右具体按你输入采样点的比例换算。3.2 核心滤波和PLL更新函数SOGI两路输出可以写成两个独立函数但注意输入历史寄存器是共享的。让alpha通道先处理beta通道再处理两个滤波器拿到的都是同一个输入历史只是输出系数不同。这样能省一点RAM也不会破坏时序。static float sogi_pll_alpha(SogiPll *pll, float v) { float y pll-b0a * v pll-b1a * pll-v_in[0] pll-b2a * pll-v_in[1] - pll-a1 * pll-a_out[0] - pll-a2 * pll-a_out[1]; pll-v_in[1] pll-v_in[0]; pll-v_in[0] v; pll-a_out[1] pll-a_out[0]; pll-a_out[0] y; return y; } static float sogi_pll_beta(SogiPll *pll, float v) { float y pll-b0b * v pll-b1b * pll-v_in[0] pll-b2b * pll-v_in[1] - pll-a1 * pll-b_out[0] - pll-a2 * pll-b_out[1]; pll-b_out[1] pll-b_out[0]; pll-b_out[0] y; return y; }有同学会问两个函数都更新了v_in会不会把输入历史搞串了。我们把alpha调用后的寄存器状态理一下alpha先用v和v_in[0]、v_in[1]计算然后做移位紧接着beta再用刚更新完的历史计算两个滤波器拿到的是同一组历史数据所以没有问题。主更新函数如下float sogi_pll_update(SogiPll *pll, float v) { float valpha sogi_pll_alpha(pll, v); float vbeta sogi_pll_beta(pll, v); float costh cosf(pll-theta); float sinth sinf(pll-theta); // Park变换得到d/q分量 float vq -valpha * sinth vbeta * costh; // 标幺化避免电压幅值变化影响PI带宽 vq * pll-inv_v_peak; // PI调节器输出角频率修正量 pll-integ_err pll-Ki * vq * pll-Ts; // 积分器限幅 float max_integ pll-omega_max - pll-omega0; float min_integ pll-omega_min - pll-omega0; if (pll-integ_err max_integ) pll-integ_err max_integ; if (pll-integ_err min_integ) pll-integ_err min_integ; float delta_omega pll-Kp * vq pll-integ_err; pll-omega pll-omega0 delta_omega; if (pll-omega pll-omega_max) pll-omega pll-omega_max; if (pll-omega pll-omega_min) pll-omega pll-omega_min; // 相位积分并做2pi归位 pll-theta pll-omega * pll-Ts; if (pll-theta SOGI_PLL_2PI) pll-theta - SOGI_PLL_2PI; else if (pll-theta 0.0f) pll-theta SOGI_PLL_2PI; // 频率变化后需要重新计算SOGI滤波系数 sogi_pll_calc_coeff(pll); return pll-theta; }PI输出delta_omega是叠加在标称频率omega0上的。这样设计的好处是即使积分器被限幅PLL也会以标称频率继续工作不会出现停机后频率跑飞到几十kHz的情况。theta用omega * Ts累加相当于一个数字压控振荡器相位连续不会出现相位跳变的毛刺。SOGI滤波系数在每次更新后重新计算这在一开始可能让人担心计算量。其实总共也就是几次浮点乘除在10kHz采样率下占用的CPU时间几乎可以忽略。真正值得注意的是系数变化不能跳变太快所以PI输出一定要做限幅尤其是频率突变的时候限制每拍角频率变化量对稳定性很有帮助。3.3 主循环调用示例在定时器中断或控制任务里每次采样到一个新的电压值就调用一次sogi_pll_update#define SAMPLE_FREQ 10000.0f #define GRID_FREQ 50.0f #define V_PEAK 311.0f static SogiPll g_pll; void control_isr(void) { float v_sensor read_voltage_adc(); // 电压采样值定义成实际电压单位V float theta sogi_pll_update(g_pll, v_sensor); // theta可以直接用于Park变换、电流环坐标变换、SPWM相位同步等 } int main(void) { // 初始化采样周期1e-4s频率50Hz额定峰值311VPI参数按上述方法整定 sogi_pll_init(g_pll, 1.0f / SAMPLE_FREQ, GRID_FREQ, V_PEAK, 266.0f, 35530.0f); // 启动定时器中断进入主循环或等待中断 while (1) { // 其他任务 } }如果你用的是DSP、ARM或者其他带FPU的MCU这组代码可以直接用。如果主控是普通8位机浮点计算会吃力建议把float改成Q格式的定点运算下面会简单提一下。4. 调试与实测经验4.1 上电波形异常怎么办我第一次调SOGI-PLL时遇到过最典型的问题是上电瞬间theta快速转圈频率估计在几十赫兹范围内来回扫。后来定位到原因是SOGI滤波器内部状态初始为0但输入电压第一次采样可能正好是311V的峰值相当于给滤波器一个很大的阶跃输入谐振器需要好几个周期才能稳定。这个阶段PLL会误以为看到了很大的相位误差PI拼命修正于是频率来回跳。解决的办法很简单初始化后先让SOGI独立跑几个周期PLLPI暂时不参与或者说在一开始把PI输出清零。更稳妥的做法是给PI积分器加软件启动斜率让omega从50Hz逐渐逼近实际值比如每20ms允许频率变化0.2Hz。这样即使输入阶跃很大PLL也不会失锁。4.2 频率跳变和相位跳变的处理电网频率不会突变但故障或切换场景下会出现频率跳变。比如从一个供电电源切换到另一个相位可能跳30度甚至更多。如果PLL带宽调得比较激进相位跳变会让PI输出瞬间饱和然后超调最后稳定下来。我习惯在代码里给delta_omega或者integ_err做变化率限制。比如每拍最大只允许频率变化0.5Hz换算成rad/s大约是3.14rad/s这样在真实频率突变时PLL能快速跟上但又不会因为一个单点噪声引起频率猛跳。当然这个限制值要结合采样率调整10kHz采样下0.5Hz/采样周期其实已经偏快实际可以用更小值。相位跳变还有一个处理技巧不要在theta累加后直接对delta_omega做限幅而是对相位误差先经过一个斜率器。比如当检测到vq大于某个阈值时说明相位误差很大可以把vq先送到一个限速模块再进PI。工程上这样能显著减少过流风险。4.3 谐波、直流偏移、电压跌落SOGI本身是一个二阶谐振器对远离中心频率的谐波有抑制作用但并不是万能的。电网里常见的5次谐波频率为250Hz距离50Hz较远SOGI能衰减不少但如果是25Hz的次谐波或者150Hz的3次谐波衰减量就有限。遇到谐波很脏的现场我会在PLL的vq后面再加一个移动平均或者二阶低通专门滤掉2倍频以上的波动代价是动态响应变慢。直流偏移是另一个很隐蔽的坑。输入采样链路上的放大器偏置电压或者ADC偏移混在电网电压里会让SOGI的beta输出带上一个直流偏置导致vq中出现与相位无关的误差分量PLL锁出来有固定角度偏差。很多方案在SOGI前加一个直流消除环路原理就是把输入电压先经过一个截止频率很低的高通或者用积分器估计直流分量并把估计值减回去。电压跌落时PLL不能跟着输出电压一起塌。通过标幺化处理vq已经变成相对值所以幅值变化不会直接影响PI动作。但SOGI滤波器的内部状态是实际电压幅度的积分电压从311V跌到100V再恢复内部状态会有瞬态这时PLL会出现短暂的角度抖动。我在项目里会加一个幅值平方检测如果估计电压幅值出现超过20%的突变就暂时把PI带宽降低一半等电压稳定再恢复。4.4 代码层面的避坑清单第一采样周期必须稳定。中断里如果有耗时操作比如通信协议解析或者Flash擦除会造成采样间隔抖动SOGI双线性变换系数是按固定Ts算的Ts一变滤波中心频率就跑偏。就算定时器还在如果实际调用不是严格等间隔PLL也会跟着抖。尽量把PLL放在最高优先级定时中断里或者至少保证调用周期抖动在1%以内。第二PI限幅不能只限制omega还要限制integ_err。很多人只对omega做了饱和积分器继续累积恢复后出现超大超调。我的代码里对integ_err单独做了限制范围就是omega_min和omega_max相对于omega0的偏差。第三theta归位要用减法不要用求余运算。嵌入式平台对浮点数取模会引入比较大的计算开销用减法加判断就够了。由于omega * Ts通常远小于2pi每拍最多减一次不存在连续减多次的问题。第四滤波系数更新不能跳过。如果为了省时间只在omega变化超过某个阈值时才重新算系数PLL动态时会出现细微的相位偏差。双线性变换系数计算量很小建议每个采样周期都算一次。5. 工程落地扩展5.1 不要忽略Ts的抖动前面提到Ts抖动问题这里再展开说。很多MCU的定时器中断里代码会不自觉地做很多其他事比如打印日志。一旦打印耗时超过采样周期的一半下一次PLL采样就会延迟等效于Ts突然变大。我看到过一种比较隐藏的问题代码在中断里开了全局中断允许结果一个耗时过的外部中断插进来把PLL的10kHz节奏打乱了。解决办法是PLL更新函数尽量只做纯计算不做任何IO、不打印、不加无谓的临界区。如果需要把theta给其他任务用就在中断里写一个影子变量主循环里读。这样即使外部逻辑有延迟PLL中断本身依然是严格等间隔执行。5.2 浮点与定点大多数现代MCU带FPU直接写float没问题。但如果主控是STM32F1这类Cortex-M3或者资源紧张的8051就要考虑定点化了。SOGI滤波系数涉及除法定点实现时把所有系数先乘以一个较大的缩放因子比如Q15或Q24然后按分数乘法做。theta积分部分尤其要注意溢出相位本身可以只保留小数部分只要在mod 2pi的范围内运算就行。定点化之后PI参数的单位会变调试时不要直接套浮点值。我的经验是先浮点仿真通过再用fixed-point converter工具或者手算把系数转成Q格式最后在硬件上对比浮点版本的theta波形和定点版本确认误差小于0.1rad再并网。5.3 三相不平衡本文还有配套的精品资源点击获取

相关新闻

最新新闻

日新闻

周新闻

月新闻