最近抽时间,制做了一个低成本的机械臂,获得些许心得。
视频演示:(由于权限不够,传不了大图了)
方案确定: 首先,要确定方案,参考了一些国内,国外的方案,在材料结构方面有亚克力的,有3D打印的,感觉3D打印的外观会好些,而且能够处理一些比较复杂的结构问题,这个会在后面讲到。 file:///C:\Users\spritez\AppData\Local\Temp\ksohtml12200\wps9.jpg 在动力方面,有使用步进电机的,也有使用大扭力舵机的,我在某宝上查了下价格,一个舵机都要七八十元,算了算,这个机械臂,简单点的话也需要4只电机(舵机),有些承受不了呀,嗯,还是用最便宜的SG90舵机吧。 在控制板的选取上,有多种选择,豪华点的用树莓派,低端的用Arduino,易上手的用microPython控制板或Javascript控制板,当然,也可以使用51单片机。我之前一直使用microPython控制板,不过最近刚做了个使用Javascript编程的控制板,刚好试试。 机械结构设计及优化: 确定大致方案后,开始动手设计机械结构,以国外的一个3D打印的机械臂为蓝本,设计的时候就发现,其垂直臂的轴会承担整个机械臂的重量,因此,这个轴的稳定度非常重要,原设计中使用了一段金属管,然后在金属管中还要穿螺丝进去,将其和垂直臂驱动舵机主轴固定在一起。这种需要深加工的东西,显然不适合我们国内普通DIY的玩家,因此在某宝找了许多能够替换的方案,最后确定使用光杆螺钉作为主轴。 file:///C:\Users\spritez\AppData\Local\Temp\ksohtml12200\wps10.jpg 不过,在初版设计并打印出来后,依然发现很多问题,其中最主要的问题集中在两个方面,一是组装起来空间狭小,有些地方根本无法完成组装,尤其是在前面提到的垂直臂旋转轴等,另一个问题就是发现机械臂的自身重量偏大,因此,又退回到设计阶段,参考了亚克力版的机械臂,将一些部件改为拼装式的,即便于组装,又减轻了机械臂自身的重量,这样,使用便宜的SG90舵机就可以游刃有余的工作了。 file:///C:\Users\spritez\AppData\Local\Temp\ksohtml12200\wps11.jpg另外,在机械爪的设计上,为减轻重量,简化了设计,并采用了特殊的弹性设计,这样使得机械爪能够适应不同的物体和形状。 这次的结构设计只是第一版,目前组装中发现的所有问题已经修改完善,不过再打印一次时间很长,目前实在是有些犹豫。至少现在这个已经运作的很好了。 file:///C:\Users\spritez\AppData\Local\Temp\ksohtml12200\wps12.jpg file:///C:\Users\spritez\AppData\Local\Temp\ksohtml12200\wps13.jpg
控制系统: 当机械臂稳定的组装好,就要看如何控制了。控制方案上想了许多,但后来发现都想的太多了,其实作为机械臂只是个执行机构,并不具备智能控制的条件。读过我以前**的朋友应该知道,如果希望能智能化控制机械臂的话,还需要传感器设备作为反馈才行。那可是个大工程,因此,想来想去,还是采用手柄控制比较简单。另外,需要说明的是,此机械臂在设计时,上面的机械臂和下方的旋转底座是分离的,用两颗螺丝连接在一起,实际上机械臂部分可以直接固定在其他平台上,比如智能小车等。这里我多设计了一个旋转底座可固定在桌面上,其实这种应用意义不大,还是固定在移动平台上更好玩一些。最终确定使用手柄控制: file:///C:\Users\spritez\AppData\Local\Temp\ksohtml12200\wps14.jpg 控制板,我这里选择使用Javascript编程的STM32控制板,兼容Espruino。 控制编程: 编程方面,由于使用JS编程,JS中没有delay这样的延时程序,这是JS语言特点决定的,因为JS是基于web而生的,因此,不能有任何阻塞函数,这让我很是适应了一阵子。但这也是有好处的,使用定时器函数,可以很轻松的编写出多线程序(注意不是多线程),而无需担心其中某一个被阻塞(前提是你遵守了JS的编程规则)。 控制程序中要先解决的是舵机控制以及手柄输入读取两个模块的编程,之后,写主程序就简单了,只需要定时查询手柄按键的状态,并控制机械臂上电机旋转角度,就可以了。需要特别指出的是,舵机的旋转是需要控制速度的,否则,舵机快速的旋转很容易损坏舵机而且也让我们控制者很难反应过来。因此,我这里每50ms检查一次手柄按键,并以增量的方式控制舵机逐步达到目标角度。 /** * QARM v1.0 2019-04-16 * Program by David Zou * * * Servo list: * s0 - base * s1 - vertical arm * s2 - horizontal arm * s3 - gripper * */
var psx_key = { PSB_SELECT : 0x0001, PSB_L3 : 0x0002, PSB_R3 : 0x0004, PSB_START : 0x0008, PSB_PAD_UP : 0x0010, PSB_PAD_RIGHT : 0x0020, PSB_PAD_DOWN : 0x0040, PSB_PAD_LEFT : 0x0080, PSB_L2 : 0x0100, PSB_R2 : 0x0200, PSB_L1 : 0x0400, PSB_R1 : 0x0800, PSB_GREEN : 0x1000, PSB_RED : 0x2000, PSB_BLUE : 0x4000, PSB_PINK : 0x8000, PSB_TRIANGLE : 0x1000, PSB_CIRCLE : 0x2000, PSB_CROSS : 0x4000, PSB_SQUARE : 0x8000 };
var prekey = [0,0,0];
SPI1.setup({sck:A5, miso:A6, mosi:A7, baud:10000, mode:2, order:"lsb"}); var psx = new require("PSX_Joystick").connect(SPI1, A4); //Use A4 for CS psx.init(false, false);//analog = false, vibration = false
var s0 = new require("Servo_Zv1").connect(A0, 100); var s1 = new require("Servo_Zv1").connect(A1, 100); var s2 = new require("Servo_Zv1").connect(A2, 100); var s3 = new require("Servo_Zv1").connect(A3, 100);
var qarm_p = { base_min:-90, base_max:90, varm_min:-30, vam_max:60, harm_min:-30, harm_max:25, grip_min:-11, grip_max:20 };
//当前位置 var qarm_pos = { base: 0, varm: 0, harm: 0, grip: 0 };
//目标位置 var qarm_spos = { base: 0, varm: 0, harm: 0, grip: 0 };
function init() { //校准s0,因为s0是需要使用到所有角度的。 s0.calibration(0.8, 1.65, 2.5); s0.setAngle(0);//正向逆时针 s1.setAngle(0);//正向向前 s2.setAngle(0);//正向上台 s3.setAngle(0);//正向打开 }
function set_servo(step) { if ((qarm_spos.base - qarm_pos.base) >= step) qarm_pos.base += step; else if ((qarm_spos.base - qarm_pos.base) <= -step) qarm_pos.base -= step; else if (qarm_pos.base != qarm_spos.base) qarm_pos.base = qarm_spos.base; s0.setAngle(qarm_pos.base);
if ((qarm_spos.varm - qarm_pos.varm) >= step) qarm_pos.varm += step; else if ((qarm_spos.varm - qarm_pos.varm) <= -step) qarm_pos.varm -= step; else if (qarm_pos.varm != qarm_spos.varm) qarm_pos.varm = qarm_spos.varm; s1.setAngle(qarm_pos.varm);
if ((qarm_spos.harm - qarm_pos.harm) >= step) qarm_pos.harm += step; else if ((qarm_spos.harm - qarm_pos.harm) <= -step) qarm_pos.harm -= step; else if (qarm_pos.harm != qarm_spos.harm) qarm_pos.harm = qarm_spos.harm; s2.setAngle(qarm_pos.harm);
if ((qarm_spos.grip - qarm_pos.grip) >= step) qarm_pos.grip += step; else if ((qarm_spos.grip - qarm_pos.grip) <= -step) qarm_pos.grip -= step; else if (qarm_pos.grip != qarm_spos.grip) qarm_pos.grip = qarm_spos.grip; s3.setAngle(qarm_pos.grip); }
init(); setInterval(function(){ var code = 0; var key = psx.getKey(); if (key) { code = ((key.key2 << 8) | (key.key1 & 0xff)) & 0xffff; if ((code !== 0) && (code == prekey[0]) && (code == prekey[1]) && (code == prekey[2])) { // console.log("Keycode = " + code); if (code & psx_key.PSB_PAD_UP) { qarm_spos.varm += 5; if (qarm_spos.varm > qarm_p.vam_max) qarm_spos.varm = qarm_p.vam_max; } if (code & psx_key.PSB_PAD_DOWN) { qarm_spos.varm -= 5; if (qarm_spos.varm < qarm_p.vam_min) qarm_spos.varm = qarm_p.vam_min; } if (code & psx_key.PSB_PAD_LEFT) { qarm_spos.base += 5; if (qarm_spos.base > qarm_p.base_max) qarm_spos.base = qarm_p.base_max; } if (code & psx_key.PSB_PAD_RIGHT) { qarm_spos.base -= 5; if (qarm_spos.base < qarm_p.base_min) qarm_spos.base = qarm_p.base_min; } if (code & psx_key.PSB_TRIANGLE) { qarm_spos.harm -= 5; if (qarm_spos.harm < qarm_p.harm_min) qarm_spos.harm = qarm_p.harm_min; } if (code & psx_key.PSB_CROSS) { qarm_spos.harm += 5; if (qarm_spos.harm > qarm_p.harm_max) qarm_spos.harm = qarm_p.harm_max; } if ((code & psx_key.PSB_R1) || (code & psx_key.PSB_SQUARE)) { qarm_spos.grip += 5; if (qarm_spos.grip > qarm_p.grip_max) qarm_spos.grip = qarm_p.grip_max; } if ((code & psx_key.PSB_R2) || (code & psx_key.PSB_CIRCLE)) { qarm_spos.grip -= 5; if (qarm_spos.grip < qarm_p.grip_min) qarm_spos.grip = qarm_p.grip_min; } if (code & psx_key.PSB_START) { qarm_spos.base = 0; qarm_spos.varm = 0; qarm_spos.harm = 0; qarm_spos.grip = 0; } prekey[0] = 0; prekey[1] = prekey[0]; prekey[2] = prekey[1]; } else { prekey[0] = code; prekey[1] = prekey[0]; prekey[2] = prekey[1]; // console.log("prekey = "+prekey+" code = "+code); } } set_servo(2); }, 50);
完整源码及更多的**和教程请关注:
|