Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

67 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Inverted Pendulum — COMP0216 Systems Engineering

Team 3 · UCL MEng Robotics and AI · March 2026

Aayan Islam · Xavier Parker · Peter Neville · Helitha Cooray


A Python simulation and control environment for a self-balancing inverted pendulum cart, built as part of the UCL COMP0216 Systems Engineering module. Includes a full physics simulation with RK4 integration, PID and LQR controllers, an interactive real-time demo, and the SysML model produced in CATIA Magic.

The physical system was built on an Arduino Giga R1 with four Pololu 25D motors, a Broadcom AS22 optical encoder, and two Motoron I²C motor drivers. The embedded code is also included.


Table of Contents


Hardware

Component Detail
Microcontroller Arduino Giga R1 (480 MHz, 1 MB RAM)
Motors 4× Pololu 25D metal gearmotors
Motor drivers 2× Motoron M3S550 (I²C, addresses 16 and 17)
Pendulum encoder Broadcom AS22 optical encoder (4096 CPR)
Position sensing Motor encoders (232.32 CPR after gearing)
Chassis 3 mm laser-cut acrylic
Supply voltage 10.8 V (nominal 12.3 V)
Pendulum length 0.60 m to centre of mass
Cart mass 1.3816 kg
Bob mass 50 g

Repo structure

.
├── simulation/
│   ├── pendulum.py       # Pendulum dynamics — 4th-order Runge–Kutta integrator
│   ├── controllers.py    # PIDController, LQRController, TrajectoryPIDController
│   ├── filters.py        # MovingAverageFilter
│   └── demo.py           # Interactive two-window visualisation with CLI flags
|   └── benchmark_54.py   # Obtains simulation benchmarking metrics for simulations
|   └── generate_plots.py # Visualisations for simulation metrics
│
├── sysml/
│   └── COMP0216_TEAM3_MBSE.mdzip   # CATIA Magic project file (all 6 SysML diagrams)
│
├── Arduino Code/
│   └── inverted_pendulum_pid_serial/
│       └── inverted_pendulum_pid_serial.ino
|   └── lqr_swingup/
│       └── lqr_swingup.ino
|   └── LQR_inverted_pendulum/
│       └── LQR_inverted_pendulum.ino 
│
├── requirements.txt
└── README.md

Getting started

Requirements

  • Python 3.9 or later
  • pip

Tested on Windows 11, Ubuntu 22.04, and macOS 14. No GPU required.

Install

git clone https://github.com/ItzSmudge/SysEng_coursework.git
cd SysEng_coursework
pip install -r requirements.txt

If you have multiple Python versions installed:

python3 -m pip install -r requirements.txt

Matplotlib backend

The demo opens two live windows and needs a display. On most desktop installs this works without any extra steps. If you hit a backend error (common on headless Linux), set the backend before running:

export MPLBACKEND=Qt5Agg   # or TkAgg, Qt6Agg, wxAgg depending on what's installed

Running the demo

demo.py opens two windows simultaneously:

  • Window 1 — pendulum animation with gain sliders, disturbance buttons, and playback controls
  • Window 2 — live plots of pendulum angle, cart position, velocities, and control input, updating in real time as the simulation runs

Flags

Flag Options Default Description
--eval A B C A Evaluation scenario
--controller lqr pid lqr Controller type
--angle 6.5 10 15 6.5 V-block start angle in degrees (Eval B only)
--jerk off Eval B: run open-loop jerk swing-up before handing to the controller

Examples

# Eval A — LQR balancing from upright
python simulation/demo.py --eval A --controller lqr

# Eval A — PID
python simulation/demo.py --eval A --controller pid

# Eval B — start at 6.5°, controller balances directly from the angle (no jerk)
python simulation/demo.py --eval B --controller lqr --angle 6.5

# Eval B — start at 10°, open-loop jerk swing-up then LQR takes over
python simulation/demo.py --eval B --controller lqr --angle 10 --jerk

# Eval B — 15° with jerk, PID after capture
python simulation/demo.py --eval B --controller pid --angle 15 --jerk

# Eval C — sprint to 2 m while balancing
python simulation/demo.py --eval C --controller lqr

Controls

Playback buttons:

Button What it does
> Play Start or resume
`
<> Reset Return to initial conditions
* Apply Apply slider changes (pauses first)
image image

Disturbance buttons (bottom of Window 1):

  • [TAP] Pendulum — angular impulse on the pendulum (theta_dot). Direction is randomised. Magnitude controlled by the slider next to it.
  • [SHOVE] Cart — linear impulse on the cart (x_dot). Has its own separate magnitude slider.

Both are injected correctly into the physics step — not scheduled through the old simulate() loop, which had a timing bug.

Gain sliders (left panel):

For LQR these adjust the Q matrix diagonal entries and R. For PID they adjust Kp, Kd, Ki for both the angle and position loops. Click * Apply to recompute and continue.


SysML diagrams

All diagrams are in sysml/COMP0216_TEAM3_MBSE.mdzip. Open this file in CATIA Magic to view and edit them.

The project contains six diagrams:

Diagram What it covers
Block Definition Diagram (BDD) System decomposition into SensorSubsystem, ControllerSubsystem, ActuatorSubsystem
Internal Block Diagram (IBD) Signal flow with ports — θ (rad), x (m), u (PWM), I²C addresses 16/17
Use Case Diagram Seven use cases across Operator, Sensor, Actuator, and Evaluator actors
Activity Diagram Eval B swing-up control flow: GO → JERK → coast → monitor → LQR capture
State Machine Diagram IDLE / JERK / BALANCING / FALLEN states with guard conditions and outputs
Requirements Diagram REQ-01/02/03 and Constraints with deriveReqt and satisfy relationships

If you do not have CATIA installed, PNG exports of all six diagrams are in the project report.


Arduino code

arduino/pendulum_lqr/pendulum_lqr.ino is the embedded implementation that ran on the physical cart during the demo.

To upload: open in the Arduino IDE, select Arduino Giga R1 WiFi as the board, install the Motoron library via the Library Manager, then upload.

How it works:

  • The Riccati equation is solved offline in Python and the resulting K vector is hardcoded. The Giga does not run Riccati at runtime.
  • Sensor sampling runs at 10 kHz via mbed::Ticker interrupt. The control loop runs at 100 Hz.
  • A 5-sample moving average filter is applied to both angle and position signals before the controller reads them.
  • A +200 unit motor bias is added to all non-zero commands to overcome the physical deadband of the Motoron drivers.
  • Serial commands: send GO to start, STOP to halt. Gains can be updated at runtime without recompiling — e.g. K 700 2250 sets k_theta and k_theta_dot.
  • Eval B jerk sequence: on GO, the cart runs at 750 raw units for 200 ms (6.5° case) or 230 ms (10° case), coasts for 40 ms, then switches to LQR when |θ| < 0.35 rad.

Notes

The physical hardware was decommissioned on 26 March 2026 following the live demonstration on 24 March. Formal benchmarking data for the hardware was not collected — all quantitative results in the report are from the simulation (Section 5). Hardware observations from pre-demonstration tuning sessions are discussed in Section 6.

The full project planning artefacts — WBS, FMEA, risk matrix, Kanban board — are on the team Miro board linked in the report appendix.


Licence

MIT — feel free to use or adapt this code. Please do not submit it as your own coursework.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages