Sitemap
Sergio Virahonda

Sergio Virahonda

Software Engineer, life-long learner and veeeery techie guy.

Building a Drone Flight Controller from Scratch: A Software Engineer’s Guide to Clean C++

How I built a customizable, low-cost ESP32 flight controller that prioritizes readability, architecture, and maintainability over magic numbers.

20 min readFeb 16, 2026

--

Press enter or click to view image in full size

The “Spaghetti Code” Problem

If you’re reading this, it’s very likely because you’ve found some tutorial out there, checked the open-source flight controller code, and instantly felt a headache coming on. It happened to me MANY times, and this project was the result of that frustration. I promised myself to build something for humans, not vibe-coded, understandable, maintainable, and extendable.

What I noticed out there is the classic software engineering problem: You usually find a single file with 3,000 lines of code. You see global variables named x, y, and temp. You see if statements nested six levels deep. You see magic numbers like PID_P = 3.54 with absolutely no explanation of why that number exists. It works, sure. But it isn't engineering. It’s spaghetti.

When I decided to build my own drone from scratch, I didn’t want to just “vibe code” my way through the physics. I didn’t want to copy-paste a sketch I didn’t understand. As a Software Engineer, I wanted to apply the same principles we use in production software to the chaotic world of aerodynamics. I hope you enjoy it, it took me 6 months of my spare time to build this.

Rapid Prototyping: Fail Fast, Upgrade Faster

I didn’t start with the perfect hardware. I started with an Arduino Nano (ATmega328P). It’s the classic choice for hobbyists, but I quickly hit a hard limit. The 8-bit, 16MHz processor struggled to handle the floating-point math required for a stable PID loop while reading sensors and managing radio communication. My loop times were inconsistent, and the drone felt sluggish. These are some photos of my first iteration. Huge difference, huh? V1 is light-years away from this.

Press enter or click to view image in full size
The V0 implementation. An absolute nightmare, but definitely worked as a baseline for great improvements.
Press enter or click to view image in full size
7 months ago, just figuring out how NRF24s work
Press enter or click to view image in full size
My first TX iteration. I have to highlight that frontend is not my thing 😆

Instead of spending weeks optimizing assembly code for an outdated chip, I pivoted. I switched to the ESP32-S3. Cost-effective, reliable, extendable, and a beast in terms of performance for this purpose.

Besides the board, cheap hardware is not always the best and I learned this by trial and error and hours of frustration. Light crashes will break cheap frames, cheap motors will introduce LOTS of vibration that will affect your gyroscope and accelerometer, and cheap props will break after minor collisions. Instead of looking for cheap hardware, look for decent hardware at decent price. Not necessarily because something’s used in a YouTube tutorial means that it’ll work for you. Avoid the regular plastic F330 frame, 2-blade propellers, and the cheap orange-ish motors — this is the ideal combo for endless frustration. Trust me, I spent months there.

Once this idea about failing fast, but upgrading even faster after learning from my mistakes, got stuck in my head, the difference was night and day. Suddenly, I had a 32-bit, 240MHz dual-core processor with native floating-point support. The “magic” happened almost instantly. The loop frequency jumped from a shaky 100Hz to a rock-solid 500Hz+. The hardware finally had the headroom to support the clean, structured software architecture I wanted to build.

A Flight Controller for Engineers

This project is my answer to the “black box” of drone firmware. It is a Flight Controller written in C++ for the ESP32-S3, designed specifically for software engineers who want to understand how a drone actually flies.

Unlike traditional hobbyist code, this project focuses on:

  • Readability: Code that explains itself. No hacky variables, no alone functions that do many things and nothing at the same time. Small code blocks. Efficient variable use and object-oriented programming.
  • Architecture: We treat the PID controller, the radio receiver, and the IMU as distinct objects with clear interfaces, not a soup of global logic.
  • Extendability: The codebase is designed to evolve. Whether you want to add a barometer, switch to a different remote protocol, or tweak the PID logic for a heavy battery, the architecture supports it without breaking the entire system.
  • Configurability: Tuning a drone shouldn’t require recompiling the kernel. Key parameters are exposed and organized, making it easy to adapt the software to different frame sizes and motor configurations.

High Performance, Low Cost

You don’t need a $100 proprietary flight stack to get stable flight. This entire system runs on a standard ESP32 and an MPU6050 sensor — hardware that costs less than your Windsurf monthly membership.

By optimizing the software rather than throwing money at expensive hardware, we achieve a high-frequency control loop (~500Hz) capable of stabilizing 7-inch “Cruiser” builds, heavy 3S battery setups, and huge carbon fiber frames.

This article isn’t just a tutorial on soldering wires; it is a walkthrough of the software architecture required to defy gravity. We will look at how to structure a real-time control loop, how to handle “dirty” sensor data, and how to write code that you — and others — can actually read six months from now.

The Bill of Materials (BOM)

Total Estimated Cost: ~$130 USD (Drone + Transmitter)

One of the goals of this project was to prove you don’t need expensive, proprietary hardware to achieve stable flight. I used off-the-shelf components available at most electronics stores or online marketplaces. You’ll get a machine capable of autonomous flight for less than the price of a fancy dinner.

The Flight Controller (The Brain)

  1. Microcontroller: ESP32-S3 Development Board
    Cost: ~$4.10 USD (70 MXN)
    Why: Dual-core 240MHz, native floating-point unit (FPU), and plenty of I/O pins. It crushes the Arduino Nano in raw performance.
    Where to find it: AliExpress
  2. IMU (Gyro/Accel): MPU-9250 (SPI Interface)
    Cost: ~$3.05 USD (52 MXN)
    Why: A 9-axis sensor (Gyro + Accel + Magnetometer). While we primarily use the 6-axis Gyro/Accel for stabilization, having the magnetometer opens the door for future compass integration.
    Where to find it: AliExpress
  3. Radio Module: NRF24L01+ PA/LNA
    Cost: ~$1.75 USD (30 MXN)
    Why: The “PA/LNA” version includes a Power Amplifier and Low Noise Amplifier, boosting the range from 10 meters to 1000+ meters with the external antenna.
    Where to find it: AliExpress
  4. Voltage Regulator: UBEC (5V 3A)
    Cost: ~$1.75 USD (30 MXN)
    Why: The ESP32 and NRF24 are power-hungry. A dedicated UBEC ensures “clean” power, separating logic voltage from the noisy motors.
    Where to find it: AliExpress

Propulsion & Frame (The Muscle)

  1. Motors: 4x Hobbywing XRotor (1300KV)
    Cost: ~$40.80 USD (700 MXN total)
    Why: 1300KV is the “sweet spot” for 7-inch props. Low KV = High Torque, which we need to spin larger props efficiently without overheating.
    Where to find it: AliExpress
  2. ESC: 4-in-1 50A BLHeli_S
    Cost: ~$29.15 USD (500 MXN)
    Why: 50A provides a massive safety margin. Even at full throttle punch-outs, we won’t burn this ESC. I personally loved this. Instead of fighting against 4 separate ESCs, trying to properly tie them to the frame arms, and then having to calibrate them, this is pure gold.
    Where to find it: AliExpress
  3. Propellers: 7035 (7x3.5x3) — 4 Pairs
    Cost: ~$5.85 USD (100 MXN)
    Why: Low pitch generates smooth lift for cruising. Buy extras — you will crash while tuning PIDs.
    Where to find these: AliExpress
  4. Frame: GEPRC Mark4 V2 (8-inch version)
    Cost: ~$35.00 USD (Estimated ~600 MXN)
    Why: Carbon fiber is stiff and lightweight. The 8-inch geometry gives us clean air for the 7-inch props (no “prop wash” hitting the camera cage). I loved this frame, definitely game changer.
    Where to find it: AliExpress

The “Secret Sauce” (Crucial Extras)

  1. Anti-Vibration Mount for MPU
    Cost: ~$2.90 USD (50 MXN)
    Why: Critical. The MPU9250 is extremely sensitive. Without this rubber damper, motor noise will flood the PID loop, causing the drone to freak out. You’ll notice that some tutorials won’t use it, but they also have an extremely complex, over-engineered PID algorithm. We all know simple is better 😉
    Where to find it: AliExpress
  2. Connectors: MT30 (Motor to ESC) & XT60 (Battery)
    Cost: Included in misc budget.
    Why: MT30s allow you to swap a damaged motor in the field without soldering.

I left the battery out of the budget because this is the most customizable piece of hardware. In my case, I bought a 2200 mAh 3S LiPo one that provides around 8 minutes of safe flight, but you could get a bigger one, just keep in mind that the larger the battery, the heavier it is.

As an extra, I added a small OLED display to get inmediate feedback during adjustments. This is optional but VERY useful.

The Custom Transmitter (TX)

This is very open to customization. In my particular case, I prefer something very similar to the regular drone pilot: I’ve accumulated many hours in flight simulators, and I've gotten used to the regular joystick, so I used the Thrustmaster T16000M instead of a classic remote controller. The code that you’ll find in the repo works for this device, but you can customize it to adapt to your needs. Let’s get into hardware pricing details:

  1. Microcontroller: Arduino Nano (Clone)
    Cost: ~$3.85 USD (66 MXN)
    Why: Perfect for reading joystick inputs and sending serial data to the radio.
    Where to find it: Anywhere, literally. I got mine from MercadoLibre, but you can find these boards cheaply anywhere.
  2. Joystick Interface: USB Host Shield 2.0
    Cost: ~$6.55 USD (112 MXN)
    Why: This allows us to plug in a standard USB gamepad (like a PS4 or Logitech controller) to fly the drone! No need to build complex gimbals from scratch.
    Where to find it: AliExpress — research on your side a little, because this particular option took forever to be delivered.
  3. Radio: NRF24L01+ PA/LNA (Same as drone)
    Cost: ~$1.75 USD (30 MXN)

If you want to enable telemetry feature, find any OLED display. These are cheap and will give you a lot of clarity of what’s happening on the drone side.

OLED display showing motor PWM and sensor data. These are cheap and very helpful.

If you’re curious, this is how my TX ended up looking:

Press enter or click to view image in full size
It’s a prototype, so as a recommendation, avoid welding stuff in early stages. Keep wiring simple.

You just need to plug the joystick, and that’s it!

Part 1: The Software Architecture

When I first took a look at the existing open-source flight controllers out there, I almost had a heart attack. I get the point, those codebases are usually written by people who know a lot about quadcopters, physics, and probably aerospace engineering, but know little about efficient code. I saw a lot of “C-style” procedural code. Huge functions, global variables, and direct hardware register manipulation mixed with flight logic.

I know you want to see the code now, and I won’t keep you waiting any longer. These are the repos — feel free to fork them at your convenience: Cortex and Synapse.

To keep this project maintainable and extendable, I adopted a Layered Architecture. The code is split into three distinct zones:

  1. The Adapters (HAL): These classes talk to the hardware (I2C, SPI, RMT). If I want to swap the MPU6050 for a BMI270 later, I just write a new Adapter. The rest of the code doesn’t care.
  2. The Models (State): These act as the “Single Source of Truth.” The Attitude object knows the drone's orientation. The DroneCommand object knows what the pilot wants.
  3. The Controllers (Logic): This is the brain. It takes the Inputs (Command), compares them to the State (Attitude), and calculates the Outputs (Motor Mix).

The Control Loop Diagram

Here is how data flows through the Cortex flight controller every 2 milliseconds (500Hz):

[HARDWARE]               [ADAPTERS / HAL]                [APPLICATION LOGIC]
+-------------+ +-------------------+ +-------------------------+
| NRF24 Radio| -------> | RadioAdapter | -------> | DroneCommand |
+-------------+ +-------------------+ | (Normalized Inputs) |
+-----------+-------------+
|
+-------------+ +-------------------+ v
| MPU6050 | -------> | MPUAdapter | +-------------------------+
| (IMU) | +-------------------+ | FlightController |
+-------------+ | | (State Machine & Mixer) |
v +-----------+-------------+
+-------------------+ |
| AttitudeModel | <--------------------+
| (PID Calculations)|
+-------------------+
|
v
+-------------------+ +-------------------------+
| MotorAdapter | -------> | 4-in-1 ESC |
| (DShot RMT) | | (Motors M1..M4) |
+-------------------+ +-------------------------+

Why this structure matters

  1. The “Rich Domain” Model: In many flight controllers, the PID logic is a loose collection of functions. In this project, the Attitude class is smart. It encapsulates the P, I, and D state variables.
    Benefit: The Main Loop doesn’t need to know how to calculate a derivative term. It just asks the Attitude object to “update sensors” and “get corrections.”
  2. The Input Normalizer (DroneCommand): Hardware is messy. Radio sticks send values from 0 to 255, or 1000 to 2000. The DroneCommand class acts as a Data Transfer Object (DTO). It normalizes all inputs into human-readable units: Pitch/Roll: Degrees (-30° to +30°), Yaw: Degrees per Second (-120 to +120), Throttle: 0% to 100%. This means the PID controller is tuning Physics, not arbitrary analog values.
  3. The Hardware Abstraction (Adapters): Notice the MotorAdapter. It uses the ESP32's RMT (Remote Control) peripheral to generate the DShot600 digital protocol. Because this is isolated in an adapter, the main flight_controller.cpp never touches a GPIO pin directly. This makes the code unit-testable and cleaner to read.

Part 2: Demystifying PID (The Driver, The Memory, and The Dampener)

At the heart of every flight controller is the PID Loop. If you look up PID on the internet, you’ll find complex integral calculus — do not let that intimidate you, common, we’re software engineers, buddy. I bet you’ve written more complex algorithms than this. The key aspect to understand: in code, a PID controller is just three simple behaviors working together to solve one problem: Error Correction.

A fun fact that keeps blowing my mind everything I remember: You Are Running Apollo 11 Code. The algorithm stabilizing your $130 drone is mathematically almost identical to the Digital Autopilot (DAP) that landed the Lunar Module on the Moon in 1969. The Apollo Guidance Computer (AGC) ran a feedback control loop to fire reaction control thrusters (RCS) based on gyro errors — just like we are doing with our motors.

The Difference?

  • Apollo 11: Ran on a computer with roughly 4KB of RAM and a clock speed of 0.043 MHz.
  • Your Drone: Runs on an ESP32 with 512KB of RAM and 240 MHz (Dual Core).

If Neil Armstrong could land on the moon with 0.0002% of your computing power, you have no excuse for crashing into a tree.

The boring theory you MUST understand

Press enter or click to view image in full size

Imagine you are driving a car towards a stop sign.

The P-Term (Proportional) — “The Present”
P is the distance to the target.

  • Behavior: If you are far from the stop sign, you press the gas hard. As you get closer, you ease off.
  • In the Drone: If the drone is tilted 30 degrees (far from level), P commands the motors to spin fast. If it’s tilted 5 degrees, P commands them to spin slowly.
  • The Trap: If P is too high, you overshoot the stop sign, reverse, overshoot again, and wobble forever.

The D-Term (Derivative) — “The Future”
D is your speed.

  • Behavior: Even if you are still far from the stop sign, if you are approaching it at 100 mph, you need to brake now or you will crash. D predicts the future by looking at how fast you are closing the gap.
  • In the Drone: This acts as a shock absorber. It fights the P-term. If the drone is snapping back to level too fast, D says “Whoa, slow down!” to prevent it from shooting past the horizon.
  • The Trap: If D is too low, the drone wobbles. If D is too high, the motors sound like they are grinding (jitter) because they are reacting to tiny vibrations.

The I-Term (Integral) — “The Past”
I is the memory of accumulated error.

  • Behavior: You stop at the sign, but you are on a steep hill. Gravity pulls you back. P isn’t enough to hold you (because you are technically “at the target”). You need to apply a constant amount of gas just to stand still. “I” remembers that gravity is pulling you and adds that extra constant gas.
  • In the Drone: This is crucial for Heading Hold (Yaw) and fighting wind or an unbalanced battery. Without I, a tail-heavy drone would constantly drift backward, just like it happened to me. I kept fighting a heavy tail for WEEKS! 😅
  • The Trap: If I is too high, the drone feels “drunk” or “robotic” because it remembers old errors for too long.

The Code Implementation

In C++, this isn’t magic. It’s just addition and subtraction.

float error = setpoint - currentAngle;
float P = Kp * error;

errorSum += error * dt;
float I = Ki * errorSum;

float D = Kd * (currentAngle - lastAngle) / dt;

float output = P + I - D;

That’s it. The entire magic of flight is balancing these three variables (Kp, Ki, Kd) to fight gravity, wind, and momentum.

Part 3: Taming Physics with Gain Scheduling

One of the hardest lessons I learned in this project is that physics is not linear.

Get Sergio Virahonda’s stories in your inbox

Join Medium for free to get updates from this writer.

I’m flying a “Cruiser” build: large 7-inch propellers and a massive 2200mAh 3S battery. This created a contradiction that a standard PID controller couldn’t handle.

The Dilemma: The “Heavy Lift” Paradox

  • At Low Throttle (Launch): The motors are spinning slowly and have very little torque (due to the low 3S voltage). The drone is heavy. To keep it level, the PID controller needs to “shout” at the motors. It needs High P-Gains (Stiffness) just to stop the drone from tipping over on the ground.
  • At High Throttle (Flight): Once the drone is in the air, the propellers are biting into the air efficiently. If I kept those same “High P-Gains,” the drone would react too aggressively to tiny errors, causing violent oscillations (wobble).

The Result

  • If I tuned for Launch, the drone would oscillate uncontrollably in flight.
  • If I tuned for Flight, the drone would be too weak to take off without drifting into a wall.

The Software Solution: Gain Scheduling

In Control Theory, there is a technique called Gain Scheduling. It means changing your PID values in real-time based on the system’s current state.

I implemented a “Linear Interpolation” strategy that blends between two different PID profiles based on the throttle position.

  1. Launch Mode (Throttle < 1350): The software uses aggressive P-Gains (Kp = 18.0) to force the heavy drone off the ground.
  2. Flight Mode (Throttle > 1500): The software switches to gentle P-Gains (Kp = 8.0) and enables the I-Term to handle wind and drift.
  3. The Transition Zone: Between those two points, the code smoothly mixes the values.

Here is the logic from flight_controller.cpp. It’s simple, readable, and incredibly effective:

// Three throttle ranges: 1 = high KP (launch), 2 = transition (blend), 3 = low KP (flight, I on).
// Trim is in degrees/deg/s and added to setpoint so I-term does not fight trim.
void FlightController::computeAttitudeCorrections(
const DroneConfig& droneConfig,
DroneCommand& command,
Attitude& attitude,
AttitudeTrim& trim,
float& pitchPD,
float& rollPD,
float& yawPD
) {
int16_t throttle = command.getThrottle();
int16_t idle = droneConfig.getThrottleIdle();
int16_t launchEnd = droneConfig.getThrottleLaunchEnd();
int16_t flightStart = droneConfig.getThrottleFlightStart();

float desiredPitch = (float)command.getPitch() + trim.getPitchTrim(); // degrees
float desiredRoll = (float)command.getRoll() + trim.getRollTrim(); // degrees
float desiredYaw = (float)command.getYaw() + trim.getYawTrim(); // deg/s

if (throttle < idle) {
attitude.resetPID();
pitchPD = 0;
rollPD = 0;
yawPD = 0;
} else {
// Three stages: launch (high KP), transition (blend), flight (low KP, I on).
float gainBlend; // 0 = launch, 1 = flight; feed into attitude for Kp/Kd blend
bool enableI;

if (throttle < launchEnd) {
// Stage 1: Below lift-off → MAX strength (launch gains, no I)
gainBlend = 0.0f;
enableI = false;
} else if (throttle > flightStart) {
// Stage 2: Flying → NORMAL strength (flight gains, I on)
gainBlend = 1.0f;
enableI = true;
} else {
// Stage 3: Transition zone → smoothly slide from launch to flight
gainBlend = mapFloat((float)throttle, (float)launchEnd, (float)flightStart, 0.0f, 1.0f);
// Enable I only in upper part of transition (0.8 = late, less wobble)
const float I_IN_TRANSITION_THRESHOLD = 0.8f;
enableI = (gainBlend >= I_IN_TRANSITION_THRESHOLD);
}

rollPD = attitude.calculateRollPD(desiredRoll, enableI, gainBlend);
pitchPD = attitude.calculatePitchPD(desiredPitch, enableI, gainBlend);
yawPD = attitude.calculateYawP(desiredYaw);
}
}

Why this matters

This is what separates “Coding” from “Engineering.”

  • Coding is finding a PID library and hoping the defaults work.
  • Engineering is analyzing the physical forces, realizing they change dynamically, and writing a mathematical model to compensate for them.

By adding these lines of code, I fixed a problem that usually requires buying more expensive motors or higher-voltage batteries. I fixed hardware with software. Probably you’ll spend some time here adjusting things to make your drone fly nicely, but it’s worthy 100%.

Part 4: Signal Processing (Why Your Motors Get Hot)

If you write a PID loop in a simulator, it works perfectly. If you run that same code on a real physical drone, your motors might instantly overheat and smoke.

Why? Because the real world is noisy. The wind is unpredictable. The weight distribution of your drone is not perfect. The real world outside of VMs is not perfect.

The Problem: The “D-Term” Nightmare

My drone uses massive 7-inch propellers. Even if balanced perfectly, they generate mechanical vibrations at high throttle. The MPU6050 accelerometer picks up these vibrations as “movement.” The problem isn’t the P-Term (Proportional); it handles noise reasonably well. The killer is the D-Term (Derivative).

  • The math: D = (current_error — last_error)/delta_time. Easy, isn’t it?
  • The reality: The Derivative term looks at the rate of change. High-frequency noise (vibrations) changes direction thousands of times a second.
  • The Consequence: To the D-Term, this vibration looks like the drone is violently flipping back and forth. It commands the motors to reverse direction at 500Hz to “fight” a movement that isn’t happening. The result? Hot motors and a drained battery.

The “Software Engineer” Mistake: The Microservices Syndrome

We software engineers love to solve problems with code, even when we shouldn’t. It is the same trap we fall into with backend architecture:

If a monolith works just fine, the code is clean, and it performs well, why migrate to microservices?

KISS (Keep It Simple, Stupid).

In the drone world, this over-engineering manifests as trying to write complex noise-reduction algorithms — like distinct Kalman Filters or heavy Moving Average buffers — running on the main CPU.

  • The Cost: Just like microservices add network latency, software filters add Phase Lag. By the time your fancy code has “cleaned” the data, the drone has already tilted. You are reacting to the past, which causes the drone to wobble.

The “Hardware Engineer” Solution: The DLPF

Instead of reinventing the wheel in software, I read the MPU-6050 Datasheet.

The sensor has a built-in Digital Low Pass Filter (DLPF) engine. By writing a single byte to a specific register, you can tell the sensor to ignore any movement faster than a certain frequency (e.g., 42Hz) before it even sends the data to your ESP32.

void MPUAdapter::begin() {
// ... initialization code ...

// ENABLE HARDWARE DLPF (The "Vibration Shield")
// Register 0x1A: CONFIG
// Value 0x03: Bandwidth = 44Hz, Delay = 4.9ms

Wire.beginTransmission(0x68); // MPU Address
Wire.write(0x1A); // Register: CONFIG
Wire.write(0x03); // Value: DLPF_CFG = 3
Wire.endTransmission();
}

Why 44Hz?

  • 0x03 (44Hz): This is the “Goldilocks” zone. It filters out the high-pitched “hum” of the motors (usually >100Hz) but is fast enough to let the legitimate flight movements (pilot stick inputs) pass through without noticeable lag.

By flipping this one bit, the sensor data goes from a jagged, unusable mess to a smooth, clean wave. The D-Term calms down, the motors run cool, and the flight becomes fluid.

Part 5: The “Unit Test” (Bench & Hand Testing)

Press enter or click to view image in full size

In software engineering, you never deploy straight to production on a Friday at 5 PM. You write unit tests, run integration tests, and test in staging.

A 7-inch drone with 7 inches carbon fiber propellers is a flying blender. If you have a bug in your code and you “deploy to production” (fly outside) immediately, you won’t just get a stack trace — you might get stitches.

Before I ever put props on, I spent hours in staging environment — my workbench.

Phase 1: The “Props Off” Logic Check

This is the equivalent of running your test suite. I verified every subsystem in isolation:

  1. Sensor Orientation: If I tilt the drone nose-down, does the pitch variable go positive? (If this is reversed, the drone will flip instantly upon takeoff).
  2. Motor Mapping: Does the “Front Left” command actually spin the Front Left motor?
  3. Failsafe: If I turn off the radio, do the motors stop instantly?

Phase 2: The Hand Test (Feeling the Math)

Hand test at home while I was applying final adjustments to the PID constants.

Once the logic passed, I moved to the Hand Test.

⚠️ Warning: This is dangerous. I wore eye protection, used a thick leather glove, and held the drone firmly from the bottom battery strap when tuning. Do not try this if you are unsure.

This is where the magic happens. You can actually feel your PID terms working against your hand. It is the most direct connection between your code and the physical world.

  • Feeling the P-Term (The Spring): When I tried to tilt the drone, it fought back. The harder I pushed, the harder it pushed back. It felt like an invisible spring was trying to keep it level. Too Low: It felt mushy and weak. Too High: It felt jittery, vibrating like it had too much caffeine.
  • Feeling the D-Term (The Honey): When I twisted the drone quickly, I felt a resistance to the motion itself, not just the angle. It felt like moving a spoon through thick honey. This “viscosity” is the derivative term dampening the movements.
  • Feeling the I-Term (The Muscle): If I held the drone at a 20-degree angle and kept it there, I could feel the motors slowly ramp up power, fighting harder and harder the longer I held it. That was the Integral term summing up the error over time, screaming, “I am supposed to be level!”

Why this matters

You cannot tune a PID loop just by making assumptions. You have to feel it.

The Hand Test gave me the confidence that my gain scheduling logic was working. I could throttle up (carefully) and feel the P-Gains soften, confirming that the transition code was doing its job.

By the time I finally walked out to the field for the maiden flight, I wasn’t guessing. I knew it would fly, because I had already felt it fly in my hand.

Part 6: Conclusion (The “Stubborn Engineer” Syndrome)

Press enter or click to view image in full size

So, was it worth it? 1000000%.

Why spend months writing C++ for a $4 chip when I could just buy a flight controller for $30, flash Betaflight, and be flying in 10 minutes?

Because I’m a Software Engineer, and, if you are reading this, you know exactly what that means: We are stubborn.

We are the kind of people who will spend three days automating a task that takes 5 minutes to do manually. We are the kind of people who look at a “black box” and feel a physical itch to rip it open and see how it works.

I didn’t just want a drone.

When I saw existing flight controllers using messy, procedural code, my engineer brain didn’t say, “Oh well, it works.” It said, “I can structure this better.” It was obstinate, perhaps a bit irrational, but absolutely necessary.

The “Hello World” of Aviation

There is a specific kind of magic in seeing a machine levitate, completely stable, purely because of the logic you wrote.

As software engineers, we spend our days moving data from one place to another. Building things we can’t touch. We rarely get to see our code physically move something. Seeing this 7-inch carbon fiber brick defy gravity because your PID loop calculated the correct derivative… that is the ultimate “Hello World.”

We started with a blank text file and ended with a stable, autonomous vehicle. We applied dependency injection, state machines, and clean architecture to the chaotic world of aerodynamics.

The Code: Cortex Flight Controller 🧠

This project, which I’ve named Cortex, is now open source.

It is designed to be a playground for stubborn engineers like us. I have stripped away the complexity of traditional flight stacks to leave only the clean, readable core.

If you are tired of “Arduino Spaghetti” and want to see what an Object-Oriented drone looks like, go check it out. Fork it. Break it. Refactor it.

And when you find yourself at 2 AM, debugging a raw gyroscope value because you refuse to let the hardware win… remember: It’s not a bug. It’s engineering.

Happy Flying. 🚁💻

--

--

Sergio Virahonda
Sergio Virahonda

Written by Sergio Virahonda

Software Engineer, life-long learner and veeeery techie guy.