DShotRMT/src/DShotRMT.cpp

443 lines
14 KiB
C++
Raw Normal View History

/**
* @file DShotRMT.cpp
2025-07-30 23:36:58 +01:00
* @brief DShot signal generation using ESP32 RMT with bidirectional support
* @author Wastl Kraus
* @date 2025-06-11
* @license MIT
*/
2021-07-04 02:01:26 +01:00
2025-10-01 23:31:24 +01:00
#include "DShotRMT.h"
2025-07-30 23:36:58 +01:00
// Constructor with GPIO number
DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool is_bidirectional, uint16_t magnet_count)
: _gpio(gpio),
_mode(mode),
_is_bidirectional(is_bidirectional),
_motor_magnet_count(magnet_count),
2025-10-01 15:50:24 +01:00
_dshot_timing(DSHOT_TIMING_US[_mode])
{
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-08-30 23:21:03 +01:00
// Constructor using pin number
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
{
// Delegates to primary constructor with type cast
2025-08-06 22:57:26 +01:00
}
// Destructor
DShotRMT::~DShotRMT()
{
// Cleanup TX channel
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
}
}
// Cleanup RX channel
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
}
}
// Cleanup encoder
if (_dshot_encoder)
{
rmt_del_encoder(_dshot_encoder);
_dshot_encoder = nullptr;
}
}
// Initialize DShotRMT
2025-09-05 11:16:19 +01:00
dshot_result_t DShotRMT::begin()
2022-11-25 15:08:58 +00:00
{
2025-10-01 15:50:24 +01:00
dshot_result_t result = init_rmt_tx_channel(_gpio, &_rmt_tx_channel, _is_bidirectional);
2025-10-01 23:31:24 +01:00
2025-10-01 15:50:24 +01:00
if (!result.success)
2025-09-17 20:41:20 +01:00
{
2025-10-01 15:50:24 +01:00
return result;
2025-09-17 20:41:20 +01:00
}
2025-08-31 10:43:48 +01:00
if (_is_bidirectional)
{
2025-10-01 15:50:24 +01:00
result = init_rmt_rx_channel(_gpio, &_rmt_rx_channel, &_rx_event_callbacks, this);
if (!result.success)
2025-08-31 10:43:48 +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-10-01 15:50:24 +01:00
return result;
2025-08-31 10:43:48 +01:00
}
}
2025-07-29 23:40:09 +01:00
2025-10-01 15:50:24 +01:00
result = init_dshot_encoder(&_dshot_encoder, _rmt_ticks, _pulse_level, _idle_level);
2025-10-01 23:31:24 +01:00
2025-10-01 15:50:24 +01:00
if (!result.success)
{
// Cleanup previously allocated channels on failure
rmt_disable(_rmt_tx_channel);
rmt_del_channel(_rmt_tx_channel);
_rmt_tx_channel = nullptr;
2025-09-25 15:15:36 +01:00
if (_rmt_rx_channel)
{
rmt_disable(_rmt_rx_channel);
rmt_del_channel(_rmt_rx_channel);
_rmt_rx_channel = nullptr;
}
2025-10-01 15:50:24 +01:00
return result;
}
2025-10-01 23:31:24 +01:00
return {true, DSHOT_INIT_SUCCESS};
2025-08-30 23:21:03 +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
if (throttle == 0)
{
2025-10-01 23:31:24 +01:00
return sendCommand(DSHOT_CMD_MOTOR_STOP);
}
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-18 10:23:39 +01:00
_packet = _buildDShotPacket(_last_throttle);
return _sendDShotFrame(_packet);
}
// Send throttle value as a percentage
dshot_result_t DShotRMT::sendThrottlePercent(float percent)
{
if (percent < 0.0f || percent > 100.0f)
{
2025-10-01 23:31:24 +01:00
return {false, DSHOT_PERCENT_NOT_IN_RANGE};
}
// 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-10-01 15:50:24 +01:00
// Sends a DShot command (0-47) to the ESC by accepting an integer value.
dshot_result_t DShotRMT::sendCommand(uint16_t command_value)
{
2025-10-01 15:50:24 +01:00
// Validate the integer command value before casting
if (command_value < DSHOT_CMD_MOTOR_STOP || command_value > DSHOT_CMD_MAX)
{
2025-10-01 23:31:24 +01:00
return {false, DSHOT_COMMAND_NOT_VALID};
2025-10-01 15:50:24 +01:00
}
return sendCommand(static_cast<dshotCommands_e>(command_value));
}
2025-10-01 15:50:24 +01:00
// Sends a DShot command (0-47) to the ESC.
dshot_result_t DShotRMT::sendCommand(dshotCommands_e command)
{
uint16_t repeat_count = DEFAULT_CMD_REPEAT_COUNT;
uint16_t delay_us = DEFAULT_CMD_DELAY_US;
switch (command)
{
case DSHOT_CMD_SAVE_SETTINGS:
case DSHOT_CMD_SPIN_DIRECTION_NORMAL:
case DSHOT_CMD_SPIN_DIRECTION_REVERSED:
repeat_count = SETTINGS_COMMAND_REPEATS;
delay_us = SETTINGS_COMMAND_DELAY_US;
break;
default:
// For other commands, use default repeat and delay
break;
}
return sendCommand(command, repeat_count, delay_us);
}
2025-10-01 08:48:26 +01:00
2025-10-01 15:50:24 +01:00
// Sends a DShot command (0-47) to the ESC with a specified repeat count and delay.
dshot_result_t DShotRMT::sendCommand(dshotCommands_e command, uint16_t repeat_count, uint16_t delay_us)
2025-09-18 15:10:59 +01:00
{
2025-10-01 23:31:24 +01:00
dshot_result_t result = {false, DSHOT_UNKNOWN, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY};
2025-09-18 15:10:59 +01:00
2025-10-01 15:50:24 +01:00
if (!_isValidCommand(command))
2025-09-18 15:10:59 +01:00
{
2025-10-01 23:31:24 +01:00
result.result_code = 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++)
{
2025-10-01 15:50:24 +01:00
dshot_result_t single_result = _executeCommand(command);
2025-09-18 15:10:59 +01:00
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-10-01 23:31:24 +01:00
result.result_code = DSHOT_COMMAND_SUCCESS;
2025-09-18 15:10:59 +01:00
}
return result;
}
// Get telemetry data
2025-10-01 08:48:26 +01:00
dshot_result_t DShotRMT::getTelemetry()
{
2025-10-01 23:31:24 +01:00
dshot_result_t result = {false, DSHOT_TELEMETRY_FAILED, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY};
if (!_is_bidirectional)
{
2025-10-01 23:31:24 +01:00
result.result_code = DSHOT_BIDIR_NOT_ENABLED;
return result;
}
// Use stored magnet count if parameter is 0 (default)
2025-10-01 08:48:26 +01:00
uint16_t final_magnet_count = _motor_magnet_count;
2025-09-18 10:23:39 +01:00
// Check if the callback has set the flag for new data
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
if (erpm != DSHOT_NULL_PACKET && final_magnet_count >= MAGNETS_PER_POLE_PAIR)
{
2025-09-18 10:23:39 +01:00
// Calculate motor RPM from eRPM and magnet count
uint8_t pole_pairs = final_magnet_count / MAGNETS_PER_POLE_PAIR;
uint32_t motor_rpm = (erpm / pole_pairs);
result.success = true;
result.erpm = erpm;
result.motor_rpm = motor_rpm;
2025-10-01 23:31:24 +01:00
result.result_code = DSHOT_TELEMETRY_SUCCESS;
}
}
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
2025-10-01 15:50:24 +01:00
return sendCommand(command, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US);
2025-09-18 15:10:59 +01:00
}
// Use with caution
dshot_result_t DShotRMT::saveESCSettings()
{
2025-10-01 15:50:24 +01:00
return sendCommand(dshotCommands_e::DSHOT_CMD_SAVE_SETTINGS, SETTINGS_COMMAND_REPEATS, SETTINGS_COMMAND_DELAY_US);
}
2025-09-18 15:10:59 +01:00
// Simple check
2025-10-01 08:48:26 +01:00
bool DShotRMT::_isValidCommand(dshotCommands_e command) const
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();
2025-10-01 08:48:26 +01:00
_packet = _buildDShotPacket(static_cast<uint16_t>(command));
dshot_result_t result = _sendDShotFrame(_packet);
2025-09-18 15:10:59 +01:00
uint64_t end_time = esp_timer_get_time();
_last_command_timestamp = end_time;
return result;
}
// Private Packet Management Functions
2025-10-01 08:48:26 +01:00
dshot_packet_t DShotRMT::_buildDShotPacket(const uint16_t &value) const
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-10-01 08:48:26 +01:00
uint16_t DShotRMT::_buildDShotFrameValue(const dshot_packet_t &packet) const
{
2025-09-18 10:23:39 +01:00
// Combine throttle, telemetry bit, and CRC into a single 16-bit frame
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-10-01 08:48:26 +01:00
uint16_t DShotRMT::_calculateCRC(const uint16_t &data) const
{
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-09-08 13:40:32 +01:00
crc = (~crc) & DSHOT_CRC_MASK;
}
2025-08-30 23:21:03 +01:00
return crc;
}
2025-09-15 14:56:04 +01:00
void DShotRMT::_preCalculateRMTTicks()
{
2025-09-18 10:23:39 +01:00
// Pre-calculate all timing values in RMT ticks to save CPU cycles later
_rmt_ticks.bit_length_ticks = static_cast<uint16_t>(_dshot_timing.bit_length_us * RMT_TICKS_PER_US);
_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
_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-18 10:23:39 +01:00
// Calculate the minimum time required between frames
// Pause between frames is frame time in us, some padding and about 30 us is added by hardware
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-18 10:23:39 +01:00
// For bidirectional, double up
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-10-01 23:31:24 +01:00
return {true, DSHOT_NONE};
}
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-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-10-01 15:50:24 +01:00
rmt_transmit_config_t tx_config = {}; // Initialize all members to zero
2025-10-01 23:31:24 +01:00
tx_config.loop_count = 0; // No automatic loops - real-time calculation
2025-10-01 15:50:24 +01:00
tx_config.flags.eot_level = _is_bidirectional ? 1 : 0;
if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, &swapped_value, tx_size_bytes, &tx_config) != DSHOT_OK)
{
2025-10-01 23:31:24 +01:00
return {false, DSHOT_TRANSMISSION_FAILED};
2025-07-30 23:36:58 +01:00
}
2025-09-26 18:24:50 +01:00
_recordFrameTransmissionTime(); // Reset the timer for the next frame
2025-10-01 23:31:24 +01:00
return {true, DSHOT_TRANSMISSION_SUCCESS};
2025-07-30 23:36:58 +01:00
}
// This function needs to be fast, as it generates the RMT symbols just before sending
2025-09-18 10:23:39 +01:00
// Placed in IRAM for high performance, as it's called from an ISR context
2025-10-01 08:48:26 +01:00
uint16_t IRAM_ATTR DShotRMT::_decodeDShotFrame(const rmt_symbol_word_t *symbols) const
{
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.
for (size_t i = 0; i < GCR_BITS_PER_FRAME; ++i)
{
bool bit_is_one = symbols[i].duration0 > symbols[i].duration1;
gcr_value = (gcr_value << 1) | bit_is_one;
}
2025-09-18 10:23:39 +01:00
// Step 2: Perform GCR decoding (GCR = Value ^ (Value >> 1))
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-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-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))
{
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);
if (received_crc != calculated_crc)
{
return DSHOT_NULL_PACKET;
}
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-10-01 08:48:26 +01:00
bool IRAM_ATTR DShotRMT::_isFrameIntervalElapsed() const
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
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)
{
DShotRMT *instance = static_cast<DShotRMT *>(user_data);
2025-09-17 20:41:20 +01:00
if (edata && edata->num_symbols == GCR_BITS_PER_FRAME)
{
uint16_t erpm = instance->_decodeDShotFrame(edata->received_symbols);
2025-09-18 10:23:39 +01:00
if (erpm != DSHOT_NULL_PACKET)
{
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);
}
}
return false;
2025-09-25 15:15:36 +01:00
}