电机三环控制指的是,直流有刷电机三环(电流环+速度环+位置环)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 函数发送编码器位置值(计数总值)到上位机。