Protoflight is flight control software.
It has the following design goals (in no particular order)
- A modular design that is built up from components in separate libraries (see below).
- Produce libraries that are usable in their own right.
- Be peformant. Support 8kHz Gyro/PID loop time.
- Support dual-core processors, in particular allow the Gyro/PID loop to have an entire core to itself.
- Run on bare metal or under FREERTOS.
- Be (relatively) easy to learn and modify.
- Give users the ability implement their own code or modifications.
- Modular architecture to make it easier to identify which bit of code to modify, without impacting other code.
- Be useful to people who want to experiment with and customize a flight controller.
- Be useful to someone who wants to understand how flight control software works.
- Be Betaflight "Tool compatible". This is be able to use Betaflight Configurator and Betaflight Blackbox Explorer.
I've called it Protoflight because:
- It can be used to prototype new ideas.
- One of the meanings of "proto" is "primitive". This software is nowhere near as sophisticated as BetaFlight or ArduPilot.
- It is related to "protean", meaning "able to change frequently or easily" or "versatile".
- It pays homage to Protea, which was the codename for the Psion Series 5
- VectorQuaternionMatrix - 3D Vectors, Quaternions, and 3x3 Matrices.
- TaskBase - base class for all tasks.
- Filters - various filters.
- PIDF - PID (Proportional Integral Derivative) controller with optional feedforward and open loop control.
- Sensors - gyroscopes, accelerometers, and barometers
- SensorFusion - sensor fusion, including: Complementary Filter, Mahony Filter, Madgwick Filter, and Versatile Quaternion Filter (VQF).
- Stabilized Vehicle - AHRS (Attitude and Heading Reference System) and VehicleControllerBase (base class for stabilized vehicles).
- MotorMixers MotorMixers. Converts desired throttle and roll, pitch, yaw torques into motor commands. Supports PWM and bidirectional DShot protocols. Also includes RPM filters and dynamic idle control.
- Receiver - receiver base class and implementations, including implementation using ESP-NOW.
- Backchannel - backchannel over ESP-NOW for telemetry, PID tuning, and benchmarking.
- StreamBuf - simple serializer/deserializer with no bounds checking
- FlashKLV - Flash Key Length Value (KLV) storage
- Blackbox - port of implementation by Nicholas Sherlock (aka thenickdude).
- MultiWiiSerialProtocol (MSP) - ported from Betaflight MultiWiiSerialProtocol.
The design/software framework has undergone a number of iterations and I don't expect to make any major changes to it. However it is still somewhat in flux and I expect there will continue to be changes. In particular:
- The Receiver class does not yet handle telemetry. Adding this may involve structural changes to the Receiver class and/or Receiver task.
The Protoflight project is a "spare time" project: updates will be made when I have the time and inclination. The following are areas that I, at some point, would like to tackle. In no particular order:
- Implement DMA on ESP32 (DMA currently only working on RPI Pico).
- Implement Backchannel over UDP. This would allow Backchannel to be used on RPI Pico2W.
- Implement DShot on ESP32 (currently only implemented for RPI Pico).
- Implement saving Blackbox to flash (currently only saves to SD-card)
- Investigate filtering the output from the PIDs and/or the inputs to the motors.
- Investigate new ways of handling PID integral windup.
- Add On-screen display (OSD) with menu system.
The core of this code was originally developed for a Self Balancing Robot (SBR).
One of the early design decisions for the SBR was to split the functionality into libraries, and make each of these libraries usable
in their own right. Two of the early libraries were the vector/quaternion library and the sensor fusion library. I named the 3D vector
class xyz_t rather than Vector to avoid any confusion with the C++ vector container class.
An SBR runs in "Angle Mode" all the time: what is being stabilized is the pitch angle. This means the the sensor fusion algorithm runs every iteration of the "Gyro/PID loop". So the performance of the sensor fusion is critical. There are a number of Arduino Madgwick Sensor fusion libraries, but they all seem to be copies of each other, presumably all copied from some original implementation. This implementation was not particularly efficient, so I wrote my own optimized version.
It turns out that for yaw rate control on an SBR you don't need a PID controller - simple open loop control (ie setting the output proportional to the setpoint) works fine. So rather than implement a separate open loop controller for yaw, I added what I then called a feed forward component to the PID controller - that is a term that gave output proportional to the setpoint. (It turns out that this is not the same as Betaflight feed forward, so this was later renamed).
My original SBR implementation was on an ESP32. So ESPNOW was used to implement the receiver. ESPNOW supports several what it calls "peers" over a radio channel, so I decided to implement a "Backchannel" that could be used for telemetry and PID tuning.
The backchannel turned out to be incredibly useful for debugging, benchmarking, and seeing the PID values in realtime, so I decided to run the "Gyro/PID loop" all the time, even when the motors are switched off. Of course you get terrible integral windup if the motors are off so I added functions to the PID controller to turn PID integration on and off.
While I was writing the code for the SBR I realized that I had written virtually everything required for a Flight Controller. The main new requirements were:
- a MotorMixer that could handle output to the 4 propeller motors on a quadcopter rather than the 2 wheel motors on an SBR
- a flight controller that could stabilize the 3 axes of roll, pitch, and yaw, rather than the single axis of pitch on an SBR
- a wider range of filters, since the vibrations on an SBR are not that severe and don't require too much filtering
So I split the MotorPairController I had written for the SBR into VehicleControllerBase and MotorPairController and used
VehicleControllerBase as a base class for a new FlightController class.
I originally developed the SBR on ESP32 hardware, and ESP32 has FREERTOS built into its development software, so it was natural to continue using FREERTOS.
It soon became clear that this rabbit hole was a bit deeper than I thought it would be.
A flight controller has many more parameters that need tuning that an SBR. The Backchannel I had developed for the SBR was very useful for developing the FlightController, but it was not sufficient. Rather than spend a lot of effort in adding features to the Backchannel I decided it would be better (and less effort) to port MultiWiiSerialProtocol and Blackbox from Betaflight. That way I could (in principle) use the Betaflight configurator and Betaflight blackbox viewer. These ports are still in progress.
While porting the Blackbox and MSP I renamed and rearranged some of the Protoflight parameters - this is ongoing.
While doing this port I also realized that the FeedForward in Betaflight was not the same as FeedForward in Protoflight. Betaflight's FeedForward is proportional to the rate of change of the setpoint, whereas Protoflight's FeedForward was proportional to the setpoint. So I changed the meaning of Protoflight's FeedForward, and added a new PID component (called 'S' for setpoint) which is proportional to the setpoint. (Betaflight also has an 'S' component for PIDs, as far as I can tell this is only used for fixed wing aircraft and is proportional to the setpoint.)
The project is still ongoing and has not yet achieved "first flight". The Self Balancing Robot has achieved "First Stabilized Drive" so I am confident in all the lower layers of the software stack (that is libraries 1-10 listed above).
I currently have ports of the software to ESP32 and Raspberry Pi Pico, but not yet to STM32.
One of the reasons for concentrating on ESP32 and RPI Pico was that they both have dual-core versions available, and one of my
primary interests is that of having the Gyro/PID loop
(what I snappily call the readIMUandUpdateOrientation/updateOutputsUsingPIDs loop) having a whole core entirely to itself.
Protoflight will run on a single core, but I find the dual core setup more interesting.
I have on occasion toyed with the idea of running the PIDs in Quaternion Space. That is, instead of having 3 PIDs (one for each of roll, pitch, and yaw) a transformation would be applied to these PIDs, mapping them onto 4 PIDs in Quaternion Space, one PID for each of w, x, y, and z.
The idea of operating in Quaternion Space is not without precedent: the Madgwick Sensor Fusion Filter can in some ways be regarded as a Complementary Filter that operates in Quaternion Space.
However operating PIDs in Quaternion Space is problematic:
- There is little advantage for controlling roll, pitch, and yaw rates.
- There is some advantage in using it for controlling roll and pitch angles, but here we would then replace two PIDs by four PIDs negating some of that advantage.
- The mapping of PIDs in Euler Angle Space to PIDs in Quaternion Space is by no means obvious.
- PID tuning in Quaternion Space is likely to be complicated.
I realized that there is an intermediate space between Euler Angle Space and Quaternion Space: the space defined by the sine of the
Euler Angles. So the measurement input to the rollAngle PID would be not the rollAngle, but would be sin(rollAngle). And the setpoint
of the rollAngle PID would not be the desiredRollAngle, but sin(desiredRollAngle) (and likewise for pitchAngle). This would avoid using the
computationally expensive use of Quaternion::calculateRollDegrees() and Quaternion::calculateRollDegrees(), instead you could
use Quaternion::sinRoll() and Quaternion::sinPitch() which are relatively cheap computationally.
But, I hear you say, you've just replaced the computationally expensive Quaternion::calculateRollDegrees() with sinf(desiredRollAngle)
so there is little benefit. This is true, but sinf(desiredRollAngle) only changes when a new stick value is received, and so can be
calculated in the Receiver Task.
What about the fact that sin(rollAngle) is a non-linear function of rollAngle? Well, PIDs deal with non-linear systems all the time, so I don't think that will be a problem. There also remains the possibility of changing the "rates" to compensate for this, if necessary.
Since the "intermediate between Euler Angle and Quaternion space" is a bit of a mouthful, and this intermediate space is closer to Quaternion Space than Euler Angle Space, I'm takings some programmer's license and using the term Quaternion Space for this space.
Preliminary benchmarks (on an ESP32 S3 running at 240MHz) that the angle mode PID calculations take 80 microseconds with running in Euler Angle Space and 50 microseconds in Quaternion Space. By alternatively calculating the roll angle values and pitch angle values in each iteration of the control loop, the Quaternion Space calculation is reduced to 25 microseconds. This means it is feasible to do the angle mode stabilization in the main control loop, even when it is running at 8kHz. Feasible doesn't mean possible, but this is certainly worth investigating. For a 4kHz control loop this certainly seems possible.
This is a description of the Main Control Loop, often called the "Gyro/PID loop". The there are variations in this loop depending on the hardware configuration, this describes operation for "ideal" hardware, namely:
- IMU connected via SPI, with interrupt pin connected
- IMU capable of 8kHz update frequency
- IMU that can be read in little-endian format
- Dual-core processor with floating point unit (FPU)
The heart of the loop is the AHRS::readIMUandUpdateOrientation function, invocation is as follows:
- The IMU generates a new reading and sets its interrupt pin.
- The IMU device driver ISR (interrupt service routine) is called and it initiates a read of the IMU via DMA (direct memory access).
- The DMA completes. The IMU device driver converts the raw IMU data into real world units, taking into account the IMU orientation within the aircraft frame.
- The IMU device driver signals the AHRS task that there is a new IMU value available by putting the IMU reading in a shared message queue
- The AHRS task receives the signal, gets the IMU value from the shared message queue, and calls the
AHRS::readIMUandUpdateOrientationfunction. - THE CLOCK STARTS NOW. For an 8kHz update frequency we have 125 microseconds (see NOTE A) to complete all the following steps, (Timings for each step in microseconds on an 240MHz ESP32 S3 are given where available).
- The AHRS gets the IMU reading from the IMU device.
- The AHRS filters the IMU reading, using the RPM filters and all other filters (1st harmonic only:15us(max19),1st+3rd:21us) (see NOTE B).
- The AHRS calculates the current orientation using sensor fusion of the gyro and accelerometers values (20us).
- The AHRS calls
FlightController::updateOutputsUsingPIDspassing the gyro, accelerometer and orientation values (acroMode:20us,angleModeQuaternionSpace:50us,angleModeEulerAngleSpace:80us). - If the blackbox is active, data is copied to the blackbox message queue for logging by the Blackbox task.
NOTE A: strictly speaking we don't have the full 125 microseconds - step 3 also uses a few microseconds of this time (which is why it is preferable to have an IMU that can be read in little-endian format - this avoids the need for byte reordering).
NOTE B: the RPM filter frequencies are set in the FlightController task in the FlightController::outputToMixer function. This function
calls MotorMixer::outputToMotors which calculates the motor mix, writes the outputs to the motors, reads the motor RPMs, and then sets
the RPM filter frequencies.
NOTE C: steps 1-5 can be made more efficient when using a Raspberry Pi Pico and PIO, namely:
- The IMU generates a new reading and sets its interrupt pin.
- The PIO state machine that was waiting on the IMU interrupt PIN reads the IMU and puts the result in its TX FIFO.
- The AHRS task which, was waiting on the PIO TX FIFO, gets the IMU value and calls the
AHRS::readIMUandUpdateOrientationfunction.
This avoids both the ISR and the shared message queue.
UML Diagrams have become somewhat unfashionable. But I like them. I agree that they have limited use in the design phase of a project, but I find they are very useful to document a design. The process of documenting a design can reveal subtleties that were overlooked, and indeed I found that when documenting Protoflight.
The Cockpit is the command center for the aircraft.
All objects are statically allocated in Main::setup().
When the hardware supports it AHRS_Task and Receiver_Task are interrupt-driven.
VehicleController_Task is triggered when AHRS::readIMUandUpdateOrientation completes.
Classes with "(eg)" suffix give examples of specific instances.
On dual-core processors AHRS_Task has the entire second core to itself.
---
config:
class:
hideEmptyMembersBox: true
---
classDiagram
class TaskBase:::taskClass {
}
link TaskBase "https://github.com/martinbudden/Library-TaskBase/blob/main/src/TaskBase.h"
TaskBase <|-- AHRS_Task
TaskBase <|-- VehicleControllerTask
TaskBase <|-- ReceiverTask
TaskBase <|-- BlackboxTask
class AHRS_Task:::taskClass {
}
link AHRS_Task "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/AHRS_Task.h"
AHRS_Task o-- IMU_Base : calls WAIT_IMU_DATA_READY
AHRS_Task --o IMU_Base : calls SIGNAL
AHRS_Task o-- AHRS : calls readIMUandUpdateOrientation
class IMU_Base {
<<abstract>>
WAIT_IMU_DATA_READY()
SIGNAL_IMU_DATA_READY_FROM_ISR()
virtual readAccGyroRPS() accGyroRPS_t
}
link IMU_Base "https://github.com/martinbudden/Library-Sensors/blob/main/src/IMU_Base.h"
IMU_Base <|-- IMU_BMI270 : overrides readAccGyroRPS
class IMU_BMI270["IMU_BMI270(eg)"]
link IMU_BMI270 "https://github.com/martinbudden/Library-Sensors/blob/main/src/IMU_BMI270.h"
class IMU_FiltersBase {
<<abstract>>
filter() *
}
link IMU_FiltersBase "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/IMU_FiltersBase.h"
class DynamicNotchFilter {
array~BiquadFilter~ notchFilters
filter()
}
link DynamicNotchFilter "https://github.com/martinbudden/protoflight/blob/main/lib/FlightController/src/DynamicNotchFilter.h"
class RPM_Filters {
array~BiquadFilterT~xyz_t~~ _filters[MOTORS][HARMONICS]
setFrequencyHzIterationStep()
filter()
}
link RPM_Filters "https://github.com/martinbudden/protoflight/blob/main/lib/FlightController/src/RPM_Filters.h"
class IMU_Filters {
FilterT~xyz_t~ _gyroLPF
BiquadFilterT~xyz_t~ _gyroNotch
filter() override
}
link IMU_Filters "https://github.com/martinbudden/protoflight/blob/main/lib/FlightController/src/IMU_Filters.h"
IMU_Filters o-- DynamicNotchFilter : calls filter
%%DynamicNotchFilter --o IMU_Filters : calls filter
IMU_Filters o-- RPM_Filters : calls filter
IMU_FiltersBase <|-- IMU_Filters : overrides filter
%%IMU_Filters o-- MotorMixerBase
class SensorFusionFilterBase {
Quaternion orientation
<<abstract>>
updateOrientation() Quaternion *
}
link SensorFusionFilterBase "https://github.com/martinbudden/Library-SensorFusion/blob/main/src/SensorFusion.h"
SensorFusionFilterBase <|-- MadgwickFilter : overrides updateOrientation
class MadgwickFilter["MadgwickFilter(eg)"] {
updateOrientation() override
}
link MadgwickFilter "https://github.com/martinbudden/Library-SensorFusion/blob/main/src/SensorFusion.h"
class AHRS {
bool readIMUandUpdateOrientation()
}
link AHRS "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/AHRS.h"
AHRS o-- IMU_Base : calls readAccGyroRPS
AHRS o-- IMU_FiltersBase : calls filter
AHRS o-- SensorFusionFilterBase : calls updateOrientation
AHRS o-- VehicleControllerBase : calls updateOutputsUsingPIDs / SIGNAL
class VehicleControllerBase {
<<abstract>>
WAIT()
SIGNAL()
outputToMixer() *
updateOutputsUsingPIDs() *
}
link VehicleControllerBase "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/VehicleControllerBase.h"
class VehicleControllerTask:::taskClass {
}
link VehicleControllerTask "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/VehicleControllerTask.h"
%%VehicleControllerBase --o VehicleControllerTask : calls WAIT / outputToMixer
VehicleControllerTask o-- VehicleControllerBase : calls WAIT / outputToMixer
class VehicleControllerMessageQueue {
WAIT()
SIGNAL()
}
link VehicleControllerMessageQueue "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/VehicleControllerMessageQueue.h"
class MotorMixerBase {
<<abstract>>
outputToMotors() *
}
link MotorMixerBase "https://github.com/martinbudden/protoflight/blob/main/lib/MotorMixers/src/MotorMixerBase.h"
class DynamicIdleController {
calculateSpeedIncrease()
}
link DynamicIdleController "https://github.com/martinbudden/protoflight/blob/main/lib/MotorMixers/src/DynamicIdleController.h"
class MotorMixerQuadX_DShot["MotorMixerQuadX_DShot(eg)"]
link MotorMixerQuadX_DShot "https://github.com/martinbudden/protoflight/blob/main/lib/MotorMixers/src/MotorMixerQuadX_DShot.h"
MotorMixerQuadX_DShot *-- RPM_Filters : calls setFrequencyHzIterationStep
%%RPM_Filters --* MotorMixerQuadX_DShot : calls setFrequencyHzIterationStep
MotorMixerQuadX_DShot *-- DynamicIdleController : calls calculateSpeedIncrease
MotorMixerBase <|-- MotorMixerQuadX_Base : overrides outputToMotors
MotorMixerQuadX_Base <|-- MotorMixerQuadX_DShot : overrides getMotorFrequencyHz
class MotorMixerQuadX_Base {
array~float,4~ _motorOutputs
}
link MotorMixerQuadX_Base "https://github.com/martinbudden/protoflight/blob/main/lib/MotorMixers/src/MotorMixerQuadX_Base.h"
class FlightController {
array~PIDF~ _pids
array~Filter~ _dTermFilters
array~Filter~ _stickSetpointFilters
outputToMixer() override
updateOutputsUsingPIDs() override
updateSetpoints()
}
link FlightController "https://github.com/martinbudden/protoflight/blob/main/lib/FlightController/src/FlightController.h"
VehicleControllerBase *-- VehicleControllerMessageQueue : calls WAIT / SIGNAL
VehicleControllerBase <|-- FlightController : overrides outputToMixer updateOutputsUsingPIDs
FlightController o-- MotorMixerBase : calls outputToMotors
class CockpitBase {
<<abstract>>
updateControls() *
checkFailsafe() *
}
link CockpitBase "https://github.com/martinbudden/Library-Receiver/blob/main/src/CockpitBase.h"
CockpitBase <|-- Cockpit : overrides updateControls
CockpitBase o--ReceiverBase : calls getAuxiliaryChannel
class Cockpit {
updateControls() override
checkFailsafe() override
getFailsafePhase()
}
link Cockpit "https://github.com/martinbudden/protoflight/blob/main/lib/Helm/src/Cockpit.h"
Cockpit o-- FlightController : calls updateSetpoints
Cockpit o-- Autopilot : calls calculateFlightControls
class ReceiverTask:::taskClass {
}
link ReceiverTask "https://github.com/martinbudden/Library-Receiver/blob/main/src/ReceiverTask.h"
ReceiverTask o-- CockpitBase : calls updateControls
ReceiverTask o-- ReceiverBase : calls update / getStickValues
%%CockpitBase --o ReceiverTask : calls updateControls
class Autopilot {
array~geographic_coordinate_t~ _waypoints
altitudeHoldCalculateThrottle()
calculateFlightControls()
}
link Autopilot "https://github.com/martinbudden/protoflight/blob/main/lib/Helm/src/Autopilot.h"
Autopilot o-- AHRS_MessageQueue : calls PEEK_AHRS_DATA
class AHRS_MessageQueue {
ahrs_data_t ahrsData
WAIT() override
SIGNAL(const ahr_data_t&)
SEND_AHRS_DATA(const ahr_data_t&)
PEEK_AHRS_DATA(const ahr_data_t&)
getReceivedAHRS_Data()
}
link AHRS_MessageQueue "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/AHRS_MessageQueue.h"
FlightController o-- AHRS_MessageQueue : calls SIGNAL
%%AHRS_MessageQueue --o FlightController : calls SIGNAL
class ReceiverBase {
<<abstract>>
WAIT_FOR_DATA_RECEIVED() *
update() *
getStickValues() *
getAuxiliaryChannel() *
}
link ReceiverBase "https://github.com/martinbudden/Library-Receiver/blob/main/src/ReceiverBase.h"
class ReceiverAtomJoyStick {
update() override
}
ReceiverBase <|-- ReceiverAtomJoyStick
class ReceiverAtomJoyStick["ReceiverAtomJoyStick(eg)"]
link ReceiverAtomJoyStick "https://github.com/martinbudden/Library-Receiver/blob/main/src/ReceiverAtomJoyStick.h"
class BlackboxTask:::taskClass {
}
link BlackboxTask "https://github.com/martinbudden/Library-Blackbox/blob/main/src/BlackboxTask.h"
BlackboxTask o-- AHRS_MessageQueue : calls WAIT
BlackboxTask o-- Blackbox : calls update
class Blackbox {
<<abstract>>
writeSystemInformation() *
start()
finish()
update() uint32_t
}
link Blackbox "https://github.com/martinbudden/Library-Blackbox/blob/main/src/Blackbox.h"
Blackbox <|-- BlackboxProtoFlight
class BlackboxProtoFlight {
writeSystemInformation() override
}
link BlackboxProtoFlight "https://github.com/martinbudden/protoflight/blob/main/lib/Blackbox/src/BlackboxProtoFlight.h"
classDef taskClass fill:#f96
The AHRS_Task and the ReceiverTask may be either interrupt driven or timer driven.
On a dual-core processor AHRS_Task has the second core all to itself.
Tasks are statically (build-time) polymorphic, not dynamically (run-time) polymorphic.
They all have task and loop functions, but these functions are not virtual.
This is deliberate.
BackchannelTask, BlackboxTask, DashboardTask, and MSP_Task are optional tasks and are not required for flight.
Task synchronization primitives are shown in CAPITALS, ie WAIT/SIGNAL
In the case where AHRS_Task and ReceiverTask are interrupt driven, we have:
- The
AHRS_Taskwaits on theIMUwhich signals it whenever the IMU has a new data reading (typically this occurs at 1kHz to 8kHz) - The
ReceiverTaskwaits on theReceiverwhich signals it whenever it receives a data packet (typically this occurs at 20Hz to 200Hz) - The
VehicleControllerTaskwaits on theVehicleControllerwhich is signalled by theAHRSeveryNtimes it callsupdateOutputsUsingPIDs, whereNis configurable. - The
BlackboxTaskwaits on theAHRS_MessageQueue, which is signaled by theAHRSwhen it has new data. TheBlackboxTaskwill write data to storage everyNtimes it is called, whereNis configurable. - The
BackchannelTaskis called on a timer. - The
MSP_Taskis called on a timer.
---
config:
class:
hideEmptyMembersBox: true
---
classDiagram
class TaskBase:::taskClass {
}
link TaskBase "https://github.com/martinbudden/Library-TaskBase/blob/main/src/TaskBase.h"
TaskBase <|-- AHRS_Task
TaskBase <|-- VehicleControllerTask
TaskBase <|-- ReceiverTask
TaskBase <|-- DashboardTask
TaskBase <|-- BlackboxTask
TaskBase <|-- BackchannelTask
TaskBase <|-- MSP_Task
class AHRS_Task:::taskClass {
}
link AHRS_Task "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/AHRS_Task.h"
AHRS_Task o-- IMU_Base : calls WAIT_IMU_DATA_READY
AHRS_Task --o IMU_Base : calls SIGNAL
AHRS_Task o-- AHRS : calls readIMUandUpdateOrientation
class IMU_Base {
<<abstract>>
WAIT_IMU_DATA_READY()
SIGNAL_IMU_DATA_READY_FROM_ISR()
virtual readAccGyroRPS() accGyroRPS_t
}
link IMU_Base "https://github.com/martinbudden/Library-Sensors/blob/main/src/IMU_Base.h"
IMU_Base <|-- IMU_BMI270 : overrides readAccGyroRPS
class IMU_BMI270["IMU_BMI270(eg)"]
link IMU_BMI270 "https://github.com/martinbudden/Library-Sensors/blob/main/src/IMU_BMI270.h"
class IMU_FiltersBase {
<<abstract>>
filter() *
}
link IMU_FiltersBase "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/IMU_FiltersBase.h"
class DynamicNotchFilter {
array~BiquadFilter~ notchFilters
filter()
}
link DynamicNotchFilter "https://github.com/martinbudden/protoflight/blob/main/lib/FlightController/src/DynamicNotchFilter.h"
class RPM_Filters {
array~BiquadFilterT~xyz_t~~ _filters[MOTORS][HARMONICS]
setFrequencyHzIterationStep()
filter()
}
link RPM_Filters "https://github.com/martinbudden/protoflight/blob/main/lib/FlightController/src/RPM_Filters.h"
class IMU_Filters {
FilterT~xyz_t~ _gyroLPF
BiquadFilterT~xyz_t~ _gyroNotch
filter() override
}
link IMU_Filters "https://github.com/martinbudden/protoflight/blob/main/lib/FlightController/src/IMU_Filters.h"
IMU_Filters o-- DynamicNotchFilter : calls filter
IMU_Filters o-- RPM_Filters : calls filter
IMU_FiltersBase <|-- IMU_Filters : overrides filter
class SensorFusionFilterBase {
Quaternion orientation
<<abstract>>
updateOrientation() Quaternion *
}
link SensorFusionFilterBase "https://github.com/martinbudden/Library-SensorFusion/blob/main/src/SensorFusion.h"
SensorFusionFilterBase <|-- MadgwickFilter : overrides updateOrientation
class MadgwickFilter["MadgwickFilter(eg)"] {
updateOrientation() override
}
link MadgwickFilter "https://github.com/martinbudden/Library-SensorFusion/blob/main/src/SensorFusion.h"
class AHRS {
bool readIMUandUpdateOrientation()
}
link AHRS "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/AHRS.h"
AHRS o-- IMU_Base : calls readAccGyroRPS
AHRS o-- IMU_FiltersBase : calls filter
AHRS o-- SensorFusionFilterBase : calls updateOrientation
AHRS o-- VehicleControllerBase : calls updateOutputsUsingPIDs / SIGNAL
class VehicleControllerBase {
<<abstract>>
WAIT()
SIGNAL()
outputToMixer() *
updateOutputsUsingPIDs() *
}
link VehicleControllerBase "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/VehicleControllerBase.h"
class VehicleControllerTask:::taskClass {
}
link VehicleControllerTask "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/VehicleControllerTask.h"
VehicleControllerTask o-- VehicleControllerBase : calls WAIT / outputToMixer
class VehicleControllerMessageQueue {
WAIT()
SIGNAL()
}
link VehicleControllerMessageQueue "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/VehicleControllerMessageQueue.h"
class MotorMixerBase {
<<abstract>>
outputToMotors() *
}
link MotorMixerBase "https://github.com/martinbudden/protoflight/blob/main/lib/MotorMixers/src/MotorMixerBase.h"
class DynamicIdleController {
calculateSpeedIncrease()
}
link DynamicIdleController "https://github.com/martinbudden/protoflight/blob/main/lib/MotorMixers/src/DynamicIdleController.h"
class MotorMixerQuadX_DShot["MotorMixerQuadX_DShot(eg)"]
link MotorMixerQuadX_DShot "https://github.com/martinbudden/protoflight/blob/main/lib/MotorMixers/src/MotorMixerQuadX_DShot.h"
MotorMixerQuadX_DShot *-- RPM_Filters : calls setFrequencyHzIterationStep
MotorMixerQuadX_DShot *-- DynamicIdleController : calls calculateSpeedIncrease
MotorMixerBase <|-- MotorMixerQuadX_Base : overrides outputToMotors
MotorMixerQuadX_Base <|-- MotorMixerQuadX_DShot : overrides getMotorFrequencyHz
class MotorMixerQuadX_Base {
array~float,4~ _motorOutputs
}
link MotorMixerQuadX_Base "https://github.com/martinbudden/protoflight/blob/main/lib/MotorMixers/src/MotorMixerQuadX_Base.h"
class FlightController {
array~PIDF~ _pids
array~Filter~ _dTermFilters
array~Filter~ _stickSetpointFilters
outputToMixer() override
updateOutputsUsingPIDs() override
updateSetpoints()
}
link FlightController "https://github.com/martinbudden/protoflight/blob/main/lib/FlightController/src/FlightController.h"
VehicleControllerBase *-- VehicleControllerMessageQueue : calls WAIT / SIGNAL
VehicleControllerBase <|-- FlightController : overrides outputToMixer updateOutputsUsingPIDs
FlightController o-- MotorMixerBase : calls outputToMotors
class CockpitBase {
<<abstract>>
updateControls() *
checkFailsafe() *
}
link CockpitBase "https://github.com/martinbudden/Library-Receiver/blob/main/src/CockpitBase.h"
CockpitBase <|-- Cockpit : overrides updateControls
CockpitBase o--ReceiverBase : calls getAuxiliaryChannel
class Cockpit {
updateControls() override
checkFailsafe() override
getFailsafePhase()
}
link Cockpit "https://github.com/martinbudden/protoflight/blob/main/lib/Helm/src/Cockpit.h"
Cockpit o-- FlightController : calls updateSetpoints
Cockpit o-- Autopilot : calls calculateFlightControls
class ReceiverTask:::taskClass {
}
link ReceiverTask "https://github.com/martinbudden/Library-Receiver/blob/main/src/ReceiverTask.h"
ReceiverTask o-- CockpitBase : calls updateControls
ReceiverTask o-- ReceiverBase : calls update / getStickValues
ReceiverTask o-- ReceiverWatcher : calls newReceiverPacketAvailable
class Autopilot {
array~geographic_coordinate_t~ _waypoints
altitudeHoldCalculateThrottle()
calculateFlightControls()
}
link Autopilot "https://github.com/martinbudden/protoflight/blob/main/lib/Helm/src/Autopilot.h"
Autopilot o-- AHRS_MessageQueue : calls PEEK_AHRS_DATA
class AHRS_MessageQueue {
ahrs_data_t ahrsData
WAIT() override
SIGNAL(const ahr_data_t&)
SEND_AHRS_DATA(const ahr_data_t&)
PEEK_AHRS_DATA(const ahr_data_t&)
getReceivedAHRS_Data()
}
link AHRS_MessageQueue "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/AHRS_MessageQueue.h"
FlightController o-- AHRS_MessageQueue : calls SIGNAL
class ReceiverBase {
<<abstract>>
WAIT_FOR_DATA_RECEIVED() *
update() *
getStickValues() *
getAuxiliaryChannel() *
}
link ReceiverBase "https://github.com/martinbudden/Library-Receiver/blob/main/src/ReceiverBase.h"
class ReceiverAtomJoyStick {
update() override
}
ReceiverBase <|-- ReceiverAtomJoyStick
class ReceiverAtomJoyStick["ReceiverAtomJoyStick(eg)"]
link ReceiverAtomJoyStick "https://github.com/martinbudden/Library-Receiver/blob/main/src/ReceiverAtomJoyStick.h"
class Screen {
newReceiverPacketAvailable() override
}
Screen o-- AHRS_MessageQueue : calls PEEK_AHRS_DATA
class ReceiverWatcher {
<<abstract>>
newReceiverPacketAvailable() *
}
ReceiverWatcher <|-- Screen
class DashboardTask:::taskClass {
+loop()
-task() [[noreturn]]
}
DashboardTask o-- Screen : calls update
DashboardTask o-- Buttons : calls update
class BlackboxTask:::taskClass {
}
link BlackboxTask "https://github.com/martinbudden/Library-Blackbox/blob/main/src/BlackboxTask.h"
BlackboxTask o-- AHRS_MessageQueue : calls WAIT
BlackboxTask o-- Blackbox : calls update
class Blackbox {
<<abstract>>
writeSystemInformation() *
start()
finish()
update() uint32_t
}
link Blackbox "https://github.com/martinbudden/Library-Blackbox/blob/main/src/Blackbox.h"
Blackbox <|-- BlackboxProtoFlight
class BlackboxProtoFlight {
writeSystemInformation() override
}
link BlackboxProtoFlight "https://github.com/martinbudden/protoflight/blob/main/lib/Blackbox/src/BlackboxProtoFlight.h"
class BackchannelTask:::taskClass {
}
link BackchannelTask "https://github.com/martinbudden/Library-Backchannel/blob/main/src/BackchannelTask.h"
BackchannelTask o-- Backchannel : calls processedReceivedPacket
class Backchannel {
processedReceivedPacket() bool
}
Backchannel o-- AHRS_MessageQueue : calls PEEK_AHRS_DATA
class MSP_Task:::taskClass {
}
link MSP_Task "https://github.com/martinbudden/Library-MultiWiiSerialProtocol/blob/main/src/MSP_Task.h"
MSP_Task o-- MSP_SerialBase : calls processInput
class MSP_SerialBase {
<<abstract>>
sendFrame() *
processInput() *
}
class MSP_Serial {
sendFrame() override
processInput() override
}
MSP_SerialBase <|-- MSP_Serial
classDef taskClass fill:#f96
BlackboxProtoFlight encodes system information (ie the header of the blackbox file) when blackbox is started.
BlackboxCallbacks encodes blackbox data during flight
All writing to the serial device is done via the BlackboxEncoder
classDiagram
class TaskBase:::taskClass {
}
link TaskBase "https://github.com/martinbudden/Library-TaskBase/blob/main/src/TaskBase.h"
TaskBase <|-- BlackboxTask
class BlackboxTask {
+loop()
-task() [[noreturn]]
}
link BlackboxTask "https://github.com/martinbudden/Library-Blackbox/blob/main/src/BlackboxTask.h"
BlackboxTask o-- MessageQueueBase : calls WAIT
BlackboxTask o-- Blackbox : calls update
class Blackbox {
<<abstract>>
virtual writeSystemInformation() *
update()
}
link Blackbox "https://github.com/martinbudden/Library-Blackbox/blob/main/src/Blackbox.h"
class MessageQueueBase {
<<abstract>>
virtual WAIT() *
}
link MessageQueueBase "https://github.com/martinbudden/Library-TaskBase/blob/main/src/MessageQueueBase.h"
class AHRS_MessageQueue {
ahrs_data_t ahrsData
WAIT() override
SIGNAL(const ahr_data_t&)
SEND_AHRS_DATA(const ahr_data_t&)
PEEK_AHRS_DATA(const ahr_data_t&)
getReceivedAHRS_Data()
}
link AHRS_MessageQueue "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/AHRS_MessageQueue.h"
class CockpitBase {
<<abstract>>
}
link CockpitBase "https://github.com/martinbudden/Library-Receiver/blob/main/src/CockpitBase.h"
CockpitBase <|-- Cockpit
class Cockpit {
getRates() rates_t const
}
link Cockpit "https://github.com/martinbudden/protoflight/blob/main/lib/Helm/src/Cockpit.h"
%%Blackbox <|-- BlackboxProtoFlight
class BlackboxProtoFlight {
writeSystemInformation() override
}
link BlackboxProtoFlight "https://github.com/martinbudden/protoflight/blob/main/lib/Blackbox/src/BlackboxProtoFlight.h"
class BlackboxCallbacksBase {
<<abstract>>
virtual loadSlowState() *
virtual loadMainState() *
}
link BlackboxCallbacksBase "https://github.com/martinbudden/Library-Blackbox/blob/main/src/BlackboxCallbacksBase.h"
class BlackboxCallbacksProtoFlight {
loadSlowState() override
loadMainState() override
}
class ReceiverBase {
<<abstract>>
}
link ReceiverBase "https://github.com/martinbudden/Library-Receiver/blob/main/src/ReceiverBase.h"
Blackbox o-- BlackboxCallbacksBase : calls loadSlow/MainState
Blackbox <|-- BlackboxProtoFlight : overrides writeSystemInformation
Blackbox o-- BlackboxSerialDevice : calls open/close
Blackbox *-- BlackboxEncoder : calls write
%%BlackboxEncoder --* Blackbox : calls write
%%BlackboxSerialDevice --o Blackbox : calls open close
BlackboxEncoder o-- BlackboxSerialDevice : calls write
class FlightController {
}
link FlightController "https://github.com/martinbudden/protoflight/blob/main/lib/FlightController/src/FlightController.h"
FlightController o-- AHRS_MessageQueue : calls SIGNAL
BlackboxCallbacksProtoFlight o-- AHRS_MessageQueue : calls getReceivedAHRS_Data
MessageQueueBase <|-- AHRS_MessageQueue
BlackboxCallbacksProtoFlight o-- FlightController : calls getPID
BlackboxCallbacksProtoFlight o-- ReceiverBase : calls getControls
BlackboxCallbacksProtoFlight o-- CockpitBase : calls getFailSafePhase
%%FlightController --o BlackboxCallbacksProtoFlight
BlackboxCallbacksBase <|-- BlackboxCallbacksProtoFlight
%%BlackboxCallbacksProtoFlight --|> BlackboxCallbacksBase
%%FlightController --o BlackboxProtoFlight
%%Blackbox --o FlightController
BlackboxProtoFlight o-- FlightController
Cockpit --o BlackboxProtoFlight : calls getRates
%%BlackboxProtoFlight o-- Cockpit : calls getRates
class BlackboxSerialDevice {
<<abstract>>
virtual open() *
virtual close() *
virtual write() *
}
link BlackboxSerialDevice "https://github.com/martinbudden/Library-Blackbox/blob/main/src/BlackboxSerialDevice.h"
class BlackboxSerialDeviceSDCard["BlackboxSerialDeviceSDCard(eg)"] {
open() override
close() override
write() override
}
link BlackboxSerialDevice "https://github.com/martinbudden/Library-Blackbox/blob/main/src/BlackboxSerialDeviceSDCard.h"
BlackboxSerialDevice <|-- BlackboxSerialDeviceSDCard
classDef taskClass fill:#f96
MSP_Base has the virtual functions processOutCommand and processInCommand which are overridden in MSP_ProtoFlight.
MSP_ProtoFlight overrides these functions to set and get values in the main flight objects (ie FlightController, AHRS etc)
when MSP commands are received.
classDiagram
class MSP_SerialBase {
<<abstract>>
sendFrame() *
processInput() *
}
link MSP_SerialBase "https://github.com/martinbudden/Library-MultiWiiSerialProtocol/blob/main/src/MSP_SerialBase.h"
class MSP_Base {
virtual processOutCommand() result_e
virtual processInCommand() result_e
}
link MSP_Base "https://github.com/martinbudden/Library-MultiWiiSerialProtocol/blob/main/src/MSP_Base.h"
class MSP_Stream {
}
link MSP_Stream "https://github.com/martinbudden/Library-MultiWiiSerialProtocol/blob/main/src/MSP_Stream.h"
MSP_Stream *-- MSP_Base
MSP_Stream o-- MSP_SerialBase
MSP_SerialBase <|-- MSP_Serial
class MSP_Serial {
sendFrame() int override
processInput() override
}
link MSP_Serial "https://github.com/martinbudden/protoflight/blob/main/lib/MSP/src/MSP_Serial.h"
MSP_Serial o-- MSP_Stream
MSP_Base <|-- MSP_ProtoFlight
class MSP_ProtoFlight {
processOutCommand() result_e override
processInCommand() result_e override
}
link MSP_ProtoFlight "https://github.com/martinbudden/protoflight/blob/main/lib/MSP/src/MSP_ProtoFlight.h"
class MSP_ProtoFlightBox {
}
link MSP_ProtoFlightBox "https://github.com/martinbudden/protoflight/blob/main/lib/MSP/src/MSP_ProtoFlightBox.h"
MSP_ProtoFlight *-- MSP_ProtoFlightBox
MSP_ProtoFlight o-- AHRS
MSP_ProtoFlight o-- FlightController
MSP_ProtoFlight o-- Cockpit
class ReceiverBase {
<<abstract>>
}
link ReceiverBase "https://github.com/martinbudden/Library-Receiver/blob/main/src/ReceiverBase.h"
MSP_ProtoFlight o-- ReceiverBase
class MSP_Box {
}
link MSP_Box "https://github.com/martinbudden/Library-MultiWiiSerialProtocol/blob/main/src/MSP_Box.h"
MSP_Box <|-- MSP_ProtoFlightBox
class TaskBase:::taskClass {
}
link TaskBase "https://github.com/martinbudden/Library-TaskBase/blob/main/src/TaskBase.h"
TaskBase <|-- MSP_Task
class MSP_Task:::taskClass {
+loop()
-task() [[noreturn]]
}
link MSP_Task "https://github.com/martinbudden/Library-MultiWiiSerialProtocol/blob/main/src/MSP_Task.h"
MSP_Task o-- MSP_SerialBase : calls processInput
classDef taskClass fill:#f96
classDiagram
class BackchannelTransceiverBase {
<<abstract>>
sendData() *
WAIT_FOR_DATA_RECEIVED() *
getReceivedDataLength() const *
setReceivedDataLengthToZero() *
getTickCountDeltaAndReset() *
#uint8_t _transmitDataBuffer[512]
#uint8_t _receivedDataBuffer[256]
}
class BackchannelBase {
<<abstract>>
WAIT_FOR_DATA_RECEIVED()
sendData() const int
processedReceivedPacket() *
sendPacket() *
}
BackchannelBase o-- BackchannelTransceiverBase : calls WAIT_FOR_DATA_RECEIVED sendData
BackchannelTransceiverBase <|-- BackchannelTransceiverUDP
BackchannelTransceiverBase <|-- BackchannelTransceiverESPNOW
BackchannelTransceiverESPNOW o-- ESPNOW_Transceiver
BackchannelBase <|-- BackchannelStabilizedVehicle : overrides sendPacket
class BackchannelStabilizedVehicle {
_backchannelID uint32_t
_telemetryID uint32_t
+sendPacket(uint8_t subCommand) bool override;
#processedReceivedPacket() bool override;
#virtual packetRequestData() bool
#virtual packetSetOffset() bool
#virtual packetControl() bool
#virtual packetSetPID() bool
}
BackchannelStabilizedVehicle o-- DashboardTask
BackchannelStabilizedVehicle o-- AHRS
BackchannelStabilizedVehicle o-- ReceiverBase
BackchannelStabilizedVehicle o-- VehicleControllerBase
BackchannelStabilizedVehicle <|--BackchannelFlightController : overrides sendPacket
class BackchannelFlightController {
+sendPacket() bool override
#packetControl() bool override;
#bool packetSetPID() bool override;
}
VehicleControllerBase <|-- FlightController
BackchannelFlightController o-- FlightController
BackchannelFlightController o-- NonVolatileStorage
class TaskBase:::taskClass {
}
link TaskBase "https://github.com/martinbudden/Library-TaskBase/blob/main/src/TaskBase.h"
TaskBase <|-- BackchannelTask
class BackchannelTask:::taskClass {
+loop()
-task() [[noreturn]]
}
BackchannelTask o-- BackchannelBase : calls processedReceivedPacket sendPacket
classDef taskClass fill:#f96
---
config:
class:
hideEmptyMembersBox: false
---
classDiagram
class FlightController {
array~PIDF~ _pids
array~Filter~ _dTermFilters
array~Filter~ _stickSetpointFilters
outputToMixer() override
updateOutputsUsingPIDs() override
updateSetpoints()
}
link FlightController "https://github.com/martinbudden/protoflight/blob/main/lib/FlightController/src/FlightController.h"
class CockpitBase {
<<abstract>>
updateControls() *
checkFailsafe() *
}
link CockpitBase "https://github.com/martinbudden/Library-Receiver/blob/main/src/CockpitBase.h"
CockpitBase o--ReceiverBase : calls getAuxiliaryChannel
CockpitBase <|-- Cockpit : overrides updateControls
class Cockpit {
updateControls() override
checkFailsafe() override
getFailsafePhase()
}
link Cockpit "https://github.com/martinbudden/protoflight/blob/main/lib/Helm/src/Cockpit.h"
Cockpit o-- FlightController : calls updateSetpoints
Cockpit o-- Autopilot : calls calculateFlightControls
class ReceiverBase {
<<abstract>>
WAIT_FOR_DATA_RECEIVED() *
update() *
getStickValues() *
getAuxiliaryChannel() *
}
link ReceiverBase "https://github.com/martinbudden/Library-Receiver/blob/main/src/ReceiverBase.h"
class ReceiverAtomJoyStick {
update() override
}
ReceiverBase <|-- ReceiverAtomJoyStick
class ReceiverAtomJoyStick["ReceiverAtomJoyStick(eg)"]
link ReceiverAtomJoyStick "https://github.com/martinbudden/Library-Receiver/blob/main/src/ReceiverAtomJoyStick.h"
class ReceiverTask:::taskClass {
}
link ReceiverTask "https://github.com/martinbudden/Library-Receiver/blob/main/src/ReceiverTask.h"
ReceiverTask o-- ReceiverBase : calls update / getStickValues
ReceiverTask o-- CockpitBase : calls updateControls
%%CockpitBase --o ReceiverTask : calls updateControls
class Autopilot {
array~geographic_coordinate_t~ _waypoints
altitudeHoldCalculateThrottle()
calculateFlightControls()
}
link Autopilot "https://github.com/martinbudden/protoflight/blob/main/lib/Helm/src/Autopilot.h"
Autopilot o-- AHRS_MessageQueue : calls PEEK_AHRS_DATA
Autopilot *-- AltitudeKalmanFilter : calls update
class AltitudeKalmanFilter {
}
class AHRS_MessageQueue {
ahrs_data_t ahrsData
WAIT() override
SIGNAL(const ahr_data_t&)
SEND_AHRS_DATA(const ahr_data_t&)
PEEK_AHRS_DATA(const ahr_data_t&)
getReceivedAHRS_Data()
}
link AHRS_MessageQueue "https://github.com/martinbudden/Library-StabilizedVehicle/blob/main/src/AHRS_MessageQueue.h"
FlightController o-- AHRS_MessageQueue : calls SIGNAL
%%AHRS_MessageQueue --o FlightController : calls SIGNAL
class BarometerBase {
}
class BarometerTask:::taskClass {
}
%%BarometerTask o-- BarometerMessageQueue : calls SEND
BarometerMessageQueue --o BarometerTask : calls SEND
BarometerTask o-- BarometerBase : calls read
class BarometerMessageQueue {
float altitude
}
class GPS_Base {
}
class GPS_Task:::taskClass {
}
class GPS_MessageQueue {
geographic_coordinate_t location
}
GPS_MessageQueue --o GPS_Task : calls SEND
GPS_Task o-- GPS_Base : calls read
Cockpit o-- BarometerMessageQueue : calls PEEK
Cockpit o-- GPS_MessageQueue : calls PEEK
classDef taskClass fill:#f96