1. 项目缘起:为什么选择这块1.3英寸LCD屏?
最近在捣鼓一个需要显示信息的小玩意儿,比如做个桌面天气站、智能家居的迷你状态屏,或者给树莓派Zero做个便携终端。找了一圈屏幕,发现1.3英寸这个尺寸的LCD模块特别有意思。它不像0.96寸那么局促,显示几行文字都费劲;也不像2寸以上的屏幕那样,对于很多微型项目来说又显得有些“大材小用”,功耗和体积都上去了。1.3寸,差不多就是一张SD卡的大小,分辨率常见的有240x240或者128x64,刚好能在方寸之间清晰地显示图标、文字甚至简单的图形界面,可以说是“麻雀虽小,五脏俱全”的典型代表。
市面上这类模块通常基于ST7789、ST7735或者SSD1306这类驱动芯片,通过SPI或I2C接口与主控(像Arduino、树莓派Pico、ESP32)通信。SPI接口的刷新速度快,适合动态显示;I2C接口则节省引脚,布线简单。这次我手头拿到的是一个240x240分辨率、采用ST7789V驱动芯片的IPS屏,色彩和可视角度都相当不错。接下来的内容,我就以这块屏为例,从硬件连接到软件驱动,再到实际应用中的一些“坑”和技巧,完整地走一遍流程。无论你是刚入门想点个灯(屏),还是有一定基础想优化显示效果,相信都能找到有用的信息。
2. 硬件探秘:模块引脚与驱动芯片解析
拿到模块,第一步永远是看引脚和芯片。别看它小,里面的门道可不少。我这款模块的背面清晰地印着“ST7789V”的字样,这是一颗非常流行的TFT LCD驱动IC。
2.1 核心引脚功能拆解
模块通常会引出8到10个左右的引脚。以最常见的8引脚SPI接口为例,其定义和功能如下:
| 引脚标号 | 引脚名称 | 功能描述 | 连接注意事项 |
|---|---|---|---|
| VCC | 电源正极 | 模块工作电压,常见为3.3V或5V | 务必确认模块电压!很多1.3寸屏是3.3V逻辑,接5V可能烧毁。 |
| GND | 电源地 | 公共接地 | 必须与主控共地。 |
| SCL/SCK | 时钟线 | SPI通信时钟信号 | 接主控SPI时钟引脚。 |
| SDA/SDI/MOSI | 数据线 | SPI主机输出从机输入数据线 | 接主控SPI的MOSI引脚。 |
| RES/RST | 复位 | 硬件复位,低电平有效 | 可接主控GPIO,用于初始硬复位,改善稳定性。 |
| DC/RS/A0 | 数据/命令选择 | 高电平写数据,低电平写命令 | 关键引脚,接主控GPIO,用于区分发送的是指令还是显存数据。 |
| CS | 片选 | 低电平选中该SPI从设备 | 接主控GPIO,如果总线上只有一个设备,可接地常选。 |
| BLK | 背光控制 | 控制背光开关,高电平点亮 | 接主控GPIO或直接接VCC(常亮)。PWM控制可实现亮度调节。 |
注意:不同厂家、不同批次的模块,引脚顺序和名称可能有细微差异。最可靠的方法是找到对应的规格书(Datasheet)或厂家提供的示例代码确认。我遇到过“DC”脚被标成“RS”或“A0”的情况,功能一样,但接线时心里要清楚。
2.2 驱动芯片ST7789V浅析
为什么是ST7789V?因为它支持最高240x320的分辨率,内置了显存(GRAM),我们通过SPI发送的像素数据就是写入这片GRAM,然后芯片会自动驱动屏幕扫描显示。它支持16位(RGB565)或18位色彩,我们常用的就是RGB565格式,即一个像素用2个字节(16位)表示:高5位红色,中间6位绿色,低5位蓝色。这种格式在色彩丰富度和数据量之间取得了很好的平衡。
芯片的初始化过程,其实就是通过SPI接口,向它发送一系列特定的配置命令(Command)和参数(Data)。这些命令序列通常由屏幕厂家提供,用于设置扫描方向、色彩模式、睡眠模式退出等。理解这个过程很重要,因为当你遇到屏幕显示方向不对、颜色反相、或者有残影时,很可能就是初始化序列中的某个参数需要调整。
3. 软件驱动:从零编写与库函数调用的抉择
硬件接好,3.3V供电,背光点亮,但屏幕还是一片空白。接下来就是让屏幕“活”过来的软件部分。这里通常有两条路:纯手工打造(自己写底层SPI驱动和命令序列)和站在巨人肩上(使用现有的开源图形库)。
3.1 方案一:手动“点屏” - 理解底层通信
对于学习而言,我强烈建议至少尝试一次手动初始化。这能让你深刻理解硬件是如何被驱动的。以Arduino平台为例,不依赖特定库,核心步骤包括:
- 引脚初始化:将
CS、DC、RST、BLK设置为输出模式,并给出正确的初始电平(例如CS拉低选中,RST拉高释放复位)。 - 实现基础SPI发送函数:需要两个函数,一个用于发送命令(
DC置低),一个用于发送数据(DC置高)。void writeCommand(uint8_t cmd) { digitalWrite(DC_PIN, LOW); // DC脚拉低,表示接下来是命令 digitalWrite(CS_PIN, LOW); // 片选拉低,开始通信 SPI.transfer(cmd); // 通过SPI发送命令字节 digitalWrite(CS_PIN, HIGH);// 片选拉高,结束本次传输 } void writeData(uint8_t data) { digitalWrite(DC_PIN, HIGH); // DC脚拉高,表示接下来是数据 digitalWrite(CS_PIN, LOW); SPI.transfer(data); digitalWrite(CS_PIN, HIGH); } - 发送初始化序列:这是一长串固定的命令和数据,用于唤醒屏幕、设置参数。这段序列通常很长,可以从厂家例程或开源库的初始化函数里复制。关键命令包括:
SWRESET:软件复位。SLPOUT:退出睡眠模式。COLMOD:设置色彩接口格式,例如0x55代表16位RGB565。MADCTL:设置内存访问控制,这个命令至关重要,它控制了显示的方向(旋转0°、90°、180°、270°)和RGB顺序。DISPON:打开显示。
- 绘制像素:初始化完成后,就可以在指定坐标画点了。这需要先通过命令设置绘图窗口(
CASET和RASET),然后连续发送像素数据(RGB565格式的两个字节)。void drawPixel(int16_t x, int16_t y, uint16_t color) { setAddrWindow(x, y, 1, 1); // 设置窗口为单个像素点 writeData(color >> 8); // 发送颜色高字节 writeData(color & 0xFF); // 发送颜色低字节 }
这个过程能让你对“屏幕驱动”有一个最根本的认识。但显然,画个矩形、写个文字都要自己算坐标、循环发数据,效率太低,不适合实际项目。
3.2 方案二:借力开源库 - Adafruit GFX/ST7789
实际项目中,我们几乎都会使用成熟的图形库。在Arduino和CircuitPython生态中,Adafruit GFX Library是事实上的标准。它提供了一套统一的API(如drawLine,fillRect,setCursor,print),底层则针对不同驱动芯片(如ST7789, ST7735, ILI9341)有相应的派生库(如Adafruit_ST7789)。
使用库的步骤就简单多了:
- 在Arduino IDE库管理中搜索并安装
Adafruit GFX Library和Adafruit ST7789。 - 包含头文件并创建对象。
#include <Adafruit_GFX.h> #include <Adafruit_ST7789.h> #define TFT_CS 10 #define TFT_DC 8 #define TFT_RST 9 // 若模块无RST引脚,可接-1并采用软件复位 Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST); - 在
setup()中初始化。void setup() { tft.init(240, 240); // 初始化屏幕,指定宽高 tft.setRotation(2); // 设置旋转方向(0,1,2,3对应0°,90°,180°,270°) tft.fillScreen(ST77XX_BLACK); // 清屏为黑色 tft.setTextColor(ST77XX_WHITE); tft.setTextSize(2); tft.setCursor(20, 50); tft.println("Hello, World!"); }
库函数帮你封装了所有底层细节,你只需要关心“画什么”和“画在哪”。setRotation()函数内部其实就是通过修改之前提到的MADCTL命令参数来实现的,非常方便。
实操心得:即使使用库,也建议浏览一下库源码中的初始化函数。当你遇到屏幕显示异常(如颜色不对、镜像)时,可以尝试修改初始化序列里
MADCTL参数的值。例如,我的这块屏默认旋转方向是横屏,但我需要竖屏显示,除了调用setRotation(3),有时还需要在初始化时传入特定的参数来适配,如tft.init(240, 240, SPI_MODE2),这个SPI_MODE2可能对应不同的SPI相位和极性,需要根据屏幕规格调整。
4. 性能优化与常见问题排查
屏幕点亮显示“Hello World”只是第一步。在实际应用中,尤其是用ESP32、树莓派Pico这类MCU做复杂UI或动画时,性能瓶颈和奇怪的问题就会浮现。
4.1 SPI时钟速度与双缓冲
刷新率是体验的关键。SPI的时钟速度(SCK频率)直接决定了数据灌入屏幕的速度。在Arduino上,可以使用SPI.beginTransaction(SPISettings(频率, 字节序, 模式))来提升速度。对于ST7789,通常可以跑到40MHz甚至更高(取决于模块PCB质量和线长)。但要注意,过高的频率可能导致数据错误,屏幕出现雪花点或错位。
对于动态内容,频繁的全屏刷新(fillScreen)会导致严重的闪烁。一个高级技巧是使用双缓冲或局部刷新。双缓冲意味着在MCU的内存中开辟一块和屏幕分辨率一样大的“虚拟屏幕”(Frame Buffer),所有的绘图操作都在这块内存中进行,完成一帧后,再通过一次高效的DMA(直接内存访问)传输,将整块内存数据“搬运”到屏幕的GRAM中。这能彻底消除闪烁,但会消耗大量RAM(2402402字节=115200字节,对于只有几百KB RAM的MCU是巨大负担)。
更实用的方法是局部刷新:只更新屏幕上发生变化的那部分区域。例如,在显示一个不断变化的数字时,只清除数字所在的矩形区域,然后重写新数字,而不是刷新整个屏幕。Adafruit GFX库的setAddrWindow函数就是为局部刷新服务的。
4.2 那些年我踩过的“坑”与解决方案
屏幕白屏或花屏:
- 检查电源:首先用万用表确认VCC电压是否为模块所需的3.3V。电流是否足够?背光点亮时峰值电流可能超过100mA,确保你的电源或LDO能提供。
- 检查接线:SCK、MOSI是否接反?
DC和CS脚是否接对?最容易被忽略的是GND,务必确保屏幕和主控之间有可靠的地线连接。 - 降低SPI速度:如果高速下花屏,尝试将SPI时钟频率降到10MHz或更低,看是否恢复正常,以排除信号完整性问题。
显示方向错误或颜色异常:
- 调整
MADCTL:这是元凶。setRotation()函数可能不适用于所有屏幕的初始化参数。你需要直接修改底层驱动库(如Adafruit_ST7789.cpp)中的初始化序列,找到MADCTL对应的数据值进行试验。常见的参数:0x00(横屏RGB),0xC0(横屏BGR),0x60/0xA0(竖屏)。BGR和RGB决定了红蓝通道是否交换。 - 检查色彩模式:确认初始化命令
COLMOD设置的是否是0x55(16位色)。
- 调整
显示有残影或拖影:
- 复位时序:确保上电后,给
RST脚一个足够长的低电平脉冲(通常>10ms)再进行初始化。有些库的begin()函数可能没做好这一点。 - 关闭省电模式:有些屏幕初始化序列包含省电命令,如果没正确退出,会导致对比度低、残影。确保
SLPOUT命令已发送。 - VCOM电压:残影有时与驱动IC的VCOM电压设置有关,这通常在初始化序列中通过特定命令调整。可以尝试搜索你屏幕型号的“初始化代码优化”。
- 复位时序:确保上电后,给
文字显示不清晰或断线:
- 字体问题:Adafruit GFX内置的字体是点阵字体,放大后会有锯齿。可以尝试使用更平滑的字体库,如
Adafruit-GFX-Library配合Fonts/目录下的其他字体,或者使用矢量字体渲染(对MCU性能要求高)。 - 抗锯齿:对于自绘图形,可以实现简单的灰度抗锯齿。例如,画一条斜线时,根据像素被线覆盖的面积来调整其亮度(颜色混合),但这会显著增加计算量。
- 字体问题:Adafruit GFX内置的字体是点阵字体,放大后会有锯齿。可以尝试使用更平滑的字体库,如
5. 进阶应用:打造一个迷你系统状态监视器
掌握了基础,我们来做个有意思的小项目:用1.3寸屏和ESP32,做一个能显示Wi-Fi信号强度、IP地址、CPU负载和内存使用率的桌面小摆件。这个项目会综合运用到网络连接、系统信息获取和动态UI刷新。
5.1 系统架构与硬件连接
我们使用ESP32作为主控,因为它内置Wi-Fi,性能也足够。连接非常简单:
VCC-> ESP32的3.3VGND-> ESP32的GNDSCL-> ESP32的SCK(GPIO 18)SDA-> ESP32的MOSI(GPIO 23)DC-> ESP32的GPIO 2RST-> ESP32的GPIO 4CS-> ESP32的GPIO 15BLK-> 接3.3V常亮
5.2 软件实现核心逻辑
代码结构会稍微复杂一些,我们需要在loop()中周期性地更新不同区域的信息,同时避免全局刷新。
#include <Adafruit_GFX.h> #include <Adafruit_ST7789.h> #include <WiFi.h> // 引脚定义、tft对象创建...(同上) // 定义显示区域 #define AREA_NETWORK 0 #define AREA_SYSTEM 1 // 全局变量存储旧值,用于判断是否需要更新 String oldSSID = ""; int oldRSSI = 0; String oldIP = ""; float oldLoad = 0.0; int oldFreeMem = 0; void setup() { Serial.begin(115200); tft.init(240, 240); tft.setRotation(1); // 根据你的摆放调整 tft.fillScreen(ST77XX_BLACK); tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK); // 设置文字颜色和背景色(用于局部覆盖) // 连接Wi-Fi... connectToWiFi(); // 绘制静态框架(标题、分割线等) drawStaticUI(); } void loop() { updateNetworkInfo(); updateSystemInfo(); delay(2000); // 每2秒更新一次 } void updateNetworkInfo() { String currentSSID = WiFi.SSID(); int currentRSSI = WiFi.RSSI(); String currentIP = WiFi.localIP().toString(); // 只有信息发生变化时才更新屏幕,避免闪烁 if (currentSSID != oldSSID || currentRSSI != oldRSSI) { tft.setCursor(10, 30); tft.setTextColor(ST77XX_CYAN, ST77XX_BLACK); tft.printf("SSID: %s\n", currentSSID.c_str()); // 绘制Wi-Fi信号图标,根据RSSI值变化 drawWifiIcon(150, 30, currentRSSI); oldSSID = currentSSID; oldRSSI = currentRSSI; } if (currentIP != oldIP) { tft.fillRect(10, 50, 200, 20, ST77XX_BLACK); // 局部清空旧IP区域 tft.setCursor(10, 50); tft.setTextColor(ST77XX_GREEN, ST77XX_BLACK); tft.printf("IP: %s", currentIP.c_str()); oldIP = currentIP; } } void updateSystemInfo() { // 获取ESP32的“负载”(可以用循环空转模拟,或监控任务运行时间) // 这里简化为例,使用随机数模拟变化 float currentLoad = random(0, 100) / 100.0; int currentFreeMem = ESP.getFreeHeap(); if (abs(currentLoad - oldLoad) > 0.05) { // 变化超过5%才更新 tft.fillRect(10, 100, 100, 20, ST77XX_BLACK); tft.setCursor(10, 100); tft.setTextColor(ST77XX_YELLOW, ST77XX_BLACK); tft.printf("Load: %.1f%%", currentLoad * 100); oldLoad = currentLoad; } if (abs(currentFreeMem - oldFreeMem) > 1024) { // 变化超过1KB才更新 tft.fillRect(10, 120, 150, 20, ST77XX_BLACK); tft.setCursor(10, 120); tft.setTextColor(ST77XX_MAGENTA, ST77XX_BLACK); tft.printf("Mem: %d KB", currentFreeMem / 1024); oldFreeMem = currentFreeMem; } } void drawWifiIcon(int x, int y, int rssi) { // 根据RSSI值绘制不同强度的Wi-Fi图标(简易版) int bars = 0; if (rssi > -55) bars = 4; else if (rssi > -65) bars = 3; else if (rssi > -75) bars = 2; else if (rssi > -85) bars = 1; // 先清空图标区域 tft.fillRect(x, y, 40, 25, ST77XX_BLACK); // 绘制信号条 for(int i=0; i<bars; i++){ tft.fillRoundRect(x + i*8, y + 20 - i*5, 6, i*5 + 4, 1, ST77XX_WHITE); } }这个例子展示了如何组织一个简单的信息显示应用。关键点在于局部刷新和状态对比更新,这能极大提升视觉流畅度并降低MCU负担。你可以进一步扩展,加入传感器数据(温湿度)、时间显示,甚至用LVGL等高级图形库做出更炫酷的界面。
6. 屏幕选型与资源获取指南
最后,如果你也想入手一块来玩,这里有一些选购和学习的建议。
选型要点:
- 接口:SPI接口通用性强,速度快,是首选。I2C接口引脚少但刷新慢,只适合显示静态或更新不频繁的内容。
- 分辨率:240x240是方形屏的黄金比例,做圆形UI或对称布局很合适。也有128x64(常为OLED)或240x135等规格,根据信息量选择。
- 面板类型:IPS屏视角广、色彩好,但价格稍高。TN屏视角窄,但响应快。对于小尺寸,IPS体验提升明显。
- 触摸功能:有些模块会集成电阻或电容触摸屏(通常标有“TP”),引脚会多出
T_IRQ和T_CS等。如果需要交互,可以考虑,但驱动会更复杂一些。 - 配套资源:优先选择卖家能提供Arduino/树莓派例程的模块,能省去很多摸索时间。
学习资源:
- 驱动芯片Datasheet:搜索“ST7789V datasheet”,这是终极参考手册,虽然枯燥,但所有命令和时序都在里面。
- Adafruit学习系统:Adafruit网站上有大量关于GFX库和具体屏幕驱动的教程,图文并茂,非常适合入门。
- 开源项目平台:在GitHub或Gitee上搜索“1.3 inch ST7789”,能找到无数现成的项目代码,从时钟、游戏到仪表盘,都是极好的学习素材。
- 社区论坛:像Arduino中文社区、ESP32论坛等,有很多关于小屏幕的讨论帖,很多奇怪的问题都能在那里找到答案。
玩转一块小屏幕,就像是给单片机项目打开了“视觉”的窗口。从点亮第一个像素到做出一个流畅的动态界面,这个过程会遇到不少挑战,但解决问题的乐趣和最终的成就感,正是嵌入式开发的魅力所在。希望这篇长文能帮你少走些弯路,更快地让这块1.3英寸的世界变得精彩起来。