Skip to content

Commit

Permalink
rgb_lcd: support restart dma transmission manually
Browse files Browse the repository at this point in the history
When doing Flash operations (e.g. OTA), LCD's DMA bandwidth will be not
sufficient, causing the desync between the LCD controller and DMA.

Added a restart function to help the user to make them sync again.
  • Loading branch information
suda-morris committed Dec 2, 2022
1 parent 7f5ecbe commit 7d39d12
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 22 deletions.
30 changes: 24 additions & 6 deletions components/esp_lcd/include/esp_lcd_panel_rgb.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ typedef struct {
/**
* @brief RGB LCD VSYNC event callback prototype
*
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel()`
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel`
* @param[in] edata Panel event data, fed by driver
* @param[in] user_ctx User data, passed from `esp_lcd_rgb_panel_register_event_callbacks()`
* @return Whether a high priority task has been waken up by this function
Expand All @@ -89,7 +89,7 @@ typedef bool (*esp_lcd_rgb_panel_vsync_cb_t)(esp_lcd_panel_handle_t panel, const
/**
* @brief Prototype for function to re-fill a bounce buffer, rather than copying from the frame buffer
*
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel()`
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel`
* @param[in] bounce_buf Bounce buffer to write data into
* @param[in] pos_px How many pixels already were sent to the display in this frame, in other words,
* at what pixel the routine should start putting data into bounce_buf
Expand Down Expand Up @@ -157,7 +157,7 @@ esp_err_t esp_lcd_new_rgb_panel(const esp_lcd_rgb_panel_config_t *rgb_panel_conf
/**
* @brief Register LCD RGB panel event callbacks
*
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel()`
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel`
* @param[in] callbacks Group of callback functions
* @param[in] user_ctx User data, which will be passed to the callback functions directly
* @return
Expand All @@ -176,18 +176,36 @@ esp_err_t esp_lcd_rgb_panel_register_event_callbacks(esp_lcd_panel_handle_t pane
* @note This function doesn't cause the hardware to update the PCLK immediately but to record the new frequency and set a flag internally.
* Only in the next VSYNC event handler, will the driver attempt to update the PCLK frequency.
*
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel()`
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel`
* @param[in] freq_hz Frequency of pixel clock, in Hz
* @return
* - ESP_ERR_INVALID_ARG: Set PCLK frequency failed because of invalid argument
* - ESP_OK: Set PCLK frequency successfully
*/
esp_err_t esp_lcd_rgb_panel_set_pclk(esp_lcd_panel_handle_t panel, uint32_t freq_hz);

/**
* @brief Restart the LCD transmission
*
* @note This function can be useful when the LCD controller is out of sync with the DMA because of insufficient bandwidth.
* To save the screen from a permanent shift, you can call this function to restart the LCD DMA.
* @note This function doesn't restart the DMA immediately but to set a flag internally.
* Only in the next VSYNC event handler, will the driver attempt to do the restart job.
* @note If CONFIG_LCD_RGB_RESTART_IN_VSYNC is enabled, you don't need to call this function manually,
* because the restart job will be done automatically in the VSYNC event handler.
*
* @param[in] panel panel LCD panel handle, returned from `esp_lcd_new_rgb_panel`
* @return
* - ESP_ERR_INVALID_ARG: Restart the LCD failed because of invalid argument
* - ESP_ERR_INVALID_STATE: Restart the LCD failed because the LCD diver is working in refresh-on-demand mode
* - ESP_OK: Restart the LCD successfully
*/
esp_err_t esp_lcd_rgb_panel_restart(esp_lcd_panel_handle_t panel);

/**
* @brief Get the address of the frame buffer(s) that allocated by the driver
*
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel()`
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel`
* @param[in] fb_num Number of frame buffer(s) to get. This value must be the same as the number of the following parameters.
* @param[out] fb0 Returned address of the frame buffer 0
* @param[out] ... List of other frame buffer addresses
Expand All @@ -202,7 +220,7 @@ esp_err_t esp_lcd_rgb_panel_get_frame_buffer(esp_lcd_panel_handle_t panel, uint3
*
* @note This function should only be called when the RGB panel is working under the `refresh_on_demand` mode.
*
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel()`
* @param[in] panel LCD panel handle, returned from `esp_lcd_new_rgb_panel`
* @return
* - ESP_ERR_INVALID_ARG: Start a refresh failed because of invalid argument
* - ESP_ERR_INVALID_STATE: Start a refresh failed because the LCD panel is not created with the `refresh_on_demand` flag enabled.
Expand Down
56 changes: 41 additions & 15 deletions components/esp_lcd/src/esp_lcd_rgb_panel.c
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ struct esp_rgb_panel_t {
uint32_t no_fb: 1; // No frame buffer allocated in the driver
uint32_t fb_in_psram: 1; // Whether the frame buffer is in PSRAM
uint32_t need_update_pclk: 1; // Whether to update the PCLK before start a new transaction
uint32_t need_restart: 1; // Whether to restart the LCD controller and the DMA
uint32_t bb_invalidate_cache: 1; // Whether to do cache invalidation in bounce buffer mode
} flags;
dma_descriptor_t *dma_links[2]; // fbs[0] <-> dma_links[0], fbs[1] <-> dma_links[1]
Expand Down Expand Up @@ -362,6 +363,19 @@ esp_err_t esp_lcd_rgb_panel_set_pclk(esp_lcd_panel_handle_t panel, uint32_t freq
return ESP_OK;
}

esp_err_t esp_lcd_rgb_panel_restart(esp_lcd_panel_handle_t panel)
{
ESP_RETURN_ON_FALSE(panel, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
esp_rgb_panel_t *rgb_panel = __containerof(panel, esp_rgb_panel_t, base);
ESP_RETURN_ON_FALSE(rgb_panel->flags.stream_mode, ESP_ERR_INVALID_STATE, TAG, "not in stream mode");

// the underlying restart job will be done in the `LCD_LL_EVENT_VSYNC_END` event handler
portENTER_CRITICAL(&rgb_panel->spinlock);
rgb_panel->flags.need_restart = true;
portEXIT_CRITICAL(&rgb_panel->spinlock);
return ESP_OK;
}

esp_err_t esp_lcd_rgb_panel_get_frame_buffer(esp_lcd_panel_handle_t panel, uint32_t fb_num, void **fb0, ...)
{
ESP_RETURN_ON_FALSE(panel, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
Expand Down Expand Up @@ -996,16 +1010,15 @@ static esp_err_t lcd_rgb_panel_create_trans_link(esp_rgb_panel_t *panel)
}
}

#if CONFIG_LCD_RGB_RESTART_IN_VSYNC
// 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;
#endif

// alloc DMA channel and connect to LCD peripheral
gdma_channel_alloc_config_t dma_chan_config = {
Expand All @@ -1030,9 +1043,31 @@ static esp_err_t lcd_rgb_panel_create_trans_link(esp_rgb_panel_t *panel)
return ESP_OK;
}

#if CONFIG_LCD_RGB_RESTART_IN_VSYNC
static IRAM_ATTR void lcd_rgb_panel_restart_transmission_in_isr(esp_rgb_panel_t *panel)
// 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.
static IRAM_ATTR void lcd_rgb_panel_try_restart_transmission(esp_rgb_panel_t *panel)
{
bool do_restart = false;
#if CONFIG_LCD_RGB_RESTART_IN_VSYNC
do_restart = true;
#else
portENTER_CRITICAL_ISR(&panel->spinlock);
if (panel->flags.need_restart) {
panel->flags.need_restart = false;
do_restart = true;
}
portEXIT_CRITICAL_ISR(&panel->spinlock);
#endif // CONFIG_LCD_RGB_RESTART_IN_VSYNC

if (!do_restart) {
return;
}

if (panel->bb_size) {
// Catch de-synced frame buffer and reset if needed.
if (panel->bounce_pos_px > panel->bb_size) {
Expand All @@ -1055,7 +1090,6 @@ static IRAM_ATTR void lcd_rgb_panel_restart_transmission_in_isr(esp_rgb_panel_t
}
}
}
#endif

static void lcd_rgb_panel_start_transmission(esp_rgb_panel_t *rgb_panel)
{
Expand Down Expand Up @@ -1109,16 +1143,8 @@ IRAM_ATTR static void lcd_default_isr_handler(void *args)
lcd_rgb_panel_try_update_pclk(rgb_panel);

if (rgb_panel->flags.stream_mode) {
#if CONFIG_LCD_RGB_RESTART_IN_VSYNC
// 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_in_isr(rgb_panel);
#endif
// check whether to restart the transmission
lcd_rgb_panel_try_restart_transmission(rgb_panel);
}

}
Expand Down
25 changes: 25 additions & 0 deletions components/esp_lcd/test_apps/rgb_lcd/main/test_rgb_panel.c
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,31 @@ TEST_CASE("lcd_rgb_panel_update_pclk", "[lcd]")
free(img);
}

TEST_CASE("lcd_rgb_panel_restart", "[lcd]")
{
uint8_t *img = malloc(TEST_IMG_SIZE);
TEST_ASSERT_NOT_NULL(img);

printf("initialize RGB panel with stream mode\r\n");
esp_lcd_panel_handle_t panel_handle = test_rgb_panel_initialization(16, 16, 0, false, NULL, NULL);
printf("flush one clock block to the LCD\r\n");
uint8_t color_byte = esp_random() & 0xFF;
int x_start = esp_random() % (TEST_LCD_H_RES - 100);
int y_start = esp_random() % (TEST_LCD_V_RES - 100);
memset(img, color_byte, TEST_IMG_SIZE);
esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_start + 100, y_start + 100, img);
printf("The LCD driver should keep flushing the color block in the background (as it's in stream mode)\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));

printf("Restart the DMA transmission in the background\r\n");
TEST_ESP_OK(esp_lcd_rgb_panel_restart(panel_handle));
vTaskDelay(pdMS_TO_TICKS(1000));

printf("delete RGB panel\r\n");
TEST_ESP_OK(esp_lcd_panel_del(panel_handle));
free(img);
}

TEST_CASE("lcd_rgb_panel_rotate", "[lcd]")
{
const int w = 200;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
CONFIG_COMPILER_DUMP_RTL_FILES=y
CONFIG_LCD_RGB_ISR_IRAM_SAFE=y
CONFIG_GDMA_CTRL_FUNC_IN_IRAM=y
CONFIG_COMPILER_OPTIMIZATION_NONE=y
# silent the error check, as the error string are stored in rodata, causing RTL check failure
CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT=y
2 changes: 1 addition & 1 deletion docs/en/api-reference/peripherals/lcd.rst
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ After we get the LCD handle, the remaining LCD operations are the same for diffe
.. note::

It should never happen in a well-designed embedded application, but it can in theory be possible that the DMA cannot deliver data as fast as the LCD consumes it. In the {IDF_TARGET_NAME} hardware, this leads to the LCD simply outputting dummy bytes while DMA waits for data. If we were to run DMA in a stream fashion, this would mean a de-sync between the LCD address the DMA 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, you can enable the :ref:`CONFIG_LCD_RGB_RESTART_IN_VSYNC` option, so the driver will restart the DMA in the VBlank interrupt; this way we always know where it starts.
In order to stop this from happening, you can either enable the :ref:`CONFIG_LCD_RGB_RESTART_IN_VSYNC` option, so the driver can restart the DMA in the VBlank interrupt automatically or call :cpp:func:`esp_lcd_rgb_panel_restart` to restart the DMA manually. Note :cpp:func:`esp_lcd_rgb_panel_restart` doesn't restart the DMA immediately, the DMA will still be restarted in the next VSYNC event.

Application Example
-------------------
Expand Down

0 comments on commit 7d39d12

Please sign in to comment.