add extended telemetrics

This commit is contained in:
Wastl Kraus 2025-11-29 00:21:50 +01:00
parent f1db875521
commit 0e1e0b954d
7 changed files with 288 additions and 59 deletions

View File

@ -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.
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

View File

@ -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");

View File

@ -7,13 +7,14 @@
*/
#include "DShotRMT.h"
#include <cstring>
// 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<int8_t>(raw_telemetry_bytes[0]);
telemetry_data.voltage = (static_cast<uint16_t>(raw_telemetry_bytes[1]) << 8) | raw_telemetry_bytes[2];
telemetry_data.current = (static_cast<uint16_t>(raw_telemetry_bytes[3]) << 8) | raw_telemetry_bytes[4];
telemetry_data.consumption = (static_cast<uint16_t>(raw_telemetry_bytes[5]) << 8) | raw_telemetry_bytes[6];
telemetry_data.rpm = (static_cast<uint16_t>(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<DShotRMT *>(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);
}
}
}

View File

@ -109,29 +109,32 @@ private:
uint64_t _last_command_timestamp = 0; // Timestamp of the last command sent
// Telemetry Related Variables
std::atomic<uint16_t> _last_erpm_atomic = 0; // Atomically stored last received eRPM value
std::atomic<bool> _telemetry_ready_flag_atomic = false; // Atomically stored flag indicating new telemetry data
std::atomic<uint16_t> _last_erpm_atomic = 0; // Atomically stored last received eRPM value
std::atomic<bool> _telemetry_ready_flag_atomic = false; // Atomically stored flag indicating new telemetry data
std::atomic<dshot_telemetry_data_t> _last_telemetry_data_atomic = {}; // Atomically stored last received full telemetry data
std::atomic<bool> _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

View File

@ -12,10 +12,10 @@
#include <driver/rmt_common.h>
// 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

View File

@ -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)

View File

@ -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