Open Smart serial TFT LCD touch screen - example Arduino code

  • I bought a $10 touchscreen for Arduino, which can be controlled via serial commands.

    Specifically the "2.4 inch UART Serial TFT LCD module Touch Screen Expansion Shield with TF card socket Touch Pen for Arduino UNO R3 Mega2560 Nano" from Aliexpress. Once you power it up it identifies itself as an "Open Smart TFT LCD".
    Driver IC is: 9325
    Firmware version is v10
    Chip: ILI9325


    • 320x240 (can be rotated in all directions)
    • 2.4 inch
    • Touch screen
    • Controlled via serial (USART/UART)
    • Adaptable brightness

    Since there weren't any libraries for it I developed a little bit of code to make working with it easier (see below).

    The device has some good points:

    • Surprisingly responsive to touch for a resistive touch model. It comes with a small stylus, but you don't need it. Very light finger pressure is already registered well.
    • Very memory efficient, since you just need to send serial commands.
    • Cheap
    • Has Dupont pins already soldered on.
    • Has a space for an SD card, can show bitmap images (but I haven't tried that).

    The downsides:

    • Slow! As far as I can tell you can only send one command at a time, and have to wait for the 'ok' response before you can send the next one. Moving the text cursor takes 8 milliseconds. Filling the screen with a color takes 50 milliseconds. Most commands are in between those extremes. In practise, this means that creating a complex menu takes about a second, and you see all the things pop into view one by one. Setting it use a higher baud-rate made no difference.
    • Has a few small menu icons built into one edge of the screen. They cannot be removed. They detract, and could confuse users.
    • Some devices I've tried have more polished commands. This screen requires HEX codes. Others allow you to print text, and are more flexible. For example:
    • You can send it characters, but only in batches of 5. If you want to display a single character, this means you have to display the character and 4 space commands.

    All in all though, I'm very happy with it. Users expect touch interfaces, and the cost is not much higher than other input hardware.


     * This is a small bit of messy demo code for the Open Smart serial touchscreen.
     * This code is provided 'as-is', and may be considered public domain.
    #define MAX_BASIC_COMMAND_LENGTH 20 // How many bytes are in the longest basic command?
    #define TOUCHSCREEN_WIDTH 240
    #define TOUCHSCREEN_HEIGHT 320
    const byte numChars = 8;
    byte receivedChars[numChars];       // An array to store the received serial data
    boolean touched = false;            // Was the touchscreen just touched?
    #include <SoftwareSerial.h>
    SoftwareSerial mySerial(4,5);       // RX, TX
    signed int touchX = 0;              // Touch screen position X
    signed int touchY = 0;              // Touch screen position Y
    // All commands for the TFT should start with 0x7E, but to save storage space this is taken care of in the basicCommand function.
    PROGMEM const byte resetTFT[] =         {0x02, 0x05, 0xEF,}; // Resets the TFT. But has no real effect.
    PROGMEM const byte testTFT[] =          {0x02, 0x00, 0xEF,}; // Test the TFT, should respond with "OK".
    PROGMEM const byte hello[] =            {0x07, 0x11, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0xEF,}; // Places the word 'hello' on the screen.
    PROGMEM const byte set_vertical[] =     {0x03, 0x04, 0x02, 0xEF,}; // To set rotation of the screen to vertical. Try 0x01 or 0x03 instead of the 0x02.
    PROGMEM const byte rectangle[] =        {0x0E, 0x2C, 0x00, 0x10,0x00, 0x10,0x00, 0x50,0x00, 0x50,0x00, 0x10, 0x00, 0xFF, 0xEF,}; // Test rectangle.
    PROGMEM const byte backlight[] =        {0x03, 0x06}; // Backlight intensity
    PROGMEM const byte fill_screen[] =      {0x04, 0x20, 0x00, 0x00, 0xEF,}; // Fill screen with one color
    PROGMEM const byte fill_screen2[] =     {0x04, 0x20, 0x00, 0x8F, 0xEF,}; // Fill screen with one color
    PROGMEM const byte setCurs[] =          {0x06, 0x01, 0x00, 0x10, 0x00, 0x10, 0xEF,}; // Sets text cursor position to a fixed position (16 pixels from the left, 16 pixels from the top).
    PROGMEM const byte serialSpeedUp[] =    {0x03, 0x40, 0x03, 0xEF,}; // Sets communication speed to 57600 (from 9600)
    PROGMEM const byte serialSlowDown[] =   {0x03, 0x40, 0x00, 0xEF,}; // Sets communication speed to 9600 again. Oddly enough, it seems it works fastest at this speed..
    PROGMEM const byte text_color_black[] = {0x04, 0x02, 0x00, 0x00, 0xEF,}; // Dark text color. fill screen with one color
    PROGMEM const byte text_color_red[] =   {0x04, 0x02, 0xF8, 0x00, 0xEF,}; // .. text color. fill screen with one color
    PROGMEM const byte text_color_white[] = {0x04, 0x02, 0xFF, 0xFF, 0xEF,}; // white text color. fill screen with one color
    The colors use this RGB565 format:
    white: FF FF = 65535;
    red: F8 00
    purple: F0 1F
    blue: 00 1F
    light blue: 07 FF
    bright green: 07 E0
    yellow: FF 20
    orange: FC 80
    // The MessageDef code below comes from
    typedef byte MessageID;
    enum {
    struct MessageDef {
      MessageID ID;
      char Description[20];
    const MessageDef MessageTable[] PROGMEM = {
      {OPTION5," ABC def ghi!"}  
    const byte MessageTableSize = sizeof(MessageTable)/sizeof(MessageDef);
    void setup() {
      Serial.begin(115200);                     // Start serial output of data.
      Serial.println(F("Hello world"));
      // Speeding up the communication has no effect on how quickly the screen responds to commands.
      //basicCommand(serialSlowDown);           // slow down the display..
      //basicCommand(serialSpeedUp);            // speed up the display..
      //mySerial.begin(57600);                  // and then speed up the connection.
      Serial.println();Serial.println(F("__set rotation"));
    void loop() {
        touched = false;
        drawPix(touchX,touchY, 65535); // Draw a pixel 
        if( touched == false && touchY < 100 ){
    void showMainMenu()
      Serial.print(F("mainMenuSize")); Serial.println(MessageTableSize);
      for(byte i = 0; i < MessageTableSize; i++ ){
        Serial.print(F(">")); Serial.println(i);
        setCur(10,50 + i * 45);
    // This function writes text to the screen. You can use the setCur function to place the cursor in the desired position first.
    void writeText(byte textID)
      byte j = 0;
      byte c = 0;
      byte stringLength = strlen_P(MessageTable[textID].Description);
      Serial.print(F("- stringLength: ")); Serial.println(stringLength);
      while ( j < stringLength ){
        byte command[9] = {0x7E, 0x07, 0x11, 0x00, 0x00, 0x20, 0x00, 0x00, 0xEF,}; // It took quite some testing to get this right. Just using 0x00 for the entire payload will not work.
        for( byte i=0; i < 5; i++ ){
          if(j+i < stringLength){
            command[3+i] = pgm_read_byte(MessageTable[textID].Description + j + i);
            //Serial.print(command[3+i]); Serial.print(F(" "));
        for( int i=0; i < sizeof(command); i++ ){
          mySerial.write( command[i] );
          Serial.print(command[i],HEX); Serial.print(F(" "));
        j = j + 5;
    void drawPix(int x, int y, int c) // Draw a pixel on the screen.
      Serial.print(F("x: ")); Serial.println(x);
      Serial.print(F("y: ")); Serial.println(y);
      Serial.print(F("c: ")); Serial.println(c);
      byte command[10] = {0x7E, 0x08, 0x21, highByte(x), lowByte(x), highByte(y), lowByte(y), highByte(c), lowByte(c), 0xEF,};
      for( int i=0; i < sizeof(command); i++ ){
        mySerial.write( command[i] );
        //Serial.print(command[i],HEX); Serial.print(F(" "));
    // This function places the text cursor anywhere on the screen.
    void setCur(int x, int y)
      Serial.print(F("x: ")); Serial.println(x);
      Serial.print(F("y: ")); Serial.println(y);
      byte command[8] = {0x7E, 0x06, 0x01, highByte(x), lowByte(x), highByte(y), lowByte(y), 0xEF,};
      for( int i=0; i < sizeof(command); i++ ){
        mySerial.write( command[i] );
        //Serial.print(command[i],HEX); Serial.print(F(" "));
    // This function outputs a variable number to the screen. It can show negative and positive numbers. It cannot show floats.
    void displayNumber(signed int number)
      Serial.print(F("DISPLAYNUMBER (")); Serial.println(number); 
      byte command[8] = {0x7E, 0x06, 0x13, 0x01, 0x0A, highByte(number), lowByte(number), 0xEF,};
      for( int i=0; i < sizeof(command); i++ ){
        mySerial.write( command[i] );
        Serial.print(command[i],HEX); Serial.print(F(" "));
    // This function reads the serial data (if available) from the screen. The implemented commands it can detect are 'ok', and the touch coordinates.
    void readResponse()
      if( mySerial.available() < 5 ){ // Any response should be at least 5 bytes.
      boolean savingMessage = false;
      byte receivedCharsPosition = 0;
      byte startMarker = 0x7E; // Any response from the screen will start with this.
      byte endMarker = 0xEF; // Ane response from the screen will end with this.
      byte rc; // Hold the byte form the serial stream that we're examining.
      while( mySerial.available() ){ //  && newData == false //  By not checking for this the entire buffer is always cleared.
        rc =;
        Serial.print(rc); Serial.print(F(" "));
        if( savingMessage == true ){ // We are now in the part of the response that is the message we were looking for.
          if(rc != endMarker){
            receivedChars[receivedCharsPosition] = rc;
            if( receivedCharsPosition >= numChars ){
              receivedCharsPosition = numChars - 1;
          else { // We've arrived at the end marker.
            receivedChars[receivedCharsPosition] = '\0'; // Terminate the string.
            savingMessage = false;
            receivedCharsPosition = 0;
            if(receivedChars[receivedCharsPosition] == 0x06 && receivedChars[receivedCharsPosition + 1] == 0x07){
              touchX = touchToX( (receivedChars[receivedCharsPosition + 2] * 256) + receivedChars[receivedCharsPosition + 3] );
              touchY = touchToY( (receivedChars[receivedCharsPosition + 4] * 256) + receivedChars[receivedCharsPosition + 5] );
              Serial.print(F("touchX=")); Serial.println(touchX);
              Serial.print(F("touchY=")); Serial.println(touchY);
              touched = true; // To indicate that a touch event has just occured.
            // OK message
            else if( receivedChars[receivedCharsPosition] == 0x03 && receivedChars[receivedCharsPosition + 1] == 0x6F && receivedChars[receivedCharsPosition + 2] == 0x6B ){
              if( receivedCharsPosition == numChars ){ return; }
        if(rc == startMarker){ // Once a valid startMarker is found, we start saving the message into the array.
          savingMessage = true;
          //Serial.print(F("(startMarker) "));
    // This function can send basic string command that don't have any variable parts in them.
    void basicCommand(const char* cmd)
      Serial.println(F("BASIC COMMAND"));
      if( mySerial.available()){  Serial.print(F("cleaning:")); } // If necessary, clear old messages from the serial buffer.
      while( mySerial.available() ){
        char x =;
      mySerial.write(0x7E); // Starting byte, is always the same.
      Serial.print(0x7E); Serial.print(F(" "));
      byte b = 0;
      while( b < MAX_BASIC_COMMAND_LENGTH ){ // How many bytes are the basic commands at most?
        mySerial.write( pgm_read_byte(&cmd[b]) );
        Serial.print( pgm_read_byte(&cmd[b]),HEX ); Serial.print(F(" "));
        if( pgm_read_byte(&cmd[b]) == 0xEF ){ // This breaks out of the loop.
    // This function can be activated after sending a command. It will wait until a response has arrived (or 100 milliseconds have passed), and then allow the Arduino to continue.
    void waitForResponse(byte expectedBytes)
      Serial.println(); Serial.println(F("WAITING FOR RESPONSE"));
      byte b = 0;
      while( mySerial.available() < expectedBytes && b < 100){
      Serial.print(F("wait time: ")); Serial.println(b);
      delay(10); // A small extra delay seems to be required.
    int touchToX(int x)
      return int constrain(((x - 80) / 3.7), 0, TOUCHSCREEN_WIDTH);
    int touchToY(int y)
      return int constrain(((y - 80) / 2.7), 0, TOUCHSCREEN_HEIGHT);

    As a side note, I also tried the Nextion devices, but I wasn't able to figure out how to remove all the startup logos and animations it comes with. I also wasn't able to remove a text scrolling widget that was apparently built in. No matter what I did, it stayed put, scrolling Chinese characters.

  • Hardware Contributor


    weird that you got problems with nextion.
    I think you have something wrong in the gui creator tool, sdcard, or code, else it should work.
    I have two here, and no problem. I'm not using them for HA, not a fan of "diy" displays, with small dpi, for home..But I always thought they are noob friendly (serial, easy to use api and gui creator tool, saving some coding time), so I'm sure you'll get it working.

  • @scalz Thanks for the tip and thinking along, I appreciate it. The things is I actually don't use the GUI creation tool.

    • It only works in windows.
    • Micro SD cards costs yet more money, so I don't use those either.
    • I try to keep my code resolution independent/flexible. Bitmaps are not.

    That's why I went looking for screens where I can draw shapes and text by sending simple commands to it. It also forces me to keep the GUI simple and clean, as I only have basic shapes available to me.

    While the Nextion supports drawing shapes and text, I wasn't able to remove some UI elements by sending serial commands. Perhaps the GUI editor tool would have allowed me to do a full reset, and remove the 'built in' demo animations. But my goal is to create plug-and-play things for beginners, and I don't want them to have to use the GUI tool in that process.

    What I like about this cheap screen is that it has built in support for drawing rounded rectangular boxes, which is very useful. It also has a 'quick horizontal line' function, which is similarly handy.