commit
6e80e6ad3c
42
README.md
42
README.md
|
|
@ -4,9 +4,13 @@
|
|||
[](https://www.arduinolibraries.com/libraries/dshot-rmt)
|
||||
[](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.
|
||||
|
||||
|
|
@ -14,20 +18,28 @@ An Arduino IDE library for generating DShot signals on ESP32 microcontrollers us
|
|||
|
||||
### DShot300 Example Output
|
||||
|
||||
Here's an example of the output from the `dshot300` example sketch:
|
||||
Here's an example of the output from the `dshot300` example sketch, now showing full telemetry:
|
||||
|
||||

|
||||
|
||||
## 🚀 Core Features
|
||||
|
||||
- **Multiple DShot Modes:** Supports DSHOT150, DSHOT300, DSHOT600, and DSHOT1200.
|
||||
- **Bidirectional DShot Support:** Implemented, but note that official support is limited due to potential instability and external hardware requirements. Use with caution (and pull-up).
|
||||
- **Robust Bidirectional DShot Support:** Now features full GCR-dekodierte telemetry data (temperature, voltage, current, consumption, and RPM) from the ESC. The library automatically differentiates between eRPM-only and full telemetry frames. This significantly enhances feedback capabilities for advanced applications.
|
||||
- **Hardware-Timed Signals:** Precise signal generation using the ESP32 RMT peripheral, ensuring stable and reliable motor control.
|
||||
- **Simple API:** Easy-to-use C++ class with intuitive methods like `sendThrottlePercent()`.
|
||||
- **Error Handling:** Provides detailed feedback on operation success or failure via `dshot_result_t`.
|
||||
- **Enhanced Error Handling:** Provides detailed feedback on operation success or failure via an enhanced `dshot_result_t` struct, now including specific error codes, eRPM data, and a `dshot_telemetry_data_t` struct for full GCR-decoded telemetry.
|
||||
- **Lightweight:** The core library has no external dependencies.
|
||||
- **Arduino and ESP-IDF Compatible:** Can be used in both Arduino and ESP-IDF projects.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
const gpio_num_t MOTOR_PIN = GPIO_NUM_27;
|
||||
|
||||
// Create a DShotRMT instance for DSHOT300
|
||||
DShotRMT motor(MOTOR_PIN, DSHOT300);
|
||||
// Create a DShotRMT instance for DSHOT300 with bidirectional telemetry enabled
|
||||
DShotRMT motor(MOTOR_PIN, DSHOT300, true);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
|
@ -83,7 +95,7 @@ void loop() {
|
|||
Serial.println("Stopping motor.");
|
||||
motor.sendThrottlePercent(0);
|
||||
|
||||
// Print DShot Info
|
||||
// Print DShot Info, which now includes detailed telemetry
|
||||
printDShotInfo(motor, Serial);
|
||||
|
||||
// Take a break before next bench run
|
||||
|
|
@ -96,7 +108,7 @@ void loop() {
|
|||
The `examples` folder contains more advanced examples:
|
||||
|
||||
- **`throttle_percent`:** A focused example showing how to control motor speed using percentage values (0-100) via the serial monitor.
|
||||
- **`dshot300`:** A more advanced example demonstrating how to send raw DShot commands and receive telemetry via the serial monitor.
|
||||
- **`dshot300`:** A more advanced example demonstrating how to send raw DShot commands and **receive comprehensive telemetry** via the serial monitor.
|
||||
- **`web_control`:** A full-featured web application for controlling a motor from a web browser. It creates a WiFi access point and serves a web page with a throttle slider and arming switch.
|
||||
- **`web_client`:** A variation of the `web_control` example that connects to an existing WiFi network instead of creating its own access point.
|
||||
|
||||
|
|
@ -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, 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)`.
|
||||
- `getTelemetry(uint16_t magnet_count = 0)`: Retrieves telemetry data from the ESC. If `magnet_count` is 0, uses the stored motor magnet count.
|
||||
- `getESCInfo()`: Sends a command to the ESC to request ESC information.
|
||||
- `sendCustomCommand(uint16_t command_value, uint16_t repeat_count, uint16_t delay_us)`: Sends a custom DShot command to the ESC. Advanced feature, use with caution.
|
||||
- `getTelemetry()`: Retrieves telemetry data from the ESC. If bidirectional DShot is enabled, this function now returns a comprehensive `dshot_result_t` containing both eRPM and a fully GCR-decoded `dshot_telemetry_data_t` struct (temperature, voltage, current, consumption, RPM) if available.
|
||||
- `setMotorSpinDirection(bool reversed)`: Sets the motor spin direction. `true` for reversed, `false` for normal.
|
||||
- `saveESCSettings()`: Sends a command to the ESC to save its current settings. Use with caution as this writes to ESC's non-volatile memory.
|
||||
- `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.
|
||||
- `isBidirectional()`: Checks if bidirectional DShot is enabled.
|
||||
- `getEncodedFrameValue()`: Gets the last encoded DShot frame value.
|
||||
- `getThrottleValue()`: Gets the last transmitted throttle value.
|
||||
- `getEncodedFrameValue()`: Gets the last encoded DShot frame value.
|
||||
|
||||
## ⚙️ 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/)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ static constexpr dshot_mode_t DSHOT_MODE = DSHOT300;
|
|||
|
||||
// BiDirectional DShot Support (default: false)
|
||||
// re-enabled for testing
|
||||
static constexpr auto IS_BIDIRECTIONAL = false;
|
||||
static constexpr auto IS_BIDIRECTIONAL = true;
|
||||
|
||||
// Motor magnet count for RPM calculation
|
||||
// static constexpr auto MOTOR01_MAGNET_COUNT = 14;
|
||||
|
|
@ -65,7 +65,7 @@ void loop()
|
|||
|
||||
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);
|
||||
|
||||
USB_SERIAL.println(" ");
|
||||
|
||||
// Get Motor RPM if bidirectional
|
||||
if (IS_BIDIRECTIONAL)
|
||||
{
|
||||
dshot_result_t telem_result = motor01.getTelemetry();
|
||||
printDShotResult(telem_result);
|
||||
// dshot_result_t telem_result = motor01.getTelemetry();
|
||||
// printDShotResult(telem_result);
|
||||
}
|
||||
|
||||
USB_SERIAL.println("Type 'help' to show Menu");
|
||||
|
|
@ -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")
|
||||
{
|
||||
// Stop motor
|
||||
throttle = 0;
|
||||
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);
|
||||
}
|
||||
else if (input == "info")
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@
|
|||
#include <DShotRMT.h>
|
||||
#include <WiFi.h>
|
||||
#include <Update.h>
|
||||
#include "web_utilities/ota_update.h"
|
||||
#include "web_utilities/web_content.h"
|
||||
#include "../web_utilities/ota_update.h"
|
||||
#include "../web_utilities/web_content.h"
|
||||
#include "../web_utilities/web_constants.h"
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <AsyncTCP.h>
|
||||
|
|
@ -30,31 +31,12 @@
|
|||
static constexpr auto *WIFI_SSID = "YOUR_SSID"; // Enter your WiFi SSID
|
||||
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
|
||||
DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, MOTOR01_MAGNET_COUNT);
|
||||
|
||||
// Web Server Configuration
|
||||
AsyncWebServer server(80);
|
||||
AsyncWebSocket ws("/ws");
|
||||
AsyncWebServer server(WEBSERVER_PORT);
|
||||
AsyncWebSocket ws(WEBSOCKET_PATH);
|
||||
|
||||
// Global variables
|
||||
static uint16_t throttle = DSHOT_CMD_MOTOR_STOP;
|
||||
|
|
@ -130,7 +112,7 @@ void loop()
|
|||
static uint64_t last_wifi_check = 0;
|
||||
|
||||
// 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)
|
||||
{
|
||||
|
|
@ -169,7 +151,7 @@ void loop()
|
|||
}
|
||||
|
||||
// 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);
|
||||
|
||||
|
|
@ -204,9 +186,9 @@ void loop()
|
|||
{
|
||||
// Generate JSON for Webserver
|
||||
JsonDocument doc;
|
||||
doc["throttle"] = isArmed ? throttle : 0;
|
||||
doc["armed"] = isArmed;
|
||||
doc["rpm"] = current_rpm;
|
||||
doc[JsonKey::THROTTLE] = isArmed ? throttle : 0;
|
||||
doc[JsonKey::ARMED] = isArmed;
|
||||
doc[JsonKey::RPM] = current_rpm;
|
||||
|
||||
String json_output;
|
||||
json_output.reserve(256);
|
||||
|
|
@ -237,16 +219,13 @@ void setupOTA()
|
|||
{ request->send_P(200, "text/html", ota_html); });
|
||||
|
||||
// Handle OTA update upload
|
||||
server.on("/update", HTTP_POST, [](AsyncWebServerRequest *request)
|
||||
server.on(
|
||||
"/update", HTTP_POST, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
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");
|
||||
request->send(response);
|
||||
|
||||
if (shouldReboot) {
|
||||
USB_SERIAL.println("OTA Update successful! Rebooting...");
|
||||
delay(1000);
|
||||
|
|
@ -254,7 +233,8 @@ void setupOTA()
|
|||
} else {
|
||||
USB_SERIAL.println("OTA Update failed!");
|
||||
}
|
||||
}, handleOTAUpload);
|
||||
},
|
||||
handleOTAUpload);
|
||||
|
||||
USB_SERIAL.println("OTA Update ready at: /update");
|
||||
}
|
||||
|
|
@ -267,7 +247,8 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind
|
|||
if (!index)
|
||||
{
|
||||
// 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());
|
||||
|
||||
|
|
@ -287,7 +268,7 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind
|
|||
}
|
||||
|
||||
// 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;
|
||||
USB_SERIAL.printf("OTA Progress: %zu bytes\n", progress);
|
||||
|
|
@ -318,7 +299,7 @@ bool connectToWiFi()
|
|||
|
||||
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);
|
||||
USB_SERIAL.print(".");
|
||||
|
|
@ -408,22 +389,22 @@ void printMenu()
|
|||
}
|
||||
|
||||
USB_SERIAL.println("***********************************************");
|
||||
USB_SERIAL.println(" arm - Arm motor");
|
||||
USB_SERIAL.println(" disarm - Disarm motor (safety)");
|
||||
USB_SERIAL.println(" <value> - Set throttle (48 – 2047)");
|
||||
USB_SERIAL.println(" 0 - Stop motor");
|
||||
USB_SERIAL.printf(" %s\t\t- Arm motor\n", SerialCmd::ARM);
|
||||
USB_SERIAL.printf(" %s\t\t- Disarm motor (safety)\n", SerialCmd::DISARM);
|
||||
USB_SERIAL.println(" <value>\t\t- Set throttle (48 – 2047)");
|
||||
USB_SERIAL.printf(" %s\t\t\t- Stop motor\n", SerialCmd::STOP);
|
||||
USB_SERIAL.println("***********************************************");
|
||||
USB_SERIAL.println(" cmd <number> - Send DShot command (0 - 47)");
|
||||
USB_SERIAL.println(" info - Show motor info");
|
||||
USB_SERIAL.println(" wifi - Show WiFi status");
|
||||
USB_SERIAL.println(" reconnect - Reconnect to WiFi");
|
||||
USB_SERIAL.println(" ota - Show OTA info");
|
||||
USB_SERIAL.printf(" %s <number>\t- Send DShot command (0 - 47)\n", SerialCmd::CMD_PREFIX);
|
||||
USB_SERIAL.printf(" %s\t\t- Show motor info\n", SerialCmd::INFO);
|
||||
USB_SERIAL.printf(" %s\t\t- Show WiFi status\n", SerialCmd::WIFI_STATUS);
|
||||
USB_SERIAL.printf(" %s\t- Reconnect to WiFi\n", SerialCmd::RECONNECT);
|
||||
USB_SERIAL.printf(" %s\t\t- Show OTA info\n", SerialCmd::OTA_INFO);
|
||||
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(" 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.printf(" Current Status: %s\n", isArmed ? "ARMED" : "DISARMED");
|
||||
USB_SERIAL.printf(" WiFi Status: %s\n", wifi_connected ? "CONNECTED" : "DISCONNECTED");
|
||||
|
|
@ -433,19 +414,19 @@ void printMenu()
|
|||
// Handle serial inputs and updates global variables
|
||||
void handleSerialInput(const String &input)
|
||||
{
|
||||
if (input == "arm")
|
||||
if (input == SerialCmd::ARM)
|
||||
{
|
||||
setArmingStatus(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (input == "0" || input == "disarm")
|
||||
if (input == SerialCmd::STOP || input == SerialCmd::DISARM)
|
||||
{
|
||||
setArmingStatus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (input == "info")
|
||||
if (input == SerialCmd::INFO)
|
||||
{
|
||||
printDShotInfo(motor01, USB_SERIAL);
|
||||
USB_SERIAL.println(" ");
|
||||
|
|
@ -453,13 +434,13 @@ void handleSerialInput(const String &input)
|
|||
return;
|
||||
}
|
||||
|
||||
if (input == "wifi")
|
||||
if (input == SerialCmd::WIFI_STATUS)
|
||||
{
|
||||
printWiFiStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (input == "ota")
|
||||
if (input == SerialCmd::OTA_INFO)
|
||||
{
|
||||
if (wifi_connected)
|
||||
{
|
||||
|
|
@ -477,7 +458,7 @@ void handleSerialInput(const String &input)
|
|||
return;
|
||||
}
|
||||
|
||||
if (input == "reconnect")
|
||||
if (input == SerialCmd::RECONNECT)
|
||||
{
|
||||
USB_SERIAL.println("Reconnecting to WiFi...");
|
||||
WiFi.disconnect();
|
||||
|
|
@ -490,7 +471,7 @@ void handleSerialInput(const String &input)
|
|||
return;
|
||||
}
|
||||
|
||||
if (input == "rpm" && IS_BIDIRECTIONAL)
|
||||
if (input == SerialCmd::RPM && IS_BIDIRECTIONAL)
|
||||
{
|
||||
if (isArmed)
|
||||
{
|
||||
|
|
@ -505,7 +486,7 @@ void handleSerialInput(const String &input)
|
|||
return;
|
||||
}
|
||||
|
||||
if (input.startsWith("cmd "))
|
||||
if (input.startsWith(SerialCmd::CMD_PREFIX))
|
||||
{
|
||||
if (!isArmed)
|
||||
{
|
||||
|
|
@ -515,7 +496,7 @@ void handleSerialInput(const String &input)
|
|||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -530,13 +511,13 @@ void handleSerialInput(const String &input)
|
|||
return;
|
||||
}
|
||||
|
||||
if (input == "h" || input == "help")
|
||||
if (input == SerialCmd::HELP || input == SerialCmd::HELP_ALT)
|
||||
{
|
||||
printMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
if (input == "status")
|
||||
if (input == SerialCmd::STATUS)
|
||||
{
|
||||
USB_SERIAL.println(" ");
|
||||
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;
|
||||
|
||||
// Handle arming status
|
||||
if (doc.containsKey("armed"))
|
||||
if (doc.containsKey(JsonKey::ARMED))
|
||||
{
|
||||
bool armed = doc["armed"];
|
||||
bool armed = doc[JsonKey::ARMED];
|
||||
setArmingStatus(armed);
|
||||
armedFromWeb = true;
|
||||
}
|
||||
|
||||
// Handle throttle value (only if armed)
|
||||
if (doc.containsKey("throttle"))
|
||||
if (doc.containsKey(JsonKey::THROTTLE))
|
||||
{
|
||||
if (!isArmed)
|
||||
{
|
||||
|
|
@ -628,7 +609,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
|
|||
return;
|
||||
}
|
||||
|
||||
uint16_t web_throttle = doc["throttle"];
|
||||
uint16_t web_throttle = doc[JsonKey::THROTTLE];
|
||||
|
||||
if (web_throttle == 0)
|
||||
{
|
||||
|
|
@ -664,8 +645,8 @@ void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventTyp
|
|||
// Send current arming status to new client
|
||||
{
|
||||
JsonDocument doc;
|
||||
doc["armed"] = isArmed;
|
||||
doc["throttle"] = isArmed ? throttle : 0;
|
||||
doc[JsonKey::ARMED] = isArmed;
|
||||
doc[JsonKey::THROTTLE] = isArmed ? throttle : 0;
|
||||
String json_output;
|
||||
serializeJson(doc, json_output);
|
||||
client->text(json_output);
|
||||
|
|
|
|||
|
|
@ -19,42 +19,27 @@
|
|||
#include <DShotRMT.h>
|
||||
#include <WiFi.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 <AsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
// Wifi Configuration
|
||||
static constexpr auto *ssid = "DShotRMT Control";
|
||||
static constexpr auto *password = "12345678";
|
||||
// Wifi AP Configuration
|
||||
static constexpr auto *WIFI_SSID_AP = "DShotRMT Control";
|
||||
static constexpr auto *WIFI_PASSWORD_AP = "12345678";
|
||||
|
||||
IPAddress local_IP(10, 10, 10, 1);
|
||||
IPAddress gateway(0, 0, 0, 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
|
||||
DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, MOTOR01_MAGNET_COUNT);
|
||||
|
||||
// Web Server Configuration
|
||||
AsyncWebServer server(80);
|
||||
AsyncWebSocket ws("/ws");
|
||||
AsyncWebServer server(WEBSERVER_PORT);
|
||||
AsyncWebSocket ws(WEBSOCKET_PATH);
|
||||
|
||||
// Global variables
|
||||
static uint16_t throttle = DSHOT_CMD_MOTOR_STOP;
|
||||
|
|
@ -81,7 +66,7 @@ void setup()
|
|||
|
||||
// Start Wifi Access Point
|
||||
USB_SERIAL.println("\nStarting Access Point...");
|
||||
WiFi.softAP(ssid, password);
|
||||
WiFi.softAP(WIFI_SSID_AP, WIFI_PASSWORD_AP);
|
||||
|
||||
IPAddress IP = WiFi.softAPIP();
|
||||
|
||||
|
|
@ -136,7 +121,7 @@ void loop()
|
|||
}
|
||||
|
||||
// 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);
|
||||
|
||||
|
|
@ -171,9 +156,9 @@ void loop()
|
|||
{
|
||||
// Generate JSON for Webserver
|
||||
JsonDocument doc;
|
||||
doc["throttle"] = isArmed ? throttle : 0;
|
||||
doc["armed"] = isArmed;
|
||||
doc["rpm"] = current_rpm;
|
||||
doc[JsonKey::THROTTLE] = isArmed ? throttle : 0;
|
||||
doc[JsonKey::ARMED] = isArmed;
|
||||
doc[JsonKey::RPM] = current_rpm;
|
||||
|
||||
String json_output;
|
||||
json_output.reserve(256);
|
||||
|
|
@ -221,19 +206,19 @@ void printMenu()
|
|||
USB_SERIAL.println("***********************************************");
|
||||
USB_SERIAL.println(" Web Config: http://10.10.10.1 ");
|
||||
USB_SERIAL.println("***********************************************");
|
||||
USB_SERIAL.println(" arm - Arm motor");
|
||||
USB_SERIAL.println(" disarm - Disarm motor (safety)");
|
||||
USB_SERIAL.println(" <value> - Set throttle (48 – 2047)");
|
||||
USB_SERIAL.println(" 0 - Stop motor");
|
||||
USB_SERIAL.printf(" %s\t\t- Arm motor\n", SerialCmd::ARM);
|
||||
USB_SERIAL.printf(" %s\t\t- Disarm motor (safety)\n", SerialCmd::DISARM);
|
||||
USB_SERIAL.println(" <value>\t\t- Set throttle (48 – 2047)");
|
||||
USB_SERIAL.printf(" %s\t\t\t- Stop motor\n", SerialCmd::STOP);
|
||||
USB_SERIAL.println("***********************************************");
|
||||
USB_SERIAL.println(" cmd <number> - Send DShot command (0 - 47)");
|
||||
USB_SERIAL.println(" info - Show motor info");
|
||||
USB_SERIAL.printf(" %s <number>\t- Send DShot command (0 - 47)\n", SerialCmd::CMD_PREFIX);
|
||||
USB_SERIAL.printf(" %s\t\t- Show motor info\n", SerialCmd::INFO);
|
||||
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(" 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.printf(" Current Status: %s\n", isArmed ? "ARMED" : "DISARMED");
|
||||
USB_SERIAL.println("***********************************************");
|
||||
|
|
@ -242,24 +227,24 @@ void printMenu()
|
|||
// Handle serial inputs and updates global variables
|
||||
void handleSerialInput(const String &input)
|
||||
{
|
||||
if (input == "arm")
|
||||
if (input == SerialCmd::ARM)
|
||||
{
|
||||
setArmingStatus(true);
|
||||
return;
|
||||
}
|
||||
if (input == "0" || input == "disarm")
|
||||
if (input == SerialCmd::STOP || input == SerialCmd::DISARM)
|
||||
{
|
||||
setArmingStatus(false);
|
||||
return;
|
||||
}
|
||||
if (input == "info")
|
||||
if (input == SerialCmd::INFO)
|
||||
{
|
||||
printDShotInfo(motor01, USB_SERIAL);
|
||||
USB_SERIAL.println(" ");
|
||||
USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED");
|
||||
return;
|
||||
}
|
||||
if (input == "rpm" && IS_BIDIRECTIONAL)
|
||||
if (input == SerialCmd::RPM && IS_BIDIRECTIONAL)
|
||||
{
|
||||
if (isArmed)
|
||||
{
|
||||
|
|
@ -273,7 +258,7 @@ void handleSerialInput(const String &input)
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (input.startsWith("cmd "))
|
||||
if (input.startsWith(SerialCmd::CMD_PREFIX))
|
||||
{
|
||||
if (!isArmed)
|
||||
{
|
||||
|
|
@ -283,7 +268,7 @@ void handleSerialInput(const String &input)
|
|||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -297,12 +282,12 @@ void handleSerialInput(const String &input)
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (input == "h" || input == "help")
|
||||
if (input == SerialCmd::HELP || input == SerialCmd::HELP_ALT)
|
||||
{
|
||||
printMenu();
|
||||
return;
|
||||
}
|
||||
if (input == "status")
|
||||
if (input == SerialCmd::STATUS)
|
||||
{
|
||||
USB_SERIAL.println(" ");
|
||||
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;
|
||||
|
||||
// Handle arming status
|
||||
if (doc.containsKey("armed"))
|
||||
if (doc.containsKey(JsonKey::ARMED))
|
||||
{
|
||||
bool armed = doc["armed"];
|
||||
bool armed = doc[JsonKey::ARMED];
|
||||
setArmingStatus(armed);
|
||||
armedFromWeb = true;
|
||||
}
|
||||
|
||||
// Handle throttle value (only if armed)
|
||||
if (doc.containsKey("throttle"))
|
||||
if (doc.containsKey(JsonKey::THROTTLE))
|
||||
{
|
||||
if (!isArmed)
|
||||
{
|
||||
|
|
@ -388,7 +373,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
|
|||
return;
|
||||
}
|
||||
|
||||
uint16_t web_throttle = doc["throttle"];
|
||||
uint16_t web_throttle = doc[JsonKey::THROTTLE];
|
||||
|
||||
if (web_throttle == 0)
|
||||
{
|
||||
|
|
@ -424,8 +409,8 @@ void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventTyp
|
|||
// Send current arming status to new client
|
||||
{
|
||||
JsonDocument doc;
|
||||
doc["armed"] = isArmed;
|
||||
doc["throttle"] = isArmed ? throttle : 0;
|
||||
doc[JsonKey::ARMED] = isArmed;
|
||||
doc[JsonKey::THROTTLE] = isArmed ? throttle : 0;
|
||||
String json_output;
|
||||
serializeJson(doc, json_output);
|
||||
client->text(json_output);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
21
keywords.txt
21
keywords.txt
|
|
@ -8,6 +8,18 @@
|
|||
|
||||
DShotRMT KEYWORD1
|
||||
|
||||
#######################################
|
||||
# Methods (KEYWORD2)
|
||||
#######################################
|
||||
|
||||
begin KEYWORD2
|
||||
sendThrottle KEYWORD2
|
||||
sendThrottlePercent KEYWORD2
|
||||
sendCommand KEYWORD2
|
||||
sendCustomCommand KEYWORD2
|
||||
getTelemetry KEYWORD2
|
||||
setMotorSpinDirection KEYWORD2
|
||||
saveESCSettings KEYWORD2
|
||||
|
||||
#######################################
|
||||
# Constants (LITERAL1)
|
||||
|
|
@ -53,15 +65,9 @@ DSHOT_CMD_LED2_OFF LITERAL1
|
|||
DSHOT_CMD_LED3_OFF LITERAL1
|
||||
DSHOT_CMD_AUDIO_STREAM_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
|
||||
DSHOT_BITS_PER_FRAME LITERAL1
|
||||
DSHOT_SWITCH_TIME LITERAL1
|
||||
DSHOT_NULL_PACKET LITERAL1
|
||||
DSHOT_RX_TIMEOUT_MS LITERAL1
|
||||
GCR_BITS_PER_FRAME LITERAL1
|
||||
|
|
@ -69,11 +75,8 @@ GCR_BITS_PER_FRAME LITERAL1
|
|||
# RMT Constants
|
||||
DSHOT_CLOCK_SRC_DEFAULT LITERAL1
|
||||
DSHOT_RMT_RESOLUTION LITERAL1
|
||||
RMT_BUFFER_SIZE LITERAL1
|
||||
RMT_BUFFER_SYMBOLS LITERAL1
|
||||
RMT_QUEUE_DEPTH LITERAL1
|
||||
DSHOT_PULSE_MIN LITERAL1
|
||||
DSHOT_PULSE_MAX LITERAL1
|
||||
|
||||
# Status Constants
|
||||
DSHOT_OK LITERAL1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
name=DShotRMT
|
||||
version=0.9.1
|
||||
version=0.9.5
|
||||
author=Wastl Kraus <wir-sind-die-matrix.de>
|
||||
maintainer=Wastl Kraus <wir-sind-die-matrix.de>
|
||||
license=MIT
|
||||
|
|
|
|||
377
src/DShotRMT.cpp
377
src/DShotRMT.cpp
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
#include "DShotRMT.h"
|
||||
#include <cstring>
|
||||
|
||||
// Constructor with GPIO number
|
||||
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),
|
||||
_dshot_timing(DSHOT_TIMING_US[_mode])
|
||||
{
|
||||
// Pre-calculate timing and bit positions for performance
|
||||
// Pre-calculate timing and ratios for performance
|
||||
_preCalculateRMTTicks();
|
||||
_percent_to_throttle_ratio = (static_cast<float>(DSHOT_THROTTLE_MAX - DSHOT_THROTTLE_MIN)) / DSHOT_PERCENT_MAX;
|
||||
}
|
||||
|
||||
// Constructor using pin number
|
||||
|
|
@ -30,28 +32,7 @@ DShotRMT::DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional, ui
|
|||
// Destructor
|
||||
DShotRMT::~DShotRMT()
|
||||
{
|
||||
// Cleanup TX channel
|
||||
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;
|
||||
}
|
||||
_cleanupRmtResources();
|
||||
}
|
||||
|
||||
// Initialize DShotRMT
|
||||
|
|
@ -89,36 +70,37 @@ dshot_result_t DShotRMT::begin()
|
|||
// Send throttle value
|
||||
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)
|
||||
{
|
||||
_last_throttle = 0;
|
||||
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);
|
||||
|
||||
_packet = _buildDShotPacket(_last_throttle);
|
||||
return _sendDShotFrame(_packet);
|
||||
return _sendPacket(_packet);
|
||||
}
|
||||
|
||||
// Send throttle value as a percentage
|
||||
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};
|
||||
}
|
||||
|
||||
// Map percent to DShot throttle range
|
||||
uint16_t throttle = static_cast<uint16_t>(DSHOT_THROTTLE_MIN + ((DSHOT_THROTTLE_MAX - DSHOT_THROTTLE_MIN) / 100.0f) * percent);
|
||||
// Map percent to DShot throttle range using pre-calculated ratio.
|
||||
uint16_t throttle = static_cast<uint16_t>(DSHOT_THROTTLE_MIN + _percent_to_throttle_ratio * percent);
|
||||
return sendThrottle(throttle);
|
||||
}
|
||||
|
||||
// Sends a DShot command (0-47) to the ESC by accepting an integer 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)
|
||||
{
|
||||
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 delay_us = DEFAULT_CMD_DELAY_US;
|
||||
|
||||
// Certain commands require more repetitions to be reliably accepted by the ESC.
|
||||
switch (command)
|
||||
{
|
||||
case DSHOT_CMD_MOTOR_STOP:
|
||||
|
|
@ -142,7 +125,7 @@ dshot_result_t DShotRMT::sendCommand(dshotCommands_e command)
|
|||
delay_us = SETTINGS_COMMAND_DELAY_US;
|
||||
break;
|
||||
default:
|
||||
// For other commands, use default repeat and delay
|
||||
// For other commands, use default repeat and delay.
|
||||
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.
|
||||
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))
|
||||
{
|
||||
result.result_code = DSHOT_INVALID_COMMAND;
|
||||
return result;
|
||||
return {false, DSHOT_INVALID_COMMAND};
|
||||
}
|
||||
|
||||
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;
|
||||
return _sendRepeatedCommand(static_cast<uint16_t>(command), repeat_count, delay_us);
|
||||
}
|
||||
|
||||
// Get telemetry data
|
||||
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)
|
||||
{
|
||||
|
|
@ -202,24 +153,38 @@ dshot_result_t DShotRMT::getTelemetry()
|
|||
return result;
|
||||
}
|
||||
|
||||
// Use stored magnet count if parameter is 0 (default)
|
||||
uint16_t final_magnet_count = _motor_magnet_count;
|
||||
// Prioritize checking for full telemetry data, as it is richer.
|
||||
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)
|
||||
{
|
||||
_telemetry_ready_flag_atomic = false; // Reset the flag
|
||||
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
|
||||
uint8_t pole_pairs = final_magnet_count / MAGNETS_PER_POLE_PAIR;
|
||||
uint32_t motor_rpm = (erpm / pole_pairs);
|
||||
|
||||
result.success = true;
|
||||
uint8_t pole_pairs = _motor_magnet_count / MAGNETS_PER_POLE_PAIR;
|
||||
result.erpm = erpm;
|
||||
result.motor_rpm = motor_rpm;
|
||||
result.motor_rpm = (erpm / pole_pairs);
|
||||
result.success = true;
|
||||
result.result_code = DSHOT_TELEMETRY_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -230,43 +195,69 @@ dshot_result_t DShotRMT::getTelemetry()
|
|||
// Reverse motor direction directly
|
||||
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;
|
||||
|
||||
return sendCommand(command, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US);
|
||||
}
|
||||
|
||||
// Sends a raw DShot command to the ESC.
|
||||
dshot_result_t DShotRMT::sendRawCommand(uint16_t command_value)
|
||||
dshot_result_t DShotRMT::sendCustomCommand(uint16_t command_value, uint16_t repeat_count, uint16_t delay_us)
|
||||
{
|
||||
_packet = _buildDShotPacket(command_value);
|
||||
return _sendDShotFrame(_packet);
|
||||
// Validate the integer command value.
|
||||
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()
|
||||
{
|
||||
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
|
||||
{
|
||||
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::_executeCommand(dshotCommands_e command)
|
||||
dshot_result_t DShotRMT::_sendRawDshotFrame(uint16_t value)
|
||||
{
|
||||
uint64_t start_time = esp_timer_get_time();
|
||||
|
||||
_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;
|
||||
_packet = _buildDShotPacket(value);
|
||||
return _sendPacket(_packet);
|
||||
}
|
||||
|
||||
// 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.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;
|
||||
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
|
||||
{
|
||||
// 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;
|
||||
return (data_and_telemetry << 4) | packet.checksum;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// For bidirectional DShot, the CRC is inverted
|
||||
// For bidirectional DShot, the CRC is inverted per specification.
|
||||
if (_is_bidirectional)
|
||||
{
|
||||
crc = (~crc) & DSHOT_CRC_MASK;
|
||||
|
|
@ -304,20 +295,59 @@ uint16_t DShotRMT::_calculateCRC(const uint16_t &data) const
|
|||
return crc;
|
||||
}
|
||||
|
||||
uint8_t DShotRMT::_calculateTelemetryCRC(const uint8_t *data, size_t len) const
|
||||
{
|
||||
uint8_t crc = 0;
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
{
|
||||
crc ^= data[i];
|
||||
for (uint8_t j = 0; j < 8; ++j)
|
||||
{
|
||||
if (crc & 0x80)
|
||||
{
|
||||
crc = (crc << 1) ^ 0x07; // DSHOT telemetry uses CRC-8 with polynomial 0x07.
|
||||
}
|
||||
else
|
||||
{
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
void DShotRMT::_extractTelemetryData(const uint8_t *raw_telemetry_bytes, dshot_telemetry_data_t &telemetry_data) const
|
||||
{
|
||||
// Ensure the telemetry_data struct is cleared before filling.
|
||||
memset(&telemetry_data, 0, sizeof(dshot_telemetry_data_t));
|
||||
|
||||
// Telemetry data is typically ordered as:
|
||||
// Byte 0: Temperature (signed 8-bit)
|
||||
// Byte 1-2: Voltage (16-bit, MSB first)
|
||||
// Byte 3-4: Current (16-bit, MSB first)
|
||||
// Byte 5-6: Consumption (16-bit, MSB first)
|
||||
// Byte 7-8: RPM (16-bit, MSB first)
|
||||
// Byte 9: CRC (8-bit) - checked separately
|
||||
|
||||
telemetry_data.temperature = static_cast<int8_t>(raw_telemetry_bytes[0]);
|
||||
telemetry_data.voltage = (static_cast<uint16_t>(raw_telemetry_bytes[1]) << 8) | raw_telemetry_bytes[2];
|
||||
telemetry_data.current = (static_cast<uint16_t>(raw_telemetry_bytes[3]) << 8) | raw_telemetry_bytes[4];
|
||||
telemetry_data.consumption = (static_cast<uint16_t>(raw_telemetry_bytes[5]) << 8) | raw_telemetry_bytes[6];
|
||||
telemetry_data.rpm = (static_cast<uint16_t>(raw_telemetry_bytes[7]) << 8) | raw_telemetry_bytes[8];
|
||||
}
|
||||
|
||||
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.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.t1l_ticks = _rmt_ticks.bit_length_ticks - _rmt_ticks.t1h_ticks;
|
||||
_rmt_ticks.t0l_ticks = _rmt_ticks.bit_length_ticks - _rmt_ticks.t0h_ticks;
|
||||
|
||||
// Calculate the minimum time required between frames.
|
||||
// Pause between frames is frame time in us, some padding and about 30 us is added by hardware.
|
||||
// Calculate the minimum time required between frames to prevent signal collision.
|
||||
_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)
|
||||
{
|
||||
_frame_timer_us = (_frame_timer_us << 1);
|
||||
|
|
@ -325,9 +355,9 @@ void DShotRMT::_preCalculateRMTTicks()
|
|||
}
|
||||
|
||||
// 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())
|
||||
{
|
||||
return {true, DSHOT_NONE};
|
||||
|
|
@ -335,9 +365,9 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet)
|
|||
|
||||
if (_is_bidirectional)
|
||||
{
|
||||
// Start the receiver to wait for incoming telemetry data
|
||||
rmt_symbol_word_t rx_symbols[GCR_BITS_PER_FRAME];
|
||||
size_t rx_size_bytes = GCR_BITS_PER_FRAME * sizeof(rmt_symbol_word_t);
|
||||
// Start the RMT receiver to wait for the ESC's telemetry response.
|
||||
rmt_symbol_word_t rx_symbols[DSHOT_TELEMETRY_FULL_GCR_BITS];
|
||||
size_t rx_size_bytes = DSHOT_TELEMETRY_FULL_GCR_BITS * sizeof(rmt_symbol_word_t);
|
||||
|
||||
rmt_receive_config_t rmt_rx_config = {
|
||||
.signal_range_min_ns = DSHOT_PULSE_MIN_NS,
|
||||
|
|
@ -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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
rmt_transmit_config_t tx_config = {}; // Initialize all members to zero
|
||||
tx_config.loop_count = 0; // No automatic loops - real-time calculation
|
||||
rmt_transmit_config_t tx_config = {.loop_count = 0}; // No automatic loops.
|
||||
|
||||
// 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)
|
||||
{
|
||||
// Disable RMT RX for sending
|
||||
if (rmt_disable(_rmt_rx_channel) != DSHOT_OK)
|
||||
{
|
||||
return {false, DSHOT_RECEIVER_FAILED};
|
||||
|
|
@ -377,7 +406,7 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet)
|
|||
return {false, DSHOT_TRANSMISSION_FAILED};
|
||||
}
|
||||
|
||||
// Re-enable RMT RX
|
||||
// Re-enable RMT RX immediately after transmission to catch the response.
|
||||
if (_is_bidirectional)
|
||||
{
|
||||
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};
|
||||
}
|
||||
|
||||
// This function needs to be fast, as it generates the RMT symbols just before sending
|
||||
// Placed in IRAM for high performance, as it's called from an ISR context.
|
||||
// This function is placed in IRAM for high performance, as it may be
|
||||
// called from an ISR context depending on RMT driver implementation.
|
||||
uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) const
|
||||
{
|
||||
uint32_t gcr_value = 0;
|
||||
|
||||
// Decode RMT symbols into a 21-bit GCR (Group Code Recording) value.
|
||||
// The ESC sends back a signal where the duration determines the bit value.
|
||||
for (size_t i = 0; i < GCR_BITS_PER_FRAME; ++i)
|
||||
// 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)
|
||||
{
|
||||
bool bit_is_one = symbols[i].duration0 > symbols[i].duration1;
|
||||
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)).
|
||||
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);
|
||||
|
||||
// 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_crc = data_and_crc & DSHOT_CRC_MASK;
|
||||
|
||||
// A valid response must have the telemetry request bit set to 1. This is a sanity check.
|
||||
if (!((received_data >> DSHOT_TELEMETRY_BIT_POSITION) & 1))
|
||||
{
|
||||
return DSHOT_NULL_PACKET;
|
||||
}
|
||||
|
||||
// Calculate and validate CRC.
|
||||
// Calculate and validate the CRC for the received data.
|
||||
uint16_t calculated_crc = _calculateCRC(received_data);
|
||||
if (received_crc != calculated_crc)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Timing Control Functions
|
||||
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 elapsed = current_time - _last_transmission_time_us;
|
||||
return elapsed >= _frame_timer_us;
|
||||
|
|
@ -443,27 +466,111 @@ bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed() const
|
|||
|
||||
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();
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
DShotRMT *instance = static_cast<DShotRMT *>(user_data);
|
||||
|
||||
if (edata && edata->num_symbols == GCR_BITS_PER_FRAME)
|
||||
if (edata)
|
||||
{
|
||||
if (edata->num_symbols == DSHOT_TELEMETRY_FULL_GCR_BITS)
|
||||
{
|
||||
instance->_processFullTelemetryFrame(edata->received_symbols, edata->num_symbols);
|
||||
}
|
||||
else if (edata->num_symbols == DSHOT_ERPM_FRAME_GCR_BITS)
|
||||
{
|
||||
uint16_t erpm = instance->_decodeDShotFrame(edata->received_symbols);
|
||||
|
||||
if (erpm != DSHOT_NULL_PACKET)
|
||||
{
|
||||
// Atomically store the new eRPM value and set the flag
|
||||
// Atomically store the new eRPM value and set the flag.
|
||||
instance->_last_erpm_atomic.store(erpm);
|
||||
instance->_telemetry_ready_flag_atomic.store(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@
|
|||
#include "dshot_definitions.h"
|
||||
#include "dshot_init.h"
|
||||
|
||||
// Forward declaration for the RMT receive callback
|
||||
class DShotRMT;
|
||||
void IRAM_ATTR rmt_rx_done_callback(rmt_channel_handle_t rx_chan, const rmt_rx_done_event_data_t *edata, void *user_data);
|
||||
// DShotRMT Library Version
|
||||
static constexpr uint8_t DSHOTRMT_MAJOR_VERSION = 0;
|
||||
static constexpr uint8_t DSHOTRMT_MINOR_VERSION = 9;
|
||||
static constexpr uint8_t DSHOTRMT_PATCH_VERSION = 5;
|
||||
|
||||
// DShot Protocol Constants
|
||||
static constexpr auto DSHOT_THROTTLE_FAILSAFE = 0;
|
||||
|
|
@ -29,10 +30,10 @@ static constexpr auto DSHOT_THROTTLE_FAILSAFE = 0;
|
|||
class DShotRMT
|
||||
{
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
// Destructor
|
||||
|
|
@ -56,8 +57,14 @@ public:
|
|||
// 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);
|
||||
|
||||
// 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.
|
||||
dshot_result_t getTelemetry();
|
||||
|
|
@ -75,6 +82,7 @@ public:
|
|||
uint16_t getEncodedFrameValue() const { return _encoded_frame_value; }
|
||||
|
||||
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);
|
||||
|
||||
// DShot Configuration Parameters
|
||||
|
|
@ -95,14 +103,16 @@ private:
|
|||
// DShot Frame Timing and State Variables
|
||||
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
|
||||
float _percent_to_throttle_ratio = 0.0f; // Pre-calculated ratio for throttle percentage conversion
|
||||
uint16_t _last_throttle = 0; // Last transmitted throttle value
|
||||
dshot_packet_t _packet; // Current DShot packet being processed
|
||||
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
|
||||
std::atomic<uint16_t> _last_erpm_atomic = 0; // Atomically stored last received eRPM value
|
||||
std::atomic<bool> _telemetry_ready_flag_atomic = false; // Atomically stored flag indicating new telemetry data
|
||||
std::atomic<dshot_telemetry_data_t> _last_telemetry_data_atomic = {}; // Atomically stored last received full telemetry data
|
||||
std::atomic<bool> _full_telemetry_ready_flag_atomic = false; // Atomically stored flag indicating new full telemetry data
|
||||
rmt_rx_event_callbacks_t _rx_event_callbacks = {
|
||||
// RMT receive event callbacks
|
||||
.on_recv_done = _on_rx_done,
|
||||
|
|
@ -110,16 +120,20 @@ private:
|
|||
|
||||
// Private Helper Functions for DShot Protocol Logic
|
||||
bool _isValidCommand(dshotCommands_e command) const; // Checks if a given DShot command is valid
|
||||
dshot_result_t _executeCommand(dshotCommands_e command); // Executes a single DShot command
|
||||
dshot_packet_t _buildDShotPacket(const uint16_t &value) const; // Builds a DShot packet from a value (throttle or command)
|
||||
uint16_t _buildDShotFrameValue(const dshot_packet_t &packet) const; // Combines packet data into a 16-bit DShot frame value
|
||||
uint16_t _calculateCRC(const uint16_t &data) const; // Calculates the 4-bit CRC for a DShot frame
|
||||
uint8_t _calculateTelemetryCRC(const uint8_t *data, size_t len) const; // Calculates the 8-bit CRC for telemetry data
|
||||
void _extractTelemetryData(const uint8_t *raw_telemetry_bytes, dshot_telemetry_data_t &telemetry_data) const; // Extracts telemetry data from raw bytes
|
||||
void _preCalculateRMTTicks(); // Pre-calculates RMT timing ticks for the selected DShot mode
|
||||
dshot_result_t _sendDShotFrame(const dshot_packet_t &packet); // Sends a DShot frame via RMT TX channel
|
||||
dshot_result_t _sendPacket(const dshot_packet_t &packet); // Sends a DShot frame via RMT TX channel
|
||||
uint16_t IRAM_ATTR _decodeDShotFrame(const rmt_symbol_word_t *symbols) const; // Decodes a received RMT symbol array into an eRPM value
|
||||
void IRAM_ATTR _processFullTelemetryFrame(const rmt_symbol_word_t *symbols, size_t num_symbols); // Processes a full telemetry frame
|
||||
bool IRAM_ATTR _isFrameIntervalElapsed() const; // Checks if enough time has passed since the last frame transmission
|
||||
void _recordFrameTransmissionTime(); // Records the current time as the last frame transmission time
|
||||
|
||||
dshot_result_t _sendRepeatedCommand(uint16_t value, uint16_t repeat_count, uint16_t delay_us);
|
||||
|
||||
// Static Callback Function for RMT RX Events
|
||||
void _cleanupRmtResources();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* @file dshot_definitions.h
|
||||
* @brief Defines DShot protocol constants, data structures, and command enums for DShotRMT library
|
||||
* @author Wastl Kraus
|
||||
* @date 2025-10-04
|
||||
* @date 2025-11-29
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
|
@ -11,36 +11,58 @@
|
|||
#include <cstdint>
|
||||
#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_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_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_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
|
||||
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;
|
||||
|
||||
// Default motor magnet count for RPM calculation
|
||||
// --- 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 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
|
||||
{
|
||||
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;
|
||||
// =================================================================================
|
||||
// DShot Timing Constants (Protocol Level)
|
||||
// =================================================================================
|
||||
static constexpr uint16_t DSHOT_PADDING_US = 20; // Pause between frames
|
||||
|
||||
// Defines the bit length and high time for a '1' bit in microseconds for each DShot mode.
|
||||
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.
|
||||
} 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.
|
||||
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.
|
||||
} 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
|
||||
{
|
||||
DSHOT_NONE = 0,
|
||||
|
|
@ -86,6 +170,7 @@ enum dshot_msg_code_t
|
|||
DSHOT_ENCODING_SUCCESS,
|
||||
DSHOT_TRANSMISSION_SUCCESS,
|
||||
DSHOT_TELEMETRY_SUCCESS,
|
||||
DSHOT_TELEMETRY_DATA_AVAILABLE,
|
||||
DSHOT_COMMAND_SUCCESS
|
||||
};
|
||||
|
||||
|
|
@ -96,8 +181,11 @@ typedef struct dshot_result
|
|||
dshot_msg_code_t result_code; // Specific error or success code.
|
||||
uint16_t erpm; // Electrical RPM (eRPM) if telemetry is successful.
|
||||
uint16_t motor_rpm; // Motor RPM if telemetry is successful and magnet count is known.
|
||||
dshot_telemetry_data_t telemetry_data; // Full telemetry data if available
|
||||
bool telemetry_available; // Flag to indicate if telemetry_data is valid
|
||||
} dshot_result_t;
|
||||
|
||||
// --- DShot Commands ---
|
||||
// Standard DShot commands by "betaflight"
|
||||
enum dshotCommands_e
|
||||
{
|
||||
|
|
@ -131,41 +219,6 @@ enum dshotCommands_e
|
|||
DSHOT_CMD_MAX_VALUE = 47
|
||||
};
|
||||
|
||||
// Custom status codes
|
||||
// --- General Status & Helper Constants ---
|
||||
static constexpr int DSHOT_OK = 0;
|
||||
static constexpr int DSHOT_ERROR = 1;
|
||||
|
||||
// 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
|
||||
};
|
||||
static constexpr float CONVERSION_FACTOR_MILLI_TO_UNITS = 1000.0f;
|
||||
|
|
@ -15,11 +15,11 @@ dshot_result_t init_rmt_tx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_ch
|
|||
.gpio_num = gpio,
|
||||
.clk_src = DSHOT_CLOCK_SRC_DEFAULT,
|
||||
.resolution_hz = DSHOT_RMT_RESOLUTION,
|
||||
.mem_block_symbols = RMT_BUFFER_SYMBOLS,
|
||||
.mem_block_symbols = RMT_TX_BUFFER_SYMBOLS,
|
||||
.trans_queue_depth = RMT_QUEUE_DEPTH,
|
||||
.flags = {
|
||||
.invert_out = is_bidirectional ? 1 : 0,
|
||||
.init_level = is_bidirectional ? 0 : 1}};
|
||||
.init_level = 0}};
|
||||
|
||||
rmt_transmit_config_t rmt_tx_config = {}; // Initialize all members to zero
|
||||
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,
|
||||
.clk_src = DSHOT_CLOCK_SRC_DEFAULT,
|
||||
.resolution_hz = DSHOT_RMT_RESOLUTION,
|
||||
.mem_block_symbols = RMT_BUFFER_SYMBOLS,
|
||||
.mem_block_symbols = RMT_RX_BUFFER_SYMBOLS,
|
||||
};
|
||||
|
||||
if (rmt_new_rx_channel(&rx_channel_config, out_channel) != DSHOT_OK)
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@
|
|||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Forward declaration of the DShotRMT class to break circular dependency
|
||||
class DShotRMT;
|
||||
#include "dshot_definitions.h"
|
||||
#include "DShotRMT.h"
|
||||
|
||||
// Error Messages
|
||||
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_FAILED[] = "Transmission 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 PERCENT_NOT_IN_RANGE[] = "Percent not in range! (0.0 - 100.0)";
|
||||
static constexpr char COMMAND_NOT_VALID[] = "Command not valid! (0 - 47)";
|
||||
static constexpr char THROTTLE_NOT_IN_RANGE[] = "Throttle not in range!";
|
||||
static constexpr char PERCENT_NOT_IN_RANGE[] = "Percent not in range!";
|
||||
static constexpr char COMMAND_NOT_VALID[] = "Command not valid!";
|
||||
static constexpr char BIDIR_NOT_ENABLED[] = "Bidirectional DShot not enabled!";
|
||||
static constexpr char TELEMETRY_SUCCESS[] = "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
|
||||
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
|
||||
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 === ");
|
||||
|
||||
uint16_t dshot_mode_val = 0;
|
||||
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.println("\n=== DShot Info ===");
|
||||
output.printf("Library Version: %d.%d.%d\n", DSHOTRMT_MAJOR_VERSION, DSHOTRMT_MINOR_VERSION, DSHOTRMT_PATCH_VERSION);
|
||||
output.printf("Mode: %s\n", get_dshot_mode_str(dshot_rmt.getMode()));
|
||||
output.printf("Bidirectional: %s\n", dshot_rmt.isBidirectional() ? "YES" : "NO");
|
||||
output.printf("Current Packet: ");
|
||||
output.printf("Last Throttle: %u\n", dshot_rmt.getThrottleValue());
|
||||
|
||||
output.print("Packet (binary): ");
|
||||
for (int i = DSHOT_BITS_PER_FRAME - 1; i >= 0; --i)
|
||||
{
|
||||
output.print((dshot_rmt.getEncodedFrameValue() >> i) & 1);
|
||||
}
|
||||
output.println();
|
||||
|
||||
output.printf("\nCurrent Value: %u\n", dshot_rmt.getThrottleValue());
|
||||
// --- Telemetry Data ---
|
||||
if (dshot_rmt.isBidirectional())
|
||||
{
|
||||
dshot_result_t telemetry_result = dshot_rmt.getTelemetry();
|
||||
|
||||
output.print("Telemetry: ");
|
||||
if (telemetry_result.success)
|
||||
{
|
||||
output.printf("OK (%s)\n", get_result_code_str(telemetry_result.result_code));
|
||||
|
||||
if (telemetry_result.erpm > 0 || telemetry_result.motor_rpm > 0)
|
||||
{
|
||||
output.printf(" eRPM: %u, Motor RPM: %u\n", telemetry_result.erpm, telemetry_result.motor_rpm);
|
||||
}
|
||||
|
||||
if (telemetry_result.telemetry_available)
|
||||
{
|
||||
output.println(" --- Full Telemetry Details ---");
|
||||
output.printf(" Temp: %d C | Volt: %.2f V | Curr: %.2f A | Cons: %u mAh\n",
|
||||
telemetry_result.telemetry_data.temperature,
|
||||
(float)telemetry_result.telemetry_data.voltage / 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue