Forward Kinematics First: You Can't Solve IK Without It

Before solving inverse kinematics, you need forward kinematics (FK) — computing the end-effector's pose from known joint angles. This is done using Denavit-Hartenberg (DH) parameters, a standard way to describe each joint's geometry with four values: link length (a), link twist (α), link offset (d), and joint angle (θ).

Editorial diagram of joint axes used by an inverse-kinematics solver.
Original editorial illustration for this article. Diagram is indicative and not to scale.

Each joint's transformation is a 4×4 homogeneous matrix. Multiplying all six matrices together gives the full transform from the base to the end-effector.

Python — forward_kinematics.py
# Standard DH transformation matrix and forward kinematics chain
import numpy as np

def dh_matrix(theta, d, a, alpha):
    ct, st = np.cos(theta), np.sin(theta)
    ca, sa = np.cos(alpha), np.sin(alpha)
    return np.array([
        [ct, -st * ca,  st * sa, a * ct],
        [st,  ct * ca, -ct * sa, a * st],
        [0,       sa,       ca,      d],
        [0,        0,        0,      1]
    ])

def forward_kinematics(dh_table, joint_angles):
    # dh_table: list of (d, a, alpha, theta_offset) per joint
    # joint_angles: list of 6 variable joint angles (radians)
    T = np.eye(4)
    for (d, a, alpha, offset), theta in zip(dh_table, joint_angles):
        T = T @ dh_matrix(theta + offset, d, a, alpha)
    return T  # 4x4 pose: T[:3,:3] = rotation, T[:3,3] = position

The dh_table values are specific to your robot's geometry — link lengths and joint offsets from its mechanical design. Every solver below builds on this function.

Geometric IK for a Spherical-Wrist Arm (the Fast, Analytical Method)

Most industrial 6-DOF arms — and a large share of hobby arms modeled after them — use a spherical wrist design: the axes of joints 4, 5, and 6 all intersect at one point. This geometric property lets you decouple the problem into two much simpler sub-problems instead of solving six equations simultaneously:

  1. Position (joints 1-3): Solve where the wrist center needs to be, using triangle geometry.
  2. Orientation (joints 4-6): Solve the remaining rotation once the wrist center position is fixed.

Step 1 — Find the Wrist Center

The wrist center is offset from the target end-effector position along the tool's approach direction (the Z-axis of the target rotation matrix) by the wrist-to-flange distance, d6:

Python — wrist center
def wrist_center(target_pos, target_rot, d6):
    approach_vector = target_rot[:, 2]  # Z-axis column of target rotation matrix
    return target_pos - d6 * approach_vector

Step 2 — Solve Joints 1, 2, 3 (Position)

With the wrist center known, joint 1 is a simple base rotation, and joints 2-3 form a triangle with the two link lengths — solved using the law of cosines:

Python — geometric_ik.py
def solve_position(wx, wy, wz, a1, a2, a3, d1, elbow_up=True):
    theta1 = np.arctan2(wy, wx)

    r = np.sqrt(wx**2 + wy**2) - a1
    s = wz - d1
    D = (r**2 + s**2 - a2**2 - a3**2) / (2 * a2 * a3)
    D = np.clip(D, -1.0, 1.0)  # guard against floating-point domain errors

    theta3 = np.arctan2(np.sqrt(1 - D**2), D) if elbow_up else np.arctan2(-np.sqrt(1 - D**2), D)
    theta2 = np.arctan2(s, r) - np.arctan2(a3 * np.sin(theta3), a2 + a3 * np.cos(theta3))

    return theta1, theta2, theta3

Note the elbow_up flag: the ± in front of the square root produces two valid solutions — "elbow up" and "elbow down." Real IK code always has to choose between multiple valid configurations; simply taking the first solution found is a common bug that causes the arm to unexpectedly flip posture between nearby targets.

Step 3 — Solve Joints 4, 5, 6 (Orientation)

Once joints 1-3 are known, compute the rotation matrix they produce (R0_3), then isolate the remaining rotation the wrist must contribute:

Python — orientation solve
def solve_orientation(R0_3, R_target):
    R3_6 = R0_3.T @ R_target  # remaining rotation after removing joints 1-3

    theta5 = np.arctan2(np.sqrt(R3_6[0,2]**2 + R3_6[1,2]**2), R3_6[2,2])
    # wrist-flip case: theta5 ≈ 0 is a singularity, handled separately below
    if abs(np.sin(theta5)) > 1e-6:
        theta4 = np.arctan2(R3_6[1,2], R3_6[0,2])
        theta6 = np.arctan2(R3_6[2,1], -R3_6[2,0])
    else:
        # Wrist singularity: theta4 and theta6 become coupled (infinite solutions)
        # Convention: fix theta4 = 0 and fold all rotation into theta6
        theta4 = 0.0
        theta6 = np.arctan2(-R3_6[0,1], R3_6[0,0])

    return theta4, theta5, theta6
This is the wrist singularity in action. When theta5 approaches zero, joint 4 and joint 6 axes align, and the split above between them becomes arbitrary — there are infinitely many (θ4, θ6) pairs that produce the same orientation. Production robot controllers detect proximity to this condition and either warn the operator or slow the motion to avoid the near-infinite joint velocities it can otherwise demand.

Numerical IK: The Jacobian Method (When There's No Clean Geometry)

The geometric method above only works cleanly because of the spherical wrist assumption. For arms without that property — unusual joint configurations, redundant arms, or when you need to respect joint limits and avoid obstacles — the standard approach is iterative: start from a guess, and nudge the joint angles toward the target using the Jacobian matrix (which relates small joint movements to small end-effector movements).

Python — jacobian_ik.py
def numerical_jacobian(dh_table, angles, epsilon=1e-6):
    n = len(angles)
    J = np.zeros((6, n))
    T0 = forward_kinematics(dh_table, angles)
    p0, R0 = T0[:3, 3], T0[:3, :3]

    for i in range(n):
        perturbed = angles.copy()
        perturbed[i] += epsilon
        Ti = forward_kinematics(dh_table, perturbed)
        pi, Ri = Ti[:3, 3], Ti[:3, :3]

        J[:3, i] = (pi - p0) / epsilon               # linear velocity part
        dR = Ri @ R0.T
        J[3:, i] = np.array([dR[2,1], dR[0,2], dR[1,0]]) / epsilon  # angular velocity part

    return J

def jacobian_ik(dh_table, target_pos, target_rot, initial_angles,
                 max_iters=200, tol=1e-4, damping=0.01):
    theta = np.array(initial_angles, dtype=float)

    for _ in range(max_iters):
        T = forward_kinematics(dh_table, theta)
        pos_error = target_pos - T[:3, 3]
        rot_error_matrix = target_rot @ T[:3, :3].T
        rot_error = np.array([
            rot_error_matrix[2,1] - rot_error_matrix[1,2],
            rot_error_matrix[0,2] - rot_error_matrix[2,0],
            rot_error_matrix[1,0] - rot_error_matrix[0,1]
        ]) * 0.5
        error = np.concatenate([pos_error, rot_error])

        if np.linalg.norm(error) < tol:
            return theta, True   # converged

        J = numerical_jacobian(dh_table, theta)
        # Damped least-squares (avoids instability near singularities)
        JT = J.T
        delta = JT @ np.linalg.solve(J @ JT + (damping**2) * np.eye(6), error)
        theta += delta

    return theta, False  # did not converge within max_iters

The damping term implements the Damped Least Squares (DLS) method instead of a raw Jacobian pseudo-inverse. Without damping, the solver produces wild, unstable joint jumps when the arm passes near a singularity — the pseudo-inverse of a near-singular Jacobian has enormous values. DLS trades a small amount of accuracy for numerical stability, which is the standard tradeoff used in libraries like Orocos KDL.

C++ Implementation Using Eigen

For real-time control loops running on an industrial PC or embedded Linux board, the same math ported to C++ with the Eigen linear algebra library runs orders of magnitude faster than the Python/NumPy version, since it avoids interpreter overhead entirely:

C++ — forward_kinematics.cpp
#include <Eigen/Dense>
#include <cmath>

Eigen::Matrix4d dhMatrix(double theta, double d, double a, double alpha) {
    double ct = std::cos(theta), st = std::sin(theta);
    double ca = std::cos(alpha), sa = std::sin(alpha);

    Eigen::Matrix4d T;
    T << ct, -st*ca,  st*sa, a*ct,
         st,  ct*ca, -ct*sa, a*st,
          0,     sa,     ca,    d,
          0,      0,      0,    1;
    return T;
}

Eigen::Matrix4d forwardKinematics(const std::vector<std::array<double,4>>& dhTable,
                                   const std::vector<double>& jointAngles) {
    Eigen::Matrix4d T = Eigen::Matrix4d::Identity();
    for (size_t i = 0; i < dhTable.size(); ++i) {
        double d = dhTable[i][0], a = dhTable[i][1];
        double alpha = dhTable[i][2], offset = dhTable[i][3];
        T = T * dhMatrix(jointAngles[i] + offset, d, a, alpha);
    }
    return T;
}

The Jacobian-based numerical solver follows the identical structure shown in Python — Eigen's JacobiSVD or ColPivHouseholderQR classes handle the damped least-squares solve shown earlier with a single method call, avoiding a hand-rolled matrix inversion.

Arduino: What's Actually Realistic

Here's the honest answer most tutorials skip: a full 6-DOF geometric IK solve involves multiple atan2, acos, and sqrt calls per solve. An 8-bit Arduino Uno (ATmega328P, no hardware FPU, 16 MHz) can technically compute this, but each trig call costs hundreds of microseconds in software floating point — for a single IK solve this is often fine for slow point-to-point motion, but too slow for continuous real-time trajectory tracking.

Two realistic paths:

For a simpler on-board case that an Arduino Uno handles well, here's geometric IK for a 4-DOF arm (base rotation + shoulder + elbow + wrist pitch) — common in low-cost educational kits and light enough for 8-bit hardware:

Arduino C++ — 4dof_ik.ino
// 4-DOF geometric IK: base, shoulder, elbow, wrist pitch
// L1 = shoulder-to-elbow length, L2 = elbow-to-wrist length (mm)

bool solveIK4DOF(float x, float y, float z, float L1, float L2,
                  float& baseAngle, float& shoulderAngle, float& elbowAngle) {
    baseAngle = atan2(y, x);

    float r = sqrt(x*x + y*y);
    float D = (r*r + z*z - L1*L1 - L2*L2) / (2 * L1 * L2);

    if (D < -1.0 || D > 1.0) return false;  // target unreachable

    elbowAngle = acos(D);  // elbow-down solution
    shoulderAngle = atan2(z, r) - atan2(L2 * sin(elbowAngle), L1 + L2 * cos(elbowAngle));

    return true;  // true = valid solution found
}

void loop() {
    float baseA, shoulderA, elbowA;
    if (solveIK4DOF(150, 50, 100, 120, 100, baseA, shoulderA, elbowA)) {
        // Convert radians to servo microseconds/degrees and write here
    }
    // else: target out of reach — skip the move, don't send garbage angles
}
Always check the reachability guard. If D falls outside [-1, 1], the target is geometrically unreachable and acos() will return NaN in software floating point — silently sending that to a servo can cause unpredictable jumps. Reject the command instead of feeding it downstream.

Open-Source Libraries Worth Using Instead of Writing Your Own

Writing your own solver is valuable for learning and for simple arms, but for production use, these maintained open-source projects handle joint limits, singularities, and multiple robot geometries more robustly than a first custom implementation typically will:

📦
ikpy (Python) — pure-Python IK library that builds the kinematic chain from a URDF file and solves numerically. Good starting point for prototyping. github.com/Phylliade/ikpy
📦
Orocos KDL (C++) — the kinematics library underneath much of ROS/ROS 2's motion stack. Implements damped least-squares IK among several solver options. orocos.org/kdl
📦
TRAC-IK (C++/ROS) — a faster, more reliable drop-in replacement for KDL's IK solver, combining numerical and SQP-based methods to reduce failed solves near singularities. github.com/traclabs/trac_ik
📦
MoveIt 2 — the standard ROS 2 motion planning framework; wraps IK solvers (KDL, TRAC-IK, or generated analytical solvers via IKFast) together with collision checking and trajectory planning. moveit.ros.org

Geometric vs. Numerical: When to Use Each

Compiled from published documentation; re-check any figure you intend to design against.
PropertyGeometric (Spherical Wrist)Numerical (Jacobian/DLS)
SpeedVery fast — closed-form, fixed number of operationsSlower — iterative, variable number of steps to converge
Requires spherical wristYesNo — works for any joint configuration
Multiple solutionsExplicit (elbow up/down, wrist flip) — you chooseConverges to whichever solution is nearest the initial guess
Behavior near singularitiesCan produce large joint jumps if not detected explicitlyDegrades gracefully with damping, but may fail to converge
Best suited forFixed-geometry industrial/collaborative arms with known DH parametersRedundant arms, unusual geometries, obstacle-aware motion

Testing Your IK Solver Before Trusting It on Real Hardware

The single most useful sanity check for any IK implementation is a round-trip test: run forward kinematics on a set of known joint angles to get a target pose, feed that pose into your IK solver, and confirm it returns joint angles that produce the same pose when run back through forward kinematics.

Python — round_trip_test.py
test_angles = [0.3, -0.5, 1.1, 0.0, 0.7, -0.2]
T_target = forward_kinematics(dh_table, test_angles)

solved_angles, converged = jacobian_ik(
    dh_table, T_target[:3,3], T_target[:3,:3], initial_angles=[0]*6
)

T_check = forward_kinematics(dh_table, solved_angles)
position_error = np.linalg.norm(T_target[:3,3] - T_check[:3,3])

assert converged and position_error < 1e-3, "IK solver failed round-trip test"

Run this across a grid of random joint angles spanning your robot's full range of motion, including configurations near singularities, before deploying any solver — geometric or numerical — to real hardware.

"An IK solver that works for one target pose and fails silently near a singularity is more dangerous than one that visibly fails everywhere — test the edges of your workspace, not just the center."
— Robotics Engineering, Motion Control Editorial Notes

Sources and Further Reading

Code samples in this article are illustrative implementations of well-established algorithms, simplified for clarity. Production use should include joint limit clamping, singularity detection, and validation against your specific robot's DH parameters.

Written by the Robotics Engineering Editorial Team
Technical content focused on 6-DOF robot arm design, motion control, and open-source robotics software. Code examples are tested for correctness of the underlying algorithm but should be validated against your specific hardware before production use.

What's the difference between forward and inverse kinematics?

Forward kinematics calculates the end-effector's position and orientation from known joint angles — a direct calculation with one unique answer. Inverse kinematics does the opposite: given a target position and orientation, it solves for the joint angles needed to reach it, and for a 6-DOF arm this can have multiple valid solutions or none at all.

Why do most 6-DOF robot arms use a spherical wrist design?

A spherical wrist means the last three joint axes intersect at a single point, letting you split inverse kinematics into a position problem (joints 1-3) and an orientation problem (joints 4-6). Without this decoupling, you generally need slower numerical methods to solve all six joints simultaneously.

Can a low-cost microcontroller like an Arduino Uno run 6-DOF inverse kinematics in real time?

An 8-bit Arduino Uno struggles with the trigonometric calls needed for full 6-DOF geometric IK at high control rates since it lacks a hardware floating-point unit. A 32-bit board like an ESP32 or Teensy handles it comfortably, or you can compute IK on a host PC and stream joint angles to the Arduino over serial.

What is a kinematic singularity and why does it matter for IK code?

A singularity is an arm configuration where the Jacobian matrix loses rank, meaning the arm momentarily loses a degree of freedom — for example, when wrist axes align. Near a singularity, small changes in desired end-effector position can require very large or physically impossible joint velocity changes, which is why production IK code detects and handles these cases explicitly.

Related Reading

Next: the 6-DOF Robot Arm Master Guide

Architecture, programming, calibration and application fundamentals in one place.

Read Full Guide →

Primary sources and further reading

These references support the general engineering concepts in this guide. Component limits vary by variant and revision; check the governing datasheet.

Links checked: August 5, 2026.