1.14英寸LCD屏驱动与项目实战:从ST7789到LVGUI开发

📅 2026/8/1 12:13:59
1.14英寸LCD屏驱动与项目实战:从ST7789到LVGUI开发
1. 项目概述一块1.14英寸LCD屏能玩出什么花样最近在捣鼓一个小玩意儿一块1.14英寸的LCD模块。别看它尺寸小分辨率通常有135x240或者240x135像素密度相当不错色彩表现也远超早期的单色屏。这种小屏在智能穿戴、微型工控HMI、DIY小设备上特别吃香。我拿到这块屏的第一反应是它绝不只是个简单的“显示器”而是一个能承载丰富交互和视觉信息的微型窗口。无论是做一个桌面天气站、一个迷你游戏机还是智能家居的副屏它都能胜任。这篇文章我就以一个硬件爱好者的视角带你从零开始彻底玩转这块1.14英寸LCD模块从驱动原理、硬件连接到软件编程再到几个实用的项目实战把每一个细节和踩过的坑都讲清楚。2. 核心驱动方案与硬件接口深度解析2.1 主流驱动芯片ST7789与GC9A01的选型之争市面上绝大多数1.14英寸LCD模块核心驱动芯片无外乎ST7789和GC9A01这两大主流。选型不是拍脑袋得看你的具体需求。ST7789可以说是老牌劲旅资料极其丰富社区支持度最高。它支持最高320x240的分辨率驱动我们这块135x240的屏绰绰有余。其通信接口主要是SPI有些变种也支持8080并行接口但模块上为了节省引脚普遍采用SPI。它的优点是稳定、成熟几乎所有的单片机平台Arduino, ESP32, STM32, Raspberry Pi Pico都有现成的库上手速度飞快。如果你追求快速验证想法或者项目对开发速度要求高ST7789是首选。GC9A01则是后起之秀性能更强。它原生支持最高240x240的分辨率同样通过SPI接口驱动。相比于ST7789GC9A01的刷新率理论上更高色彩显示也更鲜艳一些尤其是在显示渐变色彩和动画时观感更流畅。但相对的其资料和库的完善度稍逊于ST7789可能需要你多花点时间调试。如果你的项目对动画流畅度有较高要求或者想“折腾”一下新芯片GC9A01是个不错的选择。实操心得对于绝大多数入门和中级项目直接选择ST7789驱动的模块。你节省下来的大量调试时间远比那一点点性能提升有价值。我手头这块屏就是ST7789V3版本后续所有演示都将基于它。2.2 四线SPI硬件连接详解与“省引脚”技巧模块背面通常会印有引脚定义。以常见的ST7789模块为例核心引脚如下VCC电源正极通常接3.3V。注意绝对不要接5V会烧屏GND电源地。SCLSPI时钟线。SDASPI数据线MOSI。RES复位引脚低电平有效。DC数据/命令选择引脚。这是SPI驱动LCD的关键高电平时SDA上传的是要显示的像素数据低电平时传的是命令如设置地址窗口、旋转方向等。CS片选引脚低电平选中该设备。如果你的SPI总线上只有这块屏可以将其永久接地拉低。BLK背光控制接高电平常亮也可以通过PWM引脚控制亮度。连接ESP32开发板的典型接法如下表所示LCD引脚ESP32 GPIO引脚功能说明VCC3.3V电源GNDGND地SCLGPIO 18SPI时钟SDAGPIO 23SPI数据RESGPIO 4复位DCGPIO 2数据/命令选择CSGND片选简化接法BLKGPIO 15 (或 3.3V)背光控制这里有一个重要的“省引脚”技巧CS引脚可以直接接地。当你的SPI总线上只有这一个设备时无需用MCU的GPIO去控制它这样可以节省出一个宝贵的IO口。同理如果你不需要动态调节背光BLK引脚也可以直接接3.3V让背光常亮。这样下来驱动这块屏最少只需要5个引脚VCC, GND, SCL, SDA, DC, RES其中RES在初始化后也可以复用为普通IO但通常不建议因为硬复位在某些异常情况下是最后的保障。注意事项SPI的时钟线SCL和数据线SDA请务必连接到MCU硬件SPI接口对应的引脚上而不是任意GPIO。使用硬件SPI比软件模拟SPI速度有数量级的提升对于刷屏、动画至关重要。以ESP32为例其VSPI默认引脚是GPIO 18(SCK), 23(MOSI), 19(MISO)我们正好用上了前两个。3. 软件驱动库选择与底层初始化探秘3.1 Arduino平台下的TFT_eSPI库配置实战在Arduino生态中TFT_eSPI库是驱动这类SPI屏的“瑞士军刀”功能强大且高效。但它需要正确配置才能工作。很多新手卡在这一步其实就几个关键点。首先通过Arduino库管理器安装“TFT_eSPI”。安装后不要急着写代码先去找到库的配置文件。路径通常是你的Arduino安装目录/libraries/TFT_eSPI/User_Setup.h。你需要编辑这个文件。你需要根据你的模块和接线修改或确认以下宏定义选择驱动芯片找到#define ST7789_DRIVER这一行确保它被取消注释即前面没有//。定义屏幕尺寸设置#define TFT_WIDTH 135和#define TFT_HEIGHT 240。注意有些模块是240x135即旋转了90度这里根据物理像素排列来定通常135是宽度240是高度。配置引脚这是核心。找到用户引脚定义区域根据之前的接线表进行设置#define TFT_CS -1 // 因为CS接了GND这里填-1表示不使用 #define TFT_DC 2 // 对应GPIO 2 #define TFT_RST 4 // 对应GPIO 4 #define TFT_MOSI 23 // SPI数据线 #define TFT_SCLK 18 // SPI时钟线配置SPI接口确保#define SPI_FREQUENCY 40000000这一行存在。这是SPI通信频率40MHz对于ST7789是安全且高效的值。可选配置如果你使用了MISO引脚通常不需要也需要定义TFT_MISO。背光控制引脚TFT_BL如果接了GPIO 15就在这里定义。保存文件后一个最简单的测试代码如下#include TFT_eSPI.h TFT_eSPI tft TFT_eSPI(); void setup() { tft.init(); tft.setRotation(1); // 根据你的模块安装方向调整旋转0-3分别代表0°, 90°, 180°, 270° tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.drawString(Hello, 1.14\ LCD!, 10, 50, 2); } void loop() {}上传代码如果屏幕亮起并显示文字恭喜你驱动成功了3.2 初始化流程背后的原理不止是调用init()为什么tft.init()这么神奇它背后干了几件关键事理解这些有助于你排查复杂问题。硬件复位库会先控制TFT_RST引脚产生一个低电平脉冲通常持续几十毫秒让驱动芯片恢复到已知的初始状态。这是一个可靠的启动方式。发送初始化序列通过SPI向ST7789芯片发送一系列预先定义好的命令和参数。这些命令包括设置接口像素格式如16位RGB565、设置显示方向、打开显示、设置亮度等。这些序列是芯片厂商规定的库已经帮你写好了。设置内存地址窗口初始化后库会设定一个默认的“绘图区域”告诉芯片接下来接收的像素数据应该填充到屏幕的哪个矩形范围内。后续的drawPixel、fillScreen等操作底层都会先更新这个地址窗口。清屏很多库在初始化后会默认发送清屏命令将显存填充为黑色。避坑技巧如果你的屏幕初始化后花屏、颜色错乱或者完全不亮别慌。按顺序检查电源是否为3.3V接线是否牢固SPI引脚是否接错User_Setup.h配置是否与实物完全一致最常出错的点是TFT_WIDTH和TFT_HEIGHT设反或者setRotation的值不对导致绘图坐标混乱。可以尝试不同的旋转值0-3来匹配物理方向。4. 图形与文字绘制高级技巧4.1 高效图形绘制缓冲、区域更新与动画优化直接操作屏幕像素如tft.drawPixel在循环中使用是非常低效的会导致屏幕闪烁。对于动态内容有更优的策略。策略一局部更新。只重绘屏幕上发生变化的部分而不是整个屏幕。例如一个不断更新的数字时钟你只需要在下一分钟到来时用背景色覆盖旧的数字区域再绘制新的数字即可无需刷新整个屏幕。// 假设在(x, y)位置显示一个数字旧数字存在oldNum变量中 tft.setTextColor(TFT_BLACK, TFT_BLUE); // 文字色黑背景色蓝与时钟背景一致 tft.drawNumber(oldNum, x, y, font); // ... 计算新数字 newNum ... tft.drawNumber(newNum, x, y, font); oldNum newNum;策略二使用Sprite精灵/缓冲。TFT_eSPI库提供了TFT_eSprite类它允许你在MCU的内存中创建一个离屏的图形缓冲区。你所有的绘图操作画图、写字、画线都先在这个内存缓冲区中进行完成后再一次性将整个缓冲区的内容“推”到屏幕上。这种方法完全消除了闪烁是实现复杂动画和界面的利器。TFT_eSprite spr TFT_eSprite(tft); // 创建精灵关联到tft对象 // 在setup中创建缓冲区大小135x240颜色深度16位RGB565 spr.createSprite(135, 240); void loop() { spr.fillSprite(TFT_BLACK); // 用黑色填充整个缓冲区 spr.setTextColor(TFT_GREEN); spr.drawString(FPS: String(millis()), 10, 10, 2); // ... 在spr上执行其他所有绘图操作 ... spr.pushSprite(0, 0); // 将缓冲区内容一次性推送到屏幕的(0,0)位置 }使用Sprite的代价是消耗更多的RAM。一个135x240的16位色深缓冲区需要135 * 240 * 2 bytes 64,800 bytes约63KB。这对于只有520KB RAM的ESP32来说是可以接受的但对于只有2KB RAM的Arduino Uno就是不可能的。因此在资源紧张的平台必须严格使用局部更新策略。4.2 字体管理与多语言显示解决方案库内置的字体是点阵字体比较有限。要显示中文或更美观的英文字体需要导入外部字体文件。准备字体文件使用工具如TFT_eSPI库自带的“Processing”脚本或在线转换工具将.ttf或.otf字体文件转换成C语言数组形式的头文件。这个过程会指定一个字号和字符集范围例如仅包含GB2312常用汉字。导入并使用字体将生成的头文件如myFont.h放入你的项目目录。在Arduino代码中引入它#include myFont.h // 你的字体头文件 void setup() { tft.loadFont(myFont); // 加载字体到内存 tft.setTextColor(TFT_WHITE); tft.drawString(你好世界, 20, 50); // 现在可以显示中文了 tft.unloadFont(); // 使用完后卸载字体以释放内存 }注意字体文件通常很大会占用大量程序存储空间Flash和运行时内存RAM。务必只包含你需要的字符并谨慎使用。实操心得对于固定不变的界面文字如标签可以事先在电脑上用图片编辑软件做好位图然后使用Img2Code之类的工具转换成数组通过tft.pushImage()函数直接显示。这种方法不占用额外的RAM显示速度也快适合LOGO、图标和静态界面元素。5. 实战项目一打造微型网络天气站5.1 系统架构与数据流设计这个项目将综合运用网络通信、JSON解析和LCD显示。我们使用ESP32作为主控因为它内置Wi-Fi并且性能足够。数据源选择免费的开放天气API例如和风天气、OpenWeatherMap。系统工作流如下连接Wi-FiESP32启动后连接预设的家庭Wi-Fi网络。定时请求每10分钟或更长避免频繁请求被API限制向天气API发送一个HTTP GET请求。解析数据服务器返回JSON格式的天气数据温度、湿度、天气状况图标代码、城市名等。我们需要使用ArduinoJson库来解析这个复杂的JSON字符串。渲染显示根据解析出的数据在LCD上绘制界面。包括城市名称、当前温度、天气状况图标用图形或文字描述、湿度、更新时间。低功耗考虑如果使用电池供电可以在两次更新之间让ESP32进入深度睡眠Deep Sleep仅由定时器唤醒这将极大延长续航。5.2 代码实现关键点与API处理首先安装必要的库TFT_eSPI(已安装)ArduinoJsonWiFi(ESP32内置)HTTPClient(ESP32内置)。核心代码结构示例#include WiFi.h #include HTTPClient.h #include ArduinoJson.h #include TFT_eSPI.h TFT_eSPI tft TFT_eSPI(); const char* ssid 你的WiFi; const char* password 你的密码; const String apiKey 你的天气API密钥; const String city Beijing; void setup() { Serial.begin(115200); tft.init(); tft.setRotation(1); tft.fillScreen(TFT_BLACK); connectToWiFi(); displayWeather(); } void loop() { // 每30分钟更新一次 delay(30 * 60 * 1000); displayWeather(); } void displayWeather() { if (WiFi.status() ! WL_CONNECTED) { connectToWiFi(); } String url http://api.openweathermap.org/data/2.5/weather?q city appid apiKey unitsmetric; HTTPClient http; http.begin(url); int httpCode http.GET(); if (httpCode 200) { String payload http.getString(); DynamicJsonDocument doc(1024); deserializeJson(doc, payload); float temp doc[main][temp]; int humidity doc[main][humidity]; String weather doc[weather][0][main]; const char* icon doc[weather][0][icon]; // 例如 01d 代表晴天白天 // 清屏并绘制UI框架 tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_CYAN, TFT_BLACK); tft.drawString(city, 10, 10, 4); // 显示温度 tft.setTextColor(TFT_YELLOW, TFT_BLACK); tft.drawString(String(temp, 1) C, 10, 60, 4); // 显示天气状况和图标这里用文字代替图标 tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.drawString(weather, 10, 110, 2); tft.drawString(Hum: String(humidity) %, 10, 140, 2); // 可以在这里根据icon代码调用函数绘制简单的位图图标 drawWeatherIcon(icon, 100, 50); } else { tft.setTextColor(TFT_RED, TFT_BLACK); tft.drawString(API Error, 10, 100, 2); } http.end(); }drawWeatherIcon函数需要你预先根据icon代码如”01d”, “02n”, “10d”等准备好一些小尺寸的位图数组然后使用tft.pushImage()绘制在指定位置。注意事项免费天气API通常有调用频率限制如每分钟/每天多少次。务必遵守规则在代码中设置合理的延迟如30分钟更新一次避免IP被拉黑。此外API密钥是敏感信息如果代码要公开切勿硬编码可以考虑通过Wi-Fi管理器在首次启动时让用户配置。6. 实战项目二基于LVGL的嵌入式GUI开发6.1 为何选择LVGL轻量级GUI框架的优势当你的项目界面元素变得复杂有按钮、滑块、列表、图表等交互需求时手动管理绘图和事件会变得异常痛苦。这时就需要一个嵌入式GUI框架而LVGL是当前开源生态中最强大、最活跃的选择之一。LVGL的优势在于资源消耗可控高度可配置可以从只有几十KB Flash和几KB RAM的微控制器到性能更强的ESP32、STM32上运行。功能极其丰富提供按钮、标签、滑块、下拉列表、图表、动画等数十种控件Widgets。强大的工具链有官方的模拟器可以在PC上开发和调试UI还有图形化的UI设计器SquareLine Studio可以通过拖拽设计界面然后生成C代码。活跃的社区问题容易找到解决方案并且有大量的示例。6.2 在1.14寸屏上移植LVGL的详细步骤在ESP32 TFT_eSPI的环境下移植LVGL已经是一条非常成熟的路径。以下是关键步骤安装LVGL库在Arduino库管理中搜索“lvgl”并安装。通常会自动安装一个核心库和一个针对ESP32的驱动示例库如lvgl_arduino。配置显示驱动LVGL需要你提供一个“显示驱动回调函数”告诉它如何向屏幕绘制像素。幸运的是TFT_eSPI库的作者已经为我们写好了适配层。你通常可以在lvgl_arduino库的示例中找到名为TFT_eSPI的示例。这个示例里已经包含了将TFT_eSPI的tft对象与LVGL连接起来的代码。配置输入设备如果你的屏幕是触控屏电阻或电容式还需要配置输入驱动。1.14寸模块通常不带触控所以我们先做非触控版本。初始化与任务调度LVGL需要一个周期性的“心跳”任务lv_tick_inc()来管理内部计时和动画以及一个“任务处理”任务lv_task_handler()来执行渲染和事件处理。这两个函数需要你在loop()中定期调用。#include lvgl.h #include TFT_eSPI.h #include lv_examples.h TFT_eSPI tft TFT_eSPI(); void setup() { Serial.begin(115200); lv_init(); // 初始化LVGL核心 tft.begin(); // 初始化显示屏 tft.setRotation(1); // 初始化显示缓冲区。LVGL支持双缓冲以消除闪烁。 static lv_color_t buf1[DISP_BUF_SIZE]; static lv_color_t buf2[DISP_BUF_SIZE]; static lv_disp_buf_t disp_buf; lv_disp_buf_init(disp_buf, buf1, buf2, DISP_BUF_SIZE); // 创建显示驱动 lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.buffer disp_buf; disp_drv.flush_cb my_disp_flush; // 你需要实现这个函数内部调用tft.pushImage() lv_disp_drv_register(disp_drv); // 创建一个简单的界面 lv_obj_t * label lv_label_create(lv_scr_act(), NULL); lv_label_set_text(label, Hello, LVGL!); lv_obj_align(label, NULL, LV_ALIGN_CENTER, 0, 0); } void loop() { lv_task_handler(); // 让LVGL处理任务 delay(5); // 短暂延迟避免阻塞 }你需要实现my_disp_flush函数其作用是将LVGL渲染好的图形缓冲区内容搬运到你的物理屏幕上。在TFT_eSPI的适配示例中这个函数通常已经写好核心就是调用tft.pushImage()。内存与性能调优在lv_conf.h配置文件中通常需要从库目录复制到项目根目录并重命名你可以精细调整LVGLLV_MEM_SIZE设置LVGL动态内存池的大小对于ESP32设置32KB-64KB是个安全的起点。LV_COLOR_DEPTH设置为16以匹配TFT_eSPI的RGB565格式。关闭你不需要的控件和特效以节省空间。避坑技巧移植LVGL最大的挑战是内存。如果出现花屏、崩溃首先检查LV_MEM_SIZE是否足够以及显示缓冲区buf1和buf2的大小DISP_BUF_SIZE。这个大小是屏幕面积的一部分例如设置成屏幕的1/10135*240/10。双缓冲会占用双倍的这个内存。务必在ESP32的串口监视器中开启核心调试Core Debug Level查看内存分配情况。7. 功耗管理与硬件优化实战7.1 背光控制与动态刷新率调节屏幕的背光是耗电大户。1.14寸LCD的背光LED在满亮度下工作电流可能达到20-30mA。优化方法如下PWM调光将背光控制引脚BLK连接到MCU的一个支持PWM输出的GPIO上如ESP32的GPIO 15。在代码中你可以根据环境光传感器如BH1750的读数或者简单地根据时间夜间调暗动态调整PWM占空比从而改变亮度。const int backlightPin 15; const int pwmChannel 0; // 使用LEDC通道0 const int pwmFrequency 5000; // 5kHz const int pwmResolution 8; // 8位分辨率亮度范围0-255 void setup() { ledcSetup(pwmChannel, pwmFrequency, pwmResolution); ledcAttachPin(backlightPin, pwmChannel); setBacklight(128); // 设置为50%亮度 } void setBacklight(int brightness) { ledcWrite(pwmChannel, brightness); }动态刷新率LVGL或你自己的绘图循环默认会以尽可能快的速度刷新。如果界面是静态的如显示一个不变的读数持续刷新是在浪费电。你可以实现一个逻辑当界面内容稳定后大幅降低lv_task_handler()的调用频率或者直接停止定时刷新仅在数据更新时重绘一次屏幕。7.2 ESP32深度睡眠与定时唤醒策略对于电池供电的天气站这类项目让ESP32在大部分时间睡觉是终极省电方案。ESP32的深度睡眠模式Deep Sleep下电流可以降到10μA级别。实现思路在setup()中完成屏幕初始化、连接Wi-Fi、获取并显示天气数据。显示完成后将需要保持的数据如下次唤醒的时间戳存入RTC内存RTC_DATA_ATTR修饰的变量因为深度睡眠后普通RAM会丢失。配置一个唤醒源。最常用的是定时器唤醒。例如设置睡眠30分钟。void enterDeepSleep() { Serial.println(Entering deep sleep for 30 minutes...); // 关闭屏幕背光 digitalWrite(backlightPin, LOW); // 也可以在这里调用tft.sleep()命令让LCD模块进入睡眠模式如果驱动支持 esp_sleep_enable_timer_wakeup(30 * 60 * 1000000ULL); // 微秒单位 esp_deep_sleep_start(); // 进入深度睡眠程序停止运行 }30分钟后ESP32会自动复位重启从setup()开始重新执行。你可以在setup()开头通过esp_sleep_get_wakeup_cause()判断是否是定时器唤醒然后恢复之前的数据。注意事项深度睡眠时GPIO状态会保持。务必确保在睡眠前将连接LCD的引脚特别是背光设置为低电平或高阻态避免LCD模块继续耗电。同时检查你的LCD模块驱动芯片是否支持软件睡眠命令如ST7789的SLPIN命令如果支持在睡眠前发送该命令可以进一步降低模块自身功耗。8. 常见问题排查与硬件调试实录8.1 上电无显示或花屏的硬件级排查当屏幕不亮或显示异常时请遵循以下检查清单现象可能原因排查步骤屏幕完全不亮1. 电源问题2. 背光未开启3. 复位失败1. 用万用表测量VCC和GND之间电压是否为稳定的3.3V。2. 测量BLK引脚电压如果接了GPIO确认程序已将其拉高如果直接接3.3V检查连线。3. 用示波器或逻辑分析仪检查RESET引脚上电时是否有低电平脉冲约100ms。没有则检查接线或代码。屏幕亮但白屏/全彩1. 初始化序列未成功发送2. SPI通信失败3. 引脚定义错误1. 确认tft.init()被调用且无错误返回。2. 检查SCL和SDA接线确认未接反。用逻辑分析仪抓取SPI波形看是否有数据发出。3. 反复核对User_Setup.h中的引脚定义与实物连接一个都不能错。显示错位、撕裂、部分区域异常1. 屏幕宽高设置错误2. 显存地址窗口设置错误3. SPI时钟频率过高1. 确认TFT_WIDTH和TFT_HEIGHT与屏幕物理分辨率一致。2. 检查setRotation()的值尝试0,1,2,3四个值看哪个正确。3. 尝试降低SPI_FREQUENCY如降到20MHz过高的频率可能导致数据传输出错尤其是导线较长时。颜色完全不对颜色格式设置错误ST7789通常使用RGB565格式16位色。检查库的初始化序列中是否发送了正确的像素格式命令0x3A。在TFT_eSPI中这由USER_SETUP_INFO里的驱动定义决定通常无需手动修改。8.2 软件层面的典型BUG与解决思路内存不足导致崩溃症状程序运行一段时间后死机、重启或LVGL界面操作卡顿后崩溃。排查在Arduino IDE中查看编译后的“全局变量使用了X字节”和“剩余内存”信息。使用ESP.getFreeHeap()在串口打印剩余堆内存。如果内存持续下降可能存在内存泄漏。解决减少全局变量及时释放临时变量如String对象使用PROGMEM将常量数据存到Flash。在LVGL中减小显示缓冲区和LV_MEM_SIZE。SPI通信冲突症状当连接了其他SPI设备如SD卡时LCD显示异常。排查SPI总线是共享的必须通过CS片选引脚来分时复用。确保在操作不同设备时正确控制其CS引脚。解决为每个SPI设备分配独立的CS引脚并在访问前拉低对应CS访问后拉高。如果LCD的CS已接地则需要改变硬件连接为其分配一个可控的GPIO。刷新缓慢动画卡顿症状更新画面时感觉慢有拖影。排查确认使用了硬件SPI。检查SPI时钟频率是否已设置为最高支持值如40MHz。避免在循环中频繁使用fillScreen()全屏刷新。解决使用Sprite进行离屏渲染后一次性推送。在LVGL中确保lv_task_handler()的调用间隔稳定且足够快在loop()中不加延迟或只加很短延迟如5ms。折腾这块小屏的过程就像是在方寸之间构建一个完整的世界。从最底层的引脚连接到上层的GUI应用每一个环节都有值得琢磨的细节。我最深的体会是硬件项目成功的关键八成在于细致和耐心耐心检查每一根连线耐心阅读芯片数据手册的每一句描述耐心调试每一行代码。当那些像素点按照你的意愿亮起组成有意义的图形和文字时那种成就感是纯粹的。希望这篇长文能帮你扫清路上的障碍更顺畅地实现你的创意。如果遇到文中没覆盖的怪问题不妨回到电源、时钟、数据线这三大基础点上再看看往往能有意外发现。