← Charles K. Lindberg

Quadcopter Flight Controller

Charlie Lindberg · 2023–2024

Small carbon-fiber quadcopter with four black propellers and green motors, a blue 2.2 Ah LiPo battery strapped on top and yellow XT60 connectors, resting on two books on a patterned carpet. 3D CAD render of the whole flight controller PCB, shown rotated: a microSD slot and pin headers at the top, a green Teensy 4.0 in the middle, sensors and the buck converter below it, and two yellow XT60 connectors at the bottom.
The quadcopter with the flight controller installed, and a 3D CAD render of the flight controller board.

Overview

This is a quadcopter flight controller that I designed and built: a custom PCB that carries a Teensy 4.0, an IMU, a barometer, a battery voltage and current monitor, a microSD card for flight logs, and the power and signal circuitry to drive four ESCs from a radio receiver, along with the firmware that flies it.

After finishing my classes in the fall of my senior year, I had about a month of downtime before the spring semester. I decided to revisit a problem I had worked on a few years earlier, designing a flight controller, but this time for a quadcopter instead of a fixed-wing aircraft. It combines several things I enjoy working on: electrical design, control systems and embedded software. I also wanted the board to be platform independent, so it could work across different quadcopters as well as fixed-wing drones.

I chose to include an IMU, a barometer and power monitoring. GPS would have been nice, but I wanted to limit the scope and focus on the controls side of quadcopter flight. After the PCBs were designed, ordered and populated, I integrated the board with the rest of the flight hardware and wrote the software, followed by a painful testing period plagued with software bugs. Eventually it flew decently, although it still struggled in wind and crashes were common.

System architecture

I wanted to keep the board compact while staying flexible enough to work with various ESCs, receivers and batteries. Rather than putting the microcontroller directly on the board, I used a Teensy 4.0 development board. That avoided routing a fine-pitch BGA and the other issues of bringing up a new microcontroller. The Teensy 4.0 is built around an ARM Cortex-M7 running at 600 MHz; I especially liked its speed and wide range of I/O (all GPIOs are interrupt capable, and it has three I2C ports).

Flight controller PCB architecture block diagram: a 3S LiPo battery feeds an XT60 connector, a shunt and a second XT60 connector to four ESCs and brushless motors. A 5 V buck converter powers the Teensy 4.0, which connects to the INA226 power monitor, MPU-6050 IMU and LPS22HBTR barometer over I2C, a microSD card over SPI, indicator LEDs, the ESCs over PWM, and the RC receiver through 5 V to 3.3 V level shifters.
Flight controller PCB architecture (click to enlarge).

Component selection

All three sensors have I2C interfaces and share one bus at 400 kHz (addresses 0x68, 0x5C and 0x45).

PCB

I mostly finished the schematic in December 2023. Besides the Teensy and sensors, the board has a 5 V buck converter (TI TPS62933), green and yellow status LEDs driven by MOSFETs, and five 74LVC1T45 level shifters that convert (if needed) 5 V signals from the radio receiver to the Teensy's 3.3 V logic. The receiver can connect with five PWM channels or a single PPM signal. There are headers for four ESCs and two auxiliary PWM outputs.

To measure current, battery power enters through one XT60 connector, passes through a 0.5 mΩ shunt resistor monitored by the INA226, and leaves through a second XT60 to the ESCs. The output (distribution) XT60 connector is rated for 25 A, which limits the current the board can deliver to the ESCs. This required large power planes, which led to a 6-layer PCB with 1 oz copper throughout. The board measures about 79 × 42 mm.

3D render of the flight controller PCB: a green Teensy 4.0 in the middle, a microSD slot and pin headers on the left, sensors and the buck converter on the right, and two yellow XT60 connectors on the right edge.
PCB 3D render.
PCB layout view of the board in red copper with yellow silkscreen labels for the Teensy footprint U6, microSD SD1, headers, sensors and the two XT60 connectors J1 and J10.
PCB layout.

JLCPCB assembled the boards. After they arrived, I tested the basic functionality and connectivity. All peripherals and power circuits worked correctly. The board I flew was this first revision; it never needed a redesign. Once I had written the initial I2C drivers, I was ready to write the overall system software and start testing. View the schematic (PDF).

Schematics

Schematic sheet 1: Teensy 4.0 with I2C pull-ups, LPS22HBTR barometer, MPU-6050 IMU, microSD card connector, and the power and headers sub-sheets.
Schematic: Teensy 4.0, IMU, barometer and microSD (click to enlarge).
Schematic sheet 2: XT60 battery input, INA226 current and voltage monitor with shunt R1, TPS62933 5 V buck converter, and MOSFET-driven green and yellow status LEDs.
Schematic: battery input, power monitor, 5 V buck converter and status LEDs.
Schematic sheet 3: four ESC headers, two auxiliary PWM headers, the output XT60 connector, mounting holes, and five 74LVC1T45 level shifters for the receiver inputs.
Schematic: ESC and auxiliary headers, output XT60 and receiver level shifters.

Software

The firmware is a state machine with five states. On power-up, INIT sets up the LEDs, I2C bus and sensors, checks that the board is level, checks the receiver, and waits for the arm switch to be off and the throttle low. In STANDBY, the quadcopter waits to be armed: arm channel high and throttle low. ACTIVE runs the control loop until the arm switch is turned off, then POSTFLIGHT stops the motors, writes the flight log to the SD card and returns to STANDBY. FAULT stops the motors and blinks both LEDs.

State diagram: start, then INIT. If the IMU and receiver are valid and the arm switch is off with throttle low, go to STANDBY; otherwise stay in INIT, or go to FAULT on an IMU or receiver fault. From STANDBY, if arm is on and throttle is low, go to ACTIVE; otherwise stay in STANDBY, or go to FAULT on a receiver fault. From ACTIVE, when arm is turned off go to POSTFLIGHT. POSTFLIGHT returns to STANDBY, or goes to FAULT on an SD card fault. INIT STANDBY ACTIVE POSTFLIGHT FAULT IMU and Rx OK, disarmed arm on, throttle low arm off log saved IMU / Rx fault Rx fault SD fault
Flight controller state diagram (redrawn from my design notes).
Flowchart of one pass of the ACTIVE state. Run the current state (loop switches on CURRENT_STATE). In ACTIVE: get IMU data and update the complementary filter (getMPU); read the radio channels (readChannels); run the roll, pitch and yaw controllers (calcPID three times); mix and limit the motor commands to at most 2000 microseconds, with anything under 1050 set to 1000 (conditionSpeed); write the motor commands to the four ESCs (setESCs); read battery voltage and current every loop (getINA). If the arm switch is off (arm below 1500 microseconds), go to POSTFLIGHT on the next pass. If this is a logging loop (loop counter modulo 5 is zero), log a sample (add_DataEntry, 50 Hz). Count the loop, then wait, blinking the LEDs, until 4 ms have passed, reset the timer, and start the next pass, 250 times per second. Run current state loop(): switch on CURRENT_STATE Get IMU data + filter getMPU() Read radio channels readChannels() Run roll/pitch/yaw controllers calcPID() × 3 Mix + limit motor commands conditionSpeed() Write motor commands setESCs() Read battery voltage/current getINA(), every loop Go to POSTFLIGHT on next pass Log sample add_DataEntry(), 50 Hz Count loop loopCounter++ Wait for 4 ms mark blink LEDs; reset timer Arm switch off? arm < 1500 µs Log this loop? loopCounter % 5 = 0 ACTIVE yesyes nono next pass, 250 Hz
One pass of the ACTIVE state. Each pass of loop() runs the code for the current state; in ACTIVE, the pass is paced to 4 ms (250 Hz). Turning the arm switch off takes effect on the next pass.

Control loop

The ACTIVE state runs every 4 ms (250 Hz). Each loop reads the MPU-6050 and the receiver, runs the roll and pitch PID controllers and the yaw-rate PD controller, mixes their outputs into four motor commands, reads the INA226, and stores a log entry on every fifth loop.

Control loop block diagram. Three loops, for roll, pitch and yaw rate. In each, the RC receiver stick sets a setpoint, and a summing junction forms the error, measured value minus setpoint. The roll and pitch PID controllers and the yaw rate PD controller (no integral term), each limited to plus or minus 500, feed a shared motor mixer that also takes the receiver throttle and limits each motor command to 1000 to 2000 microseconds. The mixer drives four ESCs and motors, which move the quadcopter. The MPU-6050 IMU measures the motion. Roll and pitch angles come from a complementary filter of the gyro and accelerometer; yaw rate comes straight from the z-axis gyro. These are fed back to the summing junctions. Roll PID Pitch PID Yaw rate PD Motor mixer 1000–2000 µs 4 ESCs + motors Quadcopter IMU MPU-6050 Gyro z → yaw rate Complementary filter 0.98 × gyro + 0.02 × accel Rx roll stick (±25°) Rx pitch stick (±25°) Rx yaw stick (±25 °/s) Rx throttle ±500 ±500 ±500 −−− +++ roll (°) pitch (°) yaw rate (°/s) gyro z accel + gyro x, y
The three control loops, run every 4 ms: PID on roll and pitch angle, PD on yaw rate. Each stick sets a setpoint of (pulse − 1500 µs) / 20, and each controller acts on the error, measured value minus setpoint.
ESCPositionCommand
1Front rightthrottle + roll − pitch + yaw
2Back rightthrottle + roll + pitch − yaw
3Back leftthrottle − roll + pitch + yaw
4Front leftthrottle − roll − pitch − yaw

Flight logging

During a flight, log entries (timestamp, roll, pitch, yaw rate, battery voltage and battery current) are stored in a linked list in RAM. After the quadcopter is disarmed, the POSTFLIGHT state writes the list to a numbered CSV file on the SD card, with a header recording the PID gains used for that flight, so every log can be matched to its tuning. Battery voltage and current logging was only added later in testing, around flight 175, so earlier logs have no valid power data; flight 183 is the first log with both readings valid.

The firmware is written in C++ for the Teensy (Arduino), split between the main flight loop and a set of drivers for the sensors, receiver, ESCs and SD card.

Quadcopter platform

The frame and ESCs were bought off the shelf; the focus of this project was the flight controller, not building the quadcopter. Around the board are a 3S LiPo battery (11.5 V nominal, 2200 mAh), four ESCs driving four brushless motors, and an RC radio receiver.

Quadcopter frame with the flight controller board mounted under a top plate in a white holder, yellow XT60 connectors on one side, a vertical receiver antenna, and green brushless motors on the carbon-fiber arms.
The flight controller mounted on the frame.

Tuning, flight results and data

The first flight was in April 2024. The quadcopter flew decently, with my longest flights lasting about 30 seconds. It still had difficulty in wind, and crashes were common.

Because every log records its gains, the flight logs show how the tuning evolved. A selection:

FlightDateRoll P / I / DPitch P / I / DYaw rate P / D
1035/32.8 / 0 / 463.5 / 0 / 720 / 0
1135/133 / 0.0001 / 463.3 / 0.0001 / 720 / 0
1205/143 / 0.0001 / 463.3 / 0.0001 / 721 / 0.5
1415/181.8 / 1e-7 / 331.98 / 1e-7 / 43.21 / 0.5
1655/292.9 / 0 / 352.9 / 0 / 350.2 / 0.05
1755/312.9 / 7e-7 / 482.9 / 7e-7 / 381.2 / 0.6
1866/42.3 / 7e-7 / 382.9 / 7e-7 / 381.2 / 0.6
1906/52.1 / 7e-7 / 382.9 / 7e-7 / 421.5 / 0.6
1926/71.95 / 7e-7 / 382.9 / 7e-7 / 422.5 / 0.6
1976/81.85 / 7e-7 / 352.9 / 7e-7 / 423.5 / 0.6

The final gains (bold) are from flight 190, my most successful flight: one of the longest in the logs at about 23 seconds, with roll and pitch staying within about ±24° until touchdown. Later tuning, including the gains in my last copy of FLIGHT.ino (roll 1.95 / 7e-7 / 38, pitch 2.9 / 7e-7 / 42, yaw rate P / D 3.5 / 0), was intermediate; none of the later logged flights lasted as long.

Logged data from flight 190, about 23 seconds long. Top: roll and pitch angle in degrees, mostly within plus or minus 20 degrees, with pitch reaching about minus 40 degrees in the last sample. Bottom: battery current, about 0.3 A for the first 2.5 seconds, then mostly between about 8 and 22 A, peaking at 28 A. -40° -20° 0° 20° 40° 0 A 10 A 20 A 30 A 0 s 4 s 8 s 12 s 16 s 20 s 24 s Roll and pitch angle Roll Pitch Battery current Time since start of log
Logged roll and pitch angle and battery current from flight 190 (50 samples per second), the flight the final gains come from.

Lessons learned

I tuned the controllers experimentally: set gains for each axis, fly, watch the behavior in the logs and in the air (oscillation, saturation, or failure to settle), and make small adjustments. Without a good test setup, every test was a real flight, which meant a lot of crashes, a lot of broken hardware, and a very long road to a decent tune.

If I built it again, I'd do one of two things before flying. One is a better test setup that allows quick, repeatable, non-destructive tests of different gains. The other is a proper simulation of the quadcopter's dynamics to find reasonable gains first, leaving only the final tuning for flight. Either would have saved a lot of time and hardware. Controls and dynamics problems like this are just hard.