1. 从一个“点亮屏幕”的小目标说起
很多人拿到ESP32开发板之后,第一个想做的交互项目往往不是点灯,而是让一块屏幕显示出字来。原因很简单:灯只能告诉你“有电”,而屏幕能告诉你“系统活着,而且听懂了你的话”。串口屏就是实现这个目标最省事的路径之一。它把复杂的图形绘制、字库管理、触摸响应都封装在屏幕背后的控制芯片里,主控只需要通过串口发几条指令,屏幕就会乖乖把文字画出来。ESP32负责逻辑,串口屏负责显示,分工明确,各干各的活。
这篇内容围绕“咸鱼ESP32实例—串口屏文字显示”这个主题展开,面向的是刚接触ESP32、手里有一块串口屏、但不知道从哪下手的朋友。我会把硬件接线、串口参数、指令格式、字库选择、常见翻车点全部拆开讲清楚,让你看完就能自己动手把文字显示出来。关键词包括ESP32、串口屏、UART通信、文字显示、字库、指令集、波特率、TTL电平。这些词后面都会反复出现,因为它们是这条链路里绕不开的核心。
先明确一个概念:串口屏不是普通SPI或I2C屏幕。它内部通常有一颗独立的MCU或专用显示控制芯片,负责驱动液晶面板、管理字库、处理触摸事件。你发给它的不是像素数据,而是“在坐标(x,y)用字号z显示字符串abc”这样的命令。这种模式的好处是主控压力极小,ESP32只需要占用一个UART外设,剩下的计算和显存都交给屏幕自己。代价是刷新率和动画能力有限,但对于显示传感器数据、菜单、状态提示这类场景,完全够用。
我见过不少人一上来就买SPI TFT屏,结果被驱动库、字库取模、显存分配折腾得够呛,最后连“Hello World”都没显示出来。串口屏的入门门槛低得多,只要你会用串口发数据,就能让屏幕出字。这也是为什么我建议新手从串口屏开始,先把“主控与显示设备通信”这件事跑通,再去挑战更底层的屏幕方案。
2. 硬件选型与接线:别让电平问题毁掉一晚上
2.1 串口屏的常见类型与挑选思路
市面上的串口屏大致分两类:一类是“指令屏”,你发什么指令它就执行什么,屏幕本身不存逻辑;另一类是“组态屏”,配套上位机软件做界面工程,屏幕里跑一套预设的页面逻辑,主控只负责改变量。对于“文字显示”这个需求,指令屏更直接,学习成本也更低。挑选时重点看三个参数:供电电压、通信电平、默认波特率。
供电电压常见有5V和12V两种。5V的可以直接从ESP32开发板的5V引脚取电,但要注意电流是否足够,屏幕背光全开时可能吃掉几百毫安。12V的就需要额外电源,不能直接从开发板取。通信电平方面,绝大多数串口屏的UART是TTL电平,但电压域可能是5V也可能是3.3V。ESP32的IO是3.3V电平,如果屏幕的RX接受5V输入,长期看可能损伤ESP32的TX引脚,反过来屏幕的TX输出5V直接进ESP32的RX也会有问题。
注意:在接线之前,务必确认屏幕的通信电平。如果不确定,先用万用表量一下屏幕TX引脚在空闲状态下的电压,接近3.3V就直连,接近5V就需要电平转换。
2.2 ESP32与串口屏的接线方案
ESP32一般有三个UART:UART0用于下载和调试,UART1和UART2可以自由映射到任意GPIO。我习惯用UART2来连串口屏,把UART0留给串口监视器,这样调试信息不会和屏幕指令混在一起。
典型接线如下:
| ESP32引脚 | 串口屏引脚 | 说明 |
|---|---|---|
| GPIO17 (TX2) | RX | ESP32发送指令给屏幕 |
| GPIO16 (RX2) | TX | 屏幕返回数据给ESP32 |
| 5V | VCC | 供电,确认屏幕工作电压 |
| GND | GND | 共地,必须接 |
这里有个容易忽略的点:TX和RX必须交叉连接。ESP32的TX接屏幕的RX,ESP32的RX接屏幕的TX。我见过有人直连TX到TX,然后纳闷为什么屏幕一点反应都没有。另外,GND一定要接,不共地的话信号没有参考电平,通信必然失败。
如果你用的是UART1,默认引脚是GPIO9和GPIO10,但这两个引脚在某些ESP32模组上连接了内部Flash,使用时会出问题。所以更稳妥的做法是用UART2,或者在代码里把UART1重映射到其他空闲GPIO。
2.3 供电与背光的实际问题
串口屏的背光电流不容小觑。一块3.5寸屏背光全开可能达到200mA以上,加上屏幕控制芯片本身的功耗,整体可能超过300mA。ESP32开发板上的5V引脚通常来自USB供电,USB口能提供500mA左右,勉强够用,但如果同时接了其他外设,就可能出现屏幕闪烁或ESP32复位。
我的做法是给屏幕单独供5V,只把GND和ESP32共起来。这样屏幕的电流不经过开发板的稳压器,系统更稳定。如果屏幕支持背光调节指令,也可以在初始化时把背光亮度调低一些,既能省电又能延长背光寿命。
3. 串口参数与指令格式:让屏幕听懂你的话
3.1 波特率、数据位、停止位的匹配
串口通信的第一道坎就是参数匹配。串口屏出厂时通常有一个默认波特率,常见的是9600、115200、或者57600。你必须在ESP32代码里把UART配置成完全一样的参数,否则收到的就是乱码或者根本没反应。
以115200为例,ESP32端的配置如下:
Serial2.begin(115200, SERIAL_8N1, 16, 17);这行代码的意思是:使用UART2,波特率115200,8位数据位,无校验,1位停止位,RX接GPIO16,TX接GPIO17。SERIAL_8N1是绝大多数串口屏的默认格式,如果你不确定,先按这个来,不通再查屏幕手册。
有个细节值得注意:有些串口屏在启动时会打印一串开机信息,如果你在ESP32的setup里立刻发指令,可能会和开机信息冲突。我通常会在setup里先delay(500)到1000毫秒,等屏幕初始化完成后再发第一条指令。
3.2 指令帧结构拆解
串口屏的指令通常是十六进制帧,不同厂家的协议不一样,但结构上大同小异。以常见的“文本显示”指令为例,一帧可能包含以下部分:
- 帧头:固定字节,比如0xAA或0x5A,用来标识一帧的开始
- 指令类型:比如0x01表示显示文本
- 坐标:X和Y各占两个字节,高字节在前或低字节在前要看协议
- 字号:一个字节,对应屏幕内部预设的字库大小
- 颜色:两个字节,RGB565格式
- 文本内容:字符串的ASCII或GBK编码
- 帧尾:固定字节,比如0xBB,或者校验和
具体到某个屏幕,你必须拿到它的指令手册。没有手册的话,可以尝试用串口助手发一些常见帧格式,观察屏幕反应。但更靠谱的办法是找卖家要资料,或者在网上搜同型号的开源例程。
3.3 用代码封装指令发送函数
直接在主循环里拼十六进制数组会让代码很难维护。我的习惯是写一个发送函数,把坐标、字号、颜色、文本作为参数传进去,函数内部负责拼帧和发送。
void sendText(uint16_t x, uint16_t y, uint8_t font, uint16_t color, const char* text) { uint8_t frame[128]; uint8_t idx = 0; frame[idx++] = 0xAA; // 帧头 frame[idx++] = 0x01; // 指令类型:显示文本 frame[idx++] = x >> 8; frame[idx++] = x & 0xFF; frame[idx++] = y >> 8; frame[idx++] = y & 0xFF; frame[idx++] = font; frame[idx++] = color >> 8; frame[idx++] = color & 0xFF; while (*text) { frame[idx++] = *text++; } frame[idx++] = 0xBB; // 帧尾 Serial2.write(frame, idx); }这段代码是示意性的,实际帧格式要根据你的屏幕手册调整。重点在于:把拼帧逻辑封装起来,主逻辑里只需要调用sendText(10, 20, 16, 0xFFFF, "Hello"),代码可读性会好很多。
提示:如果屏幕支持GBK编码,显示中文时字符串要用GBK编码的字节数组,不能直接用UTF-8。Arduino IDE默认源文件是UTF-8,直接传中文会乱码。解决办法是用取模软件生成GBK字节数组,或者把源文件保存为GBK编码。
4. 字库与编码:中文显示的真正门槛
4.1 屏幕内置字库与外部字库的区别
很多串口屏内置了ASCII字库和一级汉字字库,这意味着你发ASCII字符或者常用汉字的GBK编码,屏幕就能直接显示。但内置字库的字号通常是固定的几档,比如16×16、24×24、32×32,不能任意缩放。如果你需要特殊字体或者更大字号,就需要用外部字库文件,通过屏幕的SD卡接口或者Flash存储来加载。
内置字库的好处是省事,发指令就能显示。坏处是字体样式单一,而且有些屏幕的内置汉字字库只覆盖常用字,生僻字显示不出来。外部字库灵活,但需要额外的取模和下载步骤。对于“文字显示”这个入门需求,先用内置字库跑通,等有特殊需求再折腾外部字库。
4.2 ASCII与GBK编码的实操处理
ASCII字符的编码很简单,一个字节对应一个字符。中文就麻烦一些,GBK编码里一个汉字占两个字节,而且这两个字节的值都可能大于0x7F。在C语言里,如果直接用char数组存GBK字符串,要注意符号扩展问题。更稳妥的做法是用uint8_t数组,明确按字节处理。
// GBK编码的“你好”字节序列 uint8_t text[] = {0xC4, 0xE3, 0xBA, 0xC3, 0x00}; Serial2.write(text, 4);如果你在Arduino代码里写中文字符串,比如sendText(10, 10, 16, 0xFFFF, "你好"),编译器会按源文件编码来存这个字符串。如果源文件是UTF-8,那“你好”就是6个字节,屏幕按GBK解析就会乱码。解决办法有两个:一是把源文件另存为GBK编码,二是用取模软件生成GBK字节数组直接嵌入代码。
我个人的习惯是用取模软件生成字节数组,虽然看起来不够优雅,但胜在可控。每次要显示新的中文内容,就用软件生成对应的GBK字节序列,复制到代码里。这样不管IDE是什么编码,显示都不会出问题。
4.3 字库大小与显示效果的权衡
字号越大,显示越清晰,但占用的屏幕空间也越大。16×16的汉字在3.5寸屏上已经能看清,24×24更适合远距离观看,32×32则适合标题或者大数字显示。选择字号时要考虑屏幕分辨率和观看距离。如果是手持设备,16或24就够了;如果是固定在墙上的显示面板,32甚至更大才看得清。
另外,不同字号的字库文件大小差异很大。一级汉字3755个,16×16字库大约120KB,24×24大约270KB,32×32大约480KB。如果屏幕的Flash空间有限,就要权衡字库覆盖范围和字号。有些屏幕支持按需加载字库,用哪个字就取哪个字的模,这样可以节省空间,但实现起来更复杂。
5. 从零跑通第一个显示例程
5.1 最小验证:先让屏幕回个话
在写显示代码之前,我建议先做一个最小验证:ESP32发一条简单指令,看屏幕有没有反应。最简单的指令通常是“清屏”或者“设置背光”。如果屏幕有返回数据的功能,也可以发一条查询指令,看ESP32能不能收到回复。
这个步骤的目的是把通信链路先打通。如果连清屏都做不到,那问题一定在接线、波特率、或者电平上,跟指令格式无关。把问题范围缩小,排查起来会快很多。
void setup() { Serial.begin(115200); // 调试串口 Serial2.begin(115200, SERIAL_8N1, 16, 17); // 屏幕串口 delay(1000); // 发送清屏指令(示例,具体字节看手册) uint8_t clearCmd[] = {0xAA, 0x02, 0xBB}; Serial2.write(clearCmd, sizeof(clearCmd)); Serial.println("Clear command sent"); } void loop() { // 空循环,先验证通信 }烧录后打开串口监视器,看是否打印了“Clear command sent”。如果屏幕有变化(比如闪一下或者变黑),说明通信链路是通的。如果没反应,先检查接线和波特率。
5.2 显示第一行文字
通信打通之后,就可以发文本显示指令了。先用ASCII字符测试,因为ASCII编码简单,不容易出错。比如显示“ESP32 OK”:
void loop() { sendText(10, 10, 16, 0xFFFF, "ESP32 OK"); delay(2000); }如果屏幕上出现了白色的“ESP32 OK”,恭喜你,核心链路已经跑通了。接下来可以尝试改坐标、改颜色、改字号,观察屏幕的变化。这个过程能帮你快速理解指令里各个参数的作用。
5.3 动态刷新与数据展示
静态文字显示只是第一步,实际项目里更常见的是动态刷新,比如显示传感器读数、倒计时、状态变化。动态刷新的关键是“局部刷新”:只更新变化的区域,而不是整屏重绘。整屏重绘会导致闪烁,而且刷新率上不去。
局部刷新的做法是:先用背景色在文字区域画一个填充矩形,把旧文字盖掉,再在新位置或者用新内容显示文字。有些屏幕支持“透明背景”模式,文字直接覆盖,不需要先填充背景。具体用哪种方式,要看屏幕指令集的支持情况。
void updateValue(int value) { // 先用背景色清除旧值区域 fillRect(100, 50, 80, 24, 0x0000); // 黑色背景 // 再显示新值 char buf[16]; sprintf(buf, "%d", value); sendText(100, 50, 24, 0x07E0, buf); // 绿色文字 }这段代码里,fillRect是假设屏幕支持填充矩形指令。如果不支持,可以用显示空格字符串的方式来覆盖旧内容。动态刷新时要注意刷新频率,太快了屏幕反应不过来,太慢了数据更新不及时。一般100到200毫秒刷新一次比较合适。
6. 那些让我熬夜的坑与排查思路
6.1 屏幕完全没反应:从供电查到指令
屏幕没反应是最常见的问题,排查顺序应该是:供电→接线→波特率→指令格式。先量屏幕的VCC和GND之间有没有电压,再看TX和RX有没有接反,然后确认ESP32和屏幕的波特率是否一致,最后才怀疑指令格式。
我遇到过一次屏幕没反应,查了半天发现是杜邦线内部断了。万用表量通断才发现,外表看不出来。所以接线排查不能只看插没插上,要量通断。
6.2 显示乱码:编码问题占九成
乱码的原因几乎都是编码不匹配。ASCII字符乱码通常是波特率不对,中文乱码通常是UTF-8和GBK搞混了。排查方法是:先只显示ASCII,如果ASCII正常,说明通信参数没问题,问题在中文编码上。如果ASCII也乱码,那就是波特率或者数据位配置错了。
还有一种乱码是“部分正常部分乱码”,比如英文字母正常,数字乱码。这种情况通常是字库覆盖范围的问题,屏幕内置字库可能不包含某些符号。解决办法是换字号或者换字库。
6.3 屏幕闪烁或ESP32复位:电源在报警
屏幕闪烁、ESP32频繁复位,十有八九是供电不足。屏幕背光启动瞬间电流很大,如果和ESP32共用USB供电,电压会被拉低,导致ESP32欠压复位。解决办法是给屏幕单独供电,或者在背光引脚上加一个大电容缓冲。
我试过在屏幕VCC和GND之间并一个470μF的电解电容,闪烁问题明显改善。电容的作用是在电流突变时提供缓冲,相当于给屏幕配了一个小水库。
6.4 指令发出去了但屏幕只执行一半
有时候屏幕会“吃掉”一部分指令,比如坐标对了但文字没显示,或者文字显示了但颜色不对。这通常是帧结构的问题:帧头帧尾不对、字节序不对、或者校验和算错了。逐字节对照手册,用串口助手手动发一遍,看屏幕反应,能快速定位问题。
还有一种可能是发送速度太快,屏幕来不及处理。在两条指令之间加一个delay(10)到delay(50),给屏幕一点处理时间。特别是连续发多条指令时,这个延时很关键。
7. 进阶思路:让文字显示更实用
7.1 用变量和格式化输出组织内容
实际项目里,显示的内容往往是动态的。用sprintf把数值格式化成字符串,再发给屏幕,是最常用的做法。注意缓冲区要足够大,避免溢出。
char buf[32]; float temp = 25.6; sprintf(buf, "Temp: %.1f C", temp); sendText(10, 100, 16, 0xFFFF, buf);浮点数格式化在ESP32上默认可能不支持,需要在Arduino IDE里开启“Enable full printf”选项,或者用dtostrf函数替代。
7.2 多页面与状态切换
如果项目需要显示多个页面,比如主界面、设置界面、数据界面,可以用一个变量记录当前页面,根据页面值决定显示哪些内容。切换页面时先清屏,再绘制新页面的内容。清屏指令通常比逐区域清除更快,但会闪一下。如果屏幕支持双缓冲或者页面切换指令,体验会更好。
7.3 与传感器结合的实际案例
串口屏文字显示最常见的应用就是配合传感器做数据展示。比如接一个温湿度传感器,每隔几秒读一次数据,格式化后显示在屏幕上。这个过程中,ESP32负责读取传感器、处理数据、发送显示指令,串口屏负责呈现。整个链路清晰,调试也方便。
我在一个环境监测的小项目里就是这么做的:ESP32通过I2C读温湿度,通过UART发给串口屏,屏幕上分三行显示温度、湿度、更新时间。代码量不大,但实用性很强。后来加了按键切换页面,可以看历史最大值最小值,整个交互就完整了。
7.4 性能与稳定性的长期观察
长时间运行后,可能会遇到屏幕花屏、通信卡死等问题。我的经验是加一个看门狗,定期检查通信状态。如果连续几次发送失败,就重新初始化UART和屏幕。另外,避免在中断里发送串口数据,中断里发数据容易导致时序问题。把要发送的内容放到队列里,在主循环里统一发送,稳定性会好很多。
还有一点:串口屏的指令响应需要时间,不要在主循环里无延时地连续发送。加一个状态机,等上一条指令的响应(如果有)或者固定延时后再发下一条。这样虽然刷新率低一点,但系统更稳。
8. 我个人的几条实操心得
第一,买屏幕的时候一定要找卖家要指令手册和例程。没有手册的屏幕,除非你愿意花大量时间逆向,否则不要碰。手册里的指令格式、波特率、字库说明,能帮你省下无数个夜晚。
第二,先用USB转TTL模块在电脑上把屏幕调通,再接到ESP32上。电脑上用串口助手发指令,屏幕能正常显示,说明屏幕本身没问题。然后再把同样的指令通过ESP32发,这样能把问题范围缩小到ESP32端。
第三,中文显示不要硬编码UTF-8字符串,用取模软件生成GBK字节数组。虽然代码看起来不够优雅,但省去了编码转换的麻烦,显示效果也稳定。
第四,供电一定要留余量。屏幕背光、ESP32、传感器加起来,电流可能超过USB口的输出能力。给屏幕单独供电,或者在电源端加足够的滤波电容,能避免很多玄学问题。
第五,调试信息走UART0,屏幕指令走UART2。不要把调试信息和屏幕指令混在同一个串口上,否则你会在串口监视器里看到一堆十六进制乱码,根本没法调试。
第六,指令之间加延时。屏幕不是电脑,处理速度有限。连续发指令时,每条之间加10到50毫秒延时,能显著降低丢指令的概率。
第七,先跑通ASCII,再折腾中文。ASCII显示正常了,说明通信链路、波特率、指令格式都没问题。这时候再处理中文编码,问题范围就小很多。
第八,保留一个“最小可运行例程”。每次改代码改出问题了,就回到最小例程,确认硬件没问题,再逐步加功能。这样能快速定位是哪一步引入的问题。
这些经验都是我在实际项目中踩坑踩出来的,希望对你有用。串口屏文字显示本身不复杂,但细节很多,把细节处理好了,后面做更复杂的界面就轻松了。