
1. 项目概述ESP32-S3-LCD-1.28 是什么如果你最近在玩物联网或者嵌入式开发大概率已经听过ESP32-S3这颗芯片的大名了。它比经典的ESP32性能更强外设更丰富尤其是USB OTG和LCD接口的支持让它成为了很多创意项目的首选。而“ESP32-S3-LCD-1.28”这个组合通常指的是一类集成了ESP32-S3主控、1.28英寸圆形LCD显示屏的开发板或核心模块。这玩意儿本质上是一个高度集成的硬件平台让你不用再头疼地自己去连接MCU和屏幕的排线开箱即用直接就能在屏幕上画点、显示文字、甚至播放动画。它解决的痛点非常明确对于想快速实现带屏交互设备的开发者、创客甚至学生来说自己从零开始选型屏幕、设计驱动电路、焊接FPC排线不仅门槛高还容易踩坑。ESP32-S3-LCD-1.28这类产品把ESP32-S3的计算能力、Wi-Fi/蓝牙双模连接能力和一块高分辨率的圆形LCD屏打包在一起提供了一个完整的“大脑眼睛”解决方案。你拿到手连上电脑写几行代码就能让屏幕亮起来显示传感器数据、制作一个智能手表界面、或者构建一个带图形界面的网络终端。无论是做产品原型、参加比赛还是个人DIY一个桌面小摆件它都能极大地缩短开发周期。2. 核心硬件架构与接口解析要玩转ESP32-S3-LCD-1.28首先得把它“拆开”看明白。虽然你拿到手的可能是一个巴掌大的完整板子但理解其内部的连接方式和关键接口是后续编程和调试的基础。2.1 ESP32-S3 核心性能与特性ESP32-S3是乐鑫在ESP32系列中的一款双核Xtensa® 32位LX7处理器主频高达240MHz。相比ESP32它的升级是全方位的更强的计算能力双核240MHz支持浮点运算单元(FPU)处理图形和复杂算法更轻松。更丰富的外设这是驱动LCD的关键。它原生集成了LCD接口控制器支持并行的RGB、I8080、SPI等多种接口模式可以直接输出数字视频信号无需额外的显示驱动芯片如ILI9341的驱动IC这大大简化了硬件设计和提高了刷新率。更大的内存通常配备8MB PSRAM对于缓存整屏的图像数据framebuffer至关重要。1.28英寸圆形屏分辨率常见为240x240使用16位色深RGB565时一帧图像需要240*240*2 115,200字节约112.5KB。8MB的PSRAM可以轻松缓存多帧实现流畅的动画。USB OTG内置USB OTG意味着它可以作为USB主机连接U盘、键盘也可以作为USB设备被电脑识别实现串口调试和程序下载“一线搞定”告别了传统的USB转串口芯片。无线连接支持2.4GHz Wi-Fi (802.11 b/g/n) 和 Bluetooth 5 (LE)为带屏设备添加网络功能提供了可能。2.2 1.28英寸LCD屏幕的关键参数这块圆形屏幕是项目的视觉核心几个参数你必须了然于胸分辨率最常见的是240x240。这是一个正方形的画布被物理切割成了圆形。编程时你操作的是一个240x240的矩形缓冲区但屏幕只显示其中的圆形区域。计算坐标时需要注意边界否则画到角落的图形是看不见的。接口类型绝大多数这类屏使用SPI接口。原因很简单节省IO口。RGB并行接口虽然速度快但需要十几根数据线对于引脚资源紧张的小型开发板不友好。SPI通常只需要4根线SCLK, MOSI, MISO, CS加上DC数据/命令和RST复位等控制线总计6-7根线即可驱动。驱动芯片屏幕背后有一颗驱动IC比如ST7789或GC9A01。这两款是1.28寸圆形SPI屏的绝对主流。你需要知道具体型号因为初始化序列和部分指令可能有细微差别。通常卖家会提供资料。色深通常支持16位RGB56565K色或18位RGB666。在SPI模式下为了传输效率我们普遍使用RGB565。每个像素用2字节16位表示高5位是红色中间6位是绿色低5位是蓝色。2.3 板载资源与扩展接口除了核心的MCU和屏幕板子上通常还会集成一些贴心设计电源管理一颗高效的LDO稳压芯片将USB的5V转换为3.3V为整个系统供电。电池管理很多板子预留了锂电池接口比如JST PH 2.0和充电电路如TP4056方便制作便携设备。传感器为了增加卖点有些板子会集成六轴传感器加速度计陀螺仪如MPU6050或电容触摸芯片如GT911用于实现计步、手势识别或触摸交互。扩展IO虽然很多引脚已经被屏幕占用但设计者通常会引出剩余的GPIO通过排母或焊盘形式提供用于连接外设如按键、LED、I2C传感器等。Boot与Reset按键这是必备的。Boot键用于进入固件下载模式Reset键用于重启系统。理解这些硬件构成后你在编程时就能清楚地知道图形数据从ESP32-S3的SPI主机控制器发出通过几根导线传输到屏幕的驱动ICIC再控制液晶分子偏转最终形成图像。SPI的时钟频率SCLK直接决定了刷屏速度是优化性能的关键参数。3. 软件开发环境搭建与驱动基础硬件在手下一步就是让代码跑起来。这里有几个主流的开发路径各有优劣。3.1 开发框架选型Arduino vs. ESP-IDF这是你首先要做的选择。Arduino IDE / PlatformIO优点生态庞大库丰富上手极其简单。对于驱动LCD屏有现成的库如TFT_eSPI经过高度优化并且通常卖家会提供适配好的配置文件。你几乎不需要关心底层SPI的初始化调用tft.drawCircle()、tft.print()这样的高级函数就能画画写字。缺点封装层次高对底层控制力较弱。当你想极致优化SPI速率、使用DMA传输、或者需要深度整合ESP-IDF的其他组件如Wi-Fi配网库、音频解码库时可能会遇到障碍。适合人群初学者、快速原型开发、对性能要求不极致的项目。ESP-IDF (乐鑫官方物联网开发框架)优点官方原生支持能调用所有硬件特性包括LCD控制器、DMA、PSRAM等。性能最优灵活性最高。你可以直接使用ESP-IDF自带的lvgl一个流行的开源图形库移植示例或者自己从寄存器层面编写驱动。缺点学习曲线陡峭需要熟悉C语言、CMake构建系统、以及ESP-IDF的组件架构。调试相对复杂。适合人群追求极限性能、产品级开发、需要深度定制的开发者。对于大多数使用“ESP32-S3-LCD-1.28”板子的玩家我建议从PlatformIO Arduino框架开始。它平衡了易用性和功能性并且PlatformIO的库管理和项目结构比原生Arduino IDE更清晰。3.2 关键驱动库的配置与使用以TFT_eSPI为例TFT_eSPI是Arduino生态下最强大的SPI屏驱动库之一。正确配置是成功的第一步。安装库在PlatformIO的platformio.ini中添加lib_deps bodmer/TFT_eSPI或者从Arduino库管理器中搜索安装。配置用户设置文件这是最关键的一步。找到库文件夹下的User_Setup.h或User_Setup_Select.h文件。通常更推荐使用User_Setup_Select.h因为它允许你通过取消注释来选择对应的屏幕驱动芯片和引脚定义。打开User_Setup_Select.h找到类似于#include User_Setups/Setup25_ST7789.h的行。你需要根据你的屏幕驱动ICST7789或GC9A01和引脚连接选择一个最接近的Setup文件或者自己创建一个。查看你的开发板原理图或卖家资料找到屏幕SPI引脚与ESP32-S3 GPIO的对应关系。例如#define TFT_CS 5 // 片选 #define TFT_DC 4 // 数据/命令 #define TFT_RST 3 // 复位如果接在ESP32的RST上可以写-1 #define TFT_MOSI 6 // SPI数据线 #define TFT_SCLK 7 // SPI时钟线 #define TFT_MISO -1 // 如果屏幕不支持读回可以设为-1 #define TFT_BL 8 // 背光控制引脚如果有还需要定义屏幕尺寸和驱动芯片#define ST7789_DRIVER #define TFT_WIDTH 240 #define TFT_HEIGHT 240 // 如果是圆形屏可能需要启用偏移定义 // #define TFT_INVERSION_ON初始化与测试编写一个简单的测试程序。#include TFT_eSPI.h TFT_eSPI tft TFT_eSPI(); void setup() { tft.init(); tft.setRotation(0); // 根据屏幕物理方向调整0-3 tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(2); tft.setCursor(20, 100); tft.println(Hello, World!); } void loop() { // 画一个随机颜色的圆 tft.fillCircle(random(tft.width()), random(tft.height()), 10, random(0xFFFF)); delay(100); }烧录程序如果屏幕亮起并显示文字和动画圆点恭喜你驱动成功了注意很多商家会提供已经配置好的User_Setup.h文件。直接用它替换库中的文件是最快的方法但建议你还是理解一下里面各项定义的含义便于后续自己调整。3.3 利用ESP32-S3的硬件特性优化显示当基础驱动跑通后可以进一步挖掘ESP32-S3的潜力来提升体验。提高SPI时钟频率在User_Setup.h中可以定义SPI的频率。ESP32-S3的SPI主机在80MHz下工作稳定。将#define SPI_FREQUENCY 40000000提高到80000000可以显著提升刷屏速度。但要注意过高的频率可能导致屏幕显示异常或通信不稳定需要实测。使用双缓冲与PSRAM对于动态画面直接画在屏幕上单缓冲会导致闪烁。可以利用ESP32-S3的大容量PSRAM创建一块或两块离线缓冲区framebuffer。单PSRAM缓冲在PSRAM中创建一块和屏幕一样大的缓冲区。所有绘图操作先在这个缓冲区里完成然后一次性通过SPI DMA传输到屏幕。这消除了闪烁。双PSRAM缓冲创建两个缓冲区A和B。当CPU在缓冲区A中绘制下一帧时DMA正在将缓冲区B的内容传输到屏幕。绘制完成后交换缓冲区。这是实现流畅动画的常用技术。TFT_eSPI库通过tft.createSprite()函数支持创建基于PSRAM的精灵Sprite本质上就是一块缓冲区。启用DMA传输DMA直接存储器访问允许数据在PSRAM和SPI外设之间直接传输无需CPU干预。在传输大量像素数据时能极大解放CPU。TFT_eSPI库在ESP32平台上通常默认启用了DMA。你可以在初始化后调用tft.initDMA()来尝试启用如果支持。4. 图形界面GUI库的集成与应用实战当你能在屏幕上画点画线后自然会想做出更漂亮的界面比如按钮、列表、图表。这时候就需要引入GUI库。4.1 LVGL轻量级通用图形库的首选LVGL是一个用C编写的开源嵌入式图形库资源消耗小功能强大支持动画、抗锯齿、多种控件按钮、滑块、图表、列表等并且有强大的设计工具——SquareLine Studio。它是ESP32-S3这类带屏项目的绝配。在PlatformIO (Arduino) 中集成LVGL安装库在platformio.ini中添加lib_deps lvgl/lvgl。配置显示驱动接口LVGL需要你提供几个底层函数告诉它如何初始化屏幕、刷新指定区域。TFT_eSPI库的作者Bodmer提供了一个完美的桥梁库lvgl_arduino但更常见的做法是手动适配。你需要实现一个disp_flush函数这个函数接收一个矩形区域和像素数据数组你的任务就是将这些数据写到屏幕的对应位置。通常你可以直接调用tft.pushImageDMA()或tft.setAddrWindow()tft.pushPixels()来实现。初始化与任务循环#include lvgl.h #include TFT_eSPI.h TFT_eSPI tft TFT_eSPI(); static lv_disp_draw_buf_t draw_buf; // 定义绘制缓冲区 static lv_color_t buf[TFT_WIDTH * 10]; // 声明一个缓冲区行缓存 void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) { uint32_t w (area-x2 - area-x1 1); uint32_t h (area-y2 - area-y1 1); tft.startWrite(); tft.setAddrWindow(area-x1, area-y1, w, h); tft.pushPixels((uint16_t *)color_p, w * h); tft.endWrite(); lv_disp_flush_ready(disp); // 告诉LVGL刷新完成 } void setup() { Serial.begin(115200); tft.begin(); tft.setRotation(0); lv_init(); // 初始化LVGL lv_disp_draw_buf_init(draw_buf, buf, NULL, TFT_WIDTH * 10); // 初始化绘制缓冲区 static lv_disp_drv_t disp_drv; // 声明一个显示驱动 lv_disp_drv_init(disp_drv); // 初始化显示驱动 disp_drv.hor_res TFT_WIDTH; // 设置水平分辨率 disp_drv.ver_res TFT_HEIGHT; // 设置垂直分辨率 disp_drv.flush_cb my_disp_flush; // 设置刷新回调函数 disp_drv.draw_buf draw_buf; // 设置绘制缓冲区 lv_disp_drv_register(disp_drv); // 注册显示驱动 // 创建一个简单的按钮 lv_obj_t *btn lv_btn_create(lv_scr_act()); lv_obj_center(btn); lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL); lv_obj_t *label lv_label_create(btn); lv_label_set_text(label, Click Me!); } void loop() { lv_timer_handler(); // 必须定期调用LVGL的任务处理器 delay(5); }这段代码将LVGL和TFT_eSPI连接起来并创建了一个居中的按钮。4.2 使用SquareLine Studio进行可视化设计手写代码创建复杂界面非常繁琐。SquareLine Studio是一个桌面软件允许你通过拖拽控件来设计界面然后生成LVGL的C代码。在SquareLine Studio中创建项目选择显示分辨率240x240然后从控件栏拖入按钮、标签、滑块等调整位置和样式。设置事件比如为按钮的“点击”事件绑定一个回调函数名。导出项目选择“导出” - “创建LVGL项目单文件”。它会生成一个ui.c和ui.h文件。将生成的文件集成到你的PlatformIO项目中。把ui.c和ui.h拷贝到src目录下。在你的主程序中包含ui.h并在setup()中调用ui_init()函数。之前在设计器中绑定的事件回调函数需要在你的代码中实现。#include ui.h void setup() { // ... 初始化LVGL和显示 ... ui_init(); // 初始化UI } void loop() { lv_timer_handler(); delay(5); } // 实现你在SquareLine Studio中为按钮“btn1”设置的“点击”事件回调 void btn1_event_handler(lv_event_t *e) { lv_event_code_t code lv_event_get_code(e); if(code LV_EVENT_CLICKED) { Serial.println(Button 1 clicked!); // 可以在这里控制其他UI元素例如改变标签文字 lv_label_set_text(ui_Label1, Clicked!); } }这样你就实现了界面设计与逻辑代码的分离开发效率倍增。4.3 制作一个简单的智能手表表盘结合LVGL和ESP32-S3的网络功能我们可以做一个能显示时间和天气的简易表盘。UI设计在SquareLine Studio中设计表盘背景、时间数字标签lv_label、日期标签和天气图标标签。时间同步使用Wi-Fi连接NTP服务器获取网络时间。#include WiFi.h #include time.h const char* ntpServer pool.ntp.org; const long gmtOffset_sec 8 * 3600; // 东八区 const int daylightOffset_sec 0; void syncTime() { configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); struct tm timeinfo; if(!getLocalTime(timeinfo)){ Serial.println(Failed to obtain time); return; } // 格式化时间并更新UI标签 char timeStr[20]; strftime(timeStr, sizeof(timeStr), %H:%M:%S, timeinfo); lv_label_set_text(ui_TimeLabel, timeStr); }天气信息通过Wi-Fi连接心知天气、和风天气等免费API解析JSON数据获取天气状况和温度然后更新到对应的标签并根据天气代码切换图标。定时刷新在loop()函数中或者使用LVGL的定时器lv_timer_create每隔1秒更新时间每隔10分钟更新一次天气。低功耗考虑如果是电池供电可以在没有交互时降低屏幕亮度通过PWM控制背光引脚TFT_BL甚至让ESP32-S3进入轻睡眠模式通过RTC定时器或外部触摸事件唤醒。这个项目综合了硬件驱动、GUI设计、网络通信和电源管理是检验ESP32-S3-LCD-1.28板子能力的一个很好的练手项目。5. 高级主题与性能深度优化当项目变得复杂对流畅度要求更高时就需要触及一些高级话题。5.1 使用ESP-IDF原生LCD驱动与LVGL如果你想获得终极性能和控制权抛弃Arduino框架直接使用ESP-IDF是正道。ESP-IDF提供了esp_lcd组件它统一了不同LCD面板的驱动模型并原生支持与LVGL集成。创建ESP-IDF项目通过idf.py menuconfig启用Component config - LCD Panel并选择对应的驱动芯片如ST7789。编写驱动层代码你需要提供SPI总线初始化、屏幕初始化序列通常是一系列寄存器写入命令等。幸运的是乐鑫和LVGL社区提供了大量示例。配置LVGL在ESP-IDF环境中通常通过lvgl_esp32_drivers这个组件来桥接它已经帮你写好了esp_lcd到LVGL的disp_flush适配代码配置起来比在Arduino中手动写更规范。启用DMA和PSRAM在menuconfig中确保PSRAM已启用并在初始化esp_lcd时配置DMA通道。ESP-IDF的SPI DMA驱动非常成熟可以稳定地在80MHz时钟下进行全帧传输CPU占用率极低。这种方式下整个图形系统的底层控制权都在你手里可以精细地调整中断优先级、DMA缓冲区大小、SPI传输模式等实现最流畅的体验。5.2 处理圆形屏幕的显示边界对于圆形屏直接绘制矩形内容会导致四个角的内容被浪费甚至可能显示异常。有几种处理方式软件裁剪在LVGL中你可以为屏幕对象设置一个遮罩mask或者重写disp_flush函数在发送像素数据前判断每个像素点是否在圆形区域内(x-120)^2 (y-120)^2 120^2如果不在则跳过。但这会增加CPU开销。利用驱动芯片的局部刷新像ST7789这样的驱动IC支持设置行和列的起始、结束地址CASET,RASET。你可以只发送圆形包围盒内的像素数据但驱动IC仍然会作用于整个矩形区域屏幕物理上不显示的区域可能表现为黑色或随机颜色。更常见的做法是将背景固定为黑色然后所有绘图都在这个黑色圆形画布内进行这样即使数据送到了角落因为背景是黑的也看不出来。这是最简单有效的方法。设计适配圆形的UI这是最根本的解决方案。在SquareLine Studio中设计界面时就有意将所有元素控制在中心区域避免使用直角边框多采用圆形、圆角矩形和径向布局。LVGL的很多控件都支持圆角半径设置可以很好地适配圆形屏幕。5.3 多任务与内存管理一个复杂的应用可能同时需要刷新GUI、运行Wi-Fi任务、处理传感器数据、播放音频等。这就需要用到FreeRTOSESP-IDF和Arduino核心都基于它。创建独立任务将不同的功能模块放在不同的FreeRTOS任务中。例如创建一个高优先级的lv_timer_handler任务专门负责调用lv_timer_handler()创建一个低优先级的网络任务负责HTTP请求。void lvgl_task(void *pvParameter) { while(1) { lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(5)); // LVGL推荐至少5ms延迟 } } void setup() { // ... 初始化 ... xTaskCreate(lvgl_task, LVGL, 4096, NULL, 2, NULL); // 创建任务 }线程安全LVGL本身不是线程安全的。这意味着所有LVGL的API调用如lv_label_set_text必须从同一个任务中发起通常就是运行lv_timer_handler的那个任务。如果你从网络任务中收到了新数据要更新UI不能直接调用LVGL函数而应该通过队列Queue、任务通知Task Notification或者LVGL的事件系统将消息发送到LVGL任务中去处理。PSRAM使用注意事项虽然PSRAM容量大但速度比内部RAM慢。频繁访问的变量如循环计数器、状态标志应放在内部RAM。大的、一次性的缓冲区如图像数组、framebuffer可以放在PSRAM。在Arduino中可以使用ps_malloc()来在PSRAM中分配内存或者使用DMAMEM属性如果支持。6. 常见问题排查与实战心得玩转这块板子的路上坑是少不了的。下面是我和社区里朋友们总结的一些典型问题和解决方法。6.1 屏幕白屏、花屏或不显示这是最常见的问题排查思路如下电源问题首先用万用表测量板子的3.3V电源是否稳定。屏幕驱动需要足够的电流USB线质量差或电脑USB口供电不足可能导致白屏。尝试换一根短的、质量好的USB线或者连接到一个有源USB集线器上。引脚配置错误99%的问题出在这里反复核对User_Setup.h中的引脚定义是否与你的板子原理图完全一致。特别是TFT_DC数据/命令引脚接错必然白屏。TFT_RST如果接在了ESP32的EN引脚上可以设为-1库会使用软件复位。驱动芯片型号错误确认你的屏幕是ST7789还是GC9A01。初始化序列不同。尝试在User_Setup.h中切换ST7789_DRIVER和GC9A01_DRIVER的定义。SPI频率过高如果你将SPI_FREQUENCY设得过高如80MHz而屏幕或PCB布线质量一般可能导致信号失真。尝试降低到40MHz或27MHz测试。初始化顺序问题确保在tft.init()之前背光控制引脚TFT_BL已设置为输出并拉高如果背光是高电平点亮。有些屏幕对复位时序有要求可以尝试在init()前手动拉低再拉高RST引脚并增加延时。6.2 显示内容错位或颜色异常旋转设置错误tft.setRotation(0/1/2/3)定义了屏幕的物理方向。如果文字上下颠倒或左右颠倒调整这个参数。颜色格式不匹配确保库中设置的色深RGB565与屏幕驱动IC支持的模式一致。有些屏幕初始化命令需要设置颜色格式。可以尝试在User_Setup.h中开启或关闭TFT_INVERSION_ON这会影响颜色的阴阳极性。坐标超出圆形区域在圆形屏上画矩形时如果矩形范围超出了物理显示区域超出的部分可能显示为乱码或黑色。确保你的绘图逻辑考虑了圆形边界。6.3 刷新速度慢动画卡顿检查SPI时钟确认SPI_FREQUENCY已设置为允许的最高值如80MHz。使用逻辑分析仪或示波器测量SCLK引脚看实际频率是否达到设定值。是否使用了DMA确认TFT_eSPI库的DMA功能已启用。查看库的日志输出启用调试或者尝试调用tft.initDMA()。绘图API效率避免使用大量单点绘制的API如drawPixel。尽量使用批量绘制API如pushImage、fillRect、drawLine等。LVGL的disp_flush函数本身是区域刷新效率已经很高。CPU被其他任务阻塞如果同时运行了复杂的网络请求或传感器滤波算法可能会阻塞主循环导致lv_timer_handler得不到及时执行。考虑将耗时任务放入独立的FreeRTOS任务并赋予合适的优先级。帧缓冲区策略对于全屏动画务必使用双缓冲。在LVGL中确保lv_disp_draw_buf_init分配的缓冲区足够大。如果使用行缓冲如TFT_WIDTH * 10LVGL需要多次调用flush_cb来绘制一帧如果使用全屏缓冲TFT_WIDTH * TFT_HEIGHT则一次flush_cb就能完成但需要大量PSRAM。根据你的内存和性能需求权衡。6.4 Wi-Fi/蓝牙与显示干扰ESP32-S3的无线功能和高速SPI都工作在高频可能存在干扰。电源噪声Wi-Fi发射时电流骤增可能引起电源电压波动导致屏幕闪烁或复位。在电源引脚附近增加一个100uF的钽电容或电解电容可以有效平滑电压。SPI线与天线干扰尽量让连接屏幕的排线远离板载PCB天线区域。如果使用外接天线效果会更好。分时复用在密集刷屏如播放视频时暂时关闭Wi-Fi/蓝牙在网络通信时降低屏幕刷新频率或暂停复杂动画。这需要你在代码中根据场景进行状态管理。6.5 触摸屏如有校准与使用如果板子带了电容触摸如GT911通常使用I2C接口。安装触摸库例如Adafruit_FT6206_Library针对FT系列或寻找GT911的专用库。校准电容屏一般不需要像电阻屏那样进行多点校准但可能需要读取原始值并映射到屏幕坐标。库的示例程序通常包含映射函数。与LVGL集成LVGL需要你提供一个touch_read回调函数。在这个函数里你需要读取触摸芯片的数据将坐标转换为屏幕坐标并填充到lv_indev_data_t结构体中然后返回是否有触摸事件。void my_touch_read(lv_indev_drv_t *drv, lv_indev_data_t *data) { uint16_t touchX, touchY; bool touched touch.read(touchX, touchY); // 调用你的触摸库函数 if(touched) { >