104人参与 • 2024-08-02 • 物联网
前一篇文章实现了使用tb6612驱动电机及编码器测速,但是在实际测速的过程中,如果我们人为给电机一个阻力,电机的速度将会下降,编码器接口获取到的脉冲数也会减少。
但是如果要使电机保持一个恒定的速度
,即使遇到阻力它的速度也不会下降。这个时候就需要引入pid算法了。
不仅仅是速度,很多参数都可以通过pid算法进行闭环控制,比如温度,角度等。
pid是反馈环的调节机制
就拿电机速度为例
电机的转速有误差,把实测转速输入和设定比较的差值用pid运算输出控制占空比,那么增减转速就可以实现精确控制了
作为一个菜鸡选手,感觉自己好菜,将学习中遇到的问题和经历记录一下
如何调节pid参数?如何根据pid算法调节电机的速度?如何使用pid实现闭环控制?
啊~,怎么这么多,自己又陷入了无限的内耗中,搞不出来,看不懂文章,pid什么鬼?
目前暂时实现了电机的速度环调参
在学习了一段时间后,自己对这个东西理解的好像深一点了,仅停留在会用的地步,对三个参数也是浅显的理解,知道了如何配合串口上位机进行调参,总的来说还是挺有收获的。
在没有引入pid时,我们控制速度的系统是一个开环系统
,控制电机的速度一般是通过控制pwm的占空比来控速。
我们需要目标速度,就一步一步的去尝试,速度慢了,就加大占空比,快了,就减小占空比,但是在实际尝试的过程中,我发现,很难人为控制占空比得到一个很精确的速度。
这个开环系统就是,设置占空比–>得到电机转速–>根据电机转速来判断占空比是否应该减小or增大–>调整占空比
pid:proportional(比例)、integral(积分)、differential(微分)的缩写。
用一句话来说,就是对输入偏差进行积分微分计算,用运算的叠加结果去控制执行机构
听起来很简单吧,一句话就讲完了。
啊😥,要是这么简单就好了。来看看下面的图,就是一个基本的pid控制框图
这就是形成了一个闭环系统,r(t)是输入量,u(t)是输出量
贴一篇链接:
一文读懂pid控制算法(抛弃公式,从原理上真正理解pid控制)
大家可以康康看,好好理解
kp增加时
,响应速度变快,当ki增加时
,能最终趋于目标值,kd增加时
,可以减小震荡。
p
:比例控制系统快速响应,快速接近于目标值,但是存在静态误差,输出到达不了目标值,会有误差。
i
:积分控制系统的准确性,消除累积的误差,输出到达目标值
d
:微分控制系统的稳定性,具有超前的控制作用,防止输出超过目标
大家理解这三个参数的作用,这样在调参的时候就会很快,是加大还是减小参数。
这方面我就不大写阔论了,好多大佬都比我讲的好,我在这里贴出几个链接,大家可以参考
增量式pid+位置式pid(电机位置闭环控制)
位置式 pid 控制算法和增量式 pid 控制算法
这个大佬的教程也很不错,我也学习了很久,顿悟了很多
而且这个大佬的教程是一系列的,大家可以有选择的学习
pid-电机速度控制-b
电机控制进阶——pid速度控制–csdn
如果大家不理解位置式pid公式的话,可以首先了解如何使用,如何调参,如何使用pid进行闭环控制,这里以速度为例
大家可以看我下面位置式pid的实现,我直接给出了源代码,大家可以参考
位置式是离散型的pid,大家记住比例kp,积分ki,微分kd这三个参数的作用,再就是位置式的代码化实现
pid.h
#ifndef __pid_h
#define __pid_h
#include "sys.h"
typedef struct
{
float target_val; //目标值
float err; //偏差值
float err_last; //上一个偏差值
float kp,ki,kd; //比例、积分、微分系数
float integral; //积分值
float output_val; //输出值
}pid;
extern pid pid;
pid.h
#include "pid.h"
#include "encoder.h"
//位置式 有误差,速度比较慢
pid pid;
void pid_param_init(void)
{
/* 初始化参数 */
pid.err=0.0;
pid.err_last=0.0;
pid.integral=0.0; //积分项
pid.kp=15.0; //最优
pid.ki=1.0; //0.05 0.1 p 15.0 i 0.1 kd 2.5
pid.kd=1.5; //调节成功 p 25.0 i 0.03 kd 0.025
//kd 4.0 最大 15 1.0 1.5
pid.target_val = 26; //目标值
pid.output_val=0.0; //输出值
}
//位置式pid 传入实际值即可
float pid_realize(float actual_val)
{
/*计算目标值与实际值的误差*/
pid.err = pid.target_val - actual_val; //目标值和实际值的误差
/*积分项*/
pid.integral += pid.err; //误差累积
/*pid算法实现*/
pid.output_val = pid.kp * pid.err +
pid.ki * pid.integral +
pid.kd * (pid.err - pid.err_last); //位置式
/*误差传递*/
pid.err_last = pid.err;
/*返回当前实际值*/
return pid.output_val;
}
还有一种位置
关于这个增量式pid大家可以参考下面的链接,好好学习,大佬还是讲的很明白的
电机速度环和位置环pid调参教程–b站
【stm32f4系列】【hal库】电机控制(转速和角度)(pid实战1)_32 hal库将pid坐标转换化为角度_hz1213825的博客-csdn博客
一般使用增量式pi就可以控制住速度了,所以我们采用增量式pi控制速度,闭环速度环,大家可以参考下面的代码,传入目标值和当前值,输出pi运算后的输出
进行pi调参,进而控制电机速度
// 速度环pi控制 使用增量式
/**************************************************************************
函数功能:增量pi控制器
入口参数:编码器测量值,目标速度
返回 值:电机pwm
根据增量式离散pid公式
out+=kp[e(k)-e(k-1)]+ki*e(k)+kd[e(k)-2e(k-1)+e(k-2)]
e(k)代表本次偏差
e(k-1)代表上一次的偏差 以此类推
out代表增量输出
在我们的速度控制闭环系统里面,只使用pi控制
pwm+=kp[e(k)-e(k-1)]+ki*e(k)
**************************************************************************/
int incremental_pi(int encoder, int target)
{
float kp = 10.0, ki = 1;
static int error, out, err_last; // 误差 输出 上一次误差
error = encoder - target; // 求出速度偏差,由测量值减去目标值。
out += kp * (error - err_last) + ki * error; // 使用增量 pi 控制器求出电机 pwm。
err_last = error; // 保存上一次偏差
return out; // 增量输出
}
参考我的这一篇文章:如何使用vofa+?一款好用的上位机软件(vofa+的三种数据传输协议)——以pid调参为例
我做出个示例
使用firewater协议格式,可以是任何类型的数据,但是以逗号隔开,最后必须以\n结尾,这样在上位机中就可以显示出波形了
下方分别代表当前速度,目标速度,输出
printf("%f,%f,%f\n",current,target,out); //脉冲,目标值,out
如果这种协议不懂得话
大家可以直接使用我编写的库,简单好用
vofa.c
/*
要点提示:
1. float和unsigned long具有相同的数据结构长度
2. union据类型里的数据存放在相同的物理空间
*/
typedef union
{
float fdata;
unsigned long ldata;
} floatlongtype;
/*
将浮点数f转化为4个字节数据存放在byte[4]中
*/
void float_to_byte(float f,unsigned char byte[])
{
floatlongtype fl;
fl.fdata=f;
byte[0]=(unsigned char)fl.ldata;
byte[1]=(unsigned char)(fl.ldata>>8);
byte[2]=(unsigned char)(fl.ldata>>16);
byte[3]=(unsigned char)(fl.ldata>>24);
}
void justfloat_test(void) //justfloat 数据协议测试
{
float a=1,b=2; //发送的数据 两个通道
u8 byte[4]={0}; //float转化为4个字节数据
u8 tail[4]={0x00, 0x00, 0x80, 0x7f}; //帧尾
//向上位机发送两个通道数据
float_to_byte(a,byte);
//u1_printf("%f\r\n",a);
u1_sendarray(byte,4); //1转化为4字节数据 就是 0x00 0x00 0x80 0x3f
float_to_byte(b,byte);
u1_sendarray(byte,4); //2转换为4字节数据 就是 0x00 0x00 0x00 0x40
//发送帧尾
u1_sendarray(tail,4); //帧尾为 0x00 0x00 0x80 0x7f
}
//向vofa发送数据 三个数据 三个通道 可视化显示 帧尾
void vofa_senddata(float a,float b,float c)
{
//float a=1,b=2; //发送的数据 两个通道
u8 byte[4]= {0}; //float转化为4个字节数据
u8 tail[4]= {0x00, 0x00, 0x80, 0x7f}; //帧尾
//向上位机发送两个通道数据
float_to_byte(a,byte);
//u1_printf("%f\r\n",a);
u1_sendarray(byte,4); //1转化为4字节数据 就是 0x00 0x00 0x80 0x3f
float_to_byte(b,byte);
u1_sendarray(byte,4); //2转换为4字节数据 就是 0x00 0x00 0x00 0x40
float_to_byte(c,byte);
u1_sendarray(byte,4);
//发送帧尾
u1_sendarray(tail,4); //帧尾为 0x00 0x00 0x80 0x7f
}
使用
void vofa_senddata(float a,float b,float c) //a,b,c代表三个通道波形
我要实现的是速度环和位置闭环
,我现在首先目标是单环控制,首先速度环,然后位置环
速度环采用增量式pi控制,位置环采用位置式pid控制
速度环没问题(在这里无论是位置式还是增量式都实现了进行速度控制,这里建议增量式pi
)
但是目前位置式pid控制位置环出现了点问题,当我调参的时候,无论目标速度调成多大,电机的转速总是会趋近于最大转速,目前这个问题还未解决,暂定
大家可以参考我的代码,采用vofa+上位机显示调参波形,根据波形进行调参,希望可以帮助到大家。
我只贴出主要控制代码,我的工程文件将会开源,大家可以下载,参考
/*
* @author: _oufen
* @date: 2023-03-31 18:23:31
* @lastedittime: 2023-04-01 19:34:31
* @description:
*/
#include "timer4.h"
#include "led.h"
#include "encoder.h"
#include "motor.h"
#include "vofa.h"
#include "usart.h"
// int encoder_speed; // 实际速度 近似脉冲
// int target_speed = 30; // 目标速度 每10ms 30个脉冲
// int moto1; // 轮子输出值
int encoder_position;
int target_position = 13;
int moto1;
void timer4_init(u16 psc, u16 arr) // timer4_init(7200-1,1000-1);
{
tim_timebaseinittypedef tim_timebasestructure;
nvic_inittypedef nvic_initstructure;
// tim_ocinittypedef tim_ocinitstructure;
rcc_apb1periphclockcmd(rcc_apb1periph_tim4, enable);
rcc_apb2periphclockcmd(rcc_apb2periph_afio, enable);
tim_deinit(tim4); // 定时器4恢复默认设置
// my_gpio_init(gpiob,gpio_pin_6,gpio_mode_af_pp);
// my_gpio_init(gpiob,gpio_pin_7,gpio_mode_af_pp);
// my_gpio_init(gpiob,gpio_pin_8,gpio_mode_af_pp);
// my_gpio_init(gpiob,gpio_pin_9,gpio_mode_af_pp);
tim_timebasestructure.tim_period = arr;
tim_timebasestructure.tim_prescaler = psc;
tim_timebasestructure.tim_countermode = tim_countermode_up;
tim_timebasestructure.tim_clockdivision = tim_ckd_div1;
tim_timebaseinit(tim4, &tim_timebasestructure);
nvic_initstructure.nvic_irqchannel = tim4_irqn;
nvic_initstructure.nvic_irqchannelpreemptionpriority = 2;
nvic_initstructure.nvic_irqchannelsubpriority = 1;
nvic_initstructure.nvic_irqchannelcmd = enable;
nvic_init(&nvic_initstructure);
tim_clearflag(tim4, tim_flag_update);
tim_clearitpendingbit(tim4, tim_it_update); // 清除更新中断请求位
tim_itconfig(tim4, tim_it_update, enable);
tim_cmd(tim4, enable);
}
// 向vofa发送数据 三个数据 三个通道 可视化显示 帧尾
void vofa_senddata(float a, float b, float c)
{
u8 byte[4] = {0}; // float转化为4个字节数据
u8 tail[4] = {0x00, 0x00, 0x80, 0x7f}; // 帧尾
// 向上位机发送两个通道数据
float_to_byte(a, byte);
// u1_printf("%f\r\n",a);
u1_sendarray(byte, 4); // 1转化为4字节数据 就是 0x00 0x00 0x80 0x3f
float_to_byte(b, byte);
u1_sendarray(byte, 4); // 2转换为4字节数据 就是 0x00 0x00 0x00 0x40
float_to_byte(c, byte);
u1_sendarray(byte, 4);
// 发送帧尾
u1_sendarray(tail, 4); // 帧尾为 0x00 0x00 0x80 0x7f
}
// 速度环pi控制 使用增量式
/**************************************************************************
函数功能:增量pi控制器
入口参数:编码器测量值,目标速度
返回 值:电机pwm
根据增量式离散pid公式
out+=kp[e(k)-e(k-1)]+ki*e(k)+kd[e(k)-2e(k-1)+e(k-2)]
e(k)代表本次偏差
e(k-1)代表上一次的偏差 以此类推
out代表增量输出
在我们的速度控制闭环系统里面,只使用pi控制
pwm+=kp[e(k)-e(k-1)]+ki*e(k)
**************************************************************************/
int incremental_pi(int encoder, int target)
{
float kp = 10.0, ki = 1;
static int error, out, err_last; // 误差 输出 上一次误差
error = encoder - target; // 求出速度偏差,由测量值减去目标值。
out += kp * (error - err_last) + ki * error; // 使用增量 pi 控制器求出电机 pwm。
err_last = error; // 保存上一次偏差
return out; // 增量输出
}
/**************************************************************************
函数功能:位置式pid控制器
入口参数:编码器测量位置信息,目标位置
返回 值:电机pwm
根据位置式离散pid公式
out=kp*e(k)+ki*∑e(k)+kd[e(k)-e(k-1)]
e(k)代表本次偏差
e(k-1)代表上一次的偏差
∑e(k)代表e(k)以及之前的偏差的累积和;其中k为1,2,,k;
out代表输出
**************************************************************************/
int position_pid(int encoder, int target)
{
float position_kp = 15, position_ki = 0.1, position_kd = 0.1; // pid
static float error, out, integral_error, error_last; // 误差 输出 积分 上一次误差
error = encoder - target; // 求出速度偏差,由测量值减去目标值。
integral_error += error; // 求出偏差的积分
out = position_kp * error + position_ki * integral_error + position_kd * (error - error_last); // 位置式pid控制器
error_last = error; // 保存上一次偏差
return out; // 增量输出
}
int myabs(int a)
{
int temp;
if (a < 0)
temp = -a;
else
temp = a;
return temp;
}
void set_pwm(int pwm)
{
if (pwm > 0)
ain1 = 0, ain2 = 1;
else
ain1 = 1, ain2 = 0;
pwma = myabs(pwm); //pwma --> tim1->ccr1
}
void xianfu_pwm(void)
{
int amplitude = 99;
if (moto1 < -amplitude)
moto1 = -amplitude;
if (moto1 > amplitude)
moto1 = amplitude;
}
// 定时器定时调用
/*void autoreloadcallback()
{
encoder_speed += read_encoder(2); // 读取真实速度
//printf("encoder = %d\r\n", encoder_speed);
moto1 = incremental_pi(encoder_speed, target_speed); // pid计算
xianfu_pwm(); // 对输出进行限幅
// printf("moto1 = %d\r\n", pwma);
set_pwm(moto1);
vofa_senddata(encoder_speed, target_speed, pwma); // 向上位机发送数据
}*/
void autoreloadcallback()
{
encoder_position = read_encoder(2); // 读取真实速度
//printf("encoder = %d\r\n", encoder_position);
moto1 = position_pid(encoder_position, target_position); // pid计算
xianfu_pwm(); // 对输出进行限幅
// printf("moto1 = %d\r\n", pwma);
set_pwm(moto1);
vofa_senddata(encoder_position, target_position, pwma); // 向上位机发送数据
}
void tim4_irqhandler(void) // 10ms
{
if (tim_getitstatus(tim4, tim_it_update) != reset)
{
autoreloadcallback(); // 定时调用
tim_clearitpendingbit(tim4, tim_it_update);
led1 = !led1;
}
}
如果有时间得话,可能会玩一玩平衡小车,现成的硬件都有,据我了解,主要是角度环,直立环,速度环,三环闭环。
也许会对pid的理解和调参更进一步
如果没有时间的话,将会直接上手搭车,根据实际练习和学习其他模块,比如循迹,电机驱动,串口通信(和mv的通信),蓝牙,实际控制逻辑的编写等。
还是任重道远,从0到1,从无到有
大家可以参考我的代码,已经全部开源
附带我的学习笔记和收集到的各种开源代码,希望大家可以学明白,也希望我的微博力量可以帮助到大家
加油加油
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论