第12章直流有刷电机位置环控制实现​


本章我们主要来学习直流有刷电机位置环PID控制的原理,并实现位置环PID控制的实验。

本章分为如下几个小节:

12.1位置环PID控制原理

12.2 硬件设计

12.3 程序设计

12.4 下载验证


12.1 位置环PID控制原理

位置环PID控制的原理非常简单,我们用编码器计数总值代表电机的位置,然后把PID控制流程中的控制对象换成电机位置即可,如图12.1.1所示:

《DMF407电机控制专题教程》第12章 直流有刷电机位置环控制实现_编码器


12.1.1位置环PID控制流程

图12.1.1中,我们首先设置目标位置,系统会计算出偏差e,然后将偏差输入到PID控制的三个环节中,PID计算后的输出值用于控制PWM的占空比,进而控制电机的位置。

12.2 硬件设计

1. 例程功能

1、本实验以电机开发板的直流有刷电机驱动接口1为例,基于编码器测速实验,加入位置环PID控制算法,对电机的位置进行闭环控制。

2、当按键0按下,就增大目标位置值,电机正转到目标位置;当按键1按下,就减小目标位置值,电机反转到目标位置;当按键2按下,电机回到初始位置。

3、屏幕显示按键功能、占空比、目标位置以及实际位置。

4、串口1和上位机进行数据通信。

5、LED0闪烁指示程序运行。

2. 硬件资源

1LED

LED0 – PE0

2)独立按键

KEY0 – PE2

KEY1 – PE3

KEY2 – PE4

3)定时器1、3、6

TIM1正常输出通道 PA8

互补输出通道 PB13

TIM3 编码器A相输入通道 PC6

TIM3 编码器B相输入通道PC7

4)SD(刹车)信号输出PF10

5)串口1

USART1_TX PB6(发送)

USART1_RX PB7(接收)

3. 原理图

《DMF407电机控制专题教程》第12章 直流有刷电机位置环控制实现_编码器_02


12.2.1 直流有刷电机接口原理图

12.2.1就是我们DMF407电机开发板的直流有刷电机接口1原理图,本实验我们除了用到基础驱动所需的引脚,还需要用到PM1_ENCA(PC6)、PM1_ENCB(PC7)这两个引脚,它们分别用于连接编码器的A、B两相

本实验的硬件接线部分和编码器测速实验一模一样,这里就不再赘述,大家可以回顾编码器测速实验的内容。

12.3 程序设计

本实验所用到的基础驱动、编码器测速的代码在前面实验都有介绍过了。我们在程序解析中只讲解位置环PID控制相关的函数,下面介绍一下位置环PID控制的配置步骤。

位置环PID控制的配置步骤

1)配置相关定时器

配置基础驱动、编码器测速相关的定时器,此部分内容和编码器测速实验一致。

2初始化串口1

初始化串口1,开启串口接收中断,串口1在PID控制中用于上位机通信。

注意:在PID控制的代码中,串口1仅用于PID数据上传,尽量不要输出其他信息,否则有可能影响PID数据。

3)定义PID参数结构体变量

为了方便管理PID相关的控制量,我们需要定义一个PID参数结构体变量,方法如下:

PID_TypeDef g_location _pid; /* 位置环PID参数结构体 */

4)初始化PID参数

把PID控制系统的目标位置值、期望输出值、累计偏差等清零,然后配置PID系数。

5)初始化上位机调试

调用debug_init函数初始化所需内存,为上位机的调试做准备。

6)编写中断服务函数

在定时器6的更新中断回调函数里面进行位置环PID计算,计算后的结果用于控制PWM的占空比。

12.3.1 程序流程图

《DMF407电机控制专题教程》第12章 直流有刷电机位置环控制实现_编码器_03


图12.3.1.1 位置环PID控制程序流程图

12.3.2 程序解析

这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。关于基础驱动、编码器测速以及上位机协议的代码,这里不再赘述,大家可以回顾相应的章节。首先我们看电流环PID控制的相关源码,其包括两个文件:pid.cpid.h。

首先看pid.h头文件的几个宏定义:

/* PID相关参数 */​

#define INCR_LOCT_SELECT 1 /* 0:位置式,1:增量式 */​

#if INCR_LOCT_SELECT​

/* 增量式PID参数相关宏 */​
#define KP 15.0f /* P参数*/​
#define KI 0.00f /* I参数*/​
#define KD 7.50f /* D参数*/​

#define SMAPLSE_PID_SPEED 50 /* 采样周期单位ms */​

#else​

/* 位置式PID参数相关宏 */​
#define KP 15.0f /* P参数*/​
#define KI 0.00f /* I参数*/​
#define KD 7.5f /* D参数*/​

#define SMAPLSE_PID_SPEED 50 /* 采样周期单位ms */​

#endif​

/*PID结构体*/​
typedef struct​
{​
__IO float SetPoint; /* 设定目标 */​
__IO float ActualValue; /* 期望输出值 */​
__IO float SumError; /* 误差累计 */​
__IO float Proportion; /* 比例常数 P */​
__IO float Integral; /* 积分常数 I */​
__IO float Derivative; /* 微分常数 D */​
__IO float Error; /* Error[1] */​
__IO float LastError; /* Error[-1] */​
__IO float PrevError; /* Error[-2] */​
} PID_TypeDef;​

extern PID_TypeDef g_location_pid; /*位置PID参数结构体*/

可以把上面的宏定义分成两部分,第一部分是PID计算方式以及PID系数的宏定义,我们可以通过改变INCR_LOCT_SELECT这个宏的值来选择相应的PID计算方式,第二部分则是PID参数相关的结构体,这个结构体用于管理PID控制所需要的控制量,本实验中我们定义了位置环PID参数的结构体变量g_ location _pid

下面看pid.c的程序,这里我们只介绍PID初始化函数,关于PID闭环控制的函数介绍请回顾8.3章节,PID初始化函数定义如下:

/**​
初始化​
无​
无​
*/​
void pid_init(void)​
{​
g_location_pid.SetPoint = 0.0; /* 设定目标值 */​
g_location_pid.ActualValue = 0.0; /* 期望输出值 */​
g_location_pid.SumError = 0.0; /* 积分值 */​
g_location_pid.Error = 0.0; /* Error[1] */​
g_location_pid.LastError = 0.0; /* Error[-1] */​
g_location_pid.PrevError = 0.0; /* Error[-2] */​
g_location_pid.Proportion = KP; /* 比例常数 Proportional Const */​
g_location_pid.Integral = KI; /* 积分常数 Integral Const */​
g_location_pid.Derivative = KD; /* 微分常数 Derivative Const */ ​
}

该函数主要是将PID控制系统的目标位置值、期望输出值、累计偏差等清零,然后配置PID系数。

最后要介绍的是定时器的更新中断回调函数,它在dcmotor_time.c中实现,具体的定义如下:

/**​
定时器更新中断回调函数​
定时器句柄指针​
此函数会被定时器中断函数共同调用的​
无​
*/​
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)​
{​
int32_t motor_pwm_temp = 0;​
static uint8_t val = 0;​

/* 定时器3相关程序 */​
if (htim->Instance == TIM3)​
{​
/* 判断CR1的DIR位 */​
if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&g_timx_encode_chy_handle)) ​
{​
g_timx_encode_count--; /* DIR位为1,也就是递减计数 */​
}​
else​
{​
g_timx_encode_count++; /* DIR位为0,也就是递增计数 */​
}​
}​

/* 定时器6相关程序​
else if (htim->Instance == TIM6)​
{​
int Encode_now = gtim_get_encode(); /* 获取编码器值,用于计算速度 */​

speed_computer(Encode_now, 5); /* 5ms计算一次速度 */​

g_motor_data.location = Encode_now; /* 获取当前计数总值,用于位置闭环控制 */​

if (val % SMAPLSE_PID_SPEED == 0) /* 进行一次pid计算 */​
{​
if (g_run_flag) /* 判断电机是否启动了*/​
{ ​
/* 位置环PID计算,输出比较值(占空比)*/​
motor_pwm_temp = increment_pid_ctrl(&g_location_pid, ​
g_motor_data.location);​
/* 进行一阶低通滤波 */​
g_motor_data.motor_pwm = (int32_t)((g_motor_data.motor_pwm * 0.5) ​
+ (motor_pwm_temp * 0.5));​

if (g_motor_data.motor_pwm >= 4200) /* 限制占空比 */​
{​
g_motor_data.motor_pwm = 4200;​
}​
else if (g_motor_data.motor_pwm <= -4200)​
{​
g_motor_data.motor_pwm = -4200;​
}​
#if DEBUG_ENABLE /* 发送基本参数*/​

/* 选择通道1,发送实际位置(波形显示)*/​
debug_send_wave_data( 1 ,g_motor_data.location); ​

/* 选择通道2,发送目标位置(波形显示)*/ ​
debug_send_wave_data( 2 ,g_location_pid.SetPoint); ​
#endif​
motor_pwm_set(g_motor_data.motor_pwm); /* 设置占空比 */​
}​
val = 0;​
}​
val ++;​
}​
}

该函数我们只介绍定时器6相关的程序,进入更新中断回调函数后,所执行的代码逻辑如下:

第一步,判断是不是定时器6的寄存器基地址,如果是则获取编码器的计数总值并存入变量Encode_now中,接着计算电机速度,把变量Encode_now的值存入g_motor_data.location这个成员中。

第二步,每隔50ms进行一次PID计算,在计算PID之前,需要判断g_run_flag是否为1,如果是则说明电机已经启动,可以开始PID计算。

第三步,进行位置环PID计算,返回的输出存放在motor_pwm_temp 这个变量中;接着进行一阶低通滤波,返回的输出存放在g_motor_data.motor_pwm这个成员中,该成员用于设置PWM的占空比,除此之外,我们还需要对返回的输出进行限制,这一点非常重要。

第四步,发送实际位置、目标位置的波形数据到上位机,最后设置PWM的占空比,进而控制电机的位置。

在main.c里面编写如下代码:

int main(void)​
{​
uint8_t key;​
uint16_t t;​
uint8_t debug_cmd = 0;​

HAL_Init(); /* 初始化HAL库 */​
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */​
delay_init(168); /* 延时初始化 */​
usart_init(115200); /* 串口1初始化,用于上位机调试 */​
led_init(); /* 初始化LED */​
lcd_init(); /* 初始化LCD */​
key_init(); /* 初始化按键 */​
pid_init(); /* 初始化PID参数 */​
atim_timx_cplm_pwm_init(8400 - 1 , 0); /* 168Mhz的计数频率 */​
dcmotor_init(); /* 初始化电机 */​
gtim_timx_encoder_chy_init(0XFFFF, 0); /* 编码器定时器初始化 */​
btim_timx_int_init(1000 - 1 , 84 - 1); /* 基本定时器初始化,1ms计数周期 */​
adc_nch_dma_init(); /* 初始化ADC、DMA */​

#if DEBUG_ENABLE /* 开启调试 */​

debug_init(); /* 初始化调试 */​
debug_send_motorcode(DC_MOTOR); /* 上传电机类型(直流有刷电机) */​
debug_send_motorstate(IDLE_STATE); /* 上传电机状态(空闲) */​

/* 同步数据(选择第1组PID,目标位置地址,P,I,D参数)到上位机 */​
debug_send_initdata(TYPE_PID1, (float *)(&g_location_pid.SetPoint), ​
KP, KI, KD);​
#endif​

g_point_color = WHITE;​
g_back_color = BLACK;​
lcd_show_string(10, 10, 200, 16, 16, "DcMotor Test", g_point_color);​
lcd_show_string(10, 30, 200, 16, 16, "KEY0:Start forward", g_point_color);​
lcd_show_string(10, 50, 200, 16, 16, "KEY1:Start backward", g_point_color);​
lcd_show_string(10, 70, 200, 16, 16, "KEY2:Stop", g_point_color);​

while (1)​
{​
key = key_scan(0); /* 按键扫描 */​
if(key == KEY0_PRES) /* 当key0按下 */​
{​
g_run_flag = 1; /* 标记电机启动 */​
dcmotor_start(); /* 开启电机 */​

正转一圈,电机旋转圈数 = 目标计数值变化量 / 44 / 30 */​
g_location_pid.SetPoint += 1320; ​

if (g_location_pid.SetPoint >= 6600) /* 限制电机位置(正转最大5圈) */​
{​
g_location_pid.SetPoint = 6600;​
}​

#if DEBUG_ENABLE​
debug_send_motorstate(RUN_STATE); /* 上传电机状态(运行) */​
#endif​
}​
else if(key == KEY1_PRES) /* 当key1按下 */​
{​
g_run_flag = 1; /* 标记电机启动 */​
dcmotor_start(); /* 开启电机 */​
g_location_pid.SetPoint -= 1320; /* 反转一圈 */​

if (g_location_pid.SetPoint <= -6600) /* 限制电机位置(反转最大5圈)*/​
{​
g_location_pid.SetPoint = -6600;​
}​
#if DEBUG_ENABLE​
debug_send_motorstate(RUN_STATE); /* 上传电机状态(运行) */​
#endif​
}​
else if(key == KEY2_PRES) /* 当key2按下 */​
{​
g_location_pid.SetPoint = 0; /* 恢复初始位置 */​
}​
#if DEBUG_ENABLE​
/* 查询接收PID助手的PID参数 */​
debug_receive_pid(TYPE_PID1, (float *)&g_location_pid.Proportion,​
(float *)&g_location_pid.Integral, (float *)&g_location_pid.Derivative);​

debug_set_point_range(6600, -6600, 6600); /* 设置目标调节范围 */​

debug_cmd = debug_receive_ctrl_code(); /* 读取上位机指令 */​

if (debug_cmd == HALT_CODE) /* 电机停机 */​
{ ​
g_location_pid.SetPoint = 0; /* 恢复初始位置 */​
}​
else if (debug_cmd == RUN_CODE) /* 电机运行 */​
{​
g_run_flag = 1; /* 标记电机启动 */​
dcmotor_start(); /* 开启电机 */​
g_location_pid.SetPoint = 1320; /* 设置目标位置 */​
debug_send_motorstate(RUN_STATE); /* 上传电机状态(运行) */​
}​
#endif​
t++;​
if(t % 20 == 0)​
{​
lcd_dis(); /* 显示数据 */​
LED0_TOGGLE(); /* LED0(红灯)翻转 */​
g_debug.encode_p = g_motor_data.location; /* 传入编码器当前总计数值 */​
debug_upload_data(&g_debug, TYPE_HAL_ENC); /* 发送编码器当前总计数值 */​
}​
delay_ms(10);​
}​
}

main.c的代码逻辑如下:

第一步,初始化相关的外设,例如定时器、串口等。

第二步,初始化PID参数、上位机调试,它们分别调用的是pid_init和debug_init函数。

第三步,上传电机的状态(空闲)、类型(直流有刷电机)、PID参数到上位机,它们分别调用的是debug_send_motorcodedebug_send_motorstate以及debug_send_initdata函数。

第四步,在while循环里面检测按键是否按下,如果key0按下,则目标位置增加1320,也就是目标计数值增加1320,根据公式:电机旋转圈数 = 目标计数值变化量 / 44 / 30,电机将会正转一圈;如果key1按下,则目标位置减小1320,电机将会反转一圈,然后上传电机状态(运行);如果key2按下,电机将回到初始位置。

第五步,接收上位机下发的PID参数、设置目标位置的调节范围,它们分别调用的是debug_receive_pid以及debug_set_point_range函数。我们这里重点介绍一下后者,debug_set_point_range函数的第一、二个入口参数分别传入目标位置的最大值、最小值,它们的正负号代表电机的正反转方向,第三个入口参数传入的是电机位置的最大突变值。

第六步,调用debug_receive_ctrl_code函数,接收上位机下发的命令。如果上位机的命令为HALT_CODE(停机),则让电机回到初始位置;如果上位机的命令是RUN_CODE,则开启电机,设置目标位置为1320,上传电机状态(运行)到上位机。

第七步,每隔200ms更新一次数据到屏幕,调用debug_upload_data函数发送编码器位置值(计数总值)到上位机。

12.4 下载验证

下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了,LCD上显示按键功能、占空比以及电机位置信息,当我们按下KEY0,目标位置将增大,电机正转;按下KEY1,目标位置将减小,电机反转;按下KEY2,电机将回到初始位置。我们再打开PID调试助手,选择对应的串口端口,我这边是COM7,接着选择通道1和2,点击“开始”按钮,即可开始显示波形,如下图12.4.1所示:

《DMF407电机控制专题教程》第12章 直流有刷电机位置环控制实现_上位机_04


12.4.1 位置环PID控制波形

图12.4.1中,橙线代表目标位置,红线代表实际位置,当我们按下KEY0,目标位置增大,橙线先发生变化,而红线(实际位置)会逐渐靠近橙线(目标位置);按下KEY1,目标速度将减小,曲线的变化同理;按下KEY2,电机将回到初始位置,目标位置将为0。

注意:1、本实验需要使用USB数据线连接开发板的串口1到电脑,并启动电机之后才会有波形变化;2、如果发现波形不对,请检查电机的M+和M-接线;3、PID系数并不是通用的,如果PID曲线不理想,大家需要根据自己的实际系统去调节。