mixer: complete refactoring & add airmode for yaw

Provides a more generic desaturation algorithm, making it easier to
handle the different mixer strategies.

python reference implementation:
https://github.com/Auterion/Flight_Control_Prototyping_Scripts/tree/master/control_allocation

The behavior for non-airmode and roll/pitch airmode should be the
same as before.

CPU load is slightly higher for the non-airmode case (+0.5%), for airmode
it is the same.
This commit is contained in:
Beat Küng
2018-12-13 09:50:07 +01:00
parent 19ccab28bb
commit 8bcf54688a
2 changed files with 256 additions and 144 deletions
+73 -1
View File
@@ -693,6 +693,78 @@ public:
};
private:
/**
* Computes the gain k by which delta_outputs has to be multiplied
* in order to unsaturate the output that has the greatest saturation.
* @see also minimize_saturation().
*
* @return desaturation gain
*/
float compute_desaturation_gain(const float *delta_outputs, const float *outputs, saturation_status &sat_status,
float min_output, float max_output) const;
/**
* Minimize the saturation of the actuators by adding or substracting a fraction of delta_outputs.
* delta_outputs is the vector that added to the output outputs, modifies the thrust or angular
* acceleration on a specific axis.
* For example, if delta_outputs is given to slide along the vertical thrust axis (thrust_scale), the
* saturation will be minimized by shifting the vertical thrust setpoint, without changing the
* roll/pitch/yaw accelerations.
*
* Note that as we only slide along the given axis, in extreme cases outputs can still contain values
* outside of [min_output, max_output].
*
* @param delta_outputs vector that is added to the outputs, e.g. thrust_scale
* @param outputs output vector that is modified
* @param sat_status saturation status output
* @param min_output minimum desired value in outputs
* @param max_output maximum desired value in outputs
* @param reduce_only if true, only allow to reduce (substract) a fraction of delta_outputs
*/
void minimize_saturation(const float *delta_outputs, float *outputs, saturation_status &sat_status,
float min_output = 0.f, float max_output = 1.f, bool reduce_only = false) const;
/**
* Mix roll, pitch, yaw, thrust and set the outputs vector.
*
* Desaturation behavior: airmode for roll/pitch:
* thrust is increased/decreased as much as required to meet the demanded roll/pitch.
* Yaw is not allowed to increase the thrust, @see mix_yaw() for the exact behavior.
*/
inline void mix_airmode_rp(float roll, float pitch, float yaw, float thrust, float *outputs);
/**
* Mix roll, pitch, yaw, thrust and set the outputs vector.
*
* Desaturation behavior: full airmode for roll/pitch/yaw:
* thrust is increased/decreased as much as required to meet demanded the roll/pitch/yaw.
*/
inline void mix_airmode_rpy(float roll, float pitch, float yaw, float thrust, float *outputs);
/**
* Mix roll, pitch, yaw, thrust and set the outputs vector.
*
* Desaturation behavior: no airmode, thrust is NEVER increased to meet the demanded
* roll/pitch/yaw. Instead roll/pitch/yaw is reduced as much as needed.
* Thrust can be reduced to unsaturate the upper side.
* @see mix_yaw() for the exact yaw behavior.
*/
inline void mix_airmode_disabled(float roll, float pitch, float yaw, float thrust, float *outputs);
/**
* Mix yaw by updating an existing output vector (that already contains roll/pitch/thrust).
*
* Desaturation behavior: thrust is allowed to be decreased up to 15% in order to allow
* some yaw control on the upper end. On the lower end thrust will never be increased,
* but yaw is decreased as much as required.
*
* @param yaw demanded yaw
* @param outputs output vector that is updated
*/
inline void mix_yaw(float yaw, float *outputs);
void update_saturation_status(unsigned index, bool clipping_high, bool clipping_low);
float _roll_scale;
float _pitch_scale;
float _yaw_scale;
@@ -702,13 +774,13 @@ private:
Airmode _airmode;
void update_saturation_status(unsigned index, bool clipping_high, bool clipping_low);
saturation_status _saturation_status;
unsigned _rotor_count;
const Rotor *_rotors;
float *_outputs_prev = nullptr;
float *_tmp_array = nullptr;
/* do not allow to copy due to ptr data members */
MultirotorMixer(const MultirotorMixer &);
+183 -143
View File
@@ -53,11 +53,6 @@
//#include <debug.h>
//#define debug(fmt, args...) syslog(fmt "\n", ##args)
/*
* Clockwise: 1
* Counter-clockwise: -1
*/
MultirotorMixer::MultirotorMixer(ControlCallback control_cb,
uintptr_t cb_handle,
MultirotorGeometry geometry,
@@ -75,7 +70,8 @@ MultirotorMixer::MultirotorMixer(ControlCallback control_cb,
_airmode(Airmode::disabled),
_rotor_count(_config_rotor_count[(MultirotorGeometryUnderlyingType)geometry]),
_rotors(_config_index[(MultirotorGeometryUnderlyingType)geometry]),
_outputs_prev(new float[_rotor_count])
_outputs_prev(new float[_rotor_count]),
_tmp_array(new float[_rotor_count])
{
for (unsigned i = 0; i < _rotor_count; ++i) {
_outputs_prev[i] = _idle_speed;
@@ -84,9 +80,8 @@ MultirotorMixer::MultirotorMixer(ControlCallback control_cb,
MultirotorMixer::~MultirotorMixer()
{
if (_outputs_prev != nullptr) {
delete[] _outputs_prev;
}
delete[] _outputs_prev;
delete[] _tmp_array;
}
MultirotorMixer *
@@ -146,146 +141,192 @@ MultirotorMixer::from_text(Mixer::ControlCallback control_cb, uintptr_t cb_handl
s[3] / 10000.0f);
}
float MultirotorMixer::compute_desaturation_gain(const float *delta_outputs, const float *outputs,
saturation_status &sat_status, float min_output, float max_output) const
{
float k_min = 0.f;
float k_max = 0.f;
for (unsigned i = 0; i < _rotor_count; i++) {
// Avoid division by zero. If delta_outputs[i] is zero, there's nothing we can do to unsaturate anyway
if (fabsf(delta_outputs[i]) < FLT_EPSILON) {
continue;
}
if (outputs[i] < min_output) {
float k = (min_output - outputs[i]) / delta_outputs[i];
if (k < k_min) { k_min = k; }
if (k > k_max) { k_max = k; }
sat_status.flags.motor_neg = true;
}
if (outputs[i] > max_output) {
float k = (max_output - outputs[i]) / delta_outputs[i];
if (k < k_min) { k_min = k; }
if (k > k_max) { k_max = k; }
sat_status.flags.motor_pos = true;
}
}
// Reduce the saturation as much as possible
return k_min + k_max;
}
void MultirotorMixer::minimize_saturation(const float *delta_outputs, float *outputs, saturation_status &sat_status,
float min_output, float max_output, bool reduce_only) const
{
float k1 = compute_desaturation_gain(delta_outputs, outputs, sat_status, min_output, max_output);
if (reduce_only && k1 > 0.f) {
return;
}
for (unsigned i = 0; i < _rotor_count; i++) {
outputs[i] += k1 * delta_outputs[i];
}
// Compute the desaturation gain again based on the updated outputs.
// In most cases it will be zero. It won't be if max(outputs) - min(outputs) > max_output - min_output.
// In that case adding 0.5 of the gain will equilibrate saturations.
float k2 = 0.5f * compute_desaturation_gain(delta_outputs, outputs, sat_status, min_output, max_output);
for (unsigned i = 0; i < _rotor_count; i++) {
outputs[i] += k2 * delta_outputs[i];
}
}
void MultirotorMixer::mix_airmode_rp(float roll, float pitch, float yaw, float thrust, float *outputs)
{
// Airmode for roll and pitch, but not yaw
// Mix without yaw
for (unsigned i = 0; i < _rotor_count; i++) {
outputs[i] = roll * _rotors[i].roll_scale +
pitch * _rotors[i].pitch_scale +
thrust * _rotors[i].thrust_scale;
// Thrust will be used to unsaturate if needed
_tmp_array[i] = _rotors[i].thrust_scale;
}
minimize_saturation(_tmp_array, outputs, _saturation_status);
// Mix yaw independently
mix_yaw(yaw, outputs);
}
void MultirotorMixer::mix_airmode_rpy(float roll, float pitch, float yaw, float thrust, float *outputs)
{
// Airmode for roll, pitch and yaw
// Do full mixing
for (unsigned i = 0; i < _rotor_count; i++) {
outputs[i] = roll * _rotors[i].roll_scale +
pitch * _rotors[i].pitch_scale +
yaw * _rotors[i].yaw_scale +
thrust * _rotors[i].thrust_scale;
// Thrust will be used to unsaturate if needed
_tmp_array[i] = _rotors[i].thrust_scale;
}
minimize_saturation(_tmp_array, outputs, _saturation_status);
}
void MultirotorMixer::mix_airmode_disabled(float roll, float pitch, float yaw, float thrust, float *outputs)
{
// Airmode disabled: never allow to increase the thrust to unsaturate a motor
// Mix without yaw
for (unsigned i = 0; i < _rotor_count; i++) {
outputs[i] = roll * _rotors[i].roll_scale +
pitch * _rotors[i].pitch_scale +
thrust * _rotors[i].thrust_scale;
// Thrust will be used to unsaturate if needed
_tmp_array[i] = _rotors[i].thrust_scale;
}
// only reduce thrust
minimize_saturation(_tmp_array, outputs, _saturation_status, 0.f, 1.f, true);
// Reduce roll/pitch acceleration if needed to unsaturate
for (unsigned i = 0; i < _rotor_count; i++) {
_tmp_array[i] = _rotors[i].roll_scale;
}
minimize_saturation(_tmp_array, outputs, _saturation_status);
for (unsigned i = 0; i < _rotor_count; i++) {
_tmp_array[i] = _rotors[i].pitch_scale;
}
minimize_saturation(_tmp_array, outputs, _saturation_status);
// Mix yaw independently
mix_yaw(yaw, outputs);
}
void MultirotorMixer::mix_yaw(float yaw, float *outputs)
{
// Add yaw to outputs
for (unsigned i = 0; i < _rotor_count; i++) {
outputs[i] += yaw * _rotors[i].yaw_scale;
// Yaw will be used to unsaturate if needed
_tmp_array[i] = _rotors[i].yaw_scale;
}
// Change yaw acceleration to unsaturate the outputs if needed (do not change roll/pitch),
// and allow some yaw response at maximum thrust
minimize_saturation(_tmp_array, outputs, _saturation_status, 0.f, 1.15f);
for (unsigned i = 0; i < _rotor_count; i++) {
_tmp_array[i] = _rotors[i].thrust_scale;
}
minimize_saturation(_tmp_array, outputs, _saturation_status, -1000.f);
}
unsigned
MultirotorMixer::mix(float *outputs, unsigned space)
{
/* Summary of mixing strategy:
1) mix roll, pitch and thrust without yaw.
2) if some outputs violate range [0,1] then try to shift all outputs to minimize violation ->
increase or decrease total thrust (boost). The total increase or decrease of thrust is limited
(max_thrust_diff). If after the shift some outputs still violate the bounds then scale roll & pitch.
In case there is violation at the lower and upper bound then try to shift such that violation is equal
on both sides.
3) mix in yaw and scale if it leads to limit violation.
4) scale all outputs to range [idle_speed,1]
*/
float roll = math::constrain(get_control(0, 0) * _roll_scale, -1.0f, 1.0f);
float pitch = math::constrain(get_control(0, 1) * _pitch_scale, -1.0f, 1.0f);
float yaw = math::constrain(get_control(0, 2) * _yaw_scale, -1.0f, 1.0f);
float thrust = math::constrain(get_control(0, 3), 0.0f, 1.0f);
float min_out = 1.0f;
float max_out = 0.0f;
float roll = math::constrain(get_control(0, 0) * _roll_scale, -1.0f, 1.0f);
float pitch = math::constrain(get_control(0, 1) * _pitch_scale, -1.0f, 1.0f);
float yaw = math::constrain(get_control(0, 2) * _yaw_scale, -1.0f, 1.0f);
float thrust = math::constrain(get_control(0, 3), 0.0f, 1.0f);
// clean out class variable used to capture saturation
_saturation_status.value = 0;
/* perform initial mix pass yielding unbounded outputs, ignore yaw */
// Do the mixing using the strategy given by the current Airmode configuration
switch (_airmode) {
case Airmode::roll_pitch:
mix_airmode_rp(roll, pitch, yaw, thrust, outputs);
break;
case Airmode::roll_pitch_yaw:
mix_airmode_rpy(roll, pitch, yaw, thrust, outputs);
break;
case Airmode::disabled:
default: // just in case: default to disabled
mix_airmode_disabled(roll, pitch, yaw, thrust, outputs);
break;
}
// Apply thrust model and scale outputs to range [idle_speed, 1].
// At this point the outputs are expected to be in [0, 1], but they can be outside, for example
// if a roll command exceeds the motor band limit.
for (unsigned i = 0; i < _rotor_count; i++) {
float out = roll * _rotors[i].roll_scale +
pitch * _rotors[i].pitch_scale +
thrust * _rotors[i].thrust_scale;
/* calculate min and max output values */
if (out < min_out) {
min_out = out;
}
if (out > max_out) {
max_out = out;
}
outputs[i] = out;
}
float boost = 0.0f; // value added to demanded thrust (can also be negative)
float roll_pitch_scale = 1.0f; // scale for demanded roll and pitch
float delta_out_max = max_out - min_out; // distance between the two extrema
// If the difference between the to extrema is smaller than 1.0, the boost can safely unsaturate a motor if needed
// without saturating another one.
// Otherwise, a scaler is computed to make the distance between the two extrema exacly 1.0 and the boost
// value is computed to maximize the roll-pitch control.
//
// Note: thrust boost is computed assuming thrust_scale==1 for all motors.
// On asymmetric platforms, some motors have thrust_scale<1,
// which may result in motor saturation after thrust boost is applied
// TODO: revise the saturation/boosting strategy
if (delta_out_max <= 1.0f) {
if (min_out < 0.0f) {
boost = -min_out;
} else if (max_out > 1.0f) {
boost = -(max_out - 1.0f);
}
} else {
roll_pitch_scale = 1.0f / (delta_out_max);
boost = 1.0f - ((max_out - thrust) * roll_pitch_scale + thrust);
}
if (_airmode == Airmode::disabled) {
// disable positive boosting if not in air-mode
// boosting can only be positive when min_out < 0.0
// roll_pitch_scale is reduced accordingly
if (boost > 0.0f) {
roll_pitch_scale = thrust / (thrust - min_out);
boost = 0.0f;
}
}
// capture saturation
if (min_out < 0.0f) {
_saturation_status.flags.motor_neg = true;
}
if (max_out > 1.0f) {
_saturation_status.flags.motor_pos = true;
}
// Thrust reduction is used to reduce the collective thrust if we hit
// the upper throttle limit
float thrust_reduction = 0.0f;
// mix again but now with thrust boost, scale roll/pitch and also add yaw
for (unsigned i = 0; i < _rotor_count; i++) {
float out = (roll * _rotors[i].roll_scale +
pitch * _rotors[i].pitch_scale) * roll_pitch_scale +
yaw * _rotors[i].yaw_scale +
(thrust + boost) * _rotors[i].thrust_scale;
// scale yaw if it violates limits. inform about yaw limit reached
if (out < 0.0f) {
if (fabsf(_rotors[i].yaw_scale) <= FLT_EPSILON) {
yaw = 0.0f;
} else {
yaw = -((roll * _rotors[i].roll_scale + pitch * _rotors[i].pitch_scale) *
roll_pitch_scale + thrust + boost) / _rotors[i].yaw_scale;
}
} else if (out > 1.0f) {
// allow to reduce thrust to get some yaw response
float prop_reduction = fminf(0.15f, out - 1.0f);
// keep the maximum requested reduction
thrust_reduction = fmaxf(thrust_reduction, prop_reduction);
if (fabsf(_rotors[i].yaw_scale) <= FLT_EPSILON) {
yaw = 0.0f;
} else {
yaw = (1.0f - ((roll * _rotors[i].roll_scale + pitch * _rotors[i].pitch_scale) *
roll_pitch_scale + (thrust - thrust_reduction) + boost)) / _rotors[i].yaw_scale;
}
}
}
// Apply collective thrust reduction, the maximum for one prop
thrust -= thrust_reduction;
// add yaw and scale outputs to range idle_speed...1
for (unsigned i = 0; i < _rotor_count; i++) {
outputs[i] = (roll * _rotors[i].roll_scale +
pitch * _rotors[i].pitch_scale) * roll_pitch_scale +
yaw * _rotors[i].yaw_scale +
(thrust + boost) * _rotors[i].thrust_scale;
/*
implement simple model for static relationship between applied motor pwm and motor thrust
model: thrust = (1 - _thrust_factor) * PWM + _thrust_factor * PWM^2
this model assumes normalized input / output in the range [0,1] so this is the right place
to do it as at this stage the outputs are in that range.
*/
// Implement simple model for static relationship between applied motor pwm and motor thrust
// model: thrust = (1 - _thrust_factor) * PWM + _thrust_factor * PWM^2
if (_thrust_factor > 0.0f) {
outputs[i] = -(1.0f - _thrust_factor) / (2.0f * _thrust_factor) + sqrtf((1.0f - _thrust_factor) *
(1.0f - _thrust_factor) / (4.0f * _thrust_factor * _thrust_factor) + (outputs[i] < 0.0f ? 0.0f : outputs[i] /
@@ -293,10 +334,9 @@ MultirotorMixer::mix(float *outputs, unsigned space)
}
outputs[i] = math::constrain(_idle_speed + (outputs[i] * (1.0f - _idle_speed)), _idle_speed, 1.0f);
}
/* slew rate limiting and saturation checking */
// Slew rate limiting and saturation checking
for (unsigned i = 0; i < _rotor_count; i++) {
bool clipping_high = false;
bool clipping_low = false;