This article describes setting up the Arduino’s TFT_eSPI library to use the ST7735s-controlled TFT LCD that was written as an example in a previous article in Python. We found that there are 2 0.96″ LCD IPS ST7735s models, which are GREENTAB160x80 and REDTAB160x80. Both modules differ in the spacing between them, as shown in Figure 1. This article uses the ESP8266, ESP32 DO-IT DevKit version with ESP32CAM and STM32F103C8T6. It is a board to test the functionality of the program.
The difference between the two types of LCD is that they have different default values, the reversal is not the same, inverting colors are not the same. The connection and the operating chip are the same, so we wasted time figuring out the errors that might be caused by the wiring and programming code. And finally found the difference above. Thus this article was born to use as a record of our work. First, install the TFT_eSPI library as shown in Figure 2.
TFT_eSPI setting
Modify the TFT_eSPI driver setup run in the User_Setup_Select.h file in the Arduino/libraries/TFT_eSPI folder to run the specific settings set as shown in Figure 3.
As for settings for GREENTAB160x80, edit the User_Setup.h file in the Arduino/libraries/TFT_eSPI folder. to be as shown in Figure 4.
In the case of using with REDTAB160x80, use the settings in the file User_Setup.h as shown in Figure 5.
Example Code
The program reads the type and size of the SD-Card by connecting the SD-Card Reader as in the following table. An example of a board to use is dCore-miniML that uses ESP32-CAM as the main board and expands the programming circuit of the switch chip (SW1/SW2) and displays the result with a TFT LCD as shown in Figure 6.
SD-Card Reader | ESP32 |
---|---|
Vcc | 5V |
GND | GND |
MISO | 19 |
MOSI | 23 |
SCK | 18 |
CS | 5 |
After the program connects and checks the type along with the size of the card to display. It will be the part of the loop to display the base color by displaying the name of the accompanying color. In which the color selection for the letter color is based on the principle of finding opposite colors by using the values according to the following equation.
opposite_color = white – background
Example program for GREENTAB160x80 is as follows
#include <SPI.h>
#include <TFT_eSPI.h>
#include "SD_MMC.h"
#include "soc/soc.h" // Disable brownour problems
#include "soc/rtc_cntl_reg.h" // Disable brownour problems
#include "driver/rtc_io.h"
#include <EEPROM.h> // read and write from flash memory
// Use hardware SPI
TFT_eSPI tft = TFT_eSPI();
#define SW1 0
#define SW2 16
uint16_t colorsBg[] = {
TFT_BLACK, TFT_NAVY, TFT_DARKGREEN, TFT_DARKCYAN,
TFT_MAROON, TFT_PURPLE,TFT_OLIVE, TFT_LIGHTGREY,
TFT_DARKGREY,TFT_BLUE,TFT_GREEN, TFT_CYAN,TFT_RED,
TFT_MAGENTA,TFT_YELLOW,TFT_WHITE,TFT_ORANGE,
TFT_GREENYELLOW,TFT_PINK, TFT_BROWN, TFT_GOLD,
TFT_SILVER,TFT_SKYBLUE, TFT_VIOLET
};
uint16_t colorsFg[] = {
TFT_WHITE - TFT_BLACK,
TFT_WHITE - TFT_NAVY,
TFT_WHITE - TFT_DARKGREEN,
TFT_WHITE - TFT_DARKCYAN,
TFT_WHITE - TFT_MAROON,
TFT_WHITE - TFT_PURPLE,
TFT_WHITE - TFT_OLIVE,
TFT_WHITE - TFT_LIGHTGREY,
TFT_BLACK,
TFT_WHITE - TFT_BLUE,
TFT_WHITE - TFT_GREEN,
TFT_WHITE - TFT_CYAN,
TFT_WHITE - TFT_RED,
TFT_WHITE - TFT_MAGENTA,
TFT_WHITE - TFT_YELLOW,
TFT_WHITE - TFT_WHITE ,
TFT_WHITE - TFT_ORANGE,
TFT_WHITE - TFT_GREENYELLOW,
TFT_WHITE - TFT_PINK,
TFT_WHITE - TFT_BROWN,
TFT_WHITE - TFT_GOLD,
TFT_WHITE - TFT_SILVER,
TFT_WHITE - TFT_SKYBLUE,
TFT_WHITE - TFT_VIOLET
};
char colorsName[][16] = {
{"TFT_BLACK"} ,{"TFT_NAVY"}, {"TFT_DARKGREEN"}, {"TFT_DARKCYAN"}, {"TFT_MAROON"},
{"TFT_PURPLE"}, {"TFT_OLIVE"}, {"TFT_LIGHTGREY"}, {"TFT_DARKGREY"}, {"TFT_BLUE"},
{"TFT_GREEN"}, {"TFT_CYAN"}, {"TFT_RED"}, {"TFT_MAGENTA"}, {"TFT_YELLOW"},
{"TFT_WHITE"}, {"TFT_ORANGE"}, {"TFT_GREENYELLOW"}, {"TFT_PINK"}, {"TFT_BROWN"},
{"TFT_GOLD"}, {"TFT_SILVER"}, {"TFT_SKYBLUE"}, {"TFT_VIOLET"}
};
int colorIdx = 0;
char msg[32];
uint8_t cardType;
uint64_t cardSize = 0;
bool sdCardStatus = true;
inline bool sw(int swX) {
return (1 - digitalRead(swX));
}
void waitKey() {
while (true) {
if (sw(SW1) || sw(SW2)) {
break;
}
}
}
void errorMsg(char title[32], char detail[64]) {
while (true) {
tft.fillScreen( TFT_RED );
tft.setTextColor( TFT_YELLOW );
tft.drawString( title, 20 , 10, 4 );
tft.drawString( title , 21 , 10, 4 );
tft.setTextColor( TFT_WHITE );
tft.drawString( detail, 20 , 40, 2 );
waitKey();
delay(500);
tft.fillScreen( TFT_BLACK );
tft.setTextColor( TFT_YELLOW );
tft.drawString( title, 20 , 10, 4 );
tft.drawString( title , 21 , 10, 4 );
tft.setTextColor( TFT_WHITE );
tft.drawString( detail, 20 , 40, 2 );
waitKey();
delay(250);
}
}
void setup() {
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
pinMode( SW1, INPUT );
pinMode( SW2, INPUT);
if (!SD_MMC.begin()) {
sdCardStatus = false;
}
tft.init();
tft.setRotation(3);
if (sdCardStatus) {
cardType = SD_MMC.cardType();
if (cardType == CARD_NONE) {
tft.fillScreen( TFT_RED );
tft.setTextColor( TFT_YELLOW );
tft.drawString( "ERROR", 40, 8, 4);
tft.setTextColor( TFT_WHITE );
tft.drawString("No SD card attached!", 30, 44, 2);
} else {
tft.fillScreen( TFT_BLACK );
tft.setTextColor( TFT_YELLOW );
tft.drawString("SD Card", 40, 8, 4);
tft.setTextColor(TFT_WHITE);
tft.drawString("Type", 30, 40, 2);
tft.setTextColor(TFT_CYAN);
if (cardType == CARD_MMC) {
tft.drawString("MMC", 80, 40, 2);
} else if (cardType == CARD_SD) {
tft.drawString("SDSC", 80, 40, 2);
} else if (cardType == CARD_SDHC) {
tft.drawString("SDHC", 80, 40, 2);
} else {
tft.drawString("UNKNOWN", 80, 40, 2);
}
cardSize = SD_MMC.cardSize();
tft.setTextColor(TFT_WHITE);
tft.drawString("Size", 30, 60, 2);
tft.setTextColor(TFT_CYAN);
tft.drawNumber( cardSize / (1024 * 1024), 80, 60, 2);
tft.setTextColor(TFT_WHITE);
tft.drawString("MB", 120, 60, 2);
}
} else {
tft.fillScreen( TFT_RED );
tft.setTextColor( TFT_YELLOW );
tft.drawString( "ERROR", 40, 8, 4);
tft.setTextColor( TFT_WHITE );
tft.drawString(" SD Card Mount failed!", 10, 44, 2);
}
waitKey();
}
void loop() {
tft.fillScreen( colorsBg[colorIdx] );
tft.setTextColor( colorsFg[colorIdx] );
tft.drawString( colorsName[colorIdx], 10 , 16, 2 ); // Font No.2
tft.drawString( colorsName[colorIdx], 11 , 16, 2 ); // Font No.2
sprintf(msg, "Sw2 = %d, Sw3 = %d\n", digitalRead( 0 ), digitalRead( 16 ));
tft.drawString( msg, 10, 48 );
delay(3000);
colorIdx++;
if (colorIdx == 24) {
colorIdx = 0;
}
}
As for REDTAB160x80, there is a slight difference in that the MAD Control must be set upside down on the display. The sample code is as follows. The board is used as an experimental board as shown in Figure 7 and the case of SD Card Reader is not found, it will report as shown in Figure 8.
#include <SPI.h>
#include <TFT_eSPI.h>
#include "SD.h"
TFT_eSPI tft = TFT_eSPI();
uint16_t colorsBg[] = {
TFT_BLACK, TFT_NAVY, TFT_DARKGREEN, TFT_DARKCYAN,
TFT_MAROON, TFT_PURPLE,TFT_OLIVE, TFT_LIGHTGREY,
TFT_DARKGREY,TFT_BLUE,TFT_GREEN, TFT_CYAN,TFT_RED,
TFT_MAGENTA,TFT_YELLOW,TFT_WHITE,TFT_ORANGE,
TFT_GREENYELLOW,TFT_PINK, TFT_BROWN, TFT_GOLD,
TFT_SILVER,TFT_SKYBLUE, TFT_VIOLET
};
uint16_t colorsFg[] = {
TFT_WHITE - TFT_BLACK,
TFT_WHITE - TFT_NAVY,
TFT_WHITE - TFT_DARKGREEN,
TFT_WHITE - TFT_DARKCYAN,
TFT_WHITE - TFT_MAROON,
TFT_WHITE - TFT_PURPLE,
TFT_WHITE - TFT_OLIVE,
TFT_WHITE - TFT_LIGHTGREY,
TFT_BLACK,
TFT_WHITE - TFT_BLUE,
TFT_WHITE - TFT_GREEN,
TFT_WHITE - TFT_CYAN,
TFT_WHITE - TFT_RED,
TFT_WHITE - TFT_MAGENTA,
TFT_WHITE - TFT_YELLOW,
TFT_WHITE - TFT_WHITE ,
TFT_WHITE - TFT_ORANGE,
TFT_WHITE - TFT_GREENYELLOW,
TFT_WHITE - TFT_PINK,
TFT_WHITE - TFT_BROWN,
TFT_WHITE - TFT_GOLD,
TFT_WHITE - TFT_SILVER,
TFT_WHITE - TFT_SKYBLUE,
TFT_WHITE - TFT_VIOLET
};
char colorsName[][16] = {
{"TFT_BLACK"} ,{"TFT_NAVY"}, {"TFT_DARKGREEN"}, {"TFT_DARKCYAN"}, {"TFT_MAROON"},
{"TFT_PURPLE"}, {"TFT_OLIVE"}, {"TFT_LIGHTGREY"}, {"TFT_DARKGREY"}, {"TFT_BLUE"},
{"TFT_GREEN"}, {"TFT_CYAN"}, {"TFT_RED"}, {"TFT_MAGENTA"}, {"TFT_YELLOW"},
{"TFT_WHITE"}, {"TFT_ORANGE"}, {"TFT_GREENYELLOW"}, {"TFT_PINK"}, {"TFT_BROWN"},
{"TFT_GOLD"}, {"TFT_SILVER"}, {"TFT_SKYBLUE"}, {"TFT_VIOLET"}
};
int colorIdx = 0;
char msg[32];
uint8_t cardType;
uint64_t cardSize = 0;
bool sdCardStatus = true;
void setup() {
pinMode( SW1, INPUT );
pinMode( SW2, INPUT);
if (!SD.begin()) {
sdCardStatus = false;
}
tft.init();
tft.setRotation(1);
tft.writecommand(ST7735_MADCTL);
tft.writedata(TFT_MAD_MV | TFT_MAD_COLOR_ORDER );
if (sdCardStatus) {
cardType = SD.cardType();
if (cardType == CARD_NONE) {
tft.fillScreen( TFT_RED );
tft.setTextColor( TFT_YELLOW );
tft.drawString( "ERROR", 40, 8, 4);
tft.setTextColor( TFT_WHITE );
tft.drawString("No SD card attached!", 30, 44, 2);
} else {
tft.fillScreen( TFT_BLACK );
tft.setTextColor( TFT_YELLOW );
tft.drawString("SD Card", 40, 8, 4);
tft.setTextColor(TFT_WHITE);
tft.drawString("Type", 30, 40, 2);
tft.setTextColor(TFT_CYAN);
if (cardType == CARD_MMC) {
tft.drawString("MMC", 80, 40, 2);
} else if (cardType == CARD_SD) {
tft.drawString("SDSC", 80, 40, 2);
} else if (cardType == CARD_SDHC) {
tft.drawString("SDHC", 80, 40, 2);
} else {
tft.drawString("UNKNOWN", 80, 40, 2);
}
cardSize = SD.cardSize();
tft.setTextColor(TFT_WHITE);
tft.drawString("Size", 30, 60, 2);
tft.setTextColor(TFT_CYAN);
tft.drawNumber( cardSize / (1024 * 1024), 80, 60, 2);
tft.setTextColor(TFT_WHITE);
tft.drawString("MB", 120, 60, 2);
}
} else {
tft.fillScreen( TFT_RED );
tft.setTextColor( TFT_YELLOW );
tft.drawString( "ERROR", 40, 8, 4);
tft.setTextColor( TFT_WHITE );
tft.drawString(" SD Card Mount failed!", 10, 44, 2);
}
delay(10000);
}
void loop() {
tft.fillScreen( colorsBg[colorIdx] );
tft.setTextColor( colorsFg[colorIdx] );
tft.drawString( colorsName[colorIdx], 10 , 16, 2 ); // Font No.2
tft.drawString( colorsName[colorIdx], 11 , 16, 2 ); // Font No.2
delay(3000);
colorIdx++;
if (colorIdx == 24) {
colorIdx = 0;
}
}
Example with ESP8266
Connecting the TFT module to the ESP8266 is shown in Table 2 and shown in Figure 9.
ST7735s | ESP8266 |
---|---|
Vcc | 3V3 |
GND | GND |
MOSI | D7 |
SCK | D5 |
DC | D6 |
RST | RST |
CS | D8 |
Figure 9 is the data in the file User_Setup.h for connecting to the pin of the TFT module as shown in Figure 10.
The sample program code is as follows.
#include <Arduino.h>
#include <SPI.h>
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
uint16_t colorsBg[] = {
TFT_BLACK, /* 0, 0, 0 */
TFT_NAVY, /* 0, 0, 128 */
TFT_DARKGREEN, /* 0, 128, 0 */
TFT_DARKCYAN, /* 0, 128, 128 */
TFT_MAROON, /* 128, 0, 0 */
TFT_PURPLE, /* 128, 0, 128 */
TFT_OLIVE, /* 128, 128, 0 */
TFT_LIGHTGREY, /* 211, 211, 211 */
TFT_DARKGREY, /* 128, 128, 128 */
TFT_BLUE, /* 0, 0, 255 */
TFT_GREEN, /* 0, 255, 0 */
TFT_CYAN,/* 0, 255, 255 */
TFT_RED,/* 255, 0, 0 */
TFT_MAGENTA,/* 255, 0, 255 */
TFT_YELLOW,/* 255, 255, 0 */
TFT_WHITE ,/* 255, 255, 255 */
TFT_ORANGE,/* 255, 180, 0 */
TFT_GREENYELLOW, /* 180, 255, 0 */
TFT_PINK, /* 255, 192, 203 */ //Lighter pink, was 0xFC9F
TFT_BROWN, /* 150, 75, 0 */
TFT_GOLD, /* 255, 215, 0 */
TFT_SILVER, /* 192, 192, 192 */
TFT_SKYBLUE, /* 135, 206, 235 */
TFT_VIOLET /* 180, 46, 226 */
};
uint16_t colorsFg[] = {
TFT_WHITE - TFT_BLACK, /* 0, 0, 0 */
TFT_WHITE - TFT_NAVY, /* 0, 0, 128 */
TFT_WHITE - TFT_DARKGREEN, /* 0, 128, 0 */
TFT_WHITE - TFT_DARKCYAN, /* 0, 128, 128 */
TFT_WHITE - TFT_MAROON, /* 128, 0, 0 */
TFT_WHITE - TFT_PURPLE, /* 128, 0, 128 */
TFT_WHITE - TFT_OLIVE, /* 128, 128, 0 */
TFT_WHITE - TFT_LIGHTGREY, /* 211, 211, 211 */
// TFT_WHITE - TFT_DARKGREY, /* 128, 128, 128 */
TFT_BLACK,
TFT_WHITE - TFT_BLUE, /* 0, 0, 255 */
TFT_WHITE - TFT_GREEN, /* 0, 255, 0 */
TFT_WHITE - TFT_CYAN, /* 0, 255, 255 */
TFT_WHITE - TFT_RED, /* 255, 0, 0 */
TFT_WHITE - TFT_MAGENTA, /* 255, 0, 255 */
TFT_WHITE - TFT_YELLOW, /* 255, 255, 0 */
TFT_WHITE - TFT_WHITE , /* 255, 255, 255 */
TFT_WHITE - TFT_ORANGE, /* 255, 180, 0 */
TFT_WHITE - TFT_GREENYELLOW, /* 180, 255, 0 */
TFT_WHITE - TFT_PINK, /* 255, 192, 203 */ //Lighter pink, was 0xFC9F
TFT_WHITE - TFT_BROWN, /* 150, 75, 0 */
TFT_WHITE - TFT_GOLD, /* 255, 215, 0 */
TFT_WHITE - TFT_SILVER, /* 192, 192, 192 */
TFT_WHITE - TFT_SKYBLUE, /* 135, 206, 235 */
TFT_WHITE - TFT_VIOLET /* 180, 46, 226 */
};
char colorsName[][16] = {
{"TFT_BLACK"} ,
{"TFT_NAVY"},
{"TFT_DARKGREEN"},
{"TFT_DARKCYAN"},
{"TFT_MAROON"},
{"TFT_PURPLE"},
{"TFT_OLIVE"},
{"TFT_LIGHTGREY"},
{"TFT_DARKGREY"},
{"TFT_BLUE"},
{"TFT_GREEN"},
{"TFT_CYAN"},
{"TFT_RED"},
{"TFT_MAGENTA"},
{"TFT_YELLOW"},
{"TFT_WHITE"},
{"TFT_ORANGE"},
{"TFT_GREENYELLOW"},
{"TFT_PINK"},
{"TFT_BROWN"},
{"TFT_GOLD"},
{"TFT_SILVER"},
{"TFT_SKYBLUE"},
{"TFT_VIOLET"}
};
int colorIdx = 0;
char msg[32];
void setup() {
tft.init();
tft.setRotation(1);
tft.writecommand(TFT_MADCTL);
tft.writedata(TFT_MAD_MV | TFT_MAD_COLOR_ORDER );
}
void loop() {
tft.fillScreen( colorsBg[colorIdx] );
tft.setTextColor( colorsFg[colorIdx] );
tft.drawString( colorsName[colorIdx], 10 , 16, 2 ); // Font No.2
tft.drawString( colorsName[colorIdx], 11 , 16, 2 ); // Font No.2
delay(3000);
colorIdx++;
if (colorIdx == 24) {
colorIdx = 0;
}
}
Example with STM32F103C8T6
Connecting ST7735 model REDTAB80x160 to the microcontroller board STM32F103C8T6 as shown in Figure 10, which has the following pin connections.
st7735 | STM32F103C8T6 |
---|---|
GND | GND |
Vcc | 3V3 |
SCL | PA5 |
SDA | PA7 |
RESET | 3V3 |
DC | PB0 |
CS | PB1 |
BLK | 3V3 |
The information in the User_Setup.h file is as follows:
#define ST7735_DRIVER
#define TFT_WIDTH 80
#define TFT_HEIGHT 160
#define ST7735_REDTAB160x80
#define TFT_INVERSION_ON
#define TFT_MISO PA6
#define TFT_MOSI PA7
#define TFT_SCLK PA5
#define TFT_CS PB1
#define TFT_DC PB0
#define TFT_RST -1
#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define SMOOTH_FONT
#define SPI_FREQUENCY 27000000
Examples of programs for testing the functionality will be found similar to other controller boards, which is an advantage of using the Arduino framework as it makes less difference in the instruction set. When compiled, 29,032 bytes of ROM is used (Figure 11), the result of the example is shown in Figure 12.
#include <TFT_eSPI.h> // Graphics and font library for ST7735 driver chip
#include <SPI.h>
TFT_eSPI tft = TFT_eSPI(); // Invoke library, pins defined in User_Setup.h
uint16_t colorsBg[] = {
TFT_BLACK, TFT_NAVY, TFT_DARKGREEN, TFT_DARKCYAN,
TFT_MAROON, TFT_PURPLE,TFT_OLIVE, TFT_LIGHTGREY,
TFT_DARKGREY,TFT_BLUE,TFT_GREEN, TFT_CYAN,TFT_RED,
TFT_MAGENTA,TFT_YELLOW,TFT_WHITE,TFT_ORANGE,
TFT_GREENYELLOW,TFT_PINK, TFT_BROWN, TFT_GOLD,
TFT_SILVER,TFT_SKYBLUE, TFT_VIOLET
};
uint16_t colorsFg[] = {
TFT_WHITE - TFT_BLACK,
TFT_WHITE - TFT_NAVY,
TFT_WHITE - TFT_DARKGREEN,
TFT_WHITE - TFT_DARKCYAN,
TFT_WHITE - TFT_MAROON,
TFT_WHITE - TFT_PURPLE,
TFT_WHITE - TFT_OLIVE,
TFT_WHITE - TFT_LIGHTGREY,
TFT_BLACK,
TFT_WHITE - TFT_BLUE,
TFT_WHITE - TFT_GREEN,
TFT_WHITE - TFT_CYAN,
TFT_WHITE - TFT_RED,
TFT_WHITE - TFT_MAGENTA,
TFT_WHITE - TFT_YELLOW,
TFT_WHITE - TFT_WHITE ,
TFT_WHITE - TFT_ORANGE,
TFT_WHITE - TFT_GREENYELLOW,
TFT_WHITE - TFT_PINK,
TFT_WHITE - TFT_BROWN,
TFT_WHITE - TFT_GOLD,
TFT_WHITE - TFT_SILVER,
TFT_WHITE - TFT_SKYBLUE,
TFT_WHITE - TFT_VIOLET
};
char colorsName[][16] = {
{"TFT_BLACK"} ,{"TFT_NAVY"}, {"TFT_DARKGREEN"}, {"TFT_DARKCYAN"}, {"TFT_MAROON"},
{"TFT_PURPLE"}, {"TFT_OLIVE"}, {"TFT_LIGHTGREY"}, {"TFT_DARKGREY"}, {"TFT_BLUE"},
{"TFT_GREEN"}, {"TFT_CYAN"}, {"TFT_RED"}, {"TFT_MAGENTA"}, {"TFT_YELLOW"},
{"TFT_WHITE"}, {"TFT_ORANGE"}, {"TFT_GREENYELLOW"}, {"TFT_PINK"}, {"TFT_BROWN"},
{"TFT_GOLD"}, {"TFT_SILVER"}, {"TFT_SKYBLUE"}, {"TFT_VIOLET"}
};
int colorIdx = 0;
char msg[32];
void setup() {
tft.init();
tft.setRotation(1);
tft.writecommand(ST7735_MADCTL);
tft.writedata(TFT_MAD_MV | TFT_MAD_COLOR_ORDER );
tft.fillScreen(0x3861);
}
void loop() {
tft.fillScreen( colorsBg[colorIdx] );
tft.setTextColor( colorsFg[colorIdx] );
tft.drawString( colorsName[colorIdx], 10 , 16, 2 ); // Font No.2
tft.drawString( colorsName[colorIdx], 11 , 16, 2 ); // Font No.2
delay(3000);
colorIdx++;
if (colorIdx == 24) {
colorIdx = 0;
}
}
Conclusion
From this article, you will find that small differences result in different functionality, so reading the documentation and understanding the manufacturer’s hardware design is a requirement for system developers to monitor and update their knowledge regularly. Finally, we hope this article will be useful to the users of this type of display module and have fun programming.
If you want to talk with us, feel free to leave comments below!!
Reference
(C) 2020-2021, By Jarut Busarathid and Danai Jedsadathitikul
Updated 2021-12-16