5. 直流电机速度环控制实现

前面我们学习了直流电机简单的PWM控制。但是我们在实际使用中并不是只是简单的PWM控制就能满足应用要求, 通常我们还需要对速度进行控制控制,如前面章节中讲到的为什么使用PID一节中列举的小车控制一样, 如果不对速度进行控制可能系统运行效果会不如预期那么好,本章节中我们就通过速度环的PID控制来实现直流电机的速度控制。

本章通过我们前面学习的位置式PID和增量式PID两种控制方式分别来实现速度环的控制, 如果还不知道什么是位置式PID和增量式PID,请务必先学习前面PID算法的通俗解说这一章节。

5.1. 硬件设计

本章配套L298N和野火使用MOS管搭建的驱动板教程。

关于详细的硬件分析在直流有刷电机和编码器的使用章节中已经讲解过,这里不再做分析, 如有不明白请参考前面章节,这里只给出接线表。

5.1.1. L298N驱动板

电机与L298N驱动板连接见下表所示。

电机与L298N驱动板连接

电机

L298N驱动板

M+

电机输出:1

M-

电机输出:2

电机与主控板连接

电机

主控板

5V

VENC

GND

GND

A

PB06

B

PB07

L298N驱动板与主控板连接见下表所示。

L298N驱动板与主控板连接

L298N驱动板

主控板

PWM1

PE11

PWM2

PE10

GND

GND

ENA

PB15

5.1.2. MOS管搭建驱动板

电机与MOS管搭建驱动板连接见下表所示。

电机与MOS管搭建驱动板连接

电机

MOS管搭建驱动板

M+

M+

5V

编码器电源:+

GND

编码器电源:-

A

A

B

B

M-

M-

MOS管搭建驱动板与主控板连接见下表所示。

MOS管搭建驱动板与主控板连接

MOS管搭建驱动板

主控板

PWM1

PE11

PWM2

PE10

SD

PB15

A

PB06

B

PB07

电源输入:5V

5V

电源输入:GND

GND

推荐使用配套的牛角排线直接连接驱动板和主控板。

5.2. 直流电机速度环控制-位置式PID实现

5.2.1. 编程要点

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

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

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

  4. 编写位置式PID算法

  5. 编写速度控制函数

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

5.2.2. 软件设计

这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 还有一些在前章节章节分析过的代码在这里也不在重复讲解,完整的代码请参考本章配套的工程。 关于编码器的部分主要查看前面的编码器章节。

5.2.2.1. 新建工程

我们直接在基础部分的直流有刷章节的 “203_Motor_BDC_Encoder_SpeedMeasurement” 例程的基础上修改程序。

对于 e2 studio 开发环境:

拷贝一份我们之前的 e2s 工程 “203_Motor_BDC_Encoder_SpeedMeasurement”, 然后将工程文件夹重命名为 204_Motor_BDC_PID_Speed_Positional,最后再将它导入到我们的 e2 studio 工作空间中。

对于 Keil 开发环境:

拷贝一份我们之前的 Keil 工程 “203_Motor_BDC_Encoder_SpeedMeasurement”, 然后将工程文件夹重命名为 204_Motor_BDC_PID_Speed_Positional,并进入该文件夹里面双击 Keil 工程文件,打开该工程。

工程新建好之后,在工程根目录的 “src” 文件夹下面,将野火提供的 “pid” 、 “protocol” 文件夹添加进去。 其中主要是PID代码和上位机通信协议代码,关于其中的原理已经在提高部分第三章节讲过,本章不再赘述,只展示部分更改的代码。 工程文件结构如下。

文件结构
204_Motor_BDC_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

本例程只需要在FSP中新建一个按键模块进行电机方向的改变,其余只进行一些修改即可。

5.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);
}

5.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);
}

5.2.2.4. 定时器中断函数

该代码段通过 GPT 定时器中断回调函数来实时计算编码器的转轴速度。 首先, pulse_period 存储的是当前的脉冲周期值, last_pulse_period 则是上一次的脉冲周期。 在每次定时器周期结束时,计算当前的脉冲周期差值并更新 new_period。 通过宏 SHAFT_SPEED(new_period) 将脉冲周期差转换为转速。由于定时器周期是 25ms,为了将单位转换为转/秒,计算时乘以 40(1000ms / 25ms)。然后, 计算得到的转速值传递给 motor_pid_control(shaft_speed) 函数进行后续的 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; // 当前脉冲周期
    static float shaft_speed = 0.00f; // 转轴速度

    if (TIMER_EVENT_CYCLE_END == p_args->event)  // 检查定时器周期结束事件
    {
            // 更新最近的脉冲周期
            new_period = (pulse_period - last_pulse_period);

            // 计算转轴速度(单位:转/秒)
            shaft_speed = SHAFT_SPEED(new_period) * 40.0f;

            // 调用PID控制函数调整电机状态
            motor_pid_control(shaft_speed);

        // 更新脉冲周期
        last_pulse_period = pulse_period;
    }
}

5.2.2.5. PID算法初始化

这里开始编写位置式PID算法, 该代码段实现了 PID 控制参数的初始化,目标值 (target_val) 设为 50.0(转轴速度),实际值、误差 (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=50.0;
    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.1;

    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 值
}

5.2.2.6. PID算法实现

该函数主要实现了位置式PID算法,用传入的目标值减去实际值得到误差值得到比例项,在对误差值进行累加得到积分项, 用本次误差减去上次的误差得到微分项,然后通过前面章节介绍的位置式PID公式实现PID算法,并返回实际控制值。

../../_images/PID_lisan51.png
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;
}

5.2.2.7. 速度控制函数

这里开始编写速度控制函数,该函数在定时器的中断里定时调用默认是25毫秒调用一次,如果改变了周期那么PID三个参数也需要做相应的调整, PID的控制周期与控制效果是息息相关的。把实际速度带入PID_realize(actual_speed)进行运算, 最后对输出的结果做一个上限处理,最后用于PWM占空比的控制,最后将实际的速度值发送到上位机绘制变化的曲线。

bsp_motor_control.c-速度环pid控制
/**
* @brief  电机位置式 PID 控制实现(定时调用)
*
* 根据当前电机位置与目标位置,通过 PID 控制算法计算并调整电机 PWM 占空比,进而控制电机运动。
*
* @param[in] actual_current  当前电机实际位置(如速度、位置等)
* @retval 无
*/
void motor_pid_control(float actual_speed)
{
  uint32_t speed = (uint32_t)actual_speed;   // 将实际位置转换为整数类型

  if (motor_state == true)   // 当电机处于启用状态时
  {
      float cont_val = 0;   // 当前控制值

      // 进行 PID 计算
      cont_val = PID_realize(actual_speed);

      // 控制值下限处理
      if (cont_val < 0)
      {
          cont_val = 0;    // 下限为 0
      }
      // 控制值上限处理
      else if (cont_val > PWM_MAX_PERIOD_COUNT)
      {
          cont_val = PWM_MAX_PERIOD_COUNT;    // 上限为 PWM 最大周期
      }

      // 设置电机方向和 PWM 占空比
      Motor_Control_SetDirAndCount(motor_dir, cont_val);

      // 向通道 1 发送当前实际位置值
      set_computer_value(SEND_FACT_CMD, CURVES_CH1, &speed, 1);
  }
}

5.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;
    }
  }
}

5.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)
{
        uint8_t target_speed = 50;

        /* TODO: add your own code here */
        LED_Init();         // LED 初始化
        Debug_UART9_Init(); // SCI9 UART 调试串口初始化

        /* 电机PWM初始化 */
        Motor_GPT_PWM_Init();
        Motor_Control_Init();

        //初始化编码器
        initEncoder();

        /*使能定时器*/
        GPT_Timing_Init();

        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
}

5.2.3. 下载验证

我们按前面介绍的硬件连接好电机和驱动板,L298N和野火使用MOS管搭建的驱动板的程序是一样的。

将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 并通过上位机来控制电机,并且可以用Key2来改变电机方向,下图是不带负载的电机运行效果图。

速度环位置式PID控制效果

下图是带负载的运行效果图,在图中1点时增加负载(手捏住电机轴,谨慎操作),可以看到速度下降,但是马上有回升, 在2点速度还是在目标值附近波动,在3点去掉负载,速度上升后马上有下降到目标值;在4点增加负载, 紧接着改变目标值,在4点到5点间负载是一直存在的,可以到速度在有负载的情况下一样可以迅速达到目标值。

速度环位置式PID控制效果

5.3. 直流电机速度环控制-增量式PID实现

5.3.1. 软件设计

通过前面位置式PID控制的学习,大家应该对速度环PID控制有了更深刻的理解, 这里将只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 还有一些在前章节章节分析过的代码在这里也不在重复讲解,完整的代码请参考本节配套的工程。

5.3.1.1. 新建工程

我们直接在基础部分的直流有刷章节的 “204_Motor_BDC_PID_Speed_Positional” 例程的基础上修改程序。

对于 e2 studio 开发环境:

拷贝一份我们之前的 e2s 工程 “204_Motor_BDC_PID_Speed_Positional”, 然后将工程文件夹重命名为 “205_Motor_BDC_PID_Speed_Incremental”,最后再将它导入到我们的 e2 studio 工作空间中。

对于 Keil 开发环境:

拷贝一份我们之前的 Keil 工程 “204_Motor_BDC_PID_Speed_Positional”, 然后将工程文件夹重命名为 “205_Motor_BDC_PID_Speed_Incremental”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。

5.3.1.2. PID算法初始化

增量式PID实现的速度环控制和位置式PID现实的速度环控制其控制代码大部分都是一样的, 在上面的编程要点中只有第4项是不同的,其他代码均相同,所以这里将只讲解不一样的部分代码, 完整代码请参考本节配套工程。

bsp_pid.c-增量式PID参数初始化
void PID_param_init()
{
    /* 初始化参数 */
    pid.target_val=50.0;
    pid.actual_val=0.0;
    pid.err = 0.0;
    pid.err_last = 0.0;
    pid.err_next = 0.0;

    pid.Kp = 6;
    pid.Ki = 2.5;
    pid.Kd = 0;

#if defined(PID_ASSISTANT_EN)
    float pid_temp[3] = {pid.Kp, pid.Ki, pid.Kd};
    set_computer_value(SEED_P_I_D_CMD, CURVES_CH1, pid_temp, 3);     // 给通道 1 发送 P I D 值
#endif
}

PID_param_init()函数把结构体pid参数初始化,将目标值pid.target_val设置为50.0,将实际值、偏差值和上一次偏差值等初始化为0, 其中pid.err用来保存本次偏差值,pid.err_last用来保存上一次偏差值,pid.err_next用来保存上上次的偏差值; set_computer_value()函数用来同步上位机显示的PID值。

5.3.1.3. PID算法实现

这个函数主要实现了增量式PID算法,用传入的目标值减去实际值得到误差值得到当前偏差值, 在第6~8行中实现了下面公式中的增量式PID算法。

../../_images/PID_lisan43.png ../../_images/PID_lisan63.png

然后进行误差传递,将本次偏差和上次偏差保存下来,供下次计算时使用。 在第6行中将计算后的结果累加到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;

  if (pid.err > -50 && pid.err < 50)
  {
    pid.err = 0;
  }

  /*PID算法实现*/
  pid.actual_val += pid.Kp*(pid.err - pid.err_next)
                + pid.Ki*pid.err
                + pid.Kd*(pid.err - 2 * pid.err_next + pid.err_last);
  /*传递误差*/
  pid.err_last = pid.err_next;
  pid.err_next = pid.err;
  /*返回当前实际值*/
  return pid.actual_val;
}

5.3.2. 下载验证

我们按前面介绍的硬件连接好电机和驱动板,L298N和野火使用MOS管搭建的驱动板的程序是一样的,

将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 并通过上位机来控制电机,并且可以用Key2来改变电机方向,下图是电机运行效果图。

速度环位置式PID控制效果