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

相关文章

Spring Boot整合Redis注解实现增删改查功能(Redis注解使用)

《SpringBoot整合Redis注解实现增删改查功能(Redis注解使用)》文章介绍了如何使用SpringBoot整合Redis注解实现增删改查功能,包括配置、实体类、Repository、Se... 目录配置Redis连接定义实体类创建Repository接口增删改查操作示例插入数据查询数据删除数据更

使用python生成固定格式序号的方法详解

《使用python生成固定格式序号的方法详解》这篇文章主要为大家详细介绍了如何使用python生成固定格式序号,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以参考一下... 目录生成结果验证完整生成代码扩展说明1. 保存到文本文件2. 转换为jsON格式3. 处理特殊序号格式(如带圈数字)4

Java使用Swing生成一个最大公约数计算器

《Java使用Swing生成一个最大公约数计算器》这篇文章主要为大家详细介绍了Java使用Swing生成一个最大公约数计算器的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下... 目录第一步:利用欧几里得算法计算最大公约数欧几里得算法的证明情形 1:b=0情形 2:b>0完成相关代码第二步:加

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

k8s按需创建PV和使用PVC详解

《k8s按需创建PV和使用PVC详解》Kubernetes中,PV和PVC用于管理持久存储,StorageClass实现动态PV分配,PVC声明存储需求并绑定PV,通过kubectl验证状态,注意回收... 目录1.按需创建 PV(使用 StorageClass)创建 StorageClass2.创建 PV