The CrowPanel 2.1inch-HMI ESP32 Rotary Display by Elecrow is a large, round module that integrates an ESP32-S3 microcontroller, a 2.1-inch 480×480 IPS circular touchscreen, and a rotary encoder with a push button in a single unit.
In this tutorial, we’ll go through the essential steps to get started with this display; from the initial setup to testing its display and input capabilities. You’ll learn how to read rotary encoder data for smooth user input and capture touch events from the circular display.
Required Parts
You will only need the CrowPanel 2.1inch-HMI ESP32 Rotary Display in this project. The display module comes with a USB cable and GPIO connector, so no further hardware is needed.

CrowPanel 2.1inch-HMI ESP32 Rotary Display
Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.
Hardware of the CrowPanel 2.1inch-HMI ESP32 Rotary Display
The CrowPanel 2.1inch-HMI ESP32 Rotary Display is a display module that combines a high-resolution round IPS display, a capacitive touchscreen and a rotary encoder with push-button functionality.
The module is built around the ESP32‑S3R8 system-on-chip which utilises a 32-bit Xtensa LX7 dual-core processor capable of clock speeds up to 240 MHz. It is equipped with 8 Mbit PSRAM along with 16 Mbyte of onboard flash storage.
The display portion is a circular 2.1-inch RGB IPS panel (ST7701 controller) with a resolution of 480 × 480 pixels, with a capacitive touch overlay supporting full-screen touch operation and a backlight.
In terms of physical operation, the hardware incorporates a rotary encoder “knob” mechanism. The knob is capable of clockwise and counter-clockwise rotation as well as an inward press (acting like a switch) for user input.
For connectivity and expansion, the module supports both wireless and wired interfaces. On the wireless side it features IEEE 802.11 a/b/g/n (2.4 GHz) WiFi and Bluetooth Low Energy (BLE) / Bluetooth 5.0.
On the wired side the module exposes a 5 V charging/interface socket that doubles as the power supply and the programming port. Additionally, the board provides three expansion interfaces: one UART interface, one I2C interface, and one FPC connector (12-pin) for further connectivity.
Pin Definitions
The following table list the pins required for controlling all components of the display module:
| Function Group | Signal / Purpose | Pin Number | Notes |
|---|---|---|---|
| I²C Interface | SDA | 38 | Primary I²C data line |
| SCL | 39 | Primary I²C clock line | |
| Rotary Encoder | Encoder A | 42 | Quadrature channel A |
| Encoder B | 4 | Quadrature channel B | |
| Display Backlight | Backlight Control | 6 | PWM-capable pin used for LCD backlight brightness |
| OLED Control | RESET | –1 | No reset pin used (software reset) |
| RGB Display Interface | CS | 16 | Chip select line |
| SCK | 2 | Display serial clock | |
| SDA | 1 | Display serial data | |
| DE | 40 | Data enable | |
| VSYNC | 7 | Vertical sync | |
| HSYNC | 15 | Horizontal sync | |
| PCLK | 41 | Pixel clock | |
| RGB Color Data Pins (5-6-5 format) | R0 | 46 | Red bit 0 |
| R1 | 3 | Red bit 1 | |
| R2 | 8 | Red bit 2 | |
| R3 | 18 | Red bit 3 | |
| R4 | 17 | Red bit 4 | |
| G0 | 14 | Green bit 0 | |
| G1 | 13 | Green bit 1 | |
| G2 | 12 | Green bit 2 | |
| G3 | 11 | Green bit 3 | |
| G4 | 10 | Green bit 4 | |
| G5 | 9 | Green bit 5 | |
| B0 | 5 | Blue bit 0 | |
| B1 | 45 | Blue bit 1 | |
| B2 | 48 | Blue bit 2 | |
| B3 | 47 | Blue bit 3 | |
| B4 | 21 | Blue bit 4 | |
| PCF8574 I/O Expander | I²C Address | 0x21 | Connected to pins 38 (SDA) and 39 (SCL) |
Technical Specification
The following table summarizes the technical specification of the CrowPanel 2.1inch-HMI ESP32 Rotary Display module:
| Specification Category | Details |
|---|---|
| Main processor | SoC: Xtensa LX7 dual-core (32-bit) in the ESP32‑S3R8 (up to 240 MHz) |
| Memory & storage | System RAM: 512 kB SRAM; PSRAM: 8 MB; Flash: 16 MB |
| Display panel | Size: 2.1 inch; Type: IPS; Resolution: 480 × 480 pixels; Touch: capacitive full-screen touch overlay |
| User input mechanisms | Rotary “knob” (clockwise / counter-clockwise rotation + full press); Capacitive touch input on the display surface |
| Wireless connectivity | WiFi: IEEE 802.11 a/b/g/n (2.4 GHz); Bluetooth: BLE / Bluetooth 5.0 |
| Interface ports / expansion | 5 V input (charging and programming); UART interface: 1× UART0 + 1× UART1 via ZX-MX 1.25-4P; I²C interface; 12-pin FPC connector (power/programming and GPIO) |
| Buttons & indicators | On-board RESET button; BOOT button; Knob-press switch (confirmation button); Power indicator LED; LCD backlight |
| Power & voltage | Module input: 5 V / 1 A; Main chip logic level: 3.3 V; Operating module at 5 V nominal |
| Mechanical / physical | Dimensions: 79 × 79 × 30 mm; Net weight: 80 g; Shell: aluminium alloy + plastic + acrylic |
| Temperature range | Operating: –20 °C to +65 °C; Storage: –40 °C to +80 °C |
| Software / development environment support | Compatible with Arduino IDE, ESP-IDF, Lua RTOS, MicroPython, PlatformIO; UI graphics library: LVGL supported |
Installation of ESP32 Core
If this is your first project with a board of the ESP32 series, you will need to install the ESP32 core first. However, for the CrowPanel 2.1inch Display that you will need to install a specific version (2.0.14) of the ESP32 core.
Start by opening the Preferences dialog in the Arduino IDE by selecting “Preferences…” from the “File” menu. This will open the Preferences dialog shown below.
Under the Settings tab you will find an edit box at the bottom of the dialog that is labelled “Additional boards manager URLs“:

In this input field copy the following URL:
https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json
This will let the Arduino IDE know, where to find the ESP32 core libraries. Next we will install the ESP32 boards using the Boards Manager.
Open the Boards Manager via “Tools -> Boards -> Board Manager”. You will see the Boards Manager appearing in the left Sidebar. Enter “ESP32” in the search field at the top and you should see two types of ESP32 boards; the “Arduino ESP32 Boards” and the “esp32 by Espressif” boards. We want the “esp32 libraries by Espressif”.
In the dropdown menu select Version “2.0.14” and then press the “INSTALL” button. The screenshot below shows what you should see in the BOARDS MANAGER after a successful installation of the ESP32 Core 2.0.14:

Note that you can install versions up to 2.0.17 but not 3.x. The libraries, which we will install in the later section will not work with the 3.x ESP32 core.
Selecting Board
Finally we need to select a ESP32 board. In case of the CrowPanel 2.1inch-HMI ESP32 Rotary Display, we pick the generic “ESP32S3 Dev Module”. For that, click on the drop-down menu and then on “Select other board and port…”:

This will open a dialog where you can enter “esp32s3 dev” in the search bar. You will see the “ESP32S3 Dev Module” board under Boards. Click on it and the COM port to activate it and then click OK:

Note that you need to connect the board via the USB cable to your computer, before you can select a COM port. Connect the USB cable that comes with CrowPanel Display Module directly to the port labelled “USB-5V-IN” as shown below:

Tool Settings
Below are the settings you should use with the CrowPanel Display Module. You find them under the Tools menu in your Arduino IDE.

Most importantly for the code examples in the following sections is to set USB CDC on Boot to “Enabled”. This allows you to send or receive data via the serial port, which you will need for debugging. Also, you will need “OPI PSRAM”, “Huge APP” and “Flash Size” set correctly set for the display code examples.
Installing Libraries
Next we need to install specific libraries and library versions to make the code for the CrowPanel 2.1inch-HMI ESP32 Rotary Display to work.
Go to the Elecrow github repo for the CrowPanel 2.1inch-HMI Display. Click on the green “<> Code” button and then “Download ZIP” to download the repo as a ZIP file:

Next unpack the ZIP file to extract it contents. You should see the following files in an unpacked folder named “CrowPanel-2.1inch-HMI-ESP32-Rotary-Display-480-480-IPS-Round-Touch-Knob-Screen-master”:

Ignore the files related to the “CrowPanel 1.28inch …” display. We only need to copy the contents of the “example/libraries” folder into the “libraries” folder for the Arduino IDE. Under Windows the “libraries” folder is typically located under:
C:\Users\<username>\OneDrive\Documents\Arduino\libraries
Since this folder already contains installed libraries, I recommend you temporarily rename it, e.g. to “_libraries” and create a new folder named “libraries”. This way you avoid conflicts with your already installed libraries and you don’t loose them either. Later you can easily revert this change. The picture below shows how your “Arduino” folder with the libraries should look like:

Next we copy the files in the “example/libraries” folder into the new “libraries” folder as shown below:

You don’t need to copy the libraries that are crossed out, since they are related to the LVGL UI, which we will not use. In the next sections, I will show you some code examples to try out the display.
Code Example: Serial Interface
We start by testing the Serial communication. Open your Arduino IDE, enter the following code and upload the code to the CrowPanel Display Module.
void setup() {
Serial.begin(115200);
}
void loop() {
Serial.println("Makerguides");
delay(2000);
}
Then open the Serial Monitor and you should see the text “Makerguides” printed every two seconds. If not, make sure that your Tools settings and Baud rate (115200) settings are correct. Specifically, you need “USB CDC on Boot” set to “Enabled”.
Code Example: Encoder
In this next code example you will learn how to use the encoder and the encoder switch. We will use an interrupt-driven approach to read a quadrature rotary encoder and the PCF8574 to read the encoder’s push button status.
The encoder rotation is processed in a fast ISR with direction detection, while the main loop focuses on reporting changes and reading the button state. Have a quick look at the code first and then we discuss its details.
#include <Wire.h>
#include <PCF8574.h>
// I2C to PCF8574
#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39
#define ENCODER_CLK 42
#define ENCODER_DT 4
volatile int counter = 0;
volatile int encState = 0;
volatile int oldState = -1;
volatile bool hasChanged = true;
PCF8574 pcf8574(0x21);
void IRAM_ATTR encoder_irq() {
encState = digitalRead(ENCODER_CLK);
if (encState != oldState) {
counter += (digitalRead(ENCODER_DT) == encState) ? +1 : -1;
oldState = encState;
hasChanged = true;
}
}
void initPins() {
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
pcf8574.pinMode(P5, INPUT_PULLUP); //encoder SW
if (!pcf8574.begin()) {
Serial.println("Can't init pcf8574");
}
}
void initEncoder() {
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
}
void setup() {
Serial.begin(115200);
initPins();
initEncoder();
}
void loop() {
if (hasChanged) {
hasChanged = false;
Serial.printf("COUNTER: %d\n", counter);
}
int button = pcf8574.digitalRead(P5, true);
if (!button) {
Serial.printf("BTN pressed\n");
delay(500);
}
delay(1);
}
Imports
The sketch starts by including two libraries that handle I2C communication and the PCF8574 I/O expander. These headers must be included before you can use Wire or PCF8574 objects.
#include <Wire.h> #include <PCF8574.h>
Wire.h provides the standard Arduino I2C (TWI) interface. PCF8574.h wraps the low-level I2C access to the PCF8574 chip so that you can treat its pins almost like normal digital pins.
Constants
Next, the code defines the pins used for I2C and for the rotary encoder:
#define I2C_SDA_PIN 38 #define I2C_SCL_PIN 39 #define ENCODER_CLK 42 #define ENCODER_DT 4
I2C_SDA_PIN and I2C_SCL_PIN select which ESP32 pins are used as SDA and SCL for the hardware I2C bus. On the ESP32 you can route I2C to various pins, so specifying them here is necessary.
ENCODER_CLK and ENCODER_DT are the two quadrature outputs of the rotary encoder. The CLK signal is typically the main channel used for the interrupt, and DT is the second channel used to determine the rotation direction.
Global State and Volatile Variables
The code uses several global variables to track the encoder state. These variables are marked volatile because they are modified inside an interrupt service routine.
volatile int counter = 0; volatile int encState = 0; volatile int oldState = -1; volatile bool hasChanged = true;
counter holds the accumulated position of the encoder. Each step of the encoder increments or decrements this value.
encState represents the current logic level of the encoder CLK pin as seen in the interrupt handler. oldState keeps the previous CLK state so that the code can detect transitions and avoid counting the same level multiple times.
hasChanged is used as a flag to signal the main loop that the counter has been updated in the interrupt. Marking these variables as volatile tells the compiler that they may change at any time and must not be cached in registers, which is critical when they are modified by an ISR.
PCF8574 Object
The code then creates a global instance of the PCF8574 class, specifying the I2C address of the chip.
PCF8574 pcf8574(0x21);
This line tells the PCF8574 library that the I/O expander is accessible at the I2C address 0x21. All subsequent calls to pcf8574.pinMode or pcf8574.digitalRead will use this address to communicate with that specific device on the I2C bus.
The Encoder Interrupt Service Routine
The most timing-sensitive part of the code is the interrupt service routine (ISR) that handles encoder changes. It is placed in IRAM using the IRAM_ATTR attribute so that it can execute fast and reliably on the ESP32.
void IRAM_ATTR encoder_irq() {
encState = digitalRead(ENCODER_CLK);
if (encState != oldState) {
counter += (digitalRead(ENCODER_DT) == encState) ? +1 : -1;
oldState = encState;
hasChanged = true;
}
}
IRAM_ATTR ensures that the function is stored in instruction RAM instead of flash, which avoids delays caused by flash access and is recommended for ISRs on ESP32.
Inside the ISR, the current level of the encoder CLK signal is read using digitalRead(ENCODER_CLK) and stored in encState. The code then checks if this state is different from oldState. This condition filters out redundant calls because the interrupt is set to trigger on any change (rising or falling edge) and the handler might run multiple times with the same logical level.
If the state has changed, the direction of rotation is computed by comparing the DT signal with the CLK signal. The following line reads the DT pin and checks whether its level matches the current CLK level:
counter += (digitalRead(ENCODER_DT) == encState) ? +1 : -1;
For a quadrature encoder, this relation indicates the rotation direction. If DT equals CLK, the encoder is moving in one direction and counter is incremented; if they differ, counter is decremented.
After updating the counter, oldState is updated to the new CLK state and hasChanged is set to true. This flag informs the main loop that there is new encoder data to process or display. The ISR itself remains short and efficient, which is good practice for interrupt routines.
Pin and I2C Initialization
The initPins function configures the I2C bus and the PCF8574 pin used for the encoder push button.
void initPins() {
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
pcf8574.pinMode(P5, INPUT_PULLUP); //encoder SW
if (!pcf8574.begin()) {
Serial.println("Can't init pcf8574");
}
}
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); initializes the I2C peripheral with custom SDA and SCL pins.
pcf8574.pinMode(P5, INPUT_PULLUP); configures pin P5 on the PCF8574 as an input with an internal pull-up resistor. This pin is connected to the encoder’s push button (switch). A pull-up configuration means the pin will read high when the button is not pressed and low when it is pressed, assuming the button connects the pin to ground.
pcf8574.begin() starts communication with the PCF8574 device at the configured I2C address. If it fails, the code prints an error message via Serial.println, which helps diagnose wiring or address configuration issues.
Encoder Initialization
The initEncoder function sets up the two encoder signals and attaches an interrupt to the CLK pin.
void initEncoder() {
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
}
pinMode(ENCODER_CLK, INPUT_PULLUP); and pinMode(ENCODER_DT, INPUT_PULLUP); configure both encoder channels as inputs with internal pull-up resistors. This is a typical configuration for mechanical rotary encoders, which usually connect the pins to ground in a gray-code pattern as the knob rotates.
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE); attaches the interrupt handler encoder_irq to the encoder CLK pin. The CHANGE mode means the ISR will be triggered on both rising and falling edges of the signal. This gives higher resolution and more accurate counting, since the encoder is sampled at every transition.
Setup
The setup function is executed once at startup and provides a initialization sequence.
void setup() {
Serial.begin(115200);
initPins();
initEncoder();
}
Serial.begin(115200); starts the serial port at 115200 baud, which is useful for debugging and monitoring. It must be called before any Serial.print or Serial.printf calls.
initPins(); initializes the I2C bus and the PCF8574 I/O expander. This ensures that the encoder button can be read and that the I2C hardware is ready.
initEncoder(); configures the GPIO pins used by the rotary encoder and sets up the interrupt so that rotation events are captured as soon as the main loop starts.
Loop
The loop function runs repeatedly and handles two main tasks: printing the encoder value when it changes and reading the encoder push button state.
void loop() {
if (hasChanged) {
hasChanged = false;
Serial.printf("COUNTER: %d\n", counter);
}
int button = pcf8574.digitalRead(P5, true);
if (!button) {
Serial.printf("BTN pressed\n");
delay(500);
}
delay(1);
}
The first block checks whether the encoder counter has been updated by the ISR. The hasChanged flag is set to true inside the interrupt when the encoder position changes.
if (hasChanged) {
hasChanged = false;
Serial.printf("COUNTER: %d\n", counter);
}
If hasChanged is true, the flag is immediately reset to false and the current value of counter is printed to the serial monitor using Serial.printf. This avoids printing in the ISR and decouples the time-critical interrupt logic from slower serial output.
Next, the code reads the state of the encoder push button, which is connected to the PCF8574.
int button = pcf8574.digitalRead(P5, true);
if (!button) {
Serial.printf("BTN pressed\n");
delay(500);
}
pcf8574.digitalRead(P5, true); reads the current logic level of pin P5. Passing true as the second argument typically tells the library to perform an immediate I2C read rather than using a cached value, which ensures a fresh reading each time.
Because the pin is configured with a pull-up, an unpressed button reads as high (non-zero), and a pressed button pulls the line to ground and reads as low (zero). Therefore, the condition if (!button) checks for the pressed state. When the button is detected as pressed, the code prints "BTN pressed" and waits for 500 milliseconds. This delay acts as a simple debounce and prevents the message from being printed continuously while the button is held down.
Finally, the loop ends with a short delay.
delay(1);
This small delay gives the scheduler a chance to handle background tasks and helps avoid a very tight, CPU-intensive loop. It also ensures that the I2C and serial subsystems have a little breathing room.
Output on Serial Monitor
The following screenshot shows what you should see on the Serial Monitor when you turn the encoder ring or press the encoder button:

Code Example: Display and Touchscreen
The following sketch shows you how to use the display and the touchpad. It sets the screen background to red, prints the text “Makerguides” in the center and draws black circles where ever the screen is touched:

Have a quick look at the complete code below before we discuss the details:
#include <Wire.h>
#include <Arduino_GFX_Library.h>
#include <PCF8574.h>
#include <Adafruit_CST8XX.h>
// I2C to PCF8574
#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39
#define BKL_PIN 6
#define I2C_TOUCH_ADDR 0x15
PCF8574 pcf8574(0x21);
Adafruit_CST8XX tsPanel = Adafruit_CST8XX();
Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel(
16 /* CS */, 2 /* SCK */, 1 /* SDA */,
40 /* DE */, 7 /* VSYNC */, 15 /* HSYNC */, 41 /* PCLK */,
46 /* R0 */, 3 /* R1 */, 8 /* R2 */, 18 /* R3 */, 17 /* R4 */,
14 /* G0 */, 13 /* G1 */, 12 /* G2 */, 11 /* G3 */, 10 /* G4 */, 9 /* G5 */,
5 /* B0 */, 45 /* B1 */, 48 /* B2 */, 47 /* B3 */, 21 /* B4 */
);
Arduino_ST7701_RGBPanel *gfx = new Arduino_ST7701_RGBPanel(
bus,
GFX_NOT_DEFINED, // RST pin (not used, we reset via PCF8574)
0, // rotation
false, // IPS
480, 480, // width, height
st7701_type5_init_operations,
sizeof(st7701_type5_init_operations),
true, // BGR
10, 4, 20, // hsync front porch, pulse width, back porch
10, 4, 20 // vsync front porch, pulse width, back porch
);
void initBacklight() {
pinMode(BKL_PIN, OUTPUT);
analogWrite(BKL_PIN, 200);
}
void initPins() {
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
pcf8574.pinMode(P0, OUTPUT); //tp RST
pcf8574.pinMode(P2, OUTPUT); //tp INT
pcf8574.pinMode(P3, OUTPUT); //lcd power
pcf8574.pinMode(P4, OUTPUT); //lcd reset
if (!pcf8574.begin()) {
Serial.println("Can't init pcf8574");
}
// LCD
pcf8574.digitalWrite(P3, HIGH);
delay(100);
pcf8574.digitalWrite(P4, HIGH);
delay(100);
pcf8574.digitalWrite(P4, LOW);
delay(120);
pcf8574.digitalWrite(P4, HIGH);
delay(120);
// Touchpad
pcf8574.digitalWrite(P0, HIGH);
delay(100);
pcf8574.digitalWrite(P0, LOW);
delay(120);
pcf8574.digitalWrite(P0, HIGH);
delay(120);
pcf8574.digitalWrite(P2, HIGH);
delay(120);
if (!tsPanel.begin(&Wire, I2C_TOUCH_ADDR)) {
Serial.println("No touchscreen found");
}
}
void initLCD() {
gfx->begin();
gfx->fillScreen(RED);
gfx->setTextSize(5);
gfx->setTextColor(WHITE);
}
void drawOnLCD() {
gfx->setCursor(90, 210);
gfx->print("Makerguides");
}
void setup() {
Serial.begin(115200);
initPins();
initBacklight();
initLCD();
drawOnLCD();
}
void loop() {
if (tsPanel.touched()) {
CST_TS_Point p = tsPanel.getPoint(0);
Serial.printf("TOUCH: %d, %d\n", p.x, p.y);
gfx->fillCircle(p.x, p.y, 10, BLACK);
delay(10);
}
}
Imports
This sketch begins by including the required libraries for I2C, graphics, the I/O expander, and the touch controller.
#include <Wire.h> #include <Arduino_GFX_Library.h> #include <PCF8574.h> #include <Adafruit_CST8XX.h>
Wire.h is the same I2C library you used before with the PCF8574. Arduino_GFX_Library.h provides the graphics abstraction for driving the RGB panel and the ST7701 display driver. PCF8574.h again handles the I/O expander on the I2C bus. Adafruit_CST8XX.h is the driver for the CST8xx capacitive touch controller, which is connected over I2C and will be used to read touch coordinates from the round display.
Constants and I2C Configuration
The code defines pin assignments for the I2C bus, the backlight control, and the I2C address for the touch controller. This follows the same pattern as in the previous example where SDA and SCL pins were explicitly configured.
#define I2C_SDA_PIN 38 #define I2C_SCL_PIN 39 #define BKL_PIN 6 #define I2C_TOUCH_ADDR 0x15
I2C_SDA_PIN and I2C_SCL_PIN select the ESP32 pins used for the shared I2C bus that connects both the PCF8574 and the CST8xx touch controller. BKL_PIN is a PWM-capable pin that drives the LCD backlight. I2C_TOUCH_ADDR specifies the 7-bit I2C address of the touch controller so that the driver can communicate with it.
Global Objects: PCF8574, Touch Controller, RGB Bus and Panel
The global objects set up access to the I/O expander, the touch controller and the display.
pcf8574(0x21) is the same as in the previous example: it creates an instance bound to I2C address 0x21. This chip controls several control lines such as LCD power, LCD reset and touch reset and interrupt.
tsPanel is an instance of the Adafruit CST8xx driver, created with the default constructor and later initialised with begin() using the shared Wire interface and the touch address.
PCF8574 pcf8574(0x21); Adafruit_CST8XX tsPanel = Adafruit_CST8XX();
RGB Panel
The next object configures the RGB data bus between the ESP32-S3 and the ST7701 display driver. It lists all the timing and color pins used for the parallel RGB interface.
Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel( 16 /* CS */, 2 /* SCK */, 1 /* SDA */, 40 /* DE */, 7 /* VSYNC */, 15 /* HSYNC */, 41 /* PCLK */, 46 /* R0 */, 3 /* R1 */, 8 /* R2 */, 18 /* R3 */, 17 /* R4 */, 14 /* G0 */, 13 /* G1 */, 12 /* G2 */, 11 /* G3 */, 10 /* G4 */, 9 /* G5 */, 5 /* B0 */, 45 /* B1 */, 48 /* B2 */, 47 /* B3 */, 21 /* B4 */ );
This Arduino_ESP32RGBPanel object defines the exact mapping between ESP32 GPIOs and the RGB panel signals. The first three pins are used as control lines for the panel bus (chip select, clock, and data for the configuration interface). DE, VSYNC, HSYNC, and PCLK are the timing signals, similar to those on a classic RGB display interface. The remaining groups map the five red, six green, and five blue bits of the 16-bit color bus (5-6-5 format) to specific GPIO pins.
The high-level graphics object gfx represents the ST7701 controlled LCD that is driven via this RGB bus.
Arduino_ST7701_RGBPanel *gfx = new Arduino_ST7701_RGBPanel( bus, GFX_NOT_DEFINED, // RST pin (not used, we reset via PCF8574) 0, // rotation false, // IPS 480, 480, // width, height st7701_type5_init_operations, sizeof(st7701_type5_init_operations), true, // BGR 10, 4, 20, // hsync front porch, pulse width, back porch 10, 4, 20 // vsync front porch, pulse width, back porch );
The first argument is the previously defined RGB bus. The reset pin is set to GFX_NOT_DEFINED because the panel is reset using PCF8574 pins instead of a direct ESP32 GPIO. The rotation parameter is zero, meaning the default orientation. The false IPS flag is a panel option, and 480, 480 define the display resolution.
The st7701_type5_init_operations pointer and size provide the low-level initialization sequence for the ST7701 driver, which the library will send to the panel at startup. The true BGR flag tells the driver to treat color data as BGR instead of RGB.
Finally, the horizontal and vertical porch and pulse values define the RGB timing, similar to parameters found in classic video timing: front porch, pulse width and back porch for both HSYNC and VSYNC.
Backlight Initialization
The backlight is controlled separately from the panel electronics. The initBacklight function configures the backlight pin and sets an initial brightness using PWM.
void initBacklight() {
pinMode(BKL_PIN, OUTPUT);
analogWrite(BKL_PIN, 200);
}
pinMode(BKL_PIN, OUTPUT); configures the backlight pin as an output. analogWrite(BKL_PIN, 200); enables PWM on that pin with a duty cycle corresponding to a brightness level of 200 on a typical 0–255 scale. This allows you to adjust backlight brightness later by writing a different value without changing the hardware.
Pin and Peripheral Initialization
The initPins function sets up the shared I2C bus, configures the PCF8574 pins, performs reset and power sequences for the LCD and touch controller, and initialises the touch driver. It plays a similar role to initPins in the previous example but with more peripherals involved.
void initPins() {
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
pcf8574.pinMode(P0, OUTPUT); //tp RST
pcf8574.pinMode(P2, OUTPUT); //tp INT
pcf8574.pinMode(P3, OUTPUT); //lcd power
pcf8574.pinMode(P4, OUTPUT); //lcd reset
if (!pcf8574.begin()) {
Serial.println("Can't init pcf8574");
}
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); is identical in concept to the earlier sketch: it starts the I2C peripheral with custom SDA and SCL pins. The four pcf8574.pinMode calls configure specific PCF8574 lines as outputs: P0 for touch reset, P2 for touch interrupt line , P3 for LCD power enable, and P4 for LCD reset. As before, pcf8574.begin() initializes communication with the expander and prints an error message if this fails.
The next block performs the power and reset sequence for the LCD. These precise delays and toggling patterns are often required by LCD controllers and are encoded according to the display’s datasheet.
// LCD pcf8574.digitalWrite(P3, HIGH); delay(100); pcf8574.digitalWrite(P4, HIGH); delay(100); pcf8574.digitalWrite(P4, LOW); delay(120); pcf8574.digitalWrite(P4, HIGH); delay(120);
Setting P3 high enables the LCD power rail. After a short delay to let the supply stabilise, P4 is toggled to generate a proper reset pulse for the panel. The sequence high, low, high with specific delays ensures the ST7701 controller starts in a known state.
The touchpad section performs a very similar reset sequence, but on the touch controller’s reset and interrupt lines.
// Touchpad pcf8574.digitalWrite(P0, HIGH); delay(100); pcf8574.digitalWrite(P0, LOW); delay(120); pcf8574.digitalWrite(P0, HIGH); delay(120); pcf8574.digitalWrite(P2, HIGH); delay(120);
Here P0 is toggled to reset the CST8xx controller, and P2 is driven high to configure the interrupt line in a defined state. These actions ensure the touch controller is ready before the driver attempts to communicate over I2C.
Finally, the touch controller is initialised using the Adafruit driver.
if (!tsPanel.begin(&Wire, I2C_TOUCH_ADDR)) {
Serial.println("No touchscreen found");
}
}
tsPanel.begin(&Wire, I2C_TOUCH_ADDR) tells the CST8xx object to use the global Wire instance and connects it to the previously defined I2C address. When this call fails, the sketch prints a diagnostic message.
LCD Initialization
The initLCD function prepares the graphics context and configures a basic drawing state.
void initLCD() {
gfx->begin();
gfx->fillScreen(RED);
gfx->setTextSize(5);
gfx->setTextColor(WHITE);
}
gfx->begin(); initialises the ST7701 panel over the RGB bus and runs the previously supplied init operations. After this call the display is ready to accept drawing commands.
gfx->fillScreen(RED); clears the entire screen with a red background. gfx->setTextSize(5); sets a relatively large text scaling factor so that text is easily readable on the 480×480 round display. gfx->setTextColor(WHITE); defines the foreground text color for subsequent text drawing operations.
drawOnLCD
The drawOnLCD function encapsulates a simple drawing action, placing a text label in the centre region of the screen.
void drawOnLCD() {
gfx->setCursor(90, 210);
gfx->print("Makerguides");
}
gfx->setCursor(90, 210); moves the text cursor to the position (90, 210) in pixel coordinates. With a 480×480 display, this is roughly central depending on font size. gfx->print("Makerguides"); then renders the text string with the previously configured text size and color onto the screen.
Setup
The setup function again provides an initialization sequence, similar to the previous sketch that set up serial, pins and encoder.
void setup() {
Serial.begin(115200);
initPins();
initBacklight();
initLCD();
drawOnLCD();
}
Serial.begin(115200); starts serial communication for debugging. initPins(); configures the I2C bus, PCF8574 pins, and performs the LCD and touch reset sequences as described above. initBacklight(); enables and sets the backlight brightness so the content on the display is visible. initLCD(); initialises the graphics driver and paints the red background, and drawOnLCD(); places the initial “Makerguides” string on the screen.
Loop
The main loop constantly checks whether the touch panel is being touched. When a touch is detected, it reads the coordinates and draws a small black circle at that position.
void loop() {
if (tsPanel.touched()) {
CST_TS_Point p = tsPanel.getPoint(0);
Serial.printf("TOUCH: %d, %d\n", p.x, p.y);
gfx->fillCircle(p.x, p.y, 10, BLACK);
delay(10);
}
}
tsPanel.touched(); queries the CST8xx driver to see if any touch points are currently active. If the function returns true, at least one finger is on the screen. tsPanel.getPoint(0); retrieves the first touch point as a CST_TS_Point structure containing x and y coordinates. These coordinates are printed to the serial monitor for debugging with Serial.printf, similar to how you printed the encoder counter and button state previously.
gfx->fillCircle(p.x, p.y, 10, BLACK); draws a filled circle with radius 10 pixels in black at the location of the touch. The delay(10); call provides a short pause to limit the update rate and avoid flooding the I2C bus and graphics driver with too many operations per second.
Code Example: Display, Touch and Encoder
This last sketch brings together the concepts from the previous examples: I2C configuration and PCF8574 control, RGB display and touch handling, and an interrupt-driven rotary encoder.
The code allows you to control the brightness of the display by turning the encoder ring and also display the current brightness value (0…255) in the center of the screen. The encoder button triggers a color change of the display to blue and touch events are still registered as black dots on the display.

Have a quick look at the complete code below and then we will discuss the its details.
#include <Wire.h>
#include <Arduino_GFX_Library.h>
#include <PCF8574.h>
#include <Adafruit_CST8XX.h>
// I2C to PCF8574
#define I2C_SDA_PIN 38
#define I2C_SCL_PIN 39
#define BKL_PIN 6
#define ENCODER_CLK 42
#define ENCODER_DT 4
#define I2C_TOUCH_ADDR 0x15
volatile int brightness = 100;
volatile int encState = 0;
volatile int oldState = -1;
volatile bool brightnessChanged = true;
PCF8574 pcf8574(0x21);
Adafruit_CST8XX tsPanel = Adafruit_CST8XX();
Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel(
16 /* CS */, 2 /* SCK */, 1 /* SDA */,
40 /* DE */, 7 /* VSYNC */, 15 /* HSYNC */, 41 /* PCLK */,
46 /* R0 */, 3 /* R1 */, 8 /* R2 */, 18 /* R3 */, 17 /* R4 */,
14 /* G0 */, 13 /* G1 */, 12 /* G2 */, 11 /* G3 */, 10 /* G4 */, 9 /* G5 */,
5 /* B0 */, 45 /* B1 */, 48 /* B2 */, 47 /* B3 */, 21 /* B4 */
);
Arduino_ST7701_RGBPanel *gfx = new Arduino_ST7701_RGBPanel(
bus,
GFX_NOT_DEFINED, // RST pin (not used, we reset via PCF8574)
0, // rotation
false, // IPS
480, 480, // width, height
st7701_type5_init_operations,
sizeof(st7701_type5_init_operations),
true, // BGR
10, 4, 20, // hsync front porch, pulse width, back porch
10, 4, 20 // vsync front porch, pulse width, back porch
);
void initBacklight() {
pinMode(BKL_PIN, OUTPUT);
analogWrite(BKL_PIN, brightness);
}
void initPins() {
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
pcf8574.pinMode(P0, OUTPUT); //tp RST
pcf8574.pinMode(P2, OUTPUT); //tp INT
pcf8574.pinMode(P3, OUTPUT); //lcd power
pcf8574.pinMode(P4, OUTPUT); //lcd reset
pcf8574.pinMode(P5, INPUT_PULLUP); //encoder SW
if (!pcf8574.begin()) {
Serial.println("Can't init pcf8574");
}
// LCD
pcf8574.digitalWrite(P3, HIGH);
delay(100);
pcf8574.digitalWrite(P4, HIGH);
delay(100);
pcf8574.digitalWrite(P4, LOW);
delay(120);
pcf8574.digitalWrite(P4, HIGH);
delay(120);
// Touchpad
pcf8574.digitalWrite(P0, HIGH);
delay(100);
pcf8574.digitalWrite(P0, LOW);
delay(120);
pcf8574.digitalWrite(P0, HIGH);
delay(120);
pcf8574.digitalWrite(P2, HIGH);
delay(120);
if (!tsPanel.begin(&Wire, I2C_TOUCH_ADDR)) {
Serial.println("No touchscreen found");
}
}
void IRAM_ATTR encoder_irq() {
encState = digitalRead(ENCODER_CLK);
if (encState != oldState) {
brightness += (digitalRead(ENCODER_DT) == encState) ? -5 : +5;
brightness = constrain(brightness, 5, 255);
oldState = encState;
brightnessChanged = true;
}
}
void initEncoder() {
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
}
void initLCD() {
gfx->begin();
gfx->fillScreen(RED);
gfx->setTextSize(10);
gfx->setTextColor(WHITE);
}
void updateBrightness() {
analogWrite(BKL_PIN, brightness);
gfx->fillScreen(RED);
gfx->setCursor(150, 200);
gfx->printf("%3d", brightness);
}
void setup() {
Serial.begin(115200);
initPins();
initEncoder();
initBacklight();
initLCD();
updateBrightness();
}
void loop() {
int button = pcf8574.digitalRead(P5, true);
if (!button) {
Serial.printf("BTN pressed\n");
gfx->fillScreen(BLUE);
}
if (tsPanel.touched()) {
CST_TS_Point p = tsPanel.getPoint(0);
Serial.printf("TOUCH: %d, %d\n", p.x, p.y);
gfx->fillCircle(p.x, p.y, 10, BLACK);
}
if (brightnessChanged) {
brightnessChanged = false;
updateBrightness();
}
delay(100);
}
Imports
This sketch combines everything from the previous examples: I2C peripherals, the RGB display, touch, and an interrupt-driven rotary encoder, and includes the necessary libraries:
#include <Wire.h> #include <Arduino_GFX_Library.h> #include <PCF8574.h> #include <Adafruit_CST8XX.h>
Constants and Global State
The next section defines the pins for I2C, backlight, encoder signals, and the touch controller address. It also introduces several volatile globals that hold the current brightness and encoder state, similar to the previous encoder counter example:
// I2C to PCF8574 #define I2C_SDA_PIN 38 #define I2C_SCL_PIN 39 #define BKL_PIN 6 #define ENCODER_CLK 42 #define ENCODER_DT 4 #define I2C_TOUCH_ADDR 0x15 volatile int brightness = 100; volatile int encState = 0; volatile int oldState = -1; volatile bool brightnessChanged = true;
I2C_SDA_PIN and I2C_SCL_PIN configure the shared I2C bus, as before. BKL_PIN is the PWM pin used to drive the LCD backlight. ENCODER_CLK and ENCODER_DT are the quadrature signals of the rotary encoder, identical in role to the previous encoder sketch. I2C_TOUCH_ADDR holds the address of the CST8xx touch controller.
brightness stores the current backlight brightness value. It is declared volatile because it is modified inside an interrupt service routine. encState and oldState are used to detect transitions on the encoder CLK line, and brightnessChanged is a flag informing the main loop that a new brightness level is available.
PCF8574, Touch and Display Objects
The global objects for the I/O expander, touch panel and display bus are the same as in the previous display-and-touch sketch. They define how the ESP32 interacts with the external chips and the RGB panel.
PCF8574 pcf8574(0x21); Adafruit_CST8XX tsPanel = Adafruit_CST8XX(); Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel( 16 /* CS */, 2 /* SCK */, 1 /* SDA */, 40 /* DE */, 7 /* VSYNC */, 15 /* HSYNC */, 41 /* PCLK */, 46 /* R0 */, 3 /* R1 */, 8 /* R2 */, 18 /* R3 */, 17 /* R4 */, 14 /* G0 */, 13 /* G1 */, 12 /* G2 */, 11 /* G3 */, 10 /* G4 */, 9 /* G5 */, 5 /* B0 */, 45 /* B1 */, 48 /* B2 */, 47 /* B3 */, 21 /* B4 */ );
The pcf8574 object is bound to address 0x21 and is responsible for LCD power, reset and the encoder push button. The tsPanel object wraps the CST8xx touch controller. The bus pointer defines the mapping of RGB and timing signals to ESP32 GPIOs exactly as before, using your R0–R4, G0–G5 and B0–B4 pin table.
The high-level graphics object for the ST7701 RGB panel is then created on top of this bus.
Arduino_ST7701_RGBPanel *gfx = new Arduino_ST7701_RGBPanel( bus, GFX_NOT_DEFINED, // RST pin (not used, we reset via PCF8574) 0, // rotation false, // IPS 480, 480, // width, height st7701_type5_init_operations, sizeof(st7701_type5_init_operations), true, // BGR 10, 4, 20, // hsync front porch, pulse width, back porch 10, 4, 20 // vsync front porch, pulse width, back porch );
As before, this encapsulates the low-level ST7701 initialisation sequence, timing parameters and color format. The reset pin is marked undefined because the reset is driven via the PCF8574 pins during initPins.
Backlight Initialization
The backlight initialization function takes the current brightness value and applies it to the backlight PWM pin:
void initBacklight() {
pinMode(BKL_PIN, OUTPUT);
analogWrite(BKL_PIN, brightness);
}
pinMode(BKL_PIN, OUTPUT); configures the backlight pin as a digital output. analogWrite(BKL_PIN, brightness); starts PWM output on this pin using the brightness value as duty cycle.
Pin and Peripheral Initialization
The initPins function is an extended version of the one you saw previously. It sets up I2C, the PCF8574 pins, the LCD power and reset sequence, the touch reset sequence and also configures the PCF8574 pin that reads the encoder push button.
void initPins() {
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
pcf8574.pinMode(P0, OUTPUT); //tp RST
pcf8574.pinMode(P2, OUTPUT); //tp INT
pcf8574.pinMode(P3, OUTPUT); //lcd power
pcf8574.pinMode(P4, OUTPUT); //lcd reset
pcf8574.pinMode(P5, INPUT_PULLUP); //encoder SW
if (!pcf8574.begin()) {
Serial.println("Can't init pcf8574");
}
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); starts the I2C bus with the specified pins, as in the previous sketches. P0, P2, P3 and P4 are configured as outputs to control touch reset, touch interrupt line, LCD power and LCD reset. P5 is configured as INPUT_PULLUP because it is connected to the encoder switch. This mirrors how you configured P5 as the encoder button input in the original encoder-only example.
The LCD power and reset timing sequence follows, identical in structure to the earlier display code.
// LCD pcf8574.digitalWrite(P3, HIGH); delay(100); pcf8574.digitalWrite(P4, HIGH); delay(100); pcf8574.digitalWrite(P4, LOW); delay(120); pcf8574.digitalWrite(P4, HIGH); delay(120);
P3 enables the LCD power rail, then P4 is toggled with specific delays to perform a hardware reset of the ST7701 controller.
The touchpad reset and configuration sequence is also the same as before.
// Touchpad pcf8574.digitalWrite(P0, HIGH); delay(100); pcf8574.digitalWrite(P0, LOW); delay(120); pcf8574.digitalWrite(P0, HIGH); delay(120); pcf8574.digitalWrite(P2, HIGH); delay(120);
P0 is pulsed to reset the CST8xx controller and P2 is driven high to establish a defined state for the touch interrupt line.
The touch controller is then initialized. tsPanel.begin(&Wire, I2C_TOUCH_ADDR) binds the CST8xx driver to the shared I2C bus and specified address, and prints a diagnostic message if the device cannot be found:
if (!tsPanel.begin(&Wire, I2C_TOUCH_ADDR)) {
Serial.println("No touchscreen found");
}
}
Encoder Interrupt Service Routine
The encoder interrupt handler is similar to the earlier encoder_irq function, but instead of maintaining a position counter, it updates the brightness variable in steps of five.
void IRAM_ATTR encoder_irq() {
encState = digitalRead(ENCODER_CLK);
if (encState != oldState) {
brightness += (digitalRead(ENCODER_DT) == encState) ? -5 : +5;
brightness = constrain(brightness, 5, 255);
oldState = encState;
brightnessChanged = true;
}
}
IRAM_ATTR ensures that the ISR is placed in instruction RAM for fast execution on ESP32, as discussed previously. Inside the function, encState is set to the current logic level of the encoder CLK pin using digitalRead(ENCODER_CLK). The condition if (encState != oldState) ensures that the code only reacts when the CLK signal actually changes, preventing multiple updates on the same level.
The direction of rotation is again determined by comparing the DT signal with the current CLK state. In this sketch, the conditional is inverted with respect to the earlier example to give a natural feel for increasing and decreasing brightness.
The following line either subtracts 5 or adds 5 to the brightness variable based on the relative phase of the quadrature signals. Positive rotation increases brightness, negative rotation decreases it.
brightness += (digitalRead(ENCODER_DT) == encState) ? -5 : +5;
And the following line ensures the brightness stays within a defined lower bound of 5 (avoiding a completely off display) and upper bound of 255 (the maximum PWM value for full brightness):
brightness = constrain(brightness, 5, 255);
oldState is updated to the new CLK state, and brightnessChanged is set to true to notify the main loop that the backlight and on-screen display should be refreshed. As before, all heavy work such as serial and graphics I/O is kept out of the ISR and is handled in the main loop.
Encoder Initialization
The initEncoder function configures the encoder pins and attaches the interrupt to the CLK line. This is effectively the same pattern as in your original encoder sketch.
void initEncoder() {
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), encoder_irq, CHANGE);
}
Both encoder channels are configured as inputs with internal pull-up resistors. The interrupt is attached to the CLK pin using attachInterrupt, with mode CHANGE to trigger on both rising and falling edges. On each change, the encoder_irq handler is invoked, updating the brightness.
LCD Initialization and Brightness Display
The LCD initialisation function brings up the ST7701 panel and configures text settings. It is similar to the earlier initLCD, but uses a larger text size to display the brightness value prominently:
void initLCD() {
gfx->begin();
gfx->fillScreen(RED);
gfx->setTextSize(10);
gfx->setTextColor(WHITE);
}
gfx->begin(); initialises the display controller and sends the configuration sequence. gfx->fillScreen(RED); sets a red background. gfx->setTextSize(10); chooses a large scaling factor so that the numeric brightness value stands out clearly. gfx->setTextColor(WHITE); configures white as the foreground color for text rendering.
The updateBrightness function ties the logical brightness value to both the physical backlight and the on-screen display.
void updateBrightness() {
analogWrite(BKL_PIN, brightness);
gfx->fillScreen(RED);
gfx->setCursor(150, 200);
gfx->printf("%3d", brightness);
}
analogWrite(BKL_PIN, brightness); updates the PWM duty cycle, actually changing the LED backlight intensity. The screen is then cleared again to red using gfx->fillScreen(RED);.
The cursor is placed at coordinates (150, 200), and gfx->printf("%3d", brightness); prints the brightness as a three-digit number.
Setup
The setup function initializes all subsystems: serial, I2C pins and external chips, the encoder, the backlight and the LCD, and finally renders the initial brightness on the display.
void setup() {
Serial.begin(115200);
initPins();
initEncoder();
initBacklight();
initLCD();
updateBrightness();
}
Serial.begin(115200); starts the UART for debugging output. initPins(); prepares the I2C bus, PCF8574, LCD and touch controller as discussed earlier. initEncoder(); enables the interrupt-driven encoder interface. initBacklight(); applies the initial brightness value to the backlight pin. initLCD(); brings up the graphics context, and updateBrightness(); immediately synchronises the on-screen display and the backlight PWM with the current brightness value.
Loop
The main loop periodically checks the encoder button, touch input and brightness update flag. It reacts to each of these events using the peripherals configured earlier.
void loop() {
int button = pcf8574.digitalRead(P5, true);
if (!button) {
Serial.printf("BTN pressed\n");
gfx->fillScreen(BLUE);
}
if (tsPanel.touched()) {
CST_TS_Point p = tsPanel.getPoint(0);
Serial.printf("TOUCH: %d, %d\n", p.x, p.y);
gfx->fillCircle(p.x, p.y, 10, BLACK);
}
if (brightnessChanged) {
brightnessChanged = false;
updateBrightness();
}
delay(100);
}
The first block reads the encoder push button via the PCF8574. pcf8574.digitalRead(P5, true); reads pin P5 with an immediate I2C transaction. Because P5 is configured as INPUT_PULLUP, the button reads high when released and low when pressed. The condition if (!button) detects the pressed state. On press, the sketch prints a message to the serial monitor and fills the display with blue, providing a simple visual indication that the button was pressed.
The next block handles capacitive touch input, reusing the same logic from the previous drawing example on the LCD. If tsPanel.touched() returns true, at least one touch point is active. The function tsPanel.getPoint(0); fetches the first touch point, which is then printed to the serial monitor. gfx->fillCircle(p.x, p.y, 10, BLACK); draws a small black circle at the touch coordinates, allowing the user to draw on top of the current screen content.
The third block checks whether the encoder ISR has updated the brightness variable. If brightnessChanged is true, the main loop clears the flag and calls updateBrightness();. This applies the new brightness to the backlight and redraws the numeric value on the screen. Moving the encoder quickly will generate multiple interrupts and set brightnessChanged repeatedly, but the loop ensures that brightness updates are handled in the main context where it is safe to do serial and graphics operations.
The final delay(100); introduces a short pause to limit the loop frequency and smooth out user interaction without overloading the CPU or I2C.
Conclusions
This tutorial provided you with code examples to get started with the CrowPanel 2.1inch-HMI ESP32 Rotary Display. See Elecrow’s Wiki for additional information and for more code examples see the github repo.
Note that you need install older libraries and an older version of the ESP32 core (2.0.14) to make the code work. Also some the of code examples there use the LVGL library, which I avoided here to keep the code simple.
If you are looking for a similar display module with a rotary encoder ring have look at the CrowPanel 1.28inch-HMI ESP32 Rotary Display or the Matouch 1.28″ ToolSet_Controller. Should you need only a round display (without the rotary encoder ring), the Digital Clock on CrowPanel 1.28″ Round Display tutorial might be useful.
Finally, if you want to learn more about rotary encoders, have a look at our How To Interface A Quadrature Rotary Encoder tutorial.
If you have any questions feel free to leave them in the comment section.
Happy Tinkering 😉
Stefan is a professional software developer and researcher. He has worked in robotics, bioinformatics, image/audio processing and education at Siemens, IBM and Google. He specializes in AI and machine learning and has a keen interest in DIY projects involving Arduino and 3D printing.



