Screen orientations for the Elegoo 2.8" TFT LCD Touch Display

I struggled for some time with the concepts of screen rotation for this display. I decided I'd modify the Paint sketch to show the four rotations and how code can be adapted.

The sample sketch given here isn't intended to be efficient and compact; rather it aims to illustrate the concepts and techniques used. Please contact me if you spot errors that need correction. Otherwise please use and enjoy.

/*
   This sketch was created for the Elegoo 2.8" TFT LCD Touch Screen that interfaces directly
   with the Arduino UNO and DUEMILANOVE
   It will probably work with other displays that are compatible with the Elegoo libraries.
   The sketch:
    a) is over-commented so that it acts as a tutorial
    b) cleans up a lot of messy code in the original
    c) shows how screen rotations work for all four modes (0, 1, 2, 3)
    d) provides examples of the mapping of touchscreen (TS) values to display screen (TFT) coordinates
       using the map() function

    I am indebted to the late David Prentice for the many suggestions and tips he's made over many years
    and whose clarity of thought is still an inspiration.
    ric_m July 2023

   Only the next two lines need to be edited to try different rotations
*/
#define VERSION 2.00  // (optional) Sent to serial monitor (useful to ensure current version if compilation is cached)
int rotation = 0;     // 0 = default; change to 1, 2 or 3 accordingly

/*
  // Paint example specifically for the TFTLCD breakout board.
  // If using the Arduino shield, use the tftpaint_shield.pde sketch instead!
  // DOES NOT CURRENTLY WORK ON ARDUINO LEONARDO
  // Technical support:goodtft@163.com
*/

#include <Elegoo_GFX.h>    // Core graphics library
#include <Elegoo_TFTLCD.h> // Hardware-specific library
#include <TouchScreen.h>

#if defined(__SAM3X8E__)
#undef __FlashStringHelper::F(string_literal)
#define F(string_literal) string_literal
#endif

// When using the BREAKOUT BOARD only, use these 8 data lines to the LCD:
// For the Arduino Uno, Duemilanove, Diecimila, etc.:
//   D0 connects to digital pin 8  (Notice these are
//   D1 connects to digital pin 9   NOT in order!)
//   D2 connects to digital pin 2
//   D3 connects to digital pin 3
//   D4 connects to digital pin 4
//   D5 connects to digital pin 5
//   D6 connects to digital pin 6
//   D7 connects to digital pin 7

// For the Arduino Mega, use digital pins 22 through 29
// (on the 2-row header at the end of the board).
//   D0 connects to digital pin 22
//   D1 connects to digital pin 23
//   D2 connects to digital pin 24
//   D3 connects to digital pin 25
//   D4 connects to digital pin 26
//   D5 connects to digital pin 27
//   D6 connects to digital pin 28
//   D7 connects to digital pin 29

// For the Arduino Due, use digital pins 33 through 40
// (on the 2-row header at the end of the board).
//   D0 connects to digital pin 33
//   D1 connects to digital pin 34
//   D2 connects to digital pin 35
//   D3 connects to digital pin 36
//   D4 connects to digital pin 37
//   D5 connects to digital pin 38
//   D6 connects to digital pin 39
//   D7 connects to digital pin 40

/*
  #define YP 9  // must be an analog pin, use "An" notation!
  #define XM 8  // must be an analog pin, use "An" notation!
  #define YM A2   // can be a digital pin
  #define XP A3   // can be a digital pin
*/

#define YP A3  // must be an analog pin, use "An" notation!
#define XM A2  // must be an analog pin, use "An" notation!
#define YM 9   // can be a digital pin
#define XP 8   // can be a digital pin

// Here are the scaling values for the touchscreen that overlays
// the TFT LCD display

/*
   These are defaults provided by Elegoo. Presumably the values are the mean
   of representative sample boards?
  //Touch For New ILI9341 TP
  #define TS_MINX 120
  #define TS_MAXX 900
  #define TS_MINY 70
  #define TS_MAXY 920
*/
// Recalibrated Touch For My ILI9341 TS by ric_m
// =============================================
// These values never change once the touchscreen has been calibrated
// Nor do these values change when screen orientation is changed -
// They are tied to the physical touch screen overlay and its hard wiring
// chnange these if your calibration gives different values
#define TS_MINX 118 // (LEFT)
#define TS_MAXX 925 // (RIGHT)
#define TS_MINY  70 // (TOP)
#define TS_MAXY 910 // (BOTTOM)
// It turns out that my touchscreen values are not so very different from the defaults.
// In other literature David Prentice finds a different set of descriptors easier to visualise. He
// suggests something similar to the following:
// TS_MINX ==> TS_LFT    minimum possible value is 0
// TS_MAXX ==> TS_RGT    maximum possible value is 1023
// TS_MINY ==> TS_TOP    minimum possible value is 0
// TS_MAXY ==> TS_BTM    maximum possible value is 1023
// I have not incorporated this idea because the aim is to illustrate the way in which screen (TFT)
// and touchpad (TS) values behave in the various orientations. Most manufacturers (Adafruit, Elegoo,
// McuFriend) use the TS_MINX type of notation; accordingly I have retained it so that individuals can see
// more easily the changes I have made when comparing my code with their own.

// In LANDSCAPE orientation (1 or 3) the x and y directions are swapped so it is easier
// if we hold the co-ordinates of the touched point in two variables, px and py rather than use p.x and p.y
// NOTE: the construct in most of these sketches is destructive with, for example,
// p.x = map(p.x .... etc
// this construct replaces the old value of p.x with the new, scaled value. This really messes
// up the next statement in landscape mode for p.y where the value of p.x has changed:
// p.y = map(p.x ..... etc
//           ^^^
//        new value!

// Instead, the construct advocated by David Prentice is used here where px and py hold the coordinates
// TSPoint(int16_t x, int16_t y, int16_t z)
int16_t px = 0;
int16_t py = 0;

// For better pressure precision, we need to know the resistance
// between X+ and X- Use any multimeter to read it
// For the one we're using, its 300 ohms across the X plate
TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);

#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
// optional
#define LCD_RESET A4

// Assign human-readable names to some common 16-bit color values:
#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF
#define GREY    0x8410

Elegoo_TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RESET);

#define BOXSIZE 40    // size of the pallete boxes - 1 row x 6 colours (portrait); 1 row x 8 boxes (landscape)
#define CLEARSIZE 30  // size of the area for clearing the display
const String CLEARTEXT = "Clear";
#define PENRADIUS 3
int oldcolor, currentcolor;

#define MINPRESSURE 10
#define MAXPRESSURE 1000

// Finally, there are quite a few Serial.print statements that can be used for debugging. Enabling any one or
// more of these in the loop() section will seriously degrade performance during drawing with the stylus. Turn
// off all print statements once debugging has completed.

void setup(void) {
  Serial.begin(9600);
  Serial.println(F("Portrait and Landscape Paint"));
  Serial.print("VERSION: "); Serial.println(VERSION);

  tft.reset();

  uint16_t identifier = tft.readID();
  if (identifier == 0x9325) {
    Serial.println(F("Found ILI9325 LCD driver"));
  } else if (identifier == 0x9328) {
    Serial.println(F("Found ILI9328 LCD driver"));
  } else if (identifier == 0x4535) {
    Serial.println(F("Found LGDP4535 LCD driver"));
  } else if (identifier == 0x7575) {
    Serial.println(F("Found HX8347G LCD driver"));
  } else if (identifier == 0x9341) {
    Serial.println(F("Found ILI9341 LCD driver"));
  } else if (identifier == 0x8357) {
    Serial.println(F("Found HX8357D LCD driver"));
  } else if (identifier == 0x0101)
  {
    identifier = 0x9341;
    Serial.println(F("Found 0x9341 LCD driver"));
  } else {
    Serial.print(F("Unknown LCD driver chip: "));
    Serial.println(identifier, HEX);
    Serial.println(F("If using the Elegoo 2.8\" TFT Arduino shield, the line:"));
    Serial.println(F("  #define USE_Elegoo_SHIELD_PINOUT"));
    Serial.println(F("should appear in the library header (Elegoo_TFT.h)."));
    Serial.println(F("If using the breakout board, it should NOT be #defined!"));
    Serial.println(F("Also if using the breakout, double-check that all wiring"));
    Serial.println(F("matches the tutorial."));
    identifier = 0x9341;
  }

  tft.begin(identifier);
  tft.setRotation(rotation);
  // Serial.print("Rotation: "); Serial.println(rotation);
  tft.fillScreen(BLACK);

  tft.fillRect(0, 0, BOXSIZE, BOXSIZE, RED);
  tft.fillRect(BOXSIZE, 0, BOXSIZE, BOXSIZE, YELLOW);
  tft.fillRect(BOXSIZE * 2, 0, BOXSIZE, BOXSIZE, GREEN);
  tft.fillRect(BOXSIZE * 3, 0, BOXSIZE, BOXSIZE, CYAN);
  tft.fillRect(BOXSIZE * 4, 0, BOXSIZE, BOXSIZE, BLUE);
  tft.fillRect(BOXSIZE * 5, 0, BOXSIZE, BOXSIZE, MAGENTA);

  // We have a bit more room in landscape mode for two more colours
  // either way, report the orientation to the user
  switch (rotation) {
    case 0:
      Serial.println("Rotation = 0; PORTRAIT");
      break;
    case 1:
      Serial.println("Rotation = 1; LANDSCAPE");
      tft.fillRect(BOXSIZE * 6, 0, BOXSIZE, BOXSIZE, WHITE);
      tft.fillRect(BOXSIZE * 7, 0, BOXSIZE, BOXSIZE, GREY);
      break;
    case 2:
      Serial.println("Rotation = 2; PORTRAIT_REV");
      break;
    case 3:
      Serial.println("Rotation = 3; LANDSCAPE_REV");
      tft.fillRect(BOXSIZE * 6, 0, BOXSIZE, BOXSIZE, WHITE);
      tft.fillRect(BOXSIZE * 7, 0, BOXSIZE, BOXSIZE, GREY);
      break;
  }

  // draw the Clear 'button' at bottom of screen
  //  LEFT is 0, right is tft.width(), bottom is tft.height() and button height is CLEARSIZE
  // fillRoundRect (int16_t x, int16_t y, int16_t w, int16_t h, uint8_t R , uint16_t t)
  tft.fillRoundRect(0, tft.height() - CLEARSIZE, tft.width(), CLEARSIZE, 5, GREY);
  // drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t R , uint16_t t)
  tft.drawRoundRect(0, tft.height() - CLEARSIZE, tft.width(), CLEARSIZE, 5, WHITE);
  tft.setCursor(tft.width() / 2 - CLEARTEXT.length() - 1, tft.height() - 17); // position varies with orientation
  tft.setTextColor(WHITE, GREY);
  tft.print("Clear");

  tft.drawRect(0, 0, BOXSIZE, BOXSIZE, WHITE);    // highlight the default colour with a white outline
  currentcolor = RED;
  pinMode(13, OUTPUT);
}

void loop()
{
  digitalWrite(13, HIGH);
  TSPoint p = ts.getPoint();
  digitalWrite(13, LOW);

  // if sharing pins, you'll need to fix the directions of the touchscreen pins
  //pinMode(XP, OUTPUT);
  pinMode(XM, OUTPUT);
  pinMode(YP, OUTPUT);
  //pinMode(YM, OUTPUT);

  // we have some minimum pressure we consider 'valid'
  // pressure of 0 means no pressing!
  if (p.z > MINPRESSURE && p.z < MAXPRESSURE) {
    /*
      Serial.print("X = "); Serial.print(p.x);
      Serial.print("\tY = "); Serial.print(p.y);
      Serial.print("\tPressure = "); Serial.println(p.z);
    */
    // Serial.print("p.x (RAW) = "); Serial.print(p.x); Serial.print(", p.y (RAW) = "); Serial.print(p.y);

    // TS has values in the interval 0->1023. These values must be scaled to screen (TFT) coordinates in the
    // interval 0->320 and 0->240 (the screen size). In addition to this re-mapping the directions change when
    // orientation changes. The code must allow for this. (It is this bit that most vexed me initially and
    // probably many others too).
    // This is all taken care of by the map function.

    // The manufacturer's code doesn't help understanding. It is hard to read; the following construct particularly so:
    // py = tft.height() - map (p.x, TS_MINX, TS_MAXX, 0, tft.height());
    // The construct ("height" - "the rest") simply reverses the values 0<=1023, counting down rather than up.
    // A better construct is to use the reversal built-into the map function. The arguments are:
    // map(value, fromLow, fromHigh, toLow, toHigh)
    // Switching values supplied to the last two arguments provides the necessary counting down we need, effectively:
    // map(value, fromLow, fromHigh, toHigh, toLow)
    // Not only is this more efficient, it is more intuitive (providing you are already clear about
    // the map function's arguments).
    // Serial.print("; Rotation = 1; p.x = "); Serial.print(p.x); Serial.print("; p.y = "); Serial.println(p.y);
    switch (rotation) {
      case 0:   // PORTRAIT: rotation = 0
        px = map(p.x, TS_MAXX, TS_MINX, tft.width(), 0);
        py = map(p.y, TS_MAXY, TS_MINY, 0, tft.height());   // note the reversal in the last two arguments
        //Serial.print("; Rotation = 0; px = "); Serial.print(px); Serial.print("; py = "); Serial.println(py);
        break;
      case 1:   // LANDSCAPE: rotation = 1
        px = map(p.y, TS_MINY, TS_MAXY, tft.width(), 0);
        py = map (p.x, TS_MINX, TS_MAXX, tft.height(), 0);  // note the reversal in the last two arguments
        // Serial.print("; Rotation = 1; px = "); Serial.print(px); Serial.print("; py = "); Serial.println(py);
        break;
      case 2:   // PORTRAIT_REV: rotation =  2
        px = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
        py = map(p.y, TS_MINY, TS_MAXY, 0, tft.height());   // note the reversal in the last two arguments
        // Serial.print("; Rotation = 2; px = "); Serial.print(px); Serial.print("; py = "); Serial.println(py);
        break;
      case 3:   // LANDSCAPE_REV: rotation = 3
        px = map(p.y, TS_MINY, TS_MAXY, 0, tft.width());
        py = map(p.x, TS_MINX, TS_MAXX, 0, tft.height());
        //Serial.print("; Rotation = 3; px = "); Serial.print(px); Serial.print("; py = "); Serial.println(py);
        break;
    }

    // From hereon all co-ordinates are screen (TFT) coordinates; we are no longer interested
    // in the touchscreen (TS) co-ordinates (i.e., the raw values in the interval 0->1023 because
    // these have been converteed to the former. Sanity at last!
    // Additionally, screen (TFT) coordinates are contained in px and py; p.x and p.y are both redundant

    // Clear the screen if the Clear 'button' is pressed
    if (py > (tft.height() - CLEARSIZE)) {
      // erase beween the palette boxes and the Clear 'button'
      // Serial.println("erase");
      tft.fillRect(0, BOXSIZE, tft.width(), tft.height() - CLEARSIZE - BOXSIZE, BLACK);
    }

    if (py < BOXSIZE) {
      // get new colour
      oldcolor = currentcolor;

      if (px < BOXSIZE) {    // box no 1, on the left, red
        currentcolor = RED;
        tft.drawRect(0, 0, BOXSIZE, BOXSIZE, WHITE);

      } else if (px < BOXSIZE * 2) {
        currentcolor = YELLOW;
        tft.drawRect(BOXSIZE, 0, BOXSIZE, BOXSIZE, WHITE);

      } else if (p.x < BOXSIZE * 3) {
        currentcolor = GREEN;
        tft.drawRect(BOXSIZE * 2, 0, BOXSIZE, BOXSIZE, WHITE);

      } else if (px < BOXSIZE * 4) {
        currentcolor = CYAN;
        tft.drawRect(BOXSIZE * 3, 0, BOXSIZE, BOXSIZE, WHITE);

      } else if (px < BOXSIZE * 5) {
        currentcolor = BLUE;
        tft.drawRect(BOXSIZE * 4, 0, BOXSIZE, BOXSIZE, WHITE);

      } else if (px < BOXSIZE * 6) {
        currentcolor = MAGENTA;
        tft.drawRect(BOXSIZE * 5, 0, BOXSIZE, BOXSIZE, WHITE);

      } else if (px < BOXSIZE * 7) {
        currentcolor = WHITE;
        tft.drawRect(BOXSIZE * 6, 0, BOXSIZE, BOXSIZE, BLACK);  // use border colour BLACK for highlight

      } else if (px < BOXSIZE * 8) {
        currentcolor = GREY;
        tft.drawRect(BOXSIZE * 7, 0, BOXSIZE, BOXSIZE, WHITE);
      }

      if (oldcolor != currentcolor) {
        if (oldcolor == RED) tft.fillRect(0, 0, BOXSIZE, BOXSIZE, RED);
        if (oldcolor == YELLOW) tft.fillRect(BOXSIZE, 0, BOXSIZE, BOXSIZE, YELLOW);
        if (oldcolor == GREEN) tft.fillRect(BOXSIZE * 2, 0, BOXSIZE, BOXSIZE, GREEN);
        if (oldcolor == CYAN) tft.fillRect(BOXSIZE * 3, 0, BOXSIZE, BOXSIZE, CYAN);
        if (oldcolor == BLUE) tft.fillRect(BOXSIZE * 4, 0, BOXSIZE, BOXSIZE, BLUE);
        if (oldcolor == MAGENTA) tft.fillRect(BOXSIZE * 5, 0, BOXSIZE, BOXSIZE, MAGENTA);
        if (oldcolor == WHITE) tft.fillRect(BOXSIZE * 6, 0, BOXSIZE, BOXSIZE, WHITE);
        if (oldcolor == GREY) tft.fillRect(BOXSIZE * 7, 0, BOXSIZE, BOXSIZE, GREY);
      }
    }

    // plot the point if vertically it lies within the top pallette boxes and the bottom clear button,
    // allowing for the size of the pen not to bleed over boundaries

    if (((py - PENRADIUS) > BOXSIZE) && ((py + PENRADIUS) < tft.height() - CLEARSIZE)) {
      // Serial.print("Plot point: px = "); Serial.print(px); Serial.print("; py = "); Serial.println(py);
      tft.fillCircle(px, py, PENRADIUS, currentcolor);
    }
  }
}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.