Setting Up a Touch Display with ESP32 and Adafruit RA8875
Have you ever wanted to a responsive touch interface to your embedded systems project project?
We’ve chosen a hydro application to give you a clear idea of making such a setup.
In this guide, we’ll walk you through setting up a simple interface to control a water pump using the Adafruit RA8875 display controller with the ESP32. We’ll even throw in some cool features like real-time water level displays.
Sounds exciting? Let’s jump right in!
What You’ll Need
- ESP32 Development Board
- Adafruit RA8875 Controller
- Touch Display — Resistive Touch LCD
- Jumper wires: F-F
Step 1: Connecting the Hardware
The first step is to wire up the display and the RA8875 controller to your ESP32. Since the controller is sold separately from the display, make sure you’ve got both and that they’re connected as follows,
Step 2: Setting Up the Code
First, you’ll need to install the Adafruit_GFX, and Adafruit_RA8875 libraries from the Arduino Library Manager to make things easier.
Or else, you can use this google drive folder to download the libraries. After that, you need to unzip the folder and add the libraries to documents-> Libraries folder.
Start by initializing the display and setting it up for use with the touch controller,
#include <SPI.h>
#include "Adafruit_GFX.h"
#include "Adafruit_RA8875.h"
#define RA8875_INT 27
#define RA8875_CS 5
#define RA8875_RESET 4
Adafruit_RA8875 tft = Adafruit_RA8875(RA8875_CS, RA8875_RESET);
void setup() {
Serial.begin(9600);
if (!tft.begin(RA8875_480x272)) {
Serial.println("RA8875 Not Found!");
while (1);
}
Serial.println("Found RA8875");
tft.displayOn(true);
tft.GPIOX(true); // Enable TFT - display enable tied to GPIOX
tft.PWM1config(true, RA8875_PWM_CLK_DIV1024); // PWM for brightness control
tft.fillScreen(RA8875_BLACK);
// Enable touch functionality
tft.touchEnable(true);
}
Step 3: Designing the User Interface
We’re going to design a simple UI with two buttons. One for turning the pump ON and one for turning it OFF. We’ll also include water level indicators to make it more interactive.
Here’s how to draw the UI,
void drawUI() {
drawButton("ON", 50, 200, 100, 50, RA8875_GREEN); // ON button
drawButton("OFF", 180, 200, 100, 50, RA8875_RED); // OFF button
tft.textMode();
tft.textColor(RA8875_WHITE, RA8875_BLACK);
tft.textEnlarge(1); // Smaller text size
tft.textSetCursor(20, 50);
tft.textWrite("Reservoir 1:");
tft.textSetCursor(20, 120);
tft.textWrite("Reservoir 2:");
}
Step 4: Making the Buttons Interactive
Touch functionality is where the magic happens. Let’s make it so you can press those “ON” and “OFF” buttons and see something happen.
Note: Even though we made the buttons from the pixel range (50, 200) to (150, 250), the touch input did not correspond to that, we had to tune it through debugging. It was tuned to start from (150, 750) to (320, 850).
To make the UI more interactive, we added a feature to change the color of the button to white when clicked.
void handleTouch() {
// Get the current time
unsigned long currentTime = millis();
if (tft.touched()) {
uint16_t x, y;
tft.touchRead(&x, &y);
// Check if debounce delay has passed
if (currentTime - lastTouchTime >= debounceDelay) {
// Update the last touch time
lastTouchTime = currentTime;
// Correctly formatted output for debugging
Serial.print("Touch detected at: X=");
Serial.print(x);
Serial.print(", Y=");
Serial.println(y);
// Check if the touch falls within the "ON" button bounds
if (x >= 150 && x <= 320 && y >= 750 && y <= 850) {
// "ON" button pressed
pumpState = PUMP_ON;
Serial.println("Pump ON"); // Print when "ON" button is pressed
tft.fillRect(50, 200, 100, 50, RA8875_BLACK); // Clear the old bar for Reservoir 1
tft.fillRect(180, 200, 100, 50, RA8875_BLACK);
// Change button color to lighter green
drawButton("ON", 50, 200, 100, 50, RA8875_WHITE);
// Re-draw "OFF" button in normal color
drawButton("OFF", 180, 200, 100, 50, RA8875_RED);
}
// Check if the touch falls within the "OFF" button bounds
else if (x >= 400 && x <= 600 && y >= 750 && y <= 850) {
// "OFF" button pressed
pumpState = PUMP_OFF;
Serial.println("Pump OFF"); // Print when "OFF" button is pressed
tft.fillRect(50, 200, 100, 50, RA8875_BLACK); // Clear the old bar for Reservoir 1
tft.fillRect(180, 200, 100, 50, RA8875_BLACK);
// Change button color to lighter red
drawButton("OFF", 180, 200, 100, 50, RA8875_WHITE);
// Re-draw "ON" button in normal color
drawButton("ON", 50, 200, 100, 50, RA8875_GREEN);
}
} else {
Serial.println("Touch ignored due to debounce");
}
}
}
Step 5: Adding Water Level Indicators
For the demo, we’re simulating water levels in two reservoirs. Every time you run the code, the display will show random water levels for each reservoir. You can adjust it to a real value when implementing using sensor data.
void displayWaterLevels(int level1, int level2) {
// Clear old levels
tft.fillRect(221, 51, 150, 28, RA8875_BLACK);
tft.fillRect(221, 121, 150, 28, RA8875_BLACK);
// Display new water levels
tft.fillRect(221, 51, level1, 28, RA8875_BLUE);
tft.fillRect(221, 121, level2, 28, RA8875_BLUE);
// Display percentages
tft.textMode();
tft.textColor(RA8875_WHITE, RA8875_BLACK);
tft.textEnlarge(1);
tft.textSetCursor(370, 45);
tft.textWrite(String(level1).c_str());
tft.textWrite("%");
tft.textSetCursor(370, 115);
tft.textWrite(String(level2).c_str());
tft.textWrite("%");
}
Final Code
#include <SPI.h>
#include "Adafruit_GFX.h"
#include "Adafruit_RA8875.h"
#define RA8875_INT 27
#define RA8875_CS 5
#define RA8875_RESET 4
#define PUMP_ON 1
#define PUMP_OFF 0
// Dummy water level values for demonstration
int waterLevelReservoir1 = 75; // 75% full
int waterLevelReservoir2 = 50; // 50% full
// Variables to control the pump
int pumpState = PUMP_OFF;
// Create RA8875 object
Adafruit_RA8875 tft = Adafruit_RA8875(RA8875_CS, RA8875_RESET);
// Define lighter colors manually
#define LIGHT_GREEN 0x07E0 // Light green color
#define LIGHT_RED 0xF800 // Light red color
// Debounce configuration
unsigned long lastTouchTime = 0; // Time of the last touch event
const unsigned long debounceDelay = 300; // 300ms debounce delay
void setup() {
Serial.begin(9600);
// Initialize the RA8875 display
if (!tft.begin(RA8875_480x272)) {
Serial.println("RA8875 Not Found!");
while (1);
}
Serial.println("Found RA8875");
tft.displayOn(true);
tft.GPIOX(true); // Enable TFT - display enable tied to GPIOX
tft.PWM1config(true, RA8875_PWM_CLK_DIV1024); // PWM for brightness control
tft.fillScreen(RA8875_BLACK);
// Enable touch functionality
tft.touchEnable(true);
drawUI();
}
void loop() {
// Continuously update the water levels (dummy values for now)
displayWaterLevels(waterLevelReservoir1, waterLevelReservoir2);
// Check for touch input with debounce
handleTouch();
// Simulate a change in water levels
delay(2000); // For demo purposes, change levels every 2 seconds
waterLevelReservoir1 = random(0, 100);
waterLevelReservoir2 = random(0, 100);
}
// Function to draw the initial UI
void drawUI() {
// Draw "ON" button
drawButton("ON", 50, 200, 100, 50, RA8875_GREEN);
// Draw "OFF" button
drawButton("OFF", 180, 200, 100, 50, RA8875_RED);
// Draw labels for the reservoirs
tft.textMode();
tft.textColor(RA8875_WHITE, RA8875_BLACK); // Corrected textColor with 2 arguments
tft.textEnlarge(1); // Smaller text size
tft.textSetCursor(20, 50);
tft.textWrite("Reservoir 1:");
tft.textSetCursor(20, 120);
tft.textWrite("Reservoir 2:");
// Draw water level bars
tft.drawRect(221, 50, 150, 30, RA8875_WHITE); // Reservoir 1 bar
tft.drawRect(221, 120, 150, 30, RA8875_WHITE); // Reservoir 2 bar
}
// Function to draw buttons
void drawButton(const char* label, int x, int y, int width, int height, uint16_t color) {
tft.fillRect(x, y, width, height, color);
tft.textMode();
tft.textColor(RA8875_BLACK, color); // Corrected textColor with 2 arguments
tft.textEnlarge(1); // Text size
// Calculate center position for the text
int textWidth = strlen(label) * 12; // Estimate text width (approx)
int textX = x + (width / 2) - (textWidth / 2); // Center text horizontally
tft.textSetCursor(textX, y + 15); // Center text vertically
tft.textWrite(label);
}
// Function to display water levels
void displayWaterLevels(int level1, int level2) {
// Clear previous values (shifted by 50 pixels on the X-axis)
tft.fillRect(221, 51, 168, 28, RA8875_BLACK); // Clear the old bar for Reservoir 1
tft.fillRect(221, 121, 168, 28, RA8875_BLACK); // Clear the old bar for Reservoir 2
// Draw new water level percentages (shifted by 50 pixels on the X-axis)
int width1 = map(level1, 0, 100, 0, 150); // Map the percentage to the width of the bar
int width2 = map(level2, 0, 100, 0, 150);
// Fill bar for Reservoir 1 (shifted by 50 pixels)
tft.fillRect(221, 51, width1, 28, RA8875_BLUE);
// Fill bar for Reservoir 2 (shifted by 50 pixels)
tft.fillRect(221, 121, width2, 28, RA8875_BLUE);
// Display water level percentages
tft.textMode();
tft.textColor(RA8875_WHITE, RA8875_BLACK); // Corrected textColor with 2 arguments
tft.textEnlarge(1);
// Move the percentage text to the right side of the bars
tft.textSetCursor(370, 45);
tft.textWrite(String(level1).c_str());
tft.textWrite("%");
tft.textSetCursor(370, 115);
tft.textWrite(String(level2).c_str());
tft.textWrite("%");
}
// Function to handle touch inputs with debounce
void handleTouch() {
// Get the current time
unsigned long currentTime = millis();
if (tft.touched()) {
uint16_t x, y;
tft.touchRead(&x, &y);
// Check if debounce delay has passed
if (currentTime - lastTouchTime >= debounceDelay) {
// Update the last touch time
lastTouchTime = currentTime;
// Correctly formatted output for debugging
Serial.print("Touch detected at: X=");
Serial.print(x);
Serial.print(", Y=");
Serial.println(y);
// Check if the touch falls within the "ON" button bounds
if (x >= 150 && x <= 320 && y >= 750 && y <= 850) {
// "ON" button pressed
pumpState = PUMP_ON;
Serial.println("Pump ON"); // Print when "ON" button is pressed
tft.fillRect(50, 200, 100, 50, RA8875_BLACK); // Clear the old bar for Reservoir 1
tft.fillRect(180, 200, 100, 50, RA8875_BLACK);
// Change button color to lighter green
drawButton("ON", 50, 200, 100, 50, RA8875_WHITE);
// Re-draw "OFF" button in normal color
drawButton("OFF", 180, 200, 100, 50, RA8875_RED);
}
// Check if the touch falls within the "OFF" button bounds
else if (x >= 400 && x <= 600 && y >= 750 && y <= 850) {
// "OFF" button pressed
pumpState = PUMP_OFF;
Serial.println("Pump OFF"); // Print when "OFF" button is pressed
tft.fillRect(50, 200, 100, 50, RA8875_BLACK); // Clear the old bar for Reservoir 1
tft.fillRect(180, 200, 100, 50, RA8875_BLACK);
// Change button color to lighter red
drawButton("OFF", 180, 200, 100, 50, RA8875_WHITE);
// Re-draw "ON" button in normal color
drawButton("ON", 50, 200, 100, 50, RA8875_GREEN);
}
} else {
Serial.println("Touch ignored due to debounce");
}
}
}
Step 6: Uploading the code
Upload this code to your ESP32, and you’re all set! You’ll have an interactive display where you can turn the pump ON and OFF, with real-time updates for water levels.
Here is a demo Video,
You can experiment by adding more buttons, animations, or even real sensors if you want to go beyond a demo. Now, you are set to do wonders with this setup.
Hope you enjoyed the article. Please comment below or send us an email to info@protonest.co, if you face any issues when implementing.
Note: We can design you custom PCBs with RA8875 chips, for your specific application.
Contact us for any consultations or projects related to IoT and embedded systems.
Email: info@protonest.co
Protonest for more details.
Protonest specializes in transforming IoT ideas into reality. We offer prototyping services from concept to completion. Our commitment ensures that your visionary IoT concepts become tangible, innovative, and advanced prototypes.
Our Website: https://www.protonest.co/
Cheers!