9. 步进电机速度环控制实现¶
在前面基础部分的章节中,我们已经详细的讲解了步进电机的基本控制方法,从这一章开始我们将为大家讲解如何对步进电机进行更高级的控制。
9.1. 步进电机闭环控制原理概述¶
根据前面基础部分的学习,我们知道步进电机是一种数字信号驱动的电机,其主要优点之一就是拥有很好的开环控制能力,控制系统不需要传感器和相应电路的反馈电机信息, 在负载不超载和脉冲频率合适的情况下,步进电机接收到的脉冲数和转子的角位移就是严格成正比关系。既然如此,那为什么要在步进电机上引入闭环控制呢? 虽然步进电机可以很好的开环控制,但实际在一些开环系统中,步进电机有可能由于自身性能及系统机械结构等因素的影响,在快速启停或负载突变时出现失步、过冲甚至堵转, 控制器无法知晓和矫正,这些现象在某些对精度要求较高的系统中可能导致严重后果。而加入传感器反馈组成闭环系统后,可以检测是否有失步等现象发生并及时纠正偏差。
目前的步进电机闭环控制方案有很多种,有些比较简单,只做失步的检测和矫正,有些非常复杂,可以完全控制步进电机的转矩和位置,改善步进电机的转矩频率特性, 降低发热和平均功耗,提高电机运行效率。平时听到的一些控制名词,比如速度环、位置环和电流环这些,就可以用于步进电机的闭环控制, 当然这些名词同样适用于其他的电机闭环系统。
在这里我们介绍一种以步进电机转速作为被控量(也就是速度环),使用旋转编码器作为反馈传感器,PID算法进行控制的闭环控制系统, 系统框图如下所示,为了方便叙述,没有将步机电机驱动器加入系统框图:

图中的旋转编码器作为反馈通道,负责收集步进电机转子在单位采样时间内的实际执行的步数,系统将实际步数转换为转速并与目标转速进行比较然后计算出偏差, 偏差值输入到PID控制器中,控制器输出经过修正的期望转速,最后交由步进电机执行。从框图中我们可以知道,当步进电机在运动过程中发生丢步, 会出现实际转速偏离目标转速,编码器将实际转速反馈给系统,控制器就能及时做出偏差修正。
9.2. 步进电机速度环控制–位置式PID实现¶
本实验的硬件设计与编码器的使用章节中的硬件设计完全相同,在此不再赘述。
本实验会结合之前章节的PID控制和步进电机编码器测速,来讲解如何使用增量式PID对步进电机进行速度闭环控制。 学习本小节内容时,请打开配套的工程配合阅读。
9.2.1. 编程要点¶
配置定时器可以输出PWM控制电机
配置定时器可以读取编码器的计数值
配置基本定时器可以产生定时中断来执行PID运算
编写位置式PID算法
编写速度控制函数
增加上位机曲线观察相关代码
9.2.2. 软件设计¶
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 还有一些在前章节章节分析过的代码在这里也不在重复讲解,完整的代码请参考本章配套的工程。 关于编码器的部分主要查看前面的编码器章节。
9.2.2.1. 新建工程¶
我们直接在基础部分的直流有刷章节的 “301_Motor_Stepper_Encoder_SpeedMeasurement” 例程的基础上修改程序。
- 对于 e2 studio 开发环境:
拷贝一份我们之前的 e2s 工程 “301_Motor_Stepper_Encoder_SpeedMeasurement”, 然后将工程文件夹重命名为 302_Motor_Stepper_PID_Speed_Positional,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前的 Keil 工程 “301_Motor_Stepper_Encoder_SpeedMeasurement”, 然后将工程文件夹重命名为 302_Motor_Stepper_PID_Speed_Positional,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
工程新建好之后,在工程根目录的 “src” 文件夹下面,将野火提供的 “pid” 、 “protocol” 文件夹添加进去。 其中主要是PID代码和上位机通信协议代码,关于其中的原理已经在提高部分第三章节讲过,本章不再赘述,只展示部分更改的代码。 工程文件结构如下。
302_Motor_Stepper_PID_Speed_Positional
├─ ......
└─ src
├─ beep
│ ├─ bsp_beep.c
│ └─ bsp_beep.h
├─ debug_uart
│ ├─ bsp_debug_uart.c
│ └─ bsp_debug_uart.h
├─ encoder
│ ├─ bsp_encoder.c
│ └─ bsp_encoder.h
├─ gpt
│ ├─ bsp_gpt_pwm_output.c
│ └─ bsp_gpt_pwm_output.h
│ ├─ bsp_gpt_timing.c
│ └─ bsp_gpt_timing.h
├─ key
│ ├─ bsp_key_irq.c
│ └─ bsp_key_irq.h
├─ led
│ ├─ bsp_led.c
│ └─ bsp_led.h
├─ motor_controls
│ ├─ bsp_motor_control.c
│ └─ bsp_motor_control.h
├─ pid
│ ├─ bsp_pid.c //新建文件
│ └─ bsp_pid.h //新建文件
├─ protocol
│ ├─ protocol.c //新建文件
│ └─ protocol.h //新建文件
└─ hal_entry.c
本例程只需要新添加一个按键中断模块,以及更改一些其它的参数即可,详细内容请参考例程,这里不再赘述。
9.2.2.2. GPT初始化代码¶
在编程要点中的1和2在前章节已经讲解过,这里就不在讲解,如果不明白请先学习前面相关章节的内容。 这里实现“配置基本定时器可以产生定时中断来执行PID运算”。
该代码用于初始化 GPT0 定时器,获取定时器周期的计数次数,并将计算后的周期时间发送至上位机,同时启动定时器以开始计时操作。
#define CLK_GPT_COUNTS 120000000 /**< @brief GPT 定时器时钟频率 (120 MHz) */
/**
* @brief GPT 初始化函数
*
* 初始化 GPT0 定时器模块,获取计时器周期的计数次数,并将目标值发送至上位机。
*
* @param[in] 无
* @retval 无
*/
void GPT_Timing_Init(void)
{
timer_info_t info; // 定时器信息结构体
uint32_t temp = 0; // 临时变量,用于存储周期时间(单位:毫秒)
/* 初始化 GPT0 模块 */
R_GPT_Open(&g_timer_gpt0_ctrl, &g_timer_gpt0_cfg);
/* 获取计时器一个周期所需的计数次数 */
R_GPT_InfoGet(&g_timer_gpt0_ctrl, &info);
// 计算周期时间(单位:毫秒)
temp = (uint32_t)(((float)info.period_counts / (float)CLK_GPT_COUNTS) * 1000);
/* 向通道 1 发送目标值 */
set_computer_value(SEND_PERIOD_CMD, CURVES_CH1, &temp, 1);
/* 启动 GPT0 定时器 */
R_GPT_Start(&g_timer_gpt0_ctrl);
}
9.2.2.3. 定时器配置函数¶
该代码用于设置 GPT 定时器的周期。 根据传入的毫秒周期值,计算出对应的周期计数值,并通过调用定时器的周期设置函数,更新定时器的周期。
/**
* @brief 设置 GPT 定时器的周期,单位为毫秒
* @param ms_period: 要设置的周期,单位为毫秒
* @retval 无
*/
void SET_BASIC_TIM_PERIOD(uint32_t ms_period)
{
// 计算周期计数值
uint32_t period_counts = (uint32_t)(((float)ms_period / 1000.0f) * (float)CLK_GPT_COUNTS);
// 调用 GPT 的周期设置函数
R_GPT_PeriodSet(&g_timer_gpt0_ctrl, period_counts);
}
9.2.2.4. 定时器中断函数¶
该代码段通过 GPT 定时器中断回调函数来实时计算编码器的转轴速度。 首先,pulse_period 存储的是当前的脉冲周期值,last_pulse_period 则是上一次的脉冲周期。 在每次定时器周期结束时,如果当前的脉冲周期与上次不同且有效, 计算当前的脉冲周期差值并更新 new_period。通过后续的 PID 控制调整电机状态。 至于 motor_pid_control 函数如何工作,在下面进行讲解。
/* 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) // 判断是否为周期结束事件
{
if (pulse_period > 0 && pulse_period != last_pulse_period) // 判断脉冲周期是否有效并且与上次周期不同
{
// 计算当前周期与上次周期的差值
new_period = (pulse_period - last_pulse_period);
// 调用电机PID控制函数,传入当前周期
motor_pid_control((float)new_period);
}
last_pulse_period = pulse_period; // 更新上一次脉冲周期
}
}
9.2.2.5. PID算法初始化¶
这里开始编写位置式PID算法, 该代码段实现了 PID 控制参数的初始化,目标值 (target_val) 设为 TARGET_SPR(1转/秒所需的脉冲),实际值、误差 (err) 和误差积累值 (integral) 都初始化为 0.0, 而比例系数 Kp、积分系数 Ki 和微分系数 Kd 分别设定为 5.0、2.0 和 0.1。 其中pid.Kp、pid.Ki和pid.Kd是我们配套电机运行效果相对比较好的参数,不同的电机该参数是不同的。 接下来,通过 set_computer_value 函数将这三个参数(Kp、Ki、Kd)发送到上位机的通道 1,用于后续的调整和监控。
/**
* @brief PID参数初始化
* @note 无
* @retval 无
*/
void PID_param_init()
{
/* 初始化参数 */
pid.target_val=TARGET_SPR;
pid.actual_val=0.0;
pid.err=0.0;
pid.err_last=0.0;
pid.integral=0.0;
pid.Kp = 5.0;
pid.Ki = 2.0;
pid.Kd = 0.1f;
float pid_temp[3] = {pid.Kp, pid.Ki, pid.Kd};
set_computer_value(SEND_P_I_D_CMD, CURVES_CH1, pid_temp, 3); // 给通道 1 发送 P I D 值
}
9.2.2.6. PID算法实现¶
该函数主要实现了位置式PID算法,用传入的目标值减去实际值得到误差值得到比例项,在对误差值进行累加得到积分项, 用本次误差减去上次的误差得到微分项,然后通过前面章节介绍的位置式PID公式实现PID算法,并返回实际控制值。
/**
* @brief 位置式PID算法实现
* @param actual_val:当前实际值
* @note 无
* @retval 通过PID计算后的输出
*/
float PID_realize(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;
}
9.2.2.7. 速度控制函数¶
这里开始编写速度控制函数,该函数在定时器的中断里定时调用默认是25毫秒调用一次,如果改变了周期那么PID三个参数也需要做相应的调整, PID的控制周期与控制效果是息息相关的。把实际速度带入PID_realize(actual_speed)进行运算, 最后对输出的结果做一个上限处理,最后用于PWM占空比的控制,最后将实际的速度值发送到上位机绘制变化的曲线。
/**
* @brief 步进电机位置式PID控制
* @retval 无
* @note 基本定时器中断内调用
*/
void motor_pid_control(float actual_speed)
{
uint32_t speed = (uint32_t)actual_speed; // 将实际速度转换为整数类型
/* 经过pid计算后的期望值 */
volatile float cont_val = 0;
/* 当电机运动时才启动pid计算 */
if(motor_state == true )
{
/* 将实际速度作为输入传入PID控制器进行计算 */
cont_val = PID_realize(actual_speed); // 进行 PID 计算
// 控制值下限处理
if (cont_val < 0)
{
cont_val = 0; // 下限为 0
}
// 控制值上限处理
else if (cont_val > PWM_MAX_FREQUENCY)
{
cont_val = PWM_MAX_FREQUENCY; // 上限为 PWM 最大频率
}
// 设置电机速度
Motor_Control_SetSpeed((uint32_t)cont_val);
// 给通道 1 发送实际值
set_computer_value(SEND_FACT_CMD, CURVES_CH1, &speed, 1);
}
}
9.2.2.8. 上位机相关代码¶
最后增加上位机曲线观察相关代码,该函数用于处理上位机发下的数据,在主函数中循环调用,可以使用上位机调整PID参数,使用上位机可以非常方便的调整PID参数, 这样可以不用每次修改PID参数时都要改代码、编译和下载代码;可以使用上位机设置目标速度;可以启动和停止电机; 可以使用上位机复位系统;可以使用上位机设置定时器的周期;具体功能的实现请参考配套工程代码。
/**
* @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; // 命令类型
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]);
memcpy(&p_temp, &temp0, sizeof(float));
memcpy(&i_temp, &temp1, sizeof(float));
memcpy(&d_temp, &temp2, sizeof(float));
set_p_i_d(p_temp, i_temp, d_temp); // 设置 P I D
}
break;
case SET_TARGET_CMD:
{
int actual_temp = COMPOUND_32BIT(&frame_data[13]); // 得到数据
set_pid_target(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;
}
}
}
9.2.2.9. hal_entry函数¶
在主函数里面首先做了一些外设的初始化,然后通过上位机来观察、控制电机。
// 按键2中断回调函数
void sw2_irq_callback(external_irq_callback_args_t *p_args)
{
// 防止回调函数中没有使用形参的警告产生
FSP_PARAMETER_NOT_USED(p_args);
// 反转电机方向
Motor_Control_Reverse();
}
void hal_entry(void)
{
/* TODO: add your own code here */
/*目标值*/
uint32_t target_speed = 100;
LED_Init(); // LED 初始化
Debug_UART9_Init();//调试串口初始化
IRQ_Init();//按键中断初始化
Motor_Control_Init(); //初始化电机定时器
/*使能定时器*/
GPT_Timing_Init();
//初始化编码器
initEncoder();
Motor_Control_Stop(); // 调用电机停止函数
/* 协议初始化 */
protocol_init();
/*PID参数初始化*/
PID_param_init();
set_computer_value(SEND_STOP_CMD, CURVES_CH1, NULL, 0); // 同步上位机的启动按钮状态
set_computer_value(SEND_TARGET_CMD, CURVES_CH1, &target_speed, 1); // 给通道 1 发送目标值
while(1)
{
/* 接收数据处理 */
receiving_process();
}
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
9.2.3. 下载验证¶
我们按前面介绍的硬件连接好电机,将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 并通过上位机来控制电机,并且可以用Key2来改变电机方向,下图是电机运行效果图。

9.3. 步进电机速度环控制-增量式PID实现¶
9.3.1. 软件设计¶
通过前面位置式PID控制的学习,大家应该对速度环PID控制有了更深刻的理解, 这里将只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 还有一些在前章节章节分析过的代码在这里也不在重复讲解,完整的代码请参考本节配套的工程。
9.3.1.1. 新建工程¶
我们直接在基础部分的直流有刷章节的 “302_Motor_Stepper_PID_Speed_Positional” 例程的基础上修改程序。
- 对于 e2 studio 开发环境:
拷贝一份我们之前的 e2s 工程 “302_Motor_Stepper_PID_Speed_Positional”, 然后将工程文件夹重命名为 “303_Motor_Stepper_PID_Speed_Incremental”,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前的 Keil 工程 “302_Motor_Stepper_PID_Speed_Positional”, 然后将工程文件夹重命名为 “303_Motor_Stepper_PID_Speed_Incremental”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
9.3.1.2. PID算法初始化¶
增量式PID实现的速度环控制和位置式PID现实的速度环控制其控制代码大部分都是一样的, 在上面的编程要点中只有第4项是不同的,其他代码均相同,所以这里将只讲解不一样的部分代码, 完整代码请参考本节配套工程。
/**
* @brief PID参数初始化
* @note 无
* @retval 无
*/
void PID_param_init()
{
/* 初始化参数 */
pid.target_val=TARGET_SPR;
pid.actual_val=0.0;
pid.err=0.0;
pid.err_last=0.0;
pid.integral=0.0;
pid.Kp = 5.0;
pid.Ki = 2.0;
pid.Kd = 0.1f;
float pid_temp[3] = {pid.Kp, pid.Ki, pid.Kd};
set_computer_value(SEND_P_I_D_CMD, CURVES_CH1, pid_temp, 3); // 给通道 1 发送 P I D 值
}
9.3.1.3. PID算法实现¶
这个函数主要实现了增量式PID算法,用传入的目标值减去实际值得到误差值得到当前偏差值, 然后进行误差传递,将本次偏差和上次偏差保存下来,供下次计算时使用。 将计算后的结果累加到pid.actual_val变量,最后返回该变量,用于控制电机的PWM占空比。
/**
* @brief 增量式PID算法实现
* @param actual_val:实际值
* @note 无
* @retval 通过PID计算后的输出
*/
float PID_realize(float actual_val)
{
/*计算目标值与实际值的误差*/
pid.err=pid.target_val-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;
/*返回当前实际值*/
return pid.actual_val;
}
9.3.2. 下载验证¶
我们按前面介绍的硬件连接好电机和驱动板,将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 并通过上位机来控制电机,并且可以用Key2来改变电机方向,下图是电机运行效果图。
