文章目录
- 一 总体思路
- 1 功能原理
- 2 硬件准备
- 3 Arduino库
- 二 代码设计
- 1 舵机部分
- 2 电位器
- 3 记录路径法
- 4 触控板与三色灯板
- 三 展示效果
- 视频链接
一 总体思路
1 功能原理
- 实现功能:使用电位器控制每一个舵机运动,进而操作机械臂;设置学习模式,打开后可以记录手动操作的路径,并且能复位执行记录好的路径,能够路径信息打印出来调用。
- 原理:电位器操纵舵机,使用Servo库自带的
map映射
,实现控制效果非常容易;
学习模式:本项目设计了一种方法,记录每个舵机一次运动中的开始角度
与结束角度
,加上舵机编号
存入一个结构体数组中,具体实现详见代码部分。这种方法的优势在于,节省空间,支持机械臂复杂操作,可以流畅复位,理论上结构体数组范围可以开多大,就可以支持多少次的舵机运动,代码部分详细介绍,该方法仅涉及c/c++基本语法结构。
简单说3种模式:1 纯电位器操控,2 学习模式,可记录操作的路径,3 复位模式,不可操作,用来执行记录的路径。
2 硬件准备
- NANO版
- SG90伺服舵机
- 触控板
- 电位器
- 3d打印机械臂结构(某宝)
- 三色灯板
3 Arduino库
- 仅需要Servo库,其余为简单代码
本项目除了记录舵机路径的方法之外,其余部分皆为基础。逻辑清晰,有c++基础的小白也比较容易实现。
二 代码设计
源代码较长,该部分分块进行介绍:舵机,电位器,路径记录法
,触控板与三色灯板。
1 舵机部分
该部分主要包含:舵机复位方法init_pos,一个固定的抓取路径catchs(度数),和一些舵机基础全局变量。该部分为基础设定。此处有疑问可以参考我另一篇颜色识别机械臂博客中,有详细介绍。
再次强调:实现机械臂的前提一定先搞清楚自己手里舵机的角度范围和旋转方向,舵机参数都了解清楚后,才开始编码,这是最基础也是最重要的一步。本项目舵机引脚与角度范围值仅做参考,以自己实际情况为主。
#include <Servo.h>//1 舵机
Servo myservo1,myservo2,myservo3,myservo4;
int pos1 = 0; //每个舵机的角度值,12345分别对应:底座,大臂,小臂,尖端,爪子舵机
int pos2 = 0;
int pos3 = 130;
int pos4 = 80;
int speed = 7; //机械臂运动速度
//setup里记得写连接舵机初始引脚,还有write初始角度pos变量/*
读取舵机角度参数,控制舵机流畅的回到初始位置
*/
void init_pos(){pos1=myservo1.read(); //当前各舵机角度值pos2=myservo2.read();pos3=myservo3.read();pos4=myservo4.read();for(int i=pos1;i>=0;i--){ //1号舵机,归位0myservo1.write(i);delay(speed);}myservo1.write(0);for(int i=pos3;i<=130;i++){ //3号舵机,归位130myservo3.write(i);delay(speed);}myservo3.write(130);for(int i=pos2;i>=0;i--){ //2号舵机,归位0myservo2.write(i);delay(speed);}myservo2.write(0);for(int i=pos4;i<=80;i++){ //4号舵机,归位80°myservo4.write(i);delay(speed);}myservo4.write(80);pos1=0;//更新pospos2=0;pos3=130;pos4=80;}/*
一个固定路径的机械臂抓取函数,可设置旋转度数spin。
我把这个设置成了开机动画,开机一动不动像木头一样太无趣了
*/
void catchs(int spin) {//先下放小臂for (pos3 = 130; pos3 >= 30; pos3 -= 1) {myservo3.write(pos3);delay(speed);}//打开爪子for (pos4 = 80; pos4 >= 0; pos4 -= 1) {myservo4.write(pos4);delay(speed);}//放大臂for (pos2 = 0; pos2 <= 70; pos2 += 1) {myservo2.write(pos2);delay(speed);}//夹取物品for (pos4 = 0; pos4 <= 80; pos4 += 1) {myservo4.write(pos4);delay(speed);}//抬起大臂for (pos2 = 70; pos2 >= 0; pos2 -= 1) {myservo2.write(pos2);delay(speed);}//转向for(int i=0;i<spin;i++){ //转动spin度,spin不同颜色可以手动设置转动区间myservo1.write(pos1++);delay(speed);} //大臂放下for (pos2 = 0; pos2 <= 15; pos2 += 1) {myservo2.write(pos2);delay(speed);}//打开爪子for (pos4 = 80; pos4 >= 0; pos4 -= 1) {myservo4.write(pos4);delay(speed);}init_pos();//机械臂流畅复位
}
2 电位器
加入电位器的目的是,可以通过手操电位器来控制舵机,也就是控制机械臂关节移动的效果。本项目用了4个SG90伺服舵机,故使用4个电位器操纵。在Arduino下载Servo库后,可以查看knob的示例文件,学习map映射操作舵机的方法。本处代码为,电位器控舵机部分,该部分很简单。
//2 电位器
int potpin1=A0; //potentiometer pin:电位器引脚
int potpin2=A1; //这里开个变量存引脚,是为了代码容易阅读
int potpin3=A2; //若直接写入具体引脚,引脚多了后,鬼知道这些引脚是干嘛用的
int potpin4=A3;
int val1,val2,val3,val4; //存取电位器模拟引脚的数值
//使用电位器,setup中不需要写额外内容/*
重复判定是否操纵了电位器
*/
void loop(){//电位器底座控制与信息记录val1=analogRead(potpin1); //读取模拟引脚值val1=map(val1,0,1023,0,120);//模拟值映射为舵机角度值,[0,1023]到[0,180]myservo1.write(val1); //根据自己实际情况设映射范围//电位器大臂控制与信息记录val2=analogRead(potpin2);val2=map(val2,0,1023,0,70);myservo2.write(val2);//电位器小臂控制与信息记录val3=analogRead(potpin3);val3=map(val3,0,1023,30,130);myservo3.write(val3);//电位器爪子控制与信息记录val4=analogRead(potpin4);val4=map(val4,0,1023,0,80);myservo4.write(val4);//测试每个舵机实时角度,可观察电位器操作引起的舵机角度变化// delay(15); // Serial.print("val1:\t");// Serial.print(val1);// Serial.print("\tval2:\t");// Serial.print(val2);// Serial.print("\tval3\t");// Serial.print(val3);// Serial.print("\tval4:\t");// Serial.println(val4);}
3 记录路径法
本方法为该项目最有趣的地方,也是最不好理解的地方,(嗯,因为我手搓的方法),简称该方法为路径穿梭。需要定义一个结构体,包含舵机编号,记录一次舵机运动中初始角度st与结束角度ed,然后用结构体数组记录全部舵机运动信息。简单原理就是:学习模式打开,然后结构体存入舵机运动信息,最后学习结束后,结构体里有舵机编号,有开始角度和结束角度,并且还是按运动顺序记录的,直接for循环遍历一遍,就能重复动作。
思路很直白,不过麻烦的点在于怎么精准的记录每个舵机运动的时候,每次开始和结束的角度,通过什么判断一次运动的开始与结束?我的思路是,设一个变量con
实时记录这一刻哪一个舵机正在被操控
,con赋值为舵机的编号。有了con之后,记录一个舵机的一次连续运动,我还需要为每一个舵机设一个变量fct
,表示该舵机是否处于连续运动
的状态,只要真假2种状态。然后开始一次记录:以1号舵机为例:
- 舵机开始运动角度:当1号舵机角度发生变化,此时让con=1,如果fct=0时,表示现在1号舵机被操纵,并且1号舵机之前还未被操纵,说明这是1号舵机的初始角度,此时把1号舵机现在的角度计入结构体数组中,做初始角度即可;
- 持续操纵: 当1号舵机角度发生变化,但是fct为真时,表明1号舵机在被持续操纵,不需要记录,及时更新舵机角度就行。
- 舵机运动结束: 当检测1号舵机的舵机角度值没有变化时,此时有两中可能,一是我们没有进行任何电位器操作,不做处理;二实我们控制了1号舵机外的舵机。这就表示1号舵机这一次运动已经结束了,需要将结束信息记录道结构体中去。可以发现,这个方法是依据其它舵机被操纵来记录上一个舵机运动的结束角度,这会带来一个问题:最后一次操作的结束角度怎么办?于是就需要再添加一个判断,来确定本次是不是最后一次操作,如果是也记录一下结束角度。
本方法大概原理就这些,代码中很多地方用了一些小细节,比如fct赋值k啦,k自增的时机啦,改变舵机角度在代码中的位置啦,等等,都是有一些背后原因的,就不在详细介绍。用条件,循环加结构体强行模拟捏了一个方法,没什么算法思想,就纯逻辑,开源欢迎大家学习改进。
//3 记录路径信息结构体
struct memory{ //记录操纵电位器的过程中,每个舵机运动的初始与结束角度int num; //舵机编号int st; //初始变化角度;int ed; //最终变化角度
}a[200];
int k=1; //记录结构体储存记录数量,后面复位时要使用int con=0; //control:表示现在是哪个舵机在被控制,赋值为01234,0代表初始值
int fct1=-1,fct2=-1,fct3=-1,fct4=-1;//flag continue:表示舵机持续运动状态//-1:关闭,学习模式不可用;0:待机;非0:运动
/*
简称穿梭法:记录每个舵机每一次运动开始与运动结束时的角度位置,复位时起点直达重点。
优点:复位速度流畅,支持记录复杂操作,支持同时控制多个电位(容易撑爆数组),可输出舵机运动角度表
缺点:因为本方法的性质,会自动过滤一些无效操作,无法完全复制机械臂行动,比如同一个舵机运动起点与终点一致的运动
*/
void loop(){//电位器底座控制val1=analogRead(potpin1);val1=map(val1,0,1023,0,120);//if中表示当检测到1号舵机有操作时的情况:if(abs(myservo1.read()-val1)>=5){ //收舵机精度与map映射误差影响,防止连续多次记录1°记录,abs是求绝对值。con=1; //现在控制的是1号舵机;if(!fct1){ //记录1号舵机初始状态,fct1==0时才开始。fct1=k; //在本次状态结束时,k可能已发生变化,不能a[k].ed记录,这里顺手废物利用一下fct1充当下标a[k].num=1;a[k].st=myservo1.read();//记录最早的一方k++; //k自在记录到一次开始后就增加。}myservo1.write(val1); //先记录,再写入;否则a[k].st记录不到read()的角度了}else{ //else中表示当1号舵机无操作时://(1)如果234舵机被控制了,并且1号舵机操纵态为打开,应结束1号机的运动并记录:// if条件: con!=0,1说明现在其他舵机被控,fct1>0则说明1号舵机与运动态为打开;//(2)如果1号机仍然被控,但是触控板按下结束,也需结束1号机的运动并记录:// 因为穿梭法记录原理:当下一个舵机被控时,才会结束上次的记录,所以最后一次是记不上的,// 需要特判学习模式结束时记录最后一次的结束操作;// if条件: con=1和触控板结束按下,可以确定本次为最后一次操作if((con!=1&&con!=0&&fct1>0)||((con==1)&&(ed_touchval==HIGH))){a[fct1].ed=val1; cout_road(fct1); //这是一个打印本次路径的方法,几行Serial输出即可,略。作用是打印本次路径fct1=0; //1号机运动态,归0。待机等待下一次记录;}}//电位器大臂控制val2=analogRead(potpin2);val2=map(val2,0,1023,0,70);//注意,因为穿梭法依据下一个舵机被控来结束上一次记录,并且fct1234都设为全局变量,//所以234号舵机不能写一个通用的函数方法调用,必须一个个写if(abs(myservo2.read()-val2)>=5){con=2;if(!fct2){fct2=k;a[k].num=2;a[k].st=myservo2.read();k++;}myservo2.write(val2);}else{if((con!=2&&con!=0&&fct2>0)||((con==2)&&(ed_touchval==HIGH))){a[fct2].ed=val2;cout_road(fct2);//测试!fct2=0;}}//电位器小臂控制val3=analogRead(potpin3);val3=map(val3,0,1023,30,130);if(abs(myservo3.read()-val3)>=10){con=3;if(!fct3){fct3=k;a[k].num=3;a[k].st=myservo3.read();k++;}myservo3.write(val3);}else{if((con!=3&&con!=0&&fct3>0)||((con==3)&&(ed_touchval==HIGH))){a[fct3].ed=val3;cout_road(fct3);//测试! fct3=0;}}//电位器爪子控制val4=analogRead(potpin4);val4=map(val4,0,1023,0,80);if(abs(myservo4.read()-val4)>=8){con=4;if(!fct4){fct4=k;a[k].num=4;a[k].st=myservo4.read();k++;}myservo4.write(val4);}else{if((con!=4&&con!=0&&fct4>0)||((con==4)&&(ed_touchval==HIGH))){a[fct4].ed=val4;cout_road(fct4);//测试! fct4=0;}}}
4 触控板与三色灯板
该部分也比较简单,添加2个触控板表示学习模式的开始与结束;使用三色灯板,不同颜色可以提示现在正处于哪种模式,方便我们观察。
//4 触控板与三色灯板
//三色灯
int lightr=13; //红灯:复位中,请勿操作
int lightg=12; //绿灯,手操模式
int lightb=11; //蓝灯,学习模式
//触控板
int st_touchpin=3; //开始学习pin
int ed_touchpin=2; //结束学习pin
int st_touchval; //读触控板1输入值:HIGH,LOW
int ed_touchval; //读触控板2输入值:HIGH,LOW//初始引脚,输入输出模式,与初始灯光
void setup() {Serial.begin(9600);pinMode(st_touchpin,INPUT);//触控板引脚设置pinMode(ed_touchpin,INPUT);pinMode(lightr,OUTPUT);//rgb灯引脚设置pinMode(lightg,OUTPUT);pinMode(lightb,OUTPUT);digitalWrite(lightr,LOW); //初始绿灯,手操模式digitalWrite(lightg,HIGH);digitalWrite(lightb,LOW);catchs(60); //机械臂开机热身动画:60度转身操~}
void loop(){//注意了,触控板状态判断这两个if结构,需要放到loop中的最下面//因为在前一个穿梭法中,记录好最后一次操作后,才可以进行复位操作//控制灯光变化与学习模式开和关if(st_touchval==HIGH){ //按下开始触摸板,开始记录路径digitalWrite(lightr,LOW); digitalWrite(lightg,LOW);digitalWrite(lightb,HIGH);//蓝灯,学习模式fct1=0; //舵机持续运动状态设为:待机fct2=0;fct3=0;fct4=0;}if(ed_touchval==HIGH){ //按下关闭触控板,且学习模式已打开,任意fct必然>=0digitalWrite(lightr,HIGH);digitalWrite(lightg,LOW); //复位模式:红灯digitalWrite(lightb,LOW);Serial.println("动作回放中......."); back(); //复位:该方法就是遍历一遍结构体数组,让对应编号的舵机遍历写入一遍角度Serial.println("动作回放结束,学习模式关闭!");digitalWrite(lightr,LOW); digitalWrite(lightg,HIGH);//回到操纵状态,绿灯digitalWrite(lightb,LOW);fct1=-1; //-1表示所有舵机持续运动态:关闭fct2=-1;fct3=-1;fct4=-1;con=0; //控制被控舵机初始化k=1; //记录路径编号初始化}}
三 展示效果
视频链接
开机执行了我设置的左转60度健身操,手动狗头~
Ardunio智能学习机械臂