case 02 · Personal · 2025
STM32 quadcopter
A flight controller built from scratch on the STM32F446RE — sensor fusion, PID stabilization, PWM motor drive, all in C on bare metal.
↗ github.com/Prateek2174/stm32-quadcopterWhat it does
The firmware reads the IMU and magnetometer over I²C, runs a PID loop against a target orientation, and outputs four PWM signals to the ESCs. UART is used for telemetry and tuning. The whole thing runs bare-metal on an STM32 NUCLEO-F446RE.
Modules I wrote
Each peripheral and sensor has its own driver — header in Core/Inc,
implementation in Core/Src:
Inc/
gpio.h // pin setup
i2c.h // I²C transactions
mpu6050.h// 6-axis IMU
lis3mdl.h// magnetometer
pid.h // PID controller
pwm.h // timer-driven ESC outputs
usart.h // telemetry UART
What was interesting
Most of the work isn't in the controller math — it's in keeping the sensor data honest. The MPU6050 is noisy and drifts; the LIS3MDL helps anchor yaw but adds its own bias. Getting a stable estimate before the PID even runs is most of the battle.
Tuning the PID gains on a real quad is its own thing. You start with conservative P, no I, no D, and inch up until you can hear the oscillation; the textbook says one thing and the physics says another.
float pid_update(PID_controller *pid, float setpoint, float measurement){
//Error signal
float error = setpoint - measurement;
//Proportional
float proportional = pid -> kp * error;
//Integral
pid -> integrator = pid -> integrator + 0.5f*(pid -> ki * pid -> Ts)*(error - pid -> prev_error);
//Anti-wind-up with dynamic integrator clamping
//clamp lower than physical limit
float min_limit_integrator;
float max_limit_integrator;
//Compute integrator limits
if(pid -> max_output_signal > proportional){
max_limit_integrator = pid -> max_output_signal - proportional;
}else{
max_limit_integrator = 0.0f;
}
if(pid -> min_output_signal < proportional){
min_limit_integrator = pid -> min_output_signal - proportional;
}else{
min_limit_integrator = 0.0f;
}
//Clamp Integrator
if(pid -> integrator > max_limit_integrator){
pid -> integrator = max_limit_integrator;
}else if(pid -> integrator < min_limit_integrator){
pid -> integrator = min_limit_integrator;
}
//Derivative (band-limited differentiator)
pid -> differentiator = -(2.0f * pid->kd * (measurement - pid->prev_measurement) +
(2.0f * pid->tau - pid->Ts) * pid->differentiator)
/ (2.0f * pid->tau + pid->Ts);
//Compute Output and Apply Limits
pid -> output = proportional + pid -> integrator + pid -> differentiator;
if(pid -> output > pid->max_output_signal){
pid->output = pid->max_output_signal;
}else if(pid->output < pid->min_output_signal){
pid->output = pid->min_output_signal;
}
//Store error and measurement
pid->prev_measurement = measurement;
pid -> prev_error = error;
return pid -> output;
}
- MCU
- STM32F446RE @ 180 MHz
- IMU
- InvenSense MPU6050 (6-axis)
- Magnetometer
- ST LIS3MDL (3-axis)
- Motor drive
- 4× PWM @ 50 Hz / 400 Hz to ESCs
- Comms
- I²C (sensors) · USART (telemetry)
- IDE
- STM32CubeIDE
What's next
The firmware core is running — sensor fusion, PID loop, and motor outputs are all working. What's still left before it flies:
- RC receiver — wire up a PWM/PPM receiver so it can be flown manually before any autonomous mode is attempted
- Flight testing & PID tuning — get it on a real frame and iterate on gains; textbook values and the actual airframe will disagree
- Battery monitoring — ADC on the battery rail with a low-voltage cutoff to protect the LiPo
- Madgwick filter — replace the current complementary fusion with a proper attitude filter to cut drift on longer flights
- Flight logging — stream IMU, PID state, and motor outputs over UART at high rate for post-flight analysis and tuning