From 0e1e0b954dc067eafbff333e5bf1e8615b4e8747 Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Sat, 29 Nov 2025 00:21:50 +0100 Subject: [PATCH] add extended telemetrics --- README.md | 26 ++--- examples/dshot300/dshot300.ino | 8 +- src/DShotRMT.cpp | 189 ++++++++++++++++++++++++++++++--- src/DShotRMT.h | 31 +++--- src/dshot_definitions.h | 38 +++++-- src/dshot_init.cpp | 4 +- src/dshot_utils.h | 51 ++++++++- 7 files changed, 288 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 116a117..952d0df 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ [![Arduino Library](https://img.shields.io/badge/Arduino-Library-blue.svg)](https://www.arduinolibraries.com/libraries/dshot-rmt) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -An Arduino IDE library for generating DShot signals on ESP32 microcontrollers using the **modern ESP-IDF 5 RMT Encoder API** (`rmt_tx.h` / `rmt_rx.h`). This library specifically leverages the official `rmt_bytes_encoder` API for an efficient, hardware-timed and maintainable implementation. It provides a simple way to control BLHeli ESCs in both Arduino and ESP-IDF projects. +An Arduino IDE library for generating DShot signals on ESP32 microcontrollers using the **latest ESP-IDF 5.5 RMT Encoder API** (`rmt_tx.h` / `rmt_rx.h`). This library specifically leverages the official `rmt_bytes_encoder` API for an efficient, hardware-timed and maintainable implementation. It provides a simple way to control BLHeli ESCs in both Arduino and ESP-IDF projects. -### Bidirectional DShot re-enabled for testing. +### Enhanced Bidirectional DShot with Full Telemetry Support. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. @@ -14,17 +14,17 @@ An Arduino IDE library for generating DShot signals on ESP32 microcontrollers us ### DShot300 Example Output -Here's an example of the output from the `dshot300` example sketch: +Here's an example of the output from the `dshot300` example sketch, now showing full telemetry: ![DShot300 Example Output](img/dshot300.png) ## 🚀 Core Features - **Multiple DShot Modes:** Supports DSHOT150, DSHOT300, DSHOT600, and DSHOT1200. -- **Bidirectional DShot Support:** Implemented, but note that official support is limited due to potential instability and external hardware requirements. Use with caution (and pull-up). +- **Robust Bidirectional DShot Support:** Now features full GCR-dekodierte telemetry data (temperature, voltage, current, consumption, and RPM) from the ESC. The library automatically differentiates between eRPM-only and full telemetry frames. This significantly enhances feedback capabilities for advanced applications. - **Hardware-Timed Signals:** Precise signal generation using the ESP32 RMT peripheral, ensuring stable and reliable motor control. - **Simple API:** Easy-to-use C++ class with intuitive methods like `sendThrottlePercent()`. -- **Error Handling:** Provides detailed feedback on operation success or failure via `dshot_result_t`. +- **Enhanced Error Handling:** Provides detailed feedback on operation success or failure via an enhanced `dshot_result_t` struct, now including specific error codes, eRPM data, and a `dshot_telemetry_data_t` struct for full GCR-decoded telemetry. - **Lightweight:** The core library has no external dependencies. - **Arduino and ESP-IDF Compatible:** Can be used in both Arduino and ESP-IDF projects. @@ -34,7 +34,7 @@ The library is architected around a single C++ class, `DShotRMT`. It abstracts t 1. **Signal Generation (TX):** The library uses an RMT 'bytes_encoder'. This encoder is configured with the specific pulse durations for DShot '0' and '1' bits based on the selected speed (e.g., DSHOT300, DSHOT600). When a user calls `sendThrottle()`, the library constructs a 16-bit DShot frame (11-bit throttle, 1-bit telemetry request, 4-bit CRC) and hands it to the RMT encoder. The RMT hardware then autonomously generates the correct electrical signal on the specified GPIO pin. -2. **Bidirectional Telemetry (RX):** For bidirectional communication, the library configures a second RMT channel in receive mode on the same GPIO. An interrupt service routine (`_on_rx_done`) is registered. When the ESC sends back a telemetry signal, the RMT peripheral captures it and triggers the interrupt. The interrupt code decodes the GCR-encoded signal, validates its CRC, and stores the resulting eRPM value in a thread-safe `atomic` variable. The main application can then poll for this data using the `getTelemetry()` method. +2. **Bidirectional Telemetry (RX) - Now with Full GCR Telemetry:** For bidirectional communication, the library configures a second RMT channel in receive mode on the same GPIO. An interrupt service routine (`_on_rx_done`) is registered. When the ESC sends back a telemetry signal, the RMT peripheral captures it. The interrupt code intelligently differentiates between eRPM-only frames (21 GCR bits) and full telemetry frames (110 GCR bits). It then decodes the GCR-encoded signal (including 5B/4B GCR decoding for full telemetry), validates its CRC, and stores the resulting eRPM value or full telemetry data (temperature, voltage, current, consumption, RPM) in thread-safe `atomic` variables. The main application can then poll for this data using the `getTelemetry()` method, which now returns a comprehensive `dshot_result_t` with all available telemetry fields. ## ⏱️ DShot Timing Information @@ -66,8 +66,8 @@ Here's a basic example of how to use the `DShotRMT` library to control a motor. // Define the GPIO pin connected to the motor ESC const gpio_num_t MOTOR_PIN = GPIO_NUM_27; -// Create a DShotRMT instance for DSHOT300 -DShotRMT motor(MOTOR_PIN, DSHOT300); +// Create a DShotRMT instance for DSHOT300 with bidirectional telemetry enabled +DShotRMT motor(MOTOR_PIN, DSHOT300, true); void setup() { Serial.begin(115200); @@ -91,7 +91,7 @@ void loop() { Serial.println("Stopping motor."); motor.sendThrottlePercent(0); - // Print DShot Info + // Print DShot Info, which now includes detailed telemetry printDShotInfo(motor, Serial); // Take a break before next bench run @@ -104,7 +104,7 @@ void loop() { The `examples` folder contains more advanced examples: - **`throttle_percent`:** A focused example showing how to control motor speed using percentage values (0-100) via the serial monitor. -- **`dshot300`:** A more advanced example demonstrating how to send raw DShot commands and receive telemetry via the serial monitor. +- **`dshot300`:** A more advanced example demonstrating how to send raw DShot commands and **receive comprehensive telemetry** via the serial monitor. - **`web_control`:** A full-featured web application for controlling a motor from a web browser. It creates a WiFi access point and serves a web page with a throttle slider and arming switch. - **`web_client`:** A variation of the `web_control` example that connects to an existing WiFi network instead of creating its own access point. @@ -128,7 +128,7 @@ The main class is `DShotRMT`. Here are the most important methods: - `sendCommand(dshotCommands_e command, uint16_t repeat_count, uint16_t delay_us)`: Sends a DShot command to the ESC with a specified repeat count and delay. This is a blocking function. - `sendCommand(uint16_t command_value)`: Sends a DShot command to the ESC by accepting an integer value. It validates the input and then calls `sendCommand(dshotCommands_e command)`. - `sendCustomCommand(uint16_t command_value, uint16_t repeat_count, uint16_t delay_us)`: Sends a custom DShot command to the ESC. Advanced feature, use with caution. -- `getTelemetry()`: Retrieves telemetry data from the ESC. If bidirectional DShot is enabled, this function will return the last received telemetry data. +- `getTelemetry()`: Retrieves telemetry data from the ESC. If bidirectional DShot is enabled, this function now returns a comprehensive `dshot_result_t` containing both eRPM and a fully GCR-decoded `dshot_telemetry_data_t` struct (temperature, voltage, current, consumption, RPM) if available. - `setMotorSpinDirection(bool reversed)`: Sets the motor spin direction. `true` for reversed, `false` for normal. - `saveESCSettings()`: Sends a command to the ESC to save its current settings. Use with caution as this writes to ESC's non-volatile memory. - `getMode()`: Gets the current DShot mode. @@ -138,7 +138,7 @@ The main class is `DShotRMT`. Here are the most important methods: ## ⚙️ ESP-IDF Integration -This library is built upon the ESP-IDF framework, specifically leveraging its RMT (Remote Control Peripheral) module for precise signal generation. For detailed information on the underlying ESP-IDF components and their usage, please refer to the official ESP-IDF documentation: +This library is built upon the ESP-IDF framework, specifically leveraging its RMT (Remote Control Peripheral) module for precise signal generation. The library is tested with **ESP-IDF v5.5.1** and makes extensive use of its modern RMT APIs. For detailed information on the underlying ESP-IDF components and their usage, please refer to the official ESP-IDF documentation: * [ESP-IDF v5.5.1 Documentation](https://docs.espressif.com/projects/esp-idf/en/v5.5.1/) @@ -150,4 +150,4 @@ Contributions are welcome! Please fork the repository, create a feature branch, ## 📄 License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index 6e24e0b..cf6c763 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -21,7 +21,7 @@ static constexpr dshot_mode_t DSHOT_MODE = DSHOT300; // BiDirectional DShot Support (default: false) // re-enabled for testing -static constexpr auto IS_BIDIRECTIONAL = false; +static constexpr auto IS_BIDIRECTIONAL = true; // Motor magnet count for RPM calculation // static constexpr auto MOTOR01_MAGNET_COUNT = 14; @@ -80,13 +80,11 @@ void loop() { printDShotInfo(motor01, USB_SERIAL); - USB_SERIAL.println(" "); - // Get Motor RPM if bidirectional if (IS_BIDIRECTIONAL) { - dshot_result_t telem_result = motor01.getTelemetry(); - printDShotResult(telem_result); + // dshot_result_t telem_result = motor01.getTelemetry(); + // printDShotResult(telem_result); } USB_SERIAL.println("Type 'help' to show Menu"); diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 657af05..2fcbde5 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -7,13 +7,14 @@ */ #include "DShotRMT.h" +#include // Configuration Constants static constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000; static constexpr auto DSHOT_FULL_PACKET = 0b1111111111111111; static constexpr auto DSHOT_RX_TIMEOUT_MS = 2; static constexpr auto DSHOT_PADDING_US = 20; // Pause between frames -static constexpr auto GCR_BITS_PER_FRAME = 21; // GCR bits in a DShot answer frame + static constexpr auto POLE_PAIRS_MIN = 1; static constexpr auto MAGNETS_PER_POLE_PAIR = 2; static constexpr auto NO_DSHOT_TELEMETRY = 0; @@ -91,7 +92,7 @@ dshot_result_t DShotRMT::sendThrottle(uint16_t throttle) { // A throttle value of 0 is a disarm command if (throttle == 0) - { + { // just to be sure _last_throttle = 0; return sendCommand(DSHOT_CMD_MOTOR_STOP); @@ -346,6 +347,51 @@ uint16_t DShotRMT::_calculateCRC(const uint16_t &data) const return crc; } +uint8_t DShotRMT::_calculateTelemetryCRC(const uint8_t *data, size_t len) const +{ + uint8_t crc = 0; + for (size_t i = 0; i < len; ++i) + { + crc ^= data[i]; + for (uint8_t j = 0; j < 8; ++j) + { + if (crc & 0x80) + { + crc = (crc << 1) ^ 0x07; // DSHOT telemetry uses CRC-8 with polynomial 0x07 + } + else + { + crc <<= 1; + } + } + } + return crc; +} + +void DShotRMT::_extractTelemetryData(const uint8_t *raw_telemetry_bytes, dshot_telemetry_data_t &telemetry_data) const +{ + // Ensure the telemetry_data struct is cleared before filling + memset(&telemetry_data, 0, sizeof(dshot_telemetry_data_t)); + + // Telemetry data is typically ordered as: + // Byte 0: Temperature (signed 8-bit) + // Byte 1-2: Voltage (16-bit, MSB first) + // Byte 3-4: Current (16-bit, MSB first) + // Byte 5-6: Consumption (16-bit, MSB first) + // Byte 7-8: RPM (16-bit, MSB first) + // Byte 9: CRC (8-bit) - checked separately + + telemetry_data.temperature = static_cast(raw_telemetry_bytes[0]); + telemetry_data.voltage = (static_cast(raw_telemetry_bytes[1]) << 8) | raw_telemetry_bytes[2]; + telemetry_data.current = (static_cast(raw_telemetry_bytes[3]) << 8) | raw_telemetry_bytes[4]; + telemetry_data.consumption = (static_cast(raw_telemetry_bytes[5]) << 8) | raw_telemetry_bytes[6]; + telemetry_data.rpm = (static_cast(raw_telemetry_bytes[7]) << 8) | raw_telemetry_bytes[8]; + + // Error flags/count can be derived from other parts of the telemetry data if available, + // or set based on CRC check result. For now, we leave telemetry_data.errors to 0 + // or handle it implicitly through the success/failure of the CRC check. +} + void DShotRMT::_preCalculateRMTTicks() { // Pre-calculate all timing values in RMT ticks to save CPU cycles later. @@ -378,8 +424,8 @@ dshot_result_t DShotRMT::_sendPacket(const dshot_packet_t &packet) if (_is_bidirectional) { // Start the receiver to wait for incoming telemetry data - rmt_symbol_word_t rx_symbols[GCR_BITS_PER_FRAME]; - size_t rx_size_bytes = GCR_BITS_PER_FRAME * sizeof(rmt_symbol_word_t); + rmt_symbol_word_t rx_symbols[DSHOT_TELEMETRY_FULL_GCR_BITS]; + size_t rx_size_bytes = DSHOT_TELEMETRY_FULL_GCR_BITS * sizeof(rmt_symbol_word_t); rmt_receive_config_t rmt_rx_config = { .signal_range_min_ns = DSHOT_PULSE_MIN_NS, @@ -401,7 +447,7 @@ dshot_result_t DShotRMT::_sendPacket(const dshot_packet_t &packet) // The DShot frame is 16 bits, which is 2 bytes size_t tx_size_bytes = sizeof(swapped_value); - rmt_transmit_config_t tx_config = { .loop_count = 0 }; // No automatic loops - real-time calculation + rmt_transmit_config_t tx_config = {.loop_count = 0}; // No automatic loops - real-time calculation // In bidirectional mode, the RMT RX channel must be disabled before transmitting. // This is to prevent the receiver from picking up the transmitted signal, which would cause a loopback issue. @@ -442,7 +488,7 @@ uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) // Decode RMT symbols into a 21-bit GCR (Group Code Recording) value. // The ESC sends back a signal where the duration determines the bit value. - for (size_t i = 0; i < GCR_BITS_PER_FRAME; ++i) + for (size_t i = 0; i < DSHOT_ERPM_FRAME_GCR_BITS; ++i) { bool bit_is_one = symbols[i].duration0 > symbols[i].duration1; gcr_value = (gcr_value << 1) | bit_is_one; @@ -491,20 +537,137 @@ void DShotRMT::_recordFrameTransmissionTime() } // Static Callback Functions +// Processes a full telemetry frame +void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *symbols, size_t num_symbols) +{ + if (num_symbols != DSHOT_TELEMETRY_FULL_GCR_BITS) + { + return; // Incorrect number of symbols for full telemetry + } + + uint8_t gcr_decoded_bytes[DSHOT_TELEMETRY_FRAME_LENGTH_BYTES + 1]; // 10 data bytes + 1 CRC byte + memset(gcr_decoded_bytes, 0, sizeof(gcr_decoded_bytes)); + + uint8_t data_bit_idx = 0; + for (size_t i = 0; i < DSHOT_TELEMETRY_FULL_GCR_BITS; i += 5) + { + uint8_t gcr_group_5bits = 0; + for (size_t j = 0; j < 5; ++j) + { + if (i + j < DSHOT_TELEMETRY_FULL_GCR_BITS) + { + gcr_group_5bits = (gcr_group_5bits << 1) | ((symbols[i + j].duration0 > symbols[i + j].duration1) ? 1 : 0); + } + } + + uint8_t decoded_nibble; // 4 data bits + switch (gcr_group_5bits) + { + case 0b11110: + decoded_nibble = 0b0000; + break; + case 0b01001: + decoded_nibble = 0b0001; + break; + case 0b10100: + decoded_nibble = 0b0010; + break; + case 0b10101: + decoded_nibble = 0b0011; + break; + case 0b01010: + decoded_nibble = 0b0100; + break; + case 0b01011: + decoded_nibble = 0b0101; + break; + case 0b01110: + decoded_nibble = 0b0110; + break; + case 0b01111: + decoded_nibble = 0b0111; + break; + case 0b10010: + decoded_nibble = 0b1000; + break; + case 0b10011: + decoded_nibble = 0b1001; + break; + case 0b10110: + decoded_nibble = 0b1010; + break; + case 0b10111: + decoded_nibble = 0b1011; + break; + case 0b11010: + decoded_nibble = 0b1100; + break; + case 0b11011: + decoded_nibble = 0b1101; + break; + case 0b11100: + decoded_nibble = 0b1110; + break; + case 0b11101: + decoded_nibble = 0b1111; + break; + default: + return; // Invalid GCR group, discard frame + } + + // Place the 4 decoded bits into the data_bytes array + for (int k = 3; k >= 0; --k) + { + if (data_bit_idx < (DSHOT_TELEMETRY_FRAME_LENGTH_BITS + DSHOT_TELEMETRY_CRC_LENGTH_BITS)) + { + size_t byte_idx = data_bit_idx / 8; + size_t bit_pos = data_bit_idx % 8; + if (byte_idx < sizeof(gcr_decoded_bytes)) + { + gcr_decoded_bytes[byte_idx] |= ((decoded_nibble >> k) & 1) << (7 - bit_pos); + } + data_bit_idx++; + } + } + } + + // Now gcr_decoded_bytes contains the 10 telemetry bytes + 1 CRC byte. + // Perform CRC validation. + uint8_t received_crc = gcr_decoded_bytes[DSHOT_TELEMETRY_FRAME_LENGTH_BYTES]; + uint8_t calculated_crc = _calculateTelemetryCRC(gcr_decoded_bytes, DSHOT_TELEMETRY_FRAME_LENGTH_BYTES); + + if (received_crc == calculated_crc) + { + dshot_telemetry_data_t telemetry_data; + // Extract from the first 10 bytes (excluding the CRC byte) + _extractTelemetryData(gcr_decoded_bytes, telemetry_data); + + _last_telemetry_data_atomic.store(telemetry_data); + _full_telemetry_ready_flag_atomic.store(true); + } +} + // This function is called by the RMT driver's ISR when a frame is received bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data) { DShotRMT *instance = static_cast(user_data); - if (edata && edata->num_symbols == GCR_BITS_PER_FRAME) + if (edata) { - uint16_t erpm = instance->_decodeDShotFrame(edata->received_symbols); - - if (erpm != DSHOT_NULL_PACKET) + if (edata->num_symbols == DSHOT_TELEMETRY_FULL_GCR_BITS) { - // Atomically store the new eRPM value and set the flag - instance->_last_erpm_atomic.store(erpm); - instance->_telemetry_ready_flag_atomic.store(true); + instance->_processFullTelemetryFrame(edata->received_symbols, edata->num_symbols); + } + else if (edata->num_symbols == DSHOT_ERPM_FRAME_GCR_BITS) + { + uint16_t erpm = instance->_decodeDShotFrame(edata->received_symbols); + + if (erpm != DSHOT_NULL_PACKET) + { + // Atomically store the new eRPM value and set the flag + instance->_last_erpm_atomic.store(erpm); + instance->_telemetry_ready_flag_atomic.store(true); + } } } diff --git a/src/DShotRMT.h b/src/DShotRMT.h index 072d897..79c072e 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -109,29 +109,32 @@ private: uint64_t _last_command_timestamp = 0; // Timestamp of the last command sent // Telemetry Related Variables - std::atomic _last_erpm_atomic = 0; // Atomically stored last received eRPM value - std::atomic _telemetry_ready_flag_atomic = false; // Atomically stored flag indicating new telemetry data + std::atomic _last_erpm_atomic = 0; // Atomically stored last received eRPM value + std::atomic _telemetry_ready_flag_atomic = false; // Atomically stored flag indicating new telemetry data + std::atomic _last_telemetry_data_atomic = {}; // Atomically stored last received full telemetry data + std::atomic _full_telemetry_ready_flag_atomic = false; // Atomically stored flag indicating new full telemetry data rmt_rx_event_callbacks_t _rx_event_callbacks = { // RMT receive event callbacks .on_recv_done = _on_rx_done, }; // Private Helper Functions for DShot Protocol Logic - bool _isValidCommand(dshotCommands_e command) const; // Checks if a given DShot command is valid - dshot_result_t _executeCommand(dshotCommands_e command); // Executes a single DShot command - dshot_packet_t _buildDShotPacket(const uint16_t &value) const; // Builds a DShot packet from a value (throttle or command) - uint16_t _buildDShotFrameValue(const dshot_packet_t &packet) const; // Combines packet data into a 16-bit DShot frame value - uint16_t _calculateCRC(const uint16_t &data) const; // Calculates the 4-bit CRC for a DShot frame - void _preCalculateRMTTicks(); // Pre-calculates RMT timing ticks for the selected DShot mode - dshot_result_t _sendPacket(const dshot_packet_t &packet); // Sends a DShot frame via RMT TX channel - uint16_t IRAM_ATTR _decodeDShotFrame(const rmt_symbol_word_t *symbols) const; // Decodes a received RMT symbol array into an eRPM value - bool IRAM_ATTR _isFrameIntervalElapsed() const; // Checks if enough time has passed since the last frame transmission - void _recordFrameTransmissionTime(); // Records the current time as the last frame transmission time + bool _isValidCommand(dshotCommands_e command) const; // Checks if a given DShot command is valid + dshot_result_t _executeCommand(dshotCommands_e command); // Executes a single DShot command + dshot_packet_t _buildDShotPacket(const uint16_t &value) const; // Builds a DShot packet from a value (throttle or command) + uint16_t _buildDShotFrameValue(const dshot_packet_t &packet) const; // Combines packet data into a 16-bit DShot frame value + uint16_t _calculateCRC(const uint16_t &data) const; // Calculates the 4-bit CRC for a DShot frame + uint8_t _calculateTelemetryCRC(const uint8_t *data, size_t len) const; // Calculates the 8-bit CRC for telemetry data + void _extractTelemetryData(const uint8_t *raw_telemetry_bytes, dshot_telemetry_data_t &telemetry_data) const; // Extracts telemetry data from raw bytes + void _preCalculateRMTTicks(); // Pre-calculates RMT timing ticks for the selected DShot mode + dshot_result_t _sendPacket(const dshot_packet_t &packet); // Sends a DShot frame via RMT TX channel + uint16_t IRAM_ATTR _decodeDShotFrame(const rmt_symbol_word_t *symbols) const; // Decodes a received RMT symbol array into an eRPM value + void IRAM_ATTR _processFullTelemetryFrame(const rmt_symbol_word_t *symbols, size_t num_symbols); // Processes a full telemetry frame + bool IRAM_ATTR _isFrameIntervalElapsed() const; // Checks if enough time has passed since the last frame transmission + void _recordFrameTransmissionTime(); // Records the current time as the last frame transmission time // Static Callback Function for RMT RX Events void _cleanupRmtResources(); }; #include "dshot_utils.h" // Include for helper functions - - diff --git a/src/dshot_definitions.h b/src/dshot_definitions.h index 140c102..d1a9e06 100644 --- a/src/dshot_definitions.h +++ b/src/dshot_definitions.h @@ -12,10 +12,10 @@ #include // DShot protocol definitions -static constexpr uint16_t DSHOT_FRAME_LENGTH = 16; // 11 throttle bits + 1 telemetry bit + 4 CRC bits +static constexpr uint16_t DSHOT_FRAME_LENGTH = 16; // 11 throttle bits + 1 telemetry bit + 4 CRC bits static constexpr uint16_t DSHOT_BITS_PER_FRAME = 16; -static constexpr uint16_t DSHOT_THROTTLE_MAX = 2047; // Maximum throttle value (0-2047) -static constexpr uint16_t DSHOT_THROTTLE_MIN = 48; // Minimum throttle value for motor spin +static constexpr uint16_t DSHOT_THROTTLE_MAX = 2047; // Maximum throttle value (0-2047) +static constexpr uint16_t DSHOT_THROTTLE_MIN = 48; // Minimum throttle value for motor spin static constexpr float DSHOT_PERCENT_MIN = 0.0f; static constexpr float DSHOT_PERCENT_MAX = 100.0f; static constexpr uint16_t DSHOT_CMD_MIN = 0; // Minimum command value @@ -23,6 +23,15 @@ static constexpr uint16_t DSHOT_CMD_MAX = 47; // Maximum command static constexpr uint16_t DSHOT_TELEMETRY_BIT_MASK = 0x0800; // Bit mask for telemetry request bit (11th bit) static constexpr uint16_t DSHOT_CRC_MASK = 0x000F; // Bit mask for CRC bits +// GCR frame definitions +static constexpr uint16_t DSHOT_ERPM_FRAME_GCR_BITS = 21; // GCR bits in a DShot answer frame for eRPM +static constexpr uint16_t DSHOT_TELEMETRY_FULL_GCR_BITS = 110; // GCR bits for a full 10-byte telemetry frame (80 data bits + 8 CRC bits = 88 bits, 88 * 5/4 = 110 GCR bits) + +// Telemetry frame definitions +static constexpr uint16_t DSHOT_TELEMETRY_FRAME_LENGTH_BITS = 80; // 10 bytes * 8 bits/byte +static constexpr uint16_t DSHOT_TELEMETRY_FRAME_LENGTH_BYTES = 10; +static constexpr uint16_t DSHOT_TELEMETRY_CRC_LENGTH_BITS = 8; // 8-bit CRC for telemetry + // Default motor magnet count for RPM calculation static constexpr uint16_t DEFAULT_MOTOR_MAGNET_COUNT = 14; @@ -88,16 +97,30 @@ enum dshot_msg_code_t DSHOT_ENCODING_SUCCESS, DSHOT_TRANSMISSION_SUCCESS, DSHOT_TELEMETRY_SUCCESS, + DSHOT_TELEMETRY_DATA_AVAILABLE, DSHOT_COMMAND_SUCCESS }; +// Structure for decoded DShot telemetry data (from ESC) +typedef struct dshot_telemetry_data +{ + uint16_t rpm; // Motor RPM + uint16_t voltage; // Voltage in mV + uint16_t current; // Current in mA + uint16_t consumption; // Consumption in mAh + int8_t temperature; // Temperature in Celsius + uint8_t errors; // Error flags / count +} dshot_telemetry_data_t; + // Contains the success status, an error code, and optional telemetry data. typedef struct dshot_result { bool success; - dshot_msg_code_t result_code; // Specific error or success code. - uint16_t erpm; // Electrical RPM (eRPM) if telemetry is successful. - uint16_t motor_rpm; // Motor RPM if telemetry is successful and magnet count is known. + dshot_msg_code_t result_code; // Specific error or success code. + uint16_t erpm; // Electrical RPM (eRPM) if telemetry is successful. + uint16_t motor_rpm; // Motor RPM if telemetry is successful and magnet count is known. + dshot_telemetry_data_t telemetry_data; // Full telemetry data if available + bool telemetry_available; // Flag to indicate if telemetry_data is valid } dshot_result_t; // Standard DShot commands by "betaflight" @@ -141,7 +164,8 @@ static constexpr int DSHOT_ERROR = 1; static constexpr auto DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT; static constexpr auto DSHOT_RMT_RESOLUTION = 8000000; // 8 MHz resolution static constexpr auto RMT_TICKS_PER_US = DSHOT_RMT_RESOLUTION / 1000000; // RMT Ticks per microsecond -static constexpr auto RMT_BUFFER_SYMBOLS = 64; +static constexpr auto RMT_TX_BUFFER_SYMBOLS = 64; +static constexpr auto RMT_RX_BUFFER_SYMBOLS = DSHOT_TELEMETRY_FULL_GCR_BITS; static constexpr auto RMT_QUEUE_DEPTH = 1; // Timing parameters for each DShot mode diff --git a/src/dshot_init.cpp b/src/dshot_init.cpp index f11937e..237c621 100644 --- a/src/dshot_init.cpp +++ b/src/dshot_init.cpp @@ -15,7 +15,7 @@ dshot_result_t init_rmt_tx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_ch .gpio_num = gpio, .clk_src = DSHOT_CLOCK_SRC_DEFAULT, .resolution_hz = DSHOT_RMT_RESOLUTION, - .mem_block_symbols = RMT_BUFFER_SYMBOLS, + .mem_block_symbols = RMT_TX_BUFFER_SYMBOLS, .trans_queue_depth = RMT_QUEUE_DEPTH, .flags = { .invert_out = is_bidirectional ? 1 : 0, @@ -44,7 +44,7 @@ dshot_result_t init_rmt_rx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_ch .gpio_num = gpio, .clk_src = DSHOT_CLOCK_SRC_DEFAULT, .resolution_hz = DSHOT_RMT_RESOLUTION, - .mem_block_symbols = RMT_BUFFER_SYMBOLS, + .mem_block_symbols = RMT_RX_BUFFER_SYMBOLS, }; if (rmt_new_rx_channel(&rx_channel_config, out_channel) != DSHOT_OK) diff --git a/src/dshot_utils.h b/src/dshot_utils.h index f69dcbf..c7a8ffc 100644 --- a/src/dshot_utils.h +++ b/src/dshot_utils.h @@ -132,20 +132,61 @@ inline void printDShotResult(dshot_result_t &result, Stream &output = Serial) } // Helper to print DShot signal info -inline void printDShotInfo(const DShotRMT &dshot_rmt, Stream &output = Serial) +inline void printDShotInfo(DShotRMT &dshot_rmt, Stream &output = Serial) { - output.println("\n === DShot Signal Info === "); + output.println("\n=== DShot Info ==="); output.printf("Library Version: %d.%d.%d\n", DSHOTRMT_MAJOR_VERSION, DSHOTRMT_MINOR_VERSION, DSHOTRMT_PATCH_VERSION); - output.printf("Current Mode: %s\n", get_dshot_mode_str(dshot_rmt.getMode())); + output.printf("Mode: %s\n", get_dshot_mode_str(dshot_rmt.getMode())); output.printf("Bidirectional: %s\n", dshot_rmt.isBidirectional() ? "YES" : "NO"); - output.printf("Current Packet: "); + output.printf("Last Throttle: %u\n", dshot_rmt.getThrottleValue()); + output.print("Packet (binary): "); for (int i = DSHOT_BITS_PER_FRAME - 1; i >= 0; --i) { output.print((dshot_rmt.getEncodedFrameValue() >> i) & 1); } + output.println(); - output.printf("\nCurrent Value: %u\n", dshot_rmt.getThrottleValue()); + // --- Telemetry Data --- + if (dshot_rmt.isBidirectional()) + { + dshot_result_t telemetry_result = dshot_rmt.getTelemetry(); + + output.print("Telemetry: "); + if (telemetry_result.success) + { + output.printf("OK (%s)\n", get_result_code_str(telemetry_result.result_code)); + + if (telemetry_result.erpm > 0 || telemetry_result.motor_rpm > 0) + { + output.printf(" eRPM: %u, Motor RPM: %u\n", telemetry_result.erpm, telemetry_result.motor_rpm); + } + + if (telemetry_result.telemetry_available) + { + output.println(" --- Full Telemetry Details ---"); + output.printf(" Temp: %d C | Volt: %.2f V | Curr: %.2f A | Cons: %u mAh\n", + telemetry_result.telemetry_data.temperature, + (float)telemetry_result.telemetry_data.voltage / 1000.0f, // Convert mV to V + (float)telemetry_result.telemetry_data.current / 1000.0f, // Convert mA to A + telemetry_result.telemetry_data.consumption); + output.printf(" Telemetry RPM: %u\n", telemetry_result.telemetry_data.rpm); + } + else + { + output.println(" (Full telemetry not yet available or CRC failed for full frame)"); + } + } + else + { + output.printf("FAILED (%s)\n", get_result_code_str(telemetry_result.result_code)); + } + } + else + { + output.println("Telemetry: Disabled (Bidirectional mode OFF)"); + } + output.println("===========================\n"); // End separator } // Helper to print CPU info