feat: 添加ESP32触摸眼睛动画及接线文档
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# ESP32-S3 与 2.8 英寸 SPI 触摸屏接线记录
|
||||
|
||||
更新:2026-09-14。
|
||||
|
||||
## 设备与当前状态
|
||||
|
||||
- 主控:ESP32-S3 N16R8 开发板,已焊排针。
|
||||
- 屏幕:2.8英寸、240×320、SPI TFT,带触摸和SD卡座。
|
||||
- 显示驱动:当前程序按ILI9341配置,用户提供的型号尚未通过芯片读取确认。
|
||||
- 触摸驱动:当前按XPT2046兼容控制器配置;具体芯片型号尚未独立确认。
|
||||
- 用户已反馈屏幕接好并点亮、触摸线已接好;动态眼睛程序将X/Y同时翻转后,用户确认点击与视线方向正确。
|
||||
- SD卡功能暂不连接、不初始化。
|
||||
|
||||
## 接线表
|
||||
|
||||
GPIO编号指开发板丝印数字,不是排针从一端数起的序号。插拔杜邦线前先断开USB供电。
|
||||
|
||||
### 显示部分
|
||||
|
||||
| 屏幕标注 | ESP32-S3 | 说明 |
|
||||
|---|---|---|
|
||||
| VCC | 3V3(此前试接方案) | 用户反馈已亮,但未单独确认最终电源接法;按实物补核,不据此改接5V |
|
||||
| GND | GND | 公共地 |
|
||||
| CS | GPIO10 | 显示片选,低有效 |
|
||||
| RESET | GPIO8 | 显示复位,由程序控制 |
|
||||
| DC | GPIO9 | 显示数据/命令选择 |
|
||||
| SDI〈MOSI〉 | GPIO11 | 主控向屏幕发送数据 |
|
||||
| SCK | GPIO12 | SPI时钟 |
|
||||
| LED | ESP32-S3 的 3V3(用户已确认) | 直接接3.3V,背光常亮;不接GPIO,当前程序不控制背光亮度或开关 |
|
||||
| SDO〈MISO〉 | 不接 | 暂不读取显示芯片寄存器或像素,不影响写入图像 |
|
||||
|
||||
### 触摸部分
|
||||
|
||||
| 屏幕标注 | ESP32-S3 | 说明 |
|
||||
|---|---|---|
|
||||
| T_CLK | GPIO12 | 与显示SCK共用 |
|
||||
| T_DIN | GPIO11 | 与显示SDI/MOSI共用 |
|
||||
| T_DO | GPIO13 | 触摸数据返回,即MISO |
|
||||
| T_CS | GPIO7 | 触摸独立片选,不能与显示CS合并 |
|
||||
| T_IRQ | GPIO6 | 触摸通知;现有程序以轮询读取,暂不依赖中断 |
|
||||
|
||||
### 共用连线示意
|
||||
|
||||
```text
|
||||
ESP32-S3 GPIO12 ─┬─ 屏幕 SCK
|
||||
└─ 触摸 T_CLK
|
||||
|
||||
ESP32-S3 GPIO11 ─┬─ 屏幕 SDI / MOSI
|
||||
└─ 触摸 T_DIN
|
||||
|
||||
ESP32-S3 GPIO13 ─── 触摸 T_DO
|
||||
ESP32-S3 GPIO10 ─── 屏幕 CS
|
||||
ESP32-S3 GPIO7 ─── 触摸 T_CS
|
||||
ESP32-S3 GPIO9 ─── 屏幕 DC
|
||||
ESP32-S3 GPIO8 ─── 屏幕 RESET
|
||||
ESP32-S3 GPIO6 ─── 触摸 T_IRQ
|
||||
ESP32-S3 GND ─── 屏幕 GND
|
||||
```
|
||||
|
||||
GPIO11、GPIO12各需要分接两根线,可通过面包板或分线连接。触摸T_DO与显示SDO是不同引脚,目前只连接T_DO。SD卡接口所有信号保持不接,测试时不插SD卡。
|
||||
|
||||
## 软件配置
|
||||
|
||||
Arduino IDE开发板:`ESP32S3 Dev Module`(安装Espressif Systems提供的esp32开发板包)。
|
||||
|
||||
库管理器安装:
|
||||
|
||||
- Adafruit ILI9341
|
||||
- Adafruit GFX Library,以及安装提示中的依赖
|
||||
- XPT2046_Touchscreen,作者Paul Stoffregen
|
||||
|
||||
与接线对应的关键配置:
|
||||
|
||||
```cpp
|
||||
Adafruit_ILI9341 tft(&SPI, 9, 10, 8); // DC, CS, RESET
|
||||
XPT2046_Touchscreen touch(7); // T_CS;采用轮询
|
||||
|
||||
// 初始化总线前,两个片选均设为输出高电平。
|
||||
pinMode(10, OUTPUT);
|
||||
digitalWrite(10, HIGH);
|
||||
pinMode(7, OUTPUT);
|
||||
digitalWrite(7, HIGH);
|
||||
pinMode(6, INPUT_PULLUP);
|
||||
|
||||
SPI.begin(12, 13, 11, 10); // SCK, MISO, MOSI, SS
|
||||
tft.begin(10000000); // 显示SPI初始10MHz
|
||||
tft.setRotation(1); // 横屏320×240
|
||||
touch.begin(SPI);
|
||||
touch.setRotation(1);
|
||||
```
|
||||
|
||||
## 测试程序
|
||||
|
||||
### 笑脸与触摸基础测试
|
||||
|
||||
文件:`esp32_smile_test/esp32_smile_test.ino`。
|
||||
|
||||
- 启动切换红、绿、蓝,再显示笑脸。
|
||||
- 按下屏幕时笑脸在黄色/青色之间切换,显示触摸次数。
|
||||
- 串口115200输出原始坐标、压力及IRQ电平;原始坐标不是屏幕像素坐标。
|
||||
|
||||
### 动态眼睛测试
|
||||
|
||||
文件:`esp32_touch_eyes/esp32_touch_eyes.ino`。
|
||||
|
||||
- 已取消开机三点校准,直接显示眼睛,使用代码顶部的默认触摸范围。
|
||||
- 点击时眨眼,视线朝向触摸位置;按住移动可跟随。
|
||||
- 最后一次有效触摸后3秒,视线平滑回到正前方。
|
||||
- 连续15秒没有触摸后,随机眨眼、向周围看,并间歇回正;触摸立即接管。当前“无人互动”仅按触摸计时,不代表摄像头检测到无人。
|
||||
- 根据用户实测上下左右均反向,已设TOUCH_FLIP_X=true、TOUCH_FLIP_Y=true,TOUCH_SWAP_XY仍为false;重新上传后用户确认方向正确。边缘误差调整TOUCH_MIN/MAX_X/Y。IDLE_AFTER_MS控制闲置等待时间。
|
||||
|
||||
两个程序分别打开、分别上传,不将两个ino复制到同一草图目录。动态眼睛程序已获用户实机反馈:触摸与视线方向正确;长期运行及完整功能验收尚未记录。
|
||||
|
||||
## 排查提示
|
||||
|
||||
- 背光亮不代表显示初始化成功;白屏时检查ILI9341型号、CS/DC/RESET和SPI连线。
|
||||
- `XPT2046_Touchscreen.h: No such file or directory`表示缺库,先安装触摸库。
|
||||
- 显示正常但触摸无响应:检查T_DO→GPIO13、T_CS→GPIO7,以及共用的T_CLK/T_DIN。
|
||||
- 触摸方向不对:调整动态眼睛程序顶部的轴交换/翻转和范围参数,更改屏幕旋转方向后需重新检查映射。
|
||||
- 当前不要将触摸信号接5V,也不要用显示GPIO给电机供电。
|
||||
|
||||
## 参考与适用边界
|
||||
|
||||
同类[MSP2807模块接口说明](https://www.lcdwiki.com/2.8inch_SPI_Module_ILI9341_SKU:MSP2807)与实物照片的引脚名称相符,但不能由外观证明具体板卡版本完全一致。用户已确认LED直接接ESP32-S3的3V3,背光常亮;VCC的最终接法和触摸芯片型号仍需单独核对,不能由LED接法推定。
|
||||
|
||||
本记录仅适用于当前2.8英寸模块,不能直接套用到此前的GC9A01小圆屏。
|
||||
@@ -0,0 +1,94 @@
|
||||
// ESP32-S3 + ILI9341 240x320 display test.
|
||||
// Install Adafruit ILI9341, Adafruit GFX Library and their dependencies.
|
||||
// Display: SCK=12, SDI/MOSI=11, CS=10, DC=9, RESET=8, GND=GND.
|
||||
// Touch: T_CLK=12, T_DIN=11, T_DO=13, T_CS=7, T_IRQ=6.
|
||||
// Install XPT2046_Touchscreen by Paul Stoffregen as well.
|
||||
// Display SDO and SD card are not connected. Backlight is externally powered.
|
||||
#include <SPI.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_ILI9341.h>
|
||||
#include <XPT2046_Touchscreen.h>
|
||||
|
||||
constexpr int TFT_SCK = 12;
|
||||
constexpr int TFT_MOSI = 11;
|
||||
constexpr int TFT_CS = 10;
|
||||
constexpr int TFT_DC = 9;
|
||||
constexpr int TFT_RST = 8;
|
||||
constexpr int TOUCH_MISO = 13;
|
||||
constexpr int TOUCH_CS = 7;
|
||||
constexpr int TOUCH_IRQ = 6;
|
||||
|
||||
Adafruit_ILI9341 tft(&SPI, TFT_DC, TFT_CS, TFT_RST);
|
||||
// Poll first to test SPI independently of the IRQ wire.
|
||||
XPT2046_Touchscreen touch(TOUCH_CS);
|
||||
bool wasPressed = false;
|
||||
bool alternateColor = false;
|
||||
uint32_t touchCount = 0;
|
||||
uint32_t lastReport = 0;
|
||||
|
||||
void drawFace(uint16_t color);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(300);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
pinMode(TOUCH_CS, OUTPUT);
|
||||
digitalWrite(TOUCH_CS, HIGH);
|
||||
pinMode(TOUCH_IRQ, INPUT_PULLUP);
|
||||
SPI.begin(TFT_SCK, TOUCH_MISO, TFT_MOSI, TFT_CS);
|
||||
tft.begin(10000000); // Start conservatively at 10 MHz for jumper wires.
|
||||
tft.setRotation(1); // Landscape: 320 x 240. Use 3 if upside down.
|
||||
touch.begin(SPI);
|
||||
touch.setRotation(1);
|
||||
|
||||
const uint16_t colors[] = {ILI9341_RED, ILI9341_GREEN, ILI9341_BLUE};
|
||||
for (uint16_t color : colors) {
|
||||
tft.fillScreen(color);
|
||||
delay(400);
|
||||
}
|
||||
|
||||
tft.fillScreen(ILI9341_NAVY);
|
||||
drawFace(ILI9341_YELLOW);
|
||||
Serial.println("Touch test ready. Raw coordinates are not calibrated pixels.");
|
||||
}
|
||||
|
||||
void drawFace(uint16_t color) {
|
||||
const int cx = tft.width() / 2;
|
||||
const int cy = 105;
|
||||
tft.fillCircle(cx, cy, 82, color);
|
||||
tft.fillCircle(cx - 28, cy - 22, 10, ILI9341_BLACK);
|
||||
tft.fillCircle(cx + 28, cy - 22, 10, ILI9341_BLACK);
|
||||
|
||||
// Parabolic smile: the center is lower than the corners.
|
||||
for (int x = -42; x <= 42; ++x) {
|
||||
int y = cy + 43 - (x * x) / 65;
|
||||
tft.fillCircle(cx + x, y, 3, ILI9341_BLACK);
|
||||
}
|
||||
|
||||
tft.fillRect(0, 199, tft.width(), 41, ILI9341_NAVY);
|
||||
tft.setTextColor(ILI9341_WHITE, ILI9341_NAVY);
|
||||
tft.setTextSize(2);
|
||||
tft.setCursor(16, 207);
|
||||
tft.print("Touches: ");
|
||||
tft.print(touchCount);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
bool pressed = touch.touched();
|
||||
if (pressed) {
|
||||
TS_Point point = touch.getPoint();
|
||||
if (!wasPressed) {
|
||||
++touchCount;
|
||||
alternateColor = !alternateColor;
|
||||
drawFace(alternateColor ? ILI9341_CYAN : ILI9341_YELLOW);
|
||||
}
|
||||
if (millis() - lastReport >= 150) {
|
||||
lastReport = millis();
|
||||
Serial.printf("raw x=%d y=%d pressure=%d IRQ=%d\n",
|
||||
point.x, point.y, point.z, digitalRead(TOUCH_IRQ));
|
||||
}
|
||||
}
|
||||
wasPressed = pressed;
|
||||
delay(20);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// ESP32-S3 N16R8 + ILI9341 + XPT2046-compatible touch.
|
||||
// Libraries: Adafruit ILI9341, Adafruit GFX Library, XPT2046_Touchscreen.
|
||||
// LCD: SCK=12 MOSI=11 CS=10 DC=9 RESET=8. LCD SDO disconnected.
|
||||
// Touch: T_CLK=12 T_DIN=11 T_DO=13 T_CS=7 T_IRQ=6 (polled).
|
||||
// SD disconnected. Keep the existing working power/backlight connections.
|
||||
// No startup calibration. Adjust touch mapping constants below if needed.
|
||||
#include <SPI.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_ILI9341.h>
|
||||
#include <XPT2046_Touchscreen.h>
|
||||
#include <math.h>
|
||||
|
||||
Adafruit_ILI9341 tft(&SPI, 9, 10, 8);
|
||||
XPT2046_Touchscreen touch(7);
|
||||
// Small off-screen canvas avoids visible erase/redraw flashes.
|
||||
GFXcanvas16 eyes(280, 112);
|
||||
// Approximate raw range after touch.setRotation(1), not calibrated pixels.
|
||||
constexpr int TOUCH_MIN_X = 200, TOUCH_MAX_X = 3900;
|
||||
constexpr int TOUCH_MIN_Y = 200, TOUCH_MAX_Y = 3900;
|
||||
constexpr bool TOUCH_SWAP_XY = false;
|
||||
constexpr bool TOUCH_FLIP_X = true, TOUCH_FLIP_Y = true;
|
||||
constexpr uint32_t IDLE_AFTER_MS = 15000;
|
||||
uint32_t idleBlinkAt = 0, idleLookAt = 0;
|
||||
bool idle = false, idleLookingAway = false;
|
||||
float gazeX = 0, gazeY = 0, targetX = 0, targetY = 0;
|
||||
uint32_t lastTouch = 0, blinkStart = 0, lastFrame = 0, lastDown = 0;
|
||||
bool looking = false, blinking = false, contact = false;
|
||||
int previousX = 999, previousY = 999, previousOpen = -1;
|
||||
|
||||
void renderEyes(int dx, int dy, int opening) {
|
||||
eyes.fillScreen(ILI9341_BLACK);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
int x = i == 0 ? 72 : 208;
|
||||
eyes.fillRoundRect(x-49, 12, 98, 88, 30, ILI9341_WHITE);
|
||||
eyes.fillCircle(x+dx, 56+dy, 25, ILI9341_CYAN);
|
||||
eyes.fillCircle(x+dx, 56+dy, 14, ILI9341_BLACK);
|
||||
eyes.fillCircle(x+dx-7, 49+dy, 5, ILI9341_WHITE);
|
||||
int cover = (100-opening)*44/100;
|
||||
if (cover > 0) {
|
||||
eyes.fillRect(x-50, 12, 100, cover, ILI9341_BLACK);
|
||||
eyes.fillRect(x-50, 100-cover, 100, cover, ILI9341_BLACK);
|
||||
}
|
||||
if (opening < 7) eyes.fillRoundRect(x-39, 54, 78, 4, 2, ILI9341_CYAN);
|
||||
}
|
||||
tft.drawRGBBitmap(20, 54, eyes.getBuffer(), 280, 112);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
pinMode(10, OUTPUT); digitalWrite(10, HIGH);
|
||||
pinMode(7, OUTPUT); digitalWrite(7, HIGH);
|
||||
pinMode(6, INPUT_PULLUP);
|
||||
SPI.begin(12, 13, 11, 10);
|
||||
tft.begin(10000000);
|
||||
tft.setRotation(1);
|
||||
touch.begin(SPI);
|
||||
touch.setRotation(1);
|
||||
if (!eyes.getBuffer()) {
|
||||
tft.fillScreen(ILI9341_RED);
|
||||
Serial.println("Eye canvas allocation failed.");
|
||||
while (true) delay(1000);
|
||||
}
|
||||
randomSeed(micros());
|
||||
tft.fillScreen(ILI9341_BLACK);
|
||||
tft.setTextColor(ILI9341_CYAN);
|
||||
tft.setTextSize(2);
|
||||
tft.setCursor(46, 207);
|
||||
tft.print("Touch to look!");
|
||||
lastFrame = lastTouch = millis();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
uint32_t now = millis();
|
||||
if (touch.touched()) {
|
||||
TS_Point p = touch.getPoint();
|
||||
float rawX = TOUCH_SWAP_XY ? p.y : p.x;
|
||||
float rawY = TOUCH_SWAP_XY ? p.x : p.y;
|
||||
float px = constrain((rawX-TOUCH_MIN_X)*319.0f/(TOUCH_MAX_X-TOUCH_MIN_X), 0.0f, 319.0f);
|
||||
if (TOUCH_FLIP_X) px = 319.0f-px;
|
||||
float py = constrain((rawY-TOUCH_MIN_Y)*239.0f/(TOUCH_MAX_Y-TOUCH_MIN_Y), 0.0f, 239.0f);
|
||||
if (TOUCH_FLIP_Y) py = 239.0f-py;
|
||||
idle = false;
|
||||
idleLookingAway = false;
|
||||
if (!contact) { blinking = true; blinkStart = now; }
|
||||
contact = true;
|
||||
lastDown = lastTouch = now;
|
||||
looking = true;
|
||||
targetX = (px-159.5f)/159.5f*20.0f;
|
||||
targetY = (py-119.5f)/119.5f*14.0f;
|
||||
} else if (contact && now-lastDown > 100) {
|
||||
contact = false; // Short release debounce prevents repeated blinks.
|
||||
}
|
||||
if (looking && now-lastTouch >= 3000) {
|
||||
looking = false;
|
||||
targetX = targetY = 0;
|
||||
}
|
||||
// "Unattended" currently means no touch, not camera-based presence detection.
|
||||
if (!contact && !looking && now-lastTouch >= IDLE_AFTER_MS) {
|
||||
if (!idle) {
|
||||
idle = true;
|
||||
idleLookingAway = false;
|
||||
idleBlinkAt = now + random(400, 1600);
|
||||
idleLookAt = now + random(900, 2200);
|
||||
}
|
||||
if (!blinking && int32_t(now-idleBlinkAt) >= 0) {
|
||||
blinking = true;
|
||||
blinkStart = now;
|
||||
idleBlinkAt = now + random(2500, 6500);
|
||||
}
|
||||
if (int32_t(now-idleLookAt) >= 0) {
|
||||
idleLookingAway = !idleLookingAway;
|
||||
if (idleLookingAway) {
|
||||
targetX = random(0, 2) ? random(9, 21) : -random(9, 21);
|
||||
targetY = random(-12, 13);
|
||||
idleLookAt = now + random(800, 2000);
|
||||
} else {
|
||||
targetX = targetY = 0;
|
||||
idleLookAt = now + random(1800, 4500);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (now-lastFrame < 33) { delay(2); return; }
|
||||
float dt = (now-lastFrame)/1000.0f;
|
||||
lastFrame = now;
|
||||
float blend = 1.0f-expf(-12.0f*dt);
|
||||
gazeX += (targetX-gazeX)*blend;
|
||||
gazeY += (targetY-gazeY)*blend;
|
||||
int opening = 100;
|
||||
if (blinking) {
|
||||
uint32_t elapsed = now-blinkStart;
|
||||
if (elapsed < 100) opening = 100-int(elapsed);
|
||||
else if (elapsed < 170) opening = 0;
|
||||
else if (elapsed < 330) opening = int((elapsed-170)*100/160);
|
||||
else blinking = false;
|
||||
}
|
||||
int dx = lroundf(gazeX), dy = lroundf(gazeY);
|
||||
if (dx != previousX || dy != previousY || opening != previousOpen) {
|
||||
renderEyes(dx, dy, opening);
|
||||
previousX = dx; previousY = dy; previousOpen = opening;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user