|
本帖最后由 正点原子 于 2022-8-15 18:03 编辑
1)实验平台:正点原子DMF407电机开发板
2)平台购买地址: https://detail.tmall.com/item.htm?&id=677230699323
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/stm32dj/ATK-DMF407.html
4)对正点原子电机开发板感兴趣的同学可以加群讨论: 592929122
第11章 直流有刷电机电流环控制实现
本章我们主要来学习直流有刷电机电流环PID控制的原理,并实现电流环PID控制的实验。
本章分为如下几个小节:
11.1 电流环PID控制原理
11.2 硬件设计
11.3 程序设计
11.4 下载验证
11.1 电流环PID控制原理
电流环PID控制的原理非常简单,我们只需要把PID控制流程中的控制对象换成电机电流即可,如图11.1.1所示:
图11.1.1电流环PID控制流程
图11.1.1中,我们先设置目标电流,系统会计算出偏差e,然后将偏差输入到PID控制的三个环节中,PID计算后的输出值用于控制PWM的占空比,进而控制电机的电流。
11.2 硬件设计
1. 例程功能
1、本实验以电机开发板的直流有刷电机驱动接口1为例,基于电压温度电流采集实验,加入电流环PID控制算法,对电机的电流进行闭环控制。
2、当按键0按下,就增大目标电流值;当按键1按下,就减小目标电流值,目标电流的大小决定电机的速度。按下按键2则马上停止电机。
3、屏幕显示按键功能、占空比、目标电流以及实际电流。
4、串口1和上位机进行数据通信。
5、LED0闪烁指示程序运行。
2. 硬件资源
1)LED灯
LED0 – PE0
2)独立按键
KEY0 – PE2
KEY1 – PE3
KEY2 – PE4
3)定时器1
TIM1正常输出通道 PA8
TIM1互补输出通道 PB13
4)SD(刹车)信号输出 PF10
5)ADC
ADC1通道8 PB0(电流)
6)串口1
USART1_TX PB6(发送)
USART1_RX PB7(接收)
3. 原理图
图11.2.1 直流有刷电机接口原理图
图11.2.1就是我们DMF407电机开发板的直流有刷电机接口1原理图,本实验我们只需要用到了PM1_PWM_UH(PA8)、PM1_PWM_UL(PB13)、PM1_CTRL_SD(PF10)和PM1_AMPU(PB0)这4个引脚,其中PA8和PB13用于输出所需的PWM控制信号,PF10用于输出刹车信号,PB0用于检测电流采集电路对应的输出电压。
本实验的硬件接线部分和编码器测速实验一模一样,这里就不再赘述,大家可以回顾编码器测速实验的内容。
11.3 程序设计
本实验所用到的基础驱动、电流采集的代码在前面实验都有介绍过了。我们在程序解析中只讲解电流环PID控制相关的函数,下面介绍一下电流环PID控制的配置步骤。
电流环PID控制的配置步骤
1)配置相关定时器、ADC
配置基础驱动相关的定时器以及电流采集相关的ADC。
2)初始化串口1
初始化串口1,开启串口接收中断,串口1在PID控制代码中用于上位机通信。
注意:在PID控制的代码中,串口1仅用于PID数据上传,尽量不要输出其他信息,否则有可能影响PID数据。
3)定义PID参数结构体变量
为了方便管理PID相关的控制量,我们需要定义一个PID参数结构体变量,方法如下:
PID_TypeDef g_current_pid; /* 电流环PID参数结构体 */
4)初始化PID参数
把PID控制系统的期望输出值、累计偏差等清零,然后配置目标电流初始值和PID系数。
5)初始化上位机调试
调用debug_init函数初始化所需内存,为上位机的调试做准备。
6)编写中断服务函数
在定时器6的更新中断回调函数里面进行电流环PID计算,计算后的结果用于控制PWM的占空比。
11.3.1 程序流程图
图11.3.1.1 电流环PID控制程序流程图
11.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。关于基础驱动、电流采集以及上位机协议的代码,这里不再赘述,大家可以回顾相应的章节。首先我们看电流环PID控制的相关源码,其包括两个文件:pid.c和pid.h。
首先看pid.h头文件的几个宏定义:
/* PID相关参数 */
#define INCR_LOCT_SELECT 0 /* 0:位置式,1:增量式 */
#if INCR_LOCT_SELECT
/* 增量式PID参数相关宏 */
#define KP 0.0f /* P参数*/
#define KI 6.0f /* I参数*/
#define KD 4.0f /* D参数*/
#define SMAPLSE_PID_SPEED 50 /* 采样周期 单位ms*/
#else
/* 位置式PID参数相关宏 */
#define KP 10.0f /* P参数*/
#define KI 7.0f /* I参数*/
#define KD 2.0f /* 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_current_pid; /* 电流环PID参数结构体 */
可以把上面的宏定义分成两部分,第一部分是PID计算方式以及PID系数的宏定义,我们可以通过改变INCR_LOCT_SELECT这个宏的值来选择相应的PID计算方式,第二部分则是PID参数相关的结构体,这个结构体用于管理PID控制所需要的控制量,本实验中我们定义了电流环PID参数的结构体变量g_ current _pid。
下面看pid.c的程序,这里我们只介绍PID初始化函数,关于PID闭环控制的函数介绍请回顾8.3章节,PID初始化函数定义如下:
/**
* @brief pid初始化
* @param 无
* @retval 无
*/
void pid_init(void)
{
g_ current _pid.SetPoint = 40; /* 设定目标值 */
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 = KP; /* 比例常数 Proportional Const */
g_ current _pid.Integral = KI; /* 积分常数 Integral Const */
g_ current _pid.Derivative = KD; /* 微分常数 Derivative Const */
}
该函数主要是将PID控制系统的期望输出值、累计偏差等清零,然后配置初始目标电流以及PID系数。这里配置初始目标电流为40mA,是因为电机启动时所需的电流较大,太小的电流无法正常启动电机。
接下来看电流采集相关的源码,我们只介绍滤波算法的内容,该算法在adc.c的ADC采集中断回调函数中实现,具体定义如下:
/**
* @brief ADC采集中断回调函数
* @param 无
* @retval 无
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
float temp_c = 0.0;
static float add_adc = 0;
static float init_adc_value = 0;
static uint8_t adc_count1 = 0, adc_count2 = 0;
if ( hadc->Instance == ADC_ADCX ) /* 判断是不是ADC1 */
{
adc_count1++;
HAL_ADC_Stop_DMA(&g_adc_nch_dma_handle); /* 关闭DMA转换 */
calc_adc_val(g_adc_val); /* 计算ADC的平均值 */
add_adc += g_adc_val[2]; /* 取出电流通道对应的ADC值进行累计 */
if (adc_count1 >= 15) /* 累计15次 */
{
add_adc = (float)(add_adc / adc_count1); /* 取平均值 */
if (adc_count2 <= 16) /* 采集16次ADC平均值计算参考电压的ADC值 */
{
adc_count2++;
init_adc_value += add_adc; /* 对平均值累计求和 */
if (adc_count2 == 16) /* 平均值累计16次 */
{
adc_count2 = 17; /* 不再进入 */
init_adc_value = (init_adc_value / 16.0f); /* 存储初始ADC值 */
}
}
if (adc_count2 >= 17) /* 采集完参考ADC值后,采集电流通道当前ADC值 */
{
temp_c = (add_adc - init_adc_value) * ADC2CURT; /* 计算电流 */
g_motor_data.current = (float)((g_motor_data.current *
(float)0.60) + ((float)0.40 * temp_c)); /* 一阶低通滤波 */
if (g_motor_data.current <= 20) /* 过滤掉微弱浮动电流 */
{
g_motor_data.current = 0.0;
}
}
add_adc = 0;
adc_count1 = 0;
}
HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, (uint32_t *)&g_adc_value,
(uint32_t)(ADC_SUM)); /* 启动DMA转换 */
}
}
进入ADC采集中断回调函数后,所执行的代码逻辑如下:
第一步,判断是不是ADC1的寄存器基地址,如果是则让变量adc_count1自增1,然后关闭DMA传输,计算电流采集通道对应的ADC平均值,最后把平均值累计到变量add_adc中(第一次滤波)。
第二步,当变量adc_count1自增到15时,变量add_adc已经累计15次ADC平均值,此时再次计算ADC平均值并返回到变量add_adc中(第二次滤波)。
第三步,当变量adc_count2小于等于16时,不断地累计变量add_adc的值,存入到变量init_adc_value中,然后再次计算ADC平均值并返回变量init_adc_value中(第三次滤波),该变量存储的是参考电压对应的ADC值,后续将用于电流的计算。
第四步,当变量adc_count2大于等于17时,根据公式计算电流值并存入变量temp_c中,接着进行一阶低通滤波,过滤掉微弱的浮动电流,最后再启动DMA转换。
注意:电流的滤波对于PID系统的控制至关重要,如果电流环的PID曲线出现了严重的振荡,通过调整PID系数已经无法优化,此时可以适当地调整滤波的次数,在系统的响应速度和稳定性之间寻求平衡点。
最后要介绍的是定时器的更新中断回调函数,它在dcmotor_time.c中实现,具体的定义如下:
/**
* @brief 定时器更新中断回调函数
* @param htim:定时器句柄指针
* @note 此函数会被定时器中断函数共同调用的
* @retval 无
*/
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计算一次速度 */
if (val % SMAPLSE_PID_SPEED == 0) /* 进行一次pid计算 */
{
if (g_run_flag) /* 判断电机是否启动了*/
{
/* 电流环PID计算,输出比较值(占空比)*/
motor_pwm_temp = increment_pid_ctrl(&g_current_pid,
g_motor_data.current);
/* 进行一阶低通滤波 */
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 >= 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.current);
/* 选择通道2,发送目标电流(波形显示)*/
debug_send_wave_data( 2 ,g_current_pid.SetPoint);
/* 选择通道3,发送占空比(波形显示)*/
debug_send_wave_data( 3 ,g_motor_data.motor_pwm * 100 / 8400 );
#endif
motor_pwm_set(g_motor_data.motor_pwm); /* 设置占空比 */
}
val = 0;
}
val ++;
}
}
该函数我们只介绍定时器6相关的程序,进入更新中断回调函数后,所执行的代码逻辑如下:
第一步,判断是不是定时器6的寄存器基地址,如果是则获取编码器的计数总值,然后每隔5ms计算一次电机速度。
第二步,每隔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_current_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);
delay_ms(500);
while (1)
{
key = key_scan(0); /* 按键扫描 */
if(key == KEY0_PRES) /* 当key0按下 */
{
g_run_flag = 1; /* 标记电机启动 */
dcmotor_start(); /* 启动电机 */
g_current_pid.SetPoint += 20;
if (g_current_pid.SetPoint >= 120) /* 限制电流:120mA */
{
g_current_pid.SetPoint = 120;
}
#if DEBUG_ENABLE
debug_send_motorstate(RUN_STATE); /* 上传电机状态(运行)*/
#endif
}
else if(key == KEY1_PRES) /* 当key1按下 */
{
g_current_pid.SetPoint -= 20;
if (g_current_pid.SetPoint < 60) /* 目标电流小于60mA */
{
dcmotor_stop(); /* 停止电机 */
pid_init(); /* 重置pid参数 */
g_run_flag = 0; /* 标记电机停止 */
g_motor_data.motor_pwm = 0;
motor_pwm_set(g_motor_data.motor_pwm); /* 设置电机转向、速度 */
#if DEBUG_ENABLE
debug_send_motorstate(BREAKED_STATE); /* 上传电机状态(刹车) */
debug_send_initdata(TYPE_PID1,
(float *)(&g_current_pid.SetPoint), KP, KI, KD);
#endif
}
}
else if(key == KEY2_PRES) /* 当key2按下 */
{
dcmotor_stop(); /* 停止电机 */
pid_init(); /* 重置pid参数 */
g_run_flag = 0; /* 标记电机停止 */
g_motor_data.motor_pwm = 0;
motor_pwm_set(g_motor_data.motor_pwm); /* 设置电机转向、速度 */
#if DEBUG_ENABLE
debug_send_motorstate(BREAKED_STATE); /* 上传电机状态(刹车)*/
debug_send_initdata(TYPE_PID1, (float *)(&g_current_pid.SetPoint),
KP, KI, KD);
#endif
}
#if DEBUG_ENABLE
/* 查询接收PID助手的PID参数 */
debug_receive_pid(TYPE_PID1, (float *)&g_current_pid.Proportion,
(float *)&g_current_pid.Integral, (float *)&g_current_pid.Derivative);
debug_set_point_range(120, 0, 120); /* 设置目标调节范围 */
debug_cmd = debug_receive_ctrl_code(); /* 读取上位机指令 */
if (debug_cmd == BREAKED) /* 电机刹车 */
{
dcmotor_stop(); /* 停止电机 */
pid_init(); /* 重置pid参数 */
g_run_flag = 0; /* 标记电机停止 */
g_motor_data.motor_pwm = 0;
motor_pwm_set(g_motor_data.motor_pwm); /* 设置电机转向、速度 */
debug_send_motorstate(BREAKED_STATE); /* 上传电机状态(刹车)*/
debug_send_initdata(TYPE_PID1, (float *)(&g_current_pid.SetPoint),
KP, KI, KD);
}
else if (debug_cmd == RUN_CODE) /* 电机运行 */
{
dcmotor_start(); /* 启动电机 */
g_current_pid.SetPoint = 60; /* 目标电流:60mA */
g_run_flag = 1; /* 标记电机启动 */
debug_send_motorstate(RUN_STATE); /* 上传电机状态(运行)*/
}
#endif
t++;
if(t % 20 == 0)
{
lcd_dis(); /* 显示数据 */
LED0_TOGGLE(); /* LED0(红灯) 翻转 */
#if DEBUG_ENABLE
debug_send_current(g_motor_data.current * 0.001f, 0, 0);/* 发送电流 */
debug_send_speed(g_motor_data.speed); /* 发送速度 */
#endif
}
delay_ms(10);
}
}
main.c的代码逻辑如下:
第一步,初始化相关的外设,例如定时器、串口以及ADC等。
第二步,初始化PID参数、上位机调试,它们分别调用的是pid_init和debug_init函数。
第三步,上传电机的状态(空闲)、类型(直流有刷电机)、PID参数到上位机,它们分别调用的是debug_send_motorcode、debug_send_motorstate以及debug_send_initdata函数。
第四步,在while循环里面检测按键是否按下,如果key0按下,则目标电流增加20,上传电机状态(运行);如果key1按下,则目标电流减小20,上传电机状态(运行);当目标电流小于60mA或者key2按下时,停止电机,重置PID参数,并重新上传电机状态(刹车)以及PID参数到上位机。
第五步,接收上位机下发的PID参数、设置目标电流的调节范围,它们分别调用的是debug_receive_pid以及debug_set_point_range函数。我们这里重点介绍一下后者,debug_set_point_range函数的第一、二个入口参数分别传入目标电流的最大值、最小值,第三个入口参数传入的是电机电流的最大突变值。
第六步,调用debug_receive_ctrl_code函数,接收上位机下发的命令。如果上位机的命令为BREAKED(刹车),则停止电机,重置PID参数,并重新上传电机状态(刹车)以及PID参数到上位机;如果上位机的命令是RUN_CODE,则开启电机,设置目标电流为60mA,上传电机状态(运行)到上位机。
第七步,每隔200ms更新一次数据到屏幕,发送实际速度、实际电流值到上位机。
11.4 下载验证
下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了,LCD上显示按键功能、占空比以及电机电流信息,当我们按下KEY0,目标电流将增大;按下KEY1,目标电流将减小;按下KEY2,电机将停止。我们再打开PID调试助手,选择对应的串口端口,我这边是COM7,接着选择通道1和2,点击“开始”按钮,即可开始显示波形,如下图11.4.1所示:
图11.4.1 电流环PID控制波形
图11.4.1中,橙线代表目标电流,红线代表实际电流,当我们按下KEY0,目标电流增大,橙线先发生变化,而红线(实际电流)会逐渐靠近橙线(目标电流);按下KEY1,目标电流将减小,曲线的变化同理;按下KEY2,电机将停止。
注意:1、电流环的波形存在小幅振荡属于正常现象,如果希望波形更稳定,可以适当地调整PID系数以及增大滤波次数;2、滤波次数越多则系统的响应越慢,大家需要在系统的响应速度和稳定性之间寻找平衡点;3、开发板上电或者复位之后,需要等待电流稳定(大约3s)再启动电机;4、PID系数并不是通用的,如果PID曲线不理想,大家需要根据自己的实际系统去调节。
|
阿莫论坛20周年了!感谢大家的支持与爱护!!
曾经有一段真挚的爱情摆在我的面前,我没有珍惜,现在想起来,还好我没有珍惜……
|