Skip to content

필사 모드: The Robot Arm Control Loop: From Trajectory Generation to PID and Gravity Compensation

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Introduction — Write the Target Angle Straight In and the Arm Jerks

Say inverse kinematics has finished its work. You've decided where to put the end effector, and you now know what angle each joint needs. Shoulder 0 degrees, elbow 90 degrees.

So you write this into your code:

shoulder.write(0);
elbow.write(90);

The arm lurches. It shakes back and forth a couple times near the target angle, trembles all over, and stops. If it had been holding something, it would have dropped it.

The angle was exactly right, but the motion is a mess. This post is about that gap.

Here's what's actually going on. Those two lines demanded of the joint: "move 90 degrees within 0.02 seconds." From the servo's point of view, that's an average angular velocity of 4500 degrees per second, and infinite acceleration at the instant of starting and stopping. Real torque has a ceiling, so the joint pushes with the maximum torque it can produce, overshoots the target, comes back, and overshoots again.

Control splits into two layers. The upper layer generates a target that changes over time — trajectory generation — and the lower layer is a feedback loop that makes the actual angle follow that target. Without the upper layer, no matter how good the lower layer is, the arm jerks. Without the lower layer, no matter how smooth the upper layer is, gravity drags the arm down.

Trapezoidal Velocity Profile — The Simplest Trajectory

This is the oldest and most widely used shape of trajectory generation. As the name says, it shapes velocity into a trapezoid: ramp up at constant acceleration, cruise at max velocity, ramp down at constant acceleration.

Let's plug in numbers. Move the shoulder 90 degrees, or 1.5708 rad. Take this joint's max velocity as 1.0 rad/s and max acceleration as 2.0 rad/s².

The time to reach max velocity during the acceleration phase is:

t_a = v_max / a_max = 1.0 / 2.0 = 0.5s

The angle traveled during that time is:

d_a = 0.5 × a_max × t_a² = 0.5 × 2.0 × 0.25 = 0.25 rad

The deceleration phase is symmetric, so it's also 0.25 rad. Together they use up 0.5 rad, leaving:

1.5708 - 0.5 = 1.0708 rad

This stretch is covered at max velocity, so:

t_c = 1.0708 / 1.0 = 1.0708s

The total move time is:

T = 0.5 + 1.0708 + 0.5 = 2.0708s

Compare this to the 0.02-second command from earlier and it's 100 times slower. And that 100x factor is exactly why the arm doesn't jerk.

There's an important exception here. If the move distance is short, deceleration has to start before max velocity is even reached. Since the minimum distance needed for accel-plus-decel was 0.5 rad, moving only 0.2 rad produces a triangular profile with no cruise phase at all. The peak velocity reached is:

v_peak = √(a_max × d) = √(2.0 × 0.2) = 0.6325 rad/s
T = 2 × v_peak / a_max = 2 × 0.6325 / 2.0 = 0.6325s

Miss this branch in your code and the profile overshoots the target on short moves. The implementation below includes this check.

import numpy as np


def trapezoid(dq, v_max, a_max):
    """Computes the segment times of a trapezoidal (or triangular) velocity profile."""
    dq = abs(dq)
    d_a = v_max ** 2 / (2 * a_max)      # distance covered during the acceleration phase
    if 2 * d_a >= dq:                    # max velocity never gets reached
        v_peak = np.sqrt(a_max * dq)
        t_a = v_peak / a_max
        return t_a, 0.0, t_a, v_peak
    t_a = v_max / a_max
    t_c = (dq - 2 * d_a) / v_max
    return t_a, t_c, t_a, v_max


def sample(t, dq, v_max, a_max):
    """Returns the position and velocity at time t. The sign is applied at the end."""
    sign = 1.0 if dq >= 0 else -1.0
    t_a, t_c, t_d, v_p = trapezoid(dq, v_max, a_max)
    T = t_a + t_c + t_d
    t = min(max(t, 0.0), T)
    if t < t_a:
        q, v = 0.5 * a_max * t * t, a_max * t
    elif t < t_a + t_c:
        q, v = 0.5 * a_max * t_a ** 2 + v_p * (t - t_a), v_p
    else:
        td = t - t_a - t_c
        q = 0.5 * a_max * t_a ** 2 + v_p * t_c + v_p * td - 0.5 * a_max * td * td
        v = v_p - a_max * td
    return sign * q, sign * v


DQ, VMAX, AMAX = np.radians(90), 1.0, 2.0
ta, tc, td, vp = trapezoid(DQ, VMAX, AMAX)
print(f"accel {ta:.4f}s  cruise {tc:.4f}s  decel {td:.4f}s  total {ta+tc+td:.4f}s")
for t in (0.0, 0.25, 0.5, 1.0, 1.5708, 2.0708):
    q, v = sample(t, DQ, VMAX, AMAX)
    print(f"  t={t:.4f}s  q={np.degrees(q):8.4f}°  v={v:.5f} rad/s")

Here's the output.

accel 0.5000s  cruise 1.0708s  decel 0.5000s  total 2.0708s
  t=0.0000s  q=  0.0000°  v=0.00000 rad/s
  t=0.2500s  q=  3.5810°  v=0.50000 rad/s
  t=0.5000s  q= 14.3239°  v=1.00000 rad/s
  t=1.0000s  q= 42.9718°  v=1.00000 rad/s
  t=1.5708s  q= 75.6763°  v=0.99999 rad/s
  t=2.0708s  q= 90.0000°  v=0.00000 rad/s

A trapezoidal profile still has one problem left over. At t=0 and t=0.5, acceleration jumps like a step from 0 to 2.0, then from 2.0 back to 0. Jerk — the derivative of acceleration — is infinite at those instants. A light arm barely notices, but a long, slender link takes that shock and shows visible residual vibration.

Quintic Polynomial Trajectory — Chaining Acceleration Together Too

The standard way to eliminate the jerk problem is to write position as a polynomial in time, and specify position, velocity, and acceleration at both ends. That's six conditions, so six unknowns, which means a 5th-order (quintic) polynomial.

q(t) = a0 + a1·t + a2·t² + a3·t³ + a4·t⁴ + a5·t⁵

With velocity and acceleration set to zero at both ends, the coefficients come out in closed form. With Δq as the total move and T as the total time:

a0 = q0,  a1 = 0,  a2 = 0
a3 =  10·Δq / T³
a4 = -15·Δq / T⁴
a5 =   6·Δq / T⁵

Two practically important numbers fall out here. In normalized time τ = t/T, velocity peaks at τ = 0.5, and acceleration peaks at τ = 0.2113 and τ = 0.7887, with these values:

v_max = 1.875 × Δq / T
a_max = (10/√3) × Δq / T² = 5.7735 × Δq / T²

Now let's compare against the trapezoidal profile under the same conditions. Move Δq = 1.5708 rad in T = 2.0708s:

v_max = 1.875 × 1.5708 / 2.0708 = 1.4223 rad/s
a_max = 5.7735 × 1.5708 / 2.0708² = 2.1149 rad/s²

Same total time, but the max velocity required is 1.4223, not 1.0 — 42 percent faster. Flip it around: cap max velocity at 1.0 instead, and:

T = 1.875 × 1.5708 / 1.0 = 2.9452s

The 2.0708-second move becomes 2.9452 seconds. Same 42 percent difference again.

ItemTrapezoidalQuintic polynomial
Max velocity at the same total time (2.0708s)1.0000 rad/s1.4223 rad/s
Max acceleration at the same total time2.0000 rad/s²2.1149 rad/s²
Time needed with max velocity capped at 1.02.0708s2.9452s
JerkInfinite at start and endFinite everywhere
Motor performance utilizationHighLow
Computation cost3 conditional branches6 coefficients, no branches

What this table says isn't that one is superior. Industrial robots use the trapezoidal family because cycle time directly is throughput, and precision measurement equipment or camera gimbals use polynomials because residual vibration directly is quality. The S-curve profile, a blend of both, is actually the most widely used in practice — think of it as a trapezoidal profile with its accel/decel corners rounded off by a polynomial.

import numpy as np


def quintic(q0, qf, T):
    """Returns the coefficients of a 5th-order polynomial whose velocity and acceleration are both zero at both ends."""
    d = qf - q0
    return np.array([q0, 0.0, 0.0, 10 * d / T ** 3, -15 * d / T ** 4, 6 * d / T ** 5])


def evaluate(coef, t):
    powers = np.array([t ** i for i in range(6)])
    dpow = np.array([i * t ** (i - 1) if i >= 1 else 0.0 for i in range(6)])
    ddpow = np.array([i * (i - 1) * t ** (i - 2) if i >= 2 else 0.0 for i in range(6)])
    return coef @ powers, coef @ dpow, coef @ ddpow


T = 2.0
c = quintic(0.0, np.radians(90), T)
print("Coefficients:", np.round(c, 6))
for t in (0.0, 0.5, 1.0, 1.5, 2.0):
    q, v, a = evaluate(c, t)
    print(f"  t={t}s  q={np.degrees(q):8.4f}°  v={v:8.5f} rad/s  a={a:8.5f} rad/s²")
print(f"  predicted max velocity {1.875*np.radians(90)/T:.6f}, max acceleration {10/np.sqrt(3)*np.radians(90)/T**2:.6f}")

Here's the output.

Coefficients: [ 0.        0.        0.        1.963495 -1.472622  0.294524]
  t=0.0s  q=  0.0000°  v= 0.00000 rad/s  a= 0.00000 rad/s²
  t=0.5s  q=  9.3164°  v= 0.82835 rad/s  a= 2.20893 rad/s²
  t=1.0s  q= 45.0000°  v= 1.47262 rad/s  a= 0.00000 rad/s²
  t=1.5s  q= 80.6836°  v= 0.82835 rad/s  a=-2.20893 rad/s²
  t=2.0s  q= 90.0000°  v= 0.00000 rad/s  a= 0.00000 rad/s²
  predicted max velocity 1.472622, max acceleration 2.267249

At t=1.0, it's exactly halfway through at 45 degrees, with velocity peaking at 1.4726, and velocity and acceleration are both zero at both ends. The predicted max velocity matches the sampled value.

The Different Paths Joint-Space and Task-Space Interpolation Produce

Here's a fork in the road. Should you generate the trajectory in terms of joint angles, or in terms of end-effector position?

Joint-space interpolation fills in the profile above between the start and end angle. Each joint follows its own trajectory independently. It's cheap to compute, joint velocity and acceleration limits are directly respected, and you don't need to worry about singularities.

Task-space interpolation connects the start and end position with a straight line, and solves inverse kinematics at every point along that line to get the joint angles. The end effector actually traces a straight line.

Let's calculate exactly how different the paths these two methods produce can be. Same 2-link arm as in the forward kinematics post: upper arm L1 = 0.20 m, forearm L2 = 0.15 m.

With the start pose at (30°, 45°) and the end pose at (-30°, 45°), the end effector is at:

Start (0.212028, 0.244889) m
End   (0.318094, -0.061177) m

In joint space, the exact midpoint (0°, 45°) puts the end effector at:

x = 0.20·cos(0) + 0.15·cos(45°) = 0.20 + 0.106066 = 0.306066
y = 0.20·sin(0) + 0.15·sin(45°) = 0 + 0.106066 = 0.106066

In task space, the midpoint of the straight line is the average of the two endpoints:

x = (0.212028 + 0.318094) / 2 = 0.265061
y = (0.244889 - 0.061177) / 2 = 0.091856

The distance between the two points is:

√((0.306066 - 0.265061)² + (0.106066 - 0.091856)²)
= √(0.041005² + 0.014210²)
= √(0.00168141 + 0.00020193)
= 0.043398 m = 43.4 mm

The two methods connect the exact same two points, and diverge by 43 millimeters in the middle. The arm's total length is 350 millimeters, so that's over 12 percent. If the motion is pouring water into a cup, this difference is the line between success and failure. On the other hand, if the motion is just moving from point A to point B above a box, a path that bulges out by 43 millimeters can actually help clear an obstacle.

CriterionJoint-space interpolationTask-space interpolation
End-effector pathA curve that's hard to predictA straight line (or whatever curve you specify)
Joint velocity limitsDirectly respectedIndirect, easy to violate
SingularitiesFine passing throughJoint velocity diverges passing through
Per-cycle computationOne polynomial evaluationOne inverse-kinematics solve
Solution-flip problemNoneCan jump between elbow-up and elbow-down
Used forPoint-to-point moves, returning to a rest poseWelding, coating, insertion, pouring

The last row is where this hurts most in practice. Solve inverse kinematics independently at every point along the line, and at some point a different solution can suddenly get selected, flipping the elbow to the opposite side. The end effector is still on the straight line, but the whole arm has jumped to the other side within a single cycle. The fix is to force each solve to pick the solution closest to the previous one, instead of solving fresh every time.

PID — What Each of the Three Terms Actually Fixes

Once the trajectory produces a target angle every cycle, the lower layer needs to make the actual angle follow it.

Let's model a single joint like this. Rotational inertia J = 0.02 kg·m², viscous friction b = 0.05 N·m·s/rad, and the shoulder's gravity torque of 1.79 N·m from the forward kinematics and static torque post pulling downward. The target is 90 degrees.

J·q̈ = τ - b·q̇ - τ_g

Let's switch on the three terms one at a time.

The proportional term pushes proportional to the error. Set Kp = 20 and turn on only this term, and the joint blows well past the target and oscillates for a long while. Friction alone can't remove enough energy.

The derivative term pulls back proportional to velocity. Physically, it's creating artificial friction. Add Kd = 1.0 and the overshoot disappears. But it never reaches the target — it stops 5.13 degrees short.

This 5.13 degrees isn't a random value. At rest, velocity is zero, so the derivative term is zero, and the proportional term alone balances against gravity.

Kp × e = τ_g
e = 1.79 / 20 = 0.0895 rad = 5.129°

Since the end effector is 0.35 meters from the shoulder:

0.0895 rad × 0.35 m = 0.0313 m = 31.3 mm

It sags 31 millimeters. The arm sits quietly, its error stable, but it's simply in the wrong place.

The integral term eliminates this leftover error by accumulating it over time. Add Ki = 40 and steady-state error drops to 0.07 degrees. In exchange, it produces 4.75 degrees of overshoot. The integral term remembers the past, so its response lags a beat.

import numpy as np

J_INERTIA = 0.02     # kg·m²
B_VISCOUS = 0.05     # N·m·s/rad
TAU_GRAVITY = 1.79   # N·m, torque gravity produces when the shoulder is horizontal
DT, T_END = 0.001, 2.0
REF = np.radians(90)


def simulate(kp, ki, kd, feedforward=0.0):
    q, dq, integral = 0.0, 0.0, 0.0
    log = []
    for _ in range(int(T_END / DT)):
        e = REF - q
        integral += e * DT
        tau = kp * e + ki * integral + kd * (0.0 - dq) + feedforward
        ddq = (tau - B_VISCOUS * dq - TAU_GRAVITY) / J_INERTIA
        dq += ddq * DT
        q += dq * DT
        log.append(q)
    return np.array(log)


for name, (kp, ki, kd, ff) in {
    "P only":      (20, 0, 0.0, 0.0),
    "PD":          (20, 0, 1.0, 0.0),
    "PID":         (20, 40, 1.0, 0.0),
    "PD+gravity":  (20, 0, 1.0, TAU_GRAVITY),
}.items():
    log = simulate(kp, ki, kd, ff)
    err = np.degrees(REF - log[-1])
    over = max(0.0, np.degrees(log.max() - REF))
    print(f"{name:<11} final {np.degrees(log[-1]):8.4f}°  steady-state error {err:8.4f}°  overshoot {over:7.4f}°")

Here's the output.

P only      final  78.4275°  steady-state error  11.5725°  overshoot 69.8285°
PD          final  84.8720°  steady-state error   5.1280°  overshoot  0.0000°
PID         final  90.0718°  steady-state error  -0.0718°  overshoot  4.7486°
PD+gravity  final  90.0000°  steady-state error  -0.0000°  overshoot  0.6777°

PD's steady-state error of 5.1280 degrees matches the 5.129 degrees we worked out by hand. When theory and simulation agree, that means the model is right.

The 0.6777 degrees of overshoot left over on the last line means something too. Strip gravity out entirely and what's left is a pure second-order system, and computing the damping ratio at these gains:

ζ = (Kd + b) / (2·√(Kp·J)) = 1.05 / (2·√(20 × 0.02)) = 1.05 / 1.2649 = 0.830

At 0.83, the theoretical overshoot is about 0.9 percent, or 0.84 degrees on a 90-degree move. The same order of magnitude as the simulation's 0.68 degrees. The reason PD alone had zero overshoot wasn't that the controller was doing a great job — it was that gravity kept pulling the arm downward, acting as a free brake. Gravity compensation removes that free brake too, so once you turn it on, you need to look at Kd again.

There's one device you absolutely need to add whenever you use the integral term. If the arm is physically blocked, or the motor hits its torque limit, the error stops shrinking — but the integral term keeps accumulating the whole time regardless. The moment the obstacle clears later, that hugely inflated integral term flings the arm violently. This is integral windup, and the fix is to freeze the integral once the output saturates, or cap the integral value itself.

// PID for a single joint on an Arduino. Assumes it runs on a fixed period.
const float KP = 20.0f, KI = 40.0f, KD = 1.0f;
const float DT = 0.005f;             // 200Hz. This value must match the actual period.
const float I_LIMIT = 3.0f;          // Integral windup guard, in N·m terms
const float TAU_LIMIT = 8.0f;        // Torque limit the motor can produce

float integral = 0.0f;
float prevMeasured = 0.0f;

float pidStep(float target, float measured, float gravityFeedforward) {
  float error = target - measured;

  // The derivative is computed from the measurement, not the error. If the target
  // jumps like a step, the derivative of the error would momentarily explode
  // and the output would spike.
  float derivative = -(measured - prevMeasured) / DT;
  prevMeasured = measured;

  float unsaturated = KP * error + KI * integral + KD * derivative + gravityFeedforward;

  // Don't keep accumulating the integral in the direction that's pushing into the limit.
  bool pushingIntoLimit =
      (unsaturated > TAU_LIMIT && error > 0) || (unsaturated < -TAU_LIMIT && error < 0);
  if (!pushingIntoLimit) {
    integral += error * DT;
    if (integral > I_LIMIT) integral = I_LIMIT;
    if (integral < -I_LIMIT) integral = -I_LIMIT;
  }

  float tau = KP * error + KI * integral + KD * derivative + gravityFeedforward;
  if (tau > TAU_LIMIT) tau = TAU_LIMIT;
  if (tau < -TAU_LIMIT) tau = -TAU_LIMIT;
  return tau;
}

Pay attention to computing the derivative from the measurement rather than the error. The instant a step-changing target arrives, the derivative of the error is theoretically infinite, and in practice the output spikes to its limit within a single cycle. Using the derivative of the measurement means this term only ever looks at the joint's actual velocity, no matter how the target changes.

Feedforward and Gravity Compensation

Let's go back to the last line of the simulation above. Adding the gravity torque straight into PD brought steady-state error to 0.0000 degrees — without using the integral term at all.

This is feedforward. Feedback only reacts after an error has already appeared. But gravity is predictable. Knowing only the joint angle, you can compute how much gravity is pulling right now. There's no reason to wait for the arm to sag and then correct it.

Let's write the shoulder's gravity torque as a function of angle. Multiply each mass's horizontal distance by its weight and add them up.

τ_g(θ1, θ2) = g × [ m1·(L1/2)·cos(θ1)
                  + m2·(L1·cos(θ1) + (L2/2)·cos(θ1+θ2))
                  + (m_grip + m_pay)·(L1·cos(θ1) + L2·cos(θ1+θ2)) ]

Plug in the values from the earlier post (m1 = 0.15, m2 = 0.10, gripper 0.15, payload 0.25 kg):

Shoulder angleElbow angleGravity torque
0° (horizontal)1.7903 N·m
30°1.5505 N·m
60°0.8952 N·m
90° (vertical)0.0000 N·m
45°1.5964 N·m
90°1.1282 N·m

Standing it up vertically, it's exactly zero, and folding the elbow shrinks it as the center of mass moves inward. This table is exactly the feedforward term. Compute this formula every cycle with the current angle and add it to the control output, and feedback only has to handle whatever the model missed.

Something worth being honest about here: this equation is a model. If it doesn't precisely match the arm's real mass distribution, the computed value will be off, and feedback still has to handle whatever's off by. Even so, having feedback carry the full 1.79 N·m versus only 0.2 N·m is a completely different problem. The Kp you need shrinks accordingly, and a smaller Kp means less noise amplification and less vibration too.

Gravity isn't the only thing you can feed forward. Since the trajectory already knows the target velocity and target acceleration:

τ_ff = J·q̈_desired + b·q̇_desired + τ_g(q_desired)

Put all three terms in and, when the model is perfect, feedback output goes to zero. This is called computed torque control, and it's largely what an industrial robot's controller does. Making feedback work only as hard as the model is wrong is the general direction of control design.

How Control Period and Latency Eat Away at Stability

Everything up to this point was continuous time. A real controller runs discretely, and that fact directly affects stability.

A digital control loop has at least two sources of delay: the zero-order hold's average half-period, and the time it takes to read the sensor, compute, and produce the output. A common practical rule of thumb is to lump these together as roughly 1.5 periods.

Pure delay's effect on phase is proportional to frequency.

Phase lag (radians) = ω × T_delay

Take the crossover frequency — where the loop gain equals 1 — as 20 rad/s (about 3.2Hz) and calculate:

Control rateEffective delay (1.5 periods)Phase lag at 20 rad/s
1000Hz1.50 ms1.72°
200Hz7.50 ms8.59°
100Hz15.00 ms17.19°
50Hz30.00 ms34.38°
50Hz:   20 × 0.030 = 0.600 rad = 34.38°
1000Hz: 20 × 0.0015 = 0.030 rad = 1.72°
Difference: 32.66°

Drop the control rate from 1 kilohertz to 50 hertz and 32.7 degrees of phase margin just vanishes. A typical design target is 45 to 60 degrees, so losing 32.7 degrees leaves almost nothing. Not a single gain got touched, and yet the system oscillates.

The symptom has a distinctive shape. The arm trembles near the target at a fixed frequency, quiets down if you lower the gain, and trembles at the same frequency again if you raise it back up. If that frequency has anything to do with the control period, the cause is delay, not gain.

A wobbling period is the same kind of problem. It's common to see code that puts PID inside loop() on an Arduino and matches the period with delay(5) — but the actual period is 5 milliseconds plus whatever computation time got added on top, and it stretches noticeably further if there's even a serial print in there. Since the derivative and integral terms both divide and multiply by DT, if DT doesn't match reality, the gains get skewed by that same ratio. Using a hardware timer interrupt for a fixed period, or at minimum measuring actual elapsed time and using that, is the safer approach.

Software isn't the only source of delay. Communication is delay too. If you're daisy-chaining smart servos on a single bus, the time it takes to read the state of all six joints in one pass becomes your minimum control period. If you're reading an encoder over I2C, I2C bus characteristics mean clock stretching and slave response time land directly in your period.

Why Gains Tuned in a Simulator Break on Real Hardware

In this post's simulation, Kp = 20 and Kd = 1.0 worked beautifully. Put those same values on a real arm and they usually break. There are several reasons, and every one of them is something the model didn't have.

First, backlash. In the simulation, the motor shaft and the joint shaft are at the same angle. In reality, they're off by however much slop the gearbox has. In that region, the motor turns but the joint doesn't move, so as far as the controller's concerned, gain is zero in that zone. The instant the teeth mesh again, gain snaps back to normal. A system whose gain differs region to region is hard for a single set of gains to handle well everywhere. The symptom is a low-frequency limit cycle near the target — small oscillations back and forth that never settle.

Second, joint flexibility. Links were treated as rigid, but real ones flex. A harmonic drive uses an elastic element by design, a 3D-printed link visibly bends, and a belt stretches. This flexibility creates resonance between the motor and the link, and above that resonant frequency, phase flips 180 degrees. The simulation's rigid-body model doesn't have this pole at all.

Third, friction isn't viscous. The model's b·q̇ is viscous friction proportional to velocity, but what actually dominates in a real joint is Coulomb friction, independent of velocity, and static friction, which is even larger at rest. At low speed you get stick-slip — sticking, then slipping, repeatedly. This is exactly why commanding very slow motion is the hardest command to execute well.

Fourth, encoder quantization and derivative noise. A 12-bit encoder divides one revolution into 4096 steps, giving a resolution of 0.0879 degrees. Differentiate a single tick of noise at 200Hz:

0.0879° / 0.005 second = 17.6°/second

Even with the joint completely still, if the reading wobbles by one tick, the derivative term sees a velocity equivalent to 17.6 degrees per second. With a large Kd, this noise goes straight through to the output and the motor whines. That's why the derivative term almost always gets a low-pass filter.

Fifth, the power supply. In the simulation, the motor produces the requested torque instantly. In reality, if the power driving the motor can't deliver the current, torque doesn't show up. The instant several joints accelerate at once, voltage collapses, and the controller keeps computing its next step believing it commanded that torque and got it. If things only go strange when the arm moves into a heavy pose, measure current before you touch the gains.

The practical order looks like this. What you get from a simulator isn't your final gains — it's the order of magnitude and structure of the gains. It's a tool for confirming which terms you need, roughly what size they should be, and whether the trajectory stays within joint limits. On real hardware, turn on gravity compensation first, hold Ki and Kd at zero and raise Kp until it starts oscillating, then halve it; next raise Kd to damp the oscillation; and only at the very end, if steady-state error remains, add in a very small Ki. Change the order and you lose track of what fixed what.

Conclusion — A Good Controller Is Usually a Good Trajectory

The countermeasures covered in this post operate at different layers.

Trajectory generation is about never demanding the impossible in the first place. No controller can execute "get to 90 degrees in 0.02 seconds" well. Tell it to get there in 2.07 seconds, and even an ordinary controller follows along just fine.

Feedforward is about feeding in what you already know ahead of time. Gravity can be computed at every instant — there's no reason to wait for an error to appear first.

Feedback handles everything else: friction the model missed, unexpected external forces, part-to-part variation. The smaller that remainder is, the smaller the gains can be, and the smaller the gains are, the more forgiving the whole system becomes.

When you run into a jerky arm, the first thing to check isn't the gains — it's the command. Problems that no amount of gain-tweaking ever fixed disappear all the time with a single line of trajectory. Delay comes next, and gains are usually last.

There's one experiment you can run yourself to confirm this order: cut the control period in half. If the symptom improves, it's a delay problem; if it doesn't change, it's a model problem. Gains are what you touch after you've told those two apart. The math underlying all of this is laid out in order in the math you need for robotics post.

현재 단락 (1/276)

Say [inverse kinematics](/blog/electronics/2026-08-02-inverse-kinematics-explained) has finished its...

작성 글자: 0원문 글자: 22,767작성 단락: 0/276