เหตุเกิดจากทางทีมงานเราได้ซื้อจอแสดงผลขนาด 3.5″ สำหรับบอร์ด Raspberry Pi มาใช้งาน แล้วเกิดคำถามว่าถ้านำมาใช้กับบอร์ด ESP32 จะสามารถทำได้หรือไม่ ด้วยเหตุนี้ บทความนี้กล่าวถึงวิธีการใช้งานบอร์ดแสดงผล TFT LCD Shield ที่ออกแบบมาใช้กับ Raspberry Pi ของบริษัท Waveshare มาใช้กับไมโครคอนโทรลเลอร์ ESP32 รุ่น TTGO T8 ผ่านทางไลบรารี TFT_eSPI เพื่อแสดงผล และใช้งานระบบหน้าจอสัมผัส (Touch Screen) ดังตัวอย่างภาพที่ 1
Raspberry Pi 3.5″ TFT LCD Shield Touch Screen
จอแสดงผล Raspberry Pi 3.5″ TFT LCD Shield Touch Screen เป็นจอแสดงผลประเภท TFT ขนาด 3.5″ พร้อมระบบจอสัมผัสที่ออกแบบเพื่อใช้กับบอร์ด Raspberry Pi ดังภาพที่ 2 ซึ่งผลิตโดยบริษัท Waveshare
คุณสมบัติ
คุณสมบัติของโมดูลแสดงผลมีดังนี้
- ความละเอียดในการแสดงผล 480×320 จุด
- ใช้การเชื่อมต่อผ่านบัส SPI
- โมดูลควบคุมส่วนแสดงผลเป็น ILI9486 (ILI9486 Driver)
- ระบบควบคุมการสัมผัสเป็นแบบ Resistive
- รองรับการใช้กับบอร์ด Raspberry Pi ด้วยการเสียบบนบอร์ดโดยตรง
- ไดรเวอร์ใช้ได้กับ Raspbian, Ubuntu และ Kali Linux
- โมดูลควบคุมระบบจอสัมผัสใช้ XPT2046
- แสดงสีแบบ 16 บิต หรือ 65,535 สี
- อัตราส่วนการแสดงผล 8:5
การตั้งค่า TFT_eSPI
การเชื่อมต่อระหว่างขาของโมดูล LCD กับ ESP32 เป็นดังนี้
RPi 3.5″ TFT&Touch | ESP32 |
---|---|
5V | 5V |
GND | GND |
LCD_RS (ขา 18) | gpio15 |
LCD_SI/TP_SI (ขา 19) | gpio12 |
TP_SO (ขา 21) | gpio2 |
RST (ขา 22) | gpio13 |
LCD_SCK/TP_SCK (ขา 23) | gpio14 |
LCD_CS (ขา 24) | gpio5 |
TFT_CS (ขา 26) | gpio18 |
การตั้งค่าในไฟล์ส่วนหัวของ TFT_eSPI เป็นดังนี้
#define RPI_DISPLAY_TYPE
#define ILI9486_DRIVER
#define TOUCH_CS 18
#define TFT_MISO 2
#define TFT_MOSI 12
#define TFT_SCLK 14
#define TFT_CS 5
#define TFT_DC 15
#define TFT_RST 13
#define LOAD_GLCD
#define LOAD_FONT2
#define LOAD_FONT4
#define LOAD_FONT6
#define LOAD_FONT7
#define LOAD_FONT8
#define LOAD_GFXFF
#define SMOOTH_FONT
#define SPI_FREQUENCY 26000000
#define SPI_TOUCH_FREQUENCY 2500000
ตัวอย่างโปรแกรม
ตัวอย่างโปรแกรมทดสอบการแสดงผลและรับค่าของ TFT_eSPI ชื่อ Keypad_480x320 มีโค้ดดังนี้ และตัวอย่างหน้าจอผลลัพธ์ได้ดังภาพที่ 3
โค้ดโปรแกรม
/*
The TFT_eSPI library incorporates an Adafruit_GFX compatible
button handling class, this sketch is based on the Arduin-o-phone
example.
This example diplays a keypad where numbers can be entered and
send to the Serial Monitor window.
The sketch has been tested on the ESP8266 (which supports SPIFFS)
The minimum screen size is 320 x 240 as that is the keypad size.
TOUCH_CS and SPI_TOUCH_FREQUENCY must be defined in the User_Setup.h file
for the touch functions to do anything.
*/
// The SPIFFS (FLASH filing system) is used to hold touch screen
// calibration data
#include "FS.h"
#include <SPI.h>
#include <TFT_eSPI.h> // Hardware-specific library
TFT_eSPI tft = TFT_eSPI(); // Invoke custom library
// This is the file name used to store the calibration data
// You can change this to create new calibration files.
// The SPIFFS file name must start with "/".
#define CALIBRATION_FILE "/TouchCalData2"
// Set REPEAT_CAL to true instead of false to run calibration
// again, otherwise it will only be done once.
// Repeat calibration if you change the screen rotation.
#define REPEAT_CAL false
// Keypad start position, key sizes and spacing
#define KEY_X 40 // Centre of key
#define KEY_Y 96
#define KEY_W 62 // Width and height
#define KEY_H 30
#define KEY_SPACING_X 18 // X and Y gap
#define KEY_SPACING_Y 20
#define KEY_TEXTSIZE 1 // Font size multiplier
// Using two fonts since numbers are nice when bold
#define LABEL1_FONT &FreeSansOblique12pt7b // Key label font 1
#define LABEL2_FONT &FreeSansBold12pt7b // Key label font 2
// Numeric display box size and location
#define DISP_X 1
#define DISP_Y 10
#define DISP_W 238
#define DISP_H 50
#define DISP_TSIZE 3
#define DISP_TCOLOR TFT_CYAN
// Number length, buffer for storing it and character index
#define NUM_LEN 12
char numberBuffer[NUM_LEN + 1] = "";
uint8_t numberIndex = 0;
// We have a status line for messages
#define STATUS_X 120 // Centred on this
#define STATUS_Y 65
// Create 15 keys for the keypad
char keyLabel[15][5] = {"New", "Del", "Send", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "0", "#" };
uint16_t keyColor[15] = {TFT_RED, TFT_DARKGREY, TFT_DARKGREEN,
TFT_BLUE, TFT_BLUE, TFT_BLUE,
TFT_BLUE, TFT_BLUE, TFT_BLUE,
TFT_BLUE, TFT_BLUE, TFT_BLUE,
TFT_BLUE, TFT_BLUE, TFT_BLUE
};
// Invoke the TFT_eSPI button class and create all the button objects
TFT_eSPI_Button key[15];
//------------------------------------------------------------------------------------------
void setup() {
// Use serial port
Serial.begin(9600);
// Initialise the TFT screen
tft.init();
// Set the rotation before we calibrate
tft.setRotation(1);
// Calibrate the touch screen and retrieve the scaling factors
touch_calibrate();
// Clear the screen
tft.fillScreen(TFT_BLACK);
// Draw keypad background
tft.fillRect(0, 0, 240, 320, TFT_DARKGREY);
// Draw number display area and frame
tft.fillRect(DISP_X, DISP_Y, DISP_W, DISP_H, TFT_BLACK);
tft.drawRect(DISP_X, DISP_Y, DISP_W, DISP_H, TFT_WHITE);
// Draw keypad
drawKeypad();
}
//------------------------------------------------------------------------------------------
void loop(void) {
uint16_t t_x = 0, t_y = 0; // To store the touch coordinates
// Pressed will be set true is there is a valid touch on the screen
bool pressed = tft.getTouch(&t_x, &t_y);
// / Check if any key coordinate boxes contain the touch coordinates
for (uint8_t b = 0; b < 15; b++) {
if (pressed && key[b].contains(t_x, t_y)) {
key[b].press(true); // tell the button it is pressed
} else {
key[b].press(false); // tell the button it is NOT pressed
}
}
// Check if any key has changed state
for (uint8_t b = 0; b < 15; b++) {
if (b < 3) tft.setFreeFont(LABEL1_FONT);
else tft.setFreeFont(LABEL2_FONT);
if (key[b].justReleased()) key[b].drawButton(); // draw normal
if (key[b].justPressed()) {
key[b].drawButton(true); // draw invert
// if a numberpad button, append the relevant # to the numberBuffer
if (b >= 3) {
if (numberIndex < NUM_LEN) {
numberBuffer[numberIndex] = keyLabel[b][0];
numberIndex++;
numberBuffer[numberIndex] = 0; // zero terminate
}
status(""); // Clear the old status
}
// Del button, so delete last char
if (b == 1) {
numberBuffer[numberIndex] = 0;
if (numberIndex > 0) {
numberIndex--;
numberBuffer[numberIndex] = 0;//' ';
}
status(""); // Clear the old status
}
if (b == 2) {
status("Sent value to serial port");
Serial.println(numberBuffer);
}
// we dont really check that the text field makes sense
// just try to call
if (b == 0) {
status("Value cleared");
numberIndex = 0; // Reset index to 0
numberBuffer[numberIndex] = 0; // Place null in buffer
}
// Update the number display field
tft.setTextDatum(TL_DATUM); // Use top left corner as text coord datum
tft.setFreeFont(&FreeSans18pt7b); // Choose a nicefont that fits box
tft.setTextColor(DISP_TCOLOR); // Set the font colour
// Draw the string, the value returned is the width in pixels
int xwidth = tft.drawString(numberBuffer, DISP_X + 4, DISP_Y + 12);
// Now cover up the rest of the line up by drawing a black rectangle. No flicker this way
// but it will not work with italic or oblique fonts due to character overlap.
tft.fillRect(DISP_X + 4 + xwidth, DISP_Y + 1, DISP_W - xwidth - 5, DISP_H - 2, TFT_BLACK);
delay(10); // UI debouncing
}
}
}
//------------------------------------------------------------------------------------------
void drawKeypad()
{
// Draw the keys
for (uint8_t row = 0; row < 5; row++) {
for (uint8_t col = 0; col < 3; col++) {
uint8_t b = col + row * 3;
if (b < 3) tft.setFreeFont(LABEL1_FONT);
else tft.setFreeFont(LABEL2_FONT);
key[b].initButton(&tft, KEY_X + col * (KEY_W + KEY_SPACING_X),
KEY_Y + row * (KEY_H + KEY_SPACING_Y), // x, y, w, h, outline, fill, text
KEY_W, KEY_H, TFT_WHITE, keyColor[b], TFT_WHITE,
keyLabel[b], KEY_TEXTSIZE);
key[b].drawButton();
}
}
}
//------------------------------------------------------------------------------------------
void touch_calibrate()
{
uint16_t calData[5];
uint8_t calDataOK = 0;
// check file system exists
if (!SPIFFS.begin()) {
Serial.println("Formating file system");
SPIFFS.format();
SPIFFS.begin();
}
// check if calibration file exists and size is correct
if (SPIFFS.exists(CALIBRATION_FILE)) {
if (REPEAT_CAL)
{
// Delete if we want to re-calibrate
SPIFFS.remove(CALIBRATION_FILE);
}
else
{
File f = SPIFFS.open(CALIBRATION_FILE, "r");
if (f) {
if (f.readBytes((char *)calData, 14) == 14)
calDataOK = 1;
f.close();
}
}
}
if (calDataOK && !REPEAT_CAL) {
// calibration data valid
tft.setTouch(calData);
} else {
// data not valid so recalibrate
tft.fillScreen(TFT_BLACK);
tft.setCursor(20, 0);
tft.setTextFont(2);
tft.setTextSize(1);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.println("Touch corners as indicated");
tft.setTextFont(1);
tft.println();
if (REPEAT_CAL) {
tft.setTextColor(TFT_RED, TFT_BLACK);
tft.println("Set REPEAT_CAL to false to stop this running again!");
}
tft.calibrateTouch(calData, TFT_MAGENTA, TFT_BLACK, 15);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.println("Calibration complete!");
// store data
File f = SPIFFS.open(CALIBRATION_FILE, "w");
if (f) {
f.write((const unsigned char *)calData, 14);
f.close();
}
}
}
//------------------------------------------------------------------------------------------
// Print something in the mini status bar
void status(const char *msg) {
tft.setTextPadding(240);
//tft.setCursor(STATUS_X, STATUS_Y);
tft.setTextColor(TFT_WHITE, TFT_DARKGREY);
tft.setTextFont(0);
tft.setTextDatum(TC_DATUM);
tft.setTextSize(1);
tft.drawString(msg, STATUS_X, STATUS_Y);
}
//------------------------------------------------------------------------------------------
คำอธิบาย
จากโค้ดตัวอย่างจะมี 3 ส่วนหลัก คือ
- ส่วนของการตั้งค่าระบบจอสัมผัสผ่านทางฟังก์ชัน touch_calibrate() ที่ตรวจสอบว่า ได้มีการปรับค่ามาก่อนหรือไม่ด้วยการค้นหาไฟล์ CALIBRATION_FILE ในหน่วยความจำแฟลชผ่านทาง SPIFFS ถ้ายังไม่มี หรือต้องการทำใหม่จะเรียกการตั้งค่าจอสัมผัสผ่านทาง tft.calibrateTouch() ส่วนคำสั่งสำหรับการนำค่าที่ตั้งค่าไว้ไปใช้งานคือ tft.setTouch()
- ส่วนแสดงหน้าจอ
- แสดงแป้นด้วยการเรียกฟังก์ชันชื่อ drawKaypad() โดยแต่ละปุ่มใช้คลาส TFT_eSPI_Button ซึ่งเป็นคลาส UI (User Interface) ของ TFT_eSPI เพื่อแสดงปุ่มดังต่อไปนี้
- ปุ่ม New ในแถวแรก
- ปุ่ม Del ในแถวแรก
- ปุ่ม Send ในแถวแรก
- ปุ่ม 1 ในแถวที่ 2
- ปุ่ม 2 ในแถวที่ 2
- ปุ่ม 3 ในแถวที่ 2
- ปุ่ม 4 ในแถวที่ 3
- ปุ่ม 5 ในแถวที่ 3
- ปุ่ม 6 ในแถวที่ 3
- ปุ่ม 7 ในแถวที่ 4
- ปุ่ม 8 ในแถวที่ 4
- ปุ่ม 9 ในแถวที่ 4
- ปุ่ม . ในแถวล่างสุด
- ปุ่ม 0 ในแถวล่างสุด
- ปุ่ม # ในแถวล่างสุด
- ส่วนแสดงข้อความ
- ส่วน status อยู่ต่อจากส่วนแสดงข้อความ
- แสดงแป้นด้วยการเรียกฟังก์ชันชื่อ drawKaypad() โดยแต่ละปุ่มใช้คลาส TFT_eSPI_Button ซึ่งเป็นคลาส UI (User Interface) ของ TFT_eSPI เพื่อแสดงปุ่มดังต่อไปนี้
- ส่วนรับค่าจากส่วนจอสัมผัสและทำงาน
- อ่านค่าตำแหน่ง (x,y) จาก tft.getTouch()
- ถ้ากดถูกปุ่มตัวเลขจะนำตัวเลขไปแสดงผลในส่วนของข้อความด้านบน
- ถ้ากดปุ่ม New จะทำการล้างค่าส่วนของข้อความ
- ถ้ากด Del จะลบตัวเลขสุดท้ายที่กดออกจากส่วนของข้อความ
- ถ้ากด Send จะนำข้อมูลส่วนของข้อความที่กดไว้นั้นส่งไปยังพอร์ตสื่อสารอนุกรม
- ลดการเกิด bounce จากการสัมผัสด้วยการหน่วงเวลา 10 มิลลิวินาที
- การแสดงผลข้อความนั้นใช้การตั้งค่าต่อไปนี้
- กำหนด Datum เป็น TL_DATUM เป็นการแสดงข้อความจากซ้ายไปขวา
- เลือกใช้ฟอนต์แบบ FreeSans18pt7b
- สีตัวอักษรตามที่กำหนดในค่าคงที่ DISP_TCOLOR
- วาดข้อมูลผ่านทางฟังก์ชัน drawString() ด้วยการส่งค่าที่เก็บในตัวแปร numberBuffer
สรุป
จากการนำโมดูลแสดงผลของ Raspberry Pi มาใช้กับไมโครคอนโทรลเลอร์ ESP32 ทางเราพบว่า สามารถนำมาใช้ได้โดยไม่มีปัญหาและสามารถทำงานได้ทั้งส่วนแสดงผลและจอสัมผัส แต่การที่ในเอกสารไม่ได้ระบุตัวไดรเวอร์ของส่วนแสดงผลทำให้ทางทีมงานต้องแกะโค้ดของ LCD_Show และทดลองเปลี่ยนไดรเวอร์ที่ใช้ได้กับจอแสดงผลของ RPi จนพบว่าไดรเวอร์ที่ทำงานได้คือ ili9486 จากเหตุการณ์นี้จะพบว่า การเขียนโปรแกรมโดยไม่รู้จักกับอุปกรณ์ที่ใช้งานนั้นจะทำให้ไม่สามารถสั่งงานอุปกรณ์ได้ และยิ่งเข้าใจการทำงานของอุปกรณ์แต่ละชิ้นที่ใช้งานด้วยการอ่านเอกสารประกอบยิ่งทำให้สามารถเขียนโค้ดที่มีประสิทธิภาำได้มากขึ้น สุดท้ายนี้ ขอให้สนุกกับการเขียนโปรแกรมครับ
ท่านใดต้องการพดคุยสามารถคอมเมนท์ได้เลยครับ
(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-10-04, 2021-12-19