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/DShotRMT.h b/DShotRMT.h index 29d290d..f7e8113 100644 --- a/DShotRMT.h +++ b/DShotRMT.h @@ -6,5 +6,8 @@ * @license MIT */ +#pragma once + +#include #include "src/DShotRMT.h" \ No newline at end of file diff --git a/LICENSE b/LICENSE index e77c526..433d0ec 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Wastl Kraus +Copyright (c) 2023 derdoktor667 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index c3421a6..75697ae 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ # DShotRMT - ESP32 RMT DShot Driver [![Arduino CI](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml/badge.svg)](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml) +[![Arduino Library](https://img.shields.io/badge/Arduino-Library-blue.svg)](https://www.arduinolibraries.com/libraries/dshot-rmt) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -An Arduino IDElibrary 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 brushless motors in both Arduino and ESP-IDF projects. +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. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. +--- + ### DShot300 Example Output Here's an example of the output from the `dshot300` example sketch: @@ -15,7 +19,7 @@ Here's an example of the output from the `dshot300` example sketch: ## 🚀 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. +- **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). - **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`. @@ -43,11 +47,11 @@ The DShot protocol defines specific timing characteristics for each mode. The fo ## ⚡ Quick Start -Here's a basic example of how to use the `DShotRMT` library to control a motor. Please use example sketches for more detailes: +Here's a basic example of how to use the `DShotRMT` library to control a motor. Note that `DShotRMT.h` now includes all necessary dependencies, so you only need to include this single header. Please use example sketches for more detailes: ```cpp #include -#include // Include the DShotRMT library +#include // Define the GPIO pin connected to the motor ESC const gpio_num_t MOTOR_PIN = GPIO_NUM_27; @@ -65,14 +69,13 @@ void setup() { printCpuInfo(Serial); Serial.println("Motor initialized. Ramping up to 25% throttle..."); - -} + } void loop() { // Ramp up to 25% throttle over 2.5 seconds for (int i = 0; i <= 25; i++) { motor.sendThrottlePercent(i); - delay(100); + delay(200); } Serial.println("Stopping motor."); @@ -80,6 +83,9 @@ void loop() { // Print DShot Info printDShotInfo(motor, Serial); + + // Take a break before next bench run + delay(3000); } ``` @@ -108,8 +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(uint16_t command)`: Sends a single DShot command (0-47) to the ESC. -- `sendCommand(dshotCommands_e dshot_command, uint16_t repeat_count = DEFAULT_CMD_REPEAT_COUNT, uint16_t delay_us = DEFAULT_CMD_DELAY_US)`: Sends a DShot command multiple times with a delay between repetitions. This is a blocking function. +- `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. @@ -123,6 +130,14 @@ The main class is `DShotRMT`. Here are the most important methods: - `getEncodedFrameValue()`: Gets the last encoded DShot frame value. - `getThrottleValue()`: Gets the last transmitted throttle 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: + +* [ESP-IDF v5.5.1 Documentation](https://docs.espressif.com/projects/esp-idf/en/v5.5.1/) + +--- + ## 🤝 Contributing Contributions are welcome! Please fork the repository, create a feature branch, and submit a pull request. diff --git a/examples/throttle_percent/throttle_percent.ino b/examples/throttle_percent/throttle_percent.ino index 5903a7b..5bf6dfa 100644 --- a/examples/throttle_percent/throttle_percent.ino +++ b/examples/throttle_percent/throttle_percent.ino @@ -1,6 +1,6 @@ /** * @file throttle_percent.ino - * @brief Demo sketch for DShotRMT library using percentage throttle. + * @brief Example sketch for DShotRMT library demonstrating throttle control by percentage * @author Wastl Kraus * @date 2025-09-20 * @license MIT @@ -28,7 +28,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, MOTOR01_MAGNET_COUNT); +DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, DEFAULT_MOTOR_MAGNET_COUNT); // Forward declaration void handleSerialInput(const String &input); diff --git a/examples/web_client/web_client.ino b/examples/web_client/web_client.ino index 147a69e..7bae523 100644 --- a/examples/web_client/web_client.ino +++ b/examples/web_client/web_client.ino @@ -1,6 +1,6 @@ /** * @file web_client.ino - * @brief DShotRMT Web Control as WiFi Client + * @brief Example sketch for DShotRMT library demonstrating web control via a client * @author Wastl Kraus * @date 2025-09-11 * @license MIT @@ -16,12 +16,11 @@ ******************************************************************/ #include -#include -#include - #include -#include -#include +#include +#include +#include "web_utilities/ota_update.h" +#include "web_utilities/web_content.h" #include #include @@ -51,7 +50,7 @@ static constexpr auto IS_BIDIRECTIONAL = false; // Note: Bidirectional DShot is static constexpr auto MOTOR01_MAGNET_COUNT = 14; // Creates the motor instance -DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL); +DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, MOTOR01_MAGNET_COUNT); // Web Server Configuration AsyncWebServer server(80); @@ -177,7 +176,7 @@ void loop() // Get Motor RPM if bidirectional and armed if (IS_BIDIRECTIONAL && isArmed) { - dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + dshot_result_t telem_result = motor01.getTelemetry(); printDShotResult(telem_result); } @@ -194,7 +193,7 @@ void loop() if (IS_BIDIRECTIONAL && isArmed) { - dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + dshot_result_t telem_result = motor01.getTelemetry(); if (telem_result.success && telem_result.motor_rpm > 0) { current_rpm = String(telem_result.motor_rpm); @@ -235,11 +234,11 @@ void setupOTA() // Serve OTA update page server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send_P(200, "text/html", ota_html); }); + { request->send_P(200, "text/html", ota_html); }); // Handle OTA update upload server.on("/update", HTTP_POST, [](AsyncWebServerRequest *request) - { + { bool shouldReboot = !Update.hasError(); AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", @@ -254,7 +253,8 @@ void setupOTA() ESP.restart(); } else { USB_SERIAL.println("OTA Update failed!"); - } }, handleOTAUpload); + } + }, handleOTAUpload); USB_SERIAL.println("OTA Update ready at: /update"); } @@ -344,7 +344,7 @@ void printWiFiStatus() { USB_SERIAL.println(); USB_SERIAL.println("***********************************************"); - USB_SERIAL.println(" --- WIFI INFO --- "); + USB_SERIAL.println(" --- WIFI INFO ---"); USB_SERIAL.println("***********************************************"); USB_SERIAL.printf("SSID: %s\n", WiFi.SSID().c_str()); USB_SERIAL.printf("IP Address: %s\n", WiFi.localIP().toString().c_str()); @@ -393,7 +393,7 @@ void printMenu() { USB_SERIAL.println(" "); USB_SERIAL.println("***********************************************"); - USB_SERIAL.println(" --- DShotRMT Web Client Demo --- "); + USB_SERIAL.println(" --- DShotRMT Web Client Demo ---"); USB_SERIAL.println("***********************************************"); if (wifi_connected) @@ -494,7 +494,7 @@ void handleSerialInput(const String &input) { if (isArmed) { - dshot_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + dshot_result_t result = motor01.getTelemetry(); printDShotResult(result); } else @@ -684,4 +684,4 @@ void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventTyp case WS_EVT_ERROR: break; } -} +} \ No newline at end of file diff --git a/examples/web_control/web_control.ino b/examples/web_control/web_control.ino index d78edb2..0a88df2 100644 --- a/examples/web_control/web_control.ino +++ b/examples/web_control/web_control.ino @@ -1,6 +1,6 @@ /** * @file web_control.ino - * @brief Demo sketch for DShotRMT library + * @brief Example sketch for DShotRMT library demonstrating web control via an access point * @author Wastl Kraus * @date 2025-09-09 * @license MIT @@ -16,10 +16,10 @@ ******************************************************************/ #include -#include - #include -#include +#include +#include +#include "web_utilities/web_content.h" #include #include @@ -50,7 +50,7 @@ static constexpr auto IS_BIDIRECTIONAL = false; // Note: Bidirectional DShot is static constexpr auto MOTOR01_MAGNET_COUNT = 14; // Creates the motor instance -DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL); +DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL, MOTOR01_MAGNET_COUNT); // Web Server Configuration AsyncWebServer server(80); @@ -145,7 +145,7 @@ void loop() // Get Motor RPM if bidirectional and armed if (IS_BIDIRECTIONAL && isArmed) { - dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + dshot_result_t telem_result = motor01.getTelemetry(); printDShotResult(telem_result); } @@ -160,7 +160,7 @@ void loop() if (IS_BIDIRECTIONAL && isArmed) { - dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + dshot_result_t telem_result = motor01.getTelemetry(); if (telem_result.success && telem_result.motor_rpm > 0) { current_rpm = String(telem_result.motor_rpm); @@ -263,7 +263,7 @@ void handleSerialInput(const String &input) { if (isArmed) { - dshot_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); + dshot_result_t result = motor01.getTelemetry(); printDShotResult(result); } else diff --git a/library.properties b/library.properties index 36cdefa..24e98af 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=DShotRMT -version=0.8.9 +version=0.9.0 author=Wastl Kraus maintainer=Wastl Kraus license=MIT diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 91896aa..bba62e1 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -6,7 +6,7 @@ * @license MIT */ -#include +#include "DShotRMT.h" // Constructor with GPIO number DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count) @@ -14,7 +14,7 @@ DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, ui _mode(mode), _is_bidirectional(is_bidirectional), _motor_magnet_count(magnet_count), - _dshot_timing(DSHOT_TIMING_US[static_cast(mode)]) + _dshot_timing(DSHOT_TIMING_US[_mode]) { // Pre-calculate timing and bit positions for performance _preCalculateRMTTicks(); @@ -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 @@ -61,41 +57,33 @@ DShotRMT::~DShotRMT() // Initialize DShotRMT dshot_result_t DShotRMT::begin() { - if (!_initTXChannel().success) + dshot_result_t result = init_rmt_tx_channel(_gpio, &_rmt_tx_channel, _is_bidirectional); + + if (!result.success) { - return {false, dshot_msg_code_t::DSHOT_TX_INIT_FAILED}; + _cleanupRmtResources(); // Clean up any allocated resources on failure + return result; } if (_is_bidirectional) { - if (!_initRXChannel().success) + 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; - return {false, dshot_msg_code_t::DSHOT_RX_INIT_FAILED}; + _cleanupRmtResources(); // Clean up any allocated resources on failure + return result; } } - if (!_initDShotEncoder().success) + result = init_dshot_encoder(&_dshot_encoder, _rmt_ticks, _pulse_level, _idle_level); + + 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; - } - - return {false, dshot_msg_code_t::DSHOT_ENCODER_INIT_FAILED}; + _cleanupRmtResources(); // Clean up any allocated resources on failure + return result; } - return {true, dshot_msg_code_t::DSHOT_INIT_SUCCESS}; + return {true, DSHOT_INIT_SUCCESS}; } // Send throttle value @@ -104,7 +92,7 @@ dshot_result_t DShotRMT::sendThrottle(uint16_t throttle) // A throttle value of 0 is a disarm command if (throttle == 0) { - return sendCommand(dshotCommands_e::DSHOT_CMD_MOTOR_STOP); + return sendCommand(DSHOT_CMD_MOTOR_STOP); } // Constrain throttle to the valid DShot range @@ -119,7 +107,7 @@ dshot_result_t DShotRMT::sendThrottlePercent(float percent) { if (percent < 0.0f || percent > 100.0f) { - return {false, dshot_msg_code_t::DSHOT_PERCENT_NOT_IN_RANGE}; + return {false, DSHOT_PERCENT_NOT_IN_RANGE}; } // Map percent to DShot throttle range @@ -127,23 +115,47 @@ dshot_result_t DShotRMT::sendThrottlePercent(float percent) return sendThrottle(throttle); } -// Send DShot command to ESC -dshot_result_t DShotRMT::sendCommand(dshotCommands_e command) +// Sends a DShot command (0-47) to the ESC by accepting an integer value. +dshot_result_t DShotRMT::sendCommand(uint16_t command_value) { - _packet = _buildDShotPacket(static_cast(command)); - return _sendDShotFrame(_packet); + // Validate the integer command value before casting + if (command_value < DSHOT_CMD_MOTOR_STOP || command_value > DSHOT_CMD_MAX) + { + return {false, DSHOT_COMMAND_NOT_VALID}; + } + return sendCommand(static_cast(command_value)); } - -// Send full DShot commands for setup etc -// This is a blocking function that uses delayMicroseconds for repetitions. -dshot_result_t DShotRMT::_sendCommandInternal(dshotCommands_e dshot_command, uint16_t repeat_count, uint16_t delay_us) +// Sends a DShot command (0-47) to the ESC. +dshot_result_t DShotRMT::sendCommand(dshotCommands_e command) { - dshot_result_t result = {false, dshot_msg_code_t::DSHOT_UNKNOWN, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY}; + uint16_t repeat_count = DEFAULT_CMD_REPEAT_COUNT; + uint16_t delay_us = DEFAULT_CMD_DELAY_US; - if (!_isValidCommand(dshot_command)) + switch (command) { - result.result_code = dshot_msg_code_t::DSHOT_INVALID_COMMAND; + case DSHOT_CMD_SAVE_SETTINGS: + case DSHOT_CMD_SPIN_DIRECTION_NORMAL: + case DSHOT_CMD_SPIN_DIRECTION_REVERSED: + repeat_count = SETTINGS_COMMAND_REPEATS; + delay_us = SETTINGS_COMMAND_DELAY_US; + break; + default: + // For other commands, use default repeat and delay + break; + } + + return sendCommand(command, repeat_count, delay_us); +} + +// 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; } @@ -152,7 +164,7 @@ dshot_result_t DShotRMT::_sendCommandInternal(dshotCommands_e dshot_command, uin // Send command multiple times with delay for (uint16_t i = 0; i < repeat_count; i++) { - dshot_result_t single_result = _executeCommand(dshot_command); + dshot_result_t single_result = _executeCommand(command); if (!single_result.success) { @@ -168,12 +180,11 @@ dshot_result_t DShotRMT::_sendCommandInternal(dshotCommands_e dshot_command, uin } } - // result.success = all_successful; if (result.success) { - result.result_code = dshot_msg_code_t::DSHOT_COMMAND_SUCCESS; + result.result_code = DSHOT_COMMAND_SUCCESS; } return result; @@ -182,11 +193,11 @@ dshot_result_t DShotRMT::_sendCommandInternal(dshotCommands_e dshot_command, uin // Get telemetry data dshot_result_t DShotRMT::getTelemetry() { - dshot_result_t result = {false, dshot_msg_code_t::DSHOT_TELEMETRY_FAILED, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY}; + dshot_result_t result = {false, DSHOT_TELEMETRY_FAILED, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY}; if (!_is_bidirectional) { - result.result_code = dshot_msg_code_t::DSHOT_BIDIR_NOT_ENABLED; + result.result_code = DSHOT_BIDIR_NOT_ENABLED; return result; } @@ -208,7 +219,7 @@ dshot_result_t DShotRMT::getTelemetry() result.success = true; result.erpm = erpm; result.motor_rpm = motor_rpm; - result.result_code = dshot_msg_code_t::DSHOT_TELEMETRY_SUCCESS; + result.result_code = DSHOT_TELEMETRY_SUCCESS; } } @@ -221,24 +232,22 @@ 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 _sendCommandInternal(command, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); + return sendCommand(command, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); } - - // Use with caution dshot_result_t DShotRMT::saveESCSettings() { - return _sendCommandInternal(dshotCommands_e::DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); + return sendCommand(dshotCommands_e::DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); } // Simple check 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(); @@ -252,104 +261,6 @@ dshot_result_t DShotRMT::_executeCommand(dshotCommands_e command) return result; } -// Private Initialization Functions -dshot_result_t DShotRMT::_initTXChannel() -{ - _tx_channel_config.gpio_num = _gpio; - _tx_channel_config.clk_src = DSHOT_CLOCK_SRC_DEFAULT; - _tx_channel_config.resolution_hz = DSHOT_RMT_RESOLUTION; - _tx_channel_config.mem_block_symbols = RMT_BUFFER_SYMBOLS; - _tx_channel_config.trans_queue_depth = RMT_QUEUE_DEPTH; - - _rmt_tx_config.loop_count = 0; // No automatic loops - real-time calculation - _rmt_tx_config.flags.eot_level = _is_bidirectional ? 1 : 0; - - if (rmt_new_tx_channel(&_tx_channel_config, &_rmt_tx_channel) != DSHOT_OK) - { - return {false, dshot_msg_code_t::DSHOT_TX_INIT_FAILED}; - } - - if (rmt_enable(_rmt_tx_channel) != DSHOT_OK) - { - return {false, dshot_msg_code_t::DSHOT_TX_INIT_FAILED}; - } - - return {true, dshot_msg_code_t::DSHOT_TX_INIT_SUCCESS}; -} - -dshot_result_t DShotRMT::_initRXChannel() -{ - // Double check if bidirectional mode is enabled - if (!_is_bidirectional) - { - return {true, dshot_msg_code_t::DSHOT_NONE}; - } - - _rx_channel_config.gpio_num = _gpio; - _rx_channel_config.clk_src = DSHOT_CLOCK_SRC_DEFAULT; - _rx_channel_config.resolution_hz = DSHOT_RMT_RESOLUTION; - _rx_channel_config.mem_block_symbols = RMT_BUFFER_SYMBOLS; - - // Filter for pulses that are within a reasonable range for DShot telemetry - _rmt_rx_config.signal_range_min_ns = DSHOT_PULSE_MIN_NS; - _rmt_rx_config.signal_range_max_ns = DSHOT_PULSE_MAX_NS; - - if (rmt_new_rx_channel(&_rx_channel_config, &_rmt_rx_channel) != DSHOT_OK) - { - return {false, dshot_msg_code_t::DSHOT_RX_INIT_FAILED}; - } - - // Register the callback function that will be triggered when a frame is received - _rx_event_callbacks.on_recv_done = _on_rx_done; - if (rmt_rx_register_event_callbacks(_rmt_rx_channel, &_rx_event_callbacks, this) != DSHOT_OK) - { - return {false, dshot_msg_code_t::DSHOT_CALLBACK_REGISTERING_FAILED}; - } - - if (rmt_enable(_rmt_rx_channel) != DSHOT_OK) - { - return {false, dshot_msg_code_t::DSHOT_RX_INIT_FAILED}; - } - - // 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); - if (rmt_receive(_rmt_rx_channel, rx_symbols, rx_size_bytes, &_rmt_rx_config) != DSHOT_OK) - { - return {false, dshot_msg_code_t::DSHOT_RECEIVER_FAILED}; - } - - return {true, dshot_msg_code_t::DSHOT_RX_INIT_SUCCESS}; -} - -// -dshot_result_t DShotRMT::_initDShotEncoder() -{ - rmt_bytes_encoder_config_t encoder_config = { - .bit0 = { - .duration0 = _rmt_ticks.t0h_ticks, - .level0 = _pulse_level, - .duration1 = _rmt_ticks.t0l_ticks, - .level1 = _idle_level, - }, - .bit1 = { - .duration0 = _rmt_ticks.t1h_ticks, - .level0 = _pulse_level, - .duration1 = _rmt_ticks.t1l_ticks, - .level1 = _idle_level, - }, - .flags = { - .msb_first = 1 // DShot is MSB first - }}; - - if (rmt_new_bytes_encoder(&encoder_config, &_dshot_encoder) != DSHOT_OK) - { - return {false, dshot_msg_code_t::DSHOT_ENCODER_INIT_FAILED}; - } - - return {true, dshot_msg_code_t::DSHOT_ENCODER_INIT_SUCCESS}; -} - // Private Packet Management Functions dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t &value) const { @@ -387,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); @@ -411,7 +322,7 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) // Ensure enough time has passed since the last transmission if (!_isFrameIntervalElapsed()) { - return {true, dshot_msg_code_t::DSHOT_NONE}; + return {true, DSHOT_NONE}; } _encoded_frame_value = _buildDShotFrameValue(packet); @@ -422,24 +333,28 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) // The DShot frame is 16 bits, which is 2 bytes size_t tx_size_bytes = sizeof(swapped_value); - if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, &swapped_value, tx_size_bytes, &_rmt_tx_config) != DSHOT_OK) + rmt_transmit_config_t tx_config = {}; // Initialize all members to zero + tx_config.loop_count = 0; // No automatic loops - real-time calculation + tx_config.flags.eot_level = _is_bidirectional ? 1 : 0; + + if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, &swapped_value, tx_size_bytes, &tx_config) != DSHOT_OK) { - return {false, dshot_msg_code_t::DSHOT_TRANSMISSION_FAILED}; + return {false, DSHOT_TRANSMISSION_FAILED}; } _recordFrameTransmissionTime(); // Reset the timer for the next frame - return {true, dshot_msg_code_t::DSHOT_TRANSMISSION_SUCCESS}; + 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 +// 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) { @@ -447,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) { @@ -509,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 bffad6b..211cb26 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -1,146 +1,124 @@ /** * @file DShotRMT.h - * @brief Optimized DShot signal generation using ESP32 RMT with bidirectional support + * @brief DShot signal generation using ESP32 RMT with bidirectional support * @author Wastl Kraus - * @date 2025-09-18 + * @date 2025-06-11 * @license MIT */ #pragma once -#include -#include -#include #include +#include + +#include +#include +#include #include "dshot_definitions.h" -#include +#include "dshot_init.h" -// DShotRMT Class Definition +// 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; + +// DShotRMT class for generating DShot signals and receiving telemetry. class DShotRMT { public: - // Constructor for DShotRMT with GPIO number. - explicit DShotRMT(gpio_num_t gpio = GPIO_NUM_16, dshot_mode_t mode = DSHOT300, 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 for DShotRMT with Arduino pin number. - DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional, 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); // Destructor ~DShotRMT(); - // Public Core Functions - // Initializes the DShot RMT channels and encoder. + // Initialize DShotRMT dshot_result_t begin(); - // Sends a throttle value as a percentage (0.0-100.0) to the ESC. - dshot_result_t sendThrottlePercent(float percent); - - // Sends a raw throttle value (48-2047) to the ESC. A value of 0 sends a motor stop command. + // Sends a raw throttle value to the ESC. dshot_result_t sendThrottle(uint16_t throttle); - // Sends a DShot command (0-47) to the ESC. + // Sends a throttle value as a percentage to the ESC. + dshot_result_t sendThrottlePercent(float percent); + + // Sends a DShot command to the ESC by accepting an integer value. + dshot_result_t sendCommand(uint16_t command_value); + + // Sends a DShot command to the ESC. dshot_result_t sendCommand(dshotCommands_e command); + // 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); + // Retrieves telemetry data from the ESC. dshot_result_t getTelemetry(); - // Sets the motor spin direction. 'true' for reversed, 'false' for normal. + // Sets the motor spin direction. dshot_result_t setMotorSpinDirection(bool reversed); - // Sends a command to the ESC to save its current settings. Use with caution as this writes to ESC's non-volatile memory. + // Sends a command to the ESC to save its current settings. dshot_result_t saveESCSettings(); - // Public Utility & Info Functions - // Sets the motor magnet count for RPM calculation. - void setMotorMagnetCount(uint16_t magnet_count); - - // Gets the current DShot mode. + // Getters for DShot info dshot_mode_t getMode() const { return _mode; } - - // Checks if bidirectional DShot is enabled. bool isBidirectional() const { return _is_bidirectional; } - - // Gets the encoded frame value. + uint16_t getThrottleValue() const { return _last_throttle; } uint16_t getEncodedFrameValue() const { return _encoded_frame_value; } - // Gets the last transmitted throttle value. - uint16_t getThrottleValue() const { return _packet.throttle_value; } - - // Testing return "verbose" messages - const char *getDShotMsg(dshot_result_t &result) const { return (_get_result_code_str(result.result_code)); } - - // Deprecated Methods - // Deprecated. Use sendThrottle() instead. - [[deprecated("Use sendThrottle() instead")]] - dshot_result_t sendValue(uint16_t value) { return sendThrottle(value); } - - // Deprecated. Use sendCommand() instead. - [[deprecated("Use sendCommand() instead")]] - dshot_result_t sendCommand(uint16_t command) { return sendCommand(static_cast(command)); } - private: - // --- UTILITY METHODS --- - bool _isValidCommand(dshotCommands_e command) const; - dshot_result_t _executeCommand(dshotCommands_e command); - dshot_result_t _sendCommandInternal(dshotCommands_e dshot_command, uint16_t repeat_count, uint16_t delay_us); + 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); - // Core Configuration Variables - gpio_num_t _gpio; - dshot_mode_t _mode; - bool _is_bidirectional; - uint16_t _motor_magnet_count; - const dshot_timing_us_t &_dshot_timing; - uint64_t _frame_timer_us = 0; + // 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 & Packet Variables - rmt_ticks_t _rmt_ticks{}; - uint16_t _last_throttle = dshotCommands_e::DSHOT_CMD_MOTOR_STOP; - uint64_t _last_transmission_time_us = 0; - uint64_t _last_command_timestamp = 0; - uint16_t _encoded_frame_value = 0; - dshot_packet_t _packet{}; - uint16_t _pulse_level = 1; // DShot protocol: Signal is idle-low, so pulses start by going HIGH. - uint16_t _idle_level = 0; // DShot protocol: Signal returns to LOW after the high pulse. + // 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) - // RMT Hardware Handles - rmt_channel_handle_t _rmt_tx_channel = nullptr; - rmt_channel_handle_t _rmt_rx_channel = nullptr; - rmt_encoder_handle_t _dshot_encoder = nullptr; + // 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 - // RMT Configuration Structures - rmt_tx_channel_config_t _tx_channel_config{}; - rmt_rx_channel_config_t _rx_channel_config{}; - rmt_transmit_config_t _rmt_tx_config{}; - rmt_receive_config_t _rmt_rx_config{}; + // 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, + }; - // Bidirectional / Telemetry Variables - rmt_rx_event_callbacks_t _rx_event_callbacks{}; - std::atomic _last_erpm_atomic{0}; - std::atomic _telemetry_ready_flag_atomic{false}; + // 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 - // Private Initialization Functions - dshot_result_t _initTXChannel(); - dshot_result_t _initRXChannel(); - dshot_result_t _initDShotEncoder(); - - // Private Packet Management Functions - 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(); - - // Private Frame Processing Functions - dshot_result_t _sendDShotFrame(const dshot_packet_t &packet); - uint16_t _decodeDShotFrame(const rmt_symbol_word_t *symbols) const; - - // Private Timing Control Functions - bool _isFrameIntervalElapsed() const; - void _recordFrameTransmissionTime(); - - // Static Callback Functions - static bool _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 utility functions after class definition -#include "dshot_utils.h" +#include "dshot_utils.h" // Include for helper functions diff --git a/src/dshot_definitions.h b/src/dshot_definitions.h index d8db6cb..5841a34 100644 --- a/src/dshot_definitions.h +++ b/src/dshot_definitions.h @@ -1,10 +1,28 @@ +/** + * @file dshot_definitions.h + * @brief Defines DShot protocol constants, data structures, and command enums for DShotRMT library + * @author Wastl Kraus + * @date 2025-10-04 + * @license MIT + */ + #pragma once -#include -#include -#include -#include -#include +#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 @@ -13,8 +31,7 @@ enum dshot_mode_t DSHOT150, DSHOT300, DSHOT600, - DSHOT1200, - DSHOT_MODE_MAX + DSHOT1200 }; // Represents the 16-bit DShot data packet sent to the ESC. @@ -43,7 +60,7 @@ typedef struct rmt_ticks } rmt_ticks_t; // Enum class for specific error and success codes -enum class dshot_msg_code_t +enum dshot_msg_code_t { DSHOT_NONE = 0, DSHOT_UNKNOWN, @@ -110,17 +127,9 @@ 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 }; -// DShot Protocol Constants -static constexpr auto DSHOT_THROTTLE_FAILSAFE = 0; -static constexpr auto DSHOT_THROTTLE_MIN = 48; -static constexpr auto DSHOT_THROTTLE_MAX = 2047; -static constexpr auto DSHOT_BITS_PER_FRAME = 16; -static constexpr auto DEFAULT_MOTOR_MAGNET_COUNT = 14; - // Custom status codes static constexpr int DSHOT_OK = 0; static constexpr int DSHOT_ERROR = 1; @@ -128,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 @@ -158,93 +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.00, 0.00} // DSHOT_MODE_MAX (dummy entry) -}; - -// Error Messages -static constexpr char NONE[] = ""; -static constexpr char UNKNOWN_ERROR[] = "Unknown Error!"; -static constexpr char INIT_SUCCESS[] = "SignalGeneratorRMT initialized successfully"; -static constexpr char INIT_FAILED[] = "SignalGeneratorRMT init failed!"; -static constexpr char TX_INIT_SUCCESS[] = "TX RMT channel initialized successfully"; -static constexpr char TX_INIT_FAILED[] = "TX RMT channel init failed!"; -static constexpr char RX_INIT_SUCCESS[] = "RX RMT channel initialized successfully"; -static constexpr char RX_INIT_FAILED[] = "RX RMT channel init failed!"; -static constexpr char ENCODER_INIT_SUCCESS[] = "RMT encoder initialized successfully"; -static constexpr char ENCODER_INIT_FAILED[] = "RMT encoder init failed!"; -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 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!"; -static constexpr char INVALID_MAGNET_COUNT[] = "Invalid motor magnet count!"; -static constexpr char TIMING_CORRECTION[] = "Timing correction!"; -static constexpr char CALLBACK_REGISTERING_FAILED[] = "RMT RX Callback registering failed!"; -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) -{ - switch (code) - { - case dshot_msg_code_t::DSHOT_NONE: - return NONE; - case dshot_msg_code_t::DSHOT_UNKNOWN: - return UNKNOWN_ERROR; - case dshot_msg_code_t::DSHOT_TX_INIT_FAILED: - return TX_INIT_FAILED; - case dshot_msg_code_t::DSHOT_RX_INIT_FAILED: - return RX_INIT_FAILED; - case dshot_msg_code_t::DSHOT_ENCODER_INIT_FAILED: - return ENCODER_INIT_FAILED; - case dshot_msg_code_t::DSHOT_CALLBACK_REGISTERING_FAILED: - return CALLBACK_REGISTERING_FAILED; - case dshot_msg_code_t::DSHOT_RECEIVER_FAILED: - return RECEIVER_FAILED; - case dshot_msg_code_t::DSHOT_TRANSMISSION_FAILED: - return TRANSMISSION_FAILED; - case dshot_msg_code_t::DSHOT_THROTTLE_NOT_IN_RANGE: - return THROTTLE_NOT_IN_RANGE; - case dshot_msg_code_t::DSHOT_PERCENT_NOT_IN_RANGE: - return PERCENT_NOT_IN_RANGE; - case dshot_msg_code_t::DSHOT_COMMAND_NOT_VALID: - return COMMAND_NOT_VALID; - case dshot_msg_code_t::DSHOT_BIDIR_NOT_ENABLED: - return BIDIR_NOT_ENABLED; - case dshot_msg_code_t::DSHOT_TELEMETRY_FAILED: - return TELEMETRY_FAILED; - case dshot_msg_code_t::DSHOT_INVALID_MAGNET_COUNT: - return INVALID_MAGNET_COUNT; - case dshot_msg_code_t::DSHOT_INVALID_COMMAND: - return INVALID_COMMAND; - case dshot_msg_code_t::DSHOT_TIMING_CORRECTION: - return TIMING_CORRECTION; - case dshot_msg_code_t::DSHOT_INIT_FAILED: - return INIT_FAILED; - case dshot_msg_code_t::DSHOT_INIT_SUCCESS: - return INIT_SUCCESS; - case dshot_msg_code_t::DSHOT_TX_INIT_SUCCESS: - return TX_INIT_SUCCESS; - case dshot_msg_code_t::DSHOT_RX_INIT_SUCCESS: - return RX_INIT_SUCCESS; - case dshot_msg_code_t::DSHOT_ENCODER_INIT_SUCCESS: - return ENCODER_INIT_SUCCESS; - case dshot_msg_code_t::DSHOT_ENCODING_SUCCESS: - return ENCODING_SUCCESS; - case dshot_msg_code_t::DSHOT_TRANSMISSION_SUCCESS: - return TRANSMISSION_SUCCESS; - case dshot_msg_code_t::DSHOT_TELEMETRY_SUCCESS: - return TELEMETRY_SUCCESS; - case dshot_msg_code_t::DSHOT_COMMAND_SUCCESS: - return COMMAND_SUCCESS; - default: - return UNKNOWN_ERROR; - } -} \ No newline at end of file + {0.83, 0.67} // DSHOT1200 +}; \ No newline at end of file diff --git a/src/dshot_init.cpp b/src/dshot_init.cpp new file mode 100644 index 0000000..4f2f6c3 --- /dev/null +++ b/src/dshot_init.cpp @@ -0,0 +1,107 @@ +/** + * @file dshot_init.cpp + * @brief RMT configuration and initialization functions for DShotRMT library + * @author Wastl Kraus + * @date 2025-10-04 + * @license MIT + */ + +#include "dshot_init.h" + +// Function to initialize the RMT TX channel +dshot_result_t init_rmt_tx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_channel, bool is_bidirectional) +{ + rmt_tx_channel_config_t tx_channel_config = { + .gpio_num = gpio, + .clk_src = DSHOT_CLOCK_SRC_DEFAULT, + .resolution_hz = DSHOT_RMT_RESOLUTION, + .mem_block_symbols = RMT_BUFFER_SYMBOLS, + .trans_queue_depth = RMT_QUEUE_DEPTH, + }; + + rmt_transmit_config_t rmt_tx_config = {}; // Initialize all members to zero + rmt_tx_config.loop_count = 0; // No automatic loops - real-time calculation + rmt_tx_config.flags.eot_level = is_bidirectional ? 1 : 0; + + if (rmt_new_tx_channel(&tx_channel_config, out_channel) != DSHOT_OK) + { + return {false, DSHOT_TX_INIT_FAILED}; + } + + if (rmt_enable(*out_channel) != DSHOT_OK) + { + return {false, DSHOT_TX_INIT_FAILED}; + } + + return {true, DSHOT_TX_INIT_SUCCESS}; +} + +// Function to initialize the RMT RX channel +dshot_result_t init_rmt_rx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_channel, rmt_rx_event_callbacks_t *rx_event_callbacks, void *user_data) +{ + rmt_rx_channel_config_t rx_channel_config = { + .gpio_num = gpio, + .clk_src = DSHOT_CLOCK_SRC_DEFAULT, + .resolution_hz = DSHOT_RMT_RESOLUTION, + .mem_block_symbols = RMT_BUFFER_SYMBOLS, + }; + + if (rmt_new_rx_channel(&rx_channel_config, out_channel) != DSHOT_OK) + { + return {false, DSHOT_RX_INIT_FAILED}; + } + + if (rmt_rx_register_event_callbacks(*out_channel, rx_event_callbacks, user_data) != DSHOT_OK) + { + return {false, DSHOT_CALLBACK_REGISTERING_FAILED}; + } + + if (rmt_enable(*out_channel) != DSHOT_OK) + { + return {false, DSHOT_RX_INIT_FAILED}; + } + + // 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); + + rmt_receive_config_t rmt_rx_config = { + .signal_range_min_ns = DSHOT_PULSE_MIN_NS, + .signal_range_max_ns = DSHOT_PULSE_MAX_NS, + }; + + if (rmt_receive(*out_channel, rx_symbols, rx_size_bytes, &rmt_rx_config) != DSHOT_OK) + { + return {false, DSHOT_RECEIVER_FAILED}; + } + + return {true, DSHOT_RX_INIT_SUCCESS}; +} + +// Function to initialize the DShot RMT encoder +dshot_result_t init_dshot_encoder(rmt_encoder_handle_t *out_encoder, const rmt_ticks_t &rmt_ticks, uint16_t pulse_level, uint16_t idle_level) +{ + rmt_bytes_encoder_config_t encoder_config = { + .bit0 = { + .duration0 = rmt_ticks.t0h_ticks, + .level0 = pulse_level, + .duration1 = rmt_ticks.t0l_ticks, + .level1 = idle_level, + }, + .bit1 = { + .duration0 = rmt_ticks.t1h_ticks, + .level0 = pulse_level, + .duration1 = rmt_ticks.t1l_ticks, + .level1 = idle_level, + }, + .flags = { + .msb_first = 1 // DShot is MSB first + }}; + + if (rmt_new_bytes_encoder(&encoder_config, out_encoder) != DSHOT_OK) + { + return {false, DSHOT_ENCODER_INIT_FAILED}; + } + + return {true, DSHOT_ENCODER_INIT_SUCCESS}; +} diff --git a/src/dshot_init.h b/src/dshot_init.h new file mode 100644 index 0000000..7003f5e --- /dev/null +++ b/src/dshot_init.h @@ -0,0 +1,23 @@ +/** + * @file dshot_init.h + * @brief RMT configuration and initialization function declarations for DShotRMT library + * @author Wastl Kraus + * @date 2025-10-04 + * @license MIT + */ + +#pragma once + +#include +#include + +#include "dshot_definitions.h" + +// Function to initialize the RMT TX channel +dshot_result_t init_rmt_tx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_channel, bool is_bidirectional); + +// Function to initialize the RMT RX channel +dshot_result_t init_rmt_rx_channel(gpio_num_t gpio, rmt_channel_handle_t *out_channel, rmt_rx_event_callbacks_t *rx_event_callbacks, void *user_data); + +// Function to initialize the DShot RMT encoder +dshot_result_t init_dshot_encoder(rmt_encoder_handle_t *out_encoder, const rmt_ticks_t &rmt_ticks, uint16_t pulse_level, uint16_t idle_level); diff --git a/src/dshot_utils.h b/src/dshot_utils.h index a1abc18..72202f2 100644 --- a/src/dshot_utils.h +++ b/src/dshot_utils.h @@ -1,12 +1,109 @@ +/** + * @file dshot_utils.h + * @brief Utility functions for DShotRMT library + * @author Wastl Kraus + * @date 2025-10-04 + * @license MIT + */ + #pragma once #include -#include "DShotRMT.h" // Include the full class definition + +// Forward declaration of the DShotRMT class to break circular dependency +class DShotRMT; + +// Error Messages +static constexpr char NONE[] = ""; +static constexpr char UNKNOWN_ERROR[] = "Unknown Error!"; +static constexpr char INIT_SUCCESS[] = "SignalGeneratorRMT initialized successfully"; +static constexpr char INIT_FAILED[] = "SignalGeneratorRMT init failed!"; +static constexpr char TX_INIT_SUCCESS[] = "TX RMT channel initialized successfully"; +static constexpr char TX_INIT_FAILED[] = "TX RMT channel init failed!"; +static constexpr char RX_INIT_SUCCESS[] = "RX RMT channel initialized successfully"; +static constexpr char RX_INIT_FAILED[] = "RX RMT channel init failed!"; +static constexpr char ENCODER_INIT_SUCCESS[] = "RMT encoder initialized successfully"; +static constexpr char ENCODER_INIT_FAILED[] = "RMT encoder init failed!"; +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 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!"; +static constexpr char INVALID_MAGNET_COUNT[] = "Invalid motor magnet count!"; +static constexpr char TIMING_CORRECTION[] = "Timing correction!"; +static constexpr char CALLBACK_REGISTERING_FAILED[] = "RMT RX Callback registering failed!"; +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) +{ + switch (code) + { + case DSHOT_NONE: + return NONE; + case DSHOT_UNKNOWN: + return UNKNOWN_ERROR; + case DSHOT_TX_INIT_FAILED: + return TX_INIT_FAILED; + case DSHOT_RX_INIT_FAILED: + return RX_INIT_FAILED; + case DSHOT_ENCODER_INIT_FAILED: + return ENCODER_INIT_FAILED; + case DSHOT_CALLBACK_REGISTERING_FAILED: + return CALLBACK_REGISTERING_FAILED; + case DSHOT_RECEIVER_FAILED: + return RECEIVER_FAILED; + case DSHOT_TRANSMISSION_FAILED: + return TRANSMISSION_FAILED; + case DSHOT_THROTTLE_NOT_IN_RANGE: + return THROTTLE_NOT_IN_RANGE; + case DSHOT_PERCENT_NOT_IN_RANGE: + return PERCENT_NOT_IN_RANGE; + case DSHOT_COMMAND_NOT_VALID: + return COMMAND_NOT_VALID; + case DSHOT_BIDIR_NOT_ENABLED: + return BIDIR_NOT_ENABLED; + case DSHOT_TELEMETRY_FAILED: + return TELEMETRY_FAILED; + case DSHOT_INVALID_MAGNET_COUNT: + return INVALID_MAGNET_COUNT; + case DSHOT_INVALID_COMMAND: + return INVALID_COMMAND; + case DSHOT_TIMING_CORRECTION: + return TIMING_CORRECTION; + case DSHOT_INIT_FAILED: + return INIT_FAILED; + case DSHOT_INIT_SUCCESS: + return INIT_SUCCESS; + case DSHOT_TX_INIT_SUCCESS: + return TX_INIT_SUCCESS; + case DSHOT_RX_INIT_SUCCESS: + return RX_INIT_SUCCESS; + case DSHOT_ENCODER_INIT_SUCCESS: + return ENCODER_INIT_SUCCESS; + case DSHOT_ENCODING_SUCCESS: + return ENCODING_SUCCESS; + case DSHOT_TRANSMISSION_SUCCESS: + return TRANSMISSION_SUCCESS; + case DSHOT_TELEMETRY_SUCCESS: + return TELEMETRY_SUCCESS; + case DSHOT_COMMAND_SUCCESS: + return COMMAND_SUCCESS; + default: + return UNKNOWN_ERROR; + } +} // 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)) diff --git a/src/ota_update.h b/src/web_utilities/ota_update.h similarity index 99% rename from src/ota_update.h rename to src/web_utilities/ota_update.h index 1d78be7..1d85cc3 100644 --- a/src/ota_update.h +++ b/src/web_utilities/ota_update.h @@ -2,7 +2,7 @@ * @file ota_update.h * @brief Contains the HTML, CSS, and JavaScript for the OTA (Over-The-Air) update web page. * @author Wastl Kraus - * @date 2025-09-13 + * @date 2025-10-04 * @license MIT */ diff --git a/src/web_content.h b/src/web_utilities/web_content.h similarity index 99% rename from src/web_content.h rename to src/web_utilities/web_content.h index d5b3a0a..c41a6e2 100644 --- a/src/web_content.h +++ b/src/web_utilities/web_content.h @@ -2,7 +2,7 @@ * @file web_content.h * @brief Contains the HTML, CSS, and JavaScript for the main DShot control web page. * @author Wastl Kraus - * @date 2025-09-13 + * @date 2025-10-04 * @license MIT */