Arduino智能小车——小车测速
准备材料
1. 测速模块
2.
3. 固定铜柱
a) 建议铜柱长度30CM,大小为M3
b) 由于铜柱导电,有些电路板如果固定孔设计不当的话很容易导致电路板烧坏,尤其是在以后的项目中可能会用到更高的电压,因此在这里我建议大家可以准备一些尼龙柱来固定电路板。尼龙柱的尺寸跟铜柱尺寸相同M3,30CM。大家也可以顺便买一点尼龙螺丝。
c)
4. 准备杜邦线
5. 驱动模块的安装需要由尼龙柱支撑
6.
7. 驱动模块安装时需要注意,不能影响轮子的正常工作,不能触碰到轮轴上的码盘。
编码器上有三个引脚分别是“VCC”,“GND”,“OUT”。左右两边两个测速模块的“VCC”引脚接电源或开发板的“5V”或“3.3V”引脚,“GND”接电源或开发板的“GND”引脚,左边测速模块“OUT”接开发板的“3”引脚,右边测速模块“OUT”接开发板的“2”引脚。引脚接错的话可以再随后调试过程中换过来,也可以在代码里更改。
8.
9. 测速模块的工作原理比较简单,如下图所示,在于电机同轴的码盘上有很多开孔(光栅),编码器相当于光敏元件。码盘随着小车轮子的运动转动时,码盘(光栅)会不断遮挡光敏元件发出的光波,这时候编码器就会根据光栅的遮挡不断的产生方波信号,方波信号会从“OUT”引脚输出,我们只需不断检测“OUT”引脚的输出,根据方波信号的周期简介计算出小车运行的速度。小车上使用的码盘(光栅)精度不高,在某些高精度的编码器上光栅会更加密集,测量效果会更好。
10.
11. 我们使用的Arduino UNO只有“2”,“3”引脚可以触发外部中断,因此在接线的时候我们便将左右两边的输出“OUT”引脚分别接在“2”“3”引脚上。
12. 在程序初始化阶段中调用函数attachInterrupt(interrupt,function, mode)可以对中断引脚初始化,其中
13. interrupt:要初始化的外部中断编号,由上表可知我们Arduino UNO只能使用外部中断0和外部中断1;
14. function:中断服务函数的名字,即当外部中断被触发时,将会自动调用这个函数;
15. mode:中断触发的方式,可选方式如下
16. 脉宽调制(PWM)基本原理:控制方式就是对逆变电路开关器件的通断进行控制,使输出端得到一系列幅值相等的脉冲,用这些脉冲来代替正弦波或所需要的波形。也就是在输出波形的半个周期中产生多个脉冲,使各脉冲的等值电压为正弦波形,所获得的输出平滑且低次谐波少。按一定的规则对各脉冲的宽度进行调制,即可改变逆变电路输出电压的大小,也可改变输出频率。
17.
18. 通俗一点讲那,就是当如果我们想输出5V电压时,只需一直输出高电平即可;当我们想输出3.75V电压时,那我们就需要在一个周期内(一个高电平和一个低电平为一个周期)3.75÷5=75%时间输出高电平,25%时间输出低电平;同理,如果想输出2.5V电压时,我们需要在一个周期内50%时间输出高电平,50%时间输出低电平。
19.
20. Arduino UNO开发板上只有带有“~”表示的引脚才具有PWM功能,因此我们在控制驱动时可以使用这几个引脚。
21. 在前面的教程中已经讲过如果想控制驱动的输出时,需要对驱动的“ENA”“ENB”进行控制,因此我们需要将图中被选中部分的两个跳线帽拔掉。并将“ENA”连接Arduino UNO开发板的“5”引脚,“ENB”连接“6”引脚。
22. 由于之前设计不太合理,占用了太多的PWM引脚,因此在代码里对控制小车电机的引脚做了点小改动
23. 16到19号引脚
24.
25. 大家记得要把对应的接线也改过来
a) int leftMotor1 = 5;
b) int leftMotor2 = 6;
c) int rightMotor1 = 7;
d) int rightMotor2 = 8;
e) 现在改为
f) int leftMotor1 = 16;
g) int leftMotor2 = 17;
h) int rightMotor1 = 18;
i) int rightMotor2 = 19;
26. 代码如下
a) #include <ros.h>
b) #include <Servo.h>
c) #include<geometry_msgs/Twist.h>
d)
e) int leftCounter = 0,rightCounter = 0;
f) unsigned long time = 0,old_time = 0; //时间标记
g) unsigned long time1 = 0;
h) float lv, rv; //左、右轮速度
i)
j) //定义五中运动状态
k) #define STOP 0
l) #define FORWARD 1
m) #define BACKWARD 2
n) #define TURNLEFT 3
o) #define TURNRIGHT 4
p) #define CHANGESPEED 5 //换挡的
q) //定义需要用到的引脚
r) int leftMotor1 = 16;
s) int leftMotor2 = 17;
t) int rightMotor1 = 18;
u) int rightMotor2 = 19;
v) bool speedLevel = 1;
w)
x) //调速
y) int leftPWM =5;
z) int rightPWM = 6;
aa)
bb) //x轴方向的速度
cc) double lin_vel = 0.0;
dd) //y轴方向的速度
ee) double ang_vel = 0.0;
ff) //定义接受的键
gg) int cmd_ctrl = 0;
hh)
ii) //注册ROS节点
jj) ros::NodeHandle nh;
kk)
ll) //回调函数
mm)voidmessageCb(const geometry_msgs::Twist& vel)
nn) {
oo) lin_vel = vel.linear.x;
pp) ang_vel = vel.angular.z;
qq) cmd_ctrl = 1 * lin_vel + 3 * ang_vel;
rr) }
ss) //设置订阅的消息类型和发布的主题
tt) ros::Subscriber<geometry_msgs::Twist>sub("/turtle1/cmd_vel", messageCb);
uu)
vv) void setup() {
ww) // put your setup code here,to run once:
xx) nh.initNode();
yy) nh.subscribe(sub);
zz)
aaa) attachInterrupt(0,RightCount_CallBack, FALLING);
bbb) attachInterrupt(1, LeftCount_CallBack,FALLING);
ccc)
ddd) pinMode(leftMotor1, OUTPUT);
eee) pinMode(leftMotor2,OUTPUT);
fff) pinMode(rightMotor1, OUTPUT);
ggg) pinMode(rightMotor2, OUTPUT);
hhh) pinMode(leftPWM,OUTPUT);
iii) pinMode(rightPWM, OUTPUT);
jjj) }
kkk)
lll) void loop() {
mmm) // put your main code here,to run repeatedly:
nnn) SpeedDetection();
ooo) switch(cmd_ctrl)
ppp) {
qqq) case 2:
rrr) motorRun(FORWARD);
sss) delay(1000);
ttt) motorRun(STOP);
uuu) break;
vvv) case -2:
www) motorRun(BACKWARD);
xxx) delay(1000);
yyy) motorRun(STOP);
zzz) break;
aaaa) case 6:
bbbb) motorRun(TURNLEFT);
cccc) delay(1000);
dddd) break;
eeee) case -6:
ffff) motorRun(TURNRIGHT);
gggg) delay(1000);
hhhh) motorRun(STOP);
iiii) break;
jjjj) default:
kkkk) motorRun(STOP);
llll) break;
mmmm) }
nnnn) if (speedLevel) //根据不通的档位输出不同速度
oooo) {
pppp) analogWrite(leftPWM, 120);
qqqq) analogWrite(rightPWM, 120);
rrrr) }
ssss) else
tttt) {
uuuu) analogWrite(leftPWM, 250);
vvvv) analogWrite(rightPWM, 250);
wwww) }
xxxx) motorRun(STOP);
yyyy) nh.spinOnce();
zzzz) }
aaaaa) /*
bbbbb) * *速度计算
ccccc) */
ddddd) bool SpeedDetection()
eeeee) {
fffff) time = millis(); //以毫秒为单位,计算当前时间
ggggg) if (abs(time - old_time) >= 1000) // 如果计时时间已达1秒
hhhhh) {
iiiii) detachInterrupt(0);// 关闭外部中断0
jjjjj) detachInterrupt(1);// 关闭外部中断1
kkkkk) //把每一秒钟编码器码盘计得的脉冲数,换算为当前转速值
lllll) //转速单位是每分钟多少转,即r/min。这个编码器码盘为20个空洞。
mmmmm) lv = (float)leftCounter * 60 / 20;//小车车轮电机转速
nnnnn) rv = (float)rightCounter * 60 / 20;
ooooo) //恢复到编码器测速的初始状态
ppppp) leftCounter = 0; //把脉冲计数值清零,以便计算下一秒的脉冲计数
qqqqq) rightCounter = 0;
rrrrr) old_time = millis(); // 记录每秒测速时的时间节点
sssss) attachInterrupt(0,RightCount_CallBack, FALLING); // 重新开放外部中断0
ttttt) attachInterrupt(1,LeftCount_CallBack, FALLING);
uuuuu) return 1;
vvvvv) }
wwwww) else
xxxxx) return 0;
yyyyy) }
zzzzz)
aaaaaa)
bbbbbb) /*
cccccc) * *右轮编码器中断服务函数
dddddd) */
eeeeee) void RightCount_CallBack()
ffffff) {
gggggg) rightCounter++;
hhhhhh) }
iiiiii) /*
jjjjjj) * *左轮编码器中断服务函数
kkkkkk) */
llllll) void LeftCount_CallBack()
mmmmmm) {
nnnnnn) leftCounter++;
oooooo) }
pppppp)
qqqqqq)
rrrrrr) //运动控制函数
ssssss) void motorRun(int cmd)
tttttt) {
uuuuuu) switch(cmd) {
vvvvvv) case FORWARD:
wwwwww) digitalWrite(leftMotor1, LOW);
xxxxxx) digitalWrite(leftMotor2,HIGH);
yyyyyy) digitalWrite(rightMotor1, HIGH);
zzzzzz) digitalWrite(rightMotor2, LOW);
aaaaaaa) break;
bbbbbbb) case BACKWARD:
ccccccc) digitalWrite(leftMotor1, HIGH);
ddddddd) digitalWrite(leftMotor2, LOW);
eeeeeee) digitalWrite(rightMotor1, LOW);
fffffff) digitalWrite(rightMotor2, HIGH);
ggggggg) break;
hhhhhhh) case TURNLEFT:
iiiiiii) digitalWrite(leftMotor1,LOW);
jjjjjjj) digitalWrite(leftMotor2, HIGH);
kkkkkkk) digitalWrite(rightMotor1, LOW);
lllllll) digitalWrite(rightMotor2,HIGH);
mmmmmmm) break;
nnnnnnn) case TURNRIGHT:
ooooooo) digitalWrite(leftMotor1, HIGH);
ppppppp) digitalWrite(leftMotor2, LOW);
qqqqqqq) digitalWrite(rightMotor1, HIGH);
rrrrrrr) digitalWrite(rightMotor2, LOW);
sssssss) break;
ttttttt) case CHANGESPEED:
uuuuuuu) if (speedLevel) //接收到换挡命令的时候切换档位
vvvvvvv) speedLevel = 0;
wwwwwww) else
xxxxxxx) speedLevel = 1;
yyyyyyy) break;
zzzzzzz) default:
aaaaaaaa) digitalWrite(leftMotor1, LOW);
bbbbbbbb) digitalWrite(leftMotor2,LOW);
cccccccc) digitalWrite(rightMotor1, LOW);
dddddddd) digitalWrite(rightMotor2, LOW);
eeeeeeee) }
ffffffff) }
27. Arduion的PWM引脚需要和正常引脚一样,在voidsetup()函数中初始化为输出模式
28.
a) pinMode(leftPWM, OUTPUT);
b) pinMode(rightPWM, OUTPUT);
29. 在小车的控制状态函数void motorRun(intcmd)中添加多一个选择项,用来切换速度。
30.
a) case CHANGESPEED:
b) Serial.println("CHANGE SPEED");//输出状态
c) if(speedLevel) //接收到换挡命令的时候切换档位
d) speedLevel=0;
e) else
f) speedLevel=1;
g) break;
31. 在主函数void loop()中添加PWM输出的函数,analogWrite(pin, value)函数中“pin”代表使用的引脚,“value”代表输出PWM值的大小,范围是0~255。
32.
a) if(speedLevel) //根据不通的档位输出不同速度
b) {
c) analogWrite(leftPWM, 120);
d) analogWrite(rightPWM, 120);
e) }
f) else
g) {
h) analogWrite(leftPWM, 250);
i) analogWrite(rightPWM, 250);
j) }