diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76f1123..2cfda7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -env: - ESP32_CORE_VERSION: '3.3.0' - jobs: # ============================================================================ # Code Quality & Linting @@ -33,15 +30,15 @@ jobs: uses: actions/cache@v4 with: path: | - ~/.arduino15 - key: ${{ runner.os }}-arduino-${{ env.ESP32_CORE_VERSION }}-${{ hashFiles('**/libraries/**') }} + ~/ + key: ${{ runner.os }}-arduino-${{ hashFiles('**/libraries/**') }} restore-keys: | - ${{ runner.os }}-arduino-${{ env.ESP32_CORE_VERSION }}- + ${{ runner.os }}-arduino- - name: Install ESP32 core run: | arduino-cli core update-index - arduino-cli core install esp32:esp32@${{ env.ESP32_CORE_VERSION }} + arduino-cli core install esp32:esp32 - name: Arduino Lint uses: arduino/arduino-lint-action@v1 @@ -79,15 +76,15 @@ jobs: uses: actions/cache@v4 with: path: | - ~/.arduino15 - key: ${{ runner.os }}-arduino-${{ env.ESP32_CORE_VERSION }}-${{ hashFiles('**/libraries/**') }} + ~/ + key: ${{ runner.os }}-arduino-${{ hashFiles('**/libraries/**') }} restore-keys: | - ${{ runner.os }}-arduino-${{ env.ESP32_CORE_VERSION }}- + ${{ runner.os }}-arduino- - name: Install ESP32 core run: | arduino-cli core update-index - arduino-cli core install esp32:esp32@${{ env.ESP32_CORE_VERSION }} + arduino-cli core install esp32:esp32 - name: Install Repo as Library run: | @@ -117,15 +114,15 @@ jobs: uses: actions/cache@v4 with: path: | - ~/.arduino15 - key: ${{ runner.os }}-arduino-${{ env.ESP32_CORE_VERSION }}-${{ hashFiles('**/libraries/**') }} + ~/ + key: ${{ runner.os }}-arduino-${{ hashFiles('**/libraries/**') }} restore-keys: | - ${{ runner.os }}-arduino-${{ env.ESP32_CORE_VERSION }}- + ${{ runner.os }}-arduino- - name: Install ESP32 core run: | arduino-cli core update-index - arduino-cli core install esp32:esp32@${{ env.ESP32_CORE_VERSION }} + arduino-cli core install esp32:esp32 - name: Install Cppcheck run: sudo apt-get update && sudo apt-get install -y cppcheck diff --git a/DShotCommandManager.cpp b/DShotCommandManager.cpp index 9391640..ac5cb17 100644 --- a/DShotCommandManager.cpp +++ b/DShotCommandManager.cpp @@ -22,7 +22,7 @@ dshot_result_t DShotCommandManager::begin() { dshot_result_t result; result.success = true; - result.error_message = "Success"; + result.msg = "Success"; return result; } @@ -36,10 +36,10 @@ dshot_result_t DShotCommandManager::sendCommand(dshot_commands_t command, uint16 dshot_result_t DShotCommandManager::sendCommandWithDelay(dshot_commands_t command, uint16_t repeat_count, uint32_t delay_ms) { dshot_result_t result = {false, "Unknown error"}; - + if (!isValidCommand(command)) { - result.error_message = "Invalid command"; + result.msg = "Invalid command"; return result; } @@ -53,7 +53,7 @@ dshot_result_t DShotCommandManager::sendCommandWithDelay(dshot_commands_t comman if (!single_result.success) { all_successful = false; - result.error_message = single_result.error_message; + result.msg = single_result.msg; break; } @@ -64,12 +64,12 @@ dshot_result_t DShotCommandManager::sendCommandWithDelay(dshot_commands_t comman } } - // + // result.success = all_successful; if (result.success) { - result.error_message = "Success"; + result.msg = "Success"; } return result; @@ -194,7 +194,7 @@ dshot_result_t DShotCommandManager::setSilentMode(bool enable) } // --- SEQUENCE COMMANDS --- -dshot_result_t DShotCommandManager::executeSequence(const dshot_command_sequence_item_t *sequence, size_t sequence_length) +dshot_result_t DShotCommandManager::executeSequence(const dshot_commandmanager_item_t *sequence, size_t sequence_length) { dshot_result_t result = {true, "Success"}; uint64_t total_start_time = esp_timer_get_time(); @@ -209,7 +209,7 @@ dshot_result_t DShotCommandManager::executeSequence(const dshot_command_sequence if (!item_result.success) { result.success = false; - result.error_message = item_result.error_message; + result.msg = item_result.msg; break; } @@ -229,7 +229,7 @@ dshot_result_t DShotCommandManager::executeSequence(const dshot_command_sequence dshot_result_t DShotCommandManager::executeInitSequence() { // Basic ESC initialization sequence - dshot_command_sequence_item_t init_sequence[] = { + dshot_commandmanager_item_t init_sequence[] = { {DSHOT_CMD_MOTOR_STOP, 5, 100}, // Stop motor, repeat 5 times, wait 100ms {DSHOT_CMD_EXTENDED_TELEMETRY_ENABLE, 1, 50}, // Enable telemetry, wait 50ms {DSHOT_CMD_ESC_INFO, 1, 100} // Request ESC info, wait 100ms @@ -242,7 +242,7 @@ dshot_result_t DShotCommandManager::executeInitSequence() dshot_result_t DShotCommandManager::executeCalibrationSequence() { // Basic ESC calibration sequence - dshot_command_sequence_item_t calibration_sequence[] = { + dshot_commandmanager_item_t calibration_sequence[] = { {DSHOT_CMD_MOTOR_STOP, 10, 500}, // Ensure motor is stopped {DSHOT_CMD_SPIN_DIRECTION_NORMAL, 10, 100}, // Set normal spin direction {DSHOT_CMD_3D_MODE_OFF, 10, 100}, // Disable 3D mode diff --git a/DShotCommandManager.h b/DShotCommandManager.h index aa47ff1..1c7dcff 100644 --- a/DShotCommandManager.h +++ b/DShotCommandManager.h @@ -12,13 +12,13 @@ #include #include -// Command sequence item +// Command item typedef struct { dshot_commands_t command; uint16_t repeat_count; uint32_t delay_ms; -} dshot_command_sequence_item_t; +} dshot_commandmanager_item_t; // Advanced DShot command manager class class DShotCommandManager @@ -76,7 +76,7 @@ public: // --- SEQUENCE COMMANDS --- // Execute a sequence of DShot commands - dshot_result_t executeSequence(const dshot_command_sequence_item_t *sequence, size_t sequence_length); + dshot_result_t executeSequence(const dshot_commandmanager_item_t *sequence, size_t sequence_length); // Execute ESC initialization sequence dshot_result_t executeInitSequence(); diff --git a/DShotRMT.cpp b/DShotRMT.cpp index 823e67e..edcbd13 100644 --- a/DShotRMT.cpp +++ b/DShotRMT.cpp @@ -23,25 +23,27 @@ DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional) : _gpio(gpio), _mode(mode), _is_bidirectional(is_bidirectional), + _last_erpm_atomic(0), + _telemetry_ready_flag(false), _frame_timer_us(0), _timing_config(DSHOT_TIMINGS[mode]), + _last_throttle(DSHOT_CMD_MOTOR_STOP), _last_transmission_time(0), - _last_erpm(0), _parsed_packet(0), _packet{0}, - _total_transmissions(0), - _failed_transmissions(0), + _bitPositions{0}, + _level0(_is_bidirectional ? 0 : 1), + _level1(_is_bidirectional ? 1 : 0), _rmt_tx_channel(nullptr), _rmt_rx_channel(nullptr), _dshot_encoder(nullptr), _tx_channel_config{}, _rx_channel_config{}, _transmit_config{}, - _receive_config{}, - _rx_queue(nullptr) + _receive_config{} { // Calculate frame timing including switch/pause time - _frame_timer_us = _timing_config.frame_length_us + DSHOT_SWITCH_TIME; + _frame_timer_us = _timing_config.frame_length_us + DSHOT_PAUSE_US; // Double frame time for bidirectional mode (includes response time) if (_is_bidirectional) @@ -60,79 +62,66 @@ DShotRMT::DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional) // Destructor for "better" code DShotRMT::~DShotRMT() { - // ...kill them all + // ...TX if (_rmt_tx_channel) { - rmt_disable(_rmt_tx_channel); - rmt_del_channel(_rmt_tx_channel); - _rmt_tx_channel = nullptr; + if (rmt_disable(_rmt_tx_channel) == DSHOT_OK) + { + rmt_del_channel(_rmt_tx_channel); + _rmt_tx_channel = nullptr; + } } - // + // ...RX if (_rmt_rx_channel) { - rmt_disable(_rmt_rx_channel); - rmt_del_channel(_rmt_rx_channel); - _rmt_rx_channel = nullptr; + if (rmt_disable(_rmt_rx_channel) == DSHOT_OK) + { + rmt_del_channel(_rmt_rx_channel); + _rmt_rx_channel = nullptr; + } } - // + // ...Encoder if (_dshot_encoder) { rmt_del_encoder(_dshot_encoder); _dshot_encoder = nullptr; } - - // - if (_rx_queue) - { - vQueueDelete(_rx_queue); - _rx_queue = nullptr; - } } -// Initialize DShotRMT +// Init DShotRMT dshot_result_t DShotRMT::begin() { - dshot_result_t result = {false, UNKNOWN_ERROR}; - uint64_t start_time = esp_timer_get_time(); - // Init RX channel first if (_is_bidirectional) { - if (!_initRXChannel()) + if (!_initRXChannel().success) { - result.error_message = RX_INIT_FAILED; - _dshot_log(RX_INIT_FAILED); - return result; + return {false, RX_INIT_FAILED}; } } // Init TX channel - if (!_initTXChannel()) + if (!_initTXChannel().success) { - result.error_message = TX_INIT_FAILED; - _dshot_log(TX_INIT_FAILED); - return result; + return {false, TX_INIT_FAILED}; } // Init DShot encoder - if (_initDShotEncoder() != DSHOT_OK) + if (!_initDShotEncoder().success) { - result.error_message = ENCODER_INIT_FAILED; - _dshot_log(ENCODER_INIT_FAILED); - return result; + return {false, ENCODER_INIT_FAILED}; } - uint64_t end_time = esp_timer_get_time(); - result.success = true; - result.error_message = INIT_SUCCESS; + // Bit positions precalculation + _preCalculateBitPositions(); - return result; + return {true, INIT_SUCCESS}; } // Init RMT TX channel -bool DShotRMT::_initTXChannel() +dshot_result_t DShotRMT::_initTXChannel() { // Configure TX channel _tx_channel_config.gpio_num = _gpio; @@ -148,22 +137,24 @@ bool DShotRMT::_initTXChannel() // Create RMT TX channel if (rmt_new_tx_channel(&_tx_channel_config, &_rmt_tx_channel) != DSHOT_OK) { - _dshot_log(TX_INIT_FAILED); - return DSHOT_ERROR; + return {false, TX_INIT_FAILED}; } - return (rmt_enable(_rmt_tx_channel) == DSHOT_OK); + // + if (rmt_enable(_rmt_tx_channel) != DSHOT_OK) + { + return {false, TX_INIT_FAILED}; + } + + return {true, TX_INIT_SUCCESS}; } // Init RMT RX channel -bool DShotRMT::_initRXChannel() +dshot_result_t DShotRMT::_initRXChannel() { - // Create a queue for RX callback data - _rx_queue = xQueueCreate(RMT_QUEUE_DEPTH, sizeof(rmt_rx_done_event_data_t)); - if (_rx_queue == nullptr) - { - return DSHOT_ERROR; - } + + // Direct RMT symbol processing - Performance optimized + _rx_event_callbacks.on_recv_done = _rmt_rx_done_callback; // Config RMT RX _rx_channel_config.gpio_num = _gpio; @@ -178,37 +169,43 @@ bool DShotRMT::_initRXChannel() // Create RMT RX channel if (rmt_new_rx_channel(&_rx_channel_config, &_rmt_rx_channel) != DSHOT_OK) { - _dshot_log(RX_INIT_FAILED); - return DSHOT_ERROR; + return {false, RX_INIT_FAILED}; } - // Register RX callback - _rx_event_callbacks.on_recv_done = _rmt_rx_done_callback; - - if (rmt_rx_register_event_callbacks(_rmt_rx_channel, &_rx_event_callbacks, _rx_queue) != DSHOT_OK) + // + if (rmt_enable(_rmt_rx_channel) != DSHOT_OK) { - _dshot_log(RX_INIT_FAILED); - return DSHOT_ERROR; + return {false, RX_INIT_FAILED}; } - return (rmt_enable(_rmt_rx_channel) == DSHOT_OK); + return {true, RX_INIT_SUCCESS}; } // Callback for RMT RX bool IRAM_ATTR DShotRMT::_rmt_rx_done_callback(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data) { - // Init RX buffer - QueueHandle_t rx_queue = (QueueHandle_t)user_data; - BaseType_t xHigherPriorityTaskWoken = pdFALSE; + DShotRMT *instance = static_cast(user_data); - // Copy callback data into RX buffer - xQueueGenericSendFromISR(rx_queue, edata, &xHigherPriorityTaskWoken, queueSEND_TO_BACK); + // ISR check for valid data + if (edata && edata->num_symbols >= GCR_BITS_PER_FRAME && edata->num_symbols <= GCR_BITS_PER_FRAME) + { - return (xHigherPriorityTaskWoken == pdTRUE); + // Direct decoding + uint16_t erpm = instance->_decodeDShotFrame(edata->received_symbols); + + if (erpm != DSHOT_NULL_PACKET) + { + // Atomic writes - thread-safe + instance->_last_erpm_atomic = erpm; + instance->_telemetry_ready_flag = true; + } + } + + return false; } // Initialize DShot encoder -bool DShotRMT::_initDShotEncoder() +dshot_result_t DShotRMT::_initDShotEncoder() { // Create copy encoder configuration rmt_copy_encoder_config_t encoder_config = {}; @@ -216,37 +213,27 @@ bool DShotRMT::_initDShotEncoder() // Create encoder instance if (rmt_new_copy_encoder(&encoder_config, &_dshot_encoder) != DSHOT_OK) { - _dshot_log(ENCODER_INIT_FAILED); - return DSHOT_ERROR; + return {false, ENCODER_INIT_FAILED}; } - return DSHOT_OK; + return {true, TX_INIT_SUCCESS}; } // Send throttle value dshot_result_t DShotRMT::sendThrottle(uint16_t throttle) { - static uint16_t last_throttle = DSHOT_CMD_MOTOR_STOP; - dshot_result_t result = {false, UNKNOWN_ERROR}; - // Special case: if throttle is 0, use sendCommand() instead if (throttle == 0) { return sendCommand(DSHOT_CMD_MOTOR_STOP); } - // Log only if throttle is out of range and different from last time - if ((throttle < DSHOT_THROTTLE_MIN || throttle > DSHOT_THROTTLE_MAX) && throttle != last_throttle) - { - _dshot_log(THROTTLE_NOT_IN_RANGE); - result.error_message = THROTTLE_NOT_IN_RANGE; - } - // Always store the original throttle value - last_throttle = throttle; + _last_throttle = throttle; // Constrain throttle for transmission and send uint16_t new_throttle = constrain(throttle, DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX); + _packet = _buildDShotPacket(new_throttle); return _sendDShotFrame(_packet); @@ -255,79 +242,52 @@ dshot_result_t DShotRMT::sendThrottle(uint16_t throttle) // Send DShot command to ESC dshot_result_t DShotRMT::sendCommand(uint16_t command) { - dshot_result_t result = {false, UNKNOWN_ERROR}; - // Validate command is within DShot specification range if (command < DSHOT_CMD_MOTOR_STOP || command > DSHOT_CMD_MAX) { - _dshot_log(COMMAND_NOT_VALID); - result.error_message = COMMAND_NOT_VALID; - return result; + return {false, COMMAND_NOT_VALID}; } // Build packet and transmit _packet = _buildDShotPacket(command); + return _sendDShotFrame(_packet); } // Get telemetry data with timing and error handling -dshot_telemetry_result_t DShotRMT::getTelemetry(uint8_t magnet_count) +dshot_telemetry_result_t DShotRMT::getTelemetry(uint16_t magnet_count) { - dshot_telemetry_result_t result = {false, 0, 0, UNKNOWN_ERROR}; + // Result container + dshot_telemetry_result_t result = {false, NO_DSHOT_ERPM, NO_DSHOT_RPM, TELEMETRY_FAILED}; // Check if bidirectional mode is enabled if (!_is_bidirectional) { - result.error_message = BIDIR_NOT_ENABLED; - _dshot_log(BIDIR_NOT_ENABLED); + result.msg = BIDIR_NOT_ENABLED; return result; } - // Get eRPM - uint16_t erpm = getERPM(); - - if (erpm == DSHOT_NULL_PACKET) + // + if (_telemetry_ready_flag) { - result.error_message = TELEMETRY_TIMEOUT; - return result; - } + _telemetry_ready_flag = false; - // Calculate motor RPM - uint8_t pole_pairs = max(1, magnet_count / 2); - uint32_t motor_rpm = erpm / pole_pairs; + uint16_t erpm = _last_erpm_atomic; - result.success = true; - result.erpm = erpm; - result.motor_rpm = motor_rpm; - result.error_message = TELEMETRY_SUCCESS; - - return result; -} - -// Get RPM from ESC (bidirectional mode only) - backward compatibility -uint16_t DShotRMT::getERPM() -{ - // Check if bidirectional mode is enabled - if (!_is_bidirectional) - { - _dshot_log(BIDIR_NOT_ENABLED); - return _last_erpm; - } - - // RMT RX event data - rmt_rx_done_event_data_t rx_data; - - // Wait for data from the RX callback for a certain timeout - if (xQueueReceive(_rx_queue, &rx_data, pdMS_TO_TICKS(DSHOT_RX_TIMEOUT_MS)) == pdTRUE) - { - // Decode the received symbols if a valid frame was received - if (rx_data.num_symbols > DSHOT_NULL_PACKET) + // + if (erpm != DSHOT_NULL_PACKET && magnet_count >= 1) { - _last_erpm = _decodeDShotFrame(rx_data.received_symbols); + uint8_t pole_pairs = max(MIN_POLE_PAIRS, (magnet_count / MAGNETS_PER_POLE_PAIR)); + uint32_t motor_rpm = (erpm / pole_pairs); + + result.success = true; + result.erpm = erpm; + result.motor_rpm = motor_rpm; + result.msg = TELEMETRY_SUCCESS; } } - return _last_erpm; + return result; } // Build a complete DShot packet @@ -339,8 +299,6 @@ dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t value) // Re-check for valid value if (value > DSHOT_THROTTLE_MAX) { - _dshot_log(PACKET_BUILD_ERROR); - // Something is really wrong return packet; } @@ -371,39 +329,47 @@ uint16_t DShotRMT::_parseDShotPacket(const dshot_packet_t &packet) uint16_t DShotRMT::_calculateCRC(const uint16_t data) { // DShot CRC - uint16_t crc = (data ^ (data >> 4) ^ (data >> 8)) & 0b0000000000001111; + uint16_t crc = (data ^ (data >> 4) ^ (data >> 8)) & DSHOT_CRC_MASK; // Invert CRC for bidirectional DShot mode if (_is_bidirectional) { - crc = (~crc) & 0b0000000000001111; + crc = (~crc) & DSHOT_CRC_MASK; } return crc; } +// Per calculate bits - Performance optimized +void DShotRMT::_preCalculateBitPositions() +{ + for (int i = 0; i < DSHOT_BITS_PER_FRAME; ++i) + { + _bitPositions[i] = DSHOT_BITS_PER_FRAME - 1 - i; + } +} + // Transmit DShot packet via RMT dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) { - dshot_result_t result = {false, UNKNOWN_ERROR}; - uint64_t start_time = esp_timer_get_time(); - // Check timing requirements if (!_timer_signal()) { - return result; + return {false, TIMING_CORRECTION}; } // Enable RMT RX before RMT TX if (_is_bidirectional) { + // Calculate transmission data size + size_t rx_size_bytes = GCR_BITS_PER_FRAME * sizeof(rmt_symbol_word_t); + // Performance reasons rmt_symbol_word_t rx_symbols[DSHOT_BITS_PER_FRAME]; - if (rmt_receive(_rmt_rx_channel, rx_symbols, sizeof(rx_symbols), &_receive_config) != DSHOT_OK) + if (rmt_receive(_rmt_rx_channel, rx_symbols, rx_size_bytes, &_receive_config) != DSHOT_OK) { - result.error_message = RX_RMT_RECEIVER_ERROR; - return result; + return {false, RECEIVER_FAILED}; } } @@ -422,18 +388,14 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) // Disable RMT RX for sending if (rmt_disable(_rmt_rx_channel) != DSHOT_OK) { - result.error_message = RX_RMT_RECEIVER_ERROR; - _dshot_log(RX_RMT_RECEIVER_ERROR); - return result; + return {false, RECEIVER_FAILED}; } } // Perform RMT transmission if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, tx_symbols, tx_size_bytes, &_transmit_config) != DSHOT_OK) { - result.error_message = TRANSMITTER_ERROR; - _dshot_log(TRANSMITTER_ERROR); - return result; + return {false, TRANSMISSION_FAILED}; } // Re-enable RMT RX @@ -441,20 +403,14 @@ dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet) { if (rmt_enable(_rmt_rx_channel) != DSHOT_OK) { - result.error_message = RX_RMT_RECEIVER_ERROR; - _dshot_log(RX_RMT_RECEIVER_ERROR); - return result; + return {false, RECEIVER_FAILED}; } } // Update timestamp and calculate execution time _timer_reset(); - uint64_t end_time = esp_timer_get_time(); - result.success = true; - result.error_message = TRANSMISSION_SUCCESS; - - return result; + return {true, TRANSMISSION_SUCCESS}; } // Encode DShot packet into RMT symbol format (placed in IRAM for performance) @@ -462,16 +418,16 @@ bool IRAM_ATTR DShotRMT::_encodeDShotFrame(const dshot_packet_t &packet, rmt_sym { _parsed_packet = _parseDShotPacket(packet); - const uint16_t level0 = _is_bidirectional ? 0 : 1; - const uint16_t level1 = _is_bidirectional ? 1 : 0; - - for (int i = 0; i < DSHOT_BITS_PER_FRAME; i++) + // Decode MSB + for (int i = 0; i < DSHOT_BITS_PER_FRAME; ++i) { - // Decode MSB - bool bit = (_parsed_packet >> (DSHOT_BITS_PER_FRAME - 1 - i)) & 0b0000000000000001; - symbols[i].level0 = level0; + // Use precalculated bit positions - Performace optimized + int bit_position = _bitPositions[i]; + + bool bit = (_parsed_packet >> bit_position) & 0b0000000000000001; + symbols[i].level0 = _level0; symbols[i].duration0 = bit ? _timing_config.ticks_one_high : _timing_config.ticks_zero_high; - symbols[i].level1 = level1; + symbols[i].level1 = _level1; symbols[i].duration1 = bit ? _timing_config.ticks_one_low : _timing_config.ticks_zero_low; } @@ -497,15 +453,15 @@ uint16_t DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) // Extract 16 data bits and 4 CRC bits from 20-bit frame. // The first bit of the GCR frame is a start bit and is discarded. - uint16_t data_and_crc = (decoded_frame & 0xFFFF); + uint16_t data_and_crc = (decoded_frame & DSHOT_FULL_PACKET); // Cutting 4 bits? uint16_t received_data = data_and_crc >> 4; // Masking CRC - uint16_t received_crc = data_and_crc & 0b0000000000001111; + uint16_t received_crc = data_and_crc & DSHOT_CRC_MASK; - // Telemetry request bit is always 1. + // Telemetry request bit has to be 1 if (!(received_data & (1 << 11))) { return DSHOT_NULL_PACKET; @@ -522,7 +478,7 @@ uint16_t DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) } // Return the eRPM value (first 11 bits of received data). - return received_data & 0b0000011111111111; + return received_data & DSHOT_THROTTLE_MAX; } // Check if enough time has passed for next transmission @@ -540,6 +496,7 @@ bool IRAM_ATTR DShotRMT::_timer_signal() bool DShotRMT::_timer_reset() { _last_transmission_time = esp_timer_get_time(); + return DSHOT_OK; } @@ -550,22 +507,19 @@ void DShotRMT::printDShotInfo(Stream &output) const output.println(" === DShot Signal Info === "); // Current DShot mode - output.printf("Current Mode: DSHOT%d\n", - _mode == DSHOT150 ? 150 : - _mode == DSHOT300 ? 300 : - _mode == DSHOT600 ? 600 : - _mode == DSHOT1200 ? 1200 : 0); + 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"); - // Timing Info - output.printf("Frame Length: %u us\n", _timing_config.frame_length_us); - // Packet Info output.printf("Current Packet: "); // Print bit by bit - for (int i = 15; i >= 0; --i) + for (int i = DSHOT_BITS_PER_FRAME - 1; i >= 0; --i) { if ((_parsed_packet >> i) & 1) { @@ -592,3 +546,25 @@ void DShotRMT::printCpuInfo(Stream &output) const output.printf("XTAL Freq = %lu MHz\n", getXtalFrequencyMhz()); output.printf("APB Freq = %lu Hz\n", getApbFrequency()); } + +// --- HELPERS --- +void printDShotResult(dshot_result_t &result, Stream &output) +{ + output.printf("Status: %s - %s\n", result.success ? "SUCCESS" : "FAILED", result.msg); + output.println(" "); +} + +// +void printDShotTelemetry(dshot_telemetry_result_t &result, Stream &output) +{ + if (result.success) + { + output.printf("Telemetry: eRPM=%u, Motor RPM=%u\n", result.erpm, result.motor_rpm); + } + else + { + output.printf("Telemetry: FAILED - %s\n", result.msg); + } + + output.println(" "); +} diff --git a/DShotRMT.h b/DShotRMT.h index bdf40ee..c3aabe0 100644 --- a/DShotRMT.h +++ b/DShotRMT.h @@ -15,19 +15,26 @@ #include // DShot Protocol Constants -constexpr auto DSHOT_THROTTLE_FAILSAFE = 0; -constexpr auto DSHOT_THROTTLE_MIN = 48; -constexpr auto DSHOT_THROTTLE_MAX = 2047; -constexpr auto DSHOT_BITS_PER_FRAME = 16; -constexpr auto DSHOT_SWITCH_TIME = 30; // Time in us between TX and RX -constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000; -constexpr auto DSHOT_RX_TIMEOUT_MS = 2; +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 DSHOT_PAUSE_US = 30; // Additional frame pause time +static constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000; +static constexpr auto DSHOT_FULL_PACKET = 0b1111111111111111; +static constexpr auto DSHOT_CRC_MASK = 0b0000000000001111; +static constexpr auto DSHOT_RX_TIMEOUT_MS = 2; // Never reached, just a timeeout +static constexpr auto GCR_BITS_PER_FRAME = 21; // Number of GCR bits in a DShot answer frame (1 start + 16 data + 4 CRC) +static constexpr auto DEFAULT_MOTOR_MAGNET_COUNT = 14; +static constexpr auto MAGNETS_PER_POLE_PAIR = 2; +static constexpr auto MIN_POLE_PAIRS = 1; +static constexpr auto NO_DSHOT_ERPM = 0; +static constexpr auto NO_DSHOT_RPM = 0; // RMT Configuration Constants constexpr auto DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT; constexpr auto DSHOT_RMT_RESOLUTION = 10 * 1000 * 1000; // 10 MHz resolution constexpr auto RMT_BUFFER_SIZE = DSHOT_BITS_PER_FRAME; -constexpr auto GCR_BITS_PER_FRAME = 21; // Number of GCR bits in a DShot answer frame (1 start + 16 data + 4 CRC) constexpr auto RMT_BUFFER_SYMBOLS = 64; constexpr auto RMT_QUEUE_DEPTH = 1; @@ -36,7 +43,7 @@ constexpr auto RMT_QUEUE_DEPTH = 1; constexpr uint32_t DSHOT_PULSE_MIN = 3000; constexpr uint32_t DSHOT_PULSE_MAX = 60000; -// DShot Mode Enumeration +// DShot Modes typedef enum { DSHOT_OFF, @@ -46,7 +53,7 @@ typedef enum DSHOT1200 } dshot_mode_t; -// DShot Packet Structure +// DShot Packet typedef struct { uint16_t throttle_value : 11; @@ -54,7 +61,7 @@ typedef struct uint16_t checksum : 4; } dshot_packet_t; -// DShot Timing Configuration Structure +// DShot Timing Configuration typedef struct { uint32_t frame_length_us; @@ -65,25 +72,29 @@ typedef struct uint16_t ticks_zero_low; } dshot_timing_t; -// Command execution result structure +// Error handling typedef struct { bool success; - const char *error_message; + const char *msg; } dshot_result_t; -// DShot telemetry result structure +// DShot telemetry result typedef struct { bool success; uint16_t erpm; - uint32_t motor_rpm; - const char *error_message; + uint16_t motor_rpm; + const char *msg; } dshot_telemetry_result_t; // Naming convention typedef dshotCommands_e dshot_commands_t; +// --- HELPERS --- +void printDShotResult(dshot_result_t &result, Stream &output = Serial); +void printDShotTelemetry(dshot_telemetry_result_t &result, Stream &output = Serial); + // class DShotRMT { @@ -106,21 +117,12 @@ public: // Send DShot command (0-47) dshot_result_t sendCommand(uint16_t command); - // Get telemetry data (bidirectional mode only) - dshot_telemetry_result_t getTelemetry(uint8_t magnet_count = 14); - - // Get eRPM only (bidirectional mode only) - uint16_t getERPM(); - // --- GETTERS --- gpio_num_t getGPIO() const { return _gpio; } uint16_t getDShotPacket() const { return _parsed_packet; } bool is_bidirectional() const { return _is_bidirectional; } dshot_mode_t getMode() const { return _mode; } - - // Get execution statistics - uint32_t getTotalTransmissions() const { return _total_transmissions; } - uint32_t getFailedTransmissions() const { return _failed_transmissions; } + dshot_telemetry_result_t getTelemetry(uint16_t magnet_count = DEFAULT_MOTOR_MAGNET_COUNT); // --- INFO --- void printDShotInfo(Stream &output = Serial) const; @@ -141,7 +143,7 @@ public: return result.success; } - [[deprecated("Use getTelemetry() instead")]] + [[deprecated("Use getTelemetry() instead")]] uint32_t getMotorRPM(uint8_t magnet_count) { auto result = getTelemetry(magnet_count); @@ -155,16 +157,15 @@ private: bool _is_bidirectional; uint32_t _frame_timer_us; const dshot_timing_t &_timing_config; + uint16_t _last_throttle; - // --- TIMING & STATE VARIABLES --- + // --- TIMING & PACKET VARIABLES --- uint64_t _last_transmission_time; - uint16_t _last_erpm; uint16_t _parsed_packet; dshot_packet_t _packet; - - // --- STATISTICS --- - uint32_t _total_transmissions; - uint32_t _failed_transmissions; + uint8_t _bitPositions[DSHOT_BITS_PER_FRAME]; + uint16_t _level0; + uint16_t _level1; // --- RMT HARDWARE HANDLES --- rmt_channel_handle_t _rmt_tx_channel; @@ -178,14 +179,15 @@ private: rmt_receive_config_t _receive_config; // --- INITS --- - bool _initTXChannel(); - bool _initRXChannel(); - bool _initDShotEncoder(); + dshot_result_t _initTXChannel(); + dshot_result_t _initRXChannel(); + dshot_result_t _initDShotEncoder(); // --- PACKET MANAGEMENT --- dshot_packet_t _buildDShotPacket(const uint16_t value); uint16_t _parseDShotPacket(const dshot_packet_t &packet); uint16_t _calculateCRC(const uint16_t data); + void _preCalculateBitPositions(); // --- FRAME PROCESSING --- dshot_result_t _sendDShotFrame(const dshot_packet_t &packet); @@ -197,34 +199,38 @@ private: bool _timer_reset(); // -- CALLBACKS --- - QueueHandle_t _rx_queue; rmt_rx_event_callbacks_t _rx_event_callbacks; + volatile rmt_symbol_word_t _rx_symbols_direct[GCR_BITS_PER_FRAME]; + volatile uint16_t _last_erpm_atomic; + volatile bool _telemetry_ready_flag; static bool IRAM_ATTR _rmt_rx_done_callback(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data); // --- DSHOT DEFAULTS --- - static constexpr auto DSHOT_TELEMETRY_INVALID = (0xffff); - - // --- ERROR HANDLING & LOGGING --- - void _dshot_log(const char *msg, Stream &output = Serial) const { output.println(msg); } + static constexpr auto const DSHOT_TELEMETRY_INVALID = (0xffff); // --- CONSTANTS & ERROR MESSAGES --- static constexpr bool DSHOT_OK = 0; static constexpr bool DSHOT_ERROR = 1; - static constexpr char *NEW_LINE = " "; - static constexpr char *UNKNOWN_ERROR = "Unknown Error!"; - static constexpr char *TX_INIT_FAILED = "Failed to initialize TX channel!"; - static constexpr char *RX_INIT_FAILED = "Failed to initialize RX channel!"; - static constexpr char *ENCODER_INIT_FAILED = "Failed to initialize DShot encoder!"; - static constexpr char *CRC_CHECK_FAILED = "RX CRC Check failed!"; - static constexpr char *THROTTLE_NOT_IN_RANGE = "Throttle value not in range (48 - 2047)!"; - static constexpr char *COMMAND_NOT_VALID = "Not a valid DShot Command (0 - 47)!"; - static constexpr char *BIDIR_NOT_ENABLED = "Bidirectional DShot support not enabled!"; - static constexpr char *RX_RMT_RECEIVER_ERROR = "RX RMT receiver error!"; - static constexpr char *PACKET_BUILD_ERROR = "Value too big for DShot Packet!"; - static constexpr char *TRANSMITTER_ERROR = "RMT TX Transmitter Error!"; - static constexpr char *INIT_SUCCESS = "DShotRMT initialized successfully"; - static constexpr char *TELEMETRY_SUCCESS = "Telemetry read successfully"; - static constexpr char *TELEMETRY_TIMEOUT = "Telemetry read timeout"; - static constexpr char *TRANSMISSION_SUCCESS = "Transmission successful"; + 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 *RX_BUFFER_FAILED = "RX RMT buffer 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 *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 *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!"; }; diff --git a/README.md b/README.md index c7c716a..8c3a75c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # DShotRMT - ESP32 Library (Rewrite for ESP-IDF 5) A modern, robust C++ library for generating DShot signals on the ESP32 using the new ESP-IDF 5 RMT encoder API (`rmt_tx.h` / `rmt_rx.h`). -Supports all standard DShot modes (150, 300, 600) and features continuous frame transmission with configurable pause. +Supports all standard DShot modes (150, 300, 600, 1200) and features continuous frame transmission with configurable timing. **Now with BiDirectional DShot support and advanced command management!** > The legacy version (using the old `rmt.h` API) is still available in the `oldAPI` branch. @@ -12,20 +12,36 @@ Supports all standard DShot modes (150, 300, 600) and features continuous frame ## ๐Ÿš€ Features -- **All DShot Modes:** DSHOT150, DSHOT300 (default), DSHOT600, (DSHOT1200) -- **BiDirectional DShot:** Experimental support for RPM feedback +- **All DShot Modes:** DSHOT150, DSHOT300 (default), DSHOT600, DSHOT1200 +- **BiDirectional DShot:** Full support for RPM telemetry feedback - **Advanced Command Manager:** High-level API for ESC configuration and control - **Command Sequences:** Predefined initialization and calibration sequences -- **Continuous Frames:** Independent timed, Hardware signal generation -- **Configurable Pause:** Ensures ESCs can reliably detect frame boundaries +- **Hardware-Timed Signals:** Independent, precise signal generation using ESP32 RMT peripheral +- **Configurable Timing:** Ensures ESCs can reliably detect frame boundaries +- **Error Handling:** Comprehensive result reporting with success/failure status - **Simple API:** Easy integration into your Arduino or ESP-IDF project --- ## ๐Ÿ“ฆ Installation -Clone this repository and add it to your Arduino libraries or ESP-IDF components. +### Arduino IDE +1. Search "Arduino Library Manager" for "DShotRMT" +or + +1. Clone this repository or download as ZIP +2. Place in your Arduino libraries folder (`~/Arduino/libraries/DShotRMT/`) +3. Restart Arduino IDE + +### PlatformIO +Add to your `platformio.ini`: +```ini +lib_deps = + https://github.com/derdoktor667/DShotRMT.git +``` + +### Manual Installation ```sh git clone https://github.com/derdoktor667/DShotRMT.git ``` @@ -39,216 +55,208 @@ git clone https://github.com/derdoktor667/DShotRMT.git ```cpp #include -// Create motor instance -DShotRMT motor(17, DSHOT300); +// Create motor instance (GPIO 17, DSHOT300, non-bidirectional) +DShotRMT motor(17, DSHOT300, false); void setup() { Serial.begin(115200); - motor.begin(); + + // Initialize the motor + dshot_result_t result = motor.begin(); + if (result.success) { + Serial.println("Motor initialized successfully"); + } else { + Serial.printf("Motor init failed: %s\n", result.msg); + } } void loop() { - motor.sendThrottle(1000); // Send throttle value - delay(20); + // Send throttle value (48-2047) + dshot_result_t result = motor.sendThrottle(1000); + if (!result.success) { + Serial.printf("Throttle command failed: %s\n", result.msg); + } } ``` -### Advanced Usage (DShotCommandManager) +### Bidirectional DShot (RPM Telemetry) ```cpp #include -#include -// Create motor and command manager instances -DShotRMT motor(17, DSHOT300); -DShotCommandManager cmdManager(motor); +// Enable bidirectional mode for telemetry +DShotRMT motor(17, DSHOT300, true); void setup() { Serial.begin(115200); motor.begin(); - cmdManager.begin(); - - // Execute initialization sequence - cmdManager.executeInitSequence(); } void loop() { - // Your main code here + // Send throttle + motor.sendThrottle(1000); + + // Get telemetry data + dshot_telemetry_result_t telemetry = motor.getTelemetry(14); // 14 magnets + if (telemetry.success) { + Serial.printf("eRPM: %u, Motor RPM: %u\n", + telemetry.erpm, + telemetry.motor_rpm); + } } ``` --- -## ๐ŸŽ›๏ธ DShotCommandManager API - -The `DShotCommandManager` provides a high-level interface for ESC control and configuration: - -### Motor Control -- `stopMotor()` - Stop motor immediately -- `set3DMode(bool enable)` - Enable/disable 3D mode -- `setSpinDirection(bool reversed)` - Set motor spin direction -- `saveSettings()` - Save current settings to ESC - -### LED Control (BLHeli32 only) -- `setLED(uint8_t led_number, bool state)` - Control ESC LEDs (0-3) - -### Beacon Functions -- `activateBeacon(uint8_t beacon_number)` - Activate motor beeping (1-5) - -### Telemetry -- `setExtendedTelemetry(bool enable)` - Enable/disable extended telemetry -- `requestESCInfo()` - Request ESC information - -### Command Sequences -- `executeInitSequence()` - Basic ESC initialization -- `executeCalibrationSequence()` - ESC calibration sequence -- `executeSequence(sequence, length)` - Custom command sequences - -### Utility Functions -- `getCommandName(command)` - Get command name as string -- `isValidCommand(command)` - Validate command -- `printStatistics()` - Print execution statistics -- `resetStatistics()` - Reset execution counters - ---- - ## ๐Ÿ“š Examples -### 1. Basic DShot Control -Use the `dshot300.ino` example for simple throttle control. +The library includes comprehensive examples: -### 2. Advanced Command Management -Use the `command_manager.ino` example for interactive ESC control: +### 1. Basic DShot Control (`dshot300.ino`) +- Simple throttle control +- Command execution +- Serial interface for testing +- Telemetry reading (if bidirectional enabled) +### 2. Advanced Command Management (`command_manager.ino`) +Interactive ESC control with full menu system: ``` === DShot Command Manager Menu === -Basic Commands: - 1 - Stop Motor - 2 - Activate Beacon 1 - 3 - Set Normal Spin Direction - 4 - Set Reversed Spin Direction - 5 - Enable 3D Mode - 6 - Disable 3D Mode - 7 - Save Settings - 8 - Turn LED 0 ON - 9 - Turn LED 0 OFF + 1 - Stop Motor + 2 - Activate Beacon 1 + 3 - Set Normal Spin Direction + 4 - Set Reversed Spin Direction + 5 - Get ESC Info + 6 - Turn LED 0 ON + 7 - Turn LED 0 OFF + 0 - Emergency Stop -Sequences: - i - Execute Initialization Sequence - c - Execute Calibration Sequence - -Advanced: - cmd - Send DShot command (0 - 47) - throttle - Set throttle (48 - 2047) - throttle 0 - Stop sending throttle +Advanced Commands: + cmd - Send DShot command (0-47) + throttle - Set throttle (48-2047) + repeat cmd count - Repeat command ``` --- -## ๐Ÿ“š DShot Protocol Overview +## ๐Ÿ”ง Hardware Configuration -DShot transmits 16-bit packets to brushless ESCs: +### Supported DShot Modes -- **11 bits:** Throttle value -- **1 bit:** Telemetry request -- **4 bits:** Checksum (CRC) +| Mode | Bitrate | Bit Time | Frame Time | Use Case | +|----------|-------------|----------|------------|----------| +| DSHOT150 | 150 kbit/s | 6.67 ยตs | ~107 ยตs | Long wires, EMI-prone | +| DSHOT300 | 300 kbit/s | 3.33 ยตs | ~53 ยตs | Standard (recommended) | +| DSHOT600 | 600 kbit/s | 1.67 ยตs | ~27 ยตs | High performance | +| DSHOT1200| 1200 kbit/s | 0.83 ยตs | ~13 ยตs | Racing applications | -Data is sent MSB-first. Pulse timing depends on the selected DShot mode. +### GPIO Configuration +```cpp +// Using GPIO number +DShotRMT motor(17, DSHOT300); -| DSHOT | Bitrate | TH1 | TH0 | Bit Time (ยตs) | Frame Time (ยตs) | -|-------|-------------|-------|--------|---------------|-----------------| -| 150 | 150 kbit/s | 5.00 | 2.50 | 6.67 | ~106.72 | -| 300 | 300 kbit/s | 2.50 | 1.25 | 3.33 | ~53.28 | -| 600 | 600 kbit/s | 1.25 | 0.625 | 1.67 | ~26.72 | +// Using GPIO enum +DShotRMT motor(GPIO_NUM_17, DSHOT300); -Each frame is followed by a pause to help ESCs detect separate frames. - -![DShotRMT](https://raw.githubusercontent.com/derdoktor667/DShotRMT/refs/heads/main/img/dshot300.png) - ---- - -## ๐Ÿ”’ Checksum Calculation - -The checksum is calculated over the first 12 bits (throttle + telemetry): - -```c -crc = (value ^ (value >> 4) ^ (value >> 8)) & 0x0F; +// With bidirectional support +DShotRMT motor(17, DSHOT300, true); ``` -### Bidirectional DSHOT - -Bidirectional DSHOT (sometimes called "inverted DSHOT") inverts the signal level: -A logical '1' is low, and a '0' is high. This signals the ESC to send telemetry packets back. - -**Bidirectional CRC:** - -```c -crc = (~(value ^ (value >> 4) ^ (value >> 8))) & 0x0F; -``` - -> **Note:** Bidirectional DShot is experimental. Further hardware testing is needed. - ---- - -## ๐Ÿ› ๏ธ ESP32 RMT Peripheral - -The RMT (Remote Control) peripheral generates accurate, hardware-timed signals for controlling external devices. -Perfect for DShot: -- Utilizes latest ESP-IDF APIs -- Hardware-timed pulses -- CPU-independent -- Loop mode with inter-frame pause -- Reliable under system load - ---- - -## ๐Ÿ“ Core API Reference - -### DShotRMT Class -- `DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool isBidirectional)` -- `uint16_t begin()` -- `bool sendThrottle(uint16_t throttle)` -- `bool sendCommand(uint16_t command)` -- `uint16_t getERPM()` - Get eRPM (bidirectional mode only) -- `uint32_t getMotorRPM(uint8_t magnet_count)` - Convert to motor RPM - -### DShotCommandManager Class -- `DShotCommandManager(DShotRMT &dshot_instance)` -- `bool begin()` -- `dshot_command_result_t sendCommand(dshot_commands_t command, uint16_t repeat_count = 1)` -- `dshot_command_result_t sendCommandWithDelay(dshot_commands_t command, uint16_t repeat_count, uint32_t delay_ms)` - -All command methods return a `dshot_command_result_t` structure containing: -- `bool success` - Command execution status -- `uint32_t execution_time_us` - Execution time in microseconds -- `const char* error_message` - Error description --- ## ๐ŸŽฏ DShot Commands -The library supports all standard DShot commands: - -| Command | Value | Description | -|---------|-------|-------------| -| MOTOR_STOP | 0 | Stop motor | -| BEACON1-5 | 1-5 | Motor beeping | -| ESC_INFO | 6 | Request ESC information | -| SPIN_DIRECTION_1/2 | 7-8 | Set spin direction | -| 3D_MODE_OFF/ON | 9-10 | 3D mode control | -| SAVE_SETTINGS | 12 | Save settings to ESC | -| EXTENDED_TELEMETRY_ENABLE/DISABLE | 13-14 | Telemetry control | -| LED0-3_ON/OFF | 22-29 | LED control (BLHeli32) | -| AUDIO_STREAM_MODE | 30 | KISS audio mode | -| SILENT_MODE | 31 | KISS silent mode | +| Command | Value | Description | Usage | +|---------|-------|-------------|-------| +| MOTOR_STOP | 0 | Stop motor | Always available | +| BEACON1 - 5 | 1 - 5 | Motor beeping | Motor identification | +| ESC_INFO | 6 | Request ESC info | Get ESC version/settings | +| SPIN_DIRECTION_1/2 | 7 - 8 | Set spin direction | Motor configuration | +| 3D_MODE_OFF/ON | 9 - 10 | 3D mode control | Bidirectional flight | +| SAVE_SETTINGS | 12 | Save to EEPROM | Permanent configuration | +| EXTENDED_TELEMETRY_ENABLE/DISABLE | 13 - 14 | Telemetry control | Data transmission | +| SPIN_DIRECTION_NORMAL/REVERSED | 20 - 21 | Spin direction | Alias commands | +| LED0-3_ON/OFF | 22 - 29 | LED control | BLHeli32 only | +| AUDIO_STREAM_MODE | 30 | Audio mode toggle | KISS ESCs | +| SILENT_MODE | 31 | Silent mode toggle | KISS ESCs | --- -## ๐Ÿ“– References +## ๐Ÿ“š DShot Protocol Details +![DShotRMT](https://raw.githubusercontent.com/derdoktor667/DShotRMT/refs/heads/main/img/dshot300.png) + +### Packet Structure +Each DShot frame consists of 16 bits: +- **11 bits:** Throttle/command value (0-2047) +- **1 bit:** Telemetry request flag +- **4 bits:** CRC checksum + +### Checksum Calculation +```cpp +// Standard DShot CRC +uint16_t crc = (data ^ (data >> 4) ^ (data >> 8)) & 0x0F; + +// Bidirectional DShot (inverted CRC) +uint16_t crc = (~(data ^ (data >> 4) ^ (data >> 8))) & 0x0F; +``` + +### Bidirectional DShot +- **Inverted Logic:** High/low levels are inverted +- **GCR Encoding:** Telemetry uses Group Code Recording +- **21-bit Response:** 1 start + 16 data + 4 CRC bits +- **eRPM Data:** Electrical RPM transmitted back to controller + +--- + +## ๐Ÿ› ๏ธ ESP32 RMT Peripheral + +The library utilizes the ESP32's RMT (Remote Control) peripheral for precise signal generation: + +### Advantages +- **Hardware Timing:** No CPU intervention during transmission +- **Concurrent Operation:** Multiple channels can run simultaneously +- **DMA Support:** Efficient memory-to-peripheral transfers + +--- + +## ๐Ÿ“– References & Documentation + +### DShot Protocol - [DSHOT โ€“ the missing Handbook](https://brushlesswhoop.com/dshot-and-bidirectional-dshot/) - [DSHOT in the Dark](https://dmrlawson.co.uk/index.php/2017/12/04/dshot-in-the-dark/) +- [Betaflight DShot Implementation](https://github.com/betaflight/betaflight) + +### ESP32 Documentation - [ESP32 Technical Reference Manual](https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf) +- [ESP-IDF RMT Driver](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/rmt.html) +- [Arduino ESP32 Core](https://github.com/espressif/arduino-esp32) + +--- + +## ๐Ÿค Contributing + +We welcome contributions! Please: + +1. Fork the repository +2. Create a feature branch +3. Make your changes with tests +4. Submit a pull request + +### Development Guidelines +- Follow existing code style +- Add documentation for new features +- Include examples where appropriate +- Test with real hardware when possible + +### Reporting Issues +When reporting issues, please include: +- ESP32 board type and version +- Arduino/ESP-IDF version +- ESC type and firmware +- Complete error messages +- Minimal reproduction code --- @@ -261,5 +269,5 @@ MIT License โ€“ see [LICENSE](LICENSE) ## ๐Ÿ‘ค Author **Wastl Kraus** -GitHub: [@derdoktor667](https://github.com/derdoktor667) -Website: [wir-sind-die-matrix.de](https://wir-sind-die-matrix.de) +- GitHub: [@derdoktor667](https://github.com/derdoktor667) +- Website: [wir-sind-die-matrix.de](https://wir-sind-die-matrix.de) diff --git a/examples/command_manager/command_manager.ino b/examples/command_manager/command_manager.ino index a07cbe2..9b4e974 100644 --- a/examples/command_manager/command_manager.ino +++ b/examples/command_manager/command_manager.ino @@ -37,7 +37,7 @@ void printTelemetryResult(const dshot_telemetry_result_t &result) } else { - USB_SERIAL.printf("Telemetry: FAILED - %s\n", result.error_message); + USB_SERIAL.printf("Telemetry: FAILED - %s\n", result.msg); } } @@ -270,7 +270,7 @@ void printResult(const dshot_result_t &result) } else { - USB_SERIAL.printf("FAILED - %s \n", result.error_message); + USB_SERIAL.printf("FAILED - %s \n", result.msg); } } diff --git a/examples/dshot300/dshot300.ino b/examples/dshot300/dshot300.ino index 21f68a8..4c7d8ff 100644 --- a/examples/dshot300/dshot300.ino +++ b/examples/dshot300/dshot300.ino @@ -53,7 +53,7 @@ void loop() static bool continuous_throttle = true; // Time Measurement - static uint32_t last_stats_print = 0; + static uint64_t last_stats_print = 0; // Handle serial input if (USB_SERIAL.available() > 0) @@ -73,20 +73,24 @@ void loop() motor01.sendThrottle(throttle); } - // Print motor stats every 5 seconds in continuous mode - if (continuous_throttle && (millis() - last_stats_print >= 5000)) + // Print motor stats every 3 seconds in continuous mode + if (continuous_throttle && (esp_timer_get_time() - last_stats_print >= 3000000)) { motor01.printDShotInfo(); + USB_SERIAL.println(" "); + // Get Motor RPM if bidirectional if (IS_BIDIRECTIONAL) { dshot_telemetry_result_t telem_result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - printTelemetryResult(telem_result); + printDShotTelemetry(telem_result); } + USB_SERIAL.println("Type 'help' to show Menu"); + // Time Stamp - last_stats_print = millis(); + last_stats_print = esp_timer_get_time(); } } @@ -94,47 +98,21 @@ void loop() void printMenu() { USB_SERIAL.println(" "); - USB_SERIAL.println("******************************************"); - USB_SERIAL.println(" DShotRMT Demo "); - USB_SERIAL.println("******************************************"); + USB_SERIAL.println("*******************************************"); + USB_SERIAL.println(" DShotRMT Demo "); + USB_SERIAL.println("*******************************************"); USB_SERIAL.println(" - Set throttle (48 โ€“ 2047)"); USB_SERIAL.println(" 0 - Stop motor"); - USB_SERIAL.println("******************************************"); - USB_SERIAL.println(" cmd - Send DShot command (0-47)"); + USB_SERIAL.println("*******************************************"); + USB_SERIAL.println(" cmd - Send DShot command (0 - 47)"); USB_SERIAL.println(" info - Show motor info"); if (IS_BIDIRECTIONAL) { USB_SERIAL.println(" rpm - Get telemetry data"); } - USB_SERIAL.println("******************************************"); + USB_SERIAL.println("*******************************************"); USB_SERIAL.println(" h / help - Show this Menu"); - USB_SERIAL.println("******************************************"); -} - -// Helper to print command results -void printCommandResult(const dshot_result_t &result, const String &operation) -{ - if (result.success) - { - USB_SERIAL.printf("%s: SUCCESS\n", operation.c_str()); - } - else - { - USB_SERIAL.printf("%s: FAILED - %s\n", operation.c_str(), result.error_message); - } -} - -// Helper to print telemetry results -void printTelemetryResult(const dshot_telemetry_result_t &result) -{ - if (result.success) - { - USB_SERIAL.printf("Telemetry: eRPM=%u, Motor RPM=%u (%s)\n", result.erpm, result.motor_rpm, result.error_message); - } - else - { - USB_SERIAL.printf("Telemetry: FAILED - %s\n", result.error_message); - } + USB_SERIAL.println("*******************************************"); } // @@ -144,19 +122,18 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous { // Stop motor throttle = 0; - continuous_throttle = true; // kill motor for sure + continuous_throttle = true; dshot_result_t result = motor01.sendCommand(DSHOT_CMD_MOTOR_STOP); - printCommandResult(result, "Stop Motor"); + printDShotResult(result); } else if (input == "info") { - continuous_throttle = false; motor01.printDShotInfo(); } else if (input == "rpm" && IS_BIDIRECTIONAL) { dshot_telemetry_result_t result = motor01.getTelemetry(MOTOR01_MAGNET_COUNT); - printTelemetryResult(result); + printDShotTelemetry(result); } else if (input.startsWith("cmd ")) { @@ -168,7 +145,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); - printCommandResult(result, "DShot Command " + String(cmd_num)); + printDShotResult(result); } else { @@ -190,13 +167,12 @@ void handleSerialInput(const String &input, uint16_t &throttle, bool &continuous continuous_throttle = true; dshot_result_t result = motor01.sendThrottle(throttle); - printCommandResult(result, "Set Throttle " + String(throttle)); - - USB_SERIAL.println("Continuous throttle mode enabled. Send '0' to stop."); + printDShotResult(result); } else { - USB_SERIAL.printf("Invalid input: '%s'\n", input.c_str()); + USB_SERIAL.println(" "); + USB_SERIAL.printf("Invalid input: '%s'\n", input); USB_SERIAL.printf("Valid throttle range: %d - %d\n", DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX); } } diff --git a/library.properties b/library.properties index 0f27f35..19b7cda 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=DShotRMT -version=0.7.1 +version=0.7.2 author=derdoktor667 maintainer=derdoktor667 sentence=DShotRMT Library supporting all DShot Types and speeds. Tested with BlHeli_S.