diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f536deb..186c763 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: ~/.arduino15/packages ~/.arduino15/cache ~/Arduino/libraries - key: linux-arduino-esp32-v2-${{ hashFiles('**/library.properties') }} + key: linux-arduino-esp32-v2-v3 - name: Install Dependencies if: steps.cache-arduino.outputs.cache-hit != 'true' @@ -83,7 +83,7 @@ jobs: ~/.arduino15/packages ~/.arduino15/cache ~/Arduino/libraries - key: linux-arduino-esp32-v2-${{ hashFiles('**/library.properties') }} + key: linux-arduino-esp32-v2-v3 - name: Run Arduino Lint uses: arduino/arduino-lint-action@v2 @@ -125,7 +125,7 @@ jobs: ~/.arduino15/packages ~/.arduino15/cache ~/Arduino/libraries - key: linux-arduino-esp32-v2-${{ hashFiles('**/library.properties') }} + key: linux-arduino-esp32-v2-v3 - name: Compile Sketch run: arduino-cli compile --fqbn esp32:esp32:esp32 --library ${{ github.workspace }} "${{ matrix.sketch }}" diff --git a/README.md b/README.md index ac47120..75697ae 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,9 @@ The main class is `DShotRMT`. Here are the most important methods: - `begin()`: Initializes the DShot RMT channels and encoder. - `sendThrottlePercent(float percent)`: Sends a throttle value as a percentage (0.0-100.0) to the ESC. - `sendThrottle(uint16_t throttle)`: Sends a raw throttle value (48-2047) to the ESC. A value of 0 sends a motor stop command. -- `sendCommand(dshotCommands_e command)`: Sends a DShot command (0-47) 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 (0-47) to the ESC with a specified repeat count and delay. This is a blocking function. -- `sendCommand(uint16_t command_value)`: Sends a DShot command (0-47) to the ESC by accepting an integer value. It validates the input and then calls `sendCommand(dshotCommands_e command)`. +- `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. - `setMotorSpinDirection(bool reversed)`: Sets the motor spin direction. `true` for reversed, `false` for normal. diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index 8d3ae5c..85c5f26 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -29,7 +29,7 @@ static constexpr auto IS_BIDIRECTIONAL = false; // static constexpr auto MOTOR01_MAGNET_COUNT = 14; // Creates the motor instance -DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, DEFAULT_MOTOR_MAGNET_COUNT); +DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL); // void setup() diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 34504d5..bba62e1 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -33,21 +33,17 @@ DShotRMT::~DShotRMT() // Cleanup TX channel if (_rmt_tx_channel) { - if (rmt_disable(_rmt_tx_channel) == DSHOT_OK) - { - rmt_del_channel(_rmt_tx_channel); - _rmt_tx_channel = nullptr; - } + rmt_disable(_rmt_tx_channel); + rmt_del_channel(_rmt_tx_channel); + _rmt_tx_channel = nullptr; } // Cleanup RX channel if (_rmt_rx_channel) { - if (rmt_disable(_rmt_rx_channel) == DSHOT_OK) - { - rmt_del_channel(_rmt_rx_channel); - _rmt_rx_channel = nullptr; - } + rmt_disable(_rmt_rx_channel); + rmt_del_channel(_rmt_rx_channel); + _rmt_rx_channel = nullptr; } // Cleanup encoder @@ -65,6 +61,7 @@ dshot_result_t DShotRMT::begin() if (!result.success) { + _cleanupRmtResources(); // Clean up any allocated resources on failure return result; } @@ -73,10 +70,7 @@ dshot_result_t DShotRMT::begin() result = init_rmt_rx_channel(_gpio, &_rmt_rx_channel, &_rx_event_callbacks, this); if (!result.success) { - // Cleanup previously allocated TX channel on failure - rmt_disable(_rmt_tx_channel); - rmt_del_channel(_rmt_tx_channel); - _rmt_tx_channel = nullptr; + _cleanupRmtResources(); // Clean up any allocated resources on failure return result; } } @@ -85,17 +79,7 @@ dshot_result_t DShotRMT::begin() if (!result.success) { - // Cleanup previously allocated channels on failure - rmt_disable(_rmt_tx_channel); - rmt_del_channel(_rmt_tx_channel); - _rmt_tx_channel = nullptr; - - if (_rmt_rx_channel) - { - rmt_disable(_rmt_rx_channel); - rmt_del_channel(_rmt_rx_channel); - _rmt_rx_channel = nullptr; - } + _cleanupRmtResources(); // Clean up any allocated resources on failure return result; } @@ -260,10 +244,10 @@ dshot_result_t DShotRMT::saveESCSettings() // Simple check bool DShotRMT::_isValidCommand(dshotCommands_e command) const { - return (command >= dshotCommands_e::DSHOT_CMD_MOTOR_STOP && command <= dshotCommands_e::DSHOT_CMD_MAX); + return (command >= dshotCommands_e::DSHOT_CMD_MOTOR_STOP && command <= DSHOT_CMD_MAX); } -// +// Executes a single DShot command by building and sending a DShot frame. dshot_result_t DShotRMT::_executeCommand(dshotCommands_e command) { uint64_t start_time = esp_timer_get_time(); @@ -314,18 +298,18 @@ uint16_t DShotRMT::_calculateCRC(const uint16_t &data) const 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 later. _rmt_ticks.bit_length_ticks = static_cast(_dshot_timing.bit_length_us * RMT_TICKS_PER_US); _rmt_ticks.t1h_ticks = static_cast(_dshot_timing.t1h_lenght_us * RMT_TICKS_PER_US); - _rmt_ticks.t0h_ticks = _rmt_ticks.t1h_ticks >> 1; // High time for a 1 is always double of 0 + _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. + // Pause between frames is frame time in us, some padding and about 30 us is added by hardware. _frame_timer_us = (static_cast(_dshot_timing.bit_length_us * DSHOT_BITS_PER_FRAME) << 1) + DSHOT_PADDING_US; - // For bidirectional, double up + // For bidirectional, double up. if (_is_bidirectional) { _frame_timer_us = (_frame_timer_us << 1); @@ -365,12 +349,12 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) // 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 +// Placed in IRAM for high performance, as it's called from an ISR context. uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) const { uint32_t gcr_value = 0; - // Step 1: Decode RMT symbols into a 21-bit GCR (Group Code Recording) value. + // Decode RMT symbols into a 21-bit GCR (Group Code Recording) value. // The ESC sends back a signal where the duration determines the bit value. for (size_t i = 0; i < GCR_BITS_PER_FRAME; ++i) { @@ -378,23 +362,23 @@ uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) gcr_value = (gcr_value << 1) | bit_is_one; } - // Step 2: Perform GCR decoding (GCR = Value ^ (Value >> 1)) + // Perform GCR decoding (GCR = Value ^ (Value >> 1)). uint32_t decoded_frame = gcr_value ^ (gcr_value >> 1); - // Step 3: Extract the 16-bit DShot frame from the decoded data + // Extract the 16-bit DShot frame from the decoded data. uint16_t data_and_crc = (decoded_frame & DSHOT_FULL_PACKET); - // Step 4: Extract data and CRC from the 16-bit frame + // Extract data and CRC from the 16-bit frame. uint16_t received_data = data_and_crc >> DSHOT_CRC_BIT_SHIFT; uint16_t received_crc = data_and_crc & DSHOT_CRC_MASK; - // Step 5: A valid response must have the telemetry request bit set to 1. This is a sanity check. + // 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; } - // Step 6: Calculate and validate CRC + // Calculate and validate CRC. uint16_t calculated_crc = _calculateCRC(received_data); if (received_crc != calculated_crc) { @@ -440,3 +424,26 @@ bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const return false; } + +void DShotRMT::_cleanupRmtResources() +{ + if (_rmt_tx_channel) + { + rmt_disable(_rmt_tx_channel); + rmt_del_channel(_rmt_tx_channel); + _rmt_tx_channel = nullptr; + } + + if (_rmt_rx_channel) + { + rmt_disable(_rmt_rx_channel); + rmt_del_channel(_rmt_rx_channel); + _rmt_rx_channel = nullptr; + } + + if (_dshot_encoder) + { + rmt_del_encoder(_dshot_encoder); + _dshot_encoder = nullptr; + } +} diff --git a/src/DShotRMT.h b/src/DShotRMT.h index a76819c..211cb26 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -18,18 +18,19 @@ #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); + // DShot Protocol Constants static constexpr auto DSHOT_THROTTLE_FAILSAFE = 0; -static constexpr auto DSHOT_THROTTLE_MIN = 48; -static constexpr auto DSHOT_BITS_PER_FRAME = 16; -static constexpr auto DEFAULT_MOTOR_MAGNET_COUNT = 14; -// +// DShotRMT class for generating DShot signals and receiving telemetry. class DShotRMT { public: - // Constructor with GPIO number - DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); + // Constructor for DShotRMT. + 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 DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); @@ -40,28 +41,28 @@ public: // Initialize DShotRMT dshot_result_t begin(); - // Send throttle value + // Sends a raw throttle value to the ESC. dshot_result_t sendThrottle(uint16_t throttle); - // Send throttle value as a percentage + // Sends a throttle value as a percentage to the ESC. dshot_result_t sendThrottlePercent(float percent); - // Sends a DShot command (0-47) to the ESC by accepting an integer value. + // Sends a DShot command to the ESC by accepting an integer value. dshot_result_t sendCommand(uint16_t command_value); - // Sends a DShot command (0-47) to the ESC. + // Sends a DShot command to the ESC. dshot_result_t sendCommand(dshotCommands_e command); - // Sends a DShot command (0-47) to the ESC with a specified repeat count and delay. + // Sends a DShot command to the ESC with a specified repeat count and delay. dshot_result_t sendCommand(dshotCommands_e command, uint16_t repeat_count, uint16_t delay_us); - // Get telemetry data + // Retrieves telemetry data from the ESC. dshot_result_t getTelemetry(); - // Reverse motor direction directly + // Sets the motor spin direction. dshot_result_t setMotorSpinDirection(bool reversed); - // Use with caution + // Sends a command to the ESC to save its current settings. dshot_result_t saveESCSettings(); // Getters for DShot info @@ -71,50 +72,53 @@ public: uint16_t getEncodedFrameValue() const { return _encoded_frame_value; } private: - // Configuration - gpio_num_t _gpio; - dshot_mode_t _mode; - bool _is_bidirectional; - uint16_t _motor_magnet_count; - dshot_timing_us_t _dshot_timing; + 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); - // RMT Handles & Config - rmt_channel_handle_t _rmt_tx_channel = nullptr; - rmt_channel_handle_t _rmt_rx_channel = nullptr; - rmt_encoder_handle_t _dshot_encoder = nullptr; - rmt_ticks_t _rmt_ticks; - uint16_t _pulse_level = 1; // Default to high - uint16_t _idle_level = 0; // Default to low + // DShot Configuration Parameters + gpio_num_t _gpio; // GPIO pin used for DShot communication + dshot_mode_t _mode; // DShot mode (e.g., DSHOT300, DSHOT600) + bool _is_bidirectional; // True if bidirectional DShot is enabled + uint16_t _motor_magnet_count; // Number of magnets in the motor for RPM calculation + dshot_timing_us_t _dshot_timing; // DShot timing parameters in microseconds - // Timing & State - uint64_t _last_transmission_time_us = 0; - uint64_t _frame_timer_us = 0; - uint16_t _last_throttle = 0; - dshot_packet_t _packet; - uint16_t _encoded_frame_value = 0; - uint64_t _last_command_timestamp = 0; + // RMT Hardware Handles and Configuration + rmt_channel_handle_t _rmt_tx_channel = nullptr; // RMT transmit channel handle + rmt_channel_handle_t _rmt_rx_channel = nullptr; // RMT receive channel handle + rmt_encoder_handle_t _dshot_encoder = nullptr; // DShot RMT encoder handle + rmt_ticks_t _rmt_ticks; // Pre-calculated RMT timing ticks + uint16_t _pulse_level = 1; // Output level for a pulse (typically high) + uint16_t _idle_level = 0; // Output level for idle (typically low) - // Telemetry - std::atomic _last_erpm_atomic = 0; - std::atomic _telemetry_ready_flag_atomic = false; + // 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 + 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 _last_erpm_atomic = 0; // Atomically stored last received eRPM value + std::atomic _telemetry_ready_flag_atomic = false; // Atomically stored flag indicating new telemetry data rmt_rx_event_callbacks_t _rx_event_callbacks = { + // RMT receive event callbacks .on_recv_done = _on_rx_done, }; - // Private helper functions - bool _isValidCommand(dshotCommands_e command) const; - dshot_result_t _executeCommand(dshotCommands_e command); - dshot_packet_t _buildDShotPacket(const uint16_t &value) const; - uint16_t _buildDShotFrameValue(const dshot_packet_t &packet) const; - uint16_t _calculateCRC(const uint16_t &data) const; - void _preCalculateRMTTicks(); - dshot_result_t _sendDShotFrame(const dshot_packet_t &packet); - uint16_t IRAM_ATTR _decodeDShotFrame(const rmt_symbol_word_t *symbols) const; - bool IRAM_ATTR _isFrameIntervalElapsed() const; - void _recordFrameTransmissionTime(); + // Private Helper Functions for DShot Protocol Logic + bool _isValidCommand(dshotCommands_e command) const; // Checks if a given DShot command is valid + dshot_result_t _executeCommand(dshotCommands_e command); // Executes a single DShot command + dshot_packet_t _buildDShotPacket(const uint16_t &value) const; // Builds a DShot packet from a value (throttle or command) + uint16_t _buildDShotFrameValue(const dshot_packet_t &packet) const; // Combines packet data into a 16-bit DShot frame value + uint16_t _calculateCRC(const uint16_t &data) const; // Calculates the 4-bit CRC for a DShot frame + void _preCalculateRMTTicks(); // Pre-calculates RMT timing ticks for the selected DShot mode + dshot_result_t _sendDShotFrame(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 - // Static Callback Functions - static bool IRAM_ATTR _on_rx_done(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data); + // Static Callback Function for RMT RX Events + void _cleanupRmtResources(); }; -#include "dshot_utils.h" // Include for helper functions \ No newline at end of file +#include "dshot_utils.h" // Include for helper functions diff --git a/src/dshot_definitions.h b/src/dshot_definitions.h index e01cbe2..5841a34 100644 --- a/src/dshot_definitions.h +++ b/src/dshot_definitions.h @@ -11,6 +11,19 @@ #include #include +// DShot protocol definitions +static constexpr uint16_t DSHOT_FRAME_LENGTH = 16; // 11 throttle bits + 1 telemetry bit + 4 CRC bits +static constexpr uint16_t DSHOT_BITS_PER_FRAME = 16; +static constexpr uint16_t DSHOT_THROTTLE_MAX = 2047; // Maximum throttle value (0-2047) +static constexpr uint16_t DSHOT_THROTTLE_MIN = 48; // Minimum throttle value for motor spin +static constexpr uint16_t DSHOT_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 + +// Default motor magnet count for RPM calculation +static constexpr uint16_t DEFAULT_MOTOR_MAGNET_COUNT = 14; + // Defines the available DShot communication speeds. enum dshot_mode_t { @@ -114,8 +127,7 @@ enum dshotCommands_e DSHOT_CMD_LED2_OFF, DSHOT_CMD_LED3_OFF, DSHOT_CMD_AUDIO_STREAM_MODE_ON_OFF = 30, - DSHOT_CMD_SILENT_MODE_ON_OFF = 31, - DSHOT_CMD_MAX = 47 + DSHOT_CMD_SILENT_MODE_ON_OFF = 31 }; // Custom status codes @@ -125,7 +137,6 @@ 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_CRC_MASK = 0b0000000000001111; 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 @@ -137,7 +148,6 @@ static constexpr auto GCR_BITS_PER_FRAME = 21; // GCR bits in a DShot answer fra 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_THROTTLE_MAX = 2047; 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; @@ -156,5 +166,5 @@ const dshot_timing_us_t DSHOT_TIMING_US[] = { {6.67, 5.00}, // DSHOT150 {3.33, 2.50}, // DSHOT300 {1.67, 1.25}, // DSHOT600 - {0.83, 0.67} // DSHOT1200 -}; + {0.83, 0.67} // DSHOT1200 +}; \ No newline at end of file diff --git a/src/dshot_utils.h b/src/dshot_utils.h index 31d15bf..72202f2 100644 --- a/src/dshot_utils.h +++ b/src/dshot_utils.h @@ -41,7 +41,7 @@ static constexpr char INVALID_COMMAND[] = "Invalid command!"; static constexpr char COMMAND_SUCCESS[] = "DShot command sent successfully"; // Helper to get result code string -inline const char *_get_result_code_str(dshot_msg_code_t code) +inline const char *get_result_code_str(dshot_msg_code_t code) { switch (code) { @@ -103,7 +103,7 @@ inline const char *_get_result_code_str(dshot_msg_code_t code) // Helper to quick print DShot result codes inline void printDShotResult(dshot_result_t &result, Stream &output = Serial) { - output.printf("Status: %s - %s", result.success ? "SUCCESS" : "FAILED", _get_result_code_str(result.result_code)); + output.printf("Status: %s - %s", result.success ? "SUCCESS" : "FAILED", get_result_code_str(result.result_code)); // Print telemetry data if available if (result.success && (result.erpm > 0 || result.motor_rpm > 0))