Arduino UNO控制带AB相磁通量式编码器电动推杆(测试阻尼)实录(L289N电机驱动)

前段时间为了测试实验器材的阻尼,需要去开发一套装置来测试。提出用Arduino单片机来控制电动推杆(Linear Actuator)来制造相应速度的运动,搭配上测力计,从而根据F=cv来测得阻尼,在这里简单记录一下全过程。这款电动推杆是带AB相增量式磁编码器的,但是我们也可以选择不利用编码器,即将其当作一个普通电机进行使用。本文代码可以直接拷贝使用。


目录

一、基本原理

二、不带编码器(PWM)接线

1、L289N接线

2、Arduino接线 

三、带编码器接线 

四、Arduino代码(不带编码器控制电动推杆速度)

五、Arduino代码(带编码器控制电动推杆速度)

六、拓展:MATLAB与Arduino连接及代码


一、基本原理

(28条消息) 关于电机编码器的知识汇总,都在这里了!_张巧龙的博客-CSDN博客https://blog.csdn.net/best_xiaolong/article/details/114274883?ops_request_misc=&request_id=&biz_id=102&utm_term=%E5%B8%A6%E7%BC%96%E7%A0%81%E5%99%A8%E7%94%B5%E6%9C%BA&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-3-114274883.nonecase&spm=1018.2226.3001.4187一、PWM:

简单来说就是通过带一定频率的开关动作来实现调节平均电压的方法。这里通过发送数字信号(即满电压1或无电压0),这里利用方波的占空比被调制的方法对一个具体模拟信号的电平进行编码。

二、编码器:

编码器在电动推杆中是一种检测线性位移的传感器,其由电动机来驱动,可以将线性位移进行编码。

三、带编码器和不带编码器之间的区别:

不带编码器指的就是开环控制(Open-loop control),想调节电机的速度只取决于单片机给到电机的PWM信号(即脉冲信号数量和之间的间隔)。其优点是系统简单,缺点是运行效率很低,因为其总是在最大电流和0之间切换以防止失步。并且实际发现占空比设置得较小的时候,推杆速度将会不稳定。另外通过人工去测量电动推杆的运行速度,误差很大。

带编码器指的是闭环控制(Closed-loop control),也指的是PID控制。加入了AB相磁通量式编码器来作为监测器,其能够将位移转换成周期性的电信号,再将电信号转换为计数脉冲,有AB两个相可以正反向计数。单片机能够通过读取编码器输出的脉冲的上升沿和脉冲个数来测得电动推杆的转动方向和实际速度,再根据PI控制器可以算出一个将电动推杆调整到目标速度的PWM值,最后将PWM值输出给电机。这个[对比输入信号和实际信号,进行一个反馈,修正输入信号]的过程会一直循环,会形成一个闭环系统。对比不带编码器的情况,其可以不必保持最大电流,降低系统整体功耗,而且控制也会更精确,输出的速度更加稳定。

PID控制分为增量式PID和位置式PID,在本文中,指的是增量式PID(市面上大多数编码器电机),可以用于让速度动态维持在一个目标值。PID对比PWM方法去控制电机转速,其优势是能够使速度更稳定(如实际操作中,转速很难达到并保持166.5 RPM,有可能为150、170 RPM,反正就不会保持166.5 RPM,此时就需要用到一个闭环的算法(增量式PID))。并且抗干扰能力强,这也是闭环系统对比开环系统的优势。

二、不带编码器(PWM)接线

(26条消息) STM32f4日记5之AB相编码器测速实验(TIM定时器的编码器模式使用)_@SHAWN_shawn的博客-CSDN博客_ab相霍尔编码器https://blog.csdn.net/qq_51564898/article/details/113359450

我这里采用了Arduino uno+L289N+电动推杆(电动机+编码器)的组合。接线图如下:

1、L289N接线注意事项

用Arduino和L298N控制直流电机的正反转和调速_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1eE411F7HP/?spm_id_from=autoNext&vd_source=1fcf4febef247ec78f783870e62a07d6以上这个视频从电压的角度讲了接线的问题以及功率的问题。接线顺序由左至右:

接线注释:

  1. 输出端Out1 Out2:分别连接电机的正负极。(若想让电机反转,只需要改变输出A的正负两极方向即可。)
  2. 输出端旁边的板载5V电源的跳线帽需要接上。
  3. GND:连接变压电源的负极+连接Arduino的负极。(共地很重要,因为如果不连接在一起,电压就没有参考电平,就无法进行正常的控制了)
  4. +5V供电口:有电源模块可以给单片机供电,这里可以不接。(图上是接的)
  5. ENA:是左侧Out1、Out2的使能端,这里由于需要实现PWM控制电机转速,因此跳线帽需要拔下来。如果不拔下来,使能端一直就为高电平,即全速运转。ENA下方引脚与Arduino相连。
  6. IN1、IN2:作为逻辑输入连接Arduino。可以接受高、低电平信号。

2、Arduino UNO接线注意事项 

接线注释:

  1. 7、8号口接L289N驱动的IN1、IN2;
  2. 9号口作为(PWM带波浪号~)输出信号端与ENA下侧引脚连接;
  3. 与电脑利用USB进行连接;使用USB供电时,5V接口直接输出USB提供的5v电压;使用外部电源供电时,5V输出稳压后的5v电压。
  4. 这里由于L289N有外接的电源,这里就不供5V的电压进去了。(但图上是接了的)
  5. GND与L289N驱动的GND相连;

三、带编码器接线 

由于Arduino UNO上没有stm32上那种编码器模式,因此这里利用了MsTimer2.h的定时中断库。接线方式基本和不带编码器接线时的一致,如图所示:

 接线注释:

  1. Arduino UNO板子中,2、3号接口是可以输入外部中断信号的;外部中断信号分为低电平LOW,电平状态改变CHANGE(即上升沿和下降沿都触发),上升沿RISING触发,下降沿FALLING触发。
  2. 9号口作为(PWM带波浪号~)输出信号端与ENA下侧引脚连接;

四、Arduino代码(不带编码器控制电动推杆速度)

这里有两个子程序,要用哪个,就把另外一个注释掉就好。对比stm32,类似analogWrite是封装了很多隐藏库的,因此只需要一条语句即可进行输出,无须像stm32一样配置寄存器库。

#define ENA 9   //以下三个都是输出
#define IN1 8
#define IN2 7
void setup() {
  pinMode(ENA,OUTPUT);
  pinMode(IN1,OUTPUT);
  pinMode(IN2,OUTPUT);
  Serial.begin(9600);
}
void loop() {
  Pos_NegRotation(); //以下有两个子程序
  PWMcontrol(); //脉宽调制控制速度从小到大
}
void Pos_NegRotation() //这个是控制电机正、反转、刹车的
{
  analogWrite(ENA,80);  //这个80/255即指的是占空比
  digitalWrite(IN1,HIGH); //高电平
  digitalWrite(IN2,LOW);  //低电平
  delay(2000);   //暂停2秒
  digitalWrite(IN1,HIGH); //高电平
  digitalWrite(IN2,HIGH);  //低电平
  delay(2000);   //暂停2秒
  digitalWrite(IN1,LOW); //高电平
  digitalWrite(IN2,HIGH);  //低电平
  delay(2000);   //暂停2秒
  digitalWrite(IN1,LOW); //高电平
  digitalWrite(IN2,LOW);  //低电平
  delay(2000);   //暂停2秒
}




void Pos_NegRotation() //这个是控制电机速度从小到大的
{
  int i;  //预定义
  digitalWrite(IN1,HIGH); //预定义电机反转
  digitalWrite(IN2,LOW); 
    for(i=0;i<=255;i++)   //即占空比由小到大
    {
      Serial.print("Value_i ="); //以下两行为串口打印数值
      Serial.println(i);  
      analogWrite(ENA,i); //把i写入使能口=D9
      delay(20); //延时20 ms
    }
    digitalWrite(IN1,HIGH); //停2秒
    digitalWrite(IN2,HIGH); //
    delay(2000);
}


五、Arduino代码(带编码器、引入定时器库)

PID参数遵循:先调I,再调P。速度闭环不使用D微分项。

在这里插入图片描述

06_Arduino_PID教程_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1xB4y1v72z/?spm_id_from=333.337.search-card.all.click&vd_source=1fcf4febef247ec78f783870e62a07d6

这个是来自清华大学电机系分享的PID教程,代码讲得很清楚。 下图解释了怎么计数。

这里很重要的是解释一下商家提供的编码器信息,这里涉及如何在串口监视器中正确的表示目前实际的速度:霍尔编码器16线,丝杠导程60速度的是9mm。即采用M法测速的时候,速度=M0/(C*T0),M0为设定时间内的脉冲数,这里是代码自己会运行出来的,C为商家提供的单圈总脉冲数,即16;T0为自己设定的时间,我这里设置为0.05s;因此此时的速度为圈/秒,如果需要将其转换成电动推杆垂直升/降的速度,则需要再乘以导程(导程:螺旋线绕圆柱体转一圈,沿圆柱体轴线方向移动的距离),即每一圈电动推杆将会走过9mm。此时便可以得到电动推杆的速度。

另外请注意:我这里选择引用了定时器库,因此void loop()是空的,即把需要循环的内容都放在了定时器函数中。若选择不引用定时器库,可以参考代码二。同时我这里仅仅实现了编码器一倍频,即仅在A的下降沿进行计数,并且我仅使用了PI控制器,对应的PID控制器代码在文中有备注。

  • 定义引脚部分:

#define ENCODER_A 2                //编码器A相引脚,在uno板子中,中断号0指的是引脚2,其可以输入外部中断信号

#define ENCODER_B 3                //编码器B相引脚,在uno板子中,中断号1指的是引脚3,其可以输入外部中断信号

#define L298N_IN1 7                //IN1

#define L298N_IN2 8                //IN2

#define Motor_PWM 9                //ENA

  • 一些库的引入及变量定义及参数设置:

#include <TimerOne.h>  //引用了库便不用在循环中写定时器函数,这是最主要区别

float V; //定义速度 单位mm/s

int PWM;  //定义存储通过PI控制器计算得到的用于调整电机转速的PWM值的整型变量

float Target_V=10;   //目标速度,单位mm/s,此编码器最大是60mm/s

float Velocity_KP =0, Velocity_KI =0.2;     //Velocity_KP,Velocity_KI.PI参数  Target目标值,这里是PI控制

float Bias=0,Last_bias=0;                       //定义全局静态浮点型变量 PWM并预设为0,Last_bias(上次偏差)并预设为0

  • setup()函数,是Arduino 必备

void setup()

{

  pinMode(ENCODER_A, INPUT);       //连接编码器A,设置为输入模式

  pinMode(ENCODER_B, INPUT);       //连接编码器B,设置为输入模式

  pinMode(L298N_IN1,OUTPUT);       //连接L289N驱动IN1, 设置为输出模式

  pinMode(L298N_IN2,OUTPUT);       //连接L289N驱动IN2, 设置为输出模式

  pinMode(Motor_PWM,OUTPUT);       //连接L289N的使能A引脚, 设置为输出模式

  Serial.begin(9600);//初始化波特率为9600

  attachInterrupt(0, READ_ENCONDER_A, FALLING);// 外部中断触发方式为FALLING,表示下降沿触发,触发的中断函数为counter0

  Timer1.initialize(50000); //即每隔50000微秒(=50ms=0.05s)调用一次中断函数

  Timer1.attachInterrupt(timerIsr); //中断函数为timerIsr

}

  • loop()函数,Arduino必备

void loop() {}

  • 外部中断函数(解决怎样计数的问题)

void READ_ENCONDER_A()

{

    if (digitalRead(ENCODER_B) == LOW)//正转

    {

        count += 1;

    }

    if(digitalRead(ENCODER_B) == HIGH)//反转

    {

        count -= 1;

    }

}

  • PI控制器

int Incremental_PI(float current,float Target)   //这里对应loop函数中的VTarget_V

{  

   Bias=Target-current;                                  //计算偏差,目标值减去当前值

Last_bias=Bias;                                       //保存上一次偏差

   Float dPWM = Velocity_KP*(Bias-Last_bias)+Velocity_KI*Bias;   //增量式PI控制器计算

      return dPWM;                                           //增量输出

}

  • 定时器中断(解决在一定时间内计数的问题)

void timerIsr()   //因为这个是每隔0.05秒便会执行一次,因此不需要放在loop

{  

   V=((motor*9*PI)/(0.05*16));   //单位mm/s 测量得的脉冲数*10mm*导程9mm

   count =0;       //count需要归0,以便中断结束后的50ms内去进行重新计数

   pwm += Incremental_PI(V,Target_V);//根据当前速度去求得所需要释放的PWM

if(dPWM >250){dPWM =250;}                 //限幅250,最高为255

   if(dPWM <-250){dPWM =-250;}               //限幅  

   if(pwm <0) //用于处理目标转速为负数时情况

   {

        digitalWrite(IN1,LOW); //反转

        digitalWrite(IN2,HIGH);

        pwm = -pwm;

   }

   else

   {

        digitalWrite(IN1,HIGH); //正转

        digitalWrite(IN2,LOW);

   }

   analogWrite(PWM,pwm);

   Serial.print("Target");

   Serial.print(Target_V);

   Serial.print("V");

   Serial.println(V);   //将信息显示在命令窗口中,输出光标换行定位在下一行开头。

}

六、Arduino代码(带编码器、不引入定时器库)

以下代码是针对不选择引入库,选择在在loop循环中去实现定时器功能。

#define ENCODER_A 2                //编码器A相引脚,在uno板子中,中断号0指的是引脚2,其可以输入外部中断信号
#define ENCODER_B 3                //编码器B相引脚,在uno板子中,中断号1指的是引脚3,其可以输入外部中断信号
#define L298N_IN1 7                //IN1
#define L298N_IN2 8                //IN2
#define Motor_PWM 9                //ENA

volatile float motor=0;//中断变量,子脉冲计数
float V=0; //速度 单位cm/s
float Target_V=20;   //目标速度,单位cm/s
int PWM=0;  //用于存储通过PI控制器计算得到的用于调整电机转速的PWM值的整形变量 
float Velocity_KP =7.2, Velocity_KI =0.68,Target=0;     //Velocity_KP,Velocity_KI.PI参数  Target目标值,这里是PI控制

/*
 * Arduino初始化函数
 */
void setup() 
{
  Motor_Init();                    //电机端口初始化
  Serial.begin(9600);              //打开串口
  Serial.println("/*****开始驱动*****/"); 
}

void loop() {
  Read_Motor_V();//读取脉冲计算速度ok
  PWM= Incremental_PI(V,Target_V);//PI运算ok
  Serial.println(V);  //直接用串口绘图画出速度曲线ok
  Set_Pwm(1,PWM);  //设置方向,设置速度(两个方向都可以试试,最好能用之前那个方法,自动调整一下方向)ok
}

void Set_Pwm(int mode,int speed){ 
  if(mode==1){
  //正转模式,如果方向有问题,就换另外一个模式。ok
  digitalWrite(L298N_IN1,LOW);
  digitalWrite(L298N_IN2,HIGH);
  analogWrite(Motor_PWM,speed);
  }
  else if(mode==2){
  //反转模式
  digitalWrite(L298N_IN1,HIGH);
  digitalWrite(L298N_IN2,LOW);
  analogWrite(Motor_PWM,speed);
  }
}

/*
 * 电机端口初始化函数,控制芯片引脚全部拉低
 */
void Motor_Init(){
  //电动推杆
  pinMode(ENCODER_A,INPUT); //左轮编码器A引脚,设置为输入模式
  pinMode(ENCODER_B,INPUT); //左轮编码器B引脚,设置为输入模式
  pinMode(L298N_IN1,OUTPUT);  //设置两个驱动引脚为输出模式  
  pinMode(L298N_IN2,OUTPUT);  //
  pinMode(Motor_PWM,OUTPUT);   //设置使能引脚为输出模式

  //驱动芯片控制引脚全部拉低
  digitalWrite(L298N_IN1,LOW); 
  digitalWrite(L298N_IN2,LOW);
  digitalWrite(Motor_PWM,LOW);
}
    


/*********************************************************
 * 增量式调速算法的C语言表达
 * 函数功能:增量式PI控制器
 * 入口参数:当前速度(编码器测量值),目标速度
 * 返回值:电机PWM 
 * 参考资料: 
 *    增量式离散PID公式:
 *                Pwm-=Kp*[e(k)-e(k-1)]+Ki*e(k)+Kd*[e(k)-2e(k-1)+e(k-2)]
 *                e(k):本次偏差
 *                e(k-1):上一次偏差
 *                e(k-2):上上次偏差
 *                Pwm:代表增量输出
 *    在速度闭环控制系统里面我们只使用PI控制,因此对PID公式可简化为:
 *                Pwm-=Kp*[e(k)-e(k-1)]+Ki*e(k)
 *                e(k):本次偏差
 *                e(k-1):上一次偏差
 *                Pwm:代表增量输出
 *                Kp是让实际值保持在一个固定值左右的浮动
 *                Ki是让固定值为目标值,即消除稳态误差
 *     注意增量式PID先调I,再调P,最后再调D
/****PI控制器****/
int Incremental_PI(float current,float Target)
{  
   static float pwm,Bias,Last_bias;                       //定义全局静态浮点型变量 PWM并预设为0,Last_bias(上次偏差)并预设为0
   Bias=Target-current;                                  //计算偏差,目标值减去当前值
   pwm+=Velocity_KP*(Bias-Last_bias)+Velocity_KI*Bias;   //增量式PI控制器计算
   if(pwm>250)pwm=250;                 //限幅250,最高为255
   if(pwm<-250)pwm=-250;               //限幅  
   Last_bias=Bias;                                       //保存上一次偏差 
   return pwm;                                           //增量输出
}



/***********************************
 * 电机实际速度计算:
 * 公式:
 * 采用M法测速:n=M0/(C*T0),
 * C为商家提供的单圈总脉冲数(商家提到的是直流电机主轴旋转一圈,在霍尔传感器每个引脚是有16个脉冲信号输出)
 * T0为自己设定的时间
 * M0为设定时间内的脉冲数(测量得)
 * n单位为圈/每秒(这里求得的是角速度,要乘以半径才等于电动推杆的速度,半径商家给的是10mm?)
 *
 * 外部中断触发模式:
 *    LOW:低电平触发;
 *    CHANGE:电平变化触发;
 *    RISING :上升沿触发(由LOW变为HIGH);
 *    FALLING:下降沿触发(由HIGH变为LOW); 
 *    HIGH:高电平触发(该中断模式仅适用于Arduino due);
 *
 ***********************************/
void Read_Motor(){
  motor++;  //中断函数,读脉冲。感觉这里可以根据继续修改,即根据电平来判断是否加或者减。
}


 void Read_Motor_V(){
  unsigned long nowtime=0;
  motor=0;
  nowtime=millis()+50;//读50毫秒
  attachInterrupt(digitalPinToInterrupt(ENCODER_A),Read_Motor,RISING);(改为只触发上升沿)触发方式为CHANGE,表示电平变化就会触发,即上升沿和下降沿都触发,触发的中断函数为Read_Moto_L  。ok
  while(millis()<nowtime); //达到50毫秒关闭中断ok
  detachInterrupt(digitalPinToInterrupt(ENCODER_A));//左轮脉冲关中断计数ok
  V=((motor/16)*9*PI)/0.05;   //单位mm/s ok
 }




/****实际控制函数****
*(1)计数器方向:商家给出的那一幅图的信息是:A相上升沿触发时,B相是低电平,跟A相下降沿触发时B是高电平是一个方向。将这种将count累计为正;
*(2)电机输出方向(控制电机转速方向的接线是正着接还是反着接)
*(3)PI 控制器 里面的误差(Basi)运算是目标值减当前值(Target-Encoder),还是当前值减目标值(Encoder-Target)
*三个方向只有对应上才会有效果否则你接上就是使劲的朝着一个方向(一般来说是反方向)满速旋转***/

七、四倍频代码(即以电平变化"CHANGE"来进行计数)

/*****函数功能:外部中断读取编码器数据,具有二倍频功能 注意外部中断是跳变沿触发********/
void READ_ENCODER_L() {
  if (digitalRead(ENCODER_L) == LOW) {     //如果是下降沿触发的中断
    if (digitalRead(DIRECTION_L) == LOW)      Velocity_L--;  //根据另外一相电平判定方向
    else      Velocity_L++;
  }
  else {     //如果是上升沿触发的中断
    if (digitalRead(DIRECTION_L) == LOW)      Velocity_L++; //根据另外一相电平判定方向
    else     Velocity_L--;
  }
}
/*****函数功能:外部中断读取编码器数据,具有二倍频功能 注意外部中断是跳变沿触发********/
void READ_ENCODER_R() {
  if (digitalRead(ENCODER_R) == LOW) { //如果是下降沿触发的中断
    if (digitalRead(DIRECTION_R) == LOW)      Velocity_R++;//根据另外一相电平判定方向
    else      Velocity_R--;
  }
  else {   //如果是上升沿触发的中断
    if (digitalRead(DIRECTION_R) == LOW)      Velocity_R--; //根据另外一相电平判定方向
    else     Velocity_R++;
  }
}

八、拓展:MATLAB与Arduino连接及代码

由于自己的工作开展主要都是在Matlab上,因此在其上的工作会比较顺手,而且Matlab功能强大,算力强大,若将其与单片机结合起来,将能够实现更多的工作。

猜你喜欢

转载自blog.csdn.net/m0_56146217/article/details/127136535