目录
4.1 Arduino 与TB6612的电机编码器输出引脚的连接
一、硬件材料
1、Arduino UNO R3
图1 Arduino UNO R3
2、电机驱动板——TB6612
图2 TB6612
3、MC520编码器电机x2
图3 MC520编码器电机
4、电池
图4 电池
二、软件材料
1、Arduino IDE
官方下载链接:https://www.arduino.cc/en/Main/Software
图5 Arduino IDE
启动并配置 Arduino IDE:
图6 启动配置界面
三、驱动电机转动
通过Arduino实现电机转动,由于Arduino的输出电流不足以直接驱动电机,因此,需要通过电机驱动板(作者选用TB6612)进行放大电机控制信号。
3.1 编码器电机接口的连接
MOTOR_A连接右电机,MOTOR_B连接左电机,如图7-8所示。
图7 编码器电机与驱动板连接
图8 编码器电机与驱动板连接
3.2 Arduino与TB6612的控制信号输入引脚连接
Arduino与TB6612的引脚连接方式如图9-10所示。
Arduino UNO R3 : 13 12 11 10 9 8 7
TB6612驱动板: AIN1 STBY PWMA PWMB BIN1 BIN2 AIN2
注意:
①A表示右电机,B表示左电机。
②Arduino UNO R3只有引脚3、5、6、9、10、11具有PWM输出功能。
图9 Arduino与TB6612的引脚连接
图10 Arduino与TB6612的引脚连接
3.3 电机驱动逻辑实现
TB6612真值表如图11所示,由于A控制右电机,B控制左电机,因此想要实现小车的前进,AIN1(电机A的A相)和BIN1(电机B的A相)同时为高电压,而AIN2(电机A的B相)和BIN2(电机B的B相)同时为低电压,可实现左右电机的正转,即小车的前进。
图11 TB6612真值表
左右电机同时正转,小车前进程序如下:
图12 左右电机同时前进
先定义引脚关系,再在setup()中通过pinMode()函数设置控制信号引脚为输出(即为写:上位机写入至下位机(从上位机输出至下位机)),最后在loop中通过digitalWrite()给相应的引脚写入高低电平。
//右电机的引脚连接情况:DIR_A1 = 13控制反转、DIR_A2 =7控制正转
int DIR_A1 = 13;
int DIR_A2 = 7;
//左电机的引脚连接情况:DIR_B1 = 9控制反转、DIR_B2 = 8控制正转
int DIR_B1 = 9;
int DIR_B2 = 8;
void setup() {
// put your setup code here, to run once:
// 设置控制信号引脚为输出
pinMode(DIR_A1,OUTPUT);
pinMode(DIR_A2,OUTPUT);
pinMode(DIR_B1,OUTPUT);
pinMode(DIR_B2,OUTPUT);
}
void loop() {
// put your main code here, to run repeatedly:
//右电机正转
digitalWrite(DIR_A1,HIGH);
digitalWrite(DIR_A2,LOW);
//左电机正转
digitalWrite(DIR_B1,HIGH);
digitalWrite(DIR_B2,LOW);
}
同理,通过digitalWrite()将AIN1和BIN1同时设置为低电压,而AIN2和BIN2同时为高电压,可实现左右电机的反转,即小车的后退。
将AIN1设置为高电压,AIN2设置为低电压,使得右电机的正转;BIN1设置为低电压,BIN2设置为高电压,可实现左电机的反转,即小车的左转。
将AIN1设置为低电压,AIN2设置为高电压,使得右电机的反转;BIN1设置为高电压,BIN2设置为低电压,可实现左电机的反转,即小车的右转。
通过analogWrite() 函数调节PWM值(占空比)可控制小车不同的转速,并加入delay()延时函数,实现持续的状态。
以下程序为小车慢速正转1s后,反转1s,如此循环的实现:
//右电机的引脚连接情况:DIR_A1 = 13控制反转、DIR_A2 =7控制正转
int DIR_A1 = 13;
int DIR_A2 = 7;
int PWM_A = 11;
//左电机的引脚连接情况:DIR_B1 = 9控制反转、DIR_B2 = 8控制正转
int DIR_B1 = 9;
int DIR_B2 = 8;
int PWM_B = 10;
void setup() {
// put your setup code here, to run once:
// 设置控制信号引脚为输入
pinMode(DIR_A1,OUTPUT);
pinMode(DIR_A2,OUTPUT);
pinMode(PWM_A,OUTPUT);
pinMode(DIR_B1,OUTPUT);
pinMode(DIR_B2,OUTPUT);
pinMode(PWM_B,OUTPUT);
}
void loop() {
// put your main code here, to run repeatedly:
//右电机正转
digitalWrite(DIR_A1,HIGH);
digitalWrite(DIR_A2,LOW);
analogWrite(PWM_A,25);
//左电机正转
digitalWrite(DIR_B1,HIGH);
digitalWrite(DIR_B2,LOW);
analogWrite(PWM_B,25);
delay(1000);
//右电机反转
digitalWrite(DIR_A1,LOW);
digitalWrite(DIR_A2,HIGH);
analogWrite(PWM_A,25);
//左电机反转
digitalWrite(DIR_B1,LOW);
digitalWrite(DIR_B2,HIGH);
analogWrite(PWM_B,25);
delay(1000);
}
四、实现电机的脉冲计数
4.1 Arduino 与TB6612的电机编码器输出引脚的连接
要实现输出电机的脉冲计数,需要知道Arduino 与TB6612的电机编码器输出引脚的对应关系。
并且在编码器数据输出中需要应用中断函数,因此需要知道中断引脚。
以下为作者设置的编码器输出引脚对应关系:
Arduino UNO R3 : 2 4 3 5
TB6612驱动板: E1A E1B E2A E2B
注意:
①Arduino UNO R3只有引脚2、3为中断引脚。
② E1A表示右电机的A相编码器,E1B表示右电机的B相编码器;E2A表示左电机的A相编码器,E2B表示左电机的B相编码器;
图13 编码器输出引脚对应关系
图14 AB相传感器
4.2 脉冲计数原理
作者使用的此编码器电机为增量式编码器,将位移转换成周期性的电信号,再把这个电信号转变成计数脉冲,用脉冲的个数表示位移的大小。
该编码器分为A相与B相,也称为AB相编码器。每一相每转过单位的角度就发出一个脉冲信号(一圈可以发出N个脉冲信号),A相、B相为相互延迟1/4周期的脉冲输出,根据延迟关系可以区别正反转,而且通过取A相、B相的上升和下降沿可以进行单频或2倍频或4倍频测速,如图15所示。
图15 正反转时AB相电平对应的状态
统计方波/脉冲个数:
①单倍频计数:一个脉冲统计一次
②双倍频计数:一个脉冲统计两次
③四倍频计数:两个脉冲统计四次
即:单倍、双倍频计数都是只计A相的脉冲,所以是一个脉冲。并且如果只是统计一次电平的突变则为单倍频计数,统计两次次电平的突变则为双倍频计数。而四倍频计数是计了A、B相的脉冲,为两个脉冲,并分别统计了两个相的两次突变。
用计数器count来计数电平的突变,当是正转时,count++,而反转时,count--,并且由A相电平突变时对应B相此时的电平来判断正反转。
脉冲计数实现逻辑思路:
定义引脚关系,并初始化两电机的计数变量,设置串口通信的波特率,在setup()中通过pinMode()函数将编码器输出引脚设置为输入模式(即为读:上位机从下位机读取(从下位机输入至上位机)),并设置中断函数attachInterrupt(),编写中断函数,这是关键!最后在loop()中打印输出计数。
在中断函数中,需要做出判断,当电机编码器A相的电平等于B相的电平时,count++,计数加一,即为正转;否则,count--,计数减一,即为反转。详见以下程序。
补充中断函数attachInterrupt()语法:
attachInterrupt(interrupt, function, mode)
interrupt:中断引脚数
function:中断发生时调用的函数,此函数必须不带参数和不返回任何值。该函数有时被称为中断服务程序。
mode:定义何时发生中断以下四个contstants预定有效值:
-
LOW 当引脚为低电平时,触发中断
-
CHANGE 当引脚电平发生改变时,触发中断
-
RISING 当引脚由低电平变为高电平时,触发中断
-
FALLING 当引脚由高电平变为低电平时,触发中断.
#define E1A 2 // 电机 A 编码器 A 相
#define E1B 4 // 电机 A 编码器 B 相
#define E2A 3 // 电机 B 编码器 A 相
#define E2B 5 // 电机 B 编码器 B 相
volatile int countA = 0; // 电机 A 的脉冲计数
volatile int countB = 0; // 电机 B 的脉冲计数
//右电机的引脚连接情况:DIR_A1 = 13控制反转、DIR_A2 =7控制正转
int DIR_A1 = 13;
int DIR_A2 = 7;
int PWM_A = 11;
//左电机的引脚连接情况:DIR_B1 = 9控制反转、DIR_B2 = 8控制正转
int DIR_B1 = 9;
int DIR_B2 = 8;
int PWM_B = 10;
void setup() {
// put your setup code here, to run once:
Serial.begin(57600);
// 设置编码器引脚为输入
pinMode(E1A, INPUT);
pinMode(E1B, INPUT);
pinMode(E2A, INPUT);
pinMode(E2B, INPUT);
// 设置控制信号引脚为输入
pinMode(DIR_A1,OUTPUT);
pinMode(DIR_A2,OUTPUT);
pinMode(PWM_A,OUTPUT);
pinMode(DIR_B1,OUTPUT);
pinMode(DIR_B2,OUTPUT);
pinMode(PWM_B,OUTPUT);
// 设置中断
attachInterrupt(digitalPinToInterrupt(E1A), handleMotorA, RISING);
attachInterrupt(digitalPinToInterrupt(E2A), handleMotorB, RISING);
}
void loop() {
// put your main code here, to run repeatedly:
// 打印编码器计数
Serial.print("Motor A Count: ");
Serial.println(countA);
Serial.print("Motor B Count: ");
Serial.println(countB);
// delay(1000); // 每秒打印一次
//右电机正转
digitalWrite(DIR_A1,HIGH);
digitalWrite(DIR_A2,LOW);
analogWrite(PWM_A,25);
//左电机正转
digitalWrite(DIR_B1,HIGH);
digitalWrite(DIR_B2,LOW);
analogWrite(PWM_B,25);
delay(1000);
//右电机反转
digitalWrite(DIR_A1,LOW);
digitalWrite(DIR_A2,HIGH);
analogWrite(PWM_A,25);
//左电机反转
digitalWrite(DIR_B1,LOW);
digitalWrite(DIR_B2,HIGH);
analogWrite(PWM_B,25);
delay(1000);
}
// 中断服务程序:处理电机 A 的编码器信号
void handleMotorA() {
if (digitalRead(E1A) == digitalRead(E1B)) {
countA++; // 正转
} else {
countA--;
}
}
// 中断服务程序:处理电机 B 的编码器信号
void handleMotorB() {
if (digitalRead(E2A) == digitalRead(E2B)) {
countB--; // 正转
} else {
countB++;
}
}
打开串口监视器,设置波特率,即可观察到输出的脉冲计数(还是有点偏差的):
图16 脉冲计数-串口监视器
五、实现电机的测速
有了脉冲计数的基础后,只需要将脉冲计数转换成为速度即可实现测速。
主要关注中断的实现。
转速逻辑思路:
需要一个开始时间(用于记录每个测速周期的开始时刻),还需要定义一个时间区间(比如50毫秒),实时的获取当前时刻,当当前时刻 - 上传结束时刻 >= 时间区间时,就获取当前计数并根据测速公式计算时时速度,计算完毕,计数器归零,重置开始时间。
当使用中断函数中的变量时,需要先禁止中断noInterrupts(),调用完毕,再重启中断interrupts()
#define E1A 2 // 电机 A 编码器 A 相
#define E1B 4 // 电机 A 编码器 B 相
#define E2A 3 // 电机 B 编码器 A 相
#define E2B 5 // 电机 B 编码器 B 相
volatile int countA = 0; // 电机 A 的脉冲计数
volatile int countB = 0; // 电机 B 的脉冲计数
//右电机的引脚连接情况:DIR_A1 = 13控制反转、DIR_A2 =7控制正转
int DIR_A1 = 13;
int DIR_A2 = 7;
int PWM_A = 11;
//左电机的引脚连接情况:DIR_B1 = 9控制反转、DIR_B2 = 8控制正转
int DIR_B1 = 9;
int DIR_B2 = 8;
int PWM_B = 10;
int reducation = 90;//减速比,根据电机参数设置,比如 15 | 30 | 60
int pulse = 11; //编码器旋转一圈产生的脉冲数该值需要参考商家电机参数
int per_round = pulse * reducation * 4;//一圈输出的脉冲数×减速比×4倍频
long start_time = millis();//一个计算周期的开始时刻,初始值为 millis(); 获取开始的时间
long interval_time = 50;//一个计算周期 50ms //单位时间
double current_velA;
double current_velB;
void setup() {
// put your setup code here, to run once:
Serial.begin(57600);
// 设置编码器引脚为输入
pinMode(E1A, INPUT);
pinMode(E1B, INPUT);
pinMode(E2A, INPUT);
pinMode(E2B, INPUT);
// 设置控制信号引脚为输入
pinMode(DIR_A1,OUTPUT);
pinMode(DIR_A2,OUTPUT);
pinMode(PWM_A,OUTPUT);
pinMode(DIR_B1,OUTPUT);
pinMode(DIR_B2,OUTPUT);
pinMode(PWM_B,OUTPUT);
// 设置中断
attachInterrupt(digitalPinToInterrupt(E1A), handleMotorA, RISING);
attachInterrupt(digitalPinToInterrupt(E2A), handleMotorB, RISING);
}
void loop() {
// put your main code here, to run repeatedly:
// 打印编码器计数
Serial.print("Motor A Count: ");
Serial.println(countA);
Serial.print("Motor B Count: ");
Serial.println(countB);
// delay(1000); // 每秒打印一次
//打印转速
// get_current_vel();
//右电机正转
digitalWrite(DIR_A1,HIGH);
digitalWrite(DIR_A2,LOW);
analogWrite(PWM_A,50);
//左电机正转
digitalWrite(DIR_B1,HIGH);
digitalWrite(DIR_B2,LOW);
analogWrite(PWM_B,50);
delay(1000);
//右电机反转
digitalWrite(DIR_A1,LOW);
digitalWrite(DIR_A2,HIGH);
analogWrite(PWM_A,50);
//左电机反转
digitalWrite(DIR_B1,LOW);
digitalWrite(DIR_B2,HIGH);
analogWrite(PWM_B,50);
delay(1000);
}
// 中断服务程序:处理电机 A 的编码器信号
void handleMotorA() {
if (digitalRead(E1A) == digitalRead(E1B)) {
countA++; // 正转
} else {
countA--;
}
}
// 中断服务程序:处理电机 B 的编码器信号
void handleMotorB() {
if (digitalRead(E2A) == digitalRead(E2B)) {
countB--; // 正转
} else {
countB++;
}
}
//获取当前转速的函数
void get_current_vel(){
long right_now = millis();
long past_time = right_now - start_time;//计算逝去的时间
if(past_time >= interval_time){//如果逝去时间大于等于一个计算周期
//1.禁止中断
noInterrupts();
//2.计算转速 转速单位可以是秒,也可以是分钟... 自定义即可
current_velA = (double)countA / per_round / past_time * 1000 * 60;
current_velB = (double)countB / per_round / past_time * 1000 * 60;
//3.重置计数器
countA = 0;
countB = 0;
//4.重置开始时间
start_time = right_now;
//5.重启中断
interrupts();
Serial.println(current_velA);
Serial.println(current_velB);
}
}
打开串口监视器可看到打印的转速(由于反转的持续时间长,所以转速为负数)
图17 电机测速-串口监视器
Arduino IDE中还提供了串口绘图器:
图18 电机测速-串口绘图器
六、PID调速
6.1 PID介绍
偷个小懒,就不敲了(滑稽)
图19 PID介绍
6.2 下载PID库
Arduino-PID-Library
在 GitHub 下载 PID 库: git clone GitHub - br3ttb/Arduino-PID-Library
然后将该文件夹移动到 arduino 的 libraries下
需要把中划线去掉,即重命名Arduino-PID-Library 为ArduinoPIDLibrary
最后重启 ArduinoIDE
满足下图即安装完成。
图20 PID安装
6.3 PID调速
PID 调速实现:
使用之前电机控制以及测速代码,并且包含PID头文件,创建PID对象,在setup中启用自动调试,用Compute()输出控制电机的 PWM 值并更新PWM。
PID头文件:
#include <PID_v1.h>
创建PID对象:
获取当前转速来源于之前实现的测速。
设定目标转速target。
在PID中会根据当前转速和设定的目标转速,不断实时的调整计数PWM值,并将计算出来的PWM值赋值给电机相应的PWM值,实现速度的调节。
调参:Kp(比例环节)、Ki(积分环节)、Kd(微分环节)
PID的参数过程是需要细心仔细的微调哦
//创建 PID 对象
//1.当前转速 ; 2.计算输出的pwm ; 3.目标转速 ; 4.kp ; 5.ki ; 6.kd ; 7.DIRECT/REVERSE(后者的P、I、D值会取反) 当输入与目标值出现偏差时,向哪个方向控制
double pwmA;//右电机A驱动的PWM值
double pwmB;//左电机B驱动的PWM值
double target = 10;
double Kp=5.0, Ki=5.0, Kd=0.08;
PID pidA(¤t_velA,&pwmA,&target,Kp,Ki,Kd,DIRECT);
PID pidB(¤t_velB,&pwmB,&target,Kp,Ki,Kd,DIRECT);
完整程序如下:
注:A表示右电机,B表示左电机
/*
* PID 调速实现:
* 1.代码准备,复制并修改电机控制以及测速代码
* 2.包含PID头文件
* 3.创建PID对象
* 4.在setup中启用自动调试
* 5.调用Compute()输出控制电机的 PWM 值并更新PWM
*
*/
//右电机表示A或1
//左电机表示B或2
#include <PID_v1.h>
#define E1A 2 // 右电机 A 编码器 A 相
#define E1B 4 // 右电机 A 编码器 B 相
#define E2A 3 // 左电机 B 编码器 A 相
#define E2B 5 // 左电机 B 编码器 B 相
int DIR_A1 = 13;//右电机A的A相-接13引脚
int DIR_A2 = 7;//右电机A的B相-接7引脚
int PWM_A = 11; //右电机A的PWM值
int DIR_B1 = 9;//左电机B的A相-接9引脚
int DIR_B2 = 8;//左电机B的B相-接8引脚
int PWM_B = 10; //左电机B的PWM值
volatile int countA = 0;//左电机B的计数
volatile int countB = 0;//右电机A的计数
// 中断服务程序:处理右电机 A 的编码器信号
//如果右电机1的A相等于右电机1的B相,则countA++计数一次;否则--。
void handleMotorA() {
if (digitalRead(E1A) == digitalRead(E1B)) {
countA++; // 正转
} else {
countA--;
}
}
// 中断服务程序:处理左电机 A 的编码器信号
//如果左电机2的A相不等于左电机2的B相,则countB++计数一次;否则--。(注意:因为与右电机的计数方向相反,这里手动调整了一下)
void handleMotorB() {
if (digitalRead(E2A) == digitalRead(E2B)) {
countB--; // 正转
} else {
countB++;
}
}
//定义"计数"换算"转速"的一些计算参数
int reducation = 90;//减速比,根据电机参数设置,比如 15 | 30 | 60
int pulse = 11; //编码器旋转一圈产生的脉冲数,该值需要参考商家电机参数
int per_round = pulse * reducation * 4;//车轮旋转一圈产生的脉冲数 *减速比*N倍频(1 2 4)
long start_time = millis();//一个计算周期的开始时刻,初始值为 millis();
long interval_time = 50;//一个计算周期 50ms
double current_velA; //定义双浮点数 current_velA来接收右电机A由countA转换后的转速
double current_velB; //定义双浮点数 current_velB来接收左电机B由countB转换后的转速
//获取当前转速的函数
void get_current_vel(){
long right_now = millis(); //获取当前时刻
long past_time = right_now - start_time;//计算逝去的时间:当前时刻-开始时刻
if(past_time >= interval_time){//如果逝去时间大于等于一个计算周期,则进入计算转速过程
//1.禁止中断
noInterrupts();
//2.计算转速 转速单位可以是秒,也可以是分钟... 自定义即可。
//用计数(double(确保count非零))count除以[车轮旋转一圈产生的脉冲数 *减速比*N倍频(1 2 4)]per_round,再除以持续的时间past_time,再分别除以1000和60将单位化成m/s
current_velA = (double)countA / per_round / past_time * 1000 * 60;
current_velB = (double)countB / per_round / past_time * 1000 * 60;
delay(1000); //延时1s再打印
// Serial.println(start_time); //可打印开始时刻
// Serial.println(right_now); //可打印当前时刻
Serial.println(current_velA); //打印转速
Serial.println(current_velB);
//3.重置计数器
countA = 0;
countB = 0;
//4.重置开始时间
start_time = right_now;
//5.重启中断
interrupts();
}
}
//-------------------------------------PID-------------------------------------------
//创建 PID 对象
//1.当前转速 ; 2.计算输出的pwm ; 3.目标转速 ; 4.kp ; 5.ki ; 6.kd ; 7.DIRECT/REVERSE(后者的P、I、D值会取反) 当输入与目标值出现偏差时,向哪个方向控制
double pwmA;//右电机A驱动的PWM值
double pwmB;//左电机B驱动的PWM值
double target = 10;
//double Kp=4.5, Ki=5.0, Kd=0.1;
double Kp=5.0, Ki=5.0, Kd=0.08;
PID pidA(¤t_velA,&pwmA,&target,Kp,Ki,Kd,DIRECT);
PID pidB(¤t_velB,&pwmB,&target,Kp,Ki,Kd,DIRECT);
//速度更新函数
void update_vel(){
//获取当前速度
get_current_vel();
// 对当前速度进行平滑处理
// static double smoothed_velA = 0;
// smoothed_velA = 0.9 * smoothed_velA + 0.1 * current_velA; // 低通滤波
//右电机A
pidA.Compute();//计算需要输出右电机A的PWM
pwmA = constrain(pwmA, 0, 255); // 限制PWM值,防止控制反应过激
digitalWrite(DIR_A1,HIGH); //右电机A的A相设置为HIGH,右电机A的B相设置为LOW,实现正转
digitalWrite(DIR_A2,LOW);
analogWrite(PWM_A,pwmA); //将 pidA.Compute()计算输出的pwmA值给右电机的PWM_A值
//左电机B
pidB.Compute();//计算需要输出左电机B的PWM
pwmB = constrain(pwmB, 0, 255); // 限制PWM值,防止控制反应过激
digitalWrite(DIR_B1,HIGH); //左电机B的A相设置为HIGH,左电机B的B相设置为LOW,实现正转
digitalWrite(DIR_B2,LOW);
analogWrite(PWM_B,pwmB); //将 pidB.Compute()计算输出的pwmB值给右电机的PWM_B值
}
void setup() {
Serial.begin(57600);//设置波特率
pinMode(E1A, INPUT);
pinMode(E1B, INPUT);
pinMode(E2A, INPUT);
pinMode(E2B, INPUT);
//两个电机驱动引脚都设置为 OUTPUT
pinMode(DIR_A1,OUTPUT);
pinMode(DIR_A2,OUTPUT);
pinMode(PWM_A,OUTPUT);
pinMode(DIR_B1,OUTPUT);
pinMode(DIR_B2,OUTPUT);
pinMode(PWM_B,OUTPUT);
attachInterrupt(digitalPinToInterrupt(E1A),handleMotorA,RISING);//当电平发生改变时触发中断函数
//四倍频统计需要为B相也添加中断
attachInterrupt(digitalPinToInterrupt(E2A),handleMotorB,RISING);
//启用PID自动控制
pidA.SetMode(AUTOMATIC);
pidB.SetMode(AUTOMATIC);
}
void loop() {
//循环调用update_vel()去获取当前转速,并通过PID调速
delay(10);
update_vel();
}
将串口绘图器打开,可以很清楚的看到,PID努力调节的样子。
当然,这也需要你——调参侠的努力!
图21 PID控制-串口绘图器
欢迎大家讨论交流学习!