3、应用层软件组件设计:电机控制算法(FOC/SVPWM)的软件封装、转矩/转速闭环控制模块、弱磁控制策略实现
好,我们接着往下聊。上一章我们把应用层的整体框架搭起来了,这一章要动真格的了——电机控制算法怎么封装?转矩转速闭环怎么做?弱磁控制又该怎么落地?
说实话,FOC(磁场定向控制)和SVPWM(空间矢量脉宽调制)这些算法,教科书上讲得已经够多了。但真正把它塞进一个嵌入式系统里,还要考虑实时性、可维护性、可移植性,那又是另一回事。我这些年踩过的坑,大多都跟「算法写对了,但软件架构没设计好」有关。
3.1 FOC/SVPWM的软件封装:别让算法裸奔
我个人习惯,把FOC和SVPWM当作两个独立的「软件组件」来设计。为什么?因为FOC是控制策略,SVPWM是调制手段,它们的变化频率不一样。你想想看,FOC可能因为弱磁策略的加入而调整,但SVPWM的算法核心基本不变。
封装的原则很简单:对外暴露接口,对内隐藏实现。我一般会这样设计头文件:
/* foc_controller.h */
typedef struct {
float id_ref; // d轴电流参考值
float iq_ref; // q轴电流参考值
float id_fb; // d轴电流反馈值
float iq_fb; // q轴电流反馈值
float theta_elec; // 电角度
float vd_ref; // d轴电压输出
float vq_ref; // q轴电压输出
float vd_ff; // d轴前馈电压(弱磁用)
float vq_ff; // q轴前馈电压
} FOC_Input_t;
typedef struct {
float v_alpha; // alpha轴电压
float v_beta; // beta轴电压
float mod_index; // 调制比(用于过调制判断)
} FOC_Output_t;
/* 接口函数 */
void FOC_Run(FOC_Input_t *in, FOC_Output_t *out);
void FOC_Reset(void);
你看,调用者只需要填好电流参考值和反馈值,拿到alpha/beta轴电压就行。至于里面是PI调节器还是复矢量调节器,调用者根本不用关心。这就是封装的好处——我换算法,你不动接口。
我的经验:曾经有个项目,FOC里集成了死区补偿和电流采样校正。一开始没封装好,所有逻辑都揉在一个大函数里。后来要换MCU,移植的时候差点没把我搞疯。从那以后,我坚持每个功能模块独立成文件,接口清晰,移植起来就是改个底层驱动的事。
SVPWM的封装也类似。我习惯把SVPWM做成一个「纯计算」模块,不依赖任何硬件定时器。它只负责算占空比:
/* svpwm.h */
typedef struct {
float v_alpha;
float v_beta;
float vdc; // 母线电压
} SVPWM_Input_t;
typedef struct {
float ta; // A相占空比
float tb; // B相占空比
float tc; // C相占空比
uint8_t sector; // 当前扇区(调试用)
} SVPWM_Output_t;
void SVPWM_Calc(SVPWM_Input_t *in, SVPWM_Output_t *out);
嗯,这里要注意:SVPWM的输出是归一化的占空比,范围0.0~1.0。至于怎么映射到硬件定时器的比较值,那是底层驱动的事。这样分层,上层算法和底层硬件就解耦了。
3.2 转矩/转速闭环控制模块:双环的「快」与「稳」
转矩和转速闭环,说白了就是两个PI调节器串在一起。但实际做起来,远不止「调个PI参数」那么简单。
我一般把转速环放在外环,转矩环(电流环)放在内环。转速环的输出作为转矩环的输入——也就是iq_ref。为什么这样设计?因为转速的响应比电流慢,外环慢、内环快,系统才稳定。
这里分享一个我踩过的坑:积分饱和。转速环的PI输出如果无限积分,一旦遇到限幅,积分项会越积越大。等转速误差变号了,积分项还得慢慢退回来,这就造成了超调和振荡。
我曾经在一个风机项目里,转速环没加抗饱和处理。结果电机在启动时转速冲过了头,直接触发过速保护停机。排查了两天才找到原因——积分项在限幅时还在累加。从那以后,我的PI调节器必带抗饱和逻辑。
抗饱和的实现方式有很多,我习惯用「条件积分法」:
/* 带抗饱和的PI调节器 */
float PI_WithAntiWindup(float error, float kp, float ki, float limit, float *integral) {
float output;
float pre_output;
/* 先计算不考虑限幅的输出 */
*integral += error * ki;
pre_output = error * kp + (*integral);
/* 限幅判断 */
if (pre_output > limit) {
output = limit;
/* 只有误差符号与输出方向一致时才积分 */
if (error > 0) {
*integral -= error * ki; // 退积分
}
} else if (pre_output < -limit) {
output = -limit;
if (error < 0) {
*integral -= error * ki;
}
} else {
output = pre_output;
}
return output;
}
你看,核心逻辑就一句话:输出饱和了,就别再往积分项里加料了。这个技巧,我建议你直接抄进代码里。
另外,转矩环的带宽设计也有讲究。我一般把电流环的带宽设在1~2kHz,转速环设在50~200Hz。为什么是这个范围?因为电流环要跟上PWM的开关频率(通常10kHz左右),而转速环的机械时间常数本来就大,没必要跑太快。
3.3 弱磁控制策略实现:让电机跑得更快
弱磁控制,说白了就是当电机转速超过基速时,通过减小d轴磁通来提升转速。但这里有个矛盾:d轴电流减小磁通,q轴电流产生转矩,两者共用同一个电流极限圆。
我常用的弱磁策略是「电压反馈法」。原理很简单:实时监测调制比,如果调制比接近1.0(比如超过0.95),说明电压已经用满了,这时候就需要弱磁。
具体实现是这样的:
/* 弱磁控制模块 */
typedef struct {
float vd_ref; // 当前d轴电压
float vq_ref; // 当前q轴电压
float vdc; // 母线电压
float id_ref; // 原始d轴电流参考值
float iq_ref; // 原始q轴电流参考值
float speed; // 当前转速
} FW_Input_t;
typedef struct {
float id_ref_fw; // 弱磁后的d轴电流参考值
float iq_ref_fw; // 弱磁后的q轴电流参考值(可能需要限幅)
uint8_t fw_active; // 弱磁是否激活
} FW_Output_t;
void FieldWeakening_Control(FW_Input_t *in, FW_Output_t *out) {
float mod_index;
float v_mag;
float v_max;
float fw_error;
static float fw_integral = 0.0f;
/* 计算当前调制比 */
v_mag = sqrtf(in->vd_ref * in->vd_ref + in->vq_ref * in->vq_ref);
v_max = in->vdc * 0.577f; // SVPWM最大不失真电压
mod_index = v_mag / v_max;
/* 如果调制比超过阈值,启动弱磁 */
if (mod_index > 0.95f) {
fw_error = mod_index - 0.95f;
fw_integral += fw_error * 0.01f; // 弱磁积分系数,需标定
/* 负的d轴电流,削弱磁通 */
out->id_ref_fw = in->id_ref - fw_integral;
/* 同时限制q轴电流,防止过流 */
out->iq_ref_fw = in->iq_ref * 0.9f; // 简单降额
out->fw_active = 1;
} else {
/* 退出弱磁,慢慢恢复 */
fw_integral *= 0.95f; // 衰减
out->id_ref_fw = in->id_ref;
out->iq_ref_fw = in->iq_ref;
out->fw_active = 0;
}
}
核心要点:弱磁不是「开」或「关」的二值控制,而是一个连续调节过程。我见过有人把弱磁做成查表法,转速到了某个点直接给一个固定的id_ref。结果电机在过渡区间抖得厉害。电压反馈法的好处是平滑,调制比高就多弱磁,调制比低就少弱磁,天然连续。
不过,弱磁控制有个风险:电流失控。因为d轴电流变负了,q轴电流又大,合成电流很容易超过电机和逆变器的额定值。所以,我一般会在弱磁模块后面再加一道「电流极限圆限制」:
/* 电流极限圆限制 */
float imax = 100.0f; // 最大电流幅值
float id_sq = out->id_ref_fw * out->id_ref_fw;
float iq_sq = out->iq_ref_fw * out->iq_ref_fw;
if ((id_sq + iq_sq) > (imax * imax)) {
/* 按比例缩小,保持角度不变 */
float scale = imax / sqrtf(id_sq + iq_sq);
out->id_ref_fw *= scale;
out->iq_ref_fw *= scale;
}
这个逻辑很简单:电流矢量不能超出圆。超出就按比例往回拉,保证电流幅值不超标。
3.4 模块间的数据流与调度
最后,我简单说一下这几个模块怎么串起来。我的习惯是做一个「电机控制调度器」,按固定时序调用:
void MotorControl_Task(void) {
/* 1. 采样电流和位置 */
CurrentSampling(&id_fb, &iq_fb);
PositionSampling(&theta_elec, &speed);
/* 2. 转速环(外环,慢速执行,比如1kHz) */
if (speed_loop_tick) {
speed_error = speed_ref - speed;
iq_ref = PI_WithAntiWindup(speed_error, kp_spd, ki_spd, iq_limit, &spd_integral);
}
/* 3. 弱磁控制(与转速环同频或更慢) */
fw_in.speed = speed;
fw_in.id_ref = 0.0f; // 基速以下id_ref=0
fw_in.iq_ref = iq_ref;
FieldWeakening_Control(&fw_in, &fw_out);
id_ref = fw_out.id_ref_fw;
iq_ref = fw_out.iq_ref_fw;
/* 4. 电流环(内环,快速执行,比如10kHz) */
foc_in.id_ref = id_ref;
foc_in.iq_ref = iq_ref;
foc_in.id_fb = id_fb;
foc_in.iq_fb = iq_fb;
foc_in.theta_elec = theta_elec;
FOC_Run(&foc_in, &foc_out);
/* 5. SVPWM调制 */
svpwm_in.v_alpha = foc_out.v_alpha;
svpwm_in.v_beta = foc_out.v_beta;
svpwm_in.vdc = vdc;
SVPWM_Calc(&svpwm_in, &svpwm_out);
/* 6. 更新PWM占空比 */
PWM_UpdateDuty(svpwm_out.ta, svpwm_out.tb, svpwm_out.tc);
}
你看,整个流程清晰明了。每个模块各司其职,数据通过结构体传递。想换算法?换个模块就行。想加功能?在调度器里插一个调用。
嗯,这一章的内容就到这里。FOC/SVPWM的封装、转矩转速闭环、弱磁控制,这三块是电机控制应用层的核心。下一章我们聊聊诊断和安全机制——毕竟车规级的东西,光跑得快不行,还得跑得稳、跑得安全。