ODrive0.5.1程序分析#4 闭环控制程序(run_closed_loop_control_loop)

闭环控制算法程序分析

固件版本fw-0.5.1(0积分下载)

代码分析

1. 状态输入

当输入状态为AXIS_STATE_CLOSED_LOOP_CONTROL时,检查电机校准和编码器校准是否已完成(参考文章#1与#2),然后运行run_closed_loop_control_loop程序

        bool status;
        switch (current_state_) {
            case AXIS_STATE_CLOSED_LOOP_CONTROL: {
                if (!motor_.is_calibrated_ || motor_.config_.direction==0)
                    goto invalid_state_label;
                if (!encoder_.is_ready_)
                    goto invalid_state_label;
                watchdog_feed();
                status = run_closed_loop_control_loop();
            } break;
         }
2. 加载编码器

进入run_closed_loop_control_loopload_encoder_axis为设定的编码器数量,其作为输入进入select_encoder函数,AXIS_COUNT值为2,检测编码器数量不超过两个时,对一系列位置速度数据进行赋值(具体含义在下面讨论)。

bool Axis::run_closed_loop_control_loop() {
    if (!controller_.select_encoder(controller_.config_.load_encoder_axis)) {
        return error_ |= ERROR_CONTROLLER_FAILED, false;
    }
bool Controller::select_encoder(size_t encoder_num) {
    if (encoder_num < AXIS_COUNT) {
        Axis* ax = axes[encoder_num];
        pos_estimate_circular_src_ = &ax->encoder_.pos_circular_;
        pos_wrap_src_ = &config_.circular_setpoint_range;
        pos_estimate_linear_src_ = &ax->encoder_.pos_estimate_;
        pos_estimate_valid_src_ = &ax->encoder_.pos_estimate_valid_;
        vel_estimate_src_ = &ax->encoder_.vel_estimate_;
        vel_estimate_valid_src_ = &ax->encoder_.vel_estimate_valid_;
        return true;
    } else {
        return set_error(Controller::ERROR_INVALID_LOAD_ENCODER), false;
    }
}
3.初始位置设定

为了 避免进入闭环产生瞬态运动,将当前位置设定为期望位置
第一个if判断的circular_setpoints在该版本值始终为false(当值为真时,进入循环位置模式,适用于无限转动的机械,取消位置设置上限,同时因为浮点舍入,而损失了位置精度)
因此执行else里的判断pos_estimate_linear_src_在上步select_encoder函数中取址了pos_estimate_
在函数encoder_.updata中利用采样时间current_meas_period * 估计速度vel_estimate_counts_(初值为0)更新估计位置pos_estimate_counts_(初值为0),然后计算与实际位置值shadow_count_的差值delta_pos_counts(单位count),差值乘以锁相环kp(单位count/s/count)再乘以采样时间current_meas_period(单位s)对位置估计值进行补偿得到新的估计位置pos_estimate_counts_,然后除以设置的cpr值得到已圈数turn为单位的位置值pos_estimate_。接上面分析pos_estimate_ 传到 select_encoder() 函数中,pos_estimate_linear_src_ 取址pos_estimate_,然后返回run_closed_loop_control_loop() 函数而pos_setpoint_input_pos_ 取址 pos_estimate_linear_src_ 得到设定位置。
然后进入 input_pos_updated函数,内容只是对input_pos_updated bool型变量赋值为ture,为之后控制做准备。

    //axis.cpp-接run_closed_loop_control_loop()函数
    // To avoid any transient on startup, we intialize the setpoint to be the current position
    if (controller_.config_.circular_setpoints) {
        if (!controller_.pos_estimate_circular_src_) {
            return error_ |= ERROR_CONTROLLER_FAILED, false;
        }
        else {
            controller_.pos_setpoint_ = *controller_.pos_estimate_circular_src_;
            controller_.input_pos_ = *controller_.pos_estimate_circular_src_;
        }
    }
    else {
        if (!controller_.pos_estimate_linear_src_) {
            return error_ |= ERROR_CONTROLLER_FAILED, false;
        }
        else {
            controller_.pos_setpoint_ = *controller_.pos_estimate_linear_src_;
            controller_.input_pos_ = *controller_.pos_estimate_linear_src_;
        }
    }
    controller_.input_pos_updated();
    //encoder.cpp-updata()函数
     run pll (for now pll is in units of encoder counts)
    // Predict current pos
    pos_estimate_counts_ += current_meas_period * vel_estimate_counts_;
    pos_cpr_counts_      += current_meas_period * vel_estimate_counts_;
    // discrete phase detector
    float delta_pos_counts = (float)(shadow_count_ - (int32_t)std::floor(pos_estimate_counts_));
    float delta_pos_cpr_counts = (float)(count_in_cpr_ - (int32_t)std::floor(pos_cpr_counts_));
    delta_pos_cpr_counts = wrap_pm(delta_pos_cpr_counts, 0.5f * (float)(config_.cpr));
    // pll feedback
    pos_estimate_counts_ += current_meas_period * pll_kp_ * delta_pos_counts;
    pos_cpr_counts_ += current_meas_period * pll_kp_ * delta_pos_cpr_counts;
    pos_cpr_counts_ = fmodf_pos(pos_cpr_counts_, (float)(config_.cpr));
    vel_estimate_counts_ += current_meas_period * pll_ki_ * delta_pos_cpr_counts;
    bool snap_to_zero_vel = false;
    if (std::abs(vel_estimate_counts_) < 0.5f * current_meas_period * pll_ki_) {
        vel_estimate_counts_ = 0.0f;  //align delta-sigma on zero to prevent jitter
        snap_to_zero_vel = true;
    }

    // Outputs from Encoder for Controller
    float pos_cpr_last = pos_cpr_;
    pos_estimate_ = pos_estimate_counts_ / (float)config_.cpr;
    vel_estimate_ = vel_estimate_counts_ / (float)config_.cpr;

4.主循环框架

回到run_closed_loop_control_loop() 函数,为避免力矩积分项饱和,对其vel_integrator_torque_ 进行初始化,该参数在下方 controller_.update() 函数中进行更新;
set_step_dir_active() 函数的输入为enable_step_dir默认设置为false,该函数为步进电机配置GPIO口为接收脉冲step及方向dir的功能。对控制伺服电机该参数配置为false即可;
run_control_loop() 函数为循环函数,当下面主循环内容返回false时循环停止,同时包含对于编码器等数据的更新,详细分析看#2文章;主循环内容包括控制模式更新controller_.update()以及电机运动参数更新motor_.update

    //axis.cpp-接run_closed_loop_control_loop()函数
    // Avoid integrator windup issues
    controller_.vel_integrator_torque_ = 0.0f;

    set_step_dir_active(config_.enable_step_dir);
    run_control_loop([this](){
        // Note that all estimators are updated in the loop prefix in run_control_loop
        float torque_setpoint;
        if (!controller_.update(&torque_setpoint))
            return error_ |= ERROR_CONTROLLER_FAILED, false;

        float phase_vel = (2*M_PI) * encoder_.vel_estimate_ * motor_.config_.pole_pairs;
        if (!motor_.update(torque_setpoint, encoder_.phase_, phase_vel))
            return false; // set_error should update axis.error_

        return true;
    });
    set_step_dir_active(config_.enable_step_dir && config_.step_dir_always_on);
    return check_for_errors();
5.控制模式更新

控制模式更新函数controller_.update(),首先是三目运算符判断pos_estimate_valid_src_ 不为0时,对pos_estimate_linear进行赋值,另两个参数同理。
anticogging_pos为补偿齿槽效应位置,下面判断参数calib_anticogging决定是否运行抗齿槽效应校准函数anticogging_calibration(),默认为false不进行抗齿槽效应校准。
circular_setpoints循环位置模式,上文已解释,为false,不执行if函数内容。

bool Controller::update(float* torque_setpoint_output) {
    float* pos_estimate_linear = (pos_estimate_valid_src_ && *pos_estimate_valid_src_)
            ? pos_estimate_linear_src_ : nullptr;
    float* pos_estimate_circular = (pos_estimate_valid_src_ && *pos_estimate_valid_src_)
            ? pos_estimate_circular_src_ : nullptr;
    float* vel_estimate_src = (vel_estimate_valid_src_ && *vel_estimate_valid_src_)
            ? vel_estimate_src_ : nullptr;

    // Calib_anticogging is only true when calibration is occurring, so we can't block anticogging_pos
    float anticogging_pos = axis_->encoder_.pos_estimate_ / axis_->encoder_.getCoggingRatio();
    if (config_.anticogging.calib_anticogging) {
        if (!axis_->encoder_.pos_estimate_valid_ || !axis_->encoder_.vel_estimate_valid_) {
            set_error(ERROR_INVALID_ESTIMATE);
            return false;
        }
        // non-blocking
        anticogging_calibration(axis_->encoder_.pos_estimate_, axis_->encoder_.vel_estimate_);
    }

    // TODO also enable circular deltas for 2nd order filter, etc.
    if (config_.circular_setpoints) {
        // Keep pos setpoint from drifting
        input_pos_ = fmodf_pos(input_pos_, config_.circular_setpoint_range);
    }

接控制模式更新函数controller_.update(),判断控制信号的输入模式,分别有闲置模式INPUT_MODE_INACTIVE,直通模式INPUT_MODE_PASSTHROUGH,速度斜波模式INPUT_MODE_VEL_RAMP,力矩斜波模式INPUT_MODE_TORQUE_RAMP,位置滤波模式INPUT_MODE_POS_FILTER,梯形轨迹控制模式INPUT_MODE_TRAP_TRAJ。本文分析闭环程序流程,以直通模式为例,其余模式分析在后面文章进行,在直通模式中,对位置速度力矩设定值pos/vel/torque_setpoint_ 进行赋值,而位置/速度/力矩的输入值 input_pos_/vel_/torque_ 通过CAN信号在函数set_input_pos_callback()中进行输入,CAN设定为8个字节即64位前32位为input_pos_32-48位为input_vel_,48-64为input_torque_。从而得到了设定值。

    switch (config_.input_mode) {
        case INPUT_MODE_INACTIVE: {
            // do nothing
        } break;
        case INPUT_MODE_PASSTHROUGH: {
            pos_setpoint_ = input_pos_;
            vel_setpoint_ = input_vel_;
            torque_setpoint_ = input_torque_; 
        } break;
       case INPUT_MODE_VEL_RAMP: {...
       } break;
       case INPUT_MODE_TORQUE_RAMP:{... 
       } break;
       case INPUT_MODE_POS_FILTER: {...
       } break;
       case INPUT_MODE_TRAP_TRAJ: {...
       } break;
       default: {
            set_error(ERROR_INVALID_INPUT_MODE);
            return false;
        }
        
    }

void CANSimple::set_input_pos_callback(Axis* axis, can_Message_t& msg) {
    axis->controller_.input_pos_ = can_getSignal<float>(msg, 0, 32, true);
    axis->controller_.input_vel_ = can_getSignal<int16_t>(msg, 32, 16, true, 0.001f, 0);
    axis->controller_.input_torque_ = can_getSignal<int16_t>(msg, 48, 16, true, 0.001f, 0);
    axis->controller_.input_pos_updated();
}
6.输出力矩计算

定义好位置速度力矩的设定值,继续往下执行程序controller_update()
首先令速度期望值等于速度设定值,然后判断控制模式(control_mode),电压模式为0,力矩模式为1,速度模式为2,位置模式为3,if语句中判断条件为是否为位置控制模式(POSITION_CONTROL),下面的if判断是否为循环位置(circular_setpoints),上文已说明不使用循环位置,此处为否,故循环位置的if中函数省略,else中判断是否已获取编码器位置,位置控制模式下,利用CAN传入的位置设定值pos_setpoint_ 减去编码器位置获取值 pos_estimate_linear,计算得到位置偏差pos_err速度设定值vel_des等于CAN输入的设定值加上位置增益pos_gain单位(turn/s/turn)乘以位置偏差pos_err单位(turn)。
然后利用clmp函数保证速度设定值vel_des在设定的最大最小值之间。
之后用if函数判断编码器速度获取值vel_estimate_src是否大于1.2(默认系数)速度限制值vel_lim,是的话报错过速ERROR_OVERSPEED返回false。

    float vel_des = vel_setpoint_;
    if (config_.control_mode >= CONTROL_MODE_POSITION_CONTROL) {
        float pos_err;

        if (config_.circular_setpoints) {...
        } else {
            if(!pos_estimate_linear) {
                set_error(ERROR_INVALID_ESTIMATE);
                return false;
            }
            pos_err = pos_setpoint_ - *pos_estimate_linear;
        }

        vel_des += config_.pos_gain * pos_err;
    }
    float vel_lim = config_.vel_limit;
    if (config_.enable_vel_limit) {
        vel_des = std::clamp(vel_des, -vel_lim, vel_lim);
    }
    if (config_.enable_overspeed_error) {  // 0.0f to disable
        if (!vel_estimate_src) {
            set_error(ERROR_INVALID_ESTIMATE);
            return false;
        }
        if (std::abs(*vel_estimate_src) > config_.vel_limit_tolerance * vel_lim) {
            set_error(ERROR_OVERSPEED);
            return false;
        }
    }

对位置速度设定好之后,进入该controller_updata() 函数的输出—力矩的设定
首先对速度增益以及速度积分项增益进行赋值,该值在config中也即在上位机能够进行配置,if函数判断是否为感应电机,感应电机相关内容忽略,之后令变量torque等于力矩设定值torque_setpoint_,再判断齿隙补偿是否使能,否忽略函数内容。
创建变量速度偏差v_err
当控制模式大于等于速度控制模式时,即在位置控制和速度控制模式下,利用速度设定值vel_des减去编码器速度获取值vel_estimate_src得出值,计算力矩torque加等于速度增益vel_gain乘以速度偏差v_err再加上速度积分增益得出的力矩vel_integrator_torque_,速度积分增益的力矩初值为0,数值在下方程序计算。
当控制模式小于速度控制模式,即在电压控制和扭矩控制模式下,先if函数判断编码器是否能正常获取数据,否的话报错ERROR_INVALID_ESTIMATE返回false。之后计算力矩torque等于limitVel() 函数内计算内容,为力矩最值TMAX/TMIN等于正/负速度增益vel_gain乘以(速度限制值vel_limit减去编码器速度获取值vel_estimate_src),然后利用clamp函数保证此时的力矩值torque也即力矩输入值torque_setpoint_ 在最值范围内。
在上面两种控制模式情况下都能算出力矩torque的值,再算出力矩的限制值Tlim,Tlim的值是根据力矩常数torque_constant乘以电流限制值current_lim得出的,力矩常数和电流限制值都可通过上位机配置。再判断力矩torque是否超过限制值,超过时取torque=±Tlim,并将布尔型变量limited置true,对于位置和速度控制模式下速度积分增益计算力矩,如果limited为false时,速度积分增益力矩vel_integrator_torque加等于速度积分增益单位[Nm/(turn/s * s)] 乘以电流采样周期 current_meas_period单位(s)再乘以 速度偏差v_err单位(turn/s)得出力矩值;当limited置true时,说明积分项饱和,不再继续增加值,而是原值乘以0.99,对积分增益力矩进行衰减。
最后将torque赋值torque_setpoint_output,也即函数的输出,进行对外力矩值的发送。

    float vel_gain = config_.vel_gain;
    float vel_integrator_gain = config_.vel_integrator_gain;
    if (axis_->motor_.config_.motor_type == Motor::MOTOR_TYPE_ACIM) {...}//感应电机内容忽略

    float torque = torque_setpoint_;
    if (anticogging_valid_ && config_.anticogging.anticogging_enabled) {...}//齿隙补偿内容忽略
    float v_err = 0.0f;
    if (config_.control_mode >= CONTROL_MODE_VELOCITY_CONTROL) {
        if (!vel_estimate_src) {
            set_error(ERROR_INVALID_ESTIMATE);
            return false;
        }

        v_err = vel_des - *vel_estimate_src;
        torque += (vel_gain * gain_scheduling_multiplier) * v_err;

        // Velocity integral action before limiting
        torque += vel_integrator_torque_;
    }

    // Velocity limiting in current mode
    if (config_.control_mode < CONTROL_MODE_VELOCITY_CONTROL && config_.enable_current_mode_vel_limit) {
        if (!vel_estimate_src) {
            set_error(ERROR_INVALID_ESTIMATE);
            return false;
        }
        torque = limitVel(config_.vel_limit, *vel_estimate_src, vel_gain, torque);
    }

    // Torque limiting
    bool limited = false;
    float Tlim = axis_->motor_.max_available_torque();
    if (torque > Tlim) {
        limited = true;
        torque = Tlim;
    }
    if (torque < -Tlim) {
        limited = true;
        torque = -Tlim;
    }

    // Velocity integrator (behaviour dependent on limiting)
    if (config_.control_mode < CONTROL_MODE_VELOCITY_CONTROL) {
        // reset integral if not in use
        vel_integrator_torque_ = 0.0f;
    } else {
        if (limited) {
            // TODO make decayfactor configurable
            vel_integrator_torque_ *= 0.99f;
        } else {
            vel_integrator_torque_ += ((vel_integrator_gain * gain_scheduling_multiplier) * current_meas_period) * v_err;
        }
    }

    if (torque_setpoint_output) *torque_setpoint_output = torque;
    return true;
7.电机控制参数更新

回到run_closed_loop_control_loop() 函数,controller_update 函数传出力矩值,之后进入对力矩的处理转化为对电机的控制参数,也即 motor_.update() 函数内容。
首先计算相位电角速度phase_vel,其中vel_estimate为编码器的角速度获取值也即机械角速度,单位turn/s乘以2pi,变为以弧度表示的角速度,单位rad/s,再乘以极对数pole_pairs,将机械角速度变为电角速度。
然后将上一把传出的力矩设定值torque_setpoint、编码器检测位置phase_以及相位电角速度phase_vel作为输出参数进入函数motor_.update()

        float phase_vel = (2*M_PI) * encoder_.vel_estimate_ * motor_.config_.pole_pairs;**相位电角速度phase_vel**
        if (!motor_.update(torque_setpoint, encoder_.phase_, phase_vel))
            return false;

进入电机控制更新程序motor_.update(),首先定义电流设定值current_setpoint,然后将相位和相速度乘以方向direction(1或-1,在编码器校准时得出此值,见#2文章),之后判断电机类型是否为感应电机,执行否的else内程序,电流设定值current_setpoint等于传入的力矩设定值torque_setpoint除以扭矩常数,电流设定值再乘以方向,根据电流限制函数得出电流限制值ilimid等于上位机设置的id设定值再用clamp函数保证范围,iq等于由扭矩算出的电流设定值current_setpoint再用clamp函数保证输出范围,之后计算下一时刻相位pwm_phase,利用传入相位phase加上相位电角速度phase_vel乘以1.5倍的采样周期current_meas_period,得出相位差的下一时刻计算相电角度。
之后将iq,id,phase,pwm_phase参数传入对应电机类型的FOC计算函数,详情可见**#3 FOC算法程序**。

bool Motor::update(float torque_setpoint, float phase, float phase_vel) {
    float current_setpoint = 0.0f;
    phase *= config_.direction;
    phase_vel *= config_.direction;

    if (config_.motor_type == MOTOR_TYPE_ACIM) {...}
    else {
        current_setpoint = torque_setpoint / config_.torque_constant;
    }
    current_setpoint *= config_.direction;

    // TODO: 2-norm vs independent clamping (current could be sqrt(2) bigger)
    float ilim = effective_current_lim();
    float id = std::clamp(current_control_.Id_setpoint, -ilim, ilim);
    float iq = std::clamp(current_setpoint, -ilim, ilim);

    if (config_.motor_type == MOTOR_TYPE_ACIM) {...}

    float pwm_phase = phase + 1.5f * current_meas_period * phase_vel;

    // Execute current command
    switch(config_.motor_type){
        case MOTOR_TYPE_HIGH_CURRENT: return FOC_current(id, iq, phase, pwm_phase); break;
        case MOTOR_TYPE_ACIM: return FOC_current(id, iq, phase, pwm_phase); break;
        case MOTOR_TYPE_GIMBAL: return FOC_voltage(id, iq, pwm_phase); break;
        default: set_error(ERROR_NOT_IMPLEMENTED_MOTOR_TYPE); return false; break;
    }
    return true;
}

本部分结束

原创不易,如果对你有帮助的话,希望可以点赞关注收藏,谢谢😊

  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值