prepare release 0.9.5

This commit is contained in:
Wastl Kraus 2025-11-29 15:21:54 +01:00
parent 0e1e0b954d
commit d241ff8a78
10 changed files with 379 additions and 399 deletions

View File

@ -6,7 +6,11 @@
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. 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.
### Enhanced Bidirectional DShot with Full Telemetry Support. ### ✨ 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.
@ -34,7 +38,7 @@ The library is architected around a single C++ class, `DShotRMT`. It abstracts t
1. **Signal Generation (TX):** The library uses an RMT 'bytes_encoder'. This encoder is configured with the specific pulse durations for DShot '0' and '1' bits based on the selected speed (e.g., DSHOT300, DSHOT600). When a user calls `sendThrottle()`, the library constructs a 16-bit DShot frame (11-bit throttle, 1-bit telemetry request, 4-bit CRC) and hands it to the RMT encoder. The RMT hardware then autonomously generates the correct electrical signal on the specified GPIO pin. 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:** 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. 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

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);
@ -237,16 +219,13 @@ void setupOTA()
{ 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");
} }
@ -267,7 +247,8 @@ 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

@ -1,5 +1,5 @@
name=DShotRMT name=DShotRMT
version=0.9.2 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

@ -9,27 +9,6 @@
#include "DShotRMT.h" #include "DShotRMT.h"
#include <cstring> #include <cstring>
// Configuration Constants
static constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000;
static constexpr auto DSHOT_FULL_PACKET = 0b1111111111111111;
static constexpr auto DSHOT_RX_TIMEOUT_MS = 2;
static constexpr auto DSHOT_PADDING_US = 20; // Pause between frames
static constexpr auto 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;
// 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)
: _gpio(gpio), : _gpio(gpio),
@ -38,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
@ -90,15 +70,14 @@ 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)
{ {
// just to be sure
_last_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);
@ -113,15 +92,15 @@ dshot_result_t DShotRMT::sendThrottlePercent(float percent)
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) / DSHOT_PERCENT_MAX) * 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};
@ -135,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:
@ -145,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;
} }
@ -155,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)
{ {
@ -205,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;
} }
} }
@ -233,60 +195,60 @@ 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);
} }
dshot_result_t DShotRMT::sendCustomCommand(uint16_t command_value, uint16_t repeat_count, uint16_t delay_us) dshot_result_t DShotRMT::sendCustomCommand(uint16_t command_value, uint16_t repeat_count, uint16_t delay_us)
{ {
// Validate the integer command value before casting // Validate the integer command value.
if (command_value < DSHOT_CMD_MOTOR_STOP || command_value > DSHOT_CMD_MAX_VALUE) if (command_value < DSHOT_CMD_MIN || command_value > DSHOT_CMD_MAX)
{ {
return {false, DSHOT_COMMAND_NOT_VALID}; return {false, DSHOT_COMMAND_NOT_VALID};
} }
return _sendRepeatedCommand(command_value, repeat_count, delay_us);
}
dshot_result_t result = {false, DSHOT_UNKNOWN, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY}; // Writes settings to the ESC's non-volatile memory; use with caution.
dshot_result_t DShotRMT::saveESCSettings()
{
return sendCommand(dshotCommands_e::DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US);
}
// 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; bool all_successful = true;
dshot_result_t last_result = {true, DSHOT_COMMAND_SUCCESS};
// Send command multiple times with delay
for (uint16_t i = 0; i < repeat_count; i++) for (uint16_t i = 0; i < repeat_count; i++)
{ {
dshot_result_t single_result = _sendRawDshotFrame(command_value); last_result = _sendRawDshotFrame(value);
if (!single_result.success) if (!last_result.success)
{ {
all_successful = false; all_successful = false;
result.result_code = single_result.result_code;
break; break;
} }
// Add delay between repetitions (except for last repetition)
if (i < repeat_count - 1) if (i < repeat_count - 1)
{ {
delayMicroseconds(delay_us); delayMicroseconds(delay_us);
} }
} }
result.success = all_successful; if (all_successful)
if (result.success)
{ {
result.result_code = DSHOT_COMMAND_SUCCESS; return {true, DSHOT_COMMAND_SUCCESS};
} }
else
return result;
}
// Use with caution
dshot_result_t DShotRMT::saveESCSettings()
{ {
return sendCommand(dshotCommands_e::DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); // Return the result from the failed transmission.
return last_result;
}
} }
// Simple check // 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);
@ -298,20 +260,6 @@ dshot_result_t DShotRMT::_sendRawDshotFrame(uint16_t value)
return _sendPacket(_packet); return _sendPacket(_packet);
} }
// Executes a single DShot command by building and sending a DShot frame.
dshot_result_t DShotRMT::_executeCommand(dshotCommands_e command)
{
uint64_t start_time = esp_timer_get_time();
_packet = _buildDShotPacket(static_cast<uint16_t>(command));
dshot_result_t result = _sendPacket(_packet);
uint64_t end_time = esp_timer_get_time();
_last_command_timestamp = end_time;
return result;
}
// Private Packet Management Functions // Private Packet Management Functions
dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t &value) const dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t &value) const
{ {
@ -320,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);
@ -329,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;
@ -357,7 +305,7 @@ uint8_t DShotRMT::_calculateTelemetryCRC(const uint8_t *data, size_t len) const
{ {
if (crc & 0x80) if (crc & 0x80)
{ {
crc = (crc << 1) ^ 0x07; // DSHOT telemetry uses CRC-8 with polynomial 0x07 crc = (crc << 1) ^ 0x07; // DSHOT telemetry uses CRC-8 with polynomial 0x07.
} }
else else
{ {
@ -370,7 +318,7 @@ uint8_t DShotRMT::_calculateTelemetryCRC(const uint8_t *data, size_t len) const
void DShotRMT::_extractTelemetryData(const uint8_t *raw_telemetry_bytes, dshot_telemetry_data_t &telemetry_data) const 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 // Ensure the telemetry_data struct is cleared before filling.
memset(&telemetry_data, 0, sizeof(dshot_telemetry_data_t)); memset(&telemetry_data, 0, sizeof(dshot_telemetry_data_t));
// Telemetry data is typically ordered as: // Telemetry data is typically ordered as:
@ -386,26 +334,20 @@ void DShotRMT::_extractTelemetryData(const uint8_t *raw_telemetry_bytes, dshot_t
telemetry_data.current = (static_cast<uint16_t>(raw_telemetry_bytes[3]) << 8) | raw_telemetry_bytes[4]; 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.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]; telemetry_data.rpm = (static_cast<uint16_t>(raw_telemetry_bytes[7]) << 8) | raw_telemetry_bytes[8];
// Error flags/count can be derived from other parts of the telemetry data if available,
// or set based on CRC check result. For now, we leave telemetry_data.errors to 0
// or handle it implicitly through the success/failure of the CRC check.
} }
void DShotRMT::_preCalculateRMTTicks() 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);
@ -415,7 +357,7 @@ void DShotRMT::_preCalculateRMTTicks()
// Private Frame Processing Functions // Private Frame Processing Functions
dshot_result_t DShotRMT::_sendPacket(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};
@ -423,7 +365,7 @@ dshot_result_t DShotRMT::_sendPacket(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[DSHOT_TELEMETRY_FULL_GCR_BITS]; rmt_symbol_word_t rx_symbols[DSHOT_TELEMETRY_FULL_GCR_BITS];
size_t rx_size_bytes = DSHOT_TELEMETRY_FULL_GCR_BITS * sizeof(rmt_symbol_word_t); size_t rx_size_bytes = DSHOT_TELEMETRY_FULL_GCR_BITS * sizeof(rmt_symbol_word_t);
@ -438,23 +380,21 @@ dshot_result_t DShotRMT::_sendPacket(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 = {.loop_count = 0}; // No automatic loops - real-time calculation rmt_transmit_config_t tx_config = {.loop_count = 0}; // No automatic loops.
// In bidirectional mode, the RMT RX channel must be disabled before transmitting. // In bidirectional mode, the RMT RX channel must be disabled before transmitting
// This is to prevent the receiver from picking up the transmitted signal, which would cause a loopback issue. // to prevent the receiver from picking up the outgoing signal (loopback).
// The ESP32's RMT peripheral TX and RX channels on the same pin are not fully independent.
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};
@ -466,7 +406,7 @@ dshot_result_t DShotRMT::_sendPacket(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)
@ -475,19 +415,19 @@ dshot_result_t DShotRMT::_sendPacket(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 < DSHOT_ERPM_FRAME_GCR_BITS; ++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;
@ -497,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;
@ -532,20 +466,20 @@ 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
// Processes a full telemetry frame // 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) void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *symbols, size_t num_symbols)
{ {
if (num_symbols != DSHOT_TELEMETRY_FULL_GCR_BITS) if (num_symbols != DSHOT_TELEMETRY_FULL_GCR_BITS)
{ {
return; // Incorrect number of symbols for full telemetry 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 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)); memset(gcr_decoded_bytes, 0, sizeof(gcr_decoded_bytes));
uint8_t data_bit_idx = 0; uint8_t data_bit_idx = 0;
@ -560,62 +494,29 @@ void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *sym
} }
} }
uint8_t decoded_nibble; // 4 data bits uint8_t decoded_nibble; // 4 data bits.
switch (gcr_group_5bits) switch (gcr_group_5bits)
{ {
case 0b11110: case 0b11110: decoded_nibble = 0b0000; break;
decoded_nibble = 0b0000; case 0b01001: decoded_nibble = 0b0001; break;
break; case 0b10100: decoded_nibble = 0b0010; break;
case 0b01001: case 0b10101: decoded_nibble = 0b0011; break;
decoded_nibble = 0b0001; case 0b01010: decoded_nibble = 0b0100; break;
break; case 0b01011: decoded_nibble = 0b0101; break;
case 0b10100: case 0b01110: decoded_nibble = 0b0110; break;
decoded_nibble = 0b0010; case 0b01111: decoded_nibble = 0b0111; break;
break; case 0b10010: decoded_nibble = 0b1000; break;
case 0b10101: case 0b10011: decoded_nibble = 0b1001; break;
decoded_nibble = 0b0011; case 0b10110: decoded_nibble = 0b1010; break;
break; case 0b10111: decoded_nibble = 0b1011; break;
case 0b01010: case 0b11010: decoded_nibble = 0b1100; break;
decoded_nibble = 0b0100; case 0b11011: decoded_nibble = 0b1101; break;
break; case 0b11100: decoded_nibble = 0b1110; break;
case 0b01011: case 0b11101: decoded_nibble = 0b1111; break;
decoded_nibble = 0b0101; default: return; // Invalid GCR group, discard frame.
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 // Place the 4 decoded bits into the data_bytes array.
for (int k = 3; k >= 0; --k) for (int k = 3; k >= 0; --k)
{ {
if (data_bit_idx < (DSHOT_TELEMETRY_FRAME_LENGTH_BITS + DSHOT_TELEMETRY_CRC_LENGTH_BITS)) if (data_bit_idx < (DSHOT_TELEMETRY_FRAME_LENGTH_BITS + DSHOT_TELEMETRY_CRC_LENGTH_BITS))
@ -631,7 +532,7 @@ void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *sym
} }
} }
// Now gcr_decoded_bytes contains the 10 telemetry bytes + 1 CRC byte. // The gcr_decoded_bytes array now contains the 10 telemetry bytes + 1 CRC byte.
// Perform CRC validation. // Perform CRC validation.
uint8_t received_crc = gcr_decoded_bytes[DSHOT_TELEMETRY_FRAME_LENGTH_BYTES]; 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); uint8_t calculated_crc = _calculateTelemetryCRC(gcr_decoded_bytes, DSHOT_TELEMETRY_FRAME_LENGTH_BYTES);
@ -639,7 +540,7 @@ void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *sym
if (received_crc == calculated_crc) if (received_crc == calculated_crc)
{ {
dshot_telemetry_data_t telemetry_data; dshot_telemetry_data_t telemetry_data;
// Extract from the first 10 bytes (excluding the CRC byte) // Extract from the first 10 bytes (excluding the CRC byte).
_extractTelemetryData(gcr_decoded_bytes, telemetry_data); _extractTelemetryData(gcr_decoded_bytes, telemetry_data);
_last_telemetry_data_atomic.store(telemetry_data); _last_telemetry_data_atomic.store(telemetry_data);
@ -647,7 +548,7 @@ void IRAM_ATTR DShotRMT::_processFullTelemetryFrame(const rmt_symbol_word_t *sym
} }
} }
// This function is called by the RMT driver's ISR when a frame is received // 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);
@ -664,7 +565,7 @@ bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const
if (erpm != DSHOT_NULL_PACKET) if (erpm != DSHOT_NULL_PACKET)
{ {
// Atomically store the new eRPM value and set the flag // Atomically store the new eRPM value and set the flag.
instance->_last_erpm_atomic.store(erpm); instance->_last_erpm_atomic.store(erpm);
instance->_telemetry_ready_flag_atomic.store(true); instance->_telemetry_ready_flag_atomic.store(true);
} }

View File

@ -21,7 +21,7 @@
// DShotRMT Library Version // DShotRMT Library Version
static constexpr uint8_t DSHOTRMT_MAJOR_VERSION = 0; static constexpr uint8_t DSHOTRMT_MAJOR_VERSION = 0;
static constexpr uint8_t DSHOTRMT_MINOR_VERSION = 9; static constexpr uint8_t DSHOTRMT_MINOR_VERSION = 9;
static constexpr uint8_t DSHOTRMT_PATCH_VERSION = 2; 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;
@ -30,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
@ -103,10 +103,10 @@ 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
@ -120,7 +120,6 @@ private:
// 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
@ -133,6 +132,8 @@ private:
bool IRAM_ATTR _isFrameIntervalElapsed() const; // Checks if enough time has passed since the last frame transmission 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 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,47 +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_TELEMETRY_BIT_POSITION = 11;
static constexpr uint16_t DSHOT_CRC_BIT_SHIFT = 4;
static constexpr uint16_t DSHOT_CRC_MASK = 0x000F;
// --- Throttle & Command Values ---
static constexpr uint16_t DSHOT_THROTTLE_MAX = 2047; // Maximum throttle value (0-2047) static constexpr uint16_t DSHOT_THROTTLE_MAX = 2047; // Maximum throttle value (0-2047)
static constexpr uint16_t DSHOT_THROTTLE_MIN = 48; // Minimum throttle value for motor spin static constexpr uint16_t DSHOT_THROTTLE_MIN = 48; // Minimum throttle value for motor spin
static constexpr float DSHOT_PERCENT_MIN = 0.0f; static constexpr float DSHOT_PERCENT_MIN = 0.0f;
static constexpr float DSHOT_PERCENT_MAX = 100.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_MIN = 0; // Minimum command value
static constexpr uint16_t DSHOT_CMD_MAX = 47; // Maximum command value 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_NULL_PACKET = 0;
static constexpr uint16_t DSHOT_CRC_MASK = 0x000F; // Bit mask for CRC bits 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;
// GCR frame definitions // --- 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_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) static constexpr uint16_t DSHOT_TELEMETRY_FULL_GCR_BITS = 110; // GCR bits for a full 10-byte telemetry frame (80 data bits + 8 CRC bits = 88 bits, 88 * 5/4 = 110 GCR bits)
// Telemetry frame definitions // --- 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_BITS = 80; // 10 bytes * 8 bits/byte
static constexpr uint16_t DSHOT_TELEMETRY_FRAME_LENGTH_BYTES = 10; 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 static constexpr uint16_t DSHOT_TELEMETRY_CRC_LENGTH_BITS = 8; // 8-bit CRC for telemetry
// Default motor magnet count for RPM calculation // --- 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
@ -60,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
{ {
@ -70,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,
@ -101,17 +174,6 @@ enum dshot_msg_code_t
DSHOT_COMMAND_SUCCESS DSHOT_COMMAND_SUCCESS
}; };
// Structure for decoded DShot telemetry data (from ESC)
typedef struct dshot_telemetry_data
{
uint16_t rpm; // Motor RPM
uint16_t voltage; // Voltage in mV
uint16_t current; // Current in mA
uint16_t consumption; // Consumption in mAh
int8_t temperature; // Temperature in Celsius
uint8_t errors; // Error flags / count
} dshot_telemetry_data_t;
// Contains the success status, an error code, and optional telemetry data. // Contains the success status, an error code, and optional telemetry data.
typedef struct dshot_result typedef struct dshot_result
{ {
@ -123,6 +185,7 @@ typedef struct dshot_result
bool telemetry_available; // Flag to indicate if telemetry_data is valid 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
{ {
@ -156,23 +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_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;
// 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

@ -19,7 +19,7 @@ dshot_result_t init_rmt_tx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_ch
.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

View File

@ -167,8 +167,8 @@ inline void printDShotInfo(DShotRMT &dshot_rmt, Stream &output = Serial)
output.println(" --- Full Telemetry Details ---"); output.println(" --- Full Telemetry Details ---");
output.printf(" Temp: %d C | Volt: %.2f V | Curr: %.2f A | Cons: %u mAh\n", output.printf(" Temp: %d C | Volt: %.2f V | Curr: %.2f A | Cons: %u mAh\n",
telemetry_result.telemetry_data.temperature, telemetry_result.telemetry_data.temperature,
(float)telemetry_result.telemetry_data.voltage / 1000.0f, // Convert mV to V (float)telemetry_result.telemetry_data.voltage / CONVERSION_FACTOR_MILLI_TO_UNITS, // Convert mV to V
(float)telemetry_result.telemetry_data.current / 1000.0f, // Convert mA to A (float)telemetry_result.telemetry_data.current / CONVERSION_FACTOR_MILLI_TO_UNITS, // Convert mA to A
telemetry_result.telemetry_data.consumption); telemetry_result.telemetry_data.consumption);
output.printf(" Telemetry RPM: %u\n", telemetry_result.telemetry_data.rpm); output.printf(" Telemetry RPM: %u\n", telemetry_result.telemetry_data.rpm);
} }