自学内容网 自学内容网

七、电机三环控制

电机三环控制指的是,直流有刷电机三环(电流环+速度环+位置环)PID 控制。

1、三环PID控制原理

三环 PID 控制就是将三个 PID 控制系统(例如:电流环、速度环以及位置环)串联起来,然后对前一个系统(外环)的输出进行偏差的计算,计算结果作为后一个系统(内环)的输入。

1. 首先设置目标位置,系统会计算出位置偏差,然后将偏差输入到位置环(最外环);

2. 位置环的输出和实际速度进行偏差的计算,计算后的偏差输入到速度环(次外环);

3. 速度环的输出和实际电流进行偏差的计算,计算后的偏差输入到电流环(内环),电流环的输出用于控制 PWM 的占空比,进而控制电机的位置。

2、程序设计

三环 PID 控制的配置步骤: 

1)配置相关定时器

配置基础驱动、编码器测速相关的定时器,实现基础驱动以及编码器测速功能。

2)配置相关 ADC

配置电流采集相关 ADC,实现电流采集功能。

4)初始化串口 1

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

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

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

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

PID_TypeDef g_location_pid;

/* 位置环 PID 参数结构体 */

PID_TypeDef g_speed_pid;

/* 速度环 PID 参数结构体 */

PID_TypeDef g_current_pid;

/* 电流环 PID 参数结构体 */

6)初始化 PID 参数

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

7)初始化上位机调试

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

8)编写中断服务函数

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

pid.h

/* PID 相关参数 */ 
#define INCR_LOCT_SELECT 0 /* 0:位置式,1:增量式 */ 
#if INCR_LOCT_SELECT 
/* 定义位置环 PID 参数相关宏 */ 
#define L_KP 0.06f /* P 参数 */ 
#define L_KI 0.00f /* I 参数 */ 
#define L_KD 0.01f /* D 参数 */ 
/* 定义速度环 PID 参数相关宏 */ 
#define S_KP 5.00f /* P 参数 */ 
#define S_KI 0.30f /* I 参数 */ 
#define S_KD 0.01f /* D 参数 */ 
/* 定义电流环 PID 参数相关宏 */ 
#define C_KP 8.00f /* P 参数 */ 
#define C_KI 4.00f /* I 参数 */ 
#define C_KD 1.00f /* D 参数 */ 
#define SMAPLSE_PID_SPEED 50 /* 采样周期 单位 ms */ 
#else 
/* 定义位置环 PID 参数相关宏 */ 
#define L_KP 0.06f /* P 参数 */ 
#define L_KI 0.00f /* I 参数 */ 
#define L_KD 0.01f /* D 参数 */ 
/* 定义速度环 PID 参数相关宏 */ 
#define S_KP 5.00f /* P 参数 */ 
#define S_KI 0.30f /* I 参数 */ 
#define S_KD 0.01f /* D 参数 */ 
/* 定义电流环 PID 参数相关宏 */ 
#define C_KP 8.00f /* P 参数 */ 
#define C_KI 4.00f /* I 参数 */ 
#define C_KD 1.00f /* 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 参数结构体 */ 
extern PID_TypeDef g_speed_pid; 
/* 速度环 PID 参数结构体 */ 
extern PID_TypeDef g_current_pid; /* 电流环 PID 参数结构体 */

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

pid.c

/** 
* @brief pid 初始化 
* @param 无 
* @retval 无 
*/ 
void pid_init(void) 
{ 
/* 初始化位置环 PID 参数 */ 
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 = L_KP; /* 比例常数 Proportional Const */ 
g_location_pid.Integral = L_KI; 
/* 积分常数 Integral Const */ 
g_location_pid.Derivative = L_KD; /* 微分常数 Derivative Const */ 
/* 初始化速度环 PID 参数 */ 
g_speed_pid.SetPoint = 0.0; 
/* 目标值 */ 
g_speed_pid.ActualValue = 0.0; 
/* 期望输出值 */ 
g_speed_pid.SumError = 0.0; 
/* 积分值 */ 
g_speed_pid.Error = 0.0; 
/* Error[1] */ 
g_speed_pid.LastError = 0.0; 
/* Error[-1] */ 
g_speed_pid.PrevError = 0.0; 
/* Error[-2] */ 
g_speed_pid.Proportion = S_KP; 
/* 比例常数 Proportional Const */ 
g_speed_pid.Integral = S_KI; 
/* 积分常数 Integral Const */ 
g_speed_pid.Derivative = S_KD; 
/* 微分常数 Derivative Const */ 
/* 初始化电流环 PID 参数 */ 
g_current_pid.SetPoint = 0.0; 
/* 目标值 */ 
g_current_pid.ActualValue = 0.0; 
/* 期望输出值 */ 
g_current_pid.SumError = 0.0; 
/* 积分值*/ 
g_current_pid.Error = 0.0; 
/* Error[1]*/ 
g_current_pid.LastError = 0.0; 
/* Error[-1]*/ 
g_current_pid.PrevError = 0.0; 
/* Error[-2]*/ 
g_current_pid.Proportion = C_KP; 
/* 比例常数 Proportional Const */ 
g_current_pid.Integral = C_KI; 
/* 积分常数 Integral Const */ 
g_current_pid.Derivative = C_KD; 
/* 微分常数 Derivative Const */ 
} 


/** 
* @brief 积分限幅 
* @param *PID:PID 结构体变量地址 
* @param max_limit:最大值 
* @param min_limit:最小值 
* @retval 无
*/ 
void integral_limit( PID_TypeDef *PID , float max_limit, float min_limit ) 
{ 
if (PID->SumError >= max_limit) /* 超过限幅 */ 
{ 
PID->SumError = max_limit; /* 限制积分 */ 
} 
else if (PID->SumError <= min_limit) /* 超过限幅 */ 
{ 
PID->SumError = min_limit; 
} 
} 

该函数主要是将三环 PID 控制系统的目标值、期望输出值、累计偏差等清零,然后配置PID 系数。integral_limit()函数可以对位置式 PID 的积分进行限幅,避免出现积分深度饱和的问题。在使用该函数之前,需要先调整好 PID 系数,再根据系统能达到的最大累计偏差来设置限幅。

dcmotor_time.c

/** 
* @brief 定时器更新中断回调函数 
* @param htim:定时器句柄指针 
* @note 此函数会被定时器中断函数共同调用的 
* @retval 无 
*/ 
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) 
{ 
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 计算一次速度 */ 
if (val % SMAPLSE_PID_SPEED == 0) /* 进行一次 pid 计算 */ 
{ 
if (g_run_flag) /* 判断电机是否启动了 */ 
{ 
/* 获取当前编码器总计数值,用于位置闭环控制 */ 
g_motor_data.location = (float)Encode_now; 
integral_limit(&g_location_pid , 1000 ,-1000); /* 位置环积分限幅 */ 
integral_limit(&g_speed_pid , 200 ,-200); /* 速度环积分限幅 */ 
integral_limit(&g_current_pid , 150 ,-150); /* 电流环积分限幅 */ 
/* 设置闭环死区,避免小幅度位置抖动 */ 
if((g_location_pid.Error <= 20)&&(g_location_pid.Error >= -20)) 
{ 
g_location_pid.Error = 0; /* 偏差太小了,直接清零 */
g_location_pid.SumError = 0; /* 清除积分 */ 
} 
g_motor_data.motor_pwm = increment_pid_ctrl(&g_location_pid, 
g_motor_data.location); /* 位置环 PID 控制(最外环) */ 
if (g_motor_data.motor_pwm >= 120) /* 限制外环输出(目标速度) */ 
{ 
g_motor_data.motor_pwm = 120; 
} 
else if (g_motor_data.motor_pwm <= -120) 
{ 
g_motor_data.motor_pwm = -120; 
} 
/* 设置目标速度,外环输出作为内环输入 */ 
g_speed_pid.SetPoint = g_motor_data.motor_pwm; 
g_motor_data.motor_pwm = increment_pid_ctrl(&g_speed_pid, 
g_motor_data.speed); /* 速度环 PID 控制(次外环) */ 
if ( g_motor_data.motor_pwm > 0) /* 判断速度环输出值是否为正数 */ 
{ 
dcmotor_dir(0); /* 输出为正数,设置电机正转 */ 
} 
else 
{ 
/* 输出取反 */ 
g_motor_data.motor_pwm = -g_motor_data.motor_pwm; 
dcmotor_dir(1); /* 设置电机反转 */ 
} 
if (g_motor_data.motor_pwm >= 100) /* 限制外环输出(目标电流) */ 
{ 
g_motor_data.motor_pwm = 100; 
} 
/* 设置目标电流,外环输出作为内环输入 */ 
g_current_pid.SetPoint = g_motor_data.motor_pwm; 
g_motor_data.motor_pwm = increment_pid_ctrl(&g_current_pid, 
g_motor_data.current); 
/* 电流环 PID 控制(内环) */ 
if (g_motor_data.motor_pwm >= 8200) /* 限制占空比 */ 
{ 
g_motor_data.motor_pwm = 8200; 
} 
else if (g_motor_data.motor_pwm <= 0) /* 滤掉无效输出 */ 
{ 
g_motor_data.motor_pwm = 0; 
} 
#if DEBUG_ENABLE /* 发送基本参数*/ 
/* 选择通道 1,发送实际位置(波形显示)*/ 
debug_send_wave_data( 1 ,g_motor_data.location); 
/* 选择通道 2,发送目标位置(波形显示)*/ 
debug_send_wave_data( 2 ,g_location_pid.SetPoint); 
#endif 
dcmotor_speed(g_motor_data.motor_pwm); /* 设置占空比 */ 
} 
val = 0; 
} 
val ++; 
} 
}

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

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

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

第三步,把变量 Encode_now 的值存入 g_motor_data.location 这个成员中,然后对三环的积分进行限幅并设置闭环死区。当系统偏差值进入死区范围之内,我们直接让偏差等于 0,并清除累计偏差,此时 PID 系统不再参与控制,这样可以避免小幅度偏差带来的位置抖动。

第四步,进行位置环 PID 计算并限制其输出,然后将位置环的输出作为速度环的目标值;速度环 PID 计算完后,我们先判断其输出,如果输出是正数,则设置电机正转,如果输出是负数,则将输出取反,设置电机反转。因为电流环 PID 的目标值和输出都是正数,我们无法通过电流环的输出来确定电机的转向,所以这里需要通过速度环的输出来确定电机的转向,并保证电流环的目标值是正数。

第五步,将速度环的输出作为电流环的目标值,然后进行电流环 PID 计算并限制其输出。

第六步,发送实际位置、目标位置的波形数据到上位机,最后设置 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(); 
#if DEBUG_ENABLE 
/* 开启调试 */ 
debug_init(); 
/* 初始化调试 */ 
debug_send_motorcode(DC_MOTOR); 
/* 上传电机类型(直流有刷电机) */ 
debug_send_motorstate(IDLE_STATE); 
/* 上传电机状态(空闲) */ 
/* 同步数据 PID 参数到上位机 ,无论同步哪一组数据,目标值地址只能是外环 PID 的 */ 
debug_send_initdata(TYPE_PID1, (float *)(&g_location_pid.SetPoint), 
L_KP, L_KI, L_KD); /* 位置环 PID 参数(PID1)*/ 
debug_send_initdata(TYPE_PID2, (float *)(&g_location_pid.SetPoint), 
S_KP, S_KI, S_KD); /* 速度环 PID 参数(PID2)*/ 
debug_send_initdata(TYPE_PID3, (float *)(&g_location_pid.SetPoint), 
C_KP, C_KI, C_KD); /* 电流环 PID 参数(PID3)*/ 
#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) 
/* 限制电机位置 */ 
{ 
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) 
/* 限制电机位置 */ 
{ 
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); 
/* 接收 PID 助手设置的速度环 PID 参数 */ 
debug_receive_pid(TYPE_PID2,(float *)&g_speed_pid.Proportion, 
(float *)&g_speed_pid.Integral,(float *)&g_speed_pid.Derivative); 
/* 接收 PID 助手设置的电流环 PID 参数 */ 
debug_receive_pid(TYPE_PID3,(float *)&g_current_pid.Proportion, 
(float *)&g_current_pid.Integral,(float *)&g_current_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(红灯) 翻转 */ 
#if DEBUG_ENABLE 
debug_send_speed(g_motor_data.speed); 
/* 发送速度 */ 
g_debug.encode_p = g_motor_data.location; 
/* 传入编码器当前总计数值 */ 
debug_upload_data(&g_debug, TYPE_HAL_ENC); /* 发送编码器当前总计数值 */ 
#endif 
} 
delay_ms(10); 
} 
} 

main.c 的代码逻辑如下:

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

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

第三步,同步电机的状态(空闲)、类型(直流有刷电机)、PID 参数到上位机。需要注意的是,在多环控制中,只有最外环的目标值可以手动设置,因此,无论同步哪一组 PID 参数,目标值地址只能是最外环 PID 的(本实验最外环为位置环)。

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

第五步,接收上位机下发的 PID 参数、设置目标位置的调节范围。

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

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


原文地址:https://blog.csdn.net/weixin_59334478/article/details/143861729

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!