6. 直流电机电流环控制实现¶

通过上一章节《直流电机速度环控制实现》的学习,我们大致的了解了PID在实际应用中是如何发挥它自动控制的作用的。在实际应用中,并不仅仅只有使用速度作为反馈来调节直流电机工作。相反地,现实中有许多参数可以作为反馈量,为特定场景下的直流电机控制带来更好的效果,例如本章节要讲到的电流环控制。电流环控制,简单的说,就是希望以恒定的电流来驱动电机运转,即希望电机能输出恒定的转矩。接下来,本章节就来介绍一下使用电流环来实现直流电机的电流控制。

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

6.1. 硬件设计¶

本章实验需要连接开发板和驱动板,这里给出接线表。

6.1.1. MOS管搭建驱动板¶

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

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

电机

MOS管搭建驱动板

电机输出M+

M+

电机输出M-

M-

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

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

MOS管搭建驱动板

主控板

PWM1

PA9

PWM2

PA8

SD

PG12

信号检测-电流

PB1

信号检测-电压

PB0

电源输入:5V

5V

电源输入:GND

GND

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

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

6.2.1. 软件设计1¶

软件部分和上一章大体相同,区别在于更换了输入PID的参数为电流值,代码中也增加了电流获取的功能,本章代码在野火电机驱动例程中\improve_part\F407\直流有刷电机-电流环控制-位置式PID目录下,下面我们详细来看。

6.2.1.1. 编程要点1¶

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

配置定时器可以读取当前电路中驱动电机的电流值

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

编写位置式PID算法

编写电流控制函数

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

编写按键控制代码

6.2.2. 软件分析1¶

6.2.2.1. PWM定时器配置¶

定时器如何配置前面章节多次提到,这边就略过不讲了,只展示初始化输出PWM的定时器初始化所调用的函数。如有疑问,之前的章节有详细讲解,代码也非常简单易懂。

Motor_TIMx_Configuration()函数¶

1

2

3

4

5

6

7

8

9

10 /**

* @brief 初始化控制通用定时器

* @param 无

* @retval 无

*/

void Motor_TIMx_Configuration(void)

{

TIMx_GPIO_Config();

TIM_PWMOUTPUT_Config();

}

6.2.2.2. 通用定时器定时获取电流值、计算PID¶

此代码位于野火电机开发板例程:直流有刷电机-电流环控制-位置式PID\User\motor_control\bsp_motor_control.c

motor_pid_control()函数¶

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 /**

* @brief 电机位置式 PID 控制实现(定时调用)

* @param 无

* @retval 无

*/

void motor_pid_control(void)

{

int32_t actual_current = get_curr_val(); // 读取当前电流值

if (is_motor_en == 1) // 电机在使能状态下才进行控制处理

{

float cont_val = 0; // 当前控制值

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

if (cont_val < 0)

{

cont_val = 0; // 下限处理

}

else if (cont_val > PWM_MAX_PERIOD_COUNT)

{

cont_val = PWM_MAX_PERIOD_COUNT; // 速度上限处理

}

set_motor_speed(cont_val); // 设置 PWM 占空比

#if defined(PID_ASSISTANT_EN)

set_computer_value(SEND_FACT_CMD, CURVES_CH1, &actual_current, 1); // 给通道 1 发送实际值

#else

printf("实际值:%d. 目标值:%.0f\n", actual_speed, get_pid_actual()); // 打印实际值和目标值

#endif

}

}

/**

* @brief 定时器每25ms产生一次中断回调函数

* @param htim:定时器句柄

* @retval 无

*/

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)

{

if(htim==(&TIM_TimeBaseStructure))

{

motor_pid_control();

}

}

代码中,由基本定时器每25ms产生一次中断,并在中断中调用motor_pid_control函数。该函数主要功能是获取当前驱动电机电流,通过PID计算反馈值并输出到PWM中。在代码的最后,使用set_computer_value发送下位机参数与上位机进行同步,这样一来就可以很直观的在上位机看到PID调整输出的过程。接下来我们在看看电流环的位置式PID算法。

6.2.2.3. 电流环位置式PID算法实现¶

此代码位于野火电机开发板例程:直流有刷电机-电流环控制-位置式PID\User\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

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43 /**

* @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;

}

/**

* @brief PID参数初始化

* @note 无

* @retval 无

*/

void PID_param_init()

{

/* 初始化参数 */

pid.target_val=80.0;

pid.actual_val=0.0;

pid.err=0.0;

pid.err_last=0.0;

pid.integral=0.0;

pid.Kp = 0;

pid.Ki = 3.5;

pid.Kd = 0;

#if defined(PID_ASSISTANT_EN)

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 值

#endif

}

这个函数主要实现了位置式PID算法,用传入的目标值减去实际值得到误差值得到比例项,在对误差值进行累加得到积分项,

用本次误差减去上次的误差得到微分项,然后通过前面章节介绍的位置式PID公式实现PID算法,并返回实际控制值。

这个公式就是代码第14行中的公式形式,公式和代码的计算方式基本一致,只不过在公式中第二项的Ki是使用的对误差积分,

在代码中变成了对误差的累加,虽然表达形式不一样,但是达到的效果和目的是一样的。

计算过后将误差传递用于下一次使用,并将实际值返回。

其实不难发现,上述的代码与上章的速度环位置式PID控制几乎没有区别,甚至是仅有PID参数的差异,可以看出PID控制算法,基本是有一定规律性的,难点就在于调参。上述代码中实现了PID参数的初始化,和PID算法的具体实现,代码看起来也是简单易懂的。

6.2.2.4. 主函数¶

主函数中主要初始化一些外设与上位机的协议,例如串口、定时器、ADC等,最后在主循环中轮询按键的事件和处理上位机数据。

主函数¶

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/**

* @brief 主函数

* @param 无

* @retval 无

*/

int main(void)

{

int32_t target_curr = 80;

/* HAL 库初始化 */

HAL_Init();

/* 初始化系统时钟为168MHz */

SystemClock_Config();

/* 初始化按键 GPIO */

Key_GPIO_Config();

/* 初始化 LED */

LED_GPIO_Config();

/* 协议初始化 */

protocol_init();

/* 初始化串口 */

DEBUG_USART_Config();

/* 电机初始化 */

motor_init();

set_motor_disable(); // 停止电机

/* 初始化基本定时器,用于处理定时任务 */

TIMx_Configuration();

/* PID 参数初始化 */

PID_param_init();

/* ADC 始化 */

ADC_Init();

#if defined(PID_ASSISTANT_EN)

set_computer_value(SEND_STOP_CMD, CURVES_CH1, NULL, 0); // 同步上位机的启动按钮状态

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

#endif

while(1)

{

/* 接收数据处理 */

receiving_process();

/* 扫描KEY1 */

if( Key_Scan(KEY1_GPIO_PORT, KEY1_PIN) == KEY_ON)

{

#if defined(PID_ASSISTANT_EN)

set_computer_value(SEND_START_CMD, CURVES_CH1, NULL, 0); // 同步上位机的启动按钮状态

#endif

set_pid_target(target_curr); // 设置目标值

set_motor_enable(); // 使能电机

}

/* 扫描KEY2 */

if( Key_Scan(KEY2_GPIO_PORT, KEY2_PIN) == KEY_ON)

{

set_motor_disable(); // 停止电机

#if defined(PID_ASSISTANT_EN)

set_computer_value(SEND_STOP_CMD, CURVES_CH1, NULL, 0); // 同步上位机的启动按钮状态

#endif

}

/* 扫描KEY3 */

if( Key_Scan(KEY3_GPIO_PORT, KEY3_PIN) == KEY_ON)

{

/* 增大目标速度 */

target_curr += 10;

if(target_curr > 120)

target_curr = 120;

set_pid_target(target_curr);

#if defined(PID_ASSISTANT_EN)

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

#endif

}

/* 扫描KEY4 */

if( Key_Scan(KEY4_GPIO_PORT, KEY4_PIN) == KEY_ON)

{

/* 减小目标速度 */

target_curr -= 10;

if(target_curr < 25)

target_curr = 25;

set_pid_target(target_curr);

#if defined(PID_ASSISTANT_EN)

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

#endif

}

}

}

6.2.3. 下载验证¶

下载程序到电机开发板,我们按下Key1键启动电机(看主函数),也可以通过上位机给PID算法输入目标值启动,开发板就能实时的通过PID运算并控制输出,见下图。

可以清楚的看到PID调整输出的过程,先急后缓。由于电流容易波动,采集出来的数据并不是特别稳定,也受采集的精度影响,所以调整曲线看起不是特别平滑,但是控制效果并不受太大的影响。

6.3. 直流电机电流环控制-增量式PID实现¶

6.3.1. 软件设计2¶

增量式PID与位置式PID总体上看就是实现算法的部分有所不同,但是实际控制输出等其他代码功能是完全相通的,所以在此只介绍增量式PID算法与位置式的不同之处。本章代码在野火电机驱动例程中\improve_part\F407\直流有刷电机-电流环控制-增量式PID目录下,下面我们详细来看。

6.3.1.1. 编程要点2¶

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

配置定时器可以读取当前电路中驱动电机的电流值

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

编写增量式PID算法

编写电流控制函数

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

编写按键控制代码

6.3.2. 软件分析2¶

6.3.2.1. 增量式PID参数初始化¶

增量式PID参数初始化-\pid\bsp_pid.c¶

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23/**

* @brief PID参数初始化

* @note 无

* @retval 无

*/

void PID_param_init()

{

/* 初始化参数 */

pid.target_val=80;

pid.actual_val=0.0;

pid.err = 0.0;

pid.err_last = 0.0;

pid.err_next = 0.0;

pid.Kp = 0;

pid.Ki = 2.8;

pid.Kd = 0;

#if defined(PID_ASSISTANT_EN)

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 值

#endif

}

从代码中,可以到看增量式PID的参数与位置式PID有所区别,这也是有增量式算法的算式决定的,下面我们再看看增量式算法的实现。

6.3.2.2. 增量式PID算法实现¶

增量式PID算法实现-\pid\bsp_pid.c¶

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20/**

* @brief PID算法实现

* @param actual_val:实际值

* @note 无

* @retval 通过PID计算后的输出

*/

float PID_realize(float actual_val)

{

/*计算目标值与实际值的误差*/

pid.err=pid.target_val-actual_val;

/*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;

}

这个函数主要实现了增量式PID算法,用传入的目标值减去实际值,得到当前偏差值,

在第12~14行中实现了下面公式中的增量式PID算法。

然后进行误差传递,将本次偏差和上次偏差保存下来,供下次计算时使用。

在第12行中将计算后的结果累加到pid.actual_val变量,最后返回该变量,用于控制电机的PWM占空比。

6.3.3. 下载验证¶

下载程序到电机开发板,我们按下Key1键启动电机),开发板就能实时的通过PID运算并控制输出,见下图。

从图中看到,增量式pid和位置式pid的控制效果区别不大,说明这两种算法的本质是相同的,只是实现的角度不一样。