From 24d3ab700fa85a9591f8963ff4e0e0e814de2bdc Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Thu, 25 Sep 2025 13:12:25 +0200 Subject: [PATCH 1/8] Further optimizing --- README.md | 14 ++++++-------- src/DShotRMT.cpp | 8 ++++---- src/DShotRMT.h | 2 +- src/dshot_commands.h | 2 -- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4ac852e..7508dde 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,15 @@ [![Arduino CI](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml/badge.svg)](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml) -A C++ library for generating DShot signals on ESP32 microcontrollers using the RMT (Remote Control) peripheral. It's designed for both Arduino and ESP-IDF projects, providing a simple and efficient way to control brushless motors. - -This library is a rewrite using the modern ESP-IDF 5 RMT encoder API (`rmt_tx.h` / `rmt_rx.h`) for improved performance and flexibility. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. +A C++ library for generating DShot signals on ESP32 microcontrollers using the **modern ESP-IDF 5 RMT encoder API** (`rmt_tx.h` / `rmt_rx.h`). It provides a simple, efficient, and hardware-timed way to control brushless motors in both Arduino and ESP-IDF projects. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. ## ๐Ÿš€ Core Features - **Multiple DShot Modes:** Supports DSHOT150, DSHOT300, DSHOT600, and DSHOT1200. -- **Bidirectional DShot:** Implemented, but currently not officially supported due to instability and external hardware requirements. +- **Bidirectional DShot Support:** Implemented, but note that official support is limited due to potential instability and external hardware requirements. Use with caution. - **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()`. +- **Robust Error Handling:** Provides detailed feedback on operation success or failure via `dshot_result_t`. - **Efficient and Lightweight:** The core library has no external dependencies. - **Arduino and ESP-IDF Compatible:** Can be used in both Arduino and ESP-IDF projects. @@ -38,7 +37,7 @@ Here's a basic example of how to use the `DShotRMT` library to control a motor: ```cpp #include -#include +#include // Include the DShotRMT library // Define the GPIO pin connected to the motor ESC const gpio_num_t MOTOR_PIN = GPIO_NUM_27; @@ -100,14 +99,13 @@ lib_deps = The main class is `DShotRMT`. Here are the most important methods: -- `DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional = false)`: Constructor to create a new DShotRMT instance. (Note: Bidirectional DShot is currently not officially supported.) +- `DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT)`: Constructor to create a new DShotRMT instance. (Note: Bidirectional DShot is currently not officially supported.) - `begin()`: Initializes the RMT peripheral and the DShot encoder. - `sendThrottlePercent(float percent)`: Sends a throttle value as a percentage (0.0-100.0). - `sendThrottle(uint16_t throttle)`: Sends a raw throttle value (48-2047) to the motor. - `sendCommand(uint16_t command)`: Sends a DShot command (0-47) to the motor. - `getTelemetry(uint16_t magnet_count)`: Receives and parses telemetry data from the motor (for bidirectional DShot, which is currently not officially supported). - -For more details, please refer to the `DShotRMT.h` header file. +- `printDShotResult(dshot_result_t &result, Stream &output = Serial)`: Helper function to print DShot operation results and telemetry to a specified serial output. ## ๐Ÿค Contributing diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 7c64131..dbea4fa 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -44,7 +44,7 @@ DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, ui _last_throttle(DSHOT_CMD_MOTOR_STOP), _last_transmission_time_us(0), _last_command_timestamp(0), - _parsed_packet(0), + _encoded_frame_value(0), _packet{0}, _bitPositions{0}, _level0(1), // DShot standard: signal is idle-low, so pulses start by going HIGH @@ -306,7 +306,7 @@ void DShotRMT::printDShotInfo(Stream &output) const for (int i = DSHOT_BITS_PER_FRAME - 1; i >= 0; --i) { - output.print((_parsed_packet >> i) & 1); + output.print((_encoded_frame_value >> i) & 1); } output.printf("\nCurrent Value: %u\n", _packet.throttle_value); @@ -520,12 +520,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 dshot_result_t IRAM_ATTR DShotRMT::_encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols) { - _parsed_packet = _parseDShotPacket(packet); + _encoded_frame_value = _parseDShotPacket(packet); for (int i = 0; i < DSHOT_BITS_PER_FRAME; ++i) { int bit_position = _bitPositions[i]; - bool bit = (_parsed_packet >> bit_position) & 1; + bool bit = (_encoded_frame_value >> bit_position) & 1; // A '1' bit has a longer high-time, a '0' bit has a shorter high-time symbols[i].level0 = _level0; // Go HIGH diff --git a/src/DShotRMT.h b/src/DShotRMT.h index 0966141..76370ab 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -189,7 +189,7 @@ private: uint16_t _last_throttle; uint64_t _last_transmission_time_us; uint64_t _last_command_timestamp; - uint16_t _parsed_packet; + uint16_t _encoded_frame_value; dshot_packet_t _packet; uint8_t _bitPositions[DSHOT_BITS_PER_FRAME]; uint16_t _level0; // Signal level for the first part of a pulse (always HIGH for DShot) diff --git a/src/dshot_commands.h b/src/dshot_commands.h index 319aaba..702a065 100644 --- a/src/dshot_commands.h +++ b/src/dshot_commands.h @@ -20,8 +20,6 @@ #pragma once -#define DSHOT_MAX_COMMAND 47 - /* DshotSettingRequest (KISS24). Spin direction, 3d and save Settings require 10 requests.. and the TLM Byte must always be high if 1-47 are used to send settings From 57d6a27f740b53b08131b8f68dc297e95e759bd2 Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Thu, 25 Sep 2025 14:03:55 +0200 Subject: [PATCH 2/8] typedef enum to class --- examples/dshot300/dshot300.ino | 19 +-- library.properties | 2 +- src/DShotRMT.cpp | 161 ++++++-------------- src/DShotRMT.h | 265 ++++++++++++++++---------------- src/DShotRMT_Utils.cpp | 30 ++++ src/DShotRMT_Utils.h | 21 +++ src/dshot_commands.h | 68 --------- src/dshot_definitions.h | 267 +++++++++++++++++++++++++++++++++ 8 files changed, 508 insertions(+), 325 deletions(-) create mode 100644 src/DShotRMT_Utils.cpp create mode 100644 src/DShotRMT_Utils.h delete mode 100644 src/dshot_commands.h create mode 100644 src/dshot_definitions.h diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index d13533c..70fa73a 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -7,7 +7,8 @@ */ #include -#include +#include "DShotRMT.h" +#include "DShotRMT_Utils.h" // Include utility functions // USB serial port settings static constexpr auto &USB_SERIAL = Serial0; @@ -18,7 +19,7 @@ static constexpr gpio_num_t MOTOR01_PIN = GPIO_NUM_27; // static constexpr auto MOTOR01_PIN = 17; // Supported: DSHOT150, DSHOT300, DSHOT600, (DSHOT1200) -static constexpr dshot_mode_t DSHOT_MODE = DSHOT300; +static constexpr dshot_mode_t DSHOT_MODE = dshot_mode_t::DSHOT300; // BiDirectional DShot Support (default: false) // Note: Bidirectional DShot is currently not officially supported @@ -41,7 +42,7 @@ void setup() motor01.begin(); // Print CPU Info - motor01.printCpuInfo(); + printCpuInfo(USB_SERIAL); // printMenu(); @@ -51,7 +52,7 @@ void setup() void loop() { // Safety first - static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; + static uint16_t throttle = static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP); // Initialize the esc with "0" static bool continuous_throttle = true; @@ -80,7 +81,7 @@ void loop() // Print motor stats every 3 seconds in continuous mode if (continuous_throttle && (esp_timer_get_time() - last_stats_print >= 3000000)) { - motor01.printDShotInfo(); + printDShotInfo(motor01, USB_SERIAL); USB_SERIAL.println(" "); @@ -127,12 +128,12 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous // Stop motor throttle = 0; continuous_throttle = true; - dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + dshot_result_t result = motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); printDShotResult(result); } else if (input == "info") { - motor01.printDShotInfo(); + printDShotInfo(motor01, USB_SERIAL); } else if (input == "rpm" && IS_BIDIRECTIONAL) { @@ -146,14 +147,14 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous // Send DShot command int cmd_num = input.substring(4).toInt(); - if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) + if (cmd_num >= static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP) && cmd_num <= static_cast(dshotCommands_e::DSHOT_CMD_MAX)) { dshot_result_t result = motor01.sendCommand(cmd_num); printDShotResult(result); } else { - USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX); + USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, static_cast(dshotCommands_e::DSHOT_CMD_MAX)); } } else if (input == "h" || input == "help") diff --git a/library.properties b/library.properties index 7ee1a53..ed727ea 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=DShotRMT -version=0.8.3 +version=0.8.5 author=Wastl Kraus maintainer=Wastl Kraus license=MIT diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index dbea4fa..0ac1e2b 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -8,29 +8,6 @@ #include -// Timing parameters for each DShot mode -// Format: {bit_length_us, t1h_length_us} -static constexpr dshot_timing_us_t DSHOT_TIMING_US[] = { - {0.00, 0.00}, // DSHOT_OFF - {6.67, 5.00}, // DSHOT150 - {3.33, 2.50}, // DSHOT300 - {1.67, 1.25}, // DSHOT600 - {0.83, 0.67}}; // DSHOT1200 - -// Helper function to print DShot results and telemetry -void printDShotResult(dshot_result_t &result, Stream &output) -{ - output.printf("Status: %s - %s", result.success ? "SUCCESS" : "FAILED", result.msg); - - // Print telemetry data if available - if (result.success && (result.erpm > 0 || result.motor_rpm > 0)) - { - output.printf(" | eRPM: %u, Motor RPM: %u", result.erpm, result.motor_rpm); - } - - output.println(); -} - // Constructors & Destructor // Constructor with GPIO number DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count) @@ -38,15 +15,14 @@ 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[mode]), + _dshot_timing(DSHOT_TIMING_US[static_cast(mode)]), _frame_timer_us(0), _rmt_ticks{0}, - _last_throttle(DSHOT_CMD_MOTOR_STOP), + _last_throttle(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)), _last_transmission_time_us(0), _last_command_timestamp(0), _encoded_frame_value(0), _packet{0}, - _bitPositions{0}, _level0(1), // DShot standard: signal is idle-low, so pulses start by going HIGH _level1(0), // DShot standard: signal returns to LOW after the high pulse _rmt_tx_channel(nullptr), @@ -62,7 +38,6 @@ DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, ui { // Pre-calculate timing and bit positions for performance _preCalculateRMTTicks(); - _preCalculateBitPositions(); // Activate internal pullup resistor // gpio_set_pull_mode(_gpio, GPIO_PULLUP_ONLY); @@ -112,7 +87,7 @@ dshot_result_t DShotRMT::begin() { if (!_initTXChannel().success) { - return {false, TX_INIT_FAILED}; + return {false, dshot_msg_code_t::DSHOT_ERROR_TX_INIT_FAILED}; } if (_is_bidirectional) @@ -123,7 +98,7 @@ dshot_result_t DShotRMT::begin() rmt_disable(_rmt_tx_channel); rmt_del_channel(_rmt_tx_channel); _rmt_tx_channel = nullptr; - return {false, RX_INIT_FAILED}; + return {false, dshot_msg_code_t::DSHOT_ERROR_RX_INIT_FAILED}; } } @@ -140,10 +115,10 @@ dshot_result_t DShotRMT::begin() _rmt_rx_channel = nullptr; } - return {false, ENCODER_INIT_FAILED}; + return {false, dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED}; } - return {true, INIT_SUCCESS}; + return {true, dshot_msg_code_t::DSHOT_ERROR_INIT_SUCCESS}; } // Send throttle value @@ -152,7 +127,7 @@ dshot_result_t DShotRMT::sendThrottle(uint16_t throttle) // A throttle value of 0 is a disarm command if (throttle == 0) { - return sendCommand(DSHOT_CMD_MOTOR_STOP); + return sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); } // Constrain throttle to the valid DShot range @@ -167,7 +142,7 @@ dshot_result_t DShotRMT::sendThrottlePercent(float percent) { if (percent < 0.0f || percent > 100.0f) { - return {false, PERCENT_NOT_IN_RANGE}; + return {false, dshot_msg_code_t::DSHOT_ERROR_PERCENT_NOT_IN_RANGE}; } // Map percent to DShot throttle range @@ -179,9 +154,9 @@ dshot_result_t DShotRMT::sendThrottlePercent(float percent) // Send DShot command to ESC dshot_result_t DShotRMT::sendCommand(uint16_t command) { - if (command > DSHOT_CMD_MAX) + if (command > static_cast(dshotCommands_e::DSHOT_CMD_MAX)) { - return {false, COMMAND_NOT_VALID}; + return {false, dshot_msg_code_t::DSHOT_ERROR_COMMAND_NOT_VALID}; } _packet = _buildDShotPacket(command); @@ -189,13 +164,14 @@ dshot_result_t DShotRMT::sendCommand(uint16_t command) } // Send full DShot commands for setup etc -dshot_result_t DShotRMT::sendCommand(dshot_commands_t dshot_command, uint16_t repeat_count, uint16_t delay_us) +// This is a blocking function that uses delayMicroseconds for repetitions. +dshot_result_t DShotRMT::sendCommand(dshotCommands_e dshot_command, uint16_t repeat_count, uint16_t delay_us) { - dshot_result_t result = {false, UNKNOWN_ERROR}; + dshot_result_t result = {false, dshot_msg_code_t::DSHOT_ERROR_UNKNOWN}; if (!_isValidCommand(dshot_command)) { - result.msg = INVALID_COMMAND; + result.error_code = dshot_msg_code_t::DSHOT_ERROR_INVALID_COMMAND; return result; } @@ -209,7 +185,7 @@ dshot_result_t DShotRMT::sendCommand(dshot_commands_t dshot_command, uint16_t re if (!single_result.success) { all_successful = false; - result.msg = single_result.msg; + result.error_code = single_result.error_code; break; } @@ -225,7 +201,7 @@ dshot_result_t DShotRMT::sendCommand(dshot_commands_t dshot_command, uint16_t re if (result.success) { - result.msg = COMMAND_SUCCESS; + result.error_code = dshot_msg_code_t::DSHOT_ERROR_COMMAND_SUCCESS; } return result; @@ -234,11 +210,11 @@ dshot_result_t DShotRMT::sendCommand(dshot_commands_t dshot_command, uint16_t re // Get telemetry data dshot_result_t DShotRMT::getTelemetry(uint16_t magnet_count) { - dshot_result_t result = {false, TELEMETRY_FAILED, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY}; + dshot_result_t result = {false, dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_FAILED, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY}; if (!_is_bidirectional) { - result.msg = BIDIR_NOT_ENABLED; + result.error_code = dshot_msg_code_t::DSHOT_ERROR_BIDIR_NOT_ENABLED; return result; } @@ -260,7 +236,7 @@ dshot_result_t DShotRMT::getTelemetry(uint16_t magnet_count) result.success = true; result.erpm = erpm; result.motor_rpm = motor_rpm; - result.msg = TELEMETRY_SUCCESS; + result.error_code = dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_SUCCESS; } } @@ -271,66 +247,30 @@ dshot_result_t DShotRMT::getTelemetry(uint16_t magnet_count) dshot_result_t DShotRMT::setMotorSpinDirection(bool reversed) { // Use command as a yes / no switch - dshot_commands_t command = reversed ? DSHOT_CMD_SPIN_DIRECTION_REVERSED : DSHOT_CMD_SPIN_DIRECTION_NORMAL; + dshotCommands_e command = reversed ? dshotCommands_e::DSHOT_CMD_SPIN_DIRECTION_REVERSED : dshotCommands_e::DSHOT_CMD_SPIN_DIRECTION_NORMAL; return sendCommand(command, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); } dshot_result_t DShotRMT::getESCInfo() { - return sendCommand(DSHOT_CMD_ESC_INFO); + return sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_ESC_INFO)); } // Use with caution dshot_result_t DShotRMT::saveESCSettings() { - return sendCommand(DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); -} - -// Public Info & Debug Functions -void DShotRMT::setMotorMagnetCount(uint16_t magnet_count) -{ - _motor_magnet_count = magnet_count; -} - -void DShotRMT::printDShotInfo(Stream &output) const -{ - output.println("\n === DShot Signal Info === "); - output.printf("Current Mode: DSHOT%d\n", _mode == DSHOT150 ? 150 : - _mode == DSHOT300 ? 300 : - _mode == DSHOT600 ? 600 : - _mode == DSHOT1200 ? 1200 : 0); - - output.printf("Bidirectional: %s\n", _is_bidirectional ? "YES" : "NO"); - output.printf("Current Packet: "); - - for (int i = DSHOT_BITS_PER_FRAME - 1; i >= 0; --i) - { - output.print((_encoded_frame_value >> i) & 1); - } - - output.printf("\nCurrent Value: %u\n", _packet.throttle_value); -} - -// -void DShotRMT::printCpuInfo(Stream &output) const -{ - output.println("\n === CPU Info === "); - output.printf("Chip Model: %s\n", ESP.getChipModel()); - output.printf("Chip Revision: %d\n", ESP.getChipRevision()); - output.printf("CPU Freq = %lu MHz\n", ESP.getCpuFreqMHz()); - output.printf("XTAL Freq = %lu MHz\n", getXtalFrequencyMhz()); - output.printf("APB Freq = %lu Hz\n", getApbFrequency()); + return sendCommand(dshotCommands_e::DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US); } // Simple check -bool DShotRMT::_isValidCommand(dshot_commands_t command) +bool DShotRMT::_isValidCommand(dshotCommands_e command) { - return (command >= DSHOT_CMD_MOTOR_STOP && command <= DSHOT_CMD_MAX); + return (static_cast(command) >= static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP) && static_cast(command) <= static_cast(dshotCommands_e::DSHOT_CMD_MAX)); } // -dshot_result_t DShotRMT::_executeCommand(dshot_commands_t command) +dshot_result_t DShotRMT::_executeCommand(dshotCommands_e command) { uint64_t start_time = esp_timer_get_time(); @@ -357,14 +297,14 @@ dshot_result_t DShotRMT::_initTXChannel() if (rmt_new_tx_channel(&_tx_channel_config, &_rmt_tx_channel) != DSHOT_OK) { - return {false, TX_INIT_FAILED}; + return {false, dshot_msg_code_t::DSHOT_ERROR_TX_INIT_FAILED}; } if (rmt_enable(_rmt_tx_channel) != DSHOT_OK) { - return {false, TX_INIT_FAILED}; + return {false, dshot_msg_code_t::DSHOT_ERROR_TX_INIT_FAILED}; } - return {true, TX_INIT_SUCCESS}; + return {true, dshot_msg_code_t::DSHOT_ERROR_TX_INIT_SUCCESS}; } dshot_result_t DShotRMT::_initRXChannel() @@ -372,7 +312,7 @@ dshot_result_t DShotRMT::_initRXChannel() // Double check if bidirectional mode is enabled if (!_is_bidirectional) { - return {true, NONE}; + return {true, dshot_msg_code_t::DSHOT_ERROR_NONE}; } _rx_channel_config.gpio_num = _gpio; @@ -386,19 +326,19 @@ dshot_result_t DShotRMT::_initRXChannel() if (rmt_new_rx_channel(&_rx_channel_config, &_rmt_rx_channel) != DSHOT_OK) { - return {false, RX_INIT_FAILED}; + return {false, dshot_msg_code_t::DSHOT_ERROR_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, CALLBACK_REGISTERING_FAILED}; + return {false, dshot_msg_code_t::DSHOT_ERROR_CALLBACK_REGISTERING_FAILED}; } if (rmt_enable(_rmt_rx_channel) != DSHOT_OK) { - return {false, RX_INIT_FAILED}; + return {false, dshot_msg_code_t::DSHOT_ERROR_RX_INIT_FAILED}; } // Start the receiver to wait for incoming telemetry data @@ -406,10 +346,10 @@ dshot_result_t DShotRMT::_initRXChannel() 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, RECEIVER_FAILED}; + return {false, dshot_msg_code_t::DSHOT_ERROR_RECEIVER_FAILED}; } - return {true, RX_INIT_SUCCESS}; + return {true, dshot_msg_code_t::DSHOT_ERROR_RX_INIT_SUCCESS}; } dshot_result_t DShotRMT::_initDShotEncoder() @@ -418,10 +358,10 @@ dshot_result_t DShotRMT::_initDShotEncoder() if (rmt_new_copy_encoder(&encoder_config, &_dshot_encoder) != DSHOT_OK) { - return {false, ENCODER_INIT_FAILED}; + return {false, dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED}; } - return {true, ENCODER_INIT_SUCCESS}; + return {true, dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_SUCCESS}; } // Private Packet Management Functions @@ -439,7 +379,7 @@ dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t &value) return packet; } -uint16_t DShotRMT::_parseDShotPacket(const dshot_packet_t &packet) +uint16_t DShotRMT::_buildDShotFrameValue(const dshot_packet_t &packet) { // Combine throttle, telemetry bit, and CRC into a single 16-bit frame uint16_t data_and_telemetry = (packet.throttle_value << 1) | packet.telemetric_request; @@ -479,22 +419,13 @@ void DShotRMT::_preCalculateRMTTicks() } } -void DShotRMT::_preCalculateBitPositions() -{ - // Pre-calculate bit positions to avoid redundant calculations in the encoding loop - for (int i = 0; i < DSHOT_BITS_PER_FRAME; ++i) - { - _bitPositions[i] = DSHOT_BITS_PER_FRAME - 1 - i; - } -} - // Private Frame Processing Functions dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) { // Ensure enough time has passed since the last transmission if (!_timer_signal()) { - return {true, NONE}; + return {true, dshot_msg_code_t::DSHOT_ERROR_NONE}; } rmt_symbol_word_t tx_symbols[DSHOT_BITS_PER_FRAME]; @@ -509,22 +440,22 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, tx_symbols, tx_size_bytes, &_rmt_tx_config) != DSHOT_OK) { - return {false, TRANSMISSION_FAILED}; + return {false, dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_FAILED}; } _timer_reset(); // Reset the timer for the next frame - return {true, TRANSMISSION_SUCCESS}; + return {true, dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_SUCCESS}; } // This function needs to be fast, as it generates the RMT symbols just before sending dshot_result_t IRAM_ATTR DShotRMT::_encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols) { - _encoded_frame_value = _parseDShotPacket(packet); + _encoded_frame_value = _buildDShotFrameValue(packet); for (int i = 0; i < DSHOT_BITS_PER_FRAME; ++i) { - int bit_position = _bitPositions[i]; + int bit_position = DSHOT_BITS_PER_FRAME - 1 - i; bool bit = (_encoded_frame_value >> bit_position) & 1; // A '1' bit has a longer high-time, a '0' bit has a shorter high-time @@ -534,7 +465,7 @@ dshot_result_t IRAM_ATTR DShotRMT::_encodeDShotFrame(const dshot_packet_t &packe symbols[i].duration1 = bit ? _rmt_ticks.t1l_ticks : _rmt_ticks.t0l_ticks; } - return {true, ENCODING_SUCCESS}; + return {true, dshot_msg_code_t::DSHOT_ERROR_ENCODING_SUCCESS}; } // Placed in IRAM for high performance, as it's called from an ISR context @@ -557,11 +488,11 @@ uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) uint16_t data_and_crc = (decoded_frame & DSHOT_FULL_PACKET); // Step 4: Extract data and CRC from the 16-bit frame - uint16_t received_data = data_and_crc >> 4; + 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. - if (!((received_data >> 11) & 1)) + if (!((received_data >> DSHOT_TELEMETRY_BIT_POSITION) & 1)) { return DSHOT_NULL_PACKET; } @@ -612,4 +543,4 @@ bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const } return false; -} +} \ No newline at end of file diff --git a/src/DShotRMT.h b/src/DShotRMT.h index 76370ab..ebda5d0 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -9,101 +9,146 @@ #pragma once #include -#include +#include "dshot_definitions.h" #include #include #include #include -// DShot Protocol Constants & Types -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 auto DSHOT_OK = 0; -static constexpr auto DSHOT_ERROR = 1; - -// DShot Modes -typedef enum dshot_mode -{ - DSHOT_OFF, - DSHOT150, - DSHOT300, - DSHOT600, - DSHOT1200 -} dshot_mode_t; - -// DShot Packet Structure -typedef struct dshot_packet -{ - uint16_t throttle_value : 11; - bool telemetric_request : 1; - uint16_t checksum : 4; -} dshot_packet_t; - -// DShot Timing Configuration -typedef struct dshot_timing -{ - double bit_length_us; - double t1h_lenght_us; -} dshot_timing_us_t; - -// RMT Timing Configuration -typedef struct rmt_ticks -{ - uint16_t bit_length_ticks; - uint16_t t1h_ticks; - uint16_t t1l_ticks; - uint16_t t0h_ticks; - uint16_t t0l_ticks; -} rmt_ticks_t; - -// Unified DShot Result Structure -typedef struct dshot_result -{ - bool success; - const char *msg; - uint16_t erpm; - uint16_t motor_rpm; -} dshot_result_t; - -// Command Type Alias -typedef dshotCommands_e dshot_commands_t; - -// Helper Functions -void printDShotResult(dshot_result_t &result, Stream &output = Serial); - -// -// DShotRMT Main Class +/** + * @brief DShotRMT Main Class for DShot signal generation and reception. + * + * This class provides an interface to generate DShot signals for Electronic Speed Controllers (ESCs) + * and to receive telemetry data using the ESP32's RMT peripheral. + */ class DShotRMT { public: - // Constructors & Destructor - 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); + /** + * @brief Constructor for DShotRMT with GPIO number. + * @param gpio The GPIO pin number to use for DShot communication. + * @param mode The DShot mode (e.g., DSHOT150, DSHOT300, DSHOT600). + * @param is_bidirectional True if bidirectional DShot is enabled, false otherwise. + * @param magnet_count The number of magnets in the motor for RPM calculation. + */ + explicit DShotRMT(gpio_num_t gpio = GPIO_NUM_16, dshot_mode_t mode = dshot_mode_t::DSHOT300, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); + + /** + * @brief Constructor for DShotRMT with Arduino pin number. + * @param pin_nr The Arduino pin number to use for DShot communication. + * @param mode The DShot mode (e.g., DSHOT150, DSHOT300, DSHOT600). + * @param is_bidirectional True if bidirectional DShot is enabled, false otherwise. + * @param magnet_count The number of magnets in the motor for RPM calculation. + */ DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); + /** + * @brief Destructor for DShotRMT. + * Cleans up RMT channels and encoder resources. + */ ~DShotRMT(); // Public Core Functions + /** + * @brief Initializes the DShot RMT channels and encoder. + * @return dshot_result_t indicating success or failure of the initialization. + */ dshot_result_t begin(); + + /** + * @brief Sends a DShot throttle value to the ESC. + * @param throttle The throttle value (48-2047). A value of 0 sends a motor stop command. + * @return dshot_result_t indicating success or failure of the transmission. + */ dshot_result_t sendThrottle(uint16_t throttle); + + /** + * @brief Sends a DShot throttle value as a percentage to the ESC. + * @param percent The throttle percentage (0.0f - 100.0f). + * @return dshot_result_t indicating success or failure of the transmission. + */ dshot_result_t sendThrottlePercent(float percent); + + /** + * @brief Sends a single DShot command to the ESC. + * @param command The DShot command value (0-47). + * @return dshot_result_t indicating success or failure of the transmission. + */ dshot_result_t sendCommand(uint16_t command); - dshot_result_t sendCommand(dshot_commands_t dshot_command, uint16_t repeat_count = DEFAULT_CMD_REPEAT_COUNT, uint16_t delay_us = DEFAULT_CMD_DELAY_US); + + /** + * @brief Sends a DShot command multiple times with a delay between repetitions. This is a blocking function. + * @param dshot_command The DShot command to send. + * @param repeat_count The number of times to repeat the command. + * @param delay_us The delay in microseconds between repetitions. + * @return dshot_result_t indicating success or failure of the transmission. + */ + dshot_result_t sendCommand(dshotCommands_e dshot_command, uint16_t repeat_count = DEFAULT_CMD_REPEAT_COUNT, uint16_t delay_us = DEFAULT_CMD_DELAY_US); + + /** + * @brief Retrieves telemetry data from the ESC. + * @param magnet_count The number of magnets in the motor. If 0, uses the stored motor_magnet_count. + * @return dshot_result_t containing telemetry data (eRPM, motor RPM) if successful. + */ dshot_result_t getTelemetry(uint16_t magnet_count = 0); + + /** + * @brief Sends a command to the ESC to request ESC information. + * @return dshot_result_t indicating success or failure of the command transmission. + */ dshot_result_t getESCInfo(); + + /** + * @brief Sets the motor spin direction. + * @param reversed True for reversed direction, false for normal. + * @return dshot_result_t indicating success or failure of the command transmission. + */ dshot_result_t setMotorSpinDirection(bool reversed); + + /** + * @brief Sends a command to the ESC to save its current settings. + * Use with caution as this writes to ESC's non-volatile memory. + * @return dshot_result_t indicating success or failure of the command transmission. + */ dshot_result_t saveESCSettings(); // Public Utility & Info Functions + /** + * @brief Sets the motor magnet count for RPM calculation. + * @param magnet_count The number of magnets in the motor. + */ void setMotorMagnetCount(uint16_t magnet_count); - void printDShotInfo(Stream &output = Serial) const; - void printCpuInfo(Stream &output = Serial) const; + + /** + * @brief Gets the current DShot mode. + * @return The current dshot_mode_t. + */ + dshot_mode_t getMode() const { return _mode; } + + /** + * @brief Checks if bidirectional DShot is enabled. + * @return True if bidirectional DShot is enabled, false otherwise. + */ + bool isBidirectional() const { return _is_bidirectional; } + + /** + * @brief Gets the last encoded DShot frame value. + * @return The 16-bit encoded DShot frame value. + */ + uint16_t getEncodedFrameValue() const { return _encoded_frame_value; } + + /** + * @brief Gets the last transmitted throttle value. + * @return The last transmitted throttle value. + */ + uint16_t getThrottleValue() const { return _packet.throttle_value; } // Deprecated Methods + /** + * @brief Deprecated. Use sendThrottle() instead. + * @param throttle The throttle value. + * @return True on success, false on failure. + */ [[deprecated("Use sendThrottle() instead")]] bool setThrottle(uint16_t throttle) { @@ -111,6 +156,11 @@ public: return result.success; } + /** + * @brief Deprecated. Use sendCommand() instead. + * @param command The DShot command. + * @return True on success, false on failure. + */ [[deprecated("Use sendCommand() instead")]] bool sendDShotCommand(uint16_t command) { @@ -118,6 +168,11 @@ public: return result.success; } + /** + * @brief Deprecated. Use getTelemetry() instead. + * @param magnet_count The number of magnets in the motor. + * @return The motor RPM. + */ [[deprecated("Use getTelemetry() instead")]] uint32_t getMotorRPM(uint8_t magnet_count) { @@ -126,55 +181,9 @@ public: } private: - // Configuration Constants - static constexpr auto const DSHOT_NULL_PACKET = 0b0000000000000000; - static constexpr auto const DSHOT_FULL_PACKET = 0b1111111111111111; - static constexpr auto const DSHOT_CRC_MASK = 0b0000000000001111; - static constexpr auto const DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT; - static constexpr auto const DSHOT_RMT_RESOLUTION = 8 * 1000 * 1000; // 8 MHz resolution - static constexpr auto const RMT_TICKS_PER_US = DSHOT_RMT_RESOLUTION / (1 * 1000 * 1000); // RMT Ticks per microsecond - static constexpr auto const DSHOT_RX_TIMEOUT_MS = 2; - static constexpr auto const DSHOT_PADDING_US = 20; // Add to pause between frames for compatibility - static constexpr auto const RMT_BUFFER_SYMBOLS = 128; - static constexpr auto const RMT_QUEUE_DEPTH = 1; - static constexpr auto const GCR_BITS_PER_FRAME = 21; // Number of GCR bits in a DShot answer frame - static constexpr auto const POLE_PAIRS_MIN = 1; - static constexpr auto const MAGNETS_PER_POLE_PAIR = 2; - static constexpr auto const NO_DSHOT_TELEMETRY = 0; - static constexpr auto const DSHOT_PULSE_MIN = 800; // 0.8us minimum pulse - static constexpr auto const DSHOT_PULSE_MAX = 8000; // 8.0us maximum pulse - static constexpr auto const DSHOT_TELEMETRY_INVALID = DSHOT_THROTTLE_MAX; - - // Error Messages - static constexpr char const *NONE = ""; - static constexpr char const *UNKNOWN_ERROR = "Unknown Error!"; - static constexpr char const *INIT_SUCCESS = "SignalGeneratorRMT initialized successfully"; - static constexpr char const *INIT_FAILED = "SignalGeneratorRMT init failed!"; - static constexpr char const *TX_INIT_SUCCESS = "TX RMT channel initialized successfully"; - static constexpr char const *TX_INIT_FAILED = "TX RMT channel init failed!"; - static constexpr char const *RX_INIT_SUCCESS = "RX RMT channel initialized successfully"; - static constexpr char const *RX_INIT_FAILED = "RX RMT channel init failed!"; - static constexpr char const *ENCODER_INIT_SUCCESS = "RMT encoder initialized successfully"; - static constexpr char const *ENCODER_INIT_FAILED = "RMT encoder init failed!"; - static constexpr char const *ENCODING_SUCCESS = "Packet encoded successfully"; - static constexpr char const *TRANSMISSION_SUCCESS = "Transmission successfully"; - static constexpr char const *TRANSMISSION_FAILED = "Transmission failed!"; - static constexpr char const *RECEIVER_FAILED = "RMT receiver failed!"; - static constexpr char const *THROTTLE_NOT_IN_RANGE = "Throttle not in range! (48 - 2047)"; - static constexpr char const *PERCENT_NOT_IN_RANGE = "Percent not in range! (0.0 - 100.0)"; - static constexpr char const *COMMAND_NOT_VALID = "Command not valid! (0 - 47)"; - static constexpr char const *BIDIR_NOT_ENABLED = "Bidirectional DShot not enabled!"; - static constexpr char const *TELEMETRY_SUCCESS = "Valid Telemetric Frame received!"; - static constexpr char const *TELEMETRY_FAILED = "No valid Telemetric Frame received!"; - static constexpr char const *INVALID_MAGNET_COUNT = "Invalid motor magnet count!"; - static constexpr char const *TIMING_CORRECTION = "Timing correction!"; - static constexpr char const *CALLBACK_REGISTERING_FAILED = "RMT RX Callback registering failed!"; - static constexpr char const *INVALID_COMMAND = "Invalid command!"; - static constexpr char const *COMMAND_SUCCESS = "DShot command sent successfully"; - // --- UTILITY METHODS --- - bool _isValidCommand(dshot_commands_t command); - dshot_result_t _executeCommand(dshot_commands_t command); + bool _isValidCommand(dshotCommands_e command); + dshot_result_t _executeCommand(dshotCommands_e command); // Core Configuration Variables gpio_num_t _gpio; @@ -188,12 +197,11 @@ private: rmt_ticks_t _rmt_ticks; uint16_t _last_throttle; uint64_t _last_transmission_time_us; - uint64_t _last_command_timestamp; - uint16_t _encoded_frame_value; - dshot_packet_t _packet; - uint8_t _bitPositions[DSHOT_BITS_PER_FRAME]; - uint16_t _level0; // Signal level for the first part of a pulse (always HIGH for DShot) - uint16_t _level1; // Signal level for the second part of a pulse (always LOW for DShot) + uint64_t _last_command_timestamp; + uint16_t _encoded_frame_value; + dshot_packet_t _packet; + uint16_t _level0; // DShot protocol: Signal is idle-low, so pulses start by going HIGH. + uint16_t _level1; // DShot protocol: Signal returns to LOW after the high pulse. // RMT Hardware Handles rmt_channel_handle_t _rmt_tx_channel; @@ -218,10 +226,9 @@ private: // Private Packet Management Functions dshot_packet_t _buildDShotPacket(const uint16_t &value); - uint16_t _parseDShotPacket(const dshot_packet_t &packet); + uint16_t _buildDShotFrameValue(const dshot_packet_t &packet); uint16_t _calculateCRC(const uint16_t &data); void _preCalculateRMTTicks(); - void _preCalculateBitPositions(); // Private Frame Processing Functions dshot_result_t _sendDShotFrame(const dshot_packet_t &packet); @@ -234,10 +241,4 @@ private: // 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); - - // 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; // Settings commands need 10 repeats - static constexpr auto SETTINGS_COMMAND_DELAY_US = 5; -}; +}; \ No newline at end of file diff --git a/src/DShotRMT_Utils.cpp b/src/DShotRMT_Utils.cpp new file mode 100644 index 0000000..08681c8 --- /dev/null +++ b/src/DShotRMT_Utils.cpp @@ -0,0 +1,30 @@ +#include "DShotRMT_Utils.h" +#include "DShotRMT.h" // Include DShotRMT.h for DShotRMT class definition + +void printDShotInfo(const DShotRMT &dshot_rmt, Stream &output) +{ + output.println("\n === DShot Signal Info === "); + output.printf("Current Mode: DSHOT%d\n", dshot_rmt.getMode() == dshot_mode_t::DSHOT150 ? 150 : + dshot_rmt.getMode() == dshot_mode_t::DSHOT300 ? 300 : + dshot_rmt.getMode() == dshot_mode_t::DSHOT600 ? 600 : + dshot_rmt.getMode() == dshot_mode_t::DSHOT1200 ? 1200 : 0); + output.printf("Bidirectional: %s\n", dshot_rmt.isBidirectional() ? "YES" : "NO"); + output.printf("Current Packet: "); + + for (int i = DSHOT_BITS_PER_FRAME - 1; i >= 0; --i) + { + output.print((dshot_rmt.getEncodedFrameValue() >> i) & 1); + } + + output.printf("\nCurrent Value: %u\n", dshot_rmt.getThrottleValue()); +} + +void printCpuInfo(Stream &output) +{ + output.println("\n === CPU Info === "); + output.printf("Chip Model: %s\n", ESP.getChipModel()); + output.printf("Chip Revision: %d\n", ESP.getChipRevision()); + output.printf("CPU Freq = %lu MHz\n", ESP.getCpuFreqMHz()); + output.printf("XTAL Freq = %lu MHz\n", getXtalFrequencyMhz()); + output.printf("APB Freq = %lu Hz\n", getApbFrequency()); +} diff --git a/src/DShotRMT_Utils.h b/src/DShotRMT_Utils.h new file mode 100644 index 0000000..97983ca --- /dev/null +++ b/src/DShotRMT_Utils.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "DShotRMT.h" // Include DShotRMT.h for DShotRMT class definition + +/** + * @brief Utility functions for printing DShot and CPU information + */ + +/** + * @brief Prints detailed DShot signal information for a given DShotRMT instance. + * @param dshot_rmt The DShotRMT instance to get information from. + * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. + */ +void printDShotInfo(const DShotRMT &dshot_rmt, Stream &output = Serial); + +/** + * @brief Prints detailed CPU information. + * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. + */ +void printCpuInfo(Stream &output = Serial); diff --git a/src/dshot_commands.h b/src/dshot_commands.h deleted file mode 100644 index 702a065..0000000 --- a/src/dshot_commands.h +++ /dev/null @@ -1,68 +0,0 @@ -/* - * This file is part of Cleanflight and Betaflight. - * - * Cleanflight and Betaflight are free software. You can redistribute - * this software and/or modify this software under the terms of the - * GNU General Public License as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) - * any later version. - * - * Cleanflight and Betaflight are distributed in the hope that they - * will be useful, but WITHOUT ANY WARRANTY; without even the implied - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this software. - * - * If not, see . - */ - -#pragma once - -/* - DshotSettingRequest (KISS24). Spin direction, 3d and save Settings require 10 requests.. and the TLM Byte must always be high if 1-47 are used to send settings - - 3D Mode: - 0 = stop - 48 (low) - 1047 (high) -> negative direction - 1048 (low) - 2047 (high) -> positive direction - */ - -typedef enum -{ - DSHOT_CMD_MOTOR_STOP = 0, - DSHOT_CMD_BEACON1, - DSHOT_CMD_BEACON2, - DSHOT_CMD_BEACON3, - DSHOT_CMD_BEACON4, - DSHOT_CMD_BEACON5, - DSHOT_CMD_ESC_INFO, // V2 includes settings - DSHOT_CMD_SPIN_DIRECTION_1, - DSHOT_CMD_SPIN_DIRECTION_2, - DSHOT_CMD_3D_MODE_OFF, - DSHOT_CMD_3D_MODE_ON, - DSHOT_CMD_SETTINGS_REQUEST, // Currently not implemented - DSHOT_CMD_SAVE_SETTINGS, - DSHOT_CMD_EXTENDED_TELEMETRY_ENABLE, - DSHOT_CMD_EXTENDED_TELEMETRY_DISABLE, - DSHOT_CMD_SPIN_DIRECTION_NORMAL = 20, - DSHOT_CMD_SPIN_DIRECTION_REVERSED = 21, - DSHOT_CMD_LED0_ON, // BLHeli32 only - DSHOT_CMD_LED1_ON, // BLHeli32 only - DSHOT_CMD_LED2_ON, // BLHeli32 only - DSHOT_CMD_LED3_ON, // BLHeli32 only - DSHOT_CMD_LED0_OFF, // BLHeli32 only - DSHOT_CMD_LED1_OFF, // BLHeli32 only - DSHOT_CMD_LED2_OFF, // BLHeli32 only - DSHOT_CMD_LED3_OFF, // BLHeli32 only - DSHOT_CMD_AUDIO_STREAM_MODE_ON_OFF = 30, // KISS audio Stream mode on/Off - DSHOT_CMD_SILENT_MODE_ON_OFF = 31, // KISS silent Mode on/Off - DSHOT_CMD_MAX = 47 -} dshotCommands_e; - -typedef enum -{ - DSHOT_CMD_TYPE_INLINE = 0, // dshot commands sent inline with motor signal (motors must be enabled) - DSHOT_CMD_TYPE_BLOCKING // dshot commands sent in blocking method (motors must be disabled) -} dshotCommandType_e; diff --git a/src/dshot_definitions.h b/src/dshot_definitions.h new file mode 100644 index 0000000..e8ed232 --- /dev/null +++ b/src/dshot_definitions.h @@ -0,0 +1,267 @@ +#pragma once + +#include +#include // Added for gpio_num_t +#include +#include +#include // Added for std::atomic + +/** + * @brief DShot Modes + * Defines the available DShot communication speeds. + */ +enum class dshot_mode_t +{ + DSHOT_OFF, + DSHOT150, + DSHOT300, + DSHOT600, + DSHOT1200 +}; + +/** + * @brief DShot Packet Structure + * Represents the 16-bit DShot data packet sent to the ESC. + */ +typedef struct dshot_packet +{ + uint16_t throttle_value : 11; ///< 11-bit throttle value or command. + bool telemetric_request : 1; ///< 1-bit telemetry request flag. + uint16_t checksum : 4; ///< 4-bit CRC checksum. +} dshot_packet_t; + +/** + * @brief DShot Timing Configuration + * Defines the bit length and high time for a '1' bit in microseconds for each DShot mode. + */ +typedef struct dshot_timing +{ + double bit_length_us; ///< Total duration of one bit in microseconds. + double t1h_lenght_us; ///< High time duration for a '1' bit in microseconds. +} dshot_timing_us_t; + +/** + * @brief RMT Timing Configuration + * Stores pre-calculated timing values in RMT ticks for efficient signal generation. + */ +typedef struct rmt_ticks +{ + uint16_t bit_length_ticks; ///< Total duration of one bit in RMT ticks. + uint16_t t1h_ticks; ///< High time duration for a '1' bit in RMT ticks. + uint16_t t1l_ticks; ///< Low time duration for a '1' bit in RMT ticks. + uint16_t t0h_ticks; ///< High time duration for a '0' bit in RMT ticks. + uint16_t t0l_ticks; ///< Low time duration for a '0' bit in RMT ticks. +} rmt_ticks_t; + +/** + * @brief DShot Error Codes + * Enum class for specific error and success codes returned by DShotRMT functions. + */ +enum class dshot_msg_code_t +{ + DSHOT_ERROR_NONE = 0, + DSHOT_ERROR_UNKNOWN, + DSHOT_ERROR_TX_INIT_FAILED, + DSHOT_ERROR_RX_INIT_FAILED, + DSHOT_ERROR_ENCODER_INIT_FAILED, + DSHOT_ERROR_CALLBACK_REGISTERING_FAILED, + DSHOT_ERROR_RECEIVER_FAILED, + DSHOT_ERROR_TRANSMISSION_FAILED, + DSHOT_ERROR_THROTTLE_NOT_IN_RANGE, + DSHOT_ERROR_PERCENT_NOT_IN_RANGE, + DSHOT_ERROR_COMMAND_NOT_VALID, + DSHOT_ERROR_BIDIR_NOT_ENABLED, + DSHOT_ERROR_TELEMETRY_FAILED, + DSHOT_ERROR_INVALID_MAGNET_COUNT, + DSHOT_ERROR_INVALID_COMMAND, + DSHOT_ERROR_TIMING_CORRECTION, + DSHOT_ERROR_INIT_FAILED, + DSHOT_ERROR_INIT_SUCCESS, + DSHOT_ERROR_TX_INIT_SUCCESS, + DSHOT_ERROR_RX_INIT_SUCCESS, + DSHOT_ERROR_ENCODER_INIT_SUCCESS, + DSHOT_ERROR_ENCODING_SUCCESS, + DSHOT_ERROR_TRANSMISSION_SUCCESS, + DSHOT_ERROR_TELEMETRY_SUCCESS, + DSHOT_ERROR_COMMAND_SUCCESS +}; + +/** + * @brief Unified DShot Result Structure + * Contains the success status, an error code, and optional telemetry data. + */ +typedef struct dshot_result +{ + bool success; + dshot_msg_code_t error_code; ///< Specific error or success code. + uint16_t erpm; ///< Electrical RPM (eRPM) if telemetry is successful. + uint16_t motor_rpm; ///< Motor RPM if telemetry is successful and magnet count is known. +} dshot_result_t; + +/** + * @brief DShot Commands + * Enum class for standard DShot commands that can be sent to an ESC. + */ +enum class dshotCommands_e +{ + DSHOT_CMD_MOTOR_STOP = 0, + DSHOT_CMD_BEACON1, + DSHOT_CMD_BEACON2, + DSHOT_CMD_BEACON3, + DSHOT_CMD_BEACON4, + DSHOT_CMD_BEACON5, + DSHOT_CMD_ESC_INFO, // V2 includes settings + DSHOT_CMD_SPIN_DIRECTION_1, + DSHOT_CMD_SPIN_DIRECTION_2, + DSHOT_CMD_3D_MODE_OFF, + DSHOT_CMD_3D_MODE_ON, + DSHOT_CMD_SETTINGS_REQUEST, // Currently not implemented + DSHOT_CMD_SAVE_SETTINGS, + DSHOT_CMD_EXTENDED_TELEMETRY_ENABLE, + DSHOT_CMD_EXTENDED_TELEMETRY_DISABLE, + DSHOT_CMD_SPIN_DIRECTION_NORMAL = 20, + DSHOT_CMD_SPIN_DIRECTION_REVERSED = 21, + DSHOT_CMD_LED0_ON, // BLHeli32 only + DSHOT_CMD_LED1_ON, // BLHeli32 only + DSHOT_CMD_LED2_ON, // BLHeli32 only + DSHOT_CMD_LED3_ON, // BLHeli32 only + DSHOT_CMD_LED0_OFF, // BLHeli32 only + DSHOT_CMD_LED1_OFF, // BLHeli32 only + DSHOT_CMD_LED2_OFF, // BLHeli32 only + DSHOT_CMD_LED3_OFF, // BLHeli32 only + DSHOT_CMD_AUDIO_STREAM_MODE_ON_OFF = 30, // KISS audio Stream mode on/Off + DSHOT_CMD_SILENT_MODE_ON_OFF = 31, // KISS silent Mode on/Off + DSHOT_CMD_MAX = 47 +}; + +/** + * @brief DShot Command Type Enum + * Defines how DShot commands are sent. + */ +enum class dshotCommandType_e +{ + DSHOT_CMD_TYPE_INLINE = 0, ///< Commands sent inline with motor signal (motors must be enabled). + DSHOT_CMD_TYPE_BLOCKING ///< Commands sent in blocking method (motors must be disabled). +}; + +// DShot Protocol Constants +const uint16_t DSHOT_THROTTLE_FAILSAFE = 0; +const uint16_t DSHOT_THROTTLE_MIN = 48; +const uint16_t DSHOT_THROTTLE_MAX = 2047; +const uint16_t DSHOT_BITS_PER_FRAME = 16; +const uint16_t DEFAULT_MOTOR_MAGNET_COUNT = 14; + +// Custom status codes +const int DSHOT_OK = 0; +const int DSHOT_ERROR = 1; + +// Configuration Constants (from DShotRMT.h private section) +const uint16_t DSHOT_NULL_PACKET = 0b0000000000000000; +const uint16_t DSHOT_FULL_PACKET = 0b1111111111111111; +const uint16_t DSHOT_CRC_MASK = 0b0000000000001111; +const rmt_clock_source_t DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT; +const uint32_t DSHOT_RMT_RESOLUTION = 8 * 1000 * 1000; // 8 MHz resolution +const uint16_t RMT_TICKS_PER_US = DSHOT_RMT_RESOLUTION / (1 * 1000 * 1000); // RMT Ticks per microsecond +const uint16_t DSHOT_RX_TIMEOUT_MS = 2; +const uint16_t DSHOT_PADDING_US = 20; // Add to pause between frames for compatibility +const uint16_t RMT_BUFFER_SYMBOLS = 64; +const uint16_t RMT_QUEUE_DEPTH = 1; +const uint16_t GCR_BITS_PER_FRAME = 21; // Number of GCR bits in a DShot answer frame +const uint16_t POLE_PAIRS_MIN = 1; +const uint16_t MAGNETS_PER_POLE_PAIR = 2; +const uint16_t NO_DSHOT_TELEMETRY = 0; +const uint16_t DSHOT_PULSE_MIN = 800; // 0.8us minimum pulse +const uint16_t DSHOT_PULSE_MAX = 8000; // 8.0us maximum pulse +const uint16_t DSHOT_TELEMETRY_INVALID = DSHOT_THROTTLE_MAX; +const uint16_t DSHOT_TELEMETRY_BIT_POSITION = 11; // Bit position of the telemetry request flag in the DShot frame +const uint16_t DSHOT_CRC_BIT_SHIFT = 4; // Number of bits to shift to extract data from data_and_crc + +// Command Constants (from DShotRMT.h private section) +const uint16_t DEFAULT_CMD_DELAY_US = 10; +const uint16_t DEFAULT_CMD_REPEAT_COUNT = 1; +const uint16_t SETTINGS_COMMAND_REPEATS = 10; // Settings commands need 10 repeats +const uint16_t SETTINGS_COMMAND_DELAY_US = 5; + +// Timing parameters for each DShot mode +// Format: {bit_length_us, t1h_length_us} +const dshot_timing_us_t DSHOT_TIMING_US[] = { + {0.00, 0.00}, // DSHOT_OFF + {6.67, 5.00}, // DSHOT150 + {3.33, 2.50}, // DSHOT300 + {1.67, 1.25}, // DSHOT600 + {0.83, 0.67}}; // DSHOT1200 + +// Error Messages +const char *const NONE = ""; +const char *const UNKNOWN_ERROR = "Unknown Error!"; +const char *const INIT_SUCCESS = "SignalGeneratorRMT initialized successfully"; +const char *const INIT_FAILED = "SignalGeneratorRMT init failed!"; +const char *const TX_INIT_SUCCESS = "TX RMT channel initialized successfully"; +const char *const TX_INIT_FAILED = "TX RMT channel init failed!"; +const char *const RX_INIT_SUCCESS = "RX RMT channel initialized successfully"; +const char *const RX_INIT_FAILED = "RX RMT channel init failed!"; +const char *const ENCODER_INIT_SUCCESS = "RMT encoder initialized successfully"; +const char *const ENCODER_INIT_FAILED = "RMT encoder init failed!"; +const char *const ENCODING_SUCCESS = "Packet encoded successfully"; +const char *const TRANSMISSION_SUCCESS = "Transmission successfully"; +const char *const TRANSMISSION_FAILED = "Transmission failed!"; +const char *const RECEIVER_FAILED = "RMT receiver failed!"; +const char *const THROTTLE_NOT_IN_RANGE = "Throttle not in range! (48 - 2047)"; +const char *const PERCENT_NOT_IN_RANGE = "Percent not in range! (0.0 - 100.0)"; +const char *const COMMAND_NOT_VALID = "Command not valid! (0 - 47)"; +const char *const BIDIR_NOT_ENABLED = "Bidirectional DShot not enabled!"; +const char *const TELEMETRY_SUCCESS = "Valid Telemetric Frame received!"; +const char *const TELEMETRY_FAILED = "No valid Telemetric Frame received!"; +const char *const INVALID_MAGNET_COUNT = "Invalid motor magnet count!"; +const char *const TIMING_CORRECTION = "Timing correction!"; +const char *const CALLBACK_REGISTERING_FAILED = "RMT RX Callback registering failed!"; +const char *const INVALID_COMMAND = "Invalid command!"; +const char *const COMMAND_SUCCESS = "DShot command sent successfully"; + +// Helper Functions +/** + * @brief Prints the result of a DShot operation to the specified output stream. + * @param result The dshot_result_t structure containing the operation's outcome. + * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. + */ +inline void printDShotResult(dshot_result_t &result, Stream &output = Serial) +{ + const char *msg_str; + switch (result.error_code) + { + case dshot_msg_code_t::DSHOT_ERROR_NONE: msg_str = "None"; break; + case dshot_msg_code_t::DSHOT_ERROR_UNKNOWN: msg_str = "Unknown Error!"; break; + case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_FAILED: msg_str = "TX RMT channel init failed!"; break; + case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_FAILED: msg_str = "RX RMT channel init failed!"; break; + case dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED: msg_str = "RMT encoder init failed!"; break; + case dshot_msg_code_t::DSHOT_ERROR_CALLBACK_REGISTERING_FAILED: msg_str = "RMT RX Callback registering failed!"; break; + case dshot_msg_code_t::DSHOT_ERROR_RECEIVER_FAILED: msg_str = "RMT receiver failed!"; break; + case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_FAILED: msg_str = "Transmission failed!"; break; + case dshot_msg_code_t::DSHOT_ERROR_THROTTLE_NOT_IN_RANGE: msg_str = "Throttle not in range! (48 - 2047)"; break; + case dshot_msg_code_t::DSHOT_ERROR_PERCENT_NOT_IN_RANGE: msg_str = "Percent not in range! (0.0 - 100.0)"; break; + case dshot_msg_code_t::DSHOT_ERROR_COMMAND_NOT_VALID: msg_str = "Command not valid! (0 - 47)"; break; + case dshot_msg_code_t::DSHOT_ERROR_BIDIR_NOT_ENABLED: msg_str = "Bidirectional DShot not enabled!"; break; + case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_FAILED: msg_str = "No valid Telemetric Frame received!"; break; + case dshot_msg_code_t::DSHOT_ERROR_INVALID_MAGNET_COUNT: msg_str = "Invalid motor magnet count!"; break; + case dshot_msg_code_t::DSHOT_ERROR_INVALID_COMMAND: msg_str = "Invalid command!"; break; + case dshot_msg_code_t::DSHOT_ERROR_TIMING_CORRECTION: msg_str = "Timing correction!"; break; + case dshot_msg_code_t::DSHOT_ERROR_INIT_FAILED: msg_str = "SignalGeneratorRMT init failed!"; break; + case dshot_msg_code_t::DSHOT_ERROR_INIT_SUCCESS: msg_str = "SignalGeneratorRMT initialized successfully"; break; + case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_SUCCESS: msg_str = "TX RMT channel initialized successfully"; break; + case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_SUCCESS: msg_str = "RX RMT channel initialized successfully"; break; + case dshot_msg_code_t::DSHOT_ERROR_ENCODING_SUCCESS: msg_str = "Packet encoded successfully"; break; + case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_SUCCESS: msg_str = "Transmission successfully"; break; + case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_SUCCESS: msg_str = "Valid Telemetric Frame received!"; break; + case dshot_msg_code_t::DSHOT_ERROR_COMMAND_SUCCESS: msg_str = "DShot command sent successfully"; break; + default: msg_str = "Unhandled Error Code!"; break; + } + output.printf("Status: %s - %s", result.success ? "SUCCESS" : "FAILED", msg_str); + + // Print telemetry data if available + if (result.success && (result.erpm > 0 || result.motor_rpm > 0)) + { + output.printf(" | eRPM: %u, Motor RPM: %u", result.erpm, result.motor_rpm); + } + + output.println(); +} \ No newline at end of file From 7447855c3221ac2aa2912957c6cd0c8f0c9ff88c Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Thu, 25 Sep 2025 16:15:36 +0200 Subject: [PATCH 3/8] Reimplement utils --- examples/dshot300/dshot300.ino | 11 +- .../throttle_percent/throttle_percent.ino | 8 +- examples/web_client/web_client.ino | 30 +++-- examples/web_control/web_control.ino | 23 ++-- src/DShotRMT.cpp | 46 ++++++-- src/DShotRMT.h | 25 ++++- src/DShotRMT_Utils.cpp | 30 ----- src/DShotRMT_Utils.h | 21 ---- src/dshot_definitions.h | 106 +++++++++++++----- 9 files changed, 168 insertions(+), 132 deletions(-) delete mode 100644 src/DShotRMT_Utils.cpp delete mode 100644 src/DShotRMT_Utils.h diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index 70fa73a..be4b9f5 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -8,7 +8,6 @@ #include #include "DShotRMT.h" -#include "DShotRMT_Utils.h" // Include utility functions // USB serial port settings static constexpr auto &USB_SERIAL = Serial0; @@ -22,9 +21,9 @@ static constexpr gpio_num_t MOTOR01_PIN = GPIO_NUM_27; static constexpr dshot_mode_t DSHOT_MODE = dshot_mode_t::DSHOT300; // BiDirectional DShot Support (default: false) -// Note: Bidirectional DShot is currently not officially supported +// Note: Bidirectional DShot is currently not officially supported // due to instability and external hardware requirements. -static constexpr auto IS_BIDIRECTIONAL = false; +static constexpr auto IS_BIDIRECTIONAL = false; // Motor magnet count for RPM calculation static constexpr auto MOTOR01_MAGNET_COUNT = 14; @@ -42,7 +41,7 @@ void setup() motor01.begin(); // Print CPU Info - printCpuInfo(USB_SERIAL); + DShotRMT::printCpuInfo(USB_SERIAL); // printMenu(); @@ -81,7 +80,7 @@ void loop() // Print motor stats every 3 seconds in continuous mode if (continuous_throttle && (esp_timer_get_time() - last_stats_print >= 3000000)) { - printDShotInfo(motor01, USB_SERIAL); + DShotRMT::printDShotInfo(motor01, USB_SERIAL); USB_SERIAL.println(" "); @@ -133,7 +132,7 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous } else if (input == "info") { - printDShotInfo(motor01, USB_SERIAL); + DShotRMT::printDShotInfo(motor01, USB_SERIAL); } else if (input == "rpm" && IS_BIDIRECTIONAL) { diff --git a/examples/throttle_percent/throttle_percent.ino b/examples/throttle_percent/throttle_percent.ino index 3cb4cf5..1cc47a4 100644 --- a/examples/throttle_percent/throttle_percent.ino +++ b/examples/throttle_percent/throttle_percent.ino @@ -17,7 +17,7 @@ static constexpr auto USB_SERIAL_BAUD = 115200; static constexpr gpio_num_t MOTOR01_PIN = GPIO_NUM_27; // Supported: DSHOT150, DSHOT300, DSHOT600, (DSHOT1200) -static constexpr dshot_mode_t DSHOT_MODE = DSHOT300; +static constexpr dshot_mode_t DSHOT_MODE = dshot_mode_t::DSHOT300; // BiDirectional DShot Support (default: false) // Note: Bidirectional DShot is currently not officially supported @@ -98,7 +98,7 @@ void handleSerialInput(const String &input) } else if (input == "info") { - motor01.printDShotInfo(); + DShotRMT::printDShotInfo(motor01, USB_SERIAL); } else if (input == "rpm" && IS_BIDIRECTIONAL) { @@ -110,14 +110,14 @@ void handleSerialInput(const String &input) // Send DShot command int cmd_num = input.substring(4).toInt(); - if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) + if (cmd_num >= static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP) && cmd_num <= static_cast(dshotCommands_e::DSHOT_CMD_MAX)) { dshot_result_t result = motor01.sendCommand(cmd_num); printDShotResult(result); } else { - USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX); + USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, static_cast(dshotCommands_e::DSHOT_CMD_MAX)); } } else if (input == "h" || input == "help") diff --git a/examples/web_client/web_client.ino b/examples/web_client/web_client.ino index 41491f0..1f45084 100644 --- a/examples/web_client/web_client.ino +++ b/examples/web_client/web_client.ino @@ -42,7 +42,7 @@ static constexpr auto USB_SERIAL_BAUD = 115200; static constexpr auto MOTOR01_PIN = 17; // Supported: DSHOT150, DSHOT300, DSHOT600, (DSHOT1200) -static constexpr dshot_mode_t DSHOT_MODE = DSHOT300; +static constexpr dshot_mode_t DSHOT_MODE = dshot_mode_t::DSHOT300; // BiDirectional DShot Support (default: false) static constexpr auto IS_BIDIRECTIONAL = false; // Note: Bidirectional DShot is currently not officially supported due to instability and external hardware requirements. @@ -58,7 +58,7 @@ AsyncWebServer server(80); AsyncWebSocket ws("/ws"); // Global variables -static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; +static uint16_t throttle = static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP); static bool isArmed = false; static bool last_sent_armed = false; static bool continuous_throttle = true; @@ -126,7 +126,7 @@ void setup() void loop() { static uint64_t last_serial_update = 0; - static uint16_t last_sent_throttle = DSHOT_CMD_MOTOR_STOP; + static uint16_t last_sent_throttle = static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP); static String last_sent_rpm = "N/A"; static uint64_t last_wifi_check = 0; @@ -166,16 +166,13 @@ void loop() } else if (!isArmed && continuous_throttle) { - // Ensure motor is stopped when disarmed - motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); } // Print motor stats every 3 seconds in continuous mode if ((esp_timer_get_time() - last_serial_update >= 3000000)) { - motor01.printDShotInfo(); - - USB_SERIAL.println(" "); + DShotRMT::printDShotInfo(motor01, USB_SERIAL); // Get Motor RPM if bidirectional and armed if (IS_BIDIRECTIONAL && isArmed) @@ -269,9 +266,8 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind if (!index) { - // Safety: Ensure motor is stopped during update - motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); - setArmingStatus(false); + // Safety: Ensure motor is stopped during update + motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); setArmingStatus(false); USB_SERIAL.printf("OTA Update Start: %s\n", filename.c_str()); @@ -384,7 +380,7 @@ void setArmingStatus(bool armed) // Safety: Stop motor and reset throttle when disarming throttle = 0; continuous_throttle = false; - motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); USB_SERIAL.println(" "); USB_SERIAL.println("=== MOTOR DISARMED - SAFETY STOP EXECUTED ==="); @@ -451,7 +447,7 @@ void handleSerialInput(const String &input) if (input == "info") { - motor01.printDShotInfo(); + DShotRMT::printDShotInfo(motor01, USB_SERIAL); USB_SERIAL.println(" "); USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); return; @@ -521,7 +517,7 @@ void handleSerialInput(const String &input) continuous_throttle = false; int cmd_num = input.substring(4).toInt(); - if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) + if (cmd_num >= static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP) && cmd_num <= static_cast(dshotCommands_e::DSHOT_CMD_MAX)) { dshot_result_t result = motor01.sendCommand(cmd_num); printDShotResult(result); @@ -529,7 +525,7 @@ void handleSerialInput(const String &input) else { USB_SERIAL.println(" "); - USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX); + USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, static_cast(dshotCommands_e::DSHOT_CMD_MAX)); } return; } @@ -584,7 +580,7 @@ void handleSerialInput(const String &input) { throttle = 0; continuous_throttle = false; - dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + dshot_result_t result = motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); printDShotResult(result); return; } @@ -638,7 +634,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) { throttle = 0; continuous_throttle = false; - motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); } else if (web_throttle >= DSHOT_THROTTLE_MIN && web_throttle <= DSHOT_THROTTLE_MAX) { diff --git a/examples/web_control/web_control.ino b/examples/web_control/web_control.ino index 38b0915..c6e2182 100644 --- a/examples/web_control/web_control.ino +++ b/examples/web_control/web_control.ino @@ -41,7 +41,7 @@ static constexpr auto USB_SERIAL_BAUD = 115200; static constexpr auto MOTOR01_PIN = 17; // Supported: DSHOT150, DSHOT300, DSHOT600, (DSHOT1200) -static constexpr dshot_mode_t DSHOT_MODE = DSHOT300; +static constexpr dshot_mode_t DSHOT_MODE = dshot_mode_t::DSHOT300; // BiDirectional DShot Support (default: false) static constexpr auto IS_BIDIRECTIONAL = false; // Note: Bidirectional DShot is currently not officially supported due to instability and external hardware requirements. @@ -57,7 +57,7 @@ AsyncWebServer server(80); AsyncWebSocket ws("/ws"); // Global variables -static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; +static uint16_t throttle = static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP); static bool isArmed = false; static bool continuous_throttle = true; @@ -109,7 +109,7 @@ void setup() void loop() { static uint64_t last_serial_update = 0; - static uint16_t last_sent_throttle = DSHOT_CMD_MOTOR_STOP; + static uint16_t last_sent_throttle = static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP); static bool last_sent_armed = false; static String last_sent_rpm = "N/A"; @@ -131,14 +131,13 @@ void loop() } else if (!isArmed && continuous_throttle) { - // Ensure motor is stopped when disarmed - motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); } // Print motor stats every 3 seconds in continuous mode if ((esp_timer_get_time() - last_serial_update >= 3000000)) { - motor01.printDShotInfo(); + DShotRMT::printDShotInfo(motor01, USB_SERIAL); USB_SERIAL.println(" "); @@ -207,7 +206,7 @@ void setArmingStatus(bool armed) // Safety: Stop motor and reset throttle when disarming throttle = 0; continuous_throttle = false; - motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); USB_SERIAL.println(" "); USB_SERIAL.println("=== MOTOR DISARMED - SAFETY STOP EXECUTED ==="); } @@ -254,7 +253,7 @@ void handleSerialInput(const String &input) } if (input == "info") { - motor01.printDShotInfo(); + DShotRMT::printDShotInfo(motor01, USB_SERIAL); USB_SERIAL.println(" "); USB_SERIAL.printf("Arming Status: %s\n", isArmed ? "ARMED" : "DISARMED"); return; @@ -285,7 +284,7 @@ void handleSerialInput(const String &input) continuous_throttle = false; int cmd_num = input.substring(4).toInt(); - if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) + if (cmd_num >= static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP) && cmd_num <= static_cast(dshotCommands_e::DSHOT_CMD_MAX)) { dshot_result_t result = motor01.sendCommand(cmd_num); printDShotResult(result); @@ -293,7 +292,7 @@ void handleSerialInput(const String &input) else { USB_SERIAL.println(" "); - USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX); + USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, static_cast(dshotCommands_e::DSHOT_CMD_MAX)); } return; } @@ -340,7 +339,7 @@ void handleSerialInput(const String &input) { throttle = 0; continuous_throttle = false; - dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + dshot_result_t result = motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); printDShotResult(result); return; } @@ -394,7 +393,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) { throttle = 0; continuous_throttle = false; - motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); + motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); } else if (web_throttle >= DSHOT_THROTTLE_MIN && web_throttle <= DSHOT_THROTTLE_MAX) { diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 0ac1e2b..25c86bf 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -58,8 +58,8 @@ DShotRMT::~DShotRMT() { if (rmt_disable(_rmt_tx_channel) == DSHOT_OK) { - rmt_del_channel(_rmt_tx_channel); - _rmt_tx_channel = nullptr; + rmt_del_channel(_rmt_tx_channel); + _rmt_tx_channel = nullptr; } } @@ -68,8 +68,8 @@ DShotRMT::~DShotRMT() { if (rmt_disable(_rmt_rx_channel) == DSHOT_OK) { - rmt_del_channel(_rmt_rx_channel); - _rmt_rx_channel = nullptr; + rmt_del_channel(_rmt_rx_channel); + _rmt_rx_channel = nullptr; } } @@ -109,12 +109,13 @@ dshot_result_t DShotRMT::begin() rmt_del_channel(_rmt_tx_channel); _rmt_tx_channel = nullptr; - if (_rmt_rx_channel) { + 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_ERROR_ENCODER_INIT_FAILED}; } @@ -292,7 +293,7 @@ dshot_result_t DShotRMT::_initTXChannel() _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.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) @@ -355,7 +356,7 @@ dshot_result_t DShotRMT::_initRXChannel() dshot_result_t DShotRMT::_initDShotEncoder() { rmt_copy_encoder_config_t encoder_config = {}; - + if (rmt_new_copy_encoder(&encoder_config, &_dshot_encoder) != DSHOT_OK) { return {false, dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED}; @@ -543,4 +544,33 @@ bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const } return false; +} + +// Public Static Utility Functions +void DShotRMT::printDShotInfo(const DShotRMT &dshot_rmt, Stream &output) +{ + output.println("\n === DShot Signal Info === "); + output.printf("Current Mode: DSHOT%d\n", dshot_rmt.getMode() == dshot_mode_t::DSHOT150 ? 150 : dshot_rmt.getMode() == dshot_mode_t::DSHOT300 ? 300 + : dshot_rmt.getMode() == dshot_mode_t::DSHOT600 ? 600 + : dshot_rmt.getMode() == dshot_mode_t::DSHOT1200 ? 1200 + : 0); + output.printf("Bidirectional: %s\n", dshot_rmt.isBidirectional() ? "YES" : "NO"); + output.printf("Current Packet: "); + + for (int i = DSHOT_BITS_PER_FRAME - 1; i >= 0; --i) + { + output.print((dshot_rmt.getEncodedFrameValue() >> i) & 1); + } + + output.printf("\nCurrent Value: %u\n", dshot_rmt.getThrottleValue()); +} + +void DShotRMT::printCpuInfo(Stream &output) +{ + output.println("\n === CPU Info === "); + output.printf("Chip Model: %s\n", ESP.getChipModel()); + output.printf("Chip Revision: %d\n", ESP.getChipRevision()); + output.printf("CPU Freq = %lu MHz\n", ESP.getCpuFreqMHz()); + output.printf("XTAL Freq = %lu MHz\n", getXtalFrequencyMhz()); + output.printf("APB Freq = %lu Hz\n", getApbFrequency()); } \ No newline at end of file diff --git a/src/DShotRMT.h b/src/DShotRMT.h index ebda5d0..a9cc23c 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -9,7 +9,7 @@ #pragma once #include -#include "dshot_definitions.h" +#include #include #include #include @@ -17,7 +17,7 @@ /** * @brief DShotRMT Main Class for DShot signal generation and reception. - * + * * This class provides an interface to generate DShot signals for Electronic Speed Controllers (ESCs) * and to receive telemetry data using the ESP32's RMT peripheral. */ @@ -113,6 +113,19 @@ public: dshot_result_t saveESCSettings(); // Public Utility & Info Functions + /** + * @brief Prints detailed DShot signal information for a given DShotRMT instance. + * @param dshot_rmt The DShotRMT instance to get information from. + * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. + */ + static void printDShotInfo(const DShotRMT &dshot_rmt, Stream &output = Serial); + + /** + * @brief Prints detailed CPU information. + * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. + */ + static void printCpuInfo(Stream &output = Serial); + /** * @brief Sets the motor magnet count for RPM calculation. * @param magnet_count The number of magnets in the motor. @@ -197,9 +210,9 @@ private: rmt_ticks_t _rmt_ticks; uint16_t _last_throttle; uint64_t _last_transmission_time_us; - uint64_t _last_command_timestamp; - uint16_t _encoded_frame_value; - dshot_packet_t _packet; + uint64_t _last_command_timestamp; + uint16_t _encoded_frame_value; + dshot_packet_t _packet; uint16_t _level0; // DShot protocol: Signal is idle-low, so pulses start by going HIGH. uint16_t _level1; // DShot protocol: Signal returns to LOW after the high pulse. @@ -241,4 +254,4 @@ private: // 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); -}; \ No newline at end of file +}; diff --git a/src/DShotRMT_Utils.cpp b/src/DShotRMT_Utils.cpp deleted file mode 100644 index 08681c8..0000000 --- a/src/DShotRMT_Utils.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "DShotRMT_Utils.h" -#include "DShotRMT.h" // Include DShotRMT.h for DShotRMT class definition - -void printDShotInfo(const DShotRMT &dshot_rmt, Stream &output) -{ - output.println("\n === DShot Signal Info === "); - output.printf("Current Mode: DSHOT%d\n", dshot_rmt.getMode() == dshot_mode_t::DSHOT150 ? 150 : - dshot_rmt.getMode() == dshot_mode_t::DSHOT300 ? 300 : - dshot_rmt.getMode() == dshot_mode_t::DSHOT600 ? 600 : - dshot_rmt.getMode() == dshot_mode_t::DSHOT1200 ? 1200 : 0); - output.printf("Bidirectional: %s\n", dshot_rmt.isBidirectional() ? "YES" : "NO"); - output.printf("Current Packet: "); - - for (int i = DSHOT_BITS_PER_FRAME - 1; i >= 0; --i) - { - output.print((dshot_rmt.getEncodedFrameValue() >> i) & 1); - } - - output.printf("\nCurrent Value: %u\n", dshot_rmt.getThrottleValue()); -} - -void printCpuInfo(Stream &output) -{ - output.println("\n === CPU Info === "); - output.printf("Chip Model: %s\n", ESP.getChipModel()); - output.printf("Chip Revision: %d\n", ESP.getChipRevision()); - output.printf("CPU Freq = %lu MHz\n", ESP.getCpuFreqMHz()); - output.printf("XTAL Freq = %lu MHz\n", getXtalFrequencyMhz()); - output.printf("APB Freq = %lu Hz\n", getApbFrequency()); -} diff --git a/src/DShotRMT_Utils.h b/src/DShotRMT_Utils.h deleted file mode 100644 index 97983ca..0000000 --- a/src/DShotRMT_Utils.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include "DShotRMT.h" // Include DShotRMT.h for DShotRMT class definition - -/** - * @brief Utility functions for printing DShot and CPU information - */ - -/** - * @brief Prints detailed DShot signal information for a given DShotRMT instance. - * @param dshot_rmt The DShotRMT instance to get information from. - * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. - */ -void printDShotInfo(const DShotRMT &dshot_rmt, Stream &output = Serial); - -/** - * @brief Prints detailed CPU information. - * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. - */ -void printCpuInfo(Stream &output = Serial); diff --git a/src/dshot_definitions.h b/src/dshot_definitions.h index e8ed232..994607c 100644 --- a/src/dshot_definitions.h +++ b/src/dshot_definitions.h @@ -94,8 +94,8 @@ typedef struct dshot_result { bool success; dshot_msg_code_t error_code; ///< Specific error or success code. - uint16_t erpm; ///< Electrical RPM (eRPM) if telemetry is successful. - uint16_t motor_rpm; ///< Motor RPM if telemetry is successful and magnet count is known. + uint16_t erpm; ///< Electrical RPM (eRPM) if telemetry is successful. + uint16_t motor_rpm; ///< Motor RPM if telemetry is successful and magnet count is known. } dshot_result_t; /** @@ -174,7 +174,7 @@ const uint16_t DSHOT_PULSE_MIN = 800; // 0.8us minimum pulse const uint16_t DSHOT_PULSE_MAX = 8000; // 8.0us maximum pulse const uint16_t DSHOT_TELEMETRY_INVALID = DSHOT_THROTTLE_MAX; const uint16_t DSHOT_TELEMETRY_BIT_POSITION = 11; // Bit position of the telemetry request flag in the DShot frame -const uint16_t DSHOT_CRC_BIT_SHIFT = 4; // Number of bits to shift to extract data from data_and_crc +const uint16_t DSHOT_CRC_BIT_SHIFT = 4; // Number of bits to shift to extract data from data_and_crc // Command Constants (from DShotRMT.h private section) const uint16_t DEFAULT_CMD_DELAY_US = 10; @@ -229,31 +229,81 @@ inline void printDShotResult(dshot_result_t &result, Stream &output = Serial) const char *msg_str; switch (result.error_code) { - case dshot_msg_code_t::DSHOT_ERROR_NONE: msg_str = "None"; break; - case dshot_msg_code_t::DSHOT_ERROR_UNKNOWN: msg_str = "Unknown Error!"; break; - case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_FAILED: msg_str = "TX RMT channel init failed!"; break; - case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_FAILED: msg_str = "RX RMT channel init failed!"; break; - case dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED: msg_str = "RMT encoder init failed!"; break; - case dshot_msg_code_t::DSHOT_ERROR_CALLBACK_REGISTERING_FAILED: msg_str = "RMT RX Callback registering failed!"; break; - case dshot_msg_code_t::DSHOT_ERROR_RECEIVER_FAILED: msg_str = "RMT receiver failed!"; break; - case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_FAILED: msg_str = "Transmission failed!"; break; - case dshot_msg_code_t::DSHOT_ERROR_THROTTLE_NOT_IN_RANGE: msg_str = "Throttle not in range! (48 - 2047)"; break; - case dshot_msg_code_t::DSHOT_ERROR_PERCENT_NOT_IN_RANGE: msg_str = "Percent not in range! (0.0 - 100.0)"; break; - case dshot_msg_code_t::DSHOT_ERROR_COMMAND_NOT_VALID: msg_str = "Command not valid! (0 - 47)"; break; - case dshot_msg_code_t::DSHOT_ERROR_BIDIR_NOT_ENABLED: msg_str = "Bidirectional DShot not enabled!"; break; - case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_FAILED: msg_str = "No valid Telemetric Frame received!"; break; - case dshot_msg_code_t::DSHOT_ERROR_INVALID_MAGNET_COUNT: msg_str = "Invalid motor magnet count!"; break; - case dshot_msg_code_t::DSHOT_ERROR_INVALID_COMMAND: msg_str = "Invalid command!"; break; - case dshot_msg_code_t::DSHOT_ERROR_TIMING_CORRECTION: msg_str = "Timing correction!"; break; - case dshot_msg_code_t::DSHOT_ERROR_INIT_FAILED: msg_str = "SignalGeneratorRMT init failed!"; break; - case dshot_msg_code_t::DSHOT_ERROR_INIT_SUCCESS: msg_str = "SignalGeneratorRMT initialized successfully"; break; - case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_SUCCESS: msg_str = "TX RMT channel initialized successfully"; break; - case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_SUCCESS: msg_str = "RX RMT channel initialized successfully"; break; - case dshot_msg_code_t::DSHOT_ERROR_ENCODING_SUCCESS: msg_str = "Packet encoded successfully"; break; - case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_SUCCESS: msg_str = "Transmission successfully"; break; - case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_SUCCESS: msg_str = "Valid Telemetric Frame received!"; break; - case dshot_msg_code_t::DSHOT_ERROR_COMMAND_SUCCESS: msg_str = "DShot command sent successfully"; break; - default: msg_str = "Unhandled Error Code!"; break; + case dshot_msg_code_t::DSHOT_ERROR_NONE: + msg_str = "None"; + break; + case dshot_msg_code_t::DSHOT_ERROR_UNKNOWN: + msg_str = "Unknown Error!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_FAILED: + msg_str = "TX RMT channel init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_FAILED: + msg_str = "RX RMT channel init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED: + msg_str = "RMT encoder init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_CALLBACK_REGISTERING_FAILED: + msg_str = "RMT RX Callback registering failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_RECEIVER_FAILED: + msg_str = "RMT receiver failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_FAILED: + msg_str = "Transmission failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_THROTTLE_NOT_IN_RANGE: + msg_str = "Throttle not in range! (48 - 2047)"; + break; + case dshot_msg_code_t::DSHOT_ERROR_PERCENT_NOT_IN_RANGE: + msg_str = "Percent not in range! (0.0 - 100.0)"; + break; + case dshot_msg_code_t::DSHOT_ERROR_COMMAND_NOT_VALID: + msg_str = "Command not valid! (0 - 47)"; + break; + case dshot_msg_code_t::DSHOT_ERROR_BIDIR_NOT_ENABLED: + msg_str = "Bidirectional DShot not enabled!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_FAILED: + msg_str = "No valid Telemetric Frame received!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INVALID_MAGNET_COUNT: + msg_str = "Invalid motor magnet count!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INVALID_COMMAND: + msg_str = "Invalid command!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TIMING_CORRECTION: + msg_str = "Timing correction!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INIT_FAILED: + msg_str = "SignalGeneratorRMT init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INIT_SUCCESS: + msg_str = "SignalGeneratorRMT initialized successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_SUCCESS: + msg_str = "TX RMT channel initialized successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_SUCCESS: + msg_str = "RX RMT channel initialized successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_ENCODING_SUCCESS: + msg_str = "Packet encoded successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_SUCCESS: + msg_str = "Transmission successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_SUCCESS: + msg_str = "Valid Telemetric Frame received!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_COMMAND_SUCCESS: + msg_str = "DShot command sent successfully"; + break; + default: + msg_str = "Unhandled Error Code!"; + break; } output.printf("Status: %s - %s", result.success ? "SUCCESS" : "FAILED", msg_str); From 3f3f1ff90c5c14355b770d438d454f8570597f8d Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Thu, 25 Sep 2025 16:32:40 +0200 Subject: [PATCH 4/8] ...simplify user api --- .github/workflows/ci.yml | 108 ++++++++---------- README.md | 8 ++ examples/dshot300/dshot300.ino | 8 +- .../throttle_percent/throttle_percent.ino | 4 +- examples/web_client/web_client.ino | 18 +-- examples/web_control/web_control.ino | 17 +-- keywords.txt | 52 --------- library.properties | 4 +- src/dshot_definitions.h | 2 +- 9 files changed, 81 insertions(+), 140 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dc4cc6..aa77d15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,9 @@ jobs: - name: Ensure ESP32 Core is installed (Linux) shell: bash run: | - arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + if [[ "${{ steps.cache-core-linux.outputs.cache-hit }}" != "true" ]]; then + arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + fi if ! arduino-cli core list | grep -q 'esp32'; then arduino-cli core install esp32:esp32 fi @@ -73,9 +75,12 @@ jobs: restore-keys: | arduino-core-windows- - name: Ensure ESP32 Core is installed (Windows) + if: steps.cache-core-win.outputs.cache-hit != 'true' shell: pwsh run: | - arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + if ($env:cache-core-win.outputs.cache-hit -ne 'true') { + arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + } if (-not (arduino-cli core list | Select-String 'esp32')) { arduino-cli core install esp32:esp32 } @@ -98,79 +103,52 @@ jobs: # Compilation Test # ============================================================================ - compile-test-linux: - name: Compile Sketches (Linux) - runs-on: ubuntu-latest + compile-all-sketches: + name: Compile ${{ matrix.sketch }} (${{ matrix.os }}) + runs-on: ${{ matrix.os }} timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + sketch: + - examples/throttle_percent/throttle_percent.ino + - examples/dshot300/dshot300.ino + - examples/web_control/web_control.ino + - examples/web_client/web_client.ino steps: - uses: actions/checkout@v5 - uses: arduino/setup-arduino-cli@v2 + - name: Restore Arduino Core & Libraries Cache - id: cache-all-linux + id: cache-all uses: actions/cache/restore@v4 with: path: | ~/.arduino15/packages ~/.arduino15/cache ~/Arduino/libraries - key: arduino-full-linux-esp32-v1 + key: ${{ runner.os }}-arduino-full-esp32-v1-${{ hashFiles('**/library.properties', '**/platform.txt') }} restore-keys: | - arduino-full-linux- + ${{ runner.os }}-arduino-full-esp32-v1- + - name: Ensure ESP32 Core and Dependencies (Linux) + if: runner.os == 'Linux' && steps.cache-all.outputs.cache-hit != 'true' shell: bash run: | arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - if ! arduino-cli core list | grep -q 'esp32'; then - arduino-cli core install esp32:esp32 - fi + arduino-cli core install esp32:esp32 mkdir -p ~/Arduino/libraries - if [ ! -d ~/Arduino/libraries/ArduinoJson ]; then - git clone --depth=1 https://github.com/bblanchon/ArduinoJson.git ~/Arduino/libraries/ArduinoJson - fi - if [ ! -d ~/Arduino/libraries/ESPAsyncWebServer ]; then - git clone --depth=1 https://github.com/ESP32Async/ESPAsyncWebServer ~/Arduino/libraries/ESPAsyncWebServer - fi - if [ ! -d ~/Arduino/libraries/AsyncTCP ]; then - git clone --depth=1 https://github.com/ESP32Async/AsyncTCP ~/Arduino/libraries/AsyncTCP - fi - - name: Save Arduino Core & Libraries Cache - if: always() && steps.cache-all-linux.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - path: | - ~/.arduino15/packages - ~/.arduino15/cache - ~/Arduino/libraries - key: arduino-full-linux-esp32-v1 - - name: Compile Sketch - run: | - arduino-cli compile --fqbn esp32:esp32:esp32 --library ${{ github.workspace }} examples/dshot300/dshot300.ino + git clone --depth=1 https://github.com/bblanchon/ArduinoJson.git ~/Arduino/libraries/ArduinoJson + git clone --depth=1 https://github.com/ESP32Async/ESPAsyncWebServer ~/Arduino/libraries/ESPAsyncWebServer + git clone --depth=1 https://github.com/ESP32Async/AsyncTCP ~/Arduino/libraries/AsyncTCP - compile-test-windows: - name: Compile Sketches (Windows) - runs-on: windows-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v5 - - uses: arduino/setup-arduino-cli@v2 - - name: Restore Arduino Core & Libraries Cache - id: cache-all-win - uses: actions/cache/restore@v4 - with: - path: | - ~/.arduino15/packages - ~/.arduino15/cache - ~/Arduino/libraries - key: arduino-full-windows-esp32-v1 - restore-keys: | - arduino-full-windows- - name: Ensure ESP32 Core and Dependencies (Windows) + if: runner.os == 'Windows' && steps.cache-all.outputs.cache-hit != 'true' shell: pwsh run: | arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - if (-not (arduino-cli core list | Select-String 'esp32')) { - arduino-cli core install esp32:esp32 - } + arduino-cli core install esp32:esp32 if (-not (Test-Path -Path "$env:USERPROFILE/Arduino/libraries/ArduinoJson")) { git clone --depth=1 https://github.com/bblanchon/ArduinoJson.git "$env:USERPROFILE/Arduino/libraries/ArduinoJson" } @@ -180,19 +158,26 @@ jobs: if (-not (Test-Path -Path "$env:USERPROFILE/Arduino/libraries/AsyncTCP")) { git clone --depth=1 https://github.com/ESP32Async/AsyncTCP "$env:USERPROFILE/Arduino/libraries/AsyncTCP" } + - name: Save Arduino Core & Libraries Cache - if: always() && steps.cache-all-win.outputs.cache-hit != 'true' + if: always() && steps.cache-all.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: path: | ~/.arduino15/packages ~/.arduino15/cache ~/Arduino/libraries - key: arduino-full-windows-esp32-v1 - - name: Compile Sketch - run: | - arduino-cli compile --fqbn esp32:esp32:esp32 --library ${{ github.workspace }} examples/dshot300/dshot300.ino + key: ${{ runner.os }}-arduino-full-esp32-v1-${{ hashFiles('**/library.properties', '**/platform.txt') }} + - name: Compile Sketch (Linux) + shell: bash + if: runner.os == 'Linux' + run: arduino-cli compile --fqbn esp32:esp32:esp32 --library ${{ github.workspace }} "${{ matrix.sketch }}" + + - name: Compile Sketch (Windows) + shell: pwsh + if: runner.os == 'Windows' + run: arduino-cli compile --fqbn esp32:esp32:esp32 --library ${{ github.workspace }} "${{ matrix.sketch }}" # ============================================================================ # Build Status Report # ============================================================================ @@ -203,8 +188,7 @@ jobs: needs: - quality-check-linux - quality-check-windows - - compile-test-linux - - compile-test-windows + - compile-all-sketches steps: - name: Create Build Summary run: | @@ -216,13 +200,13 @@ jobs: && echo "| ๐Ÿ“‹ Quality Check | โœ… Passed | Arduino Lint completed successfully |" >> $GITHUB_STEP_SUMMARY \ || echo "| ๐Ÿ“‹ Quality Check | โŒ Failed | Check Arduino Lint report |" >> $GITHUB_STEP_SUMMARY - [[ "${{ needs.compile-test-linux.result }}" == "success" && "${{ needs.compile-test-windows.result }}" == "success" ]] \ + [[ "${{ needs.compile-all-sketches.result }}" == "success" ]] \ && echo "| ๐Ÿ”จ Compilation | โœ… Passed | All examples compiled successfully |" >> $GITHUB_STEP_SUMMARY \ || echo "| ๐Ÿ”จ Compilation | โŒ Failed | Compilation errors detected |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - [[ "${{ needs.quality-check-linux.result }}" == "success" && "${{ needs.quality-check-windows.result }}" == "success" && "${{ needs.compile-test-linux.result }}" == "success" && "${{ needs.compile-test-windows.result }}" == "success" ]] \ + [[ "${{ needs.quality-check-linux.result }}" == "success" && "${{ needs.quality-check-windows.result }}" == "success" && "${{ needs.compile-all-sketches.result }}" == "success" ]] \ && echo "## ๐ŸŽ‰ All Checks Passed!" >> $GITHUB_STEP_SUMMARY \ && echo "Your DShotRMT library is ready for deployment." >> $GITHUB_STEP_SUMMARY \ || echo "## โš ๏ธ Action Required" >> $GITHUB_STEP_SUMMARY \ diff --git a/README.md b/README.md index 7508dde..81ea373 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,9 @@ void setup() { // Initialize the DShot motor motor.begin(); + // Print CPU Info + DShotRMT::printCpuInfo(Serial); + Serial.println("Motor initialized. Ramping up to 25% throttle..."); // Ramp up to 25% throttle over 2.5 seconds @@ -61,6 +64,9 @@ void setup() { Serial.println("Stopping motor."); motor.sendThrottlePercent(0); + + // Print DShot Info + DShotRMT::printDShotInfo(motor, Serial); } void loop() { @@ -106,6 +112,8 @@ The main class is `DShotRMT`. Here are the most important methods: - `sendCommand(uint16_t command)`: Sends a DShot command (0-47) to the motor. - `getTelemetry(uint16_t magnet_count)`: Receives and parses telemetry data from the motor (for bidirectional DShot, which is currently not officially supported). - `printDShotResult(dshot_result_t &result, Stream &output = Serial)`: Helper function to print DShot operation results and telemetry to a specified serial output. +- `DShotRMT::printDShotInfo(const DShotRMT &dshot_rmt, Stream &output = Serial)`: Static helper function to print detailed DShot signal information for a given DShotRMT instance. +- `DShotRMT::printCpuInfo(Stream &output = Serial)`: Static helper function to print detailed CPU information. ## ๐Ÿค Contributing diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index be4b9f5..abdb6bd 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -51,7 +51,7 @@ void setup() void loop() { // Safety first - static uint16_t throttle = static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP); + static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; // Initialize the esc with "0" static bool continuous_throttle = true; @@ -127,7 +127,7 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous // Stop motor throttle = 0; continuous_throttle = true; - dshot_result_t result = motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); + dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); printDShotResult(result); } else if (input == "info") @@ -146,14 +146,14 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous // Send DShot command int cmd_num = input.substring(4).toInt(); - if (cmd_num >= static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP) && cmd_num <= static_cast(dshotCommands_e::DSHOT_CMD_MAX)) + if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); printDShotResult(result); } else { - USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, static_cast(dshotCommands_e::DSHOT_CMD_MAX)); + USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX); } } else if (input == "h" || input == "help") diff --git a/examples/throttle_percent/throttle_percent.ino b/examples/throttle_percent/throttle_percent.ino index 1cc47a4..5fb9000 100644 --- a/examples/throttle_percent/throttle_percent.ino +++ b/examples/throttle_percent/throttle_percent.ino @@ -110,14 +110,14 @@ void handleSerialInput(const String &input) // Send DShot command int cmd_num = input.substring(4).toInt(); - if (cmd_num >= static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP) && cmd_num <= static_cast(dshotCommands_e::DSHOT_CMD_MAX)) + if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); printDShotResult(result); } else { - USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, static_cast(dshotCommands_e::DSHOT_CMD_MAX)); + USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX); } } else if (input == "h" || input == "help") diff --git a/examples/web_client/web_client.ino b/examples/web_client/web_client.ino index 1f45084..9d1ca57 100644 --- a/examples/web_client/web_client.ino +++ b/examples/web_client/web_client.ino @@ -58,7 +58,7 @@ AsyncWebServer server(80); AsyncWebSocket ws("/ws"); // Global variables -static uint16_t throttle = static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP); +static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; static bool isArmed = false; static bool last_sent_armed = false; static bool continuous_throttle = true; @@ -126,7 +126,7 @@ void setup() void loop() { static uint64_t last_serial_update = 0; - static uint16_t last_sent_throttle = static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP); + static uint16_t last_sent_throttle = DSHOT_CMD_MOTOR_STOP; static String last_sent_rpm = "N/A"; static uint64_t last_wifi_check = 0; @@ -166,7 +166,7 @@ void loop() } else if (!isArmed && continuous_throttle) { - motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); } // Print motor stats every 3 seconds in continuous mode @@ -267,7 +267,7 @@ void handleOTAUpload(AsyncWebServerRequest *request, String filename, size_t ind if (!index) { // Safety: Ensure motor is stopped during update - motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); setArmingStatus(false); + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); setArmingStatus(false); USB_SERIAL.printf("OTA Update Start: %s\n", filename.c_str()); @@ -380,7 +380,7 @@ void setArmingStatus(bool armed) // Safety: Stop motor and reset throttle when disarming throttle = 0; continuous_throttle = false; - motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); USB_SERIAL.println(" "); USB_SERIAL.println("=== MOTOR DISARMED - SAFETY STOP EXECUTED ==="); @@ -517,7 +517,7 @@ void handleSerialInput(const String &input) continuous_throttle = false; int cmd_num = input.substring(4).toInt(); - if (cmd_num >= static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP) && cmd_num <= static_cast(dshotCommands_e::DSHOT_CMD_MAX)) + if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); printDShotResult(result); @@ -525,7 +525,7 @@ void handleSerialInput(const String &input) else { USB_SERIAL.println(" "); - USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, static_cast(dshotCommands_e::DSHOT_CMD_MAX)); + USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX); } return; } @@ -580,7 +580,7 @@ void handleSerialInput(const String &input) { throttle = 0; continuous_throttle = false; - dshot_result_t result = motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); + dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); printDShotResult(result); return; } @@ -634,7 +634,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) { throttle = 0; continuous_throttle = false; - motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); } else if (web_throttle >= DSHOT_THROTTLE_MIN && web_throttle <= DSHOT_THROTTLE_MAX) { diff --git a/examples/web_control/web_control.ino b/examples/web_control/web_control.ino index c6e2182..6cc773f 100644 --- a/examples/web_control/web_control.ino +++ b/examples/web_control/web_control.ino @@ -57,7 +57,7 @@ AsyncWebServer server(80); AsyncWebSocket ws("/ws"); // Global variables -static uint16_t throttle = static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP); +static uint16_t throttle = DSHOT_CMD_MOTOR_STOP; static bool isArmed = false; static bool continuous_throttle = true; @@ -109,7 +109,7 @@ void setup() void loop() { static uint64_t last_serial_update = 0; - static uint16_t last_sent_throttle = static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP); + static uint16_t last_sent_throttle = DSHOT_CMD_MOTOR_STOP; static bool last_sent_armed = false; static String last_sent_rpm = "N/A"; @@ -131,7 +131,8 @@ void loop() } else if (!isArmed && continuous_throttle) { - motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); + // Ensure motor is stopped when disarmed + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); } // Print motor stats every 3 seconds in continuous mode @@ -206,7 +207,7 @@ void setArmingStatus(bool armed) // Safety: Stop motor and reset throttle when disarming throttle = 0; continuous_throttle = false; - motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); USB_SERIAL.println(" "); USB_SERIAL.println("=== MOTOR DISARMED - SAFETY STOP EXECUTED ==="); } @@ -284,7 +285,7 @@ void handleSerialInput(const String &input) continuous_throttle = false; int cmd_num = input.substring(4).toInt(); - if (cmd_num >= static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP) && cmd_num <= static_cast(dshotCommands_e::DSHOT_CMD_MAX)) + if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); printDShotResult(result); @@ -292,7 +293,7 @@ void handleSerialInput(const String &input) else { USB_SERIAL.println(" "); - USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, static_cast(dshotCommands_e::DSHOT_CMD_MAX)); + USB_SERIAL.printf("Invalid command: %d (valid range: 0 - %d)\n", cmd_num, DSHOT_CMD_MAX); } return; } @@ -339,7 +340,7 @@ void handleSerialInput(const String &input) { throttle = 0; continuous_throttle = false; - dshot_result_t result = motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); + dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); printDShotResult(result); return; } @@ -393,7 +394,7 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) { throttle = 0; continuous_throttle = false; - motor01.sendCommand(static_cast(dshotCommands_e::DSHOT_CMD_MOTOR_STOP)); + motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); } else if (web_throttle >= DSHOT_THROTTLE_MIN && web_throttle <= DSHOT_THROTTLE_MAX) { diff --git a/keywords.txt b/keywords.txt index 16404c7..1571472 100644 --- a/keywords.txt +++ b/keywords.txt @@ -7,59 +7,7 @@ ####################################### DShotRMT KEYWORD1 -DShotCommandManager KEYWORD1 -dshot_mode_t KEYWORD1 -dshot_packet_t KEYWORD1 -dshot_timing_t KEYWORD1 -dshot_commands_t KEYWORD1 -dshot_command_result_t KEYWORD1 -dshot_command_sequence_item_t KEYWORD1 -dshotCommands_e KEYWORD1 -dshotCommandType_e KEYWORD1 -####################################### -# Methods and Functions (KEYWORD2) -####################################### - -# DShotRMT Methods -begin KEYWORD2 -setThrottle KEYWORD2 -sendThrottle KEYWORD2 -sendDShotCommand KEYWORD2 -sendCommand KEYWORD2 -getERPM KEYWORD2 -getMotorRPM KEYWORD2 -getGPIO KEYWORD2 -getDShotPacket KEYWORD2 -is_bidirectional KEYWORD2 -printDShotInfo KEYWORD2 -printCpuInfo KEYWORD2 - -# DShotCommandManager Methods -sendCommand KEYWORD2 -sendCommandWithDelay KEYWORD2 -stopMotor KEYWORD2 -set3DMode KEYWORD2 -setSpinDirection KEYWORD2 -saveSettings KEYWORD2 -setExtendedTelemetry KEYWORD2 -requestESCInfo KEYWORD2 -setLED KEYWORD2 -activateBeacon KEYWORD2 -setAudioStreamMode KEYWORD2 -setSilentMode KEYWORD2 -executeSequence KEYWORD2 -executeInitSequence KEYWORD2 -executeCalibrationSequence KEYWORD2 -getCommandName KEYWORD2 -isValidCommand KEYWORD2 -printStatistics KEYWORD2 -resetStatistics KEYWORD2 -getTotalCommandCount KEYWORD2 -getFailedCommandCount KEYWORD2 -getLastExecutionTime KEYWORD2 -printMenu KEYWORD2 -handleMenuInput KEYWORD2 ####################################### # Constants (LITERAL1) diff --git a/library.properties b/library.properties index ed727ea..635b303 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=DShotRMT -version=0.8.5 +version=0.8.6 author=Wastl Kraus maintainer=Wastl Kraus license=MIT @@ -8,4 +8,4 @@ paragraph=This library can control a BlHeli_S by using encoded DShot commands. category=Signal Input/Output url=https://github.com/derdoktor667/DShotRMT architectures=esp32 -provides_includes=DShotRMT.h, dshot_commands.h, web_content.h, ota_update.h \ No newline at end of file +provides_includes=DShotRMT.h, dshot_definitions.h, web_content.h, ota_update.h \ No newline at end of file diff --git a/src/dshot_definitions.h b/src/dshot_definitions.h index 994607c..268f342 100644 --- a/src/dshot_definitions.h +++ b/src/dshot_definitions.h @@ -102,7 +102,7 @@ typedef struct dshot_result * @brief DShot Commands * Enum class for standard DShot commands that can be sent to an ESC. */ -enum class dshotCommands_e +enum dshotCommands_e { DSHOT_CMD_MOTOR_STOP = 0, DSHOT_CMD_BEACON1, From 96b5595c7299d40aefc10d5b6f0471672ef32012 Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Fri, 26 Sep 2025 17:07:05 +0200 Subject: [PATCH 5/8] Use official RMT Encoder API --- .github/workflows/ci.yml | 230 +++++++----------- README.md | 2 +- examples/dshot300/dshot300.ino | 10 +- .../throttle_percent/throttle_percent.ino | 8 +- examples/web_client/web_client.ino | 8 +- examples/web_control/web_control.ino | 8 +- src/DShotRMT.cpp | 181 +++++++++++--- src/DShotRMT.h | 17 +- src/dshot_definitions.h | 98 -------- 9 files changed, 259 insertions(+), 303 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa77d15..c950f20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,98 +1,92 @@ +# This GitHub Actions workflow is designed to ensure code quality and +# successful compilation for an ESP32 Arduino library. +# It runs on every push to any branch. + name: ESP32 Build & Quality Check +# Set permissions for the GitHub token. +# 'contents: read' is required to checkout the repository. permissions: contents: read +# Define the trigger for the workflow. +# It runs on any push to any branch. on: push: branches: [ '*' ] +# Concurrency control ensures that only the latest commit for a branch +# runs, canceling any workflows already in progress for the same branch. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # ============================================================================ - # Code Quality & Linting + # Prepare Environment & Cache + # This job installs the Arduino core and libraries and saves them to a cache + # for later jobs to use. # ============================================================================ - - quality-check-linux: - name: Arduino Lint Check (Linux) + prepare-cache: + name: Prepare Cache (Linux) runs-on: ubuntu-latest - timeout-minutes: 10 + steps: - - uses: actions/checkout@v5 - - uses: arduino/setup-arduino-cli@v2 - - name: Restore Arduino Core Cache - id: cache-core-linux - uses: actions/cache/restore@v4 + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Arduino CLI + uses: arduino/setup-arduino-cli@v2 + + - name: Restore Arduino Core & Libraries Cache + uses: actions/cache@v4 + id: cache-arduino with: path: | ~/.arduino15/packages ~/.arduino15/cache - key: arduino-core-linux-esp32-v1 - restore-keys: | - arduino-core-linux- - - name: Ensure ESP32 Core is installed (Linux) + ~/Arduino/libraries + key: linux-arduino-esp32-v2-${{ hashFiles('**/library.properties') }} + + - name: Install Dependencies + if: steps.cache-arduino.outputs.cache-hit != 'true' shell: bash run: | - if [[ "${{ steps.cache-core-linux.outputs.cache-hit }}" != "true" ]]; then - arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - fi - if ! arduino-cli core list | grep -q 'esp32'; then - arduino-cli core install esp32:esp32 - fi - - name: Save Arduino Core Cache - if: always() && steps.cache-core-linux.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - path: | - ~/.arduino15/packages - ~/.arduino15/cache - key: arduino-core-linux-esp32-v1 - - uses: arduino/arduino-lint-action@v2 - with: - path: ${{ github.workspace }} - compliance: strict - library-manager: update - verbose: true + arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + arduino-cli core install esp32:esp32 + arduino-cli lib install "ArduinoJson" + mkdir -p "$HOME/Arduino/libraries" + git clone --depth=1 https://github.com/ESP32Async/ESPAsyncWebServer "$HOME/Arduino/libraries/ESPAsyncWebServer" + git clone --depth=1 https://github.com/ESP32Async/AsyncTCP "$HOME/Arduino/libraries/AsyncTCP" - quality-check-windows: - name: Arduino Lint Check (Windows) - runs-on: windows-latest + # ============================================================================ + # Code Quality & Linting + # This job runs after the cache is prepared and checks the code quality. + # ============================================================================ + quality-check: + name: Arduino Lint Check (Linux) + needs: prepare-cache + runs-on: ubuntu-latest timeout-minutes: 10 + steps: - - uses: actions/checkout@v5 - - uses: arduino/setup-arduino-cli@v2 - - name: Restore Arduino Core Cache - id: cache-core-win - uses: actions/cache/restore@v4 + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Arduino CLI + uses: arduino/setup-arduino-cli@v2 + + - name: Restore Arduino Core & Libraries Cache + uses: actions/cache@v4 with: path: | ~/.arduino15/packages ~/.arduino15/cache - key: arduino-core-windows-esp32-v1 - restore-keys: | - arduino-core-windows- - - name: Ensure ESP32 Core is installed (Windows) - if: steps.cache-core-win.outputs.cache-hit != 'true' - shell: pwsh - run: | - if ($env:cache-core-win.outputs.cache-hit -ne 'true') { - arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - } - if (-not (arduino-cli core list | Select-String 'esp32')) { - arduino-cli core install esp32:esp32 - } - - name: Save Arduino Core Cache - if: always() && steps.cache-core-win.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - path: | - ~/.arduino15/packages - ~/.arduino15/cache - key: arduino-core-windows-esp32-v1 - - uses: arduino/arduino-lint-action@v2 + ~/Arduino/libraries + key: linux-arduino-esp32-v2-${{ hashFiles('**/library.properties') }} + + - name: Run Arduino Lint + uses: arduino/arduino-lint-action@v2 with: path: ${{ github.workspace }} compliance: strict @@ -101,114 +95,66 @@ jobs: # ============================================================================ # Compilation Test + # This job compiles all example sketches. # ============================================================================ - - compile-all-sketches: - name: Compile ${{ matrix.sketch }} (${{ matrix.os }}) - runs-on: ${{ matrix.os }} + compile-sketches: + name: Compile ${{ matrix.sketch }} (Linux) + needs: prepare-cache + runs-on: ubuntu-latest timeout-minutes: 15 strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] sketch: - examples/throttle_percent/throttle_percent.ino - examples/dshot300/dshot300.ino - examples/web_control/web_control.ino - examples/web_client/web_client.ino + steps: - - uses: actions/checkout@v5 - - uses: arduino/setup-arduino-cli@v2 + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Arduino CLI + uses: arduino/setup-arduino-cli@v2 - name: Restore Arduino Core & Libraries Cache - id: cache-all - uses: actions/cache/restore@v4 + uses: actions/cache@v4 with: path: | ~/.arduino15/packages ~/.arduino15/cache ~/Arduino/libraries - key: ${{ runner.os }}-arduino-full-esp32-v1-${{ hashFiles('**/library.properties', '**/platform.txt') }} - restore-keys: | - ${{ runner.os }}-arduino-full-esp32-v1- - - - name: Ensure ESP32 Core and Dependencies (Linux) - if: runner.os == 'Linux' && steps.cache-all.outputs.cache-hit != 'true' - shell: bash - run: | - arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - arduino-cli core install esp32:esp32 - mkdir -p ~/Arduino/libraries - git clone --depth=1 https://github.com/bblanchon/ArduinoJson.git ~/Arduino/libraries/ArduinoJson - git clone --depth=1 https://github.com/ESP32Async/ESPAsyncWebServer ~/Arduino/libraries/ESPAsyncWebServer - git clone --depth=1 https://github.com/ESP32Async/AsyncTCP ~/Arduino/libraries/AsyncTCP - - - name: Ensure ESP32 Core and Dependencies (Windows) - if: runner.os == 'Windows' && steps.cache-all.outputs.cache-hit != 'true' - shell: pwsh - run: | - arduino-cli core update-index --additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - arduino-cli core install esp32:esp32 - if (-not (Test-Path -Path "$env:USERPROFILE/Arduino/libraries/ArduinoJson")) { - git clone --depth=1 https://github.com/bblanchon/ArduinoJson.git "$env:USERPROFILE/Arduino/libraries/ArduinoJson" - } - if (-not (Test-Path -Path "$env:USERPROFILE/Arduino/libraries/ESPAsyncWebServer")) { - git clone --depth=1 https://github.com/ESP32Async/ESPAsyncWebServer "$env:USERPROFILE/Arduino/libraries/ESPAsyncWebServer" - } - if (-not (Test-Path -Path "$env:USERPROFILE/Arduino/libraries/AsyncTCP")) { - git clone --depth=1 https://github.com/ESP32Async/AsyncTCP "$env:USERPROFILE/Arduino/libraries/AsyncTCP" - } - - - name: Save Arduino Core & Libraries Cache - if: always() && steps.cache-all.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - path: | - ~/.arduino15/packages - ~/.arduino15/cache - ~/Arduino/libraries - key: ${{ runner.os }}-arduino-full-esp32-v1-${{ hashFiles('**/library.properties', '**/platform.txt') }} - - - name: Compile Sketch (Linux) - shell: bash - if: runner.os == 'Linux' + key: linux-arduino-esp32-v2-${{ hashFiles('**/library.properties') }} + + - name: Compile Sketch run: arduino-cli compile --fqbn esp32:esp32:esp32 --library ${{ github.workspace }} "${{ matrix.sketch }}" - - name: Compile Sketch (Windows) - shell: pwsh - if: runner.os == 'Windows' - run: arduino-cli compile --fqbn esp32:esp32:esp32 --library ${{ github.workspace }} "${{ matrix.sketch }}" # ============================================================================ # Build Status Report + # This job runs last and provides a summary of the build status. # ============================================================================ build-summary: name: Build Summary runs-on: ubuntu-latest - if: always() + if: always() # This ensures the job runs even if previous jobs fail needs: - - quality-check-linux - - quality-check-windows - - compile-all-sketches + - quality-check + - compile-sketches + steps: - name: Create Build Summary run: | echo "# ๐Ÿ”ง DShotRMT Build Report" >> $GITHUB_STEP_SUMMARY echo "| Check | Status | Details |" >> $GITHUB_STEP_SUMMARY echo "|-------|--------|---------|" >> $GITHUB_STEP_SUMMARY - - [[ "${{ needs.quality-check-linux.result }}" == "success" && "${{ needs.quality-check-windows.result }}" == "success" ]] \ - && echo "| ๐Ÿ“‹ Quality Check | โœ… Passed | Arduino Lint completed successfully |" >> $GITHUB_STEP_SUMMARY \ - || echo "| ๐Ÿ“‹ Quality Check | โŒ Failed | Check Arduino Lint report |" >> $GITHUB_STEP_SUMMARY - - [[ "${{ needs.compile-all-sketches.result }}" == "success" ]] \ - && echo "| ๐Ÿ”จ Compilation | โœ… Passed | All examples compiled successfully |" >> $GITHUB_STEP_SUMMARY \ - || echo "| ๐Ÿ”จ Compilation | โŒ Failed | Compilation errors detected |" >> $GITHUB_STEP_SUMMARY - - echo "" >> $GITHUB_STEP_SUMMARY - - [[ "${{ needs.quality-check-linux.result }}" == "success" && "${{ needs.quality-check-windows.result }}" == "success" && "${{ needs.compile-all-sketches.result }}" == "success" ]] \ - && echo "## ๐ŸŽ‰ All Checks Passed!" >> $GITHUB_STEP_SUMMARY \ - && echo "Your DShotRMT library is ready for deployment." >> $GITHUB_STEP_SUMMARY \ - || echo "## โš ๏ธ Action Required" >> $GITHUB_STEP_SUMMARY \ - && echo "Please review the failed checks and address any issues." >> $GITHUB_STEP_SUMMARY - \ No newline at end of file + if [[ "${{ needs.quality-check.result }}" == "success" ]]; then + echo "| ๐Ÿ“‹ Quality Check | โœ… Passed | Arduino Lint completed successfully |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ“‹ Quality Check | โŒ Failed | Check Arduino Lint report for errors |" >> $GITHUB_STEP_SUMMARY + fi + if [[ "${{ needs.compile-sketches.result }}" == "success" ]]; then + echo "| ๐Ÿ”จ Compilation | โœ… Passed | All examples compiled successfully |" >> $GITHUB_STEP_SUMMARY + else + echo "| ๐Ÿ”จ Compilation | โŒ Failed | One or more examples failed to compile |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/README.md b/README.md index 81ea373..890cb95 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Arduino CI](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml/badge.svg)](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml) -A C++ library for generating DShot signals on ESP32 microcontrollers using the **modern ESP-IDF 5 RMT encoder API** (`rmt_tx.h` / `rmt_rx.h`). It provides a simple, efficient, and hardware-timed way to control brushless motors in both Arduino and ESP-IDF projects. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. +A C++ library for generating DShot signals on ESP32 microcontrollers using the **modern ESP-IDF 5 RMT encoder API** (`rmt_tx.h` / `rmt_rx.h`). It leverages the standard `rmt_bytes_encoder` to ensure an efficient, hardware-timed, and maintainable implementation. The library provides a simple way to control brushless motors in both Arduino and ESP-IDF projects. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. ## ๐Ÿš€ Core Features diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index abdb6bd..619833d 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -88,7 +88,7 @@ void loop() if (IS_BIDIRECTIONAL) { dshot_result_t telem_result = motor01.getTelemetry(); - printDShotResult(telem_result); + DShotRMT::printDShotResult(telem_result); } USB_SERIAL.println("Type 'help' to show Menu"); @@ -128,7 +128,7 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous throttle = 0; continuous_throttle = true; dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else if (input == "info") { @@ -137,7 +137,7 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous else if (input == "rpm" && IS_BIDIRECTIONAL) { dshot_result_t result = motor01.getTelemetry(); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else if (input.startsWith("cmd ")) { @@ -149,7 +149,7 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else { @@ -171,7 +171,7 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous continuous_throttle = true; dshot_result_t result = motor01.sendThrottle(throttle); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else { diff --git a/examples/throttle_percent/throttle_percent.ino b/examples/throttle_percent/throttle_percent.ino index 5fb9000..13e7f8f 100644 --- a/examples/throttle_percent/throttle_percent.ino +++ b/examples/throttle_percent/throttle_percent.ino @@ -94,7 +94,7 @@ void handleSerialInput(const String &input) { // Stop motor dshot_result_t result = motor01.sendThrottlePercent(0.0f); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else if (input == "info") { @@ -103,7 +103,7 @@ void handleSerialInput(const String &input) else if (input == "rpm" && IS_BIDIRECTIONAL) { dshot_result_t result = motor01.getTelemetry(); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else if (input.startsWith("cmd ")) { @@ -113,7 +113,7 @@ void handleSerialInput(const String &input) if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else { @@ -132,7 +132,7 @@ void handleSerialInput(const String &input) if (throttle_percent >= 0.0f && throttle_percent <= 100.0f) { dshot_result_t result = motor01.sendThrottlePercent(throttle_percent); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else { diff --git a/examples/web_client/web_client.ino b/examples/web_client/web_client.ino index 9d1ca57..92e0ca3 100644 --- a/examples/web_client/web_client.ino +++ b/examples/web_client/web_client.ino @@ -178,7 +178,7 @@ void loop() if (IS_BIDIRECTIONAL && isArmed) { dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - printDShotResult(telem_result); + DShotRMT::printDShotResult(telem_result); } USB_SERIAL.println("Type 'help' to show Menu"); @@ -495,7 +495,7 @@ void handleSerialInput(const String &input) if (isArmed) { dshot_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else { @@ -520,7 +520,7 @@ void handleSerialInput(const String &input) if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else { @@ -581,7 +581,7 @@ void handleSerialInput(const String &input) throttle = 0; continuous_throttle = false; dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); - printDShotResult(result); + DShotRMT::printDShotResult(result); return; } diff --git a/examples/web_control/web_control.ino b/examples/web_control/web_control.ino index 6cc773f..76cc775 100644 --- a/examples/web_control/web_control.ino +++ b/examples/web_control/web_control.ino @@ -146,7 +146,7 @@ void loop() if (IS_BIDIRECTIONAL && isArmed) { dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - printDShotResult(telem_result); + DShotRMT::printDShotResult(telem_result); } USB_SERIAL.println("Type 'help' to show Menu"); @@ -264,7 +264,7 @@ void handleSerialInput(const String &input) if (isArmed) { dshot_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else { @@ -288,7 +288,7 @@ void handleSerialInput(const String &input) if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); - printDShotResult(result); + DShotRMT::printDShotResult(result); } else { @@ -341,7 +341,7 @@ void handleSerialInput(const String &input) throttle = 0; continuous_throttle = false; dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); - printDShotResult(result); + DShotRMT::printDShotResult(result); return; } diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 25c86bf..04f36f0 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -23,8 +23,8 @@ DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, ui _last_command_timestamp(0), _encoded_frame_value(0), _packet{0}, - _level0(1), // DShot standard: signal is idle-low, so pulses start by going HIGH - _level1(0), // DShot standard: signal returns to LOW after the high pulse + _pulse_level(1), // DShot standard: signal is idle-low, so pulses start by going HIGH + _idle_level(0), // DShot standard: signal returns to LOW after the high pulse _rmt_tx_channel(nullptr), _rmt_rx_channel(nullptr), _dshot_encoder(nullptr), @@ -355,9 +355,25 @@ dshot_result_t DShotRMT::_initRXChannel() dshot_result_t DShotRMT::_initDShotEncoder() { - rmt_copy_encoder_config_t encoder_config = {}; + 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_copy_encoder(&encoder_config, &_dshot_encoder) != DSHOT_OK) + if (rmt_new_bytes_encoder(&encoder_config, &_dshot_encoder) != DSHOT_OK) { return {false, dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED}; } @@ -424,50 +440,27 @@ void DShotRMT::_preCalculateRMTTicks() dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) { // Ensure enough time has passed since the last transmission - if (!_timer_signal()) + if (!_isFrameIntervalElapsed()) { return {true, dshot_msg_code_t::DSHOT_ERROR_NONE}; } - rmt_symbol_word_t tx_symbols[DSHOT_BITS_PER_FRAME]; - dshot_result_t result = _encodeDShotFrame(packet, tx_symbols); + _encoded_frame_value = _buildDShotFrameValue(packet); - if (!result.success) - { - return result; - } + // The DShot frame is 16 bits, which is 2 bytes + size_t tx_size_bytes = sizeof(_encoded_frame_value); - size_t tx_size_bytes = DSHOT_BITS_PER_FRAME * sizeof(rmt_symbol_word_t); - - if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, tx_symbols, tx_size_bytes, &_rmt_tx_config) != DSHOT_OK) + if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, &_encoded_frame_value, tx_size_bytes, &_rmt_tx_config) != DSHOT_OK) { return {false, dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_FAILED}; } - _timer_reset(); // Reset the timer for the next frame + _recordFrameTransmissionTime(); // Reset the timer for the next frame return {true, dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_SUCCESS}; } -// This function needs to be fast, as it generates the RMT symbols just before sending -dshot_result_t IRAM_ATTR DShotRMT::_encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols) -{ - _encoded_frame_value = _buildDShotFrameValue(packet); - for (int i = 0; i < DSHOT_BITS_PER_FRAME; ++i) - { - int bit_position = DSHOT_BITS_PER_FRAME - 1 - i; - bool bit = (_encoded_frame_value >> bit_position) & 1; - - // A '1' bit has a longer high-time, a '0' bit has a shorter high-time - symbols[i].level0 = _level0; // Go HIGH - symbols[i].duration0 = bit ? _rmt_ticks.t1h_ticks : _rmt_ticks.t0h_ticks; - symbols[i].level1 = _level1; // Go LOW - symbols[i].duration1 = bit ? _rmt_ticks.t1l_ticks : _rmt_ticks.t0l_ticks; - } - - return {true, dshot_msg_code_t::DSHOT_ERROR_ENCODING_SUCCESS}; -} // 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) @@ -510,7 +503,7 @@ uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) } // Timing Control Functions -bool IRAM_ATTR DShotRMT::_timer_signal() +bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed() { // Check if the minimum interval between frames has passed uint64_t current_time = esp_timer_get_time(); @@ -518,11 +511,10 @@ bool IRAM_ATTR DShotRMT::_timer_signal() return elapsed >= _frame_timer_us; } -bool DShotRMT::_timer_reset() +void DShotRMT::_recordFrameTransmissionTime() { // Record the time of the current transmission _last_transmission_time_us = esp_timer_get_time(); - return true; } // Static Callback Functions @@ -546,14 +538,123 @@ bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const return false; } +void DShotRMT::printDShotResult(const dshot_result_t &result, Stream &output) +{ + const char *msg_str; + switch (result.error_code) + { + case dshot_msg_code_t::DSHOT_ERROR_NONE: + msg_str = "None"; + break; + case dshot_msg_code_t::DSHOT_ERROR_UNKNOWN: + msg_str = "Unknown Error!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_FAILED: + msg_str = "TX RMT channel init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_FAILED: + msg_str = "RX RMT channel init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED: + msg_str = "RMT encoder init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_CALLBACK_REGISTERING_FAILED: + msg_str = "RMT RX Callback registering failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_RECEIVER_FAILED: + msg_str = "RMT receiver failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_FAILED: + msg_str = "Transmission failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_THROTTLE_NOT_IN_RANGE: + msg_str = "Throttle not in range! (48 - 2047)"; + break; + case dshot_msg_code_t::DSHOT_ERROR_PERCENT_NOT_IN_RANGE: + msg_str = "Percent not in range! (0.0 - 100.0)"; + break; + case dshot_msg_code_t::DSHOT_ERROR_COMMAND_NOT_VALID: + msg_str = "Command not valid! (0 - 47)"; + break; + case dshot_msg_code_t::DSHOT_ERROR_BIDIR_NOT_ENABLED: + msg_str = "Bidirectional DShot not enabled!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_FAILED: + msg_str = "No valid Telemetric Frame received!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INVALID_MAGNET_COUNT: + msg_str = "Invalid motor magnet count!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INVALID_COMMAND: + msg_str = "Invalid command!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TIMING_CORRECTION: + msg_str = "Timing correction!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INIT_FAILED: + msg_str = "SignalGeneratorRMT init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INIT_SUCCESS: + msg_str = "SignalGeneratorRMT initialized successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_SUCCESS: + msg_str = "TX RMT channel initialized successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_SUCCESS: + msg_str = "RX RMT channel initialized successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_ENCODING_SUCCESS: + msg_str = "Packet encoded successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_SUCCESS: + msg_str = "Transmission successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_SUCCESS: + msg_str = "Valid Telemetric Frame received!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_COMMAND_SUCCESS: + msg_str = "DShot command sent successfully"; + break; + default: + msg_str = "Unhandled Error Code!"; + break; + } + output.printf("Status: %s - %s", result.success ? "SUCCESS" : "FAILED", msg_str); + + // Print telemetry data if available + if (result.success && (result.erpm > 0 || result.motor_rpm > 0)) + { + output.printf(" | eRPM: %u, Motor RPM: %u", result.erpm, result.motor_rpm); + } + + output.println(); +} + // Public Static Utility Functions void DShotRMT::printDShotInfo(const DShotRMT &dshot_rmt, Stream &output) { output.println("\n === DShot Signal Info === "); - output.printf("Current Mode: DSHOT%d\n", dshot_rmt.getMode() == dshot_mode_t::DSHOT150 ? 150 : dshot_rmt.getMode() == dshot_mode_t::DSHOT300 ? 300 - : dshot_rmt.getMode() == dshot_mode_t::DSHOT600 ? 600 - : dshot_rmt.getMode() == dshot_mode_t::DSHOT1200 ? 1200 - : 0); + + int mode_val = 0; + switch (dshot_rmt.getMode()) + { + case dshot_mode_t::DSHOT150: + mode_val = 150; + break; + case dshot_mode_t::DSHOT300: + mode_val = 300; + break; + case dshot_mode_t::DSHOT600: + mode_val = 600; + break; + case dshot_mode_t::DSHOT1200: + mode_val = 1200; + break; + default: + break; + } + output.printf("Current Mode: DSHOT%d\n", mode_val); + output.printf("Bidirectional: %s\n", dshot_rmt.isBidirectional() ? "YES" : "NO"); output.printf("Current Packet: "); diff --git a/src/DShotRMT.h b/src/DShotRMT.h index a9cc23c..7a45321 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -113,6 +113,13 @@ public: dshot_result_t saveESCSettings(); // Public Utility & Info Functions + /** + * @brief Prints the result of a DShot operation to the specified output stream. + * @param result The dshot_result_t structure containing the operation's outcome. + * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. + */ + static void printDShotResult(const dshot_result_t &result, Stream &output = Serial); + /** * @brief Prints detailed DShot signal information for a given DShotRMT instance. * @param dshot_rmt The DShotRMT instance to get information from. @@ -213,8 +220,8 @@ private: uint64_t _last_command_timestamp; uint16_t _encoded_frame_value; dshot_packet_t _packet; - uint16_t _level0; // DShot protocol: Signal is idle-low, so pulses start by going HIGH. - uint16_t _level1; // DShot protocol: Signal returns to LOW after the high pulse. + uint16_t _pulse_level; // DShot protocol: Signal is idle-low, so pulses start by going HIGH. + uint16_t _idle_level; // DShot protocol: Signal returns to LOW after the high pulse. // RMT Hardware Handles rmt_channel_handle_t _rmt_tx_channel; @@ -245,12 +252,12 @@ private: // Private Frame Processing Functions dshot_result_t _sendDShotFrame(const dshot_packet_t &packet); - dshot_result_t _encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols); + uint16_t _decodeDShotFrame(const rmt_symbol_word_t *symbols); // Private Timing Control Functions - bool _timer_signal(); - bool _timer_reset(); + bool _isFrameIntervalElapsed(); + 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); diff --git a/src/dshot_definitions.h b/src/dshot_definitions.h index 268f342..a3b6725 100644 --- a/src/dshot_definitions.h +++ b/src/dshot_definitions.h @@ -217,101 +217,3 @@ const char *const TIMING_CORRECTION = "Timing correction!"; const char *const CALLBACK_REGISTERING_FAILED = "RMT RX Callback registering failed!"; const char *const INVALID_COMMAND = "Invalid command!"; const char *const COMMAND_SUCCESS = "DShot command sent successfully"; - -// Helper Functions -/** - * @brief Prints the result of a DShot operation to the specified output stream. - * @param result The dshot_result_t structure containing the operation's outcome. - * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. - */ -inline void printDShotResult(dshot_result_t &result, Stream &output = Serial) -{ - const char *msg_str; - switch (result.error_code) - { - case dshot_msg_code_t::DSHOT_ERROR_NONE: - msg_str = "None"; - break; - case dshot_msg_code_t::DSHOT_ERROR_UNKNOWN: - msg_str = "Unknown Error!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_FAILED: - msg_str = "TX RMT channel init failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_FAILED: - msg_str = "RX RMT channel init failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED: - msg_str = "RMT encoder init failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_CALLBACK_REGISTERING_FAILED: - msg_str = "RMT RX Callback registering failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_RECEIVER_FAILED: - msg_str = "RMT receiver failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_FAILED: - msg_str = "Transmission failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_THROTTLE_NOT_IN_RANGE: - msg_str = "Throttle not in range! (48 - 2047)"; - break; - case dshot_msg_code_t::DSHOT_ERROR_PERCENT_NOT_IN_RANGE: - msg_str = "Percent not in range! (0.0 - 100.0)"; - break; - case dshot_msg_code_t::DSHOT_ERROR_COMMAND_NOT_VALID: - msg_str = "Command not valid! (0 - 47)"; - break; - case dshot_msg_code_t::DSHOT_ERROR_BIDIR_NOT_ENABLED: - msg_str = "Bidirectional DShot not enabled!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_FAILED: - msg_str = "No valid Telemetric Frame received!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_INVALID_MAGNET_COUNT: - msg_str = "Invalid motor magnet count!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_INVALID_COMMAND: - msg_str = "Invalid command!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TIMING_CORRECTION: - msg_str = "Timing correction!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_INIT_FAILED: - msg_str = "SignalGeneratorRMT init failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_INIT_SUCCESS: - msg_str = "SignalGeneratorRMT initialized successfully"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_SUCCESS: - msg_str = "TX RMT channel initialized successfully"; - break; - case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_SUCCESS: - msg_str = "RX RMT channel initialized successfully"; - break; - case dshot_msg_code_t::DSHOT_ERROR_ENCODING_SUCCESS: - msg_str = "Packet encoded successfully"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_SUCCESS: - msg_str = "Transmission successfully"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_SUCCESS: - msg_str = "Valid Telemetric Frame received!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_COMMAND_SUCCESS: - msg_str = "DShot command sent successfully"; - break; - default: - msg_str = "Unhandled Error Code!"; - break; - } - output.printf("Status: %s - %s", result.success ? "SUCCESS" : "FAILED", msg_str); - - // Print telemetry data if available - if (result.success && (result.erpm > 0 || result.motor_rpm > 0)) - { - output.printf(" | eRPM: %u, Motor RPM: %u", result.erpm, result.motor_rpm); - } - - output.println(); -} \ No newline at end of file From 0028db7dca816db872926bf8c02af4ea03520474 Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Fri, 26 Sep 2025 19:01:55 +0200 Subject: [PATCH 6/8] Revert "Use official RMT Encoder API" This reverts commit 96b5595c7299d40aefc10d5b6f0471672ef32012. --- .github/workflows/ci.yml | 1 + README.md | 2 +- examples/dshot300/dshot300.ino | 10 +- .../throttle_percent/throttle_percent.ino | 8 +- examples/web_client/web_client.ino | 8 +- examples/web_control/web_control.ino | 8 +- src/DShotRMT.cpp | 181 ++++-------------- src/DShotRMT.h | 17 +- src/dshot_definitions.h | 98 ++++++++++ 9 files changed, 162 insertions(+), 171 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c950f20..f536deb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,3 +158,4 @@ jobs: else echo "| ๐Ÿ”จ Compilation | โŒ Failed | One or more examples failed to compile |" >> $GITHUB_STEP_SUMMARY fi + \ No newline at end of file diff --git a/README.md b/README.md index 890cb95..81ea373 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Arduino CI](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml/badge.svg)](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml) -A C++ library for generating DShot signals on ESP32 microcontrollers using the **modern ESP-IDF 5 RMT encoder API** (`rmt_tx.h` / `rmt_rx.h`). It leverages the standard `rmt_bytes_encoder` to ensure an efficient, hardware-timed, and maintainable implementation. The library provides a simple way to control brushless motors in both Arduino and ESP-IDF projects. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. +A C++ library for generating DShot signals on ESP32 microcontrollers using the **modern ESP-IDF 5 RMT encoder API** (`rmt_tx.h` / `rmt_rx.h`). It provides a simple, efficient, and hardware-timed way to control brushless motors in both Arduino and ESP-IDF projects. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. ## ๐Ÿš€ Core Features diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index 619833d..abdb6bd 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -88,7 +88,7 @@ void loop() if (IS_BIDIRECTIONAL) { dshot_result_t telem_result = motor01.getTelemetry(); - DShotRMT::printDShotResult(telem_result); + printDShotResult(telem_result); } USB_SERIAL.println("Type 'help' to show Menu"); @@ -128,7 +128,7 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous throttle = 0; continuous_throttle = true; dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else if (input == "info") { @@ -137,7 +137,7 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous else if (input == "rpm" && IS_BIDIRECTIONAL) { dshot_result_t result = motor01.getTelemetry(); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else if (input.startsWith("cmd ")) { @@ -149,7 +149,7 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else { @@ -171,7 +171,7 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous continuous_throttle = true; dshot_result_t result = motor01.sendThrottle(throttle); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else { diff --git a/examples/throttle_percent/throttle_percent.ino b/examples/throttle_percent/throttle_percent.ino index 13e7f8f..5fb9000 100644 --- a/examples/throttle_percent/throttle_percent.ino +++ b/examples/throttle_percent/throttle_percent.ino @@ -94,7 +94,7 @@ void handleSerialInput(const String &input) { // Stop motor dshot_result_t result = motor01.sendThrottlePercent(0.0f); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else if (input == "info") { @@ -103,7 +103,7 @@ void handleSerialInput(const String &input) else if (input == "rpm" && IS_BIDIRECTIONAL) { dshot_result_t result = motor01.getTelemetry(); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else if (input.startsWith("cmd ")) { @@ -113,7 +113,7 @@ void handleSerialInput(const String &input) if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else { @@ -132,7 +132,7 @@ void handleSerialInput(const String &input) if (throttle_percent >= 0.0f && throttle_percent <= 100.0f) { dshot_result_t result = motor01.sendThrottlePercent(throttle_percent); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else { diff --git a/examples/web_client/web_client.ino b/examples/web_client/web_client.ino index 92e0ca3..9d1ca57 100644 --- a/examples/web_client/web_client.ino +++ b/examples/web_client/web_client.ino @@ -178,7 +178,7 @@ void loop() if (IS_BIDIRECTIONAL && isArmed) { dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - DShotRMT::printDShotResult(telem_result); + printDShotResult(telem_result); } USB_SERIAL.println("Type 'help' to show Menu"); @@ -495,7 +495,7 @@ void handleSerialInput(const String &input) if (isArmed) { dshot_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else { @@ -520,7 +520,7 @@ void handleSerialInput(const String &input) if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else { @@ -581,7 +581,7 @@ void handleSerialInput(const String &input) throttle = 0; continuous_throttle = false; dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); - DShotRMT::printDShotResult(result); + printDShotResult(result); return; } diff --git a/examples/web_control/web_control.ino b/examples/web_control/web_control.ino index 76cc775..6cc773f 100644 --- a/examples/web_control/web_control.ino +++ b/examples/web_control/web_control.ino @@ -146,7 +146,7 @@ void loop() if (IS_BIDIRECTIONAL && isArmed) { dshot_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - DShotRMT::printDShotResult(telem_result); + printDShotResult(telem_result); } USB_SERIAL.println("Type 'help' to show Menu"); @@ -264,7 +264,7 @@ void handleSerialInput(const String &input) if (isArmed) { dshot_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else { @@ -288,7 +288,7 @@ void handleSerialInput(const String &input) if (cmd_num >= DSHOT_CMD_MOTOR_STOP && cmd_num <= DSHOT_CMD_MAX) { dshot_result_t result = motor01.sendCommand(cmd_num); - DShotRMT::printDShotResult(result); + printDShotResult(result); } else { @@ -341,7 +341,7 @@ void handleSerialInput(const String &input) throttle = 0; continuous_throttle = false; dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); - DShotRMT::printDShotResult(result); + printDShotResult(result); return; } diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 04f36f0..25c86bf 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -23,8 +23,8 @@ DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, ui _last_command_timestamp(0), _encoded_frame_value(0), _packet{0}, - _pulse_level(1), // DShot standard: signal is idle-low, so pulses start by going HIGH - _idle_level(0), // DShot standard: signal returns to LOW after the high pulse + _level0(1), // DShot standard: signal is idle-low, so pulses start by going HIGH + _level1(0), // DShot standard: signal returns to LOW after the high pulse _rmt_tx_channel(nullptr), _rmt_rx_channel(nullptr), _dshot_encoder(nullptr), @@ -355,25 +355,9 @@ dshot_result_t DShotRMT::_initRXChannel() 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 - } - }; + rmt_copy_encoder_config_t encoder_config = {}; - if (rmt_new_bytes_encoder(&encoder_config, &_dshot_encoder) != DSHOT_OK) + if (rmt_new_copy_encoder(&encoder_config, &_dshot_encoder) != DSHOT_OK) { return {false, dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED}; } @@ -440,27 +424,50 @@ void DShotRMT::_preCalculateRMTTicks() dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) { // Ensure enough time has passed since the last transmission - if (!_isFrameIntervalElapsed()) + if (!_timer_signal()) { return {true, dshot_msg_code_t::DSHOT_ERROR_NONE}; } - _encoded_frame_value = _buildDShotFrameValue(packet); + rmt_symbol_word_t tx_symbols[DSHOT_BITS_PER_FRAME]; + dshot_result_t result = _encodeDShotFrame(packet, tx_symbols); - // The DShot frame is 16 bits, which is 2 bytes - size_t tx_size_bytes = sizeof(_encoded_frame_value); + if (!result.success) + { + return result; + } - if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, &_encoded_frame_value, tx_size_bytes, &_rmt_tx_config) != DSHOT_OK) + size_t tx_size_bytes = DSHOT_BITS_PER_FRAME * sizeof(rmt_symbol_word_t); + + if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, tx_symbols, tx_size_bytes, &_rmt_tx_config) != DSHOT_OK) { return {false, dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_FAILED}; } - _recordFrameTransmissionTime(); // Reset the timer for the next frame + _timer_reset(); // Reset the timer for the next frame return {true, dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_SUCCESS}; } +// This function needs to be fast, as it generates the RMT symbols just before sending +dshot_result_t IRAM_ATTR DShotRMT::_encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols) +{ + _encoded_frame_value = _buildDShotFrameValue(packet); + for (int i = 0; i < DSHOT_BITS_PER_FRAME; ++i) + { + int bit_position = DSHOT_BITS_PER_FRAME - 1 - i; + bool bit = (_encoded_frame_value >> bit_position) & 1; + + // A '1' bit has a longer high-time, a '0' bit has a shorter high-time + symbols[i].level0 = _level0; // Go HIGH + symbols[i].duration0 = bit ? _rmt_ticks.t1h_ticks : _rmt_ticks.t0h_ticks; + symbols[i].level1 = _level1; // Go LOW + symbols[i].duration1 = bit ? _rmt_ticks.t1l_ticks : _rmt_ticks.t0l_ticks; + } + + return {true, dshot_msg_code_t::DSHOT_ERROR_ENCODING_SUCCESS}; +} // 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) @@ -503,7 +510,7 @@ uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) } // Timing Control Functions -bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed() +bool IRAM_ATTR DShotRMT::_timer_signal() { // Check if the minimum interval between frames has passed uint64_t current_time = esp_timer_get_time(); @@ -511,10 +518,11 @@ bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed() return elapsed >= _frame_timer_us; } -void DShotRMT::_recordFrameTransmissionTime() +bool DShotRMT::_timer_reset() { // Record the time of the current transmission _last_transmission_time_us = esp_timer_get_time(); + return true; } // Static Callback Functions @@ -538,123 +546,14 @@ bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const return false; } -void DShotRMT::printDShotResult(const dshot_result_t &result, Stream &output) -{ - const char *msg_str; - switch (result.error_code) - { - case dshot_msg_code_t::DSHOT_ERROR_NONE: - msg_str = "None"; - break; - case dshot_msg_code_t::DSHOT_ERROR_UNKNOWN: - msg_str = "Unknown Error!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_FAILED: - msg_str = "TX RMT channel init failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_FAILED: - msg_str = "RX RMT channel init failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED: - msg_str = "RMT encoder init failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_CALLBACK_REGISTERING_FAILED: - msg_str = "RMT RX Callback registering failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_RECEIVER_FAILED: - msg_str = "RMT receiver failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_FAILED: - msg_str = "Transmission failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_THROTTLE_NOT_IN_RANGE: - msg_str = "Throttle not in range! (48 - 2047)"; - break; - case dshot_msg_code_t::DSHOT_ERROR_PERCENT_NOT_IN_RANGE: - msg_str = "Percent not in range! (0.0 - 100.0)"; - break; - case dshot_msg_code_t::DSHOT_ERROR_COMMAND_NOT_VALID: - msg_str = "Command not valid! (0 - 47)"; - break; - case dshot_msg_code_t::DSHOT_ERROR_BIDIR_NOT_ENABLED: - msg_str = "Bidirectional DShot not enabled!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_FAILED: - msg_str = "No valid Telemetric Frame received!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_INVALID_MAGNET_COUNT: - msg_str = "Invalid motor magnet count!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_INVALID_COMMAND: - msg_str = "Invalid command!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TIMING_CORRECTION: - msg_str = "Timing correction!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_INIT_FAILED: - msg_str = "SignalGeneratorRMT init failed!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_INIT_SUCCESS: - msg_str = "SignalGeneratorRMT initialized successfully"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_SUCCESS: - msg_str = "TX RMT channel initialized successfully"; - break; - case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_SUCCESS: - msg_str = "RX RMT channel initialized successfully"; - break; - case dshot_msg_code_t::DSHOT_ERROR_ENCODING_SUCCESS: - msg_str = "Packet encoded successfully"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_SUCCESS: - msg_str = "Transmission successfully"; - break; - case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_SUCCESS: - msg_str = "Valid Telemetric Frame received!"; - break; - case dshot_msg_code_t::DSHOT_ERROR_COMMAND_SUCCESS: - msg_str = "DShot command sent successfully"; - break; - default: - msg_str = "Unhandled Error Code!"; - break; - } - output.printf("Status: %s - %s", result.success ? "SUCCESS" : "FAILED", msg_str); - - // Print telemetry data if available - if (result.success && (result.erpm > 0 || result.motor_rpm > 0)) - { - output.printf(" | eRPM: %u, Motor RPM: %u", result.erpm, result.motor_rpm); - } - - output.println(); -} - // Public Static Utility Functions void DShotRMT::printDShotInfo(const DShotRMT &dshot_rmt, Stream &output) { output.println("\n === DShot Signal Info === "); - - int mode_val = 0; - switch (dshot_rmt.getMode()) - { - case dshot_mode_t::DSHOT150: - mode_val = 150; - break; - case dshot_mode_t::DSHOT300: - mode_val = 300; - break; - case dshot_mode_t::DSHOT600: - mode_val = 600; - break; - case dshot_mode_t::DSHOT1200: - mode_val = 1200; - break; - default: - break; - } - output.printf("Current Mode: DSHOT%d\n", mode_val); - + output.printf("Current Mode: DSHOT%d\n", dshot_rmt.getMode() == dshot_mode_t::DSHOT150 ? 150 : dshot_rmt.getMode() == dshot_mode_t::DSHOT300 ? 300 + : dshot_rmt.getMode() == dshot_mode_t::DSHOT600 ? 600 + : dshot_rmt.getMode() == dshot_mode_t::DSHOT1200 ? 1200 + : 0); output.printf("Bidirectional: %s\n", dshot_rmt.isBidirectional() ? "YES" : "NO"); output.printf("Current Packet: "); diff --git a/src/DShotRMT.h b/src/DShotRMT.h index 7a45321..a9cc23c 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -113,13 +113,6 @@ public: dshot_result_t saveESCSettings(); // Public Utility & Info Functions - /** - * @brief Prints the result of a DShot operation to the specified output stream. - * @param result The dshot_result_t structure containing the operation's outcome. - * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. - */ - static void printDShotResult(const dshot_result_t &result, Stream &output = Serial); - /** * @brief Prints detailed DShot signal information for a given DShotRMT instance. * @param dshot_rmt The DShotRMT instance to get information from. @@ -220,8 +213,8 @@ private: uint64_t _last_command_timestamp; uint16_t _encoded_frame_value; dshot_packet_t _packet; - uint16_t _pulse_level; // DShot protocol: Signal is idle-low, so pulses start by going HIGH. - uint16_t _idle_level; // DShot protocol: Signal returns to LOW after the high pulse. + uint16_t _level0; // DShot protocol: Signal is idle-low, so pulses start by going HIGH. + uint16_t _level1; // DShot protocol: Signal returns to LOW after the high pulse. // RMT Hardware Handles rmt_channel_handle_t _rmt_tx_channel; @@ -252,12 +245,12 @@ private: // Private Frame Processing Functions dshot_result_t _sendDShotFrame(const dshot_packet_t &packet); - + dshot_result_t _encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols); uint16_t _decodeDShotFrame(const rmt_symbol_word_t *symbols); // Private Timing Control Functions - bool _isFrameIntervalElapsed(); - void _recordFrameTransmissionTime(); + bool _timer_signal(); + bool _timer_reset(); // 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); diff --git a/src/dshot_definitions.h b/src/dshot_definitions.h index a3b6725..268f342 100644 --- a/src/dshot_definitions.h +++ b/src/dshot_definitions.h @@ -217,3 +217,101 @@ const char *const TIMING_CORRECTION = "Timing correction!"; const char *const CALLBACK_REGISTERING_FAILED = "RMT RX Callback registering failed!"; const char *const INVALID_COMMAND = "Invalid command!"; const char *const COMMAND_SUCCESS = "DShot command sent successfully"; + +// Helper Functions +/** + * @brief Prints the result of a DShot operation to the specified output stream. + * @param result The dshot_result_t structure containing the operation's outcome. + * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. + */ +inline void printDShotResult(dshot_result_t &result, Stream &output = Serial) +{ + const char *msg_str; + switch (result.error_code) + { + case dshot_msg_code_t::DSHOT_ERROR_NONE: + msg_str = "None"; + break; + case dshot_msg_code_t::DSHOT_ERROR_UNKNOWN: + msg_str = "Unknown Error!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_FAILED: + msg_str = "TX RMT channel init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_FAILED: + msg_str = "RX RMT channel init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED: + msg_str = "RMT encoder init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_CALLBACK_REGISTERING_FAILED: + msg_str = "RMT RX Callback registering failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_RECEIVER_FAILED: + msg_str = "RMT receiver failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_FAILED: + msg_str = "Transmission failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_THROTTLE_NOT_IN_RANGE: + msg_str = "Throttle not in range! (48 - 2047)"; + break; + case dshot_msg_code_t::DSHOT_ERROR_PERCENT_NOT_IN_RANGE: + msg_str = "Percent not in range! (0.0 - 100.0)"; + break; + case dshot_msg_code_t::DSHOT_ERROR_COMMAND_NOT_VALID: + msg_str = "Command not valid! (0 - 47)"; + break; + case dshot_msg_code_t::DSHOT_ERROR_BIDIR_NOT_ENABLED: + msg_str = "Bidirectional DShot not enabled!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_FAILED: + msg_str = "No valid Telemetric Frame received!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INVALID_MAGNET_COUNT: + msg_str = "Invalid motor magnet count!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INVALID_COMMAND: + msg_str = "Invalid command!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TIMING_CORRECTION: + msg_str = "Timing correction!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INIT_FAILED: + msg_str = "SignalGeneratorRMT init failed!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_INIT_SUCCESS: + msg_str = "SignalGeneratorRMT initialized successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TX_INIT_SUCCESS: + msg_str = "TX RMT channel initialized successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_RX_INIT_SUCCESS: + msg_str = "RX RMT channel initialized successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_ENCODING_SUCCESS: + msg_str = "Packet encoded successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_SUCCESS: + msg_str = "Transmission successfully"; + break; + case dshot_msg_code_t::DSHOT_ERROR_TELEMETRY_SUCCESS: + msg_str = "Valid Telemetric Frame received!"; + break; + case dshot_msg_code_t::DSHOT_ERROR_COMMAND_SUCCESS: + msg_str = "DShot command sent successfully"; + break; + default: + msg_str = "Unhandled Error Code!"; + break; + } + output.printf("Status: %s - %s", result.success ? "SUCCESS" : "FAILED", msg_str); + + // Print telemetry data if available + if (result.success && (result.erpm > 0 || result.motor_rpm > 0)) + { + output.printf(" | eRPM: %u, Motor RPM: %u", result.erpm, result.motor_rpm); + } + + output.println(); +} \ No newline at end of file From d899ba1471e21516341d40506c69c64a5ac5ae21 Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Fri, 26 Sep 2025 19:24:50 +0200 Subject: [PATCH 7/8] Use official byte-Encoder --- README.md | 2 +- src/DShotRMT.cpp | 65 ++++++++++++++++++++++-------------------------- src/DShotRMT.h | 9 +++---- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 81ea373..370f231 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Arduino CI](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml/badge.svg)](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml) -A C++ library for generating DShot signals on ESP32 microcontrollers using the **modern ESP-IDF 5 RMT encoder API** (`rmt_tx.h` / `rmt_rx.h`). It provides a simple, efficient, and hardware-timed way to control brushless motors in both Arduino and ESP-IDF projects. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. +A C++ library for generating DShot signals on ESP32 microcontrollers using the **modern ESP-IDF 5 RMT encoder API** (`rmt_tx.h` / `rmt_rx.h`). It leverages the standard `rmt_bytes_encoder` to ensure an efficient, hardware-timed, and maintainable implementation. **Note:** A byte-swapping fix has been implemented to address endianness differences when using `rmt_bytes_encoder` on ESP32. The library provides a simple way to control brushless motors in both Arduino and ESP-IDF projects. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. ## ๐Ÿš€ Core Features diff --git a/src/DShotRMT.cpp b/src/DShotRMT.cpp index 25c86bf..964342e 100644 --- a/src/DShotRMT.cpp +++ b/src/DShotRMT.cpp @@ -23,8 +23,8 @@ DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, ui _last_command_timestamp(0), _encoded_frame_value(0), _packet{0}, - _level0(1), // DShot standard: signal is idle-low, so pulses start by going HIGH - _level1(0), // DShot standard: signal returns to LOW after the high pulse + _pulse_level(1), // DShot standard: signal is idle-low, so pulses start by going HIGH + _idle_level(0), // DShot standard: signal returns to LOW after the high pulse _rmt_tx_channel(nullptr), _rmt_rx_channel(nullptr), _dshot_encoder(nullptr), @@ -355,9 +355,25 @@ dshot_result_t DShotRMT::_initRXChannel() dshot_result_t DShotRMT::_initDShotEncoder() { - rmt_copy_encoder_config_t encoder_config = {}; + 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_copy_encoder(&encoder_config, &_dshot_encoder) != DSHOT_OK) + if (rmt_new_bytes_encoder(&encoder_config, &_dshot_encoder) != DSHOT_OK) { return {false, dshot_msg_code_t::DSHOT_ERROR_ENCODER_INIT_FAILED}; } @@ -424,50 +440,30 @@ void DShotRMT::_preCalculateRMTTicks() dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) { // Ensure enough time has passed since the last transmission - if (!_timer_signal()) + if (!_isFrameIntervalElapsed()) { return {true, dshot_msg_code_t::DSHOT_ERROR_NONE}; } - rmt_symbol_word_t tx_symbols[DSHOT_BITS_PER_FRAME]; - dshot_result_t result = _encodeDShotFrame(packet, tx_symbols); + _encoded_frame_value = _buildDShotFrameValue(packet); - if (!result.success) - { - return result; - } + // Byte-swap the 16-bit value for correct transmission order (ESP32 is little-endian, DShot is MSB first) + uint16_t swapped_value = __builtin_bswap16(_encoded_frame_value); - size_t tx_size_bytes = DSHOT_BITS_PER_FRAME * sizeof(rmt_symbol_word_t); + // 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, tx_symbols, tx_size_bytes, &_rmt_tx_config) != DSHOT_OK) + if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, &swapped_value, tx_size_bytes, &_rmt_tx_config) != DSHOT_OK) { return {false, dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_FAILED}; } - _timer_reset(); // Reset the timer for the next frame + _recordFrameTransmissionTime(); // Reset the timer for the next frame return {true, dshot_msg_code_t::DSHOT_ERROR_TRANSMISSION_SUCCESS}; } // This function needs to be fast, as it generates the RMT symbols just before sending -dshot_result_t IRAM_ATTR DShotRMT::_encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols) -{ - _encoded_frame_value = _buildDShotFrameValue(packet); - - for (int i = 0; i < DSHOT_BITS_PER_FRAME; ++i) - { - int bit_position = DSHOT_BITS_PER_FRAME - 1 - i; - bool bit = (_encoded_frame_value >> bit_position) & 1; - - // A '1' bit has a longer high-time, a '0' bit has a shorter high-time - symbols[i].level0 = _level0; // Go HIGH - symbols[i].duration0 = bit ? _rmt_ticks.t1h_ticks : _rmt_ticks.t0h_ticks; - symbols[i].level1 = _level1; // Go LOW - symbols[i].duration1 = bit ? _rmt_ticks.t1l_ticks : _rmt_ticks.t0l_ticks; - } - - return {true, dshot_msg_code_t::DSHOT_ERROR_ENCODING_SUCCESS}; -} // 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) @@ -510,7 +506,7 @@ uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) } // Timing Control Functions -bool IRAM_ATTR DShotRMT::_timer_signal() +bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed() { // Check if the minimum interval between frames has passed uint64_t current_time = esp_timer_get_time(); @@ -518,11 +514,10 @@ bool IRAM_ATTR DShotRMT::_timer_signal() return elapsed >= _frame_timer_us; } -bool DShotRMT::_timer_reset() +void DShotRMT::_recordFrameTransmissionTime() { // Record the time of the current transmission _last_transmission_time_us = esp_timer_get_time(); - return true; } // Static Callback Functions diff --git a/src/DShotRMT.h b/src/DShotRMT.h index a9cc23c..4117245 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -213,8 +213,8 @@ private: uint64_t _last_command_timestamp; uint16_t _encoded_frame_value; dshot_packet_t _packet; - uint16_t _level0; // DShot protocol: Signal is idle-low, so pulses start by going HIGH. - uint16_t _level1; // DShot protocol: Signal returns to LOW after the high pulse. + uint16_t _pulse_level; // DShot protocol: Signal is idle-low, so pulses start by going HIGH. + uint16_t _idle_level; // DShot protocol: Signal returns to LOW after the high pulse. // RMT Hardware Handles rmt_channel_handle_t _rmt_tx_channel; @@ -245,12 +245,11 @@ private: // Private Frame Processing Functions dshot_result_t _sendDShotFrame(const dshot_packet_t &packet); - dshot_result_t _encodeDShotFrame(const dshot_packet_t &packet, rmt_symbol_word_t *symbols); uint16_t _decodeDShotFrame(const rmt_symbol_word_t *symbols); // Private Timing Control Functions - bool _timer_signal(); - bool _timer_reset(); + bool _isFrameIntervalElapsed(); + 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); From eb5b469dd003a6a6b21f0c8c39c3acad08f8db06 Mon Sep 17 00:00:00 2001 From: Wastl Kraus Date: Sat, 27 Sep 2025 14:59:23 +0200 Subject: [PATCH 8/8] prepare release 0.8.6 --- DShotRMT.h | 2 +- README.md | 46 ++++++++++--- src/DShotRMT.h | 140 ++++++++-------------------------------- src/dshot_definitions.h | 75 +++++++-------------- src/web_content.h | 2 +- 5 files changed, 88 insertions(+), 177 deletions(-) diff --git a/DShotRMT.h b/DShotRMT.h index 367f706..29d290d 100644 --- a/DShotRMT.h +++ b/DShotRMT.h @@ -6,5 +6,5 @@ * @license MIT */ - #include "src/DShotRMT.h" +#include "src/DShotRMT.h" \ No newline at end of file diff --git a/README.md b/README.md index 370f231..4186cf2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,13 @@ [![Arduino CI](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml/badge.svg)](https://github.com/derdoktor667/DShotRMT/actions/workflows/ci.yml) -A C++ library for generating DShot signals on ESP32 microcontrollers using the **modern ESP-IDF 5 RMT encoder API** (`rmt_tx.h` / `rmt_rx.h`). It leverages the standard `rmt_bytes_encoder` to ensure an efficient, hardware-timed, and maintainable implementation. **Note:** A byte-swapping fix has been implemented to address endianness differences when using `rmt_bytes_encoder` on ESP32. The library provides a simple way to control brushless motors in both Arduino and ESP-IDF projects. The legacy version using the old `rmt.h` API is available in the `oldAPI` branch. +A C++ 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 brushless motors 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: + +![DShot300 Example Output](img/dshot300.png) ## ๐Ÿš€ Core Features @@ -14,6 +20,17 @@ A C++ library for generating DShot signals on ESP32 microcontrollers using the * - **Efficient and Lightweight:** The core library has no external dependencies. - **Arduino and ESP-IDF Compatible:** Can be used in both Arduino and ESP-IDF projects. +## โฑ๏ธ 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: + +| DShot Mode | Bit Length (ยตs) | T1H Length (ยตs) | T0H Length (ยตs) | Frame Length (ยตs) | +| :--------- | :-------------- | :-------------- | :-------------- | :---------------- | +| DSHOT150 | 6.67 | 5.00 | 2.50 | 106.72 | +| DSHOT300 | 3.33 | 2.50 | 1.25 | 53.28 | +| DSHOT600 | 1.67 | 1.25 | 0.625 | 26.72 | +| DSHOT1200 | 0.83 | 0.67 | 0.335 | 13.28 | + ## ๐Ÿ“ฆ Installation ### Arduino IDE @@ -105,15 +122,24 @@ lib_deps = The main class is `DShotRMT`. Here are the most important methods: -- `DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT)`: Constructor to create a new DShotRMT instance. (Note: Bidirectional DShot is currently not officially supported.) -- `begin()`: Initializes the RMT peripheral and the DShot encoder. -- `sendThrottlePercent(float percent)`: Sends a throttle value as a percentage (0.0-100.0). -- `sendThrottle(uint16_t throttle)`: Sends a raw throttle value (48-2047) to the motor. -- `sendCommand(uint16_t command)`: Sends a DShot command (0-47) to the motor. -- `getTelemetry(uint16_t magnet_count)`: Receives and parses telemetry data from the motor (for bidirectional DShot, which is currently not officially supported). -- `printDShotResult(dshot_result_t &result, Stream &output = Serial)`: Helper function to print DShot operation results and telemetry to a specified serial output. -- `DShotRMT::printDShotInfo(const DShotRMT &dshot_rmt, Stream &output = Serial)`: Static helper function to print detailed DShot signal information for a given DShotRMT instance. -- `DShotRMT::printCpuInfo(Stream &output = Serial)`: Static helper function to print detailed CPU information. +- `DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT)`: Constructor to create a new DShotRMT instance. +- `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. +- `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. +- `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. +- `DShotRMT::printDShotInfo(const DShotRMT &dshot_rmt, Stream &output = Serial)`: Prints detailed DShot signal information for a given DShotRMT instance. +- `DShotRMT::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. ## ๐Ÿค Contributing diff --git a/src/DShotRMT.h b/src/DShotRMT.h index 4117245..9505881 100644 --- a/src/DShotRMT.h +++ b/src/DShotRMT.h @@ -15,153 +15,75 @@ #include #include -/** - * @brief DShotRMT Main Class for DShot signal generation and reception. - * - * This class provides an interface to generate DShot signals for Electronic Speed Controllers (ESCs) - * and to receive telemetry data using the ESP32's RMT peripheral. - */ +// Main class for DShot signal generation and reception. +// This class provides an interface to generate DShot signals for Electronic Speed Controllers (ESCs) +// and to receive telemetry data using the ESP32's RMT peripheral. class DShotRMT { public: - /** - * @brief Constructor for DShotRMT with GPIO number. - * @param gpio The GPIO pin number to use for DShot communication. - * @param mode The DShot mode (e.g., DSHOT150, DSHOT300, DSHOT600). - * @param is_bidirectional True if bidirectional DShot is enabled, false otherwise. - * @param magnet_count The number of magnets in the motor for RPM calculation. - */ + // Constructor for DShotRMT with GPIO number. explicit DShotRMT(gpio_num_t gpio = GPIO_NUM_16, dshot_mode_t mode = dshot_mode_t::DSHOT300, bool is_bidirectional = false, uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); - /** - * @brief Constructor for DShotRMT with Arduino pin number. - * @param pin_nr The Arduino pin number to use for DShot communication. - * @param mode The DShot mode (e.g., DSHOT150, DSHOT300, DSHOT600). - * @param is_bidirectional True if bidirectional DShot is enabled, false otherwise. - * @param magnet_count The number of magnets in the motor for RPM calculation. - */ + // 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); - /** - * @brief Destructor for DShotRMT. - * Cleans up RMT channels and encoder resources. - */ + // Destructor for DShotRMT. + // Cleans up RMT channels and encoder resources. ~DShotRMT(); // Public Core Functions - /** - * @brief Initializes the DShot RMT channels and encoder. - * @return dshot_result_t indicating success or failure of the initialization. - */ + // Initializes the DShot RMT channels and encoder. dshot_result_t begin(); - /** - * @brief Sends a DShot throttle value to the ESC. - * @param throttle The throttle value (48-2047). A value of 0 sends a motor stop command. - * @return dshot_result_t indicating success or failure of the transmission. - */ + // Sends a DShot throttle value to the ESC. dshot_result_t sendThrottle(uint16_t throttle); - /** - * @brief Sends a DShot throttle value as a percentage to the ESC. - * @param percent The throttle percentage (0.0f - 100.0f). - * @return dshot_result_t indicating success or failure of the transmission. - */ + // Sends a DShot throttle value as a percentage to the ESC. dshot_result_t sendThrottlePercent(float percent); - /** - * @brief Sends a single DShot command to the ESC. - * @param command The DShot command value (0-47). - * @return dshot_result_t indicating success or failure of the transmission. - */ + // Sends a single DShot command to the ESC. dshot_result_t sendCommand(uint16_t command); - /** - * @brief Sends a DShot command multiple times with a delay between repetitions. This is a blocking function. - * @param dshot_command The DShot command to send. - * @param repeat_count The number of times to repeat the command. - * @param delay_us The delay in microseconds between repetitions. - * @return dshot_result_t indicating success or failure of the transmission. - */ + // Sends a DShot command multiple times with a delay between repetitions. This is a blocking function. dshot_result_t sendCommand(dshotCommands_e dshot_command, uint16_t repeat_count = DEFAULT_CMD_REPEAT_COUNT, uint16_t delay_us = DEFAULT_CMD_DELAY_US); - /** - * @brief Retrieves telemetry data from the ESC. - * @param magnet_count The number of magnets in the motor. If 0, uses the stored motor_magnet_count. - * @return dshot_result_t containing telemetry data (eRPM, motor RPM) if successful. - */ + // Retrieves telemetry data from the ESC. dshot_result_t getTelemetry(uint16_t magnet_count = 0); - /** - * @brief Sends a command to the ESC to request ESC information. - * @return dshot_result_t indicating success or failure of the command transmission. - */ + // Sends a command to the ESC to request ESC information. dshot_result_t getESCInfo(); - /** - * @brief Sets the motor spin direction. - * @param reversed True for reversed direction, false for normal. - * @return dshot_result_t indicating success or failure of the command transmission. - */ + // Sets the motor spin direction. dshot_result_t setMotorSpinDirection(bool reversed); - /** - * @brief Sends a command to the ESC to save its current settings. - * Use with caution as this writes to ESC's non-volatile memory. - * @return dshot_result_t indicating success or failure of the command transmission. - */ + // Sends a command to the ESC to save its current settings. + // Use with caution as this writes to ESC's non-volatile memory. dshot_result_t saveESCSettings(); // Public Utility & Info Functions - /** - * @brief Prints detailed DShot signal information for a given DShotRMT instance. - * @param dshot_rmt The DShotRMT instance to get information from. - * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. - */ + // Prints detailed DShot signal information for a given DShotRMT instance. static void printDShotInfo(const DShotRMT &dshot_rmt, Stream &output = Serial); - /** - * @brief Prints detailed CPU information. - * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. - */ + // Prints detailed CPU information. static void printCpuInfo(Stream &output = Serial); - /** - * @brief Sets the motor magnet count for RPM calculation. - * @param magnet_count The number of magnets in the motor. - */ + // Sets the motor magnet count for RPM calculation. void setMotorMagnetCount(uint16_t magnet_count); - /** - * @brief Gets the current DShot mode. - * @return The current dshot_mode_t. - */ + // Gets the current DShot mode. dshot_mode_t getMode() const { return _mode; } - /** - * @brief Checks if bidirectional DShot is enabled. - * @return True if bidirectional DShot is enabled, false otherwise. - */ + // Checks if bidirectional DShot is enabled. bool isBidirectional() const { return _is_bidirectional; } - /** - * @brief Gets the last encoded DShot frame value. - * @return The 16-bit encoded DShot frame value. - */ + // Gets the last encoded DShot frame value. uint16_t getEncodedFrameValue() const { return _encoded_frame_value; } - /** - * @brief Gets the last transmitted throttle value. - * @return The last transmitted throttle value. - */ + // Gets the last transmitted throttle value. uint16_t getThrottleValue() const { return _packet.throttle_value; } // Deprecated Methods - /** - * @brief Deprecated. Use sendThrottle() instead. - * @param throttle The throttle value. - * @return True on success, false on failure. - */ + // Deprecated. Use sendThrottle() instead. [[deprecated("Use sendThrottle() instead")]] bool setThrottle(uint16_t throttle) { @@ -169,11 +91,7 @@ public: return result.success; } - /** - * @brief Deprecated. Use sendCommand() instead. - * @param command The DShot command. - * @return True on success, false on failure. - */ + // Deprecated. Use sendCommand() instead. [[deprecated("Use sendCommand() instead")]] bool sendDShotCommand(uint16_t command) { @@ -181,11 +99,7 @@ public: return result.success; } - /** - * @brief Deprecated. Use getTelemetry() instead. - * @param magnet_count The number of magnets in the motor. - * @return The motor RPM. - */ + // Deprecated. Use getTelemetry() instead. [[deprecated("Use getTelemetry() instead")]] uint32_t getMotorRPM(uint8_t magnet_count) { diff --git a/src/dshot_definitions.h b/src/dshot_definitions.h index 268f342..b5f1877 100644 --- a/src/dshot_definitions.h +++ b/src/dshot_definitions.h @@ -6,10 +6,7 @@ #include #include // Added for std::atomic -/** - * @brief DShot Modes - * Defines the available DShot communication speeds. - */ +// Defines the available DShot communication speeds. enum class dshot_mode_t { DSHOT_OFF, @@ -19,44 +16,32 @@ enum class dshot_mode_t DSHOT1200 }; -/** - * @brief DShot Packet Structure - * Represents the 16-bit DShot data packet sent to the ESC. - */ +// Represents the 16-bit DShot data packet sent to the ESC. typedef struct dshot_packet { - uint16_t throttle_value : 11; ///< 11-bit throttle value or command. - bool telemetric_request : 1; ///< 1-bit telemetry request flag. - uint16_t checksum : 4; ///< 4-bit CRC checksum. + uint16_t throttle_value : 11; // 11-bit throttle value or command. + bool telemetric_request : 1; // 1-bit telemetry request flag. + uint16_t checksum : 4; // 4-bit CRC checksum. } dshot_packet_t; -/** - * @brief DShot Timing Configuration - * Defines the bit length and high time for a '1' bit in microseconds for each DShot mode. - */ +// Defines the bit length and high time for a '1' bit in microseconds for each DShot mode. typedef struct dshot_timing { - double bit_length_us; ///< Total duration of one bit in microseconds. - double t1h_lenght_us; ///< High time duration for a '1' bit in microseconds. + double bit_length_us; // Total duration of one bit in microseconds. + double t1h_lenght_us; // High time duration for a '1' bit in microseconds. } dshot_timing_us_t; -/** - * @brief RMT Timing Configuration - * Stores pre-calculated timing values in RMT ticks for efficient signal generation. - */ +// Stores pre-calculated timing values in RMT ticks for efficient signal generation. typedef struct rmt_ticks { - uint16_t bit_length_ticks; ///< Total duration of one bit in RMT ticks. - uint16_t t1h_ticks; ///< High time duration for a '1' bit in RMT ticks. - uint16_t t1l_ticks; ///< Low time duration for a '1' bit in RMT ticks. - uint16_t t0h_ticks; ///< High time duration for a '0' bit in RMT ticks. - uint16_t t0l_ticks; ///< Low time duration for a '0' bit in RMT ticks. + uint16_t bit_length_ticks; // Total duration of one bit in RMT ticks. + uint16_t t1h_ticks; // High time duration for a '1' bit in RMT ticks. + uint16_t t1l_ticks; // Low time duration for a '1' bit in RMT ticks. + uint16_t t0h_ticks; // High time duration for a '0' bit in RMT ticks. + uint16_t t0l_ticks; // Low time duration for a '0' bit in RMT ticks. } rmt_ticks_t; -/** - * @brief DShot Error Codes - * Enum class for specific error and success codes returned by DShotRMT functions. - */ +// Enum class for specific error and success codes returned by DShotRMT functions. enum class dshot_msg_code_t { DSHOT_ERROR_NONE = 0, @@ -86,22 +71,16 @@ enum class dshot_msg_code_t DSHOT_ERROR_COMMAND_SUCCESS }; -/** - * @brief Unified DShot Result Structure - * Contains the success status, an error code, and optional telemetry data. - */ +// Contains the success status, an error code, and optional telemetry data. typedef struct dshot_result { bool success; - dshot_msg_code_t error_code; ///< Specific error or success code. - uint16_t erpm; ///< Electrical RPM (eRPM) if telemetry is successful. - uint16_t motor_rpm; ///< Motor RPM if telemetry is successful and magnet count is known. + dshot_msg_code_t error_code; // Specific error or success code. + uint16_t erpm; // Electrical RPM (eRPM) if telemetry is successful. + uint16_t motor_rpm; // Motor RPM if telemetry is successful and magnet count is known. } dshot_result_t; -/** - * @brief DShot Commands - * Enum class for standard DShot commands that can be sent to an ESC. - */ +// Enum class for standard DShot commands that can be sent to an ESC. enum dshotCommands_e { DSHOT_CMD_MOTOR_STOP = 0, @@ -134,14 +113,11 @@ enum dshotCommands_e DSHOT_CMD_MAX = 47 }; -/** - * @brief DShot Command Type Enum - * Defines how DShot commands are sent. - */ +// Defines how DShot commands are sent. enum class dshotCommandType_e { - DSHOT_CMD_TYPE_INLINE = 0, ///< Commands sent inline with motor signal (motors must be enabled). - DSHOT_CMD_TYPE_BLOCKING ///< Commands sent in blocking method (motors must be disabled). + DSHOT_CMD_TYPE_INLINE = 0, // Commands sent inline with motor signal (motors must be enabled). + DSHOT_CMD_TYPE_BLOCKING // Commands sent in blocking method (motors must be disabled). }; // DShot Protocol Constants @@ -219,11 +195,6 @@ const char *const INVALID_COMMAND = "Invalid command!"; const char *const COMMAND_SUCCESS = "DShot command sent successfully"; // Helper Functions -/** - * @brief Prints the result of a DShot operation to the specified output stream. - * @param result The dshot_result_t structure containing the operation's outcome. - * @param output The output stream (e.g., Serial) to print to. Defaults to Serial. - */ inline void printDShotResult(dshot_result_t &result, Stream &output = Serial) { const char *msg_str; diff --git a/src/web_content.h b/src/web_content.h index 104adcb..2b8130d 100644 --- a/src/web_content.h +++ b/src/web_content.h @@ -9,7 +9,7 @@ #pragma once // Web Site Content -static constexpr char index_html[] = R"rawliteral( +const char *index_html = R"rawliteral(