把仿真模型里刚整干净的波形搬到台架上,电机咔嗒抖了两下,随即电流飙升触发过流保护——这是我在做编号“1bf”的无传感器矢量控制项目时,印象最深的一晚。这个项目目标很直接:用一套完整C代码加一套仿真模型,把无传感器矢量控制系统从电流环到速度环全部跑通,不依赖霍尔和编码器。它适合做电机驱动、电动工具、风机水泵、家电压缩机等场景的工程师参考,也适合想系统学一遍FOC控制链路的嵌入式开发者。下面我会把控制链路、代码结构、仿真建模和联调心得串起来讲,尽量做到拿去就能复现。
1. 为什么要把位置传感器去掉:1bf项目的起点与无传感控制的核心逻辑
1.1 霍尔和编码器带来的三种麻烦
做电机驱动的人都知道,带传感器的方案在初始开发阶段确实省心:霍尔能直接告诉你换相点,编码器能给你高分辨率角度。可一旦从样机走向量产,传感器带来的麻烦会成倍放大。
第一是成本与装配。霍尔元件本身不贵,但磁环安装公差、霍尔PWB板固定、线束和接插件都是钱。编码器更贵,光一个光电编码器的价格就能超过一台低价电机驱动器的整机成本。第二是可靠性。电机里面往往有振动、油污、高温,霍尔磁钢在高温下有退磁风险,编码器线在长期弯折之后容易出现断线或接触不良。第三是空间。很多电机应用,比如内置式压缩机、轮毂电机、潜水泵,转子内部根本没有位置去装传感器。
1bf项目启动时我们面对的就是这样一个场景:电机本体紧凑,主控板面积也卡得死,量产目标成本非常敏感。拆掉霍尔和编码器之后,能走的路其实只剩两条:要么用反电动势做方波换相,要么做无传感器矢量控制。方波换相便宜但转矩脉动大、噪音明显,低速特性差;矢量控制在特性上全面占优,代价是算法复杂。1bf最终选了后者,而且把“一套代码兼顾电流环、速度环、观测器、SVPWM和启动策略”定成了交付物。
1.2 FOC的基本链路:电流矢量怎么被旋转到dq坐标系
FOC的直接目标不是控制三相正弦电流本身,而是把三相电流换算到跟随转子磁链旋转的dq坐标系里,让励磁分量和转矩分量解耦。理解这条链路,只需要抓住两次坐标变换。
第一步是Clarke变换,把三相静止坐标系下的ia、ib、ic投影到两相静止αβ坐标系。实际控制器通常采样两相电流,第三相用平衡条件算出来:ic = -ia - ib。Clarke变换写成工程常用的形式:
[ i_\alpha = i_a ]
[ i_\beta = \frac{i_a + 2i_b}{\sqrt{3}} ]
第二步是Park变换,把αβ轴上的交流量旋转成dq轴上的直流量:
[ i_d = i_\alpha \cos\theta + i_\beta \sin\theta ]
[ i_q = -i_\alpha \sin\theta + i_\beta \cos\theta ]
其中θ是转子磁链的电角度。经过这两步,id对应励磁电流,iq对应转矩电流,两个都是直流量,常规PI控制器就能做到无静差控制。电流环输出ud、uq,再经过反Park变换得到uα、uβ,最后交给SVPWM去合成三相桥臂开关矢量。整套链路就是:三相电流采样 → Clarke → Park → 电流PI → 反Park → SVPWM → 三相逆变器。
1.3 转子位置不是测出来的,是算出来的
带传感器的系统里,θ直接来自编码器或霍尔。无传感器系统里,θ必须从电压方程里推出来。永磁同步电机的定子电压方程里,反电动势这一项天然携带转子位置信息:
[ e_\alpha = -\psi_f \omega_e \sin\theta ]
[ e_\beta = \psi_f \omega_e \cos\theta ]
所以只要估算出反电动势在αβ轴上的分量,就能用反正切函数还原转子角度。转速越高,反电动势越大,估算越稳;转速越低,反电动势越小,估算就越困难。这也是为什么无传感器系统在零速和极低速段通常需要额外的启动策略,一般转速起来之后才能真正切入闭环矢量控制。
1bf项目采用的观测器是滑模观测器(SMO),不是扩展卡尔曼或者龙伯格那种花哨的滤波结构。选它的原因很实际:参数少、计算量小、调参直观,C语言里几十行就能实现。后面我会详细讲代码。
2. 功主电路与控制软件骨架:1bf控制器的分层设计
2.1 电流采样方案:低端分流电阻与母线电压监测
无传感器FOC对电流采样的要求比方波驱动高得多,因为每一拍的坐标变换和PI都依赖瞬时电流。1bf的功率级选择了三相全桥加低端三电阻采样方案,三个分流电阻分别串在三相下桥MOS管的源极与地之间,通过运放放大后进MCU的ADC。
这里有个关键点:PWM在任何时刻至少有一路下桥导通,电流才可能在分流电阻上形成压降。所以要等PWM的开关状态稳定后再触发ADC,最常见的是中心对齐PWM,ADC在PWM计数器的谷底或峰值附近触发,避开MOS开通关断的振铃窗口。
母线电压没有用隔离采样,直接用电阻分压加运放跟随送进另一路ADC。母线电压在前馈和过压保护里都要用,比如SVPWM输出占空比时需要把电压指令除以母线电压得到标幺值。
2.2 PWM输出与故障保护:死区、过流、堵转
三相PWM用的是互补输出,带硬件死区插入。死区时间到底取多大,取决于所用MOS管关断延迟和栅极驱动芯片的传播延迟,1bf项目上最终定为1微秒左右。死区太小会存在上下桥直通的风险,太大则会明显增加电流谐波,需要在安全与波形质量之间做平衡。
过流保护这条路径不经过软件判断。硬件比较器检测分流电阻的实时压降,一旦超过阈值就直接封锁PWM输出,从故障发生到封波控制在微秒级。等PWM封掉之后,软件状态机再根据故障标志决定是自恢复还是锁存。堵转、母线过压、欠压这些慢故障放到慢速环里去处理。
2.3 软件状态机与中断分层:快速环和慢速环各干什么
1bf的软件按“频率”分层。PWM中断频率设为10kHz,或者说开关频率10kHz,这个中断里执行完整电流环:ADC采样结果读取、坐标变换、两个电流PI、观测器、SVPWM、占空比更新。中断周期只有100微秒,所以里面的代码必须高度精简,尽量避免除法、三角函数调用。
速度环放在1kHz的慢速任务里,由定时器驱动或由电流环中断计数分频触发。速度环输出iq指令给电流环。启动状态机按200Hz运行,处理预定位、强拖、切换、停机这几种状态。为什么要分这么细?因为电流环带宽必须远高于速度环,不然两个环路耦合在一起很容易振荡。电流环快,负责把“给定电流”变成“实际电流”;速度环慢,负责把“目标转速”变成“电流指令”。
3. C代码核心模块逐段拆解:从Clarke变换到SVPWM再到观测器
3.1 电流环入口:坐标变换这段代码可以直接抄
先看电流环入口的三个函数。ADC在PWM中段完成采样后,中断里做电流校准换算、Clarke和Park,然后进入PI。
void clarke_transform(float ia, float ib, float *ialpha, float *ibeta) { /* 三相平衡条件下 ic = -ia - ib */ *ialpha = ia; *ibeta = (ia + 2.0f * ib) / 1.7320508f; } void park_transform(float ialpha, float ibeta, float theta, float *id, float *iq) { float c = cosf(theta); float s = sinf(theta); *id = ialpha * c + ibeta * s; *iq = -ialpha * s + ibeta * c; } void motor_foc_isr(void) { float ia = adc_current_to_amp(adc_ia_raw); float ib = adc_current_to_amp(adc_ib_raw); float ialpha, ibeta, id, iq; clarke_transform(ia, ib, &ialpha, &ibeta); park_transform(ialpha, ibeta, rotor_theta_elec, &id, &iq); /* d轴励磁电流给0,q轴电流给速度环输出 */ id_ref = 0.0f; pi_run(&pi_id, id_ref - id, PID_T, &ud_out); pi_run(&pi_iq, iq_ref - iq, PID_T, &uq_out); /* 反Park到alpha/beta,再过SVPWM */ inv_park_transform(ud_out, uq_out, rotor_theta_elec, &ualpha, &ubeta); svpwm_by_minmax(ualpha, ubeta, udc_bus, &duty_a, &duty_b, &duty_c); pwm_update_duty(duty_a, duty_b, duty_c); }这段代码里我刻意用了浮点,实际量产时可以考虑定点Q格式。但开发阶段先用浮点把算法逻辑跑通,重点在于变换公式本身要写对。Clarke变换里那条除法在有些单片机上比较慢,可以用常量代替:1 / 1.7320508f。
3.2 PI调节器的离散化与积分限幅
电流环PI是FOC里最容易“调死”的模块。堵转的时候电流误差一直很大,积分项会一直涨,等堵转解除,积分项释放出来会让电流猛地冲上去,直接把保护触发。所以积分限幅必须做。
typedef struct { float kp; float ki; float integral; float out_min; float out_max; } pi_ctrl_t; void pi_reset(pi_ctrl_t *p) { p->integral = 0.0f; } float pi_run(pi_ctrl_t *p, float err, float ts) { float output; /* 积分项累加之前先看会不会饱和 */ p->integral += p->ki * err * ts; if (p->integral > p->out_max) p->integral = p->out_max; if (p->integral < p->out_min) p->integral = p->out_min; output = p->kp * err + p->integral; if (output > p->out_max) output = p->out_max; if (output < p->out_min) output = p->out_min; return output; }我把Ki和采样周期Ts放在积分累加项里,这样换一个采样周期时不用重新标定参数。还有人喜欢在输出限幅后做积分反算,也就是back-calculation抗饱和。这个对电机驱动来说不是必须的,只要积分项先限幅,再把输出限幅,大多数场景都够用。
3.3 SVPWM用中点注入实现,比查扇区表更不容易出bug
传统的SVPWM教程会教你判断扇区、计算相邻非零矢量的作用时间、然后再映射到三相比较寄存器。这套流程没错,但代码写起来细节多,扇区切换处很容易因为边界条件出毛刺。1bf项目上我直接用“最大值最小值中点注入”的办法,效果等价于七段式SVPWM,代码量只有查表法的一半。
void svpwm_by_minmax(float ualpha, float ubeta, float udc, float *duty_a, float *duty_b, float *duty_c) { float va = ualpha; float vb = -0.5f * ualpha + 0.8660254f * ubeta; float vc = -0.5f * ualpha - 0.8660254f * ubeta; float vmax, vmin, voffset; vmax = fmaxf(fmaxf(va, vb), vc); vmin = fminf(fminf(va, vb), vc); /* 中点注入的本质:把三相电压整体抬高/降低,让最大和最小正好对称 */ voffset = -(vmax + vmin) * 0.5f; va += voffset; vb += voffset; vc += voffset; /* 占空比百分比,0~100 */ *duty_a = (0.5f + va / udc) * 100.0f; *duty_b = (0.5f + vb / udc) * 100.0f; *duty_c = (0.5f + vc / udc) * 100.0f; }为什么这个办法等价于SVPWM?因为SVPWM本身就是在正弦相电压上叠加一个零序分量,把调制比往线性区外扩一截。中点注入正是这个零序分量的一种标准取法。实际代码里注意udc不能接近0,否则除以母线电压会溢出,所以软件里要加一个最小母线电压保护。
3.4 滑模观测器和角度估计的C代码骨架
SMO的思想很直观:用电流观测误差的符号构造一个开关函数,把它当成反电动势的等价量。反电动势被提取出来之后,转子角度和转速再用锁相环跟踪。
#define RS_SMO 0.12f /* 定子电阻 */ #define LS_SMO 0.00012f /* 定子电感 */ #define SMO_K 1.0f /* 滑模增益 */ #define T_LPF 0.001f /* 反电动势低通滤波时间常数 */ void smo_update(float ualpha, float ubeta, float ialpha, float ibeta, float *ealpha, float *ebeta, float ts) { static float est_ialpha = 0.0f; static float est_ibeta = 0.0f; float err_alpha = ialpha - est_ialpha; float err_beta = ibeta - est_ibeta; float zalpha = (err_alpha > 0.0f) ? SMO_K : -SMO_K; float zbeta = (err_beta > 0.0f) ? SMO_K : -SMO_K; float ea = *ealpha; float eb = *ebeta; /* 电流观测方程 */ est_ialpha += (ts / LS_SMO) * (ualpha - RS_SMO * est_ialpha - ea + zalpha); est_ibeta += (ts / LS_SMO) * (ubeta - RS_SMO * est_ibeta - eb + zbeta); /* 低通滤波后得到反电动势 */ ea += (ts / T_LPF) * (zalpha - ea); eb += (ts / T_LPF) * (zbeta - eb); *ealpha = ea; *ebeta = eb; }角度和转速的提取我用了一个线性锁相环,比直接对角度做微分更平滑。角度微分在低速和噪声下会变成高频噪声源,锁相环天然带滤波作用。
float pll_update(float theta_est, float ts) { float err = wrap_angle(theta_est - pll_theta); pll_omega += pll_ki * err * ts; pll_theta += (pll_omega + pll_kp * err) * ts; return pll_omega; }反电动势角度到底怎么算,符号必须和电机模型对齐。1bf的模型里:
theta_est = atan2f(-e_alpha, e_beta);这里负号取决于反电动势方程的坐标定义,换一个模型就要重新推一遍。最稳妥的办法是先在仿真里把真实角度和估计角度打在一张图上看,方向反了立刻能发现。
3.5 零速启动策略:I/F强拖与闭环切换
无传感器系统在零速时反电动势为零,观测器没有任何输入信号,物理上就不可能估算出角度。所以必须“开环把转子拖起来”,等转速上来了再切换闭环。
1bf的启动流程分三步:
- 预定位:给一个固定的d轴电压矢量,把转子磁链角拖到和给定矢量一致。
- I/F强拖:电流环工作在幅值控制模式,输出一个恒定电流幅值,相位角按斜坡从0°开始匀速增加。转子在电流矢量拖动下跟着转起来。
- 切换闭环:当观测器估算的反电动势幅值超过额定值的一定比例(比如5%),且估算角度与强拖角度的误差连续200ms保持在一个范围内,就把强拖角度换成观测器角度,平滑进入闭环矢量控制。
这一步看着简单,却是1bf项目里最容易出问题的地方。切换瞬间如果两个角度差得太大,dq变换会突变,电流环会立刻饱和。我给切换逻辑加了一个“渐变时间”,在50毫秒内从强拖角度线性过渡到观测角度,基本上消除了切换冲击。
4. 仿真模型和C代码如何互相验证:搭建MIL/SIL环境的完整思路
4.1 电机被控对象的建模粒度
仿真模型不能拍脑袋随便搭。1bf的仿真环境至少包含两个部分:电机本体模型和逆变器模型。
电机部分我建议用三相静止坐标系下的永磁同步电机模型,也就是直接以abc相电压为输入,以三相电流、转子转速、转角为输出。虽然dq方程模型算起来快,但它天然假设了正弦反电动势,一旦后面要加死区效应、谐波等,dq模型就表达不干净。三相瞬时模型反而更贴近实物。
逆变器部分分两档:做波形细节观察时,用带PWM开关状态的理想开关模型,把死区时间也加进去;做参数整定和批量跑工况时,用平均模型,把PWM周期平均掉,仿真步长能放大不少。这两档模型在同一个工程文件里做成可选。
4.2 把C代码挂到仿真里:MIL与SIL的分工
仿真模型建好以后,控制算法不能用手写一个孤立的“仿真版本”,否则就是两套代码,迟早会出现仿真里很顺、台架上却调不通的尴尬。1bf的做法是把真实C代码编译成仿真平台的自定义回调函数,注册成控制模块,让仿真里的“控制器”和台架上的“控制器”是同一份源码。
这个阶段分两层:
| 验证阶段 | 控制算法载体 | 被控对象 | 主要发现的问题 |
|---|---|---|---|
| 模型在环MIL | C代码编译成仿真回调函数 | 仿真电机模型 | 算法逻辑错误、PI极性错误、启动切换条件不合理 |
| 软件在环SIL | PC上编译运行C代码 | 仿真电机模型 | 采样时间差异、定点化误差、int类型溢出 |
我先跑MIL,把启动、稳态、突加负载、堵转这些工况在仿真里过一遍,再把代码放到SIL里做定点化验证。这一步最大价值是:如果代码在仿真环境里都跑不出稳定波形,就不要浪费时间上台架。
4.3 仿真步长、开关频率与数值稳定性
FOC仿真里最容易踩的坑是步长设置不合理。PWM开关频率10kHz,开关周期100us。如果仿真主步长也取100us,等于每周期只看一个点,死区、电流纹波全被压扁了。我一般分两级:逆变器开关模型用1微秒步长,控制算法按100us执行一次;如果跑长时间工况,就把逆变器换成平均模型,步长放到5~10微秒,速度能快不少。
还有一个细节:仿真里的电流环必须按离散周期执行,不能写成连续时间控制器。否则仿真里的PI增益和C代码里的PI增益永远对不上,调好的参数一上台架又得重新来。
5. 联调阶段的调参顺序与典型坑位
5.1 电流环PI初值怎么算:从电感和电阻出手
1bf用的电机参数大致是:定子电感120微亨,定子电阻0.12欧姆,母线电压48V,额定电流约20A。电流环带宽我一开始取2000rad/s,按连续时间PI设计公式估算:
[ K_p = L_s \cdot \omega_{cc} = 0.00012 \times 2000 = 0.24 ]
[ K_i = R_s \cdot \omega_{cc} = 0.12 \times 2000 = 240 ]
这里Kp单位是“伏特每安培”,Ki积分累加时还要乘采样周期100微秒,所以每一步积分增量大概是0.024伏特。注意这个公式在电流反馈和输出电压都用真实单位时成立;一旦控制器内部做了标幺化,增益还要乘转换系数。
调参顺序也很重要:先把d轴电流环单独打通,给id一个小阶跃,看电流在1ms内能不能跟上;再调q轴。两个电流环分别稳定后再调速度环。千万不要直接上速度闭环,否则出问题了分不清是电流环还是速度环的锅。
5.2 观测器的增益调法与反电动势波形判读
滑模增益SMO_K先取“额定转速下反电动势幅值的3到5倍附近”。增益太小,电流观测误差压不住,估算角度会抖;增益太大,开关函数引起的抖振明显,反电动势波形上能看到高频毛刺。
调观测器的时候,我给调试板临时加了一个1000线的光电编码器做对照,把真实角度和估算角度同时录到示波器上。判断标准很简单:从10%额定转速到100%额定转速之间,估算角与真实角的偏差始终能压到10°电角度以内。低速段偏差大一点可以接受,但方向不能反,一旦方向反说明模型符号或者PLL符号有问题,必须停下来查。
5.3 我踩过的几个坑:ADC采样点、零点漂移、死区影响
第一个坑是ADC采样点。一开始我在PWM周期起始时刻采样,正好采到上桥开通瞬间的电流尖峰,电流波形上全是毛刺,电流环跟着呜呜响。后来改成中心对齐PWM的谷底采样,毛刺立刻消掉大半。
第二个坑是零点漂移。电流采样电路在MOS关断时输出并不是严格的0V,而是有几十毫伏的漂移。对20A额定电流来说,这个漂移折算成电流误差可能偏离好几安培。我加了一个上电校准流程:三相下桥全开、等待母线电容放电稳定,连续采集100次电流取平均,作为零点补偿值存起来。这个流程花不了几毫秒,但效果非常明显。
第三个坑是死区。死区时间在低速时会让电流波形产生明显畸变,特别是在零电流穿越的时刻。仿真里没加死区模型,所以台架低速波形一开始比仿真难看很多。后来在SVPWM输出前加了一个简单的死区补偿项:根据相电流方向,把输出电压指令加上死区时间乘母线电压折合过来的等效电压。低速带载能力立刻改善。
下面这张表是我同事根据试错经验总结的速查表,感觉一直有用:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 低速电流波形“顶切” | 死区效应、电流环输出饱和 | 加死区补偿,检查SVPWM占空比限幅 |
| 满载转速差偏大 | 观测角偏差、速度环带宽不足 | 对比真实角度,加大速度环带宽 |
| 启动瞬间过流 | 强拖角度与观测角度切换突变 | 渐过渡角度,确认切换条件 |
| 电流环啸叫 | ADC采样点偏早,采到开关尖峰 | 调整ADC触发到PWM中心 |
6. 台架验证与项目心得:1bf控制器能跑到什么水平
6.1 台架搭建和验证指标
1bf项目最后在台架上跑了一个1000W级永磁同步电机,负载侧用另一台电机做加载。测试时临时装扭矩传感器和电流探头,控制器只保留FOC板和驱动板。验证指标定得比较务实:速度阶跃响应超调小于5%,调节时间小于150ms;突加额定负载时转速跌落不超过3%,恢复时间小于300ms;额定转速范围内观测角度误差小于5°电角度;堵转保护在10ms内可靠动作。
这组数据不算是多惊艳,但对一个低成本无传感器方案来说,稳定性和一致性才是更重要的。我做了连续12小时的老化运行,中间反复切入切出负载,控制器温度和电流波形都保持正常。
6.2 这套代码能怎么往外扩展
跑通1bf之后,我再拿到另一台电机时,只需要改参数表里的电感、电阻、磁链、额定电流、开关频率,再重新自适应调一遍电流环,基本就能动起来。最近还在往三个方向做扩展:一是内置式永磁电机的MTPA控制,利用凸极效应在同样电流下多榨一点转矩;二是弱磁控制,让电机在更高转速下还能输出功率;三是在线参数辨识,把定子电阻和电感在运行过程中自动辨识出来,减少对电机参数表的依赖。
6.3 最后再分享一条经验
如果让我重新做一遍这个项目,我会把“仿真模型先加死区和电流偏移”这件事提到最前面。最初仿真模型太理想,所有参数在仿真里调到无比顺滑,结果一上台架就打回过流保护,浪费了整整一周。先把麻烦留给仿真,把噪声和缺陷都建模进去,台架上的路就会好走很多。1bf这个项目看似是靠观测器和SVPWM撑起来的,真正让人咬牙的部分,反而是采样、死区、零点漂移这些“不性感”的工程细节。