Arduino三轮全向小车(二):编码马达的使用

上一篇文章介绍了编码马达的原理以及特性,本篇话不多说,直接开始实用。

马达控制

观察以下编码马达的接口图片:(原谅我这样拍照,实在懒得把马达拆下来了)

总共六根线,其中两根是控制马达转速以及转向的,也就是说和普通马达完全相同,你需要找到这两根线,应该会有注明,例如我的马达上标注为:马达线1马达线2。将这两根线接入马达驱动板,这里我推荐TB6612模块,小巧又强大,也不算很贵,但是使用时一定要小心,我买回来的第一个,刚一接线就烧了(不怪人家,是我把12v接上了5v的口)。

关于TB6612的使用,这里简单介绍一下,马达的两根线应该接入AO1和AO2(或BO1和BO2),PWMA(或PWMB)接入Arduino的某一个PWM端口,这个是管马达速度的。UNO的PWM口其实蛮少的,做三轮车很吃紧,我直接用了Mega。AIN1和AIN2(或BIN1和BIN2)分别接入Arduino的非PWM的数字口,这两个是管马达转向的。剩下的口一定要注意,千万别接错,否则就像我一样,VM和随便一个GND分别接大电源的正负极,大电源电压尽量不要高于12V,不要低于8V,最好用个稳压模块。VCC和另外一个GND口分别接入Arduino的+5v和GND口。STBY可以直接接一个+5V口,也可以接入数字口然后给高电平,这是使能端。

ok,终于把这个复杂的接口讲完了,其实这一部分普通马达和编码马达都是一样的,代码也都一样,下面是我的习惯写法:

void MotorControl(float sp){
	if (sp > 0) {
    	digitalWrite(MOTOR_PIN1, HIGH);
    	digitalWrite(MOTOR_PIN2, LOW);
    	analogWrite(MOTOR_SP, sp);
  	} else if (sp < 0) {
    	digitalWrite(MOTOR_PIN1, LOW);
    	digitalWrite(MOTOR_PIN2, HIGH);
    	analogWrite(MOTOR_SP, -1 * sp);
  	} else {
    	digitalWrite(MOTOR_PIN1, LOW);
    	digitalWrite(MOTOR_PIN2, LOW);
  	}
}

其中MOTOR_SP是PWM口,而其他两个则为控制转向的接口。这些都和普通马达一样,也很好理解,这里不再详细解释。
推荐你把它写成个头文件放库里,反正只要是做小车,都会用到它。

读取码盘数据

接下来这部分才是使用编码马达的“重头戏”:读取码盘的数据。根据之前我们理解的码盘的原理,我们只需要测量A相和B相的频率以及相位差,即可测算出马达的转速和转向。马达的另外四根线,一根是A相,一根是B相,分别接入Arduino的中断端口(Mega的中断口有2, 3, 18, 19, 20, 21;UNO只有2,3,如果是三轮车,看上去只能用Mega了)和其他数字口,另外两根则是编码器的5V和GND,接Arduino的5V和GND即可。

实际上码盘返回的是计数,频率需要自己测算,这就涉及到Arduino的一个功能:中断。就好比你正在你家看电影,做着自己的事情,突然你朋友敲门来找你玩,于是你暂停电影,去给朋友开门并把他打发走,又回来继续看电影。这就是中断。Arduino可以运行中,因为某些触发时间而暂停,然后去执行其他任务,执行结束后在回来继续当前的程序。

Arduino中有一个函数是

attachInterrupt(digitalPinToInterrupt(pin),ISR,mode);

其中各参数分别为:

  • digitalPinToInterrupt(pin):中断端口号(并不是真正的端口号)
  • ISR:中断时要执行的程序
  • mode:有以下三种值:
    • LOW:在引脚为低电平时触发
    • HIGH:在引脚为高电平时触发
    • CHANGE:在引脚电平状态改变时触发

如果学过数电,可以结合触发器触发条件的知识理解一下上面的触发是怎么个原理。

这里有个很狗的一点是,中断端口号并不是真正的引脚编号,也就是你插了Mega的19引脚,实际上你应该写4号,对应关系如下:
引脚对应
或者也可以直接写digitalPinToInterrupt(19),但无形中多写了好多哇…(狗头)

我们的思路是,当A相的引脚发生变化时(CHANGE),中断主程序,根据我们上一篇文章所说的原理:

当A相上行时可以看到B相是高电平,我们定义这时电机为正转,反转时,它们的相位差是原来的相反数,也就是负九十度。这时,A相上行时B相为低电平,这样即可简单地判断出电机转动方向。

也就是说,要判断此时电机的转向,我们需要先判断A相是上行还是下行,之后在判断此时B相是高电平还是低电平,这个其实很好判断,几个if完事。当我们判断出电机是正转时,我们就让计数变量加1,证明码盘扫过一格,否则减1,即反转扫过一格。代码如下:

void Count() {
  if (digitalRead(pinA) == LOW) {
    if (digitalRead(pinB) == HIGH) pps++;
    else if (digitalRead(pinB) == LOW) pps--;
  } else {
    if (digitalRead(pinB) == HIGH) pps--;
    else if (digitalRead(pinB) == LOW) pps++;
  }
}

其中pps即为计数变量,它的含义其实就是码盘转过的格数,如果你知道码盘一圈有几格(比如我的电机是390,一般这个参数会被称为精度),那你就可以算出目前电机转过的角度。

注意把attachInterrupt()放在setup里,比如我的A相接了19,对应的中断口号为4,则我就在setup里这么写:

attachInterrupt(4, Count1, CHANGE);

好了,至此你就已经可以顺利读出码盘数据了,不过不知道你有没有发现这个问题,有了码盘转动的数据,我们也只能算出位移,如何计算速度

在这里,我们使用另外一种中断:定时中断,也就是每隔一段时间,我就中断主函数,去算一下速度和位移,这会用到一个库:MsTimer(这个库可能需要在网上下载导入,直接百度一波,找不到可以给我留言,留下qq号我发你)。该库中有一个函数是这样的:

MsTimer2::set(t, SpeedDetection);

其中,t为时间间隔,也就是每隔这么多时间中断一次,第二个参数为中断函数名,也就是中断后去执行该函数的内容。看名字我已经取好了:SpeedDetection,速度检测,顾名思义这个函数就是测速度的。

测速度的方法也很简单,我们先用两次间隔的pss之差计算出电机转角,再用该转角除以时间间隔t,即可求出角速度。

float SpeedDetection() {
  detachInterrupt(4);	//先停止另一种中断,以免中断套中断(禁止套娃)
  velocity = (float)pps * (1000 / t) / 780.0;	//测量速度
  m += pps * (1000 / t) * 360 / 780;			//测量转角
    //    Serial.print("Omega:");
    //    Serial.print(velocity);
    //    Serial.print("r/s  Theta:");
    //    Serial.print(m);
    //    Serial.print("°  ");//串口输出调试信息
  }
  pps = 0;		//每次该函数最后将pps清零。
  attachInterrupt(4, Count1, CHANGE);	//不要忘了再打开另一种中断。
}

该函数中的速度和转角计算公式正是考验你数学功底的时候,值得注意的是,虽然我的电机精度为390(也就是码盘一周有390个豁),但是我们在每次A相电平状态改变时,不管上行还是下行都会记一次数,相当于一个豁口我们计两次数,因此真正的精度其实是780。这是在计算时应该注意的。其他没有什么难理解的地方了。

当然之后还需要在setup里写:

MsTimer2::set(t, SpeedDetection);
MsTimer2::start();		//计时开始的意思

OK,大功告成,接下来我们就可以利用起这些数据做些闭环控制算法了,当然最常见的就是PID,这个我留到下一篇文章讲(有可能又会鸽好久,很尴尬,太忙了)。

下面附上整个源码(三轮版本)

#include <motor.h>		//这是我自己写的,在github上https://github.com/mond538/motor
#include <PinChangeInt.h>	//引脚中断库
#include <MsTimer2.h>
//-------------端口定义---------------
#define pinA_1 19			//A相
#define pinB_1 29			//B相
#define MOTOR1_SP 13		//PWM端口
#define MOTOR1_PIN1 27		//IN1
#define MOTOR1_PIN2 28		//IN2

#define pinA_2 2
#define pinB_2 23
#define MOTOR2_SP 11
#define MOTOR2_PIN1 24
#define MOTOR2_PIN2 25

#define pinA_3 3
#define pinB_3 26
#define MOTOR3_SP 12
#define MOTOR3_PIN1 30
#define MOTOR3_PIN2 31
//------------------------------------

motor Motor1(pinA_1, pinB_1, MOTOR1_SP, MOTOR1_PIN1, MOTOR1_PIN2);
motor Motor2(pinA_2, pinB_2, MOTOR2_SP, MOTOR2_PIN1, MOTOR2_PIN2);
motor Motor3(pinA_3, pinB_3, MOTOR3_SP, MOTOR3_PIN1, MOTOR3_PIN2);
//motor类是我自己在motor.h中定义的类,参数均为端口。
volatile long m[3] = {0, 0, 0};
int pps[3] = {0, 0, 0};
float velocity[3] = {0, 0, 0};
int t = 50;

int adjust(int sp) {
	//限制马达最大和最小速度。
  int max_sp = 150;
  if (sp >= max_sp) sp = max_sp;
  else if (sp <= -1 * max_sp) sp = -1 * max_sp;
  if (sp >= -10 && sp <= 10) sp = 0;
  return sp;
}

float SpeedDetection() {
  detachInterrupt(4);
  detachInterrupt(0);
  detachInterrupt(1);
  for (int i = 0; i < 3; i++) {
    velocity[i] = (float)pps[i] * (1000 / t) / 780.0;
    m[i] += pps[i] * (1000 / t) * 360 / 780;
    //    Serial.print("M");
    //    Serial.print(i + 1);
    //    Serial.print(": ");
    //    Serial.print(velocity[i]);
    //    Serial.print("r/s ");
    //    Serial.print(m[i]);
    //    Serial.print("°  ");
  }
  for (int i = 0; i < 3; i++)  pps[i] = 0;
  attachInterrupt(4, Count1, CHANGE);
  attachInterrupt(0, Count2, CHANGE);
  attachInterrupt(1, Count3, CHANGE);
}

void setup() {
  pinMode(pinA_1, INPUT);
  pinMode(pinB_1, INPUT);
  pinMode(MOTOR1_SP, OUTPUT);
  pinMode(MOTOR1_PIN1, OUTPUT);
  pinMode(MOTOR1_PIN2, OUTPUT);

  pinMode(pinA_2, INPUT);
  pinMode(pinB_2, INPUT);
  pinMode(MOTOR2_SP, OUTPUT);
  pinMode(MOTOR2_PIN1, OUTPUT);
  pinMode(MOTOR2_PIN2, OUTPUT);

  pinMode(pinA_3, INPUT);
  pinMode(pinB_3, INPUT);
  pinMode(MOTOR3_SP, OUTPUT);
  pinMode(MOTOR3_PIN1, OUTPUT);
  pinMode(MOTOR3_PIN2, OUTPUT);

  Serial.begin(9600);

  attachInterrupt(4, Count1, CHANGE);
  attachInterrupt(0, Count2, CHANGE);
  attachInterrupt(1, Count3, CHANGE);
  
  MsTimer2::set(t, SpeedDetection);
  MsTimer2::start();
}

void loop() {
  MotorControlTri(1, 1, 1);
  delay(t);
  //随便跑一跑做调试
}

void MotorControlTri(float sp1, float sp2, float sp3) {
  sp1 = adjust(sp1);
  sp2 = adjust(sp2);
  sp3 = adjust(sp3);
  Motor1.MotorControl(sp1);
  Motor2.MotorControl(sp2);
  Motor3.MotorControl(sp3);
  //该MotorControl方法也是在我自己写的库函数中,参数为速度,正负代表方向
}

void Count1() {
  if (digitalRead(pinA_1) == LOW) {
    if (digitalRead(pinB_1) == HIGH) pps[0]++;
    else if (digitalRead(pinB_1) == LOW) pps[0]--;
  } else {
    if (digitalRead(pinB_1) == HIGH) pps[0]--;
    else if (digitalRead(pinB_1) == LOW) pps[0]++;
  }
}
void Count2() {
  if (digitalRead(pinA_2) == LOW) {
    if (digitalRead(pinB_2) == HIGH) pps[1]++;
    else if (digitalRead(pinB_2) == LOW) pps[1]--;
  } else {
    if (digitalRead(pinB_2) == HIGH) pps[1]--;
    else if (digitalRead(pinB_2) == LOW) pps[1]++;
  }
}
void Count3() {
  if (digitalRead(pinA_3) == LOW) {
    if (digitalRead(pinB_3) == HIGH) pps[2]++;
    else if (digitalRead(pinB_3) == LOW) pps[2]--;
  } else {
    if (digitalRead(pinB_3) == HIGH) pps[2]--;
    else if (digitalRead(pinB_3) == LOW) pps[2]++;
  }
}

下一篇:PID算法简介

评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值