ICAM Toulouse · Group 8A · Model, Intelligence & Autonomy referent

DELIVERWHOA two-wheeled delivery robot for the campus. No kickstand, no third wheel: it stays upright because the code says so.

My role
MIA referent: behaviour laws, Simulink model, C++ firmware, controller interface
Team
7 engineers · CAD, sizing, electronics, quality, procurement
Brief
5 kg payload · 50×30×30 cm · 2 h autonomy · 80 cm doors
Budget
€500 cap, reached
Deliverables
43-page technical dossier · assembly guide · test videos
SolidWorks assembled view of the robot
SolidWorks assembly · aluminium profiles, ABS plates
Stability test: the robot balances on two wheels in a corridor
Stability test 3 · 11 June 2025
01 · The problemCampus logistics

Parcels on campus were hand-delivered. The volume kept growing; the people didn’t.

The robot has to cross indoor and outdoor ground, fit through standard doors and lifts, carry a 5 kg parcel and run two hours. Two driven wheels keep it compact and agile, and turn balance into the central engineering problem.

What I owned

The maths of balance and locomotion, the Simulink digital twin, then the C++ control firmware and the controller interface, with Erwan on the code.

Standards in scope

ISO 13482 (personal-care robots), 9001, 14001, 45001.

02 · Sizing the drivetrainInputs → motor choice
Inputsm = 30 kg · v = 5 km/h = 1.39 m/s · 0 → v in 5 s · wheel Ø 0.5 m
Acceleration a = Δv / Δt0.278m/s²
Driving force F = m · a8.34N
Wheel torque C = F · r2.09N·m
Wheel speed N = v / (2πr)53.1rpm
Mechanical power P = C · ω11.6W
Electrical power at η = 0.814.5W

Chosen: a 57BLR50-24-01 brushless motor (84 W, 24 V, 0.23 N·m at 3500 rpm) behind a 10:1 MG23-G10-D8 gearbox, so 2.3 N·m at 350 rpm with a 400 N load rating.

2.09 < 2.3 N·mrequired torque below gearbox output
53 < 350 rpmrequired speed well inside range
147 < 400 Nwheel load below gearbox rating
03 · Digital twin firstMATLAB Simulink · Simscape Multibody

Tune the loops on the model, then port them to the metal.

A multibody model of chassis, wheels and centre of gravity, with two PID loops: one holds the tilt angle, the other the travel speed. Gains were tuned by hand until a simulated push returned to equilibrium quickly, with low steady-state error and controlled overshoot.

Simulink · angle PID
P4.75I0.49D0.15N5.6
Simulink · speed PID
P0.2I100D2N20
Firmware · main PID
P10.0I0.4D1.5

Continuous tilt control, setpoint driven by the user.

Firmware · fine PID
P3.0I0D0.12

Active only within ±2° of the setpoint to kill micro-oscillations.

04 · Embedded controlC++ · Arduino framework · ESP32
Sensing

One reliable angle

MPU-6050 IMU fused with a complementary filter, 98 % gyroscope / 2 % accelerometer. Auto-calibration at boot averages 1 000 samples.

Actuation

Two drivers, one sign

BLD-4055 brushless drivers under PWM, polarity flipped with the direction of travel. The robot takes small pushes and rights itself.

Interface

A PS5 controller

DualSense over Bluetooth. Left stick sets the tilt setpoint, right stick spins in place, ✕ toggles stabilisation, △ recalibrates the gyro. ±12 deadband on both sticks.

05 · The control loopWhat the firmware does, 200 times a second

The Simulink block diagram became a single C++ loop on the ESP32. Read the IMU, fuse it into one angle, run the right PID, write PWM to both drivers, handle the controller. Same structure as the model, so the gains transferred with only re-tuning.

  • Two PIDs, one output. The main loop tracks the tilt setpoint; inside ±2° a softer fine loop takes over so the robot stops hunting.
  • Filter before control. The gyro integrates cleanly but drifts; the accelerometer is noisy but absolute. 98 / 2 keeps the best of both.
  • Calibrate every boot. 1 000 samples averaged on the ground so the zero angle is the real zero.
  • Deadbands and toggles. ±12 on the sticks; ✕ disarms the loop, △ recalibrates.
// simplified from the ESP32 firmware
void loop() {
  float gyro  = mpu.gyroX() * dt;
  float accel = atan2(mpu.accY(), mpu.accZ());
  angle = 0.98 * (angle + gyro) + 0.02 * accel;

  float target = map(ps5.LStickY(), deadband 12);
  float err = target - angle;
  float out = fabs(err) < 2.0
    ? finePID.step(err)   // P 3.0  I 0    D 0.12
    : mainPID.step(err);  // P 10.0 I 0.4  D 1.5

  float turn = map(ps5.RStickX(), deadband 12);
  driveL.pwm(out + turn);  // BLD-4055, sign = direction
  driveR.pwm(out - turn);

  if (ps5.Cross())    stabilise = !stabilise;
  if (ps5.Triangle()) calibrate(1000);
}
Control loop · C++ · Arduino framework · ps5Controller library
06 · Test logSpring 2025
16 AprBase componentsPartialModules healthy, integration not yet working.
30 AprMotor control · direction ×3ValidatedDrive and steering respond; stability still to do.
11 JunStability ×3Good balanceSlight tipping at steady state.
13 JunControl + stability, fwd/backFunctionalCorrect reaction to commands, one bug left to fix.
07 · Mechanics & costSolidWorks · FEA · Costing
Exploded view of the robot
Exploded view · every part named
Bill of materials
Bill of materials
08 · NextLeft for the next team
Electronics
Custom PCB

KiCad board to replace the breadboard prototype.

Perception
LIDAR + ultrasound

Obstacle detection and indoor SLAM.

Outdoor
GPS

Routing between buildings.

Body
Bumpers, mudguards

And the full electrical integration in CAD.

MATLAB SimulinkSimscape MultibodyC++ · ArduinoESP32MPU-6050SolidWorks · FEAKiCadSysMLOpenProject