使用PID控制器设计基于Arduino的编码器电机

控制/MCU

1814人已加入

描述

  在工业自动化和控制中,PID控制器已经成为最可靠的控制算法之一,可以实现稳定任何系统的输出响应。PID 代表比例积分微分。这三种类型的控制机制组合在一起,会产生一个误差信号,这个误差信号被用作反馈来控制最终应用程序。PID 控制器可以在广泛的工业和商业应用中找到,例如用于调节压力、线性运动和许多其他变量。PID温度控制器是您可以在 Internet 上找到的最常见的应用程序。如果没有 PID 控制器,手动完成这项工作可能是一个乏味的过程。在这个先进的数字电子设备和微控制器时代,在任何系统中设计和实施 PID 控制器变得更加容易。

  什么是 PID 控制器,它是如何工作的?

  正如我们在介绍部分告诉您的, PID是比例、积分和微分的首字母缩写词。但这甚至意味着什么,有没有更简单的方法来理解它?就在这里。为此,让我们以我们在之前的一个项目中使用 Arduino 的 DIY 智能吸尘机器人为例 。对我来说,这是一个非常酷的项目,在电路和控制机制方面非常简单。但它的主要缺点是它没有任何基于 PID 的控制机制。现在,假设机器人正在清洁自己并且靠近楼梯,它有一个接近传感器在检测到这种情况并切断电机电源的机器人下方,但由于惯性,机器人不会立即停止。如果发生这种情况,机器人很可能会从楼梯上绊倒。现在,假设你有一辆机器人汽车,你想把它停在某个位置,如果没有 PID,这可能会非常困难,因为如果你只是切断电源,汽车绝对会因为它的动量而错过目标。

  现在我们知道了这个概念,我们可以继续前进并理解一些高级部分。如果您在线搜索 PID 控制器,您将得到的第一个结果来自PID 控制器 - 维基百科,在这篇文章中,您会找到一个框图和一个方程。但是这个等式甚至意味着什么,我们如何在我们的微控制器中实现它?好问题,现在继续,你会明白如何,

Arduino

  该控制器以错误的处理方式命名,然后被求和,然后发送到工厂/过程中。让我解释!在框图中,您可以看到在比例路径中,误差乘以常数Kp。在积分路径中,误差乘以常数Ki然后积分,在导数路径中,误差乘以Kd,然后微分。之后,将三个值相加以产生输出。现在在控制器中,Kp、Kd 和 Ki 参数称为增益。并且它们被调整或调整以满足一组特定的要求,并且通过更改这些值,您可以调整您的系统对这些不同参数(P、I 或 D 参数)中的每一个的敏感程度。让我通过单独检查每个参数来解释它。

  P 控制器:

Arduino

  假设系统中的错误随着时间的推移而变化,正如您在红线中观察到的那样。在比例控制器中,输出是由增益 Kp 定义的误差。可以看到,当误差很大时,输出会产生很大的输出,当误差为零时,输出误差为零,当误差为负时,输出为负

  I-控制器:

Arduino

  在积分控制器中,随着误差值随时间变化,积分将开始对误差开始求和,并将其与常数 Ki 相乘。在这种类型的控制器中,很容易看出积分结果是曲线下方的区域,其中蓝色区域为正区域,黄色区域为负区域。在复杂系统中,积分控制器用于消除控制系统中的恒定误差。不管常数误差有多小,最终,误差的总和将足以调整控制器的输出。在上图中,错误用绿线表示。

  D-控制器:

Arduino

  在微分控制器中,影响输出信号的是误差的变化率。当误差变化相对缓慢时,我们可以使用正弦波的起始位置作为示例。如上图所示(由绿线表示),导数输出将很小。并且误差变化越快,输出就越大。

  现在,您可以将三个输出相加,您就有了 PID 控制器。但通常您不需要所有三个控制器一起工作,相反,我们可以通过将设定点设置为零来移除任何人。例如,我们可以通过将 D 值设置为零来获得 PI 控制器,否则我们可以通过将 I 参数设置为零来获得 PD 控制器。现在我们有了一个清晰的想法,我们可以进入实际的硬件示例。

  什么是编码器电机,它是如何工作的?

  编码器电机的概念非常简单:它是一个附有编码器的有刷直流电机。在上一篇文章中,我们已经详细讨论了旋转编码器,如果您想了解更多有关该主题的信息,可以查看。

Arduino

  在编码器电机中,旋转编码器安装在直流电机上,直流电机通过跟踪电机轴的速度或位置向系统提供反馈。有许多不同类型的电机可用,所有这些电机都可以具有不同类型的编码器配置,例如增量或绝对、光学、空心轴、磁性等,不胜枚举。不同类型的电机适用于不同类型的应用。不仅直流电机,许多伺服电机、步进电机和交流电机都带有内置编码器。在上图中,您可以看到N20 永磁式编码电机,它在附加变速箱的帮助下将输出 RPM 降低到 15。您还可以看到两个霍尔传感器附在 PCB 上。这些霍尔传感器获取电机旋转的方向,在微控制器的帮助下,我们可以很容易地读取它。

  构建启用 PID 的编码器电机控制器所需的组件

  在这一点上,我们对 PID 控制器的工作有了一个很好的了解,并且我们也知道了我们的最终目标。基于此,我们决定使用 Arduino 和其他一些互补组件来构建电路。这些补充组件的列表如下所示。

Arduino

  Arduino 纳米 - 1

  N20 编码电机 - 1

  BD139 - 2

  BD140 - 2

  BC548 - 2

  100R 电阻 - 2

  4.7K电阻 - 2

  面包板

  跳线

  电源

  用于测试启用 PID 的编码器电机控制器的示意图

  启用 PID 的编码器电机控制器的完整原理图如下所示。这个电路的工作原理很简单,下面就来介绍一下。

Arduino

  电路非常简单。首先,在原理图中,我们有N20 编码器电机,它有六个引脚,引脚标记为M1、M2,用于为电机供电,因为这是一个非常小的电机,额定电压为 3.3V。接下来,我们有用于为编码器电路供电的VCC和GND引脚。要为编码器电路供电,您必须给它+5V,否则编码器电路将无法正常工作。接下来,我们有PIN_A和PIN_B的电机。这两个引脚直接连接到编码器。通过读取这些引脚的状态,我们可以很容易地测量转速,这个 15RPM N20 电机的齿轮比为 1:2098,这意味着主电机轴需要旋转 2098 次,辅助轴旋转一次。PIN_A和PIN_B连接到Arduino的引脚 9 和引脚 10,引脚 9 和 10 都是支持 PWM 的引脚;所选引脚必须具有 PWM 功能,否则代码将不起作用。PID 控制器通过控制 PWM 来控制电机。

  接下来,我们有我们的H桥电机驱动器,电机驱动器的制作使得我们只需使用Arduino的两个引脚即可控制电机,甚至可以防止电机误触发。

Arduino

支持 PID 的编码器电机控制器的 Arduino 代码

此项目中使用的完整代码可在此页面底部找到。添加所需的头文件和源文件后,您应该可以直接编译Arduino代码而不会出现任何错误。您可以从下面给出的链接下载PID 控制器库,或者您可以使用板管理器方法安装该库。

为 Arduino 下载 PID 控制器库

ino中的代码说明。文件 如下。首先,我们首先包含所有必需的库。在这个程序中,我们只使用PID 控制器库, 所以我们需要首先包含它。之后,我们定义读取编码器和驱动电机所需的所有必要引脚。完成后,我们定义 Kp、Ki 和 Kd 的所有值。

 

#include 
/* ENCODER_A 和 ENCODER_B 引脚用于读取编码器
 * 来自微控制器的数据,来自编码器的数据
 * 来得非常快,所以这两个引脚必须启用中断
 * 引脚
*/
#define ENCODER_A 2
#define ENCODER_B 3
/* MOTOR_CW 和 MOTOR_CCW 引脚用于驱动 H 桥
 * H 桥然后驱动电机,这两个引脚必须
 * 启用 PWM,否则代码将无法工作。
*/
#define MOTOR_CW 9
#define MOTOR_CCW 10

 

接下来,我们为代码定义了__Kp、__Ki和__Kd值。这三个常量负责为我们的代码设置输出响应。在这一点上,请注意,对于这个项目,我使用了试错法来设置常量,但还有其他方法可以很好地完成这项工作。

 

/*在本节中,我们定义了增益值
 * 我设置的比例、积分和微分控制器
 * 在试错法的帮助下获得增益值。
*/
#define __Kp 260 // 比例常数
#define __Ki 2.7 // 积分常数
#define __Kd 2000 // 导数常数

 

接下来,我们定义了此代码中所需的所有必要变量。首先,我们有encoder_count 变量,用于计算产生的中断数;因此它计算圈数。接下来,我们定义了一个 unsigned int类型变量整数值,用于存储我们放入串行监视器中的值。接下来,我们定义了一个 char 类型的变量incomingByte来临时存储传入的串行数据。接下来,我们在这段代码中定义了最重要的变量,就是motor_pwm_value变量,通过PWM算法计算出数据后存储在这个变量中。定义这些变量后,我们为PID控制器。一旦我们这样做了,我们就可以进入我们的setup()函数。

 

volatile long int encoder_count = 0; // 存储当前编码器计数
无符号整数整数值 = 0; // 存储传入的序列值。最大值为 65535
char 传入字节;// 一个一个地解析并存储每个字符
int motor_pwm_value = 255; // 在 PID 计算数据存储在这个变量中之后。
PIDController pid_controller;

 

在设置函数中,我们将ENCODER_A和ENCODER_B引脚分配为输入,并将MOTOR_CW和MOTOR_CCW引脚定义为输出。接下来,我们将ENCODER_A分配为中断,在上升沿,这将调用函数encoder(); 接下来的三行再次是最重要的,因为我们使用begin()方法启用了 PID 控制器,并且我们还使用 Kp、Ki 和 Kd 值调整了控制器。最后,我们为 PID 控制器输出设置了限制。

 

无效设置(){
  序列号.开始(115200);// 调试串口
  pinMode(ENCODER_A,输入);// ENCODER_A 作为输入
  pinMode(ENCODER_B,输入);// ENCODER_B 作为输入
  pinMode(MOTOR_CW,输出);// MOTOR_CW 作为输出
  pinMode(MOTOR_CCW,输出);// MOTOR_CW 作为输出
/* 将中断附加到 Arduino 的 ENCODER_A 引脚,当脉冲处于上升沿时调用函数 encoder()。
*/
  attachInterrupt(digitalPinToInterrupt(ENCODER_A),编码器,RISING);
  pidcontroller.begin(); //初始化PID实例
  pidcontroller.tune(__Kp , __Ki , __Kd); // 调整 PID,参数:kP, kI, kD
  pidcontroller.limit(-255, 255); // 将 PID 输出限制在 -255 到 255 之间,这对于消除积分饱和很重要!
}

 

接下来,我们有我们的loop()部分。在循环部分,我们首先检查串行是否可用。如果序列号可用,我们解析整数值并将其保存到整数值变量中。接下来,我们有一个' /n'字符进入。我们把它放在incomingByte变量中,并用if语句检查这个变量,如果为真,我们继续循环,接下来我们用pidcontroller设置目标点.setpoint(整数值);并传递我们刚刚从串行接收到的整数值。接下来,我们打印接收到的值进行调试。

我们有motor_pwm_value变量,我们计算 PID 值并将其放入该变量中。如果该值大于零,我们调用 motor_ccw(motor_pwm_value)函数并传入该值,否则,我们调用motor_cw(abs(motor_pwm_value))函数。这标志着我们循环部分的结束。

 

无效循环(){
  而 (Serial.available() > 0) {
    integerValue = Serial.parseInt(); // 存储整数值
    传入字节 = Serial.read(); // 存储 /n 字符
    pidcontroller.setpoint(整数值);// PID 控制器试图“达到”的“目标”,
    Serial.println(integerValue); // 打印传入的值以进行调试
    if (incomingByte == '\n') // 如果我们收到换行符,我们将继续循环
      继续;
  }
  motor_pwm_value = pidcontroller.compute(encoder_count); //让PID计算值,返回计算出的最优输出
  Serial.print(motor_pwm_value); // 打印计算值以供调试
  序列号.print("");
  if (motor_pwm_value > 0) // 如果 motor_pwm_value 大于零,我们顺时针旋转电机
    MotorCounterClockwise(motor_pwm_value);
  else // 否则,我们逆时针方向移动它
    MotorClockwise(abs(motor_pwm_value));
  Serial.println(encoder_count);// 打印最终的编码器计数。
}

 

接下来,我们有编码器功能。当ENCODER_B中出现 tan 上升沿中断时调用此函数。如果为真,我们使用if (digitalRead(ENCODER_B) == HIGH) 再次检查该语句。一旦为真,我们的计数器变量就会增加。否则,它会递减。

 

无效编码器(){
  if (digitalRead(ENCODER_B) == HIGH) // 如果 ENCODER_B 为高,则增加计数
    编码器计数++;// 增加计数
  else // 否则减少计数
    编码器计数——;// 减少计数
}

 

接下来,我们有使电机顺时针旋转的功能。调用此函数时,它会检查该值是否大于 100。如果是这样,我们按顺时针方向旋转电机,否则我们停止电机。

 

void motor_cw(int power){
  如果(功率 > 100){
    模拟写入(MOTOR_CW,电源);
    数字写入(MOTOR_CCW,低);
  }
// 两个引脚都设置为低
  别的 {
    数字写入(MOTOR_CW,低);
    数字写入(MOTOR_CCW,低);
  }
}

 

逆时针旋转电机的功能也是如此。调用此函数时,我们检查该值并逆时针旋转电机。

 

void motor_ccw(int power){
  如果(功率 > 100){
    模拟写入(MOTOR_CCW,电源);
    数字写入(MOTOR_CW,低);
  }
  别的 {
    数字写入(MOTOR_CW,低);
    数字写入(MOTOR_CCW,低);
  }
}

 

  这标志着我们编码部分的结束。

  测试启用 PID 的电机控制器

  以下设置用于测试电路。如您所见,我使用了一个带有一些双面胶带的电箱来固定电机,并且我使用了一个小型降压转换器模块为电机供电,因为电机在 3.3V 上运行。

Arduino

  您还可以看到,我们已将 USB 电缆与 Arduino 连接,用于设置 PID 控制器的设定值。我们还通过 USB 从 Arduino 获取调试信息。在这种情况下,它给出了当前的编码器计数。下图将使您更好地了解该过程。

Arduino
 

#include
/* ENCODER_A 和 ENCODER_B 引脚用于读取编码器
来自微控制器的数据,来自编码器的数据
来得非常快,所以这两个引脚必须启用中断
引脚
*/
#define ENCODER_A 2
#define ENCODER_B 3
/* MOTOR_CW 和 MOTOR_CCW 引脚用于驱动 H 桥
然后 H 桥驱动电机,这两个引脚必须
启用 PWM,否则代码将无法工作。
*/
#define MOTOR_CW 9
#define MOTOR_CCW 10
/*在本节中,我们定义了增益值
我设置的比例、积分和微分控制器
在试错法的帮助下获得增益值。
*/
#define __Kp 260 // 比例常数
#define __Ki 2.7 // 积分常数
#define __Kd 2000 // 导数常数
volatile long int encoder_count = 0; // 存储当前编码器计数
无符号整数整数值 = 0; // 存储传入的序列值。最大值为 65535
char 传入字节;// 一个一个地解析和存储每个单独的字符
int motor_pwm_value = 255; // 在 PID 计算数据存储在这个变量中之后。
PIDController PID控制器;
无效设置(){
序列号.开始(115200);// 调试串口
pinMode(ENCODER_A,输入);// ENCODER_A 作为输入
pinMode(ENCODER_B,输入);// ENCODER_B 作为输入
pinMode(MOTOR_CW,输出);// MOTOR_CW 作为输出
pinMode(MOTOR_CCW,输出);// MOTOR_CW 作为输出
/* 将中断附加到 Arduino 的 ENCODER_A 引脚,当
脉冲处于上升沿,称为函数 encoder()。
*/
attachInterrupt(digitalPinToInterrupt(ENCODER_A),编码器,RISING);
pidcontroller.begin(); //初始化PID实例
pidcontroller.tune(260, 2.7, 2000); // 调整 PID,参数:kP, kI, kD
pidcontroller.limit(-255, 255); // 将 PID 输出限制在 -255 到 255 之间,这对于消除积分饱和很重要!
}
无效循环(){
而 (Serial.available() > 0) {
integerValue = Serial.parseInt(); // 存储整数值
传入字节 = Serial.read(); // 存储 /n 字符
if (incomingByte == '\n') // 如果我们收到换行符,我们将继续循环
继续;
}
pidcontroller.setpoint(整数值);// PID 控制器试图“达到”的“目标”,
Serial.println(integerValue); // 打印传入的值以进行调试
motor_pwm_value = pidcontroller.compute(encoder_count); //让PID计算值,返回计算出的最优输出
Serial.print(motor_pwm_value); // 打印计算值以供调试
序列号.print("");
if (motor_pwm_value > 0) // 如果 motor_pwm_value 大于零,我们顺时针旋转电机
motor_ccw(motor_pwm_value);
else // 否则我们按逆时针方向移动它
motor_cw(abs(motor_pwm_value));
Serial.println(encoder_count);// 打印最终的编码器计数。
}
无效编码器(){
if (digitalRead(ENCODER_B) == HIGH) // 如果 ENCODER_B 为高,则增加计数
编码器计数++;// 增加计数
else // 否则减少计数
编码器计数——;// 减少计数
}
void motor_cw(int power){
如果(功率 > 100){
模拟写入(MOTOR_CW,电源);//如果值大于100,则旋转电机
数字写入(MOTOR_CCW,低);// 使另一个引脚为低电平
}
别的 {
// 两个引脚都设置为低
数字写入(MOTOR_CW,低);
数字写入(MOTOR_CCW,低);
}
}
void motor_ccw(int power){
如果(功率 > 100){
模拟写入(MOTOR_CCW,电源);
数字写入(MOTOR_CW,低);
}
别的 {
数字写入(MOTOR_CW,低);
数字写入(MOTOR_CCW,低);
}
}

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分