7. 直流电机位置环控制实现

位置控制模式一般是通过编码器产生的脉冲的个数来确定转动的角度或者是转的圈数, 由于位置模式可以位置进行严格的控制, 所以一般应用于定位装置。应用领域如数控机床、印刷机械等等。

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

7.1. 硬件设计

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

7.1.1. L298N驱动板

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

电机与L298N驱动板连接

电机

L298N驱动板

M+

电机输出:1

M-

电机输出:2

电机与主控板连接见下表所示。主控板上的J27需要用跳冒将VENC连接到5V。

电机与主控板连接

电机

主控板

5V

VENC

GND

GND

A

PB06

B

PB07

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

L298N驱动板与主控板连接

L298N驱动板

主控板

PWM1

PE11

PWM2

PE10

GND

GND

ENA

PB15

在L298N驱动板与主控板连接中,ENA可以不接PG12,使用跳冒连接到5V。

7.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

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

7.2. 直流电机位置环控制-位置式PID实现

7.2.1. 编程要点

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

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

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

  4. 编写位置式PID算法

  5. 编写速度控制函数

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

  7. 编写按键控制代码

7.2.2. 软件设计

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

7.2.2.1. 新建工程

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

对于 e2 studio 开发环境:

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

对于 Keil 开发环境:

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

工程文件结构如下。

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

7.2.2.2. FSP配置

本例程需要使用按键来调节位置,这样更有利于观察实验现象,所以只需要创建按键中断函数即可, 关于按键中断的内容可以查看《瑞萨RA系列FSP库开发实战指南》的 “ICU——外部中断” 章节, 也可以参考 “002_GPIO_KEY_IRQ” 例程,这里不再赘述。

7.2.2.3. 编码器定时器中断函数

在编程要点1在前章节已经讲解过,这里就不在讲解,如果不明白请先学习前面相关章节的内容。 这里需要对读取编码器的计数值部分进行更改。

这段代码的改动主要目的是在位置环控制中实现对电机方向的准确控制,通过让 pulse_period 支持正负数值, 可以直接用来区分正转(计数增加)和反转(计数减少),从而避免额外的方向处理逻辑。 在正转事件 TIMER_EVENT_CAPTURE_A 中,pulse_period 自增,表示编码器正向移动; 而在反转事件 TIMER_EVENT_CAPTURE_B 中,pulse_period 自减,表示编码器反向移动。

定时器配置函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* 全局变量 */
volatile int32_t pulse_period = 0;   // 脉冲数
volatile _Bool flag = true;          // 方向标志,true 为正转,false 为反转

/* 编码器中断回调函数 */
void encoder_callback(timer_callback_args_t *p_args)
{
  switch (p_args->event)
  {
      /* 捕获事件:正转计数(捕获A事件) */
      case TIMER_EVENT_CAPTURE_A:
          flag = true; // 设置为正转方向
          //更新计数值
          pulse_period++;
          break;

      /* 捕获事件:反转计数(捕获B事件) */
      case TIMER_EVENT_CAPTURE_B:
          flag = false; // 设置为反转方向
          //更新计数值
          pulse_period--;
          break;

      /* 定时器溢出事件 */
      case TIMER_EVENT_CYCLE_END:
          overflow_count++; // 增加溢出计数
          break;

      default:
          break;
  }
}

7.2.2.4. 定时器中断函数

这段代码的作用是通过 GPT 定时器周期结束事件触发位置环控制逻辑。在定时器中断回调函数 gpt0_timing_callback 中, 利用 pulse_period 的值调用 motor_pid_control 函数,调整电机的运行状态。 这里将 pulse_period 显式转换为浮点数 (float)pulse_period, 以确保与 motor_pid_control 函数的输入类型匹配,同时避免整型溢出或精度不足的问题。

这种实现方式通过 pulse_period 的正负值和大小直接反映当前位置的偏移量, 结合 PID 控制算法能够有效地实现电机的精准定位。 需要注意的是,pulse_period 的更新和计算准确性在很大程度上决定了位置环的控制精度。 因此,建议在编码器的事件回调中严格确保 pulse_period 的正确性和实时性。

bsp_motor_control.c-定时器中断函数
/* GPT定时器中断回调函数 */
void gpt0_timing_callback(timer_callback_args_t *p_args)
{

  if (TIMER_EVENT_CYCLE_END == p_args->event)  // 检查定时器周期结束事件
  {
          // 调用PID控制函数调整电机状态
          motor_pid_control((float)pulse_period);
  }
}

7.2.2.5. PID算法初始化

这里开始编写位置式PID算法, 该代码段实现了 PID 控制参数的初始化,目标值 (target_val) 设为 CIRCLE_PULSES(转一圈所捕获的脉冲值), 实际值、误差 (err) 和误差积累值 (integral) 都初始化为 0.0, 其中pid.Kp、pid.Ki和pid.Kd是我们配套电机运行效果相对比较好的参数,不同的电机该参数是不同的。 接下来,通过 set_computer_value 函数将这三个参数(KpKiKd)发送到上位机的通道 1,用于后续的调整和监控。

bsp_pid.c-位置式PID算法初始化
// 编码器一圈可以捕获的脉冲
#define CIRCLE_PULSES    (ENCODER_TOTAL_RESOLUTION * REDUCTION_RATIO)

/**
  * @brief  PID参数初始化
  *   @note   无
  * @retval 无
  */
void PID_param_init()
{
        /* 初始化参数 */
        pid.target_val=CIRCLE_PULSES;
        pid.actual_val=0.0;
        pid.err=0.0;
        pid.err_last=0.0;
        pid.integral=0.0;

        pid.Kp = 24;
        pid.Ki = 0;
        pid.Kd = 28;

        float pid_temp[3] = {pid.Kp, pid.Ki, pid.Kd};
        set_computer_value(SEND_P_I_D_CMD, CURVES_CH1, pid_temp, 3);     // 给通道1发送PID值

}

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

7.2.2.7. 速度控制函数

该代码实现了电机位置式 PID 控制的核心逻辑,根据当前电机的实际速度,通过 PID 算法计算出控制值, 并设置电机的运行方向和 PWM 占空比以控制电机的运动状态。 该函数在定时器的中断里定时调用默认是25毫秒调用一次,如果改变了周期那么PID三个参数也需要做相应的调整, PID的控制周期与控制效果是息息相关的。以下是代码的主要说明:

  • PID 计算:调用 PID_realize(actual_location) 函数计算出控制值 cont_val,该值决定电机运行方向和占空比。

  • 方向设置:通过判断 cont_val 的正负值来设置电机方向。若 cont_val 为负,则需取其绝对值并设置为反方向。

  • 控制值限制:为了避免控制值过大导致电机满占空比运行,将 cont_val 限制在 PWM 最大周期的 50% 以内,可以根据实际表现调整。

  • 电机控制:调用 Motor_Control_SetDirAndCount(motor_dir, cont_val) 函数设置电机的方向和 PWM 占空比,实现具体的运动控制。

  • 数据传输:通过 set_computer_value(SEND_FACT_CMD, CURVES_CH1, &location, 1) 函数将当前实际速度发送到上位机,方便监测和记录。

后续可以结合目标位置、当前位置的偏差进一步完善位置环控制逻辑。

bsp_motor_control.c-位置环pid控制
/**
* @brief  电机位置式 PID 控制实现(定时调用)
*
* 根据当前电机位置,通过 PID 控制算法计算并调整电机 PWM 占空比,进而控制电机运动方向和速度。
*
* @param[in] actual_location  当前电机实际速度
* @retval 无
*/
void motor_pid_control(float actual_location)
{
    int32_t location = (int32_t)actual_location;  // 将实际速度转换为整数类型
    float cont_val = 0;                     // 当前控制值

    if (motor_state == true)  // 当电机处于启用状态时
    {
        // 进行 PID 计算
        cont_val = PID_realize(actual_location);

        // 根据控制值设置电机方向
        if (cont_val > 0)
        {
            motor_dir = true;  // 正方向
        }
        else
        {
            cont_val = -cont_val;  // 取正值
            motor_dir = false;     // 反方向
        }

        // 控制值上限处理
        if (cont_val > PWM_MAX_PERIOD_COUNT * 0.5)
        {
            cont_val = PWM_MAX_PERIOD_COUNT * 0.5;  // 限制最大 PWM 周期为 50%
        }

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

        // 向通道 1 发送当前实际速度值
        set_computer_value(SEND_FACT_CMD, CURVES_CH1, &location, 1);
    }
}

7.2.2.8. 按键中断函数

这段代码通过按键中断回调函数实现目标位置的动态调整,适用于位置环控制。 按键2的回调函数会将目标位置 target_location 增加一圈( CIRCLE_PULSES ), 按键3的回调函数则减少一圈。每次调整后,通过 set_pid_target 将目标值传递给 PID 控制器, 并调用 set_computer_value 将目标位置发送至上位机,方便实时监测位置目标的变化。

hal_entry.c-按键中断函数
/*目标值*/
int32_t target_location = CIRCLE_PULSES;

//按键2中断回调函数
void sw2_irq_callback(external_irq_callback_args_t *p_args)
{
    //防止回调函数中没有使用形参的警告产生
    FSP_PARAMETER_NOT_USED(p_args);

    /* 增加一圈 */
    target_location +=  (uint32_t)CIRCLE_PULSES;
    set_pid_target((float)target_location);

    set_computer_value(SEND_TARGET_CMD, CURVES_CH1, &target_location, 1);     // 给通道 1 发送目标值
}

//按键3中断回调函数
void sw3_irq_callback(external_irq_callback_args_t *p_args)
{
    //防止回调函数中没有使用形参的警告产生
    FSP_PARAMETER_NOT_USED(p_args);

    /* 增加一圈 */
    target_location -=  (int32_t)CIRCLE_PULSES;
    set_pid_target((float)target_location);

    set_computer_value(SEND_TARGET_CMD, CURVES_CH1, &target_location, 1);     // 给通道 1 发送目标值
}

7.2.2.9. hal_entry函数

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

hal_entry.c-hal_entry主函数
void hal_entry(void)
{

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

    /* 电机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_location, 1);     // 给通道 1 发送目标值

    while(1)
    {
        /* 接收数据处理 */
        receiving_process();
    }


#if BSP_TZ_SECURE_BUILD
    /* Enter non-secure code */
    R_BSP_NonSecureEnter();
#endif
}

7.2.3. 下载验证

我们按前面介绍的硬件连接好电机和驱动板,L298N和野火使用MOS管搭建的驱动板的程序是一样的。 将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 并通过上位机来控制电机,按下Key2增加一圈,按下Key3减少一圈,下图是电机运行效果图。

位置环位置式PID控制效果

7.3. 直流电机位置环控制-增量式PID实现

7.3.1. 软件设计

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

7.3.1.1. 新建工程

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

对于 e2 studio 开发环境:

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

对于 Keil 开发环境:

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

7.3.1.2. PID算法初始化

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

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

    pid.Kp = 23;
    pid.Ki = 0.03f;
    pid.Kd = 28;

    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 值

}

7.3.1.3. PID算法实现

这个函数主要实现了增量式PID算法,用传入的目标值减去实际值得到误差值得到当前偏差值。 然后进行误差传递,将本次偏差和上次偏差保存下来,供下次计算时使用。

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

7.3.2. 下载验证

我们按前面介绍的硬件连接好电机和驱动板,L298N和野火使用MOS管搭建的驱动板的程序是一样的。 将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 并通过上位机来控制电机,按下Key2增加一圈,按下Key3减少一圈,下图是电机运行效果图。

位置环增量式PID控制效果