尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Arduino OLED显示中文乱码?SSD1306自定义汉字点阵取模与绘制全攻略

Arduino OLED显示中文乱码?SSD1306自定义汉字点阵取模与绘制全攻略 1. 从一次“屏幕全是方块”的调试说起如果你玩过一阵子 Arduino 配 0.96 寸 OLED大概率经历过这个场景代码烧录成功屏幕也亮了但本该显示“温度25℃”的地方出现了一排整齐的小方块或者干脆是几个问号。第一次遇到这种情况很多人会怀疑是屏幕坏了、I2C 地址错了、库版本不对折腾半天换线换模块最后才发现——问题根本不在硬件而在“字”上。Arduino 的 SSD1306 库默认只带一套 ASCII 字库也就是英文字母、数字和常见符号。你写display.print(Hello)没问题但写display.print(你好)库在字库里找不到对应点阵就只能画个占位方块。这不是 bug是设计如此中文字库体积太大不可能塞进默认库里。所以“显示自定义中文”这件事本质上是三件事的组合把汉字转成点阵数据、把点阵数据放进代码、让 SSD1306 按位把点阵画到屏幕上。听起来简单但每一步都有坑。取模软件的参数怎么设、字节是正序还是倒序、扫描方式选逐行还是逐列、数组怎么在代码里调用任何一个环节错了屏幕上就是一团乱码或者偏移的图形。这篇内容就是把这套流程从头到尾走一遍。我会用 SSD1306 的 I2C 版本最常见的 0.96 寸 128x64 蓝屏/白屏作为硬件基础配合 Arduino IDE 和一款常用的取模软件把“自定义中文显示”拆成可复现的步骤。适合已经能点亮 OLED、会写基础display.print的玩家也适合刚接触嵌入式显示、想搞清楚点阵原理的新手。看完之后你不仅能显示“你好”还能显示任意汉字、图标甚至自己拼一个简单的动态效果。2. 为什么 SSD1306 默认显示不了中文2.1 字库的本质一张“点阵地图”要理解乱码先得理解字是怎么变成屏幕上的像素的。SSD1306 是一块 128x64 像素的 OLED 驱动芯片每个像素只有亮和灭两种状态。一个 16x16 的汉字就是 256 个像素点每个点用 0 或 1 表示按某种顺序排列成一串二进制数据这就是“点阵字模”。英文字符简单8x8 或 5x7 的点阵就够用ASCII 一共 128 个字符全塞进库也就几百字节。但汉字有几万个常用字也有三千多每个 16x16 就是 32 字节三千字就是 96KB而 Arduino UNO 的 Flash 只有 32KBRAM 只有 2KB。把整套中文字库塞进去程序直接爆掉。所以主流做法是只取你要用的那几个字用取模软件生成点阵数组手动放进代码里。用几个字就取几个字内存占用可控这也是嵌入式中文显示最通用的思路。2.2 SSD1306 库的绘制逻辑以常用的Adafruit_SSD1306和U8g2为例它们画字符的流程是拿到一个字符编码去字库里查对应的点阵数据然后按页page写入显存。Adafruit 库的print最终调用的是drawChar而drawChar只认 ASCII 范围内的编码。你传进去一个 UTF-8 的中文字符它会被拆成多个字节每个字节都超出 ASCII 范围查不到字模就画成空白或方块。U8g2 稍微好一点它支持自定义字库但默认的中文字库要么体积大要么需要你自己用工具生成。所以无论用哪个库自定义中文的核心都是“自己提供点阵数据”库只负责把数据画出来。2.3 乱码的三种典型表现实际调试中中文显示失败通常有三种样子对应不同原因现象可能原因一排小方块字库缺失库用占位符代替图形错位、上下颠倒取模扫描方式与库的绘制方式不匹配显示半个字或花屏字节序高位在前/低位在前设置错误完全无显示数组长度不对越界或没调用绘制函数搞清楚这三种现象后面排查就有方向了。接下来先讲取模软件怎么用这是整个流程里最容易出错的一环。3. 取模软件参数怎么设才不出错3.1 取模软件的选择与基本界面市面上取模软件不少PCtoLCD2002、字模提取软件、Image2Lcd 都有人用。做中文点阵PCtoLCD2002 是最经典的一款免费、体积小、参数清晰。打开之后主要关注几个区域左上角的文字输入框、中间的预览区、右下角的参数设置区。操作流程很直接在输入框里打字设置字体和大小点“生成字模”下面就会输出一串十六进制数组。但真正决定成败的是参数设置默认参数往往和 SSD1306 库不匹配直接复制过去大概率花屏。3.2 四个关键参数逐个拆解点阵大小常用 16x16 显示标准汉字12x12 适合小字24x24 适合标题。SSD1306 是 128x64一行最多放 8 个 16x16 汉字128/168竖着放 4 行64/164。选 16x16 最通用。取模方式这是最容易错的地方。PCtoLCD2002 里有“阴码/阳码”“逐行/逐列”“顺向/逆向”几组选项。SSD1306 的显存是按页组织的每页 8 行像素横向 128 列。Adafruit 库的drawBitmap期望的数据格式是逐行扫描、高位在前MSB first、阴码1 表示亮。对应到 PCtoLCD2002 的设置就是阴码因为 OLED 是 1 点亮逐行式顺向高位在前十六进制C51 格式输出格式选“C51 格式”生成的数组会带0x前缀和逗号直接能粘进 Arduino 代码。如果选“A51 格式”或纯十六进制还得手动改没必要。自定义格式有些版本可以设置每行几个字节、是否换行。建议每行 16 个字节方便阅读也不影响编译。3.3 生成后的数据长什么样以“中”字为例16x16 点阵按上面的参数生成会得到 32 个字节的数组类似const unsigned char zhong[] { 0x00,0x00,0x01,0x00,0x01,0x00,0x01,0x00, 0x3F,0xF8,0x21,0x08,0x21,0x08,0x21,0x08, 0x21,0x08,0x21,0x08,0x3F,0xF8,0x21,0x08, 0x01,0x00,0x01,0x00,0x01,0x00,0x00,0x00 };这 32 个字节就是“中”字的点阵地图。前 16 个字节是上半部分后 16 个是下半部分每个字节代表一行里的 8 个像素。理解这个结构后面写绘制函数就不迷糊了。注意不同版本的 PCtoLCD2002 生成的字节顺序可能略有差异如果显示出来是上下颠倒或者左右镜像优先检查“顺向/逆向”和“逐行/逐列”这两组设置而不是去改代码。4. 把点阵数据画到屏幕上的完整代码4.1 硬件连接与库准备先确认硬件。0.96 寸 SSD1306 I2C 模块一般有四个引脚VCC、GND、SCL、SDA。接 Arduino UNO 时VCC 接 3.3V 或 5V多数模块自带稳压接 5V 也能用但稳妥起见接 3.3VGND 接 GNDSCL 接 A5SDA 接 A4。这是 UNO 的 I2C 默认引脚别接错。库方面用 Adafruit 的Adafruit_SSD1306和Adafruit_GFX就够了。在 Arduino IDE 的库管理器里搜这两个名字安装最新版。装完之后File - Examples - Adafruit SSD1306 - ssd1306_128x64_i2c可以跑一下官方示例确认屏幕和接线没问题再开始改中文。4.2 用 drawBitmap 绘制单个汉字Adafruit_GFX 提供了一个drawBitmap函数可以直接画点阵数据。它的原型是void drawBitmap(int16_t x, int16_t y, const uint8_t bitmap[], int16_t w, int16_t h, uint16_t color);x、y 是左上角坐标bitmap 是点阵数组w 和 h 是宽高color 是颜色SSD1306 用 WHITE 或 BLACK。对于 16x16 的汉字调用就是display.drawBitmap(0, 0, zhong, 16, 16, WHITE);这行代码会在屏幕左上角画出“中”字。注意drawBitmap期望的数据格式是逐行、高位在前和前面取模软件的设置一致所以能对上。4.3 封装一个显示任意汉字的函数如果每个字都写一遍drawBitmap代码会很啰嗦。更好的做法是把多个字的点阵放进一个二维数组写一个函数按索引取字const unsigned char hanzi[][32] { {0x00,0x00,0x01,0x00, ...}, // 中 {0x00,0x00,0x00,0x00, ...}, // 文 {0x00,0x00,0x00,0x00, ...} // 显 }; void showHanzi(int index, int x, int y) { display.drawBitmap(x, y, hanzi[index], 16, 16, WHITE); }调用时showHanzi(0, 0, 0)显示“中”showHanzi(1, 16, 0)显示“文”。这样代码结构清晰加字也方便。4.4 显示一行中文的排版处理一行要显示多个汉字需要处理间距。16x16 的字如果紧挨着放视觉上会挤在一起通常留 1 到 2 像素间距。所以第二个字的 x 坐标是 16218第三个是 36以此类推。写个循环void showHanziLine(int startIndex, int count, int x, int y) { for (int i 0; i count; i) { display.drawBitmap(x i * 18, y, hanzi[startIndex i], 16, 16, WHITE); } }这样一行能放 7 个字7*18126接近 128 宽度。如果要居中先算总宽度count*18-2再用(128 - 总宽度)/2得到起始 x。4.5 和普通文本混排的注意事项实际项目里经常是“中文标签 英文数值”比如“温度25.5C”。这时候要注意中文用drawBitmap画英文和数字用display.print画两者的坐标要手动对齐。display.print的默认字体高度是 8 像素或 16 像素取决于setTextSize而汉字是 16 像素基线不一样。我的做法是汉字画在 y0英文画在 y4 左右让视觉重心对齐。或者干脆把数值也做成点阵统一用drawBitmap虽然麻烦但排版最可控。如果数值是动态变化的还是用print方便对齐问题多调几次坐标就能解决。5. 那些让我熬夜的坑与排查思路5.1 显示出来是镜像或倒置第一次取模的时候我按默认参数生成结果“中”字显示出来是左右镜像的。排查了半天最后发现是取模软件的“顺向/逆向”选反了。SSD1306 的显存写入是从左到右、从上到下每个字节的高位对应左边的像素。如果取模时选了“逆向”低位在前画出来就是镜像。解决办法回到取模软件把“顺向”勾上重新生成。如果已经生成了一堆字不想重做也可以在代码里对每个字节做位反转但那样效率低不如重新取模。5.2 字只显示了一半有一次显示“温”字上半部分正常下半部分是空白。检查数组长度发现只有 16 个字节而不是 32 个。原因是取模软件里点阵大小设成了 16x8只取了上半部分。16x16 的汉字必须是 32 字节少一个都不行。还有一种情况是数组长度对但显示时只画了前 16 字节。这通常是drawBitmap的 h 参数写成了 8改成 16 就好。5.3 多个字连在一起时错位显示“你好世界”四个字结果第二个字开始就偏移了。原因是每个字的数组长度不一致有的 32 字节有的 34 字节多打了逗号或换行。用二维数组时必须保证每个字都是严格的 32 字节。我的习惯是生成后数一遍或者用sizeof检查if (sizeof(hanzi[0]) ! 32) { /* 报错 */ }另外如果手动在数组里加了注释或空行也可能导致长度变化尽量保持格式统一。5.4 屏幕刷新时的闪烁问题用display.clearDisplay()再重画所有内容屏幕会闪。这是因为清屏和重画之间有一段时间屏幕是空的。解决办法是用display.clearDisplay()之后立刻画或者用双缓冲SSD1306 的库本身支持display.display()才真正刷新。如果只是局部更新可以只清局部区域display.fillRect(x, y, w, h, BLACK);然后再画新内容这样闪烁会小很多。5.5 内存不够用的信号当你取了几十个汉字编译时可能会报“data section exceeds available space”或者程序跑起来随机重启。这是 RAM 不够了。Arduino UNO 只有 2KB RAM一个 16x16 汉字占 32 字节60 个字就是 1920 字节加上库本身的开销直接爆。解决办法有两个一是把点阵数组放到 Flash 里用PROGMEM关键字读取时用pgm_read_byte二是换 ESP32 或 STM32 这类 RAM 更大的板子。如果只是显示几个字UNO 够用但字多了必须上 PROGMEM。const unsigned char zhong[] PROGMEM { ... }; // 读取时 uint8_t b pgm_read_byte(zhong[i]);注意drawBitmap不直接支持 PROGMEM需要自己写绘制函数逐字节读取或者用支持 PROGMEM 的库版本。6. 从静态汉字到动态显示的进阶玩法6.1 用数组做简单的多帧动画汉字点阵不仅能显示字还能做动画。原理很简单准备几组点阵数据每隔一段时间切换一组屏幕上的图形就动起来了。比如做一个“加载中”的动画取三帧不同的点阵循环显示const unsigned char frame1[] PROGMEM { ... }; const unsigned char frame2[] PROGMEM { ... }; const unsigned char frame3[] PROGMEM { ... }; void loop() { display.clearDisplay(); display.drawBitmap(56, 24, frame1, 16, 16, WHITE); display.display(); delay(200); // 切换 frame2、frame3 }帧率不用太高200ms 一帧就很流畅。如果要做更复杂的动画可以把每帧拆成多个 16x16 块按块更新。6.2 结合传感器做实时中文显示中文显示真正的用武之地是配合传感器。比如 DHT11 测温度湿度OLED 上显示“温度25℃ 湿度60%”。中文部分用点阵数值部分用print。代码结构showHanziLine(0, 2, 0, 0); // 显示“温度” display.setCursor(40, 4); display.print(t); display.print(C);这样既有中文标签又有实时数值做一个小型气象站很实用。注意刷新频率别太高DHT11 本身采样就慢1 秒一次足够。6.3 用取模软件做自定义图标除了汉字取模软件还能做图标。把一张 32x32 的 BMP 图片导入 Image2Lcd设置同样的参数就能生成图标的点阵数组。用drawBitmap画出来可以做开机 Logo、状态图标、电池电量条等。图标和汉字可以混用只要坐标算好排版很灵活。6.4 多级菜单的中文界面如果做带按键的项目可以用中文点阵做菜单。比如主菜单显示“设置”“测量”“关于”用按键切换高亮项。高亮可以用反色实现先画黑底白字选中时画白底黑字。具体做法是画一个填充矩形作为背景再用drawBitmap画字颜色参数反过来。// 未选中 display.fillRect(x, y, 36, 16, BLACK); display.drawBitmap(x, y, item, 16, 16, WHITE); // 选中 display.fillRect(x, y, 36, 16, WHITE); display.drawBitmap(x, y, item, 16, 16, BLACK);这样菜单交互就有反馈了用户体验比纯英文好很多。7. 几个提高效率的实操习惯取模这件事做多了就有自己的套路。我现在的习惯是建一个专门的文件夹里面放一个 Excel 表记录每个字的点阵数组和对应的索引号。每次取新字先查表有就直接用没有就取模后加进去。这样项目之间可以复用不用重复劳动。另外取模软件的字体选择也有讲究。宋体在 16x16 下笔画清晰黑体更粗但小字号容易糊。如果显示效果不理想可以换字体试试或者手动微调点阵数据——PCtoLCD2002 支持手动编辑像素点一下就能改一个点适合做精细调整。还有一点生成的数组最好加上注释标明是什么字。不然过一个月回头看代码面对一堆0x00,0x01完全想不起来哪个是哪个。我的注释格式是const unsigned char hanzi_zhong[] { ... }; // 中变量名用拼音注释用汉字既不影响编译又方便维护。最后如果你用的是 ESP32 或 STM32内存充裕可以直接上完整的中文字库用 U8g2 的drawUTF8函数不需要手动取模。但 UNO 这类小内存板子手动取模仍然是最靠谱的方案。工具在变原理不变点阵数据 绘制函数 屏幕上的字。把这套流程跑通一次以后遇到任何显示需求都能自己搞定。
返回列表