Shop OBEX P1 Docs P2 Docs Learn Events
Inverse Kinematics in FORTH and Java — Parallax Forums

Inverse Kinematics in FORTH and Java

Martin_HMartin_H Posts: 4,051
edited 2013-11-28 17:00 in General Discussion
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:
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

  • Martin_HMartin_H Posts: 4,051
    edited 2013-11-12 11:07
    I found Heater's integer square root function which solved one problem. I completed translation of all the commented out Java to FORTH. I've also run it through the interpreter to make sure it was syntactically valid FORTH. A big undefined at the moment are the trigonometry functions acos and atan2. I figure I will use 16 bit binary radians as units of angular measure, but I need to figure out how to define those functions using the Propeller's sine table.
    \ Values for common angles in brad16
    hex
    002e constant STEP_ANGLE	\ PI / 360
    2000 constant RIGHT_ANGLE	\ PI / 2
    4000 constant STRAIGHT_ANGLE	\ PI
    decimal
    
    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_brads 1 cells allot
    create elbow_angle_brads    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
    
    \ Assume the existence of the following functions
    
    \ sin function using brads ( brad16 -- sin )
    : sin
    ;
    
    \ cosin function using brads ( brad16 -- cos )
    : cos
    ;
    
    \ arccosine function using brads ( cos -- brad16 )
    : acos
    ;
    
    : atan2 ( tan -- brad16 )
    ;
    
    \ Heater's Integer sqrt function
    : isqrtloop
        begin
            dup 0=  if exit then
            swap over or
            swap rot rot 2dup swap <=
            if
                swap over -
                swap rot swap over +
            else
                rot swap over -
            then
            1 rshift
            swap 2 rshift
        again
    ;
    
    \ Integer square root ( i -- s )
    : isqrt
        0 1 30 lshift isqrtloop drop swap drop
    ;
    
    \ squares the input ( i -- i*i )
    : square
        dup *
    ;
    
    \ 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 - brad value
    \  index - servo index
    \ ( angle index -- )
    : servo_position
        dup width_per_radian \ ( angle index width_per_radian )
        swap servo_center    \ ( angle width_per_radian center )
        -rot                 \ ( center angle width_per_radian )
        * +                  \ (pulse_width)
    \ _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 !
        square humerus_sq !
    
        \ pre-calculations
        dup ulna !
        square ulna_sq !
    ;
    
    \ Sets the should angle member, computes the servo pulse width for that
    \ angle, and sets the associated servo.
    \ shoulder_angle_brads - 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_brads !
        SHOULDER servo_position
    ;
    
    \ 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_brads !
        ELBOW servo_position
    ;
    
    \ 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
    ;
    
    \ Clamps n between -32,768 to 32,767 to prevent inverse trig function overflow
    \ ( n -- clamped_value )
    : clamp
        -32768 max
        32767 min
    ;
    
    \ 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 !
    
        \ Use Pythagorean theorem to calculate shoulder to wrist distance.
        x @ square
        y @ square
        +
        dup s_w !
        isqrt s_w_sqrt !
    
        \ s_w angle to centerline
        x @ y @ atan2 \ a1 is on the stack.
    
        \ s_w angle to humerus, note the need to clamp this value to prevent overflow!
        \ compute (_humerus_sq - ulna_sq + s_w) / (2 * humerus * s_w_sqrt)
        humerus_sq @ ulna_sq @ - s_w @ +
        2 humerus @ * s_w_sqrt @ *
        /
        clamp acos \ a2 is on the stack
    
        \ shoulder angle = a1 + a2
        + set_shoulder
    
        \ elbow angle
        \ compute ( humerus_sq + ulna_sq - s_w ) / ( 2 * humerus * ulna )
    
        humerus_sq @ ulna_sq @ + s_w @ -
        2 humerus @ * ulna @ *
        /
        acos
    
        \ Set the joints
        set_elbow
    ;
    
    \ 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
    ;
    
    
    \ Arm dimensions( mm )
    94 \ shoulder-to-elbow "bone" 3.7"
    78 \ elbow-to-wrist "bone" 3.1"
    scara_arm
    park
    
  • prof_brainoprof_braino Posts: 4,313
    edited 2013-11-12 11:55
    I tried to do Stienhart-Hart on the prop, but it turned out to be easier doing the crunching on a PC spread sheet, and making a table. Some stuff just doesn't work out well, or I just get lazy.

    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.
  • Martin_HMartin_H Posts: 4,051
    edited 2013-11-12 12:06
    Will you use fixed point / scaled interger math, or would you use software floating point? Also, which forth do you plan to use?.

    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.
  • max72max72 Posts: 1,155
    edited 2013-11-12 14:04
    Atan2 calls for cordic.
    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
  • max72max72 Posts: 1,155
    edited 2013-11-12 14:43
    Found my object:
    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
  • Martin_HMartin_H Posts: 4,051
    edited 2013-11-12 16:09
    Thanks, I should be able to learn something from that. I believe it is possible to interleave pasm and FORTH, so we'll see what I can do.
  • prof_brainoprof_braino Posts: 4,313
    edited 2013-11-12 18:29
    Martin_H wrote: »
    I believe it is possible to interleave pasm and FORTH,

    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.
  • Martin_HMartin_H Posts: 4,051
    edited 2013-11-13 13:21
    Updated with ROM table look-up based sin and cos, and a CORDIC based atan2. All I need now is acos. I think I can do that using a binary search of the cos values, or find a CORDIC version of that.
    \ Values for common angles in brad13
    $0016 constant DEGREE_ANGLE
    $0400 constant EIGHTH_ANGLE
    $0800 constant RIGHT_ANGLE
    $1000 constant STRAIGHT_ANGLE
    $1400 constant FIVEEIGHTH_ANGLE
    
    
    \ arctangent table from Beau Schwabe's cordic
    create table_atan , $20000000 , \ atan(1)   = 45    degrees   iteration  0
                        $12E4051E , \ atan(1/2) = 26.56 degrees   iteration  1
                        $09FB385B , \ atan(1/4)                   iteration  2
                        $051111D4 , \ atan(1/8)                   iteration  3
                        $028B0D43 , \ atan(1/16)                  iteration  4
                        $0145D7E1 , \ atan(1/32)                  iteration  5
                        $00A2F61E , \ atan(1/64)                  iteration  6
                        $00517C55 , \ atan(1/128)                 iteration  7
                        $0028BE53 , \ atan(1/256)                 iteration  8
                        $00145F2F , \ atan(1/512)                 iteration  9
                        $000A2F98 , \ atan(1/1024)                iteration 10
                        $000517CC , \ atan(1/2048)                iteration 11
                        $00028BE6 , \ atan(1/4096)                iteration 12
    Does> swap cells  + ;
    
    0 constant SHOULDER
    1 constant ELBOW
    2 constant COUNT
    
    \ Size of robot bones in consistent units (mm recommended).
    create humerus    1 cells allot
    create ulna       1 cells allot
    create humerus_sq 1 cells allot
    create ulna_sq    1 cells allot
    
    \ Coordinate of pen tip in Cartesian space.
    create x 1 cells allot
    create y 1 cells allot
    
    \ Coordinates in arm scara polar space.
    create shoulder_angle 1 cells allot
    create elbow_angle    1 cells allot
    
    \ working variables during IK calculations
    create s_w      1 cells allot
    create s_w_sqrt 1 cells 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
    
    \ word to long sign extension function.
    \ n has junk in the top 16 bits, so sign extend
    \ ( n -- n )
    : w>l
        dup
        $ffff
        and
        swap
        $8000 and
        if
            $ffff0000 +
        then
    ;
    
    \ 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
    
    \ Integer trig functions.
    
    \ sin function using brads ( brad13 -- sin32 )
    \ The sine table contains one quadrant, so we'll need to
    \ work around that when generating the index into the table.
    \ note use um* with the return value!
    : sin
        dup \ save a copy of input for later.
        dup RIGHT_ANGLE and
        if
            \ ranges from $800 to $001
            negate $fff and
        else
            \ ranges from $000 to $7FF
            $7ff and
        then
    
        \ index into sin table and bit shift to 32 bits.
        $e000 + w@ 15 lshift
    
        \ use the sign from the preserved value.
        swap
        STRAIGHT_ANGLE and
        if 
            negate
        then
    ;
    
    \ cos defined in terms of sin by coordinate rotation.
    : cos
        RIGHT_ANGLE +
        sin
    ;
    
    \ inverse cosine function using brads ( cos -- brad13 )
    : acos
    ;
    
    create atan2_theta 1 cells allot
    
    \ 13-bit fixed point four-quadrant arctangent. Given Cartesian vector (x, y),
    \ finds the angle subtended by the vector and the positive x-axis.
    \ x a signed integer
    \ y a signed integer
    \ ( x y -- brad13 )
    : atan2
        0 atan2_theta !
        over 0 <
        if
           negate swap \ y = -y
           negate swap \ x = -x
           STRAIGHT_ANGLE atan2_theta !
        then
    
        12 0 do
            dup 0 <
            if
                over i 2/ over +  \ calculate y + (x ~> i)
                -rot              \ push to bottom of stack
                i 2/ -            \ calculate x - (y ~> i)
                swap              \ update y
    
                atan2_theta @     \ get the approximation and
                i table_atan @ -  \ substract the lookup value.
                atan2_theta !     \ assign it back.
            else
                over i 2/ over -  \ calculate y - (x ~> i)
                -rot              \ push to bottom of stack
                i 2/ +            \ calculate x + (y ~> i)
                swap              \ update y
    
                atan2_theta @     \ get the approximation and
                i table_atan @ +  \ add the lookup value.
                atan2_theta !     \ assign it back.
            then
        loop
    
        atan2_theta @ 19 rshift
    ;
    
    \ Heater's Integer sqrt function
    : isqrtloop
        begin
            dup 0=  if exit then
            swap over or
            swap rot rot 2dup swap <=
            if
                swap over -
                swap rot swap over +
            else
                rot swap over -
            then
            1 rshift
            swap 2 rshift
        again
    ;
    
    \ Integer square root ( i -- s )
    : isqrt
        0 1 30 lshift isqrtloop drop swap drop
    ;
    
    \ squares the input ( i -- i*i )
    : square
        dup *
    ;
    
    \ 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 - brad value
    \  index - servo index
    \ ( angle index -- )
    : servo_position
        dup width_per_radian \ ( angle index width_per_radian )
        swap servo_center    \ ( angle width_per_radian center )
        -rot                 \ ( center angle width_per_radian )
        * +                  \ (pulse_width)
    \ _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 !
        square humerus_sq !
    
        \ pre-calculations
        dup ulna !
        square ulna_sq !
    ;
    
    \ Sets the should angle member, computes the servo pulse width for that
    \ angle, and sets the associated servo.
    \ shoulder_angle - 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 !
        SHOULDER servo_position
    ;
    
    \ 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 !
        ELBOW servo_position
    ;
    
    \ 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
    ;
    
    \ Clamps n between -32,768 to 32,767 to prevent inverse trig function overflow
    \ ( n -- clamped_value )
    : clamp
        -32768 max
        32767 min
    ;
    
    \ 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 !
    
        \ Use Pythagorean theorem to calculate shoulder to wrist distance.
        x @ square
        y @ square
        +
        dup s_w !
        isqrt s_w_sqrt !
    
        \ s_w angle to centerline
        x @ y @ atan2 \ a1 is on the stack.
    
        \ s_w angle to humerus, note the need to clamp this value to prevent overflow!
        \ compute (_humerus_sq - ulna_sq + s_w) / (2 * humerus * s_w_sqrt)
        humerus_sq @ ulna_sq @ - s_w @ +
        2 humerus @ * s_w_sqrt @ *
        /
        clamp acos \ a2 is on the stack
    
        \ shoulder angle = a1 + a2
        + set_shoulder
    
        \ elbow angle
        \ compute ( humerus_sq + ulna_sq - s_w ) / ( 2 * humerus * ulna )
    
        humerus_sq @ ulna_sq @ + s_w @ -
        2 humerus @ * ulna @ *
        /
        clamp acos
    
        \ Set the elbow
        set_elbow
    ;
    
    \ 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
    ;
    
    
    \ Arm dimensions( mm )
    94 \ shoulder-to-elbow "bone" 3.7"
    78 \ elbow-to-wrist "bone" 3.1"
    scara_arm
    park
    
  • max72max72 Posts: 1,155
    edited 2013-11-13 22:47
  • Martin_HMartin_H Posts: 4,051
    edited 2013-11-16 10:37
    I've been able to get this program to compile under PropForth with the addition of some compatibility words, and avoiding the use of a the hex prefix. It's debugged up to the atan2 function, so integer sin and cos are working.
    \ Note: The goal is to write code that is as portable as possible.
    \ So I'll define gforth compatibility words to avoid using PropForth-isms
    : cells 4 * ;
    : -rot rot rot ;
    : ! L! ;
    : @ L@ ;
    
    0 constant SHOULDER
    1 constant ELBOW
    2 constant WRIST
    3 constant COUNT
    
    \ Size of robot bones in consistent units (mm recommended).
    variable humerus
    variable ulna
    variable humerus_sq
    variable ulna_sq
    
    \ Coordinate of pen tip in Cartesian space.
    variable x
    variable y
    
    \ Coordinates in arm scara polar space.
    variable shoulder_angle
    variable elbow_angle
    
    \ working variables during IK calculations
    variable s_w
    variable s_w_sqrt
    
    \ Heater's Integer sqrt function
    : isqrtloop
        begin
            dup 0=  if exit then
            swap over or
            swap -rot 2dup swap <=
            if
                swap over -
                swap rot swap over +
            else
                rot swap over -
            then
            1 rshift
            swap 2 rshift
        0 until
    ;
    
    \ Integer square root ( i -- s )
    : isqrt
        0 1 30 lshift isqrtloop drop swap drop
    ;
    
    \ squares the input ( i -- i*i )
    : square
        dup *
    ;
    
    hex \ Change radix in a portable manner.
    
    \ word to long sign extension function.
    \ n has junk in the top 16 bits, so sign extend
    \ ( n -- n )
    : w>l
        dup
        FFFF
        and
        swap
        8000 and
        if
            FFFF0000 +
        then
    ;
    
    \ Values for common angles in brad13
    0016 wconstant DEGREE_ANGLE
    0400 wconstant EIGHTH_ANGLE
    0800 wconstant RIGHT_ANGLE
    1000 wconstant STRAIGHT_ANGLE
    1400 wconstant FIVEEIGHTH_ANGLE
    
    decimal
    
    \ Create a set of arrays using a workaround for the missing create does>
    variable table_atan 13 cells allot
    : atan() cells table_atan + ;
    
    hex \ Change radix in a portable manner.
    
    \ load array with arctangent table iteration from Beau Schwabe's cordic
    20000000 0 atan() ! \ atan(1)   = 45    degrees
    12E4051E 1 atan() ! \ atan(1/2) = 26.56 degrees
    09FB385B 2 atan() ! \ atan(1/4)
    051111D4 3 atan() ! \ atan(1/8)
    028B0D43 4 atan() ! \ atan(1/16)
    0145D7E1 5 atan() ! \ atan(1/32)
    00A2F61E 6 atan() ! \ atan(1/64)
    00517C55 7 atan() ! \ atan(1/128)
    0028BE53 8 atan() ! \ atan(1/256)
    00145F2F 9 atan() ! \ atan(1/512)
    000A2F98 A atan() ! \ atan(1/1024)
    000517CC B atan() ! \ atan(1/2048)
    00028BE6 C atan() ! \ atan(1/4096)
    
    decimal
    
    variable servo_center COUNT cells allot
    : servo_center() cells servo_center + ;
    
    variable servo_min COUNT cells allot
    : servo_min() cells servo_min + ;
    
    variable servo_min COUNT cells allot
    : servo_min() cells servo_min + ;
    
    variable servo_max COUNT cells allot
    : servo_max() cells servo_max + ;
    
    variable servo_pin COUNT cells allot
    : servo_pin() cells servo_pin + ;
    
    variable width_per_radian COUNT cells allot
    : width_per_radian() cells width_per_radian + ;
    
    hex \ Change radix in a portable manner.
    
    \ sin function using brads ( brad13 -- sin32 )
    \ The sine table contains one quadrant, so we'll need to
    \ work around that when generating the index into the table.
    \ note use um* with the return value!
    : sin
        dup \ save a copy of input for later.
        dup RIGHT_ANGLE and
        if
            \ ranges from 800 to 001
            negate FFF and
        else
            \ ranges from 000 to 7FF
            7FF and
        then
    
        \ convert to word count and index into sin table
        2* E000 + W@
    
        \ bit shift to 32 bits for future um*
        F lshift
    
        \ use the sign from the preserved value.
        swap
        STRAIGHT_ANGLE and
        if 
            negate
        then
    ;
    
    decimal
    
    \ cos defined in terms of sin by coordinate rotation.
    : cos
        RIGHT_ANGLE +
        sin
    ;
    
    \ stub to allow compilation
    : acos ;
    
    variable atan2_theta
    
    \ 13-bit fixed point four-quadrant arctangent. Given Cartesian vector (x, y),
    \ finds the angle subtended by the vector and the positive x-axis.
    \ x a signed integer
    \ y a signed integer
    \ ( x y -- brad13 )
    : atan2
        0 atan2_theta !
        over 0 <
        if
           negate swap \ y = -y
           negate swap \ x = -x
           STRAIGHT_ANGLE atan2_theta !
        then
    
        12 0 do
            dup 0 <
            if
                over i 2/ over +  \ calculate y + (x ~> i)
                -rot              \ push to bottom of stack
                i 2/ -            \ calculate x - (y ~> i)
                swap              \ update y
    
                atan2_theta @    \ get the approximation and
                i atan() @ -     \ substract the lookup value.
                atan2_theta !    \ assign it back.
            else
                over i 2/ over -  \ calculate y - (x ~> i)
                -rot              \ push to bottom of stack
                i 2/ +            \ calculate x + (y ~> i)
                swap              \ update y
    
                atan2_theta @    \ get the approximation and
                i atan() @ +     \ add the lookup value.
                atan2_theta !    \ assign it back.
            then
        loop
    
        atan2_theta @ 19 rshift
    ;
    
    \ 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 - brad value
    \  index - servo index
    \ ( angle index -- )
    : servo_position
        dup width_per_radian \ ( angle index width_per_radian )
        swap servo_center    \ ( angle width_per_radian center )
        -rot                 \ ( center angle width_per_radian )
        * +                  \ (pulse_width)
    \ _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 !
        square humerus_sq !
    
        \ pre-calculations
        dup ulna !
        square ulna_sq !
    ;
    
    \ Sets the should angle member, computes the servo pulse width for that
    \ angle, and sets the associated servo.
    \ shoulder_angle - 32-bit floating point radian value
    \ ( shoulderAngleRads -- )
    : set_shoulder
        \ 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 !
        SHOULDER servo_position
    ;
    
    \ Sets elbow angle data member, computes the servo pulse width for that
    \ angle, and sets the associated servo.
    \ elbowAngle - floating point radian value
    \ ( elbowAngle -- )
    : set_elbow
        \ 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 !
        ELBOW servo_position
    ;
    
    \ 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
    ;
    
    \ Clamps n between -32,768 to 32,767 to prevent inverse trig function overflow
    \ ( n -- clamped_value )
    : clamp
        -32768 max
        32767 min
    ;
    
    \ 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.
    \ ( x y -- )
    : set_position
        \ Save the Cartesian space coordinates.
        y ! x !
    
        \ Use Pythagorean theorem to calculate shoulder to wrist distance.
        x @ square
        y @ square
        +
        dup s_w !
        isqrt s_w_sqrt !
    
        \ s_w angle to centerline
        x @ y @ atan2 \ a1 is on the stack.
    
        \ s_w angle to humerus, note the need to prevent overflow!
        \ compute (_humerus_sq - ulna_sq + s_w) / (2 * humerus * s_w_sqrt)
        humerus_sq @ ulna_sq @ - s_w @ +
        2 humerus @ * s_w_sqrt @ *
        /
        clamp acos \ a2 is on the stack
    
        \ shoulder angle = a1 + a2
        + set_shoulder
    
        \ elbow angle
        \ compute ( humerus_sq + ulna_sq - s_w ) / ( 2 * humerus * ulna )
    
        humerus_sq @ ulna_sq @ + s_w @ -
        2 humerus @ * ulna @ *
        /
        clamp acos
    
        \ Set the elbow
        set_elbow
    ;
    
    \ 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
    \ ( newY -- )
    : set_y
        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
    \ ( newX -- )
    : set_x
        y @
        set_position
    ;
    
    
    \ Arm dimensions( mm )
    94 \ shoulder-to-elbow "bone" 3.7"
    78 \ elbow-to-wrist "bone" 3.1"
    scara_arm
    park
    
  • Peter JakackiPeter Jakacki Posts: 10,193
    edited 2013-11-16 19:04
    Hi Martin,

    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:
    [FONT=courier new][COLOR=#000000]TACHYON[/COLOR]
    [/FONT]
    [FONT=courier new][COLOR=#000000]{ Martin_H's Inverse Kinematics converted from PropForth to Tachyon[/COLOR][/FONT]
    [FONT=courier new]
    [COLOR=#000000][/COLOR][/FONT][FONT=courier new][COLOR=#000000]LOAD STATS:[/COLOR][/FONT]
    [FONT=courier new][COLOR=#000000]NAMES: 0647 bytes added[/COLOR][/FONT]
    [FONT=courier new][COLOR=#000000]CODE:  0790 bytes added[/COLOR][/FONT]
    
    [FONT=courier new]
    [COLOR=#000000][/COLOR][/FONT][FONT=courier new][COLOR=#000000]Execution time:[/COLOR][/FONT]
    [FONT=courier new][COLOR=#000000]#94 #78 NEWCNT scara_arm park .LAP 104us[/COLOR][/FONT]
    [FONT=courier new]
    [COLOR=#000000][/COLOR][/FONT][FONT=courier new][COLOR=#000000]}[/COLOR][/FONT]
    [FONT=courier new]
    [COLOR=#000000][/COLOR][/FONT][FONT=courier new][COLOR=#000000]pub cells 2* 2* ;
    
    0     == SHOULDER
    1     == ELBOW
    2      == WRIST
    3      == COUNT
    
    \ Size of robot bones in consistent units (mm recommended).
    LONG humerus,ulna,,humerus_sq,ulna_sq
    
    \ Coordinate of pen tip in Cartesian space.
    LONG x,y[/COLOR][/FONT]
    [FONT=courier new][COLOR=#000000]
    \ Coordinates in arm scara polar space.
    LONG  shoulder_angle,elbow_angle
    
    \ working variables during IK calculations
    LONG s_w,s_w_sqrt
    
    \ Heater's Integer sqrt function
    pub isqrtloop
        BEGIN
            DUP 0EXIT[/COLOR][/FONT]
    [FONT=courier new][COLOR=#000000]       SWAP OVER + OR[/COLOR][/FONT]
    [FONT=courier new][COLOR=#000000]       SWAP - ROT 2DUP SWAP <=
            IF
                SWAP OVER -
                SWAP ROT SWAP OVER +
            ELSE
                ROT SWAP OVER -
            THEN
            2/ SWAP 2/ 2/
        AGAIN
    ;
    
    \ Integer square root ( i -- s )
    pub isqrt
        0 1 #30 SHL isqrtloop DROP NIP
    ;
    
    \ squares the input ( i -- i*i )
    pub square
        DUP *
    ;
    
    
    \ word to long sign extension function.
    \ n has junk in the top 16 bits, so sign extend
    \ ( n -- n )
    : w>l
        DUP
        $FFFF
        AND
        SWAP
        $8000 AND
        IF
            $FFFF0000 +
        THEN
    ;
    
    \ Values for common angles in brad13
    $0016     == DEGREE_ANGLE
    $0400     == EIGHTH_ANGLE
    $0800     == RIGHT_ANGLE
    $1000     == STRAIGHT_ANGLE
    $1400     == FIVEEIGHTH_ANGLE
    
    \ Create a set of arrays using a workaround for the missing create does>
    TABLE table_atan[/COLOR][/FONT]
    [FONT=courier new][COLOR=#000000]\ load array with arctangent table iteration from Beau Schwabe's cordic
    $20000000 ,            \ atan(1)   = 45    degrees
    $12E4051E ,            \ atan(1/2) = 26.56 degrees
    $09FB385B ,            \ atan(1/4)
    $051111D4 ,            \ atan(1/8)
    $028B0D43 ,            \ atan(1/16)
    $0145D7E1 ,            \ atan(1/32)
    $00A2F61E ,            \ atan(1/64)
    $00517C55 ,            \ atan(1/128)
    $0028BE53 ,            \ atan(1/256)
    $00145F2F ,            \ atan(1/512)
    $000A2F98 ,            \ atan(1/1024)
    $000517CC ,            \ atan(1/2048)
    $00028BE6 ,            \ atan(1/4096)
    [/COLOR][/FONT]
    [FONT=courier new][COLOR=#000000]pub atan() cells table_atan + ;
    [/COLOR][/FONT]
    [FONT=courier new][COLOR=#000000]
    LONG servo_center COUNT cells ALLOT
    pub servo_center() cells servo_center + ;
    
    LONG servo_min COUNT cells ALLOT
    pub servo_min() cells servo_min + ;
    
    LONG servo_min COUNT cells ALLOT
    pub servo_min() cells servo_min + ;
    [/COLOR][/FONT]
    [FONT=courier new][COLOR=#000000]LONG servo_max COUNT cells ALLOT
    pub servo_max() cells servo_max + ;
    
    LONG servo_pin COUNT cells ALLOT
    pub servo_pin() cells servo_pin + ;
    
    LONG width_per_radian COUNT cells ALLOT
    pub width_per_radian() cells width_per_radian + ;
    
    
    \ sin function using brads ( brad13 -- sin32 )
    \ The sine table contains one quadrant, so we'll need to
    \ work around that when generating the index into the table.
    \ note use um* with the return value!
    pub sin
        DUP \ save a copy of input for later.
        DUP RIGHT_ANGLE AND
        IF
            \ ranges from 800 to 001
            NEGATE $FFF AND
        ELSE
            \ ranges from 000 to 7FF
            $7FF AND
        THEN
    
        \ convert to word count and index into sin table
        2* $E000 + W@
    
        \ bit shift to 32 bits for future um*
        $0F SHL
    
        \ use the sign from the preserved value.
        SWAP
        STRAIGHT_ANGLE AND ?NEGATE
    ;
    
    
    \ cos defined in terms of sin by coordinate rotation.
    pub cos
        RIGHT_ANGLE +
        sin
    ;
    
    \ stub to allow compilation
    pub acos ;
    
    LONG atan2_theta
    
    \ 13-bit fixed point four-quadrant arctangent. Given Cartesian vector (x, y),
    \ finds the angle subtended by the vector and the positive x-axis.
    \ x a signed integer
    \ y a signed integer
    \ ( x y -- brad13 )
    pub atan2
        atan2_theta ~
        OVER 0<
        IF
           NEGATE SWAP \ y = -y
           NEGATE SWAP \ x = -x
           STRAIGHT_ANGLE atan2_theta !
        THEN
    
        #12 0 DO
            DUP 0<
            IF
                OVER I 2/ OVER +      \ calculate y + (x ~> i)
                ROT ROT                  \ push to bottom of stack
                I 2/ -                \ calculate x - (y ~> i)
                SWAP                  \ update y
    
                atan2_theta @            \ get the approximation and
                I atan() @ -             \ substract the lookup value.
                atan2_theta !            \ assign it back.
            ELSE
                OVER I 2/ OVER -      \ calculate y - (x ~> i)
                ROT ROT                  \ push to bottom of stack
                I 2/ +                \ calculate x + (y ~> i)
                SWAP                  \ update y
    
                atan2_theta @    \ get the approximation and
                I atan() @ +     \ add the lookup value.
                atan2_theta !    \ assign it back.
            THEN
        LOOP
    
        atan2_theta @ #19 SHR
    ;
    
    \ 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 - brad value
    \  index - servo index
    \ ( angle index -- )
    pub servo_position
        DUP width_per_radian \ ( angle index width_per_radian )
        SWAP servo_center    \ ( angle width_per_radian center )
        ROT ROT                 \ ( center angle width_per_radian )
        * +                  \ (pulse_width)
    \ _servo.writeMicroseconds(pulseWidth)
    ;
    
    \ Used to initialize arm parameters.
    \ humerus - shoulder-to-elbow "bone" in mm
    \ ulna - elbow-to-wrist "bone" in mm
    pub scara_arm \ (humerus ulna -- )
        DUP humerus !
        square humerus_sq !
    
        \ pre-calculations
        DUP ulna !
        square ulna_sq !
    ;
    
    \ Sets the should angle member, computes the servo pulse width for that
    \ angle, and sets the associated servo.
    \ shoulder_angle - 32-bit floating point radian value
    \ ( shoulderAngleRads -- )
    pub set_shoulder
        \ 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 !
        SHOULDER servo_position
    ;
    
    \ Sets elbow angle data member, computes the servo pulse width for that
    \ angle, and sets the associated servo.
    \ elbowAngle - floating point radian value
    \ ( elbowAngle -- )
    pub set_elbow
        \ 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 !
        ELBOW servo_position
    ;
    
    \ 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.
    \ ( -- )
    pub park
        RIGHT_ANGLE set_shoulder
        RIGHT_ANGLE set_elbow
    ;
    
    \ Clamps n between -32,768 to 32,767 to prevent inverse trig function overflow
    \ ( n -- clamped_value )
    pub clamp
        -#32768 MAX
        #32767 MIN
    ;
    
    \ 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.
    \ ( x y -- )
    pub set_position
        \ Save the Cartesian space coordinates.
        y ! x !
    
        \ Use Pythagorean theorem to calculate shoulder to wrist distance.
        x @ square
        y @ square
        +
        DUP s_w !
        isqrt s_w_sqrt !
    
        \ s_w angle to centerline
        x @ y @ atan2 \ a1 is on the stack.
    
        \ s_w angle to humerus, note the need to prevent overflow!
        \ compute (_humerus_sq - ulna_sq + s_w) / (2 * humerus * s_w_sqrt)
        humerus_sq @ ulna_sq @ - s_w @ +
        2 humerus @ * s_w_sqrt @ *
        /
        clamp acos \ a2 is on the stack
    
        \ shoulder angle = a1 + a2
        + set_shoulder
    
        \ elbow angle
        \ compute ( humerus_sq + ulna_sq - s_w ) / ( 2 * humerus * ulna )
    
        humerus_sq @ ulna_sq @ + s_w @ -
        2 humerus @ * ulna @ *
        /
        clamp acos
    
        \ Set the elbow
        set_elbow
    ;
    
    \ 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
    \ ( newY -- )
    pub set_y
        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
    \ ( newX -- )
    pub set_x
        y @
        set_position
    ;
    
    DECIMAL
    \ Arm dimensions( mm )
    94 \ shoulder-to-elbow "bone" 3.7"
    78 \ elbow-to-wrist "bone" 3.1"
    scara_arm
    park[/COLOR][/FONT]
    [FONT=courier new]
    [COLOR=#000000][/COLOR][/FONT][FONT=courier new][COLOR=#000000]END[/COLOR][/FONT]
    [FONT=courier new]
    [COLOR=#000000][/COLOR][/FONT]
    
  • Martin_HMartin_H Posts: 4,051
    edited 2013-11-17 04:12
    Thanks Peter. As a Forth noob I've been a bit intimidated by Tachyon, so that will help if I give it a try.
  • Peter JakackiPeter Jakacki Posts: 10,193
    edited 2013-11-17 05:30
    Martin_H wrote: »
    Thanks Peter. As a Forth noob I've been a bit intimidated by Tachyon, so that will help if I give it a try.

    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?
  • Martin_HMartin_H Posts: 4,051
    edited 2013-11-17 08:39
    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:
    \ Note: The goal is to write code that is as portable as possible.
    \ So I'll define gforth compatibility words to avoid using PropForth-isms
    : cells 4 * ;
    : -rot rot rot ;
    : ! L! ;
    : @ L@ ;
    
    \ arithmetic shift right n i bits
    \ ( n i -- n )
    : d2/
        \ while i > 0 shift right.
        0 do
            2/
        loop
    ;
    
    0 constant SHOULDER
    1 constant ELBOW
    2 constant WRIST
    3 constant COUNT
    
    \ Size of robot bones in consistent units (mm recommended).
    variable humerus
    variable ulna
    variable humerus_sq
    variable ulna_sq
    
    \ Coordinate of pen tip in Cartesian space.
    variable x
    variable y
    
    \ Coordinates in arm scara polar space.
    variable shoulder_angle
    variable elbow_angle
    
    \ working variables during IK calculations
    variable s_w
    variable s_w_sqrt
    
    \ Heater's Integer sqrt function
    : isqrtloop
        begin
            dup 0=  if exit then
            swap over or
            swap -rot 2dup swap <=
            if
                swap over -
                swap rot swap over +
            else
                rot swap over -
            then
            1 rshift
            swap 2 rshift
        0 until
    ;
    
    \ Integer square root ( i -- s )
    : isqrt
        0 1 30 lshift isqrtloop drop swap drop
    ;
    
    \ squares the input ( i -- i*i )
    : square
        dup *
    ;
    
    hex \ Change radix in a portable manner.
    
    \ word to long sign extension function.
    \ n has junk in the top 16 bits, so sign extend
    \ ( n -- n )
    : w>l
        dup
        FFFF
        and
        swap
        8000 and
        if
            FFFF0000 +
        then
    ;
    
    \ Values for common angles in brad13
    0016 wconstant DEGREE_ANGLE
    0400 wconstant EIGHTH_ANGLE
    0800 wconstant RIGHT_ANGLE
    1000 wconstant STRAIGHT_ANGLE
    1400 wconstant FIVEEIGHTH_ANGLE
    
    decimal
    
    \ Create a set of arrays using a workaround for the missing create does>
    variable table_atan 17 cells allot
    : atan() cells table_atan + ;
    
    hex \ Change radix in a portable manner.
    
    80000000 constant MIN_INT
    
    \ load array with arctangent table iteration from Beau Schwabe's cordic
    20000000  0 atan() ! \ atan(1)   = 45    degrees
    12E4051E  1 atan() ! \ atan(1/2) = 26.56 degrees
    09FB385B  2 atan() ! \ atan(1/4)
    051111D4  3 atan() ! \ atan(1/8)
    028B0D43  4 atan() ! \ atan(1/16)
    0145D7E1  5 atan() ! \ atan(1/32)
    00A2F61E  6 atan() ! \ atan(1/64)
    00517C55  7 atan() ! \ atan(1/128)
    0028BE53  8 atan() ! \ atan(1/256)
    00145F2F  9 atan() ! \ atan(1/512)
    000A2F98  A atan() ! \ atan(1/1024)
    000517CC  B atan() ! \ atan(1/2048)
    00028BE6  C atan() ! \ atan(1/4096)
    000145F3  D atan() ! \ atan(1/8192)
    0000A2FA  E atan() ! \ atan(1/16384)
    0000517D  F atan() ! \ atan(1/32768)
    000028BE 10 atan() ! \ atan(1/65536)
    
    decimal
    
    variable servo_center COUNT cells allot
    : servo_center() cells servo_center + ;
    
    variable servo_min COUNT cells allot
    : servo_min() cells servo_min + ;
    
    variable servo_min COUNT cells allot
    : servo_min() cells servo_min + ;
    
    variable servo_max COUNT cells allot
    : servo_max() cells servo_max + ;
    
    variable servo_pin COUNT cells allot
    : servo_pin() cells servo_pin + ;
    
    variable width_per_radian COUNT cells allot
    : width_per_radian() cells width_per_radian + ;
    
    hex \ Change radix in a portable manner.
    
    \ sin function using brads ( brad13 -- sin32 )
    \ The sine table contains one quadrant, so we'll need to
    \ work around that when generating the index into the table.
    \ note use um* with the return value!
    : sin
        dup \ save a copy of input for later.
        dup RIGHT_ANGLE and
        if
            \ ranges from 800 to 001
            negate FFF and
        else
            \ ranges from 000 to 7FF
            7FF and
        then
    
        \ convert to word count and index into sin table
        2* E000 + W@
    
        \ bit shift to 32 bits for future um*
        F lshift
    
        \ use the sign from the preserved value.
        swap
        STRAIGHT_ANGLE and
        if 
            negate
        then
    ;
    
    decimal
    
    \ cos defined in terms of sin by coordinate rotation.
    : cos
        RIGHT_ANGLE +
        sin
    ;
    
    
    \ Create a set of arrays using a workaround for the missing create does>
    variable table_asin 13 cells allot
    : asin() cells table_asin + ;
    
    \ load array with place shifted angles for asin binary search
    4096  0 asin() ! \ 45    degrees
    2048  1 asin() ! \ 22.5  degrees
    1024  2 asin() ! \ 11.25 degrees
     512  3 asin() ! \  5.62 degrees
     256  4 asin() ! \  2.81 degrees
     128  5 asin() ! \  1.40 degrees
      64  6 asin() ! \  0.70 degrees
      32  7 asin() ! \  0.35 degrees
      16  8 asin() ! \  0.17 degrees
       8  9 asin() ! \  0.08 degrees
       4 10 asin() ! \  0.04 degrees
       2 11 asin() ! \  0.02 degrees
       1 12 asin() ! \  0.01 degrees
    
    \ compute asin via a binary search 
    \ ( sin - brad13 )
    : asin
        \ push the first approximation
        0
    
        \ refine approximation iteratively
        13 0 do
            \ compare the sine of the approximation to the value.
            2dup 4/ sin >
            if
                \ The approximation is too small, so increase it
                i asin() @ +
            else
                \ The approximation is too large, so decrease it.
                i asin() @ -
            then
        loop
    
        \ return only the approximation.
        swap drop 4/
    ;
    
    \ acos is defined in terms of asin
    : acos
        asin RIGHT_ANGLE - negate
    ;
    
    \ holds the atan approximation.
    variable atan2_theta
    
    \ init the approximation based on which side of the X axis.
    \ ( x y -- x y )
    : atan2_init
        over 0<
        if
            negate swap \ y = -y
            negate swap \ x = -x
            MIN_INT atan2_theta !
        else
            0 atan2_theta !
        then
    ;
    
    \ 13-bit fixed point four-quadrant arctangent. Given Cartesian vector (x, y),
    \ finds the angle subtended by the vector and the positive x-axis.
    \ x a signed integer
    \ y a signed integer
    \ ( x y -- brad13 )
    : atan2
        atan2_init   
    
        17 0 do
            dup 0<
            if
                over i d2/ over + \ calculate y + (x ~> i)
                -rot              \ push to bottom of stack
                i d2/ -           \ calculate x - (y ~> i)
                swap              \ update y
    
                atan2_theta @    \ get the approximation and
                i atan() @ -     \ substract the lookup value.
                atan2_theta !    \ assign it back.
            else
                over i d2/ over - \ calculate y - (x ~> i)
                -rot              \ push to bottom of stack
                i d2/ +           \ calculate x + (y ~> i)
                swap              \ update y
    
                atan2_theta @    \ get the approximation and
                i atan() @ +     \ add the lookup value.
                atan2_theta !    \ assign it back.
            then
        loop
        drop drop                \ x and y are no longer needed
    
        atan2_theta @ 19 rshift
    ;
    
    10 0 atan2 22 / .    \ wrong ~360 expected
    0 10 atan2 22 / .    \ wrong ~90 expected
    -10 0 atan2 22 / .   \ wrong ~180 expected
    
    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
    
    \ 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 - brad value
    \  index - servo index
    \ ( angle index -- )
    : servo_position
        dup width_per_radian \ ( angle index width_per_radian )
        swap servo_center    \ ( angle width_per_radian center )
        -rot                 \ ( center angle width_per_radian )
        * +                  \ (pulse_width)
    \ _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 !
        square humerus_sq !
    
        \ pre-calculations
        dup ulna !
        square ulna_sq !
    ;
    
    \ Sets the should angle member, computes the servo pulse width for that
    \ angle, and sets the associated servo.
    \ shoulder_angle - 32-bit floating point radian value
    \ ( shoulderAngleRads -- )
    : set_shoulder
        \ 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 !
        SHOULDER servo_position
    ;
    
    \ Sets elbow angle data member, computes the servo pulse width for that
    \ angle, and sets the associated servo.
    \ elbowAngle - floating point radian value
    \ ( elbowAngle -- )
    : set_elbow
        \ 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 !
        ELBOW servo_position
    ;
    
    \ 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
    ;
    
    \ Clamps n between -32,768 to 32,767 to prevent inverse trig function overflow
    \ ( n -- clamped_value )
    : clamp
        -32768 max
        32767 min
    ;
    
    \ 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.
    \ ( x y -- )
    : set_position
        \ Save the Cartesian space coordinates.
        y ! x !
    
        \ Use Pythagorean theorem to calculate shoulder to wrist distance.
        x @ square
        y @ square
        +
        dup s_w !
        isqrt s_w_sqrt !
    
        \ s_w angle to centerline
        x @ y @ atan2 \ a1 is on the stack.
    
        \ s_w angle to humerus, note the need to prevent overflow!
        \ compute (_humerus_sq - ulna_sq + s_w) / (2 * humerus * s_w_sqrt)
        humerus_sq @ ulna_sq @ - s_w @ +
        2 humerus @ * s_w_sqrt @ *
        /
        clamp acos \ a2 is on the stack
    
        \ shoulder angle = a1 + a2
        + set_shoulder
    
        \ elbow angle
        \ compute ( humerus_sq + ulna_sq - s_w ) / ( 2 * humerus * ulna )
    
        humerus_sq @ ulna_sq @ + s_w @ -
        2 humerus @ * ulna @ *
        /
        clamp acos
    
        \ Set the elbow
        set_elbow
    ;
    
    \ 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
    \ ( newY -- )
    : set_y
        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
    \ ( newX -- )
    : set_x
        y @
        set_position
    ;
    
    
    \ Arm dimensions( mm )
    94 \ shoulder-to-elbow "bone" 3.7"
    78 \ elbow-to-wrist "bone" 3.1"
    scara_arm
    park
    

    UPDATE: I added a working asin and acos functions.
  • Martin_HMartin_H Posts: 4,051
    edited 2013-11-18 19:03
    I finally got all the fixed point trig functions working. I had to abandon the CORDIC atan2 because I couldn't get satisfactory accuracy. It is usually off by a degree, but often off by up to five degrees. This might be OK for some applications, but is unsatisfactory for Inverse Kinematics. So I extended the binary search technique I used for asin and acos to atan2. The results were excellent and I can get to within a single brad of the correct answer in 12 iterations. The Propeller chip's sine table really earned its keep with this problem.

    Here's the latest:
    \ Note: The goal is to write code that is as portable as possible.
    \ So I'll define gforth compatibility words to avoid using PropForth-isms
    : cells 4 * ;
    : -rot rot rot ;
    : ! L! ;
    : @ L@ ;
    
    \ arithmetic shift right n i bits
    \ ( n i -- n )
    : asr/
        dup 0>
        if
            \ while i > 0 shift right.
            0 do
                2/
            loop
        else
            drop
        then
    ;
    
    hex \ Change radix in a portable manner.
    
    \ begin of fixed point trig functions.
    
    \ Values for common angles in brad13
    0016 constant DEGREE_ANGLE
    0400 constant EIGHTH_ANGLE
    0800 constant RIGHT_ANGLE
    1000 constant STRAIGHT_ANGLE
    
    \ sin function using brads ( brad13 -- sin32 )
    \ The sine table contains one quadrant, so we'll need to
    \ work around that when generating the index into the table.
    \ note use um* with the return value!
    : sin
        dup \ save a copy of input for later.
        dup RIGHT_ANGLE and
        if
            \ ranges from 800 to 001
            negate FFF and
        else
            \ ranges from 000 to 7FF
            7FF and
        then
    
        \ convert to word count and index into sin table
        2* E000 + W@
    
        \ bit shift to 32 bits for future um*
        F lshift
    
        \ use the sign from the preserved value.
        swap
        STRAIGHT_ANGLE and
        if 
            negate
        then
    ;
    
    decimal
    
    \ cos defined in terms of sin by coordinate rotation.
    : cos
        RIGHT_ANGLE +
        sin
    ;
    
    \ tangent can be derived from sine / cos. Both sine and cos are
    \ fast table lookups, so this is only a division more expensive
    \ than sine.
    \ ( brad13 -- tan )
    : tan
        dup
        sin
        swap cos
    
        \ Note that we shift the cos down because we only want the 
        \ numerator shifted left!
        15 asr/
        /
    ;
    
    \ compute asin via a binary search.
    \ Note: we're shifting angles up two bits to allow for greater
    \ precision during the search. It will be shifted back before return.
    \ ( sin - brad13 )
    : asin
        \ push the first approximation and correction
        0 EIGHTH_ANGLE 4*
    
        \ refine approximation and correction iteratively
        13 0 do
            \ move the correction to the bottom of stack.
            -rot
    
            \ compare the sine of the approximation to the value.
            2dup 2/ 2/ sin >
    
            if
                \ The approximation is too small, add the correction.
                rot dup -rot +
            else
                \ The approximation is too large, so decrease it.
                rot dup -rot -
            then
    
            \ half the correction factor for next iterration.
            swap 2/
        loop
    
        \ return only the approximation.
        drop swap drop 2/ 2/
    ;
    
    \ acos is defined in terms of asin
    \ ( cos -- brad13 )
    : acos
        asin RIGHT_ANGLE - negate
    ;
    
    \ ( y x -- brad13 )
    : atan2
        \ save x to use its sign to adjust results
        dup -rot
    
        \ form a fixed point y/x ratio from the inputs
        swap 15 lshift swap /
    
        \ push the first approximation and correction
        0 EIGHTH_ANGLE 4*
    
        \ refine approximation and correction iteratively
        13 0 do
            \ move the correction to the bottom of stack.
            -rot
    
            \ compare the sine of the approximation to the value.
            2dup 2/ 2/ tan >
    
            if
                \ The approximation is too small, add the correction.
                rot dup -rot +
            else
                \ The approximation is too large, so decrease it.
                rot dup -rot -
            then
    
            \ half the correction factor for next iterration.
            swap 2/
        loop
    
        \ return only the approximation.
        drop swap drop 2/ 2/
    
        \ if x's sign was negative, then move results into 3 & 4 quadrants.
        swap 0< if
            STRAIGHT_ANGLE +
        then
    ;
    
    \ end of fixed point trig functions.
    
    \ begin of utility functions.
    
    \ Clamps n between max and min to prevent inverse trig function overflow
    \ ( n -- clamped_value )
    : clamp
        RIGHT_ANGLE negate sin max
        RIGHT_ANGLE sin min
    ;
    
    \ Heater's Integer sqrt function
    : isqrtloop
        begin
            dup 0=  if exit then
            swap over or
            swap -rot 2dup swap <=
            if
                swap over -
                swap rot swap over +
            else
                rot swap over -
            then
            1 rshift
            swap 2 rshift
        0 until
    ;
    
    \ Integer square root ( i -- s )
    : isqrt
        0 1 30 lshift isqrtloop drop swap drop
    ;
    
    \ squares the input ( i -- i*i )
    : square
        dup *
    ;
    
    \ end utility functions
    
    \ begin inverse kinematics code
    
    0 constant SHOULDER
    1 constant ELBOW
    2 constant WRIST
    3 constant COUNT
    
    \ Size of robot bones in consistent units (mm recommended).
    variable humerus
    variable ulna
    variable humerus_sq
    variable ulna_sq
    
    \ Coordinate of pen tip in Cartesian space.
    variable x
    variable y
    
    \ Coordinates in arm scara polar space.
    variable shoulder_angle
    variable elbow_angle
    
    \ working variables during IK calculations
    variable s_w
    variable s_w_sqrt
    
    \ arrays to hold servo properties
    variable servo_center COUNT cells allot
    : servo_center() cells servo_center + ;
    
    variable servo_min COUNT cells allot
    : servo_min() cells servo_min + ;
    
    variable servo_min COUNT cells allot
    : servo_min() cells servo_min + ;
    
    variable servo_max COUNT cells allot
    : servo_max() cells servo_max + ;
    
    variable servo_pin COUNT cells allot
    : servo_pin() cells servo_pin + ;
    
    variable width_per_radian COUNT cells allot
    : width_per_radian() cells width_per_radian + ;
    
    \ 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 - brad value
    \  index - servo index
    \ ( angle index -- )
    : servo_position
        dup width_per_radian \ ( angle index width_per_radian )
        swap servo_center    \ ( angle width_per_radian center )
        -rot                 \ ( center angle width_per_radian )
        * +                  \ (pulse_width)
    \ _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 !
        square humerus_sq !
    
        \ pre-calculations
        dup ulna !
        square ulna_sq !
    ;
    
    \ Sets the should angle member, computes the servo pulse width for that
    \ angle, and sets the associated servo.
    \ shoulder_angle - 32-bit floating point radian value
    \ ( shoulderAngleRads -- )
    : set_shoulder
        \ 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 !
        SHOULDER servo_position
    ;
    
    \ Sets elbow angle data member, computes the servo pulse width for that
    \ angle, and sets the associated servo.
    \ elbowAngle - floating point radian value
    \ ( elbowAngle -- )
    : set_elbow
        \ 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 !
        ELBOW servo_position
    ;
    
    \ 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.
    \ ( x y -- )
    : set_position
        \ Save the Cartesian space coordinates.
        y ! x !
    
        \ Use Pythagorean theorem to calculate shoulder to wrist distance.
        x @ square
        y @ square
        +
        dup s_w !
        isqrt s_w_sqrt !
    
        \ s_w angle to centerline
        x @ y @ atan2 \ a1 is on the stack.
    
        \ s_w angle to humerus, note the need to prevent overflow!
        \ compute (_humerus_sq - ulna_sq + s_w) / (2 * humerus * s_w_sqrt)
        humerus_sq @ ulna_sq @ - s_w @ +
        2 humerus @ * s_w_sqrt @ *
        /
        clamp acos \ a2 is on the stack
    
        \ shoulder angle = a1 + a2
        + set_shoulder
    
        \ elbow angle
        \ compute ( humerus_sq + ulna_sq - s_w ) / ( 2 * humerus * ulna )
    
        humerus_sq @ ulna_sq @ + s_w @ -
        2 humerus @ * ulna @ *
        /
        clamp acos
    
        \ Set the elbow
        set_elbow
    ;
    
    \ 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
    \ ( newY -- )
    : set_y
        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
    \ ( newX -- )
    : set_x
        y @
        set_position
    ;
    
    \ Arm dimensions( mm )
    94 \ shoulder-to-elbow "bone" 3.7"
    78 \ elbow-to-wrist "bone" 3.1"
    scara_arm
    park
    

    Now I can finally get to the meat of the problem which is IK in FORTH.
  • max72max72 Posts: 1,155
    edited 2013-11-19 03:33
    Just an idea about the cordic atan:
    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
  • Martin_HMartin_H Posts: 4,051
    edited 2013-11-19 08:20
    max72 wrote: »
    Just an idea about the cordic atan:
    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!

    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.
  • prof_brainoprof_braino Posts: 4,313
    edited 2013-11-19 14:22
    Solved!

    Video please?
  • Martin_HMartin_H Posts: 4,051
    edited 2013-11-19 15:55
    Solved!

    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.
  • Martin_HMartin_H Posts: 4,051
    edited 2013-11-28 17:00
    To check my algorithm I took the Java version of my code and ported it to the processing environment (http://processing.org/). I then added a graphical display to model the robot arm's movement. Here's what the display looks like:

    processing.jpg


    Here's the updated Java code:
    int originX = 320;
    int originY = 200;
    
    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;
    
      /**
       * 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;
      }
    
      /**
       * 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( y, x );
    
        // s_w angle to humerus, note the need to clamp this value between -1 to 1!
        double q = (double)(_humerusSq - _ulnaSq + s_w) / (2.0 * _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((double)(_humerusSq + _ulnaSq - s_w) / ( 2.0 * _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;
      }
    
      /**
       * 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;
      }
      
      /**
       */
      public void drawArm()
      { 
        double hx = _humerus * Math.cos(_shoulderAngleRads);
        double hy = _humerus * Math.sin(_shoulderAngleRads);
        
        int screenX = (int)(originX + hx);
        int screenY = (int)(originY - hy);
        line(originX, originY, screenX, screenY);
    
        // Convert elbow angle so that it is relative to shoulder angle
        double elbow = _elbowAngleRads - STRAIGHT_ANGLE + _shoulderAngleRads;
    
        int wx = (int)(_ulna * Math.cos(elbow)) + screenX;
        int wy = screenY - (int)(_ulna * Math.sin(elbow));
        line( screenX, screenY, wx, wy);
    
        text("(" + _x + ", " + _y + ") a1 = " + (int)Math.toDegrees(_shoulderAngleRads) + ", a2 = " + (int)Math.toDegrees(_elbowAngleRads), wx, wy);
      }
    };
    
    int HUMERUS = 132;
    int ULNA = 142;
    
    ScaraArm robotArm = new ScaraArm(HUMERUS, ULNA);
    
    PFont f;
    
    void setup()
    {
      size(2 * originX, 2 * originY);
      f = createFont("Arial", 16, true);
      robotArm.park();
    }
    
    void draw() {
      background(255);
      textFont(f, 16);
      fill(0);
    
      line(originX, 0, originX, 2 * originY);
      line(0, originY, 2 * originX, originY);
      int localX = mouseX - originX;
      int localY = originY - mouseY;
      robotArm.setPosition(localX, localY);
      robotArm.drawArm();
    }
    
    819 x 518 - 28K
Sign In or Register to comment.