{"id":4753,"date":"2019-07-17T10:51:50","date_gmt":"2019-07-17T17:51:50","guid":{"rendered":"https:\/\/visualgdb.com\/w\/?p=4753"},"modified":"2025-09-09T19:28:56","modified_gmt":"2025-09-10T02:28:56","slug":"using-the-lcd-display-of-the-esp32-wrover-board","status":"publish","type":"post","link":"https:\/\/visualgdb.com\/tutorials\/esp32\/lcd\/","title":{"rendered":"Using the LCD Display of the ESP32-WROVER Board"},"content":{"rendered":"<p>This tutorial shows how to use the on-board display on the ESP32-WROVER board. We will clone a basic LCD example project from the ESP-IDF, show the role of different components of the sample and modify it to continuously download a JPG picture from an arbitrary online URL and display it on the screen.<\/p>\n<p>Before you begin, install Visual Studio and VisualGDB 5.4 or later.<\/p>\n<ol>\n<li>Start Visual Studio and open the VisualGDB ESP32 Project Wizard:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/01-newprj-4.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4754\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/01-newprj-4.png\" alt=\"\" width=\"941\" height=\"653\" \/><\/a><\/li>\n<li>On the first page of the wizard select the recommended CMake build subsystem:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/02-cmake-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4755\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/02-cmake-1.png\" alt=\"\" width=\"856\" height=\"693\" \/><\/a><\/li>\n<li>On the next page pick your toolchain and an ESP-IDF checkout. For all new projects we recommend always using the latest toolchain and the latest stable ESP-IDF release:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/03-esp32.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4756\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/03-esp32.png\" alt=\"\" width=\"856\" height=\"693\" \/><\/a><strong>Update:<\/strong> For better compatibility with the latest ESP32 tools, we recommend selecting the <a href=\"https:\/\/visualgdb.com\/documentation\/espidf\/consolidated\/\">consolidated toolchain<\/a> instead.<\/li>\n<li>On the <strong>Project Sample<\/strong> page pick the <strong>spi_master<\/strong> sample. VisualGDB will automatically clone it into a new project when you complete the wizard:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/04-spi-master.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4757\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/04-spi-master.png\" alt=\"\" width=\"856\" height=\"693\" \/><\/a><\/li>\n<li>Finally, choose the debugging settings that match your target. The ESP32-WROVER module comes with an on-board JTAG programmer, so enabling JTAG debugging via jumpers and plugging the board in should be sufficient for VisualGDB to detect it. In case of any problems, follow our <a href=\"https:\/\/visualgdb.com\/tutorials\/esp32\/wrover\/\">ESP32-WROVER tutorial<\/a> to get debugging to work:\u00a0 <a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/05-debug-2.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4758\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/05-debug-2.png\" alt=\"\" width=\"856\" height=\"693\" \/><\/a><\/li>\n<li>Press &#8220;Finish&#8221; to create the project. Once it has been created, press F5 to build and run it and observe the LCD screen of the WROVER module. You should see waves going over the &#8220;ESP32&#8221; text :<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/esp32_logo.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4765\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/esp32_logo.jpg\" alt=\"\" width=\"1200\" height=\"973\" \/><\/a><\/li>\n<li>Now we will look into the internals of the example to understand the involved components. Run the Debug-&gt;Break All command and switch to the main thread via the <strong>Threads<\/strong> view. You should see the <strong>pretty_effect_calc_lines()<\/strong> function being called by <strong>display_pretty_colors()<\/strong>:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/06-display.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4759\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/06-display.png\" alt=\"\" width=\"1201\" height=\"796\" \/><\/a><\/li>\n<li>You can use the CodeJumps navigation (clickable links on top of function names) to quickly jump between the functions calling each other:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/07-calls.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4760\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/07-calls.png\" alt=\"\" width=\"1201\" height=\"796\" \/><\/a><\/li>\n<li>In this example, the following logic is used to display the image on the LCD screen:\n<ul>\n<li>The FLASH image generated by the project contains a copy of the <strong>image.jpg<\/strong> (this is done by setting the <strong>COMPONENT_EMBED_FILES<\/strong> variable in <strong>CMakeLists.txt<\/strong>)<\/li>\n<li>At startup,<strong> app_main()<\/strong> calls <strong>pretty_effect_init()<\/strong> that in turn calls the <strong>decode_image()<\/strong> function that decodes the <strong>image.jpg<\/strong> file from the FLASH memory.<\/li>\n<li>Then, <strong>app_main()<\/strong> calls <strong>display_pretty_colors() <\/strong>that starts applying the wave effect to the image. More specifically, it does the following\n<ul>\n<li>Allocates space for 16 lines of the image using the <strong>heap_caps_malloc()<\/strong> function (note the <strong>MALLOC_CAP_DMA<\/strong> flag that ensures that the allocated memory can be used by the DMA engine).<\/li>\n<li>Splits the image into 16-line sections. Then it uses double-buffering to optimize the image updating:\n<ul>\n<li>If the previous iteration has computed some pixel values, <strong>display_pretty_colors()<\/strong> starts sending them to the display via a background DMA transfer.<\/li>\n<li>At the same time, it uses the CPU to compute the values for the next batch of pixels.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p><a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/08-decode.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4761\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/08-decode.png\" alt=\"\" width=\"1201\" height=\"796\" \/><\/a><\/li>\n<li>We will now change the example to download the image from a specific URL instead of hardcoding it in the FLASH memory. First of all, add the code below to the main source file:\n<pre class=\"\">#include \"freertos\/FreeRTOS.h\"\r\n#include \"freertos\/task.h\"\r\n#include \"freertos\/event_groups.h\"\r\n#include \"esp_system.h\"\r\n#include \"esp_wifi.h\"\r\n#include \"esp_event_loop.h\"\r\n#include \"esp_log.h\"\r\n#include \"nvs_flash.h\"\r\n\r\nstatic EventGroupHandle_t s_WifiEventGroup;\r\nconst int WIFI_CONNECTED_BIT = BIT0;\r\n\r\nstatic esp_err_t event_handler(void *ctx, system_event_t *event)\r\n{\r\n    switch (event-&gt;event_id)\r\n    {\r\n    case SYSTEM_EVENT_STA_START:\r\n        esp_wifi_connect();\r\n        break;\r\n    case SYSTEM_EVENT_STA_GOT_IP:\r\n        xEventGroupSetBits(s_WifiEventGroup, WIFI_CONNECTED_BIT);\r\n        break;\r\n    case SYSTEM_EVENT_STA_DISCONNECTED:\r\n        esp_wifi_connect();\r\n        xEventGroupClearBits(s_WifiEventGroup, WIFI_CONNECTED_BIT);\r\n        break;\r\n    default:\r\n        break;\r\n    }\r\n    return ESP_OK;\r\n}\r\n\r\nvoid wifi_init_sta()\r\n{\r\n    s_WifiEventGroup = xEventGroupCreate();\r\n    esp_err_t ret = nvs_flash_init();\r\n\r\n    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)\r\n    {\r\n        ESP_ERROR_CHECK(nvs_flash_erase());\r\n        ret = nvs_flash_init();\r\n    }\r\n\r\n    ESP_ERROR_CHECK(ret);\r\n    tcpip_adapter_init();\r\n    ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL));\r\n    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();\r\n    ESP_ERROR_CHECK(esp_wifi_init(&amp;cfg));\r\n\r\n    wifi_config_t wifi_config = {\r\n        .sta = {\r\n            .ssid = \"espnet\",\r\n            .password = \"sysprogs\"},\r\n    };\r\n\r\n    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));\r\n    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &amp;wifi_config));\r\n    ESP_ERROR_CHECK(esp_wifi_start());\r\n}<\/pre>\n<p>Then replace the SSID and password with the values corresponding to your Wi-FI network and add the following lines to main() before the call to <strong>display_pretty_colors()<\/strong>:<\/p>\n<pre class=\"\">    wifi_init_sta();\r\n    xEventGroupWaitBits(s_WifiEventGroup, WIFI_CONNECTED_BIT, pdFALSE, pdFALSE, portMAX_DELAY);\r\n<\/pre>\n<p>This will make the WROVER board connect to a Wi-Fi network and set the bit #0 in the\u00a0s_WifiEventGroup object once the board get assigned an IP address. The call to xEventGroupWaitBits() will wait for that bit to be set.<\/li>\n<li>Before we add the logic for downloading the JPG file from a given URL, run the current version of the firmware to test the Wi-Fi connection. If you are using the custom version of VisualGDB, try enabling the raw terminal on the WROVER&#8217;s COM port so that you can see the diagnostic messages from the board: <a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/09-serial.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4762\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/09-serial.png\" alt=\"\" width=\"1201\" height=\"796\" \/><\/a><\/li>\n<li>Run your program and ensure it connects to the Wi-Fi network (the display will stay blank until the connection is established):<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/10-conn.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4763\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/10-conn.png\" alt=\"\" width=\"1201\" height=\"796\" \/><\/a><\/li>\n<li>Add the <strong>buffer<\/strong> and <strong>size<\/strong> parameters to <strong>decode_image()<\/strong> and modify it to use the<strong> buffer<\/strong> argument instead of the hardcoded JPG file (don&#8217;t forget to update the header file as well): <a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/11-decode-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4766\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/11-decode-1.png\" alt=\"\" width=\"1201\" height=\"796\" \/><\/a>The <strong>decode_image()<\/strong> function will automatically provide the necessary callbacks to the <strong>jd_decomp()<\/strong> function ensuring that the image gets decoded to the raw format compatible with the ESP32-WROVER&#8217;s LCD display.<\/li>\n<li>Finally, add the DownloadAndDisplay() function below to the main source file and call it from <strong>app_main()<\/strong> instead of <strong>display_pretty_colors()<\/strong>:\n<pre class=\"\">#include \"esp_http_client.h\"\r\n#include \"decode_image.h\"\r\n\r\nstatic void DownloadAndDisplay(spi_device_handle_t spi)\r\n{\r\n    const int kMaxJpegFileSize = 32768;\r\n    char *pBuf = (char *)malloc(kMaxJpegFileSize);\r\n    uint16_t *pDMABuf = heap_caps_malloc(320 * PARALLEL_LINES * sizeof(uint16_t), MALLOC_CAP_DMA);\r\n\r\n    for (;;)\r\n    {\r\n        esp_http_client_config_t config = {\r\n            .url = \"http:\/\/visualgdb.com\/demo\/vgdb320.jpg\",\r\n        };\r\n\r\n        esp_http_client_handle_t client = esp_http_client_init(&amp;config);\r\n        esp_http_client_open(client, 0);\r\n        int content_length = esp_http_client_fetch_headers(client);\r\n        int total_read_len = 0, read_len = 0;\r\n        if (total_read_len &lt; content_length &amp;&amp; content_length &lt;= kMaxJpegFileSize)\r\n            read_len = esp_http_client_read(client, pBuf, content_length);\r\n        esp_http_client_close(client);\r\n        esp_http_client_cleanup(client);\r\n\r\n        if (read_len)\r\n        {\r\n            uint16_t **decoded = NULL;\r\n            esp_err_t rc = decode_image(pBuf, read_len, &amp;decoded);\r\n            if (!rc)\r\n            {\r\n                for (int y = 0; y &lt; 240; y += PARALLEL_LINES)\r\n                {\r\n                    for (int yo = 0; yo &lt; PARALLEL_LINES; yo++)\r\n                        memcpy(pDMABuf + yo * 320, decoded[y + yo], 320 * sizeof(uint16_t));\r\n                    send_lines(spi, y, pDMABuf);\r\n                    send_line_finish(spi);\r\n                }\r\n            }\r\n\r\n            if (decoded)\r\n            {\r\n                for (int i = 0; i &lt; 256; i++)\r\n                {\r\n                    free((decoded)[i]);\r\n                }\r\n                free(decoded);\r\n            }\r\n\r\n            vTaskDelay(5000 \/ portTICK_RATE_MS);\r\n        }\r\n    }\r\n}<\/pre>\n<p>Also remove the <strong>pretty_effect_init()<\/strong> function and the call from <strong>app_main()<\/strong> to it as it is no longer required.<\/li>\n<li>The code we have added creates a DMA-accessible buffer similar <strong>display_pretty_colors() <\/strong>and the following actions in an infinite loop:\n<ul>\n<li>Downloads the <a href=\"http:\/\/visualgdb.com\/demo\/vgdb320.jpg\">http:\/\/visualgdb.com\/demo\/vgdb320.jpg<\/a> image into a temporary buffer (note the 32KB size limit).<\/li>\n<li>If the image has been downloaded successfully, it is decoded using the <strong>decode_image()<\/strong> that was previously used to load the image from the FLASH memory.<\/li>\n<li>Finally, the image is copied to the DMA-accessible memory and sent to the on-board display. For simplicity, our example does not use double-buffering.<\/li>\n<\/ul>\n<p>When you run the final version of the example, it will download the demo image from <a href=\"https:\/\/visualgdb.com\/\">visualgdb.com<\/a> and will show it on the on-board LCD display:<a href=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/vgdb_gears.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4768\" src=\"https:\/\/visualgdb.com\/w\/wp-content\/uploads\/2019\/06\/vgdb_gears.jpg\" alt=\"\" width=\"1280\" height=\"882\" \/><\/a><\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>This tutorial shows how to use the on-board display on the ESP32-WROVER board. We will clone a basic LCD example<\/p>\n","protected":false},"author":1,"featured_media":4769,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[142],"tags":[138,101,56,99],"_links":{"self":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts\/4753"}],"collection":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/comments?post=4753"}],"version-history":[{"count":3,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts\/4753\/revisions"}],"predecessor-version":[{"id":9041,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/posts\/4753\/revisions\/9041"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/media\/4769"}],"wp:attachment":[{"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/media?parent=4753"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/categories?post=4753"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/visualgdb.com\/w\/wp-json\/wp\/v2\/tags?post=4753"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}