ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ESP32+ST7789显示中文实战:乱码、偏移与性能优化的解决之道

ESP32+ST7789显示中文实战:乱码、偏移与性能优化的解决之道 有一说一ESP32搭配ST7789这颗屏驱动芯片本身不难随便接几根线跑个官方例程点亮屏幕、画个方块、刷个图片基本半小时就能搞定。但一旦把需求从“Hello World”变成“让屏幕显示中文”事情就开始往奇怪的方向发展。我这次做的项目是个小桌面信息终端核心功能就是一块240x240的IPS屏上显示温湿度、时间、状态提醒之类的中文信息。就这么个需求硬是让我从周四晚上折腾到周六下午前后两天踩了三个大坑。每个坑单独看都不算深但串在一起再加上网上一堆互相矛盾的说法就非常消磨耐心。这篇文章不打算写完整的项目教程那些TFT_eSPI的入门流程网上一抓一大把。我重点把“显示中文”这条路上最容易卡住普通人的三个问题拆开讲清楚编码和字库、ST7789的参数配置、刷新性能的调优。每个问题我都会说清楚现象、根因、排查过程和最终的解决方案顺便把一些我试过但没用的弯路也写出来帮你省掉那两天的时间。先说结论这三个坑分别是UTF-8编码和GB2312字库索引错位导致的中文乱码、ST7789模块因各个厂家裁切不同而需要手动调整偏移量同时颜色格式还分RGB和BGR、以及全屏刷新的性能瓶颈和内存分配不合理造成的闪烁卡顿。下面逐个说。1. 项目背景为什么选择ESP32和ST7789以及整体方案设计1.1 选型思路ESP32的性价比和ST7789的适用场景先交代一下背景。我要做的这块信息屏核心诉求有三个能联网拿数据、能无线调试、能稳定驱动一块小尺寸屏幕。ESP32基本是绕不开的选择。它的双核240MHz处理器跑UI逻辑绰绰有余Wi-Fi和蓝牙都在一颗芯片里价格还便宜开发环境也成熟。作为一个桌面摆件级的项目ESP32的功耗不是问题重点是开发效率和扩展性。屏幕方面我选的是1.54英寸、240x240分辨率的IPS屏驱动芯片是ST7789。为什么不用更常见的0.96寸OLEDSSD1306因为那个屏像素太少只有128x64显示中文只能用小号字库而且OLED的物理尺寸太小放桌上根本看不清。ST7789的240x240分辨率配合IPS广视角做信息看板非常合适色彩表现也鲜艳。另一个原因是ST7789和ILI9341的程序结构非常接近这次调通了以后换大屏也能复用大部分代码。1.2 整体方案显示中文的三种常见路线在开始动手之前我先梳理了一下在ESP32上显示中文的常见路线这也是网上讨论最多的部分。第一种是使用u8g2库。这个库对中文的支持非常友好内置了多种中文字体API简单调用u8g2.setFont()然后u8g2.drawStr()就能直接显示中文字符串。它的原理是内部实现了UTF-8解码自动从字库里检索对应的点阵数据。听起来很完美但缺点是字库占空间很大比如一个16x16的GB2312全量字库就有几百KB虽然可以裁剪但裁剪之后新增汉字又得重新处理灵活性一般。第二种是使用LVGL等GUI框架。LVGL对中文的支持也很完善它内置了字体转换工具能把你需要的文字做成自定义字体文件。这个方案适合界面复杂、有交互逻辑的项目但学习成本高对于我这种只需要在固定位置显示几个固定字段的小工具来说有点“杀鸡用牛刀”。第三种是我最终选择的自定义方案自己准备字库文件把中文转成字模数组然后通过像素操作函数直接画点阵。这个方案最繁琐但可控性最强esp32项目里各种花式显示效果全靠这个方案。而且不依赖特定的图形库以后想换驱动、想加特效都能自己控制。我不建议新手一上来就直接用第三种方案但如果你像我一样有定制化显示需求比如要显示特殊符号、要旋转字体、要实现打字机效果那这条路迟早要走。我这次也是因为项目里有几个固定的界面切换每个界面的中文都是预知的数量不多所以自己搞字库反而最舒服。2. 第一个坑中文乱码的真相——UTF-8编码和字库索引错位2.1 问题现象汉字变成一团乱麻第一天晚上我按照最简单的方式写了个测试程序。先在PCtoLCD2002里用16x16点阵、逐行式取模方式生成了“温度”两个字对应的字模数组然后把数组硬编码到代码里想着像显示英文一样直接调用画点函数把数组里的每一位画到屏幕上。结果一上电屏幕上显示的两个汉字完全看不出来是什么字。仔细看能看出有笔画轮廓但笔画全都错位了像是把一张完整的字模图切成竖条然后左右颠倒又重新拼起来还有一些点点块块的噪声。我当时第一反应是取模方式不对于是翻来覆去换了好几种取模方式逐行式、逐列式、逆向逐行、逆向逐列全都试了一遍。每次重新生成数组、重新编译、重新烧录折腾了一个多小时始终没有一次正确显示。2.2 根因分析你以为的字符和编译器理解的字符不一样后来冷静下来用一个最简单的方式测试我不用“温度”这两个字而是直接用自己手写的数组比如一个全0xFF的数组或者一个只有左上角有填黑的数组看屏幕上显示的形状。这个测试让我发现问题不只在取模方式而在于中文字符串的处理方式。这里要讲一个很多新手都会忽略的细节Arduino IDE对于源代码的编码方式是有默认处理的。在Arduino环境下.cpp文件保存为UTF-8格式是常态编译器里的字符串常量也是UTF-8编码的。而“温度”这两个汉字在UTF-8编码下每个汉字占3个字节不是2个字节。我怎么知道我的代码里存的到底是什么最简单的办法是在串口监视器里直接把printf(%02X , str[i])打印出来。我试了一下屏幕上那几行十六进制数据每个汉字对应的3个字节和GB2312的2字节编码完全不同。也就是说我按照GB2312的双字节编码去查字库查到的地址根本就是错的。这才是乱码的真正原因不是点阵数据错了而是用UTF-8的三字节序列去索引一个按GB2312双字节编码生成的字库索引完全对不上号。网上很多中文显示教程里字库是按GB2312或者Unicode或UCS2组织的教程因为环境不同没有踩到这个编码的坑于是“显示中文先取模”这个结论看着简单实际执行时却各种偏差。2.3 解决方案统一编码还是在字库里直接按Unicode索引搞清楚了根因解决思路就清楚了要么让代码里的字符串使用GB2312编码或者GBK要么让字库按Unicode/UCS2编码来组织用UTF-8解码后的Unicode码点去索引。方案A把源代码转成GB2312编码保存。这个方法最简单在Arduino IDE或VS Code里把文件编码改成GB2312然后把字符串硬编码进去。但我个人不建议长期依赖这个方案因为很多现代编辑工具默认强制UTF-8而且一旦和别人协作文件编码混用会出现各种莫名其妙的编译错误。方案B程序先把UTF-8字符串解码成Unicode码点然后用码点去索引一个按Unicode从小到大排列的字库。这个方法代码量适中也是我最终采用的方式。核心代码可以这样写// 这里的font_16x16[] 是一个按Unicode码点递增排列的字模数组 // 首先实现UTF-8解码得到当前字符的Unicode码点 uint16_t utf8_decode(const char *str, uint16_t *index) { uint8_t c str[*index]; uint16_t code 0; if (c 0x80) { code c; (*index); } else if (c 5 0x6) { code ((c 0x1F) 6) | (str[(*index) 1] 0x3F); *index 2; } else if (c 4 0xE) { code ((c 0x0F) 12) | ((str[(*index) 1] 0x3F) 6) | (str[(*index) 2] 0x3F); *index 3; } return code; } // 然后在字模数组中二分查找找到code对应的字模数据 // 每个字模大小是 16行 × 2字节 32字节这种做法的优点是源代码保持UTF-8和现代工具链完全兼容字库表一劳永逸增加新汉字时往数组里追加一项就行而且你不需要依赖任何特定库。缺点是需要自己实现一次UTF-8解码以及维护一个“Unicode码点和字模数组下标”的映射关系。为了省事我还见过有人直接在程序里写一大堆if (code 0x6E29) 则返回“温”字对应的字模这种方式代码难看但胜在直观适合只有几个汉字的场景。我当时测试时就是这么干的方便快速验证。3. 第二个坑ST7789屏显示偏位和颜色不对问题出在驱动参数3.1 现象屏幕底色正常但内容整体偏移颜色发紫发蓝解决了中文乱码之后我满以为万事大吉结果把界面一画发现新的问题屏幕上显示的内容整体往右下方偏了一截最明显的是顶部的状态栏跑到屏幕中央去了。而且是那种“内容在正确渲染但坐标系不对”的感觉。我明明画了左上角的边框边框却出现在屏幕中下部。同时颜色也不对红色变成蓝色绿色变成紫色。这两种颜色错乱的问题虽然表面看起来一个是位置、一个是颜色但实际上都源于ST7789驱动初始化的配置参数不对。3.2 根因ST7789厂商裁切和“RGB/BGR”颜色顺序先解释颜色错乱。ST7789是一颗支持RGB565格式的驱动芯片理论上通过SPI发送两个字节就可以控制一个像素的颜色。但问题在于屏幕厂家在生产模组时把RGB三个子像素的排列方式可以做成RGB也可以做成BGR。如果你按照RGB方式发送颜色数据而屏幕物理排列是BGR那你看到的红和蓝就会互换。红色显示成蓝色蓝色显示成红色杂色就会变成偏紫偏绿。这个在TFT_eSPI这类库中有一个专门的开关在用户配置文件里找到#define TFT_RGB_ORDER TFT_BGR或者#define TFT_RGB_ORDER TFT_RGB把宏改一下就行。但如果你用的不是这类库而是自己写初始化序列或者用通用SPI库就得自己处理字节序。具体来说如果你手头只有通用SPI接口函数可以在发送颜色数据前交换高低字节uint16_t swap_color(uint16_t color) { return (color 8) | (color 8); }对很多模组来说这行代码就能修复颜色错乱。不少在STM32上做过LCD驱动的人对setSwapBytes(true)这个函数应该不陌生TFT_eSPI里就是用它来控制字节交换。这一行的作用就是告诉库你发送的每个像素颜色需要交换高低字节然后ST7789才能按正确的BGR顺序解析。再说位置偏移。240x240的屏幕模块市面上很多用的其实是240x320的玻璃然后厂家通过驱动IC的“窗口模式”只露出240x240的区域这样同一块ST7789芯片就能驱动多个尺寸的屏幕。问题在于不同厂家露出的区域在驱动IC看来起始坐标不一样。有些模块的行起始位置是0有些是80还有些是40。如果你按照默认的0作为起点显示内容就会整体偏移。怎么确认偏移量我在TFT_eSPI里用了一种最土但最有效的方式先全屏刷一种颜色比如红色然后逐步修改初始化序列里的_offset值同时用手机拍下屏幕现象。通过屏幕边缘是否有杂色条、显示是否居中就能判断偏移量。注意不同封装的ST7789模块偏移量可能是0、40或者80甚至可能是负数——我之前在一些资料里看到过全局偏移为 (0, 80) 的写法。TFT_eSPI里对应的配置项是#define TFT_WIDTH 240 #define TFT_HEIGHT 240 // 偏移量在User_Setup.h里通常不是直接暴露的需要改驱动文件 // 但有些版本提供了 MADCTL 配置可以配合SetRotation使用如果你的屏幕是自己从原厂拿的“裸屏”确定偏移量的标准方法是读ST7789的初始化命令设置CASET列地址0x2A和RASET行地址0x2B。CASET和RASET里设置的地址范围决定了驱动IC认为屏幕的行列起始坐标。如果RASET从80开始到319结束那这个屏就是240x320的玻璃上方有80行不显示。在这种情况下你一旦用setAddrWindow(x, y, x w - 1, y h - 1)设置坐标就不能直接用一个独立的“偏移”参数去加而是要正确设置行列起始地址。我在调试时写了一个测试程序轮换设置不同的偏移值并填充纯色大概花了半小时通过色块的位置找到了我这块屏幕的偏移规律。这个问题说到底是厂家兼容性问题不同批次都可能不一样网上查到的“标准答案”不一定适用于你的屏。3.3 SPI通信参数的坑频率太高一样会花屏还有一个和ST7789参数相关的问题是SPI速率。说实话这个问题一开始我完全没怀疑到。因为用TFT_eSPI默认的40MHz SPI频率显示英文和图片都正常但我在刷新整个界面的时候偶尔会在屏幕顶部出现一条横向的噪点带看起来像撕裂又有点像花屏。后来我把SPI频率调到20MHz一切恢复正常。ESP32的SPI外设最高可以跑到80MHz但ST7789不是所有批次都能从容应对高速率信号尤其是当你的杜邦线比较长、接线又不规范的时候信号质量会变差。我建议是优先用40MHz如果花屏、噪点、偶尔黑线就降到20MHz或26MHz对显示静态信息来说帧率没有明显差别。如果你用的是普通的蓝色PCB模块还要注意背光电流。有些模块背光引脚直接接3.3V最大电流达数十毫安如果板子的稳压芯片质量一般背光一开主控电压就会被拖低导致SPI信号电平异常出现随机花屏。这个问题我在另一个屏上遇到过这次提前改成用独立引脚控制背光PWM就一劳永逸了。4. 第三个坑界面刷新卡顿和内存分配不合理的性能调优4.1 现象全屏刷新时可见明显闪烁切换界面卡半秒中文乱码修好了坐标偏了也修好了正当我准备收工时新的不舒服出现了。我的界面里有一个“温度”的大数字每秒刷新一次还有一个时间显示每分钟刷新一次。问题在于每次数据变化我都是整屏重绘先fillScreen()清屏然后把所有控件重新画一遍。结果就是屏幕以肉眼可见的速度闪来闪去尤其是整屏刷新的那一瞬间亮度忽然变低再忽然恢复。这对一个桌面摆件来说体验太差了。我用示波器粗粗看了一下列总线发现一次全屏重绘大概要250ms左右也就是每秒只能刷不到4次全屏。虽然只是刷新局部信息但整屏重绘实在太浪费而且会让人感觉“卡”。4.2 性能瓶颈不是ST7789慢而是你的刷新策略有问题这里需要理清一个基本概念ST7789的刷新速度其实能到60帧甚至更高你自己画像素才是性能瓶颈。TFT_eSPI这种库底层已经做了大量的SPI优化比如连续写多个像素时只发一次地址。但如果你每次画一个点就调用一次drawPixel那SPI的吞吐量就全浪费在发送重复的地址命令上了。更常见的性能问题是很多库函数在高频调用时存在大量栈操作。比如我自己写显示汉字函数最初是每个像素点做一次颜色判断然后写屏后来改成把整行点阵组合成缓冲区再一次性推给屏幕。把数据攒起来用连续写像素方式发送比逐点写快一个数量级。具体优化手段可以分三层第一层避免全屏重绘。把界面分成静态区和动态区。静态区边框、标题、图标只在初始化时画一次动态区才在数据变化时局部更新。比如温度数字变了只更新数字所在的矩形区域setAddrWindow(20, 40, 20 60, 40 16)然后重新发送这几个汉字或数字的字模整个刷新时间从250ms降到2ms以下。第二层预生成缓冲区。如果某个界面重绘比较频繁可以先把整个界面的内容画到一个内存缓冲区里然后用pushImage()一次性刷屏。但对于一块240x240的RGB565屏幕整块缓冲区需要240*240*2 115200字节ESP32虽然有320KB SRAM但也不能随便乱用特别是你还要跑Wi-Fi协议栈的时候。我的做法是只给数字区域开一个小的缓冲区比如uint16_t buf[60 * 16]这种方式既高效又不占太多内存。第三层合理使用DMA。TFT_eSPI的pushImageDMA()可以在后台搬运数据省去CPU等待SPI发送的时间。尤其适合全屏图片展示。需要注意的是一次DMA发送的数据量不能太大超过一定长度会触发底层缓冲区问题实际长度上限和SPI主机驱动的配置有关。4.3 内存分配ESP32不是内存无限别把字库全部塞进RAM内存这块也要单独说一下。如果你采用的是“把所有汉字字模头文件包含进来”的方式比如一个头文件里定义了几百个汉字每个汉字32字节几百个也就是几万字节表面上问题不大。但如果你把字库文件放在SPIFFS或者LittleFS里然后通过open()读取文件来渲染就要注意文件读取的IO缓冲和内存碎片问题。我实际测试后发现文件系统读取方式虽然省内存但每次读取都有约几毫秒的延迟如果界面有几十个汉字一帧下来延迟就很可观了。所以我的最终方案是把常用汉字做成C数组放进代码里编译到Flash把生僻字或动态内容用额外文件放在SPIFFS里。Flash空间在ESP32模块上非常充裕完全没有必要全塞进RAM。另外不要想着在ESP32上保存TrueType字体文件并用软件渲染矢量字。ESP32的240Mhz性能确实能跑FreeType但内存和Flash占用太大对于小屏幕完全没有必要。点阵字模才是效率最高的选择。5. 常见问题速查与排错思路5.1 问题现象与解决方案速查表我把这两天遇到的典型问题和最终解决方案整理成了表格方便你对照排查其中每个问题我都自己实测过不是从文档里抄来的。现象根本原因排查方法解决方案中文变成乱码/笔画错位字符串编码和字库索引方式不一致串口打印字符串的十六进制字节确认是UTF-8还是GB2312统一编码或解码UTF-8后用Unicode码点索引字库颜色中红蓝互换、杂色偏紫ST7789驱动IC的RGB/BGR顺序和厂家模组不一致发送纯红0xF800、纯绿0x07E0、纯蓝0x001F测试色块在库配置里设置TFT_RGB_ORDER或交换颜色高低字节内容整体偏移、边框位置不对240x320玻璃裁切导致的起始行列偏移全屏填充纯色逐步修改行起始地址观察屏幕边缘正确设置CASET/RASET地址范围或調整偏移量偶尔出现顶部噪点带/花屏SPI速率过高导致信号质量差降低SPI时钟频率排除供电问题使用40MHz以下频率检查接线长度和背光供电全屏刷新明显闪烁整屏重绘速度慢且每秒都在刷全部像素用示波器或者代码计时看刷新耗时局部区域刷新使用小缓冲区pushImage动态区和静态区分离界面卡顿切换界面卡半秒CPU在频繁等待SPI发送完成检查是否大量使用drawPixel类函数批量函数写入支持DMA的库开启DMA5.2 我总结的排查顺序先把显示链路走通再谈优化踩了两天坑之后我总结出一套适合这类项目的调试顺序分享出来应该能帮你省不少时间。第一步先别管中文跑一个简单的颜色测试程序。确认ST7789驱动、SPI接口和接线都没问题重点是验证颜色顺序和坐标是否正常。这一步用125%的可靠方法就是填充纯红、纯绿、纯蓝然后用眼睛直接看。如果颜色不对先处理RGB/BGR不要往后面走因为后面的所有内容都依赖颜色正确。第二步确认坐标范围正常。画一个屏幕边框再画一个对角线看四条边是否和屏幕边缘重合。如果不重合去调整偏移量或CASET/RASET。这一步通过之后你的屏幕物理显示能力就完全正常了。第三步再测试中文字库。先用一个已知字符、用手工硬编码字模测试比如自己构造一个全黑方块16x16全1确认你能用字模数组的方式显示成功。这一步走通之后中文字库的制作、编码转换、索引都没问题再回头扩展大批量汉字。第四步最后再做性能优化。这一步不必一开始就做等所有功能正常之后用代码计时统计一下各部分的耗时再决定哪些地方需要局部刷新、哪些地方用DMA。这四步走完一个稳定、流畅的中文显示功能就基本成型了。我现在做任何带屏幕的ESP32小项目都会按这个顺序来后面几乎没再遇到卡壳的时候。6. 写在最后一些不一定写在文档里的经验折腾完这批屏幕我最大的体感是嵌入式显示这块很多时候真正麻烦的不是芯片本身而是“屏幕模组不是标准品”。同一个ST7789不同厂家的模块延展出的配置差异、接线差异、质量差异可以让你把同样的代码在不同模块上重写好几遍。这个问题在中文显示场景下被进一步放大了因为字库方案和编码方案混在一起一旦出问题你很难判断到底错在哪个环节。我个人现在的习惯是每个新屏幕到手第一件事就是查模块PCB上的丝印、确认玻璃尺寸和驱动IC的完整型号。如果驱动IC型号后缀不一样直接去搜对应型号的手册看MADCTL0x36寄存器里RGB/BGR位的默认值以及有没有厂家自定义的偏移设置。这一步看着繁琐但比画试色块和试偏移要快得多。最后再分享一个实用的小技巧如果只显示少量固定的中文不要纠结于做全量字库和动态解码。你完全可以只做一个“字表”把需要用到的汉字一个一个列出来程序里直接用最土的办法查表显示。比如我在这个项目里一共只需要显示“温度、湿度、时间、状态”等十几个字我就直接写了一个结构体数组把Unicode码点和字模放一起显示时逐个匹配。代码虽然不太优雅但内存占用极小、速度极快而且完全不用碰文件系统、不用解密编码问题。等你真的需要动态显示任意汉字了再考虑引入通用字库框架也不迟。
返回列表