
1. 项目缘起与整体设计思路1.1 为什么选择串口屏做显示方案做过嵌入式项目的人大概都有这种体会主控芯片选型、传感器驱动、通信协议这些底层逻辑都跑通了结果卡在“怎么把数据展示出来”这一步。用OLED小屏吧显示内容有限做个稍微复杂点的界面就得自己写菜单框架用TFT彩屏加触摸吧光是GUI移植和界面布局就能吃掉好几天时间用手机APP吧又得考虑配网、协议、跨平台兼容性开发链路一下子拉长了。串口屏这个方案说白了就是把“显示”这件事外包出去。屏幕模组自己带一颗独立的处理芯片和存储空间界面上的控件、图片、字库都提前烧录在里面主控只需要通过串口发送简单的指令就能控制页面切换、文本更新、数值显示。对于ESP32这类资源有限但又需要一定交互能力的场景来说这个分工非常合理。我这次用的是一块常见的3.5寸串口屏模组分辨率480×320支持触摸内置字库和基本的控件系统。ESP32这边只负责两件事一是通过UART把要显示的文字发过去二是接收屏幕发回来的触摸事件。整个通信协议简单到用几十行代码就能跑通不需要移植任何图形库。1.2 硬件选型与接线逻辑ESP32的UART资源比较灵活一共有三个硬件串口其中UART0默认被日志输出占用UART1和UART2可以自由映射到任意GPIO。我习惯用UART2来做外设通信因为它的引脚分配最灵活不会和下载电路冲突。接线方面串口屏和ESP32之间只需要三根线TX、RX、GND。注意这里是交叉连接——ESP32的TX接屏幕的RXESP32的RX接屏幕的TX。供电方面大部分3.5寸串口屏的功耗在200mA到400mA之间峰值可能更高所以不建议直接从ESP32的3.3V引脚取电最好用独立的5V供电或者从开发板的5V引脚引出来。有个细节容易被忽略串口屏的IO电平。市面上大部分串口屏是3.3V电平兼容的但也有部分老款模组是5V电平。如果屏幕的RX引脚不接受3.3V高电平通信就会不稳定甚至完全没反应。我一般会在接线前先用万用表量一下屏幕TX引脚的空闲电平确认是3.3V还是5V。如果是5VESP32的RX引脚需要加一个电平转换或者简单的电阻分压。1.3 通信协议的基本框架串口屏的通信协议通常是一套自定义的帧格式不同厂家略有差异但核心结构大同小异。以我手头这块屏为例一帧数据的基本结构是帧头2字节 数据长度1字节 指令1字节 数据载荷N字节 帧尾2字节。帧头一般是固定的十六进制数比如0x5A 0xA5用来标识一帧数据的开始。数据长度表示从指令字节到数据载荷结束的总字节数。指令字节决定了这帧数据要执行什么操作比如0x01是更新文本控件0x02是切换页面0x03是读取触摸事件。数据载荷就是具体的参数比如控件ID、字符串内容等。这种协议设计的好处是解析逻辑非常清晰ESP32端只需要一个状态机就能处理接收发送端则按照固定格式拼包即可。坏处是不同厂家的指令集不通用换一块屏就得重新对照手册改代码。不过好在大部分常用指令文本更新、数值更新、页面切换的语义是相似的改起来也就是换个指令码的事。注意串口屏的波特率默认通常是9600或115200建议在调试阶段先用9600确认通信正常后再提高到115200。高波特率下如果杜邦线太长或者接触不良容易出现丢包。2. 核心细节解析与实操要点2.1 串口屏的控件体系与文本更新机制串口屏的界面不是用代码画出来的而是在PC端的组态软件里拖拽生成的。你可以把它理解成一个极简版的UI设计工具左边是控件库右边是画布拖一个文本控件到画布上设置好坐标、字体、颜色然后给它分配一个唯一的控件ID。这个ID就是ESP32和屏幕之间沟通的“地址”。文本更新的指令通常包含三个关键参数控件ID、字符串内容、字符串长度。有些屏幕还支持附加参数比如是否覆盖显示、是否自动换行、对齐方式等。我用的这块屏文本更新指令的格式是0x5A 0xA5 0x0A 0x01 0x00 0x01 0x05 H e l l o 0x0D 0x0A。其中0x00 0x01是控件ID0x05是字符串长度后面跟着五个字节的ASCII字符。这里有个坑字符串长度必须是实际字节数不是字符数。如果你要显示中文一个汉字在GBK编码下占两个字节在UTF-8下占三个字节。串口屏内置的字库通常是GBK编码的所以发中文之前要确认编码格式。我一般会在ESP32端把UTF-8字符串转成GBK再发送转换表可以用现成的库也可以自己写一个简单的映射。另一个需要注意的点是字符串的结束符。有些屏幕要求字符串以\0结尾有些则不需要长度字段已经说明了字节数。这个必须对照手册确认发错了要么显示乱码要么直接没反应。2.2 ESP32端UART配置的关键参数ESP32的UART配置看起来简单但有几个参数如果设错了通信就会时好时坏。首先是波特率必须和屏幕端完全一致误差不能超过2%。ESP32的UART时钟源默认是APB时钟80MHz分频后一般能精确匹配常用波特率。但如果你用了非标准波特率比如57600或者256000就要算一下分频系数是否整数否则累积误差会导致帧错误。数据位、停止位、校验位这三个参数通常固定为8N1即8位数据位、无校验、1位停止位。但有些屏幕默认是8E1偶校验如果你没改过来收到的数据会全是乱码。我一般会在初始化时把这三个参数都显式设置一遍避免依赖默认值。流控方面除非你的数据量特别大或者屏幕处理速度跟不上否则不需要开启硬件流控。软件流控XON/XOFF在二进制协议里基本不能用因为数据载荷里可能出现和流控字符相同的字节导致误触发。接收缓冲区的大小也值得注意。ESP32的UART FIFO只有128字节如果屏幕发回来的触摸事件比较频繁或者你一次发送的字符串很长就需要在驱动层加一个环形缓冲区否则容易丢数据。我一般会开一个512字节的环形缓冲区用中断或者FreeRTOS任务来处理收发。2.3 文本编码转换的实操细节前面提到了GBK和UTF-8的转换问题这里展开说一下具体怎么做。ESP32的ESP-IDF默认使用UTF-8编码而串口屏的字库通常是GBK。如果你直接发UTF-8的中文字符串屏幕上会显示一堆乱码或者问号。最简单的解决方案是在PC端组态软件里把字库设置成UTF-8但很多屏幕不支持这个选项。那就只能在ESP32端做转换。转换的方法有两种一是用现成的转换表把常用汉字的UTF-8和GBK编码对应关系存成一个数组查表转换二是用开源的转换库比如iconv的嵌入式移植版。我一般用第一种方法因为常用汉字的数量有限一个包含几千个汉字的转换表大概占几十KB的Flash空间对于ESP32来说完全可以接受。转换的时候先判断当前字节是不是ASCII字符小于0x80如果是就直接透传如果是多字节UTF-8序列就查表找到对应的GBK编码替换进去。实操心得如果你的项目只需要显示固定的几个中文词汇比如“温度”“湿度”“设置”完全可以在代码里直接写GBK编码的字节数组省去转换表的开销。我做过一个环境监测的项目界面上只有十来个中文词直接硬编码GBK字节代码简洁又高效。3. 实操过程与核心环节实现3.1 硬件连接与供电检查先把硬件连起来。我用的ESP32开发板是常见的DevKitC款式串口屏是3.5寸电阻触摸屏供电5V通信3.3V电平。接线如下ESP32引脚串口屏引脚说明GPIO17RXESP32发送屏幕接收GPIO16TXESP32接收屏幕发送GNDGND共地5VVCC供电建议独立5V接好线之后先别急着写代码。用USB转TTL模块单独连屏幕在PC上用串口助手发几条测试指令确认屏幕能正常响应。这一步很关键因为如果屏幕本身有问题你在ESP32端怎么调都是白费功夫。测试指令可以用厂家提供的示例比如切换页面、更新文本。发完之后观察屏幕反应如果没反应先检查波特率、接线、供电。我遇到过好几次屏幕没反应最后发现是供电电流不够屏幕背光亮了但主控没启动。换一个能输出1A以上的5V电源就好了。3.2 ESP32端代码框架搭建代码部分我分成三个模块UART驱动、协议封装、业务逻辑。UART驱动负责初始化和收发协议封装负责拼包和解包业务逻辑就是具体的显示更新。UART初始化代码如下#include driver/uart.h #include driver/gpio.h #define UART_PORT_NUM UART_NUM_2 #define UART_TX_PIN GPIO_NUM_17 #define UART_RX_PIN GPIO_NUM_16 #define UART_BAUD_RATE 115200 #define BUF_SIZE 512 void uart_init(void) { uart_config_t uart_config { .baud_rate UART_BAUD_RATE, .data_bits UART_DATA_8_BITS, .parity UART_PARITY_DISABLE, .stop_bits UART_STOP_BITS_1, .flow_ctrl UART_HW_FLOWCTRL_DISABLE, .source_clk UART_SCLK_APB, }; uart_driver_install(UART_PORT_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 0, NULL, 0); uart_param_config(UART_PORT_NUM, uart_config); uart_set_pin(UART_PORT_NUM, UART_TX_PIN, UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); }这段代码里uart_driver_install的第二个和第三个参数分别是接收缓冲区和发送缓冲区的大小。我设成了1024字节足够应付大部分场景。如果你要发送长文本比如一整段说明文字可以适当加大。协议封装部分我写了一个简单的函数来发送文本更新指令void screen_update_text(uint16_t widget_id, const char *text, uint8_t len) { uint8_t frame[256]; uint8_t idx 0; frame[idx] 0x5A; frame[idx] 0xA5; frame[idx] len 4; // 长度字段指令1字节 控件ID2字节 字符串len字节 校验1字节 frame[idx] 0x01; // 指令码更新文本 frame[idx] (widget_id 8) 0xFF; frame[idx] widget_id 0xFF; memcpy(frame[idx], text, len); idx len; // 校验和从长度字段到数据载荷结束的所有字节累加取低8位 uint8_t checksum 0; for (uint8_t i 2; i idx; i) { checksum frame[i]; } frame[idx] checksum; uart_write_bytes(UART_PORT_NUM, (const char *)frame, idx); }这个函数里有个细节长度字段的计算方式。不同厂家的定义可能不同有的把帧头也算进去有的只算数据载荷。我用的这块屏是从指令码开始算所以是len 4。这个必须对照手册确认算错了屏幕会直接丢弃整帧数据。3.3 触摸事件的接收与解析串口屏的触摸事件是主动上报的当用户点击屏幕上的按钮或者触摸区域时屏幕会通过串口发一帧数据给ESP32。这帧数据的格式通常是帧头 长度 指令码0x03表示触摸事件 控件ID 事件类型按下/弹起 校验。ESP32端需要一个任务来持续读取UART数据解析出触摸事件后再根据控件ID执行对应的业务逻辑。我一般用FreeRTOS任务来做这件事void uart_rx_task(void *arg) { uint8_t buf[256]; while (1) { int len uart_read_bytes(UART_PORT_NUM, buf, sizeof(buf), pdMS_TO_TICKS(100)); if (len 0) { parse_screen_frame(buf, len); } } }parse_screen_frame函数里实现一个状态机先找帧头再读长度然后根据指令码分发处理。触摸事件的处理逻辑一般是如果控件ID是按钮A就执行某个动作如果是按钮B就执行另一个动作。这里有个容易踩的坑触摸事件可能会连发多次。用户按一下按钮屏幕可能上报“按下”和“弹起”两个事件如果你在“按下”时就执行了动作在“弹起”时又执行一次就会重复触发。我一般只在“弹起”事件里执行业务逻辑或者在代码里加一个简单的去抖逻辑。3.4 实际显示效果调试代码写完之后烧录到ESP32观察屏幕反应。第一次调试的时候我遇到了几个问题第一个问题是屏幕完全没反应。用逻辑分析仪抓了一下ESP32的TX引脚发现数据确实发出去了但屏幕没动静。后来发现是波特率设错了屏幕默认是9600我设成了115200。改过来之后屏幕正常显示了。第二个问题是中文显示乱码。我发的是UTF-8编码的“温度25℃”屏幕上显示的是几个问号。这就是前面说的编码问题改成GBK编码之后正常了。第三个问题是文本更新有延迟。我每隔1秒更新一次数值但屏幕上要过两三秒才变。后来发现是屏幕的刷新机制问题有些屏幕在收到新文本后不会立即刷新需要发送一个“刷新”指令或者等待屏幕自己的刷新周期。我在每次更新文本后加了一个短延时问题就解决了。实操心得调试串口屏的时候强烈建议用一个USB转TTL模块同时监听ESP32和屏幕之间的通信。这样你能看到双方到底发了什么是ESP32没发出去还是屏幕没响应一目了然。我用的是一款带独立收发缓冲的监听工具能同时抓双向数据排查效率比盲猜高得多。4. 常见问题与排查技巧实录4.1 通信失败类问题速查现象可能原因排查方法解决方案屏幕完全无反应供电不足测量屏幕VCC引脚电压换用独立5V/1A以上电源屏幕背光亮但无显示主控未启动测量屏幕TX引脚空闲电平检查供电电流是否足够发送数据后屏幕乱码波特率不匹配用示波器测量实际波特率统一两端波特率中文显示问号编码格式错误检查发送的字节序列转为GBK编码触摸事件无响应屏幕未开启上报检查屏幕配置在组态软件中开启触摸上报数据偶尔丢失缓冲区溢出检查接收缓冲区大小加大环形缓冲区长文本显示不全长度字段错误核对手册中的长度定义修正长度计算方式这个表格里的问题我几乎每一个都实际遇到过。最让人头疼的是“偶尔丢失数据”因为不是必现的排查起来很费时间。后来用逻辑分析仪抓了很久才发现是接收缓冲区太小屏幕连续上报触摸事件时ESP32来不及处理后面的数据就被覆盖了。把缓冲区从128字节加大到1024字节之后再也没丢过。4.2 显示效果类问题的处理经验显示效果方面最常见的问题是文字重叠和刷新闪烁。文字重叠通常是因为新文本比旧文本长覆盖了后面的内容。解决方法是在更新文本前先发送一个“清空控件”指令或者确保新文本长度不超过旧文本。我一般会在组态软件里把文本控件的宽度设得足够大并且在代码里限制字符串的最大长度。刷新闪烁的问题有些屏幕在更新文本时会先清空整个区域再重绘如果更新频率太高肉眼就能看到闪烁。解决方法是降低更新频率或者使用屏幕的“局部刷新”功能如果支持的话。我做过一个实时数据显示的项目刷新频率是10Hz屏幕闪烁很明显。后来改成只在数值变化超过一定阈值时才更新闪烁就基本消失了。还有一个细节屏幕的背光亮度。有些屏幕默认背光是100%在室内环境下太刺眼而且功耗也高。可以在组态软件里设置默认亮度或者通过指令动态调节。我一般会把默认亮度设成70%左右既看得清又不会太耗电。4.3 稳定性与长期运行注意事项如果你的项目需要长时间运行比如7×24小时不间断工作有几个地方需要特别注意。首先是UART的错误处理。ESP32的UART在受到干扰时可能会产生帧错误或者奇偶校验错误如果不处理错误会累积最终导致通信完全中断。我一般会在UART驱动里注册一个事件回调检测到错误时自动清除错误标志并重新初始化UART。其次是屏幕的发热问题。串口屏的背光LED和主控芯片都会发热如果散热不好长时间运行后屏幕可能会变暗或者触摸失灵。我一般会在屏幕背面贴一块小散热片或者在外壳上开散热孔。实测下来加散热片之后屏幕表面温度能降低10℃左右。最后是看门狗。ESP32的硬件看门狗和任务看门狗都要开启防止程序跑飞。UART接收任务里要定期喂狗但也不能喂得太频繁否则看门狗就失去意义了。我一般设置看门狗超时为5秒任务里每1秒喂一次。注意如果屏幕和ESP32之间的连线较长超过20厘米建议使用屏蔽线并且在屏幕端加一个100nF的滤波电容。我遇到过因为连线太长导致通信不稳定的情况换了屏蔽线之后问题就消失了。4.4 性能优化与资源占用分析ESP32的资源虽然不算紧张但如果你同时跑WiFi、蓝牙和串口屏通信还是要注意一下资源分配。UART接收任务我一般分配4096字节的栈空间优先级设为5比WiFi任务低比空闲任务高。发送任务如果只是偶尔发一帧数据可以直接在业务逻辑里调用不需要单独开任务。内存方面UART驱动本身会占用一部分DMA缓冲区加上我自己的环形缓冲区总共大概2KB左右。对于ESP32的520KB SRAM来说这个开销完全可以接受。如果你用的是ESP32-C3或者S2系列SRAM小一些但跑串口屏通信也绰绰有余。CPU占用方面115200波特率下每字节的传输时间是87微秒。一帧20字节的数据传输时间大概1.7毫秒。即使每秒发送100帧CPU占用也不到1%。所以串口屏通信本身对CPU的负担很轻主要的开销还是在业务逻辑和网络通信上。我在实际项目中做过一个测试ESP32同时跑WiFi MQTT、串口屏通信和传感器采集CPU占用率大概在30%左右内存剩余200KB以上。这个资源余量对于大部分物联网项目来说都够用了。4.5 不同屏幕型号的适配经验市面上的串口屏品牌和型号很多指令集和组态软件各不相同。我前后用过五六种不同的屏幕总结下来有几个适配要点。第一是确认指令集。有些屏幕的文本更新指令只需要控件ID和字符串有些还需要指定字体、颜色、对齐方式。适配新屏幕的第一步就是拿到指令手册把常用的几条指令文本更新、数值更新、页面切换、触摸上报的格式整理出来写成宏定义或者函数。第二是确认字库。有些屏幕内置了中文字库可以直接显示中文有些只支持ASCII显示中文需要自己生成字库文件烧录进去。如果屏幕不支持中文而你的项目又必须显示中文那就只能换屏幕或者用图片代替文字。第三是确认触摸类型。电阻屏和电容屏的触摸事件格式可能不同电阻屏通常上报坐标电容屏通常上报控件ID。如果你的项目依赖触摸交互选型时一定要确认屏幕支持哪种上报方式。第四是确认刷新机制。有些屏幕是即时刷新收到指令立刻更新显示有些是周期刷新比如每100毫秒刷新一次。如果你的项目对实时性要求高就要选即时刷新的屏幕或者在代码里加一个刷新指令。我个人的经验是选串口屏的时候不要只看价格和尺寸一定要先拿一块样品测试把常用的功能都跑一遍确认没有坑之后再批量采购。我吃过一次亏批量买了二十块屏结果发现触摸上报有延迟项目进度被拖了好几天。后来换了一个型号虽然贵了一点但省心很多。