adafruit / Adafruit_NeoPixel

Arduino library for controlling single-wire LED pixels (NeoPixel, WS2812, etc.)
GNU Lesser General Public License v3.0
3.02k stars 1.25k forks source link

Understanding how the RMT is configured for the neopixels #271

Open krupis opened 3 years ago

krupis commented 3 years ago

Hello. I am learning about the RMT and how it is specifically configured to generate required signals for driving the neopixels. I have come across this library and been digging through the code. I notice that you are using the RMT therefore I have decided to use this project as an example.

When the show() fmethod is being called, the following functions are being used:

#define ADAFRUIT_RMT_CHANNEL_MAX RMT_CHANNEL_MAX

#define RMT_LL_HW_BASE (&RMT)

bool rmt_reserved_channels[ADAFRUIT_RMT_CHANNEL_MAX];

static void IRAM_ATTR ws2812_rmt_adapter(const void *src, rmt_item32_t *dest, size_t src_size,
                                         size_t wanted_num, size_t *translated_size, size_t *item_num)
{
    if (src == NULL || dest == NULL)
    {
        *translated_size = 0;
        *item_num = 0;
        return;
    }
    const rmt_item32_t bit0 = {{{t0h_ticks, 1, t0l_ticks, 0}}}; //Logical 0
    const rmt_item32_t bit1 = {{{t1h_ticks, 1, t1l_ticks, 0}}}; //Logical 1
    size_t size = 0;
    size_t num = 0;
    uint8_t *psrc = (uint8_t *)src;
    rmt_item32_t *pdest = dest;
    while (size < src_size && num < wanted_num)
    {
        for (int i = 0; i < 8; i++)
        {
            // MSB first
            if (*psrc & (1 << (7 - i)))
            {
                pdest->val = bit1.val;
            }
            else
            {
                pdest->val = bit0.val;
            }
            num++;
            pdest++;
        }
        size++;
        psrc++;
    }
    *translated_size = size;
    *item_num = num;
}

void espShow(uint8_t pin, uint8_t *pixels, uint32_t numBytes)
{
    // Reserve channel
    rmt_channel_t channel = ADAFRUIT_RMT_CHANNEL_MAX;
    for (size_t i = 0; i < ADAFRUIT_RMT_CHANNEL_MAX; i++)
    {
        if (!rmt_reserved_channels[i])
        {
            rmt_reserved_channels[i] = true;
            channel = i;
        }
    }
    if (channel == ADAFRUIT_RMT_CHANNEL_MAX)
    {
        // Ran out of channels!
        return;
    }

    // Match default TX config from ESP-IDF version 3.4
    rmt_config_t config = {
        .rmt_mode = RMT_MODE_TX,
        .channel = channel,
        .gpio_num = pin,
        .clk_div = 2,
        .mem_block_num = 1,
        .tx_config = {
            //.carrier_freq_hz = 38000,
            //.carrier_level = RMT_CARRIER_LEVEL_HIGH,
            .idle_level = RMT_IDLE_LEVEL_LOW,
            //.carrier_duty_percent = 33,
            .carrier_en = false,
            .loop_en = false,
            .idle_output_en = true,
        }};
    rmt_config(&config);
    rmt_driver_install(config.channel, 0, 0);

    // Convert NS timings to ticks
    uint32_t counter_clk_hz = 0;

    // this emulates the rmt_get_counter_clock() function from ESP-IDF 3.4
    if (RMT_LL_HW_BASE->conf_ch[config.channel].conf1.ref_always_on == RMT_BASECLK_REF)
    {
        uint32_t div_cnt = RMT_LL_HW_BASE->conf_ch[config.channel].conf0.div_cnt;
        uint32_t div = div_cnt == 0 ? 256 : div_cnt;
        counter_clk_hz = REF_CLK_FREQ / (div);
    }
    else
    {
        uint32_t div_cnt = RMT_LL_HW_BASE->conf_ch[config.channel].conf0.div_cnt;
        uint32_t div = div_cnt == 0 ? 256 : div_cnt;
        counter_clk_hz = APB_CLK_FREQ / (div);
    }

    // NS to tick converter
    float ratio = (float)counter_clk_hz / 1e9;

    t0h_ticks = (uint32_t)(ratio * WS2812_T0H_NS);
    t0l_ticks = (uint32_t)(ratio * WS2812_T0L_NS);
    t1h_ticks = (uint32_t)(ratio * WS2812_T1H_NS);
    t1l_ticks = (uint32_t)(ratio * WS2812_T1L_NS);

    // Initialize automatic timing translator
    rmt_translator_init(config.channel, ws2812_rmt_adapter);

    // Write and wait to finish
    rmt_write_sample(config.channel, pixels, (size_t)numBytes, true);
    rmt_wait_tx_done(config.channel, pdMS_TO_TICKS(100));

    // Free channel again
    rmt_driver_uninstall(config.channel);
    rmt_reserved_channels[channel] = false;

    gpio_set_direction(pin, GPIO_MODE_OUTPUT);
}

I have also read RMT documentation from espressif: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/rmt.html

And after configuring the rmt, they are suggesting to use follwing functions:

rmt_write_items()
rmt_wait_tx_done()

However, in adafruit neopixel, a different method is used which involves the use of translator unit with a callback and rmt_write_sample instead of rmt_write_sample.

I am not able to fully understand what is happening in the ws2812_rmt_adapter and esp_show. Could someone help me understand this rmt code ? Is it possible to simplify it and rewrite it using rmt_write_items() ?

krupis commented 3 years ago

I am also confused why library reserves all RMT channels and then just uses the last channel available?

rmt_channel_t channel = ADAFRUIT_RMT_CHANNEL_MAX;
for (size_t i = 0; i < ADAFRUIT_RMT_CHANNEL_MAX; i++)
{
    if (!rmt_reserved_channels[i])
    {
        rmt_reserved_channels[i] = true;
        channel = i;
    }
}
if (channel == ADAFRUIT_RMT_CHANNEL_MAX)
{
    // Ran out of channels!
    return 0;
}

Wouldnt it make more sense to return after one non reserved rmt channel is found? Why need to reserve all the channels?

ladyada commented 3 years ago

@hierophect any thoughts?

hierophect commented 3 years ago

I last worked on this several IDF versions ago which could explain the API differences, but the reservation thing definitely doesn't make any sense. I'll check out my notes to try and see if that was a debugging edit, or a merge issue, or what.

hierophect commented 3 years ago

@krupis I had some time to catch up on this this morning. To start with, I don't know if this code is an ideal example for using the RMT with an up to date ESP-IDF submodule - at the time it was written, the IDF in the ESP32 Arduino core was lagging a bit behind their main release (3.2 vs 3.4), and has some hacks involved to get around the API differences present at that time.

The ws2812_rmt_adapter is based off the LED strip demo from the ESP-IDF. It functions to translate the color bytes provided to espShow into the rmt_item32_t structure required by the RMT, calculating the number of ticks for the line to be high/low in a way the Neopixel understands, and putting them into an array that the RMT can output.

While I used it to avoid reinventing the wheel here, the RMT Translator concept isn't super well documented by Espressif, and I don't know whether or not there's a performance benefit over just calling the contents of ws2812_rmt_adapter manually every time and then using rmt_write_items. If that's something you'd like to test out yourself, it might be an interesting experiment.

As for the reservation problem, that's a bug! There should be a break; in there. Thanks for pointing it out!

krupis commented 3 years ago

Thanks for clarifying!