11. 步进电机位置速度双环控制实现¶
在前面有关闭环控制的章节中,我们已经详细的讲解了步进电机单环闭环控制方法,虽然单环控制已经能很好地提高步进电机的性能了,但是仍有其局限性。
在前面两个章节的实验中不难发现,我们虽然使用速度环精确控制了步进电机的转速,但是停止的位置我们难以精确控制; 我们使用位置环来精确控制了步进电机转过的角度,却不得不人为限制速度来防止堵转。
那么我们能不能将位置环和速度环控制融合在一起,各取其长处,既实现位置的精确调节又实现速度的自动控制? 当然是可以的!这一章,我们将为大家讲解如何实现对位置环和速度环的套娃操作以及步进电机在位置和速度双闭环下的控制方法。
本章内容是对之前有关步进电机知识的综合应用,请充分学习和理解相关知识后再进行本章内容的学习。 在学习本章内容时,请打开配套的例程配合阅读。
11.1. 步进电机位置速度双闭环控制原理¶
步进电机闭环控制的原理已经在 步进电机速度环控制实现 这一章节详细说明,本章只对双环控制的原理做讲解。
在位置速度双环控制中,位置环为外环,速度环为内环。先向位置PID控制器传入目标位置作为期望值, 然后传入编码器的转轴位置作为实际值进行计算,我们将会得到一个计算值。 此时再把这个计算值传入速度PID控制器作为速度PID控制器的期望值(也就是目标速度), 然后传入编码器的转轴速度作为速度PID控制器的实际值进行计算, 最后我们将速度PID控制器的计算结果输出给步进电机。下图为步进电机位置速度双闭环控制的流程图, 为了方便叙述,没有将步机电机驱动器加入系统框图:

在这个系统中,编码器不仅起到了反馈位置的作用,也起到了反馈速度的作用。
小技巧
在PID参数整定时,采取先内环再外环的方法,也就是先单独使用速度环控制,得到满意的参数后, 再把位置环套在外面,整定位置环参数,最后根据整体效果对速度环参数进行微调。
11.2. 步进电机位置速度双环控制¶
11.2.1. 编程要点¶
在串级PID控制中,最外环一般选择期望控制的参数的环节,例如对应速度快慢的速度环、位置的位置环, 本代码的选择位置环作为最外环,位置作为控制量,期望控制电机实际位置,在主函数中,可以看出这点。
配置基本定时器可以产生定时中断来执行PID运算
编写位置式PID算法
编写位置环、速度环控制函数
增加上位机曲线观察相关代码
编写按键控制代码
11.2.2. 软件设计¶
11.2.2.1. 新建工程¶
我们直接在提高部分的直流有刷章节的 “304_Motor_Stepper_PID_Location_Positional” 例程的基础上修改程序。
- 对于 e2 studio 开发环境:
拷贝一份我们之前的 e2s 工程 “304_Motor_Stepper_PID_Location_Positional”, 然后将工程文件夹重命名为 306_Motor_Stepper_PID_Two_Ring,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前的 Keil 工程 “304_Motor_Stepper_PID_Location_Positional”, 然后将工程文件夹重命名为 306_Motor_Stepper_PID_Two_Ring,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
11.2.2.2. 定时器中断函数¶
定时器如何配置前面章节多次提到,这边就略过不讲了,只展示部分不同的代码。如有疑问,之前的章节有详细讲解,代码也非常简单易懂。
该代码通过获取当前的脉冲计数周期 pulse_period 和上一次周期 last_pulse_period 的差值, 将转轴方向计数 pulse_direction、和当前脉冲计数一并传入,用于电机位置环控制的调整。 最后,更新 last_pulse_period 以便为下一次周期计算准备数据。
/* GPT定时器中断回调函数 */
void gpt0_timing_callback(timer_callback_args_t *p_args)
{
static uint32_t last_pulse_period = 0; // 上一次的脉冲周期
static uint32_t new_period = 0; // 当前脉冲周期
if (TIMER_EVENT_CYCLE_END == p_args->event)
{
// 计算当前周期与上次周期的差值
new_period = (pulse_period - last_pulse_period);
// 调用PID控制函数调整电机状态
motor_pid_control((float)pulse_direction,(float)new_period);
}
last_pulse_period = pulse_period; // 更新上一次脉冲周期
}
11.2.2.3. PID算法初始化¶
与前面章节不同的是,由于引入了双环控制(位置环、速度环),所以在PID初始化时,对应的有两套PID参数。
从代码中,可以看到两套PID参数配置结构体,它们分别是位置环和速度环的PID参数配置, 每套 PID 参数均包括目标值、实际值、误差等变量的初始化,以及 Kp、Ki、Kd 系数的设置。 这两套配置,需要我们从内环到外环依次的调参。以本章工程为例,本章工程是位置环作为外环, 速度环作为内环,所以进行PID调参时,从速度环开始调参。
/**
* @brief PID参数初始化
* @note 无
* @retval 无
*/
void PID_param_init()
{
/* 初始化位置环PID参数 */
move_pid.target_val=ENCODER_TOTAL_RESOLUTION;
move_pid.actual_val=0.0;
move_pid.err=0.0;
move_pid.err_last=0.0;
move_pid.integral=0.0;
move_pid.Kp = 0.02f;
move_pid.Ki = 0.0;
move_pid.Kd = 0.0;
float move_pid_temp[3] = {move_pid.Kp, move_pid.Ki, move_pid.Kd};
set_computer_value(SEND_P_I_D_CMD, CURVES_CH1, move_pid_temp, 3);// 给通道 1 发送 P I D 值
/* 初始化速度环PID参数 */
speed_pid.target_val=0.0;
speed_pid.actual_val=0.0;
speed_pid.err=0.0;
speed_pid.err_last=0.0;
speed_pid.integral=0.0;
speed_pid.Kp = 5;
speed_pid.Ki = 2.0f;
speed_pid.Kd = 0.1f;
float speed_pid_temp[3] = {speed_pid.Kp, speed_pid.Ki, speed_pid.Kd};
set_computer_value(SEND_P_I_D_CMD, CURVES_CH2, speed_pid_temp, 3);// 给通道 2 发送 P I D 值
}
11.2.2.4. 位置环PID算法实现¶
代码的实现与前面章节基本相似,这里不再赘述。
/**
* @brief 位置式PID算法实现
* @param val:当前实际值
* @note 无
* @retval 通过PID计算后的输出
*/
float PID_realize(_pid *pid, float actual_val)
{
/*传入实际值*/
pid->actual_val = actual_val;
/*计算目标值与实际值的误差*/
pid->err = pid->target_val - pid->actual_val;
/*误差累积*/
pid->integral += pid->err;
/*PID算法实现*/
pid->actual_val = pid->Kp*pid->err+ pid->Ki*pid->integral+ pid->Kd*(pid->err-pid->err_last);
/*误差传递*/
pid->err_last = pid->err;
/*PID算法实现,并返回计算值*/
return pid->actual_val;
}
11.2.2.5. 双环控制函数¶
代码的整体实现逻辑,是位置环作为双环控制的外环,它的PID输出作为速度环(内环)的输入,以达到双环控制的目的。
当电机处于运动状态时,PID 控制器会计算目标位置与实际位置之间的误差,并通过调整电机的旋转方向和速度来减少误差。
输入与输出:
actual_location:电机的实际位置。
actual_speed:电机的实际速度。
工作流程:
使用 PID_realize 函数计算出一个新的 位置控制值 (move_cont_val),并决定电机的旋转方向。如果位置控制值为正,电机顺时针旋转;如果为负,则逆时针旋转,并将值取绝对值。
对计算得到的控制值进行上限处理,确保控制值不会超过最大目标速度( SPEED_MAX_TARGET)。
计算速度 PID 控制值 (speed_cont_val),并确保其在合理范围内(0 到 PWM 最大频率之间)。
通过 Motor_Control_SetSpeed 设置电机的目标速度,控制电机转速。
/**
* @brief 步进电机位置式PID控制
* @retval 无
* @note 基本定时器中断内调用
*/
void motor_pid_control(float actual_location,float actual_speed)
{
int32_t location = (int32_t)actual_location; // 将实际位置转换为整数类型
uint32_t speed = (uint32_t)actual_speed; // 将实际速度转换为整数类型
/* 经过pid计算后的期望值 */
static float speed_cont_val = 0.0f;
static float move_cont_val = 0.0f;
/* 当电机运动时才启动pid计算 */
if(motor_state == true )
{
/* 将实际位置作为输入传入PID控制器进行计算 */
move_cont_val = PID_realize(&move_pid, (float)actual_location); // 进行 PID 计算
// 根据计算结果控制步进电机的旋转方向
if (move_cont_val > 0)
{
STEP_CW; // 顺时针旋转
}
else
{
STEP_CCW; // 逆时针旋转
move_cont_val = -move_cont_val; // 取绝对值
}
// 控制值上限处理
move_cont_val >= SPEED_MAX_TARGET ? (move_cont_val = SPEED_MAX_TARGET) : move_cont_val;
uint32_t temp = (uint32_t)move_cont_val;
set_computer_value(SEND_TARGET_CMD, CURVES_CH2, &temp, 1); // 给通道 2 发送目标值
/* 设定速度的目标值 */
set_pid_target(&speed_pid, move_cont_val);
/* 单位时间内的编码器脉冲数作为实际值传入速度环pid控制器 */
speed_cont_val = PID_realize(&speed_pid, (float)actual_speed);// 进行 PID 计算
// 控制值下限处理
if (speed_cont_val < 0)
{
speed_cont_val = 0; // 下限为 0
}
// 控制值上限处理
else if (speed_cont_val > PWM_MAX_FREQUENCY)
{
speed_cont_val = PWM_MAX_FREQUENCY; // 上限为 PWM 最大频率
}
// 设置步进电机速度
Motor_Control_SetSpeed((uint32_t)speed_cont_val);
// 给通道 1 发送实际位置值
set_computer_value(SEND_FACT_CMD, CURVES_CH1, &location, 1);
// 给通道 2 发送实际位置值
set_computer_value(SEND_FACT_CMD, CURVES_CH2, &speed, 1);
}
}
11.2.2.6. 上位机相关代码¶
最后修改上位机曲线观察相关代码,该函数用于处理上位机发下的数据,在主函数中循环调用,可以使用上位机调整PID参数,使用上位机可以非常方便的调整PID参数, 主要对双环所接收的目标值和P、I、D值部分进行修改。
/**
* @brief 接收的数据处理
* @param void
* @return -1:没有找到一个正确的命令.
*/
int8_t receiving_process(void)
{
uint8_t frame_data[128]; // 要能放下最长的帧
uint16_t frame_len = 0; // 帧长度
uint8_t cmd_type = CMD_NONE; // 命令类型
packet_head_t packet;
while(1)
{
cmd_type = protocol_frame_parse(frame_data, &frame_len);
switch (cmd_type)
{
case CMD_NONE:
{
return -1;
}
case SET_P_I_D_CMD:
{
uint32_t temp0 = COMPOUND_32BIT(&frame_data[13]);
uint32_t temp1 = COMPOUND_32BIT(&frame_data[17]);
uint32_t temp2 = COMPOUND_32BIT(&frame_data[21]);
packet.ch = frame_data[CHX_INDEX_VAL];
float p_temp, i_temp, d_temp;
memcpy(&p_temp, &temp0, sizeof(float));
memcpy(&i_temp, &temp1, sizeof(float));
memcpy(&d_temp, &temp2, sizeof(float));
if (packet.ch == CURVES_CH1)
{
set_p_i_d(&move_pid, p_temp, i_temp, d_temp); // 设置 P I D
}
else if (packet.ch == CURVES_CH2)
{
set_p_i_d(&speed_pid, p_temp, i_temp, d_temp); // 设置 P I D
}
}
break;
case SET_TARGET_CMD:
{
int actual_temp = COMPOUND_32BIT(&frame_data[13]);
packet.ch = frame_data[CHX_INDEX_VAL];
/* 只设置位置的目标位置,速度的目标位置是由位置pid的输出决定的 */
if (packet.ch == CURVES_CH1)
{
set_pid_target(&move_pid, actual_temp); // 设置目标值
}
}
break;
case START_CMD:
{
Motor_Control_Start(); //启动电机
}
break;
case STOP_CMD:
{
Motor_Control_Stop(); //关闭电机
}
break;
case RESET_CMD:
{
NVIC_SystemReset(); //复位系统
}
break;
case SET_PERIOD_CMD:
{
uint32_t temp = COMPOUND_32BIT(&frame_data[13]); // 周期数,单位毫秒
SET_BASIC_TIM_PERIOD(temp); // 设置定时器周期1~1000ms
}
break;
default:
return -1;
}
}
}
11.2.3. 下载验证¶
下载程序到电机开发板,我们按下Key2和Key3键增加或减少电机位置,也可以通过上位机给PID算法输入目标值启动,开发板就能实时的通过PID运算并控制输出,见下图。
第一张图展示了外环控制(位置环)的输出结果。在实验中,我们对目标位置进行圈数的目标增加,可以观察到输出实际位置值逐步逼近目标位置,验证了外环 PID 的正确性。

第二张图展示了内环控制(速度环)的输出结果。在目标位置阶跃变化时,内环速度环能够迅速响应位置环的指令,并控制步进电机输出相应速度。速度变化呈现了明显的控制动态特性,表明内环 PID 控制效果良好。
