[HELP] RMT single wire bidirectional communication (dshot telemetry)

User avatar
citoyx
Posts: 14
Joined: Sat Sep 28, 2019 10:14 am

Re: [HELP] RMT single wire bidirectional communication (dshot telemetry)

Postby citoyx » Mon Dec 23, 2024 8:26 pm

Thank you very much.

Despite the fact my need is now old stories, regarding the lake of pertinent answer, i really appreciate that not only you find a solution to my question, but you took the time to reply here.

As far as im not much into mcu dev since few years now, i really wanted to express to you my gratitude.

Now, may be will take the time to catch up with your solution and try to test it on my old ESC/bldc motor.

Thanks a lot again

PurpleLilac
Posts: 1
Joined: Tue Apr 29, 2025 8:02 am

Re: [HELP] RMT single wire bidirectional communication (dshot telemetry)

Postby PurpleLilac » Tue Apr 29, 2025 8:28 am

Have you found a way yet?
I'm also very interested in using bidirectional DShot in one of my projects, but am failing to do it in a clean manner. Two not so good ways:
1) Use the telemetry wire, the telemetry frame (115200 baud, Serial 8N1) limits you to roughly 1kHz of signal rate, but for your purpose this should be ok (notice: slower erpm updates, no GCR encoding, ...)
2) Use a 1kOhm resistor between two GPIOs. One is the output, one is the input. Now connect the ESC signal wire to the input pin. The input pin will see this: dshot signal.png. From there, I think you know where to go (GCR decoding etc.)https://brushlesswhoop.com/dshot-and-bi ... nal-dshot/

But if you have found a cleaner (software) solution by using one pin as in- and output, I would be very interested to know, how you did it.
For me, this will be a long going project, so even if you find a solution in a year or more, please reply :)
Hello,
I am also trying to capture the ESC signal and i have been successful in that regard using an STM32. However, I have trouble in deciphering the captured data into eRPM. In the picture you provided and in my own testing, there seems to be only 20 bits coming back from the ESC, instead of 21. Maybe I'm missing something, but how would one go about decoding the signal from ESC?

well_tech
Posts: 9
Joined: Wed Feb 28, 2024 9:19 am

Re: [HELP] RMT single wire bidirectional communication (dshot telemetry)

Postby well_tech » Sun Jun 07, 2026 5:38 pm

Hi
I am also searching for bidirectional Dshot implementation option for ESP32-S3 and studying the thing found that in IDF5.5 there were flags .io_loop_back and .io_od_mode for rmt_tx_channel_config_t and rmt_rx_channel_config_t what (in theory) solves exactly the problem of bidirectional communication via single pin. But in 6.0 version I do not see these flags for config_t structures. Does anybody knows if they have been removed for a reason?

MicroController
Posts: 2667
Joined: Mon Oct 17, 2022 7:38 pm
Location: Europe, Germany

Re: [HELP] RMT single wire bidirectional communication (dshot telemetry)

Postby MicroController » Sun Jun 07, 2026 6:32 pm

You should be able to just initialize TX&RX on the same pin, then set that pin to OD via gpio_od_enable().

well_tech
Posts: 9
Joined: Wed Feb 28, 2024 9:19 am

Re: [HELP] RMT single wire bidirectional communication (dshot telemetry)

Postby well_tech » Sun Jun 07, 2026 8:22 pm

Hm, ok, thanks, will look into this

well_tech
Posts: 9
Joined: Wed Feb 28, 2024 9:19 am

Re: [HELP] RMT single wire bidirectional communication (dshot telemetry)

Postby well_tech » Sun Jun 21, 2026 8:43 pm

Hi

I am struggling to make bidirectional dshot working, somewhere close but cannot figure out trivial case - when there is no response from ESC (just steady high level line after TX is finished).
Below is the code with general logic:
1) sending some data (with enabled end of TX interrupt callback)
2) when TX interrupt occures launch rmt_receive function (with with enabled end of RX interrupt callback)
3) when RX interrurt occures save received symbols and notify main task that something is received

I observe the following weird behaviour
- if .signal_range_max_ns is small (1000) analyser shows that RX interrupt occures before TX that does not make sense for me
- if .signal_range_max_ns is larger (5000) every 2nd cycle I receive an error rmt_receive(399): channel not in enable state (while other cycle behaves reasonable).

Looks like some conflict with RMT FSM but I am out of ideas

Code: Select all

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/rmt_tx.h"
#include "driver/rmt_rx.h"
#include "hal/gpio_ll.h"  
#include "esp_log.h"
#include "driver/gpio.h"

#define DSHOT_GPIO              4     
#define RMT_BASE_CLK_HZ        40000000 
#define RX_BUFFER_SIZE         48
#define NUMBER_OF_BLDC_POLES   14

#define LED_TX  17
#define LED_RX  18
#define LED_GEN  8

static rmt_channel_handle_t tx_chan = NULL;
static rmt_channel_handle_t rx_chan = NULL;
static rmt_encoder_handle_t copy_encoder = NULL;

static rmt_symbol_word_t tx_buffer[17];
static rmt_symbol_word_t rx_buffer[RX_BUFFER_SIZE];

static TaskHandle_t main_task_handle = NULL;
static volatile uint8_t is_timeout = 0;
static volatile uint8_t is_buffer_full;

static const rmt_receive_config_t rx_trans_config = {
    .signal_range_min_ns = 500,
    .signal_range_max_ns = 5000,    //1000
};
static const rmt_transmit_config_t tx_trans_config = {
    .loop_count = 0,
    .flags.eot_level = 1, // high EOT level
};

void print_binary32(uint32_t num) {
    for (int i = 31; i >= 0; i--) {
        int bit = (num >> i) & 1;
        putchar(bit ? '1' : '0');
         if (i % 8 == 0) {
            putchar(' ');
        }
    }
    putchar('\n');
}

//TX callback
static bool IRAM_ATTR dshot_tx_done_callback(rmt_channel_handle_t tx_chan, const rmt_tx_done_event_data_t *edata, void *user_ctx) {
    rmt_channel_handle_t rx_chan = (rmt_channel_handle_t)user_ctx;
//activating receiver
    rmt_receive(rx_chan, rx_buffer, sizeof(rx_buffer), &rx_trans_config);
    gpio_set_level(LED_TX, 1);
    return false;
}


//RX callback
static bool IRAM_ATTR test_rx_done_callback(rmt_channel_handle_t rx_chan, const rmt_rx_done_event_data_t *edata, void *user_ctx) {
    uint32_t rx_interrupt_info = edata->num_symbols;
//determining interrupt reason
    if (edata->flags.is_last) rx_interrupt_info |= 0x80000000;        //static level timeout flag
    if (edata->num_symbols >= RX_BUFFER_SIZE) rx_interrupt_info |= 0x40000000;   //RX buffer overwhelming   
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    gpio_set_level(LED_RX, 1);
    xTaskGenericNotifyFromISR(main_task_handle, 0, rx_interrupt_info, eSetValueWithOverwrite, NULL, &xHigherPriorityTaskWoken); 
    return xHigherPriorityTaskWoken == pdTRUE;
}

// BiDShot300 TX packet (16 bits + 1 Reset marker)
void prepare_test_packet(uint16_t throttle) {
    uint16_t packet = throttle << 4;
    uint16_t crc = (~(throttle ^ (throttle >> 4) ^ (throttle >> 8))) & 0x0F;
    packet |= crc;
    const uint32_t bit_total = 133;
    const uint32_t bit1_low = 100;  
    const uint32_t bit0_low = 40;   

    for (int i = 0; i < 16; i++) {
        uint16_t bit = (packet << i) & 0x8000;
        if (bit) {
            tx_buffer[i] = (rmt_symbol_word_t) {
                .level0 = 0, .duration0 = bit1_low,
                .level1 = 1, .duration1 = bit_total - bit1_low
            };
        } else {
            tx_buffer[i] = (rmt_symbol_word_t) {
                .level0 = 0, .duration0 = bit0_low,
                .level1 = 1, .duration1 = bit_total - bit0_low
            };
        }
    }
}


void bidishot_hw_init() {

  gpio_reset_pin(LED_TX);
  gpio_set_direction(LED_TX, GPIO_MODE_OUTPUT);
  gpio_set_level(LED_TX, 0);

  gpio_reset_pin(LED_RX);
  gpio_set_direction(LED_RX, GPIO_MODE_OUTPUT);
  gpio_set_level(LED_RX, 0);

  gpio_reset_pin(LED_GEN);
  gpio_set_direction(LED_GEN, GPIO_MODE_OUTPUT);
  gpio_set_level(LED_GEN, 0);

//RX channel parameters
    rmt_rx_channel_config_t rx_config = {
        .gpio_num = DSHOT_GPIO,
        .clk_src = RMT_CLK_SRC_DEFAULT,
        .resolution_hz = RMT_BASE_CLK_HZ,
        .mem_block_symbols = 48,
    };
    ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_config, &rx_chan));

//RX callback registration
    rmt_rx_event_callbacks_t cbs = {
        .on_recv_done = test_rx_done_callback,
    };
    ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_chan, &cbs, NULL));
//RX channel enabing
    ESP_ERROR_CHECK(rmt_enable(rx_chan));


//TX channel parameters (same pin)
    rmt_tx_channel_config_t tx_config = {
        .gpio_num = DSHOT_GPIO,
        .clk_src = RMT_CLK_SRC_DEFAULT,
        .resolution_hz = RMT_BASE_CLK_HZ,
        .mem_block_symbols = 48,
        .trans_queue_depth = 1,
    };
    ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_config, &tx_chan));

//TX callback registration
    rmt_tx_event_callbacks_t tx_cbs = {
        .on_trans_done = dshot_tx_done_callback,
    };
    ESP_ERROR_CHECK(rmt_tx_register_event_callbacks(tx_chan, &tx_cbs, rx_chan));

//creating copy encoder
    rmt_copy_encoder_config_t copy_encoder_config = {};
    ESP_ERROR_CHECK(rmt_new_copy_encoder(&copy_encoder_config, &copy_encoder));

//enable open drain 
    gpio_ll_od_enable(&GPIO, DSHOT_GPIO);

//TX channel enabing
    ESP_ERROR_CHECK(rmt_enable(tx_chan));
}


void dshot_test(void) {
    main_task_handle = xTaskGetCurrentTaskHandle();
    
	bidishot_hw_init();
    while (1) {

//preparing any packet to send
        prepare_test_packet(0);
//sending
        ESP_ERROR_CHECK(rmt_transmit(tx_chan, copy_encoder, tx_buffer, 16 * sizeof(rmt_symbol_word_t), &tx_trans_config));
//waiting for interrupt from RX
        uint32_t data_from_interrupt = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(2));
        gpio_set_level(LED_GEN, 1);
        print_binary32(data_from_interrupt);

        gpio_set_level(LED_TX, 0);
        gpio_set_level(LED_RX, 0);
        gpio_set_level(LED_GEN,0);

        vTaskDelay(pdMS_TO_TICKS(5));
    }
}

well_tech
Posts: 9
Joined: Wed Feb 28, 2024 9:19 am

Re: [HELP] RMT single wire bidirectional communication (dshot telemetry)

Postby well_tech » Tue Jun 23, 2026 7:39 pm

Guys, I have narrowed down the issue, below is a very simple code
The logic is as follows:
1) configuring RX channel
2) activating rmt_receive()
3) waiting for interrupt (which sould happen after .signal_range_max_ns)
4) printing results of interrupt
5) waiting and go to 2)

Code: Select all

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/rmt_rx.h"
#include "hal/gpio_ll.h"  
#include "driver/gpio.h"

#define DSHOT_GPIO              4     
#define RMT_BASE_CLK_HZ        40000000 
#define RX_BUFFER_SIZE         48

#define LED_RX  18

static rmt_channel_handle_t rx_chan = NULL;
static rmt_symbol_word_t rx_buffer[RX_BUFFER_SIZE];
static TaskHandle_t main_task_handle = NULL;

static const rmt_receive_config_t rx_trans_config = {
    .signal_range_min_ns = 500,
    .signal_range_max_ns =60000,
};

//RX ISR
static bool IRAM_ATTR test_rx_done_callback(rmt_channel_handle_t rx_chan, const rmt_rx_done_event_data_t *edata, void *user_ctx) {
    uint32_t rx_interrupt_info = edata->num_symbols;
//checking the cause of interrupt
    if (edata->flags.is_last) rx_interrupt_info |= 0x80000000;        //timeout flag
    if (edata->num_symbols >= RX_BUFFER_SIZE) rx_interrupt_info |= 0x40000000;   //overflow flag
    gpio_set_level(LED_RX, 1);  //to control that interrupt actually occurs   
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xTaskGenericNotifyFromISR(main_task_handle, 0, rx_interrupt_info, eSetValueWithOverwrite, NULL, &xHigherPriorityTaskWoken); 
    return xHigherPriorityTaskWoken == pdTRUE;
}

void dshot_test(void) {
    main_task_handle = xTaskGetCurrentTaskHandle();

    gpio_reset_pin(LED_RX);
    gpio_set_direction(LED_RX, GPIO_MODE_OUTPUT);
    gpio_set_level(LED_RX, 0);

//oped drain pin configuration
    gpio_ll_od_enable(&GPIO, DSHOT_GPIO);
    
//RX settings
    rmt_rx_channel_config_t rx_config = {
        .gpio_num = DSHOT_GPIO,
        .clk_src = RMT_CLK_SRC_DEFAULT,
        .resolution_hz = RMT_BASE_CLK_HZ,
        .mem_block_symbols = 48,
    };
    ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_config, &rx_chan));

    ESP_ERROR_CHECK(rmt_enable(rx_chan));

//callback registration
    rmt_rx_event_callbacks_t cbs = {
        .on_recv_done = test_rx_done_callback,
    };
    ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_chan, &cbs, NULL));



    while (1) {
//activation receive
        rmt_receive(rx_chan, rx_buffer, sizeof(rx_buffer), &rx_trans_config);
//waiting for interrupt from RX
        uint32_t data_from_interrupt = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(2));
        printf("%lu\n", data_from_interrupt);
        gpio_set_level(LED_RX, 0);;
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

It should work but works only one time, after first iteration rmt_receive() returns back errors rmt_receive(399): channel not in enable state. Interesting that if RX actually receives something - it works as should, errors are returned only when nothing is received. What am I missing here? Working with S3 and 6.0.0

Who is online

Users browsing this forum: No registered users and 0 guests