This is the very first of (I hope) a series of blog post about the experience I gathered over the years on designing flight controllers.
I started working on flight controllers 8 years ago ... at first it was 100% out of curiosity, at that time the first affordable MEMS gyros became available, and I was flying RC helicopters, and I studied the PID theory in Supelec a few years before ... so why not ?
After a few months I started to get quite good results, and partners started to show up. I then decided to work with an italian company. At first I was working on it on evenings, on weekends and holidays, but at some point it became clear I needed to work full time on this project to make a good product ... so I created my design house, and we released the BRAIN flybarless unit. We worked successfully a few years together, then decided to split.
At that time the drone mass market was pretty much non existent, one day I was talking with an old friend and I asked my what was missing to my flight controllers to become fully autonomous ... and we started EZNOV. For more than 3 years we developed a fully autonous drone for professionals, and kept a flight controller dedicated to RC market (helis, planes and quads), the Neuron.
We are now developing products for multiple customers (https://3p.tools for example), still with a main focus on sensor fusion, robotics and flight / control algorithms.
This series of blog post is targeted at helping developers new to this very interesting field, I will try to answer as much questions as possible.
Here I summarize what I've learned over the years, and each entry will be detailed in a later post:
1 - SAFETY
We are talking about rotating propellers here, the first thing to have (and keep) in mind is safety. You are flashing a new firmware, setting a parameter ? Remove the propellers, or at least place the model in a position that is safe in case of unexpected results. The security features should always be the very first to be implemented, even the very first to be thought of (before even thinking how to implement a feature, think about how to make it secure). I have scars to prove "I'll do it later when the feature works" is NOT ok.
Safety comes from hardware (remove propellers, secure inputs / outputs, etc.) but also from software. Over the years we elaborated a bunch of rule of thumbs to avoid most of the problems.
a - Do NOT use standard dynamic allocation. I think this whould be most applied to any embedded project, everything should be statically allocated at compile time, for two main reasons : safety and performance (we'll come back to this later). What happens when you need memory at runtime and there is not enough available ? Of course you can handle the case at each new / malloc, but some of them are mandatory, there is no fallback solution.
Of course some data sizes are not known at compile time (incoming packet from a communication channel?), then allocate a buffer to store it, with sufficient space available, mutualize these buffers when adequate, but reserve the momery for critical tasks. This also allow you to use tools like Atollic memory analyser, and quickly have an overview of the memory used, check if the memory allocated to system stack is enough, etc.
b - Take care of each and every line of code you write.
- all variable types have their strengths and limits, always keep in mind over/underflows for fixed point, and potential precision problems for floating points.
- watch out for interrupt handling and priorities. ISR should be as short as possible, and you should rely on peripherals when possible to free the processor for more important tasks. An RTOS helps a lot.
- verify everything. If you are working in team, do peer reviews, they are an awesome way to share knowledge and prevent basic mistakes.
- use C++, encapsulation is a must to have clean interfaces, reusable code, and restrict access to private data.
2 - INSTRUMENT
I spent quite a few years relying only on my flying skills and aerodynamic knowledge to try to identify problems just by flying models and watch better pilots fly them. It works in a way, but has many many limits. You cannot always understand what is happening just by watching the model, you have to get faster, more reliable and objective data to work on. One of the problems is that you most often cannot debug as you wish at runtime, hitting a breakpoint in flight is just not an option.
Thanks to STM32 quad spi, we easily developed a high speed logger to our projects, it allows us to store data at multiple KHz sampling rate over a full flight, and analyse it afterwards.
For programming related instrumentation, there are excellent tools available, especially if you are using an RTOS. We developed our own and integrated SEGGER SystemView, it is a wonderful tool to check processing time, identify bottlenecks, verify preemption mechanisms, etc.
Use C++, it is more flexible to add and remove portions of code at compile time without performance impact, and allows for better abstraction.
3 - PERFORMANCE
For the record, the idea of this blog post comes from ST, after we showed our drone at embedded world (we were in the STM32 fan zone, as we are huge fans of STM32 !), we were asked if we were using F7 or H7 series.
The fact is we are using F427 on the EZManta, and F401 in the Neuron, and most of the time the processor usage is below 30%.
- use C++. Believe it or not, C++ is faster than C, it's a fact demonstrated multiple times. C++ is a lot about making the compiler do as much work as possible to not do this work at runtime. It also provides multiple facilities to improve code and optimize it better.
- use what your MCU has to offer. STM32 have a wide array of solutions to optimize performance out of the box (ART, cahce, prefetch, TCM, etc.). Most often the default projects won't make use of them, you can gain a lot of performance by using them, without even changing your code.
- write correct code, that can be compiled as O3. I've seen too much production code delivered as O0 binaries because the program doesn't work with optimization enabled, this has lead many to think optimization change the output of the program. This is not true (or at least shouldn't), if your code works in O0 and doesn't work in O3, then in fact it shouldn't work as O0 (it is because of side effects). Debugging O3 code is really hard, take your time, and take the time, after you write some code, to verify it is working with O3 compilation, this is often a indication your code is well written.
- O3 is just the beginning. Once your program is running in O3, you can start to check all the compiler optimization flags that you can use on top of O3. These are not included in O3 because they slightly change the behavior of the program. One example, on M4F there is a vsqrt assembly code, that calculate the square root of a floating point value: write a simple return sqrtf(value) function and compile it as O0, it will call the library sqrtf function, that most probably does not make use of the vsqrt assembly code, then compile it as O3 ... on recent GCC versions, it will use the vsqrt code, but add about 10 lines of assembly before and after to make sure the behavior is stricly the one described in the library specifications (which is not exactly what does vsqrt for very specific cases), now keep O3 but add the "fast-math" compiler flag, voilà, now GCC generates vsqrt.f32 s0, s0 / bx lr which is the minimum (and fastest) code to calculate a square root.
- last optimization flag but not least, LTO. Acronym for Link Time Optimization, it allows the compiler to optimize code over multiple compiling units. Debugging is almost impossible, but the performance and size gain is substential.
- use www.godbolt.org, this is by far the best tool I've used to understand and check what is the compiler really doing under the hood.
- use DMA. The first thing I do when I design a new board is to enumarate the communication ports I'll need (SPI, USART, I2C, etc.), then start STM32CubeMX and use it to check DMA assignments. The goal is to maximize the number of DMA you can use on your peripherals without conflict. DMAs are blessings, especially with USART and SPI, it helps a lot keeping the processor free for more interesting calculations, and maximizes the throughput.
- use an RTOS. It seems paradoxical as the RTOS itself will use some processing power, but if optimized well, it offers so many facilities to write elegant an efficient code it is a must have, and overall will produce faster code (both faster to write and faster to run).
4 - FILTERING / SENSOR FUSION / PID
These three subjects are tightly related (and coupled), fusion can do some filtering, PID relies on sensor fusion, and need special filtering (e.g derivation filtering), etc.
These are the core of a flight controller, there is a lot of documentation available online, some better than other.
We do not use kalman filters per se, we do as much direct modeling of the system to control (refer to instrumentation ...), then feed the sensors to correct estimations, as this allows to have the smallest delay.
Here is THE word ... delay. This is your worst enemy, and making a good flight controller means decreasing delay as much as possible.
Use DMAs, fast communication ports (SPI for sensors), optimize code, fine tune peripherals to decrease delay as much as possible (e.g do not use PWM freewheeling, but rather synchronise it with the main loop, use one pulse mode, and disable register shadowing). This is where the magic happens, each decrease in global delay means an increase in performance, each increase in delay (filter => delay) will decrease performance. There are many techniques for this I'll explain in a later post.
That's it for today, I'd appreciate feedback, criticism is welcome of course, and if you have a preference for the next detailed post let me know !
PS : a couple videos: