☰
Arduino OLED显示中文乱码?SSD1306自定义汉字点阵取模与绘制全攻略
2026/9/28 8:15:28 网站建设 项目流程

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 只有 32KB,RAM 只有 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/16=8),竖着放 4 行(64/16=4)。选 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.3V),GND 接 GND,SCL 接 A5,SDA 接 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 坐标是 16+2=18,第三个是 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*18=126,接近 128 宽度)。如果要居中,先算总宽度count*18-2,再用(128 - 总宽度)/2得到起始 x。

4.5 和普通文本混排的注意事项

实际项目里经常是“中文标签 + 英文数值”,比如“温度:25.5C”。这时候要注意:中文用drawBitmap画,英文和数字用display.print画,两者的坐标要手动对齐。display.print的默认字体高度是 8 像素(或 16 像素,取决于setTextSize),而汉字是 16 像素,基线不一样。

我的做法是:汉字画在 y=0,英文画在 y=4 左右,让视觉重心对齐。或者干脆把数值也做成点阵,统一用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 这类小内存板子,手动取模仍然是最靠谱的方案。工具在变,原理不变:点阵数据 + 绘制函数 = 屏幕上的字。把这套流程跑通一次,以后遇到任何显示需求,都能自己搞定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询