Path: blob/master/libraries/AP_ESC_Telem/AP_ESC_Telem.cpp
9571 views
/*1This program is free software: you can redistribute it and/or modify2it under the terms of the GNU General Public License as published by3the Free Software Foundation, either version 3 of the License, or4(at your option) any later version.56This program is distributed in the hope that it will be useful,7but WITHOUT ANY WARRANTY; without even the implied warranty of8MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the9GNU General Public License for more details.1011You should have received a copy of the GNU General Public License12along with this program. If not, see <http://www.gnu.org/licenses/>.13*/1415#include "AP_ESC_Telem.h"16#include <AP_HAL/AP_HAL.h>17#include <GCS_MAVLink/GCS.h>18#include <AP_Logger/AP_Logger.h>1920#if HAL_WITH_ESC_TELEM2122#include <AP_BoardConfig/AP_BoardConfig.h>23#include <AP_TemperatureSensor/AP_TemperatureSensor_config.h>2425#include <AP_Math/AP_Math.h>2627//#define ESC_TELEM_DEBUG2829#define ESC_RPM_CHECK_TIMEOUT_US 210000UL // timeout for motor running validity3031extern const AP_HAL::HAL& hal;3233// table of user settable parameters34const AP_Param::GroupInfo AP_ESC_Telem::var_info[] = {3536// @Param: _MAV_OFS37// @DisplayName: ESC Telemetry mavlink offset38// @Description: Offset to apply to ESC numbers when reporting as ESC_TELEMETRY packets over MAVLink. This allows high numbered motors to be displayed as low numbered ESCs for convenience on GCS displays. A value of 4 would send ESC on output 5 as ESC number 1 in ESC_TELEMETRY packets39// @Increment: 140// @Range: 0 3141// @User: Standard42AP_GROUPINFO("_MAV_OFS", 1, AP_ESC_Telem, mavlink_offset, 0),4344AP_GROUPEND45};4647AP_ESC_Telem::AP_ESC_Telem()48{49if (_singleton) {50AP_HAL::panic("Too many AP_ESC_Telem instances");51}52_singleton = this;53#if !defined(IOMCU_FW)54AP_Param::setup_object_defaults(this, var_info);55#endif56}5758// return the average motor RPM59float AP_ESC_Telem::get_average_motor_rpm(uint32_t servo_channel_mask) const60{61float rpm_avg = 0.0f;62uint8_t valid_escs = 0;6364// average the rpm of each motor65for (uint8_t i = 0; i < ESC_TELEM_MAX_ESCS; i++) {66if (BIT_IS_SET(servo_channel_mask,i)) {67float rpm;68if (get_rpm(i, rpm)) {69rpm_avg += rpm;70valid_escs++;71}72}73}7475if (valid_escs > 0) {76rpm_avg /= valid_escs;77}7879return rpm_avg;80}8182// return all the motor frequencies in Hz for dynamic filtering83uint8_t AP_ESC_Telem::get_motor_frequencies_hz(uint8_t nfreqs, float* freqs) const84{85uint8_t valid_escs = 0;8687// average the rpm of each motor as reported by BLHeli and convert to Hz88for (uint8_t i = 0; i < ESC_TELEM_MAX_ESCS && valid_escs < nfreqs; i++) {89float rpm;90if (get_rpm(i, rpm)) {91freqs[valid_escs++] = rpm * (1.0f / 60.0f);92} else if (was_rpm_data_ever_reported(_rpm_data[i])) {93// if we have ever received data on an ESC, mark it as valid but with no data94// this prevents large frequency shifts when ESCs disappear95freqs[valid_escs++] = 0.0f;96}97}9899return MIN(valid_escs, nfreqs);100}101102// get mask of ESCs that sent valid telemetry and/or rpm data in the last103// ESC_TELEM_DATA_TIMEOUT_MS/ESC_RPM_DATA_TIMEOUT_US104uint32_t AP_ESC_Telem::get_active_esc_mask() const {105uint32_t ret = 0;106for (uint8_t i = 0; i < ESC_TELEM_MAX_ESCS; i++) {107if (_telem_data[i].last_update_ms == 0 && !was_rpm_data_ever_reported(_rpm_data[i])) {108// have never seen telem from this ESC109continue;110}111if (_telem_data[i].stale() && !_rpm_data[i].data_valid) {112continue;113}114ret |= (1U << i);115}116return ret;117}118119// return an active ESC for the purposes of reporting (e.g. in the OSD)120uint8_t AP_ESC_Telem::get_max_rpm_esc() const121{122uint32_t ret = 0;123float max_rpm = 0;124for (uint8_t i = 0; i < ESC_TELEM_MAX_ESCS; i++) {125if (_telem_data[i].last_update_ms == 0 && !was_rpm_data_ever_reported(_rpm_data[i])) {126// have never seen telem from this ESC127continue;128}129if (_telem_data[i].stale() && !_rpm_data[i].data_valid) {130continue;131}132if (_rpm_data[i].rpm > max_rpm) {133max_rpm = _rpm_data[i].rpm;134ret = i;135}136}137return ret;138}139140// return number of active ESCs present141uint8_t AP_ESC_Telem::get_num_active_escs() const {142uint32_t active = get_active_esc_mask();143return __builtin_popcount(active);144}145146// return the whether all the motors in servo_channel_mask are running147bool AP_ESC_Telem::are_motors_running(uint32_t servo_channel_mask, float min_rpm, float max_rpm) const148{149150for (uint8_t i = 0; i < ESC_TELEM_MAX_ESCS; i++) {151if (BIT_IS_SET(servo_channel_mask, i)) {152const volatile AP_ESC_Telem_Backend::RpmData& rpmdata = _rpm_data[i];153// we choose a relatively strict measure of health so that failsafe actions can rely on the results154if (!rpm_data_within_timeout(rpmdata, ESC_RPM_CHECK_TIMEOUT_US)) {155return false;156}157if (rpmdata.rpm < min_rpm) {158return false;159}160if ((max_rpm > 0) && (rpmdata.rpm > max_rpm)) {161return false;162}163}164}165return true;166}167168// is telemetry active for the provided channel mask169bool AP_ESC_Telem::is_telemetry_active(uint32_t servo_channel_mask) const170{171for (uint8_t i = 0; i < ESC_TELEM_MAX_ESCS; i++) {172if (BIT_IS_SET(servo_channel_mask, i)) {173// no data received174if (get_last_telem_data_ms(i) == 0 && !was_rpm_data_ever_reported(_rpm_data[i])) {175return false;176}177}178}179return true;180}181182// get an individual ESC's slewed rpm if available, returns true on success183bool AP_ESC_Telem::get_rpm(uint8_t esc_index, float& rpm) const184{185if (esc_index >= ESC_TELEM_MAX_ESCS) {186return false;187}188189const volatile AP_ESC_Telem_Backend::RpmData& rpmdata = _rpm_data[esc_index];190191if (is_zero(rpmdata.update_rate_hz)) {192return false;193}194195const uint32_t now = AP_HAL::micros();196if (rpmdata.data_valid) {197const float slew = MIN(1.0f, (now - rpmdata.last_update_us) * rpmdata.update_rate_hz * (1.0f / 1e6f));198rpm = (rpmdata.prev_rpm + (rpmdata.rpm - rpmdata.prev_rpm) * slew);199200#if AP_SCRIPTING_ENABLED201if ((1U<<esc_index) & rpm_scale_mask) {202rpm *= rpm_scale_factor[esc_index];203}204#endif205206return true;207}208return false;209}210211// get an individual ESC's raw rpm if available, returns true on success212bool AP_ESC_Telem::get_raw_rpm_and_error_rate(uint8_t esc_index, float& rpm, float& error_rate) const213{214if (esc_index >= ESC_TELEM_MAX_ESCS) {215return false;216}217218const volatile AP_ESC_Telem_Backend::RpmData& rpmdata = _rpm_data[esc_index];219220if (!rpmdata.data_valid) {221return false;222}223224rpm = rpmdata.rpm;225error_rate = rpmdata.error_rate;226return true;227}228229// get an individual ESC's temperature in centi-degrees if available, returns true on success230bool AP_ESC_Telem::get_temperature(uint8_t esc_index, int16_t& temp) const231{232if (esc_index >= ESC_TELEM_MAX_ESCS) {233return false;234}235236const volatile AP_ESC_Telem_Backend::TelemetryData& telemdata = _telem_data[esc_index];237if (!telemdata.valid(AP_ESC_Telem_Backend::TelemetryType::TEMPERATURE | AP_ESC_Telem_Backend::TelemetryType::TEMPERATURE_EXTERNAL)) {238return false;239}240temp = telemdata.temperature_cdeg;241return true;242}243244// get an individual motor's temperature in centi-degrees if available, returns true on success245bool AP_ESC_Telem::get_motor_temperature(uint8_t esc_index, int16_t& temp) const246{247if (esc_index >= ESC_TELEM_MAX_ESCS) {248return false;249}250251const volatile AP_ESC_Telem_Backend::TelemetryData& telemdata = _telem_data[esc_index];252if (!telemdata.valid(AP_ESC_Telem_Backend::TelemetryType::MOTOR_TEMPERATURE | AP_ESC_Telem_Backend::TelemetryType::MOTOR_TEMPERATURE_EXTERNAL)) {253return false;254}255temp = telemdata.motor_temp_cdeg;256return true;257}258259// get the highest ESC temperature in centi-degrees if available, returns true if there is valid data for at least one ESC260bool AP_ESC_Telem::get_highest_temperature(int16_t& temp) const261{262uint8_t valid_escs = 0;263264for (uint8_t i = 0; i < ESC_TELEM_MAX_ESCS; i++) {265int16_t temp_temp;266if (get_temperature(i, temp_temp)) {267temp = MAX(temp, temp_temp);268valid_escs++;269}270}271272return valid_escs > 0;273}274275// get an individual ESC's current in Ampere if available, returns true on success276bool AP_ESC_Telem::get_current(uint8_t esc_index, float& amps) const277{278if (esc_index >= ESC_TELEM_MAX_ESCS) {279return false;280}281282const volatile AP_ESC_Telem_Backend::TelemetryData& telemdata = _telem_data[esc_index];283if (!telemdata.valid(AP_ESC_Telem_Backend::TelemetryType::CURRENT)) {284return false;285}286amps = telemdata.current;287return true;288}289290// get an individual ESC's voltage in Volt if available, returns true on success291bool AP_ESC_Telem::get_voltage(uint8_t esc_index, float& volts) const292{293if (esc_index >= ESC_TELEM_MAX_ESCS) {294return false;295}296297const volatile AP_ESC_Telem_Backend::TelemetryData& telemdata = _telem_data[esc_index];298if (!telemdata.valid(AP_ESC_Telem_Backend::TelemetryType::VOLTAGE)) {299return false;300}301volts = telemdata.voltage;302return true;303}304305// get an individual ESC's energy consumption in milli-Ampere.hour if available, returns true on success306bool AP_ESC_Telem::get_consumption_mah(uint8_t esc_index, float& consumption_mah) const307{308if (esc_index >= ESC_TELEM_MAX_ESCS) {309return false;310}311312const volatile AP_ESC_Telem_Backend::TelemetryData& telemdata = _telem_data[esc_index];313if (!telemdata.valid(AP_ESC_Telem_Backend::TelemetryType::CONSUMPTION)) {314return false;315}316consumption_mah = telemdata.consumption_mah;317return true;318}319320// get an individual ESC's usage time in seconds if available, returns true on success321bool AP_ESC_Telem::get_usage_seconds(uint8_t esc_index, uint32_t& usage_s) const322{323if (esc_index >= ESC_TELEM_MAX_ESCS) {324return false;325}326327const volatile AP_ESC_Telem_Backend::TelemetryData& telemdata = _telem_data[esc_index];328if (!telemdata.valid(AP_ESC_Telem_Backend::TelemetryType::USAGE)) {329return false;330}331usage_s = telemdata.usage_s;332return true;333}334335#if AP_EXTENDED_ESC_TELEM_ENABLED336// get an individual ESC's input duty cycle if available, returns true on success337bool AP_ESC_Telem::get_input_duty(uint8_t esc_index, uint8_t& input_duty) const338{339if (esc_index >= ESC_TELEM_MAX_ESCS) {340return false;341}342343const volatile AP_ESC_Telem_Backend::TelemetryData& telemdata = _telem_data[esc_index];344if (!telemdata.valid(AP_ESC_Telem_Backend::TelemetryType::INPUT_DUTY)) {345return false;346}347input_duty = telemdata.input_duty;348return true;349}350351// get an individual ESC's output duty cycle if available, returns true on success352bool AP_ESC_Telem::get_output_duty(uint8_t esc_index, uint8_t& output_duty) const353{354if (esc_index >= ESC_TELEM_MAX_ESCS) {355return false;356}357358const volatile AP_ESC_Telem_Backend::TelemetryData& telemdata = _telem_data[esc_index];359if (!telemdata.valid(AP_ESC_Telem_Backend::TelemetryType::OUTPUT_DUTY)) {360return false;361}362output_duty = telemdata.output_duty;363return true;364}365366// get an individual ESC's status flags if available, returns true on success367bool AP_ESC_Telem::get_flags(uint8_t esc_index, uint32_t& flags) const368{369if (esc_index >= ESC_TELEM_MAX_ESCS) {370return false;371}372373const volatile AP_ESC_Telem_Backend::TelemetryData& telemdata = _telem_data[esc_index];374if (!telemdata.valid(AP_ESC_Telem_Backend::TelemetryType::FLAGS)) {375return false;376}377flags = telemdata.flags;378return true;379}380381// get an individual ESC's percentage of output power if available, returns true on success382bool AP_ESC_Telem::get_power_percentage(uint8_t esc_index, uint8_t& power_percentage) const383{384if (esc_index >= ESC_TELEM_MAX_ESCS) {385return false;386}387388const volatile AP_ESC_Telem_Backend::TelemetryData& telemdata = _telem_data[esc_index];389if (!telemdata.valid(AP_ESC_Telem_Backend::TelemetryType::POWER_PERCENTAGE)) {390return false;391}392power_percentage = telemdata.power_percentage;393return true;394}395#endif // AP_EXTENDED_ESC_TELEM_ENABLED396397// send ESC telemetry messages over MAVLink398void AP_ESC_Telem::send_esc_telemetry_mavlink(uint8_t mav_chan)399{400#if HAL_GCS_ENABLED401if (!_have_data) {402// we've never had any data403return;404}405406// loop through groups of 4 ESCs407const uint8_t esc_offset = constrain_int16(mavlink_offset, 0, ESC_TELEM_MAX_ESCS-1);408409// ensure we send out partially-full groups:410const uint8_t num_idx = (ESC_TELEM_MAX_ESCS + 3) / 4;411412for (uint8_t idx = 0; idx < num_idx; idx++) {413const uint8_t i = (next_idx + idx) % num_idx;414415// return if no space in output buffer to send mavlink messages416if (!HAVE_PAYLOAD_SPACE((mavlink_channel_t)mav_chan, ESC_TELEMETRY_1_TO_4)) {417// not enough mavlink buffer space, start at this index next time418next_idx = i;419return;420}421422bool all_stale = true;423for (uint8_t j=0; j<4; j++) {424const uint8_t esc_id = (i * 4 + j) + esc_offset;425if (esc_id < ESC_TELEM_MAX_ESCS &&426(!_telem_data[esc_id].stale() || _rpm_data[esc_id].data_valid)) {427all_stale = false;428break;429}430}431if (all_stale) {432// skip this group of ESCs if no data to send433continue;434}435436437// arrays to hold output438mavlink_esc_telemetry_1_to_4_t s {};439440// fill in output arrays441for (uint8_t j = 0; j < 4; j++) {442const uint8_t esc_id = (i * 4 + j) + esc_offset;443if (esc_id >= ESC_TELEM_MAX_ESCS) {444continue;445}446volatile AP_ESC_Telem_Backend::TelemetryData const &telemdata = _telem_data[esc_id];447448s.temperature[j] = telemdata.temperature_cdeg / 100;449s.voltage[j] = constrain_float(telemdata.voltage * 100.0f, 0, UINT16_MAX);450s.current[j] = constrain_float(telemdata.current * 100.0f, 0, UINT16_MAX);451s.totalcurrent[j] = constrain_float(telemdata.consumption_mah, 0, UINT16_MAX);452float rpmf;453if (get_rpm(esc_id, rpmf)) {454// rpm can be negative455s.rpm[j] = constrain_float(fabsf(rpmf), 0, UINT16_MAX);456}457s.count[j] = telemdata.count;458}459460// make sure a msg hasn't been extended461static_assert(MAVLINK_MSG_ID_ESC_TELEMETRY_1_TO_4_LEN == MAVLINK_MSG_ID_ESC_TELEMETRY_5_TO_8_LEN &&462MAVLINK_MSG_ID_ESC_TELEMETRY_1_TO_4_LEN == MAVLINK_MSG_ID_ESC_TELEMETRY_9_TO_12_LEN &&463MAVLINK_MSG_ID_ESC_TELEMETRY_1_TO_4_LEN == MAVLINK_MSG_ID_ESC_TELEMETRY_13_TO_16_LEN &&464MAVLINK_MSG_ID_ESC_TELEMETRY_1_TO_4_LEN == MAVLINK_MSG_ID_ESC_TELEMETRY_17_TO_20_LEN &&465MAVLINK_MSG_ID_ESC_TELEMETRY_1_TO_4_LEN == MAVLINK_MSG_ID_ESC_TELEMETRY_21_TO_24_LEN &&466MAVLINK_MSG_ID_ESC_TELEMETRY_1_TO_4_LEN == MAVLINK_MSG_ID_ESC_TELEMETRY_21_TO_24_LEN &&467MAVLINK_MSG_ID_ESC_TELEMETRY_1_TO_4_LEN == MAVLINK_MSG_ID_ESC_TELEMETRY_25_TO_28_LEN &&468MAVLINK_MSG_ID_ESC_TELEMETRY_1_TO_4_LEN == MAVLINK_MSG_ID_ESC_TELEMETRY_29_TO_32_LEN,469"telem messages not compatible");470471const mavlink_channel_t chan = (mavlink_channel_t)mav_chan;472// send messages473switch (i) {474case 0:475mavlink_msg_esc_telemetry_1_to_4_send_struct(chan, &s);476break;477case 1:478mavlink_msg_esc_telemetry_5_to_8_send_struct(chan, (const mavlink_esc_telemetry_5_to_8_t *)&s);479break;480case 2:481mavlink_msg_esc_telemetry_9_to_12_send_struct(chan, (const mavlink_esc_telemetry_9_to_12_t *)&s);482break;483case 3:484mavlink_msg_esc_telemetry_13_to_16_send_struct(chan, (const mavlink_esc_telemetry_13_to_16_t *)&s);485break;486#if ESC_TELEM_MAX_ESCS > 16487case 4:488mavlink_msg_esc_telemetry_17_to_20_send_struct(chan, (const mavlink_esc_telemetry_17_to_20_t *)&s);489break;490case 5:491mavlink_msg_esc_telemetry_21_to_24_send_struct(chan, (const mavlink_esc_telemetry_21_to_24_t *)&s);492break;493case 6:494mavlink_msg_esc_telemetry_25_to_28_send_struct(chan, (const mavlink_esc_telemetry_25_to_28_t *)&s);495break;496case 7:497mavlink_msg_esc_telemetry_29_to_32_send_struct(chan, (const mavlink_esc_telemetry_29_to_32_t *)&s);498break;499#endif500}501}502// we checked for all sends without running out of buffer space,503// start at zero next time504next_idx = 0;505506#endif // HAL_GCS_ENABLED507}508509// record an update to the telemetry data together with timestamp510// this should be called by backends when new telemetry values are available511void AP_ESC_Telem::update_telem_data(const uint8_t esc_index, const AP_ESC_Telem_Backend::TelemetryData& new_data, const uint16_t data_mask)512{513// rpm and telemetry data are not protected by a semaphore even though updated from different threads514// all data is per-ESC and only written from the update thread and read by the user thread515// each element is a primitive type and the timestamp is only updated at the end, thus a caller516// can only get slightly more up-to-date information that perhaps they were expecting or might517// read data that has just gone stale - both of these are safe and avoid the overhead of locking518519if (esc_index >= ESC_TELEM_MAX_ESCS || data_mask == 0) {520return;521}522523_have_data = true;524volatile AP_ESC_Telem_Backend::TelemetryData &telemdata = _telem_data[esc_index];525526#if AP_TEMPERATURE_SENSOR_ENABLED527// always allow external data. Block "internal" if external has ever its ever been set externally then ignore normal "internal" updates528const bool has_temperature = (data_mask & AP_ESC_Telem_Backend::TelemetryType::TEMPERATURE_EXTERNAL) ||529((data_mask & AP_ESC_Telem_Backend::TelemetryType::TEMPERATURE) && !(telemdata.types & AP_ESC_Telem_Backend::TelemetryType::TEMPERATURE_EXTERNAL));530531const bool has_motor_temperature = (data_mask & AP_ESC_Telem_Backend::TelemetryType::MOTOR_TEMPERATURE_EXTERNAL) ||532((data_mask & AP_ESC_Telem_Backend::TelemetryType::MOTOR_TEMPERATURE) && !(telemdata.types & AP_ESC_Telem_Backend::TelemetryType::MOTOR_TEMPERATURE_EXTERNAL));533#else534const bool has_temperature = (data_mask & AP_ESC_Telem_Backend::TelemetryType::TEMPERATURE);535const bool has_motor_temperature = (data_mask & AP_ESC_Telem_Backend::TelemetryType::MOTOR_TEMPERATURE);536#endif537538if (has_temperature) {539telemdata.temperature_cdeg = new_data.temperature_cdeg;540}541if (has_motor_temperature) {542telemdata.motor_temp_cdeg = new_data.motor_temp_cdeg;543}544if (data_mask & AP_ESC_Telem_Backend::TelemetryType::VOLTAGE) {545telemdata.voltage = new_data.voltage;546}547if (data_mask & AP_ESC_Telem_Backend::TelemetryType::CURRENT) {548telemdata.current = new_data.current;549}550if (data_mask & AP_ESC_Telem_Backend::TelemetryType::CONSUMPTION) {551telemdata.consumption_mah = new_data.consumption_mah;552}553if (data_mask & AP_ESC_Telem_Backend::TelemetryType::USAGE) {554telemdata.usage_s = new_data.usage_s;555}556557#if AP_EXTENDED_ESC_TELEM_ENABLED558if (data_mask & AP_ESC_Telem_Backend::TelemetryType::INPUT_DUTY) {559telemdata.input_duty = new_data.input_duty;560}561if (data_mask & AP_ESC_Telem_Backend::TelemetryType::OUTPUT_DUTY) {562telemdata.output_duty = new_data.output_duty;563}564if (data_mask & AP_ESC_Telem_Backend::TelemetryType::FLAGS) {565telemdata.flags = new_data.flags;566}567if (data_mask & AP_ESC_Telem_Backend::TelemetryType::POWER_PERCENTAGE) {568telemdata.power_percentage = new_data.power_percentage;569}570#endif //AP_EXTENDED_ESC_TELEM_ENABLED571572#if AP_EXTENDED_DSHOT_TELEM_V2_ENABLED573if (data_mask & AP_ESC_Telem_Backend::TelemetryType::EDT2_STATUS) {574telemdata.edt2_status = merge_edt2_status(telemdata.edt2_status, new_data.edt2_status);575}576if (data_mask & AP_ESC_Telem_Backend::TelemetryType::EDT2_STRESS) {577telemdata.edt2_stress = merge_edt2_stress(telemdata.edt2_stress, new_data.edt2_stress);578}579#endif580581telemdata.count++;582telemdata.types |= data_mask;583telemdata.last_update_ms = AP_HAL::millis();584telemdata.any_data_valid = true;585}586587// record an update to the RPM together with timestamp, this allows the notch values to be slewed588// this should be called by backends when new telemetry values are available589void AP_ESC_Telem::update_rpm(const uint8_t esc_index, const float new_rpm, const float error_rate)590{591if (esc_index >= ESC_TELEM_MAX_ESCS) {592return;593}594595_have_data = true;596597const uint32_t now = MAX(1U ,AP_HAL::micros()); // don't allow a value of 0 in, as we use this as a flag in places598volatile AP_ESC_Telem_Backend::RpmData& rpmdata = _rpm_data[esc_index];599const auto last_update_us = rpmdata.last_update_us;600601rpmdata.prev_rpm = rpmdata.rpm;602rpmdata.rpm = new_rpm;603rpmdata.update_rate_hz = 1.0e6f / constrain_uint32((now - last_update_us), 100, 1000000U*10U); // limit the update rate 0.1Hz to 10KHz604rpmdata.last_update_us = now;605rpmdata.error_rate = error_rate;606rpmdata.data_valid = true;607608#ifdef ESC_TELEM_DEBUG609hal.console->printf("RPM: rate=%.1fhz, rpm=%f)\n", rpmdata.update_rate_hz, new_rpm);610#endif611}612613#if AP_EXTENDED_DSHOT_TELEM_V2_ENABLED614615// The following is based on https://github.com/bird-sanctuary/extended-dshot-telemetry.616// For the following part we explain the bits of Extended DShot Telemetry v2 status telemetry:617// - bits 0-3: the "stress level"618// - bit 5: the "error" bit (e.g. the stall event in Bluejay)619// - bit 6: the "warning" bit (e.g. the desync event in Bluejay)620// - bit 7: the "alert" bit (e.g. the demag event in Bluejay)621622// Since logger can read out telemetry values less frequently than they are updated,623// it makes sense to aggregate these status bits, and to collect the maximum observed stress level.624// To reduce the logging rate of the EDT2 messages, we will try to log them only once a new frame comes.625// To track this, we are going to (ab)use bit 15 of the field: 1 means there is something to write.626627// EDTv2 also features separate "stress" messages.628// These come more frequently, and are scaled differently (the allowed range is from 0 to 255),629// so we have to log them separately.630631constexpr uint16_t EDT2_TELEM_UPDATED = 0x8000U;632constexpr uint16_t EDT2_STRESS_0F_MASK = 0xfU;633constexpr uint16_t EDT2_STRESS_FF_MASK = 0xffU;634constexpr uint16_t EDT2_ERROR_MASK = 0x20U;635constexpr uint16_t EDT2_WARNING_MASK = 0x40U;636constexpr uint16_t EDT2_ALERT_MASK = 0x80U;637constexpr uint16_t EDT2_ALL_BITS = EDT2_ERROR_MASK | EDT2_WARNING_MASK | EDT2_ALERT_MASK;638639#define EDT2_HAS_NEW_DATA(status) bool((status) & EDT2_TELEM_UPDATED)640#define EDT2_STRESS_FROM_STATUS(status) uint8_t((status) & EDT2_STRESS_0F_MASK)641#define EDT2_ERROR_BIT_FROM_STATUS(status) bool((status) & EDT2_ERROR_MASK)642#define EDT2_WARNING_BIT_FROM_STATUS(status) bool((status) & EDT2_WARNING_MASK)643#define EDT2_ALERT_BIT_FROM_STATUS(status) bool((status) & EDT2_ALERT_MASK)644#define EDT2_STRESS_FROM_STRESS(stress) uint8_t((stress) & EDT2_STRESS_FF_MASK)645646uint16_t AP_ESC_Telem::merge_edt2_status(uint16_t old_status, uint16_t new_status)647{648if (EDT2_HAS_NEW_DATA(old_status)) {649new_status = uint16_t(650(old_status & ~EDT2_STRESS_0F_MASK) | // old status except for the stress is preserved651(new_status & EDT2_ALL_BITS) | // all new status bits are included652MAX(old_status & EDT2_STRESS_0F_MASK, new_status & EDT2_STRESS_0F_MASK) // the stress is maxed out653);654}655return EDT2_TELEM_UPDATED | new_status;656}657658uint16_t AP_ESC_Telem::merge_edt2_stress(uint16_t old_stress, uint16_t new_stress)659{660if (EDT2_HAS_NEW_DATA(old_stress)) {661new_stress = uint16_t(662MAX(old_stress & EDT2_STRESS_FF_MASK, new_stress & EDT2_STRESS_FF_MASK) // the stress is maxed out663);664}665return EDT2_TELEM_UPDATED | new_stress;666}667668#endif // AP_EXTENDED_DSHOT_TELEM_V2_ENABLED669670void AP_ESC_Telem::update()671{672#if HAL_LOGGING_ENABLED673AP_Logger *logger = AP_Logger::get_singleton();674const uint64_t now_us64 = AP_HAL::micros64();675676for (uint8_t i = 0; i < ESC_TELEM_MAX_ESCS; i++) {677const volatile AP_ESC_Telem_Backend::RpmData &rpmdata = _rpm_data[i];678volatile AP_ESC_Telem_Backend::TelemetryData &telemdata = _telem_data[i];679// Push received telemetry data into the logging system680if (logger && logger->logging_enabled()) {681if (telemdata.last_update_ms != _last_telem_log_ms[i]682|| rpmdata.last_update_us != _last_rpm_log_us[i]) {683684// Update last log timestamps685_last_telem_log_ms[i] = telemdata.last_update_ms;686_last_rpm_log_us[i] = rpmdata.last_update_us;687688float rpm = AP_Logger::quiet_nanf();689get_rpm(i, rpm);690float raw_rpm = AP_Logger::quiet_nanf();691float rpm_error_rate = AP_Logger::quiet_nanf();692get_raw_rpm_and_error_rate(i, raw_rpm, rpm_error_rate);693694// Write ESC status messages695// id starts from 0696// rpm, raw_rpm is eRPM (in RPM units)697// voltage is in Volt698// current is in Ampere699// esc_temp is in centi-degrees Celsius700// current_tot is in milli-Ampere hours701// motor_temp is in centi-degrees Celsius702// error_rate is in percentage703const struct log_Esc pkt{704LOG_PACKET_HEADER_INIT(uint8_t(LOG_ESC_MSG)),705time_us : now_us64,706instance : i,707rpm : rpm,708raw_rpm : raw_rpm,709voltage : telemdata.voltage,710current : telemdata.current,711esc_temp : telemdata.temperature_cdeg,712current_tot : telemdata.consumption_mah,713motor_temp : telemdata.motor_temp_cdeg,714error_rate : rpm_error_rate715};716AP::logger().WriteBlock(&pkt, sizeof(pkt));717718#if AP_EXTENDED_ESC_TELEM_ENABLED719// Write ESC extended status messages720// id: starts from 0721// input duty: duty cycle input to the ESC in percent722// output duty: duty cycle output to the motor in percent723// status flags: manufacurer-specific status flags724const bool has_ext_data = telemdata.types &725(AP_ESC_Telem_Backend::TelemetryType::INPUT_DUTY |726AP_ESC_Telem_Backend::TelemetryType::OUTPUT_DUTY |727AP_ESC_Telem_Backend::TelemetryType::FLAGS |728AP_ESC_Telem_Backend::TelemetryType::POWER_PERCENTAGE);729if (has_ext_data) {730// @LoggerMessage: ESCX731// @Description: ESC extended telemetry data732// @Field: TimeUS: Time since system startup733// @Field: Instance: starts from 0734// @Field: inpct: input duty cycle in percent735// @Field: outpct: output duty cycle in percent736// @Field: flags: manufacturer-specific status flags737// @Field: Pwr: Power percentage738AP::logger().WriteStreaming("ESCX",739"TimeUS,Instance,inpct,outpct,flags,Pwr",740"s" "#" "%" "%" "-" "%",741"F" "-" "-" "-" "-" "-",742"Q" "B" "B" "B" "I" "B",743AP_HAL::micros64(),744i,745telemdata.input_duty,746telemdata.output_duty,747telemdata.flags,748telemdata.power_percentage);749}750#endif //AP_EXTENDED_ESC_TELEM_ENABLED751752#if AP_EXTENDED_DSHOT_TELEM_V2_ENABLED753// Write an EDTv2 message, if there is any update754uint16_t edt2_status = telemdata.edt2_status;755uint16_t edt2_stress = telemdata.edt2_stress;756if (EDT2_HAS_NEW_DATA(edt2_status | edt2_stress)) {757// Could probably be faster/smaller with bitmasking, but not sure758uint8_t status = 0;759if (EDT2_HAS_NEW_DATA(edt2_stress)) {760status |= uint8_t(log_Edt2_Status::HAS_STRESS_DATA);761}762if (EDT2_HAS_NEW_DATA(edt2_status)) {763status |= uint8_t(log_Edt2_Status::HAS_STATUS_DATA);764}765if (EDT2_ALERT_BIT_FROM_STATUS(edt2_status)) {766status |= uint8_t(log_Edt2_Status::ALERT_BIT);767}768if (EDT2_WARNING_BIT_FROM_STATUS(edt2_status)) {769status |= uint8_t(log_Edt2_Status::WARNING_BIT);770}771if (EDT2_ERROR_BIT_FROM_STATUS(edt2_status)) {772status |= uint8_t(log_Edt2_Status::ERROR_BIT);773}774// An EDT2 status message is:775// id: starts from 0776// stress: the current stress which comes from edt2_stress777// max_stress: the maximum stress which comes from edt2_status778// status: the status bits which come from both779const struct log_Edt2 pkt_edt2{780LOG_PACKET_HEADER_INIT(uint8_t(LOG_EDT2_MSG)),781time_us : now_us64,782instance : i,783stress : EDT2_STRESS_FROM_STRESS(edt2_stress),784max_stress : EDT2_STRESS_FROM_STATUS(edt2_status),785status : status,786};787if (AP::logger().WriteBlock_first_succeed(&pkt_edt2, sizeof(pkt_edt2))) {788// Only clean the telem_updated bits if the write succeeded.789// This is important because, if rate limiting is enabled,790// the log-on-change behavior may lose a lot of entries791telemdata.edt2_status &= ~EDT2_TELEM_UPDATED;792telemdata.edt2_stress &= ~EDT2_TELEM_UPDATED;793}794}795#endif // AP_EXTENDED_DSHOT_TELEM_V2_ENABLED796}797}798}799#endif // HAL_LOGGING_ENABLED800801for (uint8_t i = 0; i < ESC_TELEM_MAX_ESCS; i++) {802// copy the last_updated_us timestamp to avoid any race issues803const uint32_t last_updated_us = _rpm_data[i].last_update_us;804const uint32_t now_us = AP_HAL::micros();805// Invalidate RPM data if not received for too long806if (AP_HAL::timeout_expired(last_updated_us, now_us, ESC_RPM_DATA_TIMEOUT_US)) {807_rpm_data[i].data_valid = false;808}809const uint32_t last_telem_data_ms = _telem_data[i].last_update_ms;810const uint32_t now_ms = AP_HAL::millis();811// Invalidate telemetry data if not received for too long812if (AP_HAL::timeout_expired(last_telem_data_ms, now_ms, ESC_TELEM_DATA_TIMEOUT_MS)) {813_telem_data[i].any_data_valid = false;814}815}816}817818// NOTE: This function should only be used to check timeouts other than819// ESC_RPM_DATA_TIMEOUT_US. Timeouts equal to ESC_RPM_DATA_TIMEOUT_US should820// use RpmData::data_valid, which is cheaper and achieves the same result.821bool AP_ESC_Telem::rpm_data_within_timeout(const volatile AP_ESC_Telem_Backend::RpmData &instance, const uint32_t timeout_us)822{823// copy the last_update_us timestamp to avoid any race issues824const uint32_t last_update_us = instance.last_update_us;825const uint32_t now_us = AP_HAL::micros();826// easy case, has the time window been crossed so it's invalid827if (AP_HAL::timeout_expired(last_update_us, now_us, timeout_us)) {828return false;829}830// we never got a valid data, to it's invalid831if (last_update_us == 0) {832return false;833}834// check if things generally expired on us, this is done to handle time wrapping835return instance.data_valid;836}837838bool AP_ESC_Telem::was_rpm_data_ever_reported(const volatile AP_ESC_Telem_Backend::RpmData &instance)839{840return instance.last_update_us > 0;841}842843#if AP_SCRIPTING_ENABLED844/*845set RPM scale factor from script846*/847void AP_ESC_Telem::set_rpm_scale(const uint8_t esc_index, const float scale_factor)848{849if (esc_index < ESC_TELEM_MAX_ESCS) {850rpm_scale_factor[esc_index] = scale_factor;851rpm_scale_mask |= (1U<<esc_index);852}853}854#endif855856AP_ESC_Telem *AP_ESC_Telem::_singleton = nullptr;857858/*859* Get the AP_ESC_Telem singleton860*/861AP_ESC_Telem *AP_ESC_Telem::get_singleton()862{863return AP_ESC_Telem::_singleton;864}865866namespace AP {867868AP_ESC_Telem &esc_telem()869{870return *AP_ESC_Telem::get_singleton();871}872873};874875#endif // HAL_WITH_ESC_TELEM876877878