1. 项目概述与核心价值
如果你手头正好有一块闲置的ST7920驱动的128x64点阵液晶屏,又恰好想用ESP32做个显示终端,比如温湿度监测站、小型游戏机或者设备状态面板,那么你很可能正面临一个经典问题:资料太少,无从下手。网上关于Arduino Uno驱动ST7920的教程不少,但一换到ESP32,引脚定义、库的配置方法似乎都变了样,直接照搬往往不灵。我自己在折腾这个小项目时,就深有体会,翻遍了论坛和代码库,才把这条路走通。今天,我就把自己从硬件连接到软件调试的全过程,包括中间踩过的坑和最终验证有效的方案,详细记录下来。这不仅仅是一个“接线-上传代码”的快速指南,我更想和你聊聊为什么这么接,库函数背后做了什么,以及当屏幕一片空白时,我们该如何一步步排查。无论你是刚接触嵌入式显示的爱好者,还是需要在ESP32项目里快速集成一个低成本显示屏的开发者,这篇内容都能给你提供一个清晰、可靠且深度优化的实现路径。
2. 硬件连接详解与原理剖析
2.1 认识ST7920液晶屏与接口定义
ST7920是一款非常经典的液晶显示控制器,它最大能驱动128x64的点阵,并且内部集成了中文字库(GB2312),对于显示中文信息非常友好。我们常见的带蓝色背光、绿色或黄绿色显示的12864屏幕,很多都是基于这颗芯片。它的接口方式比较灵活,支持8位/4位并行总线、串行SPI以及I2C(需转接板)模式。为了节省ESP32宝贵的GPIO引脚,我们通常选择SPI(串行外设接口)模式。
在SPI模式下,ST7920只需要4根线就能工作:
- SCLK (Serial Clock): 串行时钟线,由主设备(ESP32)产生,用于同步数据传输。
- SID (Serial Data): 串行数据线,用于传输指令或显示数据。
- CS (Chip Select) / PSB: 片选信号。这是关键!对于ST7920,当PSB引脚接低电平(GND)时,芯片进入串行模式。我们通常就直接用一根线将其固定接地,而不再用ESP32的GPIO去控制它。但有些教程或库代码中提到的“CS”引脚,在软件SPI模拟下,实际上可以复用为一个使能控制脚,不过ST7920在串行模式下对此要求不严格,这也是后面配置代码时的一个注意点。
- VCC & GND: 电源,通常是5V。但很多模块也兼容3.3V逻辑电平。
此外,屏幕还有一个V0引脚,用于调节对比度。它需要接一个可变电阻(电位器)的中心抽头,通过改变分压来调整屏幕显示的深浅。如果对比度不合适,即使程序正确,你也可能什么都看不到。
2.2 ESP32引脚选择与连接方案
ESP32的引脚功能非常灵活,但并非所有GPIO都适合用于模拟SPI。我们需要选择那些中断干扰少、输出稳定的引脚。根据广泛测试和我的实际经验,以下是一个可靠且通用的连接方案:
| ST7920 引脚 | 功能 | 连接至 ESP32 引脚 | 备注与原理 |
|---|---|---|---|
| VCC | 电源正极 | 5V 或 3.3V | 电压选择是关键。虽然ST7920逻辑电平可兼容3.3V,但其内部驱动和背光(如果直接供电)可能需要5V才能达到最佳对比度和亮度。如果使用3.3V,屏幕可能非常暗淡甚至不显示。建议优先尝试连接ESP32的5V引脚。 |
| GND | 电源地 | GND | 共地是必须的。 |
| V0 | 对比度调节 | 10kΩ电位器中心抽头 | 电位器另外两端分别接VCC和GND。旋转电位器即可调节对比度。 |
| PSB | 并行/串行选择 | GND | 必须接GND,强制屏幕进入串行SPI模式。 |
| RST | 复位 | ESP32 GPIO22 (可选) | 可接可不接。如果连接,可以通过程序控制复位;不接时,通常模块内部有上电复位电路。为求稳定,建议连接。 |
| SCLK | 串行时钟 | ESP32 GPIO18 | 这是硬件SPI的默认SCK引脚之一,输出稳定。 |
| SID | 串行数据 | ESP32 GPIO23 | 这是硬件SPI的默认MOSI引脚之一。 |
| CS | 片选 | ESP32 GPIO5 | 在软件SPI模拟中,此引脚被库用作通信使能。 |
实操心得:关于电源的坑我最开始将VCC接在了ESP32的3.3V引脚上,结果屏幕背光亮了,但无论如何调节对比度都没有任何显示。排查了很久代码和接线,最后用万用表量了一下屏幕VCC对地的电流,发现很小,怀疑是驱动电压不足。换成5V引脚后,屏幕立刻正常显示。所以,如果你的屏幕不亮,第一个要检查的就是电源电压。有些ESP32开发板(如NodeMCU-32S)的
5V引脚实际上是USB输入电压(约5V),而VIN引脚是板载稳压器的输入,接电池或外部电源时用。这里我们接5V即可。
连接实物时,建议使用杜邦线在面包板上完成。确保电位器连接正确,这是导致“白屏”或“全黑屏”的第二大常见原因。接线完成后,先不要着急上传代码,检查一遍所有连接,尤其是GND和PSB是否可靠接地。
3. U8g2库的配置与深度解析
3.1 为什么选择U8g2库?
在Arduino生态中,驱动显示屏的库有很多,比如Adafruit_GFX配合特定控制器库、TFT_eSPI等。我选择U8g2库的原因有三点:
- 支持极其广泛:U8g2几乎支持所有你能想到的单色OLED和LCD控制器,ST7920只是其中一员。学会用它,以后换屏幕会非常省事。
- 功能全面:内置了多种字体(包括中文)、绘图函数(点、线、圆、矩形)、位图显示等功能,API设计相对统一。
- 双缓冲支持:库支持“全缓冲(Full Buffer)”模式,即先在内存中构建完整的画面,再一次性发送给屏幕。这虽然耗内存,但可以完全避免画面撕裂,对于动态图形显示非常重要。
3.2 库的安装与构造函数选择
在Arduino IDE中,通过“库管理器”搜索“U8g2”,由olikraus开发的那个就是,点击安装即可。安装完成后,我们面临第一个关键选择:在示例代码GraphicsTest中,该使用哪个构造函数?
打开文件->示例->U8g2->full_buffer->GraphicsTest。 在代码开头,你会看到一堆被注释掉的构造函数。我们的任务是找到并修改适合ESP32软件SPI驱动ST7920的那一行。
原始代码中通常有这样一行:
// U8G2_ST7920_128X64_F_SW_SPI u8g2(U8G2_R0, /* clock=*/ 13, /* data=*/ 11, /* cs=*/ 10, /* reset=*/ 8);这行代码是针对Arduino Uno等AVR芯片的引脚示例。我们需要根据之前的硬件连接表修改它:
U8G2_ST7920_128X64_F_SW_SPI u8g2(U8G2_R0, /* clock=*/ 18, /* data=*/ 23, /* cs=*/ 5, /* reset=*/ 22);让我们拆解这个构造函数:
U8G2_ST7920_128X64_F_SW_SPI: 这是类名,指明了控制器型号(ST7920)、分辨率(128x64)、缓冲模式(F代表全缓冲)和通信方式(SW_SPI代表软件模拟SPI)。U8G2_R0: 屏幕旋转参数。R0表示0度旋转,R1为90度,R2为180度,R3为270度。你可以根据屏幕实际安装方向调整。clock=18: 对应我们连接的SCLK引脚(GPIO18)。data=23: 对应我们连接的SID引脚(GPIO23)。cs=5: 对应我们连接的CS引脚(GPIO5)。注意:在软件SPI模式下,这个CS引脚是必须的,库会用它来控制通信时序。reset=22: 对应我们连接的RST复位引脚(GPIO22)。如果没接,这里可以填U8X8_PIN_NONE。
注意事项:硬件SPI与软件SPI的选择在评论区有朋友提到可以使用硬件SPI以获得更快速度。这完全正确。硬件SPI由ESP32的专用外设处理,不占用CPU时间。你可以使用以下构造函数:
U8G2_ST7920_128X64_F_HW_SPI u8g2(U8G2_R0, /* cs=*/ 5, /* reset=*/ 22);注意,此时只需要指定
cs和reset引脚,clock和data引脚固定使用ESP32的默认硬件SPI引脚(VSPI: SCK=18, MOSI=23)。使用硬件SPI时,务必确保这些引脚没有被其他功能占用,并且接线对应(SCLK接SCK,SID接MOSI)。对于绝大多数应用,软件SPI已足够流畅,但如果你需要极高的刷新率或进行复杂的动画,硬件SPI是更好的选择。
3.3 GraphicsTest代码解析与上传
修改好构造函数后,你就可以直接上传GraphicsTest示例代码了。这个代码会循环运行一系列图形测试,包括画线、画圆、显示字体等,是验证屏幕是否正常工作的最佳选择。
在上传前,请务必:
- 在Arduino IDE的“工具”菜单中,正确选择你的ESP32开发板型号(如“ESP32 Dev Module”)。
- 选择正确的端口。
- 如果ESP32是第一次使用,你可能需要按住板上的“BOOT”按钮再点击上传,以进入下载模式。
上传成功后,ESP32会自动复位运行。此时观察你的屏幕,应该会看到各种图形和文字依次显示。如果屏幕有背光,背光应该常亮。
4. 从测试到应用:编写你自己的显示程序
4.1 程序框架与初始化
通过了测试,接下来我们就要编写自己的程序了。一个最基本的U8g2显示程序包含以下结构:
#include <U8g2lib.h> // 包含U8g2库 // 根据你的连接方式选择构造函数 U8G2_ST7920_128X64_F_SW_SPI u8g2(U8G2_R0, /* clock=*/ 18, /* data=*/ 23, /* cs=*/ 5, /* reset=*/ 22); void setup(void) { u8g2.begin(); // 初始化显示屏 // 其他初始化代码... } void loop(void) { u8g2.clearBuffer(); // 清除内部缓冲区 // 在此处调用各种绘图函数... u8g2.sendBuffer(); // 将缓冲区内容发送到显示屏 delay(1000); // 延时 }核心要点:
u8g2.begin(): 必须调用,用于初始化与屏幕的通信。- 绘制流程:U8g2采用“先绘制,后发送”的模式。所有
drawXxx()函数(如drawStr,drawLine)都是在内存缓冲区里操作,屏幕并不会立即变化。 u8g2.clearBuffer(): 清空缓冲区,为绘制新一帧做准备。u8g2.sendBuffer():这是关键!只有执行了这个函数,缓冲区里绘制好的内容才会被一次性发送到屏幕上显示出来。这保证了画面的完整性。
4.2 常用绘图函数示例
下面展示几个最常用的函数,你可以把它们放在loop()中的clearBuffer()和sendBuffer()之间。
1. 显示文字:
u8g2.setFont(u8g2_font_ncenB08_tr); // 设置字体(这里是一种8像素高的字体) u8g2.drawStr(0, 10, "Hello World"); // 在坐标(0,10)处绘制字符串U8g2内置了数十种字体,u8g2_font_ncenB08_tr只是其中之一。你可以在示例代码u8g2_full_buffer中的F部分找到所有字体预览。坐标(0,10)表示从左边界开始,距离顶部10个像素的位置。
2. 显示变量数值:
int sensorValue = 1234; char buffer[20]; // 定义一个字符数组作为缓冲区 sprintf(buffer, "Value: %d", sensorValue); // 将数值格式化成字符串 u8g2.drawStr(0, 20, buffer); // 绘制该字符串3. 绘制图形:
u8g2.drawFrame(5, 15, 40, 20); // 绘制一个矩形框 (x, y, 宽, 高) u8g2.drawRFrame(50, 15, 40, 20, 5); // 绘制圆角矩形框,最后一个参数是圆角半径 u8g2.drawDisc(70, 50, 15); // 绘制实心圆 (圆心x, 圆心y, 半径) u8g2.drawCircle(30, 50, 15); // 绘制空心圆 u8g2.drawLine(0, 63, 127, 63); // 绘制一条从(0,63)到(127,63)的直线,作为底部边框4. 显示中文:U8g2库对中文的支持需要额外步骤。库本身不包含完整中文字库(因为太大),但支持从外部存储(如SD卡)加载,或者使用“裁剪”后的自定义字体。对于初学者,更简单的方法是使用u8g2.setFont()设置一个包含所需中文的字体,但这类字体文件通常较大。一个折中的方案是,如果只需要少量汉字,可以将其转换为位图数组来显示。这涉及其他工具,超出了本文基础范围,但知道这个方向很重要。
4.3 一个完整的温湿度显示示例
假设我们有一个DHT11温湿度传感器,我们可以结合U8g2库,在屏幕上创建一个简单的信息面板。
#include <U8g2lib.h> #include <DHT.h> #define DHTPIN 4 // DHT数据引脚接GPIO4 #define DHTTYPE DHT11 U8G2_ST7920_128X64_F_SW_SPI u8g2(U8G2_R0, 18, 23, 5, 22); DHT dht(DHTPIN, DHTTYPE); void setup() { Serial.begin(115200); u8g2.begin(); dht.begin(); u8g2.setFont(u8g2_font_helvB10_tf); // 设置一种稍大的字体 } void loop() { delay(2000); // DHT11读取间隔至少2秒 float h = dht.readHumidity(); float t = dht.readTemperature(); if (isnan(h) || isnan(t)) { Serial.println("读取DHT失败!"); return; } u8g2.clearBuffer(); // 绘制标题 u8g2.setFont(u8g2_font_helvB12_tf); u8g2.drawStr(20, 15, "Env Monitor"); u8g2.drawLine(0, 18, 128, 18); // 显示温度 u8g2.setFont(u8g2_font_helvB10_tf); u8g2.drawStr(10, 40, "Temp:"); u8g2.setCursor(70, 40); u8g2.print(t, 1); // 显示一位小数 u8g2.drawStr(110, 40, "C"); // 显示湿度 u8g2.drawStr(10, 60, "Humi:"); u8g2.setCursor(70, 60); u8g2.print(h, 1); u8g2.drawStr(110, 60, "%"); u8g2.sendBuffer(); }这个例子展示了如何结合传感器库、使用不同字体、绘制文本和线条,以及格式化输出浮点数。u8g2.setCursor()用于设置接下来print()函数输出的起始位置,非常方便。
5. 常见问题排查与性能优化技巧
5.1 屏幕无任何显示(背光也不亮)
- 检查电源:用万用表测量屏幕VCC和GND之间的电压,确保在5V左右。检查ESP32的5V引脚是否有输出。
- 检查接地:确保屏幕的GND和ESP32的GND可靠连接。这是所有数字电路的基础。
- 检查PSB引脚:必须确保PSB引脚连接到了GND,否则屏幕处于并行模式,无法响应SPI信号。
- 检查电位器:调节电位器,尝试整个旋转范围。对比度极端情况下(一端VCC,一端GND)屏幕可能全黑或全白。
5.2 屏幕背光亮但无内容(白屏)
- 对比度问题:这是最常见的原因。缓慢旋转电位器,观察屏幕是否有变化。有时合适的对比度点非常窄。
- 代码未上传/ESP32未运行:检查ESP32上的用户LED是否在闪烁,或者通过串口监视器输出调试信息,确认程序在运行。
- 引脚定义错误:仔细核对代码中的
clock、data、cs、reset引脚号是否与实际接线一一对应。ESP32的GPIO编号有时在板子上标注的不是数字,需要查对原理图。 - 库构造函数错误:确认你取消注释并修改的是正确的构造函数。
U8G2_ST7920_128X64_F_SW_SPI是针对全缓冲软件SPI的。如果你用了U8G2_ST7920_128X64_1_SW_SPI(非全缓冲),代码逻辑会有所不同。
5.3 显示乱码、错位或部分显示
- 通信速率问题:软件SPI模拟可能因为CPU忙于其他任务(如WiFi、复杂计算)导致时序出错。尝试在
setup()中加入u8g2.setBusClock(1000000)来降低SPI时钟频率(单位Hz)。默认值可能较高,在某些接线质量下不稳定。 - 电源噪声:在ESP32��电源引脚(特别是5V)和GND之间并联一个100uF的电解电容和一个0.1uF的陶瓷电容,可以滤除电源噪声,使显示更稳定。
- 复位问题:如果接了RST引脚,尝试在
setup()的最开始,手动控制一下复位:digitalWrite(22, LOW); delay(50); digitalWrite(22, HIGH); delay(50);然后再调用u8g2.begin()。 - 缓冲区溢出:确保你的绘图操作没有超出屏幕范围(128x64)。虽然库可能不会报错,但越界写入可能导致不可预知的行为。
5.4 显示刷新慢、闪烁或卡顿
- 切换到硬件SPI:如前所述,使用
U8G2_ST7920_128X64_F_HW_SPI构造函数可以极大提升刷新速度,并释放CPU资源。 - 优化绘图代码:
- 避免在
loop()中频繁设置字体。如果字体不变,将setFont移到setup()中。 - 只重绘变化的部分。如果界面只有部分区域更新,可以不用每次
clearBuffer()全屏,而是用drawBox函数覆盖旧内容再画新的。但这需要更精细的逻辑控制。 - 减少
loop()中不必要的延迟delay()。对于需要定时更新的数据,使用millis()进行非阻塞定时。
- 避免在
- 使用更大的字体和图形谨慎:复杂的字体和大量图形填充(如
drawDisc)会消耗更多的计算时间。在性能敏感的场合,使用小字体和线框图形。
5.5 关于电位器的补充说明
评论区有朋友说“10k pot does not do anything”。这通常是因为电位器接错了线。正确的接法是:电位器的三个引脚,两端分别接VCC和GND,中间的可变端接屏幕的V0。如果接错,调节当然无效。另外,有些屏幕模块已经集成了合适的偏压电路,V0直接接一个固定电阻到GND也可能工作,但为了灵活性,接电位器仍是推荐做法。
最后,驱动ST7920这类屏幕,最需要的就是耐心。硬件连接是基础,务必扎实。软件配置上,U8g2库已经封装得非常完善,我们的工作主要是正确初始化和调用API。当屏幕成功点亮并显示出第一行“Hello World”时,那种成就感就是驱动我们继续探索嵌入式世界的最佳动力。希望这篇详细的指南能帮你扫清障碍,顺利让ESP32和你的12864屏幕对话。如果在实践中遇到新问题,不妨从电源、地线、对比度这三个最基础的物理层面重新检查,往往能事半功倍。