Merge branch 'feature/esp_lcd_bounce_buffers' into 'master'

Add bounce buffer support to esp_lcd

See merge request espressif/esp-idf!17894
This commit is contained in:
morris 2022-06-28 11:56:22 +08:00
commit eebcce87b9
2 changed files with 303 additions and 27 deletions

View File

@ -85,6 +85,75 @@ typedef struct {
*/
typedef bool (*esp_lcd_rgb_panel_frame_trans_done_cb_t)(esp_lcd_panel_handle_t panel, esp_lcd_rgb_panel_event_data_t *edata, void *user_ctx);
/**
* @brief Prototype for function to re-fill a bounce buffer. Note this is called in ISR context.
*
* @param bounce_buf Bounce buffer to write data into
* @param pos_px How many pixels already were sent to the display this frame, in other words, at what pixel
* the routine should start putting data into bounce_buf
* @param len_bytes Length, in bytes, of the bounce buffer. Routine should fill this length fully.
* @param user_ctx Opaque pointer that was passed as bounce_buffer_cb_user_ctx to esp_lcd_new_rgb_panel
* @return True if the callback woke up a higher-priority task, false otherwise.
*/
typedef bool (*esp_lcd_rgb_panel_bounce_buf_fill_cb_t)(void *bounce_buf, int pos_px, int len_bytes, void *user_ctx);
/**
* @brief LCD RGB framebuffer operation modes
*
* With regards to how the framebuffer is accessed and where it is located, the RGB LCD panel driver can
* operate in four modes:
*
* - Framebuffer in internal memory.
* - Framebuffer in PSRAM, accessed using EDMA
* - Framebuffer in PSRAM, smaller bounce buffers in internal memory.
* - No framebuffer in driver, bounce buffers in internal memory filled by callback.
*
* The first option (framebuffer in internal memory) is the default and simplest. There is a framebuffer in
* internal memory that is read out once a frame using DMA and the data is sent out to the LCD verbatim. It
* needs no CPU intervention to function, but it has the downside that it uses up a fair bit of the limited
* amount of internal memory. This is the default if you do not specify flags or bounce buffer options.
*
* The second option is useful if you have PSRAM and want to store the framebuffer there rather than in the
* limited internal memory. The LCD peripheral will use EDMA to fetch frame data directly from the PSRAM,
* bypassing the internal cache. If you use this, after writing to the framebuffer, make sure to use e.g.
* Cache_WriteBack_Addr to make sure the framebuffer is actually written back to the PSRAM. Not doing this
* will lead to image corruption.
* The downside of this is that when both the CPU as well as peripherals need access to the EDMA, the
* bandwidth will be shared between the two, that is, EDMA gets half and the CPUs the other half. If
* there's other peripherals using EDMA as well, with a high enough pixel clock this can lead to starvation
* of the LCD peripheral, leading to display corruption. However, if the pixel clock is low enough for this
* not to be an issue, this is a solution that uses almost no CPU intervention. This option can be enabled
* by setting the ``fb_in_psram`` flag.
*
* The third option makes use of two so-called 'bounce buffers' in internal memory, but a main framebuffer that
* is still in PSRAM. These bounce buffers are buffers large enough to hold e.g. a few lines of display data,
* but still significantly less than the main framebuffer. The LCD peripheral will use DMA to read data from
* one of the bounce buffers, and meanwhile an interrupt routine will use the CPU to copy data from the main
* PSRAM framebuffer into the other bounce buffer. Once the LCD peripheral has finished reading the bounce
* buffer, the two buffers change place and the CPU can fill the others. Note that as the CPU reads the
* framebuffer data through the cache, it's not needed to call Cache_WriteBack_Addr() anymore.
* The advantage here is that, as it's easier to control CPU memory bandwith use than EDMA memory bandwith
* use, doing this can lead to higher pixel clocks being supported. As the bounce buffers are larger than
* the FIFOs in the EDMA path, this method is also more robust against short bandwidth spikes. The
* downside is a major increase in CPU use. This mode is selected by setting the ``fb_in_psram`` flag and
* additionally specifying a (non-zero) bounce_buffer_size_px value. This value is dependent on your use
* case, but a suggested initial value would be e.g. 8 times the amount of pixels in one LCD line.
*
* Note that this third option also allows for a ``bb_do_cache_invalidate`` flag to be set. Enabling this
* frees up the cache lines after they're used to read out the framebuffer data from PSRAM, but it may lead
* to slight corruption if the other core writes data to the framebuffer at the exact time the cache lines
* are freed up. (Technically, a write to the framebuffer can be ignored if it falls between the cache
* writeback and the cache invalidate calls.)
*
* Finally, the fourth option is the same as the third option, but there is no PSRAM frame buffer initialized
* by the LCD driver. Instead, the user supplies a callback function that is responsible for filling the
* bounce buffers. As this driver does not care where the written pixels come from, this allows for
* the callback doing e.g. on-the-fly conversion from a smaller, 8-bit-per-pixel PSRAM framebuffer to
* an 16-bit LCD, or even procedurally-generated framebuffer-less graphics. This option is selected
* by not setting the ``fb_in_psram`` flag but supplying both a ``bounce_buffer_size_px`` value as well
* as a ``on_bounce_empty`` callback.
*/
/**
* @brief LCD RGB panel configuration structure
*/
@ -101,11 +170,15 @@ typedef struct {
int data_gpio_nums[SOC_LCD_RGB_DATA_WIDTH]; /*!< GPIOs used for data lines */
int disp_gpio_num; /*!< GPIO used for display control signal, set to -1 if it's not used */
esp_lcd_rgb_panel_frame_trans_done_cb_t on_frame_trans_done; /*!< Callback invoked when one frame buffer has transferred done */
int bounce_buffer_size_px; /*!< If not-zero, the driver uses a bounce buffer in internal memory to DMA from. Value is in pixels. */
esp_lcd_rgb_panel_bounce_buf_fill_cb_t on_bounce_empty; /*!< If we use a bounce buffer, this function gets called to fill that, rather than copying from the framebuffer */
void *bounce_buffer_cb_user_ctx; /*!< Opaque parameter to pass to the on_bounce_empty function */
void *user_ctx; /*!< User data which would be passed to on_frame_trans_done's user_ctx */
struct {
unsigned int disp_active_low: 1; /*!< If this flag is enabled, a low level of display control signal can turn the screen on; vice versa */
unsigned int relax_on_idle: 1; /*!< If this flag is enabled, the host won't refresh the LCD if nothing changed in host's frame buffer (this is usefull for LCD with built-in GRAM) */
unsigned int fb_in_psram: 1; /*!< If this flag is enabled, the frame buffer will be allocated from PSRAM preferentially */
unsigned int bb_do_cache_invalidate:1; /*!< If this flag is enabled, in bounceback mode we'll do a cache invalidate on the read data, freeing the cache. Can be dangerous if data is written from other core. */
} flags; /*!< LCD RGB panel configuration flags */
} esp_lcd_rgb_panel_config_t;

View File

@ -38,6 +38,7 @@
#include "soc/lcd_periph.h"
#include "hal/lcd_hal.h"
#include "hal/lcd_ll.h"
#include <rom/cache.h>
#if CONFIG_LCD_RGB_ISR_IRAM_SAFE
#define LCD_RGB_INTR_ALLOC_FLAGS (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_INTRDISABLED)
@ -49,8 +50,6 @@ static const char *TAG = "lcd_panel.rgb";
typedef struct esp_rgb_panel_t esp_rgb_panel_t;
// This function is located in ROM (also see esp_rom/${target}/ld/${target}.rom.ld)
extern int Cache_WriteBack_Addr(uint32_t addr, uint32_t size);
static esp_err_t rgb_panel_del(esp_lcd_panel_t *panel);
static esp_err_t rgb_panel_reset(esp_lcd_panel_t *panel);
@ -85,7 +84,13 @@ struct esp_rgb_panel_t {
esp_lcd_rgb_timing_t timings; // RGB timing parameters (e.g. pclk, sync pulse, porch width)
gdma_channel_handle_t dma_chan; // DMA channel handle
esp_lcd_rgb_panel_frame_trans_done_cb_t on_frame_trans_done; // Callback, invoked after frame trans done
int bounce_buffer_size_bytes; //If not-zero, the driver uses a bounce buffer in internal memory to DMA from. It's in bytes here.
uint8_t *bounce_buffer[2]; //Pointer to the bounce buffers
int bounce_buf_frame_start; //If frame restarts, which bb has the initial frame data?
esp_lcd_rgb_panel_bounce_buf_fill_cb_t on_bounce_empty; // If we use a bounce buffer, this function gets called to fill it rather than copying from the framebuffer
void *bounce_buffer_cb_user_ctx; //Callback data pointer
void *user_ctx; // Reserved user's data of callback functions
int bounce_pos_px; // Position in whatever source material is used for the bounce buffer, in pixels
int x_gap; // Extra gap in x coordinate, it's used when calculate the flush window
int y_gap; // Extra gap in y coordinate, it's used when calculate the flush window
portMUX_TYPE spinlock; // to protect panel specific resource from concurrent access (e.g. between task and ISR)
@ -94,10 +99,30 @@ struct esp_rgb_panel_t {
unsigned int stream_mode: 1; // If set, the LCD transfers data continuously, otherwise, it stops refreshing the LCD when transaction done
unsigned int fb_in_psram: 1; // Whether the frame buffer is in PSRAM
unsigned int need_update_pclk: 1; // Whether to update the PCLK before start a new transaction
unsigned int bb_do_cache_invalidate: 1; //If we do cache invalidate in bb psram fb mode
} flags;
dma_descriptor_t dma_restart_node; //DMA descriptor used to restart the transfer
dma_descriptor_t dma_nodes[]; // DMA descriptor pool of size `num_dma_nodes`
};
/*
A note about DMA desync:
It should never happen in a well-designed embedded application, but it can in theory be possible
that GDMA cannot deliver data as fast as the LCD consumes it. In the ESP32-S3 hardware, this
leads to the LCD simply outputting dummy bytes while GDMA waits for data. If we were to run
DMA in a simple circular fashion, this would mean a de-sync between the LCD address the GDMA
reads the data for and the LCD address the LCD peripheral thinks it outputs data for,
leading to a permanently shifted image.
In order to stop this from happening, we restart GDMA in the VBlank interrupt; this way we always
know where it starts. However, the LCD peripheral also has a FIFO, and at the time of the VBlank,
it already has read some data in there. We cannot reset this FIFO entirely, there's always one
pixel that remains. So instead, when we restart DMA, we take into account it does not need to
output the data that already is in the FIFO and we restart it using a descriptor that starts
at the position after the last pixel in the LCD fifo.
*/
esp_err_t esp_lcd_new_rgb_panel(const esp_lcd_rgb_panel_config_t *rgb_panel_config, esp_lcd_panel_handle_t *ret_panel)
{
#if CONFIG_LCD_ENABLE_DEBUG_LOG
@ -108,6 +133,11 @@ esp_err_t esp_lcd_new_rgb_panel(const esp_lcd_rgb_panel_config_t *rgb_panel_conf
ESP_GOTO_ON_FALSE(rgb_panel_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid parameter");
ESP_GOTO_ON_FALSE(rgb_panel_config->data_width == 16, ESP_ERR_NOT_SUPPORTED, err, TAG,
"unsupported data width %d", rgb_panel_config->data_width);
ESP_GOTO_ON_FALSE(!(rgb_panel_config->bounce_buffer_size_px == 0 && rgb_panel_config->on_bounce_empty != NULL),
ESP_ERR_INVALID_ARG, err, TAG, "cannot have bounce empty callback without having a bounce buffer");
ESP_GOTO_ON_FALSE(!(rgb_panel_config->bounce_buffer_size_px != 0 && rgb_panel_config->on_bounce_empty == NULL &&
!rgb_panel_config->flags.fb_in_psram), ESP_ERR_INVALID_ARG, err, TAG,
"bounce buffer without callback and main fb not in psram does not make sense");
#if CONFIG_LCD_RGB_ISR_IRAM_SAFE
if (rgb_panel_config->on_frame_trans_done) {
@ -120,9 +150,35 @@ esp_err_t esp_lcd_new_rgb_panel(const esp_lcd_rgb_panel_config_t *rgb_panel_conf
// calculate the number of DMA descriptors
size_t fb_size = rgb_panel_config->timings.h_res * rgb_panel_config->timings.v_res * rgb_panel_config->data_width / 8;
size_t num_dma_nodes = fb_size / DMA_DESCRIPTOR_BUFFER_MAX_SIZE;
if (fb_size > num_dma_nodes * DMA_DESCRIPTOR_BUFFER_MAX_SIZE) {
num_dma_nodes++;
size_t num_dma_nodes;
int bounce_bytes = 0;
if (rgb_panel_config->bounce_buffer_size_px == 0) {
// No bounce buffers. DMA descriptors need to fit entire framebuffer
num_dma_nodes = (fb_size + DMA_DESCRIPTOR_BUFFER_MAX_SIZE - 1) / DMA_DESCRIPTOR_BUFFER_MAX_SIZE;
} else {
//The FB needs to be an integer multiple of the size of a bounce buffer (so
//we end on the end of the second bounce buffer). Adjust the size of the
//bounce buffers if it is not.
int pixel_data_bytes = rgb_panel_config->data_width / 8; //size of one pixel, in bytes
int no_pixels = rgb_panel_config->timings.h_res * rgb_panel_config->timings.v_res;
bounce_bytes = rgb_panel_config->bounce_buffer_size_px * pixel_data_bytes;
if (no_pixels % (rgb_panel_config->bounce_buffer_size_px * pixel_data_bytes)) {
//Search for some value that does work. Yes, this is a stupidly simple algo, but it only
//needs to run on startup.
for (int a=rgb_panel_config->bounce_buffer_size_px; a>0; a--) {
if ((no_pixels % (a * pixel_data_bytes))==0) {
bounce_bytes = a * pixel_data_bytes;
ESP_LOGW(TAG, "Frame buffer is not an integer multiple of bounce buffers.");
ESP_LOGW(TAG, "Adjusted bounce buffer size from %d to %d pixels to fix this.",
rgb_panel_config->bounce_buffer_size_px, bounce_bytes/pixel_data_bytes);
break;
}
}
}
// DMA descriptors need to fit both bounce buffers
num_dma_nodes = (bounce_bytes + DMA_DESCRIPTOR_BUFFER_MAX_SIZE - 1) / DMA_DESCRIPTOR_BUFFER_MAX_SIZE;
num_dma_nodes = num_dma_nodes * 2; //as we have two bounce buffers
}
// DMA descriptors must be placed in internal SRAM (requested by DMA)
rgb_panel = heap_caps_calloc(1, sizeof(esp_rgb_panel_t) + num_dma_nodes * sizeof(dma_descriptor_t), MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
@ -133,6 +189,7 @@ esp_err_t esp_lcd_new_rgb_panel(const esp_lcd_rgb_panel_config_t *rgb_panel_conf
int panel_id = lcd_com_register_device(LCD_COM_DEVICE_TYPE_RGB, rgb_panel);
ESP_GOTO_ON_FALSE(panel_id >= 0, ESP_ERR_NOT_FOUND, err, TAG, "no free rgb panel slot");
rgb_panel->panel_id = panel_id;
rgb_panel->bounce_buffer_size_bytes = bounce_bytes;
// enable APB to access LCD registers
periph_module_enable(lcd_periph_signals.panels[panel_id].module);
periph_module_reset(lcd_periph_signals.panels[panel_id].module);
@ -148,17 +205,31 @@ esp_err_t esp_lcd_new_rgb_panel(const esp_lcd_rgb_panel_config_t *rgb_panel_conf
}
size_t psram_trans_align = rgb_panel_config->psram_trans_align ? rgb_panel_config->psram_trans_align : 64;
size_t sram_trans_align = rgb_panel_config->sram_trans_align ? rgb_panel_config->sram_trans_align : 4;
if (alloc_from_psram) {
// the low level malloc function will help check the validation of alignment
rgb_panel->fb = heap_caps_aligned_calloc(psram_trans_align, 1, fb_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
} else {
rgb_panel->fb = heap_caps_aligned_calloc(sram_trans_align, 1, fb_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
}
ESP_GOTO_ON_FALSE(rgb_panel->fb, ESP_ERR_NO_MEM, err, TAG, "no mem for frame buffer");
rgb_panel->psram_trans_align = psram_trans_align;
rgb_panel->sram_trans_align = sram_trans_align;
rgb_panel->fb_size = fb_size;
rgb_panel->flags.fb_in_psram = alloc_from_psram;
if (!rgb_panel_config->on_bounce_empty) {
//We need to allocate a framebuffer.
if (alloc_from_psram) {
// the low level malloc function will help check the validation of alignment
rgb_panel->fb = heap_caps_aligned_calloc(psram_trans_align, 1, fb_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
} else {
rgb_panel->fb = heap_caps_aligned_calloc(sram_trans_align, 1, fb_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
}
ESP_GOTO_ON_FALSE(rgb_panel->fb, ESP_ERR_NO_MEM, err, TAG, "no mem for frame buffer");
rgb_panel->psram_trans_align = psram_trans_align;
rgb_panel->sram_trans_align = sram_trans_align;
rgb_panel->flags.fb_in_psram = alloc_from_psram;
}
if (rgb_panel->bounce_buffer_size_bytes) {
//We need to allocate bounce buffers.
rgb_panel->bounce_buffer[0] = heap_caps_aligned_calloc(sram_trans_align, 1,
rgb_panel->bounce_buffer_size_bytes, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
rgb_panel->bounce_buffer[1] = heap_caps_aligned_calloc(sram_trans_align, 1,
rgb_panel->bounce_buffer_size_bytes, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
}
if (rgb_panel_config->on_bounce_empty) {
rgb_panel->on_bounce_empty = rgb_panel_config->on_bounce_empty;
rgb_panel->bounce_buffer_cb_user_ctx = rgb_panel_config->bounce_buffer_cb_user_ctx;
}
// initialize HAL layer, so we can call LL APIs later
lcd_hal_init(&rgb_panel->hal, panel_id);
// enable clock gating
@ -187,6 +258,7 @@ esp_err_t esp_lcd_new_rgb_panel(const esp_lcd_rgb_panel_config_t *rgb_panel_conf
rgb_panel->data_width = rgb_panel_config->data_width;
rgb_panel->disp_gpio_num = rgb_panel_config->disp_gpio_num;
rgb_panel->flags.disp_en_level = !rgb_panel_config->flags.disp_active_low;
rgb_panel->flags.bb_do_cache_invalidate = rgb_panel_config->flags.bb_do_cache_invalidate;
rgb_panel->on_frame_trans_done = rgb_panel_config->on_frame_trans_done;
rgb_panel->user_ctx = rgb_panel_config->user_ctx;
rgb_panel->spinlock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED;
@ -252,6 +324,8 @@ static esp_err_t rgb_panel_del(esp_lcd_panel_t *panel)
lcd_ll_enable_clock(rgb_panel->hal.dev, false);
periph_module_disable(lcd_periph_signals.panels[panel_id].module);
lcd_com_remove_device(LCD_COM_DEVICE_TYPE_RGB, rgb_panel->panel_id);
free(rgb_panel->bounce_buffer[0]);
free(rgb_panel->bounce_buffer[1]);
free(rgb_panel->fb);
if (rgb_panel->pm_lock) {
esp_pm_lock_release(rgb_panel->pm_lock);
@ -317,6 +391,11 @@ static esp_err_t rgb_panel_init(esp_lcd_panel_t *panel)
static esp_err_t rgb_panel_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data)
{
esp_rgb_panel_t *rgb_panel = __containerof(panel, esp_rgb_panel_t, base);
if (rgb_panel->fb == NULL) {
//Can't draw a bitmap to a non-existing framebuffer.
//This happens when e.g. we use an external callback to refill the bounce buffers.
return ESP_ERR_NOT_SUPPORTED;
}
assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position");
// adjust the flush window by adding extra gap
x_start += rgb_panel->x_gap;
@ -342,9 +421,9 @@ static esp_err_t rgb_panel_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int
to += bytes_per_line;
from += copy_bytes_per_line;
}
if (rgb_panel->flags.fb_in_psram) {
if (rgb_panel->flags.fb_in_psram && !rgb_panel->bounce_buffer_size_bytes) {
// CPU writes data to PSRAM through DCache, data in PSRAM might not get updated, so write back
// Note that if we use a bounce buffer, the data gets read by the CPU as well so no need to write back.
uint32_t bytes_to_flush = (y_end - y_start) * bytes_per_line;
Cache_WriteBack_Addr((uint32_t)(rgb_panel->fb + y_start * bytes_per_line), bytes_to_flush);
}
@ -480,18 +559,97 @@ static esp_err_t lcd_rgb_panel_select_clock_src(esp_rgb_panel_t *panel, lcd_cloc
return ret;
}
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->data_width / 8;
if (panel->on_bounce_empty) {
//We don't have a framebuffer here; we need to call a callback to refill the bounce buffer
//for us.
need_yield=panel->on_bounce_empty((void*)buffer, panel->bounce_pos_px,
panel->bounce_buffer_size_bytes, panel->bounce_buffer_cb_user_ctx);
} else {
//We do have a framebuffer; copy from there.
memcpy(buffer, &panel->fb[panel->bounce_pos_px * bytes_per_pixel], panel->bounce_buffer_size_bytes);
if (panel->flags.bb_do_cache_invalidate) {
//We don't need the bytes we copied from psram anymore.
//Make sure that if anything happened to have changed (because the line already was in cache) we write
//the data back.
Cache_WriteBack_Addr((uint32_t)&panel->fb[panel->bounce_pos_px * bytes_per_pixel], panel->bounce_buffer_size_bytes);
//Invalidate the data. Note: possible race: perhaps something on the other core can squeeze a write
//between this and the writeback, in which case that data gets discarded.
Cache_Invalidate_Addr((uint32_t)&panel->fb[panel->bounce_pos_px * bytes_per_pixel], panel->bounce_buffer_size_bytes);
}
}
panel->bounce_pos_px+=panel->bounce_buffer_size_bytes / bytes_per_pixel;
//If the bounce pos is larger than the framebuffer 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;
}
if (!panel->on_bounce_empty) {
//Preload the next bit of buffer into psram.
Cache_Start_DCache_Preload((uint32_t)&panel->fb[panel->bounce_pos_px * bytes_per_pixel],
panel->bounce_buffer_size_bytes, 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])?1:0;
return lcd_rgb_panel_fill_bounce_buffer(panel, panel->bounce_buffer[bb]);
}
//If we restart GDMA, this many pixels will already have been transfered to the
//LCD peripheral. Looks like that has 16 pixels of FIFO plus one holding register.
#define LCD_FIFO_SIZE_PX 17
static esp_err_t lcd_rgb_panel_create_trans_link(esp_rgb_panel_t *panel)
{
esp_err_t ret = ESP_OK;
// chain DMA descriptors
for (int i = 0; i < panel->num_dma_nodes; i++) {
panel->dma_nodes[i].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU;
panel->dma_nodes[i].next = &panel->dma_nodes[i + 1];
if (panel->bounce_buffer_size_bytes == 0) {
// Create DMA descriptors for main framebuffer:
// chain DMA descriptors
for (int i = 0; i < panel->num_dma_nodes; i++) {
panel->dma_nodes[i].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU;
panel->dma_nodes[i].next = &panel->dma_nodes[i + 1];
}
// loop end back to start (note: this is needed as we restart the DMA transaction in the VBlank,
// but it should already have LCD_FIFO_PIX bytes from the start of the buffer at that time)
panel->dma_nodes[panel->num_dma_nodes - 1].next = &panel->dma_nodes[0];
// mount the frame buffer to the DMA descriptors
lcd_com_mount_dma_data(panel->dma_nodes, panel->fb, panel->fb_size);
} else {
//Create DMA descriptors for bounce buffers:
// chain DMA descriptors
for (int i = 0; i < panel->num_dma_nodes; i++) {
panel->dma_nodes[i].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU;
panel->dma_nodes[i].next = &panel->dma_nodes[i + 1];
}
// loop end back to start
panel->dma_nodes[panel->num_dma_nodes - 1].next = &panel->dma_nodes[0];
// set eof on end of 1st and 2nd bounce buffer so we get an interrupt when they're fully sent
panel->dma_nodes[(panel->num_dma_nodes/2) - 1].dw0.suc_eof=1;
panel->dma_nodes[panel->num_dma_nodes - 1].dw0.suc_eof=1;
// mount the bounce buffers to the DMA descriptors
lcd_com_mount_dma_data(&panel->dma_nodes[0], panel->bounce_buffer[0], panel->bounce_buffer_size_bytes);
lcd_com_mount_dma_data(&panel->dma_nodes[(panel->num_dma_nodes/2)], panel->bounce_buffer[1], panel->bounce_buffer_size_bytes);
}
// one-off DMA chain
panel->dma_nodes[panel->num_dma_nodes - 1].next = NULL;
// mount the frame buffer to the DMA descriptors
lcd_com_mount_dma_data(panel->dma_nodes, panel->fb, panel->fb_size);
//On restart, the data sent to the LCD peripheral needs to start 17 pixels after the FB start (the length of the
//lcd fifo) so we use a special DMA node to restart the DMA transaction.
memcpy(&panel->dma_restart_node, &panel->dma_nodes[0], sizeof(panel->dma_restart_node));
int restart_skip_bytes=LCD_FIFO_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,
@ -504,6 +662,12 @@ static esp_err_t lcd_rgb_panel_create_trans_link(esp_rgb_panel_t *panel)
.sram_trans_align = panel->sram_trans_align,
};
gdma_set_transfer_ability(panel->dma_chan, &ability);
if (panel->bounce_buffer_size_bytes != 0) {
// register callback to re-fill bounce buffers once they're fully sent
gdma_tx_event_callbacks_t cbs={0};
cbs.on_trans_eof = lcd_rgb_panel_eof_handler;
gdma_register_tx_event_callbacks(panel->dma_chan, &cbs, panel);
}
// the start of DMA should be prior to the start of LCD engine
gdma_start(panel->dma_chan, (intptr_t)panel->dma_nodes);
@ -511,6 +675,28 @@ err:
return ret;
}
static IRAM_ATTR void lcd_rgb_panel_restart_transmission(esp_rgb_panel_t *panel)
{
if (panel->bounce_buffer_size_bytes != 0) {
//Catch de-synced framebuffer and reset if needed.
if (panel->bounce_pos_px > panel->bounce_buffer_size_bytes) panel->bounce_pos_px=0;
//Pre-fill bounce buffer 0, if the EOF ISR didn't do that already
if (panel->bounce_pos_px < panel->bounce_buffer_size_bytes/2) {
lcd_rgb_panel_fill_bounce_buffer(panel, panel->bounce_buffer[0]);
}
}
gdma_reset(panel->dma_chan);
gdma_start(panel->dma_chan, (intptr_t)&panel->dma_restart_node);
if (panel->bounce_buffer_size_bytes != 0) {
//Fill 2nd bounce buffer while 1st is being sent out, if needed.
if (panel->bounce_pos_px < panel->bounce_buffer_size_bytes) {
lcd_rgb_panel_fill_bounce_buffer(panel, panel->bounce_buffer[0]);
}
}
}
static void lcd_rgb_panel_start_transmission(esp_rgb_panel_t *rgb_panel)
{
// reset FIFO of DMA and LCD, incase there remains old frame data
@ -526,11 +712,20 @@ static void lcd_rgb_panel_start_transmission(esp_rgb_panel_t *rgb_panel)
portEXIT_CRITICAL_SAFE(&rgb_panel->spinlock);
lcd_ll_fifo_reset(rgb_panel->hal.dev);
//pre-fill bounce buffers if needed
if (rgb_panel->bounce_buffer_size_bytes != 0) {
rgb_panel->bounce_pos_px = 0;
lcd_rgb_panel_fill_bounce_buffer(rgb_panel, rgb_panel->bounce_buffer[0]);
lcd_rgb_panel_fill_bounce_buffer(rgb_panel, rgb_panel->bounce_buffer[1]);
}
gdma_start(rgb_panel->dma_chan, (intptr_t)rgb_panel->dma_nodes);
// delay 1us is sufficient for DMA to pass data to LCD FIFO
// in fact, this is only needed when LCD pixel clock is set too high
esp_rom_delay_us(1);
// start LCD engine
lcd_ll_enable_auto_next_frame(rgb_panel->hal.dev, rgb_panel->flags.stream_mode);
lcd_ll_start(rgb_panel->hal.dev);
}
@ -548,11 +743,19 @@ IRAM_ATTR static void lcd_default_isr_handler(void *args)
need_yield = true;
}
}
// to restart the transmission
if (rgb_panel->flags.stream_mode) {
lcd_rgb_panel_start_transmission(rgb_panel);
// As described above, we reset the GDMA channel every VBlank to stop permanent
// desyncs from happening.
// Note that this fix can lead to single-frame desyncs itself, as in: if this interrupt
// is late enough, the display will shift as the LCD controller already read out the
// first data bytes, and resetting DMA will re-send those. However, the single-frame
// desync this leads to is preferable to the permanent desync that could otherwise
// happen. It's also not super-likely as this interrupt has the entirety of the VBlank
// time to reset DMA.
lcd_rgb_panel_restart_transmission(rgb_panel);
}
}
if (need_yield) {
portYIELD_FROM_ISR();
}