From 7a5c425fa8490af90674829b9e8ab09feb10217f Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Fri, 28 Nov 2025 01:22:50 +0100 Subject: [PATCH] clean up --- README.md | 20 ++-- examples/dshot300/dshot300.ino | 6 +- {src => examples}/web_utilities/ota_update.h | 0 {src => examples}/web_utilities/web_content.h | 0 keywords.txt | 23 ++-- src/DShotRMT.cpp | 110 ++++++++++++------ src/DShotRMT.h | 13 ++- src/dshot_definitions.h | 21 +--- src/dshot_utils.h | 45 +++---- 9 files changed, 140 insertions(+), 98 deletions(-) rename {src => examples}/web_utilities/ota_update.h (100%) rename {src => examples}/web_utilities/web_content.h (100%) diff --git a/README.md b/README.md index 548e652..c75ede7 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,14 @@ Here's an example of the output from the `dshot300` example sketch: - **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. + +1. **Signal Generation (TX):** The library uses an RMT 'bytes_encoder'. This encoder is configured with the specific pulse durations for DShot '0' and '1' bits based on the selected speed (e.g., DSHOT300, DSHOT600). When a user calls `sendThrottle()`, the library constructs a 16-bit DShot frame (11-bit throttle, 1-bit telemetry request, 4-bit CRC) and hands it to the RMT encoder. The RMT hardware then autonomously generates the correct electrical signal on the specified GPIO pin. + +2. **Bidirectional Telemetry (RX):** For bidirectional communication, the library configures a second RMT channel in receive mode on the same GPIO. An interrupt service routine (`_on_rx_done`) is registered. When the ESC sends back a telemetry signal, the RMT peripheral captures it and triggers the interrupt. The interrupt code decodes the GCR-encoded signal, validates its CRC, and stores the resulting eRPM value in a thread-safe `atomic` variable. The main application can then poll for this data using the `getTelemetry()` method. + ## ⏱️ 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: @@ -119,18 +127,14 @@ 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 will return the last received telemetry data. - `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 @@ -146,4 +150,4 @@ Contributions are welcome! Please fork the repository, create a feature branch, ## 📄 License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index ac2e705..6e24e0b 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -65,7 +65,7 @@ void loop() if (input.length() > 0) { - handleSerialInput(input, throttle, continuous_throttle); + handleSerialInput(input, throttle, continuous_throttle, motor01); } } @@ -118,14 +118,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") diff --git a/src/web_utilities/ota_update.h b/examples/web_utilities/ota_update.h similarity index 100% rename from src/web_utilities/ota_update.h rename to examples/web_utilities/ota_update.h diff --git a/src/web_utilities/web_content.h b/examples/web_utilities/web_content.h similarity index 100% rename from src/web_utilities/web_content.h rename to examples/web_utilities/web_content.h diff --git a/keywords.txt b/keywords.txt index 1571472..58fe469 100644 --- a/keywords.txt +++ b/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,12 +75,9 @@ 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 -DSHOT_ERROR LITERAL1 +DSHOT_ERROR LITERAL1 \ No newline at end of file diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 56cbe8f..893d984 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -8,6 +8,27 @@ #include "DShotRMT.h" +// Configuration Constants +static constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000; +static constexpr auto DSHOT_FULL_PACKET = 0b1111111111111111; +static constexpr auto DSHOT_RX_TIMEOUT_MS = 2; +static constexpr auto DSHOT_PADDING_US = 20; // Pause between frames +static constexpr auto GCR_BITS_PER_FRAME = 21; // GCR bits in a DShot answer frame +static constexpr auto POLE_PAIRS_MIN = 1; +static constexpr auto MAGNETS_PER_POLE_PAIR = 2; +static constexpr auto NO_DSHOT_TELEMETRY = 0; +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 DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count) : _gpio(gpio), @@ -30,28 +51,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 @@ -91,7 +91,9 @@ dshot_result_t DShotRMT::sendThrottle(uint16_t throttle) { // A throttle value of 0 is a disarm command if (throttle == 0) - { + { + // just to be sure + _last_throttle = 0; return sendCommand(DSHOT_CMD_MOTOR_STOP); } @@ -99,19 +101,19 @@ dshot_result_t DShotRMT::sendThrottle(uint16_t throttle) _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(DSHOT_THROTTLE_MIN + ((DSHOT_THROTTLE_MAX - DSHOT_THROTTLE_MIN) / 100.0f) * percent); + uint16_t throttle = static_cast(DSHOT_THROTTLE_MIN + ((DSHOT_THROTTLE_MAX - DSHOT_THROTTLE_MIN) / DSHOT_PERCENT_MAX) * percent); return sendThrottle(throttle); } @@ -236,11 +238,45 @@ dshot_result_t DShotRMT::setMotorSpinDirection(bool reversed) 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 before casting + if (command_value < DSHOT_CMD_MOTOR_STOP || command_value > DSHOT_CMD_MAX_VALUE) + { + return {false, DSHOT_COMMAND_NOT_VALID}; + } + + dshot_result_t result = {false, DSHOT_UNKNOWN, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY}; + + bool all_successful = true; + + // Send command multiple times with delay + for (uint16_t i = 0; i < repeat_count; i++) + { + dshot_result_t single_result = _sendRawDshotFrame(command_value); + + 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; } // Use with caution @@ -255,13 +291,19 @@ bool DShotRMT::_isValidCommand(dshotCommands_e command) const return (command >= dshotCommands_e::DSHOT_CMD_MOTOR_STOP && command <= DSHOT_CMD_MAX); } +dshot_result_t DShotRMT::_sendRawDshotFrame(uint16_t value) +{ + _packet = _buildDShotPacket(value); + 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(command)); - dshot_result_t result = _sendDShotFrame(_packet); + dshot_result_t result = _sendPacket(_packet); uint64_t end_time = esp_timer_get_time(); _last_command_timestamp = end_time; @@ -325,7 +367,7 @@ 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 if (!_isFrameIntervalElapsed()) @@ -362,7 +404,9 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) rmt_transmit_config_t tx_config = {}; // Initialize all members to zero tx_config.loop_count = 0; // No automatic loops - real-time calculation - // TODO: Find out, why this is needed + // In bidirectional mode, the RMT RX channel must be disabled before transmitting. + // This is to prevent the receiver from picking up the transmitted signal, which would cause a loopback issue. + // The ESP32's RMT peripheral TX and RX channels on the same pin are not fully independent. if (_is_bidirectional) { // Disable RMT RX for sending diff --git a/src/DShotRMT.h b/src/DShotRMT.h index b2a147d..03f25f6 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -56,8 +56,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 +81,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 @@ -115,7 +122,7 @@ private: uint16_t _buildDShotFrameValue(const dshot_packet_t &packet) const; // Combines packet data into a 16-bit DShot frame value uint16_t _calculateCRC(const uint16_t &data) const; // Calculates the 4-bit CRC for a DShot frame void _preCalculateRMTTicks(); // Pre-calculates RMT timing ticks for the selected DShot mode - dshot_result_t _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 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 diff --git a/src/dshot_definitions.h b/src/dshot_definitions.h index cf6317f..140c102 100644 --- a/src/dshot_definitions.h +++ b/src/dshot_definitions.h @@ -16,6 +16,8 @@ static constexpr uint16_t DSHOT_FRAME_LENGTH = 16; // 11 throttle bits static constexpr uint16_t DSHOT_BITS_PER_FRAME = 16; static constexpr uint16_t DSHOT_THROTTLE_MAX = 2047; // Maximum throttle value (0-2047) static constexpr uint16_t DSHOT_THROTTLE_MIN = 48; // Minimum throttle value for motor spin +static constexpr 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) @@ -136,30 +138,11 @@ 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[] = { diff --git a/src/dshot_utils.h b/src/dshot_utils.h index 72202f2..753815c 100644 --- a/src/dshot_utils.h +++ b/src/dshot_utils.h @@ -9,6 +9,7 @@ #pragma once #include +#include "dshot_definitions.h" // Forward declaration of the DShotRMT class to break circular dependency class DShotRMT; @@ -28,9 +29,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 +101,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) { @@ -118,25 +137,7 @@ inline void printDShotResult(dshot_result_t &result, Stream &output = Serial) inline void printDShotInfo(const 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.printf("Current Mode: %s\n", get_dshot_mode_str(dshot_rmt.getMode())); output.printf("Bidirectional: %s\n", dshot_rmt.isBidirectional() ? "YES" : "NO"); output.printf("Current Packet: ");