LCD/OLED点阵屏开发:取模工具原理、字模生成与显示驱动避坑指南 简介面向嵌入式系统与物联网设备开发者的LCD/OLED字符图像取模工具用于将ASCII字符或BMP位图快速转换为适配低分辨率屏幕的点阵数据减少手工取模误差。该工具支持字符与图像两类取模操作可自定义字体样式并可将彩色图转为单色/灰度格式预览功能便于在烧录前确认显示效果。压缩包体积仅501KB内容以C语言源码示例和CHM帮助文档为主其中包含不同配置的取模参考代码方便开发者阅读与二次集成帮助文档提供中文与繁体中文版本可指导用户快速上手。目前已有1150人学习下载适合需要自制字库、处理图像显示或优化可视化方案的嵌入式软硬件工程师能够有效提升开发效率并保障有限显存下的画面表现。1. 取模工具解决的是什么点阵屏显示的第一道坎某开发者第一次把一段 OLED 驱动代码跑通画点亮了字符却乱了一个“中”字在屏上横躺着笔画拧成一片。查了半天初始化最后发现是取模软件里默认选的“逐列式”和这块屏实际用的“逐行式”扫描对不上。类似的事在 LCD/OLED 开发里太常见了而它和硬件设计无关纯粹是“LCD/OLED 开发字符图像取模工具”用没用到位的差别。取模工具做的事情很简单——把文字和图片变成单片机直接烧录的十六进制点阵数组但它的参数设置直接决定字符在屏上是正的、反的还是旋转 90 度的。本文就从原理、字模取模、图像取模到踩坑排查一次讲透适合正在做显示项目的嵌入式开发者尤其是第一次接触点阵屏、还没形成“取模方向直觉”的新手。2. 取模原理先搞懂行扫描、列扫描与字节位序2.1 字模与图模的本质点阵数据和显示驱动的中间格式单色 LCD 和 OLED 的显示原理可以抽象成一句话屏上有一个 M×N 的点阵MCU 往控制器的显存里写字节控制器再把字节里的每一位映射到对应像素。对应用层开发者来说我们不关心显存物理位置只关心“往哪个地址写什么数据、哪个像素会亮”。取模工具就是生成“什么数据”的中间环节。一个汉字“中”在 16×16 点阵里先被字体渲染引擎变成 256 个黑白点再按每 8 个点合成一个字节最后得到 32 字节的数组。这 32 字节就是字模它和你手写在格子纸上的点阵图一一对应。图像取模同理一张 48×48 的图标点阵展开后是 2304 个点按 8 点一字节打包成 288 字节。字模和图模的本质都是“点阵 打包规则”区别只在于来源字模由字体渲染出来图模由图片二值化得到字模按字符边界单独存储、按编码索引图模按图像区域整块存储、按坐标定位。这里有个新手最容易忽略的事实取模后的数组只是一串普通字节它本身不携带“我是字符还是图片”的语义。同一个数组你用“行扫描”的驱动函数显示是一张正常的图换成“列扫描”的驱动函数显示就是乱码。网上经常看到“同一段取模代码在他的屏上正常在我的屏上花屏”的求助帖原因就在这里——不是代码错了是屏的扫描方式和取模参数没对上。为了让打包过程更直观我写一个最小可运行的 C 代码模拟一块 8×8 的点阵按行扫描、每行一个字节的打包结果// 8x8点阵1表示该点需要点亮 uint8_t dot[8] { 0b00011000, // 第0行第3、4个像素亮 0b00011000, // 第1行 0b00011000, // 第2行 0b11111111, // 第3行整行点亮 0b00011000, // 第4行 0b00011000, // 第5行 0b00011000, // 第6行 0b00011000, // 第7行 };上面这个数组每个字节的二进制位对应一行里从左到右的 8 个像素dot[0] 的 00011000 表示第 0 行第 3、4 位亮屏上显示的是一个竖直的十字。如果控制器按“低位在前”解析这个字节亮点的位置会变成从右往左数第 3、4 位十字的形状不变但位置会偏移。这个例子说明了一件事位序错了不一定立刻“花屏”有时只是像素位置错位这也是它更难排查的原因。2.2 取模方向与字节位序同一个字为什么会有四种显示结果同一个汉字理论上至少有四种常见组合逐行式或逐列式再各配高位在前或低位在前。四组数据完全不同但显示成点阵时只有一组是对的取决于控制器的显存地址递增规则。不懂这个规则就只能靠“试一遍、不行就换”的笨办法浪费时间还在心流上留下阴影。直接说显存组织。以常见的 128×64 OLED 控制器为例显存被分成 8 个页每页高 8 像素。写数据时先指定页地址和列地址然后连续写入每写一个字节列地址自动加 1。也就是说控制器的自然扫描方向是从上到下每 8 行一个页页内从左到右逐字节填充。这种情况下取模软件要选“逐行式、高位在前”逐行式匹配页内从上到下的 8 行高位在前匹配字节内从左到右的位序。但 LCD 控制器不完全按这套逻辑走有的显存按行组织而不是按页组织有的支持行列地址独立设置有的干脆把扫描方向做成了寄存器配置项。当你把为 A 控制器取的模用在 B 控制器上最常见的结果就是字符旋转 90 度或整体翻转。我在实际开发中养成了一个习惯接到新屏第一件事不是写功能代码而是查数据手册里两个字段——显存组织方式页式还是行式和地址递增方向。根据这两个字段决定取模参数基本不会翻车。这里给一段验证位序的小 Python 代码不接屏也能看出高低位的区别# 位序验证同一个字节按两种方式解析 byte 0b10010001 high_first [(byte (7 - i)) 1 for i in range(8)] # 从bit7开始读 low_first [(byte i) 1 for i in range(8)] # 从bit0开始读 print(高位在前:, high_first) # [1, 0, 0, 1, 0, 0, 0, 1] print(低位在前:, low_first) # [1, 0, 0, 0, 1, 0, 0, 1]运行后两份结果不同说明同一个字节在两种位序下映射到屏上的是不同列。这类问题在字符上表现为左右镜像在图案上表现为水平翻转。记住一个判断技巧如果字符能认出来但方向反了八成是位序问题如果字符旋转了 90 度是逐行/逐列问题。把这两类分开排查比直接重取模高效得多。取模方向与常见异常对应关系我整理成一个表排查时直接对照显示异常优先检查的取模参数字符左右镜像字节内位序高位在前/低位在前字符整体倒置顺向/逆向字符旋转 90 度逐行式/逐列式整屏反白阴码/阳码字符间距错乱字宽字高与像素布局不匹配2.3 阴码阳码、字宽字高参数含义与选择依据阴码和阳码在字段名上就能劝退一批新手。不同取模软件对这两个词的定义甚至正好相反有的软件里“阳码”表示笔画为 1有的软件里“阴码”表示笔画为 1。所以别死记方向记住判断点看软件的点阵预览预览里字体是不是亮的、背景是不是灭的和屏上预期效果一致就说明阴阳码选对了。更稳的做法是生成一个测试字符用工程里已有的画点函数验证一下眼睛不会骗人。字宽字高决定了每个字符占用的点阵大小也直接决定了字模的字节数。以 16×16 为例每个字模的字节数 (16 / 8) × 16 32 字节8×16 的半高字则是 (8 / 8) × 16 16 字节。常见组合有 8×16、12×24、16×16、24×24、32×32选择依据就两条屏的分辨率以及字符的清晰度。128×64 的屏上16×16 能显示 4 行每行 8 个汉字24×24 显示效果更精致但一屏放不了几个。字宽必须能被 8 整除这一点记住能少踩很多坑。如果一个字符的字宽是 18每行点阵要拆成 3 个字节但只有 2.25 字节的数据软件会在最后一个字节里补 0。问题是不同软件补位规则不一样有的补在右边有的按字节对齐后跳到下一行驱动侧只要没按同样规则跳过补位显示就会横向错位。所以字宽老老实实选 8、16、24、32字高没有这个约束奇数高度也常见。顺向与逆向这个选项普通开发基本用不上它的定位是兼容地址反向递增的控制器。我见过有人在某些 LCD 屏上遇到图像整体水平翻转改了半天的取模方式没效果最后发现只是把顺向改成逆向就好了。如果你的屏显示出来的图像像“从背面看”优先怀疑这个参数。总之取模参数的每一档都不是随便设置的它们一一对应控制器硬件里的某个行为理解了硬件再看参数就顺了。3. 字符取模实操用经典取模软件生成汉字与 ASCII 字模3.1 最小操作流程从输入文字到生成 C 数组的七个步骤目前主流的取模软件还是 Windows 下的小工具单文档界面左边是设置项右边是输入区和点阵预览。不同软件命名略有差异但“逐行式/逐列式、高位在前/低位在前、阴码/阳码”这套核心选项是通用的学会一套换软件无成本。假设要在 128×64 的 OLED 上做一个菜单页需要显示“系统状态”四个汉字操作为打开取模软件在文字输入区输入“系统状态”。设置字宽 16、字高 16字体选黑体、字号 16。字号和字高要一致字号大于字高时字形会被截断。取模方式选“逐行式”输出方式选“高位在前”阴码/阳码先选阳码。点预览看右侧点阵图是否清晰、字形有没有被挤压或截断。点“生成字模”检查输出数组每个字的字节数是否为 32。复制生成结果粘贴到工程里的 font.h 头文件用 const uint8_t 修饰。写一个简单的显示函数一次只显示一个字确认方向正确后再批量接入界面逻辑。生成结果是一个标准数组// 16x16 汉字“系”逐行式高位在前共32字节 const uint8_t font_xi_16x16[32] { 0x00, 0x00, 0x7F, 0xFC, 0x08, 0x04, 0x08, 0x04, 0x08, 0x04, 0x08, 0x04, 0x08, 0x04, 0x08, 0x04, 0x08, 0xE4, 0xFF, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00 };数组结构和驱动函数的对应关系是前 16 字节对应上半部分 8 行后 16 字节对应下半部分 8 行每一行内部的 2 个字节按从左到右排列。如果显示时发现上半部分和下半部分错位多半是驱动里行列循环的顺序写反了不是取模软件的问题。数组本身没有“正确”与“错误”只有“与驱动是否匹配”。3.2 字高字宽、字体与偏移参数怎么定字体和字模大小的关系比看起来复杂。同样设置字宽 16、字高 16用“黑体 16 号”和“黑体 24 号”生成的结果不一样16 号直接按 16 像素网格渲染24 号先渲染 24 像素再压缩回 16×16笔画粗细和细节完全不同。所以在取模软件里“字号”和“字宽/字高”必须配套选择。“字宽 16、字高 16、字号 24”的操作会得到笔画像被强行挤压的字形这不是软件故障是选项自相矛盾。偏移量参数解决的是字形在点阵框里的位置问题。每个汉字的重心其实不完全居中比如“一”字天然偏上“口”字天然偏下字体渲染后会有 1-2 像素的偏移。对 16×16 这样的小尺寸1 像素偏移肉眼可见会破坏整行文字的对齐感。遇到这种情况我会选中该字单独调整纵向偏移重新生成数组而不是在屏幕上移动显示坐标——后者会把所有字符一起挪动解决不了单字错位。多字号管理是另一个容易踩的细节。一个项目里通常至少有两种字号状态栏用 8×16 半高字正文用 16×16 全高字。我的做法是把不同尺寸的字模分文件存储比如 font_8x16.h、font_16x16.h并在文件名里标注位序和扫描方式例如 font_16x16_str.h 表示逐行式、高位在前。这样半年后再回来维护一眼就知道哪个文件配哪块屏不会出现“这个数组是给哪块板子用的”这种灵魂拷问。3.3 把字模数组接进驱动显示函数与数组索引对应驱动侧的接入方式取决于屏的类型。字符模式 LCD 自带字符发生器只需向控制器发送 ASCII 码即可取模工具主要给自定义字符和汉字建模图形模式 OLED/TFT 没有字符发生器所有字符都要在 MCU 侧渲染字模数组就是唯一的渲染素材。ASCII 字模的索引规则很固定可见字符从 0x20空格到 0x7E波浪号按升序排列查找偏移 (ascii - 0x20) × 每字字节数。汉字因为数量大项目里通常只用到有限的几十个我一般按“用到的文字”顺序排列数组再写一个查找函数把汉字内码映射到数组下标。这种做法的好处是字模文件极小缺点是新增汉字要重新排列数组适合 UI 文字基本固定的产品。显示函数要跟取模方向严格对应这里给一个 16×16 汉字的完整示例// 在(x,y)处显示16x16汉字font_idx为字模数组下标 void lcd_show_hanzi(uint8_t x, uint8_t y, uint8_t font_idx) { const uint8_t *glyph hanzi_16x16 font_idx * 32; for (uint8_t i 0; i 16; i) { // 16行 lcd_set_cursor(x, y i); // 定位到第i行 lcd_write_data(glyph[i * 2]); // 左半字节 lcd_write_data(glyph[i * 2 1]); // 右半字节 } }这个函数假设控制器支持按行设置坐标且每行数据从左到右写入。如果你的 OLED 控制器是页模式先设置页地址再逐列写循环结构会变成两层。我建议把“坐标设置”和“数据写入”拆成两个独立函数移植到不同控制器时只改坐标设置部分取模数组可以原样保留。代码里 font_idx 的换算也要和取模软件的排列顺序一致批量生成的字模按输入顺序排列每个字符占用固定字节数index 就是第几个字符。4. 图像取模实操BMP 预处理与 C 数组生成4.1 图片转单色 BMP尺寸、位深与阈值三件事如果说字符取模是软件帮你渲染字形图像取模就是纯粹的数据搬运。工具只认点阵位图最通用的是 1 位位深的 BMP。很多新手直接拿 JPEG 或 PNG 丢进取模工具要么打不开要么生成的全是噪点原因就是位深不符合要求。尺寸是第一道关。图像取模生成数组的字节数 (宽度 / 8 向上取整) × 高度。以 128×64 为例数组是 (128 / 8) × 64 1024 字节。如果你的图片是 130×64每行要凑 17 字节而不是 16 字节数据和屏幕坐标直接对不上。我在预处理阶段就把宽度裁剪成 8 的倍数128、96、64 是最常用的三档。高度没有这个限制但尽量保持与屏的显示区域一致可以减少很多坐标换算。位深检查有个笨但有效的办法用 Python 读一下图像模式一目了然from PIL import Image img Image.open(test.bmp) print(img.mode) # 1单色 L灰度 RGB彩色 P索引色mode 为 1 才是单色位图可以直接进取模工具。如果输出 L 或 RGB说明转换没做彻底。阈值的选择取决于原图内容文字型 Logo 适合固定阈值 128黑白分明带照片质感的图片建议用 Otsu 自动阈值避免人工反复试。阈值设高了浅灰区域变白图像显得稀疏设低了暗部噪声全保留显示出来脏兮兮的。4.2 用 Python 脚本做预处理缩放、二值化与抖动我处理图像取模输入时用一套固定脚本核心是三个动作缩放插值、灰度化、阈值化。对颜色层次复杂的图片还可以加误差扩散抖动dithering它能在单色限制下保留灰度的视觉过渡适合做开机画面这类需要表现立体感的场景。from PIL import Image def to_mono_bmp(src, dst, width, height, threshold128, ditherFalse): img Image.open(src) # 1. 用LANCZOS插值缩放边缘比最近邻平滑 img img.resize((width, height), Image.LANCZOS) # 2. 转灰度 img img.convert(L) # 3. 二值化dither开启时用Floyd-Steinberg误差扩散 if dither: img img.convert(1, ditherImage.Dither.FLOYDSTEINBERG) else: img img.point(lambda p: 255 if p threshold else 0) img img.convert(1) img.save(dst, BMP) # 图标场景阈值处理保证边缘整齐 to_mono_bmp(logo.png, logo_mono.bmp, 128, 64, threshold140) # 照片场景抖动处理保留灰度过渡 to_mono_bmp(photo.png, photo_mono.bmp, 128, 64, ditherTrue)代码里 threshold 是灰度分界线大于阈值变白小于等于阈值变黑。dither 开启后不再简单二值化而是把灰度误差扩散到邻近像素单色图在视觉上保留更多层次。注意抖动更适合看整体氛围不适合需要清晰边缘的图标和文字图标务必用纯阈值方案。这段脚本每次生成两张图对比一下选效果好的那张再进取模工具比直接在工具里反复试参数快得多。4.3 整屏图与局部图按坐标裁剪再取模整屏图和局部图的数据差异在于坐标对应关系。整屏图从 (0,0) 开始数组顺序就是屏幕扫描顺序直接使用局部图要先确定图标要显示在屏的哪个区域再按目标尺寸裁剪。比如要在屏的 (20,10) 处显示一个 48×48 的图标你先要决定源图从哪里裁出这 48×48 的内容再缩放或裁剪到位。我的做法是先决定图标显示位置和尺寸再把原图缩放到对应尺寸后裁剪。缩放和裁剪的顺序影响结果——先缩放到目标尺寸再裁还是先裁再缩放最后得到的像素内容可能不同。对边缘清晰的图标通常先缩放再裁剪更可控因为裁剪框的坐标可以和屏上位置直接换算。如果有批量图标要处理把显示区域坐标写进一个配置文件脚本统一跑避免每个图标手动裁。裁剪后用取模软件生成数组时务必确认软件里设置的尺寸与实际裁剪尺寸一致。生成之后我建议做一个验证先在屏的 (0,0) 处显示这张数组看左上角是否与源图一致。验证通过后再放到目标坐标。这样能把“取模问题”和“定位问题”分开排查不会两边互相干扰。这个小习惯帮我在不少项目里省掉了整晚的调试时间。5. 取模避坑指南镜像、反白、偏移的排查与修正5.1 显示镜像或倒置取模方向与行列扫描不匹配现象屏上显示一个向右的箭头实际显示成向左显示“电”字形状能认出来但整体翻转。原因取模时选的扫描方向与控制器的显存递增方向不一致。排查方法先查控制器数据手册里的显存结构图和写时序图确认地址是行优先还是列优先再回取模软件把“逐行式/逐列式”或“顺向/逆向”改过来。更快的土办法是在屏的左上角画一个“十”字看十字的实际朝向就能判断扫描方向。这个十字验证法我每次拿到新屏都会先跑一遍比对手册快很多也避免了一次次烧录试错。5.2 字节偏移一位导致乱码MSB/LSB 位序反了现象图案大体能辨认但所有字符像被水平剪切过边缘不齐。原因取模时高位在前控制器按低位在前解析或反过来。这不会导致完全乱码而是字节内部位序反转让对称字形产生镜像。排查方法取一个只有最左侧一列点亮的简单图形比如数组首字节 0x01在屏上确认它出现在最左边还是最右边。出现在右边就是位序反了。解决办法是统一双方规则——我倾向于改驱动在写入前对字节做一次位反转比重新生成几百个数组更划算。位反转可以用查表法256 字节的表换高速划算得很。5.3 整屏反白阴码阳码设置反了现象背景全亮笔画全灭屏幕看起来像负片。OLED 上尤其明显全亮不仅难看还费电。原因取模时选的阴码/阳码和屏的初始化模式冲突。解决回取模软件切换阴码阳码重新生成。如果数组已经在多个界面大量使用也可以不改数组在驱动写入函数里对显存数据做一次按位取反一行代码的事。但要注意这只对整屏反白有效如果只是局部区域反白说明是区域坐标设置问题和阴阳码无关。阴阳码的定义在不同软件里可能恰好相反所以判断时只看预览和实屏效果不要迷信字段名。5.4 放大后锯齿明显、糊成一团缩放插值没做对现象小图放大后边缘全是锯齿笔画中间还有断点显示在屏上很脏。原因用画图工具放大时采用了最近邻插值原图 1 个像素被硬拉成 2×2 或 3×3单色图像素没有过渡锯齿完全暴露。解决用 4.2 的脚本先转灰度、LANCZOS 缩放、再阈值化。如果原图不是位图而是矢量直接按目标尺寸渲染后再取模效果比任何插值都好。另一个建议图标在设计阶段就要按目标尺寸来画不要随手画个小图指望后期放大。5.5 字模图模混用 API、字库更新不生效存储格式与构建流程两个容易混淆的问题放在一起说。第一字模按字符边界存储图模按图像区域存储两者的数组索引逻辑不同。如果驱动里只写了一个通用的“写数据”函数混用时一定乱码。我习惯在函数命名上做区分显示字符用 lcd_show_char显示图像用 lcd_show_image底层共用同一条写数据链路但上层绝不混调。第二修改取模参数重新生成数组后烧录到板子上还是旧效果先检查头文件是否被工程正确引用再检查构建系统有没有增量编译缓存。用 Makefile 管理的工程可以在取模脚本的输出路径上声明依赖让头文件每次都自动重建这一点下一章会具体讲。6. 进阶写一个命令行取模脚本把手动操作变成流水线6.1 一个够用的 Python 取模脚本GUI 取模软件适合少量字符和零星图片但如果项目里有几十个图标、多套界面状态图手动复制粘贴会让人崩溃。我自己习惯在项目根目录放一个 make_font.py直接从单色 BMP 生成 C 数组头文件省掉所有鼠标操作。# 单色BMP - C语言数组输出为 .h 头文件 from PIL import Image def bmp_to_c_array(bmp_path, array_nameimage_data): img Image.open(bmp_path).convert(1) w, h img.size bytes_per_row (w 7) // 8 # 宽度不足8的部分补0 rows [] for y in range(h): row [] for bx in range(bytes_per_row): byte 0 for bit in range(8): px bx * 8 bit if px w: pixel img.getpixel((px, y)) # 0黑 255白 if pixel 0: byte | (1 (7 - bit)) # 高位在前 row.append(byte) rows.append(row) # 生成头文件 lines [#ifndef FONT_DATA_H, #define FONT_DATA_H, #include stdint.h, fconst uint8_t {array_name}[{h}][{bytes_per_row}] {{] for r in rows: lines.append( , .join(f0x{b:02X} for b in r) ,) lines.append(};, #endif, ) with open(font_data.h, w) as f: f.write(\n.join(lines)) if __name__ __main__: bmp_to_c_array(icon_48x48.bmp, icon_48x48)脚本逻辑是逐行扫描每行按 8 像素打包一个字节黑色像素置 1白色置 0最终输出二维数组。默认按高位在前打包如果你的控制器要求低位在前把1 (7 - bit)改成1 bit即可只动这一行就够。6.2 接进编译流程从脚本到自动生成头文件脚本写完后可以接到构建流程里。我的做法是在 Makefile 里加一个 target每次编译前自动执行脚本。这样任何人改动源图重新编译时字模头文件会自动更新不会出现“改了图忘了重新取模”的情况。核心思路是让 BMP 源文件成为唯一输入头文件成为只读产物不允许手工编辑。# 取模脚本自动生成头文件 font_data.h: icons/icon_48x48.bmp python3 make_font.py这样配置之后整个取模流程就变成丢 BMP 到 icons 目录 → 编译 → 完成。如果换了控制器需要调整位序只在 make_font.py 里改一处比手工重新取模一百张图快得多。我自己保持的习惯是GUI 取模软件只用于临时验证和调试预览所有正式资源走脚本生成保证每次构建结果一致。这个习惯帮我避开过不少“这次编译出的字模和上次不一样”的玄学问题也让后来接手项目的人不用问就知道字模从哪来。希望帮到你。本文还有配套的精品资源点击获取