110 lines
5.8 KiB
Markdown
110 lines
5.8 KiB
Markdown
---
|
||
title: "Inverted Pendulum on a Recycled 3D Printer"
|
||
summary: "A pendulum balanced upright on a motorised cart, built from the axis of an old 3D printer and controlled by a Teensy 4.1 at 1 kHz."
|
||
tags: ["Hardware", "Software", "Control"]
|
||
status: "Completed"
|
||
date: 2026-03-22
|
||
repo: "https://github.com/tdehaeze/inverted-pendulum"
|
||
cover: "cover.jpg"
|
||
---
|
||
|
||
## Overview
|
||
|
||
This project is a classic control demonstrator: a rigid pendulum is mounted on a cart and kept upright by moving the cart back and forth.
|
||
The mechanics come from a recycled 3D printer (Alfawise U20): one of its linear axes, with the stepper motor and belt, carries the pendulum pivot.
|
||
A quadrature encoder measures the pendulum angle, and a Teensy 4.1 microcontroller runs the feedback loop at 1 kHz.
|
||
A small PyQt5 application on the computer is used to select the controller, change the gains and setpoints, and record data.
|
||
|
||
{{< figure src="cover.jpg" alt="Photo of the inverted pendulum setup with its main parts labelled" caption="The setup: cart on a belt-driven rail, stepper motor, pendulum encoder and the electronics (labels in French)." >}}
|
||
|
||
## Hardware
|
||
|
||
| Component | Details |
|
||
|-----------|---------|
|
||
| Microcontroller | Teensy 4.1 |
|
||
| Stepper motor | DRV8825 driver, 32 microsteps, 5 µm per step |
|
||
| Pendulum encoder | Quadrature, 4096 steps per revolution (0.088° per step) |
|
||
| Cart travel | ±200 mm hard stop, ±160 mm soft limit |
|
||
| Control rate | 1 kHz, driven by a hardware timer (`IntervalTimer`) |
|
||
|
||
The cart input is an acceleration command.
|
||
It is integrated to a velocity inside the 1 kHz interrupt, and converted into step pulses for the motor driver.
|
||
The pendulum angular velocity is estimated by finite difference of the encoder reading.
|
||
A soft limit blocks accelerations towards the end of the rail, and a hard stop at ±200 mm cuts the motor.
|
||
|
||
## Software
|
||
|
||
The repository has three parts:
|
||
|
||
- **`teensy/`**: firmware (PlatformIO / Arduino framework). Runs the control loop, drives the motor, reads the encoder and streams telemetry over USB serial.
|
||
- **`gui/`**: PyQt5 and pyqtgraph application. Sends text commands to the Teensy (`MODE PD`, `SET KP 12`, `FB ON`, ...), plots the telemetry live and saves it to CSV.
|
||
- **`model/` and `tools/`**: Python scripts for simulation, controller design, model identification and plotting experiments.
|
||
|
||
## Model and identification
|
||
|
||
With φ = θ − π the angle from the upright position and u the cart acceleration, the linearised dynamics around the upright position are:
|
||
|
||
```
|
||
D·φ̈ + c·φ̇ − m·g·l·φ = m·l·u
|
||
```
|
||
|
||
where D is the effective inertia of the pendulum about its pivot.
|
||
The state is [x, ẋ, φ, φ̇] and the input is the cart acceleration.
|
||
|
||
The pendulum length and friction coefficient are identified from a free oscillation: the pendulum hangs down, receives a small push, and the decaying oscillation is recorded for 10 to 50 seconds.
|
||
A Nelder-Mead fit (`tools/fit.py`) then adjusts the model to the measurement.
|
||
|
||
| Parameter | Symbol | Value |
|
||
|-----------|--------|-------|
|
||
| Pendulum mass | m | 0.1 kg |
|
||
| Half length (pivot to centre of mass) | l | 0.326 m |
|
||
| Friction coefficient | c | 3.4 × 10⁻⁴ N·m·s/rad |
|
||
|
||
{{< figure src="identification-fit.png" alt="Measured and fitted free oscillation of the pendulum" caption="Free oscillation of the pendulum: measurement and fitted model (figure labels in French)." >}}
|
||
|
||
{{< video src="identification.mp4" caption="Free oscillation used for the identification." >}}
|
||
|
||
## Controllers
|
||
|
||
All gains are in SI units, and the controller output is the cart acceleration.
|
||
|
||
- **P**: u = −Kp · φ
|
||
- **PD**: u = −Kp · φ − Kd · φ̇
|
||
- **LQR**: u = −K · [x, ẋ, φ, φ̇]
|
||
|
||
The LQR gain vector K is computed in Python (`model/inverted_pendulum.py`, using the `control` library) from weights on the four states and on the input, then entered in the GUI.
|
||
|
||
### PD control
|
||
|
||
A PD controller can hold the pendulum angle at zero, but it does not see the cart position.
|
||
In the experiment below the angle is regulated while the cart drifts until it reaches the end of the rail and the pendulum falls.
|
||
The same controller with other gains acts mostly as a damper: it slows down the pendulum without fully stabilising it.
|
||
|
||
{{< figure src="pd-simulation.png" alt="Simulated PD response of the pendulum angle" caption="Simulated PD response, from a 5° initial angle (figure labels in French)." >}}
|
||
|
||
{{< figure src="pd-experiment.png" alt="Measured angle and cart position with a PD controller" caption="Measured PD experiment: the angle is held near zero while the cart drifts to the end stop (figure labels in French)." >}}
|
||
|
||
{{< video src="pd-control.mp4" caption="PD control: the pendulum is stabilised, the cart slowly drifts." >}}
|
||
|
||
{{< video src="pd-damping.mp4" caption="PD control used as a damper only: the pendulum dynamics are slowed down." >}}
|
||
|
||
### LQR control
|
||
|
||
The LQR feeds back the cart position and velocity as well as the pendulum angle and angular velocity.
|
||
This keeps the cart near its setpoint while the pendulum stays upright.
|
||
Higher weights on the states give faster corrections, at the cost of more aggressive cart motion.
|
||
|
||
{{< video src="lqr.mp4" caption="LQR with a higher gain: angle and cart position are controlled together." >}}
|
||
|
||
{{< video src="lqr-setpoint-step.mp4" caption="LQR following a step in the cart position setpoint while keeping the pendulum upright." >}}
|
||
|
||
## Typical workflow
|
||
|
||
1. Flash the firmware with `pio run -e teensy41 -t upload` from `teensy/`.
|
||
2. Start the GUI (`python gui/program.py`), enable the motor and zero the cart and encoder.
|
||
3. Record a free oscillation (`tools/capture.py`) and fit the model parameters (`tools/fit.py`).
|
||
4. Pick a controller in the GUI, enter the gains, bring the pendulum close to upright and switch the feedback on.
|
||
5. Save the data to CSV and plot it with `tools/plot_experiment.py`.
|
||
|
||
The full instructions and serial command list are in the [README](https://github.com/tdehaeze/inverted-pendulum).
|