This example demonstrates how to implement a phone call user interface on the ESP32 with a 2.8-inch TFT LCD touchscreen. The program displays a numeric keypad (1-9, *, 0, #) along with "call", "end", and "dele" buttons. Users can touch the buttons to dial a number, simulate making a call, ending a call, and deleting entered digits. The interface provides visual feedback by highlighting buttons when pressed.
display_phonecall.ino file#define RTP_DOUT 39
#define RTP_DIN 32
#define RTP_SCK 25
#define RTP_CS 33
#define RTP_IRQ 36
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
The screen (portrait 240x320) is divided into two areas:
| Area | Location | Description |
|---|---|---|
| Display Area | Top of screen (y=1 to y=31) | Bordered rectangle showing dialed numbers and call status messages |
| Keypad Area | Below the display area | 5 rows x 3 columns of circular buttons |
Button layout (top to bottom):
| Row | Col 1 | Col 2 | Col 3 |
|---|---|---|---|
| 1 | 1 (cyan) | 2 (cyan) | 3 (cyan) |
| 2 | 4 (cyan) | 5 (cyan) | 6 (cyan) |
| 3 | 7 (cyan) | 8 (cyan) | 9 (cyan) |
| 4 | * (pink) | 0 (cyan) | # (pink) |
| 5 | end (red) | call (green) | dele (grey) |
⚠️ Key Notes:
setCal() parameterswhile(!digitalRead(RTP_IRQ))) to detect touch release, so no new input is accepted until the finger is liftedThis example code is based on the ESP32-WROOM-32E development board with a 2.8-inch ILI9341 TFT LCD and resistive touchscreen. The program uses the TFT_eSPI library for display and the TFT_Touch library for touch input.
The program includes parameters that may need adjustment according to your hardware:
#define RTP_DOUT 39
#define RTP_DIN 32
#define RTP_SCK 25
#define RTP_CS 33
#define RTP_IRQ 36
Touch screen pin definitions - must match your hardware wiring.
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
Touch calibration values - the seven parameters are: minX, maxX, minY, maxY, screenWidth, screenHeight, rotation. Adjust these if touch positions do not match the displayed buttons.
#define BUTTON_R 25
#define BUTTON_SPACING_X 25
#define BUTTON_SPACING_Y 5
#define EDG_Y 5
#define EDG_X 20
UI layout parameters - control button size, spacing, and edge margins.
The program defines a set of 16-bit RGB565 color constants:
#define BLACK 0x0000 /* 0, 0, 0 */
#define BLUE 0x001F /* 0, 0, 255 */
#define RED 0xF800 /* 255, 0, 0 */
#define GREEN 0x07E0 /* 0, 255, 0 */
#define CYAN 0x07FF /* 0, 255, 255 */
// ... (more colors)
These macros convert RGB values to 16-bit RGB565 format used by the ILI9341 display controller.
typedef struct _button_info
{
const char button_name[10];
uint8_t button_name_size;
uint16_t button_name_colour;
uint16_t button_colour;
int16_t button_x;
int16_t button_y;
}button_info;
button_name: Text label displayed on the button (e.g., "1", "call")button_name_size: Font size for the labelbutton_name_colour: Color of the label textbutton_colour: Background color of the buttonbutton_x, button_y: Center coordinates of the buttonbutton_info phone_button[15] =
{
"1",4,BLACK,CYAN,EDG_X+BUTTON_R-1,(int16_t)(my_lcd.height()-EDG_Y-4*BUTTON_SPACING_Y-9*BUTTON_R-1),
"2",4,BLACK,CYAN,EDG_X+3*BUTTON_R+BUTTON_SPACING_X-1,(int16_t)(my_lcd.height()-EDG_Y-4*BUTTON_SPACING_Y-9*BUTTON_R-1),
"3",4,BLACK,CYAN,EDG_X+5*BUTTON_R+2*BUTTON_SPACING_X-1,(int16_t)(my_lcd.height()-EDG_Y-4*BUTTON_SPACING_Y-9*BUTTON_R-1),
// ... (buttons 4-9, *, 0, #)
"end",2,BLACK,RED,EDG_X+BUTTON_R-1,(int16_t)(my_lcd.height()-EDG_Y-BUTTON_R-1),
"call",2,BLACK,GREEN,EDG_X+3*BUTTON_R+BUTTON_SPACING_X-1,(int16_t)(my_lcd.height()-EDG_Y-BUTTON_R-1),
"dele",2,BLACK,LIGHTGREY,EDG_X+5*BUTTON_R+2*BUTTON_SPACING_X-1,(int16_t)(my_lcd.height()-EDG_Y-BUTTON_R-1),
};
The array defines 15 buttons arranged in a 5x3 grid. Button positions are calculated based on:
EDG_X: Left edge offset (20 pixels)EDG_Y: Bottom edge offset (5 pixels)BUTTON_R: Button radius (25 pixels)BUTTON_SPACING_X: Horizontal spacing between buttons (25 pixels)BUTTON_SPACING_Y: Vertical spacing between button rows (5 pixels)The Y coordinates are calculated from the bottom of the screen (my_lcd.height()) upward, so row 1 (buttons 1,2,3) is at the top of the keypad and row 5 (end, call, dele) is at the bottom.
Button indices in the array:
uint16_t px, py;
uint16_t text_x=10,text_y=5,text_x_add = 4*phone_button[0].button_name_size+2,text_y_add = 6*phone_button[0].button_name_size;
uint16_t n=0;
px, py: Calibrated touch point coordinatestext_x, text_y: Current position for drawing the next dialed character (start at x=10, y=5)text_x_add: Character width increment = 4*4+2 = 18 pixels (based on button "1" font size 4)text_y_add: Character height = 6*4 = 24 pixelsn: Counter of entered characters (max 13)boolean is_pressed(int16_t x1,int16_t y1,int16_t x2,int16_t y2,int16_t px,int16_t py)
{
if((px > x1 && px < x2) && (py > y1 && py < y2))
{
return true;
}
else
{
return false;
}
}
(x1, y1) top-left corner, (x2, y2) bottom-right corner of the button bounding box, (px, py) touch point coordinatestrue if the touch point is within the bounding box, false otherwisevoid show_menu(void)
{
uint16_t i;
for(i = 0;i < sizeof(phone_button)/sizeof(button_info);i++)
{
my_lcd.fillCircle(phone_button[i].button_x, phone_button[i].button_y, BUTTON_R,phone_button[i].button_colour);
my_lcd.setTextColor(phone_button[i].button_name_colour);
my_lcd.drawString(phone_button[i].button_name,phone_button[i].button_x-strlen(phone_button[i].button_name)*phone_button[i].button_name_size*4/2+phone_button[i].button_name_size/2+1,phone_button[i].button_y-phone_button[i].button_name_size*6/2+phone_button[i].button_name_size/2+1,phone_button[i].button_name_size);
}
my_lcd.fillRect(1, 1, my_lcd.width()-2, 2,BLACK);
my_lcd.fillRect(1, 29, my_lcd.width()-2, 2,BLACK);
my_lcd.fillRect(1, 1, 2, 30,BLACK);
my_lcd.fillRect(my_lcd.width()-3, 1, 2, 30,BLACK);
}
This function draws the complete keypad UI:
phone_button arrayfillCircle(): Draws a filled circle with the button's background colordrawString(): Draws the button label centered on the circlefillRect() calls:
⚠️ Label Centering Calculation:
button_x - strlen(name) * size * 4 / 2 + size / 2 + 1button_y - size * 6 / 2 + size / 2 + 1This calculates the top-left corner of the text so it appears centered on the button. The font width is approximated as size * 4 / 2 pixels per character, and the font height is approximated as size * 6 pixels. This is specific to the default TFT_eSPI built-in font.
The setup() function initializes the touch IRQ pin, LCD, and touch screen, then draws the keypad UI.
void setup(void)
{
pinMode(RTP_IRQ, INPUT);
my_lcd.init();
my_lcd.setRotation(0);
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
my_touch.setRotation(0);
my_lcd.fillScreen(BLUE);
show_menu();
}
pinMode(RTP_IRQ, INPUT);
my_lcd.init();
my_lcd.setRotation(0);
my_lcd.fillScreen(BLUE);
my_lcd.init(): Initializes the TFT LCDmy_lcd.setRotation(0): Sets rotation to 0 degrees (portrait mode, 240 wide x 320 tall)my_lcd.fillScreen(BLUE): Fills the screen with blue backgroundmy_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
my_touch.setRotation(0);
setCal(): Sets touch screen calibration values (minX, maxX, minY, maxY, screenWidth, screenHeight, rotation)setRotation(0): Sets touch screen rotation to match the LCD⚠️ Notes:
show_menu();
show_menu() to draw all 15 buttons and the display area borderThe loop() function polls for touch events, identifies which button was pressed, provides visual feedback, and performs the corresponding action (dial, call, end, or delete).
void loop(void)
{
uint8_t i;
if(my_touch.Pressed()&&!digitalRead(RTP_IRQ))
{
px = my_touch.X();
py = my_touch.Y();
for(i=0;i<sizeof(phone_button)/sizeof(button_info);i++)
{
if(is_pressed(phone_button[i].button_x-BUTTON_R,phone_button[i].button_y-BUTTON_R,phone_button[i].button_x+BUTTON_R,phone_button[i].button_y+BUTTON_R,px,py))
{
my_lcd.fillCircle(phone_button[i].button_x, phone_button[i].button_y, BUTTON_R,DARKGREY);
my_lcd.setTextColor(WHITE);
my_lcd.drawString(phone_button[i].button_name,phone_button[i].button_x-strlen(phone_button[i].button_name)*phone_button[i].button_name_size*4/2+phone_button[i].button_name_size/2+1,phone_button[i].button_y-phone_button[i].button_name_size*6/2+phone_button[i].button_name_size/2+1,phone_button[i].button_name_size);
my_lcd.fillCircle(phone_button[i].button_x, phone_button[i].button_y, BUTTON_R, phone_button[i].button_colour);
my_lcd.setTextColor(phone_button[i].button_name_colour);
my_lcd.drawString(phone_button[i].button_name,phone_button[i].button_x-strlen(phone_button[i].button_name)*phone_button[i].button_name_size*4/2+phone_button[i].button_name_size/2+1,phone_button[i].button_y-phone_button[i].button_name_size*6/2+phone_button[i].button_name_size/2+1,phone_button[i].button_name_size);
if(i < 12)
{
if(n < 13)
{
my_lcd.setTextColor(GREENYELLOW);
my_lcd.drawString(phone_button[i].button_name,text_x,text_y,phone_button[i].button_name_size);
text_x += text_x_add-1;
n++;
}
}
else if(12 == i)
{
my_lcd.fillRect(0, 33, my_lcd.width(), 10,BLUE);
my_lcd.setTextColor(RED);
my_lcd.drawString("Calling ended",my_lcd.width()/2-52,33,1);
}
else if(13 == i)
{
my_lcd.fillRect(0, 33, my_lcd.width(), 10,BLUE);
my_lcd.setTextColor(GREEN);
my_lcd.drawString("Calling...",my_lcd.width()/2-52,33,1);
}
else if(14 == i)
{
if(n > 0)
{
text_x -= (text_x_add-1);
my_lcd.fillRect(text_x, text_y, text_x_add, text_y_add-1,BLUE);
n--;
}
}
while(!digitalRead(RTP_IRQ));
}
}
}
}
if(my_touch.Pressed()&&!digitalRead(RTP_IRQ))
{
px = my_touch.X();
py = my_touch.Y();
my_touch.Pressed(): Returns true if a touch is detected by the library!digitalRead(RTP_IRQ): Double-checks the IRQ pin is LOW (active-low touch interrupt)my_touch.X() / my_touch.Y(): Reads the calibrated X and Y coordinates of the touch point⚠️ Notes:
px and py for use in button hit testingfor(i=0;i<sizeof(phone_button)/sizeof(button_info);i++)
{
if(is_pressed(phone_button[i].button_x-BUTTON_R,phone_button[i].button_y-BUTTON_R,phone_button[i].button_x+BUTTON_R,phone_button[i].button_y+BUTTON_R,px,py))
{
(button_x - BUTTON_R, button_y - BUTTON_R) to (button_x + BUTTON_R, button_y + BUTTON_R) (a square enclosing the circular button)is_pressed() to check if the touch point is within this bounding boxwhile(!digitalRead(RTP_IRQ)) at the end blocks until release)my_lcd.fillCircle(phone_button[i].button_x, phone_button[i].button_y, BUTTON_R,DARKGREY);
my_lcd.setTextColor(WHITE);
my_lcd.drawString(phone_button[i].button_name, ...);
my_lcd.fillCircle(phone_button[i].button_x, phone_button[i].button_y, BUTTON_R, phone_button[i].button_colour);
my_lcd.setTextColor(phone_button[i].button_name_colour);
my_lcd.drawString(phone_button[i].button_name, ...);
⚠️ Note: There is a commented-out //delay(100); between the two redraws. Uncomment it to make the flash effect visible for a longer duration.
if(i < 12)
{
if(n < 13)
{
my_lcd.setTextColor(GREENYELLOW);
my_lcd.drawString(phone_button[i].button_name,text_x,text_y,phone_button[i].button_name_size);
text_x += text_x_add-1;
n++;
}
}
n < 13: Limits input to 13 characters maximum(text_x, text_y) in green-yellow colortext_x += text_x_add - 1: Advances the X position by 17 pixels for the next charactern++: Increments the character counterelse if(12 == i)
{
my_lcd.fillRect(0, 33, my_lcd.width(), 10,BLUE);
my_lcd.setTextColor(RED);
my_lcd.drawString("Calling ended",my_lcd.width()/2-52,33,1);
}
fillRect(): Clears the status line at y=33 by filling with blue backgroundelse if(13 == i)
{
my_lcd.fillRect(0, 33, my_lcd.width(), 10,BLUE);
my_lcd.setTextColor(GREEN);
my_lcd.drawString("Calling...",my_lcd.width()/2-52,33,1);
}
else if(14 == i)
{
if(n > 0)
{
text_x -= (text_x_add-1);
my_lcd.fillRect(text_x, text_y, text_x_add, text_y_add-1,BLUE);
n--;
}
}
n > 0: Only deletes if there are characters to removetext_x -= (text_x_add - 1): Moves the X position back 17 pixelsfillRect(): Clears the character by filling its 18x23 pixel area with the blue backgroundn--: Decrements the character counterwhile(!digitalRead(RTP_IRQ));
⚠️ Notes:
Key functions used in the program include:
my_lcd.init(): Initialize the LCDmy_lcd.setRotation(): Set screen rotationmy_lcd.fillScreen(): Fill the entire screen with a colormy_lcd.fillCircle(): Draw a filled circle (used for buttons)my_lcd.fillRect(): Draw a filled rectangle (used for clearing areas and borders)my_lcd.drawString(): Draw text on the screenmy_lcd.setTextColor(): Set text colormy_touch.setCal(): Set touch calibration valuesmy_touch.setRotation(): Set touch rotationmy_touch.Pressed(): Check if screen is touchedmy_touch.X() / my_touch.Y(): Get calibrated touch coordinatesis_pressed(): Check if a point is within a bounding boxIf you need to modify the code for different application scenarios, refer to the following aspects:
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
Use the touch pen example (29_touch_pen) to obtain calibration values for your specific screen, then replace these parameters.
#define BUTTON_R 25 // Button radius
#define BUTTON_SPACING_X 25 // Horizontal spacing between buttons
#define BUTTON_SPACING_Y 5 // Vertical spacing between button rows
#define EDG_Y 5 // Bottom edge margin
#define EDG_X 20 // Left edge margin
Adjust these to change button size, spacing, and edge margins.
button_info phone_button[15] =
{
"1",4,BLACK,CYAN, ..., // 4th field = text color, 5th field = button color
...
};
Modify the 3rd (text color) and 4th (button color) fields in each button definition.
if(n < 13) // Change 13 to your desired maximum
my_lcd.drawString("Calling...",my_lcd.width()/2-52,33,1);
my_lcd.drawString("Calling ended",my_lcd.width()/2-52,33,1);
Modify the status text strings. Adjust the X offset (-52) if the new text has a different length.
Uncomment the delay between the two redraws:
my_lcd.fillCircle(..., DARKGREY); // pressed state
my_lcd.setTextColor(WHITE);
my_lcd.drawString(...);
delay(100); // uncomment this line
my_lcd.fillCircle(..., original); // released state
// Add a buzzer or audio feedback when a button is pressed
// Example using tone on a buzzer pin:
tone(buzzerPin, 1000, 50); // 1000Hz for 50ms
Replace the blocking while(!digitalRead(RTP_IRQ)); with a non-blocking timer:
unsigned long pressStart = millis();
while(!digitalRead(RTP_IRQ)) {
if(millis() - pressStart > 500) {
// Repeat the action (e.g., delete multiple characters)
// ...
pressStart = millis();
}
}
Touch Position is Inaccurate
my_touch.setCal()Buttons Don't Respond to Touch
Screen is Blank or Shows Garbage
Button Labels are Misaligned
show_menu() and loop() if using a custom fontCharacters Overflow the Display Area
if(n < 13))Program Hangs After Touch
while(!digitalRead(RTP_IRQ)); is a blocking waitunsigned long startWait = millis();
while(!digitalRead(RTP_IRQ)) {
if(millis() - startWait > 3000) break; // 3-second timeout
}