11. 无刷电机位置环控制(BLDC)

承接上一章节的直流无刷电机速度环PID控制。 电机控制除了速度之外,另一种常见的应用就是对于电机旋转位置的控制。 本章节中我们就通过位置环的PID控制来实现直流无刷电机的位置控制。

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

11.1. 硬件设计

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

电机主控板与无刷电机驱动板连接见下表所示。

电机与无刷电机驱动板连接

电机

无刷电机驱动板

粗黄

U

粗绿

V

粗蓝

W

细红

+(编码器电源)

细黑

-(编码器电源)

细黄

HIU

细绿

HIV

细蓝

HIW

无刷电机驱动板与主控板连接见下表所示。

无刷电机驱动板与主控板连接

无刷电机驱动板

主控板

5V_IN

5V

GND

GND

U+

PE10

U-

PE13

V+

PE11

V-

PE14

W+

PE12

W-

PE15

HU

PB06

HV

PB07

HW

PA10

SD

PB15

推荐使用配套的牛角排线直接连接驱动板和主控板。连接开发板的那端,请连接在“无刷电机驱动接口2”上。

11.2. 直流无刷电机位置环控制-位置式PID实现

11.2.1. 软件设计1

这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 还有一些在前章节章节分析过的代码在这里也不在重复讲解,完整的代码请参考本章配套的工程。 本章代码在野火电机驱动例程中:\base_code\improve_part\F407\直流无刷电机-速度环控制-位置式PID目录下。

11.2.1.1. 编程要点1

  1. FSP库的定时器、外部中断配置

  2. 定时器、外部中断模块初始化

  3. 根据电机的换相表编写换相中断回调函数

  4. 根据定时器定义电机控制相关函数

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

  6. 编写位置式PID算法

  7. 编写位置控制函数

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

  9. 编写串口控制代码

11.2.2. 软件分析1

在编程要点中的与上一章节重复的部分,这里就不在详细讲解, 如果不明白请先学习前面相关章节的内容。这里主要讲解位置的获取方法和分析PID算法的控制实现部分。

bsp_motor_gpt.c-设置pwm输出的占空比
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 void set_pwm_pulse(uint16_t pulse)
 {
     timer_callback_args_t *p_args;
     p_args->event = TIMER_EVENT_CAPTURE_B;

   /* 设置定时器通道输出 PWM 的占空比 */
     bldcm_pulse = pulse;

     if ((motor_drive.enable_flag == 1))
         time1_callback(p_args);   // 执行一次换相
 }

该函数用于设置pwm输出的占空比,本函数只是将占空比存放在了bldcm_pulse变量中, 而真正设置正空比是在time1_callback()函数中, 所以在电机使能时需要调用time1_callback()设置pwm输出的占空比。

bsp_motor_gpt.c-更新电机速度和位置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 void update_motor_speed(uint32_t time)
 {
     float speed_x;
     static uint32_t now_time;
     static uint32_t last_time;

     now_time = time + (overflow_times * period);

     data_time = now_time - last_time;

     speed_x = (float)((data_time / 120000000.0) / 3.0);

     speed_x = (float)((1.0f / 12.0f) / (speed_x  / 60.0f));

     motor_drive.speed = speed_x * dir;

     motor_drive.location += dir;


     last_time = time;

     overflow_times = 0;
 }

该函数用于更新电机的当前速度和位置,其中形参time传入的是霍尔传感器有变化时定时器捕获到的值, 通过time就可以计算出一次换相的时间为(1.0/(120000000.0) * time)秒,电机旋转一圈共有12个变化信号, 所以电机的速度为:(1.0 / 12.0) / ((1.0 / (12000000.0 ) * time) / 60.0)RPM。 dir是电机的正反转方向,通过霍尔传感器去识别转子的正反转来获得。 将计算得到的速度保存在motor_drive.speed中。 同时将速度的积分–位置保存在 motor_drive.location中。

bsp_motor_gpt.c-更新电机速度方向
 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
void update_speed_dir(uint8_t dir_in)
{
    if(last_dir != 0 && (last_dir != dir_in))               //645132  623154
    {
        switch(dir_in)
        {
            case 1:
                if(last_dir == 3)
                {
                    dir = 1;
                }
                else
                {
                    dir = -1;
                }
                break;
            case 2:
                if(last_dir == 6)
                {
                    dir = 1;
                }
                else
                {
                    dir = -1;
                }
                break;
            case 3:
                if(last_dir == 2)
                {
                    dir = 1;
                }
                else
                {
                    dir = -1;
                }
                break;
            case 4:
                if(last_dir == 5)
                {
                    dir = 1;
                }
                else
                {
                    dir = -1;
                }
                break;
            case 5:
                if(last_dir == 1)
                {
                    dir = 1;
                }
                else
                {
                    dir = -1;
                }
                break;
            case 6:
                if(last_dir == 4)
                {
                    dir = 1;
                }
                else
                {
                    dir = -1;
                }
                break;
        }
    }

    last_dir = dir_in;
}

该函数用于更新电机的速度方向,使用当前读到的霍尔值,与上一次读到的霍尔值进行对比,来确定方向。

该函数用于更新电机的速度方向,使用当前读到的霍尔值,与上一次读到的霍尔值进行对比,来确定方向。

bsp_motor_gpt.c-换相实现函数
  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
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
void time1_callback(timer_callback_args_t *p_args)
{
    uint8_t step = 0;

    if(p_args->event != TIMER_EVENT_CYCLE_END)
    {
        step = Get_Hall_State();
    }

    if(p_args->event == TIMER_EVENT_CYCLE_END)
    {
        overflow_times++;
        if(bldcm_pulse != 0)            //防止在占空比减到0时,还在进行堵转判断
        {
            if (update++ >= 5)           // 有多次次在产生更新中断前霍尔传感器没有捕获到值
            {

                update = 0;

                LED1_ON;                // 点亮LED1表示堵转超时停止

                /* 堵转超时停止 PWM 输出 */
                hall_disable();         // 禁用霍尔传感器接口
                stop_pwm_output();      // 停止 PWM 输出

                while(1);
            }
        }
        else                            //占空比为0时,手动设置速度为0  因为速度的计算是通过霍尔中断进行  一旦停止转动  就会停在最后一刻的速度 而不会清0
        {
            data_time = 0;
        }

        return;
    }

    if((p_args->event == TIMER_EVENT_CAPTURE_A) || (p_args->event == TIMER_EVENT_CAPTURE_B))
    {
        update_speed_dir(step);
        update = 0;
        if(get_bldcm_direction() == MOTOR_FWD)
        {
            switch(step)
            {
                case 1:    /* U+ W- */
                    GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_u_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_HIGH);
                    break;

                case 2:     /* V+ U- */
                    GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_v_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_HIGH);

                    break;

                case 3:    /* V+ W- */
                    GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_v_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_HIGH);
                    break;

                case 4:     /* W+ V- */
                    GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_w_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_HIGH);
                    break;

                case 5:     /* U+  V- */
                    GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_u_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_HIGH);
                    break;

                case 6:     /* W+ U- */
                    GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_w_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_HIGH);
                    break;
            }
        }
        else
        {
            switch(step)
            {
                case 1:    /* W+ U- */
                    GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_w_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_HIGH);
                    break;

                case 2:     /* U+ V- */
                    GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_u_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_HIGH);

                    break;

                case 3:    /* W+ V- */
                    GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_w_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_HIGH);
                    break;

                case 4:     /* V+ W- */
                    GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_v_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_HIGH);
                    break;

                case 5:     /* V+ U- */
                    GPT_PWM_SetDuty(&motor_u_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_v_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_HIGH);
                    break;

                case 6:     /* U+ W- */
                    GPT_PWM_SetDuty(&motor_w_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_13, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_v_ctrl, 0, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_14, BSP_IO_LEVEL_LOW);

                    GPT_PWM_SetDuty(&motor_u_ctrl, bldcm_pulse, GPT_IO_PIN_GTIOCA);
                    R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_15, BSP_IO_LEVEL_HIGH);
                    break;
            }
        }
    }

    if(p_args->event == TIMER_EVENT_CAPTURE_A)
    {
        update_motor_speed(p_args->capture);
    }

}

换相的实现在直流无刷电机章节已经讲过这个不在赘述,在上面第7行的**if**里面判断了触发的中断源,如果是超时, 就进行堵转检测,如果是通道中断,就进行换向、更新速度、方向,这里注意的是,速度的计算,是通过通道A 去进行的,计算的是每次转动到指定位置所需要的时间,而更新方向,是换向的顺序,每一次换向都可以驱动方向。

bsp_pid.c-位置式PID参数初始化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 void PID_param_init()
 {
   /* 初始化参数 */
     pid.target_val=0.0;
     pid.actual_val=0.0;
     pid.err=0.0;
     pid.err_last=0.0;
     pid.integral=0.0;

     pid.Kp = 184;
     pid.Ki = 0;
     pid.Kd = 170;


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

PID_param_init()函数把结构体pid参数初始化,将目标、实际值、偏差值和积分项等初始化为0, 其中pid.Kp、pid.Ki和pid.Kd是我们配套电机运行效果相对比较好的参数,不同的电机该参数是不同的。 set_computer_value()函数用来同步上位机显示的PID值。

bsp_pid.c-位置式PID算法实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 int PID_realize(void)
 {
     float speed_a;
     speed_a = get_motor_location();   // 电机旋转的当前速度
     /*计算目标值与实际值的误差*/
     pid.err = pid.target_val - speed_a;
     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 (int)pid.actual_val;
 }

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

bsp_pid.c-电机位置式PID算法实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 void bldcm_pid_control(void)
 {
     if (bldcm_data.is_enable)
     {
         cont_val = PID_realize();

         if (cont_val < 0)
         {
                 cont_val = -cont_val;
                 bldcm_data.direction = MOTOR_REV;
         }
         else
         {
                 bldcm_data.direction = MOTOR_FWD;
         }


         cont_val = (cont_val > 990) ? 990 : cont_val;  // 上限处理

         set_bldcm_speed((uint16_t)cont_val);
     }
 }

该函数在定时器0的中断里定时调用默认是50毫秒调用一次,如果改变了周期那么PID三个参数也需要做相应的调整, PID的控制周期与控制效果是息息相关的。 调用PID_realize()去进行计算,然后得出来速度。 根据运算结果的正负,设置电机的旋转方向。 最后对输出的结果做一个上限处理,最后用于PWM占空比的控制,最后将实际的速度值发送到上位机绘制变化的曲线。

protocol.c-串口数据解析
 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
  /**
  * @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]);

        float p_temp, i_temp, d_temp;

        p_temp = *(float *)&temp0;
        i_temp = *(float *)&temp1;
        d_temp = *(float *)&temp2;

        set_p_i_d(p_temp, i_temp, d_temp);    // 设置 P I D
      }
      break;

      case SET_TARGET_CMD:
      {

        actual_temp = COMPOUND_32BIT(&frame_data[13]);    // 得到数据
        set_pid_target(actual_temp);    // 设置目标值

        set_computer_value(SEND_TARGET_CMD, CURVES_CH1,  &actual_temp, 1);
      }
      break;

      case START_CMD:
      {
          set_bldcm_enable();
      }
      break;

      case STOP_CMD:
      {

          motor_drive.speed = 0;                         //占空比为0时,手动设置速度为0  因为速度的计算是通过霍尔中断进行  一旦停止转动  就会停在最后一刻的速度 而不会清0
          set_bldcm_disable();

          set_computer_value(SEND_STOP_CMD, CURVES_CH1, NULL, 0);
      }
      break;

      case RESET_CMD:
      {
          Reset_Handler();
      }
      break;


      default:
        return -1;
    }
  }
}

这函数用于处理上位机发下的数据,在主函数中循环调用,可以使用上位机调整PID参数,使用上位机可以非常方便的调整PID参数, 这样可以不用每次修改PID参数时都要改代码、编译和下载代码;可以使用上位机设置目标速度;可以启动和停止电机; 可以使用上位机复位系统;可以使用上位机设置定时器的周期;具体功能的实现请参考配套工程代码。

main.c-主函数
 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
33
34
35
36
37
38
  void hal_entry(void)
  {
    int target_speed = 500;
    /* TODO: add your own code here */
    /* LED初始化 */
    LED_Init();

    Key_IRQ_Init();

    /* 串口初始化 */
    Debug_UART9_Init();

    /* 协议初始化 */
    protocol_init();

    PID_param_init();

    /* 电机初始化 */
    bldcm_init();
    basic_gpt0_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
  }

在主函数里面首先做了一些外设的初始化,然后在while中去检测串口是否收到数据, 并通过去解析上位机发来的数据,去进行相应的控制。

11.2.3. 下载验证1

我们按前面介绍的硬件连接好电机和驱动板。

将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 在上位机中,按下启动按钮,然后可以去发送目标值,电机就会运行起来,可以通过上位机去看速度的曲线,也可以 给电机加上一些负载,看PID实际的效果。我们可以通过上位机来观察速度的变化情况,也可以通过上位机来控制电机。

位置环位置式PID控制效果

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

11.3.1. 软件设计2

通过前面位置式PID控制的学习,大家应该对速度环PID控制有了更深刻的理解, 这里将只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 还有一些在前章节章节分析过的代码在这里也不在重复讲解,完整的代码请参考本节配套的工程。 本章代码在野火电机驱动例程中:\base_code\improve_part\F407\直流无刷电机-速度环控制-增量式PID目录下。

11.3.1.1. 编程要点2

  1. FSP库的定时器、外部中断配置

  2. 定时器、外部中断模块初始化

  3. 根据电机的换相表编写换相中断回调函数

  4. 根据定时器定义电机控制相关函数

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

  6. 编写位置式PID算法

  7. 编写位置控制函数

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

  9. 编写串口控制代码

11.3.2. 软件分析2

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

bsp_pid.c-增量式PID参数初始化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 void PID_param_init()
 {
   /* 初始化参数 */
   pid.target_val=0.0;
   pid.actual_val=0.0;
   pid.err=0.0;
   pid.err_last=0.0;

   pid.Kp = 165;
   pid.Ki = 0;
   pid.Kd = 148;


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

PID_param_init()函数把结构体pid参数初始化,将目标值、实际值、偏差值和上一次偏差值等初始化为0, 其中pid.err用来保存本次偏差值,pid.err_last用来保存上一次偏差值,pid.err_next用来保存上上次的偏差值; pid.Kp、pid.Ki和pid.Kd是我们配套电机运行效果相对比较好的参数,不同的电机该参数是不同的。 set_computer_value()函数用来同步上位机显示的PID值。

bsp_basic_gpt.c-增量式PID算法实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 int PID_realize(void)
 {
     float speed_a;
     speed_a = get_motor_location();   // 电机旋转的当前位置
     /*计算目标值与实际值的误差*/
     pid.err = pid.target_val - speed_a;


     /*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 (int)pid.actual_val;
 }

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

../../_images/PID_lisan4.png ../../_images/PID_lisan6.png

然后进行误差传递,将本次偏差和上次偏差保存下来,供下次计算时使用。 在第7行中将计算后的结果累加到pid.actual_val变量,最后返回该变量,用于控制电机的PWM占空比。

11.3.3. 下载验证2

我们按前面介绍的硬件连接好电机和驱动板。

将程序编译下载后,使用Type-C数据线连接开发板到电脑USB,打开野火调试助手-PID调试助手来观察电机的运行效果。 在上位机中,按下启动按钮,然后可以去发送目标值,电机就会运行起来,可以通过上位机去看速度的曲线,也可以 给电机加上一些负载,看PID实际的效果。我们可以通过上位机来观察速度的变化情况,也可以通过上位机来控制电机。

位置环增量式PID控制效果

注意

注意:电机正在运行时应该先停止电机再复位,而不建议直接复位开发板,因为这属于非正常操作,复位的瞬间电机还在继续运动,产生的反电动势有损坏硬件的风险。