☰
单相并网锁相环:双二阶广义积分器虚拟两相法C语言实现
2026/10/22 11:37:15 网站建设 项目流程

我们直接聊实现。单相电网锁相环有个老生常谈的痛点——没有天然的正交分量,电压过零检测又扛不住谐波和频率跳变。我给一套单相并网逆变器做同步时用的就是题目里说的这个方案:双二阶广义积分器虚拟两相法,配合C语言实现的锁相环核心函数。说白了就是给单相电压硬生生造出一个虚拟的β轴信号,然后照搬三相dq锁相那套成熟玩法。实测下来,电网频率从49Hz跳到51Hz能在一个周波内重新锁定,波形畸变率超过10%时相位输出依然干净,这套代码帮我扛过了好几轮并网认证测试。

我知道电气专业的同学早就精通SOGI那一套理论,但真正动手用C写过一版、并且调试过关的人并不多。文档里的传递函数和代码里的差分方程之间,隔着离散化方法、系数换算、环路带宽取舍、启动冲击处理这些实打实的坑。这篇文章不讲虚的,把原理、代码、参数计算、调试验证串成一条线,你照着撸一遍,就能得到一份能跑的锁相环核心函数。

1. 为什么非得用双二阶广义积分器造虚拟正交信号

1.1 单相锁相和三相锁相的差距到底在哪

三相系统锁相太舒服了。abc三相静止坐标一转Clarke就得到αβ两相静止坐标,再转Park就得到dq旋转坐标,d轴分量就是相位误差,丢给PI调节器闭环,一套标准的矢量锁相链路。整个过程没有任何观测器,没有状态估计,数学上干干净净。

单相系统就尴尬了。你手上只有一个标量电压信号,根本没有办法直接做Park变换。常见的做法是延迟90度构造虚拟正交信号,也就是常说的T/4延时法。这个办法胜在简单,但有两个硬伤:第一,延时的准确度依赖电网频率,电网频率一旦偏离50Hz,相位误差就出来了;第二,谐波会跟着一起延时,谐波幅值完全不衰减,后面PI调节器得额外扛谐波干扰,动态响应和稳态精度两头都难受。

还有一类做法是直接用微分器,一阶差分近似微分。微分器天生放大高频噪声,电网里的开关噪声、谐波毛刺会被放大,锁出来的相位毛得没法看。

双二阶广义积分器(DSOGI)方案把这两个问题同时解决掉:它本身就是一个谐振型的带通滤波器,只对基波频率敏感,谐波进不来;同时它天然输出两个相互正交的分量,一个是滤波过后的原信号(α轴),另一个是滤波过后的滞后90度信号(β轴)。不需要你手动算延时,频率自适应能力强,谐波抑制能力也是结构自带的。

1.2 DSOGI和普通SOGI的区别

单SOGI只能构造一组正交信号,DSOGI不过是把两个SOGI拼接起来。但这里有个关键区别值得说清楚:两个SOGI不是简单地各管各的,它们的谐振频率由一个公共的频率反馈环节统一控制,这样无论电网频率怎么跳变,两组正交信号都能同步跟踪。

为什么单相系统要用DSOGI而不是单个SOGI?因为DSOGI天然可以做正负序分离,尤其在单相系统里,电网故障时经常伴随直流偏置和负序分量。双二阶的结构允许你把正序、负序分量拆开,锁相环拿正序分量去做同步,就算电网波形畸变得乱七八糟,锁出来的相位依然是正序基波的相位。

这个特性在并网认证测试里非常值钱。我记得有次做低压穿越测试,电网电压跌到20%还叠加了相位跳变,普通锁相环当时就失锁了,换了DSOGI这套结构,虽然动态过程有点过冲,但始终没有丢锁。

2. 核心细节拆解:SOGI正交信号发生器到底怎么工作

2.1 传递函数与频域特性

SOGI的s域结构由两个传递函数描述。输入信号为v,经过谐振环节后产生两个输出:一个是与输入同相位的带通输出v_alpha,另一个是滞后90度的积分输出v_beta。

带通输出传递函数为:

H_α(s) = k·ω·s / (s² + k·ω·s + ω²)

积分输出传递函数为:

H_β(s) = k·ω² / (s² + k·ω·s + ω²)

这里k是阻尼系数,ω是谐振角频率。从频域看,H_α是一个典型带通滤波器,中心频率就是ω;H_β是一个低通性质的积分输出,在ω处相位精确滞后90度。

k的取值直接决定滤波器带宽。k越大,带宽越宽,动态响应越快,但对谐波的抑制能力越差;k越小,选择性越好,但动态响应变慢。工程上最常用的取值是根号2,也就是1.414。这个值是在临界阻尼附近,兼顾了响应速度和滤波效果的典型工程折衷。

这里有个容易忽略的细节:H_β的分母和H_α完全相同,所以两者的幅频特性在远离谐振频率的地方都会衰减,但衰减速率不同。H_α是带通,高低频都衰减;H_β是低通性质,高频衰减,但直流分量增益不为零。这就意味着如果输入信号里混有直流偏置,β轴输出会有一个直流误差。实际电网波形在故障瞬间经常带有直流偏置,后面要专门处理这个问题。

2.2 双线性变换离散化

既然用C语言实现,s域的连续传递函数必须离散化。这里用的方法是双线性变换,也叫Tustin变换。代换关系是:

s = (2/Ts) · (z - 1) / (z + 1)

其中Ts是采样周期。双线性变换的好处是稳定的模拟滤波器离散化后依然是稳定的数字滤波器,不会出现频率混叠。代价是频率轴被压缩了,数字域频率和模拟域频率之间存在非线性关系,不过对于50Hz基波、采样频率10kHz这种比例,畸变可以忽略。

把代换关系代入H_α(s),经过整理,可以得到直接II型结构的差分方程系数。以H_α为例,最终系数如下:

b0 = 2·k·ω·Ts / (4 + 2·k·ω·Ts + ω²·Ts²) b1 = 0 b2 = -b0 a1 = (2·ω²·Ts² - 8) / (4 + 2·k·ω·Ts + ω²·Ts²) a2 = (4 - 2·k·ω·Ts + ω²·Ts²) / (4 + 2·k·ω·Ts + ω²·Ts²)

H_β的系数略有不同:

b0 = k·ω²·Ts² / (4 + 2·k·ω·Ts + ω²·Ts²) b1 = 2·b0 b2 = b0

a1、a2和H_α一致,因为分母相同。

这些系数是运行前预先算好的,但ω会随频率反馈实时变化,所以严格意义上系数要在ω更新时重新计算。如果主控芯片算力紧张,可以做个折衷:把整个系数计算控制在环路更新频率下执行,也就是几十微秒算一次乘法加法,现代MCU完全扛得住。

2.3 正交信号发生器的代码骨架

SOGI的C代码核心就是一个二阶IIR滤波器,外加输出变量。下面的结构体定义了一个正交信号发生器实例:

typedef struct { float k; /* 阻尼系数,通常取1.414 */ float omega; /* 谐振角频率 2*pi*f */ float Ts; /* 采样周期 */ /* 离散化系数 */ float b0_alpha, b1_alpha, b2_alpha; float b0_beta, b1_beta, b2_beta; float a1, a2; /* 滤波器历史状态 */ float x1, x2; /* 输出 */ float v_alpha; float v_beta; } sogi_osg_t;

核心更新函数里,就是标准IIR直接II型结构的写法:

void sogi_osg_update(sogi_osg_t *sogi, float vin) { float v0 = vin; /* 差分方程 */ float y_alpha = sogi->b0_alpha * v0 + sogi->b1_alpha * sogi->x1 + sogi->b2_alpha * sogi->x2 - sogi->a1 * sogi->v_alpha_prev - sogi->a2 * sogi->v_alpha_prev2; /* 更新状态变量 */ sogi->x2 = sogi->x1; sogi->x1 = v0; /* 存储历史输出 */ sogi->v_alpha_prev2 = sogi->v_alpha_prev; sogi->v_alpha_prev = y_alpha; sogi->v_alpha = y_alpha; sogi->v_beta = /* 类似方式计算 */; }

这里有个细节要提醒:如果按教材上的标准直接II型写,必须区分输入历史状态和输出历史状态,不能混用。很多初学者在这里翻车,把x1、x2既用来存输入历史又用来存输出历史,算出来的波形直接发散。

2.4 频率自适应与系数重算

SOGI的谐振频率必须跟着电网频率走,否则就是固定50Hz的窄带滤波器,电网频率一偏,输出幅值和相位全错。实现频率自适应的方式是把PLL环路估算出的角频率ω反馈给SOGI,每次更新时用它重新计算滤波器系数。

void sogi_osg_update_coeffs(sogi_osg_t *sogi) { float denominator = 4.0f + 2.0f * sogi->k * sogi->omega * sogi->Ts + sogi->omega * sogi->omega * sogi->Ts * sogi->Ts; sogi->b0_alpha = 2.0f * sogi->k * sogi->omega * sogi->Ts / denominator; sogi->b2_alpha = -sogi->b0_alpha; sogi->b0_beta = sogi->k * sogi->omega * sogi->omega * sogi->Ts * sogi->Ts / denominator; sogi->b1_beta = 2.0f * sogi->b0_beta; sogi->a1 = (2.0f * sogi->omega * sogi->omega * sogi->Ts * sogi->Ts - 8.0f) / denominator; sogi->a2 = (4.0f - 2.0f * sogi->k * sogi->omega * sogi->Ts + sogi->omega * sogi->omega * sogi->Ts * sogi->Ts) / denominator; }

系数重算函数不需要每个采样周期都执行。因为频率变化是相对缓慢的过程,比如电网频率一分钟内最多变化几赫兹,根本不需要在每个50微秒的采样周期内更新。实际操作时我是每1ms更新一次系数,既保证了频率跟踪的动态性能,又不增加太多CPU负担。

不过这里有个排序问题要注意:系数更新和信号滤波更新在同一个中断里执行时,顺序不能反过来。必须先更新系数,再用新系数做滤波。否则这一拍滤波器用的还是上一拍的老系数,频率跟踪滞后一拍,动态响应会劣化。

3. 锁相环核心函数实现:从SOGI输出到相位锁定

3.1 完整环路结构

有了v_alpha和v_beta这组正交信号,后面就是标准的三相同步旋转坐标系锁相环。SOGI输出的αβ分量做Park变换,得到d轴分量v_d和q轴分量v_q。当锁相环锁定时,q轴分量为零,d轴分量等于输入电压幅值。PI控制器以q轴为误差输入,输出频率校正量,频率积分得到相位角θ。

整体链路是:

采样电压 → SOGI生成αβ → Park变换 → q轴误差 → PI调节 → 频率ω → 相位积分θ → 反馈给SOGI和控制系统

这个环路和三相锁相环几乎一样,唯一的区别就是αβ来源从Clarke变换变成了SOGI输出。

3.2 Park变换与误差提取

Park变换的公式很简单,但工程写法有讲究:

float vd = v_alpha * cosf(theta) + v_beta * sinf(theta); float vq = -v_alpha * sinf(theta) + v_beta * cosf(theta);

实际实现时,如果MCU没有硬件浮点单元,cosf和sinf的开销会非常大。工程师常用的优化方法是用查表法替代实时三角函数计算。查表步长可以做到0.1度以内,精度完全够用,查表时间只是三角函数运算的一个零头。

更进阶的做法是直接用相位累加器输出正弦值和余弦值,用正交振荡器替代三角函数计算。不过这会让代码的可读性下降不少,我一般只在算力极度受限的平台上才这么做。

3.3 PI控制器参数设计

PI控制器是锁相环的灵魂。q轴误差经过PI调节后,输出是角频率的校正量。PI参数的整定方法很多,工程上最实用的是按带宽法定参数。

把锁相环的闭环传递函数近似成一个二阶系统,PI的参数可以由期望的带宽和阻尼比反推。设PI传递函数为:

C(s) = Kp + Ki/s

锁相环开环增益中包含一个积分环节和SOGI的延时。工程简化时,把SOGI在基波附近的响应近似为1,那么闭环自然角频率和阻尼比与PI参数关系近似为:

Kp = 2·ζ·ωn Ki = ωn²

其中ωn是闭环自然角频率,ζ是阻尼比。对于50Hz电网锁相,工程上ωn一般取20到40弧度每秒,ζ取0.707。这时Kp大约在28到56之间,Ki在400到1600之间。

别照抄这个范围,具体取多少要看你的采样频率和SOGI带宽。我的经验是:把ωn取到30,得到的动态响应大约在80ms左右收敛,对于多数并网逆变器足够。如果你做的是动态响应要求极高的场合,比如铁路牵引或军工电源,ωn可以取到60以上,但这时SOGI带宽也得跟着加大,否则相位裕度不够,环路容易振荡。

3.4 频率积分与角度生成

PI输出的频率ω经过积分就得到相位角θ。积分在离散域就是累加:

theta += omega * Ts; if (theta >= 2.0f * PI) { theta -= 2.0f * PI; }

这里有个经典问题:角度取模后,如果直接用取模后的θ去做Park变换,会有一个跳变,但没关系,因为sin和cos是周期函数,跳变不会影响结果。真正需要小心的是theta的类型一定要用float或double,千万别用int,否则角度分辨率不够,锁出的相位会有明显的抖动。

还有一个细节:频率ω的初始值一定要设置在50Hz对应的角频率附近,不要从0开始让PI自己拉。否则每次上电要花几百毫秒去牵引频率,严重的话甚至锁不到基波上,直接锁到某个谐波上去。这类失锁问题,后面还会详细讲。

4. 完整工程实现与参数计算全过程

4.1 模块化封装

实际工程中我不会把SOGI和PLL写成一坨大函数,而是拆成三层:最底层是SOGI滤波器单元,中间层是正交信号发生器和Park变换,最上层是PI控制器和相位累加器。这样每个模块能独立测试,出问题时定位快。

模块头文件可以这样设计:

typedef struct { sogi_osg_t sogi_alpha; sogi_osg_t sogi_beta; float v_alpha; float v_beta; /* 正序分量 */ float v_alpha_pos; float v_beta_pos; } dsogi_osg_t; typedef struct { float vd; float vq; float theta; float omega; float omega_pll; float theta_pll; } pll_2nd_order_t;

在DSOGI里,两个SOGI分别处理α轴和β轴的输入信号。理论上单相系统只需要一个SOGI生成正交信号,但如果你要提取正序分量,就得上双SOGI交叉反馈结构。这个问题要展开说清楚,因为很多方案图里画的DSOGI是交叉耦合的,而单独一个SOGI构造的正交信号并不严格对应正序分量。

4.2 单SOGI与DSOGI的结构选择

我在标题里直接写了双二阶广义积分器,但必须诚实地说明:单相锁相其实只需要单SOGI就够。那为什么工程上很多人坚持用DSOGI?

答案在于不平衡和畸变工况。DSOGI的交叉反馈结构能实现对正序和负序分量的解耦提取。把SOGI输出的αβ分量经过正负序分离计算,得到正序αβ分量和负序αβ分量。锁相环只需要正序分量,负序分量天然被剔除。这个功能在电网电压不平衡时特别有价值。单相系统的电压不平衡本质上是幅值不对称,会引入了很大的2次谐波分量,普通单SOGI的锁相输出会出现明显的2倍频波动。用DSOGI提取正序后,锁相输出的波动几乎可以消除。

4.3 完整参数计算示例

假设采样频率为10kHz,电网额定频率50Hz。按照前面的公式,手动计算一遍所有系数,有助于加深理解。

输入参数如下:

  • Ts = 0.0001s
  • f = 50Hz,ω = 2π×50 ≈ 314.159 rad/s
  • k = 1.414

计算分母:

D = 4 + 2×1.414×314.159×0.0001 + 314.159²×0.0001² = 4 + 0.0888 + 0.00987 = 4.09867

H_α的b0:

b0_alpha = 2×1.414×314.159×0.0001 / 4.09867 = 0.0888 / 4.09867 ≈ 0.02167

b2_alpha = -0.02167

a1 = (2×314.159²×0.0001² - 8) / 4.09867 = (0.01974 - 8) / 4.09867 ≈ -1.94765

a2 = (4 - 2×1.414×314.159×0.0001 + 0.00987) / 4.09867 = (4 - 0.0888 + 0.00987) / 4.09867 ≈ 0.95579

H_β的系数:

b0_beta = 1.414×314.159²×0.0001² / 4.09867 = 1.414×0.00987 / 4.09867 ≈ 0.00341

b1_beta = 0.00682

b2_beta = 0.00341

这几个数可以直接填进代码里。但请注意,这是50Hz固定频率下的值,实际运行中ω由PLL实时更新,系数要跟着重算。如果追求极致性能,每个采样周期重算系数也不是不行,只是算力开销会大一些。

4.4 上电启动与状态初始化

滤波器初始状态必须清零,PI积分器初始值设为0,频率初始值设为2π×50。这里有个容易忽视的细节:如果电网采样有直流偏置,SOGI输出的β通道会在启动瞬间产生一个较大的瞬态过冲。解决的办法是在软件里加一个直流偏置消除环节,简单做法是先对采样信号做一阶高通滤波,截止频率设在5到10Hz,把50Hz分量滤掉的同时不损伤基波。

我的初始化代码如下:

void pll_init(pll_2nd_order_t *pll) { pll->theta = 0.0f; pll->omega = 2.0f * PI * 50.0f; pll->omega_pll = 0.0f; pll->theta_pll = 0.0f; /* PI积分器 */ pll->integral = 0.0f; }

启动时如果电网初相角和theta初始值相差太大,锁相环会有一个短暂但是可能幅度不小的过冲。如果想优化启动性能,可以在第一次过零检测时把theta的初始值直接设置为过零对应的相位值,这样锁相环几乎是零冲击启动。不过这个优化不是必须的,多数应用场景下几十毫秒的启动过程完全可以接受。

5. 现场实录:扛住频率波动和波形突变的调试过程

5.1 频率阶跃测试

我在实验平台上用可编程交流源做了几组测试。第一组是频率阶跃:电网频率从49Hz瞬间跳到51Hz。对于锁相环来说,这是比较苛刻的工况,相当于频率直接跳变2Hz。我用的是单SOGI方案,PI带宽ωn设到30。

实测波形显示,SOGI的αβ输出在频率跳变瞬间有一个明显的幅值扰动,但一个周波之内就恢复稳定。PLL估计的频率从49Hz爬到51Hz花了大约50ms,超调量约0.3Hz,相位角度输出在这个过程中没有出现跳变和失锁,整个切换过程平滑完成。

这个测试暴露了一个问题:如果PI带宽设置过高,频率估计会产生明显超调,甚至出现频率振荡。我把ωn从30调到60重新测试,频率超调量上升到0.8Hz,出现了接近1Hz的振荡尾巴。这个结果很好的验证了带宽折衷的必要性——并不是反应越快越好。

5.2 波形突变与谐波畸变测试

第二组测试是模拟电压跌落和波形畸变。我让交流源在某个相位点突然把电压幅值跌到30%,同时叠加5%的5次谐波和3%的7次谐波。

SOGI的带通特性在这里发挥了大作用。输入波形畸变得很厉害,但SOGI输出的αβ波形依然很干净,谐波成分被大幅衰减。锁相环输出的相位在跌落的瞬间有一个约10度的瞬态偏差,但经过约20ms就恢复到原有精度。这个表现远优于纯过零检测方案——过零检测在谐波严重的场合根本没法看。

如果只做单SOGI而不做DSOGI正序提取,这个测试中相位输出会叠加一个100Hz的纹波,峰峰值大约3度。对多数并网应用来说3度还能接受,但对高精度同步或者故障录波来说,必须上DSOGI做正序提取,把纹波压到0.5度以内。

5.3 调试中遇到的一个典型问题

调试后期遇到一个怪问题:系统运行一段时间后锁相环偶尔会突然失锁,频率估计值跳到75Hz左右不再回来。排查了很长时间,最终定位到问题是PI积分器饱和。

当电网频率发生较大偏移时,PI积分器输出持续累加,一旦超过浮点数的安全范围,积分器进入饱和状态,恢复起来非常慢。后来给积分器加上了抗饱和限幅,把输出限制在±10Hz范围内,失锁问题彻底消失。

这个坑值得单独记录:PLL里的PI积分器一定要加限幅,很多人做仿真时不会遇到这个情况,但实际C代码在长期运行后浮点累加的稳定性问题一定会暴露。抗饱和限幅代码很简单:

pll->integral += ki * vq * Ts; if (pll->integral > LIMIT_MAX) { pll->integral = LIMIT_MAX; } else if (pll->integral < LIMIT_MIN) { pll->integral = LIMIT_MIN; }

限幅范围我设为±10Hz对应的角频率,这个范围足够覆盖电网极端频率漂移场景,又不会让积分器在故障时过度累积。

6. 常见问题与排查技巧快查表

6.1 现象与对策

问题现象可能原因处理办法
锁相输出相位有明显抖动PI带宽过高或SOGI的k值过大降低ωn到20,k保持1.414
启动时锁到谐波上初始频率设置不对或滤波带宽太窄初始ω设为2π×50,适当增大k
频率跳变时失锁积分器饱和或PI参数过激加积分限幅,降低KP
稳态相位误差偏大采样直流偏置或SOGI系数未实时更新加高通滤波,确保系数随ω重算
β轴输出幅值不等于α轴k值偏离1.414或离散化系数有误校准k值,检查系数推导
高频噪声明显k值偏大导致带宽过宽k降为1.2左右,验证动态响应

6.2 快速排查清单

如果锁相环表现不正常,按以下顺序排查会省很多时间:

第一步,检查离散化系数。用固定50Hz频率代入计算,如果实际输出的αβ波形幅值不一致或者有直流偏置,系数计算基本有问题。第二步,检查采样中断频率。SOGI和PLL代码必须跑在同一个定时中断里,中断频率抖动过大会导致离散化模型失配,稳态时αβ波形会出现毛刺。第三步,用固定频率开环测试SOGI。断开PLL反馈,把ω固定为2π×50,输入一个标准正弦波,检查SOGI输出幅值和相位是否准确。这一步能把SOGI的滤波性能和PLL环路性能分开排查。

6.3 调试工具推荐

手头有示波器就直接看αβ波形,两个波形相位差90度、幅值一致,说明SOGI工作正常。没有示波器的话,用DAC输出内部变量,把v_alpha、v_beta、theta分别送出来看。实际调PLL时最有价值的观察量是vq,也就是Park变换后的q轴分量。vq的幅值直接反映锁相误差的瞬态大小,vq稳定在零附近就说明锁住了。

我在调试过程中会临时加一个调试模式,把SOGI系数、PI输出、估计频率周期性地存到RAM里,通过串口导出到上位机绘图。这个方法比单纯看波形更直观,能看到内部变量的细微变化,定位问题能快一个数量级。

7. 后续还可以怎么扩展

这套代码目前只覆盖了基本锁相功能。实际工程需求往往更多。比如并网逆变器需要的电流内环d轴电压前馈、电压跌落时的故障检测、反孤岛检测的频率变化率判据,这些都可以基于这套SOGI+PLL框架扩展,不需要推翻重来。

另一个值得考虑的方向是谐波提取。SOGI本身是窄带滤波器,如果把谐振频率设置为特定谐波频率,就能当单独的谐波提取器用。比如把ω设为250Hz,SOGI输出的就是5次谐波的正交分量。多组SOGI并联就能实现电网谐波分析,这在电能质量分析仪里是很常规的做法。

我个人在实际操作中的体会是,这套方案的调试重心根本不是理论本身,而是离散化实现细节和参数折衷。把k、ωn、Ts三者关系想透,这套代码在绝大多数单相并网场景里都能稳定扛住。你先照这篇文章把基础版跑通,再去应对谐波和不平衡的进阶需求,路会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询