espressif / esp-idf

Espressif IoT Development Framework. Official development framework for Espressif SoCs.
Apache License 2.0
13.62k stars 7.28k forks source link

Can't understand the way rgb lcd works with dma. (IDFGH-11519) #12644

Closed PickaxeHit closed 11 months ago

PickaxeHit commented 11 months ago

Answers checklist.

General issue report

I'm designing a driver that works very similar to an LCD. The only difference is that I need to manually switch the chip select level between the two row sends. I saw that the rgb lcd has a bounce buffer only mode of operation and I thought it would suit me well. But after looking at the TRM and related code, I don't understand how to restart the transmission after the dma generates an interrupt.


static esp_err_t lcd_rgb_panel_create_trans_link(esp_rgb_panel_t *panel)
{
    for (int i = 0; i < RGB_LCD_PANEL_DMA_LINKS_REPLICA; i++) {
        panel->dma_links[i] = &panel->dma_nodes[panel->num_dma_nodes * i];
    }
    // chain DMA descriptors
    for (int i = 0; i < panel->num_dma_nodes * RGB_LCD_PANEL_DMA_LINKS_REPLICA; i++) {
        panel->dma_nodes[i].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU;
        panel->dma_nodes[i].next = &panel->dma_nodes[i + 1];
    }

    if (panel->bb_size) {
        // loop end back to start
        panel->dma_nodes[panel->num_dma_nodes * RGB_LCD_PANEL_BOUNCE_BUF_NUM - 1].next = &panel->dma_nodes[0];
        // mount the bounce buffers to the DMA descriptors
        lcd_com_mount_dma_data(panel->dma_links[0], panel->bounce_buffer[0], panel->bb_size);
        lcd_com_mount_dma_data(panel->dma_links[1], panel->bounce_buffer[1], panel->bb_size);
    } else {
        if (panel->flags.stream_mode) {
            // circle DMA descriptors chain for each frame buffer
            for (int i = 0; i < RGB_LCD_PANEL_DMA_LINKS_REPLICA; i++) {
                panel->dma_nodes[panel->num_dma_nodes * (i + 1) - 1].next = &panel->dma_nodes[panel->num_dma_nodes * i];
            }
        } else {
            // one-off DMA descriptors chain
            for (int i = 0; i < RGB_LCD_PANEL_DMA_LINKS_REPLICA; i++) {
                panel->dma_nodes[panel->num_dma_nodes * (i + 1) - 1].next = NULL;
            }
        }
        // mount the frame buffer to the DMA descriptors
        for (size_t i = 0; i < panel->num_fbs; i++) {
            lcd_com_mount_dma_data(panel->dma_links[i], panel->fbs[i], panel->fb_size);
        }
    }

    // On restart, the data sent to the LCD peripheral needs to start LCD_FIFO_PRESERVE_SIZE_PX pixels after the FB start
    // so we use a dedicated DMA node to restart the DMA transaction
    // see also `lcd_rgb_panel_try_restart_transmission`
    memcpy(&panel->dma_restart_node, &panel->dma_nodes[0], sizeof(panel->dma_restart_node));
    int restart_skip_bytes = LCD_FIFO_PRESERVE_SIZE_PX * sizeof(uint16_t);
    uint8_t *p = (uint8_t *)panel->dma_restart_node.buffer;
    panel->dma_restart_node.buffer = &p[restart_skip_bytes];
    panel->dma_restart_node.dw0.length -= restart_skip_bytes;
    panel->dma_restart_node.dw0.size -= restart_skip_bytes;

    // alloc DMA channel and connect to LCD peripheral
    gdma_channel_alloc_config_t dma_chan_config = {
        .direction = GDMA_CHANNEL_DIRECTION_TX,
    };
    ESP_RETURN_ON_ERROR(gdma_new_channel(&dma_chan_config, &panel->dma_chan), TAG, "alloc DMA channel failed");
    gdma_connect(panel->dma_chan, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0));
    gdma_transfer_ability_t ability = {
        .psram_trans_align = panel->psram_trans_align,
        .sram_trans_align = panel->sram_trans_align,
    };
    gdma_set_transfer_ability(panel->dma_chan, &ability);

    // we need to refill the bounce buffer in the DMA EOF interrupt, so only register the callback for bounce buffer mode
    if (panel->bb_size) {
        gdma_tx_event_callbacks_t cbs = {
            .on_trans_eof = lcd_rgb_panel_eof_handler,
        };
        gdma_register_tx_event_callbacks(panel->dma_chan, &cbs, panel);
    }

    return ESP_OK;
}

You can see that on_trans_eof points to lcd_rgb_panel_eof_handler

static IRAM_ATTR bool lcd_rgb_panel_fill_bounce_buffer(esp_rgb_panel_t *panel, uint8_t *buffer)
{
    bool need_yield = false;
    int bytes_per_pixel = panel->fb_bits_per_pixel / 8;
    if (panel->num_fbs == 0) {
        if (panel->on_bounce_empty) {
            // We don't have a frame buffer here; we need to call a callback to refill the bounce buffer
            need_yield = panel->on_bounce_empty(&panel->base, buffer, panel->bounce_pos_px, panel->bb_size, panel->user_ctx);
        }
    } else {
        // We do have frame buffer; copy from there.
        // Note: if the cache is diabled, and accessing the PSRAM by DCACHE will crash.
        memcpy(buffer, &panel->fbs[panel->bb_fb_index][panel->bounce_pos_px * bytes_per_pixel], panel->bb_size);
        if (panel->flags.bb_invalidate_cache) {
            // We don't need the bytes we copied from the psram anymore
            // Make sure that if anything happened to have changed (because the line already was in cache) we write the data back.
            esp_cache_msync(&panel->fbs[panel->bb_fb_index][panel->bounce_pos_px * bytes_per_pixel], (size_t)panel->bb_size, ESP_CACHE_MSYNC_FLAG_INVALIDATE);
        }
    }
    panel->bounce_pos_px += panel->bb_size / bytes_per_pixel;
    // If the bounce pos is larger than the frame buffer size, wrap around so the next isr starts pre-loading the next frame.
    if (panel->bounce_pos_px >= panel->fb_size / bytes_per_pixel) {
        panel->bounce_pos_px = 0;
        panel->bb_fb_index = panel->cur_fb_index;
    }
    if (panel->num_fbs > 0) {
        // Preload the next bit of buffer from psram
        Cache_Start_DCache_Preload((uint32_t)&panel->fbs[panel->bb_fb_index][panel->bounce_pos_px * bytes_per_pixel],
                                   panel->bb_size, 0);
    }
    return need_yield;
}

// This is called in bounce buffer mode, when one bounce buffer has been fully sent to the LCD peripheral.
static IRAM_ATTR bool lcd_rgb_panel_eof_handler(gdma_channel_handle_t dma_chan, gdma_event_data_t *event_data, void *user_data)
{
    esp_rgb_panel_t *panel = (esp_rgb_panel_t *)user_data;
    dma_descriptor_t *desc = (dma_descriptor_t *)event_data->tx_eof_desc_addr;
    // Figure out which bounce buffer to write to.
    // Note: what we receive is the *last* descriptor of this bounce buffer.
    int bb = (desc == &panel->dma_nodes[panel->num_dma_nodes - 1]) ? 0 : 1;
    return lcd_rgb_panel_fill_bounce_buffer(panel, panel->bounce_buffer[bb]);
}

But I didn't see any code about restarting dma in these two functions. On the contrary, in the rmt driver, there is a function named gdma_append, which seems to be able to restart dma from the last one in the dma descriptor list.

static bool IRAM_ATTR rmt_dma_tx_eof_cb(gdma_channel_handle_t dma_chan, gdma_event_data_t *event_data, void *user_data)
{
    rmt_tx_channel_t *tx_chan = (rmt_tx_channel_t *)user_data;
    dma_descriptor_t *eof_desc = (dma_descriptor_t *)event_data->tx_eof_desc_addr;
    // if the DMA descriptor link is still a ring (i.e. hasn't broken down by `rmt_tx_mark_eof()`), then we treat it as a valid ping-pong event
    if (eof_desc->next && eof_desc->next->next) {
        // continue pingpong transmission
        rmt_tx_trans_desc_t *t = tx_chan->cur_trans;
        size_t encoded_symbols = t->transmitted_symbol_num;
        if (t->flags.encoding_done) {
            rmt_tx_mark_eof(tx_chan);
            encoded_symbols += 1;
        } else {
            encoded_symbols += rmt_encode_check_result(tx_chan, t);
        }
        t->transmitted_symbol_num = encoded_symbols;
        tx_chan->mem_end = tx_chan->ping_pong_symbols * 3 - tx_chan->mem_end; // mem_end equals to either ping_pong_symbols or ping_pong_symbols*2
        // tell DMA that we have a new descriptor attached
        gdma_append(dma_chan);
    }
    return false;
}

I searched the entire esp-idf repository and found that only rmt seems to use this function. Is there a special purpose? Thanks!

suda-morris commented 11 months ago

RGB LCD is a "stream-style" application, so the DMA link list there is a circular link list. In normal cases, it just keeps running, copying data from memory to the LCD FIFO. There isn't a scenario that requires us to reset or restart the DMA unless "working around some hardware issue".

PickaxeHit commented 11 months ago

Thank you, now I understand. But I still have the following questions:

  1. Does the RGB LCD allow the output to be paused when the FIFO is empty? I need to strictly control the timing to manually switch the gpio level just after sending a row of pixels. My current idea is to operate after the DMA triggers the EOF interrupt. I wonder if this is feasible?
  2. How to switch the stream-style working mode of DMA? Do I need to rewrite all functions or call the HAL library to implement it?
PickaxeHit commented 11 months ago

There is another question: "suc_eof" written in TRM will cause GDMA to trigger the EOF interrupt, so will GDMA stop at this time? Does its stopping depend on suc_eof or is the pointer of the next item in the linked list NULL? How should I configure GDMA to start sending data after I manually restart it?

PickaxeHit commented 11 months ago

@suda-morris could you please help me? thanks!