Merged in f5soh/librepilot/update_credits (pull request #529)
[librepilot.git] / flight / modules / Actuator / actuator.c
blob3cc35e19b800ca0563ecbe58ce52ebcc9f496986
1 /**
2 ******************************************************************************
3 * @addtogroup OpenPilotModules OpenPilot Modules
4 * @{
5 * @addtogroup ActuatorModule Actuator Module
6 * @brief Compute servo/motor settings based on @ref ActuatorDesired "desired actuator positions" and aircraft type.
7 * This is where all the mixing of channels is computed.
8 * @{
10 * @file actuator.c
11 * @author The LibrePilot Project, http://www.librepilot.org Copyright (C) 2015.
12 * The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
13 * @brief Actuator module. Drives the actuators (servos, motors etc).
15 * @see The GNU Public License (GPL) Version 3
17 *****************************************************************************/
19 * This program is free software; you can redistribute it and/or modify
20 * it under the terms of the GNU General Public License as published by
21 * the Free Software Foundation; either version 3 of the License, or
22 * (at your option) any later version.
24 * This program is distributed in the hope that it will be useful, but
25 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
26 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
27 * for more details.
29 * You should have received a copy of the GNU General Public License along
30 * with this program; if not, write to the Free Software Foundation, Inc.,
31 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
35 #include <openpilot.h>
37 #include "accessorydesired.h"
38 #include "actuator.h"
39 #include "actuatorsettings.h"
40 #include "systemsettings.h"
41 #include "actuatordesired.h"
42 #include "actuatorcommand.h"
43 #include "flightstatus.h"
44 #include <flightmodesettings.h>
45 #include "mixersettings.h"
46 #include "mixerstatus.h"
47 #include "cameradesired.h"
48 #include "hwsettings.h"
49 #include "manualcontrolcommand.h"
50 #include "taskinfo.h"
51 #include <systemsettings.h>
52 #include <sanitycheck.h>
53 #ifndef PIOS_EXCLUDE_ADVANCED_FEATURES
54 #include <vtolpathfollowersettings.h>
55 #endif
56 #undef PIOS_INCLUDE_INSTRUMENTATION
57 #ifdef PIOS_INCLUDE_INSTRUMENTATION
58 #include <pios_instrumentation.h>
59 static int8_t counter;
60 // Counter 0xAC700001 total Actuator body execution time(excluding queue waits etc).
61 #endif
63 // Private constants
64 #define MAX_QUEUE_SIZE 2
66 #if defined(PIOS_ACTUATOR_STACK_SIZE)
67 #define STACK_SIZE_BYTES PIOS_ACTUATOR_STACK_SIZE
68 #else
69 #define STACK_SIZE_BYTES 1312
70 #endif
72 #define TASK_PRIORITY (tskIDLE_PRIORITY + 4) // device driver
73 #define FAILSAFE_TIMEOUT_MS 100
74 #define MAX_MIX_ACTUATORS ACTUATORCOMMAND_CHANNEL_NUMELEM
76 #define CAMERA_BOOT_DELAY_MS 7000
78 #define ACTUATOR_ONESHOT_CLOCK 12000000
79 #define ACTUATOR_ONESHOT125_PULSE_FACTOR 1.5f
80 #define ACTUATOR_ONESHOT42_PULSE_FACTOR 0.5f
81 #define ACTUATOR_MULTISHOT_PULSE_FACTOR 0.24f
82 #define ACTUATOR_PWM_CLOCK 1000000
83 // Private types
86 // Private variables
87 static xQueueHandle queue;
88 static xTaskHandle taskHandle;
89 static FrameType_t frameType = FRAME_TYPE_MULTIROTOR;
90 static SystemSettingsThrustControlOptions thrustType = SYSTEMSETTINGS_THRUSTCONTROL_THROTTLE;
91 static bool camStabEnabled;
92 static bool camControlEnabled;
94 static uint8_t pinsMode[MAX_MIX_ACTUATORS];
95 // used to inform the actuator thread that actuator update rate is changed
96 static ActuatorSettingsData actuatorSettings;
97 static bool spinWhileArmed;
99 // used to inform the actuator thread that mixer settings are changed
100 static MixerSettingsData mixerSettings;
101 static int mixer_settings_count = 2;
103 // Private functions
104 static void actuatorTask(void *parameters);
105 static int16_t scaleChannel(float value, int16_t max, int16_t min, int16_t neutral);
106 static int16_t scaleMotor(float value, int16_t max, int16_t min, int16_t neutral, float maxMotor, float minMotor, bool armed, bool alwaysStabilizeWhenArmed, float throttleDesired);
107 static void setFailsafe();
108 static float MixerCurveFullRangeProportional(const float input, const float *curve, uint8_t elements, bool multirotor);
109 static float MixerCurveFullRangeAbsolute(const float input, const float *curve, uint8_t elements, bool multirotor);
110 static bool set_channel(uint8_t mixer_channel, uint16_t value);
111 static void actuator_update_rate_if_changed(bool force_update);
112 static void MixerSettingsUpdatedCb(UAVObjEvent *ev);
113 static void ActuatorSettingsUpdatedCb(UAVObjEvent *ev);
114 static void SettingsUpdatedCb(UAVObjEvent *ev);
115 float ProcessMixer(const int index, const float curve1, const float curve2,
116 ActuatorDesiredData *desired,
117 bool multirotor, bool fixedwing);
119 // this structure is equivalent to the UAVObjects for one mixer.
120 typedef struct {
121 uint8_t type;
122 int8_t matrix[5];
123 } __attribute__((packed)) Mixer_t;
126 * @brief Module initialization
127 * @return 0
129 int32_t ActuatorStart()
131 // Start main task
132 xTaskCreate(actuatorTask, "Actuator", STACK_SIZE_BYTES / 4, NULL, TASK_PRIORITY, &taskHandle);
133 PIOS_TASK_MONITOR_RegisterTask(TASKINFO_RUNNING_ACTUATOR, taskHandle);
134 #ifdef PIOS_INCLUDE_WDG
135 PIOS_WDG_RegisterFlag(PIOS_WDG_ACTUATOR);
136 #endif
137 SettingsUpdatedCb(NULL);
138 MixerSettingsUpdatedCb(NULL);
139 ActuatorSettingsUpdatedCb(NULL);
140 return 0;
144 * @brief Module initialization
145 * @return 0
147 int32_t ActuatorInitialize()
149 // Register for notification of changes to ActuatorSettings
150 ActuatorSettingsConnectCallback(ActuatorSettingsUpdatedCb);
152 // Register for notification of changes to MixerSettings
153 MixerSettingsConnectCallback(MixerSettingsUpdatedCb);
155 // Listen for ActuatorDesired updates (Primary input to this module)
156 ActuatorDesiredInitialize();
157 queue = xQueueCreate(MAX_QUEUE_SIZE, sizeof(UAVObjEvent));
158 ActuatorDesiredConnectQueue(queue);
160 // Register AccessoryDesired (Secondary input to this module)
161 AccessoryDesiredInitialize();
163 // Check if CameraStab module is enabled
164 HwSettingsOptionalModulesData optionalModules;
165 HwSettingsOptionalModulesGet(&optionalModules);
166 camStabEnabled = (optionalModules.CameraStab == HWSETTINGS_OPTIONALMODULES_ENABLED);
167 camControlEnabled = (optionalModules.CameraControl == HWSETTINGS_OPTIONALMODULES_ENABLED);
168 // Primary output of this module
169 ActuatorCommandInitialize();
171 #ifdef DIAG_MIXERSTATUS
172 // UAVO only used for inspecting the internal status of the mixer during debug
173 MixerStatusInitialize();
174 #endif
176 #ifndef PIOS_EXCLUDE_ADVANCED_FEATURES
177 VtolPathFollowerSettingsConnectCallback(&SettingsUpdatedCb);
178 #endif
179 SystemSettingsConnectCallback(&SettingsUpdatedCb);
181 return 0;
183 MODULE_INITCALL(ActuatorInitialize, ActuatorStart);
186 * @brief Main Actuator module task
188 * Universal matrix based mixer for VTOL, helis and fixed wing.
189 * Converts desired roll,pitch,yaw and throttle to servo/ESC outputs.
191 * Because of how the Throttle ranges from 0 to 1, the motors should too!
193 * Note this code depends on the UAVObjects for the mixers being all being the same
194 * and in sequence. If you change the object definition, make sure you check the code!
196 * @return -1 if error, 0 if success
198 static void actuatorTask(__attribute__((unused)) void *parameters)
200 UAVObjEvent ev;
201 portTickType lastSysTime;
202 portTickType thisSysTime;
203 uint32_t dTMilliseconds;
205 ActuatorCommandData command;
206 ActuatorDesiredData desired;
207 MixerStatusData mixerStatus;
208 FlightModeSettingsData settings;
209 FlightStatusData flightStatus;
210 float throttleDesired;
211 float collectiveDesired;
213 #ifdef PIOS_INCLUDE_INSTRUMENTATION
214 counter = PIOS_Instrumentation_CreateCounter(0xAC700001);
215 #endif
216 /* Read initial values of ActuatorSettings */
218 ActuatorSettingsGet(&actuatorSettings);
220 /* Read initial values of MixerSettings */
221 MixerSettingsGet(&mixerSettings);
223 /* Force an initial configuration of the actuator update rates */
224 actuator_update_rate_if_changed(true);
226 // Go to the neutral (failsafe) values until an ActuatorDesired update is received
227 setFailsafe();
229 // Main task loop
230 lastSysTime = xTaskGetTickCount();
231 while (1) {
232 #ifdef PIOS_INCLUDE_WDG
233 PIOS_WDG_UpdateFlag(PIOS_WDG_ACTUATOR);
234 #endif
236 // Wait until the ActuatorDesired object is updated
237 uint8_t rc = xQueueReceive(queue, &ev, FAILSAFE_TIMEOUT_MS / portTICK_RATE_MS);
238 #ifdef PIOS_INCLUDE_INSTRUMENTATION
239 PIOS_Instrumentation_TimeStart(counter);
240 #endif
242 if (rc != pdTRUE) {
243 /* Update of ActuatorDesired timed out. Go to failsafe */
244 setFailsafe();
245 continue;
248 // Check how long since last update
249 thisSysTime = xTaskGetTickCount();
250 dTMilliseconds = (thisSysTime == lastSysTime) ? 1 : (thisSysTime - lastSysTime) * portTICK_RATE_MS;
251 lastSysTime = thisSysTime;
253 FlightStatusGet(&flightStatus);
254 FlightModeSettingsGet(&settings);
255 ActuatorDesiredGet(&desired);
256 ActuatorCommandGet(&command);
258 // read in throttle and collective -demultiplex thrust
259 switch (thrustType) {
260 case SYSTEMSETTINGS_THRUSTCONTROL_THROTTLE:
261 throttleDesired = desired.Thrust;
262 ManualControlCommandCollectiveGet(&collectiveDesired);
263 break;
264 case SYSTEMSETTINGS_THRUSTCONTROL_COLLECTIVE:
265 ManualControlCommandThrottleGet(&throttleDesired);
266 collectiveDesired = desired.Thrust;
267 break;
268 default:
269 ManualControlCommandThrottleGet(&throttleDesired);
270 ManualControlCommandCollectiveGet(&collectiveDesired);
273 bool armed = flightStatus.Armed == FLIGHTSTATUS_ARMED_ARMED;
274 bool activeThrottle = (throttleDesired < -0.001f || throttleDesired > 0.001f); // for ground and reversible motors
275 bool positiveThrottle = (throttleDesired > 0.00f);
276 bool multirotor = (GetCurrentFrameType() == FRAME_TYPE_MULTIROTOR); // check if frame is a multirotor.
277 bool fixedwing = (GetCurrentFrameType() == FRAME_TYPE_FIXED_WING); // check if frame is a fixedwing.
278 bool alwaysArmed = settings.Arming == FLIGHTMODESETTINGS_ARMING_ALWAYSARMED;
279 bool alwaysStabilizeWhenArmed = flightStatus.AlwaysStabilizeWhenArmed == FLIGHTSTATUS_ALWAYSSTABILIZEWHENARMED_TRUE;
281 if (alwaysArmed) {
282 alwaysStabilizeWhenArmed = false; // Do not allow always stabilize when alwaysArmed is active. This is dangerous.
284 // safety settings
285 if (!armed) {
286 throttleDesired = 0.00f; // this also happens in scaleMotors as a per axis check
289 if ((frameType == FRAME_TYPE_GROUND && !activeThrottle) || (frameType != FRAME_TYPE_GROUND && throttleDesired <= 0.00f) || !armed) {
290 // throttleDesired should never be 0 or go below 0.
291 // force set all other controls to zero if throttle is cut (previously set in Stabilization)
292 // todo: can probably remove this
293 if (!(multirotor && alwaysStabilizeWhenArmed && armed)) { // we don't do this if this is a multirotor AND AlwaysStabilizeWhenArmed is true and the model is armed
294 if (actuatorSettings.LowThrottleZeroAxis.Roll == ACTUATORSETTINGS_LOWTHROTTLEZEROAXIS_TRUE) {
295 desired.Roll = 0.00f;
297 if (actuatorSettings.LowThrottleZeroAxis.Pitch == ACTUATORSETTINGS_LOWTHROTTLEZEROAXIS_TRUE) {
298 desired.Pitch = 0.00f;
300 if (actuatorSettings.LowThrottleZeroAxis.Yaw == ACTUATORSETTINGS_LOWTHROTTLEZEROAXIS_TRUE) {
301 desired.Yaw = 0.00f;
306 #ifdef DIAG_MIXERSTATUS
307 MixerStatusGet(&mixerStatus);
308 #endif
310 if ((mixer_settings_count < 2) && !ActuatorCommandReadOnly()) { // Nothing can fly with less than two mixers.
311 setFailsafe();
312 continue;
315 AlarmsClear(SYSTEMALARMS_ALARM_ACTUATOR);
317 float curve1 = 0.0f; // curve 1 is the throttle curve applied to all motors.
318 float curve2 = 0.0f;
320 // Interpolate curve 1 from throttleDesired as input.
321 // assume reversible motor/mixer initially. We can later reverse this. The difference is simply that -ve throttleDesired values
322 // map differently
323 curve1 = MixerCurveFullRangeProportional(throttleDesired, mixerSettings.ThrottleCurve1, MIXERSETTINGS_THROTTLECURVE1_NUMELEM, multirotor);
325 // The source for the secondary curve is selectable
326 AccessoryDesiredData accessory;
327 uint8_t curve2Source = mixerSettings.Curve2Source;
328 switch (curve2Source) {
329 case MIXERSETTINGS_CURVE2SOURCE_THROTTLE:
330 // assume reversible motor/mixer initially
331 curve2 = MixerCurveFullRangeProportional(throttleDesired, mixerSettings.ThrottleCurve2, MIXERSETTINGS_THROTTLECURVE2_NUMELEM, multirotor);
332 break;
333 case MIXERSETTINGS_CURVE2SOURCE_ROLL:
334 // Throttle curve contribution the same for +ve vs -ve roll
335 if (multirotor) {
336 curve2 = MixerCurveFullRangeProportional(desired.Roll, mixerSettings.ThrottleCurve2, MIXERSETTINGS_THROTTLECURVE2_NUMELEM, multirotor);
337 } else {
338 curve2 = MixerCurveFullRangeAbsolute(desired.Roll, mixerSettings.ThrottleCurve2, MIXERSETTINGS_THROTTLECURVE2_NUMELEM, multirotor);
340 break;
341 case MIXERSETTINGS_CURVE2SOURCE_PITCH:
342 // Throttle curve contribution the same for +ve vs -ve pitch
343 if (multirotor) {
344 curve2 = MixerCurveFullRangeProportional(desired.Pitch, mixerSettings.ThrottleCurve2,
345 MIXERSETTINGS_THROTTLECURVE2_NUMELEM, multirotor);
346 } else {
347 curve2 = MixerCurveFullRangeAbsolute(desired.Pitch, mixerSettings.ThrottleCurve2,
348 MIXERSETTINGS_THROTTLECURVE2_NUMELEM, multirotor);
350 break;
351 case MIXERSETTINGS_CURVE2SOURCE_YAW:
352 // Throttle curve contribution the same for +ve vs -ve yaw
353 if (multirotor) {
354 curve2 = MixerCurveFullRangeProportional(desired.Yaw, mixerSettings.ThrottleCurve2, MIXERSETTINGS_THROTTLECURVE2_NUMELEM, multirotor);
355 } else {
356 curve2 = MixerCurveFullRangeAbsolute(desired.Yaw, mixerSettings.ThrottleCurve2, MIXERSETTINGS_THROTTLECURVE2_NUMELEM, multirotor);
358 break;
359 case MIXERSETTINGS_CURVE2SOURCE_COLLECTIVE:
360 // assume reversible motor/mixer initially
361 curve2 = MixerCurveFullRangeProportional(collectiveDesired, mixerSettings.ThrottleCurve2,
362 MIXERSETTINGS_THROTTLECURVE2_NUMELEM, multirotor);
363 break;
364 case MIXERSETTINGS_CURVE2SOURCE_ACCESSORY0:
365 case MIXERSETTINGS_CURVE2SOURCE_ACCESSORY1:
366 case MIXERSETTINGS_CURVE2SOURCE_ACCESSORY2:
367 case MIXERSETTINGS_CURVE2SOURCE_ACCESSORY3:
368 case MIXERSETTINGS_CURVE2SOURCE_ACCESSORY4:
369 case MIXERSETTINGS_CURVE2SOURCE_ACCESSORY5:
370 if (AccessoryDesiredInstGet(mixerSettings.Curve2Source - MIXERSETTINGS_CURVE2SOURCE_ACCESSORY0, &accessory) == 0) {
371 // Throttle curve contribution the same for +ve vs -ve accessory....maybe not want we want.
372 curve2 = MixerCurveFullRangeAbsolute(accessory.AccessoryVal, mixerSettings.ThrottleCurve2, MIXERSETTINGS_THROTTLECURVE2_NUMELEM, multirotor);
373 } else {
374 curve2 = 0.0f;
376 break;
377 default:
378 curve2 = 0.0f;
379 break;
382 float *status = (float *)&mixerStatus; // access status objects as an array of floats
383 Mixer_t *mixers = (Mixer_t *)&mixerSettings.Mixer1Type;
384 float maxMotor = -1.0f; // highest motor value. Addition method needs this to be -1.0f, division method needs this to be 1.0f
385 float minMotor = 1.0f; // lowest motor value Addition method needs this to be 1.0f, division method needs this to be -1.0f
387 for (int ct = 0; ct < MAX_MIX_ACTUATORS; ct++) {
388 // During boot all camera actuators should be completely disabled (PWM pulse = 0).
389 // command.Channel[i] is reused below as a channel PWM activity flag:
390 // 0 - PWM disabled, >0 - PWM set to real mixer value using scaleChannel() later.
391 // Setting it to 1 by default means "Rescale this channel and enable PWM on its output".
392 command.Channel[ct] = 1;
394 uint8_t mixer_type = mixers[ct].type;
396 if (mixer_type == MIXERSETTINGS_MIXER1TYPE_DISABLED) {
397 // Set to minimum if disabled. This is not the same as saying PWM pulse = 0 us
398 status[ct] = -1;
399 continue;
402 if ((mixer_type == MIXERSETTINGS_MIXER1TYPE_MOTOR)) {
403 float nonreversible_curve1 = curve1;
404 float nonreversible_curve2 = curve2;
405 if (nonreversible_curve1 < 0.0f) {
406 nonreversible_curve1 = 0.0f;
408 if (nonreversible_curve2 < 0.0f) {
409 if (!multirotor) { // allow negative throttle if multirotor. function scaleMotors handles the sanity checks.
410 nonreversible_curve2 = 0.0f;
413 status[ct] = ProcessMixer(ct, nonreversible_curve1, nonreversible_curve2, &desired, multirotor, fixedwing);
414 // If not armed or motors aren't meant to spin all the time
415 if (!armed ||
416 (!spinWhileArmed && !positiveThrottle)) {
417 status[ct] = -1; // force min throttle
419 // If armed meant to keep spinning,
420 else if ((spinWhileArmed && !positiveThrottle) ||
421 (status[ct] < 0)) {
422 if (!multirotor) {
423 status[ct] = 0;
424 // allow throttle values lower than 0 if multirotor.
425 // Values will be scaled to 0 if they need to be in the scaleMotor function
428 } else if (mixer_type == MIXERSETTINGS_MIXER1TYPE_REVERSABLEMOTOR) {
429 status[ct] = ProcessMixer(ct, curve1, curve2, &desired, multirotor, fixedwing);
430 // Reversable Motors are like Motors but go to neutral instead of minimum
431 // If not armed or motor is inactive - no "spinwhilearmed" for this engine type
432 if (!armed || !activeThrottle) {
433 status[ct] = 0; // force neutral throttle
435 } else if (mixer_type == MIXERSETTINGS_MIXER1TYPE_SERVO) {
436 status[ct] = ProcessMixer(ct, curve1, curve2, &desired, multirotor, fixedwing);
437 } else {
438 status[ct] = -1;
440 // If an accessory channel is selected for direct bypass mode
441 // In this configuration the accessory channel is scaled and mapped
442 // directly to output. Note: THERE IS NO SAFETY CHECK HERE FOR ARMING
443 // these also will not be updated in failsafe mode. I'm not sure what
444 // the correct behavior is since it seems domain specific. I don't love
445 // this code
446 if ((mixer_type >= MIXERSETTINGS_MIXER1TYPE_ACCESSORY0) &&
447 (mixer_type <= MIXERSETTINGS_MIXER1TYPE_ACCESSORY5)) {
448 if (AccessoryDesiredInstGet(mixer_type - MIXERSETTINGS_MIXER1TYPE_ACCESSORY0, &accessory) == 0) {
449 status[ct] = accessory.AccessoryVal;
450 } else {
451 status[ct] = -1;
455 if ((mixer_type >= MIXERSETTINGS_MIXER1TYPE_CAMERAROLLORSERVO1) &&
456 (mixer_type <= MIXERSETTINGS_MIXER1TYPE_CAMERAYAW)) {
457 if (camStabEnabled) {
458 CameraDesiredData cameraDesired;
459 CameraDesiredGet(&cameraDesired);
460 switch (mixer_type) {
461 case MIXERSETTINGS_MIXER1TYPE_CAMERAROLLORSERVO1:
462 status[ct] = cameraDesired.RollOrServo1;
463 break;
464 case MIXERSETTINGS_MIXER1TYPE_CAMERAPITCHORSERVO2:
465 status[ct] = cameraDesired.PitchOrServo2;
466 break;
467 case MIXERSETTINGS_MIXER1TYPE_CAMERAYAW:
468 status[ct] = cameraDesired.Yaw;
469 break;
470 default:
471 break;
473 } else {
474 status[ct] = -1;
477 // Disable camera actuators for CAMERA_BOOT_DELAY_MS after boot
478 if (thisSysTime < (CAMERA_BOOT_DELAY_MS / portTICK_RATE_MS)) {
479 command.Channel[ct] = 0;
483 if (mixer_type == MIXERSETTINGS_MIXER1TYPE_CAMERATRIGGER) {
484 if (camControlEnabled) {
485 CameraDesiredTriggerGet(&status[ct]);
486 } else {
487 status[ct] = 0;
492 // If mixer type is motor we need to find which motor has the highest value and which motor has the lowest value.
493 // For use in function scaleMotor
494 if (mixers[ct].type == MIXERSETTINGS_MIXER1TYPE_MOTOR) {
495 if (maxMotor < status[ct]) {
496 maxMotor = status[ct];
498 if (minMotor > status[ct]) {
499 minMotor = status[ct];
504 // Set real actuator output values scaling them from mixers. All channels
505 // will be set except explicitly disabled (which will have PWM pulse = 0).
506 for (int i = 0; i < MAX_MIX_ACTUATORS; i++) {
507 if (command.Channel[i]) {
508 if (mixers[i].type == MIXERSETTINGS_MIXER1TYPE_MOTOR) { // If mixer is for a motor we need to find the highest value of all motors
509 command.Channel[i] = scaleMotor(status[i],
510 actuatorSettings.ChannelMax[i],
511 actuatorSettings.ChannelMin[i],
512 actuatorSettings.ChannelNeutral[i],
513 maxMotor,
514 minMotor,
515 armed,
516 alwaysStabilizeWhenArmed,
517 throttleDesired);
518 } else { // else we scale the channel
519 command.Channel[i] = scaleChannel(status[i],
520 actuatorSettings.ChannelMax[i],
521 actuatorSettings.ChannelMin[i],
522 actuatorSettings.ChannelNeutral[i]);
527 // Store update time
528 command.UpdateTime = dTMilliseconds;
529 if (command.UpdateTime > command.MaxUpdateTime) {
530 command.MaxUpdateTime = command.UpdateTime;
532 // Update output object
533 ActuatorCommandSet(&command);
534 // Update in case read only (eg. during servo configuration)
535 ActuatorCommandGet(&command);
537 #ifdef DIAG_MIXERSTATUS
538 MixerStatusSet(&mixerStatus);
539 #endif
542 // Update servo outputs
543 bool success = true;
545 for (int n = 0; n < ACTUATORCOMMAND_CHANNEL_NUMELEM; ++n) {
546 success &= set_channel(n, command.Channel[n]);
549 PIOS_Servo_Update();
551 if (!success) {
552 command.NumFailedUpdates++;
553 ActuatorCommandSet(&command);
554 AlarmsSet(SYSTEMALARMS_ALARM_ACTUATOR, SYSTEMALARMS_ALARM_CRITICAL);
556 #ifdef PIOS_INCLUDE_INSTRUMENTATION
557 PIOS_Instrumentation_TimeEnd(counter);
558 #endif
564 * Process mixing for one actuator
566 float ProcessMixer(const int index, const float curve1, const float curve2,
567 ActuatorDesiredData *desired, bool multirotor, bool fixedwing)
569 const Mixer_t *mixers = (Mixer_t *)&mixerSettings.Mixer1Type; // pointer to array of mixers in UAVObjects
570 const Mixer_t *mixer = &mixers[index];
571 float differential = 1.0f;
573 // Apply differential only for fixedwing and Roll servos
574 if (fixedwing && (mixerSettings.FirstRollServo > 0) &&
575 (mixer->type == MIXERSETTINGS_MIXER1TYPE_SERVO) &&
576 (mixer->matrix[MIXERSETTINGS_MIXER1VECTOR_ROLL] != 0)) {
577 // Positive differential
578 if (mixerSettings.RollDifferential > 0) {
579 // Check for first Roll servo (should be left aileron or elevon) and Roll desired (positive/negative)
580 if (((index == mixerSettings.FirstRollServo - 1) && (desired->Roll > 0.0f))
581 || ((index != mixerSettings.FirstRollServo - 1) && (desired->Roll < 0.0f))) {
582 differential -= (mixerSettings.RollDifferential * 0.01f);
584 } else if (mixerSettings.RollDifferential < 0) {
585 if (((index == mixerSettings.FirstRollServo - 1) && (desired->Roll < 0.0f))
586 || ((index != mixerSettings.FirstRollServo - 1) && (desired->Roll > 0.0f))) {
587 differential -= (-mixerSettings.RollDifferential * 0.01f);
592 float result = ((((float)mixer->matrix[MIXERSETTINGS_MIXER1VECTOR_THROTTLECURVE1]) * curve1) +
593 (((float)mixer->matrix[MIXERSETTINGS_MIXER1VECTOR_THROTTLECURVE2]) * curve2) +
594 (((float)mixer->matrix[MIXERSETTINGS_MIXER1VECTOR_ROLL]) * desired->Roll * differential) +
595 (((float)mixer->matrix[MIXERSETTINGS_MIXER1VECTOR_PITCH]) * desired->Pitch) +
596 (((float)mixer->matrix[MIXERSETTINGS_MIXER1VECTOR_YAW]) * desired->Yaw)) / 128.0f;
598 if (mixer->type == MIXERSETTINGS_MIXER1TYPE_MOTOR) {
599 if (!multirotor) { // we allow negative throttle with a multirotor
600 if (result < 0.0f) { // zero throttle
601 result = 0.0f;
606 return result;
611 * Interpolate a throttle curve
612 * Full range input (-1 to 1) for yaw, roll, pitch
613 * Output range (-1 to 1) reversible motor/throttle curve
615 * Input of -1 -> -lookup(1)
616 * Input of 0 -> lookup(0)
617 * Input of 1 -> lookup(1)
619 static float MixerCurveFullRangeProportional(const float input, const float *curve, uint8_t elements, bool multirotor)
621 float unsigned_value = MixerCurveFullRangeAbsolute(input, curve, elements, multirotor);
623 if (input < 0.0f) {
624 return -unsigned_value;
625 } else {
626 return unsigned_value;
631 * Interpolate a throttle curve
632 * Full range input (-1 to 1) for yaw, roll, pitch
633 * Output range (0 to 1) non-reversible motor/throttle curve
635 * Input of -1 -> lookup(1)
636 * Input of 0 -> lookup(0)
637 * Input of 1 -> lookup(1)
639 static float MixerCurveFullRangeAbsolute(const float input, const float *curve, uint8_t elements, bool multirotor)
641 float abs_input = fabsf(input);
642 float scale = abs_input * (float)(elements - 1);
643 int idx1 = scale;
645 scale -= (float)idx1; // remainder
646 if (curve[0] < -1) {
647 return abs_input;
649 int idx2 = idx1 + 1;
650 if (idx2 >= elements) {
651 idx2 = elements - 1; // clamp to highest entry in table
652 if (idx1 >= elements) {
653 if (multirotor) {
654 // if multirotor frame we can return throttle values higher than 100%.
655 // Since the we don't have elements in the curve higher than 100% we return
656 // the last element multiplied by the throttle float
657 if (input < 2.0f) { // this limits positive throttle to 200% of max value in table (Maybe this is too much allowance)
658 return curve[idx2] * input;
659 } else {
660 return curve[idx2] * 2.0f; // return 200% of max value in table
663 idx1 = elements - 1;
667 float unsigned_value = curve[idx1] * (1.0f - scale) + curve[idx2] * scale;
668 return unsigned_value;
673 * Convert channel from -1/+1 to servo pulse duration in microseconds
675 static int16_t scaleChannel(float value, int16_t max, int16_t min, int16_t neutral)
677 int16_t valueScaled;
679 // Scale
680 if (value >= 0.0f) {
681 valueScaled = (int16_t)(value * ((float)(max - neutral))) + neutral;
682 } else {
683 valueScaled = (int16_t)(value * ((float)(neutral - min))) + neutral;
686 if (max > min) {
687 if (valueScaled > max) {
688 valueScaled = max;
690 if (valueScaled < min) {
691 valueScaled = min;
693 } else {
694 if (valueScaled < max) {
695 valueScaled = max;
697 if (valueScaled > min) {
698 valueScaled = min;
702 return valueScaled;
706 * Move and compress all motor outputs so that none goes below neutral,
707 * and all motors are below or equal to max.
709 static inline int16_t scaleMotorMoveAndCompress(float valueMotor, int16_t max, int16_t neutral, float maxMotor, float minMotor)
711 // The valueMotor parameter is the desired motor value somewhere in the
712 // [minMotor, maxMotor] range, which is [< -1.00, > 1.00].
714 // Before converting valueMotor to the [neutral, max] range, we scale
715 // valueMotor to a value in the [0.0f, 1.0f] range.
717 // This is done by, first, conceptually moving all three values valueMotor,
718 // minMotor, and maxMotor, equally so that the [minMotor, maxMotor] range,
719 // are contained or overlaps with the [0.0f, 1.0f] range.
721 // Then if the [minMotor, maxMotor] range is larger than 1.0f, the values
722 // are compressed enough to shrink the [minMotor + move, maxMotor + move]
723 // range to fit within the [0.0f, 1.0f] range.
725 // First move the values so that the source range [minMotor, maxMotor]
726 // covers the target range [0.0f, 1.0f] as much as possible.
727 float moveValue = 0.0f;
729 if (minMotor <= 0.0f) {
730 // Negative minMotor always adjust to 0.
731 moveValue = -minMotor;
732 } else if (maxMotor > 1.0f) {
733 // A too large maxMotor value adjust the range down towards, but not past, the minMotor value.
734 float beyondMax = maxMotor - 1.0f;
735 moveValue = -(beyondMax < minMotor ? beyondMax : minMotor);
738 // Then calculate the compress value, if the source range is greater than 1.0f.
739 float compressValue = 1.0f;
741 float rangeMotor = maxMotor - minMotor;
742 if (rangeMotor > 1.0f) {
743 compressValue = rangeMotor;
746 // Combine the movement and compression, to get the value within [0.0f, 1.0f]
747 float movedAndCompressedValue = (valueMotor + moveValue) / compressValue;
749 // And last, convert the value into the [neutral, max] range.
750 int16_t valueScaled = movedAndCompressedValue * ((float)(max - neutral)) + neutral;
752 if (valueScaled > max) {
753 valueScaled = max; // clamp to max value only after scaling is done.
756 PIOS_Assert(valueScaled >= neutral);
758 return valueScaled;
762 * Constrain motor values to keep any one motor value from going too far out of range of another motor
764 static int16_t scaleMotor(float value, int16_t max, int16_t min, int16_t neutral, float maxMotor, float minMotor, bool armed, bool alwaysStabilizeWhenArmed, float throttleDesired)
766 int16_t valueScaled;
768 if (max > min) {
769 valueScaled = scaleMotorMoveAndCompress(value, max, neutral, maxMotor, minMotor);
770 } else {
771 // not sure what to do about reversed polarity right now. Why would anyone do this?
772 valueScaled = scaleChannel(value, max, min, neutral);
775 // I've added the bool alwaysStabilizeWhenArmed to this function. Right now we command the motors at min or a range between neutral and max.
776 // NEVER should a motor be command at between min and neutral. I don't like the idea of stabilization ever commanding a motor to min, but we give people the option
777 // This prevents motors startup sync issues causing possible ESC failures.
779 // safety checks
780 if (!armed) {
781 // if not armed return min EVERYTIME!
782 valueScaled = min;
783 } else if (!alwaysStabilizeWhenArmed && (throttleDesired <= 0.0f) && spinWhileArmed) {
784 // all motors idle is alwaysStabilizeWhenArmed is false, throttle is less than or equal to neutral and spin while armed
785 // stabilize when armed?
786 valueScaled = neutral;
787 } else if (!spinWhileArmed && (throttleDesired <= 0.0f)) {
788 // soft disarm
789 valueScaled = min;
792 return valueScaled;
796 * Set actuator output to the neutral values (failsafe)
798 static void setFailsafe()
800 /* grab only the parts that we are going to use */
801 int16_t Channel[ACTUATORCOMMAND_CHANNEL_NUMELEM];
803 ActuatorCommandChannelGet(Channel);
805 const Mixer_t *mixers = (Mixer_t *)&mixerSettings.Mixer1Type; // pointer to array of mixers in UAVObjects
807 // Reset ActuatorCommand to safe values
808 for (int n = 0; n < ACTUATORCOMMAND_CHANNEL_NUMELEM; ++n) {
809 if (mixers[n].type == MIXERSETTINGS_MIXER1TYPE_MOTOR) {
810 Channel[n] = actuatorSettings.ChannelMin[n];
811 } else if (mixers[n].type == MIXERSETTINGS_MIXER1TYPE_SERVO || mixers[n].type == MIXERSETTINGS_MIXER1TYPE_REVERSABLEMOTOR) {
812 // reversible motors need calibration wizard that allows channel neutral to be the 0 velocity point
813 Channel[n] = actuatorSettings.ChannelNeutral[n];
814 } else {
815 Channel[n] = 0;
819 // Set alarm
820 AlarmsSet(SYSTEMALARMS_ALARM_ACTUATOR, SYSTEMALARMS_ALARM_CRITICAL);
822 // Update servo outputs
823 for (int n = 0; n < ACTUATORCOMMAND_CHANNEL_NUMELEM; ++n) {
824 set_channel(n, Channel[n]);
826 // Send the updated command
827 PIOS_Servo_Update();
829 // Update output object's parts that we changed
830 ActuatorCommandChannelSet(Channel);
834 * determine buzzer or blink sequence
837 typedef enum { BUZZ_BUZZER = 0, BUZZ_ARMING = 1, BUZZ_INFO = 2, BUZZ_MAX = 3 } buzzertype;
839 static inline bool buzzerState(buzzertype type)
841 // This is for buzzers that take a PWM input
843 static uint32_t tune[BUZZ_MAX] = { 0 };
844 static uint32_t tunestate[BUZZ_MAX] = { 0 };
847 uint32_t newTune = 0;
849 if (type == BUZZ_BUZZER) {
850 // Decide what tune to play
851 if (AlarmsGet(SYSTEMALARMS_ALARM_BATTERY) > SYSTEMALARMS_ALARM_WARNING) {
852 newTune = 0b11110110110000; // pause, short, short, short, long
853 } else if (AlarmsGet(SYSTEMALARMS_ALARM_GPS) >= SYSTEMALARMS_ALARM_WARNING) {
854 newTune = 0x80000000; // pause, short
855 } else {
856 newTune = 0;
858 } else { // BUZZ_ARMING || BUZZ_INFO
859 uint8_t arming;
860 FlightStatusArmedGet(&arming);
861 // base idle tune
862 newTune = 0x80000000; // 0b1000...
864 // Merge the error pattern for InfoLed
865 if (type == BUZZ_INFO) {
866 if (AlarmsGet(SYSTEMALARMS_ALARM_BATTERY) > SYSTEMALARMS_ALARM_WARNING) {
867 newTune |= 0b00000000001111111011111110000000;
868 } else if (AlarmsGet(SYSTEMALARMS_ALARM_GPS) >= SYSTEMALARMS_ALARM_WARNING) {
869 newTune |= 0b00000000000000110110110000000000;
872 // fast double blink pattern if armed
873 if (arming == FLIGHTSTATUS_ARMED_ARMED) {
874 newTune |= 0xA0000000; // 0b101000...
878 // Do we need to change tune?
879 if (newTune != tune[type]) {
880 tune[type] = newTune;
881 // resynchronize all tunes on change, so they stay in sync
882 for (int i = 0; i < BUZZ_MAX; i++) {
883 tunestate[i] = tune[i];
887 // Play tune
888 bool buzzOn = false;
889 static portTickType lastSysTime = 0;
890 portTickType thisSysTime = xTaskGetTickCount();
891 portTickType dT = 0;
893 // For now, only look at the battery alarm, because functions like AlarmsHasCritical() can block for some time; to be discussed
894 if (tune[type]) {
895 if (thisSysTime > lastSysTime) {
896 dT = thisSysTime - lastSysTime;
897 } else {
898 lastSysTime = 0; // avoid the case where SysTimeMax-lastSysTime <80
901 buzzOn = (tunestate[type] & 1);
903 if (dT > 80) {
904 // Go to next bit in alarm_seq_state
905 for (int i = 0; i < BUZZ_MAX; i++) {
906 tunestate[i] >>= 1;
907 if (tunestate[i] == 0) { // All done, re-start the tune
908 tunestate[i] = tune[i];
911 lastSysTime = thisSysTime;
914 return buzzOn;
918 #if defined(ARCH_POSIX) || defined(ARCH_WIN32)
919 static bool set_channel(uint8_t mixer_channel, uint16_t value)
921 return true;
923 #else
924 static bool set_channel(uint8_t mixer_channel, uint16_t value)
926 switch (actuatorSettings.ChannelType[mixer_channel]) {
927 case ACTUATORSETTINGS_CHANNELTYPE_PWMALARMBUZZER:
928 PIOS_Servo_Set(actuatorSettings.ChannelAddr[mixer_channel],
929 buzzerState(BUZZ_BUZZER) ? actuatorSettings.ChannelMax[mixer_channel] : actuatorSettings.ChannelMin[mixer_channel]);
930 return true;
932 case ACTUATORSETTINGS_CHANNELTYPE_ARMINGLED:
933 PIOS_Servo_Set(actuatorSettings.ChannelAddr[mixer_channel],
934 buzzerState(BUZZ_ARMING) ? actuatorSettings.ChannelMax[mixer_channel] : actuatorSettings.ChannelMin[mixer_channel]);
935 return true;
937 case ACTUATORSETTINGS_CHANNELTYPE_INFOLED:
938 PIOS_Servo_Set(actuatorSettings.ChannelAddr[mixer_channel],
939 buzzerState(BUZZ_INFO) ? actuatorSettings.ChannelMax[mixer_channel] : actuatorSettings.ChannelMin[mixer_channel]);
940 return true;
942 case ACTUATORSETTINGS_CHANNELTYPE_PWM:
944 uint8_t mode = pinsMode[actuatorSettings.ChannelAddr[mixer_channel]];
945 switch (mode) {
946 case ACTUATORSETTINGS_BANKMODE_ONESHOT125:
947 // Remap 1000-2000 range to 125-250µs
948 PIOS_Servo_Set(actuatorSettings.ChannelAddr[mixer_channel], value * ACTUATOR_ONESHOT125_PULSE_FACTOR);
949 break;
950 case ACTUATORSETTINGS_BANKMODE_ONESHOT42:
951 // Remap 1000-2000 range to 41,666-83,333µs
952 PIOS_Servo_Set(actuatorSettings.ChannelAddr[mixer_channel], value * ACTUATOR_ONESHOT42_PULSE_FACTOR);
953 break;
954 case ACTUATORSETTINGS_BANKMODE_MULTISHOT:
955 // Remap 1000-2000 range to 5-25µs
956 PIOS_Servo_Set(actuatorSettings.ChannelAddr[mixer_channel], (value * ACTUATOR_MULTISHOT_PULSE_FACTOR) - 180);
957 break;
958 case ACTUATORSETTINGS_BANKMODE_DSHOT:
959 // Remap 0-2000 range to: 0 = disarmed, 1 to 47 = Reserved for special commands, 48 to 2047 = Active throttle control.
960 if (value > 0) {
961 value += 47; /* skip over reserved values */
963 PIOS_Servo_Set(actuatorSettings.ChannelAddr[mixer_channel], value);
964 break;
965 default:
966 PIOS_Servo_Set(actuatorSettings.ChannelAddr[mixer_channel], value);
967 break;
969 return true;
972 #if defined(PIOS_INCLUDE_I2C_ESC)
973 case ACTUATORSETTINGS_CHANNELTYPE_MK:
974 return PIOS_SetMKSpeed(actuatorSettings->ChannelAddr[mixer_channel], value);
976 case ACTUATORSETTINGS_CHANNELTYPE_ASTEC4:
977 return PIOS_SetAstec4Speed(actuatorSettings->ChannelAddr[mixer_channel], value);
979 #endif
980 default:
981 return false;
984 return false;
986 #endif /* if defined(ARCH_POSIX) || defined(ARCH_WIN32) */
989 * @brief Update the servo update rate
991 static void actuator_update_rate_if_changed(bool force_update)
993 static uint16_t prevBankUpdateFreq[ACTUATORSETTINGS_BANKUPDATEFREQ_NUMELEM];
994 static uint8_t prevBankMode[ACTUATORSETTINGS_BANKMODE_NUMELEM];
995 static uint16_t prevDShotMode;
996 bool updateMode = force_update || (memcmp(prevBankMode, actuatorSettings.BankMode, sizeof(prevBankMode)) != 0);
997 bool updateFreq = force_update || (memcmp(prevBankUpdateFreq, actuatorSettings.BankUpdateFreq, sizeof(prevBankUpdateFreq)) != 0);
999 if (force_update || (prevDShotMode != actuatorSettings.DShotMode)) {
1000 PIOS_Servo_DSHot_Rate(actuatorSettings.DShotMode);
1001 prevDShotMode = actuatorSettings.DShotMode;
1004 // check if any setting is changed
1005 if (updateMode || updateFreq) {
1006 /* Something has changed, apply the settings to HW */
1008 uint16_t freq[ACTUATORSETTINGS_BANKUPDATEFREQ_NUMELEM];
1009 uint32_t clock[ACTUATORSETTINGS_BANKUPDATEFREQ_NUMELEM] = { 0 };
1010 for (uint8_t i = 0; i < ACTUATORSETTINGS_BANKMODE_NUMELEM; i++) {
1011 enum pios_servo_bank_mode servo_bank_mode = PIOS_SERVO_BANK_MODE_PWM;
1013 switch (actuatorSettings.BankMode[i]) {
1014 case ACTUATORSETTINGS_BANKMODE_ONESHOT125:
1015 case ACTUATORSETTINGS_BANKMODE_ONESHOT42:
1016 case ACTUATORSETTINGS_BANKMODE_MULTISHOT:
1017 freq[i] = 100; // Value must be small enough so CCr isn't update until the PIOS_Servo_Update is triggered
1018 clock[i] = ACTUATOR_ONESHOT_CLOCK; // Setup an 12MHz timer clock
1019 servo_bank_mode = PIOS_SERVO_BANK_MODE_SINGLE_PULSE;
1020 break;
1021 case ACTUATORSETTINGS_BANKMODE_PWMSYNC:
1022 freq[i] = 100;
1023 clock[i] = ACTUATOR_PWM_CLOCK;
1024 servo_bank_mode = PIOS_SERVO_BANK_MODE_SINGLE_PULSE;
1025 break;
1026 case ACTUATORSETTINGS_BANKMODE_DSHOT:
1027 freq[i] = 100;
1028 clock[i] = ACTUATOR_PWM_CLOCK;
1029 servo_bank_mode = PIOS_SERVO_BANK_MODE_DSHOT;
1030 break;
1031 default: // PWM
1032 freq[i] = actuatorSettings.BankUpdateFreq[i];
1033 clock[i] = ACTUATOR_PWM_CLOCK;
1034 servo_bank_mode = PIOS_SERVO_BANK_MODE_PWM;
1035 break;
1038 if (force_update || (actuatorSettings.BankMode[i] != prevBankMode[i])) {
1039 PIOS_Servo_SetBankMode(i, servo_bank_mode);
1043 memcpy(prevBankMode,
1044 actuatorSettings.BankMode,
1045 sizeof(prevBankMode));
1047 PIOS_Servo_SetHz(freq, clock, ACTUATORSETTINGS_BANKUPDATEFREQ_NUMELEM);
1049 memcpy(prevBankUpdateFreq,
1050 actuatorSettings.BankUpdateFreq,
1051 sizeof(prevBankUpdateFreq));
1052 // retrieve mode from related bank
1053 for (uint8_t i = 0; i < MAX_MIX_ACTUATORS; i++) {
1054 uint8_t bank = PIOS_Servo_GetPinBank(i);
1055 pinsMode[i] = actuatorSettings.BankMode[bank];
1060 static void update_servo_active()
1062 /* For each mixer output that is not disabled,
1063 * figure out servo address and send allocation map to pios_servo driver.
1064 * We need to execute this when either ActuatorSettings or MixerSettings change.
1066 uint32_t servo_active = 0;
1068 Mixer_t *mixers = (Mixer_t *)&mixerSettings.Mixer1Type;
1070 for (int ct = 0; ct < MAX_MIX_ACTUATORS; ct++) {
1071 if (mixers[ct].type != MIXERSETTINGS_MIXER1TYPE_DISABLED) {
1072 servo_active |= 1 << actuatorSettings.ChannelAddr[ct];
1076 PIOS_Servo_SetActive(servo_active);
1079 static void ActuatorSettingsUpdatedCb(__attribute__((unused)) UAVObjEvent *ev)
1081 ActuatorSettingsGet(&actuatorSettings);
1082 spinWhileArmed = actuatorSettings.MotorsSpinWhileArmed == ACTUATORSETTINGS_MOTORSSPINWHILEARMED_TRUE;
1083 if (frameType == FRAME_TYPE_GROUND) {
1084 spinWhileArmed = false;
1086 actuator_update_rate_if_changed(false);
1088 update_servo_active();
1091 static void MixerSettingsUpdatedCb(__attribute__((unused)) UAVObjEvent *ev)
1093 MixerSettingsGet(&mixerSettings);
1094 mixer_settings_count = 0;
1095 Mixer_t *mixers = (Mixer_t *)&mixerSettings.Mixer1Type;
1096 for (int ct = 0; ct < MAX_MIX_ACTUATORS; ct++) {
1097 if (mixers[ct].type != MIXERSETTINGS_MIXER1TYPE_DISABLED) {
1098 mixer_settings_count++;
1102 update_servo_active();
1104 static void SettingsUpdatedCb(__attribute__((unused)) UAVObjEvent *ev)
1106 frameType = GetCurrentFrameType();
1107 #ifndef PIOS_EXCLUDE_ADVANCED_FEATURES
1108 uint8_t TreatCustomCraftAs;
1109 VtolPathFollowerSettingsTreatCustomCraftAsGet(&TreatCustomCraftAs);
1111 if (frameType == FRAME_TYPE_CUSTOM) {
1112 switch (TreatCustomCraftAs) {
1113 case VTOLPATHFOLLOWERSETTINGS_TREATCUSTOMCRAFTAS_FIXEDWING:
1114 frameType = FRAME_TYPE_FIXED_WING;
1115 break;
1116 case VTOLPATHFOLLOWERSETTINGS_TREATCUSTOMCRAFTAS_VTOL:
1117 frameType = FRAME_TYPE_MULTIROTOR;
1118 break;
1119 case VTOLPATHFOLLOWERSETTINGS_TREATCUSTOMCRAFTAS_GROUND:
1120 frameType = FRAME_TYPE_GROUND;
1121 break;
1124 #endif
1126 SystemSettingsThrustControlGet(&thrustType);
1130 * @}
1131 * @}