Inverse Kinematics in FORTH and Java
Martin_H
Posts: 4,051
I'm build a simple scara robot arm. Unlike a revolute coordinate arm it primarily operates in an XY plane. This greatly simplifies Inverse Kinematics as only two joints need to be considered (shoulder and elbow). So I took my C++ code, quickly ported it to Java, then simplified it to deal with a scara arm. What I would like to do it translate this to FORTH and I already have a good start. But the code has a number of mathematical functions that I'm unsure how they are handled in FORTH. This includes: square roots, arc tangents, arc cosine, and radian angles such as Pi.
Here's the scara arm code in Java:
Below is start of line by line translation of the IK code to FORTH. Any untranslated lines are left commented out:
tl;dr - How on Earth will I do this sort of mathematics in FORTH (any flavor) especially on the Propeller?
Here's the scara arm code in Java:
package ik; public class ScaraArm { // Angular values for common angles static final double STEP_ANGLE = Math.PI / 360; static final double RIGHT_ANGLE = Math.PI / 2; static final double STRAIGHT_ANGLE = Math.PI; static final double FULL_ROTATION = 2.0 * Math.PI; private static final int SHOULDER = 0; private static final int ELBOW = 1; private static final int COUNT = 2; // Size of robot bones in consistent units (mm recommended). private int _humerus; private int _ulna; private int _humerusSq; private int _ulnaSq; // Coordinate of pen tip in Cartesian space. private int _x, _y; // Coordinates in arm scara polar space. private double _shoulderAngleRads; private double _elbowAngleRads; private Joint[] joints = new Joint[COUNT]; /** * Constructor used to initialize arm parameters. * @param humerus - shoulder-to-elbow "bone" in mm * @param ulna - elbow-to-wrist "bone" in mm */ public ScaraArm(int humerus, int ulna) { _humerus = humerus; _ulna = ulna; /* pre-calculations */ _humerusSq = _humerus * _humerus; _ulnaSq = _ulna * _ulna; } /** * Setter to inject the servo parameters for each joint. * @param jointIdx - the enumerated name of the joint. * @param pin - the numeric value of the pin. * @param minWidth - the minimum pulse with for this joint. * @param maxWidth - the maximum pulse with for this joint. * @param center - pulse width that centers the joint * @param widthPerRadian - the pulse width to deflect the servo one radian from center */ public void setJointParameters(int jointIdx, int pin, int minWidth, int maxWidth, int center, double widthPerRadian) { joints[jointIdx].setParameters( pin, minWidth, maxWidth, center, widthPerRadian); } /** * Park - Move the arm servos to the parked position. Called during start and stop. * By parking the arm the servos will start from a known position and minimize startup * lurch. */ public void park() { setShoulder( RIGHT_ANGLE ); setElbow( RIGHT_ANGLE ); } /** * SetPosition : Arm positioning routine utilizing inverse kinematics. Since the arm * is resting on a surface Z can only be positive. Servo movement constraints prevent * y from being negative, But X can be a signed value. * Note: This must be called before and of the move routines to initialize arm state. * @param x - the side to side displacement. * @param y - the distance out from the base center. */ public void setPosition( int x, int y ) { // Save the Cartesian space coordinates. _x = x; _y = y; // Use Pythagorean theorem to calculate shoulder to wrist distance. int s_w = ( x * x ) + ( y * y ); double s_w_sqrt = Math.sqrt( s_w ); // s_w angle to centerline double a1 = Math.atan2( x, y ); // s_w angle to humerus, note the need to clamp this value between -1 to 1! double q = ((_humerusSq - _ulnaSq) + s_w) / (2 * _humerus * s_w_sqrt); if (q > 1) q = 1; else if (q < -1) q = -1; double a2 = Math.acos(q); // shoulder angle double shoulderRads = a1 + a2; setShoulder(shoulderRads); // elbow angle double elb_angle_r = Math.acos(( _humerusSq + _ulnaSq - s_w ) / ( 2 * _humerus * _ulna )); // Set the joints setElbow(elb_angle_r); } /** * SetY : Arm positioning routine utilizing inverse kinematics. Moves the arm from * the current Y coordinate to the newY passed in. It maintains all other position * state. * @param newY - the new forward/back displacement */ public void setY(int newY) { setPosition(_x, newY); } /** * SetX : Arm positioning routine utilizing inverse kinematics. Moves the arm from * the current X coordinate to the newX passed in. It maintains all other position * state. * @param newX - the new height above the table */ public void setX(int newX) { setPosition(newX, _y); } /** * setShoulder: Sets the should angle member, computes the servo pulse width for that * angle, and sets the associated servo. * @param shoulderAngleRads - 32-bit floating point radian value */ public void setShoulder(double shoulderAngleRads) { // Rotate the coordinate system by a right angle so that a straight angle is // a full extension of the joint. _shoulderAngleRads = shoulderAngleRads - RIGHT_ANGLE; joints[SHOULDER].setPosition(_shoulderAngleRads); } /** * SetElbow: Sets elbow angle data member, computes the servo pulse width for that * angle, and sets the associated servo. * @param elbowAngle - floating point radian value */ public void setElbow(double elbowAngle) { // Rotate the coordinate system by a right angle so that a straight angle is // a full extension of the joint. _elbowAngleRads = elbowAngle - STRAIGHT_ANGLE; joints[ELBOW].setPosition(_elbowAngleRads); } static public void main(String args[]) { // Arm dimensions( mm ) int HUMERUS = 94; // shoulder-to-elbow "bone" 3.7" int ULNA = 78; // elbow-to-wrist "bone" 3.1" ScaraArm robotArm = new ScaraArm(HUMERUS, ULNA); robotArm.park(); } };
Below is start of line by line translation of the IK code to FORTH. Any untranslated lines are left commented out:
\ Angular values for common angles 0 constant STEP_ANGLE \ Math.PI / 360 0 constant RIGHT_ANGLE \ Math.PI / 2 0 constant STRAIGHT_ANGLE \ Math.PI 0 constant FULL_ROTATION \ 2.0 * Math.PI 0 constant SHOULDER 1 constant ELBOW 2 constant COUNT \ Size of robot bones in consistent units (mm recommended). create humerus 1 cell allot ; create ulna 1 cell allot ; create humerus_sq 1 cell allot ; create ulna_sq 1 cell allot ; \ Coordinate of pen tip in Cartesian space. create x 1 cell allot ; create y 1 cell allot ; \ Coordinates in arm scara polar space. create shoulder_angle_rads 1 cells allot; create elbow_angle_rads 1 cells allot; \ working variables during IK calculations create s_w 1 cell allot ; create s_w_sqrt 1 cell allot ; \ Used to allocate an array of cells of the desired size passed on stack : array ( n -- addr ) create cells \ converts count into size in bytes here over erase \ clear the buffer and allot \ allocate space Does> swap cells + ; \ run time gives address of cell at index \ Create arrays for servo properties. COUNT array servo_center COUNT array servo_min COUNT array servo_max COUNT array servo_pin COUNT array width_per_radian \ servo_position: Computes the pulse width that matches the desired joint angle \ using the constraints of known angle to pulse values. Servo center is zero, \ counter clockwise a quarter circle is Pi/2, while clockwise is -Pi/2. \ angle 32-bit doubleing point radian value : servo_position ( index angle -- ) \ int pulseWidth = _center + (int)(_widthPerRadian * angle); \ _servo.writeMicroseconds(pulseWidth) ; \ Used to initialize arm parameters. \ humerus - shoulder-to-elbow "bone" in mm \ ulna - elbow-to-wrist "bone" in mm : scara_arm \ (humerus ulna -- ) dup humerus ! dup * humerus_sq ! \ pre-calculations dup ulna ! dup * ulna_sq ! ; \ Park - Move the arm servos to the parked position. Called during start and stop. \ By parking the arm the servos will start from a known position and minimize startup \ lurch. : park ( -- ) RIGHT_ANGLE set_ shoulder RIGHT_ANGLE set_elbow ; \ SetPosition : Arm positioning routine utilizing inverse kinematics. Since the arm \ is resting on a surface Z can only be positive. Servo movement constraints prevent \ y from being negative, But X can be a signed value. \ Note: This must be called before any of the move routines to initialize arm state. \ x - the side to side displacement. \ y - the distance out from the base center. : set_position ( x y -- ) \ Save the Cartesian space coordinates. y ! x ! x @ dup * y @ dup * + dup s_w ! \ Use Pythagorean theorem to calculate shoulder to wrist distance. \ double s_w_sqrt = Math.sqrt( s_w ); \ s_w angle to centerline \ double a1 = Math.atan2( x, y ); \ s_w angle to humerus, note the need to clamp this value between -1 to 1! \ double q = ((_humerusSq - _ulnaSq) + s_w) / (2 * _humerus * s_w_sqrt); \ if (q > 1) \ q = 1; \ else if (q < -1) \ q = -1; \ double a2 = Math.acos(q); \ shoulder angle \ double shoulderRads = a1 + a2; \ setShoulder(shoulderRads); \ elbow angle \ double elb_angle_r = Math.acos(( _humerusSq + _ulnaSq - s_w ) / ( 2 * _humerus * _ulna )); \ Set the joints \ setElbow(elb_angle_r); ; \ Arm positioning routine utilizing inverse kinematics. Moves the arm from \ the current Y coordinate to the newY passed in. It maintains all other \ position state. \ newY - the new forward/back displacement : set_y ( newY -- ) x @ swap set_position ; \ Arm positioning routine utilizing inverse kinematics. Moves the arm from \ the current X coordinate to the newX passed in. It maintains all other \ position state. \ newX - the new height above the table : set_x ( newX -- ) y @ set_position ; \ Sets the should angle member, computes the servo pulse width for that \ angle, and sets the associated servo. \ shoulder_angle_rads - 32-bit floating point radian value : set_shoulder ( shoulderAngleRads -- ) \ Rotate the coordinate system by a right angle so that a straight angle is \ a full extension of the joint. RIGHT_ANGLE - dup shoulder_angle_rads ! SHOULDER swap servo_position ; \ SetElbow: Sets elbow angle data member, computes the servo pulse width for that \ angle, and sets the associated servo. \ elbowAngle - floating point radian value : set_elbow ( elbowAngle -- ) \ Rotate the coordinate system by a right angle so that a straight angle is \ a full extension of the joint. STRAIGHT_ANGLE - dup elbow_angle_rads ! ELBOW swap servo_position ; \ Arm dimensions( mm ) 94 \ shoulder-to-elbow "bone" 3.7" 78 \ elbow-to-wrist "bone" 3.1" scara_arm park
tl;dr - How on Earth will I do this sort of mathematics in FORTH (any flavor) especially on the Propeller?
Comments
Will you use fixed point / scaled interger math, or would you use software floating point? Or could you pre-crunch the numbers on a PC spread sheet, and just use a look up table? That might be less work that generically using the internal table(s).
Also, which forth do you plan to use?
Propforth has a floating point package,
-edit- PropForth5.5/CurrentRelease/Extensions / float.f
but pfth can use any existing forth floating point code that is ANSI standrad compatible.
I'm contemplating using scaled integer math as it should be faster, although I'm not adverse to using floating point if it performs well. I'm following my usual development methodology which is to use gForth on the PC to get most of the way there, then port to the Propeller. I'm not sure which FORTH I will use when I try it on the Prop. I used PropForth for my last project, so I might try pfth to see what it is like.
The reason I'm using FORTH is for the same reason I used it with the Drawbot. It allows for an interactive shell which should be handy for a drawing robot.
There are many samples in the forum. I used one from Beau (in pasm) and converted it to spin (is part of a integer navigation object I posted too). In case I can post it, but I'm sure anything around is better...
Massimo
http://forums.parallax.com/showthread.php/128884-integer-only-GPS-navigation-object
As I said atan2 is adapted from Beau's code, so the base is top notch. My adaptation, well... maybe not so much..
Massimo
IF you wnat ot try it Sal's way, section 7 of the Propforth Manual (PropForth5.5/doc/ PropForth.htm ) has some instuctions for PropForth Assembler.
http://forums.parallax.com/showthread.php/132302-32-bit-CORDIC-algorithm-in-Spin
Massimo
Occasionally I convert some other dialect of Forth into Tachyon just to see if there is something I can do to improve and yet not compromise Tachyon. So just as an exercise I did this with your code and tried to keep it the same more or less. Here it is:
No worries Martin, I didn't know that Tachyon was so intimidating, that is, if someone is not intimidated by the Propeller chip itself then why should a Propeller specific dialect of Forth be intimidating? However I think that even as a noob you are doing well and certainly I see that you do not hold back by procrastinating and asking continual questions while waiting for someone to solve it for you. In fact I like that you give it a go then ask the question, well done.
I can't say if this Forth code is a good translation of the Java but this Tachyon version is just the equivalent of the PropForth version.I may take a look at the Java when I get some time and offer any suggestions for improvements that I find. Is there some area that still needs work?
I found Tachyon a bit intimidating because of its unchecked stack. I'm always running into trouble with stack underflows, so the interpreter catching that helps. Plus any deviations from ANS Forth tend to throw me off. Any comments on Forth style would be welcome, as I tend to do brute force translations which don't always produce the best Forth.
Right now I'm working on debugging the fixed point atan2 by comparing its output to the Spin version and common sense. It produces correct output in most cases, but is maddeningly off in a few. It's driving me bonkers.
10 10 atan2 22 / . \ correct
-10 10 atan2 22 / . \ correct
-10 -10 atan2 22 / . \ correct
0 -10 atan2 22 / . \ correct
10 -10 atan2 22 / . \ correct
10 0 atan2 22 / . \ wrong ~360 expected
0 10 atan2 22 / . \ wrong ~90 expected
-10 0 atan2 22 / . \ wrong ~180 expected
The current code is a bit different from your Tachyon port. I caught a couple of bugs. I had to create a new d2/ function (arithmetic shift right) and create an MIN_INT constant which is the smallest base 32 negative number as I misunderstood the Spin Negx constant at first. Here's the current updated code:
UPDATE: I added a working asin and acos functions.
Here's the latest:
Now I can finally get to the meat of the problem which is IK in FORTH.
you test values look very small, there could be an issue with that. Could you check scaling up?
During cordic you shift right at each iteration. If you start with a 5 bit number there could be a problem here.
Your atan approach looks great.. have to find time to play with it!
Thanks,
Massimo
Ah, that makes sense and explains why increasing the iteration count didn't improve accuracy. For IK distances are usually in mm, and values between 35 to 170 are typical for the kinds of robots I build. I retested with input values of 70 and resolution improved, but the binary search technique is still better for numbers that size. Thanks for suggesting CORDIC as I learned a bunch along the way, without that knowledge I wouldn't have been able to pull off the binary search technique.
Video please?
Solved in that I now have all the mathematics working and producing good looking numbers. I still have arm calibration and the end effector to build. But look for a video when I write up of my scara arm project. My plan is to use FORTH to create a command language so you can send commands to the arm from a PC.
If it goes anything like my Drawbot project the hardest part is yet to come, but please don't burst my denial.
Here's the updated Java code: