prepare release 0.9.5

prepare release 0.9.5
This commit is contained in:
Wastl Kraus 2025-11-29 15:30:07 +01:00 committed by GitHub
commit 6e80e6ad3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 665 additions and 413 deletions

View File

@ -4,9 +4,13 @@
[![Arduino Library](https://img.shields.io/badge/Arduino-Library-blue.svg)](https://www.arduinolibraries.com/libraries/dshot-rmt) [![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) [![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. ### ✨ Experimental Bidirectional DShot Support Activated! ✨
> [!CAUTION]
> **This feature is currently EXPERIMENTAL and under active development.**
> If you enable bidirectional DShot, you **MUST** connect an external pull-up resistor (e.g., 2k Ohm to 3.3V) to the DShot GPIO pin. This resistor is absolutely crucial for the ESC to properly send telemetry data back to the ESP32. Without it, bidirectional telemetry will **NOT** function correctly. Use at your own risk.
The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch.
@ -14,20 +18,28 @@ An Arduino IDE library for generating DShot signals on ESP32 microcontrollers us
### DShot300 Example Output ### 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) ![DShot300 Example Output](img/dshot300.png)
## 🚀 Core Features ## 🚀 Core Features
- **Multiple DShot Modes:** Supports DSHOT150, DSHOT300, DSHOT600, and DSHOT1200. - **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. - **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()`. - **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. - **Lightweight:** The core library has no external dependencies.
- **Arduino and ESP-IDF Compatible:** Can be used in both Arduino and ESP-IDF projects. - **Arduino and ESP-IDF Compatible:** Can be used in both Arduino and ESP-IDF projects.
## How it Works
The library is architected around a single C++ class, `DShotRMT`. It abstracts the ESP32's RMT (Remote Control) peripheral, which is a hardware timer peripheral capable of generating and receiving precisely timed signals. For a more detailed explanation of the DShot protocol, refer to this excellent article: [DShot and Bidirectional DShot](https://brushlesswhoop.com/dshot-and-bidirectional-dshot/).
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) - Now with Full GCR Telemetry:** **Note: For bidirectional DShot, an external pull-up resistor (e.g., 2k Ohm to 3.3V) is required on the DShot GPIO pin for proper telemetry reception.** 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 ## ⏱️ DShot Timing Information
The DShot protocol defines specific timing characteristics for each mode. The following table outlines the bit length, T1H (high time for a '1' bit), T0H (high time for a '0' bit), and frame length for the supported DShot modes: The DShot protocol defines specific timing characteristics for each mode. The following table outlines the bit length, T1H (high time for a '1' bit), T0H (high time for a '0' bit), and frame length for the supported DShot modes:
@ -58,8 +70,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 // Define the GPIO pin connected to the motor ESC
const gpio_num_t MOTOR_PIN = GPIO_NUM_27; const gpio_num_t MOTOR_PIN = GPIO_NUM_27;
// Create a DShotRMT instance for DSHOT300 // Create a DShotRMT instance for DSHOT300 with bidirectional telemetry enabled
DShotRMT motor(MOTOR_PIN, DSHOT300); DShotRMT motor(MOTOR_PIN, DSHOT300, true);
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
@ -83,7 +95,7 @@ void loop() {
Serial.println("Stopping motor."); Serial.println("Stopping motor.");
motor.sendThrottlePercent(0); motor.sendThrottlePercent(0);
// Print DShot Info // Print DShot Info, which now includes detailed telemetry
printDShotInfo(motor, Serial); printDShotInfo(motor, Serial);
// Take a break before next bench run // Take a break before next bench run
@ -96,7 +108,7 @@ void loop() {
The `examples` folder contains more advanced examples: 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. - **`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_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. - **`web_client`:** A variation of the `web_control` example that connects to an existing WiFi network instead of creating its own access point.
@ -119,22 +131,18 @@ The main class is `DShotRMT`. Here are the most important methods:
- `sendCommand(dshotCommands_e command)`: Sends a DShot command to the ESC. Automatically handles repetitions and delays for specific commands (e.g., `DSHOT_CMD_SAVE_SETTINGS`). - `sendCommand(dshotCommands_e command)`: Sends a DShot command to the ESC. Automatically handles repetitions and delays for specific commands (e.g., `DSHOT_CMD_SAVE_SETTINGS`).
- `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(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)`. - `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)`.
- `getTelemetry(uint16_t magnet_count = 0)`: Retrieves telemetry data from the ESC. If `magnet_count` is 0, uses the stored motor magnet count. - `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.
- `getESCInfo()`: Sends a command to the ESC to request ESC information. - `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. - `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. - `saveESCSettings()`: Sends a command to the ESC to save its current settings. Use with caution as this writes to ESC's non-volatile memory.
- `printDShotResult(dshot_result_t &result, Stream &output = Serial)`: Prints the result of a DShot operation to the specified output stream.
- `printDShotInfo(const DShotRMT &dshot_rmt, Stream &output = Serial)`: Prints detailed DShot signal information for a given DShotRMT instance.
- `printCpuInfo(Stream &output = Serial)`: Prints detailed CPU information.
- `setMotorMagnetCount(uint16_t magnet_count)`: Sets the motor magnet count for RPM calculation.
- `getMode()`: Gets the current DShot mode. - `getMode()`: Gets the current DShot mode.
- `isBidirectional()`: Checks if bidirectional DShot is enabled. - `isBidirectional()`: Checks if bidirectional DShot is enabled.
- `getEncodedFrameValue()`: Gets the last encoded DShot frame value.
- `getThrottleValue()`: Gets the last transmitted throttle value. - `getThrottleValue()`: Gets the last transmitted throttle value.
- `getEncodedFrameValue()`: Gets the last encoded DShot frame value.
## ⚙️ ESP-IDF Integration ## ⚙️ 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/) * [ESP-IDF v5.5.1 Documentation](https://docs.espressif.com/projects/esp-idf/en/v5.5.1/)

View File

@ -21,7 +21,7 @@ static constexpr dshot_mode_t DSHOT_MODE = DSHOT300;
// BiDirectional DShot Support (default: false) // BiDirectional DShot Support (default: false)
// re-enabled for testing // re-enabled for testing
static constexpr auto IS_BIDIRECTIONAL = false; static constexpr auto IS_BIDIRECTIONAL = true;
// Motor magnet count for RPM calculation // Motor magnet count for RPM calculation
// static constexpr auto MOTOR01_MAGNET_COUNT = 14; // static constexpr auto MOTOR01_MAGNET_COUNT = 14;
@ -65,7 +65,7 @@ void loop()
if (input.length() > 0) if (input.length() > 0)
{ {
handleSerialInput(input, throttle, continuous_throttle); handleSerialInput(input, throttle, continuous_throttle, motor01);
} }
} }
@ -80,13 +80,11 @@ void loop()
{ {
printDShotInfo(motor01, USB_SERIAL); printDShotInfo(motor01, USB_SERIAL);
USB_SERIAL.println(" ");
// Get Motor RPM if bidirectional // Get Motor RPM if bidirectional
if (IS_BIDIRECTIONAL) if (IS_BIDIRECTIONAL)
{ {
dshot_result_t telem_result = motor01.getTelemetry(); // dshot_result_t telem_result = motor01.getTelemetry();
printDShotResult(telem_result); // printDShotResult(telem_result);
} }
USB_SERIAL.println("Type 'help' to show Menu"); USB_SERIAL.println("Type 'help' to show Menu");
@ -118,14 +116,14 @@ void printMenu()
} }
// //
void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous_throttle) void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous_throttle, DShotRMT &session)
{ {
if (input == "0") if (input == "0")
{ {
// Stop motor // Stop motor
throttle = 0; throttle = 0;
continuous_throttle = true; continuous_throttle = true;
dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); dshot_result_t result = session.sendCommand(DSHOT_CMD_MOTOR_STOP);
printDShotResult(result); printDShotResult(result);
} }
else if (input == "info") else if (input == "info")

View File

@ -19,8 +19,9 @@
#include <DShotRMT.h> #include <DShotRMT.h>
#include <WiFi.h> #include <WiFi.h>
#include <Update.h> #include <Update.h>
#include "web_utilities/ota_update.h" #include "../web_utilities/ota_update.h"
#include "web_utilities/web_content.h" #include "../web_utilities/web_content.h"
#include "../web_utilities/web_constants.h"
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <AsyncTCP.h> #include <AsyncTCP.h>
@ -30,31 +31,12 @@
static constexpr auto *WIFI_SSID = "YOUR_SSID"; // Enter your WiFi SSID static constexpr auto *WIFI_SSID = "YOUR_SSID"; // Enter your WiFi SSID
static constexpr auto *WIFI_PASSWORD = "YOUR_PASSWORD"; // Enter your WiFi password static constexpr auto *WIFI_PASSWORD = "YOUR_PASSWORD"; // Enter your WiFi password
// Connection timeout in milliseconds
static constexpr auto WIFI_CONNECT_TIMEOUT = 20000;
// USB serial port settings
static constexpr auto &USB_SERIAL = Serial;
static constexpr auto USB_SERIAL_BAUD = 115200;
// Motor configuration - Pin number or GPIO_PIN
static constexpr gpio_num_t MOTOR01_PIN = GPIO_NUM_27;
// Supported: DSHOT150, DSHOT300, DSHOT600, (DSHOT1200)
static constexpr dshot_mode_t DSHOT_MODE = DSHOT300;
// BiDirectional DShot Support (default: false)
static constexpr auto IS_BIDIRECTIONAL = false; // Note: Bidirectional DShot is currently not officially supported due to instability and external hardware requirements.
// Motor magnet count for RPM calculation
static constexpr auto MOTOR01_MAGNET_COUNT = 14;
// Creates the motor instance // Creates the motor instance
DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, MOTOR01_MAGNET_COUNT); DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, MOTOR01_MAGNET_COUNT);
// Web Server Configuration // Web Server Configuration
AsyncWebServer server(80); AsyncWebServer server(WEBSERVER_PORT);
AsyncWebSocket ws("/ws"); AsyncWebSocket ws(WEBSOCKET_PATH);
// Global variables // Global variables
static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; static uint16_t throttle = DSHOT_CMD_MOTOR_STOP;
@ -130,7 +112,7 @@ void loop()
static uint64_t last_wifi_check = 0; static uint64_t last_wifi_check = 0;
// Check WiFi connection every 30 seconds // Check WiFi connection every 30 seconds
if (esp_timer_get_time() - last_wifi_check >= 30000000) if (esp_timer_get_time() - last_wifi_check >= WIFI_RECONNECT_CHECK_INTERVAL_US)
{ {
if (WiFi.status() != WL_CONNECTED && wifi_connected) if (WiFi.status() != WL_CONNECTED && wifi_connected)
{ {
@ -169,7 +151,7 @@ void loop()
} }
// Print motor stats every 3 seconds in continuous mode // Print motor stats every 3 seconds in continuous mode
if ((esp_timer_get_time() - last_serial_update >= 3000000)) if ((esp_timer_get_time() - last_serial_update >= MOTOR_STATS_UPDATE_INTERVAL_US))
{ {
printDShotInfo(motor01, USB_SERIAL); printDShotInfo(motor01, USB_SERIAL);
@ -204,9 +186,9 @@ void loop()
{ {
// Generate JSON for Webserver // Generate JSON for Webserver
JsonDocument doc; JsonDocument doc;
doc["throttle"] = isArmed ? throttle : 0; doc[JsonKey::THROTTLE] = isArmed ? throttle : 0;
doc["armed"] = isArmed; doc[JsonKey::ARMED] = isArmed;
doc["rpm"] = current_rpm; doc[JsonKey::RPM] = current_rpm;
String json_output; String json_output;
json_output.reserve(256); json_output.reserve(256);
@ -234,19 +216,16 @@ void setupOTA()
// Serve OTA update page // Serve OTA update page
server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send_P(200, "text/html", ota_html); }); { request->send_P(200, "text/html", ota_html); });
// Handle OTA update upload // Handle OTA update upload
server.on("/update", HTTP_POST, [](AsyncWebServerRequest *request) server.on(
{ "/update", HTTP_POST, [](AsyncWebServerRequest *request)
{
bool shouldReboot = !Update.hasError(); bool shouldReboot = !Update.hasError();
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", shouldReboot ? "OK" : "FAIL");
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain",
shouldReboot ? "OK" : "FAIL");
response->addHeader("Connection", "close"); response->addHeader("Connection", "close");
request->send(response); request->send(response);
if (shouldReboot) { if (shouldReboot) {
USB_SERIAL.println("OTA Update successful! Rebooting..."); USB_SERIAL.println("OTA Update successful! Rebooting...");
delay(1000); delay(1000);
@ -254,7 +233,8 @@ void setupOTA()
} else { } else {
USB_SERIAL.println("OTA Update failed!"); USB_SERIAL.println("OTA Update failed!");
} }
}, handleOTAUpload); },
handleOTAUpload);
USB_SERIAL.println("OTA Update ready at: /update"); USB_SERIAL.println("OTA Update ready at: /update");
} }
@ -266,8 +246,9 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind
if (!index) if (!index)
{ {
// Safety: Ensure motor is stopped during update // Safety: Ensure motor is stopped during update
motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); setArmingStatus(false); motor01.sendCommand(DSHOT_CMD_MOTOR_STOP);
setArmingStatus(false);
USB_SERIAL.printf("OTA Update Start: %s\n", filename.c_str()); USB_SERIAL.printf("OTA Update Start: %s\n", filename.c_str());
@ -287,7 +268,7 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind
} }
// Print progress every 2 seconds to avoid spam // Print progress every 2 seconds to avoid spam
if (millis() - ota_progress_millis > 2000) if (millis() - ota_progress_millis > OTA_PROGRESS_UPDATE_INTERVAL_MS)
{ {
size_t progress = index + len; size_t progress = index + len;
USB_SERIAL.printf("OTA Progress: %zu bytes\n", progress); USB_SERIAL.printf("OTA Progress: %zu bytes\n", progress);
@ -318,7 +299,7 @@ bool connectToWiFi()
uint32_t start_time = millis(); uint32_t start_time = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - start_time) < WIFI_CONNECT_TIMEOUT) while (WiFi.status() != WL_CONNECTED && (millis() - start_time) < WIFI_CONNECT_TIMEOUT_MS)
{ {
delay(500); delay(500);
USB_SERIAL.print("."); USB_SERIAL.print(".");
@ -408,22 +389,22 @@ void printMenu()
} }
USB_SERIAL.println("***********************************************"); USB_SERIAL.println("***********************************************");
USB_SERIAL.println(" arm - Arm motor"); USB_SERIAL.printf(" %s\t\t- Arm motor\n", SerialCmd::ARM);
USB_SERIAL.println(" disarm - Disarm motor (safety)"); USB_SERIAL.printf(" %s\t\t- Disarm motor (safety)\n", SerialCmd::DISARM);
USB_SERIAL.println(" <value> - Set throttle (48 2047)"); USB_SERIAL.println(" <value>\t\t- Set throttle (48 2047)");
USB_SERIAL.println(" 0 - Stop motor"); USB_SERIAL.printf(" %s\t\t\t- Stop motor\n", SerialCmd::STOP);
USB_SERIAL.println("***********************************************"); USB_SERIAL.println("***********************************************");
USB_SERIAL.println(" cmd <number> - Send DShot command (0 - 47)"); USB_SERIAL.printf(" %s <number>\t- Send DShot command (0 - 47)\n", SerialCmd::CMD_PREFIX);
USB_SERIAL.println(" info - Show motor info"); USB_SERIAL.printf(" %s\t\t- Show motor info\n", SerialCmd::INFO);
USB_SERIAL.println(" wifi - Show WiFi status"); USB_SERIAL.printf(" %s\t\t- Show WiFi status\n", SerialCmd::WIFI_STATUS);
USB_SERIAL.println(" reconnect - Reconnect to WiFi"); USB_SERIAL.printf(" %s\t- Reconnect to WiFi\n", SerialCmd::RECONNECT);
USB_SERIAL.println(" ota - Show OTA info"); USB_SERIAL.printf(" %s\t\t- Show OTA info\n", SerialCmd::OTA_INFO);
if (IS_BIDIRECTIONAL) if (IS_BIDIRECTIONAL)
{ {
USB_SERIAL.println(" rpm - Get telemetry data"); USB_SERIAL.printf(" %s\t\t- Get telemetry data\n", SerialCmd::RPM);
} }
USB_SERIAL.println("***********************************************"); USB_SERIAL.println("***********************************************");
USB_SERIAL.println(" h / help - Show this Menu"); USB_SERIAL.printf(" %s / %s\t- Show this Menu\n", SerialCmd::HELP, SerialCmd::HELP_ALT);
USB_SERIAL.println("***********************************************"); USB_SERIAL.println("***********************************************");
USB_SERIAL.printf(" Current Status: %s\n", isArmed ? "ARMED" : "DISARMED"); USB_SERIAL.printf(" Current Status: %s\n", isArmed ? "ARMED" : "DISARMED");
USB_SERIAL.printf(" WiFi Status: %s\n", wifi_connected ? "CONNECTED" : "DISCONNECTED"); USB_SERIAL.printf(" WiFi Status: %s\n", wifi_connected ? "CONNECTED" : "DISCONNECTED");
@ -433,19 +414,19 @@ void printMenu()
// Handle serial inputs and updates global variables // Handle serial inputs and updates global variables
void handleSerialInput(const String &input) void handleSerialInput(const String &input)
{ {
if (input == "arm") if (input == SerialCmd::ARM)
{ {
setArmingStatus(true); setArmingStatus(true);
return; return;
} }
if (input == "0" || input == "disarm") if (input == SerialCmd::STOP || input == SerialCmd::DISARM)
{ {
setArmingStatus(false); setArmingStatus(false);
return; return;
} }
if (input == "info") if (input == SerialCmd::INFO)
{ {
printDShotInfo(motor01, USB_SERIAL); printDShotInfo(motor01, USB_SERIAL);
USB_SERIAL.println(" "); USB_SERIAL.println(" ");
@ -453,13 +434,13 @@ void handleSerialInput(const String &input)
return; return;
} }
if (input == "wifi") if (input == SerialCmd::WIFI_STATUS)
{ {
printWiFiStatus(); printWiFiStatus();
return; return;
} }
if (input == "ota") if (input == SerialCmd::OTA_INFO)
{ {
if (wifi_connected) if (wifi_connected)
{ {
@ -477,7 +458,7 @@ void handleSerialInput(const String &input)
return; return;
} }
if (input == "reconnect") if (input == SerialCmd::RECONNECT)
{ {
USB_SERIAL.println("Reconnecting to WiFi..."); USB_SERIAL.println("Reconnecting to WiFi...");
WiFi.disconnect(); WiFi.disconnect();
@ -490,7 +471,7 @@ void handleSerialInput(const String &input)
return; return;
} }
if (input == "rpm" && IS_BIDIRECTIONAL) if (input == SerialCmd::RPM && IS_BIDIRECTIONAL)
{ {
if (isArmed) if (isArmed)
{ {
@ -505,7 +486,7 @@ void handleSerialInput(const String &input)
return; return;
} }
if (input.startsWith("cmd ")) if (input.startsWith(SerialCmd::CMD_PREFIX))
{ {
if (!isArmed) if (!isArmed)
{ {
@ -515,7 +496,7 @@ void handleSerialInput(const String &input)
} }
continuous_throttle = false; continuous_throttle = false;
int cmd_num = input.substring(4).toInt(); int cmd_num = input.substring(strlen(SerialCmd::CMD_PREFIX)).toInt();
if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX)
{ {
@ -530,13 +511,13 @@ void handleSerialInput(const String &input)
return; return;
} }
if (input == "h" || input == "help") if (input == SerialCmd::HELP || input == SerialCmd::HELP_ALT)
{ {
printMenu(); printMenu();
return; return;
} }
if (input == "status") if (input == SerialCmd::STATUS)
{ {
USB_SERIAL.println(" "); USB_SERIAL.println(" ");
USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED");
@ -608,15 +589,15 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
bool armedFromWeb = false; bool armedFromWeb = false;
// Handle arming status // Handle arming status
if (doc.containsKey("armed")) if (doc.containsKey(JsonKey::ARMED))
{ {
bool armed = doc["armed"]; bool armed = doc[JsonKey::ARMED];
setArmingStatus(armed); setArmingStatus(armed);
armedFromWeb = true; armedFromWeb = true;
} }
// Handle throttle value (only if armed) // Handle throttle value (only if armed)
if (doc.containsKey("throttle")) if (doc.containsKey(JsonKey::THROTTLE))
{ {
if (!isArmed) if (!isArmed)
{ {
@ -628,7 +609,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
return; return;
} }
uint16_t web_throttle = doc["throttle"]; uint16_t web_throttle = doc[JsonKey::THROTTLE];
if (web_throttle == 0) if (web_throttle == 0)
{ {
@ -664,8 +645,8 @@ void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventTyp
// Send current arming status to new client // Send current arming status to new client
{ {
JsonDocument doc; JsonDocument doc;
doc["armed"] = isArmed; doc[JsonKey::ARMED] = isArmed;
doc["throttle"] = isArmed ? throttle : 0; doc[JsonKey::THROTTLE] = isArmed ? throttle : 0;
String json_output; String json_output;
serializeJson(doc, json_output); serializeJson(doc, json_output);
client->text(json_output); client->text(json_output);

View File

@ -19,42 +19,27 @@
#include <DShotRMT.h> #include <DShotRMT.h>
#include <WiFi.h> #include <WiFi.h>
#include <Update.h> #include <Update.h>
#include "web_utilities/web_content.h" #include "../web_utilities/web_content.h"
#include "../web_utilities/web_constants.h"
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <AsyncTCP.h> #include <AsyncTCP.h>
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
// Wifi Configuration // Wifi AP Configuration
static constexpr auto *ssid = "DShotRMT Control"; static constexpr auto *WIFI_SSID_AP = "DShotRMT Control";
static constexpr auto *password = "12345678"; static constexpr auto *WIFI_PASSWORD_AP = "12345678";
IPAddress local_IP(10, 10, 10, 1); IPAddress local_IP(10, 10, 10, 1);
IPAddress gateway(0, 0, 0, 0); IPAddress gateway(0, 0, 0, 0);
IPAddress subnet(255, 255, 255, 0); IPAddress subnet(255, 255, 255, 0);
// USB serial port settings
static constexpr auto &USB_SERIAL = Serial;
static constexpr auto USB_SERIAL_BAUD = 115200;
// Motor configuration - Pin number or GPIO_PIN
static constexpr gpio_num_t MOTOR01_PIN = GPIO_NUM_27;
// Supported: DSHOT150, DSHOT300, DSHOT600, (DSHOT1200)
static constexpr dshot_mode_t DSHOT_MODE = DSHOT300;
// BiDirectional DShot Support (default: false)
static constexpr auto IS_BIDIRECTIONAL = false; // Note: Bidirectional DShot is currently not officially supported due to instability and external hardware requirements.
// Motor magnet count for RPM calculation
static constexpr auto MOTOR01_MAGNET_COUNT = 14;
// Creates the motor instance // Creates the motor instance
DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, MOTOR01_MAGNET_COUNT); DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, MOTOR01_MAGNET_COUNT);
// Web Server Configuration // Web Server Configuration
AsyncWebServer server(80); AsyncWebServer server(WEBSERVER_PORT);
AsyncWebSocket ws("/ws"); AsyncWebSocket ws(WEBSOCKET_PATH);
// Global variables // Global variables
static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; static uint16_t throttle = DSHOT_CMD_MOTOR_STOP;
@ -81,7 +66,7 @@ void setup()
// Start Wifi Access Point // Start Wifi Access Point
USB_SERIAL.println("\nStarting Access Point..."); USB_SERIAL.println("\nStarting Access Point...");
WiFi.softAP(ssid, password); WiFi.softAP(WIFI_SSID_AP, WIFI_PASSWORD_AP);
IPAddress IP = WiFi.softAPIP(); IPAddress IP = WiFi.softAPIP();
@ -136,7 +121,7 @@ void loop()
} }
// Print motor stats every 3 seconds in continuous mode // Print motor stats every 3 seconds in continuous mode
if ((esp_timer_get_time() - last_serial_update >= 3000000)) if ((esp_timer_get_time() - last_serial_update >= MOTOR_STATS_UPDATE_INTERVAL_US))
{ {
printDShotInfo(motor01, USB_SERIAL); printDShotInfo(motor01, USB_SERIAL);
@ -171,9 +156,9 @@ void loop()
{ {
// Generate JSON for Webserver // Generate JSON for Webserver
JsonDocument doc; JsonDocument doc;
doc["throttle"] = isArmed ? throttle : 0; doc[JsonKey::THROTTLE] = isArmed ? throttle : 0;
doc["armed"] = isArmed; doc[JsonKey::ARMED] = isArmed;
doc["rpm"] = current_rpm; doc[JsonKey::RPM] = current_rpm;
String json_output; String json_output;
json_output.reserve(256); json_output.reserve(256);
@ -221,19 +206,19 @@ void printMenu()
USB_SERIAL.println("***********************************************"); USB_SERIAL.println("***********************************************");
USB_SERIAL.println(" Web Config: http://10.10.10.1 "); USB_SERIAL.println(" Web Config: http://10.10.10.1 ");
USB_SERIAL.println("***********************************************"); USB_SERIAL.println("***********************************************");
USB_SERIAL.println(" arm - Arm motor"); USB_SERIAL.printf(" %s\t\t- Arm motor\n", SerialCmd::ARM);
USB_SERIAL.println(" disarm - Disarm motor (safety)"); USB_SERIAL.printf(" %s\t\t- Disarm motor (safety)\n", SerialCmd::DISARM);
USB_SERIAL.println(" <value> - Set throttle (48 2047)"); USB_SERIAL.println(" <value>\t\t- Set throttle (48 2047)");
USB_SERIAL.println(" 0 - Stop motor"); USB_SERIAL.printf(" %s\t\t\t- Stop motor\n", SerialCmd::STOP);
USB_SERIAL.println("***********************************************"); USB_SERIAL.println("***********************************************");
USB_SERIAL.println(" cmd <number> - Send DShot command (0 - 47)"); USB_SERIAL.printf(" %s <number>\t- Send DShot command (0 - 47)\n", SerialCmd::CMD_PREFIX);
USB_SERIAL.println(" info - Show motor info"); USB_SERIAL.printf(" %s\t\t- Show motor info\n", SerialCmd::INFO);
if (IS_BIDIRECTIONAL) if (IS_BIDIRECTIONAL)
{ {
USB_SERIAL.println(" rpm - Get telemetry data"); USB_SERIAL.printf(" %s\t\t- Get telemetry data\n", SerialCmd::RPM);
} }
USB_SERIAL.println("***********************************************"); USB_SERIAL.println("***********************************************");
USB_SERIAL.println(" h / help - Show this Menu"); USB_SERIAL.printf(" %s / %s\t- Show this Menu\n", SerialCmd::HELP, SerialCmd::HELP_ALT);
USB_SERIAL.println("***********************************************"); USB_SERIAL.println("***********************************************");
USB_SERIAL.printf(" Current Status: %s\n", isArmed ? "ARMED" : "DISARMED"); USB_SERIAL.printf(" Current Status: %s\n", isArmed ? "ARMED" : "DISARMED");
USB_SERIAL.println("***********************************************"); USB_SERIAL.println("***********************************************");
@ -242,24 +227,24 @@ void printMenu()
// Handle serial inputs and updates global variables // Handle serial inputs and updates global variables
void handleSerialInput(const String &input) void handleSerialInput(const String &input)
{ {
if (input == "arm") if (input == SerialCmd::ARM)
{ {
setArmingStatus(true); setArmingStatus(true);
return; return;
} }
if (input == "0" || input == "disarm") if (input == SerialCmd::STOP || input == SerialCmd::DISARM)
{ {
setArmingStatus(false); setArmingStatus(false);
return; return;
} }
if (input == "info") if (input == SerialCmd::INFO)
{ {
printDShotInfo(motor01, USB_SERIAL); printDShotInfo(motor01, USB_SERIAL);
USB_SERIAL.println(" "); USB_SERIAL.println(" ");
USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED");
return; return;
} }
if (input == "rpm" && IS_BIDIRECTIONAL) if (input == SerialCmd::RPM && IS_BIDIRECTIONAL)
{ {
if (isArmed) if (isArmed)
{ {
@ -273,7 +258,7 @@ void handleSerialInput(const String &input)
} }
return; return;
} }
if (input.startsWith("cmd ")) if (input.startsWith(SerialCmd::CMD_PREFIX))
{ {
if (!isArmed) if (!isArmed)
{ {
@ -283,7 +268,7 @@ void handleSerialInput(const String &input)
} }
continuous_throttle = false; continuous_throttle = false;
int cmd_num = input.substring(4).toInt(); int cmd_num = input.substring(strlen(SerialCmd::CMD_PREFIX)).toInt();
if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX)
{ {
@ -297,12 +282,12 @@ void handleSerialInput(const String &input)
} }
return; return;
} }
if (input == "h" || input == "help") if (input == SerialCmd::HELP || input == SerialCmd::HELP_ALT)
{ {
printMenu(); printMenu();
return; return;
} }
if (input == "status") if (input == SerialCmd::STATUS)
{ {
USB_SERIAL.println(" "); USB_SERIAL.println(" ");
USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED");
@ -368,15 +353,15 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
bool armedFromWeb = false; bool armedFromWeb = false;
// Handle arming status // Handle arming status
if (doc.containsKey("armed")) if (doc.containsKey(JsonKey::ARMED))
{ {
bool armed = doc["armed"]; bool armed = doc[JsonKey::ARMED];
setArmingStatus(armed); setArmingStatus(armed);
armedFromWeb = true; armedFromWeb = true;
} }
// Handle throttle value (only if armed) // Handle throttle value (only if armed)
if (doc.containsKey("throttle")) if (doc.containsKey(JsonKey::THROTTLE))
{ {
if (!isArmed) if (!isArmed)
{ {
@ -388,7 +373,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
return; return;
} }
uint16_t web_throttle = doc["throttle"]; uint16_t web_throttle = doc[JsonKey::THROTTLE];
if (web_throttle == 0) if (web_throttle == 0)
{ {
@ -424,8 +409,8 @@ void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventTyp
// Send current arming status to new client // Send current arming status to new client
{ {
JsonDocument doc; JsonDocument doc;
doc["armed"] = isArmed; doc[JsonKey::ARMED] = isArmed;
doc["throttle"] = isArmed ? throttle : 0; doc[JsonKey::THROTTLE] = isArmed ? throttle : 0;
String json_output; String json_output;
serializeJson(doc, json_output); serializeJson(doc, json_output);
client->text(json_output); client->text(json_output);

View File

@ -0,0 +1,62 @@
/**
* @file web_constants.h
* @brief Shared constants for DShotRMT web examples.
* @author Wastl Kraus
* @date 2025-11-29
* @license MIT
*/
#pragma once
// ============================================================================
// Serial Configuration & Commands
// ============================================================================
static constexpr auto& USB_SERIAL = Serial;
static constexpr auto USB_SERIAL_BAUD = 115200;
// Serial command strings
namespace SerialCmd {
constexpr char ARM[] = "arm";
constexpr char DISARM[] = "disarm";
constexpr char STOP[] = "0";
constexpr char INFO[] = "info";
constexpr char RPM[] = "rpm";
constexpr char CMD_PREFIX[] = "cmd ";
constexpr char HELP[] = "h";
constexpr char HELP_ALT[] = "help";
constexpr char STATUS[] = "status";
constexpr char WIFI_STATUS[] = "wifi";
constexpr char RECONNECT[] = "reconnect";
constexpr char OTA_INFO[] = "ota";
}
// ============================================================================
// Motor & DShot Configuration
// ============================================================================
static constexpr gpio_num_t MOTOR01_PIN = GPIO_NUM_27;
static constexpr dshot_mode_t DSHOT_MODE = DSHOT300;
static constexpr bool IS_BIDIRECTIONAL = false;
static constexpr uint16_t MOTOR01_MAGNET_COUNT = 14;
// ============================================================================
// Web Server & WebSocket Configuration
// ============================================================================
static constexpr uint16_t WEBSERVER_PORT = 80;
static constexpr char WEBSOCKET_PATH[] = "/ws";
// Keys for JSON communication
namespace JsonKey {
constexpr char THROTTLE[] = "throttle";
constexpr char ARMED[] = "armed";
constexpr char RPM[] = "rpm";
}
// ============================================================================
// Timing Intervals
// ============================================================================
static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 20000; // 20 seconds
static constexpr uint64_t MOTOR_STATS_UPDATE_INTERVAL_US = 3000000; // 3 seconds
static constexpr uint64_t WIFI_RECONNECT_CHECK_INTERVAL_US = 30000000; // 30 seconds
static constexpr uint32_t WEBSOCKET_RECONNECT_DELAY_MS = 2000; // 2 seconds
static constexpr uint32_t SERIAL_RAMP_DELAY_MS = 200; // 200 ms
static constexpr uint32_t OTA_PROGRESS_UPDATE_INTERVAL_MS = 2000; // 2 seconds

View File

@ -8,6 +8,18 @@
DShotRMT KEYWORD1 DShotRMT KEYWORD1
#######################################
# Methods (KEYWORD2)
#######################################
begin KEYWORD2
sendThrottle KEYWORD2
sendThrottlePercent KEYWORD2
sendCommand KEYWORD2
sendCustomCommand KEYWORD2
getTelemetry KEYWORD2
setMotorSpinDirection KEYWORD2
saveESCSettings KEYWORD2
####################################### #######################################
# Constants (LITERAL1) # Constants (LITERAL1)
@ -53,15 +65,9 @@ DSHOT_CMD_LED2_OFF LITERAL1
DSHOT_CMD_LED3_OFF LITERAL1 DSHOT_CMD_LED3_OFF LITERAL1
DSHOT_CMD_AUDIO_STREAM_MODE_ON_OFF LITERAL1 DSHOT_CMD_AUDIO_STREAM_MODE_ON_OFF LITERAL1
DSHOT_CMD_SILENT_MODE_ON_OFF LITERAL1 DSHOT_CMD_SILENT_MODE_ON_OFF LITERAL1
DSHOT_CMD_MAX LITERAL1
# DShot Command Types
DSHOT_CMD_TYPE_INLINE LITERAL1
DSHOT_CMD_TYPE_BLOCKING LITERAL1
# Protocol Constants # Protocol Constants
DSHOT_BITS_PER_FRAME LITERAL1 DSHOT_BITS_PER_FRAME LITERAL1
DSHOT_SWITCH_TIME LITERAL1
DSHOT_NULL_PACKET LITERAL1 DSHOT_NULL_PACKET LITERAL1
DSHOT_RX_TIMEOUT_MS LITERAL1 DSHOT_RX_TIMEOUT_MS LITERAL1
GCR_BITS_PER_FRAME LITERAL1 GCR_BITS_PER_FRAME LITERAL1
@ -69,12 +75,9 @@ GCR_BITS_PER_FRAME LITERAL1
# RMT Constants # RMT Constants
DSHOT_CLOCK_SRC_DEFAULT LITERAL1 DSHOT_CLOCK_SRC_DEFAULT LITERAL1
DSHOT_RMT_RESOLUTION LITERAL1 DSHOT_RMT_RESOLUTION LITERAL1
RMT_BUFFER_SIZE LITERAL1
RMT_BUFFER_SYMBOLS LITERAL1 RMT_BUFFER_SYMBOLS LITERAL1
RMT_QUEUE_DEPTH LITERAL1 RMT_QUEUE_DEPTH LITERAL1
DSHOT_PULSE_MIN LITERAL1
DSHOT_PULSE_MAX LITERAL1
# Status Constants # Status Constants
DSHOT_OK LITERAL1 DSHOT_OK LITERAL1
DSHOT_ERROR LITERAL1 DSHOT_ERROR LITERAL1

View File

@ -1,5 +1,5 @@
name=DShotRMT name=DShotRMT
version=0.9.1 version=0.9.5
author=Wastl Kraus <wir-sind-die-matrix.de> author=Wastl Kraus <wir-sind-die-matrix.de>
maintainer=Wastl Kraus <wir-sind-die-matrix.de> maintainer=Wastl Kraus <wir-sind-die-matrix.de>
license=MIT license=MIT

View File

@ -7,6 +7,7 @@
*/ */
#include "DShotRMT.h" #include "DShotRMT.h"
#include <cstring>
// Constructor with GPIO number // Constructor with GPIO number
DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count) DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count)
@ -16,8 +17,9 @@ DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, ui
_motor_magnet_count(magnet_count), _motor_magnet_count(magnet_count),
_dshot_timing(DSHOT_TIMING_US[_mode]) _dshot_timing(DSHOT_TIMING_US[_mode])
{ {
// Pre-calculate timing and bit positions for performance // Pre-calculate timing and ratios for performance
_preCalculateRMTTicks(); _preCalculateRMTTicks();
_percent_to_throttle_ratio = (static_cast<float>(DSHOT_THROTTLE_MAX - DSHOT_THROTTLE_MIN)) / DSHOT_PERCENT_MAX;
} }
// Constructor using pin number // Constructor using pin number
@ -30,28 +32,7 @@ DShotRMT::DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional, ui
// Destructor // Destructor
DShotRMT::~DShotRMT() DShotRMT::~DShotRMT()
{ {
// Cleanup TX channel _cleanupRmtResources();
if (_rmt_tx_channel)
{
rmt_disable(_rmt_tx_channel);
rmt_del_channel(_rmt_tx_channel);
_rmt_tx_channel = nullptr;
}
// Cleanup RX channel
if (_rmt_rx_channel)
{
rmt_disable(_rmt_rx_channel);
rmt_del_channel(_rmt_rx_channel);
_rmt_rx_channel = nullptr;
}
// Cleanup encoder
if (_dshot_encoder)
{
rmt_del_encoder(_dshot_encoder);
_dshot_encoder = nullptr;
}
} }
// Initialize DShotRMT // Initialize DShotRMT
@ -89,36 +70,37 @@ dshot_result_t DShotRMT::begin()
// Send throttle value // Send throttle value
dshot_result_t DShotRMT::sendThrottle(uint16_t throttle) dshot_result_t DShotRMT::sendThrottle(uint16_t throttle)
{ {
// A throttle value of 0 is a disarm command // Per DShot specification, a throttle value of 0 is a disarm command.
if (throttle == 0) if (throttle == 0)
{ {
_last_throttle = 0;
return sendCommand(DSHOT_CMD_MOTOR_STOP); return sendCommand(DSHOT_CMD_MOTOR_STOP);
} }
// Constrain throttle to the valid DShot range // Constrain throttle to the valid DShot range.
_last_throttle = constrain(throttle, DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX); _last_throttle = constrain(throttle, DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX);
_packet = _buildDShotPacket(_last_throttle); _packet = _buildDShotPacket(_last_throttle);
return _sendDShotFrame(_packet); return _sendPacket(_packet);
} }
// Send throttle value as a percentage // Send throttle value as a percentage
dshot_result_t DShotRMT::sendThrottlePercent(float percent) dshot_result_t DShotRMT::sendThrottlePercent(float percent)
{ {
if (percent < 0.0f || percent > 100.0f) if (percent < DSHOT_PERCENT_MIN || percent > DSHOT_PERCENT_MAX)
{ {
return {false, DSHOT_PERCENT_NOT_IN_RANGE}; return {false, DSHOT_PERCENT_NOT_IN_RANGE};
} }
// Map percent to DShot throttle range // Map percent to DShot throttle range using pre-calculated ratio.
uint16_t throttle = static_cast<uint16_t>(DSHOT_THROTTLE_MIN + ((DSHOT_THROTTLE_MAX - DSHOT_THROTTLE_MIN) / 100.0f) * percent); uint16_t throttle = static_cast<uint16_t>(DSHOT_THROTTLE_MIN + _percent_to_throttle_ratio * percent);
return sendThrottle(throttle); return sendThrottle(throttle);
} }
// Sends a DShot command (0-47) to the ESC by accepting an integer value. // Sends a DShot command (0-47) to the ESC by accepting an integer value.
dshot_result_t DShotRMT::sendCommand(uint16_t command_value) dshot_result_t DShotRMT::sendCommand(uint16_t command_value)
{ {
// Validate the integer command value before casting // Validate the integer command value before casting.
if (command_value < DSHOT_CMD_MOTOR_STOP || command_value > DSHOT_CMD_MAX_VALUE) if (command_value < DSHOT_CMD_MOTOR_STOP || command_value > DSHOT_CMD_MAX_VALUE)
{ {
return {false, DSHOT_COMMAND_NOT_VALID}; return {false, DSHOT_COMMAND_NOT_VALID};
@ -132,6 +114,7 @@ dshot_result_t DShotRMT::sendCommand(dshotCommands_e command)
uint16_t repeat_count = DEFAULT_CMD_REPEAT_COUNT; uint16_t repeat_count = DEFAULT_CMD_REPEAT_COUNT;
uint16_t delay_us = DEFAULT_CMD_DELAY_US; uint16_t delay_us = DEFAULT_CMD_DELAY_US;
// Certain commands require more repetitions to be reliably accepted by the ESC.
switch (command) switch (command)
{ {
case DSHOT_CMD_MOTOR_STOP: case DSHOT_CMD_MOTOR_STOP:
@ -142,7 +125,7 @@ dshot_result_t DShotRMT::sendCommand(dshotCommands_e command)
delay_us = SETTINGS_COMMAND_DELAY_US; delay_us = SETTINGS_COMMAND_DELAY_US;
break; break;
default: default:
// For other commands, use default repeat and delay // For other commands, use default repeat and delay.
break; break;
} }
@ -152,49 +135,17 @@ dshot_result_t DShotRMT::sendCommand(dshotCommands_e command)
// Sends a DShot command (0-47) to the ESC with a specified repeat count and delay. // Sends a DShot command (0-47) to the ESC with a specified repeat count and delay.
dshot_result_t DShotRMT::sendCommand(dshotCommands_e command, uint16_t repeat_count, uint16_t delay_us) dshot_result_t DShotRMT::sendCommand(dshotCommands_e command, uint16_t repeat_count, uint16_t delay_us)
{ {
dshot_result_t result = {false, DSHOT_UNKNOWN, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY};
if (!_isValidCommand(command)) if (!_isValidCommand(command))
{ {
result.result_code = DSHOT_INVALID_COMMAND; return {false, DSHOT_INVALID_COMMAND};
return result;
} }
return _sendRepeatedCommand(static_cast<uint16_t>(command), repeat_count, delay_us);
bool all_successful = true;
// Send command multiple times with delay
for (uint16_t i = 0; i < repeat_count; i++)
{
dshot_result_t single_result = _executeCommand(command);
if (!single_result.success)
{
all_successful = false;
result.result_code = single_result.result_code;
break;
}
// Add delay between repetitions (except for last repetition)
if (i < repeat_count - 1)
{
delayMicroseconds(delay_us);
}
}
result.success = all_successful;
if (result.success)
{
result.result_code = DSHOT_COMMAND_SUCCESS;
}
return result;
} }
// Get telemetry data // Get telemetry data
dshot_result_t DShotRMT::getTelemetry() dshot_result_t DShotRMT::getTelemetry()
{ {
dshot_result_t result = {false, DSHOT_TELEMETRY_FAILED, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY}; dshot_result_t result = {false, DSHOT_TELEMETRY_FAILED};
if (!_is_bidirectional) if (!_is_bidirectional)
{ {
@ -202,24 +153,38 @@ dshot_result_t DShotRMT::getTelemetry()
return result; return result;
} }
// Use stored magnet count if parameter is 0 (default) // Prioritize checking for full telemetry data, as it is richer.
uint16_t final_magnet_count = _motor_magnet_count; if (_full_telemetry_ready_flag_atomic)
{
_full_telemetry_ready_flag_atomic = false; // Reset the flag
result.telemetry_data = _last_telemetry_data_atomic; // Read the atomic variable
result.telemetry_available = true;
// Check if the callback has set the flag for new data // Also populate eRPM fields from the full telemetry data for consistency.
result.erpm = result.telemetry_data.rpm;
if (_motor_magnet_count >= MAGNETS_PER_POLE_PAIR) {
uint8_t pole_pairs = _motor_magnet_count / MAGNETS_PER_POLE_PAIR;
result.motor_rpm = result.telemetry_data.rpm / pole_pairs;
}
result.success = true;
result.result_code = DSHOT_TELEMETRY_DATA_AVAILABLE;
return result;
}
// If no full telemetry, check for eRPM-only data.
if (_telemetry_ready_flag_atomic) if (_telemetry_ready_flag_atomic)
{ {
_telemetry_ready_flag_atomic = false; // Reset the flag _telemetry_ready_flag_atomic = false; // Reset the flag
uint16_t erpm = _last_erpm_atomic; // Read the atomic variable uint16_t erpm = _last_erpm_atomic; // Read the atomic variable
if (erpm != DSHOT_NULL_PACKET && final_magnet_count >= MAGNETS_PER_POLE_PAIR) if (erpm != DSHOT_NULL_PACKET && _motor_magnet_count >= MAGNETS_PER_POLE_PAIR)
{ {
// Calculate motor RPM from eRPM and magnet count // Calculate motor RPM from eRPM and magnet count
uint8_t pole_pairs = final_magnet_count / MAGNETS_PER_POLE_PAIR; uint8_t pole_pairs = _motor_magnet_count / MAGNETS_PER_POLE_PAIR;
uint32_t motor_rpm = (erpm / pole_pairs);
result.success = true;
result.erpm = erpm; result.erpm = erpm;
result.motor_rpm = motor_rpm; result.motor_rpm = (erpm / pole_pairs);
result.success = true;
result.result_code = DSHOT_TELEMETRY_SUCCESS; result.result_code = DSHOT_TELEMETRY_SUCCESS;
} }
} }
@ -230,43 +195,69 @@ dshot_result_t DShotRMT::getTelemetry()
// Reverse motor direction directly // Reverse motor direction directly
dshot_result_t DShotRMT::setMotorSpinDirection(bool reversed) dshot_result_t DShotRMT::setMotorSpinDirection(bool reversed)
{ {
// Use command as a yes / no switch
dshotCommands_e command = reversed ? dshotCommands_e::DSHOT_CMD_SPIN_DIRECTION_REVERSED : dshotCommands_e::DSHOT_CMD_SPIN_DIRECTION_NORMAL; dshotCommands_e command = reversed ? dshotCommands_e::DSHOT_CMD_SPIN_DIRECTION_REVERSED : dshotCommands_e::DSHOT_CMD_SPIN_DIRECTION_NORMAL;
return sendCommand(command, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); return sendCommand(command, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US);
} }
// Sends a raw DShot command to the ESC. dshot_result_t DShotRMT::sendCustomCommand(uint16_t command_value, uint16_t repeat_count, uint16_t delay_us)
dshot_result_t DShotRMT::sendRawCommand(uint16_t command_value)
{ {
_packet = _buildDShotPacket(command_value); // Validate the integer command value.
return _sendDShotFrame(_packet); if (command_value < DSHOT_CMD_MIN || command_value > DSHOT_CMD_MAX)
{
return {false, DSHOT_COMMAND_NOT_VALID};
}
return _sendRepeatedCommand(command_value, repeat_count, delay_us);
} }
// Use with caution // Writes settings to the ESC's non-volatile memory; use with caution.
dshot_result_t DShotRMT::saveESCSettings() dshot_result_t DShotRMT::saveESCSettings()
{ {
return sendCommand(dshotCommands_e::DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); return sendCommand(dshotCommands_e::DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US);
} }
// Simple check // Private helper to send a command value multiple times.
dshot_result_t DShotRMT::_sendRepeatedCommand(uint16_t value, uint16_t repeat_count, uint16_t delay_us)
{
bool all_successful = true;
dshot_result_t last_result = {true, DSHOT_COMMAND_SUCCESS};
for (uint16_t i = 0; i < repeat_count; i++)
{
last_result = _sendRawDshotFrame(value);
if (!last_result.success)
{
all_successful = false;
break;
}
if (i < repeat_count - 1)
{
delayMicroseconds(delay_us);
}
}
if (all_successful)
{
return {true, DSHOT_COMMAND_SUCCESS};
}
else
{
// Return the result from the failed transmission.
return last_result;
}
}
// Simple check for valid command range.
bool DShotRMT::_isValidCommand(dshotCommands_e command) const bool DShotRMT::_isValidCommand(dshotCommands_e command) const
{ {
return (command >= dshotCommands_e::DSHOT_CMD_MOTOR_STOP && command <= DSHOT_CMD_MAX); return (command >= dshotCommands_e::DSHOT_CMD_MOTOR_STOP && command <= DSHOT_CMD_MAX);
} }
// Executes a single DShot command by building and sending a DShot frame. dshot_result_t DShotRMT::_sendRawDshotFrame(uint16_t value)
dshot_result_t DShotRMT::_executeCommand(dshotCommands_e command)
{ {
uint64_t start_time = esp_timer_get_time(); _packet = _buildDShotPacket(value);
return _sendPacket(_packet);
_packet = _buildDShotPacket(static_cast<uint16_t>(command));
dshot_result_t result = _sendDShotFrame(_packet);
uint64_t end_time = esp_timer_get_time();
_last_command_timestamp = end_time;
return result;
} }
// Private Packet Management Functions // Private Packet Management Functions
@ -277,7 +268,7 @@ dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t &value) const
packet.throttle_value = value & DSHOT_THROTTLE_MAX; packet.throttle_value = value & DSHOT_THROTTLE_MAX;
packet.telemetric_request = _is_bidirectional ? 1 : 0; packet.telemetric_request = _is_bidirectional ? 1 : 0;
// The data for CRC calculation includes the 11-bit value and the 1-bit telemetry flag // The data for CRC calculation includes the 11-bit value and the 1-bit telemetry flag.
uint16_t data_for_crc = (packet.throttle_value << 1) | packet.telemetric_request; uint16_t data_for_crc = (packet.throttle_value << 1) | packet.telemetric_request;
packet.checksum = _calculateCRC(data_for_crc); packet.checksum = _calculateCRC(data_for_crc);
@ -286,17 +277,17 @@ dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t &value) const
uint16_t DShotRMT::_buildDShotFrameValue(const dshot_packet_t &packet) const uint16_t DShotRMT::_buildDShotFrameValue(const dshot_packet_t &packet) const
{ {
// Combine throttle, telemetry bit, and CRC into a single 16-bit frame // Combine throttle, telemetry bit, and CRC into a single 16-bit frame.
uint16_t data_and_telemetry = (packet.throttle_value << 1) | packet.telemetric_request; uint16_t data_and_telemetry = (packet.throttle_value << 1) | packet.telemetric_request;
return (data_and_telemetry << 4) | packet.checksum; return (data_and_telemetry << 4) | packet.checksum;
} }
uint16_t DShotRMT::_calculateCRC(const uint16_t &data) const uint16_t DShotRMT::_calculateCRC(const uint16_t &data) const
{ {
// Standard DShot CRC calculation using XOR // Standard DShot CRC calculation using XOR.
uint16_t crc = (data ^ (data >> 4) ^ (data >> 8)) & DSHOT_CRC_MASK; uint16_t crc = (data ^ (data >> 4) ^ (data >> 8)) & DSHOT_CRC_MASK;
// For bidirectional DShot, the CRC is inverted // For bidirectional DShot, the CRC is inverted per specification.
if (_is_bidirectional) if (_is_bidirectional)
{ {
crc = (~crc) & DSHOT_CRC_MASK; crc = (~crc) & DSHOT_CRC_MASK;
@ -304,20 +295,59 @@ uint16_t DShotRMT::_calculateCRC(const uint16_t &data) const
return crc; 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];
}
void DShotRMT::_preCalculateRMTTicks() void DShotRMT::_preCalculateRMTTicks()
{ {
// Pre-calculate all timing values in RMT ticks to save CPU cycles later. // Pre-calculate all timing values in RMT ticks to save CPU cycles during operation.
_rmt_ticks.bit_length_ticks = static_cast<uint16_t>(_dshot_timing.bit_length_us * RMT_TICKS_PER_US); _rmt_ticks.bit_length_ticks = static_cast<uint16_t>(_dshot_timing.bit_length_us * RMT_TICKS_PER_US);
_rmt_ticks.t1h_ticks = static_cast<uint16_t>(_dshot_timing.t1h_lenght_us * RMT_TICKS_PER_US); _rmt_ticks.t1h_ticks = static_cast<uint16_t>(_dshot_timing.t1h_lenght_us * RMT_TICKS_PER_US);
_rmt_ticks.t0h_ticks = _rmt_ticks.t1h_ticks >> 1; // High time for a '0' bit is half of a '1' bit. _rmt_ticks.t0h_ticks = _rmt_ticks.t1h_ticks >> 1; // High time for a '0' bit is half of a '1' bit.
_rmt_ticks.t1l_ticks = _rmt_ticks.bit_length_ticks - _rmt_ticks.t1h_ticks; _rmt_ticks.t1l_ticks = _rmt_ticks.bit_length_ticks - _rmt_ticks.t1h_ticks;
_rmt_ticks.t0l_ticks = _rmt_ticks.bit_length_ticks - _rmt_ticks.t0h_ticks; _rmt_ticks.t0l_ticks = _rmt_ticks.bit_length_ticks - _rmt_ticks.t0h_ticks;
// Calculate the minimum time required between frames. // Calculate the minimum time required between frames to prevent signal collision.
// Pause between frames is frame time in us, some padding and about 30 us is added by hardware.
_frame_timer_us = (static_cast<uint64_t>(_dshot_timing.bit_length_us * DSHOT_BITS_PER_FRAME) << 1) + DSHOT_PADDING_US; _frame_timer_us = (static_cast<uint64_t>(_dshot_timing.bit_length_us * DSHOT_BITS_PER_FRAME) << 1) + DSHOT_PADDING_US;
// For bidirectional, double up.
if (_is_bidirectional) if (_is_bidirectional)
{ {
_frame_timer_us = (_frame_timer_us << 1); _frame_timer_us = (_frame_timer_us << 1);
@ -325,9 +355,9 @@ void DShotRMT::_preCalculateRMTTicks()
} }
// Private Frame Processing Functions // Private Frame Processing Functions
dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) dshot_result_t DShotRMT::_sendPacket(const dshot_packet_t &packet)
{ {
// Ensure enough time has passed since the last transmission // Ensure enough time has passed since the last transmission.
if (!_isFrameIntervalElapsed()) if (!_isFrameIntervalElapsed())
{ {
return {true, DSHOT_NONE}; return {true, DSHOT_NONE};
@ -335,9 +365,9 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet)
if (_is_bidirectional) if (_is_bidirectional)
{ {
// Start the receiver to wait for incoming telemetry data // Start the RMT receiver to wait for the ESC's telemetry response.
rmt_symbol_word_t rx_symbols[GCR_BITS_PER_FRAME]; rmt_symbol_word_t rx_symbols[DSHOT_TELEMETRY_FULL_GCR_BITS];
size_t rx_size_bytes = GCR_BITS_PER_FRAME * sizeof(rmt_symbol_word_t); size_t rx_size_bytes = DSHOT_TELEMETRY_FULL_GCR_BITS * sizeof(rmt_symbol_word_t);
rmt_receive_config_t rmt_rx_config = { rmt_receive_config_t rmt_rx_config = {
.signal_range_min_ns = DSHOT_PULSE_MIN_NS, .signal_range_min_ns = DSHOT_PULSE_MIN_NS,
@ -350,22 +380,21 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet)
} }
} }
// Now let's prepare the actual frame
_encoded_frame_value = _buildDShotFrameValue(packet); _encoded_frame_value = _buildDShotFrameValue(packet);
// Byte-swap the 16-bit value for correct transmission order (ESP32 is little-endian, DShot is MSB first) // Byte-swap the 16-bit value for correct transmission order.
// The RMT bytes encoder sends MSB of each byte first.
uint16_t swapped_value = __builtin_bswap16(_encoded_frame_value); uint16_t swapped_value = __builtin_bswap16(_encoded_frame_value);
// The DShot frame is 16 bits, which is 2 bytes // The DShot frame is 16 bits, which is 2 bytes.
size_t tx_size_bytes = sizeof(swapped_value); size_t tx_size_bytes = sizeof(swapped_value);
rmt_transmit_config_t tx_config = {}; // Initialize all members to zero rmt_transmit_config_t tx_config = {.loop_count = 0}; // No automatic loops.
tx_config.loop_count = 0; // No automatic loops - real-time calculation
// TODO: Find out, why this is needed // In bidirectional mode, the RMT RX channel must be disabled before transmitting
// to prevent the receiver from picking up the outgoing signal (loopback).
if (_is_bidirectional) if (_is_bidirectional)
{ {
// Disable RMT RX for sending
if (rmt_disable(_rmt_rx_channel) != DSHOT_OK) if (rmt_disable(_rmt_rx_channel) != DSHOT_OK)
{ {
return {false, DSHOT_RECEIVER_FAILED}; return {false, DSHOT_RECEIVER_FAILED};
@ -377,7 +406,7 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet)
return {false, DSHOT_TRANSMISSION_FAILED}; return {false, DSHOT_TRANSMISSION_FAILED};
} }
// Re-enable RMT RX // Re-enable RMT RX immediately after transmission to catch the response.
if (_is_bidirectional) if (_is_bidirectional)
{ {
if (rmt_enable(_rmt_rx_channel) != DSHOT_OK) if (rmt_enable(_rmt_rx_channel) != DSHOT_OK)
@ -386,20 +415,20 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet)
} }
} }
_recordFrameTransmissionTime(); // Reset the timer for the next frame _recordFrameTransmissionTime(); // Reset the timer for the next frame.
return {true, DSHOT_TRANSMISSION_SUCCESS}; return {true, DSHOT_TRANSMISSION_SUCCESS};
} }
// This function needs to be fast, as it generates the RMT symbols just before sending // This function is placed in IRAM for high performance, as it may be
// Placed in IRAM for high performance, as it's called from an ISR context. // called from an ISR context depending on RMT driver implementation.
uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) const uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) const
{ {
uint32_t gcr_value = 0; uint32_t gcr_value = 0;
// Decode RMT symbols into a 21-bit GCR (Group Code Recording) value. // 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. // The ESC sends back a signal where the duration of high vs. low 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; bool bit_is_one = symbols[i].duration0 > symbols[i].duration1;
gcr_value = (gcr_value << 1) | bit_is_one; gcr_value = (gcr_value << 1) | bit_is_one;
@ -408,34 +437,28 @@ uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols)
// Perform GCR decoding (GCR = Value ^ (Value >> 1)). // Perform GCR decoding (GCR = Value ^ (Value >> 1)).
uint32_t decoded_frame = gcr_value ^ (gcr_value >> 1); uint32_t decoded_frame = gcr_value ^ (gcr_value >> 1);
// Extract the 16-bit DShot frame from the decoded data. // Extract the 16-bit DShot-like frame from the decoded data.
uint16_t data_and_crc = (decoded_frame & DSHOT_FULL_PACKET); uint16_t data_and_crc = (decoded_frame & DSHOT_FULL_PACKET);
// Extract data and CRC from the 16-bit frame. // The eRPM telemetry frame consists of 12 data bits and 4 CRC bits.
uint16_t received_data = data_and_crc >> DSHOT_CRC_BIT_SHIFT; uint16_t received_data = data_and_crc >> DSHOT_CRC_BIT_SHIFT;
uint16_t received_crc = data_and_crc & DSHOT_CRC_MASK; uint16_t received_crc = data_and_crc & DSHOT_CRC_MASK;
// A valid response must have the telemetry request bit set to 1. This is a sanity check. // Calculate and validate the CRC for the received data.
if (!((received_data >> DSHOT_TELEMETRY_BIT_POSITION) & 1))
{
return DSHOT_NULL_PACKET;
}
// Calculate and validate CRC.
uint16_t calculated_crc = _calculateCRC(received_data); uint16_t calculated_crc = _calculateCRC(received_data);
if (received_crc != calculated_crc) if (received_crc != calculated_crc)
{ {
return DSHOT_NULL_PACKET; return DSHOT_NULL_PACKET;
} }
// Return the eRPM value (first 11 bits). // Return the eRPM value (the first 11 bits of the data).
return received_data & DSHOT_THROTTLE_MAX; return received_data & DSHOT_THROTTLE_MAX;
} }
// Timing Control Functions // Timing Control Functions
bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed() const bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed() const
{ {
// Check if the minimum interval between frames has passed // Check if the minimum interval between frames has passed.
uint64_t current_time = esp_timer_get_time(); uint64_t current_time = esp_timer_get_time();
uint64_t elapsed = current_time - _last_transmission_time_us; uint64_t elapsed = current_time - _last_transmission_time_us;
return elapsed >= _frame_timer_us; return elapsed >= _frame_timer_us;
@ -443,25 +466,109 @@ bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed() const
void DShotRMT::_recordFrameTransmissionTime() void DShotRMT::_recordFrameTransmissionTime()
{ {
// Record the time of the current transmission // Record the time of the current transmission.
_last_transmission_time_us = esp_timer_get_time(); _last_transmission_time_us = esp_timer_get_time();
} }
// Static Callback Functions // Static Callback Functions
// This function is called by the RMT driver's ISR when a frame is received // Processes a full telemetry frame from the RMT RX ISR.
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 a full telemetry frame.
}
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++;
}
}
}
// The gcr_decoded_bytes array now 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) 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); 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 (edata->num_symbols == DSHOT_TELEMETRY_FULL_GCR_BITS)
if (erpm != DSHOT_NULL_PACKET)
{ {
// Atomically store the new eRPM value and set the flag instance->_processFullTelemetryFrame(edata->received_symbols, edata->num_symbols);
instance->_last_erpm_atomic.store(erpm); }
instance->_telemetry_ready_flag_atomic.store(true); 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

@ -18,9 +18,10 @@
#include "dshot_definitions.h" #include "dshot_definitions.h"
#include "dshot_init.h" #include "dshot_init.h"
// Forward declaration for the RMT receive callback // DShotRMT Library Version
class DShotRMT; static constexpr uint8_t DSHOTRMT_MAJOR_VERSION = 0;
void IRAM_ATTR rmt_rx_done_callback(rmt_channel_handle_t rx_chan, const rmt_rx_done_event_data_t *edata, void *user_data); static constexpr uint8_t DSHOTRMT_MINOR_VERSION = 9;
static constexpr uint8_t DSHOTRMT_PATCH_VERSION = 5;
// DShot Protocol Constants // DShot Protocol Constants
static constexpr auto DSHOT_THROTTLE_FAILSAFE = 0; static constexpr auto DSHOT_THROTTLE_FAILSAFE = 0;
@ -29,10 +30,10 @@ static constexpr auto DSHOT_THROTTLE_FAILSAFE = 0;
class DShotRMT class DShotRMT
{ {
public: public:
// Constructor for DShotRMT. // Constructs a new DShotRMT object using a GPIO pin.
DShotRMT(gpio_num_t gpio, dshot_mode_t mode = DSHOT300, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); DShotRMT(gpio_num_t gpio, dshot_mode_t mode = DSHOT300, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT);
// Constructor using pin number // Constructs a new DShotRMT object using an Arduino-style integer pin number.
DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT);
// Destructor // Destructor
@ -56,8 +57,14 @@ public:
// Sends a DShot command to the ESC with a specified repeat count and delay. // Sends a DShot command to the ESC with a specified repeat count and delay.
dshot_result_t sendCommand(dshotCommands_e command, uint16_t repeat_count, uint16_t delay_us); dshot_result_t sendCommand(dshotCommands_e command, uint16_t repeat_count, uint16_t delay_us);
// Sends a raw DShot command to the ESC. /**
dshot_result_t sendRawCommand(uint16_t command_value); * @brief Sends a custom DShot command to the ESC. Advanced feature, use with caution.
* @param command_value The raw command value (0-47).
* @param repeat_count The number of times to send the command.
* @param delay_us The delay in microseconds between repetitions.
* @return dshot_result_t The result of the operation.
*/
dshot_result_t sendCustomCommand(uint16_t command_value, uint16_t repeat_count, uint16_t delay_us);
// Retrieves telemetry data from the ESC. // Retrieves telemetry data from the ESC.
dshot_result_t getTelemetry(); dshot_result_t getTelemetry();
@ -75,6 +82,7 @@ public:
uint16_t getEncodedFrameValue() const { return _encoded_frame_value; } uint16_t getEncodedFrameValue() const { return _encoded_frame_value; }
private: private:
dshot_result_t _sendRawDshotFrame(uint16_t value);
static bool IRAM_ATTR _on_rx_done(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data); static bool IRAM_ATTR _on_rx_done(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data);
// DShot Configuration Parameters // DShot Configuration Parameters
@ -95,30 +103,36 @@ private:
// DShot Frame Timing and State Variables // DShot Frame Timing and State Variables
uint64_t _last_transmission_time_us = 0; // Timestamp of the last DShot frame transmission uint64_t _last_transmission_time_us = 0; // Timestamp of the last DShot frame transmission
uint64_t _frame_timer_us = 0; // Minimum time required between DShot frames uint64_t _frame_timer_us = 0; // Minimum time required between DShot frames
float _percent_to_throttle_ratio = 0.0f; // Pre-calculated ratio for throttle percentage conversion
uint16_t _last_throttle = 0; // Last transmitted throttle value uint16_t _last_throttle = 0; // Last transmitted throttle value
dshot_packet_t _packet; // Current DShot packet being processed dshot_packet_t _packet; // Current DShot packet being processed
uint16_t _encoded_frame_value = 0; // Last encoded 16-bit DShot frame value uint16_t _encoded_frame_value = 0; // Last encoded 16-bit DShot frame value
uint64_t _last_command_timestamp = 0; // Timestamp of the last command sent
// Telemetry Related Variables // Telemetry Related Variables
std::atomic<uint16_t> _last_erpm_atomic = 0; // Atomically stored last received eRPM value 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<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_rx_event_callbacks_t _rx_event_callbacks = {
// RMT receive event callbacks // RMT receive event callbacks
.on_recv_done = _on_rx_done, .on_recv_done = _on_rx_done,
}; };
// Private Helper Functions for DShot Protocol Logic // Private Helper Functions for DShot Protocol Logic
bool _isValidCommand(dshotCommands_e command) const; // Checks if a given DShot command is valid 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)
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 _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
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 _preCalculateRMTTicks(); // Pre-calculates RMT timing ticks for the selected DShot mode void _extractTelemetryData(const uint8_t *raw_telemetry_bytes, dshot_telemetry_data_t &telemetry_data) const; // Extracts telemetry data from raw bytes
dshot_result_t _sendDShotFrame(const dshot_packet_t &packet); // Sends a DShot frame via RMT TX channel void _preCalculateRMTTicks(); // Pre-calculates RMT timing ticks for the selected DShot mode
uint16_t IRAM_ATTR _decodeDShotFrame(const rmt_symbol_word_t *symbols) const; // Decodes a received RMT symbol array into an eRPM value dshot_result_t _sendPacket(const dshot_packet_t &packet); // Sends a DShot frame via RMT TX channel
bool IRAM_ATTR _isFrameIntervalElapsed() const; // Checks if enough time has passed since the last frame transmission uint16_t IRAM_ATTR _decodeDShotFrame(const rmt_symbol_word_t *symbols) const; // Decodes a received RMT symbol array into an eRPM value
void _recordFrameTransmissionTime(); // Records the current time as the last frame transmission time 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
dshot_result_t _sendRepeatedCommand(uint16_t value, uint16_t repeat_count, uint16_t delay_us);
// Static Callback Function for RMT RX Events // Static Callback Function for RMT RX Events
void _cleanupRmtResources(); void _cleanupRmtResources();

View File

@ -2,7 +2,7 @@
* @file dshot_definitions.h * @file dshot_definitions.h
* @brief Defines DShot protocol constants, data structures, and command enums for DShotRMT library * @brief Defines DShot protocol constants, data structures, and command enums for DShotRMT library
* @author Wastl Kraus * @author Wastl Kraus
* @date 2025-10-04 * @date 2025-11-29
* @license MIT * @license MIT
*/ */
@ -11,36 +11,58 @@
#include <cstdint> #include <cstdint>
#include <driver/rmt_common.h> #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 // DShot Protocol Constants
// =================================================================================
// --- Frame Structure ---
static constexpr uint16_t DSHOT_BITS_PER_FRAME = 16; 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_TELEMETRY_BIT_POSITION = 11;
static constexpr uint16_t DSHOT_THROTTLE_MIN = 48; // Minimum throttle value for motor spin static constexpr uint16_t DSHOT_CRC_BIT_SHIFT = 4;
static constexpr uint16_t DSHOT_CMD_MIN = 0; // Minimum command value static constexpr uint16_t DSHOT_CRC_MASK = 0x000F;
static constexpr uint16_t DSHOT_CMD_MAX = 47; // Maximum command value
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
// Default motor magnet count for RPM calculation // --- Throttle & Command Values ---
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
static constexpr uint16_t DSHOT_CMD_MAX = 47; // Maximum command value
static constexpr uint16_t DSHOT_NULL_PACKET = 0;
static constexpr uint16_t DSHOT_FULL_PACKET = 0xFFFF;
static constexpr uint16_t DSHOT_TELEMETRY_INVALID = DSHOT_THROTTLE_MAX;
static constexpr uint16_t NO_DSHOT_TELEMETRY = 0;
// --- Command Behavior ---
static constexpr uint16_t DEFAULT_CMD_DELAY_US = 10;
static constexpr uint16_t DEFAULT_CMD_REPEAT_COUNT = 1;
static constexpr uint16_t SETTINGS_COMMAND_REPEATS = 10;
static constexpr uint16_t SETTINGS_COMMAND_DELAY_US = 5;
// =================================================================================
// DShot Telemetry Constants
// =================================================================================
// --- GCR Frame Structure ---
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 Payload Structure ---
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
// --- Motor Properties for RPM Calculation ---
static constexpr uint16_t DEFAULT_MOTOR_MAGNET_COUNT = 14; static constexpr uint16_t DEFAULT_MOTOR_MAGNET_COUNT = 14;
static constexpr uint16_t POLE_PAIRS_MIN = 1;
static constexpr uint16_t MAGNETS_PER_POLE_PAIR = 2;
// Defines the available DShot communication speeds.
enum dshot_mode_t : uint8_t
{
DSHOT_OFF,
DSHOT150,
DSHOT300,
DSHOT600,
DSHOT1200
};
// Represents the 16-bit DShot data packet sent to the ESC. // =================================================================================
typedef struct dshot_packet // DShot Timing Constants (Protocol Level)
{ // =================================================================================
uint16_t throttle_value : 11; // 11-bit throttle value or command. static constexpr uint16_t DSHOT_PADDING_US = 20; // Pause between frames
bool telemetric_request : 1; // 1-bit telemetry request flag.
uint16_t checksum : 4; // 4-bit CRC checksum.
} dshot_packet_t;
// Defines the bit length and high time for a '1' bit in microseconds for each DShot mode. // Defines the bit length and high time for a '1' bit in microseconds for each DShot mode.
typedef struct dshot_timing typedef struct dshot_timing
@ -49,6 +71,32 @@ typedef struct dshot_timing
double t1h_lenght_us; // High time duration for a '1' bit in microseconds. double t1h_lenght_us; // High time duration for a '1' bit in microseconds.
} dshot_timing_us_t; } dshot_timing_us_t;
// Timing parameters for each DShot mode
const dshot_timing_us_t DSHOT_TIMING_US[] = {
{0.00, 0.00}, // DSHOT_OFF
{6.67, 5.00}, // DSHOT150
{3.33, 2.50}, // DSHOT300
{1.67, 1.25}, // DSHOT600
{0.83, 0.67} // DSHOT1200
};
// =================================================================================
// ESP32 RMT Hardware Implementation Constants
// =================================================================================
// --- RMT Clock & Buffer Configuration ---
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_TX_BUFFER_SYMBOLS = 64;
static constexpr auto RMT_RX_BUFFER_SYMBOLS = DSHOT_TELEMETRY_FULL_GCR_BITS;
static constexpr auto RMT_QUEUE_DEPTH = 1;
// --- RMT Receiver Configuration ---
static constexpr uint32_t DSHOT_PULSE_MIN_NS = 800; // 0.8us minimum pulse duration for receiver
static constexpr uint32_t DSHOT_PULSE_MAX_NS = 8000; // 8.0us maximum pulse duration for receiver
// Stores pre-calculated timing values in RMT ticks for efficient signal generation. // Stores pre-calculated timing values in RMT ticks for efficient signal generation.
typedef struct rmt_ticks typedef struct rmt_ticks
{ {
@ -59,7 +107,43 @@ typedef struct rmt_ticks
uint16_t t0l_ticks; // Low time duration for a '0' bit in RMT ticks. uint16_t t0l_ticks; // Low time duration for a '0' bit in RMT ticks.
} rmt_ticks_t; } rmt_ticks_t;
// Enum class for specific error and success codes
// =================================================================================
// Library Data Structures & Enums
// =================================================================================
// --- DShot Modes ---
enum dshot_mode_t : uint8_t
{
DSHOT_OFF,
DSHOT150,
DSHOT300,
DSHOT600,
DSHOT1200
};
// --- DShot Packet Structure ---
// Represents the 16-bit DShot data packet sent to the ESC.
typedef struct dshot_packet
{
uint16_t throttle_value : 11; // 11-bit throttle value or command.
bool telemetric_request : 1; // 1-bit telemetry request flag.
uint16_t checksum : 4; // 4-bit CRC checksum.
} dshot_packet_t;
// --- Telemetry Data Structure ---
// 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;
// --- Library Result Codes & Structure ---
enum dshot_msg_code_t enum dshot_msg_code_t
{ {
DSHOT_NONE = 0, DSHOT_NONE = 0,
@ -86,6 +170,7 @@ enum dshot_msg_code_t
DSHOT_ENCODING_SUCCESS, DSHOT_ENCODING_SUCCESS,
DSHOT_TRANSMISSION_SUCCESS, DSHOT_TRANSMISSION_SUCCESS,
DSHOT_TELEMETRY_SUCCESS, DSHOT_TELEMETRY_SUCCESS,
DSHOT_TELEMETRY_DATA_AVAILABLE,
DSHOT_COMMAND_SUCCESS DSHOT_COMMAND_SUCCESS
}; };
@ -93,11 +178,14 @@ enum dshot_msg_code_t
typedef struct dshot_result typedef struct dshot_result
{ {
bool success; bool success;
dshot_msg_code_t result_code; // Specific error or success code. dshot_msg_code_t result_code; // Specific error or success code.
uint16_t erpm; // Electrical RPM (eRPM) if telemetry is successful. 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. 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; } dshot_result_t;
// --- DShot Commands ---
// Standard DShot commands by "betaflight" // Standard DShot commands by "betaflight"
enum dshotCommands_e enum dshotCommands_e
{ {
@ -131,41 +219,6 @@ enum dshotCommands_e
DSHOT_CMD_MAX_VALUE = 47 DSHOT_CMD_MAX_VALUE = 47
}; };
// Custom status codes // --- General Status & Helper Constants ---
static constexpr int DSHOT_OK = 0; static constexpr int DSHOT_OK = 0;
static constexpr int DSHOT_ERROR = 1; static constexpr float CONVERSION_FACTOR_MILLI_TO_UNITS = 1000.0f;
// Configuration Constants
static constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000;
static constexpr auto DSHOT_FULL_PACKET = 0b1111111111111111;
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 DSHOT_RX_TIMEOUT_MS = 2;
static constexpr auto DSHOT_PADDING_US = 20; // Pause between frames
static constexpr auto RMT_BUFFER_SYMBOLS = 64;
static constexpr auto RMT_QUEUE_DEPTH = 1;
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;
static constexpr auto DSHOT_PULSE_MIN_NS = 800; // 0.8us minimum pulse
static constexpr auto DSHOT_PULSE_MAX_NS = 8000; // 8.0us maximum pulse
static constexpr auto DSHOT_TELEMETRY_INVALID = DSHOT_THROTTLE_MAX;
static constexpr auto DSHOT_TELEMETRY_BIT_POSITION = 11;
static constexpr auto DSHOT_CRC_BIT_SHIFT = 4;
// Command Constants
static constexpr auto DEFAULT_CMD_DELAY_US = 10;
static constexpr auto DEFAULT_CMD_REPEAT_COUNT = 1;
static constexpr auto SETTINGS_COMMAND_REPEATS = 10;
static constexpr auto SETTINGS_COMMAND_DELAY_US = 5;
// Timing parameters for each DShot mode
const dshot_timing_us_t DSHOT_TIMING_US[] = {
{0.00, 0.00}, // DSHOT_OFF
{6.67, 5.00}, // DSHOT150
{3.33, 2.50}, // DSHOT300
{1.67, 1.25}, // DSHOT600
{0.83, 0.67} // DSHOT1200
};

View File

@ -15,11 +15,11 @@ dshot_result_t init_rmt_tx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_ch
.gpio_num = gpio, .gpio_num = gpio,
.clk_src = DSHOT_CLOCK_SRC_DEFAULT, .clk_src = DSHOT_CLOCK_SRC_DEFAULT,
.resolution_hz = DSHOT_RMT_RESOLUTION, .resolution_hz = DSHOT_RMT_RESOLUTION,
.mem_block_symbols = RMT_BUFFER_SYMBOLS, .mem_block_symbols = RMT_TX_BUFFER_SYMBOLS,
.trans_queue_depth = RMT_QUEUE_DEPTH, .trans_queue_depth = RMT_QUEUE_DEPTH,
.flags = { .flags = {
.invert_out = is_bidirectional ? 1 : 0, .invert_out = is_bidirectional ? 1 : 0,
.init_level = is_bidirectional ? 0 : 1}}; .init_level = 0}};
rmt_transmit_config_t rmt_tx_config = {}; // Initialize all members to zero rmt_transmit_config_t rmt_tx_config = {}; // Initialize all members to zero
rmt_tx_config.loop_count = 0; // No automatic loops - real-time calculation rmt_tx_config.loop_count = 0; // No automatic loops - real-time calculation
@ -44,7 +44,7 @@ dshot_result_t init_rmt_rx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_ch
.gpio_num = gpio, .gpio_num = gpio,
.clk_src = DSHOT_CLOCK_SRC_DEFAULT, .clk_src = DSHOT_CLOCK_SRC_DEFAULT,
.resolution_hz = DSHOT_RMT_RESOLUTION, .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) if (rmt_new_rx_channel(&rx_channel_config, out_channel) != DSHOT_OK)

View File

@ -9,9 +9,8 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
#include "dshot_definitions.h"
// Forward declaration of the DShotRMT class to break circular dependency #include "DShotRMT.h"
class DShotRMT;
// Error Messages // Error Messages
static constexpr char NONE[] = ""; static constexpr char NONE[] = "";
@ -28,9 +27,9 @@ static constexpr char ENCODING_SUCCESS[] = "Packet encoded successfully";
static constexpr char TRANSMISSION_SUCCESS[] = "Transmission successfully"; static constexpr char TRANSMISSION_SUCCESS[] = "Transmission successfully";
static constexpr char TRANSMISSION_FAILED[] = "Transmission failed!"; static constexpr char TRANSMISSION_FAILED[] = "Transmission failed!";
static constexpr char RECEIVER_FAILED[] = "RMT receiver failed!"; static constexpr char RECEIVER_FAILED[] = "RMT receiver failed!";
static constexpr char THROTTLE_NOT_IN_RANGE[] = "Throttle not in range! (48 - 2047)"; static constexpr char THROTTLE_NOT_IN_RANGE[] = "Throttle not in range!";
static constexpr char PERCENT_NOT_IN_RANGE[] = "Percent not in range! (0.0 - 100.0)"; static constexpr char PERCENT_NOT_IN_RANGE[] = "Percent not in range!";
static constexpr char COMMAND_NOT_VALID[] = "Command not valid! (0 - 47)"; static constexpr char COMMAND_NOT_VALID[] = "Command not valid!";
static constexpr char BIDIR_NOT_ENABLED[] = "Bidirectional DShot not enabled!"; static constexpr char BIDIR_NOT_ENABLED[] = "Bidirectional DShot not enabled!";
static constexpr char TELEMETRY_SUCCESS[] = "Valid Telemetric Frame received!"; static constexpr char TELEMETRY_SUCCESS[] = "Valid Telemetric Frame received!";
static constexpr char TELEMETRY_FAILED[] = "No valid Telemetric Frame received!"; static constexpr char TELEMETRY_FAILED[] = "No valid Telemetric Frame received!";
@ -100,6 +99,24 @@ inline const char *get_result_code_str(dshot_msg_code_t code)
} }
} }
// Helper to get DShot mode string
inline const char *get_dshot_mode_str(dshot_mode_t mode)
{
switch (mode)
{
case DSHOT150:
return "DSHOT150";
case DSHOT300:
return "DSHOT300";
case DSHOT600:
return "DSHOT600";
case DSHOT1200:
return "DSHOT1200";
default:
return "DSHOT_OFF";
}
}
// Helper to quick print DShot result codes // Helper to quick print DShot result codes
inline void printDShotResult(dshot_result_t &result, Stream &output = Serial) inline void printDShotResult(dshot_result_t &result, Stream &output = Serial)
{ {
@ -115,37 +132,61 @@ inline void printDShotResult(dshot_result_t &result, Stream &output = Serial)
} }
// Helper to print DShot signal info // 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);
uint16_t dshot_mode_val = 0; output.printf("Mode: %s\n", get_dshot_mode_str(dshot_rmt.getMode()));
switch (dshot_rmt.getMode())
{
case DSHOT150:
dshot_mode_val = 150;
break;
case DSHOT300:
dshot_mode_val = 300;
break;
case DSHOT600:
dshot_mode_val = 600;
break;
case DSHOT1200:
dshot_mode_val = 1200;
break;
}
output.printf("Current Mode: DSHOT%d\n", dshot_mode_val);
output.printf("Bidirectional: %s\n", dshot_rmt.isBidirectional() ? "YES" : "NO"); 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) for (int i = DSHOT_BITS_PER_FRAME - 1; i >= 0; --i)
{ {
output.print((dshot_rmt.getEncodedFrameValue() >> i) & 1); 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 / CONVERSION_FACTOR_MILLI_TO_UNITS, // Convert mV to V
(float)telemetry_result.telemetry_data.current / CONVERSION_FACTOR_MILLI_TO_UNITS, // 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 // Helper to print CPU info