2025-06-11 08:29:59 +01:00
|
|
|
/**
|
|
|
|
|
* @file DShotRMT.cpp
|
2025-07-30 23:36:58 +01:00
|
|
|
* @brief DShot signal generation using ESP32 RMT with bidirectional support
|
2025-06-11 08:29:59 +01:00
|
|
|
* @author Wastl Kraus
|
|
|
|
|
* @date 2025-06-11
|
|
|
|
|
* @license MIT
|
|
|
|
|
*/
|
2021-07-04 02:01:26 +01:00
|
|
|
|
2025-09-17 20:41:20 +01:00
|
|
|
#include <DShotRMT.h>
|
2025-07-30 23:36:58 +01:00
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// Constructors & Destructor
|
2025-09-03 10:16:14 +01:00
|
|
|
// Constructor with GPIO number
|
2025-09-20 15:47:39 +01:00
|
|
|
DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count)
|
2025-08-30 20:56:34 +01:00
|
|
|
: _gpio(gpio),
|
|
|
|
|
_mode(mode),
|
|
|
|
|
_is_bidirectional(is_bidirectional),
|
2025-09-20 15:47:39 +01:00
|
|
|
_motor_magnet_count(magnet_count),
|
2025-09-25 13:03:55 +01:00
|
|
|
_dshot_timing(DSHOT_TIMING_US[static_cast<int>(mode)]),
|
2025-09-13 10:54:30 +01:00
|
|
|
_frame_timer_us(0),
|
2025-09-12 22:14:34 +01:00
|
|
|
_rmt_ticks{0},
|
2025-09-29 15:51:12 +01:00
|
|
|
_last_throttle(dshotCommands_e::DSHOT_CMD_MOTOR_STOP),
|
2025-09-12 22:14:34 +01:00
|
|
|
_last_transmission_time_us(0),
|
2025-09-18 15:10:59 +01:00
|
|
|
_last_command_timestamp(0),
|
2025-09-25 12:12:25 +01:00
|
|
|
_encoded_frame_value(0),
|
2025-08-30 20:56:34 +01:00
|
|
|
_packet{0},
|
2025-09-26 18:24:50 +01:00
|
|
|
_pulse_level(1), // DShot standard: signal is idle-low, so pulses start by going HIGH
|
2025-09-30 19:37:25 +01:00
|
|
|
_idle_level(0), // DShot standard: signal returns to LOW after the high pulse
|
2025-09-03 10:16:14 +01:00
|
|
|
_rmt_tx_channel(nullptr),
|
|
|
|
|
_rmt_rx_channel(nullptr),
|
|
|
|
|
_dshot_encoder(nullptr),
|
|
|
|
|
_tx_channel_config{},
|
|
|
|
|
_rx_channel_config{},
|
2025-09-15 14:56:04 +01:00
|
|
|
_rmt_tx_config{},
|
|
|
|
|
_rmt_rx_config{},
|
2025-09-13 10:54:30 +01:00
|
|
|
_rx_event_callbacks{},
|
|
|
|
|
_last_erpm_atomic(0),
|
|
|
|
|
_telemetry_ready_flag_atomic(false)
|
2025-07-25 16:41:38 +01:00
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
// Pre-calculate timing and bit positions for performance
|
2025-09-15 14:56:04 +01:00
|
|
|
_preCalculateRMTTicks();
|
2025-07-25 16:41:38 +01:00
|
|
|
}
|
2025-06-11 08:29:59 +01:00
|
|
|
|
2025-08-30 23:21:03 +01:00
|
|
|
// Constructor using pin number
|
2025-09-20 15:47:39 +01:00
|
|
|
DShotRMT::DShotRMT(uint16_t pin_nr, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count)
|
|
|
|
|
: DShotRMT(static_cast<gpio_num_t>(pin_nr), mode, is_bidirectional, magnet_count)
|
2025-08-06 22:57:26 +01:00
|
|
|
{
|
2025-08-30 20:56:34 +01:00
|
|
|
// Delegates to primary constructor with type cast
|
2025-08-06 22:57:26 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// Destructor
|
2025-09-01 13:41:48 +01:00
|
|
|
DShotRMT::~DShotRMT()
|
|
|
|
|
{
|
2025-09-13 10:54:30 +01:00
|
|
|
// Cleanup TX channel
|
2025-09-01 13:41:48 +01:00
|
|
|
if (_rmt_tx_channel)
|
|
|
|
|
{
|
2025-09-05 22:28:08 +01:00
|
|
|
if (rmt_disable(_rmt_tx_channel) == DSHOT_OK)
|
|
|
|
|
{
|
2025-09-25 15:15:36 +01:00
|
|
|
rmt_del_channel(_rmt_tx_channel);
|
|
|
|
|
_rmt_tx_channel = nullptr;
|
2025-09-05 22:28:08 +01:00
|
|
|
}
|
2025-09-01 13:41:48 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// Cleanup RX channel
|
2025-09-01 13:41:48 +01:00
|
|
|
if (_rmt_rx_channel)
|
|
|
|
|
{
|
2025-09-05 22:28:08 +01:00
|
|
|
if (rmt_disable(_rmt_rx_channel) == DSHOT_OK)
|
|
|
|
|
{
|
2025-09-25 15:15:36 +01:00
|
|
|
rmt_del_channel(_rmt_rx_channel);
|
|
|
|
|
_rmt_rx_channel = nullptr;
|
2025-09-05 22:28:08 +01:00
|
|
|
}
|
2025-09-01 13:41:48 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// Cleanup encoder
|
2025-09-01 13:41:48 +01:00
|
|
|
if (_dshot_encoder)
|
|
|
|
|
{
|
|
|
|
|
rmt_del_encoder(_dshot_encoder);
|
2025-09-04 13:41:05 +01:00
|
|
|
_dshot_encoder = nullptr;
|
2025-09-01 13:41:48 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// Public Core Functions
|
|
|
|
|
// Initialize DShotRMT
|
2025-09-05 11:16:19 +01:00
|
|
|
dshot_result_t DShotRMT::begin()
|
2022-11-25 15:08:58 +00:00
|
|
|
{
|
2025-09-17 20:41:20 +01:00
|
|
|
if (!_initTXChannel().success)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_TX_INIT_FAILED};
|
2025-09-17 20:41:20 +01:00
|
|
|
}
|
|
|
|
|
|
2025-08-31 10:43:48 +01:00
|
|
|
if (_is_bidirectional)
|
2025-07-19 12:26:54 +01:00
|
|
|
{
|
2025-09-07 12:48:48 +01:00
|
|
|
if (!_initRXChannel().success)
|
2025-08-31 10:43:48 +01:00
|
|
|
{
|
2025-09-20 15:47:39 +01:00
|
|
|
// Cleanup previously allocated TX channel on failure
|
|
|
|
|
rmt_disable(_rmt_tx_channel);
|
|
|
|
|
rmt_del_channel(_rmt_tx_channel);
|
|
|
|
|
_rmt_tx_channel = nullptr;
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_RX_INIT_FAILED};
|
2025-08-31 10:43:48 +01:00
|
|
|
}
|
2025-07-19 12:26:54 +01:00
|
|
|
}
|
2025-07-29 23:40:09 +01:00
|
|
|
|
2025-09-07 12:48:48 +01:00
|
|
|
if (!_initDShotEncoder().success)
|
2025-07-19 12:26:54 +01:00
|
|
|
{
|
2025-09-20 15:47:39 +01:00
|
|
|
// Cleanup previously allocated channels on failure
|
|
|
|
|
rmt_disable(_rmt_tx_channel);
|
|
|
|
|
rmt_del_channel(_rmt_tx_channel);
|
|
|
|
|
_rmt_tx_channel = nullptr;
|
2025-09-23 22:48:18 +01:00
|
|
|
|
2025-09-25 15:15:36 +01:00
|
|
|
if (_rmt_rx_channel)
|
|
|
|
|
{
|
2025-09-20 15:47:39 +01:00
|
|
|
rmt_disable(_rmt_rx_channel);
|
|
|
|
|
rmt_del_channel(_rmt_rx_channel);
|
|
|
|
|
_rmt_rx_channel = nullptr;
|
|
|
|
|
}
|
2025-09-25 15:15:36 +01:00
|
|
|
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_ENCODER_INIT_FAILED};
|
2025-07-19 12:26:54 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-29 15:51:12 +01:00
|
|
|
return {true, dshot_msg_code_t::DSHOT_INIT_SUCCESS};
|
2025-08-30 23:21:03 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// Send throttle value
|
|
|
|
|
dshot_result_t DShotRMT::sendThrottle(uint16_t throttle)
|
|
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
// A throttle value of 0 is a disarm command
|
2025-09-13 10:54:30 +01:00
|
|
|
if (throttle == 0)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return sendCommand(dshotCommands_e::DSHOT_CMD_MOTOR_STOP);
|
2025-09-13 10:54:30 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Constrain throttle to the valid DShot range
|
|
|
|
|
_last_throttle = constrain(throttle, DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX);
|
2025-09-13 10:54:30 +01:00
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
_packet = _buildDShotPacket(_last_throttle);
|
2025-09-13 10:54:30 +01:00
|
|
|
return _sendDShotFrame(_packet);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-20 15:47:39 +01:00
|
|
|
// Send throttle value as a percentage
|
|
|
|
|
dshot_result_t DShotRMT::sendThrottlePercent(float percent)
|
|
|
|
|
{
|
|
|
|
|
if (percent < 0.0f || percent > 100.0f)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_PERCENT_NOT_IN_RANGE};
|
2025-09-20 15:47:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map percent to DShot throttle range
|
|
|
|
|
uint16_t throttle = static_cast<uint16_t>(DSHOT_THROTTLE_MIN + ((DSHOT_THROTTLE_MAX - DSHOT_THROTTLE_MIN) / 100.0f) * percent);
|
|
|
|
|
return sendThrottle(throttle);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// Send DShot command to ESC
|
|
|
|
|
dshot_result_t DShotRMT::sendCommand(uint16_t command)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
if (command > dshotCommands_e::DSHOT_CMD_MAX)
|
2025-09-13 10:54:30 +01:00
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_COMMAND_NOT_VALID};
|
2025-09-13 10:54:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_packet = _buildDShotPacket(command);
|
|
|
|
|
return _sendDShotFrame(_packet);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 15:10:59 +01:00
|
|
|
// Send full DShot commands for setup etc
|
2025-09-25 13:03:55 +01:00
|
|
|
// 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)
|
2025-09-18 15:10:59 +01:00
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
dshot_result_t result = {false, dshot_msg_code_t::DSHOT_UNKNOWN, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY};
|
2025-09-18 15:10:59 +01:00
|
|
|
|
|
|
|
|
if (!_isValidCommand(dshot_command))
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
result.result_code = dshot_msg_code_t::DSHOT_INVALID_COMMAND;
|
2025-09-18 15:10:59 +01:00
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool all_successful = true;
|
|
|
|
|
|
|
|
|
|
// Send command multiple times with delay
|
|
|
|
|
for (uint16_t i = 0; i < repeat_count; i++)
|
|
|
|
|
{
|
|
|
|
|
dshot_result_t single_result = _executeCommand(dshot_command);
|
|
|
|
|
|
|
|
|
|
if (!single_result.success)
|
|
|
|
|
{
|
|
|
|
|
all_successful = false;
|
2025-09-27 22:17:43 +01:00
|
|
|
result.result_code = single_result.result_code;
|
2025-09-18 15:10:59 +01:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add delay between repetitions (except for last repetition)
|
|
|
|
|
if (i < repeat_count - 1)
|
|
|
|
|
{
|
|
|
|
|
delayMicroseconds(delay_us);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
result.success = all_successful;
|
|
|
|
|
|
|
|
|
|
if (result.success)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
result.result_code = dshot_msg_code_t::DSHOT_COMMAND_SUCCESS;
|
2025-09-18 15:10:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// Get telemetry data
|
|
|
|
|
dshot_result_t DShotRMT::getTelemetry(uint16_t magnet_count)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
dshot_result_t result = {false, dshot_msg_code_t::DSHOT_TELEMETRY_FAILED, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY};
|
2025-09-13 10:54:30 +01:00
|
|
|
|
|
|
|
|
if (!_is_bidirectional)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
result.result_code = dshot_msg_code_t::DSHOT_BIDIR_NOT_ENABLED;
|
2025-09-13 10:54:30 +01:00
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-20 15:47:39 +01:00
|
|
|
// Use stored magnet count if parameter is 0 (default)
|
|
|
|
|
uint16_t final_magnet_count = (magnet_count == 0) ? _motor_magnet_count : magnet_count;
|
|
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Check if the callback has set the flag for new data
|
2025-09-13 10:54:30 +01:00
|
|
|
if (_telemetry_ready_flag_atomic)
|
|
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
_telemetry_ready_flag_atomic = false; // Reset the flag
|
|
|
|
|
uint16_t erpm = _last_erpm_atomic; // Read the atomic variable
|
2025-09-13 10:54:30 +01:00
|
|
|
|
2025-09-20 15:47:39 +01:00
|
|
|
if (erpm != DSHOT_NULL_PACKET && final_magnet_count >= MAGNETS_PER_POLE_PAIR)
|
2025-09-13 10:54:30 +01:00
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
// Calculate motor RPM from eRPM and magnet count
|
2025-09-20 15:47:39 +01:00
|
|
|
uint8_t pole_pairs = final_magnet_count / MAGNETS_PER_POLE_PAIR;
|
2025-09-13 10:54:30 +01:00
|
|
|
uint32_t motor_rpm = (erpm / pole_pairs);
|
|
|
|
|
|
|
|
|
|
result.success = true;
|
|
|
|
|
result.erpm = erpm;
|
|
|
|
|
result.motor_rpm = motor_rpm;
|
2025-09-29 15:51:12 +01:00
|
|
|
result.result_code = dshot_msg_code_t::DSHOT_TELEMETRY_SUCCESS;
|
2025-09-13 10:54:30 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 15:10:59 +01:00
|
|
|
// Reverse motor direction directly
|
|
|
|
|
dshot_result_t DShotRMT::setMotorSpinDirection(bool reversed)
|
|
|
|
|
{
|
|
|
|
|
// Use command as a yes / no switch
|
2025-09-25 13:03:55 +01:00
|
|
|
dshotCommands_e command = reversed ? dshotCommands_e::DSHOT_CMD_SPIN_DIRECTION_REVERSED : dshotCommands_e::DSHOT_CMD_SPIN_DIRECTION_NORMAL;
|
2025-09-18 15:10:59 +01:00
|
|
|
|
|
|
|
|
return sendCommand(command, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dshot_result_t DShotRMT::getESCInfo()
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return sendCommand(dshotCommands_e::DSHOT_CMD_ESC_INFO);
|
2025-09-18 15:10:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use with caution
|
|
|
|
|
dshot_result_t DShotRMT::saveESCSettings()
|
|
|
|
|
{
|
2025-09-25 13:03:55 +01:00
|
|
|
return sendCommand(dshotCommands_e::DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US);
|
2025-09-13 10:54:30 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-18 15:10:59 +01:00
|
|
|
// Simple check
|
2025-09-25 13:03:55 +01:00
|
|
|
bool DShotRMT::_isValidCommand(dshotCommands_e command)
|
2025-09-18 15:10:59 +01:00
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return (command >= dshotCommands_e::DSHOT_CMD_MOTOR_STOP && command <= dshotCommands_e::DSHOT_CMD_MAX);
|
2025-09-18 15:10:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
2025-09-25 13:03:55 +01:00
|
|
|
dshot_result_t DShotRMT::_executeCommand(dshotCommands_e command)
|
2025-09-18 15:10:59 +01:00
|
|
|
{
|
|
|
|
|
uint64_t start_time = esp_timer_get_time();
|
|
|
|
|
|
|
|
|
|
// Execute the command using the DShotRMT instance
|
2025-09-29 15:51:12 +01:00
|
|
|
dshot_result_t result = sendCommand(command);
|
2025-09-18 15:10:59 +01:00
|
|
|
|
|
|
|
|
uint64_t end_time = esp_timer_get_time();
|
|
|
|
|
_last_command_timestamp = end_time;
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// Private Initialization Functions
|
2025-09-07 12:48:48 +01:00
|
|
|
dshot_result_t DShotRMT::_initTXChannel()
|
2025-08-30 23:21:03 +01:00
|
|
|
{
|
|
|
|
|
_tx_channel_config.gpio_num = _gpio;
|
|
|
|
|
_tx_channel_config.clk_src = DSHOT_CLOCK_SRC_DEFAULT;
|
|
|
|
|
_tx_channel_config.resolution_hz = DSHOT_RMT_RESOLUTION;
|
2025-09-01 13:41:48 +01:00
|
|
|
_tx_channel_config.mem_block_symbols = RMT_BUFFER_SYMBOLS;
|
2025-09-02 14:18:37 +01:00
|
|
|
_tx_channel_config.trans_queue_depth = RMT_QUEUE_DEPTH;
|
2025-08-30 23:21:03 +01:00
|
|
|
|
2025-09-25 15:15:36 +01:00
|
|
|
_rmt_tx_config.loop_count = 0; // No automatic loops - real-time calculation
|
2025-09-18 10:23:39 +01:00
|
|
|
_rmt_tx_config.flags.eot_level = _is_bidirectional ? 1 : 0;
|
2025-08-30 23:21:03 +01:00
|
|
|
|
|
|
|
|
if (rmt_new_tx_channel(&_tx_channel_config, &_rmt_tx_channel) != DSHOT_OK)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_TX_INIT_FAILED};
|
2025-09-07 12:48:48 +01:00
|
|
|
}
|
2025-09-30 19:37:25 +01:00
|
|
|
|
2025-09-07 22:21:23 +01:00
|
|
|
if (rmt_enable(_rmt_tx_channel) != DSHOT_OK)
|
2025-09-07 12:48:48 +01:00
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_TX_INIT_FAILED};
|
2025-08-30 23:21:03 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-29 15:51:12 +01:00
|
|
|
return {true, dshot_msg_code_t::DSHOT_TX_INIT_SUCCESS};
|
2025-08-30 23:21:03 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-07 12:48:48 +01:00
|
|
|
dshot_result_t DShotRMT::_initRXChannel()
|
2025-08-30 23:21:03 +01:00
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
// Double check if bidirectional mode is enabled
|
2025-09-17 20:41:20 +01:00
|
|
|
if (!_is_bidirectional)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {true, dshot_msg_code_t::DSHOT_NONE};
|
2025-09-17 20:41:20 +01:00
|
|
|
}
|
2025-08-31 19:23:04 +01:00
|
|
|
|
2025-08-30 23:21:03 +01:00
|
|
|
_rx_channel_config.gpio_num = _gpio;
|
|
|
|
|
_rx_channel_config.clk_src = DSHOT_CLOCK_SRC_DEFAULT;
|
|
|
|
|
_rx_channel_config.resolution_hz = DSHOT_RMT_RESOLUTION;
|
2025-09-01 13:41:48 +01:00
|
|
|
_rx_channel_config.mem_block_symbols = RMT_BUFFER_SYMBOLS;
|
2025-08-30 23:21:03 +01:00
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Filter for pulses that are within a reasonable range for DShot telemetry
|
2025-09-30 09:01:47 +01:00
|
|
|
_rmt_rx_config.signal_range_min_ns = DSHOT_PULSE_MIN_NS;
|
|
|
|
|
_rmt_rx_config.signal_range_max_ns = DSHOT_PULSE_MAX_NS;
|
2025-08-30 23:21:03 +01:00
|
|
|
|
|
|
|
|
if (rmt_new_rx_channel(&_rx_channel_config, &_rmt_rx_channel) != DSHOT_OK)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_RX_INIT_FAILED};
|
2025-08-30 23:21:03 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Register the callback function that will be triggered when a frame is received
|
2025-09-17 20:41:20 +01:00
|
|
|
_rx_event_callbacks.on_recv_done = _on_rx_done;
|
|
|
|
|
if (rmt_rx_register_event_callbacks(_rmt_rx_channel, &_rx_event_callbacks, this) != DSHOT_OK)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_CALLBACK_REGISTERING_FAILED};
|
2025-09-17 20:41:20 +01:00
|
|
|
}
|
2025-09-18 10:23:39 +01:00
|
|
|
|
2025-09-07 22:21:23 +01:00
|
|
|
if (rmt_enable(_rmt_rx_channel) != DSHOT_OK)
|
2025-09-07 12:48:48 +01:00
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_RX_INIT_FAILED};
|
2025-09-07 12:48:48 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Start the receiver to wait for incoming telemetry data
|
2025-09-17 20:41:20 +01:00
|
|
|
rmt_symbol_word_t rx_symbols[GCR_BITS_PER_FRAME];
|
|
|
|
|
size_t rx_size_bytes = GCR_BITS_PER_FRAME * sizeof(rmt_symbol_word_t);
|
|
|
|
|
if (rmt_receive(_rmt_rx_channel, rx_symbols, rx_size_bytes, &_rmt_rx_config) != DSHOT_OK)
|
|
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_RECEIVER_FAILED};
|
2025-09-17 20:41:20 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-29 15:51:12 +01:00
|
|
|
return {true, dshot_msg_code_t::DSHOT_RX_INIT_SUCCESS};
|
2025-08-30 23:21:03 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-07 12:48:48 +01:00
|
|
|
dshot_result_t DShotRMT::_initDShotEncoder()
|
2025-08-30 23:21:03 +01:00
|
|
|
{
|
2025-09-26 18:24:50 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-09-25 15:15:36 +01:00
|
|
|
|
2025-09-26 18:24:50 +01:00
|
|
|
if (rmt_new_bytes_encoder(&encoder_config, &_dshot_encoder) != DSHOT_OK)
|
2025-08-30 23:21:03 +01:00
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_ENCODER_INIT_FAILED};
|
2025-08-30 23:21:03 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-29 15:51:12 +01:00
|
|
|
return {true, dshot_msg_code_t::DSHOT_ENCODER_INIT_SUCCESS};
|
2021-06-29 19:05:20 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// Private Packet Management Functions
|
2025-09-13 10:05:33 +01:00
|
|
|
dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t &value)
|
2025-07-30 23:36:58 +01:00
|
|
|
{
|
2025-08-30 23:21:03 +01:00
|
|
|
dshot_packet_t packet = {};
|
2025-06-14 17:16:45 +01:00
|
|
|
|
2025-09-15 14:56:04 +01:00
|
|
|
packet.throttle_value = value & DSHOT_THROTTLE_MAX;
|
2025-08-30 23:21:03 +01:00
|
|
|
packet.telemetric_request = _is_bidirectional ? 1 : 0;
|
2025-09-05 11:16:19 +01:00
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// The data for CRC calculation includes the 11-bit value and the 1-bit telemetry flag
|
|
|
|
|
uint16_t data_for_crc = (packet.throttle_value << 1) | packet.telemetric_request;
|
|
|
|
|
packet.checksum = _calculateCRC(data_for_crc);
|
2025-07-30 23:36:58 +01:00
|
|
|
|
2025-08-30 23:21:03 +01:00
|
|
|
return packet;
|
2025-06-14 17:16:45 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 13:03:55 +01:00
|
|
|
uint16_t DShotRMT::_buildDShotFrameValue(const dshot_packet_t &packet)
|
2025-06-17 19:56:50 +01:00
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
// Combine throttle, telemetry bit, and CRC into a single 16-bit frame
|
2025-09-01 15:31:50 +01:00
|
|
|
uint16_t data_and_telemetry = (packet.throttle_value << 1) | packet.telemetric_request;
|
2025-09-15 14:56:04 +01:00
|
|
|
return (data_and_telemetry << 4) | packet.checksum;
|
2025-06-17 19:56:50 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-15 13:40:47 +01:00
|
|
|
uint16_t DShotRMT::_calculateCRC(const uint16_t &data)
|
2025-06-17 19:56:50 +01:00
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
// Standard DShot CRC calculation using XOR
|
2025-09-08 13:40:32 +01:00
|
|
|
uint16_t crc = (data ^ (data >> 4) ^ (data >> 8)) & DSHOT_CRC_MASK;
|
2025-08-30 23:21:03 +01:00
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// For bidirectional DShot, the CRC is inverted
|
2025-08-30 23:21:03 +01:00
|
|
|
if (_is_bidirectional)
|
2025-08-17 22:00:43 +01:00
|
|
|
{
|
2025-09-08 13:40:32 +01:00
|
|
|
crc = (~crc) & DSHOT_CRC_MASK;
|
2025-08-17 22:00:43 +01:00
|
|
|
}
|
2025-08-30 23:21:03 +01:00
|
|
|
return crc;
|
2025-06-17 19:56:50 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-15 14:56:04 +01:00
|
|
|
void DShotRMT::_preCalculateRMTTicks()
|
2025-09-13 10:54:30 +01:00
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
// Pre-calculate all timing values in RMT ticks to save CPU cycles later
|
2025-09-15 13:40:47 +01:00
|
|
|
_rmt_ticks.bit_length_ticks = static_cast<uint16_t>(_dshot_timing.bit_length_us * RMT_TICKS_PER_US);
|
2025-09-13 10:54:30 +01:00
|
|
|
_rmt_ticks.t1h_ticks = static_cast<uint16_t>(_dshot_timing.t1h_lenght_us * RMT_TICKS_PER_US);
|
2025-09-18 10:23:39 +01:00
|
|
|
_rmt_ticks.t0h_ticks = _rmt_ticks.t1h_ticks >> 1; // High time for a 1 is always double of 0
|
2025-09-15 13:40:47 +01:00
|
|
|
_rmt_ticks.t1l_ticks = _rmt_ticks.bit_length_ticks - _rmt_ticks.t1h_ticks;
|
|
|
|
|
_rmt_ticks.t0l_ticks = _rmt_ticks.bit_length_ticks - _rmt_ticks.t0h_ticks;
|
2025-09-13 10:54:30 +01:00
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Calculate the minimum time required between frames
|
2025-09-13 10:54:30 +01:00
|
|
|
// Pause between frames is frame time in us, some padding and about 30 us is added by hardware
|
2025-09-15 14:56:04 +01:00
|
|
|
_frame_timer_us = (static_cast<uint64_t>(_dshot_timing.bit_length_us * DSHOT_BITS_PER_FRAME) << 1) + DSHOT_PADDING_US;
|
2025-09-13 10:54:30 +01:00
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// For bidirectional, double up
|
2025-09-13 10:54:30 +01:00
|
|
|
if (_is_bidirectional)
|
|
|
|
|
{
|
|
|
|
|
_frame_timer_us = (_frame_timer_us << 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Private Frame Processing Functions
|
2025-09-05 11:16:19 +01:00
|
|
|
dshot_result_t DShotRMT::_sendDShotFrame(const dshot_packet_t &packet)
|
2025-06-13 20:50:00 +01:00
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
// Ensure enough time has passed since the last transmission
|
2025-09-26 18:24:50 +01:00
|
|
|
if (!_isFrameIntervalElapsed())
|
2025-08-28 12:22:42 +01:00
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {true, dshot_msg_code_t::DSHOT_NONE};
|
2025-08-28 12:22:42 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-26 18:24:50 +01:00
|
|
|
_encoded_frame_value = _buildDShotFrameValue(packet);
|
2025-08-05 23:32:03 +01:00
|
|
|
|
2025-09-26 18:24:50 +01:00
|
|
|
// 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);
|
2025-08-30 20:56:34 +01:00
|
|
|
|
2025-09-26 18:24:50 +01:00
|
|
|
// The DShot frame is 16 bits, which is 2 bytes
|
|
|
|
|
size_t tx_size_bytes = sizeof(swapped_value);
|
2025-09-26 18:01:55 +01:00
|
|
|
|
2025-09-26 18:24:50 +01:00
|
|
|
if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, &swapped_value, tx_size_bytes, &_rmt_tx_config) != DSHOT_OK)
|
2025-08-28 12:22:42 +01:00
|
|
|
{
|
2025-09-29 15:51:12 +01:00
|
|
|
return {false, dshot_msg_code_t::DSHOT_TRANSMISSION_FAILED};
|
2025-07-30 23:36:58 +01:00
|
|
|
}
|
2025-08-28 12:22:42 +01:00
|
|
|
|
2025-09-26 18:24:50 +01:00
|
|
|
_recordFrameTransmissionTime(); // Reset the timer for the next frame
|
2025-09-04 13:41:05 +01:00
|
|
|
|
2025-09-29 15:51:12 +01:00
|
|
|
return {true, dshot_msg_code_t::DSHOT_TRANSMISSION_SUCCESS};
|
2025-07-30 23:36:58 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-26 18:01:55 +01:00
|
|
|
// This function needs to be fast, as it generates the RMT symbols just before sending
|
2025-07-17 22:24:28 +01:00
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// 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)
|
2025-07-17 22:24:28 +01:00
|
|
|
{
|
2025-09-04 13:41:05 +01:00
|
|
|
uint32_t gcr_value = 0;
|
|
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Step 1: Decode RMT symbols into a 21-bit GCR (Group Code Recording) value.
|
|
|
|
|
// The ESC sends back a signal where the duration determines the bit value.
|
2025-09-03 10:16:14 +01:00
|
|
|
for (size_t i = 0; i < GCR_BITS_PER_FRAME; ++i)
|
2025-07-17 22:24:28 +01:00
|
|
|
{
|
2025-09-01 13:41:48 +01:00
|
|
|
bool bit_is_one = symbols[i].duration0 > symbols[i].duration1;
|
2025-09-04 13:41:05 +01:00
|
|
|
gcr_value = (gcr_value << 1) | bit_is_one;
|
2025-09-01 13:41:48 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Step 2: Perform GCR decoding (GCR = Value ^ (Value >> 1))
|
2025-09-04 13:41:05 +01:00
|
|
|
uint32_t decoded_frame = gcr_value ^ (gcr_value >> 1);
|
|
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Step 3: Extract the 16-bit DShot frame from the decoded data
|
2025-09-08 13:40:32 +01:00
|
|
|
uint16_t data_and_crc = (decoded_frame & DSHOT_FULL_PACKET);
|
2025-09-03 10:16:14 +01:00
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Step 4: Extract data and CRC from the 16-bit frame
|
2025-09-25 13:03:55 +01:00
|
|
|
uint16_t received_data = data_and_crc >> DSHOT_CRC_BIT_SHIFT;
|
2025-09-08 13:40:32 +01:00
|
|
|
uint16_t received_crc = data_and_crc & DSHOT_CRC_MASK;
|
2025-09-01 13:41:48 +01:00
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Step 5: A valid response must have the telemetry request bit set to 1. This is a sanity check.
|
2025-09-25 13:03:55 +01:00
|
|
|
if (!((received_data >> DSHOT_TELEMETRY_BIT_POSITION) & 1))
|
2025-09-04 13:41:05 +01:00
|
|
|
{
|
|
|
|
|
return DSHOT_NULL_PACKET;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Step 6: Calculate and validate CRC
|
|
|
|
|
uint16_t calculated_crc = _calculateCRC(received_data);
|
2025-09-01 15:31:50 +01:00
|
|
|
if (received_crc != calculated_crc)
|
2025-07-19 15:41:04 +01:00
|
|
|
{
|
2025-08-30 20:56:34 +01:00
|
|
|
return DSHOT_NULL_PACKET;
|
2025-07-19 15:41:04 +01:00
|
|
|
}
|
2025-07-17 22:24:28 +01:00
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Return the eRPM value (first 11 bits).
|
2025-09-08 13:40:32 +01:00
|
|
|
return received_data & DSHOT_THROTTLE_MAX;
|
2025-07-30 23:36:58 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-17 20:41:20 +01:00
|
|
|
// Timing Control Functions
|
2025-09-26 18:24:50 +01:00
|
|
|
bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed()
|
2025-08-30 23:21:03 +01:00
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
// Check if the minimum interval between frames has passed
|
2025-09-03 10:16:14 +01:00
|
|
|
uint64_t current_time = esp_timer_get_time();
|
2025-09-12 22:14:34 +01:00
|
|
|
uint64_t elapsed = current_time - _last_transmission_time_us;
|
2025-08-30 23:21:03 +01:00
|
|
|
return elapsed >= _frame_timer_us;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-26 18:24:50 +01:00
|
|
|
void DShotRMT::_recordFrameTransmissionTime()
|
2025-08-30 23:21:03 +01:00
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
// Record the time of the current transmission
|
2025-09-12 22:14:34 +01:00
|
|
|
_last_transmission_time_us = esp_timer_get_time();
|
2025-08-30 23:21:03 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-18 10:23:39 +01:00
|
|
|
// Static Callback Functions
|
|
|
|
|
// This function is called by the RMT driver's ISR when a frame is received
|
2025-09-15 10:58:56 +01:00
|
|
|
bool IRAM_ATTR DShotRMT::_on_rx_done(rmt_channel_handle_t rmt_rx_channel, const rmt_rx_done_event_data_t *edata, void *user_data)
|
2025-08-28 12:22:42 +01:00
|
|
|
{
|
2025-09-13 10:54:30 +01:00
|
|
|
DShotRMT *instance = static_cast<DShotRMT *>(user_data);
|
2025-08-30 20:56:34 +01:00
|
|
|
|
2025-09-17 20:41:20 +01:00
|
|
|
if (edata && edata->num_symbols == GCR_BITS_PER_FRAME)
|
2025-08-30 20:56:34 +01:00
|
|
|
{
|
2025-09-13 10:54:30 +01:00
|
|
|
uint16_t erpm = instance->_decodeDShotFrame(edata->received_symbols);
|
2025-09-18 10:23:39 +01:00
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
if (erpm != DSHOT_NULL_PACKET)
|
2025-08-30 20:56:34 +01:00
|
|
|
{
|
2025-09-18 10:23:39 +01:00
|
|
|
// Atomically store the new eRPM value and set the flag
|
2025-09-17 20:41:20 +01:00
|
|
|
instance->_last_erpm_atomic.store(erpm);
|
|
|
|
|
instance->_telemetry_ready_flag_atomic.store(true);
|
2025-08-30 20:56:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
return false;
|
2025-09-25 15:15:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Public Static Utility Functions
|
|
|
|
|
void DShotRMT::printDShotInfo(const DShotRMT &dshot_rmt, Stream &output)
|
|
|
|
|
{
|
|
|
|
|
output.println("\n === DShot Signal Info === ");
|
2025-09-30 09:01:47 +01:00
|
|
|
|
|
|
|
|
uint16_t dshot_mode_val = 0;
|
|
|
|
|
switch (dshot_rmt.getMode())
|
|
|
|
|
{
|
|
|
|
|
case dshot_mode_t::DSHOT150:
|
|
|
|
|
dshot_mode_val = 150;
|
|
|
|
|
break;
|
|
|
|
|
case dshot_mode_t::DSHOT300:
|
|
|
|
|
dshot_mode_val = 300;
|
|
|
|
|
break;
|
|
|
|
|
case dshot_mode_t::DSHOT600:
|
|
|
|
|
dshot_mode_val = 600;
|
|
|
|
|
break;
|
|
|
|
|
case dshot_mode_t::DSHOT1200:
|
|
|
|
|
dshot_mode_val = 1200;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
output.printf("Current Mode: DSHOT%d\n", dshot_mode_val);
|
2025-09-30 19:37:25 +01:00
|
|
|
|
2025-09-25 15:15:36 +01:00
|
|
|
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());
|
2025-09-27 22:17:43 +01:00
|
|
|
}
|