Page 1 of 1

RMT constant speed in ESP32

Posted: Sat Apr 11, 2026 7:26 pm
by Disraeli
Hello! I'm using ESP-IDF v5.5.1 and an ESP32 board to control a stepper motor using the RMT module, following the example from:
https://github.com/espressif/esp-idf/bl ... ple_main.c
Since I'm using the ESP32, the RMT module does not support the parameter:

Code: Select all

rmt_transmit_config_t tx_config = {
    .loop_count = 5000 // ERROR on ESP32
}
I need to:
1) Run the stepper motor for N steps (user-defined);
2) Know when the rotation is complete (programmatically, without a physical encoder);
3) Do this without blocking code execution using rmt_tx_wait_all_done.

Question: How do I properly implement the constant-speed rotation phase on the ESP32 so that a callback is triggered after the movement completes (acceleration + constant speed + deceleration)?

Current issue: In my current code, only the case without a constant-speed phase works correctly. Using a loop is not suitable because I would need to add a counter in the callback to track how many steps have been taken. This seems cumbersome and inconvenient.

Initialization RMT:

Code: Select all

#define STEP_MOTOR_RESOLUTION_HZ 1000000
#define SAMPLE_POINTS_ACDEC  300
#define LOW_FREQ_HZ       50
#define MAIN_FREQ_HZ      1000
#define SYMBOL_DURATION     ((uint32_t)(STEP_MOTOR_RESOLUTION_HZ / MAIN_FREQ_HZ / 2))
#define BLOCK_SIZE            4000

rmt_channel_handle_t motor_chan = NULL;
rmt_encoder_handle_t accel_motor_encoder = NULL;
rmt_encoder_handle_t decel_motor_encoder = NULL;
rmt_encoder_handle_t uniform_motor_encoder = NULL;
rmt_encoder_handle_t copy_encoder = NULL;

rmt_copy_encoder_config_t copy_encoder_config_t = {};
static stepper_motor_curve_encoder_config_t decel_encoder_config = {
    .resolution = STEP_MOTOR_RESOLUTION_HZ,
    .sample_points = SAMPLE_POINTS_ACDEC,
    .start_freq_hz = MAIN_FREQ_HZ,
    .end_freq_hz = LOW_FREQ_HZ,
};
static stepper_motor_uniform_encoder_config_t uniform_encoder_config = {
    .resolution = STEP_MOTOR_RESOLUTION_HZ,
};
static stepper_motor_curve_encoder_config_t accel_encoder_config = {
    .resolution = STEP_MOTOR_RESOLUTION_HZ,
    .sample_points = SAMPLE_POINTS_ACDEC,
    .start_freq_hz = LOW_FREQ_HZ,
    .end_freq_hz = MAIN_FREQ_HZ,
};

ESP_LOGI(TAG_RMT, "Create RMT TX channel");
    rmt_tx_channel_config_t tx_chan_config = {
        .clk_src = RMT_CLK_SRC_DEFAULT,
        .gpio_num = STEP_MOTOR,
        .mem_block_symbols = 64,
        .resolution_hz = STEP_MOTOR_RESOLUTION_HZ,
        .trans_queue_depth = 10,
    };
    ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &motor_chan));
    
    ESP_LOGI(TAG_RMT, "Create motor encoders");
    ESP_ERROR_CHECK(rmt_new_copy_encoder(&copy_encoder_config_t, &copy_encoder));
    ESP_ERROR_CHECK(rmt_new_stepper_motor_curve_encoder(&accel_encoder_config, &accel_motor_encoder));
    ESP_ERROR_CHECK(rmt_new_stepper_motor_uniform_encoder(&uniform_encoder_config, &uniform_motor_encoder));
    ESP_ERROR_CHECK(rmt_new_stepper_motor_curve_encoder(&decel_encoder_config, &decel_motor_encoder));

    ESP_LOGI(TAG_RMT, "Enable RMT channel");
    ESP_ERROR_CHECK(rmt_enable(motor_chan));

    xRotationDone = xSemaphoreCreateBinary();
Callback:

Code: Select all

  
rmt_tx_event_callbacks_t cbs = {
            .on_trans_done = tx_done_callback
        };
ESP_ERROR_CHECK(rmt_tx_register_event_callbacks(motor_chan, &cbs, NULL));
    
bool tx_done_callback(rmt_channel_handle_t channel, const rmt_tx_done_event_data_t *edata, void *user_data)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if (xRotationDone)
    {
        xSemaphoreGiveFromISR(xRotationDone, &xHigherPriorityTaskWoken);
    }
    return xHigherPriorityTaskWoken == pdTRUE;
}
Rotation function for a given angle (only the first if branch works):

Code: Select all

bool rmt_rotate_degree(float degrees, int rotation_dir)
{
    if ((motor_chan == NULL) || (accel_motor_encoder == NULL) || (decel_motor_encoder == NULL) || (uniform_motor_encoder == NULL))
    {
        ESP_LOGE("STEPPER", "Motor not initialized!");
        return false;
    }

    if (degrees < 0.1f)
    {
        ESP_LOGW("STEPPER", "Rotation angle too small: %.2f degrees", degrees);
        return false;
    }

    gpio_set_level(DIR_MOTOR, rotation_dir);

    uint32_t total_steps = (uint32_t)(degrees * STEPS_PER_DEGREE);

    rmt_transmit_config_t tx_config = {
        .loop_count = 0,
        .flags = {
            .eot_level = 1
        }};


    if (total_steps <= (SAMPLE_POINTS_ACDEC * 2))
    {
        uint32_t half_steps = total_steps / 2;
        uint32_t remain_steps = total_steps - half_steps;
        ESP_LOGI("STEPPER", "Rotating %.2f degrees. Direction %s. Summary %lu steps: accel steps %lu, decel steps %lu", degrees, rotation_dir == DIR_CLOCKWISE ? "CLOCKWISE" : "COUNTERCLOCKWISE", total_steps, half_steps, remain_steps);
        ESP_ERROR_CHECK(rmt_transmit(motor_chan, accel_motor_encoder, &half_steps, sizeof(uint32_t), &tx_config));   /// - разгон;
        ESP_ERROR_CHECK(rmt_transmit(motor_chan, decel_motor_encoder, &remain_steps, sizeof(uint32_t), &tx_config)); /// - торможение;
    }
    else
    {
        rmt_symbol_word_t *uniform_block = NULL;
        uint32_t uniform_steps = total_steps - (SAMPLE_POINTS_ACDEC * 2);
        ESP_LOGI(TAG_RMT, "SYMBOL_DURATION = %lu, expected = %lu", SYMBOL_DURATION, STEP_MOTOR_RESOLUTION_HZ / MAIN_FREQ_HZ / 2);
        ESP_LOGI("STEPPER", "Rotating %.2f degrees. Direction %s. Summary %lu steps: accel steps %lu, uniform steps %lu, decel steps %lu", degrees, rotation_dir == DIR_CLOCKWISE ? "CLOCKWISE" : "COUNTERCLOCKWISE", total_steps, SAMPLE_POINTS_ACDEC, uniform_steps, SAMPLE_POINTS_ACDEC);
        
        uniform_block = malloc((BLOCK_SIZE * 2 + 1) * sizeof(rmt_symbol_word_t));
        if (uniform_block == NULL)
        {
            ESP_LOGE("STEPPER", "Failed to allocate memory for block!");
            return;
        }

        if (uniform_block)
        {
            for (uint32_t i = 0; i < BLOCK_SIZE; i++)
            {
                uniform_block[i * 2].level0 = 0;
                uniform_block[i * 2].duration0 = SYMBOL_DURATION;
                uniform_block[i * 2].level1 = 1;
                uniform_block[i * 2].duration1 = SYMBOL_DURATION;
            }
        }

        uniform_block[BLOCK_SIZE * 2].level0 = 0;
        uniform_block[BLOCK_SIZE * 2].duration0 = 0;
        uniform_block[BLOCK_SIZE * 2].level1 = 0;
        uniform_block[BLOCK_SIZE * 2].duration1 = 0;

        ESP_ERROR_CHECK(rmt_transmit(motor_chan, accel_motor_encoder,
                                     &accel_encoder_config.sample_points,
                                     sizeof(accel_encoder_config.sample_points),
                                     &tx_config));

        for (uint32_t remaining = uniform_steps; remaining > 0; remaining -= BLOCK_SIZE)
        {
            uint32_t this_block = (remaining < BLOCK_SIZE) ? remaining : BLOCK_SIZE;
            rmt_transmit(motor_chan, copy_encoder,
                         uniform_block,
                         (this_block * 2 + 1) * sizeof(rmt_symbol_word_t),
                         &tx_config);
        }

        ESP_ERROR_CHECK(rmt_transmit(motor_chan, decel_motor_encoder,
                                     &decel_encoder_config.sample_points,
                                     sizeof(decel_encoder_config.sample_points),
                                     &tx_config));
    }

    return true;
}
Code that works successfully (low count steps without constant speed, only acceleration + deceleration ):

Code: Select all

        rmt_rotate_degree(2, DIR_CLOCKWISE);
        xSemaphoreTake(xRotationDone, portMAX_DELAY);

Re: RMT constant speed in ESP32

Posted: Sat Apr 11, 2026 8:22 pm
by MicroController

Code: Select all

#define MAIN_FREQ_HZ      1000
At this (low) rate, I guess the most simple way to keep track of the steps is to not use the RMT at all and just go with 'manually' pulsing the desired steps out from a timer.

Using the RMT, the 'clean' solution is in fact to have a 'stateful' encoder which 'knows' when to stop producing symbols. Or to use the on_trans_done callback to send 'batches' of steps/symbols (say 128 at a time, copy-encoded from a static table) to the RMT until enough steps have been sent.

Re: RMT constant speed in ESP32

Posted: Sun Apr 12, 2026 11:51 am
by Disraeli
I had it working with a timer before switching to RMT, and it worked fine. But I didn't have the S-curve acceleration/deceleration profile, which was nicely implemented in the RMT example on GitHub.

Is there perhaps an example or solution for using RMT for constant-speed motor control on the ESP32? Because the need for continuous rotation seems obvious when the RMT module is physically available, even without a hardware buffer for looping.

Re: RMT constant speed in ESP32

Posted: Wed Apr 15, 2026 9:38 am
by Disraeli
I redesigned the project due to difficulties with loop.count on the ESP32 and implemented a constant speed using a simple loop — the number of steps at constant speed equals the number of loop iterations, waiting for the rotation to complete. But an issue arose with the emergency stop of RMT: viewtopic.php?p=154378#p154378.
This topic is no longer relevant, although the lack of an obvious solution for continuous rotation on the ESP32 is puzzling.