← Back to list

Mastering Balance: PID Control of a Pendulum Cart

How three math terms, an encoder, and careful tuning turns a falling stick into a self-balancing system.

Adithyarajagopalan · 2026-04-29 02:31 · 1 claps · 8.9 min read
#pendulum-cart #pid-controller #control-system
Open on Medium ↗
Wiki topics: 📐 · Mathematics

Mastering Balance: PID Control of a Pendulum Cart

How three math terms, an encoder, and careful tuning turns a falling stick into a self-balancing system.

1. PID Controller

Any physical device that’s being designed, be it a motor, a heater, a drone, usually involves the concept of PID. It stands for Proportional, Integral, and Derivative, and it is one of the most widely used ideas in engineering. It’s a system that watches what’s happening, compares it to what should be happening, and continuously nudges things in the right direction.

More formally, a PID controller is a closed-loop feedback mechanism. It reads a sensor value, compares it to a desired target called the setpoint, calculates how far off things are, called the error, and computes a corrective output to bring the system back in line. Then it does this in a high frequency loop until the system is where it needs to be.

PID Controller doesn’t just react to the current error, but learns from past errors and predicts the future error. The best way to understand it intuitively is with similar to a car on a road. Imagine you’re driving a car and trying to stay perfectly centred in your lane:

  • P (Proportional) sees how far right or left of centre you currently are and steers proportionally. A small drift gets a gentle correction while a large drift gets a sharp one.
  • I (Integral) notices that a persistent side-wind has been slowly pushing you off course for the last few seconds, and compensates for that accumulated drift, even if the current error looks small.
  • D (Derivative) watches how quickly you’re moving toward centre and predicts that you’re steering so hard you’re about to overshoot to the other side, so it eases off the sharpness of the change before you get there.

Together, these three components give the controller a sense of the present, the past, and the future.

2. The Math of The PID Equation

We start with one goal, drive the error to zero.

e(t) = setpoint - measured value

The simplest approach is to make the output proportional to the error:

u(t) = Kp · e(t)

This works, but has a fundamental problem. At steady state, a nonzero output requires a nonzero error, meaning the system must stay slightly off target just to maintain the corrective force, and proportional control alone cannot eliminate this residual offset.

So we add the integral of error over time:

u(t) = Kp · e(t)  +  Ki · ∫e(τ)dτ

Even a tiny error now causes the integral to grow, increasing the output until e(t) = 0. Mathematically, that is the only condition where the integral stops growing.

But now the controller has no awareness of how fast the error is changing. If the error is large but shrinking rapidly, both terms are still producing large outputs, and the system will overshoot. Adding the derivative term fixes this:

u(t) = Kp · e(t)  +  Ki · ∫e(τ)dτ  +  Kd · de(t)/dt

When the error is collapsing quickly, de(t)/dt is negative, subtracting from the output and braking the system before it overshoots. Each term exists to counter the issue from the previous one.

Thus at the heart of every PID controller is one equation:

u(t) = Kp·e(t)  +  Ki·∫e(τ)dτ  +  Kd·(de/dt)

e(t) is the error at time t, basically the setpoint minus the current measured value.

The Proportional term — Kp·e(t)

This is the simplest and most intuitive part. The output is directly proportional to the current error. Double the error, double the output. The gain ***Kp*** is a tuning knob, so if its too low the system responds sluggishly, if its too high it becomes jittery and unstable. On its own, a pure P controller almost always leaves a small residual error, because as the error approaches zero, so does the corrective force, and friction or gravity can hold the system just off-target indefinitely.

The Integral term — Ki·∫e(τ)dτ

This term accumulates all the error seen so far. If the system has been slightly off target for a long time, the integral grows large enough to overcome whatever is resisting it, be it friction, gravity bias, mechanical slop. It’s what gives the controller “memory.” The downside is that if you’re very far from the setpoint for a long time, the integral can grow to enormous values. When the system finally comes back near target, it has so much accumulated “momentum” that it massively overshoots. This is called integral windup, and handling it is one of the biggest challenges in PID implementation.

The Derivative term — Kd·(de/dt)

This term looks at how fast the error is changing. If the error is shrinking rapidly, the derivative term pushes back, predicting that you’re about to overshoot and applying a braking force before you get there. It’s sort of what a PID controller has for future-prediction. In practice, derivative control is sensitive to noisy sensors because any high-frequency jitter in the measurement gets amplified into large spikes in the derivative. For this reason, it’s common to apply a low-pass filter to the derivative term in systems.

3. The Inverted Pendulum Challenge

The project uses an inverted pendulum on a cart, a classic control systems benchmark. The rod stands upright, with its centre of mass above the pivot point. Gravity is now a destabilising force due to the position of the centre of gravity, any tiny deviation causes the pendulum to accelerate away from vertical, not back toward it.

A common misconception is that one can just write a simple conditional rule. The problem is that binary logic produces binary output, so the motor is either full-on or full-off. On a physical system with inertia, this causes wild oscillations. The key insight is that the size of the correction needs to match the size of the problem. PID does this naturally . A small lean gets a small nudge, a large lean gets a strong torque, and as the pendulum approaches vertical, the corrections taper off smoothly.

4. Implementation in the Code

The loop structure: multi-rate scheduling

The first architectural choice is running two loops at different speeds:

if (micros() - pendulumLastTime >= Ts_us) {
    pendulumLastTime += Ts_us;
    runPID();
}
if (millis() - cartTimer >= cartPeriod_ms) {
    cartTimer += cartPeriod_ms;
    runCartControl();
}

The pendulum loop runs at 1,000 Hz because balancing requires fast reaction. A pendulum can fall from nearly upright to completely horizontal in under half a second. The cart control loop only needs to run at 50 Hz since human RC input changes slowly. By separating these concerns and using non-blocking timer checks instead of delay(), both tasks share the CPU without interfering with each other. This is called multi-rate control scheduling and is a standard technique in embedded systems that have different input frequency requirements.

Delta time

unsigned long currentTime = millis();
float dt = (currentTime - lastTime) / 1000.0;
if (dt <= 0) return;

This single line is more important than it looks. The integral term is error × dt — if dt is wrong, the integral accumulates at the wrong rate. The derivative is (error change) / dt — if dt is wrong, the derivative gives the wrong rate of change. The condition if (dt <= 0) return prevents division by zero on the first iteration.

The encoder via hardware interrupt

void IRAM_ATTR handleEncoder() {
  int stateA = digitalRead(encoderA);
  int stateB = digitalRead(encoderB);
  stateA == stateB ? pulseCount++ : pulseCount--;
}

The encoder is attached to a hardware interrupt, meaning the ESP32 drops everything and runs this function the instant a pulse edge is detected, regardless of what the main loop is doing. Placed in IRAM, it runs faster than code loaded from flash. With 336 pulses per revolution, the angle is derived as (pulseCount / 336.0) × 360°. Using an interrupt here is the right decision since polling the encoder in the main loop would miss pulses during any busy period, causing the angle calculation to drift and the PID to work from incorrect data.

The motor and PWM output

The final output of the PID math is a single number, the output u(t). What moves the pendulum is a DC motor driven by an L298N motor driver, and it takes PWM output as a rapid on-off signal whose duty cycle controls how much power reaches the motor. The PID output gets converted to a PWM value and constrained between a range because outside that the motor doesn’t have enough voltage to overcome its own static friction and just sits there humming. The sign of the output determines direction, so positive error spins the motor one way, negative error spins it the other, by toggling the direction pins on the L298N.

if (output > 0) {
    ledcWrite(motorD0, pwmValue);
    ledcWrite(motorD1, 0);
} else if (output < 0) {
    ledcWrite(motorD0, 0);
    ledcWrite(motorD1, pwmValue);
}

Anti-windup to contain the integral

integral += error * dt;
integral = constrain(integral, -50, 50);

This is the practically most important line in the integral implementation. Without it, if the pendulum falls over and the robot sits in an error state for a few seconds, the integral accumulates into a huge number. When the pendulum is righted, the controller tries to pay back that accumulated error with massive motor output, causing an violent overshoot in the opposite direction. Clamping the integral to ±50 ensures it can never build up enough energy to cause this runaway behaviour.

The dead zone

if (abs(error) < 0.5) {
    integral  = 0.0;
    lastError = 0.0;
    pwmValue  = 0;
    return;
}

When the error drops below half a degree, the motor is switched off entirely. This is intentional. DC motors have a minimum threshold of current needed to overcome static friction. Commanding a PWM value below that threshold just wastes energy and causes the motor to buzz without actually moving. Another important reason for implementation of deadband is that the tiny oscillations of trying to correct a 0.5° error can actually destabilise the system. A small dead zone around the setpoint gives the system a good enough resting state and prevents unnecessary motor output.

5. Issues Faced & Tuning

The large angle recovery problem

The biggest issue was what happened beyond 60 degrees. Once the pendulum reached the setpoint and held at 90° it would stay balanced, but the moment it deviated too far, past roughly 60° in either direction, the motor couldn’t respond strongly enough to pull it back. The PID output wasn’t aggressive enough to catch it in time.

The core issue is that PID is linear. The same gains that produce smooth, controlled corrections near the setpoint are often too weak to handle large deviations where the system is moving fast and gravity is fully working against system. Near 90°, a small Kp works well. But at 40° or 30°, that same Kp produces an output that is just not physically powerful enough to reverse the fall.

The tuning process

Without a simulation on a software like matlab, the tuning of Kp, Kd and Ki must be done manually. This involves going through each combination in a particular range, uploading the respective code to the ESP32 and analysing the system. The procedure to tune involves 3 steps:

  1. Start with Kp only — Set Ki = 0 and Kd = 0. Increase Kp gradually until the pendulum starts oscillating around the setpoint, bouncing back and forth rhythmically. This tells you P is strong enough to respond, but has no damping. The oscillation frequency is also a good indicator of the system's natural dynamics.
  2. Add Kd — Increase Kd from zero while keeping Kp fixed. The derivative term will start resisting the rapid angle changes, and the oscillation will slow down and eventually die out. The pendulum should now hold close to vertical with some stiffness. If it becomes jerky or twitchy, the sensor is likely noisy and Kd needs to be reduced.
  3. Add a small Ki to hit the setpoint — If the pendulum holds steady but at a small deviation, say, 88° instead of exactly 90°, that residual error is motor friction or a gravity imbalance that P alone can’t overcome. A small Ki will slowly accumulate the error and push the system exactly onto target. Ki must be kept very small, because the anti-windup clamp gives you a safety net, but a large Ki still causes sluggish, overshooting behaviour.

The process is iterative and nonlinear. Changing one gain affects how the others should be set. The system must be put through the steps several times before the system feels right.

6. Additional Mentions

The Serial Plotter as a tuning tool — The code already prints angle and PWM data over Serial. The Arduino Serial Plotter can turn that into a live graph in real time, which at different stages of tuning like the initial instability, the oscillation under P-only control or the damped response after adding D are some of the most effective visuals to analyse.

PWM frequency and motor noise — The pendulum motor uses ledcAttach(motorD0, 50, 8). The PWM frequency is pwmFreq = 20000 (20 kHz), which is above the threshold of human hearing and far more appropriate for a DC motor driver. Using 20 kHz would make the robot quieter and the motor driver run cooler.

7. Conclusion

PID is three arithmetic operations running a thousand times per second, each one informed by a sensor reading and a operation. PID is one of the oldest ideas in modern engineering, and it still shows up in washing machines, robots, autopilot systems and self-driving cars. Figure out the governing equation, tune the parameters, and watch your real systems come to life.

GitHub link: https://github.com/AdithyaRajagopalan24/Self-Balancing-Inverted-Pendulum-Cart---ME2400-Project

Mentor for the project : Professor Manivannan P V, IIT Madras


메타데이터
post_id
f3cf857568e4
slug
mastering-balance-pid-control-of-a-pendulum-cart-f3cf857568e4
url
https://medium.com/@adithyarajagopalan/mastering-balance-pid-control-of-a-pendulum-cart-f3cf857568e4
canonical_url
https://medium.com/@adithyarajagopalan/mastering-balance-pid-control-of-a-pendulum-cart-f3cf857568e4
author_url
https://medium.com/@adithyarajagopalan
status
ok
fetched_at
2026-06-09 15:37:30