Lesson01_RotaryScreen_LVGL: ESP32-S3 Rotary Display, Touch, and Encoder Interaction¶
1. Lesson Introduction¶
This lesson uses Arduino IDE 2.3.6 and ESP32 Arduino Core 3.3.8 to drive an ESP32-S3 1.46-inch 360 × 360 rotary display. The project controls the ST77961 LCD through LovyanGFX, reads touch coordinates through the CST816T, implements the graphical interface using LVGL 9.1.0 and a UI exported from SquareLine Studio 1.5.3, and uses separate FreeRTOS tasks to handle the rotary encoder and eight NeoPixel RGB LEDs.
After the program is uploaded and the board is reset, the screen first displays a startup page with a rotating icon, followed by a fade-in transition to the main menu. The main menu contains three options: Volume, Bulb, and Light. Rotate the knob to switch the current selection, and single-click it to open the corresponding detail page. On a detail page, rotate the knob or touch the arc control to adjust the value in 5% increments. Double-click the knob or touch the Return button to return to the main menu. The Bulb value synchronously controls the PWM output on GPIO 43, while the Light value synchronously changes the screen backlight brightness. At the same time, the eight onboard RGB LEDs cycle through running-light, flashing, and multicolor breathing effects.
The focus of this lesson is not to introduce a particular LVGL API in isolation, but to verify the entire signal chain—from display power, SPI DMA screen updates, I2C touch input, and rotary encoder input to LVGL page interaction and PWM output. Note that the Volume page currently changes only the displayed value and is not connected to audio playback functionality. The touch-coordinate debugging macro is disabled by default, so touch coordinates are not printed to the serial port.
2. Learning Objectives¶
- Configure the ESP32-S3, 16 MB Flash, OPI PSRAM, and Arduino Core version according to the project requirements.
- Explain the data relationships among the ST77961 display, CST816T touch controller, LVGL flush callback, and SquareLine UI.
- Compile and upload the project, and determine whether the display chain is working correctly by checking the startup page, main menu, and detail pages.
- Use rotation, single-click, double-click, and touch operations to select pages, adjust values, and return to the previous page.
- Determine whether each function is working correctly based on UI values, GPIO 43 PWM output, screen backlight, NeoPixel lighting effects, and serial logs.
3. Required Materials¶
3.1 Hardware¶
- ESP32-S3 1.46-inch 360 × 360 rotary display development board, 1.
- USB data cable that supports data transfer, 1.
- Computer, 1.
This project uses the onboard ST77961 LCD, CST816T touch controller, rotary encoder, encoder push button, and eight NeoPixel RGB LEDs. No external modules are required for these components.
3.2 Software and Versions¶
- Arduino IDE 2.3.6.
- ESP32 Arduino Core 3.3.8.
- LVGL 9.1.0.
- LovyanGFX 1.2.7.
- Adafruit NeoPixel 1.15.1.
- CST816T 1.5.1.
- Adafruit BusIO 1.17.4, Adafruit GFX Library 1.12.4, UI files, and other dependencies are included in the project.
3.3 Required Project Files¶
RotaryScreen_1_46_Code_Core3_LVGL9.ino: Main program.RotaryScreen_1_46.h: Pin assignments, display bus, panel parameters, and global state configuration.libraries/UI/: LVGL 9.1 UI, image assets, and page code exported from SquareLine Studio.libraries/lvgl/,libraries/LovyanGFX/,libraries/cst816t/,libraries/Adafruit_NeoPixel/: Project dependencies.
Do not copy only the .ino file. If the header file in the same directory or libraries/UI/ is missing, the project cannot display the UI used in this lesson. Mixing in another major LVGL version may also cause API or generated-code incompatibilities.
4. Software Procedure¶
Arduino software installation guide: https://www.elecrow.com/wiki/Get_Started_with_Arduino_IDE.html
Select ESP32 version 3.3.8. Code repository: https://github.com/Elecrow-RD/CrowPanel-1.46inch-HMI-ESP32-Rotary-Display/tree/master/example/V1.0/Arduino/RotaryScreen_1_46_Code_Core3_LVGL9
- Copy the complete
RotaryScreen_1_46_Code_Core3_LVGL9project to a working directory without special permission restrictions. Confirm that the.inoand.hfiles remain in the same project directory. Place the files inlibraries/in the Arduino IDE library directory.
-
Start Arduino IDE 2.3.6, select File > Open, and open
RotaryScreen_1_46_Code_Core3_LVGL9.ino. After opening it, both the main program and the header file in the same directory should be visible.
-
Open Boards Manager and install or confirm that
esp32 by Espressif Systemsis version 3.3.8. If other versions are also installed on the computer, use the version validated for this project. -
Check the dependency versions: LVGL 9.1.0, LovyanGFX 1.2.7, Adafruit NeoPixel 1.15.1, and CST816T 1.5.1. If the project already includes the packaged dependencies, do not load other versions with the same names, as Arduino IDE may select the wrong libraries.
- Set the following parameters in the Tools menu: CPU Frequency to
240 MHz (WiFi), Flash Mode toQIO 80 MHz, Flash Size to16 MB (128 Mb), Partition Scheme toHuge APP (3 MB No OTA / 1 MB SPIFFS), PSRAM toOPI PSRAM, USB CDC On Boot toDisabled, Upload Mode toUART0 / Hardware CDC, and Upload Speed to921600.
- Connect the rotary display using the factory-supplied USB data cable, and select the newly detected serial port under Tools > Port. Click Verify. After compilation is complete, the console should not show errors indicating that
lvgl.h,LovyanGFX.h,cst816t.h, orui.his missing.
- Click Upload. If the development board does not automatically enter download mode, follow the board-specific download procedure to enter Boot mode and try again. After the upload is complete, open Serial Monitor, set the baud rate to
115200, and then reset the development board.
5. Hardware Procedure¶
- With the development board powered off, inspect the USB port, screen, knob, and surrounding RGB LEDs for damage. Confirm that no metallic foreign objects could cause a short circuit.
- Use a USB cable that supports data transfer to connect the rotary display to the computer. After powering it on, observe the power indicator and screen area. Do not repeatedly connect and disconnect the USB cable immediately. Perform basic operations and verify whether they meet the requirements.
6. Key Code Explanations¶
6.1 LCD Parameters and SPI Bus¶
const uint32_t screenWidth = 360;
const uint32_t screenHeight = 360;
cfg.freq_write = 80000000;
cfg.pin_sclk = 10;
cfg.pin_mosi = 11;
cfg.pin_dc = 3;
cfg.pin_cs = 9;
cfg.pin_rst = 14;
These parameters take effect when the global LGFX object is constructed and determine the LVGL display area, as well as the SPI clock and pins used by the ST77961. During normal operation, the UI should be displayed completely at 360 × 360. An incorrect resolution may cause misalignment or cropping, while incorrect SPI pins or chip-select settings usually result in a white, black, or corrupted screen. The current project uses an 80 MHz write frequency, which should not be increased further without verifying signal integrity. When troubleshooting display issues, first verify the development board model, power GPIO1/GPIO2, reset GPIO14, backlight GPIO46, and this SPI configuration.
6.2 LVGL Double Buffering and SPI DMA Screen Updates¶
size_t buffer_size = screenWidth * screenHeight * 2;
buf = (uint8_t *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
buf1 = (uint8_t *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
display = lv_display_create(screenWidth, screenHeight);
lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565);
lv_display_set_flush_cb(display, my_disp_flush);
lv_display_set_buffers(display, buf, buf1, buffer_size, LV_DISPLAY_RENDER_MODE_PARTIAL);
This code runs in setup() after the basic LCD and LVGL initialization is complete. RGB565 uses 2 bytes per pixel, so each 360 × 360 buffer requires 259200 bytes. The two buffers are allocated in PSRAM to avoid exhausting internal RAM. my_disp_flush() passes the rectangular area generated by LVGL to LovyanGFX DMA and calls lv_display_flush_ready() after submission. If OPI PSRAM is not enabled, the serial port may report a buffer-allocation failure. If the callback is not registered, LVGL may run, but the screen will not refresh correctly. If an issue occurs, first check the PSRAM settings, LVGL color depth, and lv_conf.h.
6.3 CST816T Touch Input¶
wi->setPins(6,7);
wi->begin();
touch.begin(mode_touch);
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, my_touchpad_read);
In setup(), the touch controller first starts I2C through GPIO6/GPIO7 and is then registered as an LVGL pointer input device. Afterward, lv_timer_handler() periodically calls my_touchpad_read(): when a valid sample is available, it reports the pressed state and coordinates; when no sample is available, it reports the released state. The code discards (0,0) to prevent invalid readings from overwriting LVGL's last valid position. If the UI is displayed but does not respond to touch at all, first check the CST816T SDA, SCL, INT GPIO13, RST GPIO5, and whether the callback is registered. Coordinate logging is disabled by default, so do not mistake the absence of coordinates in the serial output for a touch failure.
6.4 UI Creation and Startup Animation¶
ui_init();
lv_arc_set_range(ui_VolumeArc, 0, 100);
lv_arc_set_range(ui_BulbArc, 0, 100);
lv_arc_set_range(ui_LightArc, 0, 100);
lv_arc_set_value(ui_VolumeArc, 50);
lv_arc_set_value(ui_BulbArc, 50);
lv_arc_set_value(ui_LightArc, 50);
ui_init() creates the startup page, main menu, and three detail pages, and initially loads ui_Screen0. After the startup page loads, it triggers a 1000 ms rotation animation. Once all repetitions are complete, it switches to ui_Screen1. The main program then sets all three arcs to a range of 0–100 and sets their initial values to 50. If any image or screen file in libraries/UI is missing, linking may fail, objects may be null, or UI assets may be missing. If ui_init() is not called, the LCD driver and backlight may work correctly, but the UI for this lesson will not appear.
6.5 Percentages and PWM Output¶
int value = lv_arc_get_value(arc);
int pwm_value = (value * 255) / 100;
ledcWrite(BULB_LED_PIN, pwm_value);
// The Light page uses the same mapping for the LCD backlight.
ledcWrite(SCREEN_BACKLIGHT_PIN, pwm_value);
These two event callbacks run when the corresponding arc generates LV_EVENT_VALUE_CHANGED. The UI range of 0–100 is linearly converted to the 8-bit PWM range of 0–255: Bulb controls GPIO43, and Light controls GPIO46. During normal operation, the percentage label and brightness change together. The Volume callback only updates the label and does not control audio hardware, so this lesson does not produce changes in volume or sound. You can safely adjust the arc from 50 to 25 and then 75 to observe differences in brightness. Do not change the GPIO assignments or connect high-power loads for testing.
6.6 Cross-Core Encoder Event Queue¶
encoderEventQueue = xQueueCreate(32, sizeof(uint8_t));
xTaskCreatePinnedToCore(encTask, "ENC", 2048, NULL, 1, &encTaskHandle, 0);
while (processed < 8 &&
xQueueReceive(encoderEventQueue, &eventType, 0) == pdTRUE) {
handleEncoderEvent(eventType);
processed++;
}
The encoder task is pinned to Core 0 to poll the hardware and sends only increment, decrement, single-click, and double-click events to a queue with a length of 32. The Arduino loop() then retrieves the events and calls LVGL. This prevents multiple tasks from modifying LVGL objects simultaneously. A maximum of eight events is processed during each loop iteration so that animations and touch processing still receive execution time during rapid rotation or button bounce. If queue creation fails, the serial port outputs Failed to create encoder event queue!, and knob input will not reach the UI; touch input may still work normally.
6.7 LVGL Main Loop and Background Lighting Effects¶
loop() processes LVGL timers, animations, touch input, and redraws approximately every 5 ms. NeoPixel animations run independently in ledTestTask on Core 0. This division of work prevents the 50–250 ms delays in the lighting effects from directly blocking the UI. If lv_timer_handler() is removed, the startup animation, touch input, and UI refresh will all stop. If the loop delay is increased significantly, interaction will become sluggish. The recommended value is 5 ms. Reducing it to 0 in an attempt to improve speed is not recommended because it may fully occupy the CPU.
6.8 UI Switching¶
Here, we have added this code to the ui.c file in the code's UI folder. This allows us to switch to Screen1 after Screen0 has run for a period of time, making it easier to use the system functions.
To resolve the issue of the arc not being displayed, modify the code in ui_Screen2.c. Make the same modification in ui_Screen3.c and ui_Screen4.c.
7. UI Asset Integration Process¶
This project provides UI C files already exported from SquareLine Studio 1.5.3, but it does not include the original editable SquareLine project. Therefore, this section only explains how the existing assets are integrated and does not fabricate the original control-design process. If the original project becomes available later, complete creation screenshots should be added based on a resolution of 360 × 360, LVGL 9.1.0, and a 16-bit color depth.
UI project repository: https://github.com/Elecrow-RD/CrowPanel-1.46inch-HMI-ESP32-Rotary-Display/tree/master/example/V1.0/Arduino/1_46_SquareLine_Studio_Project
SquareLine Studio operating guide: https://www.elecrow.com/wiki/Get_Started_with_SquareLine_Studio.html
8. Expected Results¶
8.1 Power-On and Startup¶
- After USB power is applied, GPIO 1 and GPIO 2 are driven high to enable screen power, while GPIO 40 is driven low to illuminate the active-low power indicator.
- After the LCD is reset, it is first filled with black, and the backlight is enabled at approximately a 50% duty cycle.
- The Screen0 startup page appears, and the center icon performs multiple rotation animations. When they are complete, the UI transitions to the main menu with a fade-in effect.
- If the serial port outputs
Failed to allocate for LVGL buf!orFailed to allocate for LVGL buf1!, PSRAM buffer allocation has failed, and the experiment cannot be considered successful.
8.2 Main Menu and Knob¶
- The main menu displays three options:
Volume,Bulb, andLight. The middle option,Bulb, is selected initially. - When the knob is rotated, the blue selection moves among the three options and does not move beyond the first or last item.
- The serial port outputs content similar to the following. The specific direction text depends on the physical installation orientation of the knob:
- After the knob is single-clicked, the program waits for the double-click detection window to expire before opening the currently selected detail page. Double-clicking returns from any detail page to the main menu.
8.3 Three Detail Pages¶
Volume: The initial value is 50%. The knob or touch input can adjust it from 0% to 100%. It only changes the UI and does not produce sound.Bulb: The initial displayed value is 50%. When the value changes, the GPIO 43 PWM output changes synchronously. When the program initializes GPIO 43, the raw PWM value written is 50. It is not converted from the percentage to the 0–255 range until the Arc callback is triggered for the first time.Light: The initial displayed value is 50%. When the value changes, the GPIO 46 backlight PWM output changes synchronously. At 0%, the screen may appear completely black, but the program may still be running.- On all three pages, touch
Returnto go back. You cannot switch directly among the Volume, Bulb, and Light pages; you must return to the main menu first.
8.4 RGB LEDs and Stability¶
The eight NeoPixels should cycle through a white running-light effect, rapid multicolor flashing, a multicolor running-light effect, slow multicolor flashing, and a breathing effect. While the display animation, knob input, and LED animations are running simultaneously, the device should not reset repeatedly, remain stuck on one page for an extended period, or stop responding. The current code does not print touch coordinates by default; this is normal.
9. Common Issues and Troubleshooting¶
9.1 lvgl.h Is Not Found During Compilation or LVGL API Errors Occur¶
- Confirm that LVGL 9.1.0 is being used, rather than LVGL 8.x or another 9.x version.
- Confirm that Arduino IDE is not prioritizing another library with the same name in the sketchbook.
- Keep the complete
libraries/UI/directory; do not copy only the.inofile.
9.2 Compilation Reports an Incorrect Color Depth¶
The UI code requires LV_COLOR_DEPTH to be 16. Check the active lv_conf.h. Do not place the configuration file in a location Arduino cannot read, and do not retain multiple conflicting lv_conf.h files.
9.3 Upload Fails or the Port Cannot Be Found¶
- Replace the USB cable with one confirmed to support data transfer.
- Reselect
ESP32S3 Dev Moduleand the correct serial port. - If necessary, follow the board's Boot/Reset procedure to enter download mode.
- If 921600 is unstable, temporarily reduce the upload speed. This affects only the upload time and does not change program functionality.
9.4 The Screen Is Completely Black¶
Troubleshoot in the following order:
- Confirm that GPIO 1 and 2 have enabled screen power.
- Confirm that the GPIO 46 backlight has not been set to 0% from the Light page.
- Confirm the GPIO 14 reset timing and the ST77961 SPI pins.
- Check the serial output for PSRAM buffer-allocation failure messages.
- Confirm that both
ui_init()andlv_timer_handler()are executed.
9.5 The UI Is Visible but Touch Does Not Respond¶
- Check whether the CST816T uses the I2C bus on GPIO 6 and 7 and successfully calls
touch.begin(mode_touch). - Confirm that the LVGL input device has registered
my_touchpad_read(). - Do not touch the screen with sharp objects.
- To troubleshoot coordinates, temporarily enable
DEBUG_PRINTin the code and recompile. Restore the original setting after verification.
9.6 Knob Direction Is Reversed, Input Jumps, or Button Presses Are Triggered Incorrectly¶
- The direction text depends on the physical installation orientation. As long as menu selection and value changes are stable, operation is normal.
- Check the connections for GPIO 45, 42, and 41.
- A single-click must wait approximately 300 ms for the double-click detection window to expire. This behavior is intentional.
- Do not remove the debounce logic directly, or mechanical bouncing may be recognized as multiple clicks.













