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-10-01 23:31:24 +01:00
|
|
|
#include "DShotRMT.h"
|
2025-07-30 23:36:58 +01:00
|
|
|
|
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-10-01 15:50:24 +01:00
|
|
|
_dshot_timing(DSHOT_TIMING_US[_mode])
|
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-10-09 18:41:03 +01:00
|
|
|
rmt_disable(_rmt_tx_channel);
|
|
|
|
|
rmt_del_channel(_rmt_tx_channel);
|
|
|
|
|
_rmt_tx_channel = nullptr;
|
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-10-09 18:41:03 +01:00
|
|
|
rmt_disable(_rmt_rx_channel);
|
|
|
|
|
rmt_del_channel(_rmt_rx_channel);
|
|
|
|
|
_rmt_rx_channel = nullptr;
|
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
|
|
|
// 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-09 18:41:03 +01:00
|
|
|
_cleanupRmtResources(); // Clean up any allocated resources on failure
|
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-07-19 12:26:54 +01:00
|
|
|
{
|
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
|
|
|
{
|
2025-10-09 18:41:03 +01:00
|
|
|
_cleanupRmtResources(); // Clean up any allocated resources on failure
|
2025-10-01 15:50:24 +01:00
|
|
|
return result;
|
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-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)
|
2025-07-19 12:26:54 +01:00
|
|
|
{
|
2025-10-09 18:41:03 +01:00
|
|
|
_cleanupRmtResources(); // Clean up any allocated resources on failure
|
2025-10-01 15:50:24 +01:00
|
|
|
return result;
|
2025-07-19 12:26:54 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-01 23:31:24 +01:00
|
|
|
return {true, 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-10-01 23:31:24 +01:00
|
|
|
return sendCommand(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-10-01 23:31:24 +01:00
|
|
|
return {false, 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-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-09-13 10:54:30 +01:00
|
|
|
{
|
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-09-13 10:54:30 +01:00
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// Get telemetry data
|
2025-10-01 08:48:26 +01:00
|
|
|
dshot_result_t DShotRMT::getTelemetry()
|
2025-09-13 10:54:30 +01:00
|
|
|
{
|
2025-10-01 23:31:24 +01:00
|
|
|
dshot_result_t result = {false, DSHOT_TELEMETRY_FAILED, NO_DSHOT_TELEMETRY, NO_DSHOT_TELEMETRY};
|
2025-09-13 10:54:30 +01:00
|
|
|
|
|
|
|
|
if (!_is_bidirectional)
|
|
|
|
|
{
|
2025-10-01 23:31:24 +01:00
|
|
|
result.result_code = 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)
|
2025-10-01 08:48:26 +01:00
|
|
|
uint16_t final_magnet_count = _motor_magnet_count;
|
2025-09-20 15:47:39 +01:00
|
|
|
|
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-10-01 23:31:24 +01:00
|
|
|
result.result_code = 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
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2025-11-01 19:12:02 +00:00
|
|
|
// Sends a raw DShot command to the ESC.
|
|
|
|
|
dshot_result_t DShotRMT::sendRawCommand(uint16_t command_value)
|
|
|
|
|
{
|
|
|
|
|
_packet = _buildDShotPacket(command_value);
|
|
|
|
|
return _sendDShotFrame(_packet);
|
|
|
|
|
}
|
|
|
|
|
|
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-13 10:54:30 +01:00
|
|
|
}
|
|
|
|
|
|
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-10-09 18:41:03 +01:00
|
|
|
return (command >= dshotCommands_e::DSHOT_CMD_MOTOR_STOP && command <= DSHOT_CMD_MAX);
|
2025-09-18 15:10:59 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-09 18:41:03 +01:00
|
|
|
// Executes a single DShot command by building and sending a DShot frame.
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 10:54:30 +01:00
|
|
|
// 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-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-10-01 08:48:26 +01:00
|
|
|
uint16_t DShotRMT::_calculateCRC(const uint16_t &data) const
|
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-10-09 18:41:03 +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-10-09 18:41:03 +01:00
|
|
|
_rmt_ticks.t0h_ticks = _rmt_ticks.t1h_ticks >> 1; // High time for a '0' bit is half of a '1' bit.
|
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-10-09 18:41:03 +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-13 10:54:30 +01:00
|
|
|
|
2025-10-09 18:41:03 +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-10-01 23:31:24 +01:00
|
|
|
return {true, 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-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-08-28 12:22:42 +01:00
|
|
|
{
|
2025-10-01 23:31:24 +01:00
|
|
|
return {false, 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-10-01 23:31:24 +01:00
|
|
|
return {true, 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-10-09 18:41:03 +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
|
2025-07-17 22:24:28 +01:00
|
|
|
{
|
2025-09-04 13:41:05 +01:00
|
|
|
uint32_t gcr_value = 0;
|
|
|
|
|
|
2025-10-09 18:41:03 +01:00
|
|
|
// Decode RMT symbols into a 21-bit GCR (Group Code Recording) value.
|
2025-09-18 10:23:39 +01:00
|
|
|
// 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-10-09 18:41:03 +01:00
|
|
|
// 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-10-09 18:41:03 +01:00
|
|
|
// 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-10-09 18:41:03 +01:00
|
|
|
// 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-10-09 18:41:03 +01:00
|
|
|
// 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-10-09 18:41:03 +01:00
|
|
|
// Calculate and validate CRC.
|
2025-09-18 10:23:39 +01:00
|
|
|
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-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
|
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
|
|
|
}
|
2025-10-09 18:41:03 +01:00
|
|
|
|
|
|
|
|
void DShotRMT::_cleanupRmtResources()
|
|
|
|
|
{
|
|
|
|
|
if (_rmt_tx_channel)
|
|
|
|
|
{
|
|
|
|
|
rmt_disable(_rmt_tx_channel);
|
|
|
|
|
rmt_del_channel(_rmt_tx_channel);
|
|
|
|
|
_rmt_tx_channel = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_rmt_rx_channel)
|
|
|
|
|
{
|
|
|
|
|
rmt_disable(_rmt_rx_channel);
|
|
|
|
|
rmt_del_channel(_rmt_rx_channel);
|
|
|
|
|
_rmt_rx_channel = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_dshot_encoder)
|
|
|
|
|
{
|
|
|
|
|
rmt_del_encoder(_dshot_encoder);
|
|
|
|
|
_dshot_encoder = nullptr;
|
|
|
|
|
}
|
|
|
|
|
}
|