Bare-bones flight controller/stabilization package for tinkerers

nickrehm

Member
Hi all,

I'm sharing my custom flight controller code written for the Teensy 4.0/4.1 and MPU6050/9250 IMU. The best summary I've been able to prepare thus far:

"dRehmFlight VTOL is a bare-bones flight controller for hobbyists or researchers wanting to get something flying fast with the ability to quickly and intuitively adapt to unique configurations. The Arduino environment makes it easy to make your additions--from basic functionality to sensor integration to custom inner-loop control--without the hassle of dealing with spaghetti code cluttered by features you'll never use."

I started this project out on the Arduino nano, but wanted a lot more headroom/horsepower--that's when I discovered the Teensy 4.0 with cortex m7 which has been absolutely fantastic. Finally I was able to code a flight controller my non-coding brain can understand and easily modify for all my weird flying contraptions--and to be able to run a flight control loop rate of 2kHz with plenty more computing power to spare is just awesome.


Download: https://github.com/nickrehm/dRehmFlight


Features:
  • Default code supports 6 ESC outputs using OneShot125 Protocol, and 7 conventional PWM outputs for ESCs or servos, with the ability to modify the code for extra outputs for custom setups.
  • Support for conventional PWM, PPM, or SBUS receivers.
  • MPU6050 and MPY9250 IMUs supported.
  • Easy to use control mixer with stabilized axis variables and ability to pass direct, unstabilized commands to the motors or servos direct from the transmitter.
  • Three PID controller types including rate and angle-based setpoint.
  • Simple variable fading, with support for more advanced options planned in the future.
  • Comprehensive documentation with explanation of every function and variable, as well as tutorials for setting up the hardware and modifying the code for your application.

I owe a special thanks to Brian Taylor (who I see is still very active on this forum) for his SBUS, MPU6050, and MPU9250 libraries which made the current version of this project possible. Original code only supported PWM and PPM receivers and the MPU6050 over I2C, but some overhaul in the newest version added the 9250 and SBUS compatibility thanks to the bolderflight libraries.

Here is my favorite project running this code:



Anyways, I'm sold on the Teensy. This thing is just so dang powerful and easy to use that it'll be my go-to from here on out. Cheers
 
Interesting! I'm actually in the process of open-sourcing all of our flight controller software. We're still using the Teensy 3.6 microcontroller and haven't hit any performance limitations yet. Hoping to be done open sourcing it over the weekend - I'll post a link, I would love some feedback and to explore a chance to collaborate.

Brian
 
Looks really good. Have you thought about using deadtime instead of PID? I used both for controlling 3D printer hot ends and heated beds. PID requires a lot of trial and error to understand, especially in systems with high thermal mass, but deadtime has only one variable to adjust.
 
Looks really good. Have you thought about using deadtime instead of PID? I used both for controlling 3D printer hot ends and heated beds. PID requires a lot of trial and error to understand, especially in systems with high thermal mass, but deadtime has only one variable to adjust.

Not OP, but typically you try to avoid latency. Aircraft dynamics are pretty fast. Consider an aircraft that has an attitude hold control law that is trying to maintain level pitch and roll. It encounters turbulence that pitches the nose up. The control law would command a control surface to achieve a downward pitch to bring the aircraft back to level. However, if too much time passes, the aircraft, due to turbulence or its own inherent stability may now be in a downward pitch angle and the downward control input destabilizes the system.

PID is typically not too complicated. There are analytical methods to find the correct gains if you know the system dynamics. Otherwise, you are usually achieving most of your control with the proportional gain. The integral is really just to remove DC bias and is typically an order of magnitude smaller than the proportional. The derivative gain isn't used often, since it can be destabilizing.
 
I am pretty familiar with PID, and have written a fair amount about understanding it. What I don't get is whether deadtime isn't more popular because it has some inherent disadvantage, or whether it's because PID is just "what everyone does." In practice, calibrating deadtime has always been much simpler than PID whether it has been for something with rapid response (like a hot end) or something that might take tens of seconds to stabilize (like a heated bed.) I finally got to where I could handle PID somewhat easily, but that's only because I made the dozens of mistakes you have to make before you can understand how to apply it to a wide range of system inertias. Even today, the act of probing the effects of I and D (as P is usually the easiest) would take longer than calibrating a deadtime controller.
 
I'm not sure I'm familiar with deadtime...do you have any resources to bring me up to speed? Would love to know the advantages/disadvantages
 
Dead-time control is described here, on Repetier: https://www.repetier.com/dead-time-control/

The variable is described in this image:
MeasureDeadtime.png

There are only two variables: maximum drive (functions similar to P); and dead-time, which describes how long it takes the system to hit the knee of the acceleration curve. Maximum drive is generally less than the maximum amount (e.g. 255, if 8 bits), and dead-time corresponds to how much inertia must be overcome before what is being driven can reach full acceleration (whether thermal or otherwise.)

The result looks like this:
TempControlComparison.png

The advantage over PID is that whereas P is relatively easy to set, I and D have to be set as well, and changing any one of the variables changes the behavior of the other two. In dead-time, maximum drive acts like P, and dead-time reflects the drive inertia. Two interrelated variables to calibrate rather than three.
 
Dead-time control is described here, on Repetier: https://www.repetier.com/dead-time-control/

The variable is described in this image:
View attachment 22130

There are only two variables: maximum drive (functions similar to P); and dead-time, which describes how long it takes the system to hit the knee of the acceleration curve. Maximum drive is generally less than the maximum amount (e.g. 255, if 8 bits), and dead-time corresponds to how much inertia must be overcome before what is being driven can reach full acceleration (whether thermal or otherwise.)

The result looks like this:
View attachment 22131

The advantage over PID is that whereas P is relatively easy to set, I and D have to be set as well, and changing any one of the variables changes the behavior of the other two. In dead-time, maximum drive acts like P, and dead-time reflects the drive inertia. Two interrelated variables to calibrate rather than three.

Yea, I'm curious how this looks like when it's implemented... Seems to be more geared toward high-inertia systems which may be difficult to get working on something MAV-scale
 
I finished getting our software open-sourced:
https://gitlab.com/bolderflight/software

We organized the software into many focused repos of limited scope to enhance code readability, re-use, and make it easier to develop unit tests. Our flight repo pulls together all of the components to provide a framework for implementing a flight control system.
https://gitlab.com/bolderflight/software/flight

It includes data acquisition, filtering, estimation, control, actuation, data logging, and telemetry. The control module is left as a sandbox to implement whatever controllers you would like. We have a PID controller class available, which has similar functionality as the MATLAB / Simulink PID and PID2 functions / blocks.
https://gitlab.com/bolderflight/software/control

Currently, everything uses CMake and GCC as build tooling with the teensy-loader-cli for flashing our Teensy 3.6 based flight computer. Linux or WSL under Windows are intended as the development environment. I know Arduino is a lower barrier to entry for compiling and flashing code - on my "to do" list is to build steps into our CI/CD pipeline to generate Arduino compatible libraries and projects. Also on the "to do" list is adding in mirroring to GitHub.

I'd love for this to grow into a thriving open-source project and would appreciate any comments or contributions. We're actively using this code base for our research, development, and grant work and have several Universities using it as well.

Brian
 
I'd love for this to grow into a thriving open-source project and would appreciate any comments or contributions. We're actively using this code base for our research, development, and grant work and have several Universities using it as well.

Brian
Good stuff. It looks polished. A few comments.

Protocol buffers seems like a good choice for serialization format.

The license (whatever it is) should appear at the top of every file.

It is good that you want to write unit tests. Platform.io has some scaffolding for this. Some of your functions are very long, and this will make it harder to write tests. Try to make each function good at doing exactly one thing. A good rule of thumb is that if there are more than two ways to go through (e.g., two if statements, which means there are FOUR ways), it should be broken up.

I like this, because it seems like every few weeks someone is asking about putting different peripherals on different SPI or I2C buses:
Code:
static constexpr SPIClass &IMU_SPI_BUS = SPI;

There are a lot of platform-specific implementation details, like this, which are assumed to always be the right ones. You may be thinking, "so what? We designed this specifically for one board." But that board will become outdated in the future, and someone else may want to port it to another board with similar capabilities but which is nonetheless obliged to use different pins. In practice, this file could be renamed to hardware_defs_teensy_3x.h (or similar) and then whether that file should be included, or another, controlled with preprocessor directives.

The detail effector_delay_us is also in there, using magic numbers, the provenance of which is not explained. I don't know whether this is the response curve of whatever drivetrain you use, or something to do with the timing between changing control output and the effects being realized on the pin. If it's specific to delays inherent in the MCU platform, it belongs there. Otherwise, it would be better to place it in another file that's specific to the effectors. If someone is using substantially different motors and props, or even at a substantially different barometric pressure (say, you're close to sea level, and someone is using this in Denver), the response to changed current will be different.

Yea, I'm curious how this looks like when it's implemented... Seems to be more geared toward high-inertia systems which may be difficult to get working on something MAV-scale
You can see source here and more detailed discussion here. As the control algorithm is suitable for any system with inertia, and as UAVs have inertia, it should be adequate; you'd simply have a small dead-time value, and that could be adjusted with a barometer or by evaluating the measured response to orientation vs. the controller output.
 
Last edited:
@Pilot, thanks for the feedback, that's excellent!

The license (whatever it is) should appear at the top of every file.

It's MIT - agree that we should put it in every file.

It is good that you want to write unit tests. Platform.io has some scaffolding for this. Some of your functions are very long, and this will make it harder to write tests. Try to make each function good at doing exactly one thing. A good rule of thumb is that if there are more than two ways to go through (e.g., two if statements, which means there are FOUR ways), it should be broken up.

I'll be sure to check it out. We have good unit tests for libraries that aren't MCU specific - so they can be compiled and tested on Linux. I have a prototype unit tester for the Teensy's: the code uploaded to the Teensy would have all of the tests as functions that could be called and the code running on the host would dictate which function to call and get a pass / fail status back. The host would also cycle power to the Teensy and any sensors between tests to get a fresh state. It worked, but is tough to maintain, I'm hoping to find a more elegant solution.

There are a lot of platform-specific implementation details, like this, which are assumed to always be the right ones. You may be thinking, "so what? We designed this specifically for one board." But that board will become outdated in the future, and someone else may want to port it to another board with similar capabilities but which is nonetheless obliged to use different pins. In practice, this file could be renamed to hardware_defs_teensy_3x.h (or similar) and then whether that file should be included, or another, controlled with preprocessor directives.

I really really like this idea. Yes, the intention was to capture all the hardware specific stuff in hardware_defs, so there is only one place to modify when the flight computer architecture changes. But we are already using defs for specifying the Teensy MCU, why not a def to specify the flight computer hardware. Perfect, thanks!

The detail effector_delay_us is also in there, using magic numbers, the provenance of which is not explained. I don't know whether this is the response curve of whatever drivetrain you use, or something to do with the timing between changing control output and the effects being realized on the pin. If it's specific to delays inherent in the MCU platform, it belongs there. Otherwise, it would be better to place it in another file that's specific to the effectors. If someone is using substantially different motors and props, or even at a substantially different barometric pressure (say, you're close to sea level, and someone is using this in Denver), the response to changed current will be different.

That's a delay between sensing and actuation in order to give the system a fixed latency for analyzing control law robustness. I wouldn't want to have the actuators commanded immediately after the control laws are done, because then the latency would depend on the computational load. Seems like this needs to be commented / documented better.

Thanks again for all the feedback!
Brian
 
I really really like this idea. Yes, the intention was to capture all the hardware specific stuff in hardware_defs, so there is only one place to modify when the flight computer architecture changes. But we are already using defs for specifying the Teensy MCU, why not a def to specify the flight computer hardware. Perfect, thanks!
I'm glad the feedback was useful. In this particular case, I think if the project takes off, people will want to plug in all sorts of different things. It may be that the GPS receiver you used is more expensive than another for some user in another country, or they want to use a different one that they already have. If you treat this system like Legos, the MCU is one brick, the GPS receiver is another, the motor driver is another, and so on, and each component can have its own definitions. One layer up from that, a user could write an include that brings in specifically this MCU, specifically that GPS receiver, specifically another motor driver, and so on. Such a file would define a specific platform, consisting of those exact components. One layer up from that is a single header, used in every file that cares, which says e.g. "#include platforms/mcu_teensy_4x_gps_acme_4000_motor_controller_trinamic_1234.h". If you use such a system, you can actually create a dummy platform that's specifically for testing, which will compile on anything, and which can be harnessed in your tests to see whether the functions are doing what you think they are.
 
Back
Top