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

2023-10-18 23:50

本文主要是介绍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算法简介

这篇关于Arduino三轮全向小车(二):编码马达的使用的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/235892

相关文章

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows

使用Python和Pyecharts创建交互式地图

《使用Python和Pyecharts创建交互式地图》在数据可视化领域,创建交互式地图是一种强大的方式,可以使受众能够以引人入胜且信息丰富的方式探索地理数据,下面我们看看如何使用Python和Pyec... 目录简介Pyecharts 简介创建上海地图代码说明运行结果总结简介在数据可视化领域,创建交互式地

Java Stream流使用案例深入详解

《JavaStream流使用案例深入详解》:本文主要介绍JavaStream流使用案例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录前言1. Lambda1.1 语法1.2 没参数只有一条语句或者多条语句1.3 一个参数只有一条语句或者多

Java Spring 中 @PostConstruct 注解使用原理及常见场景

《JavaSpring中@PostConstruct注解使用原理及常见场景》在JavaSpring中,@PostConstruct注解是一个非常实用的功能,它允许开发者在Spring容器完全初... 目录一、@PostConstruct 注解概述二、@PostConstruct 注解的基本使用2.1 基本代

C#使用StackExchange.Redis实现分布式锁的两种方式介绍

《C#使用StackExchange.Redis实现分布式锁的两种方式介绍》分布式锁在集群的架构中发挥着重要的作用,:本文主要介绍C#使用StackExchange.Redis实现分布式锁的... 目录自定义分布式锁获取锁释放锁自动续期StackExchange.Redis分布式锁获取锁释放锁自动续期分布式

springboot使用Scheduling实现动态增删启停定时任务教程

《springboot使用Scheduling实现动态增删启停定时任务教程》:本文主要介绍springboot使用Scheduling实现动态增删启停定时任务教程,具有很好的参考价值,希望对大家有... 目录1、配置定时任务需要的线程池2、创建ScheduledFuture的包装类3、注册定时任务,增加、删

使用Python实现矢量路径的压缩、解压与可视化

《使用Python实现矢量路径的压缩、解压与可视化》在图形设计和Web开发中,矢量路径数据的高效存储与传输至关重要,本文将通过一个Python示例,展示如何将复杂的矢量路径命令序列压缩为JSON格式,... 目录引言核心功能概述1. 路径命令解析2. 路径数据压缩3. 路径数据解压4. 可视化代码实现详解1

Pandas透视表(Pivot Table)的具体使用

《Pandas透视表(PivotTable)的具体使用》透视表用于在数据分析和处理过程中进行数据重塑和汇总,本文就来介绍一下Pandas透视表(PivotTable)的具体使用,感兴趣的可以了解一下... 目录前言什么是透视表?使用步骤1. 引入必要的库2. 读取数据3. 创建透视表4. 查看透视表总结前言

Python 交互式可视化的利器Bokeh的使用

《Python交互式可视化的利器Bokeh的使用》Bokeh是一个专注于Web端交互式数据可视化的Python库,本文主要介绍了Python交互式可视化的利器Bokeh的使用,具有一定的参考价值,感... 目录1. Bokeh 简介1.1 为什么选择 Bokeh1.2 安装与环境配置2. Bokeh 基础2

Android使用ImageView.ScaleType实现图片的缩放与裁剪功能

《Android使用ImageView.ScaleType实现图片的缩放与裁剪功能》ImageView是最常用的控件之一,它用于展示各种类型的图片,为了能够根据需求调整图片的显示效果,Android提... 目录什么是 ImageView.ScaleType?FIT_XYFIT_STARTFIT_CENTE