9. 步进电机速度环控制实现

在前面基础部分的章节中,我们已经详细的讲解了步进电机的基本控制方法,从这一章开始我们将为大家讲解如何对步进电机进行更高级的控制。

9.1. 步进电机闭环控制原理概述

根据前面基础部分的学习,我们知道步进电机是一种数字信号驱动的电机,其主要优点之一就是拥有很好的开环控制能力,控制系统不需要传感器和相应电路的反馈电机信息, 在负载不超载和脉冲频率合适的情况下,步进电机接收到的脉冲数和转子的角位移就是严格成正比关系。既然如此,那为什么要在步进电机上引入闭环控制呢? 虽然步进电机可以很好的开环控制,但实际在一些开环系统中,步进电机有可能由于自身性能及系统机械结构等因素的影响,在快速启停或负载突变时出现失步、过冲甚至堵转, 控制器无法知晓和矫正,这些现象在某些对精度要求较高的系统中可能导致严重后果。而加入传感器反馈组成闭环系统后,可以检测是否有失步等现象发生并及时纠正偏差。

目前的步进电机闭环控制方案有很多种,有些比较简单,只做失步的检测和矫正,有些非常复杂,可以完全控制步进电机的转矩和位置,改善步进电机的转矩频率特性, 降低发热和平均功耗,提高电机运行效率。平时听到的一些控制名词,比如速度环、位置环和电流环这些,就可以用于步进电机的闭环控制, 当然这些名词同样适用于其他的电机闭环系统。

在这里我们介绍一种以步进电机转速作为被控量(也就是速度环),使用旋转编码器作为反馈传感器,PID算法进行控制的闭环控制系统, 系统框图如下所示,为了方便叙述,没有将步机电机驱动器加入系统框图:

../../_images/步进电机闭环控制原理.png

图中的旋转编码器作为反馈通道,负责收集步进电机转子在单位采样时间内的实际执行的步数,系统将实际步数转换为转速并与目标转速进行比较然后计算出偏差, 偏差值输入到PID控制器中,控制器输出经过修正的期望转速,最后交由步进电机执行。从框图中我们可以知道,当步进电机在运动过程中发生丢步, 会出现实际转速偏离目标转速,编码器将实际转速反馈给系统,控制器就能及时做出偏差修正。

9.2. 步进电机速度环控制–位置式PID实现

本实验的硬件设计与编码器的使用章节中的硬件设计完全相同,在此不再赘述。

本实验会结合之前章节的PID控制和步进电机编码器测速,来讲解如何使用增量式PID对步进电机进行速度闭环控制。 学习本小节内容时,请打开配套的工程配合阅读。

9.2.1. 编程要点

  1. 配置定时器可以输出PWM控制电机

  2. 配置定时器可以读取编码器的计数值

  3. 配置基本定时器可以产生定时中断来执行PID运算

  4. 编写位置式PID算法

  5. 编写速度控制函数

  6. 增加上位机曲线观察相关代码

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 定时器,获取定时器周期的计数次数,并将计算后的周期时间发送至上位机,同时启动定时器以开始计时操作。

bsp_gpt_timing.c-GPT初始化代码
#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 定时器的周期。 根据传入的毫秒周期值,计算出对应的周期计数值,并通过调用定时器的周期设置函数,更新定时器的周期。

bsp_gpt_timing.c-定时器配置函数
/**
* @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 函数如何工作,在下面进行讲解。

bsp_motor_control.c-定时器中断函数
/* 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 函数将这三个参数(KpKiKd)发送到上位机的通道 1,用于后续的调整和监控。

bsp_pid.c-位置式PID算法初始化
/**
  * @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算法,并返回实际控制值。

bsp_pid.c-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占空比的控制,最后将实际的速度值发送到上位机绘制变化的曲线。

bsp_motor_control.c-速度环pid控制
/**
  * @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参数时都要改代码、编译和下载代码;可以使用上位机设置目标速度;可以启动和停止电机; 可以使用上位机复位系统;可以使用上位机设置定时器的周期;具体功能的实现请参考配套工程代码。

protocol.c-串口数据解析
/**
* @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函数

在主函数里面首先做了一些外设的初始化,然后通过上位机来观察、控制电机。

hal_entry.c-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来改变电机方向,下图是电机运行效果图。

速度环位置式PID控制效果

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项是不同的,其他代码均相同,所以这里将只讲解不一样的部分代码, 完整代码请参考本节配套工程。

bsp_pid.c-增量式PID参数初始化
/**
  * @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占空比。

bsp_pid.c-增量式PID算法实现
/**
  * @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来改变电机方向,下图是电机运行效果图。

速度环位置式PID控制效果