This example demonstrates simple touch‑screen drawing on ESP32 with a 2.8‑inch TFT resistive touch LCD. Users draw red lines on a white canvas with a finger or stylus. A "RST" button at the top‑right clears the screen. This sketch can also be used to obtain touch‑screen calibration parameters by observing the alignment between touch position and drawn points.
touch_pen.ino#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 240×320) is divided into two regions:
| Region | Location | Description |
|---|---|---|
| Drawing Area | Most of the screen (except RST button) | White background; touch draws thick red lines |
| RST Button Area | Top‑right (x ≥ 205, y ≤ 15) | Blue "RST" text drawn by drawString(); tap clears canvas |
⚠️ Important Notes:
Size=2).px0 > width-36 && py0 < 16, effective coordinate range x: 205~239, y: 0~15 (> is strict, excluding x=204).px1=0, py1=0 (not 0xFFFF); only when released (the else branch with no touch) is px1 set to 0xFFFF, meaning "the next press should be a fresh start".px1=0, py1=0, LCD_Draw_Line(0, 0, px0, py0, 2, RED) is called, but the boundary check x1<Size (0<2) returns immediately, so the first touch point itself is not drawn; you must move your finger to start drawing from the second sample.Pressed() returns false), only px1 is set to 0xFFFF; py1 keeps its old value (but will be overwritten in the next px1==0xFFFF branch, so functionality is not affected).setCal() parameters.Size parameter in LCD_Draw_Line() (currently 2).LCD_Draw_Line() call.RTP_IRQ is defined but unused; we rely solely on my_touch.Pressed() for touch detection.This example is based on the ESP32‑WROOM‑32E board with a 2.8‑inch ILI9341 TFT LCD and resistive touch panel. It uses the TFT_eSPI library for display and the TFT_Touch library for touch input.
#define RTP_DOUT 39
#define RTP_DIN 32
#define RTP_SCK 25
#define RTP_CS 33
#define RTP_IRQ 36
Touch pin definitions – must match your wiring. RTP_IRQ is defined but not used in logic.
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
Touch calibration values – seven parameters: minX, maxX, minY, maxY, screen width, screen height, rotation. If drawing does not follow your finger, use this example for visual calibration: touch the four corners and observe the drawn dots, then adjust these parameters.
The program defines a set of 16‑bit RGB565 colour constants:
#define BLACK 0x0000
#define BLUE 0x001F
#define RED 0xF800
#define GREEN 0x07E0
#define CYAN 0x07FF
#define MAGENTA 0xF81F
#define YELLOW 0xFFE0
#define WHITE 0xFFFF
These macros convert RGB values to the 16‑bit RGB565 format used by the ILI9341 display controller. The actual colours used in this example:
WHITE (0xFFFF): canvas backgroundRED (0xF800): brush colourBLUE (0x001F): RST button text colouruint16_t px0 = 0, px1 = 0, py0 = 0, py1 = 0;
px0, py0: current touch coordinates read in the looppx1, py1: previous touch coordinates (line segment start)0xFFFF (65535) indicates "no valid previous point", so the next touch will be a fresh start (not a continuation).void LCD_Draw_Line(int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t Size,uint16_t colour)
{
uint16_t t;
int xerr=0,yerr=0,delta_x,delta_y,distance;
int incx,incy,uRow,uCol;
if(x1<Size||x2<Size||y1<Size||y2<Size)
{
return;
}
delta_x=x2-x1;
delta_y=y2-y1;
uRow=x1;
uCol=y1;
if(delta_x>0)incx=1;
else if(delta_x==0)incx=0;
else {incx=-1;delta_x=-delta_x;}
if(delta_y>0)incy=1;
else if(delta_y==0)incy=0;
else{incy=-1;delta_y=-delta_y;}
if( delta_x>delta_y)distance=delta_x;
else distance=delta_y;
for(t=0;t<=distance+1;t++ )
{
my_lcd.fillCircle(uRow, uCol, Size,colour);
xerr+=delta_x ;
yerr+=delta_y ;
if(xerr>distance)
{
xerr-=distance;
uRow+=incx;
}
if(yerr>distance)
{
yerr-=distance;
uCol+=incy;
}
}
}
This function uses the Bresenham line algorithm to draw thick line segments. Instead of a simple drawLine(), it draws filled circles along the line path to achieve thickness.
| Parameter | Type | Description |
|---|---|---|
x1, y1 |
int16_t | Start coordinates |
x2, y2 |
int16_t | End coordinates |
Size |
uint16_t | Pen radius in pixels (radius of each circle) |
colour |
uint16_t | Pen colour (RGB565) |
if(x1<Size||x2<Size||y1<Size||y2<Size)
{
return;
}
If the start or end point is closer to the left or top edge than Size (so the circle would partially go off‑screen), the function returns without drawing. This prevents visual artifacts but also makes the leftmost Size pixels and topmost Size pixels undrawable.
delta_x / delta_y: absolute total increments in X/Yincx / incy: step direction (+1, 0, -1)distance: the larger of the two deltas, determines number of stepsfor(t=0;t<=distance+1;t++)
{
my_lcd.fillCircle(uRow, uCol, Size,colour);
xerr+=delta_x ;
yerr+=delta_y ;
if(xerr>distance) { xerr-=distance; uRow+=incx; }
if(yerr>distance) { yerr-=distance; uCol+=incy; }
}
(uRow, uCol).xerr / yerr accumulate error; when exceeding distance, step in that axis.fillCircle() is slower than drawPixel() but gives smoother, rounded lines.drawPixel() or reduce Size.setup() initialises the LCD and touch, sets a white canvas, and draws the "RST" button in the top‑right corner.
void setup(void)
{
my_lcd.init();
my_lcd.setRotation(0);
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
my_touch.setRotation(0);
my_lcd.fillScreen(WHITE);
my_lcd.setTextColor(BLUE);
my_lcd.drawString("RST",my_lcd.width()-36,0,2);
}
my_lcd.init();
my_lcd.setRotation(0);
my_lcd.fillScreen(WHITE);
my_lcd.init(): initialise TFT LCD controller (ILI9341)my_lcd.setRotation(0): set rotation to 0° (portrait, width 240 × height 320)my_lcd.fillScreen(WHITE): fill the entire screen with white as canvasmy_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
my_touch.setRotation(0);
setCal(): set touch calibration values (minX, maxX, minY, maxY, screen width, screen height, rotation)setRotation(0): set touch rotation to match LCDmy_lcd.setTextColor(BLUE);
my_lcd.drawString("RST",my_lcd.width()-36,0,2);
my_lcd.width() - 36 = 240 - 36 = 204loop() continuously polls the touch state, implements drawing, line‑connection, and RST clear functions.
void loop()
{
if(my_touch.Pressed())
{
px0 = my_touch.X();
py0 = my_touch.Y();
if((px0 < my_lcd.width())&&(py0 < my_lcd.height()))
{
if(px1 == 0xFFFF)
{
px1 = px0;
py1 = py0;
}
if(px0 > (my_lcd.width()-36)&&(py0 < 16))
{
my_lcd.fillScreen(WHITE);
my_lcd.setTextColor(BLUE);
my_lcd.drawString("RST",my_lcd.width()-36,0,2);
}
else
{
LCD_Draw_Line(px1,py1,px0,py0,2,RED);
}
px1 = px0;
py1 = py0;
}
}
else
{
px1 = 0xFFFF;
}
}
if(my_touch.Pressed())
{
px0 = my_touch.X();
py0 = my_touch.Y();
my_touch.Pressed(): returns true when touch is detectedmy_touch.X() / my_touch.Y(): read calibrated touch coordinatesif((px0 < my_lcd.width())&&(py0 < my_lcd.height()))
Ensures touch coordinates are within screen bounds (width 240, height 320), filtering invalid/out‑of‑range data.
if(px1 == 0xFFFF)
{
px1 = px0;
py1 = py0;
}
When the previous point is marked as 0xFFFF (no valid previous point), set both start and end to the current point. This makes LCD_Draw_Line(px1, py1, px0, py0) draw a single dot (the brush touchdown).
if(px0 > (my_lcd.width()-36)&&(py0 < 16))
{
my_lcd.fillScreen(WHITE);
my_lcd.setTextColor(BLUE);
my_lcd.drawString("RST",my_lcd.width()-36,0,2);
}
x > 204 and y < 16 (top‑right rectangle)fillScreen(WHITE): clear canvas with whiteelse
{
LCD_Draw_Line(px1,py1,px0,py0,2,RED);
}
Calls LCD_Draw_Line() from previous touch (px1, py1) to current (px0, py0):
px1 = px0;
py1 = py0;
Whether a RST clear or normal drawing occurred, set the current point as the start for the next segment. For RST operation, this means the next drawing outside the RST area will start from near the RST button (but since px1 is reset to 0xFFFF on release, the effect is limited).
else
{
px1 = 0xFFFF;
}
When my_touch.Pressed() returns false (finger lifted), mark the previous point as 0xFFFF. On the next touch, drawing will start from the new touch point, not connected to the last position before release.
Key functions used in the program:
my_lcd.init(): initialise LCDmy_lcd.setRotation(): set screen orientationmy_lcd.fillScreen(): fill entire screen with given colour (used for clear)my_lcd.fillCircle(): draw filled circle (basic unit for thick lines)my_lcd.setTextColor(): set text colourmy_lcd.drawString(): draw text on screen (RST button)my_touch.setCal(): set touch calibrationmy_touch.setRotation(): set touch rotationmy_touch.Pressed(): check if touchedmy_touch.X() / my_touch.Y(): get calibrated coordinatesLCD_Draw_Line(): custom Bresenham thick‑line functionYou can adapt the code for different scenarios by adjusting the following:
my_touch.setCal(495, 3398, 721, 3448, 320, 240, 1);
Use this drawing example for calibration testing:
minX; if right, increase.minY; if down, increase.maxX or maxY.LCD_Draw_Line(px1,py1,px0,py0,2,RED); // change the 5th parameter (2) to desired radius
Larger values give thicker lines but slower drawing and more frame‑buffer refresh time.
LCD_Draw_Line(px1,py1,px0,py0,2,RED); // replace RED with another colour constant
Can use BLUE, GREEN, BLACK, etc., or custom 16‑bit RGB565 values.
my_lcd.fillScreen(WHITE); // both occurrences need changing
Change both fillScreen(WHITE) calls (in setup and in the RST clear branch) to another colour. Ensure the brush colour has enough contrast with the background.
my_lcd.drawString("RST",my_lcd.width()-36,0,2); // in setup()
my_lcd.drawString("RST",my_lcd.width()-36,0,2); // in RST branch of loop()
if(px0 > (my_lcd.width()-36)&&(py0 < 16)) // detection condition
All three places must be modified together: the two drawString() calls (button position and text) and the if condition (touch range). The button font size and the height threshold py0 < 16 must match (font size 2 corresponds to about 16 pixels high).
Add a colour‑switch button that cycles through colours:
// Globally add colour array and index
uint16_t colors[] = {RED, BLUE, GREEN, BLACK, MAGENTA};
uint8_t colorIdx = 0;
// In loop(), next to RST detection, add colour‑switch button detection
if(px0 > 0 && px0 < 60 && py0 < 16) {
colorIdx = (colorIdx + 1) % 5;
// Draw a colour indicator block
my_lcd.fillRect(0, 0, 20, 16, colors[colorIdx]);
}
Relax the strict boundary check in LCD_Draw_Line():
// Original: if(x1<Size||x2<Size||y1<Size||y2<Size) return;
// Modify to allow drawing to edges, only filter fully out‑of‑bounds coordinates:
if(x1<0) x1=0; if(y1<0) y1=0;
if(x2<0) x2=0; if(y2<0) y2=0;
if(x1>=my_lcd.width()) x1=my_lcd.width()-1;
if(y1>=my_lcd.height()) y1=my_lcd.height()-1;
if(x2>=my_lcd.width()) x2=my_lcd.width()-1;
if(y2>=my_lcd.height()) y2=my_lcd.height()-1;
Output coordinates to Serial after drawing:
Serial.begin(115200); // add in setup()
Serial.printf("Draw from (%d,%d) to (%d,%d)\n", px1, py1, px0, py0);
my_touch.setCal() minX, maxX, minY, maxY.TFT_Touch my_touch = TFT_Touch(RTP_CS, RTP_SCK, RTP_DIN, RTP_DOUT) has the correct parameter order.LCD_Draw_Line() (x1<Size returns early).else { px1 = 0xFFFF; } resets the start point on release.px0 > 204 && py0 < 16).Size parameter to make lines thinner and smoother.my_touch.Pressed() occasionally returns false during continuous touch.