/* * This file is part of Cleanflight and Betaflight. * * Cleanflight and Betaflight are free software. You can redistribute * this software and/or modify this software under the terms of the * GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) * any later version. * * Cleanflight and Betaflight are distributed in the hope that they * will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this software. * * If not, see . * * Author: jflyper * * Follows the extended dshot telemetry documentation found at https://github.com/bird-sanctuary/extended-dshot-telemetry */ #include #include #include #include #include "platform.h" #include "dshot.h" #ifdef USE_DSHOT #include "build/debug.h" #include "build/atomic.h" #include "common/filter.h" #include "common/maths.h" #include "config/feature.h" #include "drivers/motor_types.h" #include "drivers/timer.h" #include "drivers/dshot_command.h" #include "drivers/nvic.h" #include "pg/rpm_filter.h" #include "rx/rx.h" #define ERPM_PER_LSB 100.0f FAST_DATA_ZERO_INIT uint8_t dshotMotorCount = 0; void dshotInitEndpoints(const motorConfig_t *motorConfig, float outputLimit, float *outputLow, float *outputHigh, float *disarm, float *deadbandMotor3dHigh, float *deadbandMotor3dLow) { float outputLimitOffset = DSHOT_RANGE * (1 - outputLimit); const float motorIdlePercent = CONVERT_PARAMETER_TO_PERCENT(motorConfig->motorIdle * 0.01f); *disarm = DSHOT_CMD_MOTOR_STOP; if (featureIsEnabled(FEATURE_3D)) { *outputLow = DSHOT_MIN_THROTTLE + motorIdlePercent * (DSHOT_3D_FORWARD_MIN_THROTTLE - 1 - DSHOT_MIN_THROTTLE); *outputHigh = DSHOT_MAX_THROTTLE - outputLimitOffset / 2; *deadbandMotor3dHigh = DSHOT_3D_FORWARD_MIN_THROTTLE + motorIdlePercent * (DSHOT_MAX_THROTTLE - DSHOT_3D_FORWARD_MIN_THROTTLE); *deadbandMotor3dLow = DSHOT_3D_FORWARD_MIN_THROTTLE - 1 - outputLimitOffset / 2; } else { *outputLow = DSHOT_MIN_THROTTLE + motorIdlePercent * DSHOT_RANGE; *outputHigh = DSHOT_MAX_THROTTLE - outputLimitOffset; } } DEFINE_SCALE_FN(scaleRangeDshotFromExternal, PWM_RANGE_MIN + 1, PWM_RANGE_MAX, DSHOT_MIN_THROTTLE, DSHOT_MAX_THROTTLE) DEFINE_SCALE_FN(scaleRangeDshotFromExternal3DNeg, PWM_RANGE_MIN, PWM_RANGE_MIDDLE - 1, DSHOT_3D_FORWARD_MIN_THROTTLE - 1, DSHOT_MIN_THROTTLE) DEFINE_SCALE_FN(scaleRangeDshotFromExternal3DPos, PWM_RANGE_MIDDLE + 1, PWM_RANGE_MAX, DSHOT_3D_FORWARD_MIN_THROTTLE, DSHOT_MAX_THROTTLE) DEFINE_SCALE_FN(scaleRangeDshotToExternal, DSHOT_MIN_THROTTLE, DSHOT_MAX_THROTTLE, PWM_RANGE_MIN + 1, PWM_RANGE_MAX) DEFINE_SCALE_FN(scaleRangeDshotToExternal3DNeg, DSHOT_MIN_THROTTLE, DSHOT_3D_FORWARD_MIN_THROTTLE - 1, PWM_RANGE_MIDDLE - 1, PWM_RANGE_MIN) DEFINE_SCALE_FN(scaleRangeDshotToExternal3DPos, DSHOT_3D_FORWARD_MIN_THROTTLE, DSHOT_MAX_THROTTLE, PWM_RANGE_MIDDLE + 1, PWM_RANGE_MAX) float dshotConvertFromExternal(uint16_t externalValue) { float motorValue; externalValue = constrain(externalValue, PWM_RANGE_MIN, PWM_RANGE_MAX); if (featureIsEnabled(FEATURE_3D)) { if (externalValue == PWM_RANGE_MIDDLE) { motorValue = DSHOT_CMD_MOTOR_STOP; } else if (externalValue < PWM_RANGE_MIDDLE) { motorValue = scaleRangeDshotFromExternal3DNeg(externalValue); } else { motorValue = scaleRangeDshotFromExternal3DPos(externalValue); } } else { motorValue = (externalValue == PWM_RANGE_MIN) ? DSHOT_CMD_MOTOR_STOP : scaleRangeDshotFromExternal(externalValue); } return motorValue; } uint16_t dshotConvertToExternal(float motorValue) { float externalValue; if (featureIsEnabled(FEATURE_3D)) { if (motorValue == DSHOT_CMD_MOTOR_STOP || motorValue < DSHOT_MIN_THROTTLE) { externalValue = PWM_RANGE_MIDDLE; } else if (motorValue <= DSHOT_3D_FORWARD_MIN_THROTTLE - 1) { externalValue = scaleRangeDshotToExternal3DNeg(motorValue); } else { externalValue = scaleRangeDshotToExternal3DPos(motorValue); } } else { externalValue = (motorValue < DSHOT_MIN_THROTTLE) ? PWM_RANGE_MIN : scaleRangeDshotToExternal(motorValue); } return lrintf(externalValue); } FAST_CODE uint16_t prepareDshotPacket(dshotProtocolControl_t *pcb) { uint16_t packet; ATOMIC_BLOCK(NVIC_PRIO_DSHOT_DMA) { packet = (pcb->value << 1) | (pcb->requestTelemetry ? 1 : 0); pcb->requestTelemetry = false; // reset telemetry request to make sure it's triggered only once in a row } // compute checksum unsigned csum = 0; unsigned csum_data = packet; for (int i = 0; i < 3; i++) { csum ^= csum_data; // xor data by nibbles csum_data >>= 4; } // append checksum #ifdef USE_DSHOT_TELEMETRY if (useDshotTelemetry) { csum = ~csum; } #endif csum &= 0xf; packet = (packet << 4) | csum; return packet; } #ifdef USE_DSHOT_TELEMETRY FAST_DATA_ZERO_INIT dshotTelemetryState_t dshotTelemetryState; FAST_DATA_ZERO_INIT static pt1Filter_t motorFreqLpf[MAX_SUPPORTED_MOTORS]; FAST_DATA_ZERO_INIT static float motorFrequencyHz[MAX_SUPPORTED_MOTORS]; FAST_DATA_ZERO_INIT static float minMotorFrequencyHz; FAST_DATA_ZERO_INIT static float erpmToHz; FAST_DATA_ZERO_INIT static float dshotRpmAverage; FAST_DATA_ZERO_INIT static float dshotRpm[MAX_SUPPORTED_MOTORS]; FAST_DATA_ZERO_INIT static bool edtAlwaysDecode; // Lookup table for extended telemetry type decoding // Only contains extended telemetry types, eRPM is handled by conditional logic static const dshotTelemetryType_t extendedTelemetryLookup[8] = { DSHOT_TELEMETRY_TYPE_eRPM, // Temperature range (in degree Celsius, just like Blheli_32 and KISS) DSHOT_TELEMETRY_TYPE_TEMPERATURE, // Voltage range (0-63,75V step 0,25V) DSHOT_TELEMETRY_TYPE_VOLTAGE, // Current range (0-255A step 1A) DSHOT_TELEMETRY_TYPE_CURRENT, // Debug 1 value DSHOT_TELEMETRY_TYPE_DEBUG1, // Debug 2 value DSHOT_TELEMETRY_TYPE_DEBUG2, // Debug 3 value DSHOT_TELEMETRY_TYPE_DEBUG3, // State / events DSHOT_TELEMETRY_TYPE_STATE_EVENTS, }; void initDshotTelemetry(const timeUs_t looptimeUs) { // if bidirectional DShot is not available if (!motorConfig()->dev.useDshotTelemetry && !featureIsEnabled(FEATURE_ESC_SENSOR)) { return; } // erpmToHz is used by bidir dshot and ESC telemetry erpmToHz = ERPM_PER_LSB / SECONDS_PER_MINUTE / (motorConfig()->motorPoleCount / 2.0f); edtAlwaysDecode = motorConfig()->dev.useDshotEdt == DSHOT_EDT_FORCE; #ifdef USE_RPM_FILTER if (motorConfig()->dev.useDshotTelemetry) { // init LPFs for RPM data for (unsigned i = 0; i < dshotMotorCount; i++) { pt1FilterInit(&motorFreqLpf[i], pt1FilterGain(rpmFilterConfig()->rpm_filter_lpf_hz, looptimeUs * 1e-6f)); } } #else UNUSED(looptimeUs); #endif } static uint32_t dshot_decode_eRPM_telemetry_value(uint16_t value) { // eRPM range if (value == 0x0fff) { return 0; } // Convert value to 16 bit from the GCR telemetry format (eeem mmmm mmmm) value = (value & 0x01ff) << ((value & 0xfe00) >> 9); if (!value) { return DSHOT_TELEMETRY_INVALID; } // Convert period to erpm * 100 return (1000000 * 60 / 100 + value / 2) / value; } static void dshot_decode_telemetry_value(uint8_t motorIndex, uint32_t *pDecoded, dshotTelemetryType_t *pType) { uint16_t value = dshotTelemetryState.motorState[motorIndex].rawValue; bool isEdtEnabled = edtAlwaysDecode || (dshotTelemetryState.motorState[motorIndex].telemetryTypes & DSHOT_EXTENDED_TELEMETRY_MASK) != 0; // https://github.com/bird-sanctuary/extended-dshot-telemetry // Extract telemetry type field and check for eRPM conditions in one operation unsigned telemetryType = (value & 0x0f00) >> 8; // 3 bits type + telemetry marker bool isErpm = !isEdtEnabled || (telemetryType & 0x01) || (telemetryType == 0); if (isErpm) { *pDecoded = dshot_decode_eRPM_telemetry_value(value); *pType = DSHOT_TELEMETRY_TYPE_eRPM; // Update debug buffer if (motorIndex < dshotMotorCount && motorIndex < DEBUG16_VALUE_COUNT) { DEBUG_SET(DEBUG_DSHOT_RPM_TELEMETRY, motorIndex, *pDecoded); } } else { // Use lookup table for extended telemetry types unsigned typeIndex = telemetryType >> 1; // drop tag bit containing zero if (typeIndex < ARRAYLEN(extendedTelemetryLookup)) { *pType = extendedTelemetryLookup[typeIndex]; } // Extract data field *pDecoded = value & 0x00ff; } } static void dshotUpdateTelemetryData(uint8_t motorIndex, dshotTelemetryType_t type, uint32_t value) { // Update telemetry data dshotTelemetryState.motorState[motorIndex].telemetryData[type] = value; dshotTelemetryState.motorState[motorIndex].telemetryTypes |= (1 << type); // Update max temp if ((type == DSHOT_TELEMETRY_TYPE_TEMPERATURE) && (value > dshotTelemetryState.motorState[motorIndex].maxTemp)) { dshotTelemetryState.motorState[motorIndex].maxTemp = value; } } FAST_CODE_NOINLINE void updateDshotTelemetry(void) { if (!useDshotTelemetry) { return; } // Only process telemetry in case it hasn“t been processed yet if (dshotTelemetryState.rawValueState != DSHOT_RAW_VALUE_STATE_NOT_PROCESSED) { return; } const unsigned motorCount = MIN(MAX_SUPPORTED_MOTORS, dshotMotorCount); uint32_t erpmTotal = 0; uint32_t rpmSamples = 0; // Decode all telemetry data now to discharge interrupt from this task for (uint8_t k = 0; k < motorCount; k++) { dshotTelemetryType_t type; uint32_t value; dshot_decode_telemetry_value(k, &value, &type); if (value != DSHOT_TELEMETRY_INVALID) { dshotUpdateTelemetryData(k, type, value); if (type == DSHOT_TELEMETRY_TYPE_eRPM) { dshotRpm[k] = erpmToRpm(value); erpmTotal += value; rpmSamples++; } } } // Update average if (rpmSamples > 0) { dshotRpmAverage = erpmToRpm(erpmTotal) / (float)rpmSamples; } // update filtered rotation speed of motors for features (e.g. "RPM filter") minMotorFrequencyHz = FLT_MAX; for (unsigned motor = 0; motor < dshotMotorCount; motor++) { motorFrequencyHz[motor] = pt1FilterApply(&motorFreqLpf[motor], erpmToHz * getDshotErpm(motor)); minMotorFrequencyHz = MIN(minMotorFrequencyHz, motorFrequencyHz[motor]); } // Set state to processed dshotTelemetryState.rawValueState = DSHOT_RAW_VALUE_STATE_PROCESSED; } uint16_t getDshotErpm(uint8_t motorIndex) { return dshotTelemetryState.motorState[motorIndex].telemetryData[DSHOT_TELEMETRY_TYPE_eRPM]; } float getDshotRpm(uint8_t motorIndex) { return dshotRpm[motorIndex]; } float getDshotRpmAverage(void) { return dshotRpmAverage; } float getMotorFrequencyHz(uint8_t motorIndex) { return motorFrequencyHz[motorIndex]; } float getMinMotorFrequencyHz(void) { return minMotorFrequencyHz; } bool isDshotMotorTelemetryActive(uint8_t motorIndex) { return (dshotTelemetryState.motorState[motorIndex].telemetryTypes & (1 << DSHOT_TELEMETRY_TYPE_eRPM)) != 0; } bool isDshotTelemetryActive(void) { const unsigned motorCount = dshotMotorCount; if (motorCount) { for (unsigned i = 0; i < motorCount; i++) { if (!isDshotMotorTelemetryActive(i)) { return false; } } return true; } return false; } void dshotCleanTelemetryData(void) { memset(&dshotTelemetryState, 0, sizeof(dshotTelemetryState)); } bool getDshotSensorData(escSensorData_t *dest, int motorIndex) { // Check if DShot telemetry is active for this motor if (!isDshotMotorTelemetryActive(motorIndex)) { return false; } const dshotTelemetryMotorState_t *motorState = &dshotTelemetryState.motorState[motorIndex]; dest->rpm = motorState->telemetryData[DSHOT_TELEMETRY_TYPE_eRPM]; const bool edt = (motorState->telemetryTypes & DSHOT_EXTENDED_TELEMETRY_MASK) != 0; // Extract telemetry data if available dest->temperature = edt && (motorState->telemetryTypes & (1 << DSHOT_TELEMETRY_TYPE_TEMPERATURE)) ? motorState->telemetryData[DSHOT_TELEMETRY_TYPE_TEMPERATURE] : 0; dest->current = edt && (motorState->telemetryTypes & (1 << DSHOT_TELEMETRY_TYPE_CURRENT)) ? motorState->telemetryData[DSHOT_TELEMETRY_TYPE_CURRENT] : 0; return true; } #endif // USE_DSHOT_TELEMETRY #if defined(USE_ESC_SENSOR) || defined(USE_DSHOT_TELEMETRY) // Used with serial esc telem as well as dshot telem float erpmToRpm(uint32_t erpm) { // rpm = (erpm * ERPM_PER_LSB) / (motorConfig()->motorPoleCount / 2) return erpm * erpmToHz * SECONDS_PER_MINUTE; } #endif // USE_ESC_SENSOR || USE_DSHOT_TELEMETRY #ifdef USE_DSHOT_TELEMETRY_STATS FAST_DATA_ZERO_INIT dshotTelemetryQuality_t dshotTelemetryQuality[MAX_SUPPORTED_MOTORS]; int16_t getDshotTelemetryMotorInvalidPercent(uint8_t motorIndex) { int16_t invalidPercent = 0; if (isDshotMotorTelemetryActive(motorIndex)) { const uint32_t totalCount = dshotTelemetryQuality[motorIndex].packetCountSum; const uint32_t invalidCount = dshotTelemetryQuality[motorIndex].invalidCountSum; if (totalCount > 0) { invalidPercent = lrintf(invalidCount * 10000.0f / totalCount); } } else { invalidPercent = 10000; // 100.00% } return invalidPercent; } void updateDshotTelemetryQuality(dshotTelemetryQuality_t *qualityStats, bool packetValid, timeMs_t currentTimeMs) { uint8_t statsBucketIndex = (currentTimeMs / DSHOT_TELEMETRY_QUALITY_BUCKET_MS) % DSHOT_TELEMETRY_QUALITY_BUCKET_COUNT; if (statsBucketIndex != qualityStats->lastBucketIndex) { qualityStats->packetCountSum -= qualityStats->packetCountArray[statsBucketIndex]; qualityStats->invalidCountSum -= qualityStats->invalidCountArray[statsBucketIndex]; qualityStats->packetCountArray[statsBucketIndex] = 0; qualityStats->invalidCountArray[statsBucketIndex] = 0; qualityStats->lastBucketIndex = statsBucketIndex; } qualityStats->packetCountSum++; qualityStats->packetCountArray[statsBucketIndex]++; if (!packetValid) { qualityStats->invalidCountSum++; qualityStats->invalidCountArray[statsBucketIndex]++; } } #endif // USE_DSHOT_TELEMETRY_STATS #endif // USE_DSHOT // temporarily here, needs to be moved during refactoring void validateAndfixMotorOutputReordering(uint8_t *array, const unsigned size) { bool invalid = false; for (unsigned i = 0; i < size; i++) { if (array[i] >= size) { invalid = true; break; } } int valuesAsIndexes[size]; for (unsigned i = 0; i < size; i++) { valuesAsIndexes[i] = -1; } if (!invalid) { for (unsigned i = 0; i < size; i++) { if (-1 != valuesAsIndexes[array[i]]) { invalid = true; break; } valuesAsIndexes[array[i]] = array[i]; } } if (invalid) { for (unsigned i = 0; i < size; i++) { array[i] = i; } } }