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的封装、转矩转速闭环、弱磁控制,这三块是电机控制应用层的核心。下一章我们聊聊诊断和安全机制——毕竟车规级的东西,光跑得快不行,还得跑得稳、跑得安全。