
把仿真模型里刚整干净的波形搬到台架上电机咔嗒抖了两下随即电流飙升触发过流保护——这是我在做编号“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最常见的是中心对齐PWMADC在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轴励磁电流给0q轴电流给速度环输出 */ 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代码编译成仿真平台的自定义回调函数注册成控制模块让仿真里的“控制器”和台架上的“控制器”是同一份源码。这个阶段分两层验证阶段控制算法载体被控对象主要发现的问题模型在环MILC代码编译成仿真回调函数仿真电机模型算法逻辑错误、PI极性错误、启动切换条件不合理软件在环SILPC上编译运行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撑起来的真正让人咬牙的部分反而是采样、死区、零点漂移这些“不性感”的工程细节。