...unified

...now featuring a much more "clean" appearance ^^
This commit is contained in:
Wastl Kraus 2025-07-19 16:41:04 +02:00
parent 314e888a38
commit 1adde68072
5 changed files with 226 additions and 196 deletions

View File

@ -1,32 +1,30 @@
name: DShotRMT Example Sketch
name: Build DShotRMT Example Sketch
on:
push:
branches:
- main
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@main
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Repository as Library
run: |
mkdir -p "$HOME/Arduino/libraries"
ln -s "$PWD" "$HOME/Arduino/libraries/."
- name: Install Arduino CLI
- name: Set up Arduino CLI
run: |
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
arduino-cli core update-index
arduino-cli core install esp32:esp32
- name: Compile Sketch
- name: Install DShotRMT as library
run: |
arduino-cli compile --fqbn esp32:esp32:esp32 ${{ github.workspace }}/examples/dshot300
env:
ARDUINO_LIBRARY_PATH: ${{ github.workspace }}/libraries
ARDUINO_DATA_PATH: ${{ github.workspace }}/arduino-data
mkdir -p $HOME/Arduino/libraries
ln -s $PWD $HOME/Arduino/libraries/DShotRMT
- name: Compile dshot300.ino example
run: |
arduino-cli compile --fqbn esp32:esp32:esp32 examples/dshot300/dshot300.ino

View File

@ -11,13 +11,14 @@
// --- DShotRMT Class ---
// This class provides an abstraction for sending and optionally receiving DShot frames.
// It uses ESP32's RMT peripheral for precise timing control, including BiDirectional RX.
DShotRMT::DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool isBidirectional, uint8_t pauseDuration)
: _gpio(gpio), _mode(mode), _isBidirectional(isBidirectional), _pauseDuration(pauseDuration) {}
// Sets up RMT TX and RX channels as well as encoder configuration
// Initializes RMT TX and RX channels and encoder configuration
void DShotRMT::begin()
{
// RX RMT Channel Configuration (for BiDirectional DShot)
// Configure RX RMT Channel for BiDirectional DShot
if (_isBidirectional)
{
_rmt_rx_channel_config = {
@ -42,7 +43,7 @@ void DShotRMT::begin()
_receive_config.signal_range_max_ns = 5000;
}
// TX RMT Channel Configuration
// Configure TX RMT Channel
_rmt_tx_channel_config = {
.gpio_num = _gpio,
.clk_src = DSHOT_CLOCK_SRC_DEFAULT,
@ -51,7 +52,7 @@ void DShotRMT::begin()
.trans_queue_depth = 10,
};
// Configure transmission looping
// Transmission configuration
_transmit_config.loop_count = 0;
_transmit_config.flags.eot_level = _isBidirectional;
@ -66,7 +67,7 @@ void DShotRMT::begin()
return;
}
// Use a copy encoder to send raw symbols
// Create copy encoder for raw symbol transmission
if (!_dshot_encoder)
{
rmt_copy_encoder_config_t enc_cfg = {};
@ -78,112 +79,109 @@ void DShotRMT::begin()
}
}
// Encodes and transmits a valid DShot Throttle value (48 - 2047)
// Encodes and transmits a valid DShot throttle value (48 - 2047)
void DShotRMT::setThrottle(uint16_t throttle)
{
// Safety first - double check input range and 11 bit "translation"
throttle = constrain(throttle, DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX) & 0b0000011111111111;
// Clamp input range and mask to 11 bits
throttle = constrain(throttle, DSHOT_THROTTLE_MIN, DSHOT_THROTTLE_MAX) & 0x7FF;
_lastThrottle = throttle;
// Convert throttle value to DShot Paket Format
// Convert throttle value to DShot packet format
_tx_packet = assambleDShotPaket(_lastThrottle);
// Encode RMT symbols
size_t count = 0;
encodeDShotTX(_tx_packet, _tx_symbols, count);
// Send the packet
// Transmit the packet
if (rmt_transmit(_rmt_tx_channel, _dshot_encoder, _tx_symbols, count * sizeof(rmt_symbol_word_t), &_transmit_config) != 0)
{
Serial.println("Failed to transmit DShot packet");
return;
}
// Take a break
// Pause between frames
esp_rom_delay_us(_pauseDuration);
}
// --- Get eRPM from ESC ---
// Receives and decodes a response frame from ESC containing eRPM info
uint32_t DShotRMT::getERPM()
{
if (_isBidirectional)
{
if (_rmt_rx_channel == nullptr)
Serial.println("No bidirectional DShot support.");
return _last_erpm;
// Attempt to receive a new frame
// Try to receive a new frame
if (!rmt_receive(_rmt_rx_channel, _rx_symbols, sizeof(_rx_symbols), &_receive_config))
Serial.println("No valid DShot frame received");
return _last_erpm;
_last_erpm = decodeDShotRX(_rx_symbols, DSHOT_BITS_PER_FRAME);
return _last_erpm;
}
// Nothing to do here
// No RX possible in non-bidirectional mode
return _last_erpm;
}
// Translate eRPM value to RPM taking magnet count as parameter
// Converts eRPM value to RPM using magnet count
uint32_t DShotRMT::getMotorRPM(uint8_t magnet_count)
{
uint8_t pole_count = magnet_count / 2;
if (pole_count == 0)
pole_count = 1;
uint32_t rpm = getERPM() / pole_count;
return rpm;
return getERPM() / pole_count;
}
// Calculate CRC for DShot Paket
// Calculates CRC for DShot packet
uint16_t DShotRMT::calculateCRC(uint16_t dshot_packet)
{
uint16_t _packet = (dshot_packet << 1) | (_isBidirectional ? 1 : 0);
uint16_t packet = (dshot_packet << 1) | (_isBidirectional ? 1 : 0);
// Clear container before new calculation
// Reset CRC container
_packet_crc = DSHOT_NULL_PACKET;
// CRC calculation for DShot (4 bits)
_packet_crc = ((_packet ^ (_packet >> 4) ^ (_packet >> 8)) & 0b0000000000001111);
_packet_crc = ((packet ^ (packet >> 4) ^ (packet >> 8)) & 0xF);
// CRC is inverted for biDirectional DShot
// CRC is inverted for bidirectional DShot
if (_isBidirectional)
_packet_crc = (~_packet_crc) & 0b0000000000001111;
_packet_crc = (~_packet_crc) & 0xF;
return _packet_crc;
}
// Assamble DShot Paket (11 bit throttle + 1 bit telemetry request + 4 bit crc)
// Assembles DShot packet (11 bit throttle + 1 bit telemetry request + 4 bit CRC)
uint16_t DShotRMT::assambleDShotPaket(uint16_t value)
{
// Dummy conversion to 11 bits
uint16_t _value = value & 0b0000011111111111;
uint16_t throttle = value & 0x7FF;
// Clear container
// Reset packet container
_tx_packet = DSHOT_NULL_PACKET;
// Assemble raw DShot packet and add checksum
_packet_crc = calculateCRC(_value);
_packet_crc = calculateCRC(throttle);
_tx_packet = (_value << 1) | (_isBidirectional ? 1 : 0);
_tx_packet = (throttle << 1) | (_isBidirectional ? 1 : 0);
_tx_packet = (_tx_packet << 4) | _packet_crc;
return _tx_packet;
}
// --- Encode DShot TX Frame ---
// Converts a 16-bit packet into a valid DShot Frame for RMT
// Converts a 16-bit packet into a valid DShot frame for RMT
void DShotRMT::encodeDShotTX(uint16_t dshot_packet, rmt_symbol_word_t *symbols, size_t &count)
{
// Always start encoding from the top
count = 0;
uint32_t ticks_per_bit = 0;
uint32_t ticks_zero_high = 0;
uint32_t ticks_one_high = 0;
// Select timing based on DShot mode
switch (_mode)
{
case DSHOT150:
@ -217,10 +215,10 @@ void DShotRMT::encodeDShotTX(uint16_t dshot_packet, rmt_symbol_word_t *symbols,
uint32_t ticks_zero_low = ticks_per_bit - ticks_zero_high;
uint32_t ticks_one_low = ticks_per_bit - ticks_one_high;
// Fill the 16 DShot-Bits Array with selected timings
// Fill the 16 DShot bits array with selected timings
for (int i = 15; i >= 0; i--)
{
bool bit = (dshot_packet >> i) & 0b0000000000000001;
bool bit = (dshot_packet >> i) & 0x1;
if (_isBidirectional)
{
symbols[count].level0 = 0;
@ -242,35 +240,33 @@ void DShotRMT::encodeDShotTX(uint16_t dshot_packet, rmt_symbol_word_t *symbols,
// Decodes a response frame from ESC containing eRPM info
uint16_t DShotRMT::decodeDShotRX(const rmt_symbol_word_t *symbols, uint32_t count)
{
// Container for received frame
uint16_t _rec_frame = DSHOT_NULL_PACKET;
uint16_t received_frame = DSHOT_NULL_PACKET;
// Fill the Frame bit by bit
// Build the frame bit by bit
for (size_t i = 0; i < DSHOT_BITS_PER_FRAME && i < count; ++i)
{
bool bit = (symbols[i].duration0 < symbols[i].duration1);
_rec_frame = (_rec_frame << 1) | bit;
received_frame = (received_frame << 1) | bit;
}
// Cut the received CRC for checking
uint16_t _temp = _rec_frame >> 4;
// Store the received CRC
uint8_t crc_recv = _rec_frame & 0b0000000000001111;
// Calculate CRC for received frame again
uint8_t crc_calc = (_temp ^ (_temp >> 4) ^ (_temp >> 8)) & 0b0000000000001111;
// Extract CRC and payload
uint16_t payload = received_frame >> 4;
uint8_t crc_received = received_frame & 0xF;
// Calculate CRC for received frame
uint8_t crc_calculated = (payload ^ (payload >> 4) ^ (payload >> 8)) & 0xF;
if (_isBidirectional)
crc_calc = (~crc_calc) & 0b0000000000001111;
crc_calculated = (~crc_calculated) & 0xF;
// Checking CRC
if (crc_recv != crc_calc)
// Check CRC
if (crc_received != crc_calculated)
{
Serial.println("RX - CRC check failed.");
return _last_erpm;
}
// Cut "telemetric" bit leaving "raw" value
uint16_t raw = _temp >> 1;
// Remove telemetry bit, keep raw value
uint16_t raw = payload >> 1;
return _last_erpm = raw;
}

View File

@ -15,24 +15,24 @@
#include <driver/rmt_rx.h>
// --- DShot Protocol Constants ---
static constexpr auto DSHOT_THROTTLE_FAILSAVE = 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 uint16_t DSHOT_THROTTLE_FAILSAVE = 0;
static constexpr uint16_t DSHOT_THROTTLE_MIN = 48;
static constexpr uint16_t DSHOT_THROTTLE_MAX = 2047;
static constexpr uint8_t DSHOT_BITS_PER_FRAME = 16;
static constexpr auto DSHOT_NULL_PACKET = 0b0000000000000000;
static constexpr auto DSHOT_FULL_PACKET = 0b1111111111111111;
static constexpr auto NO_ERPM_SIGNAL = 0;
static constexpr uint16_t DSHOT_NULL_PACKET = 0x0000;
static constexpr uint16_t DSHOT_FULL_PACKET = 0xFFFF;
static constexpr uint16_t NO_ERPM_SIGNAL = 0;
// RMT configuration parameters
static constexpr auto DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT;
static constexpr auto DSHOT_RMT_RESOLUTION = 10 * 1000 * 1000; // 10 MHz Clock
static constexpr rmt_clock_source_t DSHOT_CLOCK_SRC_DEFAULT = RMT_CLK_SRC_DEFAULT;
static constexpr uint32_t DSHOT_RMT_RESOLUTION = 10 * 1000 * 1000; // 10 MHz Clock
static constexpr auto TX_BUFFER_SIZE = DSHOT_BITS_PER_FRAME;
static constexpr auto RX_BUFFER_SIZE = 32; // Padding for RX decoding
static constexpr size_t TX_BUFFER_SIZE = DSHOT_BITS_PER_FRAME;
static constexpr size_t RX_BUFFER_SIZE = 32; // Padding for RX decoding
// DShot Packet
typedef struct dshot_packet_s
// DShot Packet structure
typedef struct
{
uint16_t throttle_value : 11;
bool telemetric_request : 1;
@ -40,7 +40,7 @@ typedef struct dshot_packet_s
} dshot_packet_t;
// --- DShot Mode Selection ---
typedef enum dshot_mode_e
typedef enum
{
DSHOT_OFF,
DSHOT150,
@ -66,20 +66,20 @@ public:
uint32_t getERPM();
uint32_t getMotorRPM(uint8_t magnet_count);
// Accessors for GPIO and DShot Settings
// Accessors for GPIO and DShot settings
gpio_num_t getGPIO() const { return _gpio; }
dshot_mode_t getDShotMode() const { return _mode; }
uint8_t getPauseDuration() const { return _pauseDuration; }
void setPauseDuration(uint8_t pauseDuration) { _pauseDuration = pauseDuration; }
private:
// Calculates the checksum for throttle value
// Calculates the checksum for a DShot packet
uint16_t calculateCRC(uint16_t dshot_packet);
// Assembles DShot packet (11 bit throttle + 1 bit telemetry request + 4 bit crc)
// Assembles DShot packet (11 bit throttle + 1 bit telemetry request + 4 bit CRC)
uint16_t assambleDShotPaket(uint16_t value);
// Converts a 16-bit DShot packet into RMT symbols and appends pause
// Converts a 16-bit DShot packet into RMT symbols
void encodeDShotTX(uint16_t dshot_packet, rmt_symbol_word_t *symbols, size_t &count);
// Decodes the ESC answer
@ -96,7 +96,6 @@ private:
uint16_t _rx_packet = DSHOT_NULL_PACKET;
uint16_t _tx_packet = DSHOT_NULL_PACKET;
uint8_t _packet_crc = 0;
dshot_packet_t _dshot_packet = {};
// --- RMT Channel Handles ---
rmt_channel_handle_t _rmt_rx_channel = nullptr;

125
README.md
View File

@ -1,25 +1,70 @@
[![Arduino CI](https://github.com/derdoktor667/DShotRMT/actions/workflows/esp32.yml/badge.svg?event=push)](https://github.com/derdoktor667/DShotRMT/actions/workflows/esp32.yml)
## DShotRMT - ESP32 Library (Rewrite for ESP-IDF 5)
# DShotRMT - ESP32 Library (Rewrite for ESP-IDF 5)
This is a complete rewrite of the original DShotRMT library to support the new ESP-IDF 5 RMT encoder API (`rmt_tx.h` / `rmt_rx.h`).
The library sends continuous DShot frames with a pause between them and supports all standard DShot modes (150, 300, 600).
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.
**Now with BiDirectional DShot support!**
### Now with BiDirectional DShot Support!!!
The old Version without encoding (rmt.h) is still available by using "oldAPI" Branch.
> The legacy version (using the old `rmt.h` API) is still available in the `oldAPI` branch.
---
## The DShot Protocol
## 🚀 Features
The DShot protocol transmits 16-bit packets to brushless ESCs:
- **All DShot Modes:** DSHOT150, DSHOT300 (default), DSHOT600
- **BiDirectional DShot:** Experimental support for telemetry and RPM feedback
- **Continuous Frames:** Hardware-timed, CPU-independent signal generation
- **Configurable Pause:** Ensures ESCs can reliably detect frame boundaries
- **Simple API:** Easy integration into your Arduino or ESP-IDF project
- 11-bit throttle value
- 1-bit telemetry request
- 4-bit checksum
---
Data is transmitted MSB-first. Pulse timing depends on the selected DShot mode.
## 📦 Installation
Clone this repository and add it to your Arduino libraries or ESP-IDF components.
```sh
git clone https://github.com/derdoktor667/DShotRMT.git
```
---
## ⚡ Quick Start
```cpp
#include <DShotRMT.h>
constexpr gpio_num_t MOTOR_PIN = GPIO_NUM_17;
constexpr dshot_mode_t MODE = DSHOT300;
constexpr bool BIDIRECTIONAL = true;
DShotRMT motor(MOTOR_PIN, MODE, BIDIRECTIONAL);
void setup() {
Serial.begin(115200);
motor.begin();
motor.setThrottle(1000); // Set throttle value (482047)
}
void loop() {
// Optionally read RPM if bidirectional mode is enabled
uint32_t rpm = motor.getMotorRPM(14); // 14 magnets
Serial.println(rpm);
}
```
---
## 📚 DShot Protocol Overview
DShot transmits 16-bit packets to brushless ESCs:
- **11 bits:** Throttle value
- **1 bit:** Telemetry request
- **4 bits:** Checksum (CRC)
Data is sent MSB-first. Pulse timing depends on the selected DShot mode.
| DSHOT | Bitrate | TH1 | TH0 | Bit Time (µs) | Frame Time (µs) |
|-------|-------------|-------|--------|---------------|-----------------|
@ -27,65 +72,59 @@ Data is transmitted MSB-first. Pulse timing depends on the selected DShot mode.
| 300 | 300 kbit/s | 2.50 | 1.25 | 3.33 | ~53.28 |
| 600 | 600 kbit/s | 1.25 | 0.625 | 1.67 | ~26.72 |
Each frame is followed by a pause. This helps ESCs detect separate frames.
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
## 🔒 Checksum Calculation
The checksum is calculated over the first 12 bits (throttle + bit):
The checksum is calculated over the first 12 bits (throttle + telemetry):
```c
crc = (value ^ (value >> 4) ^ (value >> 8)) & 0x0F;
```
### Bidirectional DSHOT
Bidirectional DSHOT is also known as inverted DSHOT, because the signal level is inverted, so 1 is low and a 0 is high. This is done in order to let the ESC know, that we are operating in bidirectional mode and that it should be sending back telemetry packages.
#### Calculating the Bidirectional CRC
The calculation of the checksum is basically the same as before, but the inverted:
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;
```
...biDirectional DShot is experimental. Further Hardware testing needed.
> **Note:** Bidirectional DShot is experimental. Further hardware testing is needed.
---
## RMT on the ESP32
The RMT (Remote Control) is a peripheral designed to generate accurate and stable signals to control external devices such as LEDs, motors, and other peripherals. It is well suited for generating the DShot signals in a high-performance and accurate way on the ESP32 platform.
### Advantages:
## 🛠️ ESP32 RMT Peripheral
The RMT (Remote Control) peripheral generates accurate, hardware-timed signals for controlling external devices.
Perfect for DShot:
- Hardware-timed pulses
- CPU-independent signal generation
- CPU-independent
- Loop mode with inter-frame pause
- Reliable under system load
---
## About This Library
## 📝 API Reference
This C++ library provides a simple class to generate DShot signals using any RMT-capable GPIO.
It uses a `copy_encoder` to continuously send a prebuilt symbol buffer. New throttle values are applied only when they change.
- `DShotRMT(gpio_num_t gpio, dshot_mode_t mode, bool isBidirectional, uint8_t pauseDuration = 120)`
- `void begin()`
- `void setThrottle(uint16_t throttle)`
- `uint32_t getERPM()`
- `uint32_t getMotorRPM(uint8_t magnet_count)`
### Supported Modes (optional BiDirectional):
- DSHOT150
- DSHOT300 (default)
- DSHOT600
### Frame Structure:
- 16-bit DShot data
- 21-bit times worth of pause
See [examples/dshot300/dshot300.ino](examples/dshot300/dshot300.ino) for a full demo.
---
## References
## 📖 References
- [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/)
@ -93,14 +132,14 @@ It uses a `copy_encoder` to continuously send a prebuilt symbol buffer. New thro
---
## License
## 📄 License
MIT License see LICENSE
MIT License see [LICENSE](LICENSE)
---
## Author
## 👤 Author
Wastl Kraus
**Wastl Kraus**
GitHub: [@derdoktor667](https://github.com/derdoktor667)
Website: [wir-sind-die-matrix.de](https://wir-sind-die-matrix.de)

View File

@ -11,17 +11,17 @@
// USB serial port settings
constexpr auto &USB_SERIAL = Serial0;
constexpr auto USB_SERIAL_BAUD = 115200;
constexpr uint32_t USB_SERIAL_BAUD = 115200;
// Motor configuration
constexpr auto MOTOR01_PIN = GPIO_NUM_17;
constexpr auto DSHOT_MODE = DSHOT300;
constexpr gpio_num_t MOTOR01_PIN = GPIO_NUM_17;
constexpr dshot_mode_t DSHOT_MODE = DSHOT300;
// BiDirectional DShot Support (default: false)
constexpr auto IS_BIDIRECTIONAL = false;
constexpr bool IS_BIDIRECTIONAL = false;
// Motor Magnet count for RPM calculation
constexpr auto MOTOR01_MAGNET_COUNT = 14;
// Motor magnet count for RPM calculation
constexpr uint8_t MOTOR01_MAGNET_COUNT = 14;
// Setup Motor Pin, DShot Mode and optional BiDirectional Support
DShotRMT motor01(MOTOR01_PIN, DSHOT_MODE, IS_BIDIRECTIONAL);
@ -32,7 +32,6 @@ void printRPMPeriodically(uint16_t throttle);
// Reads throttle value from serial input
uint16_t readSerialThrottle();
//
void setup()
{
// Start the USB Serial Port
@ -49,7 +48,6 @@ void setup()
USB_SERIAL.println("Enter a throttle value (482047):");
}
//
void loop()
{
// Read value input from Serial
@ -80,7 +78,7 @@ uint16_t readSerialThrottle()
if (throttle_input < DSHOT_THROTTLE_MIN || throttle_input > DSHOT_THROTTLE_MAX)
{
USB_SERIAL.println("Invalid input. Please enter a value between 48 and 2047");
USB_SERIAL.println("Invalid input. Please enter a value between 48 and 2047.");
}
else
{