模拟 6 轴关节机器人
一个 6 轴关节机器人的小型模拟器
引言
我正在寻找一些与控制器相关的应用程序,看起来机器人是梦想中的工作之一。
所以我决定写一篇关于机器人的小文章。“机器人普及化”。
与机器人打交道很有趣,但并不容易。要进行详尽的解释,我应该写一本书。我尽量解释逻辑,但我必须提供许多链接供您深入研究。有很多计算要做。但我们可以使用计算机代数系统程序。我使用 Maxima: http://maxima.sourceforge.net/。因此,我们可以将精力集中在程序上,轻松进行各种尝试。当您从 Maxima 复制时,它已经是 C/C++ 格式。所以如果您给出正确的变量名,复制粘贴会非常容易。
机器人类型
机器人主要有两大类。并联机器人,也称为闭链机器人,http://en.wikipedia.org/wiki/Parallel_manipulator,它们操作空间小,但能达到非常高的速度和力。开链机器人或串联机器人http://en.wikipedia.org/wiki/Robotic_arm#Types,如笛卡尔机器人、SCARA 机器人,或我们的关节臂机器人。
并联机器人处理起来更重,因为我们无法泛化,必须逐次研究。而开链机器人,我们可以循着一条通用的路径。
这是我们的主角
问题类型
机器人运动学中的问题有两个:
- 正向运动学
- 逆向运动学
正向问题是给定电机约束,求出末端执行器(最后一个臂,带有您的夹持、焊接、喷漆等工具)的位置。
逆向问题是已知空间中的位置和姿态,求出电机的约束。我猜,这是我们工作时的正常情况。“工件在那里,我们必须将机器人移到那里”。但它也很难解决。而且并非总是能够解析求解。有些特定配置允许我们解析求解,非常适合计算能力较小的微控制器。我们的配置是一个“拟人臂”,允许我们解析求解。
正向问题
对于正向问题,我们只需使用矩阵。这看起来很难,但理解了逻辑之后就不难了。它只是一种需要学习的算法。
首先你必须知道什么是旋转矩阵。这些是基本的旋转矩阵,http://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations。我们的系统矩阵是通过这些矩阵的适当乘法得到的。
我们需要定义一个通用符号。常用的符号是 Denavit-Hartenberg 符号。http://en.wikipedia.org/wiki/Denavit%E2%80%93Hartenberg_parameters
这些符号定义了 Denavit-Hartenberg 参数。(这些是简化的定义,请参考链接以获取更深入的定义)
- z 轴沿关节轴线方向。重要,z 轴是电机旋转轴。z 轴上的平移称为 `dn`。
- x 轴指向下一个电机,如果 z 轴指向其他方向。x 轴上的平移称为 `an`。
实际上,我们选择坐标系,以便描述整个机器人时,我们只沿 z 或 x 轴移动。y 轴无关紧要。
Denavit-Hartenberg 矩阵是一个 4x4 矩阵。矩阵的 3x3 子矩阵是正常的旋转矩阵,第四列是平移矩阵,第四行填充零和一以获得一个方阵。
重要提示:平移列立即给出空间中的位置 (x,y,z),无需进行任何其他计算。
所以我们开始
- 我们选择一个绝对坐标系。为方便起见,我们选择地面水平,z轴平行于第一个电机(更确切地说,是关节。实际上,由于齿轮或其他技术解决方案,电机通常不与旋转轴对齐。但为了解释,我们称之为电机)。
- 从地面开始,我们沿着 z 轴平移 `h` 毫米(或英寸)。矩阵 `T0`。
- 在 `h` 处有一个驱动旋转关节。我们将角度表示为变量 `t1`。矩阵 `RZ1`。
- 如果我们相乘:M1=T0*Rz1,我们得到变换矩阵,该矩阵给定角度后会返回“附着”在电机轴顶部的坐标系的位置和方向。
- 继续第二个关节,我们沿 z 轴平移 `d1` 毫米,沿 x 轴平移 `a1` 毫米。矩阵 `T1`。
- 我们必须旋转我们的坐标系,使我们的 z 轴平行于关节的旋转点,x 轴指向第三个关节。为此,只需围绕 x 轴旋转 90°(pi/2)度即可。这不是一个变量,而是一个常量,乘法后“消失”。矩阵 `Rpx1`。
- 现在我们可以引入电机角度。与第一个电机完全类似,但这次变量称为 `t2`。矩阵 `RZ2`。
- 矩阵 M2(附着在电机 2 轴的顶部)由第一个矩阵的乘积给出:M2=M1*T1*Rpx1*Rz2。
- ...现在继续直到最后一个手臂
如果你看第四列,它精确地给出了电机顶部在我们绝对坐标系中的位置。(第二个电机对位置还没有任何影响)
这是 Maxima 的一组指令。第一组指令是矩阵的定义(它们可以一次性运行),第二组是它们的乘法(逐臂运行,注释会弄乱语法,或者删除带注释的空行)。
T0:matrix([1,0,0,0],[0,1,0,0],[0,0,1,H],[0,0,0,1]);
Rz1:matrix([cos(t1),-sin(t1),0,0],[sin(t1),cos(t1),0,0],[0,0,1,0],[0,0,0,1]);
T1x:matrix([1,0,0,_x],[0,1,0,_y],[0,0,1,_z],[0,0,0,1]);
T1:matrix([1,0,0,a1],[0,1,0,0],[0,0,1,d1],[0,0,0,1]);
Rpx1:matrix([1,0,0,0],[0,0,-1,0],[0,1,0,0],[0,0,0,1]);
Rz2:matrix([cos(t2),-sin(t2),0,0],[sin(t2),cos(t2),0,0],[0,0,1,0],[0,0,0,1]);
T2x:matrix([1,0,0, _x],[0,1,0,_y],[0,0,1,_z],[0,0,0,1]);
T2:matrix([1,0,0,a2],[0,1,0,0],[0,0,1,0],[0,0,0,1]);
Rz3:matrix([cos(t3),-sin(t3),0,0],[sin(t3),cos(t3),0,0],[0,0,1,0],[0,0,0,1]);
T3x:matrix([1,0,0,_x],[0,1,0,_y],[0,0,1,_z],[0,0,0,1]);
T3:matrix([1,0,0,a3],[0,1,0,0],[0,0,1,0],[0,0,0,1]);
Rpy3:matrix([0,0,1,0],[0,1,0,0],[-1,0,0,0],[0,0,0,1]);
Rz4:matrix([cos(t4),-sin(t4),0,0],[sin(t4),cos(t4),0,0],[0,0,1,0],[0,0,0,1]);
T4x:matrix([1,0,0,_x],[0,1,0,_y],[0,0,1,_z],[0,0,0,1]);
T4:matrix([1,0,0,0],[0,1,0,0],[0,0,1,d4],[0,0,0,1]);
Rpy4:matrix([0,0,-1,0],[0,1,0,0],[1,0,0,0],[0,0,0,1]);
Rz5:matrix([cos(t5),-sin(t5),0,0],[sin(t5),cos(t5),0,0],[0,0,1,0],[0,0,0,1]);
T5x:matrix([1,0,0,_x],[0,1,0,_y],[0,0,1,_z],[0 ,0,0,1]);
T5:matrix([1,0,0,a5],[0,1,0,0],[0,0,1,0],[0 ,0,0,1]);
Rpy6:matrix([0,0,1,0],[0,1,0,0],[-1,0,0,0],[0,0,0,1]);
R6z:matrix([cos(t6),-sin(t6),0,0],[sin(t6),cos(t6),0,0],[0,0,1,0],[0,0,0,1]);
T6x:matrix([1,0,0,_x],[0,1,0,_y],[0,0,1,_z],[0,0,0,1]);
/*0-1*/
M1:T0.Rz1;/* position of coordinate system first motor*/
/*1-2*/
M1x:M1.T1x;/* Draws 1st Arm*/
M1_1:M1.T1;/* moves to second joint*/
M1_2:M1_1.Rpx1; /*aligns the coordinate system to the next motor */
M2:M1_2.Rz2;/* coordinate system second motor*/
/*2-3*/
M2x:M2.T2x; /* Draws 2nd Arm*/
M2_1:M2.T2; /* translates to third joint*/
M3:M2_1.Rz3;/* coordinates system 3rd motor*/
M3:matrix([cos(t1)*cos(t2+t3), -cos(t1)*sin(t2+t3),sin(t1), a2*cos(t1)*cos(t2)+a1*cos(t1)],
[sin(t1)*cos(t2+t3),-sin(t1)*sin(t2+t3),-cos(t1),a2*sin(t1)*cos(t2)+a1*sin(t1)],[sin(t2+t3),cos(t2+t3),0,H+a2*sin(t2)+d1],[0,0,0,1]);
/*3-4*/
M3x:M3.T3x;/* Draws 3rd Arm*/
M3_1:M3.T3;/* translates to 4th joint*/
M3_2:M3_1.Rpy3;/*aligns the coordinate system to the next motor */
M4:M3_2.Rz4;/* coordinates system 4th motor*/
/*4-5*/
M4x:M4.T4x;/* draws 4th arm*/
M4_1:M4.T4;/* translates to 5th joint*/
M4_2:M4_1.Rpy4;/*aligns the coordinate system to the next motor*/
M5:M4_2.Rz5;/* coordinates system 5th motor*/
/*5-6*/
M5x:M5.T5x;/* draws 5th arm*/
M5_1:M5.T5;/* translates to 6th joint*/
M5_2:M5_1.Rpy6;/*aligns the coordinate system to the next joint*/
M6:M5_2.R6z;/* coordinate system 6th motor*/
/*end-effector*/
M6x:M6.T6x;
如果你看,我在计算之后重新定义了矩阵 `M3`,以允许我们引入一些重要的简化,从而减少正弦和余弦的数量(最终会变得非常庞大)。
矩阵 `Tnx` 是带有泛型变量(`_x`、`_y`、`_z`)的矩阵,这些矩阵允许我们在电机的坐标系中绘制附着点。实际上,它允许我们绘制与电机连接的臂。因此,在乘法中:`Mnx:Mn.Tnx`;有注释 /*绘制第 n 个臂*/
逆向问题
这是简单的一面。我知道,这看起来很难,但它只是一种算法。当你理解了它的工作原理后,就会自动掌握。
逆向问题不能用算法解决,你必须思考才能找到解析解。并非总是可能。有时你必须使用数值分析来解决,例如牛顿法,这取决于你的硬件配置。
你想去哪里?
这是一个 6 自由度机器人。你必须给他 6 个参数。前三个很简单,是 (x
,y
,z
)。另外三个是姿态。但是这三个数字代表什么呢?
有几种方法可以定义空间中的姿态:
- 滚转-俯仰-偏航。来自航空科学。 http://en.wikipedia.org/wiki/Aircraft_principal_axes
- ZYZ 欧拉角: http://en.wikipedia.org/wiki/Euler_angles。
- 四元数 http://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
- 任何三个未对齐矩阵旋转的组合...
这类应用中最常用的是欧拉角(需要引用)。因为它遵循手腕的实际配置(最后三个臂称为手腕)。
我在此应用程序中使用了滚转-俯仰-偏航。原因有二:首先,它对用户友好。它更直接,就像一架飞机,你有三个数字,大致就能理解飞机的姿态。其次,尝试一些不同的东西。关于欧拉角逆运动学的互联网上有很多,快速搜索你可能会找到相关例程。
就绪
这种几何结构允许我们将问题分成两部分。前三个臂负责手腕的定位。
我们知道手腕必须在哪里。因为这是我们期望的目标,我们可以做一些几何考虑:第四个电机不提供平移贡献,它与第三个臂是一个整体,只能提供旋转贡献。第六个电机也是如此。所以如果我们将平行于第五个电机 x 轴的向量乘以我们的 RPY 矩阵,我们可以检索手腕的绝对位置。
Ry:matrix([cos(b),0,sin(b)],[0,1,0],[-sin(b),0,cos(b)]);
Rz:matrix([cos(a),-sin(a),0],[sin(a),cos(a),0],[0,0,1]);
Rxx:matrix([1,0,0],[0,cos(c),-sin(c)],[0,sin(c),cos(c)]);
Ryp_1:Ry.Rxx;
Ryp:Rz.Ryp_1; /* this is our Roll-Pitch-Yaw Matrix*/
ex:transpose(matrix([1,0,0]));
N:Ryp.ex;/* this is the vector of motor 5, what interests us is the x-component oriented to the top of end effector*/
Pw:(a5+d6)*N;/* now we multiply his value for the effective distance, gives as sum of the distance from motor 5 to motor 6 + distance from motor 6 to end effector*/
此操作得到以下腕部位置(pWx
,pWy
,pWz
)
pWx = x - cos(a)*cos(b)*(d6 + a5);
pWy = y - sin(a)*cos(b)*(d6 + a5);
pWz = z + sin(b)*(d6 + a5);
其中 x,y,z,a,b
是我们期望的目标点。
由于我们机器人的几何结构,我们可以通过正常的三角函数立即确定前三个角度。
p->t1 = atan2(pWy, pWx);/*the rotation of the first arm, is given by the atan2 of the y and x coordinate of the wrist */
/*we calculate the position of the joint 2, because this is a vertex of the triangle*/
px = pWx -a1 * cos(p->t1);
py = pWy - a1 * sin(p->t1);
pz = pWz - d1 - h;
/*we apply the theorem of cousins to define the anlge of the motor 3*/
c3 = (px*px + py*py + pz*pz - pow(a3 + d4, 2) - a2*a2) / (2 * (a3 + d4) * a2);
s3 =- sqrt(1 - c3*c3);
p->t3 = atan2(s3, c3);
/*with similar consideration we find the angle of the 2nd motor*/
// http://www.rob.uni-luebeck.de/Lehre/2008w/Robotik/Vorlesung/Robotik1VL5_1_vers1.pdf pag. 50
xi = atan2(pz, sg*sqrt(px *px + py*py));// sg is my addition
fi = atan2((a3 + d4) * s3, ( a2 + (a3 + d4) * c3));
p->t2 = xi - fi;
C++ 中已包含的 atan2 函数(我也将其包含在我的代码中,但已注释掉)。它是一个在整个 2*pi 区间内定义的 atan 函数,http://en.wikipedia.org/wiki/Atan2。
最后三个角度的解。必须使用矩阵完成。
前三个电机的旋转也会对最终姿态产生不可避免的贡献,因此我们必须找到手腕的实际姿态,并从这个位置相应地设置最后三个电机的角度。关于下面 `Ri-j` 这种命名法的小说明,我用它表示从 *i* 到 *j* 的旋转矩阵。未知的是 `R3-6`,我们必须找到该矩阵与一些已知项之间的关系。矩阵 `R0-6` 是已知的。它是我们的目标。我们期望的点。但通过我们的 滚转-俯仰-偏航 矩阵以矩阵形式表示。`R0-3` 也是已知的,我们已经计算了这些角度。
- 我们计算从 0 到 3 的旋转矩阵(3x3)。我们只需要从通过正向运动学计算出的矩阵 M3 中取出 3x3 子矩阵。
- R0-6 也可以用矩阵分解表示:R0-6 = R0-3 * R3-6。
将等式两边左乘 R0-3 的逆矩阵(旋转矩阵是正交矩阵,所以 inv(m)=transpose(M)),重新排列后得到:R3-6=transpose(R0-3)* R0-6=transpose(R0-3)* Rrpy
对于 Maxim
R03:minor(M3,4,4);
R03t:transpose(R03);
R36:R03t.Ryp; /*where Ryp is our Roll Pitch Yawn matrix. known because is our target
为这个矩阵的每个项命名
我们可以直接通过这些方程来获取电机角度
p->t5 = atan2(sqrt(nz *nz + ny *ny), (nx));
p->t4 = atan2((-nz), (-ny));
p->t6 = atan2(-sx, -ax);
重要提示 这是一个非常简化的解决方案,实际上,它需要非常仔细地研究符号。为了找到每个奇点和不连续点(atan 函数不是连续的),这些会导致位置的快速变化,并给电机带来非常大的加速度。我几乎找到了每个奇点,如果有人发现任何错误,请告知我。如果他解决了它,那将是我美好的一天。
模拟器
模拟器用 C++ 编写,原因有二。首先最重要的是,向人们展示当我通常说我使用 VB.net 时,是因为我喜欢它。我也可以使用 C++。只是我不喜欢它。如果我可以使用电梯,为什么还要走楼梯。
其次,不那么重要。这段代码将由微控制器执行,而不是 PC,这些通常可以用 C 编程。因此,为了实现快速实现,我用 C++ 编写了它,尽量只使用标准 C,以实现多平台。(只需删除图形部分)。当然,对于 GUI,没办法,它必须在 Windows 中执行,我使用了它的类。
GUI 提供两组数字上下控制。第一组可用于改变电机角度。第二组可用于设定目标位置。标签中显示当前位置。当前位置比我想象的更难找到。最后我使用了暴力法。每个项的组合。
对于导出,我想您对 Robot 类感兴趣。Robot 类旨在绘制图像。当您询问它时,它会返回一张图像。
如何使用
//Declare a global variable:
Robot^ r;
...
//initalize it somewhere, giving the size of the desired bitmap. In our case the size of our picturebox
r=gcnew Robot(pictureBox1->Width, pictureBox1->Height);
//get the image. THIS MUST DONE AT EACH CHANGE...
pictureBox1->Image = r->GetImage();
//if you resize the image remember to call resize:
r->Resize(pictureBox1->Width, pictureBox1->Height);
//and again get the image.
pictureBox1->Image = r->GetImage();
//if you want move with angles call the MoveToMotorAngles function
r->MoveToMotorAngles((double)nA1->Value*dToR,
(double)nA2->Value*dToR,
(double)nA3->Value*dToR,
(double)nA4->Value*dToR,
(double)nA5->Value*dToR,
(double)nA6->Value*dToR);
//and again get the image.
pictureBox1->Image = r->GetImage();
//if you want move to a desired position call MoveToPosition function
tuple^ p;
p=r->MoveToPositions((double)nX->Value,
(double)nY->Value,
(double)nZ->Value,
(double)nA->Value*dToR,
(double)nB->Value*dToR,
(double)nC->Value*dToR);
//and again get the image.
pictureBox1->Image = r->GetImage();
//at least if you will know the actual position of the end-effect call:
tuple^ p = r->GetPosition();
我没有进行几何研究,现在它可能假设不真实的几何形状,并发生自相交。我留下这个问题是为了让您能够自行理解机器人的真正问题,从而设置适当的限制,以避免您的真实机器人因错误的角度而自毁。您只需设置角度限制,并检查是否可以使用其他配置。